Compare commits

...

102 Commits

Author SHA1 Message Date
Siyuan Miao f455d6cc18 add missing deps 2025-09-20 15:24:50 +00:00
Siyuan Miao d71b6537d2 fix deps 2025-09-20 15:24:06 +00:00
Siyuan Miao 1f9aeff313 resolve conflicts 2025-09-19 19:13:27 +00:00
Siyuan Miao 51d546d5f2 chore: upgrade deps 2025-09-19 19:12:52 +00:00
Siyuan Miao 616b625a5c chore: use UIObjSetState instead of Add+Remove 2025-09-19 19:12:30 +00:00
Siyuan Miao 82c2d6df25 remove useless lvgl config 2025-09-19 19:12:11 +00:00
Siyuan Miao 1e58e3d1bc move event handling to native 2025-09-19 19:12:11 +00:00
Siyuan Miao a05ffea205 chore: upgrade deps 2025-09-19 19:12:11 +00:00
Siyuan Miao 4f1ddc8783 refactor functions 2025-09-19 19:11:54 +00:00
Siyuan Miao 0361b24c7d remove build_afpacket from Makefile 2025-09-19 19:11:41 +00:00
Siyuan Miao aaab8beb1a feat: update github actions 2025-09-19 19:11:40 +00:00
Siyuan Miao 800100aebd feat: remove lvgl examples and demos 2025-09-19 19:11:13 +00:00
Siyuan Miao 2fcecda8c7 chore: add LV_STATE_DISABLED to obj set state 2025-09-19 19:11:13 +00:00
Siyuan Miao ff56128dc5 feat: implement about screen 2025-09-19 19:11:13 +00:00
Siyuan Miao fa1eb7332b feat: add video frame handling 2025-09-19 19:11:13 +00:00
Siyuan Miao 06ea0970eb feat: native as lib 2025-09-19 19:10:48 +00:00
Siyuan Miao 0a3e966684 fix lint issues 2025-09-19 19:09:04 +00:00
Siyuan Miao 3c67060269 feat(lldp): show neighbors in UI 2025-09-19 19:09:04 +00:00
Siyuan Miao 269222d471 feat(lldp): implement rx 2025-09-19 19:08:28 +00:00
Aveline 83caa8f82d
feat: get local version only (#813) 2025-09-19 13:45:59 +02:00
Adam Shiervani 27750b9cc2
feat: re-add keyboard and keypress report handlers to RPC (#811) 2025-09-18 17:33:08 +02:00
Adam Shiervani 5112bef19c
fix: remove unnecessary grow-0 utility from in keyboard (#810) 2025-09-18 17:02:08 +02:00
Siyuan Miao 1ffdca4fd6 build: use immediate assignment for VERSION_DEV and other vars 2025-09-18 15:41:39 +02:00
Siyuan Miao c6dba4d59f chore: bump to 0.4.7 2025-09-18 13:53:08 +02:00
Aveline afb146d78c
feat: release keyPress automatically (#796)
* feat: release keyPress automatically

* send keepalive when pressing the key

* remove logging

* clean up logging

* chore: use unreliable channel to send keepalive events

* chore: use ordered unreliable channel for pointer events

* chore: adjust auto release key interval

* chore: update logging for kbdAutoReleaseLock

* chore: update comment for KEEPALIVE_INTERVAL

* fix: should cancelAutorelease when pressed is true

* fix: handshake won't happen if webrtc reconnects

* chore: add trace log for writeWithTimeout

* chore: add timeout for KeypressReport

* chore: use the proper key to send release command

* refactor: simplify HID RPC keyboard input handling and improve key state management

- Updated `handleHidRPCKeyboardInput` to return errors directly instead of keys down state.
- Refactored `rpcKeyboardReport` and `rpcKeypressReport` to return errors instead of states.
- Introduced a queue for managing key down state updates in the `Session` struct to prevent input handling stalls.
- Adjusted the `UpdateKeysDown` method to handle state changes more efficiently.
- Removed unnecessary logging and commented-out code for clarity.

* refactor: enhance keyboard auto-release functionality and key state management

* fix: correct Windows default auto-repeat delay comment from 1ms to 1s

* refactor: send keypress as early as possible

* refactor: replace console.warn with console.info for HID RPC channel events

* refactor: remove unused NewKeypressKeepAliveMessage function from HID RPC

* fix: handle error in key release process and log warnings

* fix: log warning on keypress report failure

* fix: update auto-release keyboard interval to 225

* refactor: enhance keep-alive handling and jitter compensation in HID RPC

- Implemented staleness guard to ignore outdated keep-alive packets.
- Added jitter compensation logic to adjust timer extensions based on packet arrival times.
- Introduced new methods for managing keep-alive state and reset functionality in the Session struct.
- Updated auto-release delay mechanism to use dynamic durations based on keep-alive timing.
- Adjusted keep-alive interval in the UI to improve responsiveness.

* gofmt

* clean up code

* chore: use dynamic duration for scheduleAutoRelease

* Use harcoded timer reset value for now

* fix: prevent nil pointer dereference when stopping timers in Close method

* refactor: remove nil check for kbdAutoReleaseTimers in DelayAutoReleaseWithDuration

* refactor: optimize dependencies in useHidRpc hooks

* refactor: streamline keep-alive timer management in useKeyboard hook

* refactor: clarify comments in useKeyboard hook for resetKeyboardState function

* refactor: reduce keysDownStateQueueSize

* refactor: close and reset keysDownStateQueue in newSession function

* chore: resolve conflicts

* resolve conflicts

---------

Co-authored-by: Adam Shiervani <adam.shiervani@gmail.com>
2025-09-18 13:35:47 +02:00
Aveline 72e3013337
feat: send all paste keystrokes to backend (#789)
* feat: send all paste keystrokes to backend

* feat: cancel paste mode

* wip: send macro using hidRPC channel

* add delay

* feat: allow paste progress to be cancelled

* allow user to override delay

* chore: clear keysDownState

* fix: use currentSession.reportHidRPCKeyboardMacroState

* fix: jsonrpc.go:1142:21: Error return value is not checked (errcheck)

* fix: performance issue of Uint8Array concat

* chore: hide delay option when debugMode isn't enabled

* feat: use clientSide macro if backend doesn't support macros

* fix: update keysDownState handling

* minor issues

* refactor

* fix: send duplicated keyDownState

* chore: add max length for paste text

---------

Co-authored-by: Adam Shiervani <adam.shiervani@gmail.com>
2025-09-18 13:00:57 +02:00
Marc 25b102ac34
fix: ensure that security-key backed SSH keys are supported (#807) 2025-09-17 12:14:45 +02:00
Aveline 5c94c6c87f
chore: upgrade jetkvm native and fix the params of fadeIn / fadeOut (#808) 2025-09-17 01:38:23 +02:00
Marc Brooks cf679978be
fix(timesync): ensure that auto-update waits for time sync (#609)
- Added check to not attempt auto update if time sync is needed and not yet successful (delays 30 second to recheck).
- Added resync of time when DHCP or link state changes if online
- Added conditional* fallback from configured* NTP servers to the IP-named NTP servers, and then to the DNS named ones if that fails
- Added conditional* fallback from the configured* HTTP servers to the default DNS named ones.
- Uses the configuration* option for how many queries to run in parallel
- Added known static IPs for time servers (in case DNS resolution isn't up yet)
- Added time.cloudflare.com to fall-back NTP servers
- Added fallback to NTP via hostnames
- Logs the resultant time (and mode)
2025-09-16 15:37:02 +02:00
Marc Brooks 80a8b9e9e3
feat: Adds IPv6 disabling feature (#803)
* Allow disabling IPv6

Simply ignores any IPv6 addresses in the lease and doesn't offer them to the RPC
Also fixed display issue for IPv6 link local address.
Fixes https://github.com/orgs/jetkvm/projects/7/views/1?pane=issue&itemId=122761546&issue=jetkvm%7Ckvm%7C685

* Don't listen on disabled addresses in mDNS or web server.

* We have to set the IPv4 and IPv6 modes on the server.
2025-09-16 12:44:56 +02:00
Aveline 1717549578
fix: goroutine leak issue of cloudBlink (#801)
* fix: goroutine leak issue of cloudBlink

* chore: add lock and allow context to be cancelled earlier
2025-09-12 18:30:35 +02:00
Aveline 37b1a8bf34
docs: update pprof section of DEVELOPMENT.md (#802) 2025-09-12 11:11:28 +02:00
Marc Brooks ca8b06f4cf
chore: enhance the gzip and cacheable handling of static files
Add SVG and ICO to cacheable files.
Emit robots.txt directly.
Recognize WOFF2 (font) files as assets (so the get the immutable treatment)
Pre-gzip the entire /static/ directory (not just /static/assets/) and include SVG, ICO, and HTML files
Ensure fonts.css is processed by vite/rollup so that the preload and css reference the same immutable files (which get long-cached with hashes)
Add CircularXXWeb-Black to the preload list as it is used in the hot-path.
Handle system-driven color-scheme changes from dark to light correctly.
2025-09-12 08:41:41 +02:00
Aveline 33e099f258
update netboot.xyz-multiarch.iso to 2.0.88 (#799)
* chore: update netboot.xyz-multiarch.iso to 2.0.88

* feat: add script to update netboot.xyz iso
2025-09-12 08:41:17 +02:00
Aveline ea068414dc
feat: validate ssh public key before saving (#794)
* feat: validate ssh public key before saving

* fix: TestValidSSHKeyTypes
2025-09-11 23:32:40 +02:00
Adam Shiervani 8d1a66806c
refactor(ui): Don't fetch KeybardAndMouse Icon on every re-render (#795) 2025-09-11 19:57:35 +02:00
Aveline 6202e3cafa
chore: serve pre-compressed static files (#793) 2025-09-11 19:17:15 +02:00
dependabot[bot] c866230711
build(deps-dev): bump vite (#788)
Bumps the npm_and_yarn group with 1 update in the /ui directory: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).


Updates `vite` from 7.1.4 to 7.1.5
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.1.5/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 7.1.5
  dependency-type: direct:development
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-11 12:07:52 +02:00
Marc Brooks c8dd84c6b7
fix/Jiggler settings not saving (#786)
Ensure the jiggler config loads the defaults so they can be saved.
Ensure the file.Sync occurs before acknowledging save.
Also fixup the old KeyboardLayout to use en-US not en_US
2025-09-09 14:48:49 +02:00
Adam Shiervani c98592a412
feat(ui): Enhance EDID settings with loading state (#691)
* feat(ui): Enhance EDID settings with loading state and Fieldset component

* fix(ui): Improve notifications and adjust styling in custom EDID component

* fix(ui): specify JsonRpcResponse type
2025-09-08 11:38:49 +02:00
Marc Brooks 8fbad0112e
fix(ui): Don't render a button in a button (#782)
Gets rid of warning at initial page load.
2025-09-08 11:06:08 +02:00
Claus Holst 8a90555fad
Update URL Mount entries for Ubuntu, Fedora and Arch Linux (#783) 2025-09-08 11:02:46 +02:00
Adam Shiervani a7db0e8408
Enhance connection stats sidebar (#748)
* feat: add Metric component for data visualization

* refactor: update ConnectionStatsSidebar to use Metric component for improved data visualization

* feat: add someIterable utility function and update Metric components for consistent metric handling

- Introduced `someIterable` function to check for the presence of a metric in an iterable.
- Updated `CustomTooltip` and `Metric` components to use `metric` instead of `stat` for improved clarity.
- Refactored `StatChart` to align with the new metric naming convention.

* refactor: rename variable for clarity in Metric component

* docs: add JSDoc comments to createChartArray function in Metric component for better documentation

* feat: do an actual avg reference calc

* feat: Dont collect stats without a video track

* refactor: rename variables for clarity
2025-09-08 10:59:36 +02:00
Aveline bcc307b147
feat: hid rpc channel (#755)
* feat: use hidRpcChannel to save bandwidth

* chore: simplify handshake of hid rpc

* add logs

* chore: add timeout when writing to hid endpoints

* fix issues

* chore: show hid rpc version

* refactor hidrpc marshal / unmarshal

* add queues for keyboard / mouse event

* chore: change logging level of JSONRPC send event to trace

* minor changes related to logging

* fix: nil check

* chore: add comments and remove unused code

* add useMouse

* chore: log msg data only when debug or trace mode

* chore: make tslint happy

* chore: unlock keyboardStateLock before calling onKeysDownChange

* chore: remove keyPressReportApiAvailable

* chore: change version handle

* chore: clean up unused functions

* remove comments
2025-09-04 22:27:56 +02:00
Marc Brooks e8ef82e582
feat(ui) Fix the CapsLock and Shift key for VirtualKeyboard (#779)
* feat(ui) Fix the CapsLock and Shift key for VirtualKeyboard

* PR feedback: Default LED state in store
2025-09-04 22:26:51 +02:00
Marc Brooks 5f3dd89d55
Upgrade vite and react-router (#778)
| Package                           | From | To       |
|----------------------------------|-----------|---------|
react-router                       | ( new ) | 7.8.2 |
react-router-dom             | 6.22.3 | ( del ) |
@vitejs/plugin-react-swc | 3.10.2 | 4.0.1 |
vite                                      | 6.3.5   | 7.1.4 |
2025-09-04 12:20:01 +02:00
Marc Brooks 1dda6184da
Bumps recharts to 3.1.2 (#777)
* Upgrade UI packages

| Package                                     | From     | To         |
|-----------------------------------|----------|----------|
| dayjs                                           | 1.1.13   | 1.11.18 |
| react-simple-keyboard               | 3.8.115 | 3.8.119 |
| @types/react                              | 19.1.11 | 19.1.12 |
| @types/react-dom                     | 19.1.8   | 19.1.9   |
|  @types/semver                         | 7.7.0      | 7.7.1    |
| @types/validator                        | 13.15.2 | 13.15.3 |
| @typescript-eslint/eslint-plugin | 8.41.0   | 8.42.0   |
| @typescript-eslint/parser           | 8.41.0   | 8.42.0   |

Also fixed lint warning about the missing autocomplete on the password field

* Upgrade recharts to 3.1.2

This is a major version jump, builds and runs correctly
2025-09-04 12:07:19 +02:00
Marc Brooks 825d0311d6
Upgrade UI packages (#776)
| Package                                     | From     | To         |
|-----------------------------------|----------|----------|
| dayjs                                           | 1.1.13   | 1.11.18 |
| react-simple-keyboard               | 3.8.115 | 3.8.119 |
| @types/react                              | 19.1.11 | 19.1.12 |
| @types/react-dom                     | 19.1.8   | 19.1.9   |
|  @types/semver                         | 7.7.0      | 7.7.1    |
| @types/validator                        | 13.15.2 | 13.15.3 |
| @typescript-eslint/eslint-plugin | 8.41.0   | 8.42.0   |
| @typescript-eslint/parser           | 8.41.0   | 8.42.0   |

Also fixed lint warning about the missing autocomplete on the password field
2025-09-04 12:02:59 +02:00
Aveline f3fe78af5d
chore: upgrade deps (#780) 2025-09-04 11:40:49 +02:00
dependabot[bot] d0b3781aaa
build(deps): bump actions/checkout from 4 to 5 (#759)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-04 11:19:00 +02:00
Adam Shiervani c68e15bf89
Clean up Virtual Keyboard styling (#761)
* Clean up Virtual Keyboard styling

Make the header a bit taller
Add keyboard layout name to header
Make the detached keyboard render smaller key text so you can read them.
Updated the settings text for keyboard layout.

* Add the key graphics and missing keys

* style(ui): add cursor-pointer class to Button component for better UX

* refactor(ui): Improve header styling and detach bug

- Remove unused AttachIcon and related SVG asset.
- Replace icon usage with a styled LinkButton to improve consistency.
- Simplify and reformat VirtualKeyboard component for better readability.

* refactor(ui): Hide keyboard layout settings on mobile and fix minor styling

---------

Co-authored-by: Marc Brooks <IDisposable@gmail.com>
2025-09-03 11:33:07 +02:00
Marc Brooks 94521ef6db
chore/Deprecate browser mount (#752)
* chore/Deprecate browser mount

No longer supported.

* Remove device-side go code

* Removed diskChannel and localFile

* Removed RemoteVirtualMediaState.WebRTC

Also removed dead go code (to make that lint happy!)
2025-08-28 23:46:55 +02:00
Marc Brooks 66cccfe9e1
Add application icon for Safari and saved-bookmarks (#749) 2025-08-28 12:01:04 +02:00
Adam Shiervani a42384fed6
enhancement: add new EDID for DELL iDRAC (#693) 2025-08-27 10:16:17 +02:00
Marc Brooks 3ec243255b
Add ability to track modifier state on the device (#725)
Remove LED sync source option and add keypress reporting while still working with devices that haven't been upgraded
We return the modifiers as the valid bitmask so that the VirtualKeyboard and InfoBar can represent the correct keys as down. This is important when we have strokes like Left-Control + Right-Control + Keypad-1 (used in switching KVMs and such).
Fix handling of modifier keys in client and also removed the extraneous resetKeyboardState.
Manage state to eliminate rerenders by judicious use of useMemo.
Centralized keyboard layout and localized display maps
Move keyboardOptions to useKeyboardLayouts
Added translations for display maps.
Add documentation on the legacy support.

Return the KeysDownState from keyboardReport
Clear out the hidErrorRollOver once sent to reset the keyboard to nothing down.
Handles the returned KeysDownState from keyboardReport
Now passes all logic through handleKeyPress.
If we get a state back from a keyboardReport, use it and also enable keypressReport because we now know it's an upgraded device.
Added exposition on isoCode management

Fix de-DE chars to reflect German E2 keyboard.
https://kbdlayout.info/kbdgre2/overview+virtualkeys

Ran go modernize
Morphs Interface{} to any
Ranges over SplitSeq and FieldSeq for iterating splits
Used min for end calculation remote_mount.Read
Used range 16 in wol.createMagicPacket
DID NOT apply the Omitempty cleanup.

Strong typed in the typescript realm.
Cleanup react state management to enable upgrading Zustand
2025-08-26 17:09:35 +02:00
Marc Brooks 05bf61152b
feature/Faster loading (#746)
* feature/Faster loading

This refactors all the hot-path components for an already-setup JetKVM so that we only
lazy-load the components off the main path. This greatly reduces the initial .JS size at
initial page load from a single file of

dist/assets/index-D4LZBdmN.js                          1,969.46 kB │ gzip: 570.08 kB

To these files, of which the hot-path only loads the 963.29 kB index for a savings of
just over a megabyte (180kb savings in gzip).

dist/assets/login-DA9KVVX1.js                                     0.64 kB │ gzip:   0.40 kB
dist/assets/signup-Bb_VCzY1.js                                    0.67 kB │ gzip:   0.40 kB
dist/assets/devices._id.settings.macros.add-DpBnq5E0.js           0.82 kB │ gzip:   0.55 kB
dist/assets/devices._id.settings.appearance-VHd5B2H2.js           0.91 kB │ gzip:   0.52 kB
dist/assets/devices._id.settings.general.reboot-DsRBP5Dd.js       1.01 kB │ gzip:   0.52 kB
dist/assets/UpdateInProgressStatusCard-DJCdJo-z.js                1.05 kB │ gzip:   0.54 kB
dist/assets/devices._id.other-session-BpXjEP6K.js                 1.09 kB │ gzip:   0.56 kB
dist/assets/devices.already-adopted-BC1xoKrN.js                   1.16 kB │ gzip:   0.57 kB
dist/assets/Checkbox-DGO277w5.js                                  1.24 kB │ gzip:   0.64 kB
dist/assets/devices._id.settings.keyboard-Cno0kaUr.js             1.59 kB │ gzip:   0.81 kB
dist/assets/devices._id.settings.general._index-CNW0Pj5B.js       1.71 kB │ gzip:   0.76 kB
dist/assets/devices._id.settings.macros.edit-BYQGw2CJ.js          1.92 kB │ gzip:   1.00 kB
dist/assets/ConfirmDialog-lzerZkf7.js                             2.77 kB │ gzip:   1.13 kB
dist/assets/AuthLayout-H4vGP3TU.js                                2.96 kB │ gzip:   1.41 kB
dist/assets/AutoHeight-B-TU1fRg.js                                4.07 kB │ gzip:   1.63 kB
dist/assets/devices._id.settings.video-O3qJWstQ.js                5.68 kB │ gzip:   2.17 kB
dist/assets/devices._id.settings.advanced-Drd_iPzw.js             5.98 kB │ gzip:   2.08 kB
dist/assets/devices._id.settings.macros-D3unB0uf.js               6.05 kB │ gzip:   2.13 kB
dist/assets/devices._id.settings.access.local-auth-BltQI66N.js    6.17 kB │ gzip:   1.54 kB
dist/assets/devices._id.settings.mouse-CAwDHqxl.js               10.02 kB │ gzip:   3.59 kB
dist/assets/devices._id.settings.general.update-jkzXML1U.js      10.22 kB │ gzip:   2.67 kB
dist/assets/devices._id.settings.hardware-B7v3lfwA.js            10.41 kB │ gzip:   3.03 kB
dist/assets/devices._id.settings.network-CJYfzFt2.js             25.23 kB │ gzip:   7.21 kB
dist/assets/devices._id.mount-4AT1reig.js                        43.92 kB │ gzip:  19.81 kB
dist/assets/MacroForm-BQpdQgFn.js                                49.75 kB │ gzip:  16.25 kB
dist/assets/connectionStats-NM-PZeH3.js                         400.14 kB │ gzip: 110.33 kB
dist/assets/Terminal-Dgo3sfr-.js                                425.05 kB │ gzip: 109.49 kB
dist/assets/index-w6H2Mz3f.js                                   963.29 kB │ gzip: 294.20 kB

* Remove feral async declarations on things that have no await
2025-08-26 16:55:08 +02:00
Marc Brooks d952480c2a
fix: useJsonRpc "any" issue
PR #743 didn't have all the files included in the commit.
Mea culpa, many apologies.
2025-08-24 11:40:43 +02:00
Alex Ballas 8e27cd6b60
chore: ensure that rpc messages get processed sequentially and avoid phantom and repeated key presses (#744) 2025-08-22 20:15:46 +02:00
Marc Brooks bb87fb5a1a
fix: compiler error (#743)
Using { send } gives the resp a type instead of any
2025-08-22 12:15:27 +02:00
Adam Shiervani 8527b1eff1
feat: improve custom jiggler settings and add timezone support (#742)
* feat: add timezone support to jiggler and fix custom settings persistence

- Add timezone field to JigglerConfig with comprehensive IANA timezone list
- Fix custom settings not loading current values
- Remove business hours preset, add as examples in custom settings
- Improve error handling for invalid cron expressions

* fix: format jiggler.go with gofmt

* fix: add embedded timezone data and validation

- Import time/tzdata to embed timezone database in binary
- Add timezone validation in runJigglerCronTab() to gracefully fallback to UTC
- Add timezone to debug logging in rpcSetJigglerConfig
- Fixes 'unknown time zone' errors when system lacks timezone data

* refactor: add timezone field comments from jiggler options

* chore: move tzdata to backend

* refactor: fix JigglerSetting linting

- Adjusted useEffect dependency to include send function for better data fetching
- Modified layout classes for improved responsiveness and consistency
- Cleaned up code formatting for better readability

---------

Co-authored-by: Siyuan Miao <i@xswan.net>
2025-08-19 16:50:42 +02:00
Serhii 9f573200b1
Update mount list for new Debian 13 release (#739)
* Update mount list for new Debian 13 release

* Keep Debian 12 Bookworm as old-stable release
2025-08-18 11:20:20 +02:00
Marc Brooks 608f69db13
Lint fix from last merge. (#733) 2025-08-13 11:13:42 +02:00
jackislanding f7b8efde7c
Added crontab scheduler for jiggler (#316) 2025-08-12 20:50:03 +02:00
Marc Brooks 33ac9fe0b6
chore(ui)/package upgrades (#724)
| Package                          | From     | To           |
| -------------------------------- | ----------- | ------------ |
| @headlessui/react        | 2.2.4     | 2.2.7        |
| framer-motion              | 12.23.3 | 12.23.12 |
| react                                | 19.1.0   | 19.1.1     |
| react-dom                       | 19.1.0   | 19.1.1    |
| react-simple-keyboard | 3.8.93   | 3.8.106  |
|@eslint/js                         | 9.30.1   | 9.32.0    |
| @types/react                  | 19.1.8   | 19.1.9    |
| @types/react-dom         | 19.1.8   | 19.1.9   |
|eslint                                 | 9.30.1   | 9.32.0    |
|eslint-config-prettier       | 10.1.5   | 10.1.8   |
| typescript                         |  5.8.3    | 5.9.2     |
2025-08-12 11:24:05 +02:00
Silke pilon 55fbd6c359 docs: add comprehensive DEVELOPMENT.md for JetKVM (#692)
* docs: add comprehensive DEVELOPMENT.md for JetKVM

Add a detailed development guide covering setup, project structure,
and workflows for both full device and frontend-only development.

Include prerequisites, build commands, deployment scripts, environment
variables, and testing instructions to streamline onboarding and
contributions.

This improves developer experience and standardizes development
practices across the project.

* docs: clean up DEVELOPMENT.md by removing outdated sections

Remove the Custom Build Tags and Release Process sections to simplify
the documentation and avoid confusion with deprecated build and release
instructions. Focus the document on current performance profiling steps.

* docs: rewrite DEVELOPMENT.md for clearer setup and usage

Revise the JetKVM development guide to improve clarity and usability.
Simplify the introduction and reorganize prerequisites and setup steps
to help new developers get started quickly. Add explicit instructions for
cloning, tool verification, deployment, and testing. Streamline common
tasks sections with clear commands for UI and backend development,
testing, and log viewing. Update project layout overview for easier
navigation. These changes reduce onboarding friction and enhance the
developer experience.

* docs: remove duplicate "Get Started" header in DEVELOPMENT.md

Clean up the DEVELOPMENT.md file by deleting the repeated
"Get Started" header

* docs: add recommended development environment section

Add guidance recommending Linux or macOS for development and suggest
using WSL on Windows to ensure compatibility with shell scripts and
build tools. This improves the developer experience and reduces setup
issues across different operating systems.

* docs: add links to prerequisites in DEVELOPMENT.md

Update DEVELOPMENT.md to URLs for Go, Node.js, Git, and SSH
access prerequisites. This improves clarity and helps developers quickly
find installation resources.
2025-07-16 00:04:41 +02:00
Aveline cff3ddad29
chore: add issue templates (#686)
* chore: add issue templates

* chore: add remote device info
2025-07-14 18:10:49 +02:00
Marc Brooks b4525b8760
chore/ Fix go lint error (#683) 2025-07-11 23:41:05 +02:00
Marc Brooks 5a3ce2d6ec
chore(ui) Upgrade packages (#682) 2025-07-11 23:41:01 +02:00
Ben Kochie f1953fddbc
chore: add metrics for configuration and WOL (#193)
* Configuration load success/timestamp.
* Wake-on-Lan packets/errors.

Signed-off-by: SuperQ <superq@gmail.com>
2025-07-11 18:14:32 +02:00
Marc Brooks 9ba97ebe67
chore(ui): Clean new keyboard option (#495)
Fixed the Tailwind CSS syntax for `in` (nested) selector
Added missing React dependency for `useEffect`
2025-07-11 17:56:03 +02:00
Marc Brooks 5fb8d866ba
refactor(ui): Refactor the keyboardLayouts (#497)
Add missing keyboard mappings for most layouts
Change  pasteModel.tsx to use the new structure and vastly clarified the way that keys are emitted.
Make each layout export just the KeyboardLayout object (which is a package of isoCode, name, and chars)
Made keyboardLayouts.ts export a function to select keyboard by `isoCode`, export the keyboards as label . value pairs (for a select list) and the list of keyboards.
Changed devices.$id.settings.keyboard.tsx use the exported keyboard option list.
2025-07-11 17:49:06 +02:00
rmschooley 3359f8fca4
Remove Out Endpoint Descriptors from Absolute Mouse and Relative Mouse (#542)
* Update hid_mouse_absolute.go

Added attribute to remove unnecessary out endpoint.

* Update hid_mouse_relative.go

Added attribute to remove unnecessary out endpoint.

* Update hid_keyboard.go

Added attribute to explicitly keep currently needed out endpoint and to make listed attributes consistent across the keyboard and mouse devices.

---------

Co-authored-by: Aveline <352441+ym@users.noreply.github.com>
2025-07-11 17:43:37 +02:00
Daniel Collins ef95643a86
Implement HTTP proxy option (#515). (#521)
This commit adds a "Proxy" field to the network settings screen, which
can be used to specify a HTTP proxy for any outgoing requests from the
device.
2025-07-11 17:43:22 +02:00
Daniel Collins 1fc603b553
Add -i/--install option to dev_deploy.sh (#527)
Running `dev_deploy.sh -i` will build the app in release mode and
install it to the device for longer term development/testing or just
running a custom variant of the app.
2025-07-11 17:09:49 +02:00
Bradley Wilson-Hunt aada3d95e0
feat(metrics): adding prometheus metrics for dc power extension (#556) 2025-07-11 17:04:41 +02:00
Aveline d704fcc6c7
feat: add command to show version (#604)
* feat: add -version flag for jetkvm_app

* move code to kvm package
2025-07-11 11:32:46 +02:00
Siyuan Miao ab3dda6dee chore(network): fix linting error errcheck 2025-07-11 11:30:02 +02:00
Marc Brooks 4a23f22a55
chore: upgrade ui packages (#571)
Move to current on all non-major upgrades
Fixes the tainted hardware WebGL video renderer if video settings are at default (1.0) values

## Runtime

|  Package | From  | To  |
|---|---|---|
| @headlessui/react | 2.2.3 | 2.2.4 |
| @vitejs/plugin-basic-ssl | 2.0.0 | 2.1.0 |
| cva | 1.0.0-beta.3 | 1.0.0-beta.4 |
| focus-trap-react | 11.0.3 | 11.0.4 |
| framer-motion | 12.11.5 | 12.23.0 |
| react-simple-keyboard | 3.8.72 | 3.8.89 |
| tailwind-merge | 3.3.0 | 3.3.1 |
| validator | 13.15.0 | 13.15.15 |

## Dev

|  Package | From  | To  |
|---|---|---|
| @eslint/compat | 1.2.9 | 1.3.1 |
| @eslint/js | 9.26.0 | 9.30.1 |
| @tailwindcss/postcss | 4.1.7 | 4.1.11 |
| @tailwindcss/vite | 4.1.8 | 4.1.10 |
| @types/react | 19.1.4 | 19.1.8  |
| @types/react-dom | 19.1.5 | 19.1.6 |
| @types/validator | 13.15.0 | 13.15.2 |
| @typescript-eslint/eslint-plugin | 8.32.1 | 8.34.0 |
| @typescript-eslint/parser | 8.32.1 | 8.35.1  |
| @vitejs/plugin-react-swc | 3.9.0 | 3.10.2 |
| eslint | 9.26.0 | 9.30.1 |
| globals | 16.1.0 | 16.3.0 |
| postcss  | 8.5.3 | 8.5.6 |
| prettier | 3.5.3 | 3.6.2 |
| prettier-plugin-tailwindcss | 0.6.11 | 0.6.13 |
| tailwindcss | 4.1.7 | 4.1.11 |
2025-07-11 08:06:17 +02:00
Marc Brooks 11a095c0f6
feat(ntp): enhances time sync with DHCP NTP and custom servers (#625)
* Ensure the mDNS mode is set every time network state changes

Eliminates (mostly) duplicate code

* Add custom NTP and HTTP time sync servers

Since the ordering may have been previously defaulted and saved as "ntp,http", but that was being ignored and fallback-defaults were being used, in Ordering, `ntp` means use the fallback NTP servers, and `http` means use the fallback HTTP URLs. Thus `ntp_user_provided` and `http_user_provided` are the user specified static lists.

* Add support for using DHCP-provided NTP server
2025-07-11 08:04:19 +02:00
Aveline 584768bacf
chore: remove /device/ui-config.js endpoint (#678) 2025-07-10 12:04:47 +02:00
adammkelly 488276f3a8
feat(ui): reboot device (#421) (#505) 2025-07-10 00:02:13 +02:00
Patrick Hofmann 7267347261
feat(dc-power-extension): power restore mode in DCPowerControl component (#672)
* DC-extension: Supporting to set the power restore mode in DCPowerControl component

* fixing lint issue
2025-07-09 23:58:46 +02:00
Marc Brooks 393bc122d4
chore: fix the base usb configuration (#610)
In reviewing the config.go settings for idProduct and bcdDevice are not formatted correctly. All examples on GitHub have 0x0104 and 0x0100 respectively. The idProduct value gets overwritten with valid values when you change the configuration (because they are correct in the options), but until you do the USB initialization will not be correct.
2025-07-09 23:57:51 +02:00
Marc Brooks 6d13e1be12
chore: remove ActionBar-Ctrl-Alt-Del (#669) 2025-07-09 23:53:44 +02:00
Siyuan Miao bde0a086ab chore: bump to 0.4.7 2025-07-03 19:03:46 +02:00
Aveline 9c9335da31
chore: typo 'supression' should be 'suppression' (#671) 2025-07-03 17:28:00 +02:00
dependabot[bot] 090e0b4b47
build(deps): bump actions/setup-go from 4.2.1 to 5.5.0 (#666)
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 4.2.1 to 5.5.0.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v4.2.1...v5.5.0)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-version: 5.5.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-03 17:21:11 +02:00
dependabot[bot] 48a7a638a3
build(deps): bump github.com/pion/webrtc/v4 from 4.1.2 to 4.1.3 (#667)
Bumps [github.com/pion/webrtc/v4](https://github.com/pion/webrtc) from 4.1.2 to 4.1.3.
- [Release notes](https://github.com/pion/webrtc/releases)
- [Changelog](https://github.com/pion/webrtc/blob/master/.goreleaser.yml)
- [Commits](https://github.com/pion/webrtc/compare/v4.1.2...v4.1.3)

---
updated-dependencies:
- dependency-name: github.com/pion/webrtc/v4
  dependency-version: 4.1.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-03 17:21:04 +02:00
dependabot[bot] e4f6a713a5
build(deps): bump github.com/Masterminds/semver/v3 from 3.3.1 to 3.4.0 (#668)
Bumps [github.com/Masterminds/semver/v3](https://github.com/Masterminds/semver) from 3.3.1 to 3.4.0.
- [Release notes](https://github.com/Masterminds/semver/releases)
- [Changelog](https://github.com/Masterminds/semver/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Masterminds/semver/compare/v3.3.1...v3.4.0)

---
updated-dependencies:
- dependency-name: github.com/Masterminds/semver/v3
  dependency-version: 3.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-03 17:19:27 +02:00
Aveline 9fcf74b398
fix(display): reset display state after native binary is restarted (#654)
* fix(usbgadget): add lock for logWithSupression

* fix(display): reset display state after native binary is restarted
2025-07-03 17:18:09 +02:00
Marc Brooks 353099001f
build: upgrade packages and move to GitHub runner
* Move to GitHub runner for build action

Lint action

* Move to go 1.24.4

* Upgrade go packages

 github.com/coreos/go-oidc/v3  v3.11.0 -> v3.14.1
github.com/creack/pty v1.1.23 -> v1.1.24
github.com/gwatts/rootcerts  v0.0.0-20240401182218-3ab9db955caf -> v0.0.0-20250601184604-370a9a75f341
github.com/pion/logging v0.2.3 -> v0.2.4
github.com/pion/webrtc/v4 v4.0.16 -> v4.1.2
github.com/prometheus/common v0.62.0 -> v0.65.0
github.com/vishvananda/netlink v1.3.1 -> v1.3.1
go.bug.st/serial v1.6.2 -> v1.6.4
2025-07-01 13:39:41 +02:00
Aveline 73f5659618
fix(usbgadget): add lock for logWithSupression (#653) 2025-07-01 12:54:38 +02:00
iain MacDonnell 960f555790
fix: ensure certStore is initialised when adding custom cert (#639)
Fixes: #612
2025-06-30 18:58:39 +02:00
Siyuan Miao fe127ed41c chore: bump version to 0.4.6 2025-06-25 13:28:09 +02:00
Aveline 3e7d8fb0f5
feat(usbgadget): suppress duplicate error logs (#630). 2025-06-20 18:52:37 +02:00
Marc Brooks 0d7f47c109
fix(ui) firefox permissions error handling (#631) 2025-06-20 14:24:54 +02:00
iain MacDonnell 254c001572
fix: keyboard_layout default config (en-US/en_US) (#633) 2025-06-20 14:13:36 +02:00
Aveline 6f037a832d
feat(native): restart jetkvm_native automatically (#629) 2025-06-20 14:08:19 +02:00
Marc Brooks ccba27cedd
chore(mDNS): ensure the mDNS mode is set every time network state changes (#624)
Eliminates (mostly) duplicate code
2025-06-19 09:29:21 +02:00
ronskvm cf9c6e5cc8
chore(hid): change absolute mouse usb interface descriptor's subclass field to zero
Changed absolute mouse usb interface descriptor's subclass field to zero.
2025-06-19 09:11:21 +02:00
Siyuan Miao ffeaf8cced ui(actionBar): remove Ctrl + Alt + Del in favor of Keyboard Macros 2025-06-19 00:35:17 +02:00
Caedis a1ed28c676
build: allow the versions in the Makefile to be overwritten with ENV variables (#619) 2025-06-16 11:30:57 +02:00
282 changed files with 96295 additions and 4764 deletions

View File

@ -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"
]
}
}
}
}

24
.devcontainer/install-deps.sh Executable file
View File

@ -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

76
.github/ISSUE_TEMPLATE/bug.yml vendored Normal file
View File

@ -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

10
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -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.

46
.github/ISSUE_TEMPLATE/feature.yml vendored Normal file
View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
.gitignore vendored
View File

@ -3,4 +3,5 @@ static/*
.idea
.DS_Store
device-tests.tar.gz
device-tests.tar.gz
node_modules

View File

@ -23,6 +23,9 @@ linters:
- linters:
- errcheck
path: _test.go
- linters:
- forbidigo
path: cmd/main.go
- linters:
- gochecknoinits
path: internal/logging/sse.go

View File

@ -0,0 +1,8 @@
{
"hash": "10b5fb8d",
"configHash": "fd609f12",
"lockfileHash": "e3b0c442",
"browserHash": "4bebaebb",
"optimized": {},
"chunks": {}
}

3
.vite/deps/package.json Normal file
View File

@ -0,0 +1,3 @@
{
"type": "module"
}

View File

@ -1,3 +1,7 @@
{
"tailwindCSS.classFunctions": ["cva", "cx"]
"tailwindCSS.classFunctions": [
"cva",
"cx"
],
"cmake.sourceDirectory": "/Users/aveline/Projects/JetKVM/ymjk/internal/native/cgo"
}

356
DEVELOPMENT.md Normal file
View File

@ -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)
[![Twitter](https://img.shields.io/twitter/url/https/twitter.com/jetkvm.svg?style=social&label=Follow%20%40JetKVM)](https://twitter.com/jetkvm)
[![Go Report Card](https://goreportcard.com/badge/github.com/jetkvm/kvm)](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).

33
Dockerfile.build Normal file
View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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()
}

View File

@ -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
}

BIN
cprof.out Normal file

Binary file not shown.

53
dc_metrics.go Normal file
View File

@ -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)
}
}

View File

@ -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."

View File

@ -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
View File

@ -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
View File

@ -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
View File

@ -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=

BIN
gotestsum Executable file

Binary file not shown.

259
hidrpc.go Normal file
View File

@ -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)
}

View File

@ -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)
}

View File

@ -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) {

View File

@ -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

123
internal/hidrpc/hidrpc.go Normal file
View File

@ -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,
}
}

207
internal/hidrpc/message.go Normal file
View File

@ -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
}

84
internal/lldp/afpacket.go Normal file
View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

106
internal/lldp/lldp.go Normal file
View File

@ -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
}

57
internal/lldp/neigh.go Normal file
View File

@ -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
}

264
internal/lldp/rx.go Normal file
View File

@ -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
}

View File

@ -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
}
}

View File

@ -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...)
}

View File

@ -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;

View File

@ -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

3
internal/native/cgo/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
build
deps
ui_index.c

View File

@ -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)

55746
internal/native/cgo/Makefile Normal file

File diff suppressed because it is too large Load Diff

53
internal/native/cgo/build.sh Executable file
View File

@ -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

378
internal/native/cgo/ctrl.c Normal file
View File

@ -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();
}

View File

@ -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

180
internal/native/cgo/edid.c Normal file
View File

@ -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;
}
}

View File

@ -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

98
internal/native/cgo/log.h Normal file
View File

@ -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

View File

@ -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;
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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);
}

View File

@ -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

1
internal/native/cgo/ui Symbolic link
View File

@ -0,0 +1 @@
../eez/src/ui

View File

@ -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."

View File

@ -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

728
internal/native/cgo/video.c Normal file
View File

@ -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;
}

View File

@ -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

View File

@ -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
}

View File

@ -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
}

72
internal/native/chan.go Normal file
View File

@ -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)
}
}

1
internal/native/ctrl.h Symbolic link
View File

@ -0,0 +1 @@
cgo/ctrl.h

133
internal/native/display.go Normal file
View File

@ -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{})
}

1
internal/native/eez/.gitignore vendored Normal file
View File

@ -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

17
internal/native/log.go Normal file
View File

@ -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
}

81
internal/native/native.go Normal file
View File

@ -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)
}

21
internal/native/single.go Normal file
View File

@ -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")
}
}

25
internal/native/video.go Normal file
View File

@ -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()
}

View File

@ -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)

View File

@ -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

46
internal/network/lldp.go Normal file
View File

@ -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
}

View File

@ -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 {

View File

@ -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(),

View File

@ -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

View File

@ -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

View File

@ -73,6 +73,7 @@ var (
},
[]string{"url"},
)
metricNtpServerInfo = promauto.NewGaugeVec(
prometheus.GaugeOpts{
Name: "jetkvm_timesync_ntp_server_info",

View File

@ -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,

View File

@ -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)

72
internal/tzdata/gen.go Normal file
View File

@ -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)
}
}

602
internal/tzdata/tzdata.go Normal file
View File

@ -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",
}

View File

@ -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

View File

@ -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 != "" {

View File

@ -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

View File

@ -1,3 +1,7 @@
package usbgadget
import "time"
const dwc3Path = "/sys/bus/platform/drivers/dwc3"
const hidWriteTimeout = 10 * time.Millisecond

View File

@ -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
}

View File

@ -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

View File

@ -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

View File

@ -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
}

View File

@ -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()
}

73
internal/utils/ssh.go Normal file
View File

@ -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