├── .github └── workflows │ ├── audit.yml │ ├── rust_linux.yml │ ├── rust_macos.yml │ └── rust_windows.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE ├── README.md ├── build.rs ├── deny.toml ├── examples └── unicode.rs ├── hid-io-client ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── examples │ ├── lsnodes.rs │ ├── rpc.rs │ ├── tool.rs │ └── watchnodes.rs └── src │ └── lib.rs ├── hid-io-protocol ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── spec │ └── README.md └── src │ ├── buffer.rs │ ├── commands │ ├── mod.rs │ └── test.rs │ ├── lib.rs │ └── test.rs ├── hid-io.service ├── hidio.plist ├── logo.png ├── misc └── images │ ├── HID-IO_Box_Logo.png │ ├── HID-IO_Overview.ai │ ├── HID-IO_Overview.png │ ├── README.md │ └── hid-io_logo.png ├── python ├── .gitignore ├── LICENSE ├── Pipfile ├── README.md ├── examples │ ├── info.py │ └── unicode_text.py ├── hidiocore │ ├── __init__.py │ ├── client │ │ └── __init__.py │ └── schema │ │ ├── __init__.py │ │ ├── common.capnp │ │ ├── daemon.capnp │ │ ├── hidio.capnp │ │ └── keyboard.capnp ├── pyproject.toml ├── requirements.txt └── tests │ ├── example.py │ └── test_examples.py ├── schema ├── spec ├── README.md ├── interface │ └── README.md └── protocol ├── src ├── api │ ├── capnp.rs │ └── mod.rs ├── bin │ ├── hid-io-core.rs │ └── install_service.rs ├── device │ ├── evdev │ │ └── mod.rs │ ├── hidapi │ │ └── mod.rs │ └── mod.rs ├── lib.rs ├── logging.rs ├── mailbox.rs ├── module │ ├── daemonnode.rs │ ├── displayserver │ │ ├── mod.rs │ │ ├── quartz.rs │ │ ├── wayland.rs │ │ ├── winapi.rs │ │ └── x11.rs │ ├── mod.rs │ └── vhid │ │ ├── mod.rs │ │ └── uhid │ │ └── mod.rs └── packet.rs ├── uninstall_service.bat └── vt.sh /.github/workflows/audit.yml: -------------------------------------------------------------------------------- 1 | name: "Audit Dependencies" 2 | on: 3 | push: 4 | paths: 5 | # Run if workflow changes 6 | - '.github/workflows/audit.yml' 7 | # Run on changed dependencies 8 | - '**/Cargo.toml' 9 | - '**/Cargo.lock' 10 | # Run if the configuration file changes 11 | - '**/audit.toml' 12 | # Rerun periodicly to pick up new advisories 13 | schedule: 14 | - cron: '0 0 * * *' 15 | # Run manually 16 | workflow_dispatch: 17 | 18 | permissions: read-all 19 | 20 | jobs: 21 | audit: 22 | runs-on: ubuntu-latest 23 | permissions: 24 | issues: write 25 | steps: 26 | - uses: actions/checkout@v3 27 | with: 28 | submodules: true 29 | - uses: dtolnay/rust-toolchain@nightly 30 | - uses: actions-rust-lang/audit@v1 31 | name: Audit Rust Dependencies 32 | with: 33 | ignore: RUSTSEC-2020-0071,RUSTSEC-2021-0139 34 | 35 | deny: 36 | runs-on: ubuntu-latest 37 | permissions: 38 | issues: write 39 | steps: 40 | - uses: actions/checkout@v3 41 | with: 42 | submodules: true 43 | - uses: dtolnay/rust-toolchain@nightly 44 | - name: cargo-binstall 45 | run: | 46 | mkdir -p ~/.cargo/bin 47 | wget https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-x86_64-unknown-linux-musl.tgz 48 | tar xf cargo-binstall*.tgz -C ~/.cargo/bin 49 | - run: cargo binstall --no-confirm cargo-deny 50 | - name: Cargo Deny 51 | run: cargo deny check licenses 52 | 53 | pants: 54 | runs-on: ubuntu-latest 55 | permissions: 56 | issues: write 57 | steps: 58 | - uses: actions/checkout@v3 59 | with: 60 | submodules: true 61 | - uses: dtolnay/rust-toolchain@nightly 62 | - name: cargo-binstall 63 | run: | 64 | mkdir -p ~/.cargo/bin 65 | wget https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-x86_64-unknown-linux-musl.tgz 66 | tar xf cargo-binstall*.tgz -C ~/.cargo/bin 67 | - run: cargo binstall --no-confirm cargo-pants 68 | - name: Cargo Pants 69 | run: cargo pants 70 | -------------------------------------------------------------------------------- /.github/workflows/rust_linux.yml: -------------------------------------------------------------------------------- 1 | name: Rust Linux 2 | 3 | on: [push, pull_request] 4 | 5 | env: 6 | deb_packages: >- 7 | capnproto 8 | libclang-dev 9 | libevdev-dev 10 | libudev-dev 11 | libwayland-dev 12 | libxcb-xkb-dev 13 | libxkbcommon-dev 14 | libxkbcommon-x11-dev 15 | libxtst-dev 16 | llvm 17 | ninja-build 18 | mingw_w64_packages: >- 19 | mingw-w64 20 | wine-stable 21 | wine-binfmt 22 | 23 | jobs: 24 | check: 25 | name: Check 26 | runs-on: ubuntu-latest 27 | steps: 28 | - uses: actions/checkout@v3 29 | - uses: dtolnay/rust-toolchain@nightly 30 | - name: Install dependencies 31 | run: | 32 | sudo apt-get update 33 | sudo apt-get install -y ${{ env.deb_packages }} 34 | - name: Cargo Check 35 | run: cargo check --all 36 | 37 | test: 38 | name: Test Suite 39 | runs-on: ubuntu-latest 40 | strategy: 41 | matrix: 42 | python-version: [3.7] 43 | steps: 44 | - uses: actions/checkout@v3 45 | - uses: dtolnay/rust-toolchain@nightly 46 | - name: Set up Python ${{ matrix.python-version }} 47 | uses: actions/setup-python@v4 48 | with: 49 | python-version: ${{ matrix.python-version }} 50 | - name: Install dependencies 51 | run: | 52 | sudo apt-get update 53 | sudo apt-get install -y ${{ env.deb_packages }} 54 | python -m pip install --upgrade pip 55 | - name: Cargo Test 56 | run: cargo test --all 57 | - name: Lint with flake8 58 | working-directory: ./python 59 | run: | 60 | python -m pip install flake8 61 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 62 | flake8 . --count --max-complexity=10 --max-line-length=127 --statistics 63 | - name: Install pycapnp dependencies and run test 64 | working-directory: ./python 65 | run: | 66 | python -m pip install cython 67 | CXXFLAGS=-fPIC CFLAGS=-fPIC python -m pip install -r requirements.txt 68 | python -m pytest -vvv 69 | 70 | build: 71 | name: Build 72 | runs-on: ubuntu-latest 73 | steps: 74 | - uses: actions/checkout@v3 75 | - uses: dtolnay/rust-toolchain@nightly 76 | - name: Install dependencies 77 | run: | 78 | sudo apt-get update 79 | sudo apt-get install -y ${{ env.deb_packages }} 80 | - name: Cargo Build 81 | run: cargo build --all 82 | - name: Cargo Install 83 | run: cargo install --path . --bins --examples --root dist 84 | - uses: actions/upload-artifact@v1.0.0 85 | with: 86 | name: linux_release_binaries 87 | path: dist/bin 88 | 89 | lib_build: 90 | name: Lib Build (hidapi) 91 | runs-on: ubuntu-latest 92 | steps: 93 | - uses: actions/checkout@v3 94 | - uses: dtolnay/rust-toolchain@nightly 95 | - name: Install dependencies 96 | run: | 97 | sudo apt-get update 98 | sudo apt-get install -y ${{ env.deb_packages }} 99 | - name: Cargo Build 100 | run: cargo build --no-default-features --features "hidapi-devices" 101 | 102 | mingw_w64build: 103 | name: mingw-w64 Build 104 | runs-on: ubuntu-latest 105 | steps: 106 | - uses: actions/checkout@v3 107 | - uses: dtolnay/rust-toolchain@nightly 108 | with: 109 | targets: x86_64-pc-windows-gnu 110 | - name: Install dependencies 111 | run: | 112 | sudo apt-get update 113 | sudo apt-get install -y ${{ env.deb_packages }} 114 | sudo apt-get install -y ${{ env.mingw_w64_packages }} 115 | - name: Cargo Test 116 | run: cargo test --no-run --all --target x86_64-pc-windows-gnu 117 | 118 | hid-io-protocol_builddevice: 119 | name: hid-io-protocol Device Build 120 | runs-on: ubuntu-latest 121 | steps: 122 | - uses: actions/checkout@v3 123 | - uses: dtolnay/rust-toolchain@nightly 124 | - name: Cargo Build 125 | run: cargo build --manifest-path hid-io-protocol/Cargo.toml --no-default-features --features device --release 126 | 127 | fmt: 128 | name: Rustfmt 129 | runs-on: ubuntu-latest 130 | steps: 131 | - uses: actions/checkout@v3 132 | - uses: dtolnay/rust-toolchain@nightly 133 | with: 134 | components: rustfmt 135 | - name: Rustfmt Check 136 | uses: actions-rust-lang/rustfmt@v1 137 | 138 | clippy: 139 | name: Clippy 140 | runs-on: ubuntu-latest 141 | steps: 142 | - uses: actions/checkout@v3 143 | - uses: dtolnay/rust-toolchain@nightly 144 | with: 145 | components: clippy 146 | - name: Install dependencies 147 | run: | 148 | sudo apt-get update 149 | sudo apt-get install -y ${{ env.deb_packages }} 150 | - name: Cargo Clippy 151 | run: cargo clippy --all-targets -- -D warnings 152 | 153 | udeps: 154 | name: cargo-udeps 155 | runs-on: ubuntu-latest 156 | steps: 157 | - uses: actions/checkout@v3 158 | with: 159 | submodules: true 160 | - uses: dtolnay/rust-toolchain@nightly 161 | - name: Install dependencies 162 | run: | 163 | sudo apt-get update 164 | sudo apt-get install -y ${{ env.deb_packages }} 165 | - name: cargo-binstall 166 | run: | 167 | mkdir -p ~/.cargo/bin 168 | wget https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-x86_64-unknown-linux-musl.tgz 169 | tar xf cargo-binstall*.tgz -C ~/.cargo/bin 170 | - run: cargo binstall --no-confirm cargo-udeps 171 | - name: Cargo Udeps 172 | run: cargo udeps --all-targets 173 | -------------------------------------------------------------------------------- /.github/workflows/rust_macos.yml: -------------------------------------------------------------------------------- 1 | name: Rust macOS 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | name: Test Suite 8 | runs-on: macOS-latest 9 | strategy: 10 | matrix: 11 | python-version: [3.7] 12 | steps: 13 | - uses: actions/checkout@v3 14 | - uses: dtolnay/rust-toolchain@nightly 15 | - name: Set up Python ${{ matrix.python-version }} 16 | uses: actions/setup-python@v4 17 | with: 18 | python-version: ${{ matrix.python-version }} 19 | - name: Install dependencies 20 | run: | 21 | brew install capnp 22 | python -m pip install --upgrade pip 23 | - name: Cargo Test 24 | run: cargo test --all 25 | - name: Install pycapnp dependencies and run test 26 | working-directory: ./python 27 | run: | 28 | python -m pip install cython 29 | CXXFLAGS=-fPIC CFLAGS=-fPIC python -m pip install -r requirements.txt 30 | python -m pytest -vvv 31 | 32 | build: 33 | name: Build 34 | runs-on: macOS-latest 35 | steps: 36 | - uses: actions/checkout@v3 37 | - uses: dtolnay/rust-toolchain@nightly 38 | - name: Install dependencies 39 | run: | 40 | brew install capnp 41 | - name: Cargo Install 42 | run: cargo install --path . --bins --examples --root dist 43 | - uses: actions/upload-artifact@v1.0.0 44 | with: 45 | name: macos_release_binaries 46 | path: dist/bin 47 | -------------------------------------------------------------------------------- /.github/workflows/rust_windows.yml: -------------------------------------------------------------------------------- 1 | name: Rust Windows 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | name: Test Suite 8 | runs-on: windows-latest 9 | strategy: 10 | matrix: 11 | python-version: [3.7] 12 | steps: 13 | - uses: actions/checkout@v3 14 | - uses: dtolnay/rust-toolchain@nightly 15 | - name: Set up Python ${{ matrix.python-version }} 16 | uses: actions/setup-python@v4 17 | with: 18 | python-version: ${{ matrix.python-version }} 19 | - name: Install dependencies 20 | run: | 21 | choco install capnproto 22 | python -m pip install --upgrade pip 23 | - name: Cargo Test 24 | run: cargo test --all 25 | - name: Install pycapnp dependencies and run test 26 | shell: cmd 27 | env: 28 | CXXFLAGS: -fPIC 29 | CFLAGS: -fPIC 30 | working-directory: ./python 31 | run: | 32 | python -m pip install cython 33 | python -m pip install -r requirements.txt 34 | python -m pytest -vvv 35 | 36 | build: 37 | name: Build 38 | runs-on: windows-latest 39 | steps: 40 | - uses: actions/checkout@v3 41 | - uses: dtolnay/rust-toolchain@nightly 42 | - name: Install dependencies 43 | run: | 44 | choco install capnproto 45 | - name: Cargo Install 46 | run: cargo install --path . --bins --examples --root dist 47 | - uses: actions/upload-artifact@v1.0.0 48 | with: 49 | name: win_release_binaries 50 | path: dist/bin 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Object files 5 | *.o 6 | *.ko 7 | *.obj 8 | *.elf 9 | 10 | # Linker output 11 | *.ilk 12 | *.map 13 | *.exp 14 | 15 | # Precompiled Headers 16 | *.gch 17 | *.pch 18 | 19 | # Libraries 20 | *.lib 21 | *.a 22 | *.la 23 | *.lo 24 | 25 | # Shared objects (inc. Windows DLLs) 26 | *.dll 27 | *.so 28 | *.so.* 29 | *.dylib 30 | 31 | # Executables 32 | *.exe 33 | *.out 34 | *.app 35 | *.i*86 36 | *.x86_64 37 | *.hex 38 | 39 | # Debug files 40 | *.dSYM/ 41 | *.su 42 | *.idb 43 | *.pdb 44 | 45 | # Kernel Module Compile Results 46 | *.mod* 47 | *.cmd 48 | .tmp_versions/ 49 | modules.order 50 | Module.symvers 51 | Mkfile.old 52 | dkms.conf 53 | 54 | ## Rust specific ## 55 | # Compiled files 56 | /target/ 57 | /dist/ 58 | 59 | # Remove Cargo.lock only for executable targets 60 | Cargo.lock 61 | 62 | # Backup files from rustfmt 63 | **/*.rs.bk 64 | 65 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hid-io-core" 3 | version = "0.1.4" 4 | authors = ["Jacob Alexander "] 5 | license = "GPL-3.0-or-later" 6 | description = """ 7 | HID-IO is a host-side daemon for advanced HID devices. 8 | """ 9 | 10 | homepage = "https://github.com/hid-io/hid-io-core" 11 | repository = "https://github.com/hid-io/hid-io-core" 12 | 13 | edition = "2021" 14 | build = "build.rs" 15 | default-run = "hid-io-core" 16 | 17 | 18 | [workspace] 19 | members = [ 20 | "hid-io-client", 21 | "hid-io-protocol", 22 | ] 23 | 24 | 25 | [features] 26 | default = ["api", "dev-capture", "displayserver", "hidapi-devices", "vhid"] 27 | # api handles socket interfaces for HID-IO 28 | # e.g. capnproto interface 29 | # Disabling will reduce compile times 30 | api = [ 31 | "capnp", 32 | "capnpc", 33 | "capnp-rpc", 34 | "futures", 35 | "futures-util", 36 | "glob", 37 | "nanoid", 38 | "rcgen", 39 | "tempfile", 40 | "tokio-rustls", 41 | "tokio-stream", 42 | "tokio-util", 43 | ] 44 | # dev_capture handles any HID event capturing for standard input devices 45 | # Disabling will reduce compile times 46 | dev-capture = [ 47 | "evdev-rs", 48 | "libc", 49 | "nanoid", 50 | "udev", 51 | ] 52 | # displayserver interacts with the OS display server (e.g. winapi, quartz, x11, wayland) 53 | # Disabling will reduce compile times 54 | displayserver = [ 55 | "core-foundation", 56 | "core-graphics", 57 | "memmap2", 58 | "tempfile", 59 | "wayland-client", 60 | "wayland-protocols-misc", 61 | "winapi", 62 | "winreg", 63 | "x11", 64 | "xkbcommon", 65 | ] 66 | # hidapi_devices allows for communication directly with hid-io supported devices 67 | # This should be enabled in most scenarios unless you know what you are doing. 68 | # Disabling will reduce compile times 69 | hidapi-devices = [ 70 | "hidapi", 71 | "regex", 72 | "tokio-stream", 73 | ] 74 | # vhid (virtual hid) allows for the creation of virtual hid devices. 75 | # This is needed to support virtual devices such as virtual joysticks, mice and keyboards 76 | # Disabling will reduce compile times 77 | vhid = [ 78 | "libc", 79 | "nanoid", 80 | "udev", 81 | "uhid-virt", 82 | ] 83 | 84 | 85 | [build-dependencies] 86 | built = { version = "0.6", features = ["git2", "chrono"] } 87 | capnpc = { version = "0.14", optional = true } 88 | rustc_version = "0.2" 89 | 90 | 91 | [dependencies] 92 | capnp = { version = "0.14", optional = true } 93 | capnp-rpc = { version = "0.14", optional = true } 94 | clap = "3.1" 95 | ctrlc = "3.2" 96 | lazy_static = "1.4" 97 | flexi_logger = { version = "0.25" } 98 | futures = { version = "0.3", optional = true } 99 | futures-util = { version = "0.3", optional = true } 100 | glob = { version = "0.3", optional = true } 101 | heapless = { version = "0.7" } 102 | hidapi = { version = "2.3.3", default-features = false, features = ["linux-native"], optional = true } 103 | hid-io-protocol = { path = "hid-io-protocol", version = "^0.1.6" } 104 | libc = { version = "0.2", optional = true } 105 | log = "0.4" 106 | nanoid = { version = "0.4", optional = true } 107 | open = "3.0" 108 | rcgen = { version = "0.11", optional = true } 109 | regex = { version = "1.8", optional = true } 110 | sys-info = "0.9" 111 | tempfile = { version = "3.6", optional = true } 112 | tokio = { version = "1.18", features = ["net", "rt-multi-thread", "macros", "sync", "time"] } 113 | tokio-rustls = { version = "0.23", optional = true } 114 | tokio-stream = { version = "0.1", features = ["sync"], optional = true } 115 | tokio-util = { version = "0.7", optional = true, features = ["compat"] } 116 | 117 | 118 | [dev-dependencies] 119 | rand = "0.8" 120 | webpki = "0.22" 121 | 122 | 123 | [dev-dependencies.cargo-husky] 124 | version = "1" 125 | features = ["prepush-hook", "run-cargo-fmt", "run-cargo-clippy", "run-cargo-test"] 126 | 127 | 128 | [package.metadata.cargo-udeps.ignore] 129 | development = ["cargo-husky"] 130 | 131 | 132 | [target.'cfg(target_os = "linux")'.dependencies] 133 | evdev-rs = { version = "0.5", optional = true } 134 | memmap2 = { version = "^0.5", optional = true } 135 | udev = { version = "^0.6", optional = true } 136 | uhid-virt = { version = "0.0.6", optional = true } 137 | wayland-client = { version = "0.30.0-beta.13", optional = true } 138 | wayland-protocols-misc = { version = "0.1.0-beta.13", optional = true, features = ["client"] } 139 | xkbcommon = { version = "^0.5.0-beta", optional = true, features = ["x11"] } 140 | x11 = { version = "^2.19", optional = true } 141 | 142 | 143 | [target.'cfg(windows)'.dependencies] 144 | winapi = { version = "0.3", optional = true, features = ["std", "winuser", "winnls"] } 145 | winreg = { version = "0.10", optional = true } 146 | windows-service = "0.5" 147 | 148 | 149 | [target.'cfg(target_os = "macos")'.dependencies] 150 | core-foundation = { version = "0.9", optional = true } 151 | core-graphics = { version = "0.22", optional = true } 152 | 153 | 154 | [package.metadata.bundle] 155 | identifier = "io.github.hid-io.hid-io-core" 156 | category = "public.app-category.utilities" 157 | icon = ["128x128.png"] 158 | resources = ["hid-io-core.service"] 159 | 160 | 161 | [profile.release] 162 | panic = "abort" #windows 163 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hid-io-core 2 | 3 | [![Linux Status](https://github.com/hid-io/hid-io-core/workflows/Rust%20Linux/badge.svg)](https://github.com/hid-io/hid-io-core/actions) 4 | [![macOS Status](https://github.com/hid-io/hid-io-core/workflows/Rust%20macOS/badge.svg)](https://github.com/hid-io/hid-io-core/actions) 5 | [![Windows Status](https://github.com/hid-io/hid-io-core/workflows/Rust%20Windows/badge.svg)](https://github.com/hid-io/hid-io-core/actions) 6 | [![Doc Status](https://github.com/hid-io/hid-io-core/workflows/Rust%20Doc%20Deploy/badge.svg)](https://github.com/hid-io/hid-io-core/actions) 7 | [![Total alerts](https://img.shields.io/lgtm/alerts/g/hid-io/hid-io-core.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/hid-io/hid-io-core/alerts/) 8 | [![Language grade: Python](https://img.shields.io/lgtm/grade/python/g/hid-io/hid-io-core.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/hid-io/hid-io-core/context:python) 9 | 10 | [![docs.rs](https://docs.rs/hid-io-core/badge.svg)](https://docs.rs/hid-io-core) 11 | [![Crates.io](https://img.shields.io/crates/v/hid-io-core.svg)](https://crates.io/crates/hid-io-core) 12 | [![Crates.io](https://img.shields.io/crates/l/hid-io-core.svg)](https://crates.io/crates/hid-io-core) 13 | [![Crates.io](https://img.shields.io/crates/d/hid-io-core.svg)](https://crates.io/crates/hid-io-core) 14 | 15 | HID-IO Client Side Library and Daemon 16 | 17 | ### Overview 18 | 19 | ![Overview](misc/images/HID-IO_Overview.png) 20 | 21 | ### API Documentation 22 | 23 | * [master](https://hid-io.github.io/hid-io-core/hid_io_core/) 24 | 25 | ## Getting 26 | 27 | Currently you have to build the HID-IO daemon yourself. But it will be made available in binary form once we are ready for a public beta. 28 | 29 | ## Usage 30 | 31 | ```bash 32 | hid-io 33 | hid-io --help 34 | ``` 35 | 36 | ## RPC Terminal Example 37 | 38 | First start hid-io-core in one terminal: 39 | 40 | ```bash 41 | RUST_BACKTRACE=full RUST_LOG="info,tokio=info,tokio_core::reactor=info" cargo run 42 | ``` 43 | 44 | ``` 45 | Finished dev [unoptimized + debuginfo] target(s) in 0.12s 46 | Running `target/debug/hid-io-core` 47 | INFO [hid_io_core] -------------------------- HID-IO Core starting! -------------------------- 48 | INFO [hid_io_core] Log location -> "/tmp" 49 | Press Ctrl-C to exit... 50 | INFO [hid_io_core] Version: 0.1.0-beta3 (git v0.1.0-beta3-9-g29548e1) - debug 51 | INFO [hid_io_core] Build: rustc 1.44.0-nightly (f509b26a7 2020-03-18) (x86_64-unknown-linux-gnu) -> x86_64-unknown-linux-gnu (Sun, 29 Mar 2020 21:40:53 GMT) 52 | INFO [hid_io_core] Initializing HID-IO daemon... 53 | INFO [hid_io_core::module] Initializing modules... 54 | INFO [hid_io_core::device] Initializing devices... 55 | INFO [hid_io_core::device::hidusb] Initializing device/hidusb... 56 | INFO [hid_io_core::device::debug] Initializing device/debug... 57 | INFO [hid_io_core::api] Initializing api... 58 | INFO [hid_io_core::device::hidusb] Spawning hidusb spawning thread... 59 | INFO [hid_io_core::device::debug] Spawning device/debug spawning thread... 60 | INFO [hid_io_core::module] Current layout: us 61 | API: Listening on [::1]:7185 62 | INFO [hid_io_core::device::hidusb] Connecting to id:1 Device: "/dev/hidraw12" 63 | [308f:0013-ff1c:1100] I:4 Input Club Keyboard - Kira PixelMap USB (5337310036384B323430313035353031 - sam4s8) R:1222 64 | Connected to id:1 USB [308f:0013-ff1c:1100] [Input Club] Keyboard - Kira PixelMap USB 5337310036384B323430313035353031 - sam4s8 65 | INFO [hid_io_core::device] Registering device: 1 66 | INFO [hid_io_core::api] Node list update detected, pushing list to subscribers -> 0 67 | ``` 68 | 69 | Then in another terminal window, start the RPC example program. This will connect you to the device serial port. 70 | Once it's connected, you may begin typing commands. 71 | 72 | ```bash 73 | cargo run --example rpc 74 | ``` 75 | 76 | ``` 77 | Finished dev [unoptimized + debuginfo] target(s) in 0.12s 78 | Running `target/debug/examples/rpc` 79 | Connecting to [::1]:7185 80 | Version: 0.1.0-beta3 (git v0.1.0-beta3-9-g29548e1) 81 | Buildtime: Sun, 29 Mar 2020 21:40:53 GMT 82 | Serverarch: x86_64-unknown-linux-gnu 83 | Compiler: rustc 1.44.0-nightly (f509b26a7 2020-03-18) 84 | Key Path: /tmp/.tmpGET4P5 85 | Key: otrbfPWsyZKa6VrWFTXHL 86 | Id: 2 87 | Registering to UsbKeyboard: [308f:0013-ff1c:1100] [Input Club] Keyboard - Kira PixelMap USB (5337310036384B323430313035353031 - sam4s8) 88 | READY 89 | 90 | 91 | : 92 | 93 | : 94 | 95 | : version 96 | version 97 | Revision: e1a1d816199bf54f42432f6d9097171a1614b6a0 98 | Revision #: 1222 99 | Version: v0.5.7 (+0:1222) 100 | Branch: HEAD 101 | Tree Status: Clean 102 | 103 | Repo Origin: https://github.com/kiibohd/controller.git 104 | Commit Date: 2020-03-20 20:19:26 -0700 105 | Commit Author: Jacob Alexander 106 | Build Date: 2020-03-23 22:49:38 +0000 107 | Build OS: Ubuntu 18.04.1 LTS bionic 108 | Compiler: /usr/lib/ccache/arm-none-eabi-gcc 6.3.1 109 | Architecture: arm 110 | Chip Compiled: sam4s8 (sam4s8b) 111 | CPU: cortex-m4 112 | Device: Keyboard 113 | Modules: Scan(Kira) Macro(PixelMap) Output(USB) Debug(full) 114 | 115 | CPU Detected: SAM4S8C (Rev B) 116 | CPU Id: 0x410FC241 117 | (Implementor:ARM:0x41)(Variant:0x0)(Arch:0xF)(PartNo:Cortex-M4:0xC24)(Revision:0x1) 118 | Chip Id: 0x28AC0AE1 119 | (Version:0x1)(Proc:CM4:0x7)(NVM1:512kB:0xA)(NVM2:0kB:0x0)(SRAM:128kB:0xC)(Arch:SAM4SxC:0x8A)(NVMType:FLASH:0x2)(ExtId:0x0) 120 | Chip Ext: 0x0 121 | Unique Id: 5337310036384B323430313035353031 122 | : 123 | ``` 124 | 125 | ## Dependencies 126 | 127 | * Rust nightly (may relax over time) 128 | * capnproto >= 0.7.0 129 | 130 | ### i686-pc-windows-gnu Dependencies 131 | 132 | * `make` must be path 133 | 134 | ## Building 135 | 136 | ```bash 137 | cargo build 138 | ``` 139 | 140 | ## Testing 141 | 142 | ```bash 143 | RUST_LOG=hid_io=info RUST_BACKTRACE=1 cargo run 144 | ``` 145 | 146 | Inspecting rawhid traffic: 147 | 148 | ```bash 149 | sudo usbhid-dump -m 308f:0013 -es 150 | sudo usbhid-dump -m 1c11:b04d -es -t 0 -i 5 151 | ``` 152 | 153 | ### Running Unit Tests 154 | 155 | ```bash 156 | cargo test 157 | ``` 158 | 159 | ## Supported Keyboard Firmware 160 | 161 | * [kiibohd](https://github.com/kiibohd/controller) (KLL) - **In Progress** 162 | 163 | ## Contributing 164 | 165 | * Please run `cargo test` before submitting a pull-request 166 | * Travis will fail any commits that do not pass all tests 167 | 168 | ## Debugging 169 | 170 | ```bash 171 | echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope 172 | rust-gdb target/debug/hid-io -p $(pidof hid-io) 173 | ``` 174 | 175 | ## Packaging 176 | 177 | ```bash 178 | cargo build --release --target "x86_64-pc-windows-gnu" 179 | ``` 180 | 181 | ## Linux systemd service 182 | 183 | ```bash 184 | cp hid-io.service /etc/systemd/system 185 | systemctl daemon-reload 186 | systemctl enable --now hid-io 187 | ``` 188 | 189 | ## Windows service 190 | 191 | ```batch 192 | install_service.exe 193 | sc start hid-io 194 | sc stop hid-io 195 | sc query hid-io 196 | ``` 197 | 198 | ## OSX service 199 | 200 | ```bash 201 | cp hidio.plist ~/Library/LaunchAgents 202 | launchctl -w ~/Library/LaunchAgents/hidio.plist 203 | ``` 204 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2017-2020 by Jacob Alexander 2 | * 3 | * This file is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * 8 | * This file is distributed in the hope that it will be useful, 9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | * GNU General Public License for more details. 12 | * 13 | * You should have received a copy of the GNU General Public License 14 | * along with this file. If not, see . 15 | */ 16 | 17 | // ----- Crates ----- 18 | 19 | // ----- Modules ----- 20 | 21 | use rustc_version::{version, Version}; 22 | 23 | // ----- Functions ----- 24 | 25 | fn main() { 26 | eprintln!("Compiling for {:?}", std::env::var("CARGO_CFG_TARGET_OS")); 27 | 28 | // Assert if we don't meet the minimum version 29 | assert!(version().unwrap() >= Version::parse("1.17.0").unwrap()); 30 | 31 | // Generate build-time information 32 | built::write_built_file().expect("Failed to acquire build-time information"); 33 | 34 | // Generate Cap'n Proto rust files 35 | #[cfg(feature = "api")] 36 | capnpc::CompilerCommand::new() 37 | .src_prefix("python/hidiocore/schema") 38 | .file("python/hidiocore/schema/common.capnp") 39 | .file("python/hidiocore/schema/daemon.capnp") 40 | .file("python/hidiocore/schema/hidio.capnp") 41 | .file("python/hidiocore/schema/keyboard.capnp") 42 | .run() 43 | .expect("schema compiler command"); 44 | 45 | // Link libraries 46 | if let "linux" = std::env::var("CARGO_CFG_TARGET_OS").unwrap().as_str() { 47 | println!("cargo:rustc-link-lib=X11"); 48 | println!("cargo:rustc-link-lib=Xtst"); 49 | }; 50 | } 51 | -------------------------------------------------------------------------------- /examples/unicode.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "displayserver")] 2 | /* Copyright (C) 2019-2022 by Jacob Alexander 3 | * Copyright (C) 2019 by Rowan Decker 4 | * 5 | * This file is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This file is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this file. If not, see . 17 | */ 18 | 19 | #[cfg(target_os = "linux")] 20 | use hid_io_core::module::displayserver::x11::*; 21 | 22 | #[cfg(target_os = "linux")] 23 | use hid_io_core::module::displayserver::wayland::*; 24 | 25 | #[cfg(target_os = "macos")] 26 | use hid_io_core::module::displayserver::quartz::*; 27 | 28 | #[cfg(target_os = "windows")] 29 | use hid_io_core::module::displayserver::winapi::*; 30 | 31 | use hid_io_core::module::displayserver::DisplayOutput; 32 | 33 | #[cfg(target_os = "linux")] 34 | fn get_display() -> Box { 35 | // First attempt to connect to Wayland 36 | let wayland = WaylandConnection::new(); 37 | if let Ok(wayland) = wayland { 38 | Box::new(wayland) 39 | 40 | // Then fallback to X11 41 | } else { 42 | Box::new(XConnection::new()) 43 | } 44 | } 45 | 46 | pub fn main() { 47 | hid_io_core::logging::setup_logging_lite().unwrap(); 48 | 49 | #[cfg(target_os = "linux")] 50 | let mut connection = get_display(); 51 | #[cfg(target_os = "macos")] 52 | let mut connection = QuartzConnection::new(); 53 | #[cfg(target_os = "windows")] 54 | let mut connection = DisplayConnection::new(); 55 | 56 | log::info!("START"); 57 | connection.type_string("💣💩🔥").unwrap(); // Test unicode 58 | connection.type_string("abc💣💩🔥🔥").unwrap(); // Test quickly repeated unicode 59 | connection.type_string("\n").unwrap(); // Test enter 60 | connection.type_string("carg\t --help\n").unwrap(); // Test tab and command 61 | 62 | connection.set_held("def").unwrap(); 63 | connection.set_held("gアi").unwrap(); 64 | std::thread::sleep(std::time::Duration::from_millis(1000)); // Test hold 65 | connection.set_held("gア").unwrap(); 66 | std::thread::sleep(std::time::Duration::from_millis(1000)); // Test partial release 67 | connection.set_held("").unwrap(); 68 | log::info!("END"); 69 | } 70 | -------------------------------------------------------------------------------- /hid-io-client/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## 0.1.3 (2023-09-07) 9 | 10 | ### Bug Fixes 11 | 12 | - Only use api feature from hid-io-core in hid-io-client 13 | - hid-io-client should not have access to control display server, 14 | capture hid devices or communicate with hid endpoints 15 | 16 | ### Commit Statistics 17 | 18 | 19 | 20 | - 1 commit contributed to the release. 21 | - 1 commit was understood as [conventional](https://www.conventionalcommits.org). 22 | - 0 issues like '(#ID)' were seen in commit messages 23 | 24 | ### Commit Details 25 | 26 | 27 | 28 |
view details 29 | 30 | * **Uncategorized** 31 | - Only use api feature from hid-io-core in hid-io-client ([`c63371c`](https://github.com/hid-io/hid-io-core/commit/c63371c87e2373f2d6af3767bb80f682139e6b08)) 32 |
33 | 34 | ## 0.1.2 (2023-09-07) 35 | 36 | ### Bug Fixes 37 | 38 | - Adjust README.md after dependency fixes 39 | - Expose hid-io-client hid-io-core,capnp dependencies 40 | - Simpler dependency management for users of hid-io-client 41 | 42 | ### Commit Statistics 43 | 44 | 45 | 46 | - 3 commits contributed to the release. 47 | - 2 commits were understood as [conventional](https://www.conventionalcommits.org). 48 | - 0 issues like '(#ID)' were seen in commit messages 49 | 50 | ### Commit Details 51 | 52 | 53 | 54 |
view details 55 | 56 | * **Uncategorized** 57 | - Release hid-io-client v0.1.2 ([`0e4321c`](https://github.com/hid-io/hid-io-core/commit/0e4321c0ff7010823c239e1c3d2e4a0904f4b987)) 58 | - Adjust README.md after dependency fixes ([`78b443f`](https://github.com/hid-io/hid-io-core/commit/78b443f8a607e23b1630fe657afb13f0acf74a0e)) 59 | - Expose hid-io-client hid-io-core,capnp dependencies ([`ceb5c43`](https://github.com/hid-io/hid-io-core/commit/ceb5c43ed0c208c30f38ee01fd0997fb1a7e0d85)) 60 |
61 | 62 | ## 0.1.1 (2023-09-07) 63 | 64 | ### Bug Fixes 65 | 66 | 67 | 68 | - Update dependencies and small fixes 69 | - Fix hid-io-client example tool pixel direct range 70 | 71 | ### New Features 72 | 73 | 74 | 75 | - Add hall effect manu test data tracking 76 | - Supports partial strobe data updates (only printing after getting 77 | enough data for a full scan) 78 | 79 | ### Commit Statistics 80 | 81 | 82 | 83 | - 8 commits contributed to the release over the course of 282 calendar days. 84 | - 294 days passed between releases. 85 | - 4 commits were understood as [conventional](https://www.conventionalcommits.org). 86 | - 0 issues like '(#ID)' were seen in commit messages 87 | 88 | ### Commit Details 89 | 90 | 91 | 92 |
view details 93 | 94 | * **Uncategorized** 95 | - Release hid-io-client v0.1.1 ([`f1bdda2`](https://github.com/hid-io/hid-io-core/commit/f1bdda27b3daff27f681f680a014cc21501f057d)) 96 | - Release hid-io-protocol v0.1.6, hid-io-core v0.1.4 ([`42068a7`](https://github.com/hid-io/hid-io-core/commit/42068a7989235bbc28888d1c705a425da26ec5fd)) 97 | - Add hall effect manu test data tracking ([`87cd06d`](https://github.com/hid-io/hid-io-core/commit/87cd06d6ea76bebb924629d86fb78fa5b9f67fe2)) 98 | - Add levelcheck column and mode set commands to hid-io-core + capnp ([`6d44300`](https://github.com/hid-io/hid-io-core/commit/6d44300e247b0e74459c8e2ad54061b5346a01ce)) 99 | - Release hid-io-protocol v0.1.5, hid-io-core v0.1.3 ([`95088fc`](https://github.com/hid-io/hid-io-core/commit/95088fc5e913226d1f55b3d83ec8a7553b916368)) 100 | - Update dependencies and small fixes ([`62af0b5`](https://github.com/hid-io/hid-io-core/commit/62af0b510a7399645469e72f10fbfeffdb5edc7a)) 101 | - Latest clippy warnings (format string identifiers) ([`5597572`](https://github.com/hid-io/hid-io-core/commit/559757292afa1cb1e7a8d0ee28d75a3ae8a26ab2)) 102 | - Release hid-io-protocol v0.1.4, hid-io-core v0.1.2 ([`6906d29`](https://github.com/hid-io/hid-io-core/commit/6906d29ea854e02dbf58ef6531b4468362c0abb3)) 103 |
104 | 105 | 106 | flexi_logger 0.24 -> 0.25uhid-virt 0.0.5 -> official 0.0.6clippy fixes 107 | 108 | ## 0.1.0 (2022-11-17) 109 | 110 | ### Commit Statistics 111 | 112 | 113 | 114 | - 5 commits contributed to the release over the course of 15 calendar days. 115 | - 0 commits were understood as [conventional](https://www.conventionalcommits.org). 116 | - 0 issues like '(#ID)' were seen in commit messages 117 | 118 | ### Commit Details 119 | 120 | 121 | 122 |
view details 123 | 124 | * **Uncategorized** 125 | - Release hid-io-core v0.1.1, hid-io-client v0.1.0 ([`cd719ea`](https://github.com/hid-io/hid-io-core/commit/cd719eab05608bced35ace8f2f41f815631fca29)) 126 | - Initial CHANGELOG.md ([`173872e`](https://github.com/hid-io/hid-io-core/commit/173872ec9e1a2d0dfc95e607f0a6abc250947e29)) 127 | - Typo ([`3f1db58`](https://github.com/hid-io/hid-io-core/commit/3f1db58dd0fd72aa9a1f3748cd15d7a7a810e525)) 128 | - Update hid-io-client README.md ([`400b754`](https://github.com/hid-io/hid-io-core/commit/400b75453d21cb7e40cb93b68a4c78d7fc4468e2)) 129 | - Adding hid-io-client ([`77e5bd6`](https://github.com/hid-io/hid-io-core/commit/77e5bd6aa17a417939fec4bfba5f8ad2f6ee7ac5)) 130 |
131 | 132 | -------------------------------------------------------------------------------- /hid-io-client/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hid-io-client" 3 | version = "0.1.3" 4 | authors = ["Jacob Alexander "] 5 | license = "MIT OR Apache-2.0" 6 | description = """ 7 | HID-IO Client library for hid-io-core. 8 | """ 9 | 10 | homepage = "https://github.com/hid-io/hid-io-core" 11 | repository = "https://github.com/hid-io/hid-io-core" 12 | edition = "2021" 13 | 14 | 15 | [lib] 16 | name = "hid_io_client" 17 | 18 | 19 | [dependencies] 20 | capnp = { version = "0.14" } 21 | capnp-rpc = { version = "0.14" } 22 | futures = { version = "0.3" } 23 | hid-io-core = { version = "^0.1.4", path = "..", default-features = false, features = ["api"] } 24 | log = { version = "0.4" } 25 | rand = "0.8" 26 | rustls = { version = "0.20", features = ["dangerous_configuration"] } 27 | tokio = { version = "1.18", features = ["net", "rt-multi-thread", "macros", "sync", "time"] } 28 | tokio-rustls = { version = "0.23" } 29 | tokio-util = { version = "0.7", features = ["compat"] } 30 | 31 | [dev-dependencies] 32 | clap = "3.1" 33 | -------------------------------------------------------------------------------- /hid-io-client/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /hid-io-client/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /hid-io-client/README.md: -------------------------------------------------------------------------------- 1 | # hid-io-client 2 | 3 | HID-IO Client Side application interface 4 | 5 | The purpose of this crate is to provide a common set of functions that can be used to connect directly to hid-io-core. 6 | Please see [hid-io-client-ffi](../hid-io-client-ffi) if you are looking for an FFI-compatible library interface. 7 | 8 | ## Connecting to hid-io-core 9 | 10 | ```rust 11 | extern crate tokio; 12 | 13 | use hid_io_client::capnp; 14 | use hid_io_client::common_capnp::NodeType; 15 | use hid_io_client::setup_logging_lite; 16 | use rand::Rng; 17 | 18 | #[tokio::main] 19 | pub async fn main() -> Result<(), capnp::Error> { 20 | setup_logging_lite().ok(); 21 | tokio::task::LocalSet::new().run_until(try_main()).await 22 | } 23 | 24 | async fn try_main() -> Result<(), capnp::Error> { 25 | // Prepare hid-io-core connection 26 | let mut hidio_conn = hid_io_client::HidioConnection::new().unwrap(); 27 | let mut rng = rand::thread_rng(); 28 | 29 | // Connect and authenticate with hid-io-core 30 | let (hidio_auth, _hidio_server) = hidio_conn 31 | .connect( 32 | hid_io_client::AuthType::Priviledged, 33 | NodeType::HidioApi, 34 | "lsnodes".to_string(), 35 | format!("{:x} - pid:{}", rng.gen::(), std::process::id()), 36 | true, 37 | std::time::Duration::from_millis(1000), 38 | ) 39 | .await?; 40 | let hidio_auth = hidio_auth.expect("Could not authenticate to hid-io-core"); 41 | 42 | let nodes_resp = { 43 | let request = hidio_auth.nodes_request(); 44 | request.send().promise.await? 45 | }; 46 | let nodes = nodes_resp.get()?.get_nodes()?; 47 | 48 | println!(); 49 | for n in nodes { 50 | println!(" * {} - {}", n.get_id(), hid_io_client::format_node(n)); 51 | } 52 | 53 | hidio_conn.disconnect().await?; 54 | Ok(()) 55 | } 56 | ``` 57 | -------------------------------------------------------------------------------- /hid-io-client/examples/lsnodes.rs: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2019-2023 by Jacob Alexander 2 | * Copyright (C) 2019 by Rowan Decker 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * all copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | * THE SOFTWARE. 21 | */ 22 | 23 | extern crate tokio; 24 | 25 | use hid_io_client::capnp; 26 | use hid_io_client::common_capnp::NodeType; 27 | use hid_io_client::setup_logging_lite; 28 | use rand::Rng; 29 | 30 | #[tokio::main] 31 | pub async fn main() -> Result<(), capnp::Error> { 32 | setup_logging_lite().ok(); 33 | tokio::task::LocalSet::new().run_until(try_main()).await 34 | } 35 | 36 | async fn try_main() -> Result<(), capnp::Error> { 37 | // Prepare hid-io-core connection 38 | let mut hidio_conn = hid_io_client::HidioConnection::new().unwrap(); 39 | let mut rng = rand::thread_rng(); 40 | 41 | // Connect and authenticate with hid-io-core 42 | let (hidio_auth, _hidio_server) = hidio_conn 43 | .connect( 44 | hid_io_client::AuthType::Priviledged, 45 | NodeType::HidioApi, 46 | "lsnodes".to_string(), 47 | format!("{:x} - pid:{}", rng.gen::(), std::process::id()), 48 | true, 49 | std::time::Duration::from_millis(1000), 50 | ) 51 | .await?; 52 | let hidio_auth = hidio_auth.expect("Could not authenticate to hid-io-core"); 53 | 54 | let nodes_resp = { 55 | let request = hidio_auth.nodes_request(); 56 | request.send().promise.await? 57 | }; 58 | let nodes = nodes_resp.get()?.get_nodes()?; 59 | 60 | println!(); 61 | for n in nodes { 62 | println!(" * {} - {}", n.get_id(), hid_io_client::format_node(n)); 63 | } 64 | 65 | hidio_conn.disconnect().await?; 66 | Ok(()) 67 | } 68 | -------------------------------------------------------------------------------- /hid-io-client/examples/watchnodes.rs: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2019-2023 by Jacob Alexander 2 | * Copyright (C) 2019 by Rowan Decker 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * all copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | * THE SOFTWARE. 21 | */ 22 | 23 | extern crate tokio; 24 | 25 | use hid_io_client::capnp; 26 | use hid_io_client::capnp::capability::Promise; 27 | use hid_io_client::capnp_rpc; 28 | use hid_io_client::common_capnp::NodeType; 29 | use hid_io_client::hidio_capnp::hid_io; 30 | use hid_io_client::setup_logging_lite; 31 | use hid_io_client::HidIoCommandId; 32 | use rand::Rng; 33 | use std::collections::HashMap; 34 | use std::convert::TryFrom; 35 | use std::fmt::Write; 36 | 37 | struct Node { 38 | type_: NodeType, 39 | _name: String, 40 | _serial: String, 41 | } 42 | 43 | struct NodesSubscriberImpl { 44 | nodes_lookup: HashMap, 45 | start_time: std::time::Instant, 46 | } 47 | 48 | impl NodesSubscriberImpl { 49 | fn new() -> NodesSubscriberImpl { 50 | let nodes_lookup: HashMap = HashMap::new(); 51 | let start_time = std::time::Instant::now(); 52 | 53 | NodesSubscriberImpl { 54 | nodes_lookup, 55 | start_time, 56 | } 57 | } 58 | 59 | fn format_packet(&mut self, packet: hid_io::packet::Reader<'_>) -> String { 60 | let mut datastr = "".to_string(); 61 | for b in packet.get_data().unwrap().iter() { 62 | write!(datastr, "{:02x}", b).unwrap(); 63 | } 64 | let datalen = packet.get_data().unwrap().len(); 65 | let src = packet.get_src(); 66 | let src_node_type = if src == 0 { 67 | "All".to_string() 68 | } else if let Some(n) = self.nodes_lookup.get(&src) { 69 | format!("{:?}", n.type_) 70 | } else { 71 | format!("{:?}", NodeType::Unknown) 72 | }; 73 | 74 | let dst = packet.get_dst(); 75 | let dst_node_type = if dst == 0 { 76 | "All".to_string() 77 | } else if let Some(n) = self.nodes_lookup.get(&dst) { 78 | format!("{:?}", n.type_) 79 | } else { 80 | format!("{:?}", NodeType::Unknown) 81 | }; 82 | 83 | // TODO (HaaTa): decode packets to show fields 84 | if datalen == 0 { 85 | format!( 86 | "{} - {:?}: {}:{}->{}:{} ({:?}:{}) Len:{}", 87 | self.start_time.elapsed().as_millis(), 88 | packet.get_type().unwrap(), 89 | src, 90 | src_node_type, 91 | dst, 92 | dst_node_type, 93 | HidIoCommandId::try_from(packet.get_id()).unwrap_or(HidIoCommandId::Unused), 94 | packet.get_id(), 95 | datalen, 96 | ) 97 | } else { 98 | format!( 99 | "{} - {:?}: {}:{}->{}:{} ({:?}:{}) Len:{}\n\t{}", 100 | self.start_time.elapsed().as_millis(), 101 | packet.get_type().unwrap(), 102 | src, 103 | src_node_type, 104 | dst, 105 | dst_node_type, 106 | HidIoCommandId::try_from(packet.get_id()).unwrap_or(HidIoCommandId::Unused), 107 | packet.get_id(), 108 | datalen, 109 | datastr, 110 | ) 111 | } 112 | } 113 | } 114 | 115 | impl hid_io::nodes_subscriber::Server for NodesSubscriberImpl { 116 | fn nodes_update( 117 | &mut self, 118 | params: hid_io::nodes_subscriber::NodesUpdateParams, 119 | _results: hid_io::nodes_subscriber::NodesUpdateResults, 120 | ) -> Promise<(), capnp::Error> { 121 | // Re-create nodes_lookup on each update 122 | self.nodes_lookup = HashMap::new(); 123 | 124 | println!("nodes_update: "); 125 | for n in capnp_rpc::pry!(capnp_rpc::pry!(params.get()).get_nodes()) { 126 | println!("{} - {}", n.get_id(), hid_io_client::format_node(n)); 127 | self.nodes_lookup.insert( 128 | n.get_id(), 129 | Node { 130 | type_: n.get_type().unwrap(), 131 | _name: n.get_name().unwrap_or("").to_string(), 132 | _serial: n.get_serial().unwrap_or("").to_string(), 133 | }, 134 | ); 135 | } 136 | Promise::ok(()) 137 | } 138 | 139 | fn hidio_watcher( 140 | &mut self, 141 | params: hid_io::nodes_subscriber::HidioWatcherParams, 142 | _results: hid_io::nodes_subscriber::HidioWatcherResults, 143 | ) -> Promise<(), capnp::Error> { 144 | println!( 145 | "{}", 146 | self.format_packet(capnp_rpc::pry!(capnp_rpc::pry!(params.get()).get_packet())) 147 | ); 148 | Promise::ok(()) 149 | } 150 | } 151 | 152 | #[tokio::main] 153 | pub async fn main() -> Result<(), ::capnp::Error> { 154 | setup_logging_lite().ok(); 155 | tokio::task::LocalSet::new().run_until(try_main()).await 156 | } 157 | 158 | async fn try_main() -> Result<(), ::capnp::Error> { 159 | // Prepare hid-io-core connection 160 | let mut hidio_conn = hid_io_client::HidioConnection::new().unwrap(); 161 | let mut rng = rand::thread_rng(); 162 | 163 | loop { 164 | // Connect and authenticate with hid-io-core 165 | let (hidio_auth, hidio_server) = hidio_conn 166 | .connect( 167 | hid_io_client::AuthType::Priviledged, 168 | NodeType::HidioApi, 169 | "watchnodes".to_string(), 170 | format!("{:x} - pid:{}", rng.gen::(), std::process::id()), 171 | true, 172 | std::time::Duration::from_millis(1000), 173 | ) 174 | .await?; 175 | let hidio_auth = hidio_auth.expect("Could not authenticate to hid-io-core"); 176 | 177 | // Subscribe to nodeswatcher 178 | let nodes_subscription = capnp_rpc::new_client(NodesSubscriberImpl::new()); 179 | let mut request = hidio_auth.subscribe_nodes_request(); 180 | request.get().set_subscriber(nodes_subscription); 181 | let _callback = request.send().promise.await; 182 | 183 | loop { 184 | tokio::time::sleep(std::time::Duration::from_millis(1000)).await; 185 | 186 | // Check if the server is still alive 187 | let request = hidio_server.alive_request(); 188 | if let Err(e) = request.send().promise.await { 189 | println!("Dead: {}", e); 190 | // Break the subscription loop and attempt to reconnect 191 | break; 192 | } 193 | } 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /hid-io-client/src/lib.rs: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2022-2023 by Jacob Alexander 2 | * 3 | * Permission is hereby granted, free of charge, to any person obtaining a copy 4 | * of this software and associated documentation files (the "Software"), to deal 5 | * in the Software without restriction, including without limitation the rights 6 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | * copies of the Software, and to permit persons to whom the Software is 8 | * furnished to do so, subject to the following conditions: 9 | * 10 | * The above copyright notice and this permission notice shall be included in 11 | * all copies or substantial portions of the Software. 12 | * 13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | * THE SOFTWARE. 20 | */ 21 | 22 | extern crate tokio; 23 | 24 | pub use capnp; 25 | pub use capnp_rpc; 26 | pub use hid_io_core::built_info; 27 | pub use hid_io_core::common_capnp; 28 | pub use hid_io_core::daemon_capnp; 29 | pub use hid_io_core::hidio_capnp; 30 | pub use hid_io_core::keyboard_capnp; 31 | pub use hid_io_core::logging::setup_logging_lite; 32 | pub use hid_io_core::HidIoCommandId; 33 | 34 | use capnp_rpc::{rpc_twoparty_capnp, twoparty, Disconnector, RpcSystem}; 35 | use futures::{AsyncReadExt, FutureExt}; 36 | use hid_io_core::common_capnp::NodeType; 37 | use hid_io_core::hidio_capnp::{hid_io, hid_io_server}; 38 | use log::{debug, trace, warn}; 39 | use std::fs; 40 | use std::net::ToSocketAddrs; 41 | use std::sync::Arc; 42 | use tokio_rustls::{rustls::ClientConfig, TlsConnector}; 43 | 44 | const LISTEN_ADDR: &str = "localhost:7185"; 45 | 46 | mod danger { 47 | use std::time::SystemTime; 48 | use tokio_rustls::rustls::{Certificate, ServerName}; 49 | 50 | pub struct NoCertificateVerification {} 51 | 52 | impl rustls::client::ServerCertVerifier for NoCertificateVerification { 53 | fn verify_server_cert( 54 | &self, 55 | _end_entity: &Certificate, 56 | _intermediates: &[Certificate], 57 | _server_name: &ServerName, 58 | _scts: &mut dyn Iterator, 59 | _ocsp_response: &[u8], 60 | _now: SystemTime, 61 | ) -> Result { 62 | Ok(rustls::client::ServerCertVerified::assertion()) 63 | } 64 | } 65 | } 66 | 67 | pub fn format_node(node: hid_io_core::common_capnp::destination::Reader<'_>) -> String { 68 | format!( 69 | "{}: {} ({})", 70 | node.get_type().unwrap(), 71 | node.get_name().unwrap_or(""), 72 | node.get_serial().unwrap_or(""), 73 | ) 74 | } 75 | 76 | pub enum HidioError {} 77 | 78 | #[derive(Debug)] 79 | pub enum AuthType { 80 | /// No authentication 81 | /// Very limited, only authentication APIs available 82 | None, 83 | /// Basic auth level (restricted API access) 84 | Basic, 85 | /// Highest auth level (full control and API access) 86 | Priviledged, 87 | } 88 | 89 | pub struct BuildInfo { 90 | pub pkg_version: String, 91 | pub git_version: String, 92 | pub profile: String, 93 | pub rust_c_version: String, 94 | pub host: String, 95 | pub target: String, 96 | pub built_time_utc: String, 97 | } 98 | 99 | pub fn lib_info() -> BuildInfo { 100 | let pkg_version = built_info::PKG_VERSION.to_string(); 101 | let git_version = 102 | built_info::GIT_VERSION.map_or_else(|| "unknown".to_owned(), |v| format!("git {v}")); 103 | let profile = built_info::PROFILE.to_string(); 104 | let rust_c_version = built_info::RUSTC_VERSION.to_string(); 105 | let host = built_info::HOST.to_string(); 106 | let target = built_info::TARGET.to_string(); 107 | let built_time_utc = built_info::BUILT_TIME_UTC.to_string(); 108 | 109 | BuildInfo { 110 | pkg_version, 111 | git_version, 112 | profile, 113 | rust_c_version, 114 | host, 115 | target, 116 | built_time_utc, 117 | } 118 | } 119 | 120 | pub struct HidioConnection { 121 | /// Internal address to hid-io-core, this is always localhost 122 | addr: std::net::SocketAddr, 123 | /// TLS connection used for hid-io-core connection 124 | connector: TlsConnector, 125 | /// TLS server name used for hid-io-core connection 126 | domain: rustls::ServerName, 127 | /// Cleanup handle for rpc_system 128 | rpc_disconnector: Option>, 129 | } 130 | 131 | impl HidioConnection { 132 | pub fn new() -> Result { 133 | let addr = LISTEN_ADDR 134 | .to_socket_addrs()? 135 | .next() 136 | .expect("Could not parse address"); 137 | 138 | let config = ClientConfig::builder() 139 | .with_safe_defaults() 140 | .with_custom_certificate_verifier(Arc::new(danger::NoCertificateVerification {})) 141 | .with_no_client_auth(); 142 | let connector = TlsConnector::from(Arc::new(config)); 143 | 144 | let domain = rustls::ServerName::try_from("localhost").unwrap(); 145 | 146 | Ok(Self { 147 | addr, 148 | connector, 149 | domain, 150 | rpc_disconnector: None, 151 | }) 152 | } 153 | 154 | /// Async connect 155 | /// If retry is true, block until there is a connection. 156 | /// retry_delay sets the amount of time to sleep between retries 157 | /// 158 | /// Make sure to check the status of (hidio_auth, _) as you may 159 | /// not have successfully authenticated. 160 | pub async fn connect( 161 | &mut self, 162 | auth: AuthType, 163 | node_type: NodeType, 164 | name: String, 165 | serial_uid: String, 166 | retry: bool, 167 | retry_delay: std::time::Duration, 168 | ) -> Result<(Option, hid_io_server::Client), ::capnp::Error> { 169 | trace!("Connecting to: {}", self.addr); 170 | let stream; 171 | loop { 172 | stream = match tokio::net::TcpStream::connect(self.addr).await { 173 | Ok(stream) => stream, 174 | Err(e) => { 175 | if !retry { 176 | return Err(::capnp::Error { 177 | kind: ::capnp::ErrorKind::Failed, 178 | description: format!("Failed to connect ({}): {}", self.addr, e), 179 | }); 180 | } 181 | warn!("Failed to connect ({}): {}", self.addr, e); 182 | tokio::time::sleep(retry_delay).await; 183 | continue; 184 | } 185 | }; 186 | break; 187 | } 188 | stream.set_nodelay(true)?; 189 | let stream = self.connector.connect(self.domain.clone(), stream).await?; 190 | 191 | let (reader, writer) = tokio_util::compat::TokioAsyncReadCompatExt::compat(stream).split(); 192 | 193 | let network = Box::new(twoparty::VatNetwork::new( 194 | reader, 195 | writer, 196 | rpc_twoparty_capnp::Side::Client, 197 | Default::default(), 198 | )); 199 | let mut rpc_system = RpcSystem::new(network, None); 200 | let hidio_server: hid_io_server::Client = 201 | rpc_system.bootstrap(rpc_twoparty_capnp::Side::Server); 202 | 203 | self.rpc_disconnector = Some(rpc_system.get_disconnector()); 204 | let _handle = tokio::task::spawn_local(Box::pin(rpc_system.map(|_| {}))); 205 | 206 | // Display server version information 207 | let request = hidio_server.version_request(); 208 | let response = request.send().promise.await?; 209 | let value = response.get().unwrap().get_version().unwrap(); 210 | debug!("Version: {}", value.get_version().unwrap()); 211 | debug!("Buildtime: {}", value.get_buildtime().unwrap()); 212 | debug!("Serverarch: {}", value.get_serverarch().unwrap()); 213 | 214 | let request = hidio_server.key_request(); 215 | let response = request.send().promise.await?; 216 | let value = response.get().unwrap().get_key().unwrap(); 217 | let basic_key_path = value.get_basic_key_path().unwrap().to_string(); 218 | let auth_key_path = value.get_auth_key_path().unwrap().to_string(); 219 | debug!("Basic Key Path: {}", basic_key_path); 220 | debug!("Auth Key Path: {}", auth_key_path); 221 | 222 | // Lookup uid 223 | let uid = { 224 | let request = hidio_server.id_request(); 225 | let response = request.send().promise.await?; 226 | let value = response.get().unwrap().get_id(); 227 | value 228 | }; 229 | debug!("Id: {}", uid); 230 | 231 | // Attempt to authenticate if specified 232 | debug!("AuthType: {:?}", auth); 233 | let hidio_auth = match auth { 234 | AuthType::None => None, 235 | AuthType::Basic => { 236 | // Attempt to read the key 237 | let key = fs::read_to_string(basic_key_path)?; 238 | 239 | // Attempt authentication 240 | let mut request = hidio_server.basic_request(); 241 | let mut info = request.get().get_info()?; 242 | info.set_type(node_type); 243 | info.set_name(&name); 244 | info.set_serial(&serial_uid); 245 | info.set_id(uid); 246 | request.get().set_key(&key); 247 | 248 | Some(request.send().pipeline.get_port()) 249 | } 250 | AuthType::Priviledged => { 251 | // Attempt to read the key 252 | let key = fs::read_to_string(auth_key_path)?; 253 | 254 | // Attempt authentication 255 | let mut request = hidio_server.auth_request(); 256 | let mut info = request.get().get_info()?; 257 | info.set_type(node_type); 258 | info.set_name(&name); 259 | info.set_serial(&serial_uid); 260 | info.set_id(uid); 261 | request.get().set_key(&key); 262 | 263 | Some(request.send().pipeline.get_port()) 264 | } 265 | }; 266 | 267 | Ok((hidio_auth, hidio_server)) 268 | } 269 | 270 | /// Disconnect with hid-io-core 271 | /// And/or stop reconnecting 272 | pub async fn disconnect(&mut self) -> Result<(), ::capnp::Error> { 273 | trace!("Disconnecting from: {}", self.addr); 274 | 275 | // Only await if there's something to wait for 276 | if let Some(rpcd) = &mut self.rpc_disconnector { 277 | rpcd.await?; 278 | } 279 | 280 | Ok(()) 281 | } 282 | } 283 | -------------------------------------------------------------------------------- /hid-io-protocol/.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Object files 5 | *.o 6 | *.ko 7 | *.obj 8 | *.elf 9 | 10 | # Linker output 11 | *.ilk 12 | *.map 13 | *.exp 14 | 15 | # Precompiled Headers 16 | *.gch 17 | *.pch 18 | 19 | # Libraries 20 | *.lib 21 | *.a 22 | *.la 23 | *.lo 24 | 25 | # Shared objects (inc. Windows DLLs) 26 | *.dll 27 | *.so 28 | *.so.* 29 | *.dylib 30 | 31 | # Executables 32 | *.exe 33 | *.out 34 | *.app 35 | *.i*86 36 | *.x86_64 37 | *.hex 38 | 39 | # Debug files 40 | *.dSYM/ 41 | *.su 42 | *.idb 43 | *.pdb 44 | 45 | # Kernel Module Compile Results 46 | *.mod* 47 | *.cmd 48 | .tmp_versions/ 49 | modules.order 50 | Module.symvers 51 | Mkfile.old 52 | dkms.conf 53 | 54 | ## Rust specific ## 55 | # Compiled files 56 | /target/ 57 | 58 | # Remove Cargo.lock only for executable targets 59 | Cargo.lock 60 | 61 | # Backup files from rustfmt 62 | **/*.rs.bk 63 | 64 | -------------------------------------------------------------------------------- /hid-io-protocol/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hid-io-protocol" 3 | version = "0.1.6" 4 | authors = ["Jacob Alexander "] 5 | license = "MIT OR Apache-2.0" 6 | description = """ 7 | HID-IO protocol implementation for both servers and devices. 8 | Devices are optimized for Cortex-M devices, but may work for other platforms. 9 | """ 10 | 11 | categories = ["embedded", "no-std"] 12 | homepage = "https://github.com/hid-io/hid-io-core" 13 | repository = "https://github.com/hid-io/hid-io-core" 14 | edition = "2021" 15 | 16 | 17 | [lib] 18 | name = "hid_io_protocol" 19 | 20 | 21 | [features] 22 | default = ["server"] 23 | 24 | # device feature is intended for embedded devices such as a Cortex-M4 device 25 | # Strictly no_std 26 | device = [] 27 | 28 | # server feature is intended for use with full user-space applications 29 | # with access to std for loggging messages 30 | # Mostly no_std with some minor exceptions 31 | server = [] 32 | 33 | # Adds defmt support to useful enums and structs 34 | defmt = ["dep:defmt", "heapless/defmt-impl", "kll-core/defmt"] 35 | 36 | 37 | [dependencies] 38 | defmt = { version = "0.3", optional = true } 39 | heapless = { version = "0.7" } 40 | kll-core = { version = "0.1" } 41 | log = { version = "0.4", default-features = false } 42 | num_enum = { version = "0.5", default-features = false } 43 | 44 | 45 | [dev-dependencies] 46 | flexi_logger = "0.25" 47 | -------------------------------------------------------------------------------- /hid-io-protocol/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /hid-io-protocol/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /hid-io-protocol/src/buffer.rs: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2020-2021 by Jacob Alexander 2 | * 3 | * Permission is hereby granted, free of charge, to any person obtaining a copy 4 | * of this software and associated documentation files (the "Software"), to deal 5 | * in the Software without restriction, including without limitation the rights 6 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | * copies of the Software, and to permit persons to whom the Software is 8 | * furnished to do so, subject to the following conditions: 9 | * 10 | * The above copyright notice and this permission notice shall be included in 11 | * all copies or substantial portions of the Software. 12 | * 13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | * THE SOFTWARE. 20 | */ 21 | 22 | // ----- Crates ----- 23 | 24 | use heapless::spsc::Queue; 25 | use heapless::Vec; 26 | 27 | // ----- Enumerations ----- 28 | 29 | // ----- Structs ----- 30 | 31 | /// HID-IO byte buffer 32 | /// This buffer is a queue of vecs with static allocation 33 | /// Each vec is fixed sized as HID-IO interface 34 | /// has a fixed transport payload (even if the actual size of the 35 | /// message is less). 36 | /// This buffer has no notion of packet size so it must store the 37 | /// full transport payload. 38 | /// In the minimal scenario a queue size of 1 is used. 39 | /// 40 | /// Common HID-IO Vec capacities 41 | /// - 7 bytes (USB 2.0 LS /w HID ID byte) 42 | /// - 8 bytes (USB 2.0 LS) 43 | /// - 63 bytes (USB 2.0 FS /w HID ID byte) 44 | /// - 64 bytes (USB 2.0 FS) 45 | /// - 1023 bytes (USB 2.0 HS /w HID ID byte) 46 | /// - 1024 bytes (USB 2.0 HS) 47 | /// 48 | /// The maximum queue size is 255 49 | pub struct Buffer { 50 | queue: Queue, Q>, 51 | } 52 | 53 | // ----- Implementations ----- 54 | 55 | impl Default for Buffer { 56 | fn default() -> Self { 57 | Buffer { 58 | queue: Queue::new(), 59 | } 60 | } 61 | } 62 | 63 | impl Buffer { 64 | /// Constructor for Buffer 65 | /// 66 | /// # Remarks 67 | /// Initialize as blank 68 | /// This buffer has a limit of 65535 elements 69 | pub fn new() -> Buffer { 70 | Buffer { 71 | ..Default::default() 72 | } 73 | } 74 | 75 | /// Checks the first item array 76 | /// Returns None if there are no items in the queue 77 | /// Does not dequeue 78 | pub fn peek(&self) -> Option<&Vec> { 79 | self.queue.peek() 80 | } 81 | 82 | /// Dequeues and returns the first item array 83 | /// Returns None if there are no items in the queue 84 | pub fn dequeue(&mut self) -> Option> { 85 | self.queue.dequeue() 86 | } 87 | 88 | /// Enqueues 89 | /// Returns the array if there's not enough space 90 | pub fn enqueue(&mut self, data: Vec) -> Result<(), Vec> { 91 | self.queue.enqueue(data) 92 | } 93 | 94 | /// Clears the buffer 95 | /// Needed for some error conditions 96 | pub fn clear(&mut self) { 97 | while !self.queue.is_empty() { 98 | self.dequeue(); 99 | } 100 | } 101 | 102 | /// Capacity of buffer 103 | pub fn capacity(&self) -> usize { 104 | self.queue.capacity() 105 | } 106 | 107 | /// Number of elements stored in the buffer 108 | pub fn len(&self) -> usize { 109 | self.queue.len() 110 | } 111 | 112 | /// Buffer empty 113 | pub fn is_empty(&self) -> bool { 114 | self.queue.is_empty() 115 | } 116 | 117 | /// Buffer full 118 | pub fn is_full(&self) -> bool { 119 | self.len() == self.capacity() 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /hid-io-protocol/src/test.rs: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2017-2022 by Jacob Alexander 2 | * 3 | * Permission is hereby granted, free of charge, to any person obtaining a copy 4 | * of this software and associated documentation files (the "Software"), to deal 5 | * in the Software without restriction, including without limitation the rights 6 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | * copies of the Software, and to permit persons to whom the Software is 8 | * furnished to do so, subject to the following conditions: 9 | * 10 | * The above copyright notice and this permission notice shall be included in 11 | * all copies or substantial portions of the Software. 12 | * 13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | * THE SOFTWARE. 20 | */ 21 | 22 | #![cfg(test)] 23 | 24 | // ----- Modules ----- 25 | 26 | use super::*; 27 | use flexi_logger::Logger; 28 | 29 | // ----- Enumerations ----- 30 | 31 | enum LogError { 32 | CouldNotStartLogger, 33 | } 34 | 35 | // ----- Functions ----- 36 | 37 | /// Lite logging setup 38 | fn setup_logging_lite() -> Result<(), LogError> { 39 | match Logger::try_with_env_or_str("") 40 | .unwrap() 41 | .format(flexi_logger::colored_default_format) 42 | .format_for_files(flexi_logger::colored_detailed_format) 43 | .duplicate_to_stderr(flexi_logger::Duplicate::All) 44 | .start() 45 | { 46 | Err(_) => Err(LogError::CouldNotStartLogger), 47 | Ok(_) => Ok(()), 48 | } 49 | } 50 | 51 | /// Loopback helper 52 | /// Serializes, deserializes, then checks if same as original 53 | fn loopback_serializer(buffer: HidIoPacketBuffer, data: &mut [u8]) { 54 | // Serialize 55 | let data = match buffer.serialize_buffer(data) { 56 | Ok(data) => data, 57 | Err(err) => { 58 | panic!("Serialized Buffer failed: {:?}", err); 59 | } 60 | }; 61 | 62 | // Validate serialization worked 63 | assert!(!data.is_empty(), "Serialization bytes:{}", data.len()); 64 | 65 | // Deserialize while there are bytes left 66 | let mut deserialized = HidIoPacketBuffer::new(); 67 | let mut bytes_used = 0; 68 | while bytes_used != data.len() { 69 | // Remove already processed bytes 70 | let slice = &data[bytes_used..]; 71 | match deserialized.decode_packet(slice) { 72 | Ok(result) => { 73 | bytes_used += result as usize; 74 | } 75 | _ => { 76 | panic!("Failured decoding packet"); 77 | } 78 | }; 79 | } 80 | 81 | // Set the max_len as decode_packet does not infer this (not enough information from datastream) 82 | deserialized.max_len = buffer.max_len; 83 | 84 | // Validate buffers are the same 85 | assert!( 86 | buffer == deserialized, 87 | "\nInput:{}\nSerialized:{:#?}\nOutput:{}", 88 | buffer, 89 | data.len(), 90 | deserialized 91 | ); 92 | 93 | // Validate all bytes used 94 | assert!( 95 | data.len() == bytes_used, 96 | "Serialized:{}, Deserialized Used:{}", 97 | data.len(), 98 | bytes_used 99 | ); 100 | } 101 | 102 | // ----- Tests ----- 103 | 104 | /// Generates a sync payload and attempts to serialize 105 | /// This is the simplest hid-io packet 106 | /// Serializes, deserializes, then checks if same as original 107 | #[test] 108 | fn sync_payload_test() { 109 | setup_logging_lite().ok(); 110 | 111 | // Create single byte payload buffer 112 | let buffer = HidIoPacketBuffer::<1> { 113 | // Data packet 114 | ptype: HidIoPacketType::Sync, 115 | // Ready to go 116 | done: true, 117 | // Use defaults for other fields (unused) 118 | ..Default::default() 119 | }; 120 | 121 | // Run loopback serializer, handles all test validation 122 | let mut data = [0u8; 1]; 123 | loopback_serializer(buffer, &mut data); 124 | } 125 | 126 | /// Zero byte data payload 127 | /// This is the simplest data packet 128 | /// Serializes, deserializes, then checks if same as original 129 | #[test] 130 | fn no_payload_test() { 131 | setup_logging_lite().ok(); 132 | 133 | // Create single byte payload buffer 134 | let buffer = HidIoPacketBuffer::<0> { 135 | // Data packet 136 | ptype: HidIoPacketType::Data, 137 | // Test packet id 138 | id: HidIoCommandId::TestPacket, 139 | // Standard USB 2.0 FS packet length 140 | max_len: 64, 141 | // No payload 142 | data: Vec::new(), 143 | // Ready to go 144 | done: true, 145 | }; 146 | 147 | // Run loopback serializer, handles all test validation 148 | let mut data = [0u8; 4]; 149 | loopback_serializer(buffer, &mut data); 150 | } 151 | 152 | /// Generates a single byte payload buffer 153 | /// Serializes, deserializes, then checks if same as original 154 | #[test] 155 | fn single_byte_payload_test() { 156 | setup_logging_lite().ok(); 157 | 158 | // Create single byte payload buffer 159 | let buffer = HidIoPacketBuffer::<1> { 160 | // Data packet 161 | ptype: HidIoPacketType::Data, 162 | // Test packet id 163 | id: HidIoCommandId::TestPacket, 164 | // Standard USB 2.0 FS packet length 165 | max_len: 64, 166 | // Single byte, 0xAC 167 | data: Vec::from_slice(&[0xAC]).unwrap(), 168 | // Ready to go 169 | done: true, 170 | }; 171 | 172 | // Run loopback serializer, handles all test validation 173 | let mut data = [0u8; 5]; 174 | loopback_serializer(buffer, &mut data); 175 | } 176 | 177 | /// Generates a full packet payload buffer 178 | /// Serializes, deserializes, then checks if same as original 179 | #[test] 180 | fn full_packet_payload_test() { 181 | setup_logging_lite().ok(); 182 | 183 | // Create single byte payload buffer 184 | let buffer = HidIoPacketBuffer::<60> { 185 | // Data packet 186 | ptype: HidIoPacketType::Data, 187 | // Test packet id 188 | id: HidIoCommandId::TestPacket, 189 | // Standard USB 2.0 FS packet length 190 | max_len: 64, 191 | // 60 bytes, 0xAC; requires 2 byte header, and 2 bytes for id, which is 64 bytes 192 | data: Vec::from_slice(&[0xAC; 60]).unwrap(), 193 | // Ready to go 194 | done: true, 195 | }; 196 | 197 | // Run loopback serializer, handles all test validation 198 | let mut data = [0u8; 64]; 199 | loopback_serializer(buffer, &mut data); 200 | } 201 | 202 | /// Generates a two packet payload buffer 203 | /// Serializes, deserializes, then checks if same as original 204 | #[test] 205 | fn two_packet_continued_payload_test() { 206 | setup_logging_lite().ok(); 207 | 208 | // Create single byte payload buffer 209 | let buffer = HidIoPacketBuffer::<110> { 210 | // Data packet 211 | ptype: HidIoPacketType::Data, 212 | // Test packet id 213 | id: HidIoCommandId::TestPacket, 214 | // Standard USB 2.0 FS packet length 215 | max_len: 64, 216 | // 110 bytes, 0xAC: 60 then 50 (62 then 52) 217 | data: Vec::from_slice(&[0xAC; 110]).unwrap(), 218 | // Ready to go 219 | done: true, 220 | }; 221 | 222 | // Run loopback serializer, handles all test validation 223 | let mut data = [0u8; 118]; 224 | loopback_serializer(buffer, &mut data); 225 | } 226 | 227 | /// Generates a three packet payload buffer 228 | /// Serializes, deserializes, then checks if same as original 229 | #[test] 230 | fn three_packet_continued_payload_test() { 231 | setup_logging_lite().ok(); 232 | 233 | // Create single byte payload buffer 234 | let buffer = HidIoPacketBuffer::<170> { 235 | // Data packet 236 | ptype: HidIoPacketType::Data, 237 | // Test packet id 238 | id: HidIoCommandId::TestPacket, 239 | // Standard USB 2.0 FS packet length 240 | max_len: 64, 241 | // 170 bytes, 0xAC: 60, 60 then 50 (62, 62 then 52) 242 | data: Vec::from_slice(&[0xAC; 170]).unwrap(), 243 | // Ready to go 244 | done: true, 245 | }; 246 | 247 | // Run loopback serializer, handles all test validation 248 | let mut data = [0u8; 182]; 249 | loopback_serializer(buffer, &mut data); 250 | } 251 | 252 | /// Generates a serialized length greater than 1 byte (255) 253 | #[test] 254 | fn four_packet_continued_payload_test() { 255 | setup_logging_lite().ok(); 256 | 257 | // Create single byte payload buffer 258 | let buffer = HidIoPacketBuffer::<240> { 259 | // Data packet 260 | ptype: HidIoPacketType::Data, 261 | // Test packet id 262 | id: HidIoCommandId::TestPacket, 263 | // Standard USB 2.0 FS packet length 264 | max_len: 64, 265 | // 240 bytes, 0xAC: 60, 60, 60 then 60 (64, 64, 64, 64) 266 | data: Vec::from_slice(&[0xAC; 240]).unwrap(), 267 | // Ready to go 268 | done: true, 269 | }; 270 | 271 | // Run loopback serializer, handles all test validation 272 | let mut data = [0u8; 256]; 273 | loopback_serializer(buffer, &mut data); 274 | } 275 | 276 | /// Tests hid_bitmask2vec and hid_vec2bitmask 277 | #[test] 278 | fn hid_vec2bitmask2vec_test() { 279 | setup_logging_lite().ok(); 280 | 281 | let inputvec: Vec = Vec::from_slice(&[1, 2, 3, 4, 5, 100, 255]).unwrap(); 282 | 283 | // Convert, then convert back 284 | let bitmask = match hid_vec2bitmask(&inputvec) { 285 | Ok(bitmask) => bitmask, 286 | Err(e) => { 287 | panic!("Failed to run hid_vec2bitmask: {:?}", e); 288 | } 289 | }; 290 | let new_vec = match hid_bitmask2vec(&bitmask) { 291 | Ok(new_vec) => new_vec, 292 | Err(e) => { 293 | panic!("Failed to run hid_bitmask2vec: {:?}", e); 294 | } 295 | }; 296 | 297 | // Compare with original 298 | assert_eq!( 299 | inputvec, new_vec, 300 | "Bitmask test failed! Input: {:?}\nOutput: {:?}", 301 | inputvec, new_vec, 302 | ); 303 | } 304 | -------------------------------------------------------------------------------- /hid-io.service: -------------------------------------------------------------------------------- 1 | [Service] 2 | ExecStart=/usr/bin/hid-io-core 3 | DynamicUser=yes 4 | 5 | -------------------------------------------------------------------------------- /hidio.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Label 6 | io.github.hid-io 7 | ProgramArguments 8 | 9 | /Users/rowand/git/hid-io/target/release/hid-io 10 | 11 | StandardOutPath 12 | /var/log/hidio.log 13 | StandardErrorPath 14 | /var/log/hidio.log 15 | RunAtLoad 16 | 17 | KeepAlive 18 | 19 | Sockets 20 | 21 | Listeners 22 | 23 | SockServiceName 24 | 7185 25 | SockType 26 | dgram 27 | SockFamily 28 | IPv4 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hid-io/hid-io-core/0531cac7879d6c692e149d79bba21609f48acb58/logo.png -------------------------------------------------------------------------------- /misc/images/HID-IO_Box_Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hid-io/hid-io-core/0531cac7879d6c692e149d79bba21609f48acb58/misc/images/HID-IO_Box_Logo.png -------------------------------------------------------------------------------- /misc/images/HID-IO_Overview.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hid-io/hid-io-core/0531cac7879d6c692e149d79bba21609f48acb58/misc/images/HID-IO_Overview.ai -------------------------------------------------------------------------------- /misc/images/HID-IO_Overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hid-io/hid-io-core/0531cac7879d6c692e149d79bba21609f48acb58/misc/images/HID-IO_Overview.png -------------------------------------------------------------------------------- /misc/images/README.md: -------------------------------------------------------------------------------- 1 | # Various Images 2 | 3 | - HID-IO Font -> TT Norms (with some modifications) 4 | 5 | -------------------------------------------------------------------------------- /misc/images/hid-io_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hid-io/hid-io-core/0531cac7879d6c692e149d79bba21609f48acb58/misc/images/hid-io_logo.png -------------------------------------------------------------------------------- /python/.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled source # 2 | ################### 3 | *.com 4 | *.class 5 | *.dll 6 | *.exe 7 | *.o 8 | *.so 9 | *.elf 10 | *.bin 11 | *.hex 12 | *.lss 13 | *.sym 14 | *.map 15 | 16 | # Packages # 17 | ############ 18 | # it's better to unpack these files and commit the raw source 19 | # git has its own built in compression methods 20 | *.7z 21 | *.dmg 22 | *.gz 23 | *.iso 24 | *.jar 25 | *.rar 26 | *.tar 27 | *.zip 28 | 29 | # Logs and databases # 30 | ###################### 31 | *.log 32 | *.sql 33 | *.sqlite 34 | 35 | # OS generated files # 36 | ###################### 37 | .DS_Store 38 | .DS_Store? 39 | ._* 40 | .Spotlight-V100 41 | .Trashes 42 | ehthumbs.db 43 | Thumbs.db 44 | 45 | # Editor generated files # 46 | ########################## 47 | *.swp 48 | 49 | # Source browsing files # 50 | ######################### 51 | tags 52 | 53 | # CMake Generated Files # 54 | ######################### 55 | CMakeFiles 56 | CMakeCache.txt 57 | cmake_install.cmake 58 | 59 | # Dropbox # 60 | ########### 61 | *.attr 62 | 63 | # Misc # 64 | ######## 65 | generatedKeymap.h 66 | generatedPixelmap.c 67 | kll_defs.h 68 | tests/generated* 69 | tests/test_controller 70 | 71 | 72 | # Byte-compiled / optimized / DLL files 73 | __pycache__/ 74 | *.py[cod] 75 | *$py.class 76 | 77 | # C extensions 78 | *.so 79 | 80 | # Distribution / packaging 81 | .Python 82 | build/ 83 | develop-eggs/ 84 | dist/ 85 | downloads/ 86 | eggs/ 87 | .eggs/ 88 | lib/ 89 | lib64/ 90 | parts/ 91 | sdist/ 92 | var/ 93 | wheels/ 94 | *.egg-info/ 95 | .installed.cfg 96 | *.egg 97 | MANIFEST 98 | 99 | # PyInstaller 100 | # Usually these files are written by a python script from a template 101 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 102 | *.manifest 103 | *.spec 104 | 105 | # Installer logs 106 | pip-log.txt 107 | pip-delete-this-directory.txt 108 | 109 | # Unit test / coverage reports 110 | htmlcov/ 111 | .tox/ 112 | .coverage 113 | .coverage.* 114 | .cache 115 | nosetests.xml 116 | coverage.xml 117 | *.cover 118 | .hypothesis/ 119 | .pytest_cache/ 120 | 121 | # Translations 122 | *.mo 123 | *.pot 124 | 125 | # Django stuff: 126 | *.log 127 | local_settings.py 128 | db.sqlite3 129 | 130 | # Flask stuff: 131 | instance/ 132 | .webassets-cache 133 | 134 | # Scrapy stuff: 135 | .scrapy 136 | 137 | # Sphinx documentation 138 | docs/_build/ 139 | 140 | # PyBuilder 141 | target/ 142 | 143 | # Jupyter Notebook 144 | .ipynb_checkpoints 145 | 146 | # pyenv 147 | .python-version 148 | 149 | # celery beat schedule file 150 | celerybeat-schedule 151 | 152 | # SageMath parsed files 153 | *.sage.py 154 | 155 | # Environments 156 | .env 157 | .venv 158 | env/ 159 | venv/ 160 | ENV/ 161 | env.bak/ 162 | venv.bak/ 163 | 164 | # Spyder project settings 165 | .spyderproject 166 | .spyproject 167 | 168 | # Rope project settings 169 | .ropeproject 170 | 171 | # mkdocs documentation 172 | /site 173 | 174 | # mypy 175 | .mypy_cache/ 176 | 177 | # Other 178 | Pipfile.lock 179 | -------------------------------------------------------------------------------- /python/LICENSE: -------------------------------------------------------------------------------- 1 | This license applies to the hid-io-core python library only. 2 | 3 | MIT License 4 | 5 | Copyright (c) 2019 Jacob Alexander 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /python/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | cython = "*" 8 | pycapnp = ">=1.2.1" 9 | pytest = "*" 10 | pygtail = "*" 11 | 12 | [dev-packages] 13 | flit = "*" 14 | 15 | [requires] 16 | python_version = ">=3.7" 17 | -------------------------------------------------------------------------------- /python/README.md: -------------------------------------------------------------------------------- 1 | # hidio core Client Python Library 2 | HID-IO Core Client Side Library for Python 3 | 4 | [![Linux Status](https://github.com/hid-io/hid-io-core/workflows/Rust%20Linux/badge.svg)](https://github.com/hid-io/hid-io-core/actions) 5 | [![macOS Status](https://github.com/hid-io/hid-io-core/workflows/Rust%20macOS/badge.svg)](https://github.com/hid-io/hid-io-core/actions) 6 | [![Windows Status](https://github.com/hid-io/hid-io-core/workflows/Rust%20Windows/badge.svg)](https://github.com/hid-io/hid-io-core/actions) 7 | 8 | [![Visit our IRC channel](https://kiwiirc.com/buttons/irc.freenode.net/hid-io.png)](https://kiwiirc.com/client/irc.freenode.net/#hid-io) 9 | 10 | ## Getting 11 | 12 | ```bash 13 | pip install hidiocore 14 | ``` 15 | 16 | 17 | ## Overview 18 | 19 | This is a convenience Python library for the HID-IO daemon which handles automatic reconnection if the server goes down for any reason. 20 | The library also handles the HID-IO authentication procedure (key negotiation and TLS wrapping). 21 | 22 | 23 | ## Usage 24 | 25 | ```python 26 | import asyncio 27 | import sys 28 | 29 | import hidiocore.client 30 | 31 | # Optional callbacks 32 | class MyHidIoClient(hidiocore.client.HidIoClient): 33 | async def on_connect(self, cap): 34 | print("Connected!") 35 | print("Connected API Call", await cap.alive().a_wait()) 36 | 37 | 38 | async def on_disconnect(self): 39 | print("Disconnected!") 40 | 41 | 42 | async def main(): 43 | client = MyHidIoClient('Python example.py') 44 | # Connect the client to the server using a background task 45 | # This will automatically reconnect 46 | tasks = [asyncio.gather(*[client.connect(auth=hidiocore.client.HidIoClient.AUTH_BASIC)], return_exceptions=True)] 47 | while client.retry_connection_status(): 48 | if client.capability_hidioserver(): 49 | try: 50 | print("API Call", await asyncio.wait_for( 51 | client.capability_hidioserver().alive().a_wait(), 52 | timeout=2.0 53 | )) 54 | print("API Call", await asyncio.wait_for( 55 | client.capability_authenticated().nodes().a_wait(), 56 | timeout=2.0 57 | )) 58 | except asyncio.TimeoutError: 59 | print("Alive timeout.") 60 | continue 61 | await asyncio.sleep(5) 62 | 63 | 64 | try: 65 | loop = asyncio.get_event_loop() 66 | loop.run_until_complete(main()) 67 | except KeyboardInterrupt: 68 | print("Ctrl+C detected, exiting...") 69 | sys.exit(1) 70 | ``` 71 | -------------------------------------------------------------------------------- /python/examples/info.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | ''' 3 | Basic HID-IO Python Client Example 4 | ''' 5 | 6 | # Copyright (C) 2019-2020 Jacob Alexander 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | # THE SOFTWARE. 25 | 26 | import argparse 27 | import asyncio 28 | import logging 29 | import os 30 | import sys 31 | 32 | sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) 33 | import hidiocore.client # noqa 34 | 35 | logging.basicConfig(level=logging.DEBUG) 36 | logger = logging.getLogger(__name__) 37 | 38 | 39 | class MyHidIoClient(hidiocore.client.HidIoClient): 40 | async def on_connect(self, cap, cap_auth): 41 | logger.info("Connected!") 42 | print("Connected API Call", await cap.alive().a_wait()) 43 | 44 | async def on_disconnect(self): 45 | logger.info("Disconnected!") 46 | 47 | 48 | async def main(args): 49 | client = MyHidIoClient('Python info gathering example') 50 | # Connect the client to the server using a background task 51 | # This will automatically reconnect 52 | _tasks = [ # noqa: F841 53 | asyncio.gather( 54 | *[client.connect(auth=hidiocore.client.HidIoClient.AUTH_BASIC)], 55 | return_exceptions=True 56 | ) 57 | ] 58 | while client.retry_connection_status(): 59 | if client.capability_hidioserver(): 60 | try: 61 | # Get list of nodes 62 | nodes = (await asyncio.wait_for( 63 | client.nodes(), 64 | timeout=2.0 65 | )).nodes 66 | 67 | # Match on HidioDaemon node 68 | nodes = [n for n in nodes if n.type == 'hidioDaemon'] 69 | assert len(nodes) == 1, "There can be only one! ...hidioDaemon" 70 | 71 | # Print daemon information 72 | print(await nodes[0].node.daemon.info().a_wait()) 73 | 74 | return 75 | except asyncio.TimeoutError: 76 | logger.info("Timeout, trying again.") 77 | continue 78 | await asyncio.sleep(1) 79 | 80 | 81 | parser = argparse.ArgumentParser(description='Info gathering example for HID-IO') 82 | args = parser.parse_args() 83 | try: 84 | loop = asyncio.get_event_loop() 85 | loop.run_until_complete(main(args)) 86 | except KeyboardInterrupt: 87 | logger.warning("Ctrl+C detected, exiting...") 88 | sys.exit(1) 89 | -------------------------------------------------------------------------------- /python/examples/unicode_text.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | ''' 3 | Basic HID-IO Python Client Example 4 | ''' 5 | 6 | # Copyright (C) 2019-2020 Jacob Alexander 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | # THE SOFTWARE. 25 | 26 | import argparse 27 | import asyncio 28 | import logging 29 | import os 30 | import sys 31 | import time 32 | 33 | sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) 34 | import hidiocore.client # noqa 35 | 36 | logging.basicConfig(level=logging.DEBUG) 37 | logger = logging.getLogger(__name__) 38 | 39 | 40 | class MyHidIoClient(hidiocore.client.HidIoClient): 41 | async def on_connect(self, cap, cap_auth): 42 | logger.info("Connected!") 43 | print("Connected API Call", await cap.alive().a_wait()) 44 | 45 | async def on_disconnect(self): 46 | logger.info("Disconnected!") 47 | 48 | 49 | async def main(args): 50 | client = MyHidIoClient('Python unicode text example') 51 | # Connect the client to the server using a background task 52 | # This will automatically reconnect 53 | _tasks = [ # noqa: F841 54 | asyncio.gather( 55 | *[client.connect(auth=hidiocore.client.HidIoClient.AUTH_BASIC)], 56 | return_exceptions=True 57 | ) 58 | ] 59 | while client.retry_connection_status(): 60 | if client.capability_hidioserver(): 61 | try: 62 | # Get list of nodes 63 | nodes = (await asyncio.wait_for( 64 | client.nodes(), 65 | timeout=2.0 66 | )).nodes 67 | 68 | # Match on HidioDaemon node 69 | nodes = [n for n in nodes if n.type == 'hidioDaemon'] 70 | assert len(nodes) == 1, "There can be only one! ...hidioDaemon" 71 | 72 | # Print unicode text, these messages will be sent sequentially 73 | # HID-IO enforces strict queuing of displayserver access so you won't get text sequence overlaps 74 | values = await asyncio.gather( 75 | nodes[0].node.daemon.unicodeString("☃ Myyy text! 💣💣💩💩🔥🔥").a_wait(), 76 | nodes[0].node.daemon.unicodeString(" some more text 💣💩🔥").a_wait(), 77 | nodes[0].node.daemon.unicodeString(" and some more text 🔥").a_wait(), 78 | nodes[0].node.daemon.unicodeString("abc").a_wait(), 79 | nodes[0].node.daemon.unicodeString("2abc1").a_wait(), 80 | ) 81 | print(values) 82 | 83 | # Press a "snowman" key, hold for 1 second (to see repeat rate), then release 84 | await nodes[0].node.daemon.unicodeKeys("☃").a_wait(), 85 | time.sleep(1) 86 | await nodes[0].node.daemon.unicodeKeys("").a_wait(), 87 | 88 | return 89 | except asyncio.TimeoutError: 90 | logger.info("Timeout, trying again.") 91 | continue 92 | await asyncio.sleep(1) 93 | 94 | 95 | parser = argparse.ArgumentParser(description='Unicode text output example for HID-IO') 96 | args = parser.parse_args() 97 | try: 98 | loop = asyncio.get_event_loop() 99 | loop.run_until_complete(main(args)) 100 | except KeyboardInterrupt: 101 | logger.warning("Ctrl+C detected, exiting...") 102 | sys.exit(1) 103 | -------------------------------------------------------------------------------- /python/hidiocore/__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | HID-IO Python Library 3 | ''' 4 | 5 | # Copyright (C) 2019-2020 Jacob Alexander 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | # THE SOFTWARE. 24 | 25 | __version__ = '0.1.0' 26 | -------------------------------------------------------------------------------- /python/hidiocore/schema/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hid-io/hid-io-core/0531cac7879d6c692e149d79bba21609f48acb58/python/hidiocore/schema/__init__.py -------------------------------------------------------------------------------- /python/hidiocore/schema/common.capnp: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2017-2020 by Jacob Alexander 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | @0xeed5743611d4cb4a; 22 | 23 | ## Imports ## 24 | 25 | using import "daemon.capnp".Daemon; 26 | using import "keyboard.capnp".Keyboard; 27 | 28 | 29 | 30 | ## Enumerations ## 31 | 32 | enum NodeType { 33 | hidioDaemon @0; 34 | # HidIo Daemon node (hid-io-core functionality) 35 | # There should always be a single daemon node active 36 | 37 | hidioApi @1; 38 | # Generic HidIo API node 39 | 40 | usbKeyboard @2; 41 | # HidIo USB Keyboard 42 | 43 | bleKeyboard @3; 44 | # HidIo BLE Keyboard 45 | 46 | hidKeyboard @4; 47 | # Generic HID Keyboard 48 | 49 | hidMouse @5; 50 | # Generic HID Mouse 51 | 52 | hidJoystick @6; 53 | # Generic HID Joystick 54 | 55 | unknown @7; 56 | # Unknown Type 57 | } 58 | # Node types, please extend this enum as necessary 59 | # Should be generic types, nothing specific, use the text field for that 60 | 61 | 62 | 63 | ## Structs ## 64 | 65 | struct Source { 66 | # This struct represents the source of a signal 67 | 68 | type @0 :NodeType; 69 | # Type of node 70 | 71 | name @1 :Text; 72 | # Name of source 73 | # May or may not be unique 74 | 75 | serial @2 :Text; 76 | # Serial number (text field) of source 77 | # Zero-length if unused 78 | 79 | id @3 :UInt64; 80 | # Unique id identifying source 81 | # While daemon is running, ids are only reused once 2^64 Ids have been utilized. 82 | # This allows (for at least a brief period) a unique source (which may disappear before processing) 83 | } 84 | 85 | struct Destination { 86 | # This struct represents destination of a function 87 | # Not all functions require a destination 88 | 89 | type @0 :NodeType; 90 | # Type of node 91 | 92 | name @1 :Text; 93 | # Name of destination 94 | # May or may not be unique 95 | 96 | serial @2 :Text; 97 | # Serial number (text field) of destination 98 | # Zero-length if unused 99 | 100 | id @3 :UInt64; 101 | # Unique id identifying destination 102 | # While daemon is running, ids are only reused once 2^64 Ids have been utilized. 103 | # This allows (for at least a brief period) a unique source (which may disappear before processing) 104 | 105 | node :union { 106 | # Interface node of destination 107 | # A separate node is generated for each interface node 108 | # (i.e. there may be multiple nodes per physical/virtual device) 109 | # 110 | # May not be set depending on the type of node as there may not be any additional functionality available 111 | # to the API 112 | 113 | keyboard @4 :Keyboard; 114 | # HidIo Keyboard Node 115 | # Valid when the type is set to usbKeyboard or bleKeyboard 116 | 117 | daemon @5 :Daemon; 118 | # Daemon (hid-io-core) Command Node 119 | # Valid when the type is set to hidioDaemon 120 | } 121 | } 122 | 123 | 124 | 125 | ## Interfaces ## 126 | 127 | interface Node { 128 | # Common interface for all HidIo api nodes 129 | } 130 | -------------------------------------------------------------------------------- /python/hidiocore/schema/daemon.capnp: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2020 by Jacob Alexander 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | @0x89950abd85ed15de; 22 | 23 | ## Imports ## 24 | 25 | using Common = import "common.capnp"; 26 | 27 | 28 | 29 | ## Interfaces ## 30 | 31 | interface Daemon extends(Common.Node) { 32 | # API interface to hid-io-core 33 | # This is the main entry point for calling hid-io-core functionality. 34 | 35 | enum SubscriptionOptionType { 36 | layout @0; 37 | # OS Keyboard layout change subscription 38 | # Sends a notification whenever the OS keyboard layout changes 39 | } 40 | 41 | struct Signal { 42 | struct Layout { 43 | } 44 | 45 | time @0 :UInt64; 46 | # Signal event timestamp 47 | 48 | data :union { 49 | layout @1 :Layout; 50 | # Layout event message 51 | 52 | tmp @2 :Layout; 53 | # TODO Removeme 54 | } 55 | } 56 | 57 | struct Info { 58 | hidioMajorVersion @0 :UInt16; 59 | hidioMinorVersion @1 :UInt16; 60 | hidioPatchVersion @2 :UInt16; 61 | # HID-IO Version information 62 | 63 | os @3 :Text; 64 | osVersion @4 :Text; 65 | # OS name and version running the daemon 66 | 67 | hostName @5 :Text; 68 | # Name of the daemon 69 | } 70 | 71 | struct SubscriptionOption { 72 | type @0 :SubscriptionOptionType; 73 | 74 | struct NoneOption {} 75 | 76 | conf :union { 77 | tmp1 @1 :NoneOption; 78 | # TODO Removeme 79 | 80 | tmp2 @2 :NoneOption; 81 | # TODO Removeme 82 | } 83 | } 84 | 85 | 86 | interface Subscription { 87 | # Subscription interface 88 | # Handles subscription ownership and when to drop subscription 89 | } 90 | 91 | interface Subscriber { 92 | # Node subscriber 93 | # Handles any push notifications from hid-io-core endpoints 94 | # NOTE: Not all packets are sent by default, you must configure the subscription to enable the ones you want 95 | 96 | update @0 (signal :Signal); 97 | # Called whenever a subscribed packet type (to this device) is available 98 | # May return 1 or more packets depending on the size of the queue 99 | # 100 | # Time is when the rpc is sent. 101 | # Useful when determining Signal ordering 102 | } 103 | 104 | subscribe @0 (subscriber :Subscriber, options :List(SubscriptionOption)) -> (subscription :Subscription); 105 | # Subscribes to a Subscriber interface 106 | # Registers push notifications for this node, the packets received will depend on the SubscriptionOption list 107 | # By default no packets will be sent 108 | # Will return an error if any of the options are not supported/invalid for this device 109 | 110 | unicodeText @1 (string :Text); 111 | # Output a unicode string to the focused window 112 | 113 | unicodeState @2 (characters :Text); 114 | # Hold the specified unicode symbols on the focused window 115 | # To release the symbols, call this command again without those symbols specified 116 | # e.g. 117 | # unicodeKeys("abcd") -> Holds abcd 118 | # unicodeKeys("d") -> Releases abc but keeps d held 119 | 120 | info @3 () -> (info: Info); 121 | # Returns system and daemon information 122 | # Mirrors what's available to HID-IO devices 123 | 124 | # Unicode 125 | # TODO 126 | # String 127 | # Symbol 128 | # Get Held 129 | # Set Held 130 | 131 | # Layout 132 | # Get 133 | # Set 134 | 135 | # VirtualKeyboard 136 | # VirtualMouse 137 | # VirtualJoystick 138 | } 139 | -------------------------------------------------------------------------------- /python/hidiocore/schema/keyboard.capnp: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2017-2020 by Jacob Alexander 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | @0xe10f671900e093b0; 22 | 23 | ## Imports ## 24 | 25 | using Common = import "common.capnp"; 26 | using HidIo = import "hidio.capnp"; 27 | 28 | 29 | 30 | ## Interfaces ## 31 | 32 | interface Keyboard extends(HidIo.Node) { 33 | # HidIo Keyboard node 34 | 35 | enum SubscriptionOptionType { 36 | hostMacro @0; 37 | # Host Macro subscription option 38 | # A host macro supplies an index that can be assigned to an arbitrary function 39 | 40 | layer @1; 41 | # Layer subscription option 42 | # Sends notifications on layer changes 43 | 44 | kllTrigger @2; 45 | # KLL trigger subscription option 46 | # Scan Code subscription 47 | # This uses the devices internal numbering scheme which must be converted to HID in order to use 48 | # Only returns a list of activated scan codes 49 | # If no id/index pairs are specified, this subscribes to all KLL triggers 50 | 51 | kllTriggerDisable @3; 52 | # KLL trigger disable option 53 | # This specifies which KLL Triggers are ignored by the keyboard's internal macro engine 54 | # (and will not be sent over the HID keyboard interface). 55 | # Useful when using the API to send data directly to the application or through a virtual HID device 56 | # If no id/index pairs are specified, all kllTriggers are ignored by the keyboard. 57 | 58 | cliOutput @4; 59 | # Subscribe to CLI output 60 | # Useful when using cli commands (to see the result) or to monitor keyboard status and internal errors 61 | 62 | manufacturingResult @5; 63 | # Subscribe to Manufacturing Results 64 | # Used when getting the asynchronous updates from Manufacturing tests 65 | } 66 | 67 | 68 | struct Signal { 69 | struct Cli { 70 | # Cli Message output text 71 | output @0 :Text; 72 | } 73 | 74 | struct KLL { 75 | } 76 | 77 | struct HostMacro { 78 | } 79 | 80 | struct Layer { 81 | } 82 | 83 | struct ManufacturingResult { 84 | enum Command { 85 | ledTestSequence @0; 86 | ledCycleKeypressTest @1; 87 | hallEffectSensorTest @2; 88 | } 89 | 90 | # Cmd and Arg relate to the orignal manufacturingTest command used 91 | cmd @0 :Command; 92 | arg @1 :UInt16; 93 | # Free-form byte data from the result 94 | data @2 :List(UInt8); 95 | } 96 | 97 | time @0 :UInt64; 98 | # Signal event timestamp 99 | 100 | data :union { 101 | cli @1 :Cli; 102 | # CLI Output message 103 | 104 | kll @2 :KLL; 105 | # KLL Trigger message 106 | 107 | hostMacro @3 :HostMacro; 108 | # Host Macro message 109 | 110 | layer @4 :Layer; 111 | # Layer event message 112 | 113 | manufacturing @5 :ManufacturingResult; 114 | # Manufacturing message 115 | } 116 | } 117 | 118 | struct SubscriptionOption { 119 | type @0 :SubscriptionOptionType; 120 | 121 | struct KLLTriggerOption { 122 | id @0 :UInt8; 123 | # This maps to a KLL TriggerType 124 | # https://github.com/kiibohd/controller/blob/master/Macro/PartialMap/kll.h#L263 125 | 126 | index @1 :UInt8; 127 | # This maps to a KLL TriggerEvent index (i.e. ScanCode) 128 | # This number is always 8-bits, for higher scancodes you'll need to use a different id 129 | # See https://github.com/kiibohd/controller/blob/master/Macro/PartialMap/kll.h#L221 130 | } 131 | 132 | struct NoneOption {} 133 | 134 | conf :union { 135 | kllTrigger @1 :List(KLLTriggerOption); 136 | # Specified with a kllTrigger or kllTriggerDisable option 137 | 138 | tmp @2 :NoneOption; 139 | # TODO Removeme 140 | } 141 | } 142 | 143 | 144 | interface Subscription { 145 | # Subscription interface 146 | # Handles subscription ownership and when to drop subscription 147 | } 148 | 149 | interface Subscriber { 150 | # Node subscriber 151 | # Handles any push notifications from hid-io-core endpoints 152 | # NOTE: Not all packets are sent by default, you must configure the subscription to enable the ones you want 153 | 154 | update @0 (signal :Signal); 155 | # Called whenever a subscribed packet type (to this device) is available 156 | } 157 | 158 | 159 | subscribe @0 (subscriber :Subscriber, options :List(SubscriptionOption)) -> (subscription :Subscription); 160 | # Subscribes to a Subscriber interface 161 | # Registers push notifications for this node, the packets received will depend on the SubscriptionOption list 162 | # By default no packets will be sent 163 | # Will return an error if any of the options are not supported/invalid for this device 164 | 165 | # TODO 166 | # Scan Code -> HID Code lookup (per layer) 167 | # Pixel Control 168 | # Generic commands 169 | } 170 | -------------------------------------------------------------------------------- /python/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["flit"] 3 | build-backend = "flit.buildapi" 4 | 5 | [tool.flit.metadata] 6 | module = "hidiocore" 7 | author = "Jacob Alexander" 8 | author-email = "haata@kiibohd.com" 9 | home-page = "http://hid-io.com" 10 | classifiers = [ 11 | "Topic :: Software Development :: Libraries", 12 | "Intended Audience :: Developers", 13 | "Programming Language :: Python :: 3", 14 | ] 15 | description-file = "README.md" 16 | requires = [ 17 | "cython>=0.29", 18 | "pycapnp>=1.0.0", 19 | "pygtail>=0.11.1", 20 | ] 21 | requires-python = ">=3.7" 22 | 23 | -------------------------------------------------------------------------------- /python/requirements.txt: -------------------------------------------------------------------------------- 1 | attrs==19.3.0 2 | Cython==0.29.14 3 | importlib-metadata==1.3.0 4 | more-itertools==8.0.2 5 | packaging==20.0 6 | pluggy==0.13.1 7 | py==1.10.0 8 | pycapnp==1.2.1 9 | pygtail==0.11.1 10 | pyparsing==2.4.6 11 | pytest==7.2.0 12 | six==1.13.0 13 | wcwidth==0.1.8 14 | zipp==0.6.0 15 | -------------------------------------------------------------------------------- /python/tests/example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | ''' 3 | Basic HID-IO Python Client Example 4 | ''' 5 | 6 | # Copyright (C) 2019-2020 Jacob Alexander 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | # THE SOFTWARE. 25 | 26 | import argparse 27 | import asyncio 28 | import logging 29 | import os 30 | import sys 31 | 32 | sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) 33 | import hidiocore.client # noqa 34 | 35 | logging.basicConfig(level=logging.DEBUG) 36 | logger = logging.getLogger(__name__) 37 | 38 | 39 | class MyHidIoClient(hidiocore.client.HidIoClient): 40 | async def on_connect(self, cap, cap_auth): 41 | logger.info("Connected!") 42 | print("Connected API Call", await cap.alive().a_wait()) 43 | 44 | async def on_disconnect(self): 45 | logger.info("Disconnected!") 46 | 47 | def on_nodesupdate(self, nodes): 48 | print("Nodes Update", nodes) 49 | 50 | 51 | async def main(args): 52 | client = MyHidIoClient('Python example.py') 53 | # Connect the client to the server using a background task 54 | # This will automatically reconnect 55 | _tasks = [ # noqa: F841 56 | asyncio.gather( 57 | *[client.connect(auth=hidiocore.client.HidIoClient.AUTH_BASIC)], 58 | return_exceptions=True 59 | ) 60 | ] 61 | while client.retry_connection_status(): 62 | if client.capability_hidioserver(): 63 | try: 64 | print("API Call", await asyncio.wait_for( 65 | client.capability_hidioserver().alive().a_wait(), 66 | timeout=2.0 67 | )) 68 | print("API Call", await asyncio.wait_for( 69 | client.nodes(), 70 | timeout=2.0 71 | )) 72 | # Check if this is just a single-shot test 73 | if args.single: 74 | return 75 | except asyncio.TimeoutError: 76 | logger.info("Alive timeout.") 77 | continue 78 | await asyncio.sleep(5) 79 | 80 | 81 | parser = argparse.ArgumentParser(description='Example HID-IO client library for Python') 82 | parser.add_argument('--single', action='store_true') 83 | args = parser.parse_args() 84 | try: 85 | loop = asyncio.get_event_loop() 86 | loop.run_until_complete(main(args)) 87 | except KeyboardInterrupt: 88 | logger.warning("Ctrl+C detected, exiting...") 89 | sys.exit(1) 90 | -------------------------------------------------------------------------------- /python/tests/test_examples.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Basic test cases for HID-IO Client Python Library 3 | ''' 4 | 5 | # Copyright (C) 2019-2020 Jacob Alexander 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | # THE SOFTWARE. 24 | 25 | import os 26 | import socket 27 | import subprocess 28 | import sys 29 | import time 30 | 31 | python_dir = os.path.join(os.path.dirname(__file__)) 32 | 33 | 34 | def run_subprocesses(client, args): 35 | env = os.environ 36 | env['RUST_LOG'] = 'info,hid_io_core::api=trace' 37 | path = os.path.abspath(os.path.join(python_dir, '..', '..')) 38 | server = subprocess.Popen(['cargo', 'run'], env=env, cwd=path) 39 | 40 | # Wait for server to start 41 | addr, port = ('localhost', 7185) 42 | retries = 30 43 | while True: 44 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 45 | result = sock.connect_ex((addr, port)) 46 | if result == 0: 47 | break 48 | sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) 49 | result = sock.connect_ex((addr, port)) 50 | if result == 0: 51 | break 52 | # Give the server some small amount of time to start listening 53 | time.sleep(1) 54 | retries -= 1 55 | if retries == 0: 56 | assert False, "Timed out waiting for server to start" 57 | client_args = [sys.executable, os.path.join(python_dir, client)] 58 | client_args.extend(args) 59 | client = subprocess.Popen(client_args) 60 | 61 | ret = client.wait() 62 | server.kill() 63 | assert ret == 0, "Client did not return 0" 64 | 65 | 66 | def test_example(): 67 | client = 'example.py' 68 | args = ['--single'] 69 | run_subprocesses(client, args) 70 | -------------------------------------------------------------------------------- /schema: -------------------------------------------------------------------------------- 1 | python/hidiocore/schema -------------------------------------------------------------------------------- /spec/README.md: -------------------------------------------------------------------------------- 1 | # HID-IO Specs 2 | 3 | Specs for the various parts of the HID-IO daemon. 4 | 5 | 6 | ## protocol 7 | 8 | Details on the daemon to MCU protocol. 9 | 10 | 11 | ## interface 12 | 13 | Details on how to interface with the HID-IO daemon using external programs and scripts. 14 | 15 | -------------------------------------------------------------------------------- /spec/interface/README.md: -------------------------------------------------------------------------------- 1 | # HID-IO Interface Spec 2 | 3 | TODO 4 | 5 | 6 | # HID-IO Interface Outline/Requirements 7 | 8 | * Ability to register many different external programs/scripts at the same time 9 | + Each external script should not talk to another one through HID-IO 10 | + External scripts should not interfere with existing HID-IO protocol communications with devices 11 | * Support multiple devices at the same time (2 keyboards, keyboard + mouse, etc.) 12 | * Direct HID-IO protocol communication for some IDs 13 | + Three levels of access 14 | - Root/secure access (possibly key-presses?) 15 | - Userspace access (specific macro key was triggered, call animation, etc.) 16 | - Debug root access (directly communicate using HID-IO protocol with no restrictions, not enabled by default) 17 | + Three levels of control 18 | - Listen (wait for a specific macro event, can be listened by multiple scripts) 19 | - Request (request information from a keyboard, information is only sent back to requester) 20 | - Snoop (debug ability to monitor per Id request, not enabled by default) 21 | * Support commands for requesting information from the HID-IO daemon (not necessarily just a pass-through to the HID device) 22 | 23 | 24 | ## Requirements for RPC/Scripting Interface 25 | 26 | * Supports a wide variety of languages, preferably scripting 27 | + Python is a good starting point 28 | * Relatively straightforward to write a script to listen or request data from a keyboard 29 | * Cross-platform 30 | * Uses sockets 31 | + 7185 (0x1c11) - Not currently assigned to anything 32 | + Only allows connections from localhost (127.0.0.1) 33 | * Authentication 34 | + Thoughts? (not really settled on doing it this way, but some sort of authentication is needed for more sensitive operations) 35 | + Request a run-time key from the HID-IO daemon 36 | - hid-io --userspace-key 37 | - hid-io --secure-key # Requires root priviledges 38 | + Should use some sort of TLS for traffic 39 | - Key would be used to establish the tunnel 40 | 41 | 42 | ## Authentication 43 | 44 | Thoughts about authentication. 45 | 46 | For the initial implementation, I'll restrict elevated functions to debug mode (so I can get something up and running). 47 | 48 | 49 | ### Device 50 | 51 | * Authorized functions on device 52 | + Token generation 53 | * Uses 54 | + Jumping to bootloader 55 | + Reading keypresses directly 56 | + Features that can be dangerous if miss-used 57 | * Token is granted per "Usage" 58 | + Selection of commands that comprise an action 59 | * Should be valid, with an expiry due to: 60 | + Restarting HID-IO daemon 61 | + Restarting device 62 | + New connection to interface 63 | + Specific amount of time 64 | + At device request 65 | + At daemon request 66 | * Device will only support a limited number of simultaneous tokens 67 | 68 | 69 | ### Daemon 70 | 71 | * Authorized functions in daemon 72 | + Using requires generated token, physical device may not be available, or support token generation (bootloader mode) 73 | * Uses 74 | + Unicode output 75 | + Simulated keyboard 76 | + Features that normally require system root priviledges 77 | * Token is granted per "Usage" 78 | + Selection of commands that comprise an action 79 | * Should be valid, with an expiry due to: 80 | + Restarting HID-IO daemon 81 | + New connection to interface 82 | + Specific amount of time 83 | + At daemon request 84 | 85 | 86 | ### Usage (is there a technical term already coined for this?) 87 | 88 | A Usage is a collection of RPC functions that can define authentication terms. 89 | * How long the authentication is valid 90 | * Which RPC/functions may be called with the Usage 91 | * Possibly the order in which functions are called 92 | + Calling out of order results in immediate revoking of the token 93 | * Possibly revoking token if any RPC functions fail 94 | 95 | -------------------------------------------------------------------------------- /spec/protocol: -------------------------------------------------------------------------------- 1 | ../hid-io-protocol/spec -------------------------------------------------------------------------------- /src/bin/hid-io-core.rs: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2019-2022 by Jacob Alexander 2 | * Copyright (C) 2019 by Rowan Decker 3 | * 4 | * This file is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This file is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this file. If not, see . 16 | */ 17 | 18 | #[macro_use] 19 | extern crate log; 20 | 21 | #[cfg(windows)] 22 | #[macro_use] 23 | extern crate windows_service; 24 | 25 | #[cfg(windows)] 26 | use hid_io_core::RUNNING; 27 | 28 | #[cfg(windows)] 29 | use std::sync::atomic::Ordering; 30 | 31 | use clap::Command; 32 | use hid_io_core::api; 33 | use hid_io_core::built_info; 34 | use hid_io_core::device; 35 | use hid_io_core::logging; 36 | use hid_io_core::mailbox; 37 | use hid_io_core::module; 38 | use std::sync::Arc; 39 | 40 | #[cfg(windows)] 41 | fn main() -> Result<(), std::io::Error> { 42 | let args: Vec<_> = std::env::args().collect(); 43 | if args.len() > 1 && args[1] == "-d" { 44 | info!("-------------------------- HID-IO Core starting! --------------------------"); 45 | match service::run() { 46 | Ok(_) => (), 47 | Err(_e) => panic!("Service failed"), 48 | } 49 | } else { 50 | logging::setup_logging()?; 51 | start(); 52 | } 53 | Ok(()) 54 | } 55 | 56 | #[cfg(not(windows))] 57 | fn main() -> Result<(), std::io::Error> { 58 | logging::setup_logging()?; 59 | start(); 60 | Ok(()) 61 | } 62 | 63 | /// Main entry point 64 | fn start() { 65 | let rt: Arc = Arc::new( 66 | tokio::runtime::Builder::new_current_thread() 67 | .enable_all() 68 | .build() 69 | .unwrap(), 70 | ); 71 | 72 | rt.block_on(async { 73 | let version_info = format!( 74 | "{}{} - {}", 75 | built_info::PKG_VERSION, 76 | built_info::GIT_VERSION.map_or_else(|| "".to_owned(), |v| format!(" (git {v})")), 77 | built_info::PROFILE, 78 | ); 79 | info!("Version: {}", version_info); 80 | let after_info = format!( 81 | "{} ({}) -> {} ({})", 82 | built_info::RUSTC_VERSION, 83 | built_info::HOST, 84 | built_info::TARGET, 85 | built_info::BUILT_TIME_UTC, 86 | ); 87 | info!("Build: {}", after_info); 88 | 89 | // Process command-line arguments 90 | // Most of the information is generated from Cargo.toml using built crate (build.rs) 91 | Command::new(built_info::PKG_NAME.to_string()) 92 | .version(version_info.as_str()) 93 | .author(built_info::PKG_AUTHORS) 94 | .about(format!("\n{}", built_info::PKG_DESCRIPTION).as_str()) 95 | .after_help(after_info.as_str()) 96 | .get_matches(); 97 | 98 | // Start initialization 99 | info!("Initializing HID-IO daemon..."); 100 | 101 | // Setup mailbox 102 | let mailbox = mailbox::Mailbox::new(rt.clone()); 103 | 104 | // Wait until completion 105 | let (_, _, _) = tokio::join!( 106 | // Initialize Modules 107 | module::initialize(mailbox.clone()), 108 | // Initialize Device monitoring 109 | device::initialize(mailbox.clone()), 110 | // Initialize Cap'n'Proto API Server 111 | api::initialize(mailbox), 112 | ); 113 | 114 | info!("-------------------------- HID-IO Core exiting! --------------------------"); 115 | }); 116 | } 117 | 118 | #[cfg(windows)] 119 | fn stop() { 120 | info!("Stopping!"); 121 | let r = RUNNING.clone(); 122 | r.store(false, Ordering::SeqCst); 123 | } 124 | 125 | #[cfg(windows)] 126 | mod service { 127 | use flexi_logger::{opt_format, FileSpec, Logger}; 128 | use hid_io_core::built_info; 129 | use windows_service::service_dispatcher; 130 | 131 | const SERVICE_NAME: &str = built_info::PKG_NAME; 132 | 133 | // Generate the windows service boilerplate. 134 | // The boilerplate contains the low-level service entry function (ffi_service_main) that parses 135 | // incoming service arguments into Vec and passes them to user defined service 136 | // entry (my_service_main). 137 | define_windows_service!(ffi_service_main, my_service_main); 138 | 139 | use std::ffi::OsString; 140 | use std::time::Duration; 141 | use windows_service::service::{ 142 | ServiceControl, ServiceControlAccept, ServiceExitCode, ServiceState, ServiceStatus, 143 | ServiceType, 144 | }; 145 | use windows_service::service_control_handler::{self, ServiceControlHandlerResult}; 146 | 147 | pub fn run() -> windows_service::Result<()> { 148 | // Register generated `ffi_service_main` with the system and start the service, blocking 149 | // this thread until the service is stopped. 150 | service_dispatcher::start(SERVICE_NAME, ffi_service_main) 151 | } 152 | 153 | fn my_service_main(arguments: Vec) { 154 | Logger::try_with_env() 155 | .unwrap() 156 | .log_to_file(FileSpec::default().directory("log_files")) 157 | .format(opt_format) 158 | .start() 159 | .unwrap_or_else(|e| panic!("Logger initialization failed {}", e)); 160 | info!("Running as service!"); 161 | 162 | if let Err(_e) = run_service(arguments) { 163 | // Handle error in some way. 164 | } 165 | } 166 | 167 | fn run_service(_arguments: Vec) -> windows_service::Result<()> { 168 | let event_handler = move |control_event| -> ServiceControlHandlerResult { 169 | info!("EVENT: {:?}", control_event); 170 | match control_event { 171 | ServiceControl::Stop => { 172 | crate::stop(); 173 | ServiceControlHandlerResult::NoError 174 | } 175 | ServiceControl::Interrogate => ServiceControlHandlerResult::NoError, 176 | _ => ServiceControlHandlerResult::NotImplemented, 177 | } 178 | }; 179 | 180 | // Register system service event handler 181 | let status_handle = service_control_handler::register(SERVICE_NAME, event_handler)?; 182 | 183 | let next_status = ServiceStatus { 184 | // Should match the one from system service registry 185 | service_type: ServiceType::OWN_PROCESS, 186 | // The new state 187 | current_state: ServiceState::Running, 188 | // Accept stop events when running 189 | controls_accepted: ServiceControlAccept::STOP, 190 | // Used to report an error when starting or stopping only, otherwise must be zero 191 | exit_code: ServiceExitCode::Win32(0), 192 | // Only used for pending states, otherwise must be zero 193 | checkpoint: 0, 194 | // Only used for pending states, otherwise must be zero 195 | wait_hint: Duration::default(), 196 | // Process ID of the service (only used when querying the service) 197 | // TODO Is this correct? 198 | process_id: None, 199 | }; 200 | 201 | // Tell the system that the service is running now 202 | status_handle.set_service_status(next_status)?; 203 | 204 | crate::start(); 205 | 206 | // Tell the system that service has stopped. 207 | status_handle.set_service_status(ServiceStatus { 208 | service_type: ServiceType::OWN_PROCESS, 209 | current_state: ServiceState::Stopped, 210 | controls_accepted: ServiceControlAccept::empty(), 211 | exit_code: ServiceExitCode::Win32(0), 212 | checkpoint: 0, 213 | wait_hint: Duration::default(), 214 | process_id: None, // TODO is this correct? 215 | })?; 216 | 217 | Ok(()) 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /src/bin/install_service.rs: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2019-2020 by Jacob Alexander 2 | * Copyright (C) 2019 by Rowan Decker 3 | * 4 | * This file is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This file is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this file. If not, see . 16 | */ 17 | 18 | #[cfg(windows)] 19 | extern crate windows_service; 20 | 21 | #[cfg(windows)] 22 | use hid_io_core::built_info; 23 | 24 | #[cfg(windows)] 25 | const SERVICE_NAME: &str = built_info::PKG_NAME; 26 | 27 | #[cfg(windows)] 28 | fn main() -> windows_service::Result<()> { 29 | use std::ffi::OsString; 30 | use windows_service::service::{ 31 | ServiceAccess, ServiceErrorControl, ServiceInfo, ServiceStartType, ServiceType, 32 | }; 33 | use windows_service::service_manager::{ServiceManager, ServiceManagerAccess}; 34 | 35 | let manager_access = ServiceManagerAccess::CONNECT | ServiceManagerAccess::CREATE_SERVICE; 36 | let service_manager = ServiceManager::local_computer(None::<&str>, manager_access)?; 37 | 38 | let service_binary_path = ::std::env::current_exe() 39 | .unwrap() 40 | .with_file_name("hid-io-core.exe"); 41 | 42 | let service_info = ServiceInfo { 43 | name: OsString::from(SERVICE_NAME), 44 | display_name: OsString::from(format!("{} service", built_info::PKG_NAME)), 45 | service_type: ServiceType::OWN_PROCESS, 46 | start_type: ServiceStartType::AutoStart, //ServiceStartType::OnDemand, 47 | error_control: ServiceErrorControl::Normal, 48 | executable_path: service_binary_path, 49 | launch_arguments: vec!["-d".into()], 50 | account_name: None, // run as System 51 | account_password: None, 52 | dependencies: vec![], 53 | }; 54 | let _service = service_manager.create_service(&service_info, ServiceAccess::empty())?; 55 | Ok(()) 56 | } 57 | 58 | #[cfg(not(windows))] 59 | fn main() { 60 | panic!("This program is only intended to run on Windows."); 61 | } 62 | -------------------------------------------------------------------------------- /src/device/mod.rs: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2017-2021 by Jacob Alexander 2 | * 3 | * This file is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * 8 | * This file is distributed in the hope that it will be useful, 9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | * GNU General Public License for more details. 12 | * 13 | * You should have received a copy of the GNU General Public License 14 | * along with this file. If not, see . 15 | */ 16 | 17 | pub mod evdev; 18 | pub mod hidapi; 19 | 20 | /// Handles hidapi devices 21 | /// 22 | /// Works with both USB and BLE HID devices 23 | use crate::mailbox; 24 | use hid_io_protocol::*; 25 | use std::io::{Read, Write}; 26 | use std::time::Instant; 27 | use tokio::sync::broadcast; 28 | 29 | /// A duplex stream for HidIo to communicate over 30 | pub trait HidIoTransport: Read + Write {} 31 | 32 | const MAX_RECV_SIZE: usize = 1024; 33 | 34 | /// A raw transport plus any associated metadata 35 | /// 36 | /// Contains helpers to encode/decode HidIo packets 37 | pub struct HidIoEndpoint { 38 | socket: Box, 39 | max_packet_len: u32, 40 | } 41 | 42 | impl HidIoEndpoint { 43 | pub fn new(socket: Box, max_packet_len: u32) -> HidIoEndpoint { 44 | HidIoEndpoint { 45 | socket, 46 | max_packet_len, 47 | } 48 | } 49 | 50 | pub fn recv_chunk( 51 | &mut self, 52 | buffer: &mut mailbox::HidIoPacketBuffer, 53 | ) -> Result { 54 | let mut rbuf = [0; MAX_RECV_SIZE]; 55 | match self.socket.read(&mut rbuf) { 56 | Ok(len) => { 57 | if len > 0 { 58 | let slice = &rbuf[0..len]; 59 | let ret = buffer.decode_packet(slice); 60 | if let Err(e) = ret { 61 | error!("recv_chunk({}) {:?}", len, e); 62 | println!("received: {slice:?}"); 63 | println!("current state: {buffer:?}"); 64 | std::process::exit(2); 65 | } else { 66 | debug!("R{} {:x?}", buffer.data.len(), buffer); 67 | } 68 | } 69 | 70 | Ok(len) 71 | } 72 | Err(e) => Err(e), 73 | } 74 | } 75 | 76 | pub fn create_buffer(&self) -> mailbox::HidIoPacketBuffer { 77 | let mut buffer = HidIoPacketBuffer::new(); 78 | buffer.max_len = self.max_packet_len; 79 | buffer 80 | } 81 | 82 | pub fn send_packet( 83 | &mut self, 84 | packet: mailbox::HidIoPacketBuffer, 85 | ) -> Result<(), std::io::Error> { 86 | debug!( 87 | "Sending {:x?} len:{} chunk:{}", 88 | packet, 89 | packet.serialized_len(), 90 | self.max_packet_len 91 | ); 92 | let mut buf: Vec = Vec::new(); 93 | buf.resize_with(packet.serialized_len() as usize, Default::default); 94 | let buf = packet.serialize_buffer(&mut buf).unwrap().to_vec(); 95 | for chunk in buf 96 | .chunks(self.max_packet_len as usize) 97 | .collect::>() 98 | .iter() 99 | { 100 | let _i = self.socket.write(chunk)?; 101 | } 102 | Ok(()) 103 | } 104 | 105 | pub fn send_sync(&mut self) -> Result<(), std::io::Error> { 106 | self.send_packet(mailbox::HidIoPacketBuffer { 107 | ptype: HidIoPacketType::Sync, 108 | done: true, // Ready 109 | ..Default::default() 110 | }) 111 | } 112 | } 113 | 114 | /// A R/W channel for a single endpoint 115 | /// 116 | /// This provides an easy interface for other parts of the program to send/recv. 117 | /// messages from without having to worry about the underlying device type. 118 | /// It is responsible for managing the underlying acks/nacks, etc. 119 | /// Process must be continually called. 120 | pub struct HidIoController { 121 | mailbox: mailbox::Mailbox, 122 | uid: u64, 123 | device: HidIoEndpoint, 124 | received: mailbox::HidIoPacketBuffer, 125 | receiver: broadcast::Receiver, 126 | last_sync: Instant, 127 | } 128 | 129 | impl HidIoController { 130 | pub fn new(mailbox: mailbox::Mailbox, uid: u64, device: HidIoEndpoint) -> HidIoController { 131 | let received = device.create_buffer(); 132 | // Setup receiver so that it can queue up messages between processing loops 133 | let receiver = mailbox.sender.subscribe(); 134 | let last_sync = Instant::now(); 135 | HidIoController { 136 | mailbox, 137 | uid, 138 | device, 139 | received, 140 | receiver, 141 | last_sync, 142 | } 143 | } 144 | 145 | pub fn process(&mut self) -> Result { 146 | let mut io_events = 0; 147 | match self.device.recv_chunk(&mut self.received) { 148 | Ok(recv) => { 149 | if recv > 0 { 150 | io_events += 1; 151 | self.last_sync = Instant::now(); 152 | 153 | // Handle sync packets 154 | if let HidIoPacketType::Sync = &self.received.ptype { 155 | self.received = self.device.create_buffer(); 156 | } 157 | } 158 | } 159 | Err(e) => { 160 | return Err(e); 161 | } 162 | }; 163 | 164 | if self.received.done { 165 | // Send message to mailbox 166 | let src = mailbox::Address::DeviceHidio { uid: self.uid }; 167 | let dst = mailbox::Address::All; 168 | let msg = mailbox::Message::new(src, dst, self.received.clone()); 169 | self.mailbox.sender.send(msg).unwrap(); 170 | self.received = self.device.create_buffer(); 171 | } 172 | 173 | if self.last_sync.elapsed().as_secs() >= 5 { 174 | io_events += 1; 175 | if self.device.send_sync().is_err() { 176 | return Err(std::io::Error::new(std::io::ErrorKind::BrokenPipe, "")); 177 | }; 178 | self.received = self.device.create_buffer(); 179 | self.last_sync = Instant::now(); 180 | return Ok(io_events); 181 | } 182 | 183 | loop { 184 | match self.receiver.try_recv() { 185 | Ok(mut msg) => { 186 | // Only look at packets addressed to this endpoint 187 | if msg.dst == (mailbox::Address::DeviceHidio { uid: self.uid }) { 188 | msg.data.max_len = self.device.max_packet_len; 189 | self.device.send_packet(msg.data.clone())?; 190 | 191 | if msg.data.ptype == HidIoPacketType::Sync { 192 | self.received = self.device.create_buffer(); 193 | } 194 | } 195 | } 196 | Err(broadcast::error::TryRecvError::Empty) => { 197 | break; 198 | } 199 | Err(broadcast::error::TryRecvError::Lagged(_skipped)) => {} // TODO (HaaTa): Should probably warn if lagging 200 | Err(broadcast::error::TryRecvError::Closed) => { 201 | return Err(std::io::Error::new(std::io::ErrorKind::BrokenPipe, "")); 202 | } 203 | } 204 | } 205 | Ok(io_events) 206 | } 207 | } 208 | 209 | /// Supported Ids by this module 210 | /// recursive option applies supported ids from child modules as well 211 | #[allow(unused_variables)] 212 | #[cfg(target_os = "linux")] 213 | pub fn supported_ids(recursive: bool) -> Vec { 214 | #[cfg(feature = "dev-capture")] 215 | { 216 | let mut ids: Vec = vec![]; 217 | if recursive { 218 | ids.extend(evdev::supported_ids().iter().cloned()); 219 | } 220 | ids 221 | } 222 | 223 | #[cfg(not(feature = "dev-capture"))] 224 | vec![] 225 | } 226 | 227 | #[cfg(target_os = "macos")] 228 | pub fn supported_ids(_recursive: bool) -> Vec { 229 | vec![] 230 | } 231 | 232 | #[cfg(target_os = "windows")] 233 | pub fn supported_ids(_recursive: bool) -> Vec { 234 | vec![] 235 | } 236 | 237 | /// Module initialization 238 | /// 239 | /// # Remarks 240 | /// 241 | /// Sets up at least one thread per Device (using tokio). 242 | /// Each device is repsonsible for accepting and responding to packet requests. 243 | /// It is also possible to send requests asynchronously back to any Modules. 244 | /// Each device may have it's own RPC API. 245 | #[allow(unused_variables)] 246 | pub async fn initialize(mailbox: mailbox::Mailbox) { 247 | info!("Initializing devices..."); 248 | 249 | #[cfg(all(target_os = "linux", feature = "hidapi-devices"))] 250 | tokio::join!( 251 | // Initialize hidapi watcher 252 | hidapi::initialize(mailbox.clone()), 253 | // Initialize evdev watcher 254 | evdev::initialize(mailbox.clone()), 255 | ); 256 | 257 | // Initialize hidapi watcher 258 | #[cfg(all(target_os = "macos", feature = "hidapi-devices"))] 259 | hidapi::initialize(mailbox.clone()).await; 260 | 261 | // Initialize hidapi watcher 262 | #[cfg(all(target_os = "windows", feature = "hidapi-devices"))] 263 | hidapi::initialize(mailbox.clone()).await; 264 | } 265 | 266 | #[cfg(not(feature = "dev-capture"))] 267 | mod evdev { 268 | use crate::mailbox; 269 | 270 | #[allow(dead_code)] 271 | pub async fn initialize(_mailbox: mailbox::Mailbox) {} 272 | } 273 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2017-2023 by Jacob Alexander 2 | * 3 | * This file is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * 8 | * This file is distributed in the hope that it will be useful, 9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | * GNU General Public License for more details. 12 | * 13 | * You should have received a copy of the GNU General Public License 14 | * along with this file. If not, see . 15 | */ 16 | 17 | #![feature(extract_if)] 18 | 19 | // ----- Crates ----- 20 | 21 | #[macro_use] 22 | extern crate log; 23 | 24 | use std::sync::atomic::Ordering; 25 | 26 | pub use tokio; 27 | 28 | // ----- Modules ----- 29 | 30 | /// capnp interface for other programs to hook into 31 | pub mod api; 32 | 33 | /// communication with hidapi compatable devices 34 | pub mod device; 35 | 36 | /// logging functions 37 | pub mod logging; 38 | 39 | /// mpmc mailbox implementation for hid-io-core (e.g. packet broadcast with filters) 40 | pub mod mailbox; 41 | 42 | /// built-in features and command handlers 43 | pub mod module; 44 | 45 | /// Compile time information 46 | pub mod built_info { 47 | // This file is generated at build time using build.rs 48 | include!(concat!(env!("OUT_DIR"), "/built.rs")); 49 | } 50 | 51 | /// [AUTO GENERATED] 52 | #[cfg(feature = "api")] 53 | pub mod common_capnp { 54 | #![allow(clippy::all)] 55 | include!(concat!(env!("OUT_DIR"), "/common_capnp.rs")); 56 | } 57 | 58 | /// [AUTO GENERATED] 59 | #[cfg(feature = "api")] 60 | pub mod daemon_capnp { 61 | #![allow(clippy::all)] 62 | include!(concat!(env!("OUT_DIR"), "/daemon_capnp.rs")); 63 | } 64 | 65 | /// [AUTO GENERATED] 66 | #[cfg(feature = "api")] 67 | pub mod hidio_capnp { 68 | #![allow(clippy::all)] 69 | include!(concat!(env!("OUT_DIR"), "/hidio_capnp.rs")); 70 | } 71 | 72 | /// [AUTO GENERATED] 73 | #[cfg(feature = "api")] 74 | pub mod keyboard_capnp { 75 | #![allow(clippy::all)] 76 | include!(concat!(env!("OUT_DIR"), "/keyboard_capnp.rs")); 77 | } 78 | 79 | // ----- Functions ----- 80 | 81 | pub use hid_io_protocol::HidIoCommandId; 82 | use lazy_static::lazy_static; 83 | use std::sync::atomic::AtomicBool; 84 | use std::sync::Arc; 85 | 86 | lazy_static! { 87 | /// Any thread can set this to false to signal shutdown 88 | pub static ref RUNNING: Arc = Arc::new(AtomicBool::new(true)); 89 | } 90 | 91 | /// Supported Ids by hid-io-core 92 | /// This is used to determine all supported ids (always recursive). 93 | pub fn supported_ids() -> Vec { 94 | let mut ids = Vec::new(); 95 | 96 | ids.extend(api::supported_ids().iter().cloned()); 97 | ids.extend(device::supported_ids(true).iter().cloned()); 98 | ids.extend(module::supported_ids(true).iter().cloned()); 99 | 100 | // Sort, then deduplicate 101 | ids.sort_unstable(); 102 | ids.dedup(); 103 | 104 | ids 105 | } 106 | 107 | /// Main entry-point for the hid-io-core library 108 | pub async fn initialize(mailbox: mailbox::Mailbox) -> Result<(), std::io::Error> { 109 | // Setup signal handler 110 | let r = RUNNING.clone(); 111 | ctrlc::set_handler(move || { 112 | r.store(false, Ordering::SeqCst); 113 | }) 114 | .expect("Error setting Ctrl-C handler"); 115 | println!("Press Ctrl-C to exit..."); 116 | 117 | // Wait until completion 118 | let (_, _, _) = tokio::join!( 119 | // Initialize Modules 120 | module::initialize(mailbox.clone()), 121 | // Initialize Device monitoring 122 | device::initialize(mailbox.clone()), 123 | // Initialize Cap'n'Proto API Server 124 | api::initialize(mailbox), 125 | ); 126 | Ok(()) 127 | } 128 | -------------------------------------------------------------------------------- /src/logging.rs: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2020-2022 by Jacob Alexander 2 | * 3 | * This file is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * 8 | * This file is distributed in the hope that it will be useful, 9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | * GNU General Public License for more details. 12 | * 13 | * You should have received a copy of the GNU General Public License 14 | * along with this file. If not, see . 15 | */ 16 | 17 | /// Logging functions 18 | /// Handles general logging setup and other special functions 19 | use flexi_logger::{FileSpec, Logger}; 20 | use std::env; 21 | 22 | /// Logging setup 23 | pub fn setup_logging() -> Result<(), std::io::Error> { 24 | match Logger::try_with_env_or_str("") 25 | .unwrap() 26 | .log_to_file(FileSpec::default().directory(env::temp_dir())) 27 | //.format(flexi_logger::colored_detailed_format) 28 | .format(flexi_logger::colored_default_format) 29 | .format_for_files(flexi_logger::colored_detailed_format) 30 | .rotate( 31 | flexi_logger::Criterion::Size(1_000_000), 32 | flexi_logger::Naming::Numbers, 33 | flexi_logger::Cleanup::KeepLogFiles(5), 34 | ) 35 | .duplicate_to_stderr(flexi_logger::Duplicate::All) 36 | .start() 37 | { 38 | Err(msg) => Err(std::io::Error::new( 39 | std::io::ErrorKind::Other, 40 | format!("Could not start logger {msg}"), 41 | )), 42 | Ok(_) => { 43 | info!("-------------------------- HID-IO Core starting! --------------------------"); 44 | info!("Log location -> {:?}", env::temp_dir()); 45 | Ok(()) 46 | } 47 | } 48 | } 49 | 50 | /// Lite logging setup 51 | pub fn setup_logging_lite() -> Result<(), std::io::Error> { 52 | match Logger::try_with_env_or_str("") 53 | .unwrap() 54 | .format(flexi_logger::colored_default_format) 55 | .format_for_files(flexi_logger::colored_detailed_format) 56 | .duplicate_to_stderr(flexi_logger::Duplicate::All) 57 | .start() 58 | { 59 | Err(msg) => Err(std::io::Error::new( 60 | std::io::ErrorKind::Other, 61 | format!("Could not start logger {msg}"), 62 | )), 63 | Ok(_) => Ok(()), 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/module/daemonnode.rs: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2020 by Jacob Alexander 2 | * 3 | * This file is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * 8 | * This file is distributed in the hope that it will be useful, 9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | * GNU General Public License for more details. 12 | * 13 | * You should have received a copy of the GNU General Public License 14 | * along with this file. If not, see . 15 | */ 16 | 17 | use crate::api::common_capnp; 18 | /// HID-IO Core Daemon Node 19 | /// Handles API queries directly to HID-IO Core rather than to a specific device 20 | /// This is the standard way to interact with HID-IO Core modules from the capnp API 21 | /// 22 | /// For the most part, this is a dummy node used mainly for node accounting with the mailbox. 23 | /// The capnproto API should call the internal functions directly if possible. 24 | use crate::api::Endpoint; 25 | use crate::mailbox; 26 | 27 | pub struct DaemonNode { 28 | mailbox: mailbox::Mailbox, 29 | uid: u64, 30 | _endpoint: Endpoint, 31 | } 32 | 33 | impl DaemonNode { 34 | pub fn new(mailbox: mailbox::Mailbox) -> std::io::Result { 35 | // Assign a uid 36 | let uid = match mailbox.clone().assign_uid("".to_string(), "".to_string()) { 37 | Ok(uid) => uid, 38 | Err(e) => { 39 | panic!("Only 1 daemon node may be allocated: {e}"); 40 | } 41 | }; 42 | 43 | // Setup Endpoint 44 | let mut endpoint = Endpoint::new(common_capnp::NodeType::HidioDaemon, uid); 45 | endpoint.set_daemonnode_params(); 46 | 47 | // Register node 48 | mailbox.clone().register_node(endpoint.clone()); 49 | 50 | Ok(DaemonNode { 51 | mailbox, 52 | uid, 53 | _endpoint: endpoint, 54 | }) 55 | } 56 | } 57 | 58 | impl Drop for DaemonNode { 59 | fn drop(&mut self) { 60 | warn!("DaemonNode deallocated"); 61 | 62 | // Unregister node 63 | self.mailbox.unregister_node(self.uid); 64 | } 65 | } 66 | 67 | pub async fn initialize(mailbox: mailbox::Mailbox) { 68 | // Event loop for Daemon Node (typically not used) 69 | tokio::spawn(async { 70 | let node = DaemonNode::new(mailbox).unwrap(); 71 | info!("Initializing daemon node... uid:{}", node.uid); 72 | loop { 73 | tokio::time::sleep(std::time::Duration::from_millis(100)).await; 74 | } 75 | }); 76 | } 77 | -------------------------------------------------------------------------------- /src/module/displayserver/mod.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "displayserver")] 2 | /* Copyright (C) 2019-2022 by Jacob Alexander 3 | * Copyright (C) 2019 by Rowan Decker 4 | * 5 | * This file is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This file is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this file. If not, see . 17 | */ 18 | 19 | #[cfg(target_os = "linux")] 20 | /// Xorg impementation 21 | pub mod x11; 22 | 23 | #[cfg(target_os = "linux")] 24 | /// Wayland impementation 25 | pub mod wayland; 26 | 27 | #[cfg(target_os = "windows")] 28 | /// Winapi impementation 29 | pub mod winapi; 30 | 31 | #[cfg(target_os = "macos")] 32 | /// macOS quartz impementation 33 | pub mod quartz; 34 | 35 | use crate::mailbox; 36 | use crate::RUNNING; 37 | use hid_io_protocol::{HidIoCommandId, HidIoPacketType}; 38 | use std::string::FromUtf8Error; 39 | use std::sync::atomic::Ordering; 40 | use tokio_stream::{wrappers::BroadcastStream, StreamExt}; 41 | 42 | #[cfg(all(feature = "displayserver", target_os = "linux"))] 43 | use crate::module::displayserver::x11::*; 44 | 45 | #[cfg(all(feature = "displayserver", target_os = "linux"))] 46 | use crate::module::displayserver::wayland::*; 47 | 48 | #[cfg(all(feature = "displayserver", target_os = "windows"))] 49 | use crate::module::displayserver::winapi::*; 50 | 51 | #[cfg(all(feature = "displayserver", target_os = "macos"))] 52 | use crate::module::displayserver::quartz::*; 53 | 54 | /// Functions that can be called in a cross platform manner 55 | pub trait DisplayOutput { 56 | fn get_layout(&self) -> Result; 57 | fn set_layout(&self, layout: &str) -> Result<(), DisplayOutputError>; 58 | fn type_string(&mut self, string: &str) -> Result<(), DisplayOutputError>; 59 | fn press_symbol(&mut self, c: char, state: bool) -> Result<(), DisplayOutputError>; 60 | fn get_held(&mut self) -> Result, DisplayOutputError>; 61 | fn set_held(&mut self, string: &str) -> Result<(), DisplayOutputError>; 62 | } 63 | 64 | #[derive(Debug)] 65 | pub enum DisplayOutputError { 66 | AllocationFailed(char), 67 | Connection(String), 68 | Format(std::io::Error), 69 | General(String), 70 | LostConnection, 71 | NoKeycode, 72 | SetLayoutFailed(String), 73 | Unimplemented, 74 | Utf(FromUtf8Error), 75 | } 76 | 77 | impl std::fmt::Display for DisplayOutputError { 78 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 79 | match self { 80 | DisplayOutputError::AllocationFailed(e) => write!(f, "Allocation failed: {e}"), 81 | DisplayOutputError::Connection(e) => write!(f, "Connection: {e}"), 82 | DisplayOutputError::Format(e) => write!(f, "Format: {e}"), 83 | DisplayOutputError::General(e) => write!(f, "General: {e}"), 84 | DisplayOutputError::LostConnection => write!(f, "Lost connection"), 85 | DisplayOutputError::NoKeycode => write!(f, "No keycode mapped"), 86 | DisplayOutputError::SetLayoutFailed(e) => write!(f, "set_layout() failed: {e}"), 87 | DisplayOutputError::Unimplemented => write!(f, "Unimplemented"), 88 | DisplayOutputError::Utf(e) => write!(f, "UTF: {e}"), 89 | } 90 | } 91 | } 92 | 93 | impl From for DisplayOutputError { 94 | fn from(e: std::io::Error) -> Self { 95 | DisplayOutputError::Format(e) 96 | } 97 | } 98 | 99 | #[derive(Default)] 100 | /// Dummy impementation for unsupported platforms 101 | pub struct StubOutput {} 102 | 103 | impl StubOutput { 104 | pub fn new() -> StubOutput { 105 | StubOutput {} 106 | } 107 | } 108 | 109 | impl DisplayOutput for StubOutput { 110 | fn get_layout(&self) -> Result { 111 | warn!("Unimplemented"); 112 | Err(DisplayOutputError::Unimplemented) 113 | } 114 | fn set_layout(&self, _layout: &str) -> Result<(), DisplayOutputError> { 115 | warn!("Unimplemented"); 116 | Err(DisplayOutputError::Unimplemented) 117 | } 118 | fn type_string(&mut self, _string: &str) -> Result<(), DisplayOutputError> { 119 | warn!("Unimplemented"); 120 | Err(DisplayOutputError::Unimplemented) 121 | } 122 | fn press_symbol(&mut self, _c: char, _state: bool) -> Result<(), DisplayOutputError> { 123 | warn!("Unimplemented"); 124 | Err(DisplayOutputError::Unimplemented) 125 | } 126 | fn get_held(&mut self) -> Result, DisplayOutputError> { 127 | warn!("Unimplemented"); 128 | Err(DisplayOutputError::Unimplemented) 129 | } 130 | fn set_held(&mut self, _string: &str) -> Result<(), DisplayOutputError> { 131 | warn!("Unimplemented"); 132 | Err(DisplayOutputError::Unimplemented) 133 | } 134 | } 135 | 136 | /// Our "internal" node responsible for handling required commands 137 | struct Module { 138 | display: Box, 139 | } 140 | 141 | #[cfg(not(feature = "displayserver"))] 142 | fn get_display() -> Box { 143 | Box::new(StubOutput::new()) 144 | } 145 | 146 | #[cfg(all(feature = "displayserver", target_os = "linux"))] 147 | fn get_display() -> Box { 148 | // First attempt to connect to Wayland 149 | let wayland = WaylandConnection::new(); 150 | match wayland { 151 | Ok(wayland) => Box::new(wayland), 152 | Err(_) => Box::new(XConnection::new()), 153 | } 154 | } 155 | 156 | #[cfg(all(feature = "displayserver", target_os = "windows"))] 157 | fn get_display() -> Box { 158 | Box::new(DisplayConnection::new()) 159 | } 160 | 161 | #[cfg(all(feature = "displayserver", target_os = "macos"))] 162 | fn get_display() -> Box { 163 | Box::new(QuartzConnection::new()) 164 | } 165 | 166 | impl Module { 167 | fn new() -> Module { 168 | let connection = get_display(); 169 | 170 | match connection.get_layout() { 171 | Ok(layout) => { 172 | info!("Current layout: {}", layout); 173 | } 174 | Err(_) => { 175 | warn!("Failed to retrieve layout"); 176 | } 177 | } 178 | 179 | Module { 180 | display: connection, 181 | } 182 | } 183 | } 184 | 185 | /// Supported Ids by this module 186 | pub fn supported_ids() -> Vec { 187 | vec![ 188 | HidIoCommandId::UnicodeText, 189 | HidIoCommandId::UnicodeState, 190 | HidIoCommandId::GetInputLayout, 191 | HidIoCommandId::SetInputLayout, 192 | ] 193 | } 194 | 195 | async fn process(mailbox: mailbox::Mailbox) { 196 | // Top-level module setup 197 | let mut module = Module::new(); 198 | 199 | // Setup receiver stream 200 | let sender = mailbox.clone().sender.clone(); 201 | let receiver = sender.clone().subscribe(); 202 | tokio::pin! { 203 | let stream = BroadcastStream::new(receiver) 204 | .filter(Result::is_ok).map(Result::unwrap) 205 | .filter(|msg| msg.dst == mailbox::Address::Module) 206 | .filter(|msg| supported_ids().contains(&msg.data.id)) 207 | .filter(|msg| msg.data.ptype == HidIoPacketType::Data || msg.data.ptype == HidIoPacketType::NaData); 208 | } 209 | 210 | // Process filtered message stream 211 | while let Some(msg) = stream.next().await { 212 | let mydata = msg.data.data.clone(); 213 | debug!("Processing command: {:?}", msg.data.id); 214 | match msg.data.id { 215 | HidIoCommandId::UnicodeText => { 216 | let s = String::from_utf8(mydata.to_vec()).unwrap(); 217 | debug!("UnicodeText (start): {}", s); 218 | match module.display.type_string(&s) { 219 | Ok(_) => { 220 | msg.send_ack(sender.clone(), vec![]); 221 | } 222 | Err(_) => { 223 | warn!("Failed to type Unicode string"); 224 | msg.send_nak(sender.clone(), vec![]); 225 | } 226 | } 227 | debug!("UnicodeText (done): {}", s); 228 | } 229 | HidIoCommandId::UnicodeState => { 230 | let s = String::from_utf8(mydata.to_vec()).unwrap(); 231 | debug!("UnicodeState (start): {}", s); 232 | match module.display.set_held(&s) { 233 | Ok(_) => { 234 | msg.send_ack(sender.clone(), vec![]); 235 | } 236 | Err(_) => { 237 | warn!("Failed to set Unicode key"); 238 | msg.send_nak(sender.clone(), vec![]); 239 | } 240 | } 241 | debug!("UnicodeState (done): {}", s); 242 | } 243 | HidIoCommandId::GetInputLayout => { 244 | debug!("GetInputLayout (start)"); 245 | match module.display.get_layout() { 246 | Ok(layout) => { 247 | info!("Current layout: {}", layout); 248 | msg.send_ack(sender.clone(), layout.as_bytes().to_vec()); 249 | } 250 | Err(_) => { 251 | warn!("Failed to get input layout"); 252 | msg.send_nak(sender.clone(), vec![]); 253 | } 254 | } 255 | debug!("GetInputLayout (done)"); 256 | } 257 | HidIoCommandId::SetInputLayout => { 258 | let s = String::from_utf8(mydata.to_vec()).unwrap(); 259 | debug!("SetInputLayout (start): {}", s); 260 | /* TODO - Setting layout is more complicated for X11 (and Wayland) 261 | info!("Setting language to {}", s); 262 | msg.send_ack(sender.clone(), vec![]); 263 | */ 264 | warn!("Not implemented"); 265 | msg.send_nak(sender.clone(), vec![]); 266 | debug!("SetInputLayout (done): {}", s); 267 | } 268 | _ => {} 269 | } 270 | } 271 | } 272 | 273 | /// Display Server initialization 274 | /// The display server module selection the OS native display server to start. 275 | /// Depending on the native display server not all of the functionality may be available. 276 | pub async fn initialize(mailbox: mailbox::Mailbox) { 277 | // Setup local thread 278 | // This confusing block spawns a dedicated thread, and then runs a task LocalSet inside of it 279 | // This is required to avoid the use of the Send trait. 280 | // hid-io-core requires multiple threads like this which can dead-lock each other if run from 281 | // the same thread (which is the default behaviour of task LocalSet spawn_local) 282 | let rt = mailbox.rt.clone(); 283 | rt.clone() 284 | .spawn_blocking(move || { 285 | rt.block_on(async { 286 | let local = tokio::task::LocalSet::new(); 287 | local.spawn_local(process(mailbox)); 288 | 289 | // Wait for exit signal before cleaning up 290 | local 291 | .run_until(async move { 292 | loop { 293 | if !RUNNING.load(Ordering::SeqCst) { 294 | break; 295 | } 296 | tokio::time::sleep(std::time::Duration::from_millis(100)).await; 297 | } 298 | }) 299 | .await; 300 | }); 301 | }) 302 | .await 303 | .unwrap(); 304 | } 305 | -------------------------------------------------------------------------------- /src/module/displayserver/quartz.rs: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2019-2020 by Jacob Alexander 2 | * Copyright (C) 2019 by Rowan Decker 3 | * 4 | * This file is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This file is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this file. If not, see . 16 | */ 17 | 18 | use std::collections::HashMap; 19 | 20 | use core_graphics::event::CGEvent; 21 | use core_graphics::event_source::CGEventSource; 22 | use core_graphics::event_source::CGEventSourceStateID::HIDSystemState; 23 | 24 | use crate::module::displayserver::{DisplayOutput, DisplayOutputError}; 25 | 26 | #[allow(dead_code)] 27 | pub struct QuartzConnection { 28 | charmap: HashMap, 29 | held: Vec, 30 | } 31 | 32 | impl Default for QuartzConnection { 33 | fn default() -> Self { 34 | Self::new() 35 | } 36 | } 37 | 38 | impl QuartzConnection { 39 | pub fn new() -> QuartzConnection { 40 | let charmap = HashMap::new(); 41 | let held = Vec::new(); 42 | QuartzConnection { charmap, held } 43 | } 44 | 45 | pub fn press_key(&self, c: char, state: bool) { 46 | use core_graphics::event::CGEventTapLocation; 47 | let source = CGEventSource::new(HIDSystemState).unwrap(); 48 | 49 | let mut buf = [0; 2]; 50 | let event = CGEvent::new_keyboard_event(source, 0, state).unwrap(); 51 | event.set_string_from_utf16_unchecked(c.encode_utf16(&mut buf)); 52 | event.post(CGEventTapLocation::HID); 53 | } 54 | 55 | pub fn press_keycode(&self, keycode: core_graphics::event::CGKeyCode, state: bool) { 56 | use core_graphics::event::CGEventTapLocation; 57 | let source = CGEventSource::new(HIDSystemState).unwrap(); 58 | 59 | let event = CGEvent::new_keyboard_event(source, keycode, state).unwrap(); 60 | event.post(CGEventTapLocation::HID); 61 | } 62 | 63 | pub fn type_utf8(&self, string: &str) { 64 | use core_graphics::event::CGEventTapLocation; 65 | let source = CGEventSource::new(HIDSystemState).unwrap(); 66 | 67 | // Press 68 | let event = CGEvent::new_keyboard_event(source.clone(), 0, true).unwrap(); 69 | event.set_string(string); 70 | event.post(CGEventTapLocation::HID); 71 | 72 | // Release 73 | let event = CGEvent::new_keyboard_event(source, 0, false).unwrap(); 74 | event.set_string(string); 75 | event.post(CGEventTapLocation::HID); 76 | } 77 | } 78 | 79 | impl Drop for QuartzConnection { 80 | fn drop(&mut self) { 81 | info!("Releasing all keys"); 82 | for c in &self.held.clone() { 83 | self.press_symbol(*c, false).unwrap(); 84 | } 85 | } 86 | } 87 | 88 | impl DisplayOutput for QuartzConnection { 89 | fn get_layout(&self) -> Result { 90 | warn!("Unimplemented"); 91 | Err(DisplayOutputError::Unimplemented) 92 | } 93 | 94 | fn set_layout(&self, _layout: &str) -> Result<(), DisplayOutputError> { 95 | warn!("Unimplemented"); 96 | Err(DisplayOutputError::Unimplemented) 97 | } 98 | 99 | /// Types a UTF-8 string into the focused window 100 | /// Will handle special characters \n and \t to be Return and Tab respectively 101 | fn type_string(&mut self, string: &str) -> Result<(), DisplayOutputError> { 102 | // Splice the string into chunks split by \n and \t which need to be handled 103 | // separately. macOS seems to handle double characters well with this method 104 | // so it's not necessary to take those into account (unlike x11). 105 | // Unfortunately, double UTF-8 emojis seem to die a horrible death so we need 106 | // to implement chunking anyways for double characters...oh well. 107 | let mut queue = vec![]; 108 | for c in string.chars() { 109 | match c { 110 | '\n' | '\t' => { 111 | // If there were characters before, print now 112 | if !queue.is_empty() { 113 | let chunk: String = queue.into_iter().collect(); 114 | self.type_utf8(&chunk); 115 | queue = vec![]; 116 | } 117 | 118 | // Lookup keycode 119 | let keycode = match c { 120 | '\n' => core_graphics::event::KeyCode::RETURN, 121 | '\t' => core_graphics::event::KeyCode::TAB, 122 | _ => { 123 | continue; 124 | } 125 | }; 126 | 127 | // Press/release special key 128 | self.press_keycode(keycode, true); 129 | self.press_keycode(keycode, false); 130 | } 131 | _ => { 132 | // Check if we've already queued up this symbol 133 | // Push the current queue if there's a duplicate 134 | if queue.contains(&c) { 135 | let chunk: String = queue.into_iter().collect(); 136 | self.type_utf8(&chunk); 137 | queue = vec![]; 138 | } 139 | queue.push(c); 140 | } 141 | } 142 | } 143 | 144 | // Print final chunk of string 145 | if !queue.is_empty() { 146 | let chunk: String = queue.into_iter().collect(); 147 | self.type_utf8(&chunk); 148 | } 149 | 150 | Ok(()) 151 | } 152 | 153 | fn press_symbol(&mut self, c: char, press: bool) -> Result<(), DisplayOutputError> { 154 | self.press_key(c, press); 155 | Ok(()) 156 | } 157 | 158 | fn get_held(&mut self) -> Result, DisplayOutputError> { 159 | Ok(self.held.clone()) 160 | } 161 | 162 | fn set_held(&mut self, string: &str) -> Result<(), DisplayOutputError> { 163 | // TODO (HaaTa) 164 | // - Get system repeat rate 165 | // - Use an event thread to wake-up per 166 | // * Initial repeat 167 | // * Repeat speed 168 | // - Send most recently held key 169 | let s: Vec = string.chars().collect(); 170 | for c in &self.held.clone() { 171 | if !s.contains(c) { 172 | self.press_symbol(*c, false).unwrap(); 173 | } 174 | } 175 | for c in &s { 176 | self.press_symbol(*c, true).unwrap(); 177 | } 178 | 179 | Ok(()) 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/module/displayserver/winapi.rs: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2019-2020 by Jacob Alexander 2 | * Copyright (C) 2019 by Rowan Decker 3 | * 4 | * This file is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This file is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this file. If not, see . 16 | */ 17 | 18 | use std::collections::HashMap; 19 | use std::mem::size_of; 20 | use std::process::Command; 21 | 22 | use crate::module::displayserver::{DisplayOutput, DisplayOutputError}; 23 | 24 | use winapi::ctypes::c_int; 25 | use winapi::um::winuser; 26 | 27 | #[allow(dead_code)] 28 | pub struct DisplayConnection { 29 | charmap: HashMap, 30 | held: Vec, 31 | } 32 | 33 | impl Default for DisplayConnection { 34 | fn default() -> Self { 35 | Self::new() 36 | } 37 | } 38 | 39 | impl DisplayConnection { 40 | pub fn new() -> DisplayConnection { 41 | let charmap = HashMap::new(); 42 | let held = Vec::new(); 43 | DisplayConnection { charmap, held } 44 | } 45 | 46 | pub fn press_key(&self, c: char, state: bool) { 47 | let flags = if state { 48 | winuser::KEYEVENTF_UNICODE // Defaults to down 49 | } else { 50 | winuser::KEYEVENTF_UNICODE | winuser::KEYEVENTF_KEYUP 51 | }; 52 | 53 | // Handle converting UTF-8 to UTF-16 54 | // Since UTF-16 doesn't handle the longer 32-bit characters 55 | // the value is encoded into high and low surrogates and must 56 | // be sent in succession without a "keyup" 57 | // Sequence taken from enigo 58 | // (https://github.com/enigo-rs/enigo/blob/f8d6dea72d957c693ee65c3b6bf5b15afbc4858e/src/win/win_impl.rs#L109) 59 | let mut buffer = [0; 2]; 60 | let result = c.encode_utf16(&mut buffer); 61 | if result.len() == 1 { 62 | self.keyboard_event(flags, 0, result[0]); 63 | } else { 64 | for utf16_surrogate in result { 65 | self.keyboard_event(flags, 0, *utf16_surrogate); 66 | } 67 | } 68 | } 69 | 70 | /// Sends a keyboard event 71 | /// Used for Unicode in this module, but can be used for normal virtual keycodes and scancodes 72 | /// as well. 73 | fn keyboard_event(&self, flags: u32, vk: u16, scan: u16) { 74 | let mut event = winuser::INPUT { 75 | type_: winuser::INPUT_KEYBOARD, 76 | u: unsafe { 77 | std::mem::transmute_copy(&winuser::KEYBDINPUT { 78 | wVk: vk, // Virtual-Key Code (must be 0 when sending Unicode) 79 | wScan: scan, // Hardware scancode (set to utf16 value when Unicode, must send surrogate pairs separately if larger than u16) 80 | dwFlags: flags, 81 | // KEYEVENTF_EXTENDEDKEY 82 | // KEYEVENTF_KEYUP 83 | // KEYEVENTF_SCANCODE 84 | // KEYEVENTF_UNICODE 85 | // Unset 86 | time: 0, // TODO Can we manipulate this for lower latency? 87 | dwExtraInfo: 0, // ??? 88 | }) 89 | }, 90 | }; 91 | unsafe { 92 | winuser::SendInput( 93 | 1, 94 | &mut event as winuser::LPINPUT, 95 | size_of::() as c_int, 96 | ) 97 | }; 98 | } 99 | 100 | #[allow(dead_code)] 101 | /// Retrieves the keyboard delay from HKEY_CURRENT_USER\Control Panel\Keyboard\KeyboardDelay 102 | /// KeyboardDelay can be from 0-3 103 | /// 0 - 250 ms - Shortest 104 | /// 1 - 500 ms - Default 105 | /// 2 - 750 ms 106 | /// 3 - 1 s - Longest 107 | fn keyboard_delay(&self) -> std::io::Result { 108 | let hkcu = winreg::RegKey::predef(winreg::enums::HKEY_CURRENT_USER); 109 | let keyboard = hkcu.open_subkey("Control Panel\\Keyboard")?; 110 | let delay_val: String = keyboard.get_value("KeyboardDelay")?; 111 | let delay_val: u64 = delay_val.parse().unwrap(); 112 | Ok(std::time::Duration::from_millis(250 + delay_val * 250)) 113 | } 114 | 115 | #[allow(dead_code)] 116 | /// Retrieves the keyboard speed from HKEY_CURRENT_USER\Control Panel\Keyboard\KeyboardSpeed 117 | /// KeyboardSpeed can be from 0-31 118 | /// There are 32 levels (0-31) but the cps go from 2-30 (28 levels). 119 | /// This means that each setting level (28/31 == 0.9032258). We subtract 1 from 32 as we start 120 | /// at 2 cps on the first setting. 121 | /// Using microseconds to get more precision. 122 | /// 123 | /// 1_000_000 us / 2 cps + level * 8/7 = us per character 124 | /// 125 | /// cps - Characters per second 126 | /// 0 - 2 cps - 500_000 us - Slowest 127 | /// ... 128 | /// 31 - 30 cps - 33_000 us - Fastest - Default 129 | fn keyboard_speed(&self) -> std::io::Result { 130 | let hkcu = winreg::RegKey::predef(winreg::enums::HKEY_CURRENT_USER); 131 | let keyboard = hkcu.open_subkey("Control Panel\\Keyboard")?; 132 | let speed_val: String = keyboard.get_value("KeyboardSpeed")?; 133 | let speed_val: u64 = speed_val.parse().unwrap(); 134 | let us_delay = 1_000_000 / (2 + speed_val * 28 / 31); 135 | Ok(std::time::Duration::from_micros(us_delay)) 136 | } 137 | } 138 | 139 | impl Drop for DisplayConnection { 140 | fn drop(&mut self) { 141 | info!("Releasing all unicode keys"); 142 | self.set_held("").unwrap(); 143 | } 144 | } 145 | 146 | impl DisplayOutput for DisplayConnection { 147 | fn get_layout(&self) -> Result { 148 | let result = Command::new("powershell") 149 | .args(["-Command", "Get-WinUserLanguageList"]) 150 | .output() 151 | .expect("Failed to exec"); 152 | let output = String::from_utf8_lossy(&result.stdout); 153 | let mut map = output 154 | .lines() 155 | .filter(|l| l.contains(':')) 156 | .map(|l| l.split(':')) 157 | .map(|mut kv| (kv.next().unwrap().trim(), kv.next().unwrap().trim())); 158 | let layout = map 159 | .find(|(k, _): &(&str, &str)| *k == "LanguageTag") 160 | .map(|(_, v)| v) 161 | .unwrap_or(""); 162 | Ok(layout.to_string()) 163 | } 164 | 165 | fn set_layout(&self, layout: &str) -> Result<(), DisplayOutputError> { 166 | match Command::new("powershell") 167 | .args([ 168 | "-Command", 169 | &format!("Set-WinUserLanguageList -Force '{}'", &layout), 170 | ]) 171 | .output() 172 | { 173 | Ok(_) => Ok(()), 174 | Err(_e) => { 175 | error!("Could not set language"); 176 | Err(DisplayOutputError::SetLayoutFailed(layout.to_string())) 177 | } 178 | } 179 | } 180 | 181 | fn type_string(&mut self, string: &str) -> Result<(), DisplayOutputError> { 182 | for c in string.chars() { 183 | self.press_key(c, true); 184 | self.press_key(c, false); 185 | } 186 | Ok(()) 187 | } 188 | 189 | fn press_symbol(&mut self, c: char, press: bool) -> Result<(), DisplayOutputError> { 190 | self.press_key(c, press); 191 | 192 | if press { 193 | self.held.push(c); 194 | } else { 195 | self.held 196 | .iter() 197 | .position(|&x| x == c) 198 | .map(|e| self.held.remove(e)); 199 | } 200 | Ok(()) 201 | } 202 | 203 | fn get_held(&mut self) -> Result, DisplayOutputError> { 204 | Ok(self.held.clone()) 205 | } 206 | 207 | fn set_held(&mut self, string: &str) -> Result<(), DisplayOutputError> { 208 | let s: Vec = string.chars().collect(); 209 | for c in &self.held.clone() { 210 | if !s.contains(c) { 211 | match self.press_symbol(*c, false) { 212 | Ok(_) => {} 213 | Err(e) => { 214 | return Err(e); 215 | } 216 | } 217 | } 218 | } 219 | for c in &s { 220 | match self.press_symbol(*c, true) { 221 | Ok(_) => {} 222 | Err(e) => { 223 | return Err(e); 224 | } 225 | } 226 | } 227 | Ok(()) 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /src/module/mod.rs: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2017-2022 by Jacob Alexander 2 | * 3 | * This file is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * 8 | * This file is distributed in the hope that it will be useful, 9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | * GNU General Public License for more details. 12 | * 13 | * You should have received a copy of the GNU General Public License 14 | * along with this file. If not, see . 15 | */ 16 | 17 | /// Platform specific character output and IME control 18 | pub mod daemonnode; 19 | pub mod displayserver; 20 | pub mod vhid; 21 | 22 | use crate::api; 23 | use crate::built_info; 24 | use crate::device; 25 | use crate::mailbox; 26 | use hid_io_protocol::commands::*; 27 | use hid_io_protocol::{HidIoCommandId, HidIoPacketType}; 28 | use tokio_stream::{wrappers::BroadcastStream, StreamExt}; 29 | 30 | /// Max number of commands supported by this hid-io-core processor 31 | /// can be increased as necessary. 32 | const CMD_SIZE: usize = 200; 33 | 34 | /// hid-io-protocol CommandInterface for top-level module 35 | /// Used to serialize the Ack packets before sending them through the mailbox 36 | struct CommandInterface { 37 | src: mailbox::Address, 38 | dst: mailbox::Address, 39 | mailbox: mailbox::Mailbox, 40 | } 41 | 42 | impl 43 | Commands< 44 | { mailbox::HIDIO_PKT_BUF_DATA_SIZE }, 45 | { mailbox::HIDIO_PKT_BUF_DATA_SIZE - 1 }, 46 | { mailbox::HIDIO_PKT_BUF_DATA_SIZE - 2 }, 47 | { mailbox::HIDIO_PKT_BUF_DATA_SIZE - 4 }, 48 | CMD_SIZE, 49 | > for CommandInterface 50 | { 51 | fn tx_packetbuffer_send( 52 | &mut self, 53 | buf: &mut mailbox::HidIoPacketBuffer, 54 | ) -> Result<(), CommandError> { 55 | if let Some(rcvmsg) = self.mailbox.try_send_message(mailbox::Message { 56 | src: self.src, 57 | dst: self.dst, 58 | data: buf.clone(), 59 | })? { 60 | // Handle ack/nak 61 | self.rx_message_handling(rcvmsg.data)?; 62 | } 63 | Ok(()) 64 | } 65 | 66 | fn h0000_supported_ids_cmd( 67 | &mut self, 68 | _data: h0000::Cmd, 69 | ) -> Result, h0000::Nak> { 70 | let ids = heapless::Vec::from_slice(&crate::supported_ids()).unwrap(); 71 | Ok(h0000::Ack { ids }) 72 | } 73 | 74 | fn h0001_info_cmd( 75 | &mut self, 76 | data: h0001::Cmd, 77 | ) -> Result, h0001::Nak> { 78 | let mut ack = h0001::Ack::<{ mailbox::HIDIO_PKT_BUF_DATA_SIZE - 1 }> { 79 | property: data.property, 80 | os: h0001::OsType::Unknown, 81 | number: 0, 82 | string: heapless::String::from(""), 83 | }; 84 | match data.property { 85 | h0001::Property::MajorVersion => { 86 | ack.number = built_info::PKG_VERSION_MAJOR.parse::().unwrap(); 87 | } 88 | h0001::Property::MinorVersion => { 89 | ack.number = built_info::PKG_VERSION_MINOR.parse::().unwrap(); 90 | } 91 | h0001::Property::PatchVersion => { 92 | ack.number = built_info::PKG_VERSION_PATCH.parse::().unwrap(); 93 | } 94 | h0001::Property::OsType => { 95 | ack.os = match built_info::CFG_OS { 96 | "windows" => h0001::OsType::Windows, 97 | "macos" => h0001::OsType::MacOs, 98 | "ios" => h0001::OsType::Ios, 99 | "linux" => h0001::OsType::Linux, 100 | "android" => h0001::OsType::Android, 101 | "freebsd" => h0001::OsType::FreeBsd, 102 | "openbsd" => h0001::OsType::OpenBsd, 103 | "netbsd" => h0001::OsType::NetBsd, 104 | _ => h0001::OsType::Unknown, 105 | }; 106 | } 107 | h0001::Property::OsVersion => match sys_info::os_release() { 108 | Ok(version) => { 109 | ack.string = heapless::String::from(version.as_str()); 110 | } 111 | Err(e) => { 112 | error!("OS Release retrieval failed: {}", e); 113 | return Err(h0001::Nak { 114 | property: h0001::Property::OsVersion, 115 | }); 116 | } 117 | }, 118 | h0001::Property::HostSoftwareName => { 119 | ack.string = heapless::String::from(built_info::PKG_NAME); 120 | } 121 | _ => { 122 | return Err(h0001::Nak { 123 | property: h0001::Property::Unknown, 124 | }); 125 | } 126 | } 127 | Ok(ack) 128 | } 129 | 130 | fn h0030_openurl_cmd( 131 | &mut self, 132 | data: h0030::Cmd<{ mailbox::HIDIO_PKT_BUF_DATA_SIZE }>, 133 | ) -> Result { 134 | debug!("Open url: {}", data.url); 135 | let url = String::from(data.url.as_str()); 136 | if let Err(err) = open::that(url.clone()) { 137 | error!("Failed to open url: {:?} - {:?}", url, err); 138 | Err(h0030::Nak {}) 139 | } else { 140 | Ok(h0030::Ack {}) 141 | } 142 | } 143 | } 144 | 145 | /// Supported Ids by this module 146 | /// recursive option applies supported ids from child modules as well 147 | pub fn supported_ids(recursive: bool) -> Vec { 148 | let mut ids = vec![ 149 | HidIoCommandId::GetInfo, 150 | HidIoCommandId::OpenUrl, 151 | HidIoCommandId::SupportedIds, 152 | ]; 153 | if recursive { 154 | ids.extend(displayserver::supported_ids().iter().cloned()); 155 | } 156 | ids 157 | } 158 | 159 | /// Device initialization 160 | /// Sets up a scanning thread per Device type (using tokio). 161 | /// Each scanning thread will create a new thread per device found. 162 | /// The scanning thread is required in case devices are plugged/unplugged while running. 163 | /// If a device is unplugged, the Device thread will exit. 164 | pub async fn initialize(mailbox: mailbox::Mailbox) { 165 | info!("Initializing modules..."); 166 | 167 | // Setup local thread 168 | // Due to some of the setup in the Module struct we need to run processing in the same local 169 | // thread. 170 | let mailbox1 = mailbox.clone(); 171 | let data = tokio::spawn(async move { 172 | // Setup receiver stream 173 | let sender = mailbox1.clone().sender.clone(); 174 | let receiver = sender.clone().subscribe(); 175 | tokio::pin! { 176 | let stream = BroadcastStream::new(receiver) 177 | .filter(Result::is_ok).map(Result::unwrap) 178 | .take_while(|msg| 179 | msg.src != mailbox::Address::DropSubscription && 180 | msg.dst != mailbox::Address::CancelAllSubscriptions 181 | ) 182 | .filter(|msg| msg.dst == mailbox::Address::Module || msg.dst == mailbox::Address::All) 183 | .filter(|msg| supported_ids(false).contains(&msg.data.id)) 184 | .filter(|msg| msg.data.ptype == HidIoPacketType::Data || msg.data.ptype == HidIoPacketType::NaData); 185 | } 186 | 187 | // Process filtered message stream 188 | while let Some(msg) = stream.next().await { 189 | // Process buffer using hid-io-protocol 190 | let mut intf = CommandInterface { 191 | src: msg.dst, // Replying to message 192 | dst: msg.src, // Replying to message 193 | mailbox: mailbox1.clone(), 194 | }; 195 | if let Err(err) = intf.rx_message_handling(msg.clone().data) { 196 | warn!("Failed to process({:?}): {:?}", err, msg); 197 | } 198 | } 199 | }); 200 | 201 | // NAK unsupported command ids 202 | let mailbox2 = mailbox.clone(); 203 | let naks = tokio::spawn(async move { 204 | // Setup receiver stream 205 | let sender = mailbox2.clone().sender.clone(); 206 | let receiver = sender.clone().subscribe(); 207 | tokio::pin! { 208 | let stream = BroadcastStream::new(receiver) 209 | .filter(Result::is_ok).map(Result::unwrap) 210 | .take_while(|msg| 211 | msg.src != mailbox::Address::DropSubscription && 212 | msg.dst != mailbox::Address::CancelAllSubscriptions 213 | ) 214 | .filter(|msg| !( 215 | supported_ids(true).contains(&msg.data.id) || 216 | api::supported_ids().contains(&msg.data.id) || 217 | device::supported_ids(true).contains(&msg.data.id) 218 | )) 219 | .filter(|msg| msg.data.ptype == HidIoPacketType::Data || msg.data.ptype == HidIoPacketType::NaData); 220 | } 221 | 222 | // Process filtered message stream 223 | while let Some(msg) = stream.next().await { 224 | warn!("Unknown command ID: {:?} ({})", msg.data.id, msg.data.ptype); 225 | // Only send NAK with Data packets (NaData packets don't have acknowledgements, so just 226 | // warn) 227 | if msg.data.ptype == HidIoPacketType::Data { 228 | msg.send_nak(sender.clone(), vec![]); 229 | } 230 | } 231 | }); 232 | 233 | let (_, _, _, _, _) = tokio::join!( 234 | daemonnode::initialize(mailbox.clone()), 235 | displayserver::initialize(mailbox.clone()), 236 | naks, 237 | data, 238 | vhid::initialize(mailbox.clone()), 239 | ); 240 | } 241 | 242 | /// Used when displayserver feature is disabled 243 | #[cfg(not(feature = "displayserver"))] 244 | mod displayserver { 245 | use crate::mailbox; 246 | use hid_io_protocol::HidIoCommandId; 247 | 248 | pub async fn initialize(_mailbox: mailbox::Mailbox) {} 249 | pub fn supported_ids() -> Vec { 250 | vec![] 251 | } 252 | } 253 | 254 | /// Used when displayserver feature is disabled 255 | #[cfg(not(feature = "dev-capture"))] 256 | mod vhid { 257 | use crate::mailbox; 258 | 259 | pub async fn initialize(_mailbox: mailbox::Mailbox) {} 260 | } 261 | -------------------------------------------------------------------------------- /src/packet.rs: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2017 by Jacob Alexander 2 | * 3 | * This file is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * 8 | * This file is distributed in the hope that it will be useful, 9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | * GNU General Public License for more details. 12 | * 13 | * You should have received a copy of the GNU General Public License 14 | * along with this file. If not, see . 15 | */ 16 | 17 | mod module; 18 | mod device; 19 | 20 | 21 | /// HostPackets originate from the host and are sent to the device 22 | enum HostPacket { 23 | } 24 | 25 | /// DevicePackets originate from the device and are sent to the host 26 | enum DevicePacket { 27 | } 28 | 29 | /// HostPacketMsgs originate from the host and are sent to the device 30 | struct HostPacketMsg { 31 | packet : HostPacket, 32 | module : module::ModuleInfo, 33 | device : device::hidusb::hidapi::HidDeviceInfo, 34 | } 35 | 36 | /// DevicePacketMsgs originate from the device and are sent to the host 37 | struct DevicePacketMsg { 38 | packet : DevicePacket, 39 | module : module::ModuleInfo, 40 | device : device::hidusb::hidapi::HidDeviceInfo, 41 | } 42 | 43 | struct PacketMsg2 { 44 | pid : u32, // Packet id 45 | plength : u32, // Packet data length 46 | ptype : u8, // enum? Packet type 47 | ppayload : Vec, // Packet payload (entire payload, combines continued packets) 48 | } 49 | 50 | 51 | -------------------------------------------------------------------------------- /uninstall_service.bat: -------------------------------------------------------------------------------- 1 | sc delete hid-io 2 | -------------------------------------------------------------------------------- /vt.sh: -------------------------------------------------------------------------------- 1 | export RUST_LOG="" 2 | stty_orig=`stty -g` 3 | stty -echo -icanon min 1 time 0 4 | cargo run --example rpc 5 | stty $stty_orig 6 | --------------------------------------------------------------------------------