mirror of https://github.com/jetkvm/kvm.git
Compare commits
102 Commits
95c6a55bb0
...
f455d6cc18
| Author | SHA1 | Date |
|---|---|---|
|
|
f455d6cc18 | |
|
|
d71b6537d2 | |
|
|
1f9aeff313 | |
|
|
51d546d5f2 | |
|
|
616b625a5c | |
|
|
82c2d6df25 | |
|
|
1e58e3d1bc | |
|
|
a05ffea205 | |
|
|
4f1ddc8783 | |
|
|
0361b24c7d | |
|
|
aaab8beb1a | |
|
|
800100aebd | |
|
|
2fcecda8c7 | |
|
|
ff56128dc5 | |
|
|
fa1eb7332b | |
|
|
06ea0970eb | |
|
|
0a3e966684 | |
|
|
3c67060269 | |
|
|
269222d471 | |
|
|
83caa8f82d | |
|
|
27750b9cc2 | |
|
|
5112bef19c | |
|
|
1ffdca4fd6 | |
|
|
c6dba4d59f | |
|
|
afb146d78c | |
|
|
72e3013337 | |
|
|
25b102ac34 | |
|
|
5c94c6c87f | |
|
|
cf679978be | |
|
|
80a8b9e9e3 | |
|
|
1717549578 | |
|
|
37b1a8bf34 | |
|
|
ca8b06f4cf | |
|
|
33e099f258 | |
|
|
ea068414dc | |
|
|
8d1a66806c | |
|
|
6202e3cafa | |
|
|
c866230711 | |
|
|
c8dd84c6b7 | |
|
|
c98592a412 | |
|
|
8fbad0112e | |
|
|
8a90555fad | |
|
|
a7db0e8408 | |
|
|
bcc307b147 | |
|
|
e8ef82e582 | |
|
|
5f3dd89d55 | |
|
|
1dda6184da | |
|
|
825d0311d6 | |
|
|
f3fe78af5d | |
|
|
d0b3781aaa | |
|
|
c68e15bf89 | |
|
|
94521ef6db | |
|
|
66cccfe9e1 | |
|
|
a42384fed6 | |
|
|
3ec243255b | |
|
|
05bf61152b | |
|
|
d952480c2a | |
|
|
8e27cd6b60 | |
|
|
bb87fb5a1a | |
|
|
8527b1eff1 | |
|
|
9f573200b1 | |
|
|
608f69db13 | |
|
|
f7b8efde7c | |
|
|
33ac9fe0b6 | |
|
|
55fbd6c359 | |
|
|
cff3ddad29 | |
|
|
b4525b8760 | |
|
|
5a3ce2d6ec | |
|
|
f1953fddbc | |
|
|
9ba97ebe67 | |
|
|
5fb8d866ba | |
|
|
3359f8fca4 | |
|
|
ef95643a86 | |
|
|
1fc603b553 | |
|
|
aada3d95e0 | |
|
|
d704fcc6c7 | |
|
|
ab3dda6dee | |
|
|
4a23f22a55 | |
|
|
11a095c0f6 | |
|
|
584768bacf | |
|
|
488276f3a8 | |
|
|
7267347261 | |
|
|
393bc122d4 | |
|
|
6d13e1be12 | |
|
|
bde0a086ab | |
|
|
9c9335da31 | |
|
|
090e0b4b47 | |
|
|
48a7a638a3 | |
|
|
e4f6a713a5 | |
|
|
9fcf74b398 | |
|
|
353099001f | |
|
|
73f5659618 | |
|
|
960f555790 | |
|
|
fe127ed41c | |
|
|
3e7d8fb0f5 | |
|
|
0d7f47c109 | |
|
|
254c001572 | |
|
|
6f037a832d | |
|
|
ccba27cedd | |
|
|
cf9c6e5cc8 | |
|
|
ffeaf8cced | |
|
|
a1ed28c676 |
|
|
@ -1,27 +1,38 @@
|
|||
{
|
||||
"name": "JetKVM",
|
||||
"image": "mcr.microsoft.com/devcontainers/go:1-1.23-bookworm",
|
||||
"image": "mcr.microsoft.com/devcontainers/go:1.25-trixie",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/node:1": {
|
||||
// Should match what is defined in ui/package.json
|
||||
"version": "22.15.0"
|
||||
"version": "22.19.0"
|
||||
}
|
||||
},
|
||||
"mounts": [
|
||||
"source=${localEnv:HOME}/.ssh,target=/home/vscode/.ssh,type=bind,consistency=cached"
|
||||
"source=${localEnv:HOME}/.ssh,target=/home/vscode/.ssh,type=bind,consistency=cached"
|
||||
],
|
||||
"onCreateCommand": ".devcontainer/install-deps.sh",
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"bradlc.vscode-tailwindcss",
|
||||
// coding styles
|
||||
"chrislajoie.vscode-modelines",
|
||||
"editorconfig.editorconfig",
|
||||
// GitHub
|
||||
"GitHub.vscode-pull-request-github",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"github.vscode-github-actions",
|
||||
// Golang
|
||||
"golang.go",
|
||||
// C / C++
|
||||
"ms-vscode.cpptools",
|
||||
"ms-vscode.cpptools-extension-pack",
|
||||
// CMake / Makefile
|
||||
"ms-vscode.makefile-tools",
|
||||
"ms-vscode.cmake-tools",
|
||||
// Frontend
|
||||
"esbenp.prettier-vscode",
|
||||
"github.vscode-github-actions"
|
||||
"dbaeumer.vscode-eslint",
|
||||
"bradlc.vscode-tailwindcss"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
sudo apt-get update && sudo apt-get install -y --no-install-recommends \
|
||||
build-essential \
|
||||
device-tree-compiler \
|
||||
gperf g++-multilib gcc-multilib \
|
||||
libnl-3-dev libdbus-1-dev libelf-dev libmpc-dev dwarves \
|
||||
bc openssl flex bison libssl-dev python3 python-is-python3 texinfo kmod cmake \
|
||||
wget zstd \
|
||||
python3-venv python3-kconfiglib \
|
||||
&& sudo rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install buildkit
|
||||
BUILDKIT_VERSION="v0.2.5"
|
||||
BUILDKIT_TMPDIR="$(mktemp -d)"
|
||||
pushd "${BUILDKIT_TMPDIR}" > /dev/null
|
||||
|
||||
wget https://github.com/jetkvm/rv1106-system/releases/download/${BUILDKIT_VERSION}/buildkit.tar.zst && \
|
||||
sudo mkdir -p /opt/jetkvm-native-buildkit && \
|
||||
sudo tar --use-compress-program="unzstd --long=31" -xvf buildkit.tar.zst -C /opt/jetkvm-native-buildkit && \
|
||||
rm buildkit.tar.zst
|
||||
popd
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
---
|
||||
name: Bug report
|
||||
description: 🐛 Let us know about an unexpected error, a crash, or an unexpected behavior.
|
||||
type: 'Bug'
|
||||
labels:
|
||||
- 'type: bug'
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Disclaimer
|
||||
description: |
|
||||
For support questions, please use the [discussions][] or [Discord][] instead. Before
|
||||
opening a bug report, ensure you have read the [documentation][],
|
||||
[Troubleshooting][] and [Device FAQs][]. Only use bug reports for actual
|
||||
bugs.
|
||||
|
||||
[documentation]: https://jetkvm.com/docs
|
||||
[Troubleshooting]: https://jetkvm.com/docs/getting-started/troubleshooting
|
||||
[Device FAQs]: https://jetkvm.com/docs/getting-started/faq
|
||||
[discussions]: https://github.com/jetkvm/kvm/discussions
|
||||
[Discord]: https://jetkvm.com/discord
|
||||
options:
|
||||
- label: I have read and understood the disclaimer.
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: Application version
|
||||
description: |
|
||||
Provide the application version (can be found in General settings)
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: System version
|
||||
description: |
|
||||
Provide the system version (can be found in General settings)
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Device model
|
||||
description: Provide the device model
|
||||
options:
|
||||
- JetKVM
|
||||
- JetKVM (POE)
|
||||
validations:
|
||||
required: false
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Extension model
|
||||
description: Provide the extension model (if the bug is related to the extension)
|
||||
options:
|
||||
- ATX Power Control
|
||||
- DC Power Control
|
||||
- Serial Console
|
||||
validations:
|
||||
required: false
|
||||
- type: input
|
||||
attributes:
|
||||
label: Remote device Hardware
|
||||
description: If the bug is related to a remote device, please provide its hardware information e.g. Raspberry Pi 5
|
||||
validations:
|
||||
required: false
|
||||
- type: input
|
||||
attributes:
|
||||
label: Remote device OS
|
||||
description: If the bug is related to a remote device, please provide its OS information as detailed as possible e.g. Debian 12.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Bug description
|
||||
description: |
|
||||
Provide a description of the problem: steps to reproduce it, what you are expecting and what you got.
|
||||
validations:
|
||||
required: true
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
blank_issues_enabled: true
|
||||
|
||||
contact_links:
|
||||
- name: Hardware Issues
|
||||
url: https://jetkvm.com/contact
|
||||
about: If your hardware is not powering on or is not working, please contact us.
|
||||
|
||||
- name: Discord
|
||||
url: https://jetkvm.com/discord
|
||||
about: Engage with the JetKVM team and other community members.
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
name: Feature
|
||||
type: 'Feature'
|
||||
description: 🚀 Request a new feature.
|
||||
labels:
|
||||
- 'type: feature'
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: A note for the community
|
||||
value: |
|
||||
> [!NOTE]
|
||||
> Please vote on this issue by adding a 👍 [reaction](https://blog.github.com/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/) to the original issue to help the community and maintainers prioritize this request.
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Disclaimer
|
||||
description: |
|
||||
Before requesting a feature, check it does not already exist in the [documentation](https://jetkvm.com/docs) or our [roadmap](https://jetkvm.com/roadmap).
|
||||
You are quite welcome opening a feature request before spending time to implement it yourself.
|
||||
options:
|
||||
- label: I have read and understood the disclaimer.
|
||||
required: true
|
||||
- label: I plan to implement the feature myself.
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Subsystem
|
||||
description: Provide the subsystem of the feature you request, you can choose multiple if you think it fits in multiple areas.
|
||||
options:
|
||||
- Hardware
|
||||
- Device Compatibility
|
||||
- Keyboard
|
||||
- Mouse
|
||||
- Power
|
||||
- UI: Screen
|
||||
- UI: Application
|
||||
- UI: Cloud
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Feature description
|
||||
description: |
|
||||
Provide a description of the feature you request.
|
||||
validations:
|
||||
required: true
|
||||
|
|
@ -10,12 +10,19 @@ on:
|
|||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
runs-on: ubuntu-latest
|
||||
name: Build
|
||||
if: "github.event.review.state == 'approved' || github.event.event_type != 'pull_request_review'"
|
||||
if: github.event_name != 'pull_request_review' || github.event.review.state == 'approved'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
- name: Set up Cmake cache
|
||||
uses: buildjet/cache@v4
|
||||
with:
|
||||
path: internal/native/cgo/build
|
||||
key: jetkvm-cgo-${{ hashFiles('internal/native/cgo/**/*.c', 'internal/native/cgo/**/*.h', 'internal/native/cgo/**/*.patch', 'internal/native/cgo/**/*.txt', 'internal/native/cgo/**/*.sh', '!internal/native/cgo/build/**') }}
|
||||
restore-keys: |
|
||||
jetkvm-cgo-${{ hashFiles('internal/native/cgo/**/*.c', 'internal/native/cgo/**/*.h', 'internal/native/cgo/**/*.patch', 'internal/native/cgo/**/*.txt', 'internal/native/cgo/**/*.sh', '!internal/native/cgo/build/**') }}
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
|
|
@ -23,7 +30,7 @@ jobs:
|
|||
cache: "npm"
|
||||
cache-dependency-path: "**/package-lock.json"
|
||||
- name: Set up Golang
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v5.5.0
|
||||
with:
|
||||
go-version: "1.24.4"
|
||||
- name: Build frontend
|
||||
|
|
|
|||
|
|
@ -22,9 +22,9 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 # v4.2.2
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@19bb51245e9c80abacb2e91cc42b33fa478b8639 # v4.2.1
|
||||
uses: actions/setup-go@fa96338abe5531f6e34c5cc0bbe28c1a533d5505 # v4.2.1
|
||||
with:
|
||||
go-version: 1.24.4
|
||||
- name: Create empty resource directory
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ jobs:
|
|||
EOF
|
||||
ssh jkci "cat /tmp/device-tests.json" > device-tests.json
|
||||
- name: Set up Golang
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v5.5.0
|
||||
with:
|
||||
go-version: "1.24.4"
|
||||
- name: Golang Test Report
|
||||
|
|
|
|||
|
|
@ -14,16 +14,16 @@ permissions:
|
|||
jobs:
|
||||
ui-lint:
|
||||
name: UI Lint
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: v21.1.0
|
||||
node-version: "22"
|
||||
cache: "npm"
|
||||
cache-dependency-path: "ui/package-lock.json"
|
||||
cache-dependency-path: "**/package-lock.json"
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
cd ui
|
||||
|
|
|
|||
|
|
@ -3,4 +3,5 @@ static/*
|
|||
.idea
|
||||
.DS_Store
|
||||
|
||||
device-tests.tar.gz
|
||||
device-tests.tar.gz
|
||||
node_modules
|
||||
|
|
|
|||
|
|
@ -23,6 +23,9 @@ linters:
|
|||
- linters:
|
||||
- errcheck
|
||||
path: _test.go
|
||||
- linters:
|
||||
- forbidigo
|
||||
path: cmd/main.go
|
||||
- linters:
|
||||
- gochecknoinits
|
||||
path: internal/logging/sse.go
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"hash": "10b5fb8d",
|
||||
"configHash": "fd609f12",
|
||||
"lockfileHash": "e3b0c442",
|
||||
"browserHash": "4bebaebb",
|
||||
"optimized": {},
|
||||
"chunks": {}
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"type": "module"
|
||||
}
|
||||
|
|
@ -1,3 +1,7 @@
|
|||
{
|
||||
"tailwindCSS.classFunctions": ["cva", "cx"]
|
||||
"tailwindCSS.classFunctions": [
|
||||
"cva",
|
||||
"cx"
|
||||
],
|
||||
"cmake.sourceDirectory": "/Users/aveline/Projects/JetKVM/ymjk/internal/native/cgo"
|
||||
}
|
||||
|
|
@ -0,0 +1,356 @@
|
|||
<div align="center">
|
||||
<img alt="JetKVM logo" src="https://jetkvm.com/logo-blue.png" height="28">
|
||||
|
||||
### Development Guide
|
||||
|
||||
[Discord](https://jetkvm.com/discord) | [Website](https://jetkvm.com) | [Issues](https://github.com/jetkvm/cloud-api/issues) | [Docs](https://jetkvm.com/docs)
|
||||
|
||||
[](https://twitter.com/jetkvm)
|
||||
|
||||
[](https://goreportcard.com/report/github.com/jetkvm/kvm)
|
||||
|
||||
</div>
|
||||
|
||||
# JetKVM Development Guide
|
||||
|
||||
Welcome to JetKVM development! This guide will help you get started quickly, whether you're fixing bugs, adding features, or just exploring the codebase.
|
||||
|
||||
## Get Started
|
||||
|
||||
### Prerequisites
|
||||
- **A JetKVM device** (for full development)
|
||||
- **[Go 1.24.4+](https://go.dev/doc/install)** and **[Node.js 22.15.0](https://nodejs.org/en/download/)**
|
||||
- **[Git](https://git-scm.com/downloads)** for version control
|
||||
- **[SSH access](https://jetkvm.com/docs/advanced-usage/developing#developer-mode)** to your JetKVM device
|
||||
|
||||
### Development Environment
|
||||
|
||||
**Recommended:** Development is best done on **Linux** or **macOS**.
|
||||
|
||||
If you're using Windows, we strongly recommend using **WSL (Windows Subsystem for Linux)** for the best development experience:
|
||||
- [Install WSL on Windows](https://docs.microsoft.com/en-us/windows/wsl/install)
|
||||
- [WSL Setup Guide](https://docs.microsoft.com/en-us/windows/wsl/setup/environment)
|
||||
|
||||
This ensures compatibility with shell scripts and build tools used in the project.
|
||||
|
||||
### Project Setup
|
||||
|
||||
1. **Clone the repository:**
|
||||
```bash
|
||||
git clone https://github.com/jetkvm/kvm.git
|
||||
cd kvm
|
||||
```
|
||||
|
||||
2. **Check your tools:**
|
||||
```bash
|
||||
go version && node --version
|
||||
```
|
||||
|
||||
3. **Find your JetKVM IP address** (check your router or device screen)
|
||||
|
||||
4. **Deploy and test:**
|
||||
```bash
|
||||
./dev_deploy.sh -r 192.168.1.100 # Replace with your device IP
|
||||
```
|
||||
|
||||
5. **Open in browser:** `http://192.168.1.100`
|
||||
|
||||
That's it! You're now running your own development version of JetKVM.
|
||||
|
||||
---
|
||||
|
||||
## Common Tasks
|
||||
|
||||
### Modify the UI
|
||||
|
||||
```bash
|
||||
cd ui
|
||||
npm install
|
||||
./dev_device.sh 192.168.1.100 # Replace with your device IP
|
||||
```
|
||||
|
||||
Now edit files in `ui/src/` and see changes live in your browser!
|
||||
|
||||
### Modify the backend
|
||||
|
||||
```bash
|
||||
# Edit Go files (config.go, web.go, etc.)
|
||||
./dev_deploy.sh -r 192.168.1.100 --skip-ui-build
|
||||
```
|
||||
|
||||
### Run tests
|
||||
|
||||
```bash
|
||||
./dev_deploy.sh -r 192.168.1.100 --run-go-tests
|
||||
```
|
||||
|
||||
### View logs
|
||||
|
||||
```bash
|
||||
ssh root@192.168.1.100
|
||||
tail -f /var/log/jetkvm.log
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Project Layout
|
||||
|
||||
```
|
||||
/kvm/
|
||||
├── main.go # App entry point
|
||||
├── config.go # Settings & configuration
|
||||
├── web.go # API endpoints
|
||||
├── ui/ # React frontend
|
||||
│ ├── src/routes/ # Pages (login, settings, etc.)
|
||||
│ └── src/components/ # UI components
|
||||
└── internal/ # Internal Go packages
|
||||
```
|
||||
|
||||
**Key files for beginners:**
|
||||
|
||||
- `web.go` - Add new API endpoints here
|
||||
- `config.go` - Add new settings here
|
||||
- `ui/src/routes/` - Add new pages here
|
||||
- `ui/src/components/` - Add new UI components here
|
||||
|
||||
---
|
||||
|
||||
## Development Modes
|
||||
|
||||
### Full Development (Recommended)
|
||||
|
||||
*Best for: Complete feature development*
|
||||
|
||||
```bash
|
||||
# Deploy everything to your JetKVM device
|
||||
./dev_deploy.sh -r <YOUR_DEVICE_IP>
|
||||
```
|
||||
|
||||
### Frontend Only
|
||||
|
||||
*Best for: UI changes without device*
|
||||
|
||||
```bash
|
||||
cd ui
|
||||
npm install
|
||||
./dev_device.sh <YOUR_DEVICE_IP>
|
||||
```
|
||||
|
||||
### Quick Backend Changes
|
||||
|
||||
*Best for: API or backend logic changes*
|
||||
|
||||
```bash
|
||||
# Skip frontend build for faster deployment
|
||||
./dev_deploy.sh -r <YOUR_DEVICE_IP> --skip-ui-build
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Debugging Made Easy
|
||||
|
||||
### Check if everything is working
|
||||
|
||||
```bash
|
||||
# Test connection to device
|
||||
ping 192.168.1.100
|
||||
|
||||
# Check if JetKVM is running
|
||||
ssh root@192.168.1.100 ps aux | grep jetkvm
|
||||
```
|
||||
|
||||
### View live logs
|
||||
|
||||
```bash
|
||||
ssh root@192.168.1.100
|
||||
tail -f /var/log/jetkvm.log
|
||||
```
|
||||
|
||||
### Reset everything (if stuck)
|
||||
|
||||
```bash
|
||||
ssh root@192.168.1.100
|
||||
rm /userdata/kvm_config.json
|
||||
systemctl restart jetkvm
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Your Changes
|
||||
|
||||
### Manual Testing
|
||||
|
||||
1. Deploy your changes: `./dev_deploy.sh -r <IP>`
|
||||
2. Open browser: `http://<IP>`
|
||||
3. Test your feature
|
||||
4. Check logs: `ssh root@<IP> tail -f /var/log/jetkvm.log`
|
||||
|
||||
### Automated Testing
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
./dev_deploy.sh -r <IP> --run-go-tests
|
||||
|
||||
# Frontend linting
|
||||
cd ui && npm run lint
|
||||
```
|
||||
|
||||
### API Testing
|
||||
|
||||
```bash
|
||||
# Test login endpoint
|
||||
curl -X POST http://<IP>/auth/password-local \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"password": "test123"}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Issues & Solutions
|
||||
|
||||
### "Build failed" or "Permission denied"
|
||||
|
||||
```bash
|
||||
# Fix permissions
|
||||
ssh root@<IP> chmod +x /userdata/jetkvm/bin/jetkvm_app_debug
|
||||
|
||||
# Clean and rebuild
|
||||
go clean -modcache
|
||||
go mod tidy
|
||||
make build_dev
|
||||
```
|
||||
|
||||
### "Can't connect to device"
|
||||
|
||||
```bash
|
||||
# Check network
|
||||
ping <IP>
|
||||
|
||||
# Check SSH
|
||||
ssh root@<IP> echo "Connection OK"
|
||||
```
|
||||
|
||||
### "Frontend not updating"
|
||||
|
||||
```bash
|
||||
# Clear cache and rebuild
|
||||
cd ui
|
||||
npm cache clean --force
|
||||
rm -rf node_modules
|
||||
npm install
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
### Adding a New Feature
|
||||
|
||||
1. **Backend:** Add API endpoint in `web.go`
|
||||
2. **Config:** Add settings in `config.go`
|
||||
3. **Frontend:** Add UI in `ui/src/routes/`
|
||||
4. **Test:** Deploy and test with `./dev_deploy.sh`
|
||||
|
||||
### Code Style
|
||||
|
||||
- **Go:** Follow standard Go conventions
|
||||
- **TypeScript:** Use TypeScript for type safety
|
||||
- **React:** Keep components small and reusable
|
||||
|
||||
### Environment Variables
|
||||
|
||||
```bash
|
||||
# Enable debug logging
|
||||
export LOG_TRACE_SCOPES="jetkvm,cloud,websocket,native,jsonrpc"
|
||||
|
||||
# Frontend development
|
||||
export JETKVM_PROXY_URL="ws://<IP>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Need Help?
|
||||
|
||||
1. **Check logs first:** `ssh root@<IP> tail -f /var/log/jetkvm.log`
|
||||
2. **Search issues:** [GitHub Issues](https://github.com/jetkvm/kvm/issues)
|
||||
3. **Ask on Discord:** [JetKVM Discord](https://jetkvm.com/discord)
|
||||
4. **Read docs:** [JetKVM Documentation](https://jetkvm.com/docs)
|
||||
|
||||
---
|
||||
|
||||
## Contributing
|
||||
|
||||
### Ready to contribute?
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Make your changes
|
||||
4. Test thoroughly
|
||||
5. Submit a pull request
|
||||
|
||||
### Before submitting:
|
||||
|
||||
- [ ] Code works on device
|
||||
- [ ] Tests pass
|
||||
- [ ] Code follows style guidelines
|
||||
- [ ] Documentation updated (if needed)
|
||||
|
||||
---
|
||||
|
||||
## Advanced Topics
|
||||
|
||||
### Performance Profiling
|
||||
|
||||
1. Enable `Developer Mode` on your JetKVM device
|
||||
2. Add a password on the `Access` tab
|
||||
|
||||
```bash
|
||||
# Access profiling
|
||||
curl http://api:$JETKVM_PASSWORD@YOUR_DEVICE_IP/developer/pprof/
|
||||
```
|
||||
|
||||
### Advanced Environment Variables
|
||||
|
||||
```bash
|
||||
# Enable trace logging (useful for debugging)
|
||||
export LOG_TRACE_SCOPES="jetkvm,cloud,websocket,native,jsonrpc"
|
||||
|
||||
# For frontend development
|
||||
export JETKVM_PROXY_URL="ws://<JETKVM_IP>"
|
||||
|
||||
# Enable SSL in development
|
||||
export USE_SSL=true
|
||||
```
|
||||
|
||||
### Configuration Management
|
||||
|
||||
The application uses a JSON configuration file stored at `/userdata/kvm_config.json`.
|
||||
|
||||
#### Adding New Configuration Options
|
||||
|
||||
1. **Update the Config struct in `config.go`:**
|
||||
|
||||
```go
|
||||
type Config struct {
|
||||
// ... existing fields
|
||||
NewFeatureEnabled bool `json:"new_feature_enabled"`
|
||||
}
|
||||
```
|
||||
|
||||
2. **Update the default configuration:**
|
||||
|
||||
```go
|
||||
var defaultConfig = &Config{
|
||||
// ... existing defaults
|
||||
NewFeatureEnabled: false,
|
||||
}
|
||||
```
|
||||
|
||||
3. **Add migration logic if needed for existing installations**
|
||||
|
||||
|
||||
---
|
||||
|
||||
**Happy coding!**
|
||||
|
||||
For more information, visit the [JetKVM Documentation](https://jetkvm.com/docs) or join our [Discord Server](https://jetkvm.com/discord).
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
# syntax=docker/dockerfile:1
|
||||
FROM golang:1.24.4-bookworm
|
||||
|
||||
ENV GOTOOLCHAIN=local
|
||||
ENV GOPATH /go
|
||||
ENV PATH $GOPATH/bin:/usr/local/go/bin:$PATH
|
||||
|
||||
# Install build dependencies
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
build-essential \
|
||||
device-tree-compiler \
|
||||
gperf g++-multilib gcc-multilib \
|
||||
libnl-3-dev libdbus-1-dev libelf-dev libmpc-dev dwarves \
|
||||
bc openssl flex bison libssl-dev python3 python-is-python3 texinfo kmod cmake \
|
||||
wget zstd \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install buildkit
|
||||
ENV BUILDKIT_VERSION="v0.2.2"
|
||||
RUN wget https://github.com/jetkvm/rv1106-system/releases/download/${BUILDKIT_VERSION}/buildkit.tar.zst && \
|
||||
mkdir -p /opt/jetkvm-native-buildkit && \
|
||||
tar --use-compress-program="unzstd --long=31" -xvf buildkit.tar.zst -C /opt/jetkvm-native-buildkit && \
|
||||
rm buildkit.tar.zst
|
||||
|
||||
# Create build directory
|
||||
RUN mkdir -p /build/
|
||||
|
||||
# Copy go.mod and go.sum
|
||||
COPY go.mod go.sum /build/
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
RUN go mod download && go mod verify
|
||||
66
Makefile
66
Makefile
|
|
@ -1,14 +1,18 @@
|
|||
BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD)
|
||||
BUILDDATE ?= $(shell date -u +%FT%T%z)
|
||||
BUILDTS ?= $(shell date -u +%s)
|
||||
REVISION ?= $(shell git rev-parse HEAD)
|
||||
VERSION_DEV := 0.4.5-dev$(shell date +%Y%m%d%H%M)
|
||||
VERSION := 0.4.4
|
||||
BRANCH := $(shell git rev-parse --abbrev-ref HEAD)
|
||||
BUILDDATE := $(shell date -u +%FT%T%z)
|
||||
BUILDTS := $(shell date -u +%s)
|
||||
REVISION := $(shell git rev-parse HEAD)
|
||||
VERSION_DEV := 0.4.8-dev$(shell date +%Y%m%d%H%M)
|
||||
VERSION := 0.4.7
|
||||
|
||||
PROMETHEUS_TAG := github.com/prometheus/common/version
|
||||
KVM_PKG_NAME := github.com/jetkvm/kvm
|
||||
|
||||
GO_BUILD_ARGS := -tags netgo
|
||||
BUILDKIT_FLAVOR := arm-rockchip830-linux-uclibcgnueabihf
|
||||
BUILDKIT_PATH ?= /opt/jetkvm-native-buildkit
|
||||
|
||||
|
||||
GO_BUILD_ARGS := -tags netgo -tags timetzdata
|
||||
GO_RELEASE_BUILD_ARGS := -trimpath $(GO_BUILD_ARGS)
|
||||
GO_LDFLAGS := \
|
||||
-s -w \
|
||||
|
|
@ -17,20 +21,37 @@ GO_LDFLAGS := \
|
|||
-X $(PROMETHEUS_TAG).Revision=$(REVISION) \
|
||||
-X $(KVM_PKG_NAME).builtTimestamp=$(BUILDTS)
|
||||
|
||||
GO_CMD := GOOS=linux GOARCH=arm GOARM=7 go
|
||||
GO_ARGS := GOOS=linux GOARCH=arm GOARM=7 ARCHFLAGS="-arch arm"
|
||||
# if BUILDKIT_PATH exists, use buildkit to build
|
||||
ifneq ($(wildcard $(BUILDKIT_PATH)),)
|
||||
GO_ARGS := $(GO_ARGS) \
|
||||
CGO_CFLAGS="-I$(BUILDKIT_PATH)/$(BUILDKIT_FLAVOR)/include -I$(BUILDKIT_PATH)/$(BUILDKIT_FLAVOR)/sysroot/usr/include" \
|
||||
CGO_LDFLAGS="-L$(BUILDKIT_PATH)/$(BUILDKIT_FLAVOR)/lib -L$(BUILDKIT_PATH)/$(BUILDKIT_FLAVOR)/sysroot/usr/lib -lrockit -lrockchip_mpp -lrga -lpthread -lm" \
|
||||
CC="$(BUILDKIT_PATH)/bin/$(BUILDKIT_FLAVOR)-gcc" \
|
||||
LD="$(BUILDKIT_PATH)/bin/$(BUILDKIT_FLAVOR)-ld" \
|
||||
CGO_ENABLED=1
|
||||
# GO_RELEASE_BUILD_ARGS := $(GO_RELEASE_BUILD_ARGS) -x -work
|
||||
endif
|
||||
|
||||
GO_CMD := $(GO_ARGS) go
|
||||
|
||||
BIN_DIR := $(shell pwd)/bin
|
||||
|
||||
TEST_DIRS := $(shell find . -name "*_test.go" -type f -exec dirname {} \; | sort -u)
|
||||
|
||||
hash_resource:
|
||||
@shasum -a 256 resource/jetkvm_native | cut -d ' ' -f 1 > resource/jetkvm_native.sha256
|
||||
build_native:
|
||||
@echo "Building native..."
|
||||
cd internal/native/cgo && ./ui_index.gen.sh && \
|
||||
CC="$(BUILDKIT_PATH)/bin/$(BUILDKIT_FLAVOR)-gcc" \
|
||||
LD="$(BUILDKIT_PATH)/bin/$(BUILDKIT_FLAVOR)-ld" \
|
||||
./build.sh
|
||||
|
||||
build_dev: hash_resource
|
||||
build_dev: build_native
|
||||
@echo "Building..."
|
||||
$(GO_CMD) build \
|
||||
-ldflags="$(GO_LDFLAGS) -X $(KVM_PKG_NAME).builtAppVersion=$(VERSION_DEV)" \
|
||||
$(GO_RELEASE_BUILD_ARGS) \
|
||||
-o $(BIN_DIR)/jetkvm_app cmd/main.go
|
||||
-o $(BIN_DIR)/jetkvm_app -v cmd/main.go
|
||||
|
||||
build_test2json:
|
||||
$(GO_CMD) build -o $(BIN_DIR)/test2json cmd/test2json
|
||||
|
|
@ -62,10 +83,25 @@ build_dev_test: build_test2json build_gotestsum
|
|||
tar czfv device-tests.tar.gz -C $(BIN_DIR)/tests .
|
||||
|
||||
frontend:
|
||||
cd ui && npm ci && npm run build:device
|
||||
cd ui && npm ci && npm run build:device && \
|
||||
find ../static/ \
|
||||
-type f \
|
||||
\( -name '*.js' \
|
||||
-o -name '*.css' \
|
||||
-o -name '*.html' \
|
||||
-o -name '*.ico' \
|
||||
-o -name '*.png' \
|
||||
-o -name '*.jpg' \
|
||||
-o -name '*.jpeg' \
|
||||
-o -name '*.gif' \
|
||||
-o -name '*.svg' \
|
||||
-o -name '*.webp' \
|
||||
-o -name '*.woff2' \
|
||||
\) \
|
||||
-exec sh -c 'gzip -9 -kfv {}' \;
|
||||
|
||||
dev_release: frontend build_dev
|
||||
@echo "Uploading release..."
|
||||
@echo "Uploading release... $(VERSION_DEV)"
|
||||
@shasum -a 256 bin/jetkvm_app | cut -d ' ' -f 1 > bin/jetkvm_app.sha256
|
||||
rclone copyto bin/jetkvm_app r2://jetkvm-update/app/$(VERSION_DEV)/jetkvm_app
|
||||
rclone copyto bin/jetkvm_app.sha256 r2://jetkvm-update/app/$(VERSION_DEV)/jetkvm_app.sha256
|
||||
|
|
@ -86,4 +122,4 @@ release:
|
|||
@echo "Uploading release..."
|
||||
@shasum -a 256 bin/jetkvm_app | cut -d ' ' -f 1 > bin/jetkvm_app.sha256
|
||||
rclone copyto bin/jetkvm_app r2://jetkvm-update/app/$(VERSION)/jetkvm_app
|
||||
rclone copyto bin/jetkvm_app.sha256 r2://jetkvm-update/app/$(VERSION)/jetkvm_app.sha256
|
||||
rclone copyto bin/jetkvm_app.sha256 r2://jetkvm-update/app/$(VERSION)/jetkvm_app.sha256
|
||||
|
|
@ -37,7 +37,9 @@ JetKVM is written in Go & TypeScript. with some bits and pieces written in C. An
|
|||
|
||||
The project contains two main parts, the backend software that runs on the KVM device and the frontend software that is served by the KVM device, and also the cloud.
|
||||
|
||||
For most of local device development, all you need is to use the `./dev_deploy.sh` script. It will build the frontend and backend and deploy them to the local KVM device. Run `./dev_deploy.sh --help` for more information.
|
||||
For comprehensive development information, including setup, testing, debugging, and contribution guidelines, see **[DEVELOPMENT.md](DEVELOPMENT.md)**.
|
||||
|
||||
For quick device development, use the `./dev_deploy.sh` script. It will build the frontend and backend and deploy them to the local KVM device. Run `./dev_deploy.sh --help` for more information.
|
||||
|
||||
## Backend
|
||||
|
||||
|
|
|
|||
|
|
@ -22,25 +22,12 @@ func (r remoteImageBackend) ReadAt(p []byte, off int64) (n int, err error) {
|
|||
return 0, errors.New("image not mounted")
|
||||
}
|
||||
source := currentVirtualMediaState.Source
|
||||
mountedImageSize := currentVirtualMediaState.Size
|
||||
virtualMediaStateMutex.RUnlock()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
_, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
readLen := int64(len(p))
|
||||
if off+readLen > mountedImageSize {
|
||||
readLen = mountedImageSize - off
|
||||
}
|
||||
var data []byte
|
||||
switch source {
|
||||
case WebRTC:
|
||||
data, err = webRTCDiskReader.Read(ctx, off, readLen)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
n = copy(p, data)
|
||||
return n, nil
|
||||
case HTTP:
|
||||
return httpRangeReader.ReadAt(p, off)
|
||||
default:
|
||||
|
|
|
|||
5
cloud.go
5
cloud.go
|
|
@ -170,6 +170,7 @@ func setCloudConnectionState(state CloudConnectionState) {
|
|||
|
||||
go waitCtrlAndRequestDisplayUpdate(
|
||||
previousState != state,
|
||||
"set_cloud_connection_state",
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -475,6 +476,10 @@ func handleSessionRequest(
|
|||
|
||||
cloudLogger.Info().Interface("session", session).Msg("new session accepted")
|
||||
cloudLogger.Trace().Interface("session", session).Msg("new session accepted")
|
||||
|
||||
// Cancel any ongoing keyboard macro when session changes
|
||||
cancelKeyboardMacro()
|
||||
|
||||
currentSession = session
|
||||
_ = wsjson.Write(context.Background(), c, gin.H{"type": "answer", "data": sd})
|
||||
return nil
|
||||
|
|
|
|||
18
cmd/main.go
18
cmd/main.go
|
|
@ -1,9 +1,27 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/jetkvm/kvm"
|
||||
)
|
||||
|
||||
func main() {
|
||||
versionPtr := flag.Bool("version", false, "print version and exit")
|
||||
versionJsonPtr := flag.Bool("version-json", false, "print version as json and exit")
|
||||
flag.Parse()
|
||||
|
||||
if *versionPtr || *versionJsonPtr {
|
||||
versionData, err := kvm.GetVersionData(*versionJsonPtr)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to get version data: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println(string(versionData))
|
||||
return
|
||||
}
|
||||
|
||||
kvm.Main()
|
||||
}
|
||||
|
|
|
|||
73
config.go
73
config.go
|
|
@ -4,11 +4,14 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"github.com/jetkvm/kvm/internal/logging"
|
||||
"github.com/jetkvm/kvm/internal/network"
|
||||
"github.com/jetkvm/kvm/internal/usbgadget"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
)
|
||||
|
||||
type WakeOnLanDevice struct {
|
||||
|
|
@ -80,6 +83,7 @@ type Config struct {
|
|||
CloudToken string `json:"cloud_token"`
|
||||
GoogleIdentity string `json:"google_identity"`
|
||||
JigglerEnabled bool `json:"jiggler_enabled"`
|
||||
JigglerConfig *JigglerConfig `json:"jiggler_config"`
|
||||
AutoUpdateEnabled bool `json:"auto_update_enabled"`
|
||||
IncludePreRelease bool `json:"include_pre_release"`
|
||||
HashedPassword string `json:"hashed_password"`
|
||||
|
|
@ -102,6 +106,25 @@ type Config struct {
|
|||
DefaultLogLevel string `json:"default_log_level"`
|
||||
}
|
||||
|
||||
func (c *Config) GetDisplayRotation() uint16 {
|
||||
rotationInt, err := strconv.ParseUint(c.DisplayRotation, 10, 16)
|
||||
if err != nil {
|
||||
logger.Warn().Err(err).Msg("invalid display rotation, using default")
|
||||
return 270
|
||||
}
|
||||
return uint16(rotationInt)
|
||||
}
|
||||
|
||||
func (c *Config) SetDisplayRotation(rotation string) error {
|
||||
_, err := strconv.ParseUint(rotation, 10, 16)
|
||||
if err != nil {
|
||||
logger.Warn().Err(err).Msg("invalid display rotation, using default")
|
||||
return err
|
||||
}
|
||||
c.DisplayRotation = rotation
|
||||
return nil
|
||||
}
|
||||
|
||||
const configPath = "/userdata/kvm_config.json"
|
||||
|
||||
var defaultConfig = &Config{
|
||||
|
|
@ -115,7 +138,15 @@ var defaultConfig = &Config{
|
|||
DisplayMaxBrightness: 64,
|
||||
DisplayDimAfterSec: 120, // 2 minutes
|
||||
DisplayOffAfterSec: 1800, // 30 minutes
|
||||
TLSMode: "",
|
||||
JigglerEnabled: false,
|
||||
// This is the "Standard" jiggler option in the UI
|
||||
JigglerConfig: &JigglerConfig{
|
||||
InactivityLimitSeconds: 60,
|
||||
JitterPercentage: 25,
|
||||
ScheduleCronTab: "0 * * * * *",
|
||||
Timezone: "UTC",
|
||||
},
|
||||
TLSMode: "",
|
||||
UsbConfig: &usbgadget.Config{
|
||||
VendorId: "0x1d6b", //The Linux Foundation
|
||||
ProductId: "0x0104", //Multifunction Composite Gadget
|
||||
|
|
@ -138,6 +169,21 @@ var (
|
|||
configLock = &sync.Mutex{}
|
||||
)
|
||||
|
||||
var (
|
||||
configSuccess = promauto.NewGauge(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "jetkvm_config_last_reload_successful",
|
||||
Help: "The last configuration load succeeded",
|
||||
},
|
||||
)
|
||||
configSuccessTime = promauto.NewGauge(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "jetkvm_config_last_reload_success_timestamp_seconds",
|
||||
Help: "Timestamp of last successful config load",
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
func LoadConfig() {
|
||||
configLock.Lock()
|
||||
defer configLock.Unlock()
|
||||
|
|
@ -153,6 +199,8 @@ func LoadConfig() {
|
|||
file, err := os.Open(configPath)
|
||||
if err != nil {
|
||||
logger.Debug().Msg("default config file doesn't exist, using default")
|
||||
configSuccess.Set(1.0)
|
||||
configSuccessTime.SetToCurrentTime()
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
|
@ -161,6 +209,7 @@ func LoadConfig() {
|
|||
loadedConfig := *defaultConfig
|
||||
if err := json.NewDecoder(file).Decode(&loadedConfig); err != nil {
|
||||
logger.Warn().Err(err).Msg("config file JSON parsing failed")
|
||||
configSuccess.Set(0.0)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -177,10 +226,22 @@ func LoadConfig() {
|
|||
loadedConfig.NetworkConfig = defaultConfig.NetworkConfig
|
||||
}
|
||||
|
||||
if loadedConfig.JigglerConfig == nil {
|
||||
loadedConfig.JigglerConfig = defaultConfig.JigglerConfig
|
||||
}
|
||||
|
||||
// fixup old keyboard layout value
|
||||
if loadedConfig.KeyboardLayout == "en_US" {
|
||||
loadedConfig.KeyboardLayout = "en-US"
|
||||
}
|
||||
|
||||
config = &loadedConfig
|
||||
|
||||
logging.GetRootLogger().UpdateLogLevel(config.DefaultLogLevel)
|
||||
|
||||
configSuccess.Set(1.0)
|
||||
configSuccessTime.SetToCurrentTime()
|
||||
|
||||
logger.Info().Str("path", configPath).Msg("config loaded")
|
||||
}
|
||||
|
||||
|
|
@ -190,6 +251,11 @@ func SaveConfig() error {
|
|||
|
||||
logger.Trace().Str("path", configPath).Msg("Saving config")
|
||||
|
||||
// fixup old keyboard layout value
|
||||
if config.KeyboardLayout == "en_US" {
|
||||
config.KeyboardLayout = "en-US"
|
||||
}
|
||||
|
||||
file, err := os.Create(configPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create config file: %w", err)
|
||||
|
|
@ -202,6 +268,11 @@ func SaveConfig() error {
|
|||
return fmt.Errorf("failed to encode config: %w", err)
|
||||
}
|
||||
|
||||
if err := file.Sync(); err != nil {
|
||||
return fmt.Errorf("failed to wite config: %w", err)
|
||||
}
|
||||
|
||||
logger.Info().Str("path", configPath).Msg("config saved")
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
package kvm
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
var (
|
||||
dcCurrentGauge = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "jetkvm_dc_current_amperes",
|
||||
Help: "Current DC power consumption in amperes",
|
||||
})
|
||||
|
||||
dcPowerGauge = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "jetkvm_dc_power_watts",
|
||||
Help: "DC power consumption in watts",
|
||||
})
|
||||
|
||||
dcVoltageGauge = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "jetkvm_dc_voltage_volts",
|
||||
Help: "DC voltage in volts",
|
||||
})
|
||||
|
||||
dcStateGauge = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "jetkvm_dc_power_state",
|
||||
Help: "DC power state (1 = on, 0 = off)",
|
||||
})
|
||||
|
||||
dcMetricsRegistered sync.Once
|
||||
)
|
||||
|
||||
// registerDCMetrics registers the DC power metrics with Prometheus (called once when DC control is mounted)
|
||||
func registerDCMetrics() {
|
||||
dcMetricsRegistered.Do(func() {
|
||||
prometheus.MustRegister(dcCurrentGauge)
|
||||
prometheus.MustRegister(dcPowerGauge)
|
||||
prometheus.MustRegister(dcVoltageGauge)
|
||||
prometheus.MustRegister(dcStateGauge)
|
||||
})
|
||||
}
|
||||
|
||||
// updateDCMetrics updates the Prometheus metrics with current DC power state values
|
||||
func updateDCMetrics(state DCPowerState) {
|
||||
dcCurrentGauge.Set(state.Current)
|
||||
dcPowerGauge.Set(state.Power)
|
||||
dcVoltageGauge.Set(state.Voltage)
|
||||
if state.IsOn {
|
||||
dcStateGauge.Set(1)
|
||||
} else {
|
||||
dcStateGauge.Set(0)
|
||||
}
|
||||
}
|
||||
|
|
@ -28,6 +28,7 @@ show_help() {
|
|||
echo " --run-go-tests Run go tests"
|
||||
echo " --run-go-tests-only Run go tests and exit"
|
||||
echo " --skip-ui-build Skip frontend/UI build"
|
||||
echo " -i, --install Build for release and install the app"
|
||||
echo " --help Display this help message"
|
||||
echo
|
||||
echo "Example:"
|
||||
|
|
@ -43,6 +44,7 @@ RESET_USB_HID_DEVICE=false
|
|||
LOG_TRACE_SCOPES="${LOG_TRACE_SCOPES:-jetkvm,cloud,websocket,native,jsonrpc}"
|
||||
RUN_GO_TESTS=false
|
||||
RUN_GO_TESTS_ONLY=false
|
||||
INSTALL_APP=false
|
||||
|
||||
# Parse command line arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
|
|
@ -72,6 +74,10 @@ while [[ $# -gt 0 ]]; do
|
|||
RUN_GO_TESTS=true
|
||||
shift
|
||||
;;
|
||||
-i|--install)
|
||||
INSTALL_APP=true
|
||||
shift
|
||||
;;
|
||||
--help)
|
||||
show_help
|
||||
exit 0
|
||||
|
|
@ -139,25 +145,36 @@ EOF
|
|||
fi
|
||||
fi
|
||||
|
||||
msg_info "▶ Building go binary"
|
||||
make build_dev
|
||||
|
||||
# Kill any existing instances of the application
|
||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" "killall jetkvm_app_debug || true"
|
||||
|
||||
# Copy the binary to the remote host
|
||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" "cat > ${REMOTE_PATH}/jetkvm_app_debug" < bin/jetkvm_app
|
||||
|
||||
if [ "$RESET_USB_HID_DEVICE" = true ]; then
|
||||
msg_info "▶ Resetting USB HID device"
|
||||
msg_warn "The option has been deprecated and will be removed in a future version, as JetKVM will now reset USB gadget configuration when needed"
|
||||
# Remove the old USB gadget configuration
|
||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" "rm -rf /sys/kernel/config/usb_gadget/jetkvm/configs/c.1/hid.usb*"
|
||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" "ls /sys/class/udc > /sys/kernel/config/usb_gadget/jetkvm/UDC"
|
||||
fi
|
||||
|
||||
# Deploy and run the application on the remote host
|
||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" ash << EOF
|
||||
if [ "$INSTALL_APP" = true ]
|
||||
then
|
||||
msg_info "▶ Building release binary"
|
||||
make build_release
|
||||
|
||||
# Copy the binary to the remote host as if we were the OTA updater.
|
||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" "cat > /userdata/jetkvm/jetkvm_app.update" < bin/jetkvm_app
|
||||
|
||||
# Reboot the device, the new app will be deployed by the startup process.
|
||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" "reboot"
|
||||
else
|
||||
msg_info "▶ Building development binary"
|
||||
make build_dev
|
||||
|
||||
# Kill any existing instances of the application
|
||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" "killall jetkvm_app_debug || true"
|
||||
|
||||
# Copy the binary to the remote host
|
||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" "cat > ${REMOTE_PATH}/jetkvm_app_debug" < bin/jetkvm_app
|
||||
|
||||
if [ "$RESET_USB_HID_DEVICE" = true ]; then
|
||||
msg_info "▶ Resetting USB HID device"
|
||||
msg_warn "The option has been deprecated and will be removed in a future version, as JetKVM will now reset USB gadget configuration when needed"
|
||||
# Remove the old USB gadget configuration
|
||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" "rm -rf /sys/kernel/config/usb_gadget/jetkvm/configs/c.1/hid.usb*"
|
||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" "ls /sys/class/udc > /sys/kernel/config/usb_gadget/jetkvm/UDC"
|
||||
fi
|
||||
|
||||
# Deploy and run the application on the remote host
|
||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" ash << EOF
|
||||
set -e
|
||||
|
||||
# Set the library path to include the directory where librockit.so is located
|
||||
|
|
@ -174,7 +191,8 @@ cd "${REMOTE_PATH}"
|
|||
chmod +x jetkvm_app_debug
|
||||
|
||||
# Run the application in the background
|
||||
PION_LOG_TRACE=${LOG_TRACE_SCOPES} GODEBUG=netdns=1 ./jetkvm_app_debug
|
||||
PION_LOG_TRACE=${LOG_TRACE_SCOPES} ./jetkvm_app_debug | tee -a /tmp/jetkvm_app_debug.log
|
||||
EOF
|
||||
fi
|
||||
|
||||
echo "Deployment complete."
|
||||
echo "Deployment complete."
|
||||
|
|
|
|||
302
display.go
302
display.go
|
|
@ -1,16 +1,22 @@
|
|||
package kvm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/common/version"
|
||||
)
|
||||
|
||||
var currentScreen = "ui_Boot_Screen"
|
||||
var backlightState = 0 // 0 - NORMAL, 1 - DIMMED, 2 - OFF
|
||||
var (
|
||||
currentScreen = "boot_screen"
|
||||
backlightState = 0 // 0 - NORMAL, 1 - DIMMED, 2 - OFF
|
||||
)
|
||||
|
||||
var (
|
||||
dimTicker *time.Ticker
|
||||
|
|
@ -22,159 +28,124 @@ const (
|
|||
backlightControlClass string = "/sys/class/backlight/backlight/brightness"
|
||||
)
|
||||
|
||||
func switchToScreen(screen string) {
|
||||
_, err := CallCtrlAction("lv_scr_load", map[string]interface{}{"obj": screen})
|
||||
if err != nil {
|
||||
displayLogger.Warn().Err(err).Str("screen", screen).Msg("failed to switch to screen")
|
||||
return
|
||||
}
|
||||
currentScreen = screen
|
||||
}
|
||||
|
||||
var displayedTexts = make(map[string]string)
|
||||
|
||||
func lvObjSetState(objName string, state string) (*CtrlResponse, error) {
|
||||
return CallCtrlAction("lv_obj_set_state", map[string]interface{}{"obj": objName, "state": state})
|
||||
}
|
||||
|
||||
func lvObjAddFlag(objName string, flag string) (*CtrlResponse, error) {
|
||||
return CallCtrlAction("lv_obj_add_flag", map[string]interface{}{"obj": objName, "flag": flag})
|
||||
}
|
||||
|
||||
func lvObjClearFlag(objName string, flag string) (*CtrlResponse, error) {
|
||||
return CallCtrlAction("lv_obj_clear_flag", map[string]interface{}{"obj": objName, "flag": flag})
|
||||
}
|
||||
|
||||
func lvObjHide(objName string) (*CtrlResponse, error) {
|
||||
return lvObjAddFlag(objName, "LV_OBJ_FLAG_HIDDEN")
|
||||
}
|
||||
|
||||
func lvObjShow(objName string) (*CtrlResponse, error) {
|
||||
return lvObjClearFlag(objName, "LV_OBJ_FLAG_HIDDEN")
|
||||
}
|
||||
|
||||
func lvObjSetOpacity(objName string, opacity int) (*CtrlResponse, error) { // nolint:unused
|
||||
return CallCtrlAction("lv_obj_set_style_opa_layered", map[string]interface{}{"obj": objName, "opa": opacity})
|
||||
}
|
||||
|
||||
func lvObjFadeIn(objName string, duration uint32) (*CtrlResponse, error) {
|
||||
return CallCtrlAction("lv_obj_fade_in", map[string]interface{}{"obj": objName, "time": duration})
|
||||
}
|
||||
|
||||
func lvObjFadeOut(objName string, duration uint32) (*CtrlResponse, error) {
|
||||
return CallCtrlAction("lv_obj_fade_out", map[string]interface{}{"obj": objName, "time": duration})
|
||||
}
|
||||
|
||||
func lvLabelSetText(objName string, text string) (*CtrlResponse, error) {
|
||||
return CallCtrlAction("lv_label_set_text", map[string]interface{}{"obj": objName, "text": text})
|
||||
}
|
||||
|
||||
func lvImgSetSrc(objName string, src string) (*CtrlResponse, error) {
|
||||
return CallCtrlAction("lv_img_set_src", map[string]interface{}{"obj": objName, "src": src})
|
||||
}
|
||||
|
||||
func lvDispSetRotation(rotation string) (*CtrlResponse, error) {
|
||||
return CallCtrlAction("lv_disp_set_rotation", map[string]interface{}{"rotation": rotation})
|
||||
}
|
||||
|
||||
func updateLabelIfChanged(objName string, newText string) {
|
||||
if newText != "" && newText != displayedTexts[objName] {
|
||||
_, _ = lvLabelSetText(objName, newText)
|
||||
displayedTexts[objName] = newText
|
||||
}
|
||||
}
|
||||
|
||||
func switchToScreenIfDifferent(screenName string) {
|
||||
if currentScreen != screenName {
|
||||
displayLogger.Info().Str("from", currentScreen).Str("to", screenName).Msg("switching screen")
|
||||
switchToScreen(screenName)
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
cloudBlinkLock sync.Mutex = sync.Mutex{}
|
||||
cloudBlinkStopped bool
|
||||
cloudBlinkTicker *time.Ticker
|
||||
)
|
||||
|
||||
func updateDisplay() {
|
||||
updateLabelIfChanged("ui_Home_Content_Ip", networkState.IPv4String())
|
||||
nativeInstance.UpdateLabelIfChanged("home_info_ipv4_addr", networkState.IPv4String())
|
||||
nativeInstance.UpdateLabelAndChangeVisibility("home_info_ipv6_addr", networkState.IPv6String())
|
||||
|
||||
nativeInstance.UIObjHide("menu_btn_network")
|
||||
nativeInstance.UIObjHide("menu_btn_access")
|
||||
|
||||
nativeInstance.UpdateLabelIfChanged("home_info_mac_addr", networkState.MACString())
|
||||
|
||||
if usbState == "configured" {
|
||||
updateLabelIfChanged("ui_Home_Footer_Usb_Status_Label", "Connected")
|
||||
_, _ = lvObjSetState("ui_Home_Footer_Usb_Status_Label", "LV_STATE_DEFAULT")
|
||||
nativeInstance.UpdateLabelIfChanged("usb_status_label", "Connected")
|
||||
_, _ = nativeInstance.UIObjSetState("usb_status", "LV_STATE_DEFAULT")
|
||||
} else {
|
||||
updateLabelIfChanged("ui_Home_Footer_Usb_Status_Label", "Disconnected")
|
||||
_, _ = lvObjSetState("ui_Home_Footer_Usb_Status_Label", "LV_STATE_USER_2")
|
||||
nativeInstance.UpdateLabelIfChanged("usb_status_label", "Disconnected")
|
||||
_, _ = nativeInstance.UIObjSetState("usb_status", "LV_STATE_DISABLED")
|
||||
}
|
||||
if lastVideoState.Ready {
|
||||
updateLabelIfChanged("ui_Home_Footer_Hdmi_Status_Label", "Connected")
|
||||
_, _ = lvObjSetState("ui_Home_Footer_Hdmi_Status_Label", "LV_STATE_DEFAULT")
|
||||
nativeInstance.UpdateLabelIfChanged("hdmi_status_label", "Connected")
|
||||
_, _ = nativeInstance.UIObjSetState("hdmi_status", "LV_STATE_DEFAULT")
|
||||
} else {
|
||||
updateLabelIfChanged("ui_Home_Footer_Hdmi_Status_Label", "Disconnected")
|
||||
_, _ = lvObjSetState("ui_Home_Footer_Hdmi_Status_Label", "LV_STATE_USER_2")
|
||||
nativeInstance.UpdateLabelIfChanged("hdmi_status_label", "Disconnected")
|
||||
_, _ = nativeInstance.UIObjSetState("hdmi_status", "LV_STATE_DISABLED")
|
||||
}
|
||||
updateLabelIfChanged("ui_Home_Header_Cloud_Status_Label", fmt.Sprintf("%d active", actionSessions))
|
||||
nativeInstance.UpdateLabelIfChanged("cloud_status_label", fmt.Sprintf("%d active", actionSessions))
|
||||
|
||||
if networkState.IsUp() {
|
||||
switchToScreenIfDifferent("ui_Home_Screen")
|
||||
nativeInstance.SwitchToScreenIf("home_screen", []string{"no_network_screen", "boot_screen"})
|
||||
} else {
|
||||
switchToScreenIfDifferent("ui_No_Network_Screen")
|
||||
nativeInstance.SwitchToScreenIf("no_network_screen", []string{"home_screen", "boot_screen"})
|
||||
}
|
||||
|
||||
if cloudConnectionState == CloudConnectionStateNotConfigured {
|
||||
_, _ = lvObjHide("ui_Home_Header_Cloud_Status_Icon")
|
||||
_, _ = nativeInstance.UIObjHide("cloud_status_icon")
|
||||
} else {
|
||||
_, _ = lvObjShow("ui_Home_Header_Cloud_Status_Icon")
|
||||
_, _ = nativeInstance.UIObjShow("cloud_status_icon")
|
||||
}
|
||||
|
||||
switch cloudConnectionState {
|
||||
case CloudConnectionStateDisconnected:
|
||||
_, _ = lvImgSetSrc("ui_Home_Header_Cloud_Status_Icon", "cloud_disconnected.png")
|
||||
_, _ = nativeInstance.UIObjSetImageSrc("cloud_status_icon", "cloud_disconnected")
|
||||
stopCloudBlink()
|
||||
case CloudConnectionStateConnecting:
|
||||
_, _ = lvImgSetSrc("ui_Home_Header_Cloud_Status_Icon", "cloud.png")
|
||||
_, _ = nativeInstance.UIObjSetImageSrc("cloud_status_icon", "cloud")
|
||||
startCloudBlink()
|
||||
case CloudConnectionStateConnected:
|
||||
_, _ = lvImgSetSrc("ui_Home_Header_Cloud_Status_Icon", "cloud.png")
|
||||
_, _ = nativeInstance.UIObjSetImageSrc("cloud_status_icon", "cloud")
|
||||
stopCloudBlink()
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
cloudBlinkInterval = 2 * time.Second
|
||||
cloudBlinkDuration = 1 * time.Second
|
||||
)
|
||||
|
||||
var (
|
||||
cloudBlinkTicker *time.Ticker
|
||||
cloudBlinkCancel context.CancelFunc
|
||||
cloudBlinkLock = sync.Mutex{}
|
||||
)
|
||||
|
||||
func doCloudBlink(ctx context.Context) {
|
||||
for range cloudBlinkTicker.C {
|
||||
if cloudConnectionState != CloudConnectionStateConnecting {
|
||||
continue
|
||||
}
|
||||
|
||||
_, _ = nativeInstance.UIObjFadeOut("ui_Home_Header_Cloud_Status_Icon", uint32(cloudBlinkDuration.Milliseconds()))
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(cloudBlinkDuration):
|
||||
}
|
||||
|
||||
_, _ = nativeInstance.UIObjFadeIn("ui_Home_Header_Cloud_Status_Icon", uint32(cloudBlinkDuration.Milliseconds()))
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(cloudBlinkDuration):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func restartCloudBlink() {
|
||||
stopCloudBlink()
|
||||
startCloudBlink()
|
||||
}
|
||||
|
||||
func startCloudBlink() {
|
||||
if cloudBlinkTicker == nil {
|
||||
cloudBlinkTicker = time.NewTicker(2 * time.Second)
|
||||
} else {
|
||||
// do nothing if the blink isn't stopped
|
||||
if cloudBlinkStopped {
|
||||
cloudBlinkLock.Lock()
|
||||
defer cloudBlinkLock.Unlock()
|
||||
cloudBlinkLock.Lock()
|
||||
defer cloudBlinkLock.Unlock()
|
||||
|
||||
cloudBlinkStopped = false
|
||||
cloudBlinkTicker.Reset(2 * time.Second)
|
||||
}
|
||||
if cloudBlinkTicker == nil {
|
||||
cloudBlinkTicker = time.NewTicker(cloudBlinkInterval)
|
||||
} else {
|
||||
cloudBlinkTicker.Reset(cloudBlinkInterval)
|
||||
}
|
||||
|
||||
go func() {
|
||||
for range cloudBlinkTicker.C {
|
||||
if cloudConnectionState != CloudConnectionStateConnecting {
|
||||
continue
|
||||
}
|
||||
_, _ = lvObjFadeOut("ui_Home_Header_Cloud_Status_Icon", 1000)
|
||||
time.Sleep(1000 * time.Millisecond)
|
||||
_, _ = lvObjFadeIn("ui_Home_Header_Cloud_Status_Icon", 1000)
|
||||
time.Sleep(1000 * time.Millisecond)
|
||||
}
|
||||
}()
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cloudBlinkCancel = cancel
|
||||
|
||||
go doCloudBlink(ctx)
|
||||
}
|
||||
|
||||
func stopCloudBlink() {
|
||||
cloudBlinkLock.Lock()
|
||||
defer cloudBlinkLock.Unlock()
|
||||
|
||||
if cloudBlinkCancel != nil {
|
||||
cloudBlinkCancel()
|
||||
cloudBlinkCancel = nil
|
||||
}
|
||||
|
||||
if cloudBlinkTicker != nil {
|
||||
cloudBlinkTicker.Stop()
|
||||
}
|
||||
|
||||
cloudBlinkLock.Lock()
|
||||
defer cloudBlinkLock.Unlock()
|
||||
cloudBlinkStopped = true
|
||||
}
|
||||
|
||||
var (
|
||||
|
|
@ -183,7 +154,7 @@ var (
|
|||
waitDisplayUpdate = sync.Mutex{}
|
||||
)
|
||||
|
||||
func requestDisplayUpdate(shouldWakeDisplay bool) {
|
||||
func requestDisplayUpdate(shouldWakeDisplay bool, reason string) {
|
||||
displayUpdateLock.Lock()
|
||||
defer displayUpdateLock.Unlock()
|
||||
|
||||
|
|
@ -193,7 +164,7 @@ func requestDisplayUpdate(shouldWakeDisplay bool) {
|
|||
}
|
||||
go func() {
|
||||
if shouldWakeDisplay {
|
||||
wakeDisplay(false)
|
||||
wakeDisplay(false, reason)
|
||||
}
|
||||
displayLogger.Debug().Msg("display updating")
|
||||
//TODO: only run once regardless how many pending updates
|
||||
|
|
@ -201,29 +172,47 @@ func requestDisplayUpdate(shouldWakeDisplay bool) {
|
|||
}()
|
||||
}
|
||||
|
||||
func waitCtrlAndRequestDisplayUpdate(shouldWakeDisplay bool) {
|
||||
func waitCtrlAndRequestDisplayUpdate(shouldWakeDisplay bool, reason string) {
|
||||
waitDisplayUpdate.Lock()
|
||||
defer waitDisplayUpdate.Unlock()
|
||||
|
||||
waitCtrlClientConnected()
|
||||
requestDisplayUpdate(shouldWakeDisplay)
|
||||
// nativeInstance.WaitCtrlClientConnected()
|
||||
requestDisplayUpdate(shouldWakeDisplay, reason)
|
||||
}
|
||||
|
||||
func updateStaticContents() {
|
||||
//contents that never change
|
||||
updateLabelIfChanged("ui_Home_Content_Mac", networkState.MACString())
|
||||
systemVersion, appVersion, err := GetLocalVersion()
|
||||
if err == nil {
|
||||
updateLabelIfChanged("ui_About_Content_Operating_System_Version_ContentLabel", systemVersion.String())
|
||||
updateLabelIfChanged("ui_About_Content_App_Version_Content_Label", appVersion.String())
|
||||
nativeInstance.UpdateLabelIfChanged("home_info_mac_addr", networkState.MACString())
|
||||
|
||||
// get cpu info
|
||||
cpuInfo, err := os.ReadFile("/proc/cpuinfo")
|
||||
// get the line starting with "Serial"
|
||||
for _, line := range strings.Split(string(cpuInfo), "\n") {
|
||||
if strings.HasPrefix(line, "Serial") {
|
||||
serial := strings.SplitN(line, ":", 2)[1]
|
||||
nativeInstance.UpdateLabelAndChangeVisibility("cpu_serial", strings.TrimSpace(serial))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
updateLabelIfChanged("ui_Status_Content_Device_Id_Content_Label", GetDeviceID())
|
||||
// get kernel version
|
||||
kernelVersion, err := os.ReadFile("/proc/version")
|
||||
if err == nil {
|
||||
kernelVersion := strings.TrimPrefix(string(kernelVersion), "Linux version ")
|
||||
kernelVersion = strings.SplitN(kernelVersion, " ", 2)[0]
|
||||
nativeInstance.UpdateLabelAndChangeVisibility("kernel_version", kernelVersion)
|
||||
}
|
||||
|
||||
nativeInstance.UpdateLabelAndChangeVisibility("build_branch", version.Branch)
|
||||
nativeInstance.UpdateLabelAndChangeVisibility("build_date", version.BuildDate)
|
||||
nativeInstance.UpdateLabelAndChangeVisibility("golang_version", version.GoVersion)
|
||||
|
||||
// nativeInstance.UpdateLabelAndChangeVisibility("boot_screen_device_id", GetDeviceID())
|
||||
}
|
||||
|
||||
// setDisplayBrightness sets /sys/class/backlight/backlight/brightness to alter
|
||||
// the backlight brightness of the JetKVM hardware's display.
|
||||
func setDisplayBrightness(brightness int) error {
|
||||
func setDisplayBrightness(brightness int, reason string) error {
|
||||
// NOTE: The actual maximum value for this is 255, but out-of-the-box, the value is set to 64.
|
||||
// The maximum set here is set to 100 to reduce the risk of drawing too much power (and besides, 255 is very bright!).
|
||||
if brightness > 100 || brightness < 0 {
|
||||
|
|
@ -242,14 +231,14 @@ func setDisplayBrightness(brightness int) error {
|
|||
return err
|
||||
}
|
||||
|
||||
displayLogger.Info().Int("brightness", brightness).Msg("set brightness")
|
||||
displayLogger.Info().Int("brightness", brightness).Str("reason", reason).Msg("set brightness")
|
||||
return nil
|
||||
}
|
||||
|
||||
// tick_displayDim() is called when when dim ticker expires, it simply reduces the brightness
|
||||
// of the display by half of the max brightness.
|
||||
func tick_displayDim() {
|
||||
err := setDisplayBrightness(config.DisplayMaxBrightness / 2)
|
||||
err := setDisplayBrightness(config.DisplayMaxBrightness/2, "tick_display_dim")
|
||||
if err != nil {
|
||||
displayLogger.Warn().Err(err).Msg("failed to dim display")
|
||||
}
|
||||
|
|
@ -262,7 +251,7 @@ func tick_displayDim() {
|
|||
// tick_displayOff() is called when the off ticker expires, it turns off the display
|
||||
// by setting the brightness to zero.
|
||||
func tick_displayOff() {
|
||||
err := setDisplayBrightness(0)
|
||||
err := setDisplayBrightness(0, "tick_display_off")
|
||||
if err != nil {
|
||||
displayLogger.Warn().Err(err).Msg("failed to turn off display")
|
||||
}
|
||||
|
|
@ -275,7 +264,7 @@ func tick_displayOff() {
|
|||
// wakeDisplay sets the display brightness back to config.DisplayMaxBrightness and stores the time the display
|
||||
// last woke, ready for displayTimeoutTick to put the display back in the dim/off states.
|
||||
// Set force to true to skip the backlight state check, this should be done if altering the tickers.
|
||||
func wakeDisplay(force bool) {
|
||||
func wakeDisplay(force bool, reason string) {
|
||||
if backlightState == 0 && !force {
|
||||
return
|
||||
}
|
||||
|
|
@ -285,7 +274,11 @@ func wakeDisplay(force bool) {
|
|||
return
|
||||
}
|
||||
|
||||
err := setDisplayBrightness(config.DisplayMaxBrightness)
|
||||
if reason == "" {
|
||||
reason = "wake_display"
|
||||
}
|
||||
|
||||
err := setDisplayBrightness(config.DisplayMaxBrightness, reason)
|
||||
if err != nil {
|
||||
displayLogger.Warn().Err(err).Msg("failed to wake display")
|
||||
}
|
||||
|
|
@ -300,34 +293,6 @@ func wakeDisplay(force bool) {
|
|||
backlightState = 0
|
||||
}
|
||||
|
||||
// watchTsEvents monitors the touchscreen for events and simply calls wakeDisplay() to ensure the
|
||||
// touchscreen interface still works even with LCD dimming/off.
|
||||
// TODO: This is quite a hack, really we should be getting an event from jetkvm_native, or the whole display backlight
|
||||
// control should be hoisted up to jetkvm_native.
|
||||
func watchTsEvents() {
|
||||
ts, err := os.OpenFile(touchscreenDevice, os.O_RDONLY, 0666)
|
||||
if err != nil {
|
||||
displayLogger.Warn().Err(err).Msg("failed to open touchscreen device")
|
||||
return
|
||||
}
|
||||
|
||||
defer ts.Close()
|
||||
|
||||
// This buffer is set to 24 bytes as that's the normal size of events on /dev/input
|
||||
// Reference: https://www.kernel.org/doc/Documentation/input/input.txt
|
||||
// This could potentially be set higher, to require multiple events to wake the display.
|
||||
buf := make([]byte, 24)
|
||||
for {
|
||||
_, err := ts.Read(buf)
|
||||
if err != nil {
|
||||
displayLogger.Warn().Err(err).Msg("failed to read from touchscreen device")
|
||||
return
|
||||
}
|
||||
|
||||
wakeDisplay(false)
|
||||
}
|
||||
}
|
||||
|
||||
// startBacklightTickers starts the two tickers for dimming and switching off the display
|
||||
// if they're not already set. This is done separately to the init routine as the "never dim"
|
||||
// option has the value set to zero, but time.NewTicker only accept positive values.
|
||||
|
|
@ -335,7 +300,7 @@ func startBacklightTickers() {
|
|||
// Don't start the tickers if the display is switched off.
|
||||
// Set the display to off if that's the case.
|
||||
if config.DisplayMaxBrightness == 0 {
|
||||
_ = setDisplayBrightness(0)
|
||||
_ = setDisplayBrightness(0, "display_disabled")
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -379,17 +344,12 @@ func startBacklightTickers() {
|
|||
|
||||
func initDisplay() {
|
||||
go func() {
|
||||
waitCtrlClientConnected()
|
||||
displayLogger.Info().Msg("setting initial display contents")
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
_, _ = lvDispSetRotation(config.DisplayRotation)
|
||||
updateStaticContents()
|
||||
displayInited = true
|
||||
displayLogger.Info().Msg("display inited")
|
||||
startBacklightTickers()
|
||||
wakeDisplay(true)
|
||||
requestDisplayUpdate(true)
|
||||
requestDisplayUpdate(true, "init_display")
|
||||
}()
|
||||
|
||||
go watchTsEvents()
|
||||
}
|
||||
|
|
|
|||
114
fuse.go
114
fuse.go
|
|
@ -1,114 +0,0 @@
|
|||
package kvm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"github.com/hanwen/go-fuse/v2/fs"
|
||||
"github.com/hanwen/go-fuse/v2/fuse"
|
||||
)
|
||||
|
||||
type WebRTCStreamFile struct {
|
||||
fs.Inode
|
||||
mu sync.Mutex
|
||||
Attr fuse.Attr
|
||||
size uint64
|
||||
}
|
||||
|
||||
var _ = (fs.NodeOpener)((*WebRTCStreamFile)(nil))
|
||||
var _ = (fs.NodeOpener)((*WebRTCStreamFile)(nil))
|
||||
var _ = (fs.NodeOpener)((*WebRTCStreamFile)(nil))
|
||||
var _ = (fs.NodeOpener)((*WebRTCStreamFile)(nil))
|
||||
var _ = (fs.NodeOpener)((*WebRTCStreamFile)(nil))
|
||||
|
||||
func (f *WebRTCStreamFile) Open(ctx context.Context, flags uint32) (fh fs.FileHandle, fuseFlags uint32, errno syscall.Errno) {
|
||||
return nil, fuse.FOPEN_KEEP_CACHE, fs.OK
|
||||
}
|
||||
|
||||
func (f *WebRTCStreamFile) Write(ctx context.Context, fh fs.FileHandle, data []byte, off int64) (uint32, syscall.Errno) {
|
||||
return 0, syscall.EROFS
|
||||
}
|
||||
|
||||
var _ = (fs.NodeGetattrer)((*WebRTCStreamFile)(nil))
|
||||
|
||||
func (f *WebRTCStreamFile) Getattr(ctx context.Context, fh fs.FileHandle, out *fuse.AttrOut) syscall.Errno {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
out.Attr = f.Attr
|
||||
out.Size = f.size
|
||||
return fs.OK
|
||||
}
|
||||
|
||||
func (f *WebRTCStreamFile) Setattr(ctx context.Context, fh fs.FileHandle, in *fuse.SetAttrIn, out *fuse.AttrOut) syscall.Errno {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
out.Attr = f.Attr
|
||||
return fs.OK
|
||||
}
|
||||
|
||||
func (f *WebRTCStreamFile) Flush(ctx context.Context, fh fs.FileHandle) syscall.Errno {
|
||||
return fs.OK
|
||||
}
|
||||
|
||||
type DiskReadRequest struct {
|
||||
Start uint64 `json:"start"`
|
||||
End uint64 `json:"end"`
|
||||
}
|
||||
|
||||
var diskReadChan = make(chan []byte, 1)
|
||||
|
||||
func (f *WebRTCStreamFile) Read(ctx context.Context, fh fs.FileHandle, dest []byte, off int64) (fuse.ReadResult, syscall.Errno) {
|
||||
buf, err := webRTCDiskReader.Read(ctx, off, int64(len(dest)))
|
||||
if err != nil {
|
||||
return nil, syscall.EIO
|
||||
}
|
||||
return fuse.ReadResultData(buf), fs.OK
|
||||
}
|
||||
|
||||
func (f *WebRTCStreamFile) SetSize(size uint64) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
f.size = size
|
||||
}
|
||||
|
||||
type FuseRoot struct {
|
||||
fs.Inode
|
||||
}
|
||||
|
||||
var webRTCStreamFile = &WebRTCStreamFile{}
|
||||
|
||||
func (r *FuseRoot) OnAdd(ctx context.Context) {
|
||||
ch := r.NewPersistentInode(ctx, webRTCStreamFile, fs.StableAttr{Ino: 2})
|
||||
r.AddChild("disk", ch, false)
|
||||
}
|
||||
|
||||
func (r *FuseRoot) Getattr(ctx context.Context, fh fs.FileHandle, out *fuse.AttrOut) syscall.Errno {
|
||||
out.Mode = 0755
|
||||
return 0
|
||||
}
|
||||
|
||||
var _ = (fs.NodeGetattrer)((*FuseRoot)(nil))
|
||||
var _ = (fs.NodeOnAdder)((*FuseRoot)(nil))
|
||||
|
||||
const fuseMountPoint = "/mnt/webrtc"
|
||||
|
||||
var fuseServer *fuse.Server
|
||||
|
||||
func RunFuseServer() {
|
||||
opts := &fs.Options{}
|
||||
opts.DirectMountStrict = true
|
||||
_ = os.Mkdir(fuseMountPoint, 0755)
|
||||
var err error
|
||||
fuseServer, err = fs.Mount(fuseMountPoint, &FuseRoot{}, opts)
|
||||
if err != nil {
|
||||
logger.Warn().Err(err).Msg("failed to mount fuse")
|
||||
}
|
||||
fuseServer.Wait()
|
||||
}
|
||||
|
||||
type WebRTCImage struct {
|
||||
Size uint64 `json:"size"`
|
||||
Filename string `json:"filename"`
|
||||
}
|
||||
77
go.mod
77
go.mod
|
|
@ -1,45 +1,47 @@
|
|||
module github.com/jetkvm/kvm
|
||||
|
||||
go 1.23.4
|
||||
|
||||
toolchain go1.24.3
|
||||
go 1.24.4
|
||||
|
||||
require (
|
||||
github.com/Masterminds/semver/v3 v3.3.1
|
||||
github.com/Masterminds/semver/v3 v3.4.0
|
||||
github.com/beevik/ntp v1.4.3
|
||||
github.com/coder/websocket v1.8.13
|
||||
github.com/coreos/go-oidc/v3 v3.11.0
|
||||
github.com/creack/pty v1.1.23
|
||||
github.com/coreos/go-oidc/v3 v3.15.0
|
||||
github.com/creack/pty v1.1.24
|
||||
github.com/fsnotify/fsnotify v1.9.0
|
||||
github.com/gin-contrib/logger v1.2.6
|
||||
github.com/gin-gonic/gin v1.10.1
|
||||
github.com/go-co-op/gocron/v2 v2.16.5
|
||||
github.com/google/gopacket v1.1.19
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/guregu/null/v6 v6.0.0
|
||||
github.com/gwatts/rootcerts v0.0.0-20240401182218-3ab9db955caf
|
||||
github.com/hanwen/go-fuse/v2 v2.8.0
|
||||
github.com/pion/logging v0.2.3
|
||||
github.com/gwatts/rootcerts v0.0.0-20250901182336-dc5ae18bd79f
|
||||
github.com/jellydator/ttlcache/v3 v3.4.0
|
||||
github.com/pion/logging v0.2.4
|
||||
github.com/pion/mdns/v2 v2.0.7
|
||||
github.com/pion/webrtc/v4 v4.0.16
|
||||
github.com/pion/webrtc/v4 v4.1.4
|
||||
github.com/pojntfx/go-nbd v0.3.2
|
||||
github.com/prometheus/client_golang v1.22.0
|
||||
github.com/prometheus/common v0.62.0
|
||||
github.com/prometheus/procfs v0.16.1
|
||||
github.com/prometheus/client_golang v1.23.0
|
||||
github.com/prometheus/common v0.66.0
|
||||
github.com/prometheus/procfs v0.17.0
|
||||
github.com/psanford/httpreadat v0.1.0
|
||||
github.com/rs/xid v1.6.0
|
||||
github.com/rs/zerolog v1.34.0
|
||||
github.com/sourcegraph/tf-dag v0.2.2-0.20250131204052-3e8ff1477b4f
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/vishvananda/netlink v1.3.0
|
||||
go.bug.st/serial v1.6.2
|
||||
golang.org/x/crypto v0.39.0
|
||||
golang.org/x/net v0.41.0
|
||||
golang.org/x/sys v0.33.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/vearutop/statigz v1.5.0
|
||||
github.com/vishvananda/netlink v1.3.1
|
||||
go.bug.st/serial v1.6.4
|
||||
golang.org/x/crypto v0.41.0
|
||||
golang.org/x/net v0.43.0
|
||||
golang.org/x/sys v0.35.0
|
||||
)
|
||||
|
||||
replace github.com/pojntfx/go-nbd v0.3.2 => github.com/chemhack/go-nbd v0.0.0-20241006125820-59e45f5b1e7b
|
||||
|
||||
require (
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bytedance/sonic v1.13.2 // indirect
|
||||
github.com/bytedance/sonic v1.13.3 // indirect
|
||||
github.com/bytedance/sonic/loader v0.2.4 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.5 // indirect
|
||||
|
|
@ -47,11 +49,13 @@ require (
|
|||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.0.5 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.26.0 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect
|
||||
github.com/jonboulle/clockwork v0.5.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
|
|
@ -61,30 +65,33 @@ require (
|
|||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/pilebones/go-udev v0.9.0 // indirect
|
||||
github.com/pilebones/go-udev v0.9.1 // indirect
|
||||
github.com/pion/datachannel v1.5.10 // indirect
|
||||
github.com/pion/dtls/v3 v3.0.6 // indirect
|
||||
github.com/pion/dtls/v3 v3.0.7 // indirect
|
||||
github.com/pion/ice/v4 v4.0.10 // indirect
|
||||
github.com/pion/interceptor v0.1.40 // indirect
|
||||
github.com/pion/randutil v0.1.0 // indirect
|
||||
github.com/pion/rtcp v1.2.15 // indirect
|
||||
github.com/pion/rtp v1.8.18 // indirect
|
||||
github.com/pion/rtp v1.8.22 // indirect
|
||||
github.com/pion/sctp v1.8.39 // indirect
|
||||
github.com/pion/sdp/v3 v3.0.13 // indirect
|
||||
github.com/pion/srtp/v3 v3.0.5 // indirect
|
||||
github.com/pion/sdp/v3 v3.0.16 // indirect
|
||||
github.com/pion/srtp/v3 v3.0.7 // indirect
|
||||
github.com/pion/stun/v3 v3.0.0 // indirect
|
||||
github.com/pion/transport/v3 v3.0.7 // indirect
|
||||
github.com/pion/turn/v4 v4.0.2 // indirect
|
||||
github.com/pion/turn/v4 v4.1.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/rogpeppe/go-internal v1.11.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
github.com/vishvananda/netns v0.0.4 // indirect
|
||||
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||
github.com/vishvananda/netns v0.0.5 // indirect
|
||||
github.com/wlynxg/anet v0.0.5 // indirect
|
||||
golang.org/x/arch v0.17.0 // indirect
|
||||
golang.org/x/oauth2 v0.24.0 // indirect
|
||||
golang.org/x/text v0.26.0 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
golang.org/x/arch v0.18.0 // indirect
|
||||
golang.org/x/oauth2 v0.30.0 // indirect
|
||||
golang.org/x/sync v0.16.0 // indirect
|
||||
golang.org/x/text v0.28.0 // indirect
|
||||
google.golang.org/protobuf v1.36.8 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
|
|
|||
167
go.sum
167
go.sum
|
|
@ -1,11 +1,15 @@
|
|||
github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4=
|
||||
github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
|
||||
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
||||
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
||||
github.com/beevik/ntp v1.4.3 h1:PlbTvE5NNy4QHmA4Mg57n7mcFTmr1W1j3gcK7L1lqho=
|
||||
github.com/beevik/ntp v1.4.3/go.mod h1:Unr8Zg+2dRn7d8bHFuehIMSvvUYssHMxW3Q5Nx4RW5Q=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
|
||||
github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
|
||||
github.com/bool64/dev v0.2.39 h1:kP8DnMGlWXhGYJEZE/J0l/gVBdbuhoPGL+MJG4QbofE=
|
||||
github.com/bool64/dev v0.2.39/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg=
|
||||
github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0=
|
||||
github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
|
||||
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||
|
|
@ -18,13 +22,13 @@ github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJ
|
|||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE=
|
||||
github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
||||
github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI=
|
||||
github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0=
|
||||
github.com/coreos/go-oidc/v3 v3.15.0 h1:R6Oz8Z4bqWR7VFQ+sPSvZPQv4x8M+sJkDO5ojgwlyAg=
|
||||
github.com/coreos/go-oidc/v3 v3.15.0/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0=
|
||||
github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY=
|
||||
github.com/creack/pty v1.1.23 h1:4M6+isWdcStXEf15G/RbrMPOQj1dZ7HPZCGwE4kOeP0=
|
||||
github.com/creack/pty v1.1.23/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
||||
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
|
|
@ -38,8 +42,10 @@ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w
|
|||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
|
||||
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
|
||||
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
|
||||
github.com/go-co-op/gocron/v2 v2.16.5 h1:j228Jxk7bb9CF8LKR3gS+bK3rcjRUINjlVI+ZMp26Ss=
|
||||
github.com/go-co-op/gocron/v2 v2.16.5/go.mod h1:zAfC/GFQ668qHxOVl/D68Jh5Ce7sDqX6TJnSQyRkRBc=
|
||||
github.com/go-jose/go-jose/v4 v4.1.0 h1:cYSYxd3pw5zd2FSXk2vGdn9igQU2PS8MuxrCOCl0FdY=
|
||||
github.com/go-jose/go-jose/v4 v4.1.0/go.mod h1:GG/vqmYm3Von2nYiB2vGTXzdoNKE5tix5tuc6iAd+sw=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
|
|
@ -54,14 +60,20 @@ github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5x
|
|||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
|
||||
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248=
|
||||
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk=
|
||||
github.com/guregu/null/v6 v6.0.0 h1:N14VRS+4di81i1PXRiprbQJ9EM9gqBa0+KVMeS/QSjQ=
|
||||
github.com/guregu/null/v6 v6.0.0/go.mod h1:hrMIhIfrOZeLPZhROSn149tpw2gHkidAqxoXNyeX3iQ=
|
||||
github.com/gwatts/rootcerts v0.0.0-20240401182218-3ab9db955caf h1:JO6ISZIvEUitto5zjQ3/VEnDM5rPbqIFuOhS0U0ByeA=
|
||||
github.com/gwatts/rootcerts v0.0.0-20240401182218-3ab9db955caf/go.mod h1:5Kt9XkWvkGi2OHOq0QsGxebHmhCcqJ8KCbNg/a6+n+g=
|
||||
github.com/hanwen/go-fuse/v2 v2.8.0 h1:wV8rG7rmCz8XHSOwBZhG5YcVqcYjkzivjmbaMafPlAs=
|
||||
github.com/hanwen/go-fuse/v2 v2.8.0/go.mod h1:yE6D2PqWwm3CbYRxFXV9xUd8Md5d6NG0WBs5spCswmI=
|
||||
github.com/gwatts/rootcerts v0.0.0-20250901182336-dc5ae18bd79f h1:08t2PbrkDgW2+mwCQ3jhKUBrCM9Bc9SeH5j2Dst3B+0=
|
||||
github.com/gwatts/rootcerts v0.0.0-20250901182336-dc5ae18bd79f/go.mod h1:5Kt9XkWvkGi2OHOq0QsGxebHmhCcqJ8KCbNg/a6+n+g=
|
||||
github.com/jellydator/ttlcache/v3 v3.4.0 h1:YS4P125qQS0tNhtL6aeYkheEaB/m8HCqdMMP4mnWdTY=
|
||||
github.com/jellydator/ttlcache/v3 v3.4.0/go.mod h1:Hw9EgjymziQD3yGsQdf1FqFdpp7YjFMd4Srg5EJlgD4=
|
||||
github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
|
||||
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
|
|
@ -88,8 +100,6 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
|
|||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg=
|
||||
github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
|
|
@ -99,55 +109,58 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
|
|||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pilebones/go-udev v0.9.0 h1:N1uEO/SxUwtIctc0WLU0t69JeBxIYEYnj8lT/Nabl9Q=
|
||||
github.com/pilebones/go-udev v0.9.0/go.mod h1:T2eI2tUSK0hA2WS5QLjXJUfQkluZQu+18Cqvem3CaXI=
|
||||
github.com/pilebones/go-udev v0.9.1 h1:uN72M1C1fgzhsVmBGEM8w9RD1JY4iVsPZpr+Z6rb3O8=
|
||||
github.com/pilebones/go-udev v0.9.1/go.mod h1:Bgcl07crebF3JSeS4+nuaRvhWFdCeFoBhXXeAp93XNo=
|
||||
github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o=
|
||||
github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M=
|
||||
github.com/pion/dtls/v3 v3.0.6 h1:7Hkd8WhAJNbRgq9RgdNh1aaWlZlGpYTzdqjy9x9sK2E=
|
||||
github.com/pion/dtls/v3 v3.0.6/go.mod h1:iJxNQ3Uhn1NZWOMWlLxEEHAN5yX7GyPvvKw04v9bzYU=
|
||||
github.com/pion/dtls/v3 v3.0.7 h1:bItXtTYYhZwkPFk4t1n3Kkf5TDrfj6+4wG+CZR8uI9Q=
|
||||
github.com/pion/dtls/v3 v3.0.7/go.mod h1:uDlH5VPrgOQIw59irKYkMudSFprY9IEFCqz/eTz16f8=
|
||||
github.com/pion/ice/v4 v4.0.10 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4=
|
||||
github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw=
|
||||
github.com/pion/interceptor v0.1.40 h1:e0BjnPcGpr2CFQgKhrQisBU7V3GXK6wrfYrGYaU6Jq4=
|
||||
github.com/pion/interceptor v0.1.40/go.mod h1:Z6kqH7M/FYirg3frjGJ21VLSRJGBXB/KqaTIrdqnOic=
|
||||
github.com/pion/logging v0.2.3 h1:gHuf0zpoh1GW67Nr6Gj4cv5Z9ZscU7g/EaoC/Ke/igI=
|
||||
github.com/pion/logging v0.2.3/go.mod h1:z8YfknkquMe1csOrxK5kc+5/ZPAzMxbKLX5aXpbpC90=
|
||||
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
|
||||
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
|
||||
github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM=
|
||||
github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA=
|
||||
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||
github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo=
|
||||
github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0=
|
||||
github.com/pion/rtp v1.8.18 h1:yEAb4+4a8nkPCecWzQB6V/uEU18X1lQCGAQCjP+pyvU=
|
||||
github.com/pion/rtp v1.8.18/go.mod h1:bAu2UFKScgzyFqvUKmbvzSdPr+NGbZtv6UB2hesqXBk=
|
||||
github.com/pion/rtp v1.8.22 h1:8NCVDDF+uSJmMUkjLJVnIr/HX7gPesyMV1xFt5xozXc=
|
||||
github.com/pion/rtp v1.8.22/go.mod h1:rF5nS1GqbR7H/TCpKwylzeq6yDM+MM6k+On5EgeThEM=
|
||||
github.com/pion/sctp v1.8.39 h1:PJma40vRHa3UTO3C4MyeJDQ+KIobVYRZQZ0Nt7SjQnE=
|
||||
github.com/pion/sctp v1.8.39/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE=
|
||||
github.com/pion/sdp/v3 v3.0.13 h1:uN3SS2b+QDZnWXgdr69SM8KB4EbcnPnPf2Laxhty/l4=
|
||||
github.com/pion/sdp/v3 v3.0.13/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E=
|
||||
github.com/pion/srtp/v3 v3.0.5 h1:8XLB6Dt3QXkMkRFpoqC3314BemkpMQK2mZeJc4pUKqo=
|
||||
github.com/pion/srtp/v3 v3.0.5/go.mod h1:r1G7y5r1scZRLe2QJI/is+/O83W2d+JoEsuIexpw+uM=
|
||||
github.com/pion/sdp/v3 v3.0.16 h1:0dKzYO6gTAvuLaAKQkC02eCPjMIi4NuAr/ibAwrGDCo=
|
||||
github.com/pion/sdp/v3 v3.0.16/go.mod h1:9tyKzznud3qiweZcD86kS0ff1pGYB3VX+Bcsmkx6IXo=
|
||||
github.com/pion/srtp/v3 v3.0.7 h1:QUElw0A/FUg3MP8/KNMZB3i0m8F9XeMnTum86F7S4bs=
|
||||
github.com/pion/srtp/v3 v3.0.7/go.mod h1:qvnHeqbhT7kDdB+OGB05KA/P067G3mm7XBfLaLiaNF0=
|
||||
github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw=
|
||||
github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU=
|
||||
github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
|
||||
github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
|
||||
github.com/pion/turn/v4 v4.0.2 h1:ZqgQ3+MjP32ug30xAbD6Mn+/K4Sxi3SdNOTFf+7mpps=
|
||||
github.com/pion/turn/v4 v4.0.2/go.mod h1:pMMKP/ieNAG/fN5cZiN4SDuyKsXtNTr0ccN7IToA1zs=
|
||||
github.com/pion/webrtc/v4 v4.0.16 h1:5f8QMVIbNvJr2mPRGi2QamkPa/LVUB6NWolOCwphKHA=
|
||||
github.com/pion/webrtc/v4 v4.0.16/go.mod h1:C3uTCPzVafUA0eUzru9f47OgNt3nEO7ZJ6zNY6VSJno=
|
||||
github.com/pion/turn/v4 v4.1.1 h1:9UnY2HB99tpDyz3cVVZguSxcqkJ1DsTSZ+8TGruh4fc=
|
||||
github.com/pion/turn/v4 v4.1.1/go.mod h1:2123tHk1O++vmjI5VSD0awT50NywDAq5A2NNNU4Jjs8=
|
||||
github.com/pion/webrtc/v4 v4.1.4 h1:/gK1ACGHXQmtyVVbJFQDxNoODg4eSRiFLB7t9r9pg8M=
|
||||
github.com/pion/webrtc/v4 v4.1.4/go.mod h1:Oab9npu1iZtQRMic3K3toYq5zFPvToe/QBw7dMI2ok4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
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/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
|
||||
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
|
||||
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
|
||||
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
|
||||
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
|
||||
github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc=
|
||||
github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.66.0 h1:K/rJPHrG3+AoQs50r2+0t7zMnMzek2Vbv31OFVsMeVY=
|
||||
github.com/prometheus/common v0.66.0/go.mod h1:Ux6NtV1B4LatamKE63tJBntoxD++xmtI/lK0VtEplN4=
|
||||
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
|
||||
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
|
||||
github.com/psanford/httpreadat v0.1.0 h1:VleW1HS2zO7/4c7c7zNl33fO6oYACSagjJIyMIwZLUE=
|
||||
github.com/psanford/httpreadat v0.1.0/go.mod h1:Zg7P+TlBm3bYbyHTKv/EdtSJZn3qwbPwpfZ/I9GKCRE=
|
||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||
|
|
@ -161,42 +174,64 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
|||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQdrZk=
|
||||
github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs=
|
||||
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
|
||||
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
||||
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
||||
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
github.com/vearutop/statigz v1.5.0 h1:FuWwZiT82yBw4xbWdWIawiP2XFTyEPhIo8upRxiKLqk=
|
||||
github.com/vearutop/statigz v1.5.0/go.mod h1:oHmjFf3izfCO804Di1ZjB666P3fAlVzJEx2k6jNt/Gk=
|
||||
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
|
||||
github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4=
|
||||
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
|
||||
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
||||
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
|
||||
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
||||
go.bug.st/serial v1.6.2 h1:kn9LRX3sdm+WxWKufMlIRndwGfPWsH1/9lCWXQCasq8=
|
||||
go.bug.st/serial v1.6.2/go.mod h1:UABfsluHAiaNI+La2iESysd9Vetq7VRdpxvjx7CmmOE=
|
||||
golang.org/x/arch v0.17.0 h1:4O3dfLzd+lQewptAHqjewQZQDyEdejz3VwgeYwkZneU=
|
||||
golang.org/x/arch v0.17.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
||||
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
||||
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||
golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
|
||||
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
go.bug.st/serial v1.6.4 h1:7FmqNPgVp3pu2Jz5PoPtbZ9jJO5gnEnZIvnI1lzve8A=
|
||||
go.bug.st/serial v1.6.4/go.mod h1:nofMJxTeNVny/m6+KaafC6vJGj3miwQZ6vW4BZUGJPI=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc=
|
||||
golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
|
||||
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
|
||||
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
|
|||
|
|
@ -0,0 +1,259 @@
|
|||
package kvm
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/jetkvm/kvm/internal/hidrpc"
|
||||
"github.com/jetkvm/kvm/internal/usbgadget"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func handleHidRPCMessage(message hidrpc.Message, session *Session) {
|
||||
var rpcErr error
|
||||
|
||||
switch message.Type() {
|
||||
case hidrpc.TypeHandshake:
|
||||
message, err := hidrpc.NewHandshakeMessage().Marshal()
|
||||
if err != nil {
|
||||
logger.Warn().Err(err).Msg("failed to marshal handshake message")
|
||||
return
|
||||
}
|
||||
if err := session.HidChannel.Send(message); err != nil {
|
||||
logger.Warn().Err(err).Msg("failed to send handshake message")
|
||||
return
|
||||
}
|
||||
session.hidRPCAvailable = true
|
||||
case hidrpc.TypeKeypressReport, hidrpc.TypeKeyboardReport:
|
||||
rpcErr = handleHidRPCKeyboardInput(message)
|
||||
case hidrpc.TypeKeyboardMacroReport:
|
||||
keyboardMacroReport, err := message.KeyboardMacroReport()
|
||||
if err != nil {
|
||||
logger.Warn().Err(err).Msg("failed to get keyboard macro report")
|
||||
return
|
||||
}
|
||||
rpcErr = rpcExecuteKeyboardMacro(keyboardMacroReport.Steps)
|
||||
case hidrpc.TypeCancelKeyboardMacroReport:
|
||||
rpcCancelKeyboardMacro()
|
||||
return
|
||||
case hidrpc.TypeKeypressKeepAliveReport:
|
||||
rpcErr = handleHidRPCKeypressKeepAlive(session)
|
||||
case hidrpc.TypePointerReport:
|
||||
pointerReport, err := message.PointerReport()
|
||||
if err != nil {
|
||||
logger.Warn().Err(err).Msg("failed to get pointer report")
|
||||
return
|
||||
}
|
||||
rpcErr = rpcAbsMouseReport(pointerReport.X, pointerReport.Y, pointerReport.Button)
|
||||
case hidrpc.TypeMouseReport:
|
||||
mouseReport, err := message.MouseReport()
|
||||
if err != nil {
|
||||
logger.Warn().Err(err).Msg("failed to get mouse report")
|
||||
return
|
||||
}
|
||||
rpcErr = rpcRelMouseReport(mouseReport.DX, mouseReport.DY, mouseReport.Button)
|
||||
default:
|
||||
logger.Warn().Uint8("type", uint8(message.Type())).Msg("unknown HID RPC message type")
|
||||
}
|
||||
|
||||
if rpcErr != nil {
|
||||
logger.Warn().Err(rpcErr).Msg("failed to handle HID RPC message")
|
||||
}
|
||||
}
|
||||
|
||||
func onHidMessage(msg hidQueueMessage, session *Session) {
|
||||
data := msg.Data
|
||||
|
||||
scopedLogger := hidRPCLogger.With().
|
||||
Str("channel", msg.channel).
|
||||
Bytes("data", data).
|
||||
Logger()
|
||||
scopedLogger.Debug().Msg("HID RPC message received")
|
||||
|
||||
if len(data) < 1 {
|
||||
scopedLogger.Warn().Int("length", len(data)).Msg("received empty data in HID RPC message handler")
|
||||
return
|
||||
}
|
||||
|
||||
var message hidrpc.Message
|
||||
|
||||
if err := hidrpc.Unmarshal(data, &message); err != nil {
|
||||
scopedLogger.Warn().Err(err).Msg("failed to unmarshal HID RPC message")
|
||||
return
|
||||
}
|
||||
|
||||
if scopedLogger.GetLevel() <= zerolog.DebugLevel {
|
||||
scopedLogger = scopedLogger.With().Str("descr", message.String()).Logger()
|
||||
}
|
||||
|
||||
t := time.Now()
|
||||
|
||||
r := make(chan interface{})
|
||||
go func() {
|
||||
handleHidRPCMessage(message, session)
|
||||
r <- nil
|
||||
}()
|
||||
select {
|
||||
case <-time.After(1 * time.Second):
|
||||
scopedLogger.Warn().Msg("HID RPC message timed out")
|
||||
case <-r:
|
||||
scopedLogger.Debug().Dur("duration", time.Since(t)).Msg("HID RPC message handled")
|
||||
}
|
||||
}
|
||||
|
||||
// Tunables
|
||||
// Keep in mind
|
||||
// macOS default: 15 * 15 = 225ms https://discussions.apple.com/thread/1316947?sortBy=rank
|
||||
// Linux default: 250ms https://man.archlinux.org/man/kbdrate.8.en
|
||||
// Windows default: 1s `HKEY_CURRENT_USER\Control Panel\Accessibility\Keyboard Response\AutoRepeatDelay`
|
||||
|
||||
const expectedRate = 50 * time.Millisecond // expected keepalive interval
|
||||
const maxLateness = 50 * time.Millisecond // max jitter we'll tolerate OR jitter budget
|
||||
const baseExtension = expectedRate + maxLateness // 100ms extension on perfect tick
|
||||
|
||||
const maxStaleness = 225 * time.Millisecond // discard ancient packets outright
|
||||
|
||||
func handleHidRPCKeypressKeepAlive(session *Session) error {
|
||||
session.keepAliveJitterLock.Lock()
|
||||
defer session.keepAliveJitterLock.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
|
||||
// 1) Staleness guard: ensures packets that arrive far beyond the life of a valid key hold
|
||||
// (e.g. after a network stall, retransmit burst, or machine sleep) are ignored outright.
|
||||
// This prevents “zombie” keepalives from reviving a key that should already be released.
|
||||
if !session.lastTimerResetTime.IsZero() && now.Sub(session.lastTimerResetTime) > maxStaleness {
|
||||
return nil
|
||||
}
|
||||
|
||||
validTick := true
|
||||
timerExtension := baseExtension
|
||||
|
||||
if !session.lastKeepAliveArrivalTime.IsZero() {
|
||||
timeSinceLastTick := now.Sub(session.lastKeepAliveArrivalTime)
|
||||
lateness := timeSinceLastTick - expectedRate
|
||||
|
||||
if lateness > 0 {
|
||||
if lateness <= maxLateness {
|
||||
// --- Small lateness (within jitterBudget) ---
|
||||
// This is normal jitter (e.g., Wi-Fi contention).
|
||||
// We still accept the tick, but *reduce the extension*
|
||||
// so that the total hold time stays aligned with REAL client side intent.
|
||||
timerExtension -= lateness
|
||||
} else {
|
||||
// --- Large lateness (beyond jitterBudget) ---
|
||||
// This is likely a retransmit stall or ordering delay.
|
||||
// We reject the tick entirely and DO NOT extend,
|
||||
// so the auto-release still fires on time.
|
||||
validTick = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !validTick {
|
||||
return nil
|
||||
}
|
||||
// Only valid ticks update our state and extend the timer.
|
||||
session.lastKeepAliveArrivalTime = now
|
||||
session.lastTimerResetTime = now
|
||||
if gadget != nil {
|
||||
gadget.DelayAutoReleaseWithDuration(timerExtension)
|
||||
}
|
||||
|
||||
// On a miss: do not advance any state — keeps baseline stable.
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleHidRPCKeyboardInput(message hidrpc.Message) error {
|
||||
switch message.Type() {
|
||||
case hidrpc.TypeKeypressReport:
|
||||
keypressReport, err := message.KeypressReport()
|
||||
if err != nil {
|
||||
logger.Warn().Err(err).Msg("failed to get keypress report")
|
||||
return err
|
||||
}
|
||||
return rpcKeypressReport(keypressReport.Key, keypressReport.Press)
|
||||
case hidrpc.TypeKeyboardReport:
|
||||
keyboardReport, err := message.KeyboardReport()
|
||||
if err != nil {
|
||||
logger.Warn().Err(err).Msg("failed to get keyboard report")
|
||||
return err
|
||||
}
|
||||
return rpcKeyboardReport(keyboardReport.Modifier, keyboardReport.Keys)
|
||||
}
|
||||
|
||||
return fmt.Errorf("unknown HID RPC message type: %d", message.Type())
|
||||
}
|
||||
|
||||
func reportHidRPC(params any, session *Session) {
|
||||
if session == nil {
|
||||
logger.Warn().Msg("session is nil, skipping reportHidRPC")
|
||||
return
|
||||
}
|
||||
|
||||
if !session.hidRPCAvailable || session.HidChannel == nil {
|
||||
logger.Warn().
|
||||
Bool("hidRPCAvailable", session.hidRPCAvailable).
|
||||
Bool("HidChannel", session.HidChannel != nil).
|
||||
Msg("HID RPC is not available, skipping reportHidRPC")
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
message []byte
|
||||
err error
|
||||
)
|
||||
switch params := params.(type) {
|
||||
case usbgadget.KeyboardState:
|
||||
message, err = hidrpc.NewKeyboardLedMessage(params).Marshal()
|
||||
case usbgadget.KeysDownState:
|
||||
message, err = hidrpc.NewKeydownStateMessage(params).Marshal()
|
||||
case hidrpc.KeyboardMacroState:
|
||||
message, err = hidrpc.NewKeyboardMacroStateMessage(params.State, params.IsPaste).Marshal()
|
||||
default:
|
||||
err = fmt.Errorf("unknown HID RPC message type: %T", params)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logger.Warn().Err(err).Msg("failed to marshal HID RPC message")
|
||||
return
|
||||
}
|
||||
|
||||
if message == nil {
|
||||
logger.Warn().Msg("failed to marshal HID RPC message")
|
||||
return
|
||||
}
|
||||
|
||||
if err := session.HidChannel.Send(message); err != nil {
|
||||
if errors.Is(err, io.ErrClosedPipe) {
|
||||
logger.Debug().Err(err).Msg("HID RPC channel closed, skipping reportHidRPC")
|
||||
return
|
||||
}
|
||||
logger.Warn().Err(err).Msg("failed to send HID RPC message")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Session) reportHidRPCKeyboardLedState(state usbgadget.KeyboardState) {
|
||||
if !s.hidRPCAvailable {
|
||||
writeJSONRPCEvent("keyboardLedState", state, s)
|
||||
}
|
||||
reportHidRPC(state, s)
|
||||
}
|
||||
|
||||
func (s *Session) reportHidRPCKeysDownState(state usbgadget.KeysDownState) {
|
||||
if !s.hidRPCAvailable {
|
||||
usbLogger.Debug().Interface("state", state).Msg("reporting keys down state")
|
||||
writeJSONRPCEvent("keysDownState", state, s)
|
||||
}
|
||||
usbLogger.Debug().Interface("state", state).Msg("reporting keys down state, calling reportHidRPC")
|
||||
reportHidRPC(state, s)
|
||||
}
|
||||
|
||||
func (s *Session) reportHidRPCKeyboardMacroState(state hidrpc.KeyboardMacroState) {
|
||||
if !s.hidRPCAvailable {
|
||||
writeJSONRPCEvent("keyboardMacroState", state, s)
|
||||
}
|
||||
reportHidRPC(state, s)
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package confparser
|
|||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strconv"
|
||||
|
|
@ -15,22 +16,22 @@ import (
|
|||
type FieldConfig struct {
|
||||
Name string
|
||||
Required bool
|
||||
RequiredIf map[string]interface{}
|
||||
RequiredIf map[string]any
|
||||
OneOf []string
|
||||
ValidateTypes []string
|
||||
Defaults interface{}
|
||||
Defaults any
|
||||
IsEmpty bool
|
||||
CurrentValue interface{}
|
||||
CurrentValue any
|
||||
TypeString string
|
||||
Delegated bool
|
||||
shouldUpdateValue bool
|
||||
}
|
||||
|
||||
func SetDefaultsAndValidate(config interface{}) error {
|
||||
func SetDefaultsAndValidate(config any) error {
|
||||
return setDefaultsAndValidate(config, true)
|
||||
}
|
||||
|
||||
func setDefaultsAndValidate(config interface{}, isRoot bool) error {
|
||||
func setDefaultsAndValidate(config any, isRoot bool) error {
|
||||
// first we need to check if the config is a pointer
|
||||
if reflect.TypeOf(config).Kind() != reflect.Ptr {
|
||||
return fmt.Errorf("config is not a pointer")
|
||||
|
|
@ -54,7 +55,7 @@ func setDefaultsAndValidate(config interface{}, isRoot bool) error {
|
|||
Name: field.Name,
|
||||
OneOf: splitString(field.Tag.Get("one_of")),
|
||||
ValidateTypes: splitString(field.Tag.Get("validate_type")),
|
||||
RequiredIf: make(map[string]interface{}),
|
||||
RequiredIf: make(map[string]any),
|
||||
CurrentValue: fieldValue.Interface(),
|
||||
IsEmpty: false,
|
||||
TypeString: fieldType,
|
||||
|
|
@ -141,8 +142,8 @@ func setDefaultsAndValidate(config interface{}, isRoot bool) error {
|
|||
// now check if the field has required_if
|
||||
requiredIf := field.Tag.Get("required_if")
|
||||
if requiredIf != "" {
|
||||
requiredIfParts := strings.Split(requiredIf, ",")
|
||||
for _, part := range requiredIfParts {
|
||||
requiredIfParts := strings.SplitSeq(requiredIf, ",")
|
||||
for part := range requiredIfParts {
|
||||
partVal := strings.SplitN(part, "=", 2)
|
||||
if len(partVal) != 2 {
|
||||
return fmt.Errorf("invalid required_if for field `%s`: %s", field.Name, requiredIf)
|
||||
|
|
@ -167,7 +168,7 @@ func setDefaultsAndValidate(config interface{}, isRoot bool) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func validateFields(config interface{}, fields map[string]FieldConfig) error {
|
||||
func validateFields(config any, fields map[string]FieldConfig) error {
|
||||
// now we can start to validate the fields
|
||||
for _, fieldConfig := range fields {
|
||||
if err := fieldConfig.validate(fields); err != nil {
|
||||
|
|
@ -214,7 +215,7 @@ func (f *FieldConfig) validate(fields map[string]FieldConfig) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (f *FieldConfig) populate(config interface{}) {
|
||||
func (f *FieldConfig) populate(config any) {
|
||||
// update the field if it's not empty
|
||||
if !f.shouldUpdateValue {
|
||||
return
|
||||
|
|
@ -372,6 +373,10 @@ func (f *FieldConfig) validateField() error {
|
|||
if _, err := idna.Lookup.ToASCII(val); err != nil {
|
||||
return fmt.Errorf("field `%s` is not a valid hostname: %s", f.Name, val)
|
||||
}
|
||||
case "proxy":
|
||||
if url, err := url.Parse(val); err != nil || (url.Scheme != "http" && url.Scheme != "https") || url.Host == "" {
|
||||
return fmt.Errorf("field `%s` is not a valid HTTP proxy URL: %s", f.Name, val)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("field `%s` cannot use validate_type: unsupported validator: %s", f.Name, validateType)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,13 +39,15 @@ type testNetworkConfig struct {
|
|||
IPv6Mode null.String `json:"ipv6_mode" one_of:"slaac,dhcpv6,slaac_and_dhcpv6,static,link_local,disabled" default:"slaac"`
|
||||
IPv6Static *testIPv6StaticConfig `json:"ipv6_static,omitempty" required_if:"IPv6Mode=static"`
|
||||
|
||||
LLDPMode null.String `json:"lldp_mode,omitempty" one_of:"disabled,basic,all" default:"basic"`
|
||||
LLDPMode null.String `json:"lldp_mode,omitempty" one_of:"disabled,rx_only,tx_only,enabled" default:"enabled"`
|
||||
LLDPTxTLVs []string `json:"lldp_tx_tlvs,omitempty" one_of:"chassis,port,system,vlan" default:"chassis,port,system,vlan"`
|
||||
MDNSMode null.String `json:"mdns_mode,omitempty" one_of:"disabled,auto,ipv4_only,ipv6_only" default:"auto"`
|
||||
TimeSyncMode null.String `json:"time_sync_mode,omitempty" one_of:"ntp_only,ntp_and_http,http_only,custom" default:"ntp_and_http"`
|
||||
TimeSyncOrdering []string `json:"time_sync_ordering,omitempty" one_of:"http,ntp,ntp_dhcp,ntp_user_provided,ntp_fallback" default:"ntp,http"`
|
||||
TimeSyncOrdering []string `json:"time_sync_ordering,omitempty" one_of:"http,ntp,ntp_dhcp,ntp_user_provided,http_user_provided" default:"ntp,http"`
|
||||
TimeSyncDisableFallback null.Bool `json:"time_sync_disable_fallback,omitempty" default:"false"`
|
||||
TimeSyncParallel null.Int `json:"time_sync_parallel,omitempty" default:"4"`
|
||||
TimeSyncNTPServers []string `json:"time_sync_ntp_servers,omitempty" validate_type:"ipv4_or_ipv6" required_if:"TimeSyncOrdering=ntp_user_provided"`
|
||||
TimeSyncHTTPUrls []string `json:"time_sync_http_urls,omitempty" validate_type:"url" required_if:"TimeSyncOrdering=http_user_provided"`
|
||||
}
|
||||
|
||||
func TestValidateConfig(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ func splitString(s string) []string {
|
|||
return strings.Split(s, ",")
|
||||
}
|
||||
|
||||
func toString(v interface{}) (string, error) {
|
||||
func toString(v any) (string, error) {
|
||||
switch v := v.(type) {
|
||||
case string:
|
||||
return v, nil
|
||||
|
|
|
|||
|
|
@ -0,0 +1,123 @@
|
|||
package hidrpc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jetkvm/kvm/internal/usbgadget"
|
||||
)
|
||||
|
||||
// MessageType is the type of the HID RPC message
|
||||
type MessageType byte
|
||||
|
||||
const (
|
||||
TypeHandshake MessageType = 0x01
|
||||
TypeKeyboardReport MessageType = 0x02
|
||||
TypePointerReport MessageType = 0x03
|
||||
TypeWheelReport MessageType = 0x04
|
||||
TypeKeypressReport MessageType = 0x05
|
||||
TypeKeypressKeepAliveReport MessageType = 0x09
|
||||
TypeMouseReport MessageType = 0x06
|
||||
TypeKeyboardMacroReport MessageType = 0x07
|
||||
TypeCancelKeyboardMacroReport MessageType = 0x08
|
||||
TypeKeyboardLedState MessageType = 0x32
|
||||
TypeKeydownState MessageType = 0x33
|
||||
TypeKeyboardMacroState MessageType = 0x34
|
||||
)
|
||||
|
||||
const (
|
||||
Version byte = 0x01 // Version of the HID RPC protocol
|
||||
)
|
||||
|
||||
// GetQueueIndex returns the index of the queue to which the message should be enqueued.
|
||||
func GetQueueIndex(messageType MessageType) int {
|
||||
switch messageType {
|
||||
case TypeHandshake:
|
||||
return 0
|
||||
case TypeKeyboardReport, TypeKeypressReport, TypeKeyboardMacroReport, TypeKeyboardLedState, TypeKeydownState, TypeKeyboardMacroState:
|
||||
return 1
|
||||
case TypePointerReport, TypeMouseReport, TypeWheelReport:
|
||||
return 2
|
||||
// we don't want to block the queue for this message
|
||||
case TypeCancelKeyboardMacroReport:
|
||||
return 3
|
||||
default:
|
||||
return 3
|
||||
}
|
||||
}
|
||||
|
||||
// Unmarshal unmarshals the HID RPC message from the data.
|
||||
func Unmarshal(data []byte, message *Message) error {
|
||||
l := len(data)
|
||||
if l < 1 {
|
||||
return fmt.Errorf("invalid data length: %d", l)
|
||||
}
|
||||
|
||||
message.t = MessageType(data[0])
|
||||
message.d = data[1:]
|
||||
return nil
|
||||
}
|
||||
|
||||
// Marshal marshals the HID RPC message to the data.
|
||||
func Marshal(message *Message) ([]byte, error) {
|
||||
if message.t == 0 {
|
||||
return nil, fmt.Errorf("invalid message type: %d", message.t)
|
||||
}
|
||||
|
||||
data := make([]byte, len(message.d)+1)
|
||||
data[0] = byte(message.t)
|
||||
copy(data[1:], message.d)
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// NewHandshakeMessage creates a new handshake message.
|
||||
func NewHandshakeMessage() *Message {
|
||||
return &Message{
|
||||
t: TypeHandshake,
|
||||
d: []byte{Version},
|
||||
}
|
||||
}
|
||||
|
||||
// NewKeyboardReportMessage creates a new keyboard report message.
|
||||
func NewKeyboardReportMessage(keys []byte, modifier uint8) *Message {
|
||||
return &Message{
|
||||
t: TypeKeyboardReport,
|
||||
d: append([]byte{modifier}, keys...),
|
||||
}
|
||||
}
|
||||
|
||||
// NewKeyboardLedMessage creates a new keyboard LED message.
|
||||
func NewKeyboardLedMessage(state usbgadget.KeyboardState) *Message {
|
||||
return &Message{
|
||||
t: TypeKeyboardLedState,
|
||||
d: []byte{state.Byte()},
|
||||
}
|
||||
}
|
||||
|
||||
// NewKeydownStateMessage creates a new keydown state message.
|
||||
func NewKeydownStateMessage(state usbgadget.KeysDownState) *Message {
|
||||
data := make([]byte, len(state.Keys)+1)
|
||||
data[0] = state.Modifier
|
||||
copy(data[1:], state.Keys)
|
||||
|
||||
return &Message{
|
||||
t: TypeKeydownState,
|
||||
d: data,
|
||||
}
|
||||
}
|
||||
|
||||
// NewKeyboardMacroStateMessage creates a new keyboard macro state message.
|
||||
func NewKeyboardMacroStateMessage(state bool, isPaste bool) *Message {
|
||||
data := make([]byte, 2)
|
||||
if state {
|
||||
data[0] = 1
|
||||
}
|
||||
if isPaste {
|
||||
data[1] = 1
|
||||
}
|
||||
|
||||
return &Message{
|
||||
t: TypeKeyboardMacroState,
|
||||
d: data,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,207 @@
|
|||
package hidrpc
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Message ..
|
||||
type Message struct {
|
||||
t MessageType
|
||||
d []byte
|
||||
}
|
||||
|
||||
// Marshal marshals the message to a byte array.
|
||||
func (m *Message) Marshal() ([]byte, error) {
|
||||
return Marshal(m)
|
||||
}
|
||||
|
||||
func (m *Message) Type() MessageType {
|
||||
return m.t
|
||||
}
|
||||
|
||||
func (m *Message) String() string {
|
||||
switch m.t {
|
||||
case TypeHandshake:
|
||||
return "Handshake"
|
||||
case TypeKeypressReport:
|
||||
if len(m.d) < 2 {
|
||||
return fmt.Sprintf("KeypressReport{Malformed: %v}", m.d)
|
||||
}
|
||||
return fmt.Sprintf("KeypressReport{Key: %d, Press: %v}", m.d[0], m.d[1] == uint8(1))
|
||||
case TypeKeyboardReport:
|
||||
if len(m.d) < 2 {
|
||||
return fmt.Sprintf("KeyboardReport{Malformed: %v}", m.d)
|
||||
}
|
||||
return fmt.Sprintf("KeyboardReport{Modifier: %d, Keys: %v}", m.d[0], m.d[1:])
|
||||
case TypePointerReport:
|
||||
if len(m.d) < 9 {
|
||||
return fmt.Sprintf("PointerReport{Malformed: %v}", m.d)
|
||||
}
|
||||
return fmt.Sprintf("PointerReport{X: %d, Y: %d, Button: %d}", m.d[0:4], m.d[4:8], m.d[8])
|
||||
case TypeMouseReport:
|
||||
if len(m.d) < 3 {
|
||||
return fmt.Sprintf("MouseReport{Malformed: %v}", m.d)
|
||||
}
|
||||
return fmt.Sprintf("MouseReport{DX: %d, DY: %d, Button: %d}", m.d[0], m.d[1], m.d[2])
|
||||
case TypeKeypressKeepAliveReport:
|
||||
return "KeypressKeepAliveReport"
|
||||
case TypeKeyboardMacroReport:
|
||||
if len(m.d) < 5 {
|
||||
return fmt.Sprintf("KeyboardMacroReport{Malformed: %v}", m.d)
|
||||
}
|
||||
return fmt.Sprintf("KeyboardMacroReport{IsPaste: %v, Length: %d}", m.d[0] == uint8(1), binary.BigEndian.Uint32(m.d[1:5]))
|
||||
default:
|
||||
return fmt.Sprintf("Unknown{Type: %d, Data: %v}", m.t, m.d)
|
||||
}
|
||||
}
|
||||
|
||||
// KeypressReport ..
|
||||
type KeypressReport struct {
|
||||
Key byte
|
||||
Press bool
|
||||
}
|
||||
|
||||
// KeypressReport returns the keypress report from the message.
|
||||
func (m *Message) KeypressReport() (KeypressReport, error) {
|
||||
if m.t != TypeKeypressReport {
|
||||
return KeypressReport{}, fmt.Errorf("invalid message type: %d", m.t)
|
||||
}
|
||||
|
||||
return KeypressReport{
|
||||
Key: m.d[0],
|
||||
Press: m.d[1] == uint8(1),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// KeyboardReport ..
|
||||
type KeyboardReport struct {
|
||||
Modifier byte
|
||||
Keys []byte
|
||||
}
|
||||
|
||||
// KeyboardReport returns the keyboard report from the message.
|
||||
func (m *Message) KeyboardReport() (KeyboardReport, error) {
|
||||
if m.t != TypeKeyboardReport {
|
||||
return KeyboardReport{}, fmt.Errorf("invalid message type: %d", m.t)
|
||||
}
|
||||
|
||||
return KeyboardReport{
|
||||
Modifier: m.d[0],
|
||||
Keys: m.d[1:],
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Macro ..
|
||||
type KeyboardMacroStep struct {
|
||||
Modifier byte // 1 byte
|
||||
Keys []byte // 6 bytes: hidKeyBufferSize
|
||||
Delay uint16 // 2 bytes
|
||||
}
|
||||
type KeyboardMacroReport struct {
|
||||
IsPaste bool
|
||||
StepCount uint32
|
||||
Steps []KeyboardMacroStep
|
||||
}
|
||||
|
||||
// HidKeyBufferSize is the size of the keys buffer in the keyboard report.
|
||||
const HidKeyBufferSize = 6
|
||||
|
||||
// KeyboardMacroReport returns the keyboard macro report from the message.
|
||||
func (m *Message) KeyboardMacroReport() (KeyboardMacroReport, error) {
|
||||
if m.t != TypeKeyboardMacroReport {
|
||||
return KeyboardMacroReport{}, fmt.Errorf("invalid message type: %d", m.t)
|
||||
}
|
||||
|
||||
isPaste := m.d[0] == uint8(1)
|
||||
stepCount := binary.BigEndian.Uint32(m.d[1:5])
|
||||
|
||||
// check total length
|
||||
expectedLength := int(stepCount)*9 + 5
|
||||
if len(m.d) != expectedLength {
|
||||
return KeyboardMacroReport{}, fmt.Errorf("invalid length: %d, expected: %d", len(m.d), expectedLength)
|
||||
}
|
||||
|
||||
steps := make([]KeyboardMacroStep, 0, int(stepCount))
|
||||
offset := 5
|
||||
for i := 0; i < int(stepCount); i++ {
|
||||
steps = append(steps, KeyboardMacroStep{
|
||||
Modifier: m.d[offset],
|
||||
Keys: m.d[offset+1 : offset+7],
|
||||
Delay: binary.BigEndian.Uint16(m.d[offset+7 : offset+9]),
|
||||
})
|
||||
|
||||
offset += 1 + HidKeyBufferSize + 2
|
||||
}
|
||||
|
||||
return KeyboardMacroReport{
|
||||
IsPaste: isPaste,
|
||||
Steps: steps,
|
||||
StepCount: stepCount,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// PointerReport ..
|
||||
type PointerReport struct {
|
||||
X int
|
||||
Y int
|
||||
Button uint8
|
||||
}
|
||||
|
||||
func toInt(b []byte) int {
|
||||
return int(b[0])<<24 + int(b[1])<<16 + int(b[2])<<8 + int(b[3])<<0
|
||||
}
|
||||
|
||||
// PointerReport returns the point report from the message.
|
||||
func (m *Message) PointerReport() (PointerReport, error) {
|
||||
if m.t != TypePointerReport {
|
||||
return PointerReport{}, fmt.Errorf("invalid message type: %d", m.t)
|
||||
}
|
||||
|
||||
if len(m.d) != 9 {
|
||||
return PointerReport{}, fmt.Errorf("invalid message length: %d", len(m.d))
|
||||
}
|
||||
|
||||
return PointerReport{
|
||||
X: toInt(m.d[0:4]),
|
||||
Y: toInt(m.d[4:8]),
|
||||
Button: uint8(m.d[8]),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// MouseReport ..
|
||||
type MouseReport struct {
|
||||
DX int8
|
||||
DY int8
|
||||
Button uint8
|
||||
}
|
||||
|
||||
// MouseReport returns the mouse report from the message.
|
||||
func (m *Message) MouseReport() (MouseReport, error) {
|
||||
if m.t != TypeMouseReport {
|
||||
return MouseReport{}, fmt.Errorf("invalid message type: %d", m.t)
|
||||
}
|
||||
|
||||
return MouseReport{
|
||||
DX: int8(m.d[0]),
|
||||
DY: int8(m.d[1]),
|
||||
Button: uint8(m.d[2]),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type KeyboardMacroState struct {
|
||||
State bool
|
||||
IsPaste bool
|
||||
}
|
||||
|
||||
// KeyboardMacroState returns the keyboard macro state report from the message.
|
||||
func (m *Message) KeyboardMacroState() (KeyboardMacroState, error) {
|
||||
if m.t != TypeKeyboardMacroState {
|
||||
return KeyboardMacroState{}, fmt.Errorf("invalid message type: %d", m.t)
|
||||
}
|
||||
|
||||
return KeyboardMacroState{
|
||||
State: m.d[0] == uint8(1),
|
||||
IsPaste: m.d[1] == uint8(1),
|
||||
}, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
package lldp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"github.com/google/gopacket/afpacket"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
const (
|
||||
afPacketBufferSize = 2 // in MiB
|
||||
afPacketSnaplen = 9216
|
||||
)
|
||||
|
||||
func afPacketComputeSize(targetSizeMb int, snaplen int, pageSize int) (
|
||||
frameSize int, blockSize int, numBlocks int, err error) {
|
||||
if snaplen < pageSize {
|
||||
frameSize = pageSize / (pageSize / snaplen)
|
||||
} else {
|
||||
frameSize = (snaplen/pageSize + 1) * pageSize
|
||||
}
|
||||
|
||||
// 128 is the default from the gopacket library so just use that
|
||||
blockSize = frameSize * 128
|
||||
numBlocks = (targetSizeMb * 1024 * 1024) / blockSize
|
||||
|
||||
if numBlocks == 0 {
|
||||
return 0, 0, 0, fmt.Errorf("interface buffersize is too small")
|
||||
}
|
||||
|
||||
return frameSize, blockSize, numBlocks, nil
|
||||
}
|
||||
|
||||
func afPacketNewTPacket(ifName string) (*afpacket.TPacket, error) {
|
||||
szFrame, szBlock, numBlocks, err := afPacketComputeSize(
|
||||
afPacketBufferSize,
|
||||
afPacketSnaplen,
|
||||
os.Getpagesize())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return afpacket.NewTPacket(
|
||||
afpacket.OptInterface(ifName),
|
||||
afpacket.OptFrameSize(szFrame),
|
||||
afpacket.OptBlockSize(szBlock),
|
||||
afpacket.OptNumBlocks(numBlocks),
|
||||
afpacket.OptAddVLANHeader(false),
|
||||
afpacket.SocketRaw,
|
||||
afpacket.TPacketVersion3)
|
||||
}
|
||||
|
||||
type ifreq struct {
|
||||
ifrName [IFNAMSIZ]byte
|
||||
ifrHwaddr syscall.RawSockaddr
|
||||
}
|
||||
|
||||
func addMulticastAddr(ifName string, addr net.HardwareAddr) error {
|
||||
fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_DGRAM, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer syscall.Close(fd)
|
||||
|
||||
var name [IFNAMSIZ]byte
|
||||
copy(name[:], []byte(ifName))
|
||||
|
||||
ifr := &ifreq{
|
||||
ifrName: name,
|
||||
ifrHwaddr: toRawSockaddr(addr),
|
||||
}
|
||||
|
||||
_, _, ep := unix.Syscall(unix.SYS_IOCTL, uintptr(fd),
|
||||
unix.SIOCADDMULTI, uintptr(unsafe.Pointer(ifr)))
|
||||
|
||||
if ep != 0 {
|
||||
return syscall.Errno(ep)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
//go:build arm && linux
|
||||
|
||||
package lldp
|
||||
|
||||
import (
|
||||
"net"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func toRawSockaddr(mac net.HardwareAddr) (sockaddr syscall.RawSockaddr) {
|
||||
for i, n := range mac {
|
||||
sockaddr.Data[i] = uint8(n)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
//go:build !arm && linux
|
||||
|
||||
package lldp
|
||||
|
||||
import (
|
||||
"net"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func toRawSockaddr(mac net.HardwareAddr) (sockaddr syscall.RawSockaddr) {
|
||||
for i, n := range mac {
|
||||
sockaddr.Data[i] = int8(n)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
package lldp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/gopacket"
|
||||
"github.com/google/gopacket/afpacket"
|
||||
"github.com/jellydator/ttlcache/v3"
|
||||
"github.com/jetkvm/kvm/internal/logging"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
var defaultLogger = logging.GetSubsystemLogger("lldp")
|
||||
|
||||
type LLDP struct {
|
||||
l *zerolog.Logger
|
||||
tPacket *afpacket.TPacket
|
||||
pktSource *gopacket.PacketSource
|
||||
rxCtx context.Context
|
||||
rxCancel context.CancelFunc
|
||||
rxLock sync.Mutex
|
||||
|
||||
enableRx bool
|
||||
enableTx bool
|
||||
|
||||
packets chan gopacket.Packet
|
||||
interfaceName string
|
||||
stop chan struct{}
|
||||
|
||||
neighbors *ttlcache.Cache[string, Neighbor]
|
||||
}
|
||||
|
||||
type LLDPOptions struct {
|
||||
InterfaceName string
|
||||
EnableRx bool
|
||||
EnableTx bool
|
||||
Logger *zerolog.Logger
|
||||
}
|
||||
|
||||
func NewLLDP(opts *LLDPOptions) *LLDP {
|
||||
if opts.Logger == nil {
|
||||
opts.Logger = defaultLogger
|
||||
}
|
||||
|
||||
if opts.InterfaceName == "" {
|
||||
opts.Logger.Fatal().Msg("InterfaceName is required")
|
||||
}
|
||||
|
||||
return &LLDP{
|
||||
interfaceName: opts.InterfaceName,
|
||||
enableRx: opts.EnableRx,
|
||||
enableTx: opts.EnableTx,
|
||||
l: opts.Logger,
|
||||
neighbors: ttlcache.New(ttlcache.WithTTL[string, Neighbor](1 * time.Hour)),
|
||||
}
|
||||
}
|
||||
|
||||
func (l *LLDP) Start() error {
|
||||
l.rxLock.Lock()
|
||||
defer l.rxLock.Unlock()
|
||||
|
||||
if l.rxCtx != nil {
|
||||
l.l.Info().Msg("LLDP already started")
|
||||
return nil
|
||||
}
|
||||
|
||||
l.rxCtx, l.rxCancel = context.WithCancel(context.Background())
|
||||
|
||||
if l.enableRx {
|
||||
l.l.Info().Msg("setting up AF_PACKET")
|
||||
if err := l.setUpCapture(); err != nil {
|
||||
l.l.Error().Err(err).Msg("unable to set up AF_PACKET")
|
||||
return err
|
||||
}
|
||||
if err := l.startCapture(); err != nil {
|
||||
l.l.Error().Err(err).Msg("unable to start capture")
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
go l.neighbors.Start()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *LLDP) Stop() error {
|
||||
l.rxLock.Lock()
|
||||
defer l.rxLock.Unlock()
|
||||
|
||||
if l.rxCancel != nil {
|
||||
l.rxCancel()
|
||||
l.rxCancel = nil
|
||||
l.rxCtx = nil
|
||||
}
|
||||
|
||||
if l.enableRx {
|
||||
_ = l.shutdownCapture()
|
||||
}
|
||||
|
||||
l.neighbors.Stop()
|
||||
l.neighbors.DeleteAll()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
package lldp
|
||||
|
||||
import "time"
|
||||
|
||||
type Neighbor struct {
|
||||
Mac string `json:"mac"`
|
||||
Source string `json:"source"`
|
||||
ChassisID string `json:"chassis_id"`
|
||||
PortID string `json:"port_id"`
|
||||
PortDescription string `json:"port_description"`
|
||||
SystemName string `json:"system_name"`
|
||||
SystemDescription string `json:"system_description"`
|
||||
TTL uint16 `json:"ttl"`
|
||||
ManagementAddress string `json:"management_address"`
|
||||
Values map[string]string `json:"values"`
|
||||
}
|
||||
|
||||
func (l *LLDP) addNeighbor(mac string, neighbor Neighbor, ttl time.Duration) {
|
||||
logger := l.l.With().
|
||||
Str("mac", mac).
|
||||
Interface("neighbor", neighbor).
|
||||
Logger()
|
||||
|
||||
current_neigh := l.neighbors.Get(mac)
|
||||
if current_neigh != nil {
|
||||
current_source := current_neigh.Value().Source
|
||||
if current_source == "lldp" && neighbor.Source != "lldp" {
|
||||
logger.Info().Msg("skip updating neighbor, as LLDP has higher priority")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
logger.Info().Msg("adding neighbor")
|
||||
l.neighbors.Set(mac, neighbor, ttl)
|
||||
}
|
||||
|
||||
func (l *LLDP) deleteNeighbor(mac string) {
|
||||
logger := l.l.With().
|
||||
Str("mac", mac).
|
||||
Logger()
|
||||
|
||||
logger.Info().Msg("deleting neighbor")
|
||||
l.neighbors.Delete(mac)
|
||||
}
|
||||
|
||||
func (l *LLDP) GetNeighbors() []Neighbor {
|
||||
items := l.neighbors.Items()
|
||||
neighbors := make([]Neighbor, 0, len(items))
|
||||
|
||||
for _, item := range items {
|
||||
neighbors = append(neighbors, item.Value())
|
||||
}
|
||||
|
||||
l.l.Info().Interface("neighbors", neighbors).Msg("neighbors")
|
||||
|
||||
return neighbors
|
||||
}
|
||||
|
|
@ -0,0 +1,264 @@
|
|||
package lldp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/google/gopacket"
|
||||
"github.com/google/gopacket/layers"
|
||||
"github.com/rs/zerolog"
|
||||
"golang.org/x/net/bpf"
|
||||
)
|
||||
|
||||
const IFNAMSIZ = 16
|
||||
|
||||
var (
|
||||
lldpDefaultTTL = 120 * time.Second
|
||||
cdpDefaultTTL = 180 * time.Second
|
||||
)
|
||||
|
||||
// from lldpd
|
||||
// https://github.com/lldpd/lldpd/blob/9034c9332cca0c8b1a20e1287f0e5fed81f7eb2a/src/daemon/lldpd.h#L246
|
||||
//
|
||||
//nolint:govet
|
||||
var bpfFilter = []bpf.RawInstruction{
|
||||
{0x30, 0, 0, 0x00000000}, {0x54, 0, 0, 0x00000001}, {0x15, 0, 16, 0x00000001},
|
||||
{0x28, 0, 0, 0x0000000c}, {0x15, 0, 6, 0x000088cc},
|
||||
{0x20, 0, 0, 0x00000002}, {0x15, 2, 0, 0xc200000e},
|
||||
{0x15, 1, 0, 0xc2000003}, {0x15, 0, 2, 0xc2000000},
|
||||
{0x28, 0, 0, 0x00000000}, {0x15, 12, 13, 0x00000180},
|
||||
{0x20, 0, 0, 0x00000002}, {0x15, 0, 2, 0x52cccccc},
|
||||
{0x28, 0, 0, 0x00000000}, {0x15, 8, 9, 0x000001e0},
|
||||
{0x15, 1, 0, 0x0ccccccc}, {0x15, 0, 2, 0x81000100},
|
||||
{0x28, 0, 0, 0x00000000}, {0x15, 4, 5, 0x00000100},
|
||||
{0x20, 0, 0, 0x00000002}, {0x15, 0, 3, 0x2b000000},
|
||||
{0x28, 0, 0, 0x00000000}, {0x15, 0, 1, 0x000000e0},
|
||||
{0x6, 0, 0, 0x00040000},
|
||||
{0x6, 0, 0, 0x00000000},
|
||||
}
|
||||
|
||||
var multicastAddrs = []string{
|
||||
// LLDP
|
||||
"01:80:C2:00:00:00",
|
||||
"01:80:C2:00:00:03",
|
||||
"01:80:C2:00:00:0E",
|
||||
// CDP
|
||||
"01:00:0C:CC:CC:CC",
|
||||
}
|
||||
|
||||
func (l *LLDP) setUpCapture() error {
|
||||
logger := l.l.With().Str("interface", l.interfaceName).Logger()
|
||||
tPacket, err := afPacketNewTPacket(l.interfaceName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logger.Info().Msg("created TPacket")
|
||||
|
||||
// set up multicast addresses
|
||||
// otherwise the kernel might discard the packets
|
||||
// another workaround would be to enable promiscuous mode but that's too tricky
|
||||
for _, mac := range multicastAddrs {
|
||||
hwAddr, err := net.ParseMAC(mac)
|
||||
if err != nil {
|
||||
logger.Error().Msgf("unable to parse MAC address %s: %s", mac, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if err := addMulticastAddr(l.interfaceName, hwAddr); err != nil {
|
||||
logger.Error().Msgf("unable to add multicast address %s: %s", mac, err)
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Info().
|
||||
Interface("hwaddr", hwAddr).
|
||||
Msgf("added multicast address")
|
||||
}
|
||||
|
||||
if err = tPacket.SetBPF(bpfFilter); err != nil {
|
||||
logger.Error().Msgf("unable to set BPF filter: %s", err)
|
||||
tPacket.Close()
|
||||
return err
|
||||
}
|
||||
logger.Info().Msg("BPF filter set")
|
||||
|
||||
l.pktSource = gopacket.NewPacketSource(tPacket, layers.LayerTypeEthernet)
|
||||
l.tPacket = tPacket
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *LLDP) startCapture() error {
|
||||
logger := l.l.With().Str("interface", l.interfaceName).Logger()
|
||||
if l.tPacket == nil {
|
||||
return fmt.Errorf("AFPacket not initialized")
|
||||
}
|
||||
|
||||
if l.pktSource == nil {
|
||||
return fmt.Errorf("packet source not initialized")
|
||||
}
|
||||
|
||||
go func() {
|
||||
logger.Info().Msg("starting capture LLDP ethernet frames")
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-l.rxCtx.Done():
|
||||
logger.Info().Msg("shutting down LLDP capture")
|
||||
return
|
||||
case packet := <-l.pktSource.Packets():
|
||||
if err := l.handlePacket(packet, &logger); err != nil {
|
||||
logger.Error().Msgf("error handling packet: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *LLDP) handlePacket(packet gopacket.Packet, logger *zerolog.Logger) error {
|
||||
linkLayer := packet.LinkLayer()
|
||||
if linkLayer == nil {
|
||||
return fmt.Errorf("no link layer")
|
||||
}
|
||||
|
||||
srcMac := linkLayer.LinkFlow().Src().String()
|
||||
dstMac := linkLayer.LinkFlow().Dst().String()
|
||||
|
||||
logger.Trace().
|
||||
Str("src_mac", srcMac).
|
||||
Str("dst_mac", dstMac).
|
||||
Int("length", len(packet.Data())).
|
||||
Hex("data", packet.Data()).
|
||||
Msg("received packet")
|
||||
|
||||
lldpRaw := packet.Layer(layers.LayerTypeLinkLayerDiscovery)
|
||||
if lldpRaw != nil {
|
||||
logger.Trace().Msgf("Found LLDP Frame")
|
||||
|
||||
lldpInfo := packet.Layer(layers.LayerTypeLinkLayerDiscoveryInfo)
|
||||
if lldpInfo == nil {
|
||||
return fmt.Errorf("no LLDP info layer")
|
||||
}
|
||||
|
||||
return l.handlePacketLLDP(
|
||||
srcMac,
|
||||
lldpRaw.(*layers.LinkLayerDiscovery),
|
||||
lldpInfo.(*layers.LinkLayerDiscoveryInfo),
|
||||
)
|
||||
}
|
||||
|
||||
cdpRaw := packet.Layer(layers.LayerTypeCiscoDiscovery)
|
||||
if cdpRaw != nil {
|
||||
logger.Trace().Msgf("Found CDP Frame")
|
||||
|
||||
cdpInfo := packet.Layer(layers.LayerTypeCiscoDiscoveryInfo)
|
||||
if cdpInfo == nil {
|
||||
return fmt.Errorf("no CDP info layer")
|
||||
}
|
||||
|
||||
return l.handlePacketCDP(
|
||||
srcMac,
|
||||
cdpRaw.(*layers.CiscoDiscovery),
|
||||
cdpInfo.(*layers.CiscoDiscoveryInfo),
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *LLDP) handlePacketLLDP(mac string, raw *layers.LinkLayerDiscovery, info *layers.LinkLayerDiscoveryInfo) error {
|
||||
n := &Neighbor{
|
||||
Values: make(map[string]string),
|
||||
Source: "lldp",
|
||||
Mac: mac,
|
||||
}
|
||||
gotEnd := false
|
||||
|
||||
ttl := lldpDefaultTTL
|
||||
|
||||
for _, v := range raw.Values {
|
||||
switch v.Type {
|
||||
case layers.LLDPTLVEnd:
|
||||
gotEnd = true
|
||||
case layers.LLDPTLVChassisID:
|
||||
n.ChassisID = string(raw.ChassisID.ID)
|
||||
n.Values["chassis_id"] = n.ChassisID
|
||||
case layers.LLDPTLVPortID:
|
||||
n.PortID = string(raw.PortID.ID)
|
||||
n.Values["port_id"] = n.PortID
|
||||
case layers.LLDPTLVPortDescription:
|
||||
n.PortDescription = info.PortDescription
|
||||
n.Values["port_description"] = n.PortDescription
|
||||
case layers.LLDPTLVSysName:
|
||||
n.SystemName = info.SysName
|
||||
n.Values["system_name"] = n.SystemName
|
||||
case layers.LLDPTLVSysDescription:
|
||||
n.SystemDescription = info.SysDescription
|
||||
n.Values["system_description"] = n.SystemDescription
|
||||
case layers.LLDPTLVMgmtAddress:
|
||||
// n.ManagementAddress = info.MgmtAddress.Address
|
||||
case layers.LLDPTLVTTL:
|
||||
n.TTL = uint16(raw.TTL)
|
||||
ttl = time.Duration(n.TTL) * time.Second
|
||||
n.Values["ttl"] = fmt.Sprintf("%d", n.TTL)
|
||||
case layers.LLDPTLVOrgSpecific:
|
||||
for _, org := range info.OrgTLVs {
|
||||
n.Values[fmt.Sprintf("org_specific_%d", org.OUI)] = string(org.Info)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if gotEnd || ttl < 1*time.Second {
|
||||
l.deleteNeighbor(mac)
|
||||
} else {
|
||||
l.addNeighbor(mac, *n, ttl)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *LLDP) handlePacketCDP(mac string, raw *layers.CiscoDiscovery, info *layers.CiscoDiscoveryInfo) error {
|
||||
// TODO: implement full CDP parsing
|
||||
n := &Neighbor{
|
||||
Values: make(map[string]string),
|
||||
Source: "cdp",
|
||||
Mac: mac,
|
||||
}
|
||||
|
||||
ttl := cdpDefaultTTL
|
||||
|
||||
n.ChassisID = info.DeviceID
|
||||
n.PortID = info.PortID
|
||||
n.SystemName = info.SysName
|
||||
n.SystemDescription = info.Platform
|
||||
n.TTL = uint16(raw.TTL)
|
||||
|
||||
if n.TTL > 1 {
|
||||
ttl = time.Duration(n.TTL) * time.Second
|
||||
}
|
||||
|
||||
if len(info.MgmtAddresses) > 0 {
|
||||
n.ManagementAddress = string(info.MgmtAddresses[0])
|
||||
}
|
||||
|
||||
l.addNeighbor(mac, *n, ttl)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *LLDP) shutdownCapture() error {
|
||||
if l.tPacket != nil {
|
||||
l.l.Info().Msg("closing TPacket")
|
||||
l.tPacket.Close()
|
||||
l.tPacket = nil
|
||||
}
|
||||
|
||||
if l.pktSource != nil {
|
||||
l.l.Info().Msg("closing packet source")
|
||||
l.pktSource = nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -50,7 +50,7 @@ var (
|
|||
TimeFormat: time.RFC3339,
|
||||
PartsOrder: []string{"time", "level", "scope", "component", "message"},
|
||||
FieldsExclude: []string{"scope", "component"},
|
||||
FormatPartValueByName: func(value interface{}, name string) string {
|
||||
FormatPartValueByName: func(value any, name string) string {
|
||||
val := fmt.Sprintf("%s", value)
|
||||
if name == "component" {
|
||||
if value == nil {
|
||||
|
|
@ -121,8 +121,8 @@ func (l *Logger) updateLogLevel() {
|
|||
continue
|
||||
}
|
||||
|
||||
scopes := strings.Split(strings.ToLower(env), ",")
|
||||
for _, scope := range scopes {
|
||||
scopes := strings.SplitSeq(strings.ToLower(env), ",")
|
||||
for scope := range scopes {
|
||||
l.scopeLevels[scope] = level
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,32 +13,32 @@ type pionLogger struct {
|
|||
func (c pionLogger) Trace(msg string) {
|
||||
c.logger.Trace().Msg(msg)
|
||||
}
|
||||
func (c pionLogger) Tracef(format string, args ...interface{}) {
|
||||
func (c pionLogger) Tracef(format string, args ...any) {
|
||||
c.logger.Trace().Msgf(format, args...)
|
||||
}
|
||||
|
||||
func (c pionLogger) Debug(msg string) {
|
||||
c.logger.Debug().Msg(msg)
|
||||
}
|
||||
func (c pionLogger) Debugf(format string, args ...interface{}) {
|
||||
func (c pionLogger) Debugf(format string, args ...any) {
|
||||
c.logger.Debug().Msgf(format, args...)
|
||||
}
|
||||
func (c pionLogger) Info(msg string) {
|
||||
c.logger.Info().Msg(msg)
|
||||
}
|
||||
func (c pionLogger) Infof(format string, args ...interface{}) {
|
||||
func (c pionLogger) Infof(format string, args ...any) {
|
||||
c.logger.Info().Msgf(format, args...)
|
||||
}
|
||||
func (c pionLogger) Warn(msg string) {
|
||||
c.logger.Warn().Msg(msg)
|
||||
}
|
||||
func (c pionLogger) Warnf(format string, args ...interface{}) {
|
||||
func (c pionLogger) Warnf(format string, args ...any) {
|
||||
c.logger.Warn().Msgf(format, args...)
|
||||
}
|
||||
func (c pionLogger) Error(msg string) {
|
||||
c.logger.Error().Msg(msg)
|
||||
}
|
||||
func (c pionLogger) Errorf(format string, args ...interface{}) {
|
||||
func (c pionLogger) Errorf(format string, args ...any) {
|
||||
c.logger.Error().Msgf(format, args...)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@
|
|||
this.statsElement = statsElement;
|
||||
this.stream = null;
|
||||
this.reconnectAttempts = 0;
|
||||
this.maxReconnectAttempts = 10;
|
||||
this.maxReconnectAttempts = 500;
|
||||
this.reconnectDelay = 1000; // Start with 1 second
|
||||
this.maxReconnectDelay = 30000; // Max 30 seconds
|
||||
this.isConnecting = false;
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ func GetDefaultLogger() *zerolog.Logger {
|
|||
return &defaultLogger
|
||||
}
|
||||
|
||||
func ErrorfL(l *zerolog.Logger, format string, err error, args ...interface{}) error {
|
||||
func ErrorfL(l *zerolog.Logger, format string, err error, args ...any) error {
|
||||
// TODO: move rootLogger to logging package
|
||||
if l == nil {
|
||||
l = &defaultLogger
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
build
|
||||
deps
|
||||
ui_index.c
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
cmake_minimum_required(VERSION 3.14)
|
||||
include(FetchContent)
|
||||
include(ExternalProject)
|
||||
|
||||
project(jknative LANGUAGES C CXX)
|
||||
|
||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||
|
||||
# Rockchip SDK paths
|
||||
set(RK_SDK_BASE "/opt/jetkvm-native-buildkit")
|
||||
set(RK_MEDIA_OUTPUT "${RK_SDK_BASE}/media/out")
|
||||
set(RK_MEDIA_INCLUDE_PATH "${RK_MEDIA_OUTPUT}/include")
|
||||
set(RK_APP_MEDIA_LIBS_PATH "${RK_MEDIA_OUTPUT}/lib")
|
||||
|
||||
set(LV_USE_KCONFIG ON CACHE BOOL "" FORCE)
|
||||
set(LV_BUILD_DEFCONFIG_PATH ${CMAKE_CURRENT_SOURCE_DIR}/lvgl_defconfig CACHE PATH "" FORCE)
|
||||
|
||||
# # libgpiod
|
||||
|
||||
# ExternalProject_Add(libgpiod-project
|
||||
# URL https://mirrors.edge.kernel.org/pub/software/libs/libgpiod/libgpiod-2.2.tar.gz
|
||||
# URL_HASH SHA256=f89c2176250f1a9563265479eb8ad5f22a63f42db6a1f438effc570f0254d2f5
|
||||
# SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/deps/libgpiod
|
||||
# BINARY_DIR ${CMAKE_CURRENT_BINARY_DIR}/deps/libgpiod
|
||||
# CONFIGURE_COMMAND ${CMAKE_COMMAND} -E env CPPFLAGS=-fPIC ${CMAKE_CURRENT_SOURCE_DIR}/deps/libgpiod/configure --enable-tools=no CC=${CMAKE_C_COMPILER} --host=${CMAKE_HOST_SYSTEM_PROCESSOR}
|
||||
# BUILD_COMMAND make && make install
|
||||
# BUILD_BYPRODUCTS ${CMAKE_CURRENT_BINARY_DIR}/deps/libgpiod/lib/libgpiod.a
|
||||
# )
|
||||
|
||||
|
||||
# Fetch LVGL from GitHub
|
||||
FetchContent_Declare(
|
||||
lvgl
|
||||
GIT_REPOSITORY https://github.com/lvgl/lvgl.git
|
||||
GIT_TAG v9.3.0
|
||||
GIT_SHALLOW 1
|
||||
UPDATE_DISCONNECTED 1
|
||||
)
|
||||
FetchContent_MakeAvailable(lvgl)
|
||||
|
||||
# Get source files, excluding CMake generated files
|
||||
file(GLOB_RECURSE sources CONFIGURE_DEPENDS "*.c" "ui/*.c")
|
||||
list(FILTER sources EXCLUDE REGEX "CMakeFiles.*CompilerId.*\\.c$")
|
||||
|
||||
add_library(jknative STATIC ${sources} ${CMAKE_CURRENT_SOURCE_DIR}/ctrl.h)
|
||||
|
||||
# Include directories
|
||||
target_include_directories(jknative PRIVATE
|
||||
${RK_MEDIA_INCLUDE_PATH}
|
||||
${RK_MEDIA_INCLUDE_PATH}/libdrm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ui
|
||||
${CMAKE_CURRENT_BINARY_DIR}/deps/libgpiod/include
|
||||
)
|
||||
|
||||
# Set library search path
|
||||
target_link_directories(jknative PRIVATE ${RK_APP_MEDIA_LIBS_PATH})
|
||||
# target_link_directories(jknative PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/deps/libgpiod/lib)
|
||||
|
||||
target_link_libraries(jknative PRIVATE
|
||||
lvgl::lvgl
|
||||
pthread
|
||||
rockit
|
||||
rockchip_mpp
|
||||
rga
|
||||
m
|
||||
# libgpiod
|
||||
)
|
||||
|
||||
install(TARGETS jknative DESTINATION lib)
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,53 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
C_RST="$(tput sgr0)"
|
||||
C_ERR="$(tput setaf 1)"
|
||||
C_OK="$(tput setaf 2)"
|
||||
C_WARN="$(tput setaf 3)"
|
||||
C_INFO="$(tput setaf 5)"
|
||||
|
||||
msg() { printf '%s%s%s\n' $2 "$1" $C_RST; }
|
||||
|
||||
msg_info() { msg "$1" $C_INFO; }
|
||||
msg_ok() { msg "$1" $C_OK; }
|
||||
msg_err() { msg "$1" $C_ERR; }
|
||||
msg_warn() { msg "$1" $C_WARN; }
|
||||
|
||||
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||
BUILD_DIR=${SCRIPT_DIR}/build
|
||||
|
||||
CMAKE_TOOLCHAIN_FILE=/opt/jetkvm-native-buildkit/rv1106-jetkvm-v2.cmake
|
||||
CLEAN_ALL=${CLEAN_ALL:-0}
|
||||
|
||||
if [ "$CLEAN_ALL" -eq 1 ]; then
|
||||
rm -rf "${BUILD_DIR}"
|
||||
fi
|
||||
|
||||
TMP_DIR=$(mktemp -d)
|
||||
pushd "${SCRIPT_DIR}" > /dev/null
|
||||
|
||||
msg_info "▶ Generating UI index"
|
||||
./ui_index.gen.sh
|
||||
|
||||
msg_info "▶ Building native library"
|
||||
VERBOSE=1 cmake -B "${BUILD_DIR}" \
|
||||
-DCMAKE_SYSTEM_PROCESSOR=armv7l \
|
||||
-DCMAKE_SYSTEM_NAME=Linux \
|
||||
-DCMAKE_CROSSCOMPILING=1 \
|
||||
-DCMAKE_TOOLCHAIN_FILE=$CMAKE_TOOLCHAIN_FILE \
|
||||
-DLV_BUILD_USE_KCONFIG=ON \
|
||||
-DLV_BUILD_DEFCONFIG_PATH=${SCRIPT_DIR}/lvgl_defconfig \
|
||||
-DCONFIG_LV_BUILD_EXAMPLES=OFF \
|
||||
-DCONFIG_LV_BUILD_DEMOS=OFF \
|
||||
-DSKIP_GLIBC_NAMES=ON \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_INSTALL_PREFIX="${TMP_DIR}"
|
||||
|
||||
msg_info "▶ Copying built library and header files"
|
||||
cmake --build "${BUILD_DIR}" --target install
|
||||
cp -r "${TMP_DIR}/include" ../
|
||||
cp -r "${TMP_DIR}/lib" ../
|
||||
rm -rf "${TMP_DIR}"
|
||||
|
||||
popd > /dev/null
|
||||
|
|
@ -0,0 +1,378 @@
|
|||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <sys/un.h>
|
||||
#include <sys/socket.h>
|
||||
#include <errno.h>
|
||||
#include <unistd.h>
|
||||
#include <pthread.h>
|
||||
#include <stdint.h>
|
||||
#include <fcntl.h>
|
||||
#include "video.h"
|
||||
#include "screen.h"
|
||||
#include "edid.h"
|
||||
#include "ctrl.h"
|
||||
#include <lvgl.h>
|
||||
#include "ui_index.h"
|
||||
#include "log.h"
|
||||
#include "log_handler.h"
|
||||
|
||||
jetkvm_video_state_t state;
|
||||
jetkvm_video_state_handler_t *video_state_handler = NULL;
|
||||
|
||||
jetkvm_video_handler_t *video_handler = NULL;
|
||||
|
||||
|
||||
void jetkvm_set_log_handler(jetkvm_log_handler_t *handler) {
|
||||
log_set_handler(handler);
|
||||
}
|
||||
|
||||
void jetkvm_set_video_handler(jetkvm_video_handler_t *handler) {
|
||||
video_handler = handler;
|
||||
}
|
||||
|
||||
void jetkvm_set_indev_handler(jetkvm_indev_handler_t *handler) {
|
||||
lvgl_set_indev_handler(handler);
|
||||
}
|
||||
|
||||
const char *jetkvm_ui_event_code_to_name(int code) {
|
||||
lv_event_code_t cCode = (lv_event_code_t)code;
|
||||
return lv_event_code_get_name(code);
|
||||
}
|
||||
|
||||
void video_report_format(bool ready, const char *error, u_int16_t width, u_int16_t height, double frame_per_second)
|
||||
{
|
||||
state.ready = ready;
|
||||
state.error = error;
|
||||
state.width = width;
|
||||
state.height = height;
|
||||
state.frame_per_second = frame_per_second;
|
||||
if (video_state_handler != NULL) {
|
||||
(*video_state_handler)(&state);
|
||||
}
|
||||
}
|
||||
|
||||
int video_send_frame(const uint8_t *frame, ssize_t len)
|
||||
{
|
||||
if (video_handler != NULL) {
|
||||
(*video_handler)(frame, len);
|
||||
} else {
|
||||
log_error("video handler is not set");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Convert a hexadecimal string to an array of uint8_t bytes
|
||||
*
|
||||
* @param hex_str The input hexadecimal string
|
||||
* @param bytes The output byte array (must be pre-allocated)
|
||||
* @param max_len The maximum number of bytes that can be stored in the output array
|
||||
* @return int The number of bytes converted, or -1 on error
|
||||
*/
|
||||
int hex_to_bytes(const char *hex_str, uint8_t *bytes, size_t max_len)
|
||||
{
|
||||
size_t hex_len = strnlen(hex_str, 4096);
|
||||
if (hex_len % 2 != 0 || hex_len / 2 > max_len)
|
||||
{
|
||||
return -1; // Invalid input length or insufficient output buffer
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < hex_len; i += 2)
|
||||
{
|
||||
char byte_str[3] = {hex_str[i], hex_str[i + 1], '\0'};
|
||||
char *end_ptr;
|
||||
long value = strtol(byte_str, &end_ptr, 16);
|
||||
|
||||
if (*end_ptr != '\0' || value < 0 || value > 255)
|
||||
{
|
||||
return -1; // Invalid hexadecimal value
|
||||
}
|
||||
|
||||
bytes[i / 2] = (uint8_t)value;
|
||||
}
|
||||
|
||||
return hex_len / 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Convert an array of uint8_t bytes to a hexadecimal string, user must free the returned string
|
||||
*
|
||||
* @param bytes The input byte array
|
||||
* @param len The number of bytes in the input array
|
||||
* @return char* The output hexadecimal string (dynamically allocated, must be freed by the caller), or NULL on error
|
||||
*/
|
||||
const char *bytes_to_hex(const uint8_t *bytes, size_t len)
|
||||
{
|
||||
if (bytes == NULL || len == 0)
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
char *hex_str = malloc(2 * len + 1); // Each byte becomes 2 hex chars, plus null terminator
|
||||
if (hex_str == NULL)
|
||||
{
|
||||
return NULL; // Memory allocation failed
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < len; i++)
|
||||
{
|
||||
snprintf(hex_str + (2 * i), 3, "%02x", bytes[i]);
|
||||
}
|
||||
|
||||
hex_str[2 * len] = '\0'; // Ensure null termination
|
||||
return hex_str;
|
||||
}
|
||||
|
||||
lv_obj_flag_t str_to_lv_obj_flag(const char *flag)
|
||||
{
|
||||
if (strcmp(flag, "LV_OBJ_FLAG_HIDDEN") == 0)
|
||||
{
|
||||
return LV_OBJ_FLAG_HIDDEN;
|
||||
}
|
||||
else if (strcmp(flag, "LV_OBJ_FLAG_CLICKABLE") == 0)
|
||||
{
|
||||
return LV_OBJ_FLAG_CLICKABLE;
|
||||
}
|
||||
else if (strcmp(flag, "LV_OBJ_FLAG_SCROLLABLE") == 0)
|
||||
{
|
||||
return LV_OBJ_FLAG_SCROLLABLE;
|
||||
}
|
||||
else if (strcmp(flag, "LV_OBJ_FLAG_CLICK_FOCUSABLE") == 0)
|
||||
{
|
||||
return LV_OBJ_FLAG_CLICK_FOCUSABLE;
|
||||
}
|
||||
else if (strcmp(flag, "LV_OBJ_FLAG_SCROLL_ON_FOCUS") == 0)
|
||||
{
|
||||
return LV_OBJ_FLAG_SCROLL_ON_FOCUS;
|
||||
}
|
||||
else if (strcmp(flag, "LV_OBJ_FLAG_SCROLL_CHAIN") == 0)
|
||||
{
|
||||
return LV_OBJ_FLAG_SCROLL_CHAIN;
|
||||
}
|
||||
else if (strcmp(flag, "LV_OBJ_FLAG_PRESS_LOCK") == 0)
|
||||
{
|
||||
return LV_OBJ_FLAG_PRESS_LOCK;
|
||||
}
|
||||
else if (strcmp(flag, "LV_OBJ_FLAG_OVERFLOW_VISIBLE") == 0)
|
||||
{
|
||||
return LV_OBJ_FLAG_OVERFLOW_VISIBLE;
|
||||
}
|
||||
else
|
||||
{
|
||||
return 0; // Unknown flag
|
||||
}
|
||||
}
|
||||
|
||||
void jetkvm_ui_set_var(const char *name, const char *value) {
|
||||
for (int i = 0; i < ui_vars_size; i++) {
|
||||
if (strcmp(ui_vars[i].name, name) == 0) {
|
||||
ui_vars[i].setter(value);
|
||||
return;
|
||||
}
|
||||
}
|
||||
log_error("variable %s not found", name);
|
||||
}
|
||||
|
||||
const char *jetkvm_ui_get_var(const char *name) {
|
||||
for (int i = 0; i < ui_vars_size; i++) {
|
||||
if (strcmp(ui_vars[i].name, name) == 0) {
|
||||
return ui_vars[i].getter();
|
||||
}
|
||||
}
|
||||
log_error("variable %s not found", name);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void jetkvm_ui_init(u_int16_t rotation) {
|
||||
lvgl_init(rotation);
|
||||
}
|
||||
|
||||
void jetkvm_ui_tick() {
|
||||
lvgl_tick();
|
||||
}
|
||||
|
||||
void jetkvm_set_video_state_handler(jetkvm_video_state_handler_t *handler) {
|
||||
video_state_handler = handler;
|
||||
}
|
||||
|
||||
void jetkvm_ui_set_rotation(u_int16_t rotation)
|
||||
{
|
||||
lvgl_set_rotation(NULL, rotation);
|
||||
}
|
||||
|
||||
const char *jetkvm_ui_get_current_screen() {
|
||||
return ui_get_current_screen();
|
||||
}
|
||||
|
||||
void jetkvm_ui_load_screen(const char *obj_name) {
|
||||
lv_obj_t *obj = ui_get_obj(obj_name);
|
||||
if (obj == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (lv_scr_act() != obj) {
|
||||
lv_scr_load(obj);
|
||||
}
|
||||
}
|
||||
|
||||
int jetkvm_ui_set_text(const char *obj_name, const char *text) {
|
||||
lv_obj_t *obj = ui_get_obj(obj_name);
|
||||
if (obj == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (strcmp(lv_label_get_text(obj), text) == 0) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
lv_label_set_text(obj, text);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void jetkvm_ui_set_image(const char *obj_name, const char *image_name) {
|
||||
lv_obj_t *obj = ui_get_obj(obj_name);
|
||||
if (obj == NULL) {
|
||||
return;
|
||||
}
|
||||
lv_img_set_src(obj, image_name);
|
||||
}
|
||||
|
||||
void jetkvm_ui_set_state(const char *obj_name, const char *state_name) {
|
||||
lv_obj_t *obj = ui_get_obj(obj_name);
|
||||
if (obj == NULL) {
|
||||
return;
|
||||
}
|
||||
lv_obj_add_state(obj, LV_STATE_USER_1);
|
||||
lv_state_t state_val = LV_STATE_DEFAULT;
|
||||
if (strcmp(state_name, "LV_STATE_USER_1") == 0)
|
||||
{
|
||||
state_val = LV_STATE_USER_1;
|
||||
}
|
||||
else if (strcmp(state_name, "LV_STATE_USER_2") == 0)
|
||||
{
|
||||
state_val = LV_STATE_USER_2;
|
||||
}
|
||||
else if (strcmp(state_name, "LV_STATE_USER_3") == 0)
|
||||
{
|
||||
state_val = LV_STATE_USER_3;
|
||||
}
|
||||
else if (strcmp(state_name, "LV_STATE_USER_4") == 0)
|
||||
{
|
||||
state_val = LV_STATE_USER_4;
|
||||
}
|
||||
else if (strcmp(state_name, "LV_STATE_DISABLED") == 0)
|
||||
{
|
||||
state_val = LV_STATE_DISABLED;
|
||||
}
|
||||
// TODO: use LV_STATE_USER_* once eez supports it
|
||||
lv_obj_clear_state(obj, LV_STATE_USER_1 | LV_STATE_USER_2 | LV_STATE_USER_3 | LV_STATE_USER_4 | LV_STATE_DISABLED);
|
||||
lv_obj_add_state(obj, state_val);
|
||||
}
|
||||
|
||||
int jetkvm_ui_add_flag(const char *obj_name, const char *flag_name) {
|
||||
lv_obj_t *obj = ui_get_obj(obj_name);
|
||||
if (obj == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
lv_obj_flag_t flag_val = str_to_lv_obj_flag(flag_name);
|
||||
if (flag_val == 0)
|
||||
{
|
||||
return -2;
|
||||
}
|
||||
lv_obj_add_flag(obj, flag_val);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int jetkvm_ui_clear_flag(const char *obj_name, const char *flag_name) {
|
||||
lv_obj_t *obj = ui_get_obj(obj_name);
|
||||
if (obj == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
lv_obj_flag_t flag_val = str_to_lv_obj_flag(flag_name);
|
||||
if (flag_val == 0)
|
||||
{
|
||||
return -2;
|
||||
}
|
||||
lv_obj_clear_flag(obj, flag_val);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void jetkvm_ui_fade_in(const char *obj_name, u_int32_t duration) {
|
||||
lv_obj_t *obj = ui_get_obj(obj_name);
|
||||
if (obj == NULL) {
|
||||
return;
|
||||
}
|
||||
lv_obj_fade_in(obj, duration, 0);
|
||||
}
|
||||
|
||||
void jetkvm_ui_fade_out(const char *obj_name, u_int32_t duration) {
|
||||
lv_obj_t *obj = ui_get_obj(obj_name);
|
||||
if (obj == NULL) {
|
||||
return;
|
||||
}
|
||||
lv_obj_fade_out(obj, duration, 0);
|
||||
}
|
||||
|
||||
void jetkvm_ui_set_opacity(const char *obj_name, u_int8_t opacity) {
|
||||
lv_obj_t *obj = ui_get_obj(obj_name);
|
||||
if (obj == NULL) {
|
||||
return;
|
||||
}
|
||||
lv_obj_set_style_opa(obj, opacity, LV_PART_MAIN);
|
||||
}
|
||||
|
||||
const char *jetkvm_ui_get_lvgl_version() {
|
||||
return lv_version_info();
|
||||
}
|
||||
|
||||
void jetkvm_video_start() {
|
||||
video_start_streaming();
|
||||
}
|
||||
|
||||
void jetkvm_video_stop() {
|
||||
video_stop_streaming();
|
||||
}
|
||||
|
||||
int jetkvm_video_set_quality_factor(float quality_factor) {
|
||||
if (quality_factor < 0 || quality_factor > 1) {
|
||||
return -1;
|
||||
}
|
||||
video_set_quality_factor(quality_factor);
|
||||
return 0;
|
||||
}
|
||||
|
||||
float jetkvm_video_get_quality_factor() {
|
||||
return video_get_quality_factor();
|
||||
}
|
||||
|
||||
int jetkvm_video_set_edid(const char *edid_hex) {
|
||||
uint8_t edid[256];
|
||||
int edid_len = hex_to_bytes(edid_hex, edid, 256);
|
||||
if (edid_len < 0) {
|
||||
return -1;
|
||||
}
|
||||
return set_edid(edid, edid_len);
|
||||
}
|
||||
|
||||
char *jetkvm_video_get_edid_hex() {
|
||||
uint8_t edid[256];
|
||||
int edid_len = get_edid(edid, 256);
|
||||
if (edid_len < 0) {
|
||||
return NULL;
|
||||
}
|
||||
return bytes_to_hex(edid, edid_len);
|
||||
}
|
||||
|
||||
jetkvm_video_state_t *jetkvm_video_get_status() {
|
||||
return &state;
|
||||
}
|
||||
|
||||
int jetkvm_video_init() {
|
||||
return video_init();
|
||||
}
|
||||
|
||||
void jetkvm_video_shutdown() {
|
||||
video_shutdown();
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
#ifndef VIDEO_DAEMON_CTRL_H
|
||||
#define VIDEO_DAEMON_CTRL_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
typedef struct
|
||||
{
|
||||
bool ready;
|
||||
const char *error;
|
||||
u_int16_t width;
|
||||
u_int16_t height;
|
||||
double frame_per_second;
|
||||
} jetkvm_video_state_t;
|
||||
|
||||
typedef void (jetkvm_video_state_handler_t)(jetkvm_video_state_t *state);
|
||||
typedef void (jetkvm_log_handler_t)(int level, const char *filename, const char *funcname, int line, const char *message);
|
||||
typedef void (jetkvm_video_handler_t)(const uint8_t *frame, ssize_t len);
|
||||
typedef void (jetkvm_indev_handler_t)(int code);
|
||||
|
||||
void jetkvm_set_log_handler(jetkvm_log_handler_t *handler);
|
||||
void jetkvm_set_video_handler(jetkvm_video_handler_t *handler);
|
||||
void jetkvm_set_indev_handler(jetkvm_indev_handler_t *handler);
|
||||
void jetkvm_set_video_state_handler(jetkvm_video_state_handler_t *handler);
|
||||
|
||||
void jetkvm_ui_set_var(const char *name, const char *value);
|
||||
const char *jetkvm_ui_get_var(const char *name);
|
||||
|
||||
void jetkvm_ui_init(u_int16_t rotation);
|
||||
void jetkvm_ui_tick();
|
||||
|
||||
|
||||
void jetkvm_ui_set_rotation(u_int16_t rotation);
|
||||
const char *jetkvm_ui_get_current_screen();
|
||||
void jetkvm_ui_load_screen(const char *obj_name);
|
||||
int jetkvm_ui_set_text(const char *obj_name, const char *text);
|
||||
void jetkvm_ui_set_image(const char *obj_name, const char *image_name);
|
||||
void jetkvm_ui_set_state(const char *obj_name, const char *state_name);
|
||||
void jetkvm_ui_fade_in(const char *obj_name, u_int32_t duration);
|
||||
void jetkvm_ui_fade_out(const char *obj_name, u_int32_t duration);
|
||||
void jetkvm_ui_set_opacity(const char *obj_name, u_int8_t opacity);
|
||||
int jetkvm_ui_add_flag(const char *obj_name, const char *flag_name);
|
||||
int jetkvm_ui_clear_flag(const char *obj_name, const char *flag_name);
|
||||
|
||||
const char *jetkvm_ui_get_lvgl_version();
|
||||
|
||||
const char *jetkvm_ui_event_code_to_name(int code);
|
||||
|
||||
int jetkvm_video_init();
|
||||
void jetkvm_video_shutdown();
|
||||
void jetkvm_video_start();
|
||||
void jetkvm_video_stop();
|
||||
int jetkvm_video_set_quality_factor(float quality_factor);
|
||||
float jetkvm_video_get_quality_factor();
|
||||
int jetkvm_video_set_edid(const char *edid_hex);
|
||||
char *jetkvm_video_get_edid_hex();
|
||||
jetkvm_video_state_t *jetkvm_video_get_status();
|
||||
|
||||
void video_report_format(bool ready, const char *error, u_int16_t width, u_int16_t height, double frame_per_second);
|
||||
int video_send_frame(const uint8_t *frame, ssize_t len);
|
||||
|
||||
|
||||
|
||||
#endif //VIDEO_DAEMON_CTRL_H
|
||||
|
|
@ -0,0 +1,180 @@
|
|||
#include "edid.h"
|
||||
#include "log.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdlib.h>
|
||||
#include <stddef.h>
|
||||
#include <string.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <linux/videodev2.h>
|
||||
#include <errno.h>
|
||||
#include <sys/klog.h>
|
||||
|
||||
#define MAX_EDID_SIZE 256
|
||||
#define V4L_SUBDEV "/dev/v4l-subdev2"
|
||||
|
||||
int get_edid(uint8_t *edid, size_t max_size)
|
||||
{
|
||||
if (edid == NULL)
|
||||
{
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (max_size != 128 && max_size != 256)
|
||||
{
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
int fd;
|
||||
struct v4l2_edid v4l2_edid;
|
||||
|
||||
fd = open(V4L_SUBDEV, O_RDWR);
|
||||
if (fd < 0)
|
||||
{
|
||||
perror("Failed to open device");
|
||||
return -1;
|
||||
}
|
||||
|
||||
memset(&v4l2_edid, 0, sizeof(v4l2_edid));
|
||||
v4l2_edid.pad = 0;
|
||||
v4l2_edid.start_block = 0;
|
||||
v4l2_edid.blocks = 2;
|
||||
v4l2_edid.edid = edid;
|
||||
|
||||
if (ioctl(fd, VIDIOC_G_EDID, &v4l2_edid) < 0)
|
||||
{
|
||||
perror("Failed to get EDID");
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
|
||||
close(fd);
|
||||
return v4l2_edid.blocks * 128;
|
||||
}
|
||||
|
||||
static void fix_edid_checksum(uint8_t *edid, size_t size)
|
||||
{
|
||||
for (size_t block = 0; block < size / 128; block++)
|
||||
{
|
||||
uint8_t sum = 0;
|
||||
for (int i = 0; i < 127; i++)
|
||||
{
|
||||
sum += edid[block * 128 + i];
|
||||
}
|
||||
edid[block * 128 + 127] = (uint8_t)(256 - sum);
|
||||
}
|
||||
}
|
||||
|
||||
int set_edid(uint8_t *edid, size_t size)
|
||||
{
|
||||
if (edid == NULL)
|
||||
{
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (size != 128 && size != 256)
|
||||
{
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
int fd;
|
||||
struct v4l2_edid v4l2_edid;
|
||||
|
||||
fd = open(V4L_SUBDEV, O_RDWR);
|
||||
if (fd < 0)
|
||||
{
|
||||
perror("Failed to open device");
|
||||
return -1;
|
||||
}
|
||||
|
||||
fix_edid_checksum(edid, size);
|
||||
|
||||
memset(&v4l2_edid, 0, sizeof(v4l2_edid));
|
||||
v4l2_edid.pad = 0;
|
||||
v4l2_edid.start_block = 0;
|
||||
v4l2_edid.blocks = size / 128;
|
||||
v4l2_edid.edid = edid;
|
||||
|
||||
if (ioctl(fd, VIDIOC_S_EDID, &v4l2_edid) < 0)
|
||||
{
|
||||
perror("Failed to set EDID");
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
|
||||
close(fd);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char *videoc_log_status()
|
||||
{
|
||||
int fd;
|
||||
char *buffer = NULL;
|
||||
size_t buffer_size = 0;
|
||||
ssize_t bytes_read;
|
||||
|
||||
fd = open(V4L_SUBDEV, O_RDWR);
|
||||
if (fd < 0)
|
||||
{
|
||||
perror("Failed to open device");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (ioctl(fd, VIDIOC_LOG_STATUS) == -1)
|
||||
{
|
||||
perror("VIDIOC_LOG_STATUS failed");
|
||||
close(fd);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
close(fd);
|
||||
|
||||
char buf[40960];
|
||||
int len = -1;
|
||||
|
||||
len = klogctl(3, buf, sizeof(buf) - 1);
|
||||
|
||||
if (len >= 0)
|
||||
{
|
||||
bool found_status = false;
|
||||
char *p = buf;
|
||||
char *q;
|
||||
|
||||
buf[len] = 0;
|
||||
while ((q = strstr(p, "START STATUS")))
|
||||
{
|
||||
found_status = true;
|
||||
p = q + 1;
|
||||
}
|
||||
if (found_status)
|
||||
{
|
||||
while (p > buf && *p != '<')
|
||||
p--;
|
||||
q = p;
|
||||
while ((q = strstr(q, "<6>")))
|
||||
{
|
||||
memcpy(q, " ", 3);
|
||||
}
|
||||
}
|
||||
buffer = strdup(p);
|
||||
if (buffer == NULL)
|
||||
{
|
||||
perror("Failed to allocate memory for status");
|
||||
return NULL;
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
else
|
||||
{
|
||||
log_error("Failed to read kernel log\n");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
#ifndef EDID_H
|
||||
#define EDID_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
/**
|
||||
* @brief Read the EDID from the display
|
||||
*
|
||||
* @param edid Buffer to store the EDID data
|
||||
* @param max_size Maximum size of the buffer (should be 128 or 256)
|
||||
* @return int Number of bytes read on success, -1 on failure
|
||||
*/
|
||||
int get_edid(uint8_t *edid, size_t max_size);
|
||||
|
||||
/**
|
||||
* @brief Set the EDID of the display
|
||||
*
|
||||
* @param edid The EDID to set, it can be modified
|
||||
* @param size The size of the EDID (should be 128 or 256)
|
||||
* @return int 0 on success, -1 on failure
|
||||
*/
|
||||
int set_edid(uint8_t *edid, size_t size);
|
||||
|
||||
/**
|
||||
* @brief Get the status of the videocontroller, aka v4l2-ctl --log-status.
|
||||
* User should free the returned string
|
||||
*
|
||||
* @return const char* The status of the videocontroller
|
||||
*/
|
||||
const char* videoc_log_status();
|
||||
|
||||
#endif // EDID_H
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
#ifndef VIDEO_DAEMON_LOG_H
|
||||
#define VIDEO_DAEMON_LOG_H
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include "log_handler.h"
|
||||
|
||||
/* Default level */
|
||||
#ifndef LOG_LEVEL
|
||||
#define LOG_LEVEL LEVEL_INFO
|
||||
#endif
|
||||
|
||||
#define __FILENAME__ (strrchr(__FILE__, '/') ? strrchr(__FILE__, '/') + 1 : __FILE__)
|
||||
|
||||
void jetkvm_log(const char *message);
|
||||
|
||||
/* Log to screen */
|
||||
#define emit_log(level, file, func, line, ...) do { \
|
||||
/* call the log handler */ \
|
||||
char msg_buffer[1024]; \
|
||||
sprintf(msg_buffer, __VA_ARGS__); \
|
||||
log_message(level, file, func, line, msg_buffer); \
|
||||
} while (0)
|
||||
|
||||
/* Level enum */
|
||||
#define LEVEL_PANIC 5
|
||||
#define LEVEL_FATAL 4
|
||||
#define LEVEL_ERROR 3
|
||||
#define LEVEL_WARN 2
|
||||
#define LEVEL_INFO 1
|
||||
#define LEVEL_DEBUG 0
|
||||
#define LEVEL_TRACE -1
|
||||
|
||||
/* TRACE LOG */
|
||||
#define log_trace(...) do { \
|
||||
if (LOG_LEVEL <= LEVEL_TRACE) { \
|
||||
emit_log( \
|
||||
LEVEL_TRACE, __FILENAME__, __func__, __LINE__, __VA_ARGS__ \
|
||||
); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
/* DEBUG LOG */
|
||||
#define log_debug(...) do { \
|
||||
if (LOG_LEVEL <= LEVEL_DEBUG) { \
|
||||
emit_log( \
|
||||
LEVEL_DEBUG, __FILENAME__, __func__, __LINE__, __VA_ARGS__ \
|
||||
); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
/* INFO LOG */
|
||||
#define log_info(...) do { \
|
||||
if (LOG_LEVEL <= LEVEL_INFO) { \
|
||||
emit_log( \
|
||||
LEVEL_INFO, __FILENAME__, __func__, __LINE__, __VA_ARGS__ \
|
||||
); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
/* NOTICE LOG */
|
||||
#define log_notice(...) do { \
|
||||
if (LOG_LEVEL <= LEVEL_INFO) { \
|
||||
emit_log( \
|
||||
LEVEL_INFO, __FILENAME__, __func__, __LINE__, __VA_ARGS__ \
|
||||
); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
/* WARN LOG */
|
||||
#define log_warn(...) do { \
|
||||
if (LOG_LEVEL <= LEVEL_WARN) { \
|
||||
emit_log( \
|
||||
LEVEL_WARN, __FILENAME__, __func__, __LINE__, __VA_ARGS__ \
|
||||
); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
/* ERROR LOG */
|
||||
#define log_error(...) do { \
|
||||
if (LOG_LEVEL <= LEVEL_ERROR) { \
|
||||
emit_log( \
|
||||
LEVEL_ERROR, __FILENAME__, __func__, __LINE__, __VA_ARGS__ \
|
||||
); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
/* PANIC LOG */
|
||||
#define log_panic(...) do { \
|
||||
if (LOG_LEVEL <= LEVEL_PANIC) { \
|
||||
emit_log( \
|
||||
LEVEL_PANIC, __FILENAME__, __func__, __LINE__, __VA_ARGS__ \
|
||||
); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
#endif //VIDEO_DAEMON_LOG_H
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
#include <stddef.h>
|
||||
#include "log_handler.h"
|
||||
|
||||
/* Log handler */
|
||||
jetkvm_log_handler_t *log_handler = NULL;
|
||||
|
||||
void log_message(int level, const char *filename, const char *funcname, const int line, const char *message) {
|
||||
if (log_handler != NULL) {
|
||||
log_handler(level, filename, funcname, line, message);
|
||||
}
|
||||
}
|
||||
|
||||
void log_set_handler(jetkvm_log_handler_t *handler) {
|
||||
log_handler = handler;
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
#ifndef LOG_HANDLER_H
|
||||
#define LOG_HANDLER_H
|
||||
|
||||
typedef void (jetkvm_log_handler_t)(int level, const char *filename, const char *funcname, const int line, const char *message);
|
||||
void log_message(int level, const char *filename, const char *funcname, const int line, const char *message);
|
||||
|
||||
void log_set_handler(jetkvm_log_handler_t *handler);
|
||||
|
||||
#endif
|
||||
|
|
@ -0,0 +1,207 @@
|
|||
diff --git a/env_support/cmake/custom.cmake b/env_support/cmake/custom.cmake
|
||||
index 7da68124b..1fbe2d3de 100644
|
||||
--- a/env_support/cmake/custom.cmake
|
||||
+++ b/env_support/cmake/custom.cmake
|
||||
@@ -15,8 +15,6 @@ get_filename_component(LV_CONF_DIR ${LV_CONF_PATH} DIRECTORY)
|
||||
option(BUILD_SHARED_LIBS "Build shared libraries" OFF)
|
||||
|
||||
file(GLOB_RECURSE SOURCES ${LVGL_ROOT_DIR}/src/*.c)
|
||||
-file(GLOB_RECURSE EXAMPLE_SOURCES ${LVGL_ROOT_DIR}/examples/*.c)
|
||||
-file(GLOB_RECURSE DEMO_SOURCES ${LVGL_ROOT_DIR}/demos/*.c)
|
||||
|
||||
if (BUILD_SHARED_LIBS)
|
||||
add_library(lvgl SHARED ${SOURCES})
|
||||
@@ -25,10 +23,6 @@ else()
|
||||
endif()
|
||||
|
||||
add_library(lvgl::lvgl ALIAS lvgl)
|
||||
-add_library(lvgl_examples STATIC ${EXAMPLE_SOURCES})
|
||||
-add_library(lvgl::examples ALIAS lvgl_examples)
|
||||
-add_library(lvgl_demos STATIC ${DEMO_SOURCES})
|
||||
-add_library(lvgl::demos ALIAS lvgl_demos)
|
||||
|
||||
target_compile_definitions(
|
||||
lvgl PUBLIC $<$<BOOL:${LV_LVGL_H_INCLUDE_SIMPLE}>:LV_LVGL_H_INCLUDE_SIMPLE>
|
||||
@@ -37,15 +31,6 @@ target_compile_definitions(
|
||||
# Include root and optional parent path of LV_CONF_PATH
|
||||
target_include_directories(lvgl SYSTEM PUBLIC ${LVGL_ROOT_DIR} ${LV_CONF_DIR})
|
||||
|
||||
-# Include /examples folder
|
||||
-target_include_directories(lvgl_examples SYSTEM
|
||||
- PUBLIC ${LVGL_ROOT_DIR}/examples)
|
||||
-target_include_directories(lvgl_demos SYSTEM
|
||||
- PUBLIC ${LVGL_ROOT_DIR}/demos)
|
||||
-
|
||||
-target_link_libraries(lvgl_examples PUBLIC lvgl)
|
||||
-target_link_libraries(lvgl_demos PUBLIC lvgl)
|
||||
-
|
||||
# Lbrary and headers can be installed to system using make install
|
||||
file(GLOB LVGL_PUBLIC_HEADERS "${CMAKE_SOURCE_DIR}/lv_conf.h"
|
||||
"${CMAKE_SOURCE_DIR}/lvgl.h")
|
||||
diff --git a/lvgl.mk b/lvgl.mk
|
||||
index 0ea126daa..300fb6cbe 100644
|
||||
--- a/lvgl.mk
|
||||
+++ b/lvgl.mk
|
||||
@@ -1,5 +1,3 @@
|
||||
-include $(LVGL_DIR)/$(LVGL_DIR_NAME)/demos/lv_demos.mk
|
||||
-include $(LVGL_DIR)/$(LVGL_DIR_NAME)/examples/lv_examples.mk
|
||||
include $(LVGL_DIR)/$(LVGL_DIR_NAME)/src/core/lv_core.mk
|
||||
include $(LVGL_DIR)/$(LVGL_DIR_NAME)/src/draw/lv_draw.mk
|
||||
include $(LVGL_DIR)/$(LVGL_DIR_NAME)/src/extra/lv_extra.mk
|
||||
diff --git a/src/font/lv_font.h b/src/font/lv_font.h
|
||||
index e3b670c87..4cceffc45 100644
|
||||
--- a/src/font/lv_font.h
|
||||
+++ b/src/font/lv_font.h
|
||||
@@ -132,114 +132,10 @@ static inline lv_coord_t lv_font_get_line_height(const lv_font_t * font_p)
|
||||
|
||||
#define LV_FONT_DECLARE(font_name) extern const lv_font_t font_name;
|
||||
|
||||
-#if LV_FONT_MONTSERRAT_8
|
||||
-LV_FONT_DECLARE(lv_font_montserrat_8)
|
||||
-#endif
|
||||
-
|
||||
-#if LV_FONT_MONTSERRAT_10
|
||||
-LV_FONT_DECLARE(lv_font_montserrat_10)
|
||||
-#endif
|
||||
-
|
||||
-#if LV_FONT_MONTSERRAT_12
|
||||
-LV_FONT_DECLARE(lv_font_montserrat_12)
|
||||
-#endif
|
||||
-
|
||||
#if LV_FONT_MONTSERRAT_14
|
||||
LV_FONT_DECLARE(lv_font_montserrat_14)
|
||||
#endif
|
||||
|
||||
-#if LV_FONT_MONTSERRAT_16
|
||||
-LV_FONT_DECLARE(lv_font_montserrat_16)
|
||||
-#endif
|
||||
-
|
||||
-#if LV_FONT_MONTSERRAT_18
|
||||
-LV_FONT_DECLARE(lv_font_montserrat_18)
|
||||
-#endif
|
||||
-
|
||||
-#if LV_FONT_MONTSERRAT_20
|
||||
-LV_FONT_DECLARE(lv_font_montserrat_20)
|
||||
-#endif
|
||||
-
|
||||
-#if LV_FONT_MONTSERRAT_22
|
||||
-LV_FONT_DECLARE(lv_font_montserrat_22)
|
||||
-#endif
|
||||
-
|
||||
-#if LV_FONT_MONTSERRAT_24
|
||||
-LV_FONT_DECLARE(lv_font_montserrat_24)
|
||||
-#endif
|
||||
-
|
||||
-#if LV_FONT_MONTSERRAT_26
|
||||
-LV_FONT_DECLARE(lv_font_montserrat_26)
|
||||
-#endif
|
||||
-
|
||||
-#if LV_FONT_MONTSERRAT_28
|
||||
-LV_FONT_DECLARE(lv_font_montserrat_28)
|
||||
-#endif
|
||||
-
|
||||
-#if LV_FONT_MONTSERRAT_30
|
||||
-LV_FONT_DECLARE(lv_font_montserrat_30)
|
||||
-#endif
|
||||
-
|
||||
-#if LV_FONT_MONTSERRAT_32
|
||||
-LV_FONT_DECLARE(lv_font_montserrat_32)
|
||||
-#endif
|
||||
-
|
||||
-#if LV_FONT_MONTSERRAT_34
|
||||
-LV_FONT_DECLARE(lv_font_montserrat_34)
|
||||
-#endif
|
||||
-
|
||||
-#if LV_FONT_MONTSERRAT_36
|
||||
-LV_FONT_DECLARE(lv_font_montserrat_36)
|
||||
-#endif
|
||||
-
|
||||
-#if LV_FONT_MONTSERRAT_38
|
||||
-LV_FONT_DECLARE(lv_font_montserrat_38)
|
||||
-#endif
|
||||
-
|
||||
-#if LV_FONT_MONTSERRAT_40
|
||||
-LV_FONT_DECLARE(lv_font_montserrat_40)
|
||||
-#endif
|
||||
-
|
||||
-#if LV_FONT_MONTSERRAT_42
|
||||
-LV_FONT_DECLARE(lv_font_montserrat_42)
|
||||
-#endif
|
||||
-
|
||||
-#if LV_FONT_MONTSERRAT_44
|
||||
-LV_FONT_DECLARE(lv_font_montserrat_44)
|
||||
-#endif
|
||||
-
|
||||
-#if LV_FONT_MONTSERRAT_46
|
||||
-LV_FONT_DECLARE(lv_font_montserrat_46)
|
||||
-#endif
|
||||
-
|
||||
-#if LV_FONT_MONTSERRAT_48
|
||||
-LV_FONT_DECLARE(lv_font_montserrat_48)
|
||||
-#endif
|
||||
-
|
||||
-#if LV_FONT_MONTSERRAT_12_SUBPX
|
||||
-LV_FONT_DECLARE(lv_font_montserrat_12_subpx)
|
||||
-#endif
|
||||
-
|
||||
-#if LV_FONT_MONTSERRAT_28_COMPRESSED
|
||||
-LV_FONT_DECLARE(lv_font_montserrat_28_compressed)
|
||||
-#endif
|
||||
-
|
||||
-#if LV_FONT_DEJAVU_16_PERSIAN_HEBREW
|
||||
-LV_FONT_DECLARE(lv_font_dejavu_16_persian_hebrew)
|
||||
-#endif
|
||||
-
|
||||
-#if LV_FONT_SIMSUN_16_CJK
|
||||
-LV_FONT_DECLARE(lv_font_simsun_16_cjk)
|
||||
-#endif
|
||||
-
|
||||
-#if LV_FONT_UNSCII_8
|
||||
-LV_FONT_DECLARE(lv_font_unscii_8)
|
||||
-#endif
|
||||
-
|
||||
-#if LV_FONT_UNSCII_16
|
||||
-LV_FONT_DECLARE(lv_font_unscii_16)
|
||||
-#endif
|
||||
-
|
||||
/*Declare the custom (user defined) fonts*/
|
||||
#ifdef LV_FONT_CUSTOM_DECLARE
|
||||
LV_FONT_CUSTOM_DECLARE
|
||||
diff --git a/src/font/lv_font.mk b/src/font/lv_font.mk
|
||||
index 2201b73f2..7b2707da4 100644
|
||||
--- a/src/font/lv_font.mk
|
||||
+++ b/src/font/lv_font.mk
|
||||
@@ -2,33 +2,7 @@ CSRCS += lv_font.c
|
||||
CSRCS += lv_font_fmt_txt.c
|
||||
CSRCS += lv_font_loader.c
|
||||
|
||||
-CSRCS += lv_font_dejavu_16_persian_hebrew.c
|
||||
-CSRCS += lv_font_montserrat_8.c
|
||||
-CSRCS += lv_font_montserrat_10.c
|
||||
-CSRCS += lv_font_montserrat_12.c
|
||||
-CSRCS += lv_font_montserrat_12_subpx.c
|
||||
CSRCS += lv_font_montserrat_14.c
|
||||
-CSRCS += lv_font_montserrat_16.c
|
||||
-CSRCS += lv_font_montserrat_18.c
|
||||
-CSRCS += lv_font_montserrat_20.c
|
||||
-CSRCS += lv_font_montserrat_22.c
|
||||
-CSRCS += lv_font_montserrat_24.c
|
||||
-CSRCS += lv_font_montserrat_26.c
|
||||
-CSRCS += lv_font_montserrat_28.c
|
||||
-CSRCS += lv_font_montserrat_28_compressed.c
|
||||
-CSRCS += lv_font_montserrat_30.c
|
||||
-CSRCS += lv_font_montserrat_32.c
|
||||
-CSRCS += lv_font_montserrat_34.c
|
||||
-CSRCS += lv_font_montserrat_36.c
|
||||
-CSRCS += lv_font_montserrat_38.c
|
||||
-CSRCS += lv_font_montserrat_40.c
|
||||
-CSRCS += lv_font_montserrat_42.c
|
||||
-CSRCS += lv_font_montserrat_44.c
|
||||
-CSRCS += lv_font_montserrat_46.c
|
||||
-CSRCS += lv_font_montserrat_48.c
|
||||
-CSRCS += lv_font_simsun_16_cjk.c
|
||||
-CSRCS += lv_font_unscii_8.c
|
||||
-CSRCS += lv_font_unscii_16.c
|
||||
|
||||
DEPPATH += --dep-path $(LVGL_DIR)/$(LVGL_DIR_NAME)/src/font
|
||||
VPATH += :$(LVGL_DIR)/$(LVGL_DIR_NAME)/src/font
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
CONFIG_LV_OS_PTHREAD=y
|
||||
CONFIG_LV_USE_OBJ_ID=y
|
||||
CONFIG_LV_USE_OBJ_NAME=y
|
||||
CONFIG_LV_USE_OBJ_ID_BUILTIN=y
|
||||
CONFIG_LV_USE_OBJ_PROPERTY=y
|
||||
CONFIG_LV_USE_OBJ_PROPERTY_NAME=y
|
||||
CONFIG_LV_USE_PRIVATE_API=y
|
||||
# CONFIG_LV_USE_CALENDAR is not set
|
||||
# CONFIG_LV_USE_CHART is not set
|
||||
# CONFIG_LV_USE_CHECKBOX is not set
|
||||
# CONFIG_LV_USE_MSGBOX is not set
|
||||
# CONFIG_LV_USE_ROLLER is not set
|
||||
# CONFIG_LV_USE_SCALE is not set
|
||||
# CONFIG_LV_USE_SLIDER is not set
|
||||
# CONFIG_LV_USE_TABLE is not set
|
||||
# CONFIG_LV_USE_TABVIEW is not set
|
||||
# CONFIG_LV_USE_TILEVIEW is not set
|
||||
CONFIG_LV_USE_QRCODE=y
|
||||
CONFIG_LV_USE_LINUX_FBDEV=y
|
||||
CONFIG_LV_USE_EVDEV=y
|
||||
CONFIG_LV_USE_ST7789=y
|
||||
CONFIG_LV_BUILD_EXAMPLES=n
|
||||
CONFIG_LV_BUILD_DEMOS=n
|
||||
|
|
@ -0,0 +1,195 @@
|
|||
#include <time.h>
|
||||
#include <sys/time.h>
|
||||
#include <stdio.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "log.h"
|
||||
#include "screen.h"
|
||||
#include <lvgl.h>
|
||||
// #include "st7789/lcd.h"
|
||||
#include "ui/ui.h"
|
||||
#include "ui_index.h"
|
||||
|
||||
#define DISP_BUF_SIZE (300 * 240 * 2)
|
||||
static lv_color_t buf[DISP_BUF_SIZE];
|
||||
|
||||
indev_handler_t *indev_handler = NULL;
|
||||
|
||||
void lvgl_set_indev_handler(indev_handler_t *handler) {
|
||||
indev_handler = handler;
|
||||
}
|
||||
|
||||
void handle_indev_event(lv_event_t *e) {
|
||||
if (indev_handler == NULL) {
|
||||
return;
|
||||
}
|
||||
indev_handler(lv_event_get_code(e));
|
||||
}
|
||||
|
||||
void lvgl_init(u_int16_t rotation) {
|
||||
log_trace("initalizing lvgl");
|
||||
|
||||
/*LittlevGL init*/
|
||||
lv_init();
|
||||
|
||||
/*Linux frame buffer device init*/
|
||||
|
||||
/*Linux frame buffer device init*/
|
||||
lv_display_t *disp = lv_linux_fbdev_create();
|
||||
// lv_display_set_physical_resolution(disp, 240, 300);
|
||||
lv_display_set_resolution(disp, 240, 300);
|
||||
lv_linux_fbdev_set_file(disp, "/dev/fb0");
|
||||
|
||||
lvgl_set_rotation(disp, rotation);
|
||||
|
||||
// lv_display_t *disp = lv_st7789_create(LCD_H_RES, LCD_V_RES, LV_LCD_FLAG_NONE, lcd_send_cmd, lcd_send_color);
|
||||
// lv_display_set_resolution(disp, 240, 300);
|
||||
// lv_display_set_rotation(disp, LV_DISP_ROTATION_270);
|
||||
|
||||
// lv_color_t * buf1 = NULL;
|
||||
// lv_color_t * buf2 = NULL;
|
||||
|
||||
// uint32_t buf_size = LCD_H_RES * LCD_V_RES / 10 * lv_color_format_get_size(lv_display_get_color_format(disp));
|
||||
|
||||
// buf1 = lv_malloc(buf_size);
|
||||
// if(buf1 == NULL) {
|
||||
// log_error("display draw buffer malloc failed");
|
||||
// return;
|
||||
// }
|
||||
|
||||
// buf2 = lv_malloc(buf_size);
|
||||
// if(buf2 == NULL) {
|
||||
// log_error("display buffer malloc failed");
|
||||
// lv_free(buf1);
|
||||
// return;
|
||||
// }
|
||||
// lv_display_set_buffers(disp, buf1, buf2, buf_size, LV_DISPLAY_RENDER_MODE_PARTIAL);
|
||||
|
||||
/* Linux input device init */
|
||||
lv_indev_t *mouse = lv_evdev_create(LV_INDEV_TYPE_POINTER, "/dev/input/event1");
|
||||
lv_indev_set_group(mouse, lv_group_get_default());
|
||||
lv_indev_set_display(mouse, disp);
|
||||
|
||||
lv_indev_add_event_cb(mouse, handle_indev_event, LV_EVENT_ALL, NULL);
|
||||
|
||||
log_trace("initalizing ui");
|
||||
|
||||
ui_init();
|
||||
|
||||
log_info("ui initalized");
|
||||
// lv_label_set_text(ui_Boot_Screen_Version, "");
|
||||
// lv_label_set_text(ui_Home_Content_Ip, "...");
|
||||
// lv_label_set_text(ui_Home_Header_Cloud_Status_Label, "0 active");
|
||||
}
|
||||
|
||||
void lvgl_tick(void) {
|
||||
lv_timer_handler();
|
||||
ui_tick();
|
||||
}
|
||||
|
||||
void lvgl_set_rotation(lv_display_t *disp, u_int16_t rotation) {
|
||||
log_info("setting rotation to %d", rotation);
|
||||
if (rotation == 0) {
|
||||
lv_display_set_rotation(disp, LV_DISP_ROTATION_0);
|
||||
} else if (rotation == 90) {
|
||||
lv_display_set_rotation(disp, LV_DISP_ROTATION_90);
|
||||
} else if (rotation == 180) {
|
||||
lv_display_set_rotation(disp, LV_DISP_ROTATION_180);
|
||||
} else if (rotation == 270) {
|
||||
lv_display_set_rotation(disp, LV_DISP_ROTATION_270);
|
||||
} else {
|
||||
log_error("invalid rotation %d", rotation);
|
||||
}
|
||||
|
||||
lv_style_t *flex_screen_style = ui_get_style("flex_screen");
|
||||
if (flex_screen_style == NULL) {
|
||||
log_error("flex_screen style not found");
|
||||
return;
|
||||
}
|
||||
|
||||
lv_style_t *flex_screen_menu_style = ui_get_style("flex_screen_menu");
|
||||
if (flex_screen_menu_style == NULL) {
|
||||
log_error("flex_screen_menu style not found");
|
||||
return;
|
||||
}
|
||||
|
||||
if (rotation == 90) {
|
||||
lv_style_set_pad_left(flex_screen_style, 24);
|
||||
lv_style_set_pad_right(flex_screen_style, 44);
|
||||
} else if (rotation == 270) {
|
||||
lv_style_set_pad_left(flex_screen_style, 44);
|
||||
lv_style_set_pad_right(flex_screen_style, 24);
|
||||
}
|
||||
|
||||
log_info("refreshing objects");
|
||||
lv_obj_report_style_change(&flex_screen_style);
|
||||
lv_obj_report_style_change(&flex_screen_menu_style);
|
||||
}
|
||||
|
||||
uint32_t custom_tick_get(void)
|
||||
{
|
||||
static uint64_t start_ms = 0;
|
||||
if(start_ms == 0) {
|
||||
struct timeval tv_start;
|
||||
gettimeofday(&tv_start, NULL);
|
||||
start_ms = (tv_start.tv_sec * 1000000 + tv_start.tv_usec) / 1000;
|
||||
}
|
||||
|
||||
struct timeval tv_now;
|
||||
gettimeofday(&tv_now, NULL);
|
||||
uint64_t now_ms;
|
||||
now_ms = (tv_now.tv_sec * 1000000 + tv_now.tv_usec) / 1000;
|
||||
|
||||
uint32_t time_ms = now_ms - start_ms;
|
||||
return time_ms;
|
||||
}
|
||||
|
||||
lv_obj_t *ui_get_obj(const char *name) {
|
||||
for (size_t i = 0; i < ui_objects_size; i++) {
|
||||
if (strcmp(ui_objects[i].name, name) == 0) {
|
||||
return *ui_objects[i].obj;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
lv_style_t *ui_get_style(const char *name) {
|
||||
for (size_t i = 0; i < ui_styles_size; i++) {
|
||||
if (strcmp(ui_styles[i].name, name) == 0) {
|
||||
return ui_styles[i].getter();
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
const char *ui_get_current_screen() {
|
||||
lv_obj_t *scr = lv_scr_act();
|
||||
if (scr == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
for (size_t i = 0; i < ui_objects_size; i++) {
|
||||
if (*(ui_objects[i].obj) == scr) {
|
||||
return ui_objects[i].name;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
lv_img_dsc_t *ui_get_image(const char *name) {
|
||||
for (size_t i = 0; i < ui_images_size; i++) {
|
||||
if (strcmp(ui_images[i].name, name) == 0) {
|
||||
return ui_images[i].img;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void ui_set_text(const char *name, const char *text) {
|
||||
lv_obj_t *obj = ui_get_obj(name);
|
||||
if(obj == NULL) {
|
||||
log_error("ui_set_text %s %s, obj not found\n", name, text);
|
||||
return;
|
||||
}
|
||||
lv_label_set_text(obj, text);
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
#ifndef SCREEN_H
|
||||
#define SCREEN_H
|
||||
|
||||
#include <lvgl.h>
|
||||
|
||||
typedef void (indev_handler_t)(lv_event_code_t code);
|
||||
|
||||
void lvgl_set_indev_handler(indev_handler_t *handler);
|
||||
|
||||
void lvgl_init(u_int16_t rotation);
|
||||
void lvgl_tick(void);
|
||||
|
||||
void lvgl_set_rotation(lv_display_t *disp, u_int16_t rotation);
|
||||
|
||||
void ui_set_text(const char *name, const char *text);
|
||||
|
||||
lv_obj_t *ui_get_obj(const char *name);
|
||||
lv_style_t *ui_get_style(const char *name);
|
||||
lv_img_dsc_t *ui_get_image(const char *name);
|
||||
|
||||
#endif // SCREEN_H
|
||||
|
|
@ -0,0 +1 @@
|
|||
../eez/src/ui
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
#!/bin/bash
|
||||
|
||||
cat << EOF > ui_index.c
|
||||
// This file was generated by ui_index.gen.sh, do not edit it manually
|
||||
#include "ui_index.h"
|
||||
|
||||
ui_obj_map ui_objects[] = {
|
||||
$(grep -h "lv_obj_t \*" ui/screens.h | sed 's/lv_obj_t \*//g' | sed 's/;//g' | while read -r line; do
|
||||
echo " {\"$line\", &(objects.$line)},"
|
||||
done)
|
||||
};
|
||||
|
||||
const int ui_objects_size = sizeof(ui_objects) / sizeof(ui_objects[0]);
|
||||
|
||||
ui_style_map ui_styles[] = {
|
||||
$(grep 'lv_style_t \*get_style_' ui/styles.h | sed 's/lv_style_t \*get_style_//g' | sed 's/_MAIN_DEFAULT();//g' | sed 's/\r//' | while read -r line; do
|
||||
echo " {\"$line\", &get_style_${line}_MAIN_DEFAULT},"
|
||||
done)
|
||||
};
|
||||
|
||||
const int ui_styles_size = sizeof(ui_styles) / sizeof(ui_styles[0]);
|
||||
|
||||
ui_img_map ui_images[] = {
|
||||
$(grep "extern const lv_img_dsc_t " ui/images.h | sed 's/extern const lv_img_dsc_t //g' | sed 's/;//g' | while read -r line; do
|
||||
echo " {\"$line\", &$line},"
|
||||
done)
|
||||
};
|
||||
|
||||
const int ui_images_size = sizeof(ui_images) / sizeof(ui_images[0]);
|
||||
|
||||
ui_var_map ui_vars[] = {
|
||||
$(grep 'extern const char \*get_var_' ui/vars.h | sed 's/extern const char \*get_var_//g' | sed 's/();//g' | sed 's/\r//' | while read -r line; do
|
||||
echo " {\"$line\", &get_var_$line, &set_var_$line},"
|
||||
done)
|
||||
};
|
||||
|
||||
const int ui_vars_size = sizeof(ui_vars) / sizeof(ui_vars[0]);
|
||||
EOF
|
||||
|
||||
echo "ui_index.c has been generated successfully."
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
#ifndef UI_INDEX_H
|
||||
#define UI_INDEX_H
|
||||
|
||||
#include "ui/ui.h"
|
||||
#include "ui/screens.h"
|
||||
#include "ui/styles.h"
|
||||
#include "ui/images.h"
|
||||
#include "ui/vars.h"
|
||||
|
||||
typedef struct {
|
||||
const char *name;
|
||||
lv_obj_t **obj; // Pointer to the object pointer, as the object pointer is only populated after the ui is initialized
|
||||
} ui_obj_map;
|
||||
|
||||
extern ui_obj_map ui_objects[];
|
||||
extern const int ui_objects_size;
|
||||
|
||||
typedef struct {
|
||||
const char *name;
|
||||
lv_style_t *(*getter)();
|
||||
} ui_style_map;
|
||||
|
||||
extern ui_style_map ui_styles[];
|
||||
extern const int ui_styles_size;
|
||||
|
||||
typedef struct {
|
||||
const char *name;
|
||||
const lv_img_dsc_t *img; // Pointer to the image descriptor const
|
||||
} ui_img_map;
|
||||
|
||||
extern ui_img_map ui_images[];
|
||||
extern const int ui_images_size;
|
||||
|
||||
typedef struct {
|
||||
const char *name;
|
||||
const char *(*getter)();
|
||||
void (*setter)(const char *value);
|
||||
} ui_var_map;
|
||||
|
||||
extern ui_var_map ui_vars[];
|
||||
extern const int ui_vars_size;
|
||||
|
||||
#endif // UI_INDEX_H
|
||||
|
|
@ -0,0 +1,728 @@
|
|||
#define _POSIX_C_SOURCE 200809L
|
||||
#include <unistd.h>
|
||||
#include <time.h>
|
||||
#include <rk_type.h>
|
||||
#include <rk_mpi_venc.h>
|
||||
#include <rk_mpi_sys.h>
|
||||
#include <string.h>
|
||||
#include <rk_debug.h>
|
||||
#include <malloc.h>
|
||||
#include <stdbool.h>
|
||||
#include <rk_mpi_mb.h>
|
||||
#include <fcntl.h>
|
||||
#include <linux/videodev2.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <errno.h>
|
||||
#include <unistd.h>
|
||||
#include <stdatomic.h>
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/in.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <rk_mpi_mmz.h>
|
||||
#include <pthread.h>
|
||||
#include <assert.h>
|
||||
#include <sys/un.h>
|
||||
#include <sys/socket.h>
|
||||
#include "video.h"
|
||||
#include "ctrl.h"
|
||||
#include "log.h"
|
||||
|
||||
#define VIDEO_DEV "/dev/video0"
|
||||
#define SUB_DEV "/dev/v4l-subdev2"
|
||||
|
||||
#define RK_ALIGN(x, a) (((x) + (a)-1) & ~((a)-1))
|
||||
#define RK_ALIGN_2(x) RK_ALIGN(x, 2)
|
||||
#define RK_ALIGN_16(x) RK_ALIGN(x, 16)
|
||||
#define RK_ALIGN_32(x) RK_ALIGN(x, 32)
|
||||
|
||||
int sub_dev_fd = -1;
|
||||
#define VENC_CHANNEL 0
|
||||
MB_POOL memPool = MB_INVALID_POOLID;
|
||||
|
||||
bool should_exit = false;
|
||||
float quality_factor = 1.0f;
|
||||
|
||||
static void *venc_read_stream(void *arg);
|
||||
|
||||
RK_U64 get_us()
|
||||
{
|
||||
struct timespec time = {0, 0};
|
||||
clock_gettime(CLOCK_MONOTONIC, &time);
|
||||
return (RK_U64)time.tv_sec * 1000000 + (RK_U64)time.tv_nsec / 1000; /* microseconds */
|
||||
}
|
||||
|
||||
double calculate_bitrate(float bitrate_factor, int width, int height)
|
||||
{
|
||||
const int32_t base_bitrate_high = 2000;
|
||||
const int32_t base_bitrate_low = 512;
|
||||
|
||||
double pixels = (double)width * height;
|
||||
double ref_pixels = 1920.0 * 1080.0;
|
||||
|
||||
double scale_factor = pixels / ref_pixels;
|
||||
|
||||
int32_t base_bitrate = base_bitrate_low + (int32_t)((base_bitrate_high - base_bitrate_low) * bitrate_factor);
|
||||
|
||||
int32_t bitrate = (int32_t)(base_bitrate * scale_factor);
|
||||
|
||||
const int32_t min_bitrate = 100;
|
||||
if (bitrate < min_bitrate)
|
||||
{
|
||||
bitrate = min_bitrate;
|
||||
}
|
||||
|
||||
return bitrate;
|
||||
}
|
||||
|
||||
static void populate_venc_attr(VENC_CHN_ATTR_S *stAttr, RK_U32 bitrate, RK_U32 max_bitrate, RK_U32 width, RK_U32 height)
|
||||
{
|
||||
memset(stAttr, 0, sizeof(VENC_CHN_ATTR_S));
|
||||
|
||||
stAttr->stRcAttr.enRcMode = VENC_RC_MODE_H264VBR;
|
||||
stAttr->stRcAttr.stH264Vbr.u32BitRate = bitrate;
|
||||
stAttr->stRcAttr.stH264Vbr.u32MaxBitRate = max_bitrate;
|
||||
stAttr->stRcAttr.stH264Vbr.u32Gop = 60;
|
||||
|
||||
stAttr->stVencAttr.enType = RK_VIDEO_ID_AVC;
|
||||
stAttr->stVencAttr.enPixelFormat = RK_FMT_YUV422_YUYV;
|
||||
stAttr->stVencAttr.u32Profile = H264E_PROFILE_HIGH;
|
||||
stAttr->stVencAttr.u32PicWidth = width;
|
||||
stAttr->stVencAttr.u32PicHeight = height;
|
||||
// stAttr->stVencAttr.u32VirWidth = (width + 15) & (~15);
|
||||
// stAttr->stVencAttr.u32VirHeight = (height + 15) & (~15);
|
||||
stAttr->stVencAttr.u32VirWidth = RK_ALIGN_2(width);
|
||||
stAttr->stVencAttr.u32VirHeight = RK_ALIGN_2(height);
|
||||
stAttr->stVencAttr.u32StreamBufCnt = 3;
|
||||
stAttr->stVencAttr.u32BufSize = width * height * 3 / 2;
|
||||
stAttr->stVencAttr.enMirror = MIRROR_NONE;
|
||||
}
|
||||
|
||||
pthread_t *venc_read_thread = NULL;
|
||||
volatile bool venc_running = false;
|
||||
static int32_t venc_start(int32_t bitrate, int32_t max_bitrate, int32_t width, int32_t height)
|
||||
{
|
||||
int32_t ret;
|
||||
VENC_CHN_ATTR_S stAttr;
|
||||
populate_venc_attr(&stAttr, bitrate, max_bitrate, width, height);
|
||||
|
||||
ret = RK_MPI_VENC_CreateChn(VENC_CHANNEL, &stAttr);
|
||||
if (ret < 0)
|
||||
{
|
||||
RK_LOGE("error RK_MPI_VENC_CreateChn, %d", ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
VENC_RECV_PIC_PARAM_S stRecvParam;
|
||||
memset(&stRecvParam, 0, sizeof(VENC_RECV_PIC_PARAM_S));
|
||||
stRecvParam.s32RecvPicNum = -1;
|
||||
ret = RK_MPI_VENC_StartRecvFrame(VENC_CHANNEL, &stRecvParam);
|
||||
if (ret < 0)
|
||||
{
|
||||
RK_LOGE("error RK_MPI_VENC_StartRecvFrame, %d", ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
venc_running = true;
|
||||
venc_read_thread = malloc(sizeof(pthread_t));
|
||||
if (pthread_create(venc_read_thread, NULL, venc_read_stream, NULL) != 0)
|
||||
{
|
||||
RK_LOGE("Failed to create venc_read_thread");
|
||||
return RK_FAILURE;
|
||||
}
|
||||
|
||||
return RK_SUCCESS;
|
||||
}
|
||||
|
||||
static int32_t venc_stop()
|
||||
{
|
||||
venc_running = false;
|
||||
|
||||
int32_t ret;
|
||||
ret = RK_MPI_VENC_StopRecvFrame(VENC_CHANNEL);
|
||||
if (ret != RK_SUCCESS)
|
||||
{
|
||||
RK_LOGE("Failed to stop receiving frames for VENC_CHANNEL, error code: %d", ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
if (venc_read_thread != NULL)
|
||||
{
|
||||
pthread_join(*venc_read_thread, NULL);
|
||||
free(venc_read_thread);
|
||||
venc_read_thread = NULL;
|
||||
}
|
||||
|
||||
ret = RK_MPI_VENC_DestroyChn(VENC_CHANNEL);
|
||||
if (ret != RK_SUCCESS)
|
||||
{
|
||||
RK_LOGE("Failed to destroy VENC_CHANNEL, error code: %d", ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
return RK_SUCCESS;
|
||||
}
|
||||
|
||||
struct buffer
|
||||
{
|
||||
struct v4l2_plane plane_buffer;
|
||||
MB_BLK mb_blk;
|
||||
};
|
||||
|
||||
const int input_buffer_count = 3;
|
||||
|
||||
static int32_t buf_init()
|
||||
{
|
||||
MB_POOL_CONFIG_S stMbPoolCfg;
|
||||
memset(&stMbPoolCfg, 0, sizeof(MB_POOL_CONFIG_S));
|
||||
stMbPoolCfg.u64MBSize = 1920 * 1080 * 3; // max resolution
|
||||
stMbPoolCfg.u32MBCnt = input_buffer_count;
|
||||
stMbPoolCfg.enAllocType = MB_ALLOC_TYPE_DMA;
|
||||
stMbPoolCfg.bPreAlloc = RK_TRUE;
|
||||
memPool = RK_MPI_MB_CreatePool(&stMbPoolCfg);
|
||||
if (memPool == MB_INVALID_POOLID)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
log_info("created memory pool");
|
||||
|
||||
return RK_SUCCESS;
|
||||
}
|
||||
|
||||
pthread_t *format_thread = NULL;
|
||||
|
||||
int video_init()
|
||||
{
|
||||
if (RK_MPI_SYS_Init() != RK_SUCCESS)
|
||||
{
|
||||
log_error("RK_MPI_SYS_Init failed");
|
||||
return RK_FAILURE;
|
||||
}
|
||||
|
||||
if (sub_dev_fd < 0)
|
||||
{
|
||||
sub_dev_fd = open(SUB_DEV, O_RDWR);
|
||||
if (sub_dev_fd < 0)
|
||||
{
|
||||
log_error("failed to open control sub device %s: %s", SUB_DEV, strerror(errno));
|
||||
return errno;
|
||||
}
|
||||
log_info("opened control sub device %s", SUB_DEV);
|
||||
}
|
||||
|
||||
int32_t ret = buf_init();
|
||||
if (ret != RK_SUCCESS)
|
||||
{
|
||||
log_error("buf_init failed with error: %d", ret);
|
||||
return ret;
|
||||
}
|
||||
log_info("buf_init completed successfully");
|
||||
|
||||
format_thread = malloc(sizeof(pthread_t));
|
||||
pthread_create(format_thread, NULL, run_detect_format, NULL);
|
||||
return RK_SUCCESS;
|
||||
}
|
||||
|
||||
// static int32_t venc_set_param(int32_t bitrate, int32_t max_bitrate, int32_t width, int32_t height)
|
||||
// {
|
||||
|
||||
// VENC_CHN_ATTR_S stAttr;
|
||||
// populate_venc_attr(&stAttr, bitrate, max_bitrate, width, height);
|
||||
// VENC_CHN_PARAM_S stParam;
|
||||
// memset(&stParam, 0, sizeof(VENC_CHN_PARAM_S));
|
||||
|
||||
// RK_MPI_VENC_StopRecvFrame(VENC_CHANNEL);
|
||||
|
||||
// int32_t ret = RK_MPI_VENC_SetChnParam(VENC_CHANNEL, &stAttr);
|
||||
// if (ret < 0)
|
||||
// {
|
||||
// RK_LOGE("error RK_MPI_VENC_SetChnParam, %d", ret);
|
||||
// return ret;
|
||||
// }
|
||||
// VENC_RECV_PIC_PARAM_S stRecvParam;
|
||||
// memset(&stRecvParam, 0, sizeof(VENC_RECV_PIC_PARAM_S));
|
||||
// stRecvParam.s32RecvPicNum = -1;
|
||||
// ret = RK_MPI_VENC_StartRecvFrame(VENC_CHANNEL, &stRecvParam);
|
||||
// if (ret < 0)
|
||||
// {
|
||||
// RK_LOGE("error RK_MPI_VENC_StartRecvFrame, %d", ret);
|
||||
// return ret;
|
||||
// }
|
||||
|
||||
// return RK_SUCCESS;
|
||||
// }
|
||||
|
||||
/**
|
||||
* @brief Continuously reads encoded video streams and sends them over unix socket.
|
||||
*
|
||||
* @param arg Unused parameter (void pointer for thread compatibility)
|
||||
* @return NULL Always returns NULL
|
||||
*/
|
||||
static void *venc_read_stream(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
void *pData = RK_NULL;
|
||||
int loopCount = 0;
|
||||
int s32Ret;
|
||||
|
||||
VENC_STREAM_S stFrame;
|
||||
stFrame.pstPack = malloc(sizeof(VENC_PACK_S));
|
||||
while (venc_running)
|
||||
{
|
||||
// printf("RK_MPI_VENC_GetStream\n");
|
||||
s32Ret = RK_MPI_VENC_GetStream(VENC_CHANNEL, &stFrame, 200); // blocks max 200ms
|
||||
if (s32Ret == RK_SUCCESS)
|
||||
{
|
||||
RK_U64 nowUs = get_us();
|
||||
// printf("chn:0, loopCount:%d enc->seq:%d wd:%d pts=%llu delay=%lldus\n",
|
||||
// loopCount, stFrame.u32Seq, stFrame.pstPack->u32Len,
|
||||
// stFrame.pstPack->u64PTS, nowUs - stFrame.pstPack->u64PTS);
|
||||
pData = RK_MPI_MB_Handle2VirAddr(stFrame.pstPack->pMbBlk);
|
||||
video_send_frame(pData, (ssize_t)stFrame.pstPack->u32Len);
|
||||
s32Ret = RK_MPI_VENC_ReleaseStream(VENC_CHANNEL, &stFrame);
|
||||
if (s32Ret != RK_SUCCESS)
|
||||
{
|
||||
log_error("RK_MPI_VENC_ReleaseStream fail %x", s32Ret);
|
||||
}
|
||||
loopCount++;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (s32Ret == RK_ERR_VENC_BUF_EMPTY)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
log_error("RK_MPI_VENC_GetStream fail %x", s32Ret);
|
||||
break;
|
||||
}
|
||||
}
|
||||
log_info("exiting venc_read_stream");
|
||||
free(stFrame.pstPack);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
uint32_t detected_width, detected_height;
|
||||
bool detected_signal = false, streaming_flag = false;
|
||||
|
||||
pthread_t *streaming_thread = NULL;
|
||||
|
||||
void write_buffer_to_file(const uint8_t *buffer, size_t length, const char *filename)
|
||||
{
|
||||
FILE *file = fopen(filename, "wb");
|
||||
fwrite(buffer, 1, length, file);
|
||||
fclose(file);
|
||||
}
|
||||
|
||||
void *run_video_stream(void *arg)
|
||||
{
|
||||
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
|
||||
|
||||
log_info("running video stream");
|
||||
|
||||
while (streaming_flag)
|
||||
{
|
||||
if (detected_signal == false)
|
||||
{
|
||||
usleep(100000);
|
||||
continue;
|
||||
}
|
||||
|
||||
int video_dev_fd = open(VIDEO_DEV, O_RDWR);
|
||||
if (video_dev_fd < 0)
|
||||
{
|
||||
log_error("failed to open video capture device %s: %s", VIDEO_DEV, strerror(errno));
|
||||
usleep(1000000);
|
||||
continue;
|
||||
}
|
||||
log_info("opened video capture device %s", VIDEO_DEV);
|
||||
|
||||
uint32_t width = detected_width;
|
||||
uint32_t height = detected_height;
|
||||
struct v4l2_format fmt;
|
||||
memset(&fmt, 0, sizeof(struct v4l2_format));
|
||||
fmt.type = type;
|
||||
fmt.fmt.pix_mp.width = width;
|
||||
fmt.fmt.pix_mp.height = height;
|
||||
fmt.fmt.pix_mp.pixelformat = V4L2_PIX_FMT_YUYV;
|
||||
fmt.fmt.pix_mp.field = V4L2_FIELD_ANY;
|
||||
|
||||
if (ioctl(video_dev_fd, VIDIOC_S_FMT, &fmt) < 0)
|
||||
{
|
||||
perror("Set format fail");
|
||||
usleep(100000); // Sleep for 100 milliseconds
|
||||
close(video_dev_fd);
|
||||
continue;
|
||||
}
|
||||
|
||||
struct v4l2_buffer buf;
|
||||
|
||||
struct v4l2_requestbuffers req;
|
||||
req.count = input_buffer_count;
|
||||
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
|
||||
req.memory = V4L2_MEMORY_DMABUF;
|
||||
|
||||
if (ioctl(video_dev_fd, VIDIOC_REQBUFS, &req) < 0)
|
||||
{
|
||||
perror("VIDIOC_REQBUFS failed");
|
||||
return errno;
|
||||
}
|
||||
log_info("VIDIOC_REQBUFS successful");
|
||||
|
||||
struct buffer buffers[3] = {};
|
||||
log_info("allocated buffers");
|
||||
|
||||
for (int i = 0; i < input_buffer_count; i++)
|
||||
{
|
||||
struct v4l2_plane *planes_buffer = &buffers[i].plane_buffer;
|
||||
memset(planes_buffer, 0, sizeof(struct v4l2_plane));
|
||||
|
||||
memset(&buf, 0, sizeof(buf));
|
||||
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
|
||||
buf.memory = V4L2_MEMORY_DMABUF;
|
||||
buf.m.planes = planes_buffer;
|
||||
buf.length = 1;
|
||||
buf.index = i;
|
||||
|
||||
if (-1 == ioctl(video_dev_fd, VIDIOC_QUERYBUF, &buf))
|
||||
{
|
||||
perror("VIDIOC_QUERYBUF failed");
|
||||
req.count = i;
|
||||
return errno;
|
||||
}
|
||||
printf("VIDIOC_QUERYBUF successful for buffer %d\n", i);
|
||||
|
||||
printf("plane: length = %d\n", planes_buffer->length);
|
||||
printf("plane: offset = %d\n", planes_buffer->m.mem_offset);
|
||||
|
||||
MB_BLK blk = RK_MPI_MB_GetMB(memPool, (planes_buffer)->length, RK_TRUE);
|
||||
if (blk == NULL)
|
||||
{
|
||||
RK_LOGE("get mb blk failed!");
|
||||
return -1;
|
||||
}
|
||||
printf("Got memory block for buffer %d\n", i);
|
||||
|
||||
buffers[i].mb_blk = blk;
|
||||
|
||||
RK_S32 buf_fd = (RK_MPI_MB_Handle2Fd(blk));
|
||||
if (buf_fd < 0)
|
||||
{
|
||||
RK_LOGE("RK_MPI_MB_Handle2Fd failed!");
|
||||
return -1;
|
||||
}
|
||||
printf("Converted memory block to file descriptor for buffer %d\n", i);
|
||||
planes_buffer->m.fd = buf_fd;
|
||||
}
|
||||
|
||||
for (int i = 0; i < input_buffer_count; ++i)
|
||||
{
|
||||
struct v4l2_buffer buf;
|
||||
memset(&buf, 0, sizeof(buf));
|
||||
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
|
||||
buf.memory = V4L2_MEMORY_DMABUF;
|
||||
buf.length = 1;
|
||||
buf.index = i;
|
||||
buf.m.planes = &buffers[i].plane_buffer;
|
||||
if (ioctl(video_dev_fd, VIDIOC_QBUF, &buf) < 0)
|
||||
{
|
||||
perror("VIDIOC_QBUF failed");
|
||||
return errno;
|
||||
}
|
||||
printf("VIDIOC_QBUF successful for buffer %d\n", i);
|
||||
}
|
||||
|
||||
if (ioctl(video_dev_fd, VIDIOC_STREAMON, &type) < 0)
|
||||
{
|
||||
perror("VIDIOC_STREAMON failed");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
struct v4l2_plane tmp_plane;
|
||||
|
||||
// Set VENC parameters
|
||||
int32_t bitrate = calculate_bitrate(quality_factor, width, height);
|
||||
RK_S32 ret = venc_start(bitrate, bitrate * 2, width, height);
|
||||
if (ret != RK_SUCCESS)
|
||||
{
|
||||
log_error("Set VENC parameters failed with %#x", ret);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
fd_set fds;
|
||||
struct timeval tv;
|
||||
int r;
|
||||
uint32_t num = 0;
|
||||
VIDEO_FRAME_INFO_S stFrame;
|
||||
|
||||
while (streaming_flag)
|
||||
{
|
||||
FD_ZERO(&fds);
|
||||
FD_SET(video_dev_fd, &fds);
|
||||
tv.tv_sec = 1;
|
||||
tv.tv_usec = 0;
|
||||
|
||||
r = select(video_dev_fd + 1, &fds, NULL, NULL, &tv);
|
||||
if (r == 0)
|
||||
{
|
||||
log_info("select timeout \n");
|
||||
break;
|
||||
}
|
||||
if (r == -1)
|
||||
{
|
||||
if (errno == EINTR)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
perror("select in video streaming");
|
||||
break;
|
||||
}
|
||||
memset(&buf, 0, sizeof(buf));
|
||||
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
|
||||
buf.memory = V4L2_MEMORY_DMABUF;
|
||||
buf.m.planes = &tmp_plane;
|
||||
buf.length = 1;
|
||||
if (ioctl(video_dev_fd, VIDIOC_DQBUF, &buf) < 0)
|
||||
{
|
||||
perror("VIDIOC_DQBUF failed");
|
||||
break;
|
||||
}
|
||||
// printf("got frame, bytesused = %d\n", tmp_plane.bytesused);
|
||||
memset(&stFrame, 0, sizeof(VIDEO_FRAME_INFO_S));
|
||||
MB_BLK blk = RK_NULL;
|
||||
blk = RK_MPI_MMZ_Fd2Handle(tmp_plane.m.fd);
|
||||
assert(blk != RK_NULL);
|
||||
stFrame.stVFrame.pMbBlk = blk;
|
||||
stFrame.stVFrame.u32Width = width;
|
||||
stFrame.stVFrame.u32Height = height;
|
||||
// stFrame.stVFrame.u32VirWidth = (width + 15) & (~15);
|
||||
// stFrame.stVFrame.u32VirHeight = (height + 15) & (~15);
|
||||
stFrame.stVFrame.u32VirWidth = RK_ALIGN_2(width);
|
||||
stFrame.stVFrame.u32VirHeight = RK_ALIGN_2(height);
|
||||
stFrame.stVFrame.u32TimeRef = num; // frame number
|
||||
stFrame.stVFrame.u64PTS = get_us();
|
||||
stFrame.stVFrame.enPixelFormat = RK_FMT_YUV422_YUYV;
|
||||
stFrame.stVFrame.u32FrameFlag |= 0;
|
||||
stFrame.stVFrame.enCompressMode = COMPRESS_MODE_NONE;
|
||||
bool retried = false;
|
||||
// if (num == 100) {
|
||||
// RK_VOID *pData = RK_MPI_MB_Handle2VirAddr(stFrame.stVFrame.pMbBlk);
|
||||
// if (pData) {
|
||||
// size_t frameSize = tmp_plane.bytesused; // Use the actual size reported by the driver
|
||||
// write_buffer_to_file(pData, frameSize, "/userdata/banana.raw");
|
||||
// printf("Frame 100 written to /userdata/banana.raw\n");
|
||||
// } else {
|
||||
// printf("Failed to get virtual address for frame 100\n");
|
||||
// }
|
||||
// }
|
||||
retry_send_frame:
|
||||
if (RK_MPI_VENC_SendFrame(VENC_CHANNEL, &stFrame, 2000) != RK_SUCCESS)
|
||||
{
|
||||
if (retried == true)
|
||||
{
|
||||
RK_LOGE("RK_MPI_VENC_SendFrame retry failed");
|
||||
}
|
||||
else
|
||||
{
|
||||
RK_LOGE("RK_MPI_VENC_SendFrame failed,retrying");
|
||||
retried = true;
|
||||
usleep(1000llu);
|
||||
goto retry_send_frame;
|
||||
}
|
||||
}
|
||||
|
||||
num++;
|
||||
|
||||
if (ioctl(video_dev_fd, VIDIOC_QBUF, &buf) < 0)
|
||||
printf("failture VIDIOC_QBUF\n");
|
||||
}
|
||||
cleanup:
|
||||
if (ioctl(video_dev_fd, VIDIOC_STREAMOFF, &type) < 0)
|
||||
{
|
||||
perror("VIDIOC_STREAMOFF failed");
|
||||
}
|
||||
|
||||
venc_stop();
|
||||
|
||||
for (int i = 0; i < input_buffer_count; i++)
|
||||
{
|
||||
if (buffers[i].mb_blk != NULL)
|
||||
{
|
||||
RK_MPI_MB_ReleaseMB((buffers + i)->mb_blk);
|
||||
}
|
||||
}
|
||||
|
||||
close(video_dev_fd);
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void video_shutdown()
|
||||
{
|
||||
if (should_exit == true)
|
||||
{
|
||||
printf("shutting down in progress already\n");
|
||||
return;
|
||||
}
|
||||
video_stop_streaming();
|
||||
// if (buffers != NULL) {
|
||||
// for (int i = 0; i < input_buffer_count; i++) {
|
||||
// if ((buffers + i)->mb_blk != NULL) {
|
||||
// RK_MPI_MB_ReleaseMB((buffers + i)->mb_blk);
|
||||
// }
|
||||
// free((buffers + i)->planes_buffer);
|
||||
// }
|
||||
// free(buffers);
|
||||
// }
|
||||
should_exit = true;
|
||||
if (sub_dev_fd > 0)
|
||||
{
|
||||
shutdown(sub_dev_fd, SHUT_RDWR);
|
||||
// close(sub_dev_fd);
|
||||
printf("Closed sub_dev_fd\n");
|
||||
}
|
||||
|
||||
if (memPool != MB_INVALID_POOLID)
|
||||
{
|
||||
RK_MPI_MB_DestroyPool(memPool);
|
||||
}
|
||||
printf("Destroyed memory pool\n");
|
||||
// if (format_thread != NULL) {
|
||||
// pthread_join(*format_thread, NULL);
|
||||
// free(format_thread);
|
||||
// format_thread = NULL;
|
||||
// }
|
||||
// printf("Joined format detection thread\n");
|
||||
}
|
||||
|
||||
// TODO: mutex?
|
||||
|
||||
void video_start_streaming()
|
||||
{
|
||||
if (streaming_thread != NULL)
|
||||
{
|
||||
log_info("video streaming already started");
|
||||
return;
|
||||
}
|
||||
streaming_thread = malloc(sizeof(pthread_t));
|
||||
assert(streaming_thread != NULL);
|
||||
streaming_flag = true;
|
||||
pthread_create(streaming_thread, NULL, run_video_stream, NULL);
|
||||
}
|
||||
|
||||
void video_stop_streaming()
|
||||
{
|
||||
if (streaming_thread != NULL)
|
||||
{
|
||||
streaming_flag = false;
|
||||
pthread_join(*streaming_thread, NULL);
|
||||
free(streaming_thread);
|
||||
streaming_thread = NULL;
|
||||
log_info("video streaming stopped");
|
||||
}
|
||||
}
|
||||
|
||||
void *run_detect_format(void *arg)
|
||||
{
|
||||
struct v4l2_event_subscription sub;
|
||||
struct v4l2_event ev;
|
||||
struct v4l2_dv_timings dv_timings;
|
||||
|
||||
memset(&sub, 0, sizeof(sub));
|
||||
sub.type = V4L2_EVENT_SOURCE_CHANGE;
|
||||
if (ioctl(sub_dev_fd, VIDIOC_SUBSCRIBE_EVENT, &sub) == -1)
|
||||
{
|
||||
log_error("cannot subscribe to event");
|
||||
perror("Cannot subscribe to event");
|
||||
goto exit;
|
||||
}
|
||||
|
||||
while (!should_exit)
|
||||
{
|
||||
memset(&dv_timings, 0, sizeof(dv_timings));
|
||||
if (ioctl(sub_dev_fd, VIDIOC_QUERY_DV_TIMINGS, &dv_timings) != 0)
|
||||
{
|
||||
detected_signal = false;
|
||||
if (errno == ENOLINK)
|
||||
{
|
||||
// No timings could be detected because no signal was found.
|
||||
log_info("HDMI status: no signal");
|
||||
video_report_format(false, "no_signal", 0, 0, 0);
|
||||
}
|
||||
else if (errno == ENOLCK)
|
||||
{
|
||||
// The signal was unstable and the hardware could not lock on to it.
|
||||
log_info("HDMI status: no lock");
|
||||
video_report_format(false, "no_lock", 0, 0, 0);
|
||||
}
|
||||
else if (errno == ERANGE)
|
||||
{
|
||||
// Timings were found, but they are out of range of the hardware capabilities.
|
||||
printf("HDMI status: out of range\n");
|
||||
video_report_format(false, "out_of_range", 0, 0, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
perror("error VIDIOC_QUERY_DV_TIMINGS");
|
||||
sleep(1);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
log_info("Active width: %d", dv_timings.bt.width);
|
||||
log_info("Active height: %d", dv_timings.bt.height);
|
||||
double frames_per_second = (double)dv_timings.bt.pixelclock /
|
||||
((dv_timings.bt.height + dv_timings.bt.vfrontporch + dv_timings.bt.vsync +
|
||||
dv_timings.bt.vbackporch) *
|
||||
(dv_timings.bt.width + dv_timings.bt.hfrontporch + dv_timings.bt.hsync +
|
||||
dv_timings.bt.hbackporch));
|
||||
log_info("Frames per second: %.2f fps", frames_per_second);
|
||||
detected_width = dv_timings.bt.width;
|
||||
detected_height = dv_timings.bt.height;
|
||||
detected_signal = true;
|
||||
video_report_format(true, NULL, detected_width, detected_height, frames_per_second);
|
||||
if (streaming_flag == true)
|
||||
{
|
||||
log_info("restarting on going video streaming");
|
||||
video_stop_streaming();
|
||||
video_start_streaming();
|
||||
}
|
||||
}
|
||||
|
||||
memset(&ev, 0, sizeof(ev));
|
||||
if (ioctl(sub_dev_fd, VIDIOC_DQEVENT, &ev) != 0)
|
||||
{
|
||||
log_error("failed to VIDIOC_DQEVENT");
|
||||
perror("failed to VIDIOC_DQEVENT");
|
||||
break;
|
||||
}
|
||||
log_info("New event of type %u", ev.type);
|
||||
if (ev.type != V4L2_EVENT_SOURCE_CHANGE)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
log_info("source change detected!");
|
||||
}
|
||||
exit:
|
||||
close(sub_dev_fd);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
void video_set_quality_factor(float factor)
|
||||
{
|
||||
quality_factor = factor;
|
||||
|
||||
// TODO: update venc bitrate without stopping streaming
|
||||
|
||||
if (streaming_flag == true)
|
||||
{
|
||||
log_info("restarting on going video streaming due to quality factor change");
|
||||
video_stop_streaming();
|
||||
video_start_streaming();
|
||||
}
|
||||
}
|
||||
|
||||
float video_get_quality_factor() {
|
||||
return quality_factor;
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
#ifndef VIDEO_DAEMON_VIDEO_H
|
||||
#define VIDEO_DAEMON_VIDEO_H
|
||||
|
||||
int video_init();
|
||||
void video_shutdown();
|
||||
void *run_detect_format(void *arg);
|
||||
void video_start_streaming();
|
||||
void video_stop_streaming();
|
||||
|
||||
void video_set_quality_factor(float factor);
|
||||
float video_get_quality_factor();
|
||||
|
||||
#endif //VIDEO_DAEMON_VIDEO_H
|
||||
|
|
@ -0,0 +1,280 @@
|
|||
//go:build linux
|
||||
|
||||
package native
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"unsafe"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
/*
|
||||
#cgo LDFLAGS: -Llib -ljknative -llvgl
|
||||
#cgo CFLAGS: -Iinclude
|
||||
#include "ctrl.h"
|
||||
#include <stdlib.h>
|
||||
|
||||
typedef const char cchar_t;
|
||||
typedef const uint8_t cuint8_t;
|
||||
|
||||
extern void jetkvm_go_log_handler(int level, cchar_t *filename, cchar_t *funcname, int line, cchar_t *message);
|
||||
static inline void jetkvm_cgo_setup_log_handler() {
|
||||
jetkvm_set_log_handler(&jetkvm_go_log_handler);
|
||||
}
|
||||
|
||||
extern void jetkvm_go_video_state_handler(jetkvm_video_state_t *state);
|
||||
static inline void jetkvm_cgo_setup_video_state_handler() {
|
||||
jetkvm_set_video_state_handler(&jetkvm_go_video_state_handler);
|
||||
}
|
||||
|
||||
extern void jetkvm_go_video_handler(cuint8_t *frame, ssize_t len);
|
||||
static inline void jetkvm_cgo_setup_video_handler() {
|
||||
jetkvm_set_video_handler(&jetkvm_go_video_handler);
|
||||
}
|
||||
|
||||
extern void jetkvm_go_indev_handler(int code);
|
||||
static inline void jetkvm_cgo_setup_indev_handler() {
|
||||
jetkvm_set_indev_handler(&jetkvm_go_indev_handler);
|
||||
}
|
||||
*/
|
||||
import "C"
|
||||
|
||||
//export jetkvm_go_video_state_handler
|
||||
func jetkvm_go_video_state_handler(state *C.jetkvm_video_state_t) {
|
||||
videoState := VideoState{
|
||||
Ready: bool(state.ready),
|
||||
Error: C.GoString(state.error),
|
||||
Width: int(state.width),
|
||||
Height: int(state.height),
|
||||
FramePerSecond: float64(state.frame_per_second),
|
||||
}
|
||||
videoStateChan <- videoState
|
||||
}
|
||||
|
||||
//export jetkvm_go_log_handler
|
||||
func jetkvm_go_log_handler(level C.int, filename *C.cchar_t, funcname *C.cchar_t, line C.int, message *C.cchar_t) {
|
||||
logMessage := nativeLogMessage{
|
||||
Level: zerolog.Level(level),
|
||||
Message: C.GoString(message),
|
||||
File: C.GoString(filename),
|
||||
FuncName: C.GoString(funcname),
|
||||
Line: int(line),
|
||||
}
|
||||
|
||||
logChan <- logMessage
|
||||
}
|
||||
|
||||
//export jetkvm_go_video_handler
|
||||
func jetkvm_go_video_handler(frame *C.cuint8_t, len C.ssize_t) {
|
||||
videoFrameChan <- C.GoBytes(unsafe.Pointer(frame), C.int(len))
|
||||
}
|
||||
|
||||
//export jetkvm_go_indev_handler
|
||||
func jetkvm_go_indev_handler(code C.int) {
|
||||
indevEventChan <- int(code)
|
||||
}
|
||||
|
||||
var eventCodeToNameMap = map[int]string{}
|
||||
|
||||
func uiEventCodeToName(code int) string {
|
||||
name, ok := eventCodeToNameMap[code]
|
||||
if !ok {
|
||||
cCode := C.int(code)
|
||||
cName := C.jetkvm_ui_event_code_to_name(cCode)
|
||||
name = C.GoString(cName)
|
||||
eventCodeToNameMap[code] = name
|
||||
}
|
||||
|
||||
return name
|
||||
}
|
||||
|
||||
func setUpNativeHandlers() {
|
||||
C.jetkvm_cgo_setup_log_handler()
|
||||
C.jetkvm_cgo_setup_video_state_handler()
|
||||
C.jetkvm_cgo_setup_video_handler()
|
||||
C.jetkvm_cgo_setup_indev_handler()
|
||||
}
|
||||
|
||||
func uiInit(rotation uint16) {
|
||||
cRotation := C.u_int16_t(rotation)
|
||||
defer C.free(unsafe.Pointer(&cRotation))
|
||||
|
||||
C.jetkvm_ui_init(cRotation)
|
||||
}
|
||||
|
||||
func uiTick() {
|
||||
C.jetkvm_ui_tick()
|
||||
}
|
||||
|
||||
func videoInit() error {
|
||||
ret := C.jetkvm_video_init()
|
||||
if ret != 0 {
|
||||
return fmt.Errorf("failed to initialize video: %d", ret)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func videoShutdown() {
|
||||
C.jetkvm_video_shutdown()
|
||||
}
|
||||
|
||||
func videoStart() {
|
||||
C.jetkvm_video_start()
|
||||
}
|
||||
|
||||
func videoStop() {
|
||||
C.jetkvm_video_stop()
|
||||
}
|
||||
|
||||
func uiSetVar(name string, value string) {
|
||||
nameCStr := C.CString(name)
|
||||
defer C.free(unsafe.Pointer(nameCStr))
|
||||
|
||||
valueCStr := C.CString(value)
|
||||
defer C.free(unsafe.Pointer(valueCStr))
|
||||
|
||||
C.jetkvm_ui_set_var(nameCStr, valueCStr)
|
||||
}
|
||||
|
||||
func uiGetVar(name string) string {
|
||||
nameCStr := C.CString(name)
|
||||
defer C.free(unsafe.Pointer(nameCStr))
|
||||
|
||||
return C.GoString(C.jetkvm_ui_get_var(nameCStr))
|
||||
}
|
||||
|
||||
func uiSwitchToScreen(screen string) {
|
||||
screenCStr := C.CString(screen)
|
||||
defer C.free(unsafe.Pointer(screenCStr))
|
||||
C.jetkvm_ui_load_screen(screenCStr)
|
||||
}
|
||||
|
||||
func uiGetCurrentScreen() string {
|
||||
screenCStr := C.jetkvm_ui_get_current_screen()
|
||||
return C.GoString(screenCStr)
|
||||
}
|
||||
|
||||
func uiObjSetState(objName string, state string) (bool, error) {
|
||||
objNameCStr := C.CString(objName)
|
||||
defer C.free(unsafe.Pointer(objNameCStr))
|
||||
stateCStr := C.CString(state)
|
||||
defer C.free(unsafe.Pointer(stateCStr))
|
||||
C.jetkvm_ui_set_state(objNameCStr, stateCStr)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func uiGetLVGLVersion() string {
|
||||
return C.GoString(C.jetkvm_ui_get_lvgl_version())
|
||||
}
|
||||
|
||||
// TODO: use Enum instead of string but it's not a hot path and performance is not a concern now
|
||||
func uiObjAddFlag(objName string, flag string) (bool, error) {
|
||||
objNameCStr := C.CString(objName)
|
||||
defer C.free(unsafe.Pointer(objNameCStr))
|
||||
flagCStr := C.CString(flag)
|
||||
defer C.free(unsafe.Pointer(flagCStr))
|
||||
C.jetkvm_ui_add_flag(objNameCStr, flagCStr)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func uiObjClearFlag(objName string, flag string) (bool, error) {
|
||||
objNameCStr := C.CString(objName)
|
||||
defer C.free(unsafe.Pointer(objNameCStr))
|
||||
flagCStr := C.CString(flag)
|
||||
defer C.free(unsafe.Pointer(flagCStr))
|
||||
C.jetkvm_ui_clear_flag(objNameCStr, flagCStr)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func uiObjHide(objName string) (bool, error) {
|
||||
return uiObjAddFlag(objName, "LV_OBJ_FLAG_HIDDEN")
|
||||
}
|
||||
|
||||
func uiObjShow(objName string) (bool, error) {
|
||||
return uiObjClearFlag(objName, "LV_OBJ_FLAG_HIDDEN")
|
||||
}
|
||||
|
||||
func uiObjSetOpacity(objName string, opacity int) (bool, error) {
|
||||
objNameCStr := C.CString(objName)
|
||||
defer C.free(unsafe.Pointer(objNameCStr))
|
||||
|
||||
C.jetkvm_ui_set_opacity(objNameCStr, C.u_int8_t(opacity))
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func uiObjFadeIn(objName string, duration uint32) (bool, error) {
|
||||
objNameCStr := C.CString(objName)
|
||||
defer C.free(unsafe.Pointer(objNameCStr))
|
||||
|
||||
C.jetkvm_ui_fade_in(objNameCStr, C.u_int32_t(duration))
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func uiObjFadeOut(objName string, duration uint32) (bool, error) {
|
||||
objNameCStr := C.CString(objName)
|
||||
defer C.free(unsafe.Pointer(objNameCStr))
|
||||
|
||||
C.jetkvm_ui_fade_out(objNameCStr, C.u_int32_t(duration))
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func uiLabelSetText(objName string, text string) (bool, error) {
|
||||
objNameCStr := C.CString(objName)
|
||||
defer C.free(unsafe.Pointer(objNameCStr))
|
||||
|
||||
textCStr := C.CString(text)
|
||||
defer C.free(unsafe.Pointer(textCStr))
|
||||
|
||||
ret := C.jetkvm_ui_set_text(objNameCStr, textCStr)
|
||||
if ret < 0 {
|
||||
return false, fmt.Errorf("failed to set text: %d", ret)
|
||||
}
|
||||
return ret == 0, nil
|
||||
}
|
||||
|
||||
func uiImgSetSrc(objName string, src string) (bool, error) {
|
||||
objNameCStr := C.CString(objName)
|
||||
defer C.free(unsafe.Pointer(objNameCStr))
|
||||
|
||||
srcCStr := C.CString(src)
|
||||
defer C.free(unsafe.Pointer(srcCStr))
|
||||
|
||||
C.jetkvm_ui_set_image(objNameCStr, srcCStr)
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func uiDispSetRotation(rotation uint16) (bool, error) {
|
||||
nativeLogger.Info().Uint16("rotation", rotation).Msg("setting rotation")
|
||||
|
||||
cRotation := C.u_int16_t(rotation)
|
||||
defer C.free(unsafe.Pointer(&cRotation))
|
||||
|
||||
C.jetkvm_ui_set_rotation(cRotation)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func videoGetStreamQualityFactor() (float64, error) {
|
||||
factor := C.jetkvm_video_get_quality_factor()
|
||||
return float64(factor), nil
|
||||
}
|
||||
|
||||
func videoSetStreamQualityFactor(factor float64) error {
|
||||
C.jetkvm_video_set_quality_factor(C.float(factor))
|
||||
return nil
|
||||
}
|
||||
|
||||
func videoGetEDID() (string, error) {
|
||||
edidCStr := C.jetkvm_video_get_edid_hex()
|
||||
return C.GoString(edidCStr), nil
|
||||
}
|
||||
|
||||
func videoSetEDID(edid string) error {
|
||||
edidCStr := C.CString(edid)
|
||||
defer C.free(unsafe.Pointer(edidCStr))
|
||||
C.jetkvm_video_set_edid(edidCStr)
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
//go:build !linux
|
||||
|
||||
package native
|
||||
|
||||
func panicPlatformNotSupported() {
|
||||
panic("platform not supported")
|
||||
}
|
||||
|
||||
func setUpNativeHandlers() {
|
||||
panicPlatformNotSupported()
|
||||
}
|
||||
|
||||
func uiSetVar(name string, value string) {
|
||||
panicPlatformNotSupported()
|
||||
}
|
||||
|
||||
func uiGetVar(name string) string {
|
||||
panicPlatformNotSupported()
|
||||
return ""
|
||||
}
|
||||
|
||||
func uiSwitchToScreen(screen string) {
|
||||
panicPlatformNotSupported()
|
||||
}
|
||||
|
||||
func uiGetCurrentScreen() string {
|
||||
panicPlatformNotSupported()
|
||||
return ""
|
||||
}
|
||||
|
||||
func uiObjSetState(objName string, state string) (bool, error) {
|
||||
panicPlatformNotSupported()
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func uiObjAddFlag(objName string, flag string) (bool, error) {
|
||||
panicPlatformNotSupported()
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func uiObjClearFlag(objName string, flag string) (bool, error) {
|
||||
panicPlatformNotSupported()
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func uiObjHide(objName string) (bool, error) {
|
||||
panicPlatformNotSupported()
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func uiObjShow(objName string) (bool, error) {
|
||||
panicPlatformNotSupported()
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func uiObjSetOpacity(objName string, opacity int) (bool, error) {
|
||||
panicPlatformNotSupported()
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func uiObjFadeIn(objName string, duration uint32) (bool, error) {
|
||||
panicPlatformNotSupported()
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func uiObjFadeOut(objName string, duration uint32) (bool, error) {
|
||||
panicPlatformNotSupported()
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func uiLabelSetText(objName string, text string) (bool, error) {
|
||||
panicPlatformNotSupported()
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func uiImgSetSrc(objName string, src string) (bool, error) {
|
||||
panicPlatformNotSupported()
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func uiDispSetRotation(rotation uint16) (bool, error) {
|
||||
panicPlatformNotSupported()
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func uiEventCodeToName(code int) string {
|
||||
panicPlatformNotSupported()
|
||||
return ""
|
||||
}
|
||||
|
||||
func uiGetLVGLVersion() string {
|
||||
panicPlatformNotSupported()
|
||||
return ""
|
||||
}
|
||||
|
||||
func videoGetStreamQualityFactor() (float64, error) {
|
||||
panicPlatformNotSupported()
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func videoSetStreamQualityFactor(factor float64) error {
|
||||
panicPlatformNotSupported()
|
||||
return nil
|
||||
}
|
||||
|
||||
func videoGetEDID() (string, error) {
|
||||
panicPlatformNotSupported()
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func videoSetEDID(edid string) error {
|
||||
panicPlatformNotSupported()
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
package native
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
var (
|
||||
videoFrameChan chan []byte = make(chan []byte)
|
||||
videoStateChan chan VideoState = make(chan VideoState)
|
||||
logChan chan nativeLogMessage = make(chan nativeLogMessage)
|
||||
indevEventChan chan int = make(chan int)
|
||||
)
|
||||
|
||||
func (n *Native) handleVideoFrameChan() {
|
||||
lastFrame := time.Now()
|
||||
for {
|
||||
frame := <-videoFrameChan
|
||||
now := time.Now()
|
||||
sinceLastFrame := now.Sub(lastFrame)
|
||||
lastFrame = now
|
||||
n.onVideoFrameReceived(frame, sinceLastFrame)
|
||||
}
|
||||
}
|
||||
|
||||
func (n *Native) handleVideoStateChan() {
|
||||
for {
|
||||
state := <-videoStateChan
|
||||
n.onVideoStateChange(state)
|
||||
}
|
||||
}
|
||||
|
||||
func (n *Native) handleLogChan() {
|
||||
for {
|
||||
entry := <-logChan
|
||||
l := n.l.With().
|
||||
Str("file", entry.File).
|
||||
Str("func", entry.FuncName).
|
||||
Int("line", entry.Line).
|
||||
Logger()
|
||||
|
||||
switch entry.Level {
|
||||
case zerolog.DebugLevel:
|
||||
l.Debug().Msg(entry.Message)
|
||||
case zerolog.InfoLevel:
|
||||
l.Info().Msg(entry.Message)
|
||||
case zerolog.WarnLevel:
|
||||
l.Warn().Msg(entry.Message)
|
||||
case zerolog.ErrorLevel:
|
||||
l.Error().Msg(entry.Message)
|
||||
case zerolog.PanicLevel:
|
||||
l.Panic().Msg(entry.Message)
|
||||
case zerolog.FatalLevel:
|
||||
l.Fatal().Msg(entry.Message)
|
||||
case zerolog.TraceLevel:
|
||||
l.Trace().Msg(entry.Message)
|
||||
case zerolog.NoLevel:
|
||||
l.Info().Msg(entry.Message)
|
||||
default:
|
||||
l.Info().Msg(entry.Message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (n *Native) handleIndevEventChan() {
|
||||
for {
|
||||
event := <-indevEventChan
|
||||
name := uiEventCodeToName(event)
|
||||
n.onIndevEvent(name)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
cgo/ctrl.h
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
package native
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (n *Native) setUIVars() {
|
||||
uiSetVar("app_version", n.appVersion.String())
|
||||
uiSetVar("system_version", n.systemVersion.String())
|
||||
}
|
||||
|
||||
func (n *Native) initUI() {
|
||||
uiInit(n.displayRotation)
|
||||
n.setUIVars()
|
||||
}
|
||||
|
||||
func (n *Native) tickUI() {
|
||||
for {
|
||||
uiTick()
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
// GetLVGLVersion returns the LVGL version
|
||||
func (n *Native) GetLVGLVersion() (string, error) {
|
||||
return uiGetLVGLVersion(), nil
|
||||
}
|
||||
|
||||
// UIObjHide hides the object
|
||||
func (n *Native) UIObjHide(objName string) (bool, error) {
|
||||
return uiObjHide(objName)
|
||||
}
|
||||
|
||||
// UIObjShow shows the object
|
||||
func (n *Native) UIObjShow(objName string) (bool, error) {
|
||||
return uiObjShow(objName)
|
||||
}
|
||||
|
||||
// UIObjSetState clears the state then adds the new state
|
||||
func (n *Native) UIObjSetState(objName string, state string) (bool, error) {
|
||||
return uiObjSetState(objName, state)
|
||||
}
|
||||
|
||||
// UIObjAddFlag adds the flag to the object
|
||||
func (n *Native) UIObjAddFlag(objName string, flag string) (bool, error) {
|
||||
return uiObjAddFlag(objName, flag)
|
||||
}
|
||||
|
||||
// UIObjClearFlag clears the flag from the object
|
||||
func (n *Native) UIObjClearFlag(objName string, flag string) (bool, error) {
|
||||
return uiObjClearFlag(objName, flag)
|
||||
}
|
||||
|
||||
// UIObjSetOpacity sets the opacity of the object
|
||||
func (n *Native) UIObjSetOpacity(objName string, opacity int) (bool, error) {
|
||||
return uiObjSetOpacity(objName, opacity)
|
||||
}
|
||||
|
||||
// UIObjFadeIn fades in the object
|
||||
func (n *Native) UIObjFadeIn(objName string, duration uint32) (bool, error) {
|
||||
return uiObjFadeIn(objName, duration)
|
||||
}
|
||||
|
||||
// UIObjFadeOut fades out the object
|
||||
func (n *Native) UIObjFadeOut(objName string, duration uint32) (bool, error) {
|
||||
return uiObjFadeOut(objName, duration)
|
||||
}
|
||||
|
||||
// UIObjSetLabelText sets the text of the object
|
||||
func (n *Native) UIObjSetLabelText(objName string, text string) (bool, error) {
|
||||
return uiLabelSetText(objName, text)
|
||||
}
|
||||
|
||||
// UIObjSetImageSrc sets the image of the object
|
||||
func (n *Native) UIObjSetImageSrc(objName string, image string) (bool, error) {
|
||||
return uiImgSetSrc(objName, image)
|
||||
}
|
||||
|
||||
// DisplaySetRotation sets the rotation of the display
|
||||
func (n *Native) DisplaySetRotation(rotation uint16) (bool, error) {
|
||||
return uiDispSetRotation(rotation)
|
||||
}
|
||||
|
||||
// UpdateLabelIfChanged updates the label if the text has changed
|
||||
func (n *Native) UpdateLabelIfChanged(objName string, newText string) {
|
||||
l := n.lD.Trace().Str("obj", objName).Str("text", newText)
|
||||
|
||||
changed, err := n.UIObjSetLabelText(objName, newText)
|
||||
if err != nil {
|
||||
n.lD.Warn().Str("obj", objName).Str("text", newText).Err(err).Msg("failed to update label")
|
||||
return
|
||||
}
|
||||
|
||||
if changed {
|
||||
l.Msg("label changed")
|
||||
} else {
|
||||
l.Msg("label not changed")
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateLabelAndChangeVisibility updates the label and changes the visibility of the object
|
||||
func (n *Native) UpdateLabelAndChangeVisibility(objName string, newText string) {
|
||||
containerName := objName + "_container"
|
||||
if newText == "" {
|
||||
_, _ = n.UIObjHide(objName)
|
||||
_, _ = n.UIObjHide(containerName)
|
||||
} else {
|
||||
_, _ = n.UIObjShow(objName)
|
||||
_, _ = n.UIObjShow(containerName)
|
||||
}
|
||||
|
||||
n.UpdateLabelIfChanged(objName, newText)
|
||||
}
|
||||
|
||||
// SwitchToScreenIf switches to the screen if the screen name is different from the current screen and the screen name is in the shouldSwitch list
|
||||
func (n *Native) SwitchToScreenIf(screenName string, shouldSwitch []string) {
|
||||
currentScreen := uiGetCurrentScreen()
|
||||
if currentScreen == screenName {
|
||||
return
|
||||
}
|
||||
if !slices.Contains(shouldSwitch, currentScreen) {
|
||||
n.lD.Trace().Str("from", currentScreen).Str("to", screenName).Msg("skipping screen switch")
|
||||
return
|
||||
}
|
||||
n.lD.Info().Str("from", currentScreen).Str("to", screenName).Msg("switching screen")
|
||||
uiSwitchToScreen(screenName)
|
||||
}
|
||||
|
||||
// SwitchToScreenIfDifferent switches to the screen if the screen name is different from the current screen
|
||||
func (n *Native) SwitchToScreenIfDifferent(screenName string) {
|
||||
n.SwitchToScreenIf(screenName, []string{})
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
src/ui
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,17 @@
|
|||
package native
|
||||
|
||||
import (
|
||||
"github.com/jetkvm/kvm/internal/logging"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
var nativeLogger = logging.GetSubsystemLogger("native")
|
||||
var displayLogger = logging.GetSubsystemLogger("display")
|
||||
|
||||
type nativeLogMessage struct {
|
||||
Level zerolog.Level
|
||||
Message string
|
||||
File string
|
||||
FuncName string
|
||||
Line int
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
package native
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
type Native struct {
|
||||
ready chan struct{}
|
||||
l *zerolog.Logger
|
||||
lD *zerolog.Logger
|
||||
systemVersion *semver.Version
|
||||
appVersion *semver.Version
|
||||
displayRotation uint16
|
||||
onVideoStateChange func(state VideoState)
|
||||
onVideoFrameReceived func(frame []byte, duration time.Duration)
|
||||
onIndevEvent func(event string)
|
||||
}
|
||||
|
||||
type NativeOptions struct {
|
||||
SystemVersion *semver.Version
|
||||
AppVersion *semver.Version
|
||||
DisplayRotation uint16
|
||||
OnVideoStateChange func(state VideoState)
|
||||
OnVideoFrameReceived func(frame []byte, duration time.Duration)
|
||||
OnIndevEvent func(event string)
|
||||
}
|
||||
|
||||
func NewNative(opts NativeOptions) *Native {
|
||||
onVideoStateChange := opts.OnVideoStateChange
|
||||
if onVideoStateChange == nil {
|
||||
onVideoStateChange = func(state VideoState) {
|
||||
nativeLogger.Info().Msg("video state changed")
|
||||
}
|
||||
}
|
||||
|
||||
onVideoFrameReceived := opts.OnVideoFrameReceived
|
||||
if onVideoFrameReceived == nil {
|
||||
onVideoFrameReceived = func(frame []byte, duration time.Duration) {
|
||||
nativeLogger.Info().Msg("video frame received")
|
||||
}
|
||||
}
|
||||
|
||||
onIndevEvent := opts.OnIndevEvent
|
||||
if onIndevEvent == nil {
|
||||
onIndevEvent = func(event string) {
|
||||
nativeLogger.Info().Str("event", event).Msg("indev event")
|
||||
}
|
||||
}
|
||||
|
||||
return &Native{
|
||||
ready: make(chan struct{}),
|
||||
l: nativeLogger,
|
||||
lD: displayLogger,
|
||||
systemVersion: opts.SystemVersion,
|
||||
appVersion: opts.AppVersion,
|
||||
displayRotation: opts.DisplayRotation,
|
||||
onVideoStateChange: opts.OnVideoStateChange,
|
||||
onVideoFrameReceived: opts.OnVideoFrameReceived,
|
||||
onIndevEvent: opts.OnIndevEvent,
|
||||
}
|
||||
}
|
||||
|
||||
func (n *Native) Start() {
|
||||
// set up singleton
|
||||
setInstance(n)
|
||||
setUpNativeHandlers()
|
||||
|
||||
// start the native video
|
||||
go n.handleLogChan()
|
||||
go n.handleVideoStateChan()
|
||||
go n.handleVideoFrameChan()
|
||||
go n.handleIndevEventChan()
|
||||
|
||||
n.initUI()
|
||||
go n.tickUI()
|
||||
|
||||
close(n.ready)
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package native
|
||||
|
||||
import "sync"
|
||||
|
||||
var (
|
||||
instance *Native
|
||||
instanceLock sync.RWMutex
|
||||
)
|
||||
|
||||
func setInstance(n *Native) {
|
||||
instanceLock.Lock()
|
||||
defer instanceLock.Unlock()
|
||||
|
||||
if instance == nil {
|
||||
instance = n
|
||||
}
|
||||
|
||||
if instance != n {
|
||||
panic("instance is already set")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package native
|
||||
|
||||
type VideoState struct {
|
||||
Ready bool `json:"ready"`
|
||||
Error string `json:"error,omitempty"` //no_signal, no_lock, out_of_range
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
FramePerSecond float64 `json:"fps"`
|
||||
}
|
||||
|
||||
func (n *Native) VideoSetQualityFactor(factor float64) error {
|
||||
return videoSetStreamQualityFactor(factor)
|
||||
}
|
||||
|
||||
func (n *Native) VideoGetQualityFactor() (float64, error) {
|
||||
return videoGetStreamQualityFactor()
|
||||
}
|
||||
|
||||
func (n *Native) VideoSetEDID(edid string) error {
|
||||
return videoSetEDID(edid)
|
||||
}
|
||||
|
||||
func (n *Native) VideoGetEDID() (string, error) {
|
||||
return videoGetEDID()
|
||||
}
|
||||
|
|
@ -3,6 +3,8 @@ package network
|
|||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/guregu/null/v6"
|
||||
|
|
@ -32,8 +34,9 @@ type IPv6StaticConfig struct {
|
|||
DNS []string `json:"dns,omitempty" validate_type:"ipv6" required:"true"`
|
||||
}
|
||||
type NetworkConfig struct {
|
||||
Hostname null.String `json:"hostname,omitempty" validate_type:"hostname"`
|
||||
Domain null.String `json:"domain,omitempty" validate_type:"hostname"`
|
||||
Hostname null.String `json:"hostname,omitempty" validate_type:"hostname"`
|
||||
HTTPProxy null.String `json:"http_proxy,omitempty" validate_type:"proxy"`
|
||||
Domain null.String `json:"domain,omitempty" validate_type:"hostname"`
|
||||
|
||||
IPv4Mode null.String `json:"ipv4_mode,omitempty" one_of:"dhcp,static,disabled" default:"dhcp"`
|
||||
IPv4Static *IPv4StaticConfig `json:"ipv4_static,omitempty" required_if:"IPv4Mode=static"`
|
||||
|
|
@ -41,23 +44,24 @@ type NetworkConfig struct {
|
|||
IPv6Mode null.String `json:"ipv6_mode,omitempty" one_of:"slaac,dhcpv6,slaac_and_dhcpv6,static,link_local,disabled" default:"slaac"`
|
||||
IPv6Static *IPv6StaticConfig `json:"ipv6_static,omitempty" required_if:"IPv6Mode=static"`
|
||||
|
||||
LLDPMode null.String `json:"lldp_mode,omitempty" one_of:"disabled,basic,all" default:"basic"`
|
||||
LLDPMode null.String `json:"lldp_mode,omitempty" one_of:"disabled,rx_only,tx_only,basic,all,enabled" default:"enabled"`
|
||||
LLDPTxTLVs []string `json:"lldp_tx_tlvs,omitempty" one_of:"chassis,port,system,vlan" default:"chassis,port,system,vlan"`
|
||||
MDNSMode null.String `json:"mdns_mode,omitempty" one_of:"disabled,auto,ipv4_only,ipv6_only" default:"auto"`
|
||||
TimeSyncMode null.String `json:"time_sync_mode,omitempty" one_of:"ntp_only,ntp_and_http,http_only,custom" default:"ntp_and_http"`
|
||||
TimeSyncOrdering []string `json:"time_sync_ordering,omitempty" one_of:"http,ntp,ntp_dhcp,ntp_user_provided,ntp_fallback" default:"ntp,http"`
|
||||
TimeSyncOrdering []string `json:"time_sync_ordering,omitempty" one_of:"http,ntp,ntp_dhcp,ntp_user_provided,http_user_provided" default:"ntp,http"`
|
||||
TimeSyncDisableFallback null.Bool `json:"time_sync_disable_fallback,omitempty" default:"false"`
|
||||
TimeSyncParallel null.Int `json:"time_sync_parallel,omitempty" default:"4"`
|
||||
TimeSyncNTPServers []string `json:"time_sync_ntp_servers,omitempty" validate_type:"ipv4_or_ipv6" required_if:"TimeSyncOrdering=ntp_user_provided"`
|
||||
TimeSyncHTTPUrls []string `json:"time_sync_http_urls,omitempty" validate_type:"url" required_if:"TimeSyncOrdering=http_user_provided"`
|
||||
}
|
||||
|
||||
func (c *NetworkConfig) GetMDNSMode() *mdns.MDNSListenOptions {
|
||||
mode := c.MDNSMode.String
|
||||
listenOptions := &mdns.MDNSListenOptions{
|
||||
IPv4: true,
|
||||
IPv6: true,
|
||||
IPv4: c.IPv4Mode.String != "disabled",
|
||||
IPv6: c.IPv6Mode.String != "disabled",
|
||||
}
|
||||
|
||||
switch mode {
|
||||
switch c.MDNSMode.String {
|
||||
case "ipv4_only":
|
||||
listenOptions.IPv6 = false
|
||||
case "ipv6_only":
|
||||
|
|
@ -69,6 +73,18 @@ func (c *NetworkConfig) GetMDNSMode() *mdns.MDNSListenOptions {
|
|||
|
||||
return listenOptions
|
||||
}
|
||||
|
||||
func (s *NetworkConfig) GetTransportProxyFunc() func(*http.Request) (*url.URL, error) {
|
||||
return func(*http.Request) (*url.URL, error) {
|
||||
if s.HTTPProxy.String == "" {
|
||||
return nil, nil
|
||||
} else {
|
||||
proxyUrl, _ := url.Parse(s.HTTPProxy.String)
|
||||
return proxyUrl, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *NetworkInterfaceState) GetHostname() string {
|
||||
hostname := ToValidHostname(s.config.Hostname.String)
|
||||
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ func updateEtcHosts(hostname string, fqdn string) error {
|
|||
hostLine := fmt.Sprintf("127.0.1.1\t%s %s", hostname, fqdn)
|
||||
hostLineExists := false
|
||||
|
||||
for _, line := range strings.Split(string(lines), "\n") {
|
||||
for line := range strings.SplitSeq(string(lines), "\n") {
|
||||
if strings.HasPrefix(line, "127.0.1.1") {
|
||||
hostLineExists = true
|
||||
line = hostLine
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
package network
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/jetkvm/kvm/internal/lldp"
|
||||
)
|
||||
|
||||
func (s *NetworkInterfaceState) shouldStartLLDP() bool {
|
||||
if s.lldp == nil {
|
||||
s.l.Trace().Msg("LLDP not initialized")
|
||||
return false
|
||||
}
|
||||
|
||||
s.l.Trace().Msgf("LLDP mode: %s", s.config.LLDPMode.String)
|
||||
|
||||
return s.config.LLDPMode.String != "disabled"
|
||||
}
|
||||
|
||||
func (s *NetworkInterfaceState) startLLDP() {
|
||||
if !s.shouldStartLLDP() || s.lldp == nil {
|
||||
return
|
||||
}
|
||||
|
||||
s.l.Trace().Msg("starting LLDP")
|
||||
if err := s.lldp.Start(); err != nil {
|
||||
s.l.Error().Err(err).Msg("unable to start LLDP")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *NetworkInterfaceState) stopLLDP() {
|
||||
if s.lldp == nil {
|
||||
return
|
||||
}
|
||||
s.l.Trace().Msg("stopping LLDP")
|
||||
if err := s.lldp.Stop(); err != nil {
|
||||
s.l.Error().Err(err).Msg("unable to stop LLDP")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *NetworkInterfaceState) GetLLDPNeighbors() ([]lldp.Neighbor, error) {
|
||||
if s.lldp == nil {
|
||||
return nil, errors.New("lldp not initialized")
|
||||
}
|
||||
return s.lldp.GetNeighbors(), nil
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"sync"
|
||||
|
||||
"github.com/jetkvm/kvm/internal/confparser"
|
||||
"github.com/jetkvm/kvm/internal/lldp"
|
||||
"github.com/jetkvm/kvm/internal/logging"
|
||||
"github.com/jetkvm/kvm/internal/udhcpc"
|
||||
"github.com/rs/zerolog"
|
||||
|
|
@ -21,6 +22,7 @@ type NetworkInterfaceState struct {
|
|||
ipv6Addr *net.IP
|
||||
ipv6Addresses []IPv6Address
|
||||
ipv6LinkLocal *net.IP
|
||||
ntpAddresses []*net.IP
|
||||
macAddr *net.HardwareAddr
|
||||
|
||||
l *zerolog.Logger
|
||||
|
|
@ -29,6 +31,8 @@ type NetworkInterfaceState struct {
|
|||
config *NetworkConfig
|
||||
dhcpClient *udhcpc.DHCPClient
|
||||
|
||||
lldp *lldp.LLDP
|
||||
|
||||
defaultHostname string
|
||||
currentHostname string
|
||||
currentFqdn string
|
||||
|
|
@ -47,7 +51,7 @@ type NetworkInterfaceOptions struct {
|
|||
DefaultHostname string
|
||||
OnStateChange func(state *NetworkInterfaceState)
|
||||
OnInitialCheck func(state *NetworkInterfaceState)
|
||||
OnDhcpLeaseChange func(lease *udhcpc.Lease)
|
||||
OnDhcpLeaseChange func(lease *udhcpc.Lease, state *NetworkInterfaceState)
|
||||
OnConfigChange func(config *NetworkConfig)
|
||||
NetworkConfig *NetworkConfig
|
||||
}
|
||||
|
|
@ -76,6 +80,7 @@ func NewNetworkInterfaceState(opts *NetworkInterfaceOptions) (*NetworkInterfaceS
|
|||
onInitialCheck: opts.OnInitialCheck,
|
||||
cbConfigChange: opts.OnConfigChange,
|
||||
config: opts.NetworkConfig,
|
||||
ntpAddresses: make([]*net.IP, 0),
|
||||
}
|
||||
|
||||
// create the dhcp client
|
||||
|
|
@ -89,15 +94,31 @@ func NewNetworkInterfaceState(opts *NetworkInterfaceOptions) (*NetworkInterfaceS
|
|||
opts.Logger.Error().Err(err).Msg("failed to update network state")
|
||||
return
|
||||
}
|
||||
|
||||
_ = s.updateNtpServersFromLease(lease)
|
||||
_ = s.setHostnameIfNotSame()
|
||||
|
||||
opts.OnDhcpLeaseChange(lease)
|
||||
opts.OnDhcpLeaseChange(lease, s)
|
||||
},
|
||||
})
|
||||
|
||||
s.dhcpClient = dhcpClient
|
||||
// create the lldp service
|
||||
lldpClient := lldp.NewLLDP(&lldp.LLDPOptions{
|
||||
InterfaceName: opts.InterfaceName,
|
||||
EnableRx: true,
|
||||
EnableTx: true,
|
||||
Logger: l,
|
||||
})
|
||||
|
||||
// create the lldp service
|
||||
lldpClient = lldp.NewLLDP(&lldp.LLDPOptions{
|
||||
InterfaceName: opts.InterfaceName,
|
||||
EnableRx: true,
|
||||
EnableTx: true,
|
||||
Logger: l,
|
||||
})
|
||||
|
||||
s.dhcpClient = dhcpClient
|
||||
s.lldp = lldpClient
|
||||
return s, nil
|
||||
}
|
||||
|
||||
|
|
@ -135,6 +156,27 @@ func (s *NetworkInterfaceState) IPv6String() string {
|
|||
return s.ipv6Addr.String()
|
||||
}
|
||||
|
||||
func (s *NetworkInterfaceState) NtpAddresses() []*net.IP {
|
||||
return s.ntpAddresses
|
||||
}
|
||||
|
||||
func (s *NetworkInterfaceState) NtpAddressesString() []string {
|
||||
ntpServers := []string{}
|
||||
|
||||
if s != nil {
|
||||
s.l.Debug().Any("s", s).Msg("getting NTP address strings")
|
||||
|
||||
if len(s.ntpAddresses) > 0 {
|
||||
for _, server := range s.ntpAddresses {
|
||||
s.l.Debug().IPAddr("server", *server).Msg("converting NTP address")
|
||||
ntpServers = append(ntpServers, server.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ntpServers
|
||||
}
|
||||
|
||||
func (s *NetworkInterfaceState) MAC() *net.HardwareAddr {
|
||||
return s.macAddr
|
||||
}
|
||||
|
|
@ -216,6 +258,10 @@ func (s *NetworkInterfaceState) update() (DhcpTargetState, error) {
|
|||
ipv4Addresses = append(ipv4Addresses, addr.IP)
|
||||
ipv4AddressesString = append(ipv4AddressesString, addr.IPNet.String())
|
||||
} else if addr.IP.To16() != nil {
|
||||
if s.config.IPv6Mode.String == "disabled" {
|
||||
continue
|
||||
}
|
||||
|
||||
scopedLogger := s.l.With().Str("ipv6", addr.IP.String()).Logger()
|
||||
// check if it's a link local address
|
||||
if addr.IP.IsLinkLocalUnicast() {
|
||||
|
|
@ -264,35 +310,37 @@ func (s *NetworkInterfaceState) update() (DhcpTargetState, error) {
|
|||
}
|
||||
s.ipv4Addresses = ipv4AddressesString
|
||||
|
||||
if ipv6LinkLocal != nil {
|
||||
if s.ipv6LinkLocal == nil || s.ipv6LinkLocal.String() != ipv6LinkLocal.String() {
|
||||
scopedLogger := s.l.With().Str("ipv6", ipv6LinkLocal.String()).Logger()
|
||||
if s.ipv6LinkLocal != nil {
|
||||
scopedLogger.Info().
|
||||
Str("old_ipv6", s.ipv6LinkLocal.String()).
|
||||
Msg("IPv6 link local address changed")
|
||||
} else {
|
||||
scopedLogger.Info().Msg("IPv6 link local address found")
|
||||
if s.config.IPv6Mode.String != "disabled" {
|
||||
if ipv6LinkLocal != nil {
|
||||
if s.ipv6LinkLocal == nil || s.ipv6LinkLocal.String() != ipv6LinkLocal.String() {
|
||||
scopedLogger := s.l.With().Str("ipv6", ipv6LinkLocal.String()).Logger()
|
||||
if s.ipv6LinkLocal != nil {
|
||||
scopedLogger.Info().
|
||||
Str("old_ipv6", s.ipv6LinkLocal.String()).
|
||||
Msg("IPv6 link local address changed")
|
||||
} else {
|
||||
scopedLogger.Info().Msg("IPv6 link local address found")
|
||||
}
|
||||
s.ipv6LinkLocal = ipv6LinkLocal
|
||||
changed = true
|
||||
}
|
||||
s.ipv6LinkLocal = ipv6LinkLocal
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
s.ipv6Addresses = ipv6Addresses
|
||||
s.ipv6Addresses = ipv6Addresses
|
||||
|
||||
if len(ipv6Addresses) > 0 {
|
||||
// compare the addresses to see if there's a change
|
||||
if s.ipv6Addr == nil || s.ipv6Addr.String() != ipv6Addresses[0].Address.String() {
|
||||
scopedLogger := s.l.With().Str("ipv6", ipv6Addresses[0].Address.String()).Logger()
|
||||
if s.ipv6Addr != nil {
|
||||
scopedLogger.Info().
|
||||
Str("old_ipv6", s.ipv6Addr.String()).
|
||||
Msg("IPv6 address changed")
|
||||
} else {
|
||||
scopedLogger.Info().Msg("IPv6 address found")
|
||||
if len(ipv6Addresses) > 0 {
|
||||
// compare the addresses to see if there's a change
|
||||
if s.ipv6Addr == nil || s.ipv6Addr.String() != ipv6Addresses[0].Address.String() {
|
||||
scopedLogger := s.l.With().Str("ipv6", ipv6Addresses[0].Address.String()).Logger()
|
||||
if s.ipv6Addr != nil {
|
||||
scopedLogger.Info().
|
||||
Str("old_ipv6", s.ipv6Addr.String()).
|
||||
Msg("IPv6 address changed")
|
||||
} else {
|
||||
scopedLogger.Info().Msg("IPv6 address found")
|
||||
}
|
||||
s.ipv6Addr = &ipv6Addresses[0].Address
|
||||
changed = true
|
||||
}
|
||||
s.ipv6Addr = &ipv6Addresses[0].Address
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -310,14 +358,49 @@ func (s *NetworkInterfaceState) update() (DhcpTargetState, error) {
|
|||
}
|
||||
|
||||
if initialCheck {
|
||||
s.onInitialCheck(s)
|
||||
s.handleInitialCheck()
|
||||
} else if changed {
|
||||
s.onStateChange(s)
|
||||
s.handleStateChange()
|
||||
}
|
||||
|
||||
return dhcpTargetState, nil
|
||||
}
|
||||
|
||||
func (s *NetworkInterfaceState) updateNtpServersFromLease(lease *udhcpc.Lease) error {
|
||||
if lease != nil && len(lease.NTPServers) > 0 {
|
||||
s.l.Info().Msg("lease found, updating DHCP NTP addresses")
|
||||
s.ntpAddresses = make([]*net.IP, 0, len(lease.NTPServers))
|
||||
|
||||
for _, ntpServer := range lease.NTPServers {
|
||||
if ntpServer != nil {
|
||||
s.l.Info().IPAddr("ntp_server", ntpServer).Msg("NTP server found in lease")
|
||||
s.ntpAddresses = append(s.ntpAddresses, &ntpServer)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
s.l.Info().Msg("no NTP servers found in lease")
|
||||
s.ntpAddresses = make([]*net.IP, 0, len(s.config.TimeSyncNTPServers))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *NetworkInterfaceState) handleInitialCheck() {
|
||||
if s.IsUp() {
|
||||
s.startLLDP()
|
||||
}
|
||||
s.onInitialCheck(s)
|
||||
}
|
||||
|
||||
func (s *NetworkInterfaceState) handleStateChange() {
|
||||
if s.IsUp() {
|
||||
s.startLLDP()
|
||||
} else {
|
||||
s.stopLLDP()
|
||||
}
|
||||
s.onStateChange(s)
|
||||
}
|
||||
|
||||
func (s *NetworkInterfaceState) CheckAndUpdateDhcp() error {
|
||||
dhcpTargetState, err := s.update()
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ func (s *NetworkInterfaceState) IPv6LinkLocalAddress() string {
|
|||
func (s *NetworkInterfaceState) RpcGetNetworkState() RpcNetworkState {
|
||||
ipv6Addresses := make([]RpcIPv6Address, 0)
|
||||
|
||||
if s.ipv6Addresses != nil {
|
||||
if s.ipv6Addresses != nil && s.config.IPv6Mode.String != "disabled" {
|
||||
for _, addr := range s.ipv6Addresses {
|
||||
ipv6Addresses = append(ipv6Addresses, RpcIPv6Address{
|
||||
Address: addr.Prefix.String(),
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ func lifetimeToTime(lifetime int) *time.Time {
|
|||
return &t
|
||||
}
|
||||
|
||||
func IsSame(a, b interface{}) bool {
|
||||
func IsSame(a, b any) bool {
|
||||
aJSON, err := json.Marshal(a)
|
||||
if err != nil {
|
||||
return false
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"errors"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
|
@ -19,9 +20,9 @@ var defaultHTTPUrls = []string{
|
|||
// "http://www.msftconnecttest.com/connecttest.txt",
|
||||
}
|
||||
|
||||
func (t *TimeSync) queryAllHttpTime() (now *time.Time) {
|
||||
chunkSize := 4
|
||||
httpUrls := t.httpUrls
|
||||
func (t *TimeSync) queryAllHttpTime(httpUrls []string) (now *time.Time) {
|
||||
chunkSize := int(t.networkConfig.TimeSyncParallel.ValueOr(4))
|
||||
t.l.Info().Strs("httpUrls", httpUrls).Int("chunkSize", chunkSize).Msg("querying HTTP URLs")
|
||||
|
||||
// shuffle the http urls to avoid always querying the same servers
|
||||
rand.Shuffle(len(httpUrls), func(i, j int) { httpUrls[i], httpUrls[j] = httpUrls[j], httpUrls[i] })
|
||||
|
|
@ -57,6 +58,7 @@ func (t *TimeSync) queryMultipleHttp(urls []string, timeout time.Duration) (now
|
|||
ctx,
|
||||
url,
|
||||
timeout,
|
||||
t.networkConfig.GetTransportProxyFunc(),
|
||||
)
|
||||
duration := time.Since(startTime)
|
||||
|
||||
|
|
@ -122,10 +124,16 @@ func queryHttpTime(
|
|||
ctx context.Context,
|
||||
url string,
|
||||
timeout time.Duration,
|
||||
proxyFunc func(*http.Request) (*url.URL, error),
|
||||
) (now *time.Time, response *http.Response, err error) {
|
||||
transport := http.DefaultTransport.(*http.Transport).Clone()
|
||||
transport.Proxy = proxyFunc
|
||||
|
||||
client := http.Client{
|
||||
Timeout: timeout,
|
||||
Transport: transport,
|
||||
Timeout: timeout,
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ var (
|
|||
},
|
||||
[]string{"url"},
|
||||
)
|
||||
|
||||
metricNtpServerInfo = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "jetkvm_timesync_ntp_server_info",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package timesync
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/rand/v2"
|
||||
"strconv"
|
||||
"time"
|
||||
|
|
@ -8,22 +9,37 @@ import (
|
|||
"github.com/beevik/ntp"
|
||||
)
|
||||
|
||||
var defaultNTPServers = []string{
|
||||
var defaultNTPServerIPs = []string{
|
||||
// These servers are known by static IP and as such don't need DNS lookups
|
||||
// These are from Google and Cloudflare since if they're down, the internet
|
||||
// is broken anyway
|
||||
"162.159.200.1", // time.cloudflare.com IPv4
|
||||
"162.159.200.123", // time.cloudflare.com IPv4
|
||||
"2606:4700:f1::1", // time.cloudflare.com IPv6
|
||||
"2606:4700:f1::123", // time.cloudflare.com IPv6
|
||||
"216.239.35.0", // time.google.com IPv4
|
||||
"216.239.35.4", // time.google.com IPv4
|
||||
"216.239.35.8", // time.google.com IPv4
|
||||
"216.239.35.12", // time.google.com IPv4
|
||||
"2001:4860:4806::", // time.google.com IPv6
|
||||
"2001:4860:4806:4::", // time.google.com IPv6
|
||||
"2001:4860:4806:8::", // time.google.com IPv6
|
||||
"2001:4860:4806:c::", // time.google.com IPv6
|
||||
}
|
||||
|
||||
var defaultNTPServerHostnames = []string{
|
||||
// should use something from https://github.com/jauderho/public-ntp-servers
|
||||
"time.apple.com",
|
||||
"time.aws.com",
|
||||
"time.windows.com",
|
||||
"time.google.com",
|
||||
"162.159.200.123", // time.cloudflare.com IPv4
|
||||
"2606:4700:f1::123", // time.cloudflare.com IPv6
|
||||
"0.pool.ntp.org",
|
||||
"1.pool.ntp.org",
|
||||
"2.pool.ntp.org",
|
||||
"3.pool.ntp.org",
|
||||
"time.cloudflare.com",
|
||||
"pool.ntp.org",
|
||||
}
|
||||
|
||||
func (t *TimeSync) queryNetworkTime() (now *time.Time, offset *time.Duration) {
|
||||
chunkSize := 4
|
||||
ntpServers := t.ntpServers
|
||||
func (t *TimeSync) queryNetworkTime(ntpServers []string) (now *time.Time, offset *time.Duration) {
|
||||
chunkSize := int(t.networkConfig.TimeSyncParallel.ValueOr(4))
|
||||
t.l.Info().Strs("servers", ntpServers).Int("chunkSize", chunkSize).Msg("querying NTP servers")
|
||||
|
||||
// shuffle the ntp servers to avoid always querying the same servers
|
||||
rand.Shuffle(len(ntpServers), func(i, j int) { ntpServers[i], ntpServers[j] = ntpServers[j], ntpServers[i] })
|
||||
|
|
@ -46,6 +62,10 @@ type ntpResult struct {
|
|||
|
||||
func (t *TimeSync) queryMultipleNTP(servers []string, timeout time.Duration) (now *time.Time, offset *time.Duration) {
|
||||
results := make(chan *ntpResult, len(servers))
|
||||
|
||||
_, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
for _, server := range servers {
|
||||
go func(server string) {
|
||||
scopedLogger := t.l.With().
|
||||
|
|
@ -66,15 +86,25 @@ func (t *TimeSync) queryMultipleNTP(servers []string, timeout time.Duration) (no
|
|||
return
|
||||
}
|
||||
|
||||
if response.IsKissOfDeath() {
|
||||
scopedLogger.Warn().
|
||||
Str("kiss_code", response.KissCode).
|
||||
Msg("ignoring NTP server kiss of death")
|
||||
results <- nil
|
||||
return
|
||||
}
|
||||
|
||||
rtt := float64(response.RTT.Milliseconds())
|
||||
|
||||
// set the last RTT
|
||||
metricNtpServerLastRTT.WithLabelValues(
|
||||
server,
|
||||
).Set(float64(response.RTT.Milliseconds()))
|
||||
).Set(rtt)
|
||||
|
||||
// set the RTT histogram
|
||||
metricNtpServerRttHistogram.WithLabelValues(
|
||||
server,
|
||||
).Observe(float64(response.RTT.Milliseconds()))
|
||||
).Observe(rtt)
|
||||
|
||||
// set the server info
|
||||
metricNtpServerInfo.WithLabelValues(
|
||||
|
|
@ -91,10 +121,13 @@ func (t *TimeSync) queryMultipleNTP(servers []string, timeout time.Duration) (no
|
|||
scopedLogger.Info().
|
||||
Str("time", now.Format(time.RFC3339)).
|
||||
Str("reference", response.ReferenceString()).
|
||||
Str("rtt", response.RTT.String()).
|
||||
Float64("rtt", rtt).
|
||||
Str("clockOffset", response.ClockOffset.String()).
|
||||
Uint8("stratum", response.Stratum).
|
||||
Msg("NTP server returned time")
|
||||
|
||||
cancel()
|
||||
|
||||
results <- &ntpResult{
|
||||
now: now,
|
||||
offset: &response.ClockOffset,
|
||||
|
|
|
|||
|
|
@ -28,9 +28,8 @@ type TimeSync struct {
|
|||
syncLock *sync.Mutex
|
||||
l *zerolog.Logger
|
||||
|
||||
ntpServers []string
|
||||
httpUrls []string
|
||||
networkConfig *network.NetworkConfig
|
||||
networkConfig *network.NetworkConfig
|
||||
dhcpNtpAddresses []string
|
||||
|
||||
rtcDevicePath string
|
||||
rtcDevice *os.File //nolint:unused
|
||||
|
|
@ -64,14 +63,13 @@ func NewTimeSync(opts *TimeSyncOptions) *TimeSync {
|
|||
}
|
||||
|
||||
t := &TimeSync{
|
||||
syncLock: &sync.Mutex{},
|
||||
l: opts.Logger,
|
||||
rtcDevicePath: rtcDevice,
|
||||
rtcLock: &sync.Mutex{},
|
||||
preCheckFunc: opts.PreCheckFunc,
|
||||
ntpServers: defaultNTPServers,
|
||||
httpUrls: defaultHTTPUrls,
|
||||
networkConfig: opts.NetworkConfig,
|
||||
syncLock: &sync.Mutex{},
|
||||
l: opts.Logger,
|
||||
dhcpNtpAddresses: []string{},
|
||||
rtcDevicePath: rtcDevice,
|
||||
rtcLock: &sync.Mutex{},
|
||||
preCheckFunc: opts.PreCheckFunc,
|
||||
networkConfig: opts.NetworkConfig,
|
||||
}
|
||||
|
||||
if t.rtcDevicePath != "" {
|
||||
|
|
@ -82,34 +80,42 @@ func NewTimeSync(opts *TimeSyncOptions) *TimeSync {
|
|||
return t
|
||||
}
|
||||
|
||||
func (t *TimeSync) SetDhcpNtpAddresses(addresses []string) {
|
||||
t.dhcpNtpAddresses = addresses
|
||||
}
|
||||
|
||||
func (t *TimeSync) getSyncMode() SyncMode {
|
||||
syncMode := SyncMode{
|
||||
Ntp: true,
|
||||
Http: true,
|
||||
Ordering: []string{"ntp_dhcp", "ntp", "http"},
|
||||
NtpUseFallback: true,
|
||||
HttpUseFallback: true,
|
||||
}
|
||||
var syncModeString string
|
||||
|
||||
if t.networkConfig != nil {
|
||||
syncModeString = t.networkConfig.TimeSyncMode.String
|
||||
switch t.networkConfig.TimeSyncMode.String {
|
||||
case "ntp_only":
|
||||
syncMode.Http = false
|
||||
case "http_only":
|
||||
syncMode.Ntp = false
|
||||
}
|
||||
|
||||
if t.networkConfig.TimeSyncDisableFallback.Bool {
|
||||
syncMode.NtpUseFallback = false
|
||||
syncMode.HttpUseFallback = false
|
||||
}
|
||||
|
||||
var syncOrdering = t.networkConfig.TimeSyncOrdering
|
||||
if len(syncOrdering) > 0 {
|
||||
syncMode.Ordering = syncOrdering
|
||||
}
|
||||
}
|
||||
|
||||
switch syncModeString {
|
||||
case "ntp_only":
|
||||
syncMode.Ntp = true
|
||||
case "http_only":
|
||||
syncMode.Http = true
|
||||
default:
|
||||
syncMode.Ntp = true
|
||||
syncMode.Http = true
|
||||
}
|
||||
t.l.Debug().Strs("Ordering", syncMode.Ordering).Bool("Ntp", syncMode.Ntp).Bool("Http", syncMode.Http).Bool("NtpUseFallback", syncMode.NtpUseFallback).Bool("HttpUseFallback", syncMode.HttpUseFallback).Msg("sync mode")
|
||||
|
||||
return syncMode
|
||||
}
|
||||
|
||||
func (t *TimeSync) doTimeSync() {
|
||||
metricTimeSyncStatus.Set(0)
|
||||
for {
|
||||
|
|
@ -152,18 +158,64 @@ func (t *TimeSync) Sync() error {
|
|||
var (
|
||||
now *time.Time
|
||||
offset *time.Duration
|
||||
log zerolog.Logger
|
||||
)
|
||||
|
||||
syncMode := t.getSyncMode()
|
||||
|
||||
metricTimeSyncCount.Inc()
|
||||
|
||||
if syncMode.Ntp {
|
||||
now, offset = t.queryNetworkTime()
|
||||
}
|
||||
syncMode := t.getSyncMode()
|
||||
|
||||
if syncMode.Http && now == nil {
|
||||
now = t.queryAllHttpTime()
|
||||
Orders:
|
||||
for _, mode := range syncMode.Ordering {
|
||||
log = t.l.With().Str("mode", mode).Logger()
|
||||
switch mode {
|
||||
case "ntp_user_provided":
|
||||
if syncMode.Ntp {
|
||||
log.Info().Msg("using NTP custom servers")
|
||||
now, offset = t.queryNetworkTime(t.networkConfig.TimeSyncNTPServers)
|
||||
if now != nil {
|
||||
break Orders
|
||||
}
|
||||
}
|
||||
case "ntp_dhcp":
|
||||
if syncMode.Ntp {
|
||||
log.Info().Msg("using NTP servers from DHCP")
|
||||
now, offset = t.queryNetworkTime(t.dhcpNtpAddresses)
|
||||
if now != nil {
|
||||
break Orders
|
||||
}
|
||||
}
|
||||
case "ntp":
|
||||
if syncMode.Ntp && syncMode.NtpUseFallback {
|
||||
log.Info().Msg("using NTP fallback IPs")
|
||||
now, offset = t.queryNetworkTime(defaultNTPServerIPs)
|
||||
if now == nil {
|
||||
log.Info().Msg("using NTP fallback hostnames")
|
||||
now, offset = t.queryNetworkTime(defaultNTPServerHostnames)
|
||||
}
|
||||
if now != nil {
|
||||
break Orders
|
||||
}
|
||||
}
|
||||
case "http_user_provided":
|
||||
if syncMode.Http {
|
||||
log.Info().Msg("using HTTP custom URLs")
|
||||
now = t.queryAllHttpTime(t.networkConfig.TimeSyncHTTPUrls)
|
||||
if now != nil {
|
||||
break Orders
|
||||
}
|
||||
}
|
||||
case "http":
|
||||
if syncMode.Http && syncMode.HttpUseFallback {
|
||||
log.Info().Msg("using HTTP fallback")
|
||||
now = t.queryAllHttpTime(defaultHTTPUrls)
|
||||
if now != nil {
|
||||
break Orders
|
||||
}
|
||||
}
|
||||
default:
|
||||
log.Warn().Msg("unknown time sync mode, skipping")
|
||||
}
|
||||
}
|
||||
|
||||
if now == nil {
|
||||
|
|
@ -175,6 +227,8 @@ func (t *TimeSync) Sync() error {
|
|||
now = &newNow
|
||||
}
|
||||
|
||||
log.Info().Time("now", *now).Msg("time obtained")
|
||||
|
||||
err := t.setSystemTime(*now)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set system time: %w", err)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,72 @@
|
|||
//go:build ignore
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
var tmpl = `// Code generated by "go run gen.go". DO NOT EDIT.
|
||||
//go:generate env ZONEINFO=$GOROOT/lib/time/zoneinfo.zip go run gen.go -output tzdata.go
|
||||
package tzdata
|
||||
var TimeZones = []string{
|
||||
{{- range . }}
|
||||
"{{.}}",
|
||||
{{- end }}
|
||||
}
|
||||
`
|
||||
|
||||
var filename = flag.String("output", "tzdata.go", "output file name")
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
path := os.Getenv("ZONEINFO")
|
||||
if path == "" {
|
||||
fmt.Println("ZONEINFO is not set")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
fmt.Printf("ZONEINFO %s does not exist\n", path)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
zipfile, err := zip.OpenReader(path)
|
||||
if err != nil {
|
||||
fmt.Printf("Error opening ZONEINFO %s: %v\n", path, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer zipfile.Close()
|
||||
|
||||
timezones := []string{}
|
||||
|
||||
for _, file := range zipfile.File {
|
||||
timezones = append(timezones, file.Name)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
|
||||
tmpl, err := template.New("tzdata").Parse(tmpl)
|
||||
if err != nil {
|
||||
fmt.Printf("Error parsing template: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
err = tmpl.Execute(&buf, timezones)
|
||||
if err != nil {
|
||||
fmt.Printf("Error executing template: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
err = os.WriteFile(*filename, buf.Bytes(), 0644)
|
||||
if err != nil {
|
||||
fmt.Printf("Error writing file %s: %v\n", *filename, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,602 @@
|
|||
// Code generated by "go run gen.go". DO NOT EDIT.
|
||||
//go:generate env ZONEINFO=$GOROOT/lib/time/zoneinfo.zip go run gen.go -output tzdata.go
|
||||
package tzdata
|
||||
var TimeZones = []string{
|
||||
"Africa/Abidjan",
|
||||
"Africa/Accra",
|
||||
"Africa/Addis_Ababa",
|
||||
"Africa/Algiers",
|
||||
"Africa/Asmara",
|
||||
"Africa/Asmera",
|
||||
"Africa/Bamako",
|
||||
"Africa/Bangui",
|
||||
"Africa/Banjul",
|
||||
"Africa/Bissau",
|
||||
"Africa/Blantyre",
|
||||
"Africa/Brazzaville",
|
||||
"Africa/Bujumbura",
|
||||
"Africa/Cairo",
|
||||
"Africa/Casablanca",
|
||||
"Africa/Ceuta",
|
||||
"Africa/Conakry",
|
||||
"Africa/Dakar",
|
||||
"Africa/Dar_es_Salaam",
|
||||
"Africa/Djibouti",
|
||||
"Africa/Douala",
|
||||
"Africa/El_Aaiun",
|
||||
"Africa/Freetown",
|
||||
"Africa/Gaborone",
|
||||
"Africa/Harare",
|
||||
"Africa/Johannesburg",
|
||||
"Africa/Juba",
|
||||
"Africa/Kampala",
|
||||
"Africa/Khartoum",
|
||||
"Africa/Kigali",
|
||||
"Africa/Kinshasa",
|
||||
"Africa/Lagos",
|
||||
"Africa/Libreville",
|
||||
"Africa/Lome",
|
||||
"Africa/Luanda",
|
||||
"Africa/Lubumbashi",
|
||||
"Africa/Lusaka",
|
||||
"Africa/Malabo",
|
||||
"Africa/Maputo",
|
||||
"Africa/Maseru",
|
||||
"Africa/Mbabane",
|
||||
"Africa/Mogadishu",
|
||||
"Africa/Monrovia",
|
||||
"Africa/Nairobi",
|
||||
"Africa/Ndjamena",
|
||||
"Africa/Niamey",
|
||||
"Africa/Nouakchott",
|
||||
"Africa/Ouagadougou",
|
||||
"Africa/Porto-Novo",
|
||||
"Africa/Sao_Tome",
|
||||
"Africa/Timbuktu",
|
||||
"Africa/Tripoli",
|
||||
"Africa/Tunis",
|
||||
"Africa/Windhoek",
|
||||
"America/Adak",
|
||||
"America/Anchorage",
|
||||
"America/Anguilla",
|
||||
"America/Antigua",
|
||||
"America/Araguaina",
|
||||
"America/Argentina/Buenos_Aires",
|
||||
"America/Argentina/Catamarca",
|
||||
"America/Argentina/ComodRivadavia",
|
||||
"America/Argentina/Cordoba",
|
||||
"America/Argentina/Jujuy",
|
||||
"America/Argentina/La_Rioja",
|
||||
"America/Argentina/Mendoza",
|
||||
"America/Argentina/Rio_Gallegos",
|
||||
"America/Argentina/Salta",
|
||||
"America/Argentina/San_Juan",
|
||||
"America/Argentina/San_Luis",
|
||||
"America/Argentina/Tucuman",
|
||||
"America/Argentina/Ushuaia",
|
||||
"America/Aruba",
|
||||
"America/Asuncion",
|
||||
"America/Atikokan",
|
||||
"America/Atka",
|
||||
"America/Bahia",
|
||||
"America/Bahia_Banderas",
|
||||
"America/Barbados",
|
||||
"America/Belem",
|
||||
"America/Belize",
|
||||
"America/Blanc-Sablon",
|
||||
"America/Boa_Vista",
|
||||
"America/Bogota",
|
||||
"America/Boise",
|
||||
"America/Buenos_Aires",
|
||||
"America/Cambridge_Bay",
|
||||
"America/Campo_Grande",
|
||||
"America/Cancun",
|
||||
"America/Caracas",
|
||||
"America/Catamarca",
|
||||
"America/Cayenne",
|
||||
"America/Cayman",
|
||||
"America/Chicago",
|
||||
"America/Chihuahua",
|
||||
"America/Ciudad_Juarez",
|
||||
"America/Coral_Harbour",
|
||||
"America/Cordoba",
|
||||
"America/Costa_Rica",
|
||||
"America/Creston",
|
||||
"America/Cuiaba",
|
||||
"America/Curacao",
|
||||
"America/Danmarkshavn",
|
||||
"America/Dawson",
|
||||
"America/Dawson_Creek",
|
||||
"America/Denver",
|
||||
"America/Detroit",
|
||||
"America/Dominica",
|
||||
"America/Edmonton",
|
||||
"America/Eirunepe",
|
||||
"America/El_Salvador",
|
||||
"America/Ensenada",
|
||||
"America/Fort_Nelson",
|
||||
"America/Fort_Wayne",
|
||||
"America/Fortaleza",
|
||||
"America/Glace_Bay",
|
||||
"America/Godthab",
|
||||
"America/Goose_Bay",
|
||||
"America/Grand_Turk",
|
||||
"America/Grenada",
|
||||
"America/Guadeloupe",
|
||||
"America/Guatemala",
|
||||
"America/Guayaquil",
|
||||
"America/Guyana",
|
||||
"America/Halifax",
|
||||
"America/Havana",
|
||||
"America/Hermosillo",
|
||||
"America/Indiana/Indianapolis",
|
||||
"America/Indiana/Knox",
|
||||
"America/Indiana/Marengo",
|
||||
"America/Indiana/Petersburg",
|
||||
"America/Indiana/Tell_City",
|
||||
"America/Indiana/Vevay",
|
||||
"America/Indiana/Vincennes",
|
||||
"America/Indiana/Winamac",
|
||||
"America/Indianapolis",
|
||||
"America/Inuvik",
|
||||
"America/Iqaluit",
|
||||
"America/Jamaica",
|
||||
"America/Jujuy",
|
||||
"America/Juneau",
|
||||
"America/Kentucky/Louisville",
|
||||
"America/Kentucky/Monticello",
|
||||
"America/Knox_IN",
|
||||
"America/Kralendijk",
|
||||
"America/La_Paz",
|
||||
"America/Lima",
|
||||
"America/Los_Angeles",
|
||||
"America/Louisville",
|
||||
"America/Lower_Princes",
|
||||
"America/Maceio",
|
||||
"America/Managua",
|
||||
"America/Manaus",
|
||||
"America/Marigot",
|
||||
"America/Martinique",
|
||||
"America/Matamoros",
|
||||
"America/Mazatlan",
|
||||
"America/Mendoza",
|
||||
"America/Menominee",
|
||||
"America/Merida",
|
||||
"America/Metlakatla",
|
||||
"America/Mexico_City",
|
||||
"America/Miquelon",
|
||||
"America/Moncton",
|
||||
"America/Monterrey",
|
||||
"America/Montevideo",
|
||||
"America/Montreal",
|
||||
"America/Montserrat",
|
||||
"America/Nassau",
|
||||
"America/New_York",
|
||||
"America/Nipigon",
|
||||
"America/Nome",
|
||||
"America/Noronha",
|
||||
"America/North_Dakota/Beulah",
|
||||
"America/North_Dakota/Center",
|
||||
"America/North_Dakota/New_Salem",
|
||||
"America/Nuuk",
|
||||
"America/Ojinaga",
|
||||
"America/Panama",
|
||||
"America/Pangnirtung",
|
||||
"America/Paramaribo",
|
||||
"America/Phoenix",
|
||||
"America/Port-au-Prince",
|
||||
"America/Port_of_Spain",
|
||||
"America/Porto_Acre",
|
||||
"America/Porto_Velho",
|
||||
"America/Puerto_Rico",
|
||||
"America/Punta_Arenas",
|
||||
"America/Rainy_River",
|
||||
"America/Rankin_Inlet",
|
||||
"America/Recife",
|
||||
"America/Regina",
|
||||
"America/Resolute",
|
||||
"America/Rio_Branco",
|
||||
"America/Rosario",
|
||||
"America/Santa_Isabel",
|
||||
"America/Santarem",
|
||||
"America/Santiago",
|
||||
"America/Santo_Domingo",
|
||||
"America/Sao_Paulo",
|
||||
"America/Scoresbysund",
|
||||
"America/Shiprock",
|
||||
"America/Sitka",
|
||||
"America/St_Barthelemy",
|
||||
"America/St_Johns",
|
||||
"America/St_Kitts",
|
||||
"America/St_Lucia",
|
||||
"America/St_Thomas",
|
||||
"America/St_Vincent",
|
||||
"America/Swift_Current",
|
||||
"America/Tegucigalpa",
|
||||
"America/Thule",
|
||||
"America/Thunder_Bay",
|
||||
"America/Tijuana",
|
||||
"America/Toronto",
|
||||
"America/Tortola",
|
||||
"America/Vancouver",
|
||||
"America/Virgin",
|
||||
"America/Whitehorse",
|
||||
"America/Winnipeg",
|
||||
"America/Yakutat",
|
||||
"America/Yellowknife",
|
||||
"Antarctica/Casey",
|
||||
"Antarctica/Davis",
|
||||
"Antarctica/DumontDUrville",
|
||||
"Antarctica/Macquarie",
|
||||
"Antarctica/Mawson",
|
||||
"Antarctica/McMurdo",
|
||||
"Antarctica/Palmer",
|
||||
"Antarctica/Rothera",
|
||||
"Antarctica/South_Pole",
|
||||
"Antarctica/Syowa",
|
||||
"Antarctica/Troll",
|
||||
"Antarctica/Vostok",
|
||||
"Arctic/Longyearbyen",
|
||||
"Asia/Aden",
|
||||
"Asia/Almaty",
|
||||
"Asia/Amman",
|
||||
"Asia/Anadyr",
|
||||
"Asia/Aqtau",
|
||||
"Asia/Aqtobe",
|
||||
"Asia/Ashgabat",
|
||||
"Asia/Ashkhabad",
|
||||
"Asia/Atyrau",
|
||||
"Asia/Baghdad",
|
||||
"Asia/Bahrain",
|
||||
"Asia/Baku",
|
||||
"Asia/Bangkok",
|
||||
"Asia/Barnaul",
|
||||
"Asia/Beirut",
|
||||
"Asia/Bishkek",
|
||||
"Asia/Brunei",
|
||||
"Asia/Calcutta",
|
||||
"Asia/Chita",
|
||||
"Asia/Choibalsan",
|
||||
"Asia/Chongqing",
|
||||
"Asia/Chungking",
|
||||
"Asia/Colombo",
|
||||
"Asia/Dacca",
|
||||
"Asia/Damascus",
|
||||
"Asia/Dhaka",
|
||||
"Asia/Dili",
|
||||
"Asia/Dubai",
|
||||
"Asia/Dushanbe",
|
||||
"Asia/Famagusta",
|
||||
"Asia/Gaza",
|
||||
"Asia/Harbin",
|
||||
"Asia/Hebron",
|
||||
"Asia/Ho_Chi_Minh",
|
||||
"Asia/Hong_Kong",
|
||||
"Asia/Hovd",
|
||||
"Asia/Irkutsk",
|
||||
"Asia/Istanbul",
|
||||
"Asia/Jakarta",
|
||||
"Asia/Jayapura",
|
||||
"Asia/Jerusalem",
|
||||
"Asia/Kabul",
|
||||
"Asia/Kamchatka",
|
||||
"Asia/Karachi",
|
||||
"Asia/Kashgar",
|
||||
"Asia/Kathmandu",
|
||||
"Asia/Katmandu",
|
||||
"Asia/Khandyga",
|
||||
"Asia/Kolkata",
|
||||
"Asia/Krasnoyarsk",
|
||||
"Asia/Kuala_Lumpur",
|
||||
"Asia/Kuching",
|
||||
"Asia/Kuwait",
|
||||
"Asia/Macao",
|
||||
"Asia/Macau",
|
||||
"Asia/Magadan",
|
||||
"Asia/Makassar",
|
||||
"Asia/Manila",
|
||||
"Asia/Muscat",
|
||||
"Asia/Nicosia",
|
||||
"Asia/Novokuznetsk",
|
||||
"Asia/Novosibirsk",
|
||||
"Asia/Omsk",
|
||||
"Asia/Oral",
|
||||
"Asia/Phnom_Penh",
|
||||
"Asia/Pontianak",
|
||||
"Asia/Pyongyang",
|
||||
"Asia/Qatar",
|
||||
"Asia/Qostanay",
|
||||
"Asia/Qyzylorda",
|
||||
"Asia/Rangoon",
|
||||
"Asia/Riyadh",
|
||||
"Asia/Saigon",
|
||||
"Asia/Sakhalin",
|
||||
"Asia/Samarkand",
|
||||
"Asia/Seoul",
|
||||
"Asia/Shanghai",
|
||||
"Asia/Singapore",
|
||||
"Asia/Srednekolymsk",
|
||||
"Asia/Taipei",
|
||||
"Asia/Tashkent",
|
||||
"Asia/Tbilisi",
|
||||
"Asia/Tehran",
|
||||
"Asia/Tel_Aviv",
|
||||
"Asia/Thimbu",
|
||||
"Asia/Thimphu",
|
||||
"Asia/Tokyo",
|
||||
"Asia/Tomsk",
|
||||
"Asia/Ujung_Pandang",
|
||||
"Asia/Ulaanbaatar",
|
||||
"Asia/Ulan_Bator",
|
||||
"Asia/Urumqi",
|
||||
"Asia/Ust-Nera",
|
||||
"Asia/Vientiane",
|
||||
"Asia/Vladivostok",
|
||||
"Asia/Yakutsk",
|
||||
"Asia/Yangon",
|
||||
"Asia/Yekaterinburg",
|
||||
"Asia/Yerevan",
|
||||
"Atlantic/Azores",
|
||||
"Atlantic/Bermuda",
|
||||
"Atlantic/Canary",
|
||||
"Atlantic/Cape_Verde",
|
||||
"Atlantic/Faeroe",
|
||||
"Atlantic/Faroe",
|
||||
"Atlantic/Jan_Mayen",
|
||||
"Atlantic/Madeira",
|
||||
"Atlantic/Reykjavik",
|
||||
"Atlantic/South_Georgia",
|
||||
"Atlantic/St_Helena",
|
||||
"Atlantic/Stanley",
|
||||
"Australia/ACT",
|
||||
"Australia/Adelaide",
|
||||
"Australia/Brisbane",
|
||||
"Australia/Broken_Hill",
|
||||
"Australia/Canberra",
|
||||
"Australia/Currie",
|
||||
"Australia/Darwin",
|
||||
"Australia/Eucla",
|
||||
"Australia/Hobart",
|
||||
"Australia/LHI",
|
||||
"Australia/Lindeman",
|
||||
"Australia/Lord_Howe",
|
||||
"Australia/Melbourne",
|
||||
"Australia/NSW",
|
||||
"Australia/North",
|
||||
"Australia/Perth",
|
||||
"Australia/Queensland",
|
||||
"Australia/South",
|
||||
"Australia/Sydney",
|
||||
"Australia/Tasmania",
|
||||
"Australia/Victoria",
|
||||
"Australia/West",
|
||||
"Australia/Yancowinna",
|
||||
"Brazil/Acre",
|
||||
"Brazil/DeNoronha",
|
||||
"Brazil/East",
|
||||
"Brazil/West",
|
||||
"CET",
|
||||
"CST6CDT",
|
||||
"Canada/Atlantic",
|
||||
"Canada/Central",
|
||||
"Canada/Eastern",
|
||||
"Canada/Mountain",
|
||||
"Canada/Newfoundland",
|
||||
"Canada/Pacific",
|
||||
"Canada/Saskatchewan",
|
||||
"Canada/Yukon",
|
||||
"Chile/Continental",
|
||||
"Chile/EasterIsland",
|
||||
"Cuba",
|
||||
"EET",
|
||||
"EST",
|
||||
"EST5EDT",
|
||||
"Egypt",
|
||||
"Eire",
|
||||
"Etc/GMT",
|
||||
"Etc/GMT+0",
|
||||
"Etc/GMT+1",
|
||||
"Etc/GMT+10",
|
||||
"Etc/GMT+11",
|
||||
"Etc/GMT+12",
|
||||
"Etc/GMT+2",
|
||||
"Etc/GMT+3",
|
||||
"Etc/GMT+4",
|
||||
"Etc/GMT+5",
|
||||
"Etc/GMT+6",
|
||||
"Etc/GMT+7",
|
||||
"Etc/GMT+8",
|
||||
"Etc/GMT+9",
|
||||
"Etc/GMT-0",
|
||||
"Etc/GMT-1",
|
||||
"Etc/GMT-10",
|
||||
"Etc/GMT-11",
|
||||
"Etc/GMT-12",
|
||||
"Etc/GMT-13",
|
||||
"Etc/GMT-14",
|
||||
"Etc/GMT-2",
|
||||
"Etc/GMT-3",
|
||||
"Etc/GMT-4",
|
||||
"Etc/GMT-5",
|
||||
"Etc/GMT-6",
|
||||
"Etc/GMT-7",
|
||||
"Etc/GMT-8",
|
||||
"Etc/GMT-9",
|
||||
"Etc/GMT0",
|
||||
"Etc/Greenwich",
|
||||
"Etc/UCT",
|
||||
"Etc/UTC",
|
||||
"Etc/Universal",
|
||||
"Etc/Zulu",
|
||||
"Europe/Amsterdam",
|
||||
"Europe/Andorra",
|
||||
"Europe/Astrakhan",
|
||||
"Europe/Athens",
|
||||
"Europe/Belfast",
|
||||
"Europe/Belgrade",
|
||||
"Europe/Berlin",
|
||||
"Europe/Bratislava",
|
||||
"Europe/Brussels",
|
||||
"Europe/Bucharest",
|
||||
"Europe/Budapest",
|
||||
"Europe/Busingen",
|
||||
"Europe/Chisinau",
|
||||
"Europe/Copenhagen",
|
||||
"Europe/Dublin",
|
||||
"Europe/Gibraltar",
|
||||
"Europe/Guernsey",
|
||||
"Europe/Helsinki",
|
||||
"Europe/Isle_of_Man",
|
||||
"Europe/Istanbul",
|
||||
"Europe/Jersey",
|
||||
"Europe/Kaliningrad",
|
||||
"Europe/Kiev",
|
||||
"Europe/Kirov",
|
||||
"Europe/Kyiv",
|
||||
"Europe/Lisbon",
|
||||
"Europe/Ljubljana",
|
||||
"Europe/London",
|
||||
"Europe/Luxembourg",
|
||||
"Europe/Madrid",
|
||||
"Europe/Malta",
|
||||
"Europe/Mariehamn",
|
||||
"Europe/Minsk",
|
||||
"Europe/Monaco",
|
||||
"Europe/Moscow",
|
||||
"Europe/Nicosia",
|
||||
"Europe/Oslo",
|
||||
"Europe/Paris",
|
||||
"Europe/Podgorica",
|
||||
"Europe/Prague",
|
||||
"Europe/Riga",
|
||||
"Europe/Rome",
|
||||
"Europe/Samara",
|
||||
"Europe/San_Marino",
|
||||
"Europe/Sarajevo",
|
||||
"Europe/Saratov",
|
||||
"Europe/Simferopol",
|
||||
"Europe/Skopje",
|
||||
"Europe/Sofia",
|
||||
"Europe/Stockholm",
|
||||
"Europe/Tallinn",
|
||||
"Europe/Tirane",
|
||||
"Europe/Tiraspol",
|
||||
"Europe/Ulyanovsk",
|
||||
"Europe/Uzhgorod",
|
||||
"Europe/Vaduz",
|
||||
"Europe/Vatican",
|
||||
"Europe/Vienna",
|
||||
"Europe/Vilnius",
|
||||
"Europe/Volgograd",
|
||||
"Europe/Warsaw",
|
||||
"Europe/Zagreb",
|
||||
"Europe/Zaporozhye",
|
||||
"Europe/Zurich",
|
||||
"Factory",
|
||||
"GB",
|
||||
"GB-Eire",
|
||||
"GMT",
|
||||
"GMT+0",
|
||||
"GMT-0",
|
||||
"GMT0",
|
||||
"Greenwich",
|
||||
"HST",
|
||||
"Hongkong",
|
||||
"Iceland",
|
||||
"Indian/Antananarivo",
|
||||
"Indian/Chagos",
|
||||
"Indian/Christmas",
|
||||
"Indian/Cocos",
|
||||
"Indian/Comoro",
|
||||
"Indian/Kerguelen",
|
||||
"Indian/Mahe",
|
||||
"Indian/Maldives",
|
||||
"Indian/Mauritius",
|
||||
"Indian/Mayotte",
|
||||
"Indian/Reunion",
|
||||
"Iran",
|
||||
"Israel",
|
||||
"Jamaica",
|
||||
"Japan",
|
||||
"Kwajalein",
|
||||
"Libya",
|
||||
"MET",
|
||||
"MST",
|
||||
"MST7MDT",
|
||||
"Mexico/BajaNorte",
|
||||
"Mexico/BajaSur",
|
||||
"Mexico/General",
|
||||
"NZ",
|
||||
"NZ-CHAT",
|
||||
"Navajo",
|
||||
"PRC",
|
||||
"PST8PDT",
|
||||
"Pacific/Apia",
|
||||
"Pacific/Auckland",
|
||||
"Pacific/Bougainville",
|
||||
"Pacific/Chatham",
|
||||
"Pacific/Chuuk",
|
||||
"Pacific/Easter",
|
||||
"Pacific/Efate",
|
||||
"Pacific/Enderbury",
|
||||
"Pacific/Fakaofo",
|
||||
"Pacific/Fiji",
|
||||
"Pacific/Funafuti",
|
||||
"Pacific/Galapagos",
|
||||
"Pacific/Gambier",
|
||||
"Pacific/Guadalcanal",
|
||||
"Pacific/Guam",
|
||||
"Pacific/Honolulu",
|
||||
"Pacific/Johnston",
|
||||
"Pacific/Kanton",
|
||||
"Pacific/Kiritimati",
|
||||
"Pacific/Kosrae",
|
||||
"Pacific/Kwajalein",
|
||||
"Pacific/Majuro",
|
||||
"Pacific/Marquesas",
|
||||
"Pacific/Midway",
|
||||
"Pacific/Nauru",
|
||||
"Pacific/Niue",
|
||||
"Pacific/Norfolk",
|
||||
"Pacific/Noumea",
|
||||
"Pacific/Pago_Pago",
|
||||
"Pacific/Palau",
|
||||
"Pacific/Pitcairn",
|
||||
"Pacific/Pohnpei",
|
||||
"Pacific/Ponape",
|
||||
"Pacific/Port_Moresby",
|
||||
"Pacific/Rarotonga",
|
||||
"Pacific/Saipan",
|
||||
"Pacific/Samoa",
|
||||
"Pacific/Tahiti",
|
||||
"Pacific/Tarawa",
|
||||
"Pacific/Tongatapu",
|
||||
"Pacific/Truk",
|
||||
"Pacific/Wake",
|
||||
"Pacific/Wallis",
|
||||
"Pacific/Yap",
|
||||
"Poland",
|
||||
"Portugal",
|
||||
"ROC",
|
||||
"ROK",
|
||||
"Singapore",
|
||||
"Turkey",
|
||||
"UCT",
|
||||
"US/Alaska",
|
||||
"US/Aleutian",
|
||||
"US/Arizona",
|
||||
"US/Central",
|
||||
"US/East-Indiana",
|
||||
"US/Eastern",
|
||||
"US/Hawaii",
|
||||
"US/Indiana-Starke",
|
||||
"US/Michigan",
|
||||
"US/Mountain",
|
||||
"US/Pacific",
|
||||
"US/Samoa",
|
||||
"UTC",
|
||||
"Universal",
|
||||
"W-SU",
|
||||
"WET",
|
||||
"Zulu",
|
||||
}
|
||||
|
|
@ -101,7 +101,7 @@ func (l *Lease) SetLeaseExpiry() (time.Time, error) {
|
|||
func UnmarshalDHCPCLease(lease *Lease, str string) error {
|
||||
// parse the lease file as a map
|
||||
data := make(map[string]string)
|
||||
for _, line := range strings.Split(str, "\n") {
|
||||
for line := range strings.SplitSeq(str, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
// skip empty lines and comments
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
|
|
@ -165,7 +165,7 @@ func UnmarshalDHCPCLease(lease *Lease, str string) error {
|
|||
field.Set(reflect.ValueOf(ip))
|
||||
case []net.IP:
|
||||
val := make([]net.IP, 0)
|
||||
for _, ipStr := range strings.Fields(value) {
|
||||
for ipStr := range strings.FieldsSeq(value) {
|
||||
ip := net.ParseIP(ipStr)
|
||||
if ip == nil {
|
||||
continue
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ func NewDHCPClient(options *DHCPClientOptions) *DHCPClient {
|
|||
}
|
||||
|
||||
func (c *DHCPClient) getWatchPaths() []string {
|
||||
watchPaths := make(map[string]interface{})
|
||||
watchPaths := make(map[string]any)
|
||||
watchPaths[filepath.Dir(c.leaseFile)] = nil
|
||||
|
||||
if c.pidFile != "" {
|
||||
|
|
|
|||
|
|
@ -30,8 +30,8 @@ var defaultGadgetConfig = map[string]gadgetConfigItem{
|
|||
attrs: gadgetAttributes{
|
||||
"bcdUSB": "0x0200", // USB 2.0
|
||||
"idVendor": "0x1d6b", // The Linux Foundation
|
||||
"idProduct": "0104", // Multifunction Composite Gadget
|
||||
"bcdDevice": "0100",
|
||||
"idProduct": "0x0104", // Multifunction Composite Gadget
|
||||
"bcdDevice": "0x0100", // USB2
|
||||
},
|
||||
configAttrs: gadgetAttributes{
|
||||
"MaxPower": "250", // in unit of 2mA
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
package usbgadget
|
||||
|
||||
import "time"
|
||||
|
||||
const dwc3Path = "/sys/bus/platform/drivers/dwc3"
|
||||
|
||||
const hidWriteTimeout = 10 * time.Millisecond
|
||||
|
|
|
|||
|
|
@ -1,11 +1,15 @@
|
|||
package usbgadget
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/rs/xid"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
var keyboardConfig = gadgetConfigItem{
|
||||
|
|
@ -14,9 +18,10 @@ var keyboardConfig = gadgetConfigItem{
|
|||
path: []string{"functions", "hid.usb0"},
|
||||
configPath: []string{"hid.usb0"},
|
||||
attrs: gadgetAttributes{
|
||||
"protocol": "1",
|
||||
"subclass": "1",
|
||||
"report_length": "8",
|
||||
"protocol": "1",
|
||||
"subclass": "1",
|
||||
"report_length": "8",
|
||||
"no_out_endpoint": "0",
|
||||
},
|
||||
reportDesc: keyboardReportDesc,
|
||||
}
|
||||
|
|
@ -60,6 +65,8 @@ var keyboardReportDesc = []byte{
|
|||
|
||||
const (
|
||||
hidReadBufferSize = 8
|
||||
hidKeyBufferSize = 6
|
||||
hidErrorRollOver = 0x01
|
||||
// https://www.usb.org/sites/default/files/documents/hid1_11.pdf
|
||||
// https://www.usb.org/sites/default/files/hut1_2.pdf
|
||||
KeyboardLedMaskNumLock = 1 << 0
|
||||
|
|
@ -67,7 +74,9 @@ const (
|
|||
KeyboardLedMaskScrollLock = 1 << 2
|
||||
KeyboardLedMaskCompose = 1 << 3
|
||||
KeyboardLedMaskKana = 1 << 4
|
||||
ValidKeyboardLedMasks = KeyboardLedMaskNumLock | KeyboardLedMaskCapsLock | KeyboardLedMaskScrollLock | KeyboardLedMaskCompose | KeyboardLedMaskKana
|
||||
// power on/off LED is 5
|
||||
KeyboardLedMaskShift = 1 << 6
|
||||
ValidKeyboardLedMasks = KeyboardLedMaskNumLock | KeyboardLedMaskCapsLock | KeyboardLedMaskScrollLock | KeyboardLedMaskCompose | KeyboardLedMaskKana | KeyboardLedMaskShift
|
||||
)
|
||||
|
||||
// Synchronization between LED states and CAPS LOCK, NUM LOCK, SCROLL LOCK,
|
||||
|
|
@ -80,6 +89,13 @@ type KeyboardState struct {
|
|||
ScrollLock bool `json:"scroll_lock"`
|
||||
Compose bool `json:"compose"`
|
||||
Kana bool `json:"kana"`
|
||||
Shift bool `json:"shift"` // This is not part of the main USB HID spec
|
||||
raw byte
|
||||
}
|
||||
|
||||
// Byte returns the raw byte representation of the keyboard state.
|
||||
func (k *KeyboardState) Byte() byte {
|
||||
return k.raw
|
||||
}
|
||||
|
||||
func getKeyboardState(b byte) KeyboardState {
|
||||
|
|
@ -90,27 +106,28 @@ func getKeyboardState(b byte) KeyboardState {
|
|||
ScrollLock: b&KeyboardLedMaskScrollLock != 0,
|
||||
Compose: b&KeyboardLedMaskCompose != 0,
|
||||
Kana: b&KeyboardLedMaskKana != 0,
|
||||
Shift: b&KeyboardLedMaskShift != 0,
|
||||
raw: b,
|
||||
}
|
||||
}
|
||||
|
||||
func (u *UsbGadget) updateKeyboardState(b byte) {
|
||||
func (u *UsbGadget) updateKeyboardState(state byte) {
|
||||
u.keyboardStateLock.Lock()
|
||||
defer u.keyboardStateLock.Unlock()
|
||||
|
||||
if b&^ValidKeyboardLedMasks != 0 {
|
||||
u.log.Trace().Uint8("b", b).Msg("contains invalid bits, ignoring")
|
||||
if state&^ValidKeyboardLedMasks != 0 {
|
||||
u.log.Warn().Uint8("state", state).Msg("ignoring invalid bits")
|
||||
return
|
||||
}
|
||||
|
||||
newState := getKeyboardState(b)
|
||||
if reflect.DeepEqual(u.keyboardState, newState) {
|
||||
if u.keyboardState == state {
|
||||
return
|
||||
}
|
||||
u.log.Info().Interface("old", u.keyboardState).Interface("new", newState).Msg("keyboardState updated")
|
||||
u.keyboardState = newState
|
||||
u.log.Trace().Uint8("old", u.keyboardState).Uint8("new", state).Msg("keyboardState updated")
|
||||
u.keyboardState = state
|
||||
|
||||
if u.onKeyboardStateChange != nil {
|
||||
(*u.onKeyboardStateChange)(newState)
|
||||
(*u.onKeyboardStateChange)(getKeyboardState(state))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -122,7 +139,105 @@ func (u *UsbGadget) GetKeyboardState() KeyboardState {
|
|||
u.keyboardStateLock.Lock()
|
||||
defer u.keyboardStateLock.Unlock()
|
||||
|
||||
return u.keyboardState
|
||||
return getKeyboardState(u.keyboardState)
|
||||
}
|
||||
|
||||
func (u *UsbGadget) GetKeysDownState() KeysDownState {
|
||||
u.keyboardStateLock.Lock()
|
||||
defer u.keyboardStateLock.Unlock()
|
||||
|
||||
return u.keysDownState
|
||||
}
|
||||
|
||||
func (u *UsbGadget) SetOnKeysDownChange(f func(state KeysDownState)) {
|
||||
u.onKeysDownChange = &f
|
||||
}
|
||||
|
||||
func (u *UsbGadget) SetOnKeepAliveReset(f func()) {
|
||||
u.onKeepAliveReset = &f
|
||||
}
|
||||
|
||||
// DefaultAutoReleaseDuration is the default duration for auto-release of a key.
|
||||
const DefaultAutoReleaseDuration = 100 * time.Millisecond
|
||||
|
||||
func (u *UsbGadget) scheduleAutoRelease(key byte) {
|
||||
u.kbdAutoReleaseLock.Lock()
|
||||
defer unlockWithLog(&u.kbdAutoReleaseLock, u.log, "autoRelease scheduled")
|
||||
|
||||
if u.kbdAutoReleaseTimers[key] != nil {
|
||||
u.kbdAutoReleaseTimers[key].Stop()
|
||||
}
|
||||
|
||||
// TODO: make this configurable
|
||||
// We currently hardcode the duration to 100ms
|
||||
// However, it should be the same as the duration of the keep-alive reset called baseExtension.
|
||||
u.kbdAutoReleaseTimers[key] = time.AfterFunc(100*time.Millisecond, func() {
|
||||
u.performAutoRelease(key)
|
||||
})
|
||||
}
|
||||
|
||||
func (u *UsbGadget) cancelAutoRelease(key byte) {
|
||||
u.kbdAutoReleaseLock.Lock()
|
||||
defer unlockWithLog(&u.kbdAutoReleaseLock, u.log, "autoRelease cancelled")
|
||||
|
||||
if timer := u.kbdAutoReleaseTimers[key]; timer != nil {
|
||||
timer.Stop()
|
||||
u.kbdAutoReleaseTimers[key] = nil
|
||||
delete(u.kbdAutoReleaseTimers, key)
|
||||
|
||||
// Reset keep-alive timing when key is released
|
||||
if u.onKeepAliveReset != nil {
|
||||
(*u.onKeepAliveReset)()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (u *UsbGadget) DelayAutoReleaseWithDuration(resetDuration time.Duration) {
|
||||
u.kbdAutoReleaseLock.Lock()
|
||||
defer unlockWithLog(&u.kbdAutoReleaseLock, u.log, "autoRelease delayed")
|
||||
|
||||
u.log.Debug().Dur("reset_duration", resetDuration).Msg("delaying auto-release with dynamic duration")
|
||||
|
||||
for _, timer := range u.kbdAutoReleaseTimers {
|
||||
if timer != nil {
|
||||
timer.Reset(resetDuration)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (u *UsbGadget) performAutoRelease(key byte) {
|
||||
u.kbdAutoReleaseLock.Lock()
|
||||
|
||||
if u.kbdAutoReleaseTimers[key] == nil {
|
||||
u.log.Warn().Uint8("key", key).Msg("autoRelease timer not found")
|
||||
u.kbdAutoReleaseLock.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
u.kbdAutoReleaseTimers[key].Stop()
|
||||
u.kbdAutoReleaseTimers[key] = nil
|
||||
delete(u.kbdAutoReleaseTimers, key)
|
||||
u.kbdAutoReleaseLock.Unlock()
|
||||
|
||||
// Skip if already released
|
||||
state := u.GetKeysDownState()
|
||||
alreadyReleased := true
|
||||
|
||||
for i := range state.Keys {
|
||||
if state.Keys[i] == key {
|
||||
alreadyReleased = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if alreadyReleased {
|
||||
return
|
||||
}
|
||||
|
||||
_, err := u.keypressReport(key, false)
|
||||
if err != nil {
|
||||
u.log.Warn().Uint8("key", key).Msg("failed to release key")
|
||||
}
|
||||
}
|
||||
|
||||
func (u *UsbGadget) listenKeyboardEvents() {
|
||||
|
|
@ -141,18 +256,24 @@ func (u *UsbGadget) listenKeyboardEvents() {
|
|||
l.Info().Msg("context done")
|
||||
return
|
||||
default:
|
||||
l.Trace().Msg("reading from keyboard")
|
||||
l.Trace().Msg("reading from keyboard for LED state changes")
|
||||
if u.keyboardHidFile == nil {
|
||||
l.Error().Msg("keyboardHidFile is nil")
|
||||
u.logWithSuppression("keyboardHidFileNil", 100, &l, nil, "keyboardHidFile is nil")
|
||||
// show the error every 100 times to avoid spamming the logs
|
||||
time.Sleep(time.Second)
|
||||
continue
|
||||
}
|
||||
// reset the counter
|
||||
u.resetLogSuppressionCounter("keyboardHidFileNil")
|
||||
|
||||
n, err := u.keyboardHidFile.Read(buf)
|
||||
if err != nil {
|
||||
l.Error().Err(err).Msg("failed to read")
|
||||
u.logWithSuppression("keyboardHidFileRead", 100, &l, err, "failed to read")
|
||||
continue
|
||||
}
|
||||
l.Trace().Int("n", n).Bytes("buf", buf).Msg("got data from keyboard")
|
||||
u.resetLogSuppressionCounter("keyboardHidFileRead")
|
||||
|
||||
l.Trace().Int("n", n).Uints8("buf", buf).Msg("got data from keyboard")
|
||||
if n != 1 {
|
||||
l.Trace().Int("n", n).Msg("expected 1 byte, got")
|
||||
continue
|
||||
|
|
@ -188,38 +309,198 @@ func (u *UsbGadget) OpenKeyboardHidFile() error {
|
|||
return u.openKeyboardHidFile()
|
||||
}
|
||||
|
||||
func (u *UsbGadget) keyboardWriteHidFile(data []byte) error {
|
||||
var keyboardWriteHidFileLock sync.Mutex
|
||||
|
||||
func (u *UsbGadget) keyboardWriteHidFile(modifier byte, keys []byte) error {
|
||||
keyboardWriteHidFileLock.Lock()
|
||||
defer keyboardWriteHidFileLock.Unlock()
|
||||
if err := u.openKeyboardHidFile(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err := u.keyboardHidFile.Write(data)
|
||||
_, err := u.writeWithTimeout(u.keyboardHidFile, append([]byte{modifier, 0x00}, keys[:hidKeyBufferSize]...))
|
||||
if err != nil {
|
||||
u.log.Error().Err(err).Msg("failed to write to hidg0")
|
||||
u.logWithSuppression("keyboardWriteHidFile", 100, u.log, err, "failed to write to hidg0")
|
||||
u.keyboardHidFile.Close()
|
||||
u.keyboardHidFile = nil
|
||||
return err
|
||||
}
|
||||
|
||||
u.resetLogSuppressionCounter("keyboardWriteHidFile")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *UsbGadget) KeyboardReport(modifier uint8, keys []uint8) error {
|
||||
u.keyboardLock.Lock()
|
||||
defer u.keyboardLock.Unlock()
|
||||
|
||||
if len(keys) > 6 {
|
||||
keys = keys[:6]
|
||||
}
|
||||
if len(keys) < 6 {
|
||||
keys = append(keys, make([]uint8, 6-len(keys))...)
|
||||
func (u *UsbGadget) UpdateKeysDown(modifier byte, keys []byte) KeysDownState {
|
||||
// if we just reported an error roll over, we should clear the keys
|
||||
if keys[0] == hidErrorRollOver {
|
||||
for i := range keys {
|
||||
keys[i] = 0
|
||||
}
|
||||
}
|
||||
|
||||
err := u.keyboardWriteHidFile([]byte{modifier, 0, keys[0], keys[1], keys[2], keys[3], keys[4], keys[5]})
|
||||
state := KeysDownState{
|
||||
Modifier: modifier,
|
||||
Keys: []byte(keys[:]),
|
||||
}
|
||||
|
||||
u.keyboardStateLock.Lock()
|
||||
|
||||
if u.keysDownState.Modifier == state.Modifier &&
|
||||
bytes.Equal(u.keysDownState.Keys, state.Keys) {
|
||||
u.keyboardStateLock.Unlock()
|
||||
return state // No change in key down state
|
||||
}
|
||||
|
||||
u.keysDownState = state
|
||||
u.keyboardStateLock.Unlock()
|
||||
|
||||
if u.onKeysDownChange != nil {
|
||||
(*u.onKeysDownChange)(state) // this enques to the outgoing hidrpc queue via usb.go → currentSession.enqueueKeysDownState(...)
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
func (u *UsbGadget) KeyboardReport(modifier byte, keys []byte) error {
|
||||
defer u.resetUserInputTime()
|
||||
|
||||
if len(keys) > hidKeyBufferSize {
|
||||
keys = keys[:hidKeyBufferSize]
|
||||
}
|
||||
if len(keys) < hidKeyBufferSize {
|
||||
keys = append(keys, make([]byte, hidKeyBufferSize-len(keys))...)
|
||||
}
|
||||
|
||||
err := u.keyboardWriteHidFile(modifier, keys)
|
||||
if err != nil {
|
||||
return err
|
||||
u.log.Warn().Uint8("modifier", modifier).Uints8("keys", keys).Msg("Could not write keyboard report to hidg0")
|
||||
}
|
||||
|
||||
u.resetUserInputTime()
|
||||
return nil
|
||||
u.UpdateKeysDown(modifier, keys)
|
||||
return err
|
||||
}
|
||||
|
||||
const (
|
||||
// https://www.usb.org/sites/default/files/documents/hut1_2.pdf
|
||||
// Dynamic Flags (DV)
|
||||
LeftControl = 0xE0
|
||||
LeftShift = 0xE1
|
||||
LeftAlt = 0xE2
|
||||
LeftSuper = 0xE3 // Left GUI (e.g. Windows key, Apple Command key)
|
||||
RightControl = 0xE4
|
||||
RightShift = 0xE5
|
||||
RightAlt = 0xE6
|
||||
RightSuper = 0xE7 // Right GUI (e.g. Windows key, Apple Command key)
|
||||
)
|
||||
|
||||
const (
|
||||
// https://www.usb.org/sites/default/files/documents/hid1_11.pdf Appendix C
|
||||
ModifierMaskLeftControl = 0x01
|
||||
ModifierMaskRightControl = 0x10
|
||||
ModifierMaskLeftShift = 0x02
|
||||
ModifierMaskRightShift = 0x20
|
||||
ModifierMaskLeftAlt = 0x04
|
||||
ModifierMaskRightAlt = 0x40
|
||||
ModifierMaskLeftSuper = 0x08
|
||||
ModifierMaskRightSuper = 0x80
|
||||
)
|
||||
|
||||
// KeyCodeToMaskMap is a slice of KeyCodeMask for quick lookup
|
||||
var KeyCodeToMaskMap = map[byte]byte{
|
||||
LeftControl: ModifierMaskLeftControl,
|
||||
LeftShift: ModifierMaskLeftShift,
|
||||
LeftAlt: ModifierMaskLeftAlt,
|
||||
LeftSuper: ModifierMaskLeftSuper,
|
||||
RightControl: ModifierMaskRightControl,
|
||||
RightShift: ModifierMaskRightShift,
|
||||
RightAlt: ModifierMaskRightAlt,
|
||||
RightSuper: ModifierMaskRightSuper,
|
||||
}
|
||||
|
||||
func (u *UsbGadget) keypressReport(key byte, press bool) (KeysDownState, error) {
|
||||
defer u.resetUserInputTime()
|
||||
|
||||
l := u.log.With().Uint8("key", key).Bool("press", press).Logger()
|
||||
if l.GetLevel() <= zerolog.DebugLevel {
|
||||
requestID := xid.New()
|
||||
l = l.With().Str("requestID", requestID.String()).Logger()
|
||||
}
|
||||
|
||||
// IMPORTANT: This code parallels the logic in the kernel's hid-gadget driver
|
||||
// for handling key presses and releases. It ensures that the USB gadget
|
||||
// behaves similarly to a real USB HID keyboard. This logic is paralleled
|
||||
// in the client/browser-side code in useKeyboard.ts so make sure to keep
|
||||
// them in sync.
|
||||
var state = u.GetKeysDownState()
|
||||
l.Trace().Interface("state", state).Msg("got keys down state")
|
||||
|
||||
modifier := state.Modifier
|
||||
keys := append([]byte(nil), state.Keys...)
|
||||
|
||||
if mask, exists := KeyCodeToMaskMap[key]; exists {
|
||||
// If the key is a modifier key, we update the keyboardModifier state
|
||||
// by setting or clearing the corresponding bit in the modifier byte.
|
||||
// This allows us to track the state of dynamic modifier keys like
|
||||
// Shift, Control, Alt, and Super.
|
||||
if press {
|
||||
modifier |= mask
|
||||
} else {
|
||||
modifier &^= mask
|
||||
}
|
||||
} else {
|
||||
// handle other keys that are not modifier keys by placing or removing them
|
||||
// from the key buffer since the buffer tracks currently pressed keys
|
||||
overrun := true
|
||||
for i := range hidKeyBufferSize {
|
||||
// If we find the key in the buffer the buffer, we either remove it (if press is false)
|
||||
// or do nothing (if down is true) because the buffer tracks currently pressed keys
|
||||
// and if we find a zero byte, we can place the key there (if press is true)
|
||||
if keys[i] == key || keys[i] == 0 {
|
||||
if press {
|
||||
keys[i] = key // overwrites the zero byte or the same key if already pressed
|
||||
} else {
|
||||
// we are releasing the key, remove it from the buffer
|
||||
if keys[i] != 0 {
|
||||
copy(keys[i:], keys[i+1:])
|
||||
keys[hidKeyBufferSize-1] = 0 // Clear the last byte
|
||||
}
|
||||
}
|
||||
overrun = false // We found a slot for the key
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// If we reach here it means we didn't find an empty slot or the key in the buffer
|
||||
if overrun {
|
||||
if press {
|
||||
l.Error().Msg("keyboard buffer overflow, key not added")
|
||||
// Fill all key slots with ErrorRollOver (0x01) to indicate overflow
|
||||
for i := range keys {
|
||||
keys[i] = hidErrorRollOver
|
||||
}
|
||||
} else {
|
||||
// If we are releasing a key, and we didn't find it in a slot, who cares?
|
||||
l.Warn().Msg("key not found in buffer, nothing to release")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err := u.keyboardWriteHidFile(modifier, keys)
|
||||
return u.UpdateKeysDown(modifier, keys), err
|
||||
}
|
||||
|
||||
func (u *UsbGadget) KeypressReport(key byte, press bool) error {
|
||||
state, err := u.keypressReport(key, press)
|
||||
if err != nil {
|
||||
u.log.Warn().Uint8("key", key).Bool("press", press).Msg("failed to report key")
|
||||
}
|
||||
isRolledOver := state.Keys[0] == hidErrorRollOver
|
||||
|
||||
if isRolledOver {
|
||||
u.cancelAutoRelease(key)
|
||||
} else if press {
|
||||
u.scheduleAutoRelease(key)
|
||||
} else {
|
||||
u.cancelAutoRelease(key)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,9 +11,10 @@ var absoluteMouseConfig = gadgetConfigItem{
|
|||
path: []string{"functions", "hid.usb1"},
|
||||
configPath: []string{"hid.usb1"},
|
||||
attrs: gadgetAttributes{
|
||||
"protocol": "2",
|
||||
"subclass": "1",
|
||||
"report_length": "6",
|
||||
"protocol": "2",
|
||||
"subclass": "0",
|
||||
"report_length": "6",
|
||||
"no_out_endpoint": "1",
|
||||
},
|
||||
reportDesc: absoluteMouseCombinedReportDesc,
|
||||
}
|
||||
|
|
@ -73,27 +74,28 @@ func (u *UsbGadget) absMouseWriteHidFile(data []byte) error {
|
|||
}
|
||||
}
|
||||
|
||||
_, err := u.absMouseHidFile.Write(data)
|
||||
_, err := u.writeWithTimeout(u.absMouseHidFile, data)
|
||||
if err != nil {
|
||||
u.log.Error().Err(err).Msg("failed to write to hidg1")
|
||||
u.logWithSuppression("absMouseWriteHidFile", 100, u.log, err, "failed to write to hidg1")
|
||||
u.absMouseHidFile.Close()
|
||||
u.absMouseHidFile = nil
|
||||
return err
|
||||
}
|
||||
u.resetLogSuppressionCounter("absMouseWriteHidFile")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *UsbGadget) AbsMouseReport(x, y int, buttons uint8) error {
|
||||
func (u *UsbGadget) AbsMouseReport(x int, y int, buttons uint8) error {
|
||||
u.absMouseLock.Lock()
|
||||
defer u.absMouseLock.Unlock()
|
||||
|
||||
err := u.absMouseWriteHidFile([]byte{
|
||||
1, // Report ID 1
|
||||
buttons, // Buttons
|
||||
uint8(x), // X Low Byte
|
||||
uint8(x >> 8), // X High Byte
|
||||
uint8(y), // Y Low Byte
|
||||
uint8(y >> 8), // Y High Byte
|
||||
1, // Report ID 1
|
||||
buttons, // Buttons
|
||||
byte(x), // X Low Byte
|
||||
byte(x >> 8), // X High Byte
|
||||
byte(y), // Y Low Byte
|
||||
byte(y >> 8), // Y High Byte
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
|||
|
|
@ -11,9 +11,10 @@ var relativeMouseConfig = gadgetConfigItem{
|
|||
path: []string{"functions", "hid.usb2"},
|
||||
configPath: []string{"hid.usb2"},
|
||||
attrs: gadgetAttributes{
|
||||
"protocol": "2",
|
||||
"subclass": "1",
|
||||
"report_length": "4",
|
||||
"protocol": "2",
|
||||
"subclass": "1",
|
||||
"report_length": "4",
|
||||
"no_out_endpoint": "1",
|
||||
},
|
||||
reportDesc: relativeMouseCombinedReportDesc,
|
||||
}
|
||||
|
|
@ -63,25 +64,26 @@ func (u *UsbGadget) relMouseWriteHidFile(data []byte) error {
|
|||
}
|
||||
}
|
||||
|
||||
_, err := u.relMouseHidFile.Write(data)
|
||||
_, err := u.writeWithTimeout(u.relMouseHidFile, data)
|
||||
if err != nil {
|
||||
u.log.Error().Err(err).Msg("failed to write to hidg2")
|
||||
u.logWithSuppression("relMouseWriteHidFile", 100, u.log, err, "failed to write to hidg2")
|
||||
u.relMouseHidFile.Close()
|
||||
u.relMouseHidFile = nil
|
||||
return err
|
||||
}
|
||||
u.resetLogSuppressionCounter("relMouseWriteHidFile")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *UsbGadget) RelMouseReport(mx, my int8, buttons uint8) error {
|
||||
func (u *UsbGadget) RelMouseReport(mx int8, my int8, buttons uint8) error {
|
||||
u.relMouseLock.Lock()
|
||||
defer u.relMouseLock.Unlock()
|
||||
|
||||
err := u.relMouseWriteHidFile([]byte{
|
||||
buttons, // Buttons
|
||||
uint8(mx), // X
|
||||
uint8(my), // Y
|
||||
0, // Wheel
|
||||
buttons, // Buttons
|
||||
byte(mx), // X
|
||||
byte(my), // Y
|
||||
0, // Wheel
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
|||
|
|
@ -41,6 +41,11 @@ var defaultUsbGadgetDevices = Devices{
|
|||
MassStorage: true,
|
||||
}
|
||||
|
||||
type KeysDownState struct {
|
||||
Modifier byte `json:"modifier"`
|
||||
Keys ByteSlice `json:"keys"`
|
||||
}
|
||||
|
||||
// UsbGadget is a struct that represents a USB gadget.
|
||||
type UsbGadget struct {
|
||||
name string
|
||||
|
|
@ -60,7 +65,12 @@ type UsbGadget struct {
|
|||
relMouseHidFile *os.File
|
||||
relMouseLock sync.Mutex
|
||||
|
||||
keyboardState KeyboardState
|
||||
keyboardState byte // keyboard latched state (NumLock, CapsLock, ScrollLock, Compose, Kana)
|
||||
keysDownState KeysDownState // keyboard dynamic state (modifier keys and pressed keys)
|
||||
|
||||
kbdAutoReleaseLock sync.Mutex
|
||||
kbdAutoReleaseTimers map[byte]*time.Timer
|
||||
|
||||
keyboardStateLock sync.Mutex
|
||||
keyboardStateCtx context.Context
|
||||
keyboardStateCancel context.CancelFunc
|
||||
|
|
@ -77,8 +87,13 @@ type UsbGadget struct {
|
|||
txLock sync.Mutex
|
||||
|
||||
onKeyboardStateChange *func(state KeyboardState)
|
||||
onKeysDownChange *func(state KeysDownState)
|
||||
onKeepAliveReset *func()
|
||||
|
||||
log *zerolog.Logger
|
||||
|
||||
logSuppressionCounter map[string]int
|
||||
logSuppressionLock sync.Mutex
|
||||
}
|
||||
|
||||
const configFSPath = "/sys/kernel/config"
|
||||
|
|
@ -107,25 +122,29 @@ func newUsbGadget(name string, configMap map[string]gadgetConfigItem, enabledDev
|
|||
keyboardCtx, keyboardCancel := context.WithCancel(context.Background())
|
||||
|
||||
g := &UsbGadget{
|
||||
name: name,
|
||||
kvmGadgetPath: path.Join(gadgetPath, name),
|
||||
configC1Path: path.Join(gadgetPath, name, "configs/c.1"),
|
||||
configMap: configMap,
|
||||
customConfig: *config,
|
||||
configLock: sync.Mutex{},
|
||||
keyboardLock: sync.Mutex{},
|
||||
absMouseLock: sync.Mutex{},
|
||||
relMouseLock: sync.Mutex{},
|
||||
txLock: sync.Mutex{},
|
||||
keyboardStateCtx: keyboardCtx,
|
||||
keyboardStateCancel: keyboardCancel,
|
||||
keyboardState: KeyboardState{},
|
||||
enabledDevices: *enabledDevices,
|
||||
lastUserInput: time.Now(),
|
||||
log: logger,
|
||||
name: name,
|
||||
kvmGadgetPath: path.Join(gadgetPath, name),
|
||||
configC1Path: path.Join(gadgetPath, name, "configs/c.1"),
|
||||
configMap: configMap,
|
||||
customConfig: *config,
|
||||
configLock: sync.Mutex{},
|
||||
keyboardLock: sync.Mutex{},
|
||||
absMouseLock: sync.Mutex{},
|
||||
relMouseLock: sync.Mutex{},
|
||||
txLock: sync.Mutex{},
|
||||
keyboardStateCtx: keyboardCtx,
|
||||
keyboardStateCancel: keyboardCancel,
|
||||
keyboardState: 0,
|
||||
keysDownState: KeysDownState{Modifier: 0, Keys: []byte{0, 0, 0, 0, 0, 0}}, // must be initialized to hidKeyBufferSize (6) zero bytes
|
||||
kbdAutoReleaseTimers: make(map[byte]*time.Timer),
|
||||
enabledDevices: *enabledDevices,
|
||||
lastUserInput: time.Now(),
|
||||
log: logger,
|
||||
|
||||
strictMode: config.strictMode,
|
||||
|
||||
logSuppressionCounter: make(map[string]int),
|
||||
|
||||
absMouseAccumulatedWheelY: 0,
|
||||
}
|
||||
if err := g.Init(); err != nil {
|
||||
|
|
@ -135,3 +154,37 @@ func newUsbGadget(name string, configMap map[string]gadgetConfigItem, enabledDev
|
|||
|
||||
return g
|
||||
}
|
||||
|
||||
// Close cleans up resources used by the USB gadget
|
||||
func (u *UsbGadget) Close() error {
|
||||
// Cancel keyboard state context
|
||||
if u.keyboardStateCancel != nil {
|
||||
u.keyboardStateCancel()
|
||||
}
|
||||
|
||||
// Stop auto-release timer
|
||||
u.kbdAutoReleaseLock.Lock()
|
||||
for _, timer := range u.kbdAutoReleaseTimers {
|
||||
if timer != nil {
|
||||
timer.Stop()
|
||||
}
|
||||
}
|
||||
u.kbdAutoReleaseTimers = make(map[byte]*time.Timer)
|
||||
u.kbdAutoReleaseLock.Unlock()
|
||||
|
||||
// Close HID files
|
||||
if u.keyboardHidFile != nil {
|
||||
u.keyboardHidFile.Close()
|
||||
u.keyboardHidFile = nil
|
||||
}
|
||||
if u.absMouseHidFile != nil {
|
||||
u.absMouseHidFile.Close()
|
||||
u.absMouseHidFile = nil
|
||||
}
|
||||
if u.relMouseHidFile != nil {
|
||||
u.relMouseHidFile.Close()
|
||||
u.relMouseHidFile = nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,12 +2,44 @@ package usbgadget
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
type ByteSlice []byte
|
||||
|
||||
func (s ByteSlice) MarshalJSON() ([]byte, error) {
|
||||
vals := make([]int, len(s))
|
||||
for i, v := range s {
|
||||
vals[i] = int(v)
|
||||
}
|
||||
return json.Marshal(vals)
|
||||
}
|
||||
|
||||
func (s *ByteSlice) UnmarshalJSON(data []byte) error {
|
||||
var vals []int
|
||||
if err := json.Unmarshal(data, &vals); err != nil {
|
||||
return err
|
||||
}
|
||||
*s = make([]byte, len(vals))
|
||||
for i, v := range vals {
|
||||
if v < 0 || v > 255 {
|
||||
return fmt.Errorf("value %d out of byte range", v)
|
||||
}
|
||||
(*s)[i] = byte(v)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func joinPath(basePath string, paths []string) string {
|
||||
pathArr := append([]string{basePath}, paths...)
|
||||
return filepath.Join(pathArr...)
|
||||
|
|
@ -78,3 +110,69 @@ func compareFileContent(oldContent []byte, newContent []byte, looserMatch bool)
|
|||
|
||||
return false
|
||||
}
|
||||
|
||||
func (u *UsbGadget) writeWithTimeout(file *os.File, data []byte) (n int, err error) {
|
||||
if err := file.SetWriteDeadline(time.Now().Add(hidWriteTimeout)); err != nil {
|
||||
return -1, err
|
||||
}
|
||||
|
||||
n, err = file.Write(data)
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
u.log.Trace().
|
||||
Str("file", file.Name()).
|
||||
Bytes("data", data).
|
||||
Err(err).
|
||||
Msg("write failed")
|
||||
|
||||
if errors.Is(err, os.ErrDeadlineExceeded) {
|
||||
u.logWithSuppression(
|
||||
fmt.Sprintf("writeWithTimeout_%s", file.Name()),
|
||||
1000,
|
||||
u.log,
|
||||
err,
|
||||
"write timed out: %s",
|
||||
file.Name(),
|
||||
)
|
||||
err = nil
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (u *UsbGadget) logWithSuppression(counterName string, every int, logger *zerolog.Logger, err error, msg string, args ...any) {
|
||||
u.logSuppressionLock.Lock()
|
||||
defer u.logSuppressionLock.Unlock()
|
||||
|
||||
if _, ok := u.logSuppressionCounter[counterName]; !ok {
|
||||
u.logSuppressionCounter[counterName] = 0
|
||||
} else {
|
||||
u.logSuppressionCounter[counterName]++
|
||||
}
|
||||
|
||||
l := logger.With().Int("counter", u.logSuppressionCounter[counterName]).Logger()
|
||||
|
||||
if u.logSuppressionCounter[counterName]%every == 0 {
|
||||
if err != nil {
|
||||
l.Error().Err(err).Msgf(msg, args...)
|
||||
} else {
|
||||
l.Error().Msgf(msg, args...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (u *UsbGadget) resetLogSuppressionCounter(counterName string) {
|
||||
u.logSuppressionLock.Lock()
|
||||
defer u.logSuppressionLock.Unlock()
|
||||
|
||||
if _, ok := u.logSuppressionCounter[counterName]; !ok {
|
||||
u.logSuppressionCounter[counterName] = 0
|
||||
}
|
||||
}
|
||||
|
||||
func unlockWithLog(lock *sync.Mutex, logger *zerolog.Logger, msg string, args ...any) {
|
||||
logger.Trace().Msgf(msg, args...)
|
||||
lock.Unlock()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,73 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
// ValidSSHKeyTypes is a list of valid SSH key types
|
||||
//
|
||||
// Please make sure that all the types in this list are supported by dropbear
|
||||
// https://github.com/mkj/dropbear/blob/003c5fcaabc114430d5d14142e95ffdbbd2d19b6/src/signkey.c#L37
|
||||
//
|
||||
// ssh-dss is not allowed here as it's insecure
|
||||
var ValidSSHKeyTypes = []string{
|
||||
ssh.KeyAlgoRSA,
|
||||
ssh.KeyAlgoED25519,
|
||||
ssh.KeyAlgoECDSA256,
|
||||
ssh.KeyAlgoECDSA384,
|
||||
ssh.KeyAlgoECDSA521,
|
||||
ssh.KeyAlgoSKED25519,
|
||||
ssh.KeyAlgoSKECDSA256,
|
||||
}
|
||||
|
||||
// ValidateSSHKey validates authorized_keys file content
|
||||
func ValidateSSHKey(sshKey string) error {
|
||||
// validate SSH key
|
||||
var (
|
||||
hasValidPublicKey = false
|
||||
lastError = fmt.Errorf("no valid SSH key found")
|
||||
)
|
||||
for _, key := range strings.Split(sshKey, "\n") {
|
||||
key = strings.TrimSpace(key)
|
||||
|
||||
// skip empty lines and comments
|
||||
if key == "" || strings.HasPrefix(key, "#") {
|
||||
continue
|
||||
}
|
||||
|
||||
parsedPublicKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key))
|
||||
if err != nil {
|
||||
lastError = err
|
||||
continue
|
||||
}
|
||||
|
||||
if parsedPublicKey == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
parsedType := parsedPublicKey.Type()
|
||||
textType := strings.Fields(key)[0]
|
||||
|
||||
if parsedType != textType {
|
||||
lastError = fmt.Errorf("parsed SSH key type %s does not match type in text %s", parsedType, textType)
|
||||
continue
|
||||
}
|
||||
|
||||
if !slices.Contains(ValidSSHKeyTypes, parsedType) {
|
||||
lastError = fmt.Errorf("invalid SSH key type: %s", parsedType)
|
||||
continue
|
||||
}
|
||||
|
||||
hasValidPublicKey = true
|
||||
}
|
||||
|
||||
if !hasValidPublicKey {
|
||||
return lastError
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue