Compare commits
31 Commits
transport-
...
tinygo
| Author | SHA1 | Date | |
|---|---|---|---|
|
6ae409c1db
|
|||
|
bfa0669143
|
|||
|
2867f68a90
|
|||
|
fbb48f2295
|
|||
|
132872c2d6
|
|||
|
1d3590985e
|
|||
|
ae3f93a3bf
|
|||
|
cbf2eaea78
|
|||
|
1133a918f1
|
|||
|
f5bb6a2b6d
|
|||
|
718f550180
|
|||
|
07dc008a31
|
|||
|
cdc512c391
|
|||
|
1df3e41191
|
|||
|
dea65ad94c
|
|||
|
4c9a395ff2
|
|||
|
09eec1c8fc
|
|||
|
fbaafacd8a
|
|||
|
40e7a168ec
|
|||
|
d6e0874f05
|
|||
|
9fa05cce5c
|
|||
|
8725c68b24
|
|||
|
07512a3c88
|
|||
|
6217a889e1
|
|||
|
92e4651c0c
|
|||
|
|
a01bad919c | ||
|
|
cfc88e887b | ||
| d325ee6a2d | |||
| a45ad400f9 | |||
| f599cc4d43 | |||
| 1b3f4e59db |
@@ -1,29 +0,0 @@
|
|||||||
name: Go Benchmarks
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ "main", "master" ]
|
|
||||||
pull_request:
|
|
||||||
branches: [ "main", "master" ]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
benchmark:
|
|
||||||
name: Run Benchmarks
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout Source
|
|
||||||
uses: https://git.quad4.io/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
|
|
||||||
|
|
||||||
- name: Set up Go
|
|
||||||
uses: https://git.quad4.io/actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00
|
|
||||||
with:
|
|
||||||
go-version: '1.25'
|
|
||||||
|
|
||||||
- name: Setup Task
|
|
||||||
uses: https://git.quad4.io/actions/setup-task@0ab1b2a65bc55236a3bc64cde78f80e20e8885c2
|
|
||||||
with:
|
|
||||||
version: '3.46.3'
|
|
||||||
|
|
||||||
- name: Run Benchmarks
|
|
||||||
run: task bench
|
|
||||||
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
name: Go Build Test
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
- master
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
name: Build (${{ matrix.goos }}, ${{ matrix.goarch }})
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
goos: [linux, windows, darwin, freebsd]
|
|
||||||
goarch: [amd64, arm64, arm]
|
|
||||||
include:
|
|
||||||
- goos: js
|
|
||||||
goarch: wasm
|
|
||||||
exclude:
|
|
||||||
- goos: darwin
|
|
||||||
goarch: arm
|
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout Source
|
|
||||||
uses: https://git.quad4.io/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
|
|
||||||
|
|
||||||
- name: Set up Go
|
|
||||||
uses: https://git.quad4.io/actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00
|
|
||||||
with:
|
|
||||||
go-version: '1.25'
|
|
||||||
|
|
||||||
- name: Setup Task
|
|
||||||
uses: https://git.quad4.io/actions/setup-task@0ab1b2a65bc55236a3bc64cde78f80e20e8885c2
|
|
||||||
with:
|
|
||||||
version: '3.46.3'
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
env:
|
|
||||||
GOOS: ${{ matrix.goos }}
|
|
||||||
GOARCH: ${{ matrix.goarch }}
|
|
||||||
GOARM: ${{ matrix.goarch == 'arm' && '6' || '' }}
|
|
||||||
CGO_ENABLED: '0'
|
|
||||||
run: |
|
|
||||||
if [ "${{ matrix.goos }}" = "js" ] && [ "${{ matrix.goarch }}" = "wasm" ]; then
|
|
||||||
task build-wasm
|
|
||||||
else
|
|
||||||
task build
|
|
||||||
fi
|
|
||||||
|
|
||||||
105
.gitea/workflows/build.yml
Normal file
105
.gitea/workflows/build.yml
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
name: Go Build Multi-Platform
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "main", "master" ]
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
pull_request:
|
||||||
|
branches: [ "main", "master" ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
goos: [linux, windows, darwin, freebsd]
|
||||||
|
goarch: [amd64, arm64, arm]
|
||||||
|
include:
|
||||||
|
- goos: js
|
||||||
|
goarch: wasm
|
||||||
|
exclude:
|
||||||
|
- goos: darwin
|
||||||
|
goarch: arm
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
outputs:
|
||||||
|
build_complete: ${{ steps.build_step.outcome == 'success' }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: https://git.quad4.io/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: https://git.quad4.io/actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||||
|
with:
|
||||||
|
go-version: '1.25'
|
||||||
|
|
||||||
|
- name: Setup Task
|
||||||
|
uses: https://git.quad4.io/actions/setup-task@0ab1b2a65bc55236a3bc64cde78f80e20e8885c2 # v1
|
||||||
|
with:
|
||||||
|
version: '3.46.3'
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
id: build_step
|
||||||
|
env:
|
||||||
|
GOOS: ${{ matrix.goos }}
|
||||||
|
GOARCH: ${{ matrix.goarch }}
|
||||||
|
GOARM: ${{ matrix.goarch == 'arm' && '6' || '' }}
|
||||||
|
CGO_ENABLED: '0'
|
||||||
|
run: |
|
||||||
|
output_name="reticulum-go-${GOOS}-${GOARCH}"
|
||||||
|
if [ "$GOOS" = "js" ] && [ "$GOARCH" = "wasm" ]; then
|
||||||
|
task build-wasm
|
||||||
|
output_name+=".wasm"
|
||||||
|
mv bin/reticulum-go.wasm "${output_name}"
|
||||||
|
else
|
||||||
|
task build
|
||||||
|
if [ "$GOOS" = "windows" ]; then
|
||||||
|
output_name+=".exe"
|
||||||
|
fi
|
||||||
|
mv bin/reticulum-go "${output_name}"
|
||||||
|
fi
|
||||||
|
echo "Built: ${output_name}"
|
||||||
|
|
||||||
|
- name: Calculate SHA256 Checksum
|
||||||
|
run: |
|
||||||
|
output_name="reticulum-go-${{ matrix.goos }}-${{ matrix.goarch }}"
|
||||||
|
if [ "${{ matrix.goos }}" = "windows" ]; then
|
||||||
|
output_name+=".exe"
|
||||||
|
elif [ "${{ matrix.goos }}" = "js" ] && [ "${{ matrix.goarch }}" = "wasm" ]; then
|
||||||
|
output_name+=".wasm"
|
||||||
|
fi
|
||||||
|
BINARY_PATH="${output_name}" task checksum
|
||||||
|
|
||||||
|
- name: Upload Artifact
|
||||||
|
uses: https://git.quad4.io/actions/upload-artifact@ff15f0306b3f739f7b6fd43fb5d26cd321bd4de5 # v3.2.1
|
||||||
|
with:
|
||||||
|
name: reticulum-go-${{ matrix.goos }}-${{ matrix.goarch }}
|
||||||
|
path: |
|
||||||
|
reticulum-go-${{ matrix.goos }}-${{ matrix.goarch }}*
|
||||||
|
|
||||||
|
release:
|
||||||
|
name: Create Release
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: build
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Download All Build Artifacts
|
||||||
|
uses: https://git.quad4.io/actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a
|
||||||
|
with:
|
||||||
|
path: ./release-assets
|
||||||
|
|
||||||
|
- name: List downloaded files (for debugging)
|
||||||
|
run: ls -R ./release-assets
|
||||||
|
|
||||||
|
- name: Create Gitea Release
|
||||||
|
uses: https://git.quad4.io/actions/gitea-release-action@4875285c0950474efb7ca2df55233c51333eeb74
|
||||||
|
with:
|
||||||
|
files: ./release-assets/*/*
|
||||||
@@ -65,20 +65,41 @@ jobs:
|
|||||||
if: matrix.os == 'ubuntu-latest' && matrix.goarch == 'amd64'
|
if: matrix.os == 'ubuntu-latest' && matrix.goarch == 'amd64'
|
||||||
run: task test-race
|
run: task test-race
|
||||||
|
|
||||||
- name: Run Resource Leak tests (Linux AMD64 only)
|
|
||||||
if: matrix.os == 'ubuntu-latest' && matrix.goarch == 'amd64'
|
|
||||||
run: task test-leaks
|
|
||||||
|
|
||||||
- name: Run Network Simulation tests (Linux AMD64 only)
|
|
||||||
if: matrix.os == 'ubuntu-latest' && matrix.goarch == 'amd64'
|
|
||||||
run: task test-network
|
|
||||||
|
|
||||||
- name: Run Fuzz tests (Linux AMD64 only)
|
|
||||||
if: matrix.os == 'ubuntu-latest' && matrix.goarch == 'amd64'
|
|
||||||
run: task test-fuzz
|
|
||||||
|
|
||||||
- name: Run WebAssembly tests (Linux AMD64 only)
|
- name: Run WebAssembly tests (Linux AMD64 only)
|
||||||
if: matrix.os == 'ubuntu-latest' && matrix.goarch == 'amd64'
|
if: matrix.os == 'ubuntu-latest' && matrix.goarch == 'amd64'
|
||||||
run: |
|
run: |
|
||||||
chmod +x misc/wasm/go_js_wasm_exec
|
chmod +x misc/wasm/go_js_wasm_exec
|
||||||
task test-wasm
|
task test-wasm
|
||||||
|
|
||||||
|
- name: Test build (ensure compilation works)
|
||||||
|
run: |
|
||||||
|
echo "Testing build for current platform (${{ matrix.os }}, ${{ matrix.goarch }})..."
|
||||||
|
task build
|
||||||
|
|
||||||
|
- name: Test WebAssembly build (Linux AMD64 only)
|
||||||
|
if: matrix.os == 'ubuntu-latest' && matrix.goarch == 'amd64'
|
||||||
|
run: task build-wasm
|
||||||
|
|
||||||
|
- name: Test binary execution
|
||||||
|
run: |
|
||||||
|
echo "Testing binary execution on (${{ matrix.os }}, ${{ matrix.goarch }})..."
|
||||||
|
timeout 5s ./bin/reticulum-go || echo "Binary started successfully (timeout expected)"
|
||||||
|
|
||||||
|
- name: Test cross-compilation (AMD64 runners only)
|
||||||
|
if: matrix.goarch == 'amd64'
|
||||||
|
run: |
|
||||||
|
echo "Testing ARM64 cross-compilation from AMD64..."
|
||||||
|
GOOS=linux GOARCH=arm64 task build
|
||||||
|
env:
|
||||||
|
GOOS: linux
|
||||||
|
GOARCH: arm64
|
||||||
|
|
||||||
|
- name: Test ARMv6 cross-compilation (AMD64 runners only)
|
||||||
|
if: matrix.goarch == 'amd64'
|
||||||
|
run: |
|
||||||
|
echo "Testing ARMv6 cross-compilation from AMD64..."
|
||||||
|
GOOS=linux GOARCH=arm GOARM=6 task build
|
||||||
|
env:
|
||||||
|
GOOS: linux
|
||||||
|
GOARCH: arm
|
||||||
|
GOARM: 6
|
||||||
|
|||||||
@@ -1,97 +0,0 @@
|
|||||||
name: Go Publish
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
- master
|
|
||||||
tags:
|
|
||||||
- 'v*'
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
name: Build (${{ matrix.goos }}, ${{ matrix.goarch }})
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
goos: [linux, windows, darwin, freebsd]
|
|
||||||
goarch: [amd64, arm64, arm]
|
|
||||||
include:
|
|
||||||
- goos: js
|
|
||||||
goarch: wasm
|
|
||||||
exclude:
|
|
||||||
- goos: darwin
|
|
||||||
goarch: arm
|
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout Source
|
|
||||||
uses: https://git.quad4.io/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
|
|
||||||
|
|
||||||
- name: Set up Go
|
|
||||||
uses: https://git.quad4.io/actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00
|
|
||||||
with:
|
|
||||||
go-version: '1.25'
|
|
||||||
|
|
||||||
- name: Setup Task
|
|
||||||
uses: https://git.quad4.io/actions/setup-task@0ab1b2a65bc55236a3bc64cde78f80e20e8885c2
|
|
||||||
with:
|
|
||||||
version: '3.46.3'
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
env:
|
|
||||||
GOOS: ${{ matrix.goos }}
|
|
||||||
GOARCH: ${{ matrix.goarch }}
|
|
||||||
GOARM: ${{ matrix.goarch == 'arm' && '6' || '' }}
|
|
||||||
CGO_ENABLED: '0'
|
|
||||||
run: |
|
|
||||||
output_name="reticulum-go-${{ matrix.goos }}-${{ matrix.goarch }}"
|
|
||||||
if [ "${{ matrix.goos }}" = "js" ] && [ "${{ matrix.goarch }}" = "wasm" ]; then
|
|
||||||
task build-wasm
|
|
||||||
output_name+=".wasm"
|
|
||||||
mv bin/reticulum-go.wasm "${output_name}"
|
|
||||||
else
|
|
||||||
task build
|
|
||||||
if [ "${{ matrix.goos }}" = "windows" ]; then
|
|
||||||
output_name+=".exe"
|
|
||||||
fi
|
|
||||||
mv bin/reticulum-go "${output_name}"
|
|
||||||
fi
|
|
||||||
echo "Built: ${output_name}"
|
|
||||||
|
|
||||||
- name: Calculate SHA256 Checksum
|
|
||||||
run: |
|
|
||||||
output_name="reticulum-go-${{ matrix.goos }}-${{ matrix.goarch }}"
|
|
||||||
if [ "${{ matrix.goos }}" = "windows" ]; then
|
|
||||||
output_name+=".exe"
|
|
||||||
elif [ "${{ matrix.goos }}" = "js" ] && [ "${{ matrix.goarch }}" = "wasm" ]; then
|
|
||||||
output_name+=".wasm"
|
|
||||||
fi
|
|
||||||
BINARY_PATH="${output_name}" task checksum
|
|
||||||
|
|
||||||
- name: Upload Artifact
|
|
||||||
uses: https://git.quad4.io/actions/upload-artifact@ff15f0306b3f739f7b6fd43fb5d26cd321bd4de5
|
|
||||||
with:
|
|
||||||
name: reticulum-go-${{ matrix.goos }}-${{ matrix.goarch }}
|
|
||||||
path: |
|
|
||||||
reticulum-go-${{ matrix.goos }}-${{ matrix.goarch }}*
|
|
||||||
|
|
||||||
release:
|
|
||||||
name: Create Release
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: build
|
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
|
||||||
steps:
|
|
||||||
- name: Download All Build Artifacts
|
|
||||||
uses: https://git.quad4.io/actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a
|
|
||||||
with:
|
|
||||||
path: ./release-assets
|
|
||||||
|
|
||||||
- name: Create Gitea Release
|
|
||||||
uses: https://git.quad4.io/actions/gitea-release-action@4875285c0950474efb7ca2df55233c51333eeb74
|
|
||||||
with:
|
|
||||||
files: ./release-assets/*/*
|
|
||||||
|
|
||||||
@@ -5,29 +5,15 @@ on:
|
|||||||
branches: [ "tinygo" ]
|
branches: [ "tinygo" ]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ "tinygo" ]
|
branches: [ "tinygo" ]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
tinygo-build:
|
tinygo-build-all:
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- name: tinygo-default
|
|
||||||
target: ""
|
|
||||||
output: reticulum-go-tinygo
|
|
||||||
make_target: tinygo-build
|
|
||||||
- name: tinygo-wasm
|
|
||||||
target: wasm
|
|
||||||
output: reticulum-go.wasm
|
|
||||||
make_target: tinygo-wasm
|
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
outputs:
|
|
||||||
build_complete: ${{ steps.build_step.outcome == 'success' }}
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: https://git.quad4.io/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: https://git.quad4.io/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
@@ -37,28 +23,64 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
go-version: '1.24'
|
go-version: '1.24'
|
||||||
|
|
||||||
|
- name: Setup Task
|
||||||
|
uses: https://git.quad4.io/actions/setup-task@0ab1b2a65bc55236a3bc64cde78f80e20e8885c2 # v1
|
||||||
|
with:
|
||||||
|
version: '3.46.3'
|
||||||
|
|
||||||
- name: Install TinyGo
|
- name: Install TinyGo
|
||||||
run: |
|
run: |
|
||||||
wget https://github.com/tinygo-org/tinygo/releases/download/v0.37.0/tinygo_0.37.0_amd64.deb
|
wget https://github.com/tinygo-org/tinygo/releases/download/v0.37.0/tinygo_0.37.0_amd64.deb
|
||||||
sudo dpkg -i tinygo_0.37.0_amd64.deb
|
sudo dpkg -i tinygo_0.37.0_amd64.deb
|
||||||
|
|
||||||
- name: Build with TinyGo
|
- name: Build for all TinyGo targets
|
||||||
id: build_step
|
id: build_step
|
||||||
run: |
|
run: |
|
||||||
make ${{ matrix.make_target }}
|
task tinygo-build-all || true
|
||||||
output_name="${{ matrix.output }}"
|
echo "Build process completed (some targets may have failed)"
|
||||||
if [ -f "bin/${output_name}" ]; then
|
|
||||||
sha256sum "bin/${output_name}" | cut -d' ' -f1 > "bin/${output_name}.sha256"
|
|
||||||
echo "Built: ${output_name}"
|
|
||||||
echo "Generated checksum: bin/${output_name}.sha256"
|
|
||||||
else
|
|
||||||
echo "Build output not found: bin/${output_name}"
|
|
||||||
ls -la bin/
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Upload Artifact
|
- name: Collect build results
|
||||||
|
run: |
|
||||||
|
mkdir -p artifacts
|
||||||
|
unsupported_file="artifacts/unsupported-microcontrollers.txt"
|
||||||
|
echo "# Unsupported Microcontrollers" > "$unsupported_file"
|
||||||
|
echo "# Generated: $(date -u +"%Y-%m-%d %H:%M:%S UTC")" >> "$unsupported_file"
|
||||||
|
echo "" >> "$unsupported_file"
|
||||||
|
|
||||||
|
failed_count=0
|
||||||
|
for log_file in bin/build-*.log; do
|
||||||
|
if [ -f "$log_file" ]; then
|
||||||
|
target=$(basename "$log_file" | sed 's/build-\(.*\)\.log/\1/')
|
||||||
|
binary_file="bin/reticulum-go-${target}"
|
||||||
|
|
||||||
|
if [ ! -f "$binary_file" ] || grep -qi "error\|Error\|ERROR\|failed\|Failed\|FAILED" "$log_file"; then
|
||||||
|
failed_count=$((failed_count + 1))
|
||||||
|
echo "## $target" >> "$unsupported_file"
|
||||||
|
echo "" >> "$unsupported_file"
|
||||||
|
|
||||||
|
if grep -qi "program too large\|overflowed\|too big\|LLVM ERROR\|Error while" "$log_file"; then
|
||||||
|
grep -i "program too large\|overflowed\|too big\|LLVM ERROR\|Error while" "$log_file" | head -5 >> "$unsupported_file"
|
||||||
|
else
|
||||||
|
tail -15 "$log_file" >> "$unsupported_file"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "" >> "$unsupported_file"
|
||||||
|
echo "\`\`\`" >> "$unsupported_file"
|
||||||
|
tail -30 "$log_file" >> "$unsupported_file"
|
||||||
|
echo "\`\`\`" >> "$unsupported_file"
|
||||||
|
echo "" >> "$unsupported_file"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Total failed builds: $failed_count" >> "$unsupported_file"
|
||||||
|
echo "Generated unsupported-microcontrollers.txt with $failed_count failed targets"
|
||||||
|
|
||||||
|
- name: Upload build artifacts
|
||||||
uses: https://git.quad4.io/actions/upload-artifact@ff15f0306b3f739f7b6fd43fb5d26cd321bd4de5
|
uses: https://git.quad4.io/actions/upload-artifact@ff15f0306b3f739f7b6fd43fb5d26cd321bd4de5
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.name }}
|
name: tinygo-builds
|
||||||
path: bin/${{ matrix.output }}*
|
path: |
|
||||||
|
bin/reticulum-go-*
|
||||||
|
artifacts/unsupported-microcontrollers.txt
|
||||||
|
if-no-files-found: warn
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -28,5 +28,4 @@ Thumbs.db # Windows Explorer thumbnail cache
|
|||||||
|
|
||||||
# Swap and test binaries
|
# Swap and test binaries
|
||||||
*.swp # Swap files (e.g. vim)
|
*.swp # Swap files (e.g. vim)
|
||||||
*.test # Go test binaries
|
*.test # Go test binaries
|
||||||
test/compat/
|
|
||||||
14
CONTRIBUTORS
14
CONTRIBUTORS
@@ -1,14 +0,0 @@
|
|||||||
CONTRIBUTORS
|
|
||||||
|
|
||||||
This file lists all contributors to the Reticulum-Go project.
|
|
||||||
|
|
||||||
Sudo-Ivan
|
|
||||||
Total commits: 442
|
|
||||||
First contribution: 2024-12-30
|
|
||||||
Last contribution: 2026-01-01
|
|
||||||
|
|
||||||
|
|
||||||
Mike Coles
|
|
||||||
Total commits: 1
|
|
||||||
First contribution: 2025-08-07
|
|
||||||
Last contribution: 2025-08-07
|
|
||||||
47
Taskfile.yml
47
Taskfile.yml
@@ -141,21 +141,6 @@ tasks:
|
|||||||
cmds:
|
cmds:
|
||||||
- '{{.GOCMD}} test -race -v ./...'
|
- '{{.GOCMD}} test -race -v ./...'
|
||||||
|
|
||||||
test-fuzz:
|
|
||||||
desc: Run fuzz tests for a short duration
|
|
||||||
cmds:
|
|
||||||
- '{{.GOCMD}} test -fuzz=FuzzPacketUnpack -fuzztime=30s ./pkg/packet'
|
|
||||||
|
|
||||||
test-leaks:
|
|
||||||
desc: Run resource leak tests
|
|
||||||
cmds:
|
|
||||||
- '{{.GOCMD}} test -v ./pkg/transport -run TestTransportLeak'
|
|
||||||
|
|
||||||
test-network:
|
|
||||||
desc: Run network simulation tests
|
|
||||||
cmds:
|
|
||||||
- '{{.GOCMD}} test -v ./pkg/transport -run TestTransportNetworkSimulation'
|
|
||||||
|
|
||||||
coverage:
|
coverage:
|
||||||
desc: Generate test coverage report
|
desc: Generate test coverage report
|
||||||
cmds:
|
cmds:
|
||||||
@@ -258,7 +243,37 @@ tasks:
|
|||||||
desc: Build binary with TinyGo compiler
|
desc: Build binary with TinyGo compiler
|
||||||
cmds:
|
cmds:
|
||||||
- mkdir -p {{.BUILD_DIR}}
|
- mkdir -p {{.BUILD_DIR}}
|
||||||
- tinygo build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-tinygo -size short {{.MAIN_PACKAGE}}
|
- tinygo build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-tinygo -size short -opt=z -gc=leaking -panic=trap {{.MAIN_PACKAGE}}
|
||||||
|
|
||||||
|
tinygo-build-debug:
|
||||||
|
desc: Build binary optimized for debugging with TinyGo compiler
|
||||||
|
cmds:
|
||||||
|
- mkdir -p {{.BUILD_DIR}}
|
||||||
|
- tinygo build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-tinygo-debug -size short -opt=1 {{.MAIN_PACKAGE}}
|
||||||
|
|
||||||
|
tinygo-build-all:
|
||||||
|
desc: Build for all available TinyGo targets in parallel
|
||||||
|
cmds:
|
||||||
|
- mkdir -p {{.BUILD_DIR}}
|
||||||
|
- echo "Building for all TinyGo targets in parallel..."
|
||||||
|
- |
|
||||||
|
targets=$(tinygo targets)
|
||||||
|
failed=0
|
||||||
|
# Use xargs to build in parallel, limited to number of CPU cores
|
||||||
|
echo "$targets" | xargs -P $(nproc) -I {} sh -c '
|
||||||
|
target="{}";
|
||||||
|
if [ -n "$target" ]; then
|
||||||
|
echo "Building for target: $target";
|
||||||
|
if ! tinygo build -target "$target" -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-"$target" -size short -opt=z -gc=leaking -panic=trap {{.MAIN_PACKAGE}} 2>&1 | tee {{.BUILD_DIR}}/build-"$target".log; then
|
||||||
|
echo "Failed to build for $target" >> {{.BUILD_DIR}}/build-"$target".log;
|
||||||
|
exit 1;
|
||||||
|
fi;
|
||||||
|
fi' || failed=1
|
||||||
|
|
||||||
|
echo "Build complete. Check {{.BUILD_DIR}}/ for outputs and logs."
|
||||||
|
if [ $failed -ne 0 ]; then
|
||||||
|
echo "Some target(s) failed to build. See logs in {{.BUILD_DIR}}/build-*.log"
|
||||||
|
fi
|
||||||
|
|
||||||
tinygo-wasm:
|
tinygo-wasm:
|
||||||
desc: Build WebAssembly binary with TinyGo compiler
|
desc: Build WebAssembly binary with TinyGo compiler
|
||||||
|
|||||||
@@ -207,6 +207,34 @@ func NewReticulum(cfg *common.ReticulumConfig) (*Reticulum, error) {
|
|||||||
} else {
|
} else {
|
||||||
debug.Log(debug.DEBUG_INFO, "WebSocket interface created successfully", common.STR_NAME, name)
|
debug.Log(debug.DEBUG_INFO, "WebSocket interface created successfully", common.STR_NAME, name)
|
||||||
}
|
}
|
||||||
|
case "SerialInterface":
|
||||||
|
iface, err = interfaces.NewSerialInterface(
|
||||||
|
name,
|
||||||
|
ifaceConfig.Interface,
|
||||||
|
uint32(ifaceConfig.Bitrate), // #nosec G115
|
||||||
|
ifaceConfig.Enabled,
|
||||||
|
)
|
||||||
|
case "RNodeInterface":
|
||||||
|
// RNode usually runs over Serial
|
||||||
|
serial, sErr := interfaces.NewSerialInterface(
|
||||||
|
name+"_serial",
|
||||||
|
ifaceConfig.Interface,
|
||||||
|
uint32(ifaceConfig.Bitrate), // #nosec G115
|
||||||
|
ifaceConfig.Enabled,
|
||||||
|
)
|
||||||
|
if sErr != nil {
|
||||||
|
err = sErr
|
||||||
|
} else {
|
||||||
|
iface, err = interfaces.NewRNodeInterface(
|
||||||
|
name,
|
||||||
|
serial,
|
||||||
|
ifaceConfig.Frequency,
|
||||||
|
ifaceConfig.Bandwidth,
|
||||||
|
ifaceConfig.SF,
|
||||||
|
ifaceConfig.CR,
|
||||||
|
ifaceConfig.TXPower,
|
||||||
|
)
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
debug.Log(debug.DEBUG_CRITICAL, "Unknown interface type", common.STR_TYPE, ifaceConfig.Type)
|
debug.Log(debug.DEBUG_CRITICAL, "Unknown interface type", common.STR_TYPE, ifaceConfig.Type)
|
||||||
continue
|
continue
|
||||||
|
|||||||
BIN
examples/wasm/public/static/reticulum-go.wasm
Executable file
BIN
examples/wasm/public/static/reticulum-go.wasm
Executable file
Binary file not shown.
4
go.mod
4
go.mod
@@ -3,8 +3,6 @@ module git.quad4.io/Networks/Reticulum-Go
|
|||||||
go 1.24.0
|
go 1.24.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/vmihailenco/msgpack/v5 v5.4.1
|
github.com/shamaton/msgpack/v2 v2.4.0
|
||||||
golang.org/x/crypto v0.46.0
|
golang.org/x/crypto v0.46.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
|
||||||
|
|||||||
14
go.sum
14
go.sum
@@ -1,14 +1,4 @@
|
|||||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
github.com/shamaton/msgpack/v2 v2.4.0 h1:O5Z08MRmbo0lA9o2xnQ4TXx6teJbPqEurqcCOQ8Oi/4=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/shamaton/msgpack/v2 v2.4.0/go.mod h1:6khjYnkx73f7VQU7wjcFS9DFjs+59naVWJv1TB7qdOI=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
|
||||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
|
||||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
|
||||||
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
|
|
||||||
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
|
|
||||||
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
|
|
||||||
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
|
|
||||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
|
||||||
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
|
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
|
||||||
"github.com/vmihailenco/msgpack/v5"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
@@ -88,7 +88,7 @@ func (m *Manager) SaveRatchet(identityHash []byte, ratchetKey []byte) error {
|
|||||||
Received: time.Now().Unix(),
|
Received: time.Now().Unix(),
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := msgpack.Marshal(ratchetData)
|
data, err := common.MsgpackMarshal(ratchetData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to marshal ratchet data: %w", err)
|
return fmt.Errorf("failed to marshal ratchet data: %w", err)
|
||||||
}
|
}
|
||||||
@@ -146,7 +146,7 @@ func (m *Manager) LoadRatchets(identityHash []byte) (map[string][]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var ratchetData RatchetData
|
var ratchetData RatchetData
|
||||||
if err := msgpack.Unmarshal(data, &ratchetData); err != nil {
|
if err := common.MsgpackUnmarshal(data, &ratchetData); err != nil {
|
||||||
debug.Log(debug.DEBUG_ERROR, "Corrupted ratchet data", "file", entry.Name(), "error", err)
|
debug.Log(debug.DEBUG_ERROR, "Corrupted ratchet data", "file", entry.Name(), "error", err)
|
||||||
_ = os.Remove(filePath)
|
_ = os.Remove(filePath)
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -21,26 +21,30 @@ type ConfigProvider interface {
|
|||||||
|
|
||||||
// InterfaceConfig represents interface configuration
|
// InterfaceConfig represents interface configuration
|
||||||
type InterfaceConfig struct {
|
type InterfaceConfig struct {
|
||||||
Name string
|
Name string
|
||||||
Type string
|
Type string
|
||||||
Enabled bool
|
Enabled bool
|
||||||
Address string
|
Address string
|
||||||
Port int
|
Port int
|
||||||
TargetHost string
|
TargetHost string
|
||||||
TargetPort int
|
TargetPort int
|
||||||
TargetAddress string
|
TargetAddress string
|
||||||
Interface string
|
Interface string
|
||||||
KISSFraming bool
|
KISSFraming bool
|
||||||
I2PTunneled bool
|
I2PTunneled bool
|
||||||
PreferIPv6 bool
|
PreferIPv6 bool
|
||||||
MaxReconnTries int
|
MaxReconnTries int
|
||||||
Bitrate int64
|
Bitrate int64
|
||||||
MTU int
|
MTU int
|
||||||
GroupID string
|
GroupID string
|
||||||
DiscoveryScope string
|
DiscoveryScope string
|
||||||
DiscoveryPort int
|
DiscoveryPort int
|
||||||
DataPort int
|
DataPort int
|
||||||
MulticastAddrType string
|
Frequency uint32
|
||||||
|
Bandwidth uint32
|
||||||
|
SF uint8
|
||||||
|
CR uint8
|
||||||
|
TXPower uint8
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReticulumConfig represents the main configuration structure
|
// ReticulumConfig represents the main configuration structure
|
||||||
|
|||||||
@@ -39,10 +39,6 @@ type NetworkInterface interface {
|
|||||||
SendLinkPacket([]byte, []byte, time.Time) error
|
SendLinkPacket([]byte, []byte, time.Time) error
|
||||||
SetPacketCallback(PacketCallback)
|
SetPacketCallback(PacketCallback)
|
||||||
GetPacketCallback() PacketCallback
|
GetPacketCallback() PacketCallback
|
||||||
GetTxBytes() uint64
|
|
||||||
GetRxBytes() uint64
|
|
||||||
GetTxPackets() uint64
|
|
||||||
GetRxPackets() uint64
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// BaseInterface provides common implementation for network interfaces
|
// BaseInterface provides common implementation for network interfaces
|
||||||
@@ -60,11 +56,9 @@ type BaseInterface struct {
|
|||||||
MTU int
|
MTU int
|
||||||
Bitrate int64
|
Bitrate int64
|
||||||
|
|
||||||
TxBytes uint64
|
TxBytes uint64
|
||||||
RxBytes uint64
|
RxBytes uint64
|
||||||
TxPackets uint64
|
lastTx time.Time
|
||||||
RxPackets uint64
|
|
||||||
lastTx time.Time
|
|
||||||
|
|
||||||
Mutex sync.RWMutex
|
Mutex sync.RWMutex
|
||||||
Owner interface{}
|
Owner interface{}
|
||||||
@@ -131,30 +125,6 @@ func (i *BaseInterface) GetPacketCallback() PacketCallback {
|
|||||||
return i.PacketCallback
|
return i.PacketCallback
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *BaseInterface) GetTxBytes() uint64 {
|
|
||||||
i.Mutex.RLock()
|
|
||||||
defer i.Mutex.RUnlock()
|
|
||||||
return i.TxBytes
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *BaseInterface) GetRxBytes() uint64 {
|
|
||||||
i.Mutex.RLock()
|
|
||||||
defer i.Mutex.RUnlock()
|
|
||||||
return i.RxBytes
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *BaseInterface) GetTxPackets() uint64 {
|
|
||||||
i.Mutex.RLock()
|
|
||||||
defer i.Mutex.RUnlock()
|
|
||||||
return i.TxPackets
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *BaseInterface) GetRxPackets() uint64 {
|
|
||||||
i.Mutex.RLock()
|
|
||||||
defer i.Mutex.RUnlock()
|
|
||||||
return i.RxPackets
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *BaseInterface) Detach() {
|
func (i *BaseInterface) Detach() {
|
||||||
i.Mutex.Lock()
|
i.Mutex.Lock()
|
||||||
defer i.Mutex.Unlock()
|
defer i.Mutex.Unlock()
|
||||||
@@ -190,20 +160,10 @@ func (i *BaseInterface) GetConn() net.Conn {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (i *BaseInterface) Send(data []byte, address string) error {
|
func (i *BaseInterface) Send(data []byte, address string) error {
|
||||||
i.Mutex.Lock()
|
|
||||||
i.TxBytes += uint64(len(data))
|
|
||||||
i.TxPackets++
|
|
||||||
i.lastTx = time.Now()
|
|
||||||
i.Mutex.Unlock()
|
|
||||||
return i.ProcessOutgoing(data)
|
return i.ProcessOutgoing(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *BaseInterface) ProcessIncoming(data []byte) {
|
func (i *BaseInterface) ProcessIncoming(data []byte) {
|
||||||
i.Mutex.Lock()
|
|
||||||
i.RxBytes += uint64(len(data))
|
|
||||||
i.RxPackets++
|
|
||||||
i.Mutex.Unlock()
|
|
||||||
|
|
||||||
if i.PacketCallback != nil {
|
if i.PacketCallback != nil {
|
||||||
i.PacketCallback(data, i)
|
i.PacketCallback(data, i)
|
||||||
}
|
}
|
||||||
|
|||||||
17
pkg/common/msgpack.go
Normal file
17
pkg/common/msgpack.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/shamaton/msgpack/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Marshal returns the MessagePack encoding of v.
|
||||||
|
func MsgpackMarshal(v interface{}) ([]byte, error) {
|
||||||
|
return msgpack.Marshal(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unmarshal parses the MessagePack-encoded data and stores the result in the value pointed to by v.
|
||||||
|
func MsgpackUnmarshal(data []byte, v interface{}) error {
|
||||||
|
return msgpack.Unmarshal(data, v)
|
||||||
|
}
|
||||||
@@ -17,7 +17,6 @@ import (
|
|||||||
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
|
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
|
||||||
"git.quad4.io/Networks/Reticulum-Go/pkg/identity"
|
"git.quad4.io/Networks/Reticulum-Go/pkg/identity"
|
||||||
"git.quad4.io/Networks/Reticulum-Go/pkg/packet"
|
"git.quad4.io/Networks/Reticulum-Go/pkg/packet"
|
||||||
"github.com/vmihailenco/msgpack/v5"
|
|
||||||
"golang.org/x/crypto/curve25519"
|
"golang.org/x/crypto/curve25519"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -107,9 +106,9 @@ type Destination struct {
|
|||||||
func New(id *identity.Identity, direction byte, destType byte, appName string, transport Transport, aspects ...string) (*Destination, error) {
|
func New(id *identity.Identity, direction byte, destType byte, appName string, transport Transport, aspects ...string) (*Destination, error) {
|
||||||
debug.Log(debug.DEBUG_INFO, "Creating new destination", "app", appName, "type", destType, "direction", direction)
|
debug.Log(debug.DEBUG_INFO, "Creating new destination", "app", appName, "type", destType, "direction", direction)
|
||||||
|
|
||||||
if id == nil && destType != PLAIN {
|
if id == nil {
|
||||||
debug.Log(debug.DEBUG_ERROR, "Cannot create destination: identity is nil for non-PLAIN destination")
|
debug.Log(debug.DEBUG_ERROR, "Cannot create destination: identity is nil")
|
||||||
return nil, errors.New("identity cannot be nil for non-PLAIN destination")
|
return nil, errors.New("identity cannot be nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
d := &Destination{
|
d := &Destination{
|
||||||
@@ -144,9 +143,9 @@ func New(id *identity.Identity, direction byte, destType byte, appName string, t
|
|||||||
func FromHash(hash []byte, id *identity.Identity, destType byte, transport Transport) (*Destination, error) {
|
func FromHash(hash []byte, id *identity.Identity, destType byte, transport Transport) (*Destination, error) {
|
||||||
debug.Log(debug.DEBUG_INFO, "Creating destination from hash", "hash", fmt.Sprintf("%x", hash))
|
debug.Log(debug.DEBUG_INFO, "Creating destination from hash", "hash", fmt.Sprintf("%x", hash))
|
||||||
|
|
||||||
if id == nil && destType != PLAIN {
|
if id == nil {
|
||||||
debug.Log(debug.DEBUG_ERROR, "Cannot create destination: identity is nil for non-PLAIN destination")
|
debug.Log(debug.DEBUG_ERROR, "Cannot create destination: identity is nil")
|
||||||
return nil, errors.New("identity cannot be nil for non-PLAIN destination")
|
return nil, errors.New("identity cannot be nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
d := &Destination{
|
d := &Destination{
|
||||||
@@ -169,25 +168,19 @@ func FromHash(hash []byte, id *identity.Identity, destType byte, transport Trans
|
|||||||
func (d *Destination) calculateHash() []byte {
|
func (d *Destination) calculateHash() []byte {
|
||||||
debug.Log(debug.DEBUG_TRACE, "Calculating hash for destination", "name", d.ExpandName())
|
debug.Log(debug.DEBUG_TRACE, "Calculating hash for destination", "name", d.ExpandName())
|
||||||
|
|
||||||
|
// destination_hash = SHA256(name_hash_10bytes + identity_hash_16bytes)[:16]
|
||||||
|
// Identity hash is the truncated hash of the public key (16 bytes)
|
||||||
|
identityHash := identity.TruncatedHash(d.identity.GetPublicKey())
|
||||||
|
|
||||||
// Name hash is the FULL 32-byte SHA256, then we take first 10 bytes for concatenation
|
// Name hash is the FULL 32-byte SHA256, then we take first 10 bytes for concatenation
|
||||||
nameHashFull := sha256.Sum256([]byte(d.ExpandName()))
|
nameHashFull := sha256.Sum256([]byte(d.ExpandName()))
|
||||||
nameHash10 := nameHashFull[:10] // Only use 10 bytes
|
nameHash10 := nameHashFull[:10] // Only use 10 bytes
|
||||||
|
|
||||||
var combined []byte
|
debug.Log(debug.DEBUG_ALL, "Identity hash", "hash", fmt.Sprintf("%x", identityHash))
|
||||||
if d.identity != nil {
|
debug.Log(debug.DEBUG_ALL, "Name hash (10 bytes)", "hash", fmt.Sprintf("%x", nameHash10))
|
||||||
// destination_hash = SHA256(name_hash_10bytes + identity_hash_16bytes)[:16]
|
|
||||||
// Identity hash is the truncated hash of the public key (16 bytes)
|
|
||||||
identityHash := identity.TruncatedHash(d.identity.GetPublicKey())
|
|
||||||
debug.Log(debug.DEBUG_ALL, "Identity hash", "hash", fmt.Sprintf("%x", identityHash))
|
|
||||||
debug.Log(debug.DEBUG_ALL, "Name hash (10 bytes)", "hash", fmt.Sprintf("%x", nameHash10))
|
|
||||||
|
|
||||||
// Concatenate name_hash (10 bytes) + identity_hash (16 bytes) = 26 bytes
|
// Concatenate name_hash (10 bytes) + identity_hash (16 bytes) = 26 bytes
|
||||||
combined = append(nameHash10, identityHash...)
|
combined := append(nameHash10, identityHash...)
|
||||||
} else {
|
|
||||||
// PLAIN destination has no identity hash
|
|
||||||
combined = nameHash10
|
|
||||||
debug.Log(debug.DEBUG_ALL, "Name hash (10 bytes)", "hash", fmt.Sprintf("%x", nameHash10))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Then hash again and truncate to 16 bytes
|
// Then hash again and truncate to 16 bytes
|
||||||
finalHashFull := sha256.Sum256(combined)
|
finalHashFull := sha256.Sum256(combined)
|
||||||
@@ -613,7 +606,7 @@ func (d *Destination) persistRatchets() error {
|
|||||||
debug.Log(debug.DEBUG_PACKETS, "Persisting ratchets", "count", len(d.ratchets), "path", d.ratchetPath)
|
debug.Log(debug.DEBUG_PACKETS, "Persisting ratchets", "count", len(d.ratchets), "path", d.ratchetPath)
|
||||||
|
|
||||||
// Pack ratchets using msgpack
|
// Pack ratchets using msgpack
|
||||||
packedRatchets, err := msgpack.Marshal(d.ratchets)
|
packedRatchets, err := common.MsgpackMarshal(d.ratchets)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to pack ratchets: %w", err)
|
return fmt.Errorf("failed to pack ratchets: %w", err)
|
||||||
}
|
}
|
||||||
@@ -631,7 +624,7 @@ func (d *Destination) persistRatchets() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Pack the entire structure
|
// Pack the entire structure
|
||||||
finalData, err := msgpack.Marshal(persistedData)
|
finalData, err := common.MsgpackMarshal(persistedData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to pack ratchet data: %w", err)
|
return fmt.Errorf("failed to pack ratchet data: %w", err)
|
||||||
}
|
}
|
||||||
@@ -694,7 +687,7 @@ func (d *Destination) reloadRatchets() error {
|
|||||||
|
|
||||||
// Unpack outer structure
|
// Unpack outer structure
|
||||||
var persistedData map[string][]byte
|
var persistedData map[string][]byte
|
||||||
if err := msgpack.Unmarshal(fileData, &persistedData); err != nil {
|
if err := common.MsgpackUnmarshal(fileData, &persistedData); err != nil {
|
||||||
return fmt.Errorf("failed to unpack ratchet data: %w", err)
|
return fmt.Errorf("failed to unpack ratchet data: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -711,7 +704,7 @@ func (d *Destination) reloadRatchets() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Unpack ratchet list
|
// Unpack ratchet list
|
||||||
if err := msgpack.Unmarshal(packedRatchets, &d.ratchets); err != nil {
|
if err := common.MsgpackUnmarshal(packedRatchets, &d.ratchets); err != nil {
|
||||||
return fmt.Errorf("failed to unpack ratchet list: %w", err)
|
return fmt.Errorf("failed to unpack ratchet list: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package destination
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"crypto/sha256"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -151,28 +150,3 @@ func TestPlainDestination(t *testing.T) {
|
|||||||
t.Error("Plain destination should not decrypt")
|
t.Error("Plain destination should not decrypt")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPlainDestinationHash(t *testing.T) {
|
|
||||||
// A PLAIN destination with no identity should have a hash based only on its name
|
|
||||||
transport := &mockTransport{}
|
|
||||||
dest, err := New(nil, IN|OUT, PLAIN, "testapp", transport, "testaspect")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("New failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
hash := dest.GetHash()
|
|
||||||
if len(hash) != 16 {
|
|
||||||
t.Fatalf("Expected hash length 16, got %d", len(hash))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate manually: SHA256(SHA256("testapp.testaspect")[:10])[:16]
|
|
||||||
name := "testapp.testaspect"
|
|
||||||
nameHashFull := sha256.Sum256([]byte(name))
|
|
||||||
nameHash10 := nameHashFull[:10]
|
|
||||||
finalHashFull := sha256.Sum256(nameHash10)
|
|
||||||
expectedHash := finalHashFull[:16]
|
|
||||||
|
|
||||||
if !bytes.Equal(hash, expectedHash) {
|
|
||||||
t.Errorf("Expected hash %x, got %x", expectedHash, hash)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import (
|
|||||||
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
|
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
|
||||||
"git.quad4.io/Networks/Reticulum-Go/pkg/cryptography"
|
"git.quad4.io/Networks/Reticulum-Go/pkg/cryptography"
|
||||||
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
|
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
|
||||||
"github.com/vmihailenco/msgpack/v5"
|
|
||||||
"golang.org/x/crypto/curve25519"
|
"golang.org/x/crypto/curve25519"
|
||||||
"golang.org/x/crypto/hkdf"
|
"golang.org/x/crypto/hkdf"
|
||||||
)
|
)
|
||||||
@@ -672,7 +671,7 @@ func (i *Identity) saveRatchets(path string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Pack ratchets using msgpack
|
// Pack ratchets using msgpack
|
||||||
packedRatchets, err := msgpack.Marshal(ratchetList)
|
packedRatchets, err := common.MsgpackMarshal(ratchetList)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to pack ratchets: %w", err)
|
return fmt.Errorf("failed to pack ratchets: %w", err)
|
||||||
}
|
}
|
||||||
@@ -687,7 +686,7 @@ func (i *Identity) saveRatchets(path string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Pack the entire structure
|
// Pack the entire structure
|
||||||
finalData, err := msgpack.Marshal(persistedData)
|
finalData, err := common.MsgpackMarshal(persistedData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to pack ratchet data: %w", err)
|
return fmt.Errorf("failed to pack ratchet data: %w", err)
|
||||||
}
|
}
|
||||||
@@ -800,7 +799,7 @@ func (i *Identity) loadRatchets(path string) error {
|
|||||||
|
|
||||||
// Unpack outer structure: {"signature": ..., "ratchets": ...}
|
// Unpack outer structure: {"signature": ..., "ratchets": ...}
|
||||||
var persistedData map[string][]byte
|
var persistedData map[string][]byte
|
||||||
if err := msgpack.Unmarshal(fileData, &persistedData); err != nil {
|
if err := common.MsgpackUnmarshal(fileData, &persistedData); err != nil {
|
||||||
return fmt.Errorf("failed to unpack ratchet data: %w", err)
|
return fmt.Errorf("failed to unpack ratchet data: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -818,7 +817,7 @@ func (i *Identity) loadRatchets(path string) error {
|
|||||||
|
|
||||||
// Unpack ratchet list
|
// Unpack ratchet list
|
||||||
var ratchetList [][]byte
|
var ratchetList [][]byte
|
||||||
if err := msgpack.Unmarshal(packedRatchets, &ratchetList); err != nil {
|
if err := common.MsgpackUnmarshal(packedRatchets, &ratchetList); err != nil {
|
||||||
return fmt.Errorf("failed to unpack ratchet list: %w", err)
|
return fmt.Errorf("failed to unpack ratchet list: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
// SPDX-License-Identifier: 0BSD
|
// SPDX-License-Identifier: 0BSD
|
||||||
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
|
//go:build !tinygo
|
||||||
|
// +build !tinygo
|
||||||
|
|
||||||
package interfaces
|
package interfaces
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -34,16 +37,8 @@ const (
|
|||||||
|
|
||||||
MCAST_ADDR_TYPE_PERMANENT = "0"
|
MCAST_ADDR_TYPE_PERMANENT = "0"
|
||||||
MCAST_ADDR_TYPE_TEMPORARY = "1"
|
MCAST_ADDR_TYPE_TEMPORARY = "1"
|
||||||
|
|
||||||
MULTI_IF_DEQUE_LEN = 48
|
|
||||||
MULTI_IF_DEQUE_TTL = 750 * time.Millisecond
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type DequeEntry struct {
|
|
||||||
hash [32]byte
|
|
||||||
timestamp time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
type AutoInterface struct {
|
type AutoInterface struct {
|
||||||
BaseInterface
|
BaseInterface
|
||||||
groupID []byte
|
groupID []byte
|
||||||
@@ -53,6 +48,7 @@ type AutoInterface struct {
|
|||||||
discoveryScope string
|
discoveryScope string
|
||||||
multicastAddrType string
|
multicastAddrType string
|
||||||
mcastDiscoveryAddr string
|
mcastDiscoveryAddr string
|
||||||
|
ifacNetname string
|
||||||
peers map[string]*Peer
|
peers map[string]*Peer
|
||||||
linkLocalAddrs []string
|
linkLocalAddrs []string
|
||||||
adoptedInterfaces map[string]*AdoptedInterface
|
adoptedInterfaces map[string]*AdoptedInterface
|
||||||
@@ -67,7 +63,6 @@ type AutoInterface struct {
|
|||||||
peerJobInterval time.Duration
|
peerJobInterval time.Duration
|
||||||
peeringTimeout time.Duration
|
peeringTimeout time.Duration
|
||||||
mcastEchoTimeout time.Duration
|
mcastEchoTimeout time.Duration
|
||||||
mifDeque []DequeEntry
|
|
||||||
done chan struct{}
|
done chan struct{}
|
||||||
stopOnce sync.Once
|
stopOnce sync.Once
|
||||||
}
|
}
|
||||||
@@ -84,24 +79,6 @@ type Peer struct {
|
|||||||
addr *net.UDPAddr
|
addr *net.UDPAddr
|
||||||
}
|
}
|
||||||
|
|
||||||
func descopeLinkLocal(addr string) string {
|
|
||||||
// Drop scope specifier expressed as %ifname (macOS)
|
|
||||||
if i := strings.Index(addr, "%"); i != -1 {
|
|
||||||
addr = addr[:i]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Drop embedded scope specifier (NetBSD, OpenBSD)
|
|
||||||
// Python: re.sub(r"fe80:[0-9a-f]*::","fe80::", link_local_addr)
|
|
||||||
if strings.HasPrefix(addr, "fe80:") {
|
|
||||||
parts := strings.Split(addr, ":")
|
|
||||||
// Check for fe80:[scope]::...
|
|
||||||
if len(parts) >= 3 && parts[2] == "" && parts[1] != "" {
|
|
||||||
return "fe80::" + strings.Join(parts[3:], ":")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return addr
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewAutoInterface(name string, config *common.InterfaceConfig) (*AutoInterface, error) {
|
func NewAutoInterface(name string, config *common.InterfaceConfig) (*AutoInterface, error) {
|
||||||
groupID := DEFAULT_GROUP_ID
|
groupID := DEFAULT_GROUP_ID
|
||||||
if config.GroupID != "" {
|
if config.GroupID != "" {
|
||||||
@@ -114,9 +91,6 @@ func NewAutoInterface(name string, config *common.InterfaceConfig) (*AutoInterfa
|
|||||||
}
|
}
|
||||||
|
|
||||||
multicastAddrType := MCAST_ADDR_TYPE_TEMPORARY
|
multicastAddrType := MCAST_ADDR_TYPE_TEMPORARY
|
||||||
if config.MulticastAddrType != "" {
|
|
||||||
multicastAddrType = normalizeMulticastType(config.MulticastAddrType)
|
|
||||||
}
|
|
||||||
|
|
||||||
discoveryPort := DEFAULT_DISCOVERY_PORT
|
discoveryPort := DEFAULT_DISCOVERY_PORT
|
||||||
if config.DiscoveryPort != 0 {
|
if config.DiscoveryPort != 0 {
|
||||||
@@ -130,13 +104,8 @@ func NewAutoInterface(name string, config *common.InterfaceConfig) (*AutoInterfa
|
|||||||
|
|
||||||
groupHash := sha256.Sum256([]byte(groupID))
|
groupHash := sha256.Sum256([]byte(groupID))
|
||||||
|
|
||||||
// Python-compatible multicast address generation
|
ifacNetname := hex.EncodeToString(groupHash[:])[:16]
|
||||||
// gt = "0:" + "{:02x}".format(g[3]+(g[2]<<8)) + ":" + ...
|
mcastAddr := fmt.Sprintf("ff%s%s::%s", discoveryScope, multicastAddrType, ifacNetname)
|
||||||
gt := "0"
|
|
||||||
for i := 1; i <= 6; i++ {
|
|
||||||
gt += fmt.Sprintf(":%02x%02x", groupHash[i*2], groupHash[i*2+1])
|
|
||||||
}
|
|
||||||
mcastAddr := fmt.Sprintf("ff%s%s:%s", multicastAddrType, discoveryScope, gt)
|
|
||||||
|
|
||||||
ai := &AutoInterface{
|
ai := &AutoInterface{
|
||||||
BaseInterface: BaseInterface{
|
BaseInterface: BaseInterface{
|
||||||
@@ -158,6 +127,7 @@ func NewAutoInterface(name string, config *common.InterfaceConfig) (*AutoInterfa
|
|||||||
discoveryScope: discoveryScope,
|
discoveryScope: discoveryScope,
|
||||||
multicastAddrType: multicastAddrType,
|
multicastAddrType: multicastAddrType,
|
||||||
mcastDiscoveryAddr: mcastAddr,
|
mcastDiscoveryAddr: mcastAddr,
|
||||||
|
ifacNetname: ifacNetname,
|
||||||
peers: make(map[string]*Peer),
|
peers: make(map[string]*Peer),
|
||||||
linkLocalAddrs: make([]string, 0),
|
linkLocalAddrs: make([]string, 0),
|
||||||
adoptedInterfaces: make(map[string]*AdoptedInterface),
|
adoptedInterfaces: make(map[string]*AdoptedInterface),
|
||||||
@@ -171,7 +141,6 @@ func NewAutoInterface(name string, config *common.InterfaceConfig) (*AutoInterfa
|
|||||||
peerJobInterval: PEER_JOB_INTERVAL,
|
peerJobInterval: PEER_JOB_INTERVAL,
|
||||||
peeringTimeout: PEERING_TIMEOUT,
|
peeringTimeout: PEERING_TIMEOUT,
|
||||||
mcastEchoTimeout: MCAST_ECHO_TIMEOUT,
|
mcastEchoTimeout: MCAST_ECHO_TIMEOUT,
|
||||||
mifDeque: make([]DequeEntry, 0, MULTI_IF_DEQUE_LEN),
|
|
||||||
done: make(chan struct{}),
|
done: make(chan struct{}),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -306,7 +275,7 @@ func (ai *AutoInterface) configureInterface(iface *net.Interface) error {
|
|||||||
for _, addr := range addrs {
|
for _, addr := range addrs {
|
||||||
if ipnet, ok := addr.(*net.IPNet); ok {
|
if ipnet, ok := addr.(*net.IPNet); ok {
|
||||||
if ipnet.IP.To4() == nil && ipnet.IP.IsLinkLocalUnicast() {
|
if ipnet.IP.To4() == nil && ipnet.IP.IsLinkLocalUnicast() {
|
||||||
linkLocalAddr = descopeLinkLocal(ipnet.IP.String())
|
linkLocalAddr = ipnet.IP.String()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -415,17 +384,12 @@ func (ai *AutoInterface) handleDiscovery(conn *net.UDPConn, ifaceName string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Python: discovery_token = RNS.Identity.full_hash(self.group_id+ipv6_src[0].encode("utf-8"))
|
if n >= len(ai.groupHash) {
|
||||||
peerIP := descopeLinkLocal(remoteAddr.IP.String())
|
receivedHash := buf[:len(ai.groupHash)]
|
||||||
tokenSource := append(ai.groupID, []byte(peerIP)...)
|
if bytes.Equal(receivedHash, ai.groupHash) {
|
||||||
expectedHash := sha256.Sum256(tokenSource)
|
|
||||||
|
|
||||||
if n >= len(expectedHash) {
|
|
||||||
receivedHash := buf[:len(expectedHash)]
|
|
||||||
if bytes.Equal(receivedHash, expectedHash[:]) {
|
|
||||||
ai.handlePeerAnnounce(remoteAddr, ifaceName)
|
ai.handlePeerAnnounce(remoteAddr, ifaceName)
|
||||||
} else {
|
} else {
|
||||||
debug.Log(debug.DEBUG_TRACE, "Received discovery with mismatched group hash", "interface", ifaceName, "peer", peerIP)
|
debug.Log(debug.DEBUG_TRACE, "Received discovery with mismatched group hash", "interface", ifaceName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -444,7 +408,7 @@ func (ai *AutoInterface) handleData(conn *net.UDPConn, ifaceName string) {
|
|||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
n, remoteAddr, err := conn.ReadFromUDP(buf)
|
n, _, err := conn.ReadFromUDP(buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if ai.IsOnline() {
|
if ai.IsOnline() {
|
||||||
debug.Log(debug.DEBUG_ERROR, "Data read error", "interface", ifaceName, "error", err)
|
debug.Log(debug.DEBUG_ERROR, "Data read error", "interface", ifaceName, "error", err)
|
||||||
@@ -452,41 +416,8 @@ func (ai *AutoInterface) handleData(conn *net.UDPConn, ifaceName string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
data := buf[:n]
|
|
||||||
dataHash := sha256.Sum256(data)
|
|
||||||
now := time.Now()
|
|
||||||
|
|
||||||
ai.Mutex.Lock()
|
|
||||||
// Check for duplicate in mifDeque
|
|
||||||
isDuplicate := false
|
|
||||||
for i := 0; i < len(ai.mifDeque); i++ {
|
|
||||||
if ai.mifDeque[i].hash == dataHash && now.Sub(ai.mifDeque[i].timestamp) < MULTI_IF_DEQUE_TTL {
|
|
||||||
isDuplicate = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if isDuplicate {
|
|
||||||
ai.Mutex.Unlock()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add to deque
|
|
||||||
ai.mifDeque = append(ai.mifDeque, DequeEntry{hash: dataHash, timestamp: now})
|
|
||||||
if len(ai.mifDeque) > MULTI_IF_DEQUE_LEN {
|
|
||||||
ai.mifDeque = ai.mifDeque[1:]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Refresh peer if known
|
|
||||||
peerIP := descopeLinkLocal(remoteAddr.IP.String())
|
|
||||||
peerKey := peerIP + "%" + ifaceName
|
|
||||||
if peer, exists := ai.peers[peerKey]; exists {
|
|
||||||
peer.lastHeard = now
|
|
||||||
}
|
|
||||||
ai.Mutex.Unlock()
|
|
||||||
|
|
||||||
if callback := ai.GetPacketCallback(); callback != nil {
|
if callback := ai.GetPacketCallback(); callback != nil {
|
||||||
callback(data, ai)
|
callback(buf[:n], ai)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -557,11 +488,7 @@ func (ai *AutoInterface) sendPeerAnnounce() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Python: discovery_token = RNS.Identity.full_hash(self.group_id+link_local_address.encode("utf-8"))
|
if _, err := ai.outboundConn.WriteToUDP(ai.groupHash, mcastAddr); err != nil {
|
||||||
tokenSource := append(ai.groupID, []byte(adoptedIface.linkLocalAddr)...)
|
|
||||||
token := sha256.Sum256(tokenSource)
|
|
||||||
|
|
||||||
if _, err := ai.outboundConn.WriteToUDP(token[:], mcastAddr); err != nil {
|
|
||||||
debug.Log(debug.DEBUG_VERBOSE, "Failed to send peer announce", "interface", ifaceName, "error", err)
|
debug.Log(debug.DEBUG_VERBOSE, "Failed to send peer announce", "interface", ifaceName, "error", err)
|
||||||
} else {
|
} else {
|
||||||
debug.Log(debug.DEBUG_TRACE, "Sent peer announce", "interface", adoptedIface.name)
|
debug.Log(debug.DEBUG_TRACE, "Sent peer announce", "interface", adoptedIface.name)
|
||||||
|
|||||||
96
pkg/interfaces/auto_tinygo.go
Normal file
96
pkg/interfaces/auto_tinygo.go
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
|
//go:build tinygo
|
||||||
|
// +build tinygo
|
||||||
|
|
||||||
|
package interfaces
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
HW_MTU = 1196
|
||||||
|
DEFAULT_DISCOVERY_PORT = 29716
|
||||||
|
DEFAULT_DATA_PORT = 42671
|
||||||
|
DEFAULT_GROUP_ID = "reticulum"
|
||||||
|
BITRATE_GUESS = 10 * 1000 * 1000
|
||||||
|
)
|
||||||
|
|
||||||
|
type AutoInterface struct {
|
||||||
|
BaseInterface
|
||||||
|
groupID []byte
|
||||||
|
discoveryPort int
|
||||||
|
dataPort int
|
||||||
|
discoveryScope string
|
||||||
|
peers map[string]*Peer
|
||||||
|
linkLocalAddrs []string
|
||||||
|
adoptedInterfaces map[string]string
|
||||||
|
interfaceServers map[string]net.Conn
|
||||||
|
multicastEchoes map[string]time.Time
|
||||||
|
mutex sync.RWMutex
|
||||||
|
outboundConn net.Conn
|
||||||
|
}
|
||||||
|
|
||||||
|
type Peer struct {
|
||||||
|
ifaceName string
|
||||||
|
lastHeard time.Time
|
||||||
|
conn net.PacketConn
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAutoInterface(name string, config *common.InterfaceConfig) (*AutoInterface, error) {
|
||||||
|
ai := &AutoInterface{
|
||||||
|
BaseInterface: BaseInterface{
|
||||||
|
Name: name,
|
||||||
|
Mode: common.IF_MODE_FULL,
|
||||||
|
Type: common.IF_TYPE_AUTO,
|
||||||
|
Online: false,
|
||||||
|
Enabled: config.Enabled,
|
||||||
|
Detached: false,
|
||||||
|
IN: true,
|
||||||
|
OUT: false,
|
||||||
|
MTU: HW_MTU,
|
||||||
|
Bitrate: BITRATE_GUESS,
|
||||||
|
},
|
||||||
|
discoveryPort: DEFAULT_DISCOVERY_PORT,
|
||||||
|
dataPort: DEFAULT_DATA_PORT,
|
||||||
|
peers: make(map[string]*Peer),
|
||||||
|
linkLocalAddrs: make([]string, 0),
|
||||||
|
adoptedInterfaces: make(map[string]string),
|
||||||
|
interfaceServers: make(map[string]net.Conn),
|
||||||
|
multicastEchoes: make(map[string]time.Time),
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Port != 0 {
|
||||||
|
ai.discoveryPort = config.Port
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.GroupID != "" {
|
||||||
|
ai.groupID = []byte(config.GroupID)
|
||||||
|
} else {
|
||||||
|
ai.groupID = []byte("reticulum")
|
||||||
|
}
|
||||||
|
|
||||||
|
return ai, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ai *AutoInterface) Start() error {
|
||||||
|
// TinyGo doesn't support net.Interfaces() or multicast UDP
|
||||||
|
return fmt.Errorf("AutoInterface not supported in TinyGo - requires interface enumeration and multicast UDP")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ai *AutoInterface) Send(data []byte, address string) error {
|
||||||
|
return fmt.Errorf("Send not supported in TinyGo - requires UDP client connections")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ai *AutoInterface) Stop() error {
|
||||||
|
ai.Mutex.Lock()
|
||||||
|
defer ai.Mutex.Unlock()
|
||||||
|
ai.Online = false
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -56,22 +56,20 @@ type Interface interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type BaseInterface struct {
|
type BaseInterface struct {
|
||||||
Name string
|
Name string
|
||||||
Mode common.InterfaceMode
|
Mode common.InterfaceMode
|
||||||
Type common.InterfaceType
|
Type common.InterfaceType
|
||||||
Online bool
|
Online bool
|
||||||
Enabled bool
|
Enabled bool
|
||||||
Detached bool
|
Detached bool
|
||||||
IN bool
|
IN bool
|
||||||
OUT bool
|
OUT bool
|
||||||
MTU int
|
MTU int
|
||||||
Bitrate int64
|
Bitrate int64
|
||||||
TxBytes uint64
|
TxBytes uint64
|
||||||
RxBytes uint64
|
RxBytes uint64
|
||||||
TxPackets uint64
|
lastTx time.Time
|
||||||
RxPackets uint64
|
lastRx time.Time
|
||||||
lastTx time.Time
|
|
||||||
lastRx time.Time
|
|
||||||
|
|
||||||
Mutex sync.RWMutex
|
Mutex sync.RWMutex
|
||||||
packetCallback common.PacketCallback
|
packetCallback common.PacketCallback
|
||||||
@@ -79,22 +77,18 @@ type BaseInterface struct {
|
|||||||
|
|
||||||
func NewBaseInterface(name string, ifType common.InterfaceType, enabled bool) BaseInterface {
|
func NewBaseInterface(name string, ifType common.InterfaceType, enabled bool) BaseInterface {
|
||||||
return BaseInterface{
|
return BaseInterface{
|
||||||
Name: name,
|
Name: name,
|
||||||
Mode: common.IF_MODE_FULL,
|
Mode: common.IF_MODE_FULL,
|
||||||
Type: ifType,
|
Type: ifType,
|
||||||
Online: false,
|
Online: false,
|
||||||
Enabled: enabled,
|
Enabled: enabled,
|
||||||
Detached: false,
|
Detached: false,
|
||||||
IN: false,
|
IN: false,
|
||||||
OUT: false,
|
OUT: false,
|
||||||
MTU: common.DEFAULT_MTU,
|
MTU: common.DEFAULT_MTU,
|
||||||
Bitrate: BITRATE_MINIMUM,
|
Bitrate: BITRATE_MINIMUM,
|
||||||
TxBytes: 0,
|
lastTx: time.Now(),
|
||||||
RxBytes: 0,
|
lastRx: time.Now(),
|
||||||
TxPackets: 0,
|
|
||||||
RxPackets: 0,
|
|
||||||
lastTx: time.Now(),
|
|
||||||
lastRx: time.Now(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,7 +107,6 @@ func (i *BaseInterface) GetPacketCallback() common.PacketCallback {
|
|||||||
func (i *BaseInterface) ProcessIncoming(data []byte) {
|
func (i *BaseInterface) ProcessIncoming(data []byte) {
|
||||||
i.Mutex.Lock()
|
i.Mutex.Lock()
|
||||||
i.RxBytes += uint64(len(data))
|
i.RxBytes += uint64(len(data))
|
||||||
i.RxPackets++
|
|
||||||
i.Mutex.Unlock()
|
i.Mutex.Unlock()
|
||||||
|
|
||||||
i.Mutex.RLock()
|
i.Mutex.RLock()
|
||||||
@@ -133,7 +126,6 @@ func (i *BaseInterface) ProcessOutgoing(data []byte) error {
|
|||||||
|
|
||||||
i.Mutex.Lock()
|
i.Mutex.Lock()
|
||||||
i.TxBytes += uint64(len(data))
|
i.TxBytes += uint64(len(data))
|
||||||
i.TxPackets++
|
|
||||||
i.Mutex.Unlock()
|
i.Mutex.Unlock()
|
||||||
|
|
||||||
debug.Log(debug.DEBUG_VERBOSE, "Interface processed outgoing packet", "name", i.Name, "bytes", len(data), "total_tx", i.TxBytes)
|
debug.Log(debug.DEBUG_VERBOSE, "Interface processed outgoing packet", "name", i.Name, "bytes", len(data), "total_tx", i.TxBytes)
|
||||||
@@ -229,30 +221,6 @@ func (i *BaseInterface) IsDetached() bool {
|
|||||||
return i.Detached
|
return i.Detached
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *BaseInterface) GetTxBytes() uint64 {
|
|
||||||
i.Mutex.RLock()
|
|
||||||
defer i.Mutex.RUnlock()
|
|
||||||
return i.TxBytes
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *BaseInterface) GetRxBytes() uint64 {
|
|
||||||
i.Mutex.RLock()
|
|
||||||
defer i.Mutex.RUnlock()
|
|
||||||
return i.RxBytes
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *BaseInterface) GetTxPackets() uint64 {
|
|
||||||
i.Mutex.RLock()
|
|
||||||
defer i.Mutex.RUnlock()
|
|
||||||
return i.TxPackets
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *BaseInterface) GetRxPackets() uint64 {
|
|
||||||
i.Mutex.RLock()
|
|
||||||
defer i.Mutex.RUnlock()
|
|
||||||
return i.RxPackets
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *BaseInterface) Start() error {
|
func (i *BaseInterface) Start() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -304,6 +272,7 @@ func (i *BaseInterface) updateBandwidthStats(bytes uint64) {
|
|||||||
i.Mutex.Lock()
|
i.Mutex.Lock()
|
||||||
defer i.Mutex.Unlock()
|
defer i.Mutex.Unlock()
|
||||||
|
|
||||||
|
i.TxBytes += bytes
|
||||||
i.lastTx = time.Now()
|
i.lastTx = time.Now()
|
||||||
|
|
||||||
debug.Log(debug.DEBUG_VERBOSE, "Interface updated bandwidth stats", "name", i.Name, "tx_bytes", i.TxBytes, "last_tx", i.lastTx)
|
debug.Log(debug.DEBUG_VERBOSE, "Interface updated bandwidth stats", "name", i.Name, "tx_bytes", i.TxBytes, "last_tx", i.lastTx)
|
||||||
|
|||||||
24
pkg/interfaces/kiss.go
Normal file
24
pkg/interfaces/kiss.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
|
package interfaces
|
||||||
|
|
||||||
|
const (
|
||||||
|
KISS_FEND = 0xC0
|
||||||
|
KISS_FESC = 0xDB
|
||||||
|
KISS_TFEND = 0xDC
|
||||||
|
KISS_TFESC = 0xDD
|
||||||
|
)
|
||||||
|
|
||||||
|
func escapeKISS(data []byte) []byte {
|
||||||
|
escaped := make([]byte, 0, len(data)*2)
|
||||||
|
for _, b := range data {
|
||||||
|
if b == KISS_FEND {
|
||||||
|
escaped = append(escaped, KISS_FESC, KISS_TFEND)
|
||||||
|
} else if b == KISS_FESC {
|
||||||
|
escaped = append(escaped, KISS_FESC, KISS_TFESC)
|
||||||
|
} else {
|
||||||
|
escaped = append(escaped, b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return escaped
|
||||||
|
}
|
||||||
29
pkg/interfaces/lora_stub.go
Normal file
29
pkg/interfaces/lora_stub.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
|
//go:build !tinygo
|
||||||
|
|
||||||
|
package interfaces
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LoRaInterface struct {
|
||||||
|
BaseInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLoRaInterface(name string, spi interface{}, cs, reset, dio0 interface{}, freq uint32, bw uint32, sf uint8, cr uint8, enabled bool) (*LoRaInterface, error) {
|
||||||
|
return nil, fmt.Errorf("LoRaInterface is only supported on TinyGo targets currently")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (li *LoRaInterface) Start() error {
|
||||||
|
return fmt.Errorf("LoRaInterface is only supported on TinyGo targets currently")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (li *LoRaInterface) Stop() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (li *LoRaInterface) Send(data []byte, address string) error {
|
||||||
|
return fmt.Errorf("LoRaInterface is only supported on TinyGo targets currently")
|
||||||
|
}
|
||||||
292
pkg/interfaces/lora_tinygo.go
Normal file
292
pkg/interfaces/lora_tinygo.go
Normal file
@@ -0,0 +1,292 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
|
//go:build tinygo
|
||||||
|
|
||||||
|
package interfaces
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"machine"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
|
||||||
|
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
REG_FIFO = 0x00
|
||||||
|
REG_OP_MODE = 0x01
|
||||||
|
REG_FRF_MSB = 0x06
|
||||||
|
REG_FRF_MID = 0x07
|
||||||
|
REG_FRF_LSB = 0x08
|
||||||
|
REG_PA_CONFIG = 0x09
|
||||||
|
REG_FIFO_ADDR_PTR = 0x0D
|
||||||
|
REG_FIFO_TX_BASE_ADDR = 0x0E
|
||||||
|
REG_FIFO_RX_BASE_ADDR = 0x0F
|
||||||
|
REG_FIFO_RX_CURRENT_ADDR = 0x10
|
||||||
|
REG_IRQ_FLAGS = 0x12
|
||||||
|
REG_RX_NB_BYTES = 0x13
|
||||||
|
REG_MODEM_CONFIG_1 = 0x1D
|
||||||
|
REG_MODEM_CONFIG_2 = 0x1E
|
||||||
|
REG_PREAMBLE_MSB = 0x20
|
||||||
|
REG_PREAMBLE_LSB = 0x21
|
||||||
|
REG_PAYLOAD_LENGTH = 0x22
|
||||||
|
REG_MODEM_CONFIG_3 = 0x26
|
||||||
|
REG_RSSI_WIDEBAND = 0x2C
|
||||||
|
REG_DETECTION_OPTIMIZE = 0x31
|
||||||
|
REG_DETECTION_THRESHOLD = 0x37
|
||||||
|
REG_SYNC_WORD = 0x39
|
||||||
|
REG_DIO_MAPPING_1 = 0x40
|
||||||
|
REG_VERSION = 0x42
|
||||||
|
|
||||||
|
MODE_LONG_RANGE_MODE = 0x80
|
||||||
|
MODE_SLEEP = 0x00
|
||||||
|
MODE_STDBY = 0x01
|
||||||
|
MODE_TX = 0x03
|
||||||
|
MODE_RX_CONTINUOUS = 0x05
|
||||||
|
|
||||||
|
IRQ_RX_DONE_MASK = 0x40
|
||||||
|
IRQ_PAYLOAD_CRC_ERROR_MASK = 0x20
|
||||||
|
IRQ_TX_DONE_MASK = 0x08
|
||||||
|
|
||||||
|
MAX_PKT_LENGTH = 255
|
||||||
|
)
|
||||||
|
|
||||||
|
// LoRaInterface provides a TinyGo SPI-based LoRa interface for SX127x.
|
||||||
|
type LoRaInterface struct {
|
||||||
|
BaseInterface
|
||||||
|
spi machine.SPI
|
||||||
|
cs machine.Pin
|
||||||
|
reset machine.Pin
|
||||||
|
dio0 machine.Pin
|
||||||
|
freq uint32
|
||||||
|
bw uint32
|
||||||
|
sf uint8
|
||||||
|
cr uint8
|
||||||
|
txPower uint8
|
||||||
|
done chan struct{}
|
||||||
|
stopOnce sync.Once
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLoRaInterface initializes a new LoRaInterface.
|
||||||
|
func NewLoRaInterface(name string, spi machine.SPI, cs, reset, dio0 machine.Pin, freq uint32, bw uint32, sf uint8, cr uint8, enabled bool) (*LoRaInterface, error) {
|
||||||
|
li := &LoRaInterface{
|
||||||
|
BaseInterface: NewBaseInterface(name, common.IF_TYPE_SERIAL, enabled),
|
||||||
|
spi: spi,
|
||||||
|
cs: cs,
|
||||||
|
reset: reset,
|
||||||
|
dio0: dio0,
|
||||||
|
freq: freq,
|
||||||
|
bw: bw,
|
||||||
|
sf: sf,
|
||||||
|
cr: cr,
|
||||||
|
txPower: 17,
|
||||||
|
done: make(chan struct{}),
|
||||||
|
}
|
||||||
|
|
||||||
|
li.MTU = MAX_PKT_LENGTH
|
||||||
|
li.Bitrate = int64(bw * uint32(sf) / (1 << (sf - 1)))
|
||||||
|
|
||||||
|
if enabled {
|
||||||
|
err := li.Start()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return li, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start configures and brings the LoRaInterface online.
|
||||||
|
func (li *LoRaInterface) Start() error {
|
||||||
|
li.Mutex.Lock()
|
||||||
|
defer li.Mutex.Unlock()
|
||||||
|
|
||||||
|
if li.Online {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
li.cs.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||||
|
li.cs.High()
|
||||||
|
li.reset.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||||
|
li.dio0.Configure(machine.PinConfig{Mode: machine.PinInput})
|
||||||
|
|
||||||
|
li.reset.Low()
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
li.reset.High()
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
|
||||||
|
version := li.readReg(REG_VERSION)
|
||||||
|
if version != 0x12 {
|
||||||
|
return fmt.Errorf("LoRa chip not found, version: 0x%02x", version)
|
||||||
|
}
|
||||||
|
|
||||||
|
li.writeReg(REG_OP_MODE, MODE_LONG_RANGE_MODE|MODE_SLEEP)
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
|
||||||
|
frf := uint64(li.freq) << 19 / 32000000
|
||||||
|
li.writeReg(REG_FRF_MSB, uint8(frf>>16))
|
||||||
|
li.writeReg(REG_FRF_MID, uint8(frf>>8))
|
||||||
|
li.writeReg(REG_FRF_LSB, uint8(frf))
|
||||||
|
|
||||||
|
li.writeReg(REG_FIFO_TX_BASE_ADDR, 0)
|
||||||
|
li.writeReg(REG_FIFO_RX_BASE_ADDR, 0)
|
||||||
|
li.writeReg(0x0C, 0x23)
|
||||||
|
li.writeReg(REG_MODEM_CONFIG_3, 0x04)
|
||||||
|
li.writeReg(REG_PA_CONFIG, 0x80|(li.txPower-2))
|
||||||
|
|
||||||
|
var bwVal uint8
|
||||||
|
switch li.bw {
|
||||||
|
case 125000:
|
||||||
|
bwVal = 7
|
||||||
|
case 250000:
|
||||||
|
bwVal = 8
|
||||||
|
case 500000:
|
||||||
|
bwVal = 9
|
||||||
|
default:
|
||||||
|
bwVal = 7
|
||||||
|
}
|
||||||
|
li.writeReg(REG_MODEM_CONFIG_1, (bwVal<<4)|(li.cr-4)<<1|0x00)
|
||||||
|
li.writeReg(REG_MODEM_CONFIG_2, (li.sf<<4)|0x04)
|
||||||
|
|
||||||
|
li.writeReg(REG_SYNC_WORD, 0x12)
|
||||||
|
li.writeReg(REG_OP_MODE, MODE_LONG_RANGE_MODE|MODE_STDBY)
|
||||||
|
li.writeReg(REG_OP_MODE, MODE_LONG_RANGE_MODE|MODE_RX_CONTINUOUS)
|
||||||
|
|
||||||
|
li.Online = true
|
||||||
|
li.Enabled = true
|
||||||
|
|
||||||
|
go li.readLoop()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// readReg reads a byte from the given register.
|
||||||
|
func (li *LoRaInterface) readReg(reg uint8) uint8 {
|
||||||
|
li.cs.Low()
|
||||||
|
li.spi.Transfer(reg & 0x7F)
|
||||||
|
val, _ := li.spi.Transfer(0)
|
||||||
|
li.cs.High()
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeReg writes a byte to the given register.
|
||||||
|
func (li *LoRaInterface) writeReg(reg uint8, val uint8) {
|
||||||
|
li.cs.Low()
|
||||||
|
li.spi.Transfer(reg | 0x80)
|
||||||
|
li.spi.Transfer(val)
|
||||||
|
li.cs.High()
|
||||||
|
}
|
||||||
|
|
||||||
|
// readLoop polls the radio for received packets and dispatches them.
|
||||||
|
func (li *LoRaInterface) readLoop() {
|
||||||
|
for {
|
||||||
|
li.Mutex.RLock()
|
||||||
|
online := li.Online
|
||||||
|
done := li.done
|
||||||
|
li.Mutex.RUnlock()
|
||||||
|
|
||||||
|
if !online {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
irq := li.readReg(REG_IRQ_FLAGS)
|
||||||
|
if irq&IRQ_RX_DONE_MASK != 0 {
|
||||||
|
li.writeReg(REG_IRQ_FLAGS, IRQ_RX_DONE_MASK)
|
||||||
|
|
||||||
|
if irq&IRQ_PAYLOAD_CRC_ERROR_MASK == 0 {
|
||||||
|
currentAddr := li.readReg(REG_FIFO_RX_CURRENT_ADDR)
|
||||||
|
li.writeReg(REG_FIFO_ADDR_PTR, currentAddr)
|
||||||
|
count := li.readReg(REG_RX_NB_BYTES)
|
||||||
|
|
||||||
|
packet := make([]byte, count)
|
||||||
|
li.cs.Low()
|
||||||
|
li.spi.Transfer(REG_FIFO)
|
||||||
|
for i := uint8(0); i < count; i++ {
|
||||||
|
packet[i], _ = li.spi.Transfer(0)
|
||||||
|
}
|
||||||
|
li.cs.High()
|
||||||
|
|
||||||
|
li.ProcessIncoming(packet)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send transmits a packet over LoRa.
|
||||||
|
func (li *LoRaInterface) Send(data []byte, address string) error {
|
||||||
|
return li.ProcessOutgoing(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProcessOutgoing encodes and sends a packet.
|
||||||
|
func (li *LoRaInterface) ProcessOutgoing(data []byte) error {
|
||||||
|
li.Mutex.Lock()
|
||||||
|
defer li.Mutex.Unlock()
|
||||||
|
|
||||||
|
if !li.Online {
|
||||||
|
return fmt.Errorf("interface offline")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(data) > MAX_PKT_LENGTH {
|
||||||
|
return fmt.Errorf("packet too long for LoRa: %d", len(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
li.writeReg(REG_OP_MODE, MODE_LONG_RANGE_MODE|MODE_STDBY)
|
||||||
|
li.writeReg(REG_FIFO_ADDR_PTR, 0)
|
||||||
|
|
||||||
|
li.cs.Low()
|
||||||
|
li.spi.Transfer(REG_FIFO | 0x80)
|
||||||
|
for _, b := range data {
|
||||||
|
li.spi.Transfer(b)
|
||||||
|
}
|
||||||
|
li.cs.High()
|
||||||
|
|
||||||
|
li.writeReg(REG_PAYLOAD_LENGTH, uint8(len(data)))
|
||||||
|
li.writeReg(REG_OP_MODE, MODE_LONG_RANGE_MODE|MODE_TX)
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
for {
|
||||||
|
if li.readReg(REG_IRQ_FLAGS)&IRQ_TX_DONE_MASK != 0 {
|
||||||
|
li.writeReg(REG_IRQ_FLAGS, IRQ_TX_DONE_MASK)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if time.Since(start) > 2*time.Second {
|
||||||
|
debug.Log(debug.DEBUG_ERROR, "LoRa TX timeout")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
time.Sleep(1 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
li.writeReg(REG_OP_MODE, MODE_LONG_RANGE_MODE|MODE_RX_CONTINUOUS)
|
||||||
|
|
||||||
|
li.TxBytes += uint64(len(data))
|
||||||
|
li.lastTx = time.Now()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop disables the LoRaInterface.
|
||||||
|
func (li *LoRaInterface) Stop() error {
|
||||||
|
li.Mutex.Lock()
|
||||||
|
li.Online = false
|
||||||
|
li.Enabled = false
|
||||||
|
li.writeReg(REG_OP_MODE, MODE_LONG_RANGE_MODE|MODE_SLEEP)
|
||||||
|
li.Mutex.Unlock()
|
||||||
|
|
||||||
|
li.stopOnce.Do(func() {
|
||||||
|
if li.done != nil {
|
||||||
|
close(li.done)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
262
pkg/interfaces/rnode.go
Normal file
262
pkg/interfaces/rnode.go
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
|
||||||
|
package interfaces
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
|
||||||
|
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
RNODE_CMD_DATA = 0x00
|
||||||
|
RNODE_CMD_FREQUENCY = 0x01
|
||||||
|
RNODE_CMD_BANDWIDTH = 0x02
|
||||||
|
RNODE_CMD_TXPOWER = 0x03
|
||||||
|
RNODE_CMD_SF = 0x04
|
||||||
|
RNODE_CMD_CR = 0x05
|
||||||
|
RNODE_CMD_RADIO_STATE = 0x06
|
||||||
|
RNODE_CMD_RADIO_LOCK = 0x07
|
||||||
|
RNODE_CMD_DETECT = 0x08
|
||||||
|
RNODE_CMD_LEAVE = 0x0A
|
||||||
|
RNODE_CMD_ST_ALOCK = 0x0B
|
||||||
|
RNODE_CMD_LT_ALOCK = 0x0C
|
||||||
|
RNODE_CMD_READY = 0x0F
|
||||||
|
RNODE_CMD_STAT_RX = 0x21
|
||||||
|
RNODE_CMD_STAT_TX = 0x22
|
||||||
|
RNODE_CMD_STAT_RSSI = 0x23
|
||||||
|
RNODE_CMD_STAT_SNR = 0x24
|
||||||
|
RNODE_CMD_FW_VERSION = 0x50
|
||||||
|
RNODE_CMD_PLATFORM = 0x48
|
||||||
|
RNODE_CMD_MCU = 0x49
|
||||||
|
|
||||||
|
RNODE_DETECT_REQ = 0x73
|
||||||
|
RNODE_DETECT_RESP = 0x46
|
||||||
|
|
||||||
|
RNODE_RSSI_OFFSET = 157
|
||||||
|
)
|
||||||
|
|
||||||
|
// RNodeInterface represents a Reticulum node interface.
|
||||||
|
type RNodeInterface struct {
|
||||||
|
Interface
|
||||||
|
frequency uint32
|
||||||
|
bandwidth uint32
|
||||||
|
sf uint8
|
||||||
|
cr uint8
|
||||||
|
txPower uint8
|
||||||
|
callback common.PacketCallback
|
||||||
|
|
||||||
|
rFrequency uint32
|
||||||
|
rBandwidth uint32
|
||||||
|
rTXPower uint8
|
||||||
|
rSF uint8
|
||||||
|
rCR uint8
|
||||||
|
rState uint8
|
||||||
|
rDetected bool
|
||||||
|
rMajVer uint8
|
||||||
|
rMinVer uint8
|
||||||
|
|
||||||
|
interfaceReady bool
|
||||||
|
packetQueue [][]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRNodeInterface creates a new RNodeInterface.
|
||||||
|
func NewRNodeInterface(name string, underlying Interface, freq uint32, bw uint32, sf uint8, cr uint8, txPower uint8) (*RNodeInterface, error) {
|
||||||
|
ri := &RNodeInterface{
|
||||||
|
Interface: underlying,
|
||||||
|
frequency: freq,
|
||||||
|
bandwidth: bw,
|
||||||
|
sf: sf,
|
||||||
|
cr: cr,
|
||||||
|
txPower: txPower,
|
||||||
|
}
|
||||||
|
|
||||||
|
underlying.SetPacketCallback(ri.handleIncoming)
|
||||||
|
return ri, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetPacketCallback sets the packet callback for the RNodeInterface.
|
||||||
|
func (ri *RNodeInterface) SetPacketCallback(cb common.PacketCallback) {
|
||||||
|
ri.callback = cb
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ri *RNodeInterface) handleIncoming(data []byte, ni common.NetworkInterface) {
|
||||||
|
if len(data) < 1 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := data[0]
|
||||||
|
payload := data[1:]
|
||||||
|
|
||||||
|
switch cmd {
|
||||||
|
case RNODE_CMD_DATA:
|
||||||
|
if ri.callback != nil {
|
||||||
|
ri.callback(payload, ri)
|
||||||
|
}
|
||||||
|
case RNODE_CMD_READY:
|
||||||
|
ri.processQueue()
|
||||||
|
case RNODE_CMD_DETECT:
|
||||||
|
if len(payload) >= 1 && payload[0] == RNODE_DETECT_RESP {
|
||||||
|
ri.rDetected = true
|
||||||
|
}
|
||||||
|
case RNODE_CMD_FW_VERSION:
|
||||||
|
if len(payload) >= 2 {
|
||||||
|
ri.rMajVer = payload[0]
|
||||||
|
ri.rMinVer = payload[1]
|
||||||
|
debug.Log(debug.DEBUG_INFO, "RNode firmware version", "name", ri.GetName(), "version", fmt.Sprintf("%d.%d", ri.rMajVer, ri.rMinVer))
|
||||||
|
}
|
||||||
|
case RNODE_CMD_FREQUENCY:
|
||||||
|
if len(payload) >= 4 {
|
||||||
|
ri.rFrequency = binary.BigEndian.Uint32(payload)
|
||||||
|
}
|
||||||
|
case RNODE_CMD_BANDWIDTH:
|
||||||
|
if len(payload) >= 4 {
|
||||||
|
ri.rBandwidth = binary.BigEndian.Uint32(payload)
|
||||||
|
}
|
||||||
|
case RNODE_CMD_TXPOWER:
|
||||||
|
if len(payload) >= 1 {
|
||||||
|
ri.rTXPower = payload[0]
|
||||||
|
}
|
||||||
|
case RNODE_CMD_SF:
|
||||||
|
if len(payload) >= 1 {
|
||||||
|
ri.rSF = payload[0]
|
||||||
|
}
|
||||||
|
case RNODE_CMD_CR:
|
||||||
|
if len(payload) >= 1 {
|
||||||
|
ri.rCR = payload[0]
|
||||||
|
}
|
||||||
|
case RNODE_CMD_RADIO_STATE:
|
||||||
|
if len(payload) >= 1 {
|
||||||
|
ri.rState = payload[0]
|
||||||
|
}
|
||||||
|
case RNODE_CMD_STAT_RSSI:
|
||||||
|
if len(payload) >= 1 {
|
||||||
|
rssi := int(payload[0]) - RNODE_RSSI_OFFSET
|
||||||
|
debug.Log(debug.DEBUG_VERBOSE, "RNode RSSI", "name", ri.GetName(), "rssi", rssi)
|
||||||
|
}
|
||||||
|
case RNODE_CMD_STAT_SNR:
|
||||||
|
if len(payload) >= 1 {
|
||||||
|
snr := float32(int8(payload[0])) * 0.25
|
||||||
|
debug.Log(debug.DEBUG_VERBOSE, "RNode SNR", "name", ri.GetName(), "snr", snr)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
debug.Log(debug.DEBUG_ALL, "RNode received command", "cmd", fmt.Sprintf("0x%02x", cmd), "len", len(payload))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ri *RNodeInterface) processQueue() {
|
||||||
|
ri.interfaceReady = true
|
||||||
|
if len(ri.packetQueue) > 0 {
|
||||||
|
packet := ri.packetQueue[0]
|
||||||
|
ri.packetQueue = ri.packetQueue[1:]
|
||||||
|
_ = ri.Send(packet, "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start initializes the RNodeInterface and configures device parameters.
|
||||||
|
func (ri *RNodeInterface) Start() error {
|
||||||
|
err := ri.Interface.Start()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
|
||||||
|
if err := ri.detect(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
debug.Log(debug.DEBUG_INFO, "Initializing RNode...", "name", ri.GetName())
|
||||||
|
|
||||||
|
if ri.frequency != 0 {
|
||||||
|
freqBytes := make([]byte, 4)
|
||||||
|
binary.BigEndian.PutUint32(freqBytes, ri.frequency)
|
||||||
|
if err := ri.sendRNodeCommand(RNODE_CMD_FREQUENCY, freqBytes); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ri.bandwidth != 0 {
|
||||||
|
bwBytes := make([]byte, 4)
|
||||||
|
binary.BigEndian.PutUint32(bwBytes, ri.bandwidth)
|
||||||
|
if err := ri.sendRNodeCommand(RNODE_CMD_BANDWIDTH, bwBytes); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ri.sf != 0 {
|
||||||
|
if err := ri.sendRNodeCommand(RNODE_CMD_SF, []byte{ri.sf}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ri.cr != 0 {
|
||||||
|
if err := ri.sendRNodeCommand(RNODE_CMD_CR, []byte{ri.cr}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ri.txPower != 0 {
|
||||||
|
if err := ri.sendRNodeCommand(RNODE_CMD_TXPOWER, []byte{ri.txPower}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ri.sendRNodeCommand(RNODE_CMD_RADIO_STATE, []byte{0x01}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ri.interfaceReady = true
|
||||||
|
|
||||||
|
debug.Log(debug.DEBUG_INFO, "RNode initialized", "name", ri.GetName())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// detect attempts to detect the RNode device and obtain firmware version.
|
||||||
|
func (ri *RNodeInterface) detect() error {
|
||||||
|
detectCmd := []byte{RNODE_DETECT_REQ}
|
||||||
|
if err := ri.sendRNodeCommand(RNODE_CMD_DETECT, detectCmd); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
for !ri.rDetected {
|
||||||
|
if time.Since(start) > 2*time.Second {
|
||||||
|
debug.Log(debug.DEBUG_ERROR, "RNode detection timed out", "name", ri.GetName())
|
||||||
|
break
|
||||||
|
}
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ri.sendRNodeCommand(RNODE_CMD_FW_VERSION, []byte{0x00}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendRNodeCommand sends a command to the RNode device.
|
||||||
|
func (ri *RNodeInterface) sendRNodeCommand(cmd byte, data []byte) error {
|
||||||
|
if kissInterface, ok := ri.Interface.(interface {
|
||||||
|
SendKISS(byte, []byte) error
|
||||||
|
}); ok {
|
||||||
|
return kissInterface.SendKISS(cmd, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
frame := make([]byte, 0, len(data)+1)
|
||||||
|
frame = append(frame, cmd)
|
||||||
|
frame = append(frame, data...)
|
||||||
|
return ri.Interface.Send(frame, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send transmits data using the underlying interface.
|
||||||
|
func (ri *RNodeInterface) Send(data []byte, addr string) error {
|
||||||
|
if !ri.interfaceReady {
|
||||||
|
ri.packetQueue = append(ri.packetQueue, data)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return ri.Interface.Send(data, addr)
|
||||||
|
}
|
||||||
29
pkg/interfaces/serial_stub.go
Normal file
29
pkg/interfaces/serial_stub.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
|
//go:build !tinygo
|
||||||
|
|
||||||
|
package interfaces
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SerialInterface struct {
|
||||||
|
BaseInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSerialInterface(name string, portName string, baud uint32, enabled bool) (*SerialInterface, error) {
|
||||||
|
return nil, fmt.Errorf("SerialInterface is only supported on TinyGo targets currently")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (si *SerialInterface) Start() error {
|
||||||
|
return fmt.Errorf("SerialInterface is only supported on TinyGo targets currently")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (si *SerialInterface) Stop() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (si *SerialInterface) Send(data []byte, address string) error {
|
||||||
|
return fmt.Errorf("SerialInterface is only supported on TinyGo targets currently")
|
||||||
|
}
|
||||||
221
pkg/interfaces/serial_tinygo.go
Normal file
221
pkg/interfaces/serial_tinygo.go
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
|
//go:build tinygo
|
||||||
|
|
||||||
|
package interfaces
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"machine"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
|
||||||
|
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
SERIAL_DEFAULT_BAUD = 115200
|
||||||
|
SERIAL_MTU = 1500
|
||||||
|
)
|
||||||
|
|
||||||
|
// SerialInterface implements a serial interface using TinyGo UART.
|
||||||
|
type SerialInterface struct {
|
||||||
|
BaseInterface
|
||||||
|
uart *machine.UART
|
||||||
|
baud uint32
|
||||||
|
done chan struct{}
|
||||||
|
stopOnce sync.Once
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSerialInterface creates and initializes a new SerialInterface.
|
||||||
|
func NewSerialInterface(name string, portName string, baud uint32, enabled bool) (*SerialInterface, error) {
|
||||||
|
if baud == 0 {
|
||||||
|
baud = SERIAL_DEFAULT_BAUD
|
||||||
|
}
|
||||||
|
|
||||||
|
uart, err := getUART(portName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
si := &SerialInterface{
|
||||||
|
BaseInterface: NewBaseInterface(name, common.IF_TYPE_SERIAL, enabled),
|
||||||
|
uart: uart,
|
||||||
|
baud: baud,
|
||||||
|
done: make(chan struct{}),
|
||||||
|
}
|
||||||
|
|
||||||
|
si.MTU = SERIAL_MTU
|
||||||
|
si.Bitrate = int64(baud)
|
||||||
|
|
||||||
|
if enabled {
|
||||||
|
err := si.Start()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return si, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getUART returns a TinyGo UART handle by name or index.
|
||||||
|
func getUART(name string) (*machine.UART, error) {
|
||||||
|
switch name {
|
||||||
|
case "UART0", "0":
|
||||||
|
return machine.UART0, nil
|
||||||
|
case "UART1", "1":
|
||||||
|
return machine.UART1, nil
|
||||||
|
case "UART2", "2":
|
||||||
|
return machine.UART2, nil
|
||||||
|
default:
|
||||||
|
if name == "" {
|
||||||
|
return machine.UART0, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("unknown UART: %s", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start enables the serial interface and starts the read loop.
|
||||||
|
func (si *SerialInterface) Start() error {
|
||||||
|
si.Mutex.Lock()
|
||||||
|
defer si.Mutex.Unlock()
|
||||||
|
|
||||||
|
if si.Online {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err := si.uart.Configure(machine.UARTConfig{
|
||||||
|
BaudRate: si.baud,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to configure UART: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
si.Online = true
|
||||||
|
si.Enabled = true
|
||||||
|
|
||||||
|
go si.readLoop()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop disables the serial interface.
|
||||||
|
func (si *SerialInterface) Stop() error {
|
||||||
|
si.Mutex.Lock()
|
||||||
|
si.Online = false
|
||||||
|
si.Enabled = false
|
||||||
|
si.Mutex.Unlock()
|
||||||
|
|
||||||
|
si.stopOnce.Do(func() {
|
||||||
|
if si.done != nil {
|
||||||
|
close(si.done)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// readLoop reads and processes frames from the UART, handling KISS framing.
|
||||||
|
func (si *SerialInterface) readLoop() {
|
||||||
|
buffer := make([]byte, si.MTU)
|
||||||
|
dataBuffer := make([]byte, 0, si.MTU)
|
||||||
|
inFrame := false
|
||||||
|
escape := false
|
||||||
|
|
||||||
|
for {
|
||||||
|
si.Mutex.RLock()
|
||||||
|
online := si.Online
|
||||||
|
done := si.done
|
||||||
|
si.Mutex.RUnlock()
|
||||||
|
|
||||||
|
if !online {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
if si.uart.Buffered() > 0 {
|
||||||
|
n, err := si.uart.Read(buffer)
|
||||||
|
if err != nil {
|
||||||
|
debug.Log(debug.DEBUG_ERROR, "Serial read error", "name", si.Name, "error", err)
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if n > 0 {
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
b := buffer[i]
|
||||||
|
|
||||||
|
if b == KISS_FEND {
|
||||||
|
if inFrame && len(dataBuffer) > 0 {
|
||||||
|
packet := make([]byte, len(dataBuffer))
|
||||||
|
copy(packet, dataBuffer)
|
||||||
|
si.ProcessIncoming(packet)
|
||||||
|
dataBuffer = dataBuffer[:0]
|
||||||
|
}
|
||||||
|
inFrame = true
|
||||||
|
escape = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if inFrame {
|
||||||
|
if b == KISS_FESC {
|
||||||
|
escape = true
|
||||||
|
} else {
|
||||||
|
if escape {
|
||||||
|
if b == KISS_TFEND {
|
||||||
|
b = KISS_FEND
|
||||||
|
} else if b == KISS_TFESC {
|
||||||
|
b = KISS_FESC
|
||||||
|
}
|
||||||
|
escape = false
|
||||||
|
}
|
||||||
|
dataBuffer = append(dataBuffer, b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send transmits data using KISS protocol with the default command 0x00.
|
||||||
|
func (si *SerialInterface) Send(data []byte, address string) error {
|
||||||
|
return si.SendKISS(0x00, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendKISS sends a KISS-encoded packet over the serial UART.
|
||||||
|
func (si *SerialInterface) SendKISS(command byte, data []byte) error {
|
||||||
|
si.Mutex.RLock()
|
||||||
|
online := si.Online
|
||||||
|
si.Mutex.RUnlock()
|
||||||
|
|
||||||
|
if !online {
|
||||||
|
return fmt.Errorf("interface offline")
|
||||||
|
}
|
||||||
|
|
||||||
|
frame := make([]byte, 0, len(data)*2+3)
|
||||||
|
frame = append(frame, KISS_FEND)
|
||||||
|
frame = append(frame, command)
|
||||||
|
frame = append(frame, escapeKISS(data)...)
|
||||||
|
frame = append(frame, KISS_FEND)
|
||||||
|
|
||||||
|
_, err := si.uart.Write(frame)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
si.Mutex.Lock()
|
||||||
|
si.TxBytes += uint64(len(frame))
|
||||||
|
si.lastTx = time.Now()
|
||||||
|
si.Mutex.Unlock()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -18,11 +18,6 @@ const (
|
|||||||
HDLC_ESC = 0x7D
|
HDLC_ESC = 0x7D
|
||||||
HDLC_ESC_MASK = 0x20
|
HDLC_ESC_MASK = 0x20
|
||||||
|
|
||||||
KISS_FEND = 0xC0
|
|
||||||
KISS_FESC = 0xDB
|
|
||||||
KISS_TFEND = 0xDC
|
|
||||||
KISS_TFESC = 0xDD
|
|
||||||
|
|
||||||
DEFAULT_MTU = 1064
|
DEFAULT_MTU = 1064
|
||||||
BITRATE_GUESS_VAL = 10 * 1000 * 1000
|
BITRATE_GUESS_VAL = 10 * 1000 * 1000
|
||||||
RECONNECT_WAIT = 5
|
RECONNECT_WAIT = 5
|
||||||
@@ -167,40 +162,6 @@ func (tc *TCPClientInterface) Stop() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tc *TCPClientInterface) ProcessOutgoing(data []byte) error {
|
|
||||||
tc.Mutex.RLock()
|
|
||||||
online := tc.Online
|
|
||||||
tc.Mutex.RUnlock()
|
|
||||||
|
|
||||||
if !online {
|
|
||||||
return fmt.Errorf("interface offline")
|
|
||||||
}
|
|
||||||
|
|
||||||
tc.writing = true
|
|
||||||
defer func() { tc.writing = false }()
|
|
||||||
|
|
||||||
// For TCP connections, use HDLC framing
|
|
||||||
var frame []byte
|
|
||||||
frame = append([]byte{HDLC_FLAG}, escapeHDLC(data)...)
|
|
||||||
frame = append(frame, HDLC_FLAG)
|
|
||||||
|
|
||||||
debug.Log(debug.DEBUG_ALL, "TCP interface writing to network", "name", tc.Name, "bytes", len(frame))
|
|
||||||
|
|
||||||
tc.Mutex.RLock()
|
|
||||||
conn := tc.conn
|
|
||||||
tc.Mutex.RUnlock()
|
|
||||||
|
|
||||||
if conn == nil {
|
|
||||||
return fmt.Errorf("connection closed")
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := conn.Write(frame)
|
|
||||||
if err != nil {
|
|
||||||
debug.Log(debug.DEBUG_CRITICAL, "TCP interface write failed", "name", tc.Name, "error", err)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tc *TCPClientInterface) readLoop() {
|
func (tc *TCPClientInterface) readLoop() {
|
||||||
buffer := make([]byte, tc.MTU)
|
buffer := make([]byte, tc.MTU)
|
||||||
inFrame := false
|
inFrame := false
|
||||||
@@ -239,6 +200,8 @@ func (tc *TCPClientInterface) readLoop() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tc.UpdateStats(uint64(n), true) // #nosec G115
|
||||||
|
|
||||||
for i := 0; i < n; i++ {
|
for i := 0; i < n; i++ {
|
||||||
b := buffer[i]
|
b := buffer[i]
|
||||||
|
|
||||||
@@ -273,13 +236,70 @@ func (tc *TCPClientInterface) handlePacket(data []byte) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tc.Mutex.Lock()
|
tc.Mutex.Lock()
|
||||||
|
tc.RxBytes += uint64(len(data))
|
||||||
lastRx := time.Now()
|
lastRx := time.Now()
|
||||||
tc.lastRx = lastRx
|
tc.lastRx = lastRx
|
||||||
|
callback := tc.packetCallback
|
||||||
tc.Mutex.Unlock()
|
tc.Mutex.Unlock()
|
||||||
|
|
||||||
debug.Log(debug.DEBUG_ALL, "Received packet", "type", fmt.Sprintf("0x%02x", data[0]), "size", len(data))
|
debug.Log(debug.DEBUG_ALL, "Received packet", "type", fmt.Sprintf("0x%02x", data[0]), "size", len(data))
|
||||||
|
|
||||||
tc.ProcessIncoming(data)
|
// For RNS packets, call the packet callback directly
|
||||||
|
if callback != nil {
|
||||||
|
debug.Log(debug.DEBUG_ALL, "Calling packet callback for RNS packet")
|
||||||
|
callback(data, tc)
|
||||||
|
} else {
|
||||||
|
debug.Log(debug.DEBUG_ALL, "No packet callback set for TCP interface")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send implements the interface Send method for TCP interface
|
||||||
|
func (tc *TCPClientInterface) Send(data []byte, address string) error {
|
||||||
|
debug.Log(debug.DEBUG_ALL, "TCP interface sending bytes", "name", tc.Name, "bytes", len(data))
|
||||||
|
|
||||||
|
if !tc.IsEnabled() || !tc.IsOnline() {
|
||||||
|
return fmt.Errorf("TCP interface %s is not online", tc.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send data directly - packet type is already in the first byte of data
|
||||||
|
// TCP interface uses HDLC framing around the raw packet
|
||||||
|
return tc.ProcessOutgoing(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tc *TCPClientInterface) ProcessOutgoing(data []byte) error {
|
||||||
|
tc.Mutex.RLock()
|
||||||
|
online := tc.Online
|
||||||
|
tc.Mutex.RUnlock()
|
||||||
|
|
||||||
|
if !online {
|
||||||
|
return fmt.Errorf("interface offline")
|
||||||
|
}
|
||||||
|
|
||||||
|
tc.writing = true
|
||||||
|
defer func() { tc.writing = false }()
|
||||||
|
|
||||||
|
// For TCP connections, use HDLC framing
|
||||||
|
var frame []byte
|
||||||
|
frame = append([]byte{HDLC_FLAG}, escapeHDLC(data)...)
|
||||||
|
frame = append(frame, HDLC_FLAG)
|
||||||
|
|
||||||
|
tc.UpdateStats(uint64(len(frame)), false) // #nosec G115
|
||||||
|
|
||||||
|
debug.Log(debug.DEBUG_ALL, "TCP interface writing to network", "name", tc.Name, "bytes", len(frame))
|
||||||
|
|
||||||
|
tc.Mutex.RLock()
|
||||||
|
conn := tc.conn
|
||||||
|
tc.Mutex.RUnlock()
|
||||||
|
|
||||||
|
if conn == nil {
|
||||||
|
return fmt.Errorf("connection closed")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := conn.Write(frame)
|
||||||
|
if err != nil {
|
||||||
|
debug.Log(debug.DEBUG_CRITICAL, "TCP interface write failed", "name", tc.Name, "error", err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tc *TCPClientInterface) teardown() {
|
func (tc *TCPClientInterface) teardown() {
|
||||||
@@ -304,20 +324,6 @@ func escapeHDLC(data []byte) []byte {
|
|||||||
return escaped
|
return escaped
|
||||||
}
|
}
|
||||||
|
|
||||||
func escapeKISS(data []byte) []byte {
|
|
||||||
escaped := make([]byte, 0, len(data)*2)
|
|
||||||
for _, b := range data {
|
|
||||||
if b == KISS_FEND {
|
|
||||||
escaped = append(escaped, KISS_FESC, KISS_TFEND)
|
|
||||||
} else if b == KISS_FESC {
|
|
||||||
escaped = append(escaped, KISS_FESC, KISS_TFESC)
|
|
||||||
} else {
|
|
||||||
escaped = append(escaped, b)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return escaped
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tc *TCPClientInterface) SetPacketCallback(cb common.PacketCallback) {
|
func (tc *TCPClientInterface) SetPacketCallback(cb common.PacketCallback) {
|
||||||
tc.packetCallback = cb
|
tc.packetCallback = cb
|
||||||
}
|
}
|
||||||
@@ -447,6 +453,40 @@ func (tc *TCPClientInterface) GetRTT() time.Duration {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (tc *TCPClientInterface) GetTxBytes() uint64 {
|
||||||
|
tc.Mutex.RLock()
|
||||||
|
defer tc.Mutex.RUnlock()
|
||||||
|
return tc.TxBytes
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tc *TCPClientInterface) GetRxBytes() uint64 {
|
||||||
|
tc.Mutex.RLock()
|
||||||
|
defer tc.Mutex.RUnlock()
|
||||||
|
return tc.RxBytes
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tc *TCPClientInterface) UpdateStats(bytes uint64, isRx bool) {
|
||||||
|
tc.Mutex.Lock()
|
||||||
|
defer tc.Mutex.Unlock()
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
if isRx {
|
||||||
|
tc.RxBytes += bytes
|
||||||
|
tc.lastRx = now
|
||||||
|
debug.Log(debug.DEBUG_TRACE, "Interface RX stats", "name", tc.Name, "bytes", bytes, "total", tc.RxBytes, "last", tc.lastRx)
|
||||||
|
} else {
|
||||||
|
tc.TxBytes += bytes
|
||||||
|
tc.lastTx = now
|
||||||
|
debug.Log(debug.DEBUG_TRACE, "Interface TX stats", "name", tc.Name, "bytes", bytes, "total", tc.TxBytes, "last", tc.lastTx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tc *TCPClientInterface) GetStats() (tx uint64, rx uint64, lastTx time.Time, lastRx time.Time) {
|
||||||
|
tc.Mutex.RLock()
|
||||||
|
defer tc.Mutex.RUnlock()
|
||||||
|
return tc.TxBytes, tc.RxBytes, tc.lastTx, tc.lastRx
|
||||||
|
}
|
||||||
|
|
||||||
type TCPServerInterface struct {
|
type TCPServerInterface struct {
|
||||||
BaseInterface
|
BaseInterface
|
||||||
connections map[string]net.Conn
|
connections map[string]net.Conn
|
||||||
@@ -627,6 +667,18 @@ func (ts *TCPServerInterface) Stop() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ts *TCPServerInterface) GetTxBytes() uint64 {
|
||||||
|
ts.Mutex.RLock()
|
||||||
|
defer ts.Mutex.RUnlock()
|
||||||
|
return ts.TxBytes
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *TCPServerInterface) GetRxBytes() uint64 {
|
||||||
|
ts.Mutex.RLock()
|
||||||
|
defer ts.Mutex.RUnlock()
|
||||||
|
return ts.RxBytes
|
||||||
|
}
|
||||||
|
|
||||||
func (ts *TCPServerInterface) handleConnection(conn net.Conn) {
|
func (ts *TCPServerInterface) handleConnection(conn net.Conn) {
|
||||||
addr := conn.RemoteAddr().String()
|
addr := conn.RemoteAddr().String()
|
||||||
ts.Mutex.Lock()
|
ts.Mutex.Lock()
|
||||||
@@ -657,7 +709,14 @@ func (ts *TCPServerInterface) handleConnection(conn net.Conn) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ts.ProcessIncoming(buffer[:n])
|
ts.Mutex.Lock()
|
||||||
|
ts.RxBytes += uint64(n) // #nosec G115
|
||||||
|
callback := ts.packetCallback
|
||||||
|
ts.Mutex.Unlock()
|
||||||
|
|
||||||
|
if callback != nil {
|
||||||
|
callback(buffer[:n], ts)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -680,6 +739,7 @@ func (ts *TCPServerInterface) ProcessOutgoing(data []byte) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ts.Mutex.Lock()
|
ts.Mutex.Lock()
|
||||||
|
ts.TxBytes += uint64(len(frame)) // #nosec G115
|
||||||
conns := make([]net.Conn, 0, len(ts.connections))
|
conns := make([]net.Conn, 0, len(ts.connections))
|
||||||
for _, conn := range ts.connections {
|
for _, conn := range ts.connections {
|
||||||
conns = append(conns, conn)
|
conns = append(conns, conn)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// SPDX-License-Identifier: 0BSD
|
// SPDX-License-Identifier: 0BSD
|
||||||
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
//go:build !linux
|
//go:build !linux || tinygo
|
||||||
// +build !linux
|
// +build !linux tinygo
|
||||||
|
|
||||||
package interfaces
|
package interfaces
|
||||||
|
|
||||||
@@ -14,3 +14,11 @@ import (
|
|||||||
func platformGetRTT(fd uintptr) time.Duration {
|
func platformGetRTT(fd uintptr) time.Duration {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (tc *TCPClientInterface) setTimeoutsLinux() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tc *TCPClientInterface) setTimeoutsOSX() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// SPDX-License-Identifier: 0BSD
|
// SPDX-License-Identifier: 0BSD
|
||||||
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
//go:build linux
|
//go:build linux && !tinygo
|
||||||
// +build linux
|
// +build linux,!tinygo
|
||||||
|
|
||||||
package interfaces
|
package interfaces
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
// SPDX-License-Identifier: 0BSD
|
// SPDX-License-Identifier: 0BSD
|
||||||
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
|
//go:build !tinygo
|
||||||
|
// +build !tinygo
|
||||||
|
|
||||||
package interfaces
|
package interfaces
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -48,30 +51,6 @@ func NewUDPInterface(name string, addr string, target string, enabled bool) (*UD
|
|||||||
return ui, nil
|
return ui, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ui *UDPInterface) GetName() string {
|
|
||||||
return ui.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ui *UDPInterface) GetType() common.InterfaceType {
|
|
||||||
return ui.Type
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ui *UDPInterface) GetMode() common.InterfaceMode {
|
|
||||||
return ui.Mode
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ui *UDPInterface) IsOnline() bool {
|
|
||||||
ui.Mutex.RLock()
|
|
||||||
defer ui.Mutex.RUnlock()
|
|
||||||
return ui.Online
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ui *UDPInterface) IsDetached() bool {
|
|
||||||
ui.Mutex.RLock()
|
|
||||||
defer ui.Mutex.RUnlock()
|
|
||||||
return ui.Detached
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ui *UDPInterface) Detach() {
|
func (ui *UDPInterface) Detach() {
|
||||||
ui.Mutex.Lock()
|
ui.Mutex.Lock()
|
||||||
defer ui.Mutex.Unlock()
|
defer ui.Mutex.Unlock()
|
||||||
@@ -87,22 +66,28 @@ func (ui *UDPInterface) Detach() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ui *UDPInterface) SetPacketCallback(callback common.PacketCallback) {
|
func (ui *UDPInterface) Send(data []byte, addr string) error {
|
||||||
ui.Mutex.Lock()
|
debug.Log(debug.DEBUG_ALL, "UDP interface sending bytes", "name", ui.Name, "bytes", len(data))
|
||||||
defer ui.Mutex.Unlock()
|
|
||||||
ui.packetCallback = callback
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ui *UDPInterface) GetPacketCallback() common.PacketCallback {
|
if !ui.IsEnabled() {
|
||||||
ui.Mutex.RLock()
|
return fmt.Errorf("interface not enabled")
|
||||||
defer ui.Mutex.RUnlock()
|
|
||||||
return ui.packetCallback
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ui *UDPInterface) ProcessIncoming(data []byte) {
|
|
||||||
if callback := ui.GetPacketCallback(); callback != nil {
|
|
||||||
callback(data, ui)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ui.targetAddr == nil {
|
||||||
|
return fmt.Errorf("no target address configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.Mutex.Lock()
|
||||||
|
ui.TxBytes += uint64(len(data))
|
||||||
|
ui.Mutex.Unlock()
|
||||||
|
|
||||||
|
_, err := ui.conn.WriteTo(data, ui.targetAddr)
|
||||||
|
if err != nil {
|
||||||
|
debug.Log(debug.DEBUG_CRITICAL, "UDP interface write failed", "name", ui.Name, "error", err)
|
||||||
|
} else {
|
||||||
|
debug.Log(debug.DEBUG_ALL, "UDP interface sent bytes successfully", "name", ui.Name, "bytes", len(data))
|
||||||
|
}
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ui *UDPInterface) ProcessOutgoing(data []byte) error {
|
func (ui *UDPInterface) ProcessOutgoing(data []byte) error {
|
||||||
@@ -110,15 +95,15 @@ func (ui *UDPInterface) ProcessOutgoing(data []byte) error {
|
|||||||
return fmt.Errorf("interface offline")
|
return fmt.Errorf("interface offline")
|
||||||
}
|
}
|
||||||
|
|
||||||
if ui.targetAddr == nil {
|
_, err := ui.conn.Write(data)
|
||||||
return fmt.Errorf("no target address configured")
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := ui.conn.WriteToUDP(data, ui.targetAddr)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("UDP write failed: %v", err)
|
return fmt.Errorf("UDP write failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ui.Mutex.Lock()
|
||||||
|
ui.TxBytes += uint64(len(data))
|
||||||
|
ui.Mutex.Unlock()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,38 +111,6 @@ func (ui *UDPInterface) GetConn() net.Conn {
|
|||||||
return ui.conn
|
return ui.conn
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ui *UDPInterface) GetTxBytes() uint64 {
|
|
||||||
ui.Mutex.RLock()
|
|
||||||
defer ui.Mutex.RUnlock()
|
|
||||||
return ui.TxBytes
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ui *UDPInterface) GetRxBytes() uint64 {
|
|
||||||
ui.Mutex.RLock()
|
|
||||||
defer ui.Mutex.RUnlock()
|
|
||||||
return ui.RxBytes
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ui *UDPInterface) GetMTU() int {
|
|
||||||
return ui.MTU
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ui *UDPInterface) GetBitrate() int {
|
|
||||||
return int(ui.Bitrate)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ui *UDPInterface) Enable() {
|
|
||||||
ui.Mutex.Lock()
|
|
||||||
defer ui.Mutex.Unlock()
|
|
||||||
ui.Online = true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ui *UDPInterface) Disable() {
|
|
||||||
ui.Mutex.Lock()
|
|
||||||
defer ui.Mutex.Unlock()
|
|
||||||
ui.Online = false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ui *UDPInterface) Start() error {
|
func (ui *UDPInterface) Start() error {
|
||||||
ui.Mutex.Lock()
|
ui.Mutex.Lock()
|
||||||
if ui.conn != nil {
|
if ui.conn != nil {
|
||||||
@@ -241,19 +194,19 @@ func (ui *UDPInterface) readLoop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ui.Mutex.Lock()
|
ui.Mutex.Lock()
|
||||||
|
// #nosec G115 - Network read sizes are always positive and within safe range
|
||||||
|
ui.RxBytes += uint64(n)
|
||||||
|
|
||||||
// Auto-discover target address from first packet if not set
|
// Auto-discover target address from first packet if not set
|
||||||
if ui.targetAddr == nil {
|
if ui.targetAddr == nil {
|
||||||
debug.Log(debug.DEBUG_ALL, "UDP interface discovered peer", "name", ui.Name, "peer", remoteAddr.String())
|
debug.Log(debug.DEBUG_ALL, "UDP interface discovered peer", "name", ui.Name, "peer", remoteAddr.String())
|
||||||
ui.targetAddr = remoteAddr
|
ui.targetAddr = remoteAddr
|
||||||
}
|
}
|
||||||
|
callback := ui.packetCallback
|
||||||
ui.Mutex.Unlock()
|
ui.Mutex.Unlock()
|
||||||
|
|
||||||
ui.ProcessIncoming(buffer[:n])
|
if callback != nil {
|
||||||
|
callback(buffer[:n], ui)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ui *UDPInterface) IsEnabled() bool {
|
|
||||||
ui.Mutex.RLock()
|
|
||||||
defer ui.Mutex.RUnlock()
|
|
||||||
return ui.Enabled && ui.Online && !ui.Detached
|
|
||||||
}
|
|
||||||
|
|||||||
68
pkg/interfaces/udp_tinygo.go
Normal file
68
pkg/interfaces/udp_tinygo.go
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
||||||
|
//go:build tinygo
|
||||||
|
// +build tinygo
|
||||||
|
|
||||||
|
package interfaces
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UDPInterface struct {
|
||||||
|
BaseInterface
|
||||||
|
conn net.Conn
|
||||||
|
addr *net.UDPAddr
|
||||||
|
targetAddr *net.UDPAddr
|
||||||
|
readBuffer []byte
|
||||||
|
done chan struct{}
|
||||||
|
stopOnce sync.Once
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUDPInterface(name string, addr string, target string, enabled bool) (*UDPInterface, error) {
|
||||||
|
udpAddr, err := net.ResolveUDPAddr("udp", addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var targetAddr *net.UDPAddr
|
||||||
|
if target != "" {
|
||||||
|
targetAddr, err = net.ResolveUDPAddr("udp", target)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ui := &UDPInterface{
|
||||||
|
BaseInterface: NewBaseInterface(name, common.IF_TYPE_UDP, enabled),
|
||||||
|
addr: udpAddr,
|
||||||
|
targetAddr: targetAddr,
|
||||||
|
readBuffer: make([]byte, common.NUM_1064),
|
||||||
|
done: make(chan struct{}),
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.MTU = common.NUM_1064
|
||||||
|
|
||||||
|
return ui, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *UDPInterface) Start() error {
|
||||||
|
// TinyGo doesn't support UDP servers, only clients
|
||||||
|
return fmt.Errorf("UDPInterface not supported in TinyGo - UDP server functionality requires net.ListenUDP")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *UDPInterface) Send(data []byte, addr string) error {
|
||||||
|
// TinyGo doesn't support UDP sending
|
||||||
|
return fmt.Errorf("UDPInterface Send not supported in TinyGo - requires UDP client functionality")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *UDPInterface) Stop() error {
|
||||||
|
ui.Mutex.Lock()
|
||||||
|
defer ui.Mutex.Unlock()
|
||||||
|
ui.Online = false
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -92,12 +92,7 @@ func (wsi *WebSocketInterface) Start() error {
|
|||||||
defer wsi.Mutex.Unlock()
|
defer wsi.Mutex.Unlock()
|
||||||
|
|
||||||
if wsi.ws.Truthy() {
|
if wsi.ws.Truthy() {
|
||||||
readyState := wsi.ws.Get("readyState").Int()
|
return fmt.Errorf("WebSocket already started")
|
||||||
if readyState == 1 { // OPEN
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// If connecting, closing or closed, clean up first
|
|
||||||
wsi.closeWebSocket()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ws := js.Global().Get("WebSocket").New(wsi.wsURL)
|
ws := js.Global().Get("WebSocket").New(wsi.wsURL)
|
||||||
@@ -132,39 +127,30 @@ func (wsi *WebSocketInterface) Start() error {
|
|||||||
event := args[0]
|
event := args[0]
|
||||||
data := event.Get("data")
|
data := event.Get("data")
|
||||||
|
|
||||||
handlePacket := func(buf js.Value) {
|
var packet []byte
|
||||||
array := js.Global().Get("Uint8Array").New(buf)
|
|
||||||
length := array.Get("length").Int()
|
|
||||||
if length < 1 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
packet := make([]byte, length)
|
|
||||||
js.CopyBytesToGo(packet, array)
|
|
||||||
debug.Log(debug.DEBUG_VERBOSE, "WASM WebSocket received binary data", "name", wsi.Name, "length", length, "first_byte", fmt.Sprintf("0x%02x", packet[0]))
|
|
||||||
wsi.ProcessIncoming(packet)
|
|
||||||
}
|
|
||||||
|
|
||||||
if data.Type() == js.TypeString {
|
if data.Type() == js.TypeString {
|
||||||
packet := []byte(data.String())
|
packet = []byte(data.String())
|
||||||
debug.Log(debug.DEBUG_TRACE, "WebSocket received string data", "name", wsi.Name, "length", len(packet))
|
|
||||||
wsi.ProcessIncoming(packet)
|
|
||||||
} else if data.InstanceOf(js.Global().Get("ArrayBuffer")) {
|
|
||||||
handlePacket(data)
|
|
||||||
} else if data.InstanceOf(js.Global().Get("Blob")) {
|
|
||||||
// Handle Blob by converting to ArrayBuffer
|
|
||||||
data.Call("arrayBuffer").Call("then", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
|
||||||
if len(args) > 0 {
|
|
||||||
handlePacket(args[0])
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}))
|
|
||||||
} else if data.Type() == js.TypeObject {
|
} else if data.Type() == js.TypeObject {
|
||||||
// Fallback for other object types that might be TypedArrays
|
array := js.Global().Get("Uint8Array").New(data)
|
||||||
handlePacket(data)
|
length := array.Get("length").Int()
|
||||||
|
packet = make([]byte, length)
|
||||||
|
js.CopyBytesToGo(packet, array)
|
||||||
} else {
|
} else {
|
||||||
debug.Log(debug.DEBUG_ERROR, "Unknown WebSocket message type", "type", data.Type().String())
|
debug.Log(debug.DEBUG_ERROR, "Unknown WebSocket message type", "type", data.Type().String())
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(packet) < 1 {
|
||||||
|
debug.Log(debug.DEBUG_ERROR, "WebSocket message empty")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
wsi.Mutex.Lock()
|
||||||
|
wsi.RxBytes += uint64(len(packet))
|
||||||
|
wsi.Mutex.Unlock()
|
||||||
|
|
||||||
|
wsi.ProcessIncoming(packet)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@@ -182,10 +168,8 @@ func (wsi *WebSocketInterface) Start() error {
|
|||||||
debug.Log(debug.DEBUG_INFO, "WebSocket closed", "name", wsi.Name)
|
debug.Log(debug.DEBUG_INFO, "WebSocket closed", "name", wsi.Name)
|
||||||
|
|
||||||
if wsi.Enabled && !wsi.Detached {
|
if wsi.Enabled && !wsi.Detached {
|
||||||
go func() {
|
time.Sleep(WS_RECONNECT_DELAY)
|
||||||
time.Sleep(WS_RECONNECT_DELAY)
|
go wsi.Start()
|
||||||
_ = wsi.Start()
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -213,7 +197,15 @@ func (wsi *WebSocketInterface) closeWebSocket() {
|
|||||||
wsi.Online = false
|
wsi.Online = false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (wsi *WebSocketInterface) ProcessOutgoing(data []byte) error {
|
func (wsi *WebSocketInterface) Send(data []byte, addr string) error {
|
||||||
|
if !wsi.IsEnabled() {
|
||||||
|
return fmt.Errorf("interface not enabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
wsi.Mutex.Lock()
|
||||||
|
wsi.TxBytes += uint64(len(data))
|
||||||
|
wsi.Mutex.Unlock()
|
||||||
|
|
||||||
if !wsi.connected {
|
if !wsi.connected {
|
||||||
wsi.Mutex.Lock()
|
wsi.Mutex.Lock()
|
||||||
wsi.messageQueue = append(wsi.messageQueue, data)
|
wsi.messageQueue = append(wsi.messageQueue, data)
|
||||||
@@ -242,6 +234,10 @@ func (wsi *WebSocketInterface) sendWebSocketMessage(data []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (wsi *WebSocketInterface) ProcessOutgoing(data []byte) error {
|
||||||
|
return wsi.Send(data, "")
|
||||||
|
}
|
||||||
|
|
||||||
func (wsi *WebSocketInterface) GetConn() net.Conn {
|
func (wsi *WebSocketInterface) GetConn() net.Conn {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ import (
|
|||||||
"git.quad4.io/Networks/Reticulum-Go/pkg/resolver"
|
"git.quad4.io/Networks/Reticulum-Go/pkg/resolver"
|
||||||
"git.quad4.io/Networks/Reticulum-Go/pkg/resource"
|
"git.quad4.io/Networks/Reticulum-Go/pkg/resource"
|
||||||
"git.quad4.io/Networks/Reticulum-Go/pkg/transport"
|
"git.quad4.io/Networks/Reticulum-Go/pkg/transport"
|
||||||
"github.com/vmihailenco/msgpack/v5"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -308,7 +307,7 @@ func (l *Link) Request(path string, data []byte, timeout time.Duration) (*Reques
|
|||||||
|
|
||||||
pathHash := identity.TruncatedHash([]byte(path))
|
pathHash := identity.TruncatedHash([]byte(path))
|
||||||
requestData := []interface{}{time.Now().Unix(), pathHash, data}
|
requestData := []interface{}{time.Now().Unix(), pathHash, data}
|
||||||
packedRequest, err := msgpack.Marshal(requestData)
|
packedRequest, err := common.MsgpackMarshal(requestData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to pack request: %w", err)
|
return nil, fmt.Errorf("failed to pack request: %w", err)
|
||||||
}
|
}
|
||||||
@@ -1029,7 +1028,7 @@ func (l *Link) handleRequest(plaintext []byte, pkt *packet.Packet) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var requestData []interface{}
|
var requestData []interface{}
|
||||||
if err := msgpack.Unmarshal(plaintext, &requestData); err != nil {
|
if err := common.MsgpackUnmarshal(plaintext, &requestData); err != nil {
|
||||||
return fmt.Errorf("failed to unpack request: %w", err)
|
return fmt.Errorf("failed to unpack request: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1060,7 +1059,7 @@ func (l *Link) handleRequest(plaintext []byte, pkt *packet.Packet) error {
|
|||||||
case string:
|
case string:
|
||||||
requestPayload = []byte(payload)
|
requestPayload = []byte(payload)
|
||||||
default:
|
default:
|
||||||
packed, err := msgpack.Marshal(payload)
|
packed, err := common.MsgpackMarshal(payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to pack request_payload: %w", err)
|
return fmt.Errorf("failed to pack request_payload: %w", err)
|
||||||
}
|
}
|
||||||
@@ -1089,7 +1088,7 @@ func (l *Link) handleRequest(plaintext []byte, pkt *packet.Packet) error {
|
|||||||
|
|
||||||
func (l *Link) handleResponse(plaintext []byte) error {
|
func (l *Link) handleResponse(plaintext []byte) error {
|
||||||
var responseData []interface{}
|
var responseData []interface{}
|
||||||
if err := msgpack.Unmarshal(plaintext, &responseData); err != nil {
|
if err := common.MsgpackUnmarshal(plaintext, &responseData); err != nil {
|
||||||
return fmt.Errorf("failed to unpack response: %w", err)
|
return fmt.Errorf("failed to unpack response: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1124,7 +1123,7 @@ func (l *Link) handleResponse(plaintext []byte) error {
|
|||||||
|
|
||||||
func (l *Link) sendResponse(requestID []byte, response interface{}) error {
|
func (l *Link) sendResponse(requestID []byte, response interface{}) error {
|
||||||
responseData := []interface{}{requestID, response}
|
responseData := []interface{}{requestID, response}
|
||||||
packedResponse, err := msgpack.Marshal(responseData)
|
packedResponse, err := common.MsgpackMarshal(responseData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to pack response: %w", err)
|
return fmt.Errorf("failed to pack response: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,6 +144,8 @@ func (p *Packet) Pack() error {
|
|||||||
header := []byte{flags, p.Hops}
|
header := []byte{flags, p.Hops}
|
||||||
debug.Log(debug.DEBUG_TRACE, "Created packet header", "flags", fmt.Sprintf("%08b", flags), "hops", p.Hops)
|
debug.Log(debug.DEBUG_TRACE, "Created packet header", "flags", fmt.Sprintf("%08b", flags), "hops", p.Hops)
|
||||||
|
|
||||||
|
header = append(header, p.DestinationHash...)
|
||||||
|
|
||||||
if p.HeaderType == HeaderType2 {
|
if p.HeaderType == HeaderType2 {
|
||||||
if p.TransportID == nil {
|
if p.TransportID == nil {
|
||||||
return errors.New("transport ID required for header type 2")
|
return errors.New("transport ID required for header type 2")
|
||||||
@@ -152,8 +154,6 @@ func (p *Packet) Pack() error {
|
|||||||
debug.Log(debug.DEBUG_ALL, "Added transport ID to header", "transport_id", fmt.Sprintf("%x", p.TransportID))
|
debug.Log(debug.DEBUG_ALL, "Added transport ID to header", "transport_id", fmt.Sprintf("%x", p.TransportID))
|
||||||
}
|
}
|
||||||
|
|
||||||
header = append(header, p.DestinationHash...)
|
|
||||||
|
|
||||||
header = append(header, p.Context)
|
header = append(header, p.Context)
|
||||||
debug.Log(debug.DEBUG_PACKETS, "Final header length", "bytes", len(header))
|
debug.Log(debug.DEBUG_PACKETS, "Final header length", "bytes", len(header))
|
||||||
|
|
||||||
@@ -187,12 +187,12 @@ func (p *Packet) Unpack() error {
|
|||||||
dstLen := 16 // Truncated hash length
|
dstLen := 16 // Truncated hash length
|
||||||
|
|
||||||
if p.HeaderType == HeaderType2 {
|
if p.HeaderType == HeaderType2 {
|
||||||
// Header Type 2: Header(2) + TransportID(16) + DestHash(16) + Context(1) + Data
|
// Header Type 2: Header(2) + DestHash(16) + TransportID(16) + Context(1) + Data
|
||||||
if len(p.Raw) < 2*dstLen+3 {
|
if len(p.Raw) < 2*dstLen+3 {
|
||||||
return errors.New("packet too short for header type 2")
|
return errors.New("packet too short for header type 2")
|
||||||
}
|
}
|
||||||
p.TransportID = p.Raw[2 : dstLen+2] // Transport ID first
|
p.DestinationHash = p.Raw[2 : dstLen+2] // Destination hash first
|
||||||
p.DestinationHash = p.Raw[dstLen+2 : 2*dstLen+2] // Destination hash second
|
p.TransportID = p.Raw[dstLen+2 : 2*dstLen+2] // Transport ID second
|
||||||
p.Context = p.Raw[2*dstLen+2]
|
p.Context = p.Raw[2*dstLen+2]
|
||||||
p.Data = p.Raw[2*dstLen+3:]
|
p.Data = p.Raw[2*dstLen+3:]
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
// SPDX-License-Identifier: 0BSD
|
|
||||||
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
|
||||||
package packet
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func FuzzPacketUnpack(f *testing.F) {
|
|
||||||
// Add some valid packets as seeds
|
|
||||||
p1 := &Packet{
|
|
||||||
HeaderType: HeaderType1,
|
|
||||||
PacketType: PacketTypeData,
|
|
||||||
DestinationType: 0x01,
|
|
||||||
DestinationHash: make([]byte, 16),
|
|
||||||
Context: ContextNone,
|
|
||||||
Data: []byte("hello"),
|
|
||||||
}
|
|
||||||
if err := p1.Pack(); err == nil {
|
|
||||||
f.Add(p1.Raw)
|
|
||||||
}
|
|
||||||
|
|
||||||
p2 := &Packet{
|
|
||||||
HeaderType: HeaderType2,
|
|
||||||
PacketType: PacketTypeAnnounce,
|
|
||||||
TransportID: make([]byte, 16),
|
|
||||||
DestinationHash: make([]byte, 16),
|
|
||||||
Context: ContextNone,
|
|
||||||
Data: []byte("announce"),
|
|
||||||
}
|
|
||||||
if err := p2.Pack(); err == nil {
|
|
||||||
f.Add(p2.Raw)
|
|
||||||
}
|
|
||||||
|
|
||||||
f.Fuzz(func(t *testing.T, data []byte) {
|
|
||||||
p := &Packet{Raw: data}
|
|
||||||
// We don't care about the error, just that it doesn't panic
|
|
||||||
_ = p.Unpack()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
|
|
||||||
"github.com/vmihailenco/msgpack/v5"
|
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -117,12 +117,12 @@ func (ra *ResourceAdvertisement) Pack(segment int) ([]byte, error) {
|
|||||||
"m": hashmap,
|
"m": hashmap,
|
||||||
}
|
}
|
||||||
|
|
||||||
return msgpack.Marshal(dict)
|
return common.MsgpackMarshal(dict)
|
||||||
}
|
}
|
||||||
|
|
||||||
func UnpackResourceAdvertisement(data []byte) (*ResourceAdvertisement, error) {
|
func UnpackResourceAdvertisement(data []byte) (*ResourceAdvertisement, error) {
|
||||||
var dict map[string]interface{}
|
var dict map[string]interface{}
|
||||||
if err := msgpack.Unmarshal(data, &dict); err != nil {
|
if err := common.MsgpackUnmarshal(data, &dict); err != nil {
|
||||||
return nil, fmt.Errorf("failed to unpack advertisement: %w", err)
|
return nil, fmt.Errorf("failed to unpack advertisement: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
// SPDX-License-Identifier: 0BSD
|
|
||||||
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
|
||||||
package transport
|
|
||||||
|
|
||||||
import (
|
|
||||||
"runtime"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestTransportLeak(t *testing.T) {
|
|
||||||
// Baseline goroutine count
|
|
||||||
runtime.GC()
|
|
||||||
baseline := runtime.NumGoroutine()
|
|
||||||
|
|
||||||
cfg := &common.ReticulumConfig{}
|
|
||||||
|
|
||||||
// Create and close many transport instances
|
|
||||||
for i := 0; i < 100; i++ {
|
|
||||||
tr := NewTransport(cfg)
|
|
||||||
// Give it a tiny bit of time to start the goroutine
|
|
||||||
time.Sleep(1 * time.Millisecond)
|
|
||||||
tr.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for goroutines to finish
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
runtime.GC()
|
|
||||||
|
|
||||||
final := runtime.NumGoroutine()
|
|
||||||
|
|
||||||
// We allow a small margin for other system goroutines,
|
|
||||||
// but 100 leaks would be very obvious.
|
|
||||||
if final > baseline+5 {
|
|
||||||
t.Errorf("Potential goroutine leak: baseline %d, final %d", baseline, final)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
// SPDX-License-Identifier: 0BSD
|
|
||||||
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
|
||||||
package transport
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
|
|
||||||
)
|
|
||||||
|
|
||||||
type MockInterface struct {
|
|
||||||
common.BaseInterface
|
|
||||||
sentData [][]byte
|
|
||||||
dropRate float64 // 0.0 to 1.0
|
|
||||||
onReceive func([]byte)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockInterface) Send(data []byte, destination string) error {
|
|
||||||
m.Mutex.Lock()
|
|
||||||
defer m.Mutex.Unlock()
|
|
||||||
|
|
||||||
// Simulate packet loss
|
|
||||||
if m.dropRate > 0 {
|
|
||||||
// In a real test we'd use rand.Float64()
|
|
||||||
// For deterministic testing, let's just record everything for now
|
|
||||||
}
|
|
||||||
|
|
||||||
m.sentData = append(m.sentData, data)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockInterface) Receive(data []byte) {
|
|
||||||
if m.onReceive != nil {
|
|
||||||
m.onReceive(data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTransportNetworkSimulation(t *testing.T) {
|
|
||||||
cfg := &common.ReticulumConfig{}
|
|
||||||
tr := NewTransport(cfg)
|
|
||||||
defer tr.Close()
|
|
||||||
|
|
||||||
iface1 := &MockInterface{BaseInterface: common.NewBaseInterface("iface1", common.IF_TYPE_UDP, true)}
|
|
||||||
iface1.Enable()
|
|
||||||
|
|
||||||
iface2 := &MockInterface{BaseInterface: common.NewBaseInterface("iface2", common.IF_TYPE_UDP, true)}
|
|
||||||
iface2.Enable()
|
|
||||||
|
|
||||||
tr.RegisterInterface(iface1.GetName(), iface1)
|
|
||||||
tr.RegisterInterface(iface2.GetName(), iface2)
|
|
||||||
|
|
||||||
// Simulate receiving an announce on iface1
|
|
||||||
// [header][hops][dest_hash(16)][payload...]
|
|
||||||
announcePacket := make([]byte, 100)
|
|
||||||
announcePacket[0] = PACKET_TYPE_ANNOUNCE
|
|
||||||
announcePacket[1] = 0 // 0 hops
|
|
||||||
copy(announcePacket[2:18], []byte("destination_hash"))
|
|
||||||
|
|
||||||
// Mock the handler to avoid complex identity logic in this basic test
|
|
||||||
tr.HandlePacket(announcePacket, iface1)
|
|
||||||
|
|
||||||
// In a real scenario, it would be rebroadcast to iface2
|
|
||||||
// But HandlePacket runs in a goroutine, so we'd need to wait or use a better mock
|
|
||||||
fmt.Println("Network simulation test initialized")
|
|
||||||
}
|
|
||||||
@@ -18,6 +18,7 @@ import (
|
|||||||
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
|
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
|
||||||
"git.quad4.io/Networks/Reticulum-Go/pkg/destination"
|
"git.quad4.io/Networks/Reticulum-Go/pkg/destination"
|
||||||
"git.quad4.io/Networks/Reticulum-Go/pkg/identity"
|
"git.quad4.io/Networks/Reticulum-Go/pkg/identity"
|
||||||
|
"git.quad4.io/Networks/Reticulum-Go/pkg/interfaces"
|
||||||
"git.quad4.io/Networks/Reticulum-Go/pkg/packet"
|
"git.quad4.io/Networks/Reticulum-Go/pkg/packet"
|
||||||
"git.quad4.io/Networks/Reticulum-Go/pkg/pathfinder"
|
"git.quad4.io/Networks/Reticulum-Go/pkg/pathfinder"
|
||||||
"git.quad4.io/Networks/Reticulum-Go/pkg/rate"
|
"git.quad4.io/Networks/Reticulum-Go/pkg/rate"
|
||||||
@@ -323,12 +324,6 @@ func GetTransportInstance() *Transport {
|
|||||||
return transportInstance
|
return transportInstance
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetTransportInstance(t *Transport) {
|
|
||||||
transportMutex.Lock()
|
|
||||||
defer transportMutex.Unlock()
|
|
||||||
transportInstance = t
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Transport) RegisterInterface(name string, iface common.NetworkInterface) error {
|
func (t *Transport) RegisterInterface(name string, iface common.NetworkInterface) error {
|
||||||
t.mutex.Lock()
|
t.mutex.Lock()
|
||||||
defer t.mutex.Unlock()
|
defer t.mutex.Unlock()
|
||||||
@@ -582,11 +577,8 @@ func (t *Transport) RequestPath(destinationHash []byte, onInterface string, tag
|
|||||||
pathRequestData = append(destinationHash, tag...)
|
pathRequestData = append(destinationHash, tag...)
|
||||||
}
|
}
|
||||||
|
|
||||||
pathRequestName := "rnstransport.path.request"
|
destHashFull := sha256.Sum256([]byte("rnstransport.path.request"))
|
||||||
nameHashFull := sha256.Sum256([]byte(pathRequestName))
|
pathRequestDestHash := destHashFull[:common.SIZE_16]
|
||||||
nameHash10 := nameHashFull[:10]
|
|
||||||
finalHashFull := sha256.Sum256(nameHash10)
|
|
||||||
pathRequestDestHash := finalHashFull[:16]
|
|
||||||
|
|
||||||
pkt := packet.NewPacket(
|
pkt := packet.NewPacket(
|
||||||
packet.DestinationPlain,
|
packet.DestinationPlain,
|
||||||
@@ -594,12 +586,11 @@ func (t *Transport) RequestPath(destinationHash []byte, onInterface string, tag
|
|||||||
0x00,
|
0x00,
|
||||||
0x00,
|
0x00,
|
||||||
packet.PropagationBroadcast,
|
packet.PropagationBroadcast,
|
||||||
0x00, // Header Type 1
|
0x01,
|
||||||
nil,
|
pathRequestDestHash,
|
||||||
false,
|
false,
|
||||||
0x00,
|
0x00,
|
||||||
)
|
)
|
||||||
pkt.DestinationHash = pathRequestDestHash
|
|
||||||
|
|
||||||
if err := pkt.Pack(); err != nil {
|
if err := pkt.Pack(); err != nil {
|
||||||
return fmt.Errorf("failed to pack path request: %w", err)
|
return fmt.Errorf("failed to pack path request: %w", err)
|
||||||
@@ -890,6 +881,11 @@ func (t *Transport) HandlePacket(data []byte, iface common.NetworkInterface) {
|
|||||||
debug.Log(debug.DEBUG_ERROR, "67-byte packet detected", "header", fmt.Sprintf(common.STR_FMT_HEX, headerByte), "packet_type_bits", fmt.Sprintf(common.STR_FMT_HEX, packetType), "first_32_bytes", fmt.Sprintf("%x", data[:common.SIZE_32]))
|
debug.Log(debug.DEBUG_ERROR, "67-byte packet detected", "header", fmt.Sprintf(common.STR_FMT_HEX, headerByte), "packet_type_bits", fmt.Sprintf(common.STR_FMT_HEX, packetType), "first_32_bytes", fmt.Sprintf("%x", data[:common.SIZE_32]))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if tcpIface, ok := iface.(*interfaces.TCPClientInterface); ok {
|
||||||
|
tcpIface.UpdateStats(uint64(len(data)), true)
|
||||||
|
debug.Log(debug.DEBUG_PACKETS, "Updated TCP interface stats", "rx_bytes", len(data))
|
||||||
|
}
|
||||||
|
|
||||||
dataCopy := make([]byte, len(data))
|
dataCopy := make([]byte, len(data))
|
||||||
copy(dataCopy, data)
|
copy(dataCopy, data)
|
||||||
|
|
||||||
@@ -1114,15 +1110,16 @@ func (t *Transport) handleAnnouncePacket(data []byte, iface common.NetworkInterf
|
|||||||
// Register the path from this announce
|
// Register the path from this announce
|
||||||
// The destination is reachable via the interface that received this announce
|
// The destination is reachable via the interface that received this announce
|
||||||
if iface != nil {
|
if iface != nil {
|
||||||
|
// Use unlocked version since we may be called in a locked context
|
||||||
t.mutex.Lock()
|
t.mutex.Lock()
|
||||||
t.updatePathUnlocked(destinationHash, nil, iface.GetName(), hopCount+1)
|
t.updatePathUnlocked(destinationHash, nil, iface.GetName(), hopCount)
|
||||||
t.mutex.Unlock()
|
t.mutex.Unlock()
|
||||||
debug.Log(debug.DEBUG_INFO, "Registered path", "hash", fmt.Sprintf("%x", destinationHash), "interface", iface.GetName(), "hops", hopCount+1)
|
debug.Log(debug.DEBUG_INFO, "Registered path", "hash", fmt.Sprintf("%x", destinationHash), "interface", iface.GetName(), "hops", hopCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify handlers first, regardless of forwarding limits
|
// Notify handlers first, regardless of forwarding limits
|
||||||
debug.Log(debug.DEBUG_INFO, "Notifying announce handlers", "destHash", fmt.Sprintf("%x", destinationHash), "appDataLen", len(appData))
|
debug.Log(debug.DEBUG_INFO, "Notifying announce handlers", "destHash", fmt.Sprintf("%x", destinationHash), "appDataLen", len(appData))
|
||||||
t.notifyAnnounceHandlers(destinationHash, id, appData, hopCount+1)
|
t.notifyAnnounceHandlers(destinationHash, id, appData, hopCount)
|
||||||
debug.Log(debug.DEBUG_INFO, "Announce handlers notified")
|
debug.Log(debug.DEBUG_INFO, "Announce handlers notified")
|
||||||
|
|
||||||
// Don't forward if max hops reached
|
// Don't forward if max hops reached
|
||||||
@@ -1379,7 +1376,7 @@ func (t *Transport) InitializePathRequestHandler() error {
|
|||||||
return errors.New("transport identity not initialized")
|
return errors.New("transport identity not initialized")
|
||||||
}
|
}
|
||||||
|
|
||||||
pathRequestDest, err := destination.New(nil, destination.IN, destination.PLAIN, "rnstransport", t, "path", "request")
|
pathRequestDest, err := destination.New(t.transportIdentity, destination.IN, destination.PLAIN, "rnstransport", t, "path", "request")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create path request destination: %w", err)
|
return fmt.Errorf("failed to create path request destination: %w", err)
|
||||||
}
|
}
|
||||||
@@ -1695,14 +1692,6 @@ func (l *Link) HandleResource(resource interface{}) bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetIdentity sets the identity for the Transport.
|
|
||||||
func (t *Transport) SetIdentity(id *identity.Identity) {
|
|
||||||
t.mutex.Lock()
|
|
||||||
defer t.mutex.Unlock()
|
|
||||||
t.transportIdentity = id
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start initializes the Transport.
|
|
||||||
func (t *Transport) Start() error {
|
func (t *Transport) Start() error {
|
||||||
t.mutex.Lock()
|
t.mutex.Lock()
|
||||||
defer t.mutex.Unlock()
|
defer t.mutex.Unlock()
|
||||||
|
|||||||
@@ -3,12 +3,8 @@ package transport
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
|
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
|
||||||
"git.quad4.io/Networks/Reticulum-Go/pkg/destination"
|
|
||||||
"git.quad4.io/Networks/Reticulum-Go/pkg/identity"
|
|
||||||
"git.quad4.io/Networks/Reticulum-Go/pkg/packet"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type mockInterface struct {
|
type mockInterface struct {
|
||||||
@@ -122,68 +118,3 @@ func TestTransportStatus(t *testing.T) {
|
|||||||
t.Error("Path should be responsive again")
|
t.Error("Path should be responsive again")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAnnounceHopCount(t *testing.T) {
|
|
||||||
config := common.DefaultConfig()
|
|
||||||
tr := NewTransport(config)
|
|
||||||
defer tr.Close()
|
|
||||||
|
|
||||||
iface := &mockInterface{}
|
|
||||||
iface.Name = "wasm0"
|
|
||||||
iface.Enabled = true
|
|
||||||
_ = tr.RegisterInterface("wasm0", iface)
|
|
||||||
|
|
||||||
// Create an identity for the announce
|
|
||||||
id, _ := identity.New()
|
|
||||||
|
|
||||||
// Create a destination to get a valid hash for this identity
|
|
||||||
// NewAnnouncePacket uses "reticulum-go.node" by default
|
|
||||||
dest, _ := destination.New(id, destination.IN, destination.SINGLE, "reticulum-go.node", tr)
|
|
||||||
destHash := dest.GetHash()
|
|
||||||
|
|
||||||
// Create a raw announce packet manually to control hop count
|
|
||||||
// Header(2) + DestHash(16) + Context(1) + Payload...
|
|
||||||
// Header: 0x21 (Announce, Header Type 1, Broadcast, Destination Type Single)
|
|
||||||
// Hop count: 0
|
|
||||||
raw := make([]byte, 2+16+1+148) // header + dest + context + min_announce_payload
|
|
||||||
raw[0] = 0x21
|
|
||||||
raw[1] = 0 // Initial hop count
|
|
||||||
copy(raw[2:18], destHash)
|
|
||||||
raw[18] = 0 // context
|
|
||||||
|
|
||||||
// Announce payload: pubKey(64) + nameHash(10) + randomHash(10) + signature(64)
|
|
||||||
payload := raw[19:]
|
|
||||||
copy(payload[0:64], id.GetPublicKey())
|
|
||||||
// Name hash, random hash, signature - filling with dummy data but valid length
|
|
||||||
// Normally we would sign it properly, but handleAnnouncePacket validates it.
|
|
||||||
// Actually, handleAnnouncePacket WILL fail if signature is invalid.
|
|
||||||
// Use NewAnnouncePacket to get a valid signed packet
|
|
||||||
transportID := make([]byte, 16)
|
|
||||||
annPkt, err := packet.NewAnnouncePacket(destHash, id, []byte("test"), transportID)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("NewAnnouncePacket failed: %v", err)
|
|
||||||
}
|
|
||||||
annRaw, err := annPkt.Serialize()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Serialize failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Override hop count to 0 to simulate neighbor
|
|
||||||
annRaw[1] = 0
|
|
||||||
|
|
||||||
// Handle the packet
|
|
||||||
tr.HandlePacket(annRaw, iface)
|
|
||||||
|
|
||||||
// Wait a bit for the async processing
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
|
|
||||||
// Check stored hops
|
|
||||||
if !tr.HasPath(destHash) {
|
|
||||||
t.Fatal("Path not registered from announce")
|
|
||||||
}
|
|
||||||
|
|
||||||
hops := tr.HopsTo(destHash)
|
|
||||||
if hops != 1 {
|
|
||||||
t.Errorf("Expected 1 hop for neighbor (received 0), got %d", hops)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -24,12 +24,10 @@ var (
|
|||||||
reticulumDest *destination.Destination
|
reticulumDest *destination.Destination
|
||||||
reticulumIdentity *identity.Identity
|
reticulumIdentity *identity.Identity
|
||||||
stats = struct {
|
stats = struct {
|
||||||
packetsSent int
|
packetsSent int
|
||||||
packetsReceived int
|
packetsReceived int
|
||||||
bytesSent int
|
bytesSent int
|
||||||
bytesReceived int
|
bytesReceived int
|
||||||
announcesSent int
|
|
||||||
announcesReceived int
|
|
||||||
}{}
|
}{}
|
||||||
packetCallback js.Value
|
packetCallback js.Value
|
||||||
announceHandler js.Value
|
announceHandler js.Value
|
||||||
@@ -49,7 +47,6 @@ func RegisterJSFunctions() {
|
|||||||
"setPacketCallback": js.FuncOf(SetPacketCallback),
|
"setPacketCallback": js.FuncOf(SetPacketCallback),
|
||||||
"setAnnounceCallback": js.FuncOf(SetAnnounceCallback),
|
"setAnnounceCallback": js.FuncOf(SetAnnounceCallback),
|
||||||
"sendData": js.FuncOf(SendDataJS),
|
"sendData": js.FuncOf(SendDataJS),
|
||||||
"sendMessage": js.FuncOf(SendDataJS),
|
|
||||||
"announce": js.FuncOf(SendAnnounceJS),
|
"announce": js.FuncOf(SendAnnounceJS),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
@@ -103,31 +100,11 @@ func RequestPath(this js.Value, args []js.Value) interface{} {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func GetStats(this js.Value, args []js.Value) interface{} {
|
func GetStats(this js.Value, args []js.Value) interface{} {
|
||||||
if reticulumTransport != nil {
|
|
||||||
ifaces := reticulumTransport.GetInterfaces()
|
|
||||||
totalTxBytes := 0
|
|
||||||
totalRxBytes := 0
|
|
||||||
totalTxPackets := 0
|
|
||||||
totalRxPackets := 0
|
|
||||||
for _, iface := range ifaces {
|
|
||||||
totalTxBytes += int(iface.GetTxBytes())
|
|
||||||
totalRxBytes += int(iface.GetRxBytes())
|
|
||||||
totalTxPackets += int(iface.GetTxPackets())
|
|
||||||
totalRxPackets += int(iface.GetRxPackets())
|
|
||||||
}
|
|
||||||
stats.bytesSent = totalTxBytes
|
|
||||||
stats.bytesReceived = totalRxBytes
|
|
||||||
stats.packetsSent = totalTxPackets
|
|
||||||
stats.packetsReceived = totalRxPackets
|
|
||||||
}
|
|
||||||
|
|
||||||
return js.ValueOf(map[string]interface{}{
|
return js.ValueOf(map[string]interface{}{
|
||||||
"packetsSent": stats.packetsSent,
|
"packetsSent": stats.packetsSent,
|
||||||
"packetsReceived": stats.packetsReceived,
|
"packetsReceived": stats.packetsReceived,
|
||||||
"bytesSent": stats.bytesSent,
|
"bytesSent": stats.bytesSent,
|
||||||
"bytesReceived": stats.bytesReceived,
|
"bytesReceived": stats.bytesReceived,
|
||||||
"announcesSent": stats.announcesSent,
|
|
||||||
"announcesReceived": stats.announcesReceived,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,14 +154,6 @@ func InitReticulum(this js.Value, args []js.Value) interface{} {
|
|||||||
|
|
||||||
cfg := common.DefaultConfig()
|
cfg := common.DefaultConfig()
|
||||||
t := transport.NewTransport(cfg)
|
t := transport.NewTransport(cfg)
|
||||||
// Ensure the global instance is set for internal RNS calls (like Announce)
|
|
||||||
transport.SetTransportInstance(t)
|
|
||||||
|
|
||||||
// Set transport identity to the same as the node identity for now in WASM
|
|
||||||
t.SetIdentity(id)
|
|
||||||
if err := t.InitializePathRequestHandler(); err != nil {
|
|
||||||
debug.Log(debug.DEBUG_ERROR, "Failed to initialize path request handler", "error", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
dest, err := destination.New(
|
dest, err := destination.New(
|
||||||
id,
|
id,
|
||||||
@@ -201,6 +170,9 @@ func InitReticulum(this js.Value, args []js.Value) interface{} {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dest.SetPacketCallback(func(data []byte, ni common.NetworkInterface) {
|
dest.SetPacketCallback(func(data []byte, ni common.NetworkInterface) {
|
||||||
|
stats.packetsReceived++
|
||||||
|
stats.bytesReceived += len(data)
|
||||||
|
|
||||||
if !packetCallback.IsUndefined() {
|
if !packetCallback.IsUndefined() {
|
||||||
// Convert bytes to JS Uint8Array for performance and compatibility
|
// Convert bytes to JS Uint8Array for performance and compatibility
|
||||||
uint8Array := js.Global().Get("Uint8Array").New(len(data))
|
uint8Array := js.Global().Get("Uint8Array").New(len(data))
|
||||||
@@ -220,8 +192,12 @@ func InitReticulum(this js.Value, args []js.Value) interface{} {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wire the interface to the transport
|
wsInterface.SetPacketCallback(func(data []byte, ni common.NetworkInterface) {
|
||||||
wsInterface.SetPacketCallback(t.HandlePacket)
|
msg := fmt.Sprintf("Received packet: %d bytes (type: 0x%02x)", len(data), data[0])
|
||||||
|
js.Global().Call("log", msg, "success")
|
||||||
|
debug.Log(debug.DEBUG_INFO, "WASM received packet", "bytes", len(data), "type", fmt.Sprintf("0x%02x", data[0]))
|
||||||
|
t.HandlePacket(data, ni)
|
||||||
|
})
|
||||||
|
|
||||||
if err := t.RegisterInterface("wasm0", wsInterface); err != nil {
|
if err := t.RegisterInterface("wasm0", wsInterface); err != nil {
|
||||||
return js.ValueOf(map[string]interface{}{
|
return js.ValueOf(map[string]interface{}{
|
||||||
@@ -362,8 +338,6 @@ func (h *genericAnnounceHandler) ReceivePathResponses() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *genericAnnounceHandler) ReceivedAnnounce(destHash []byte, ident interface{}, appData []byte, hops uint8) error {
|
func (h *genericAnnounceHandler) ReceivedAnnounce(destHash []byte, ident interface{}, appData []byte, hops uint8) error {
|
||||||
debug.Log(debug.DEBUG_INFO, "WASM Announce Handler received announce", "dest", hex.EncodeToString(destHash), "hops", hops)
|
|
||||||
stats.announcesReceived++
|
|
||||||
if !announceHandler.IsUndefined() {
|
if !announceHandler.IsUndefined() {
|
||||||
hashStr := hex.EncodeToString(destHash)
|
hashStr := hex.EncodeToString(destHash)
|
||||||
announceHandler.Invoke(js.ValueOf(map[string]interface{}{
|
announceHandler.Invoke(js.ValueOf(map[string]interface{}{
|
||||||
@@ -457,6 +431,9 @@ func SendData(destHash []byte, data []byte) interface{} {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stats.packetsSent++
|
||||||
|
stats.bytesSent += len(data)
|
||||||
|
|
||||||
return js.ValueOf(map[string]interface{}{
|
return js.ValueOf(map[string]interface{}{
|
||||||
"success": true,
|
"success": true,
|
||||||
})
|
})
|
||||||
@@ -492,8 +469,6 @@ func SendAnnounce(appData []byte) interface{} {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
stats.announcesSent++
|
|
||||||
|
|
||||||
return js.ValueOf(map[string]interface{}{
|
return js.ValueOf(map[string]interface{}{
|
||||||
"success": true,
|
"success": true,
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user