├── .cargo └── config.toml ├── .github └── workflows │ ├── ci.yml │ ├── doc.yml │ ├── rktk-client.yml │ ├── setup-pnpm │ └── action.yaml │ ├── setup-rust │ └── action.yaml │ └── site.yml ├── .gitignore ├── .vscode ├── settings.json └── tasks.json ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── biome.json ├── crates ├── .cargo │ └── config.toml ├── kmsm-rktk │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── kmsm │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── interface │ │ ├── mod.rs │ │ └── state.rs │ │ ├── keycode │ │ ├── key.rs │ │ ├── layer.rs │ │ ├── media.rs │ │ ├── mod.rs │ │ ├── modifier.rs │ │ ├── mouse.rs │ │ ├── special.rs │ │ └── utils.rs │ │ ├── keymap.rs │ │ ├── lib.rs │ │ ├── macros.rs │ │ ├── state │ │ ├── hid_report.rs │ │ ├── key_resolver │ │ │ ├── combo.rs │ │ │ ├── mod.rs │ │ │ ├── normal.rs │ │ │ ├── oneshot.rs │ │ │ ├── tap_dance.rs │ │ │ └── tap_hold.rs │ │ ├── mod.rs │ │ ├── shared.rs │ │ ├── tests │ │ │ ├── action.rs │ │ │ ├── action │ │ │ │ └── tap_dance.rs │ │ │ ├── basic.rs │ │ │ ├── combo.rs │ │ │ ├── encoder.rs │ │ │ ├── keycode.rs │ │ │ ├── keymap.rs │ │ │ ├── layer.rs │ │ │ ├── mod.rs │ │ │ ├── mouse.rs │ │ │ └── special.rs │ │ └── updater │ │ │ ├── layer.rs │ │ │ ├── mod.rs │ │ │ └── mouse │ │ │ ├── aml.rs │ │ │ └── mod.rs │ │ └── time.rs ├── rktk-client │ ├── .gitignore │ ├── Cargo.toml │ ├── Dioxus.toml │ ├── assets │ │ ├── .gitignore │ │ └── favicon.ico │ ├── build.rs │ ├── index.html │ ├── input.css │ ├── package.json │ └── src │ │ ├── app │ │ ├── cache.rs │ │ ├── components │ │ │ ├── mod.rs │ │ │ ├── notification.rs │ │ │ ├── selector │ │ │ │ ├── key.rs │ │ │ │ ├── key_action.rs │ │ │ │ ├── key_code.rs │ │ │ │ └── mod.rs │ │ │ └── topbar.rs │ │ ├── disconnect.rs │ │ ├── mod.rs │ │ ├── page │ │ │ ├── connect.rs │ │ │ ├── connected │ │ │ │ ├── config.rs │ │ │ │ ├── log.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── remap.rs │ │ │ │ └── remap │ │ │ │ │ ├── bar.rs │ │ │ │ │ ├── fetcher.rs │ │ │ │ │ └── keyboard.rs │ │ │ └── mod.rs │ │ └── state.rs │ │ ├── backend │ │ ├── mod.rs │ │ ├── native.rs │ │ └── web.rs │ │ ├── main.rs │ │ └── utils.rs ├── rktk-defmt-print │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── rktk-drivers-common │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── debounce.rs │ │ ├── display │ │ ├── mod.rs │ │ └── ssd1306.rs │ │ ├── encoder.rs │ │ ├── keyscan │ │ ├── duplex_matrix.rs │ │ ├── flex_pin.rs │ │ ├── matrix.rs │ │ ├── mod.rs │ │ ├── pressed.rs │ │ └── shift_register_matrix.rs │ │ ├── lib.rs │ │ ├── mouse │ │ ├── mod.rs │ │ ├── paw3395 │ │ │ ├── config.rs │ │ │ ├── error.rs │ │ │ ├── mod.rs │ │ │ ├── power_up.rs │ │ │ └── registers.rs │ │ └── pmw3360 │ │ │ ├── error.rs │ │ │ ├── mod.rs │ │ │ ├── registers.rs │ │ │ ├── srom_liftoff.rs │ │ │ └── srom_tracking.rs │ │ ├── panic_utils.rs │ │ ├── storage │ │ ├── flash_sequential_map.rs │ │ └── mod.rs │ │ ├── trouble │ │ ├── mod.rs │ │ └── reporter │ │ │ ├── driver.rs │ │ │ ├── mod.rs │ │ │ ├── server │ │ │ ├── hid.rs │ │ │ └── mod.rs │ │ │ └── task.rs │ │ └── usb │ │ ├── builder.rs │ │ ├── defmt_logger │ │ ├── mod.rs │ │ └── task.rs │ │ ├── driver.rs │ │ ├── handler.rs │ │ ├── mod.rs │ │ ├── raw_hid.rs │ │ ├── rrp.rs │ │ └── task.rs ├── rktk-drivers-nrf │ ├── .cargo │ │ └── config.toml │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── display.rs │ │ ├── esb │ │ ├── dongle.rs │ │ ├── mod.rs │ │ └── reporter.rs │ │ ├── keyscan │ │ ├── flex_pin.rs │ │ └── mod.rs │ │ ├── lib.rs │ │ ├── mouse.rs │ │ ├── rgb │ │ ├── mod.rs │ │ └── ws2812_pwm.rs │ │ ├── sdc.rs │ │ ├── softdevice │ │ ├── ble │ │ │ ├── bonder.rs │ │ │ ├── bonder │ │ │ │ ├── storage.rs │ │ │ │ └── types.rs │ │ │ ├── constant.rs │ │ │ ├── driver.rs │ │ │ ├── mod.rs │ │ │ ├── server.rs │ │ │ ├── services │ │ │ │ ├── battery.rs │ │ │ │ ├── device_information.rs │ │ │ │ ├── hid.rs │ │ │ │ ├── hid │ │ │ │ │ └── descriptor.rs │ │ │ │ └── mod.rs │ │ │ └── task.rs │ │ ├── flash.rs │ │ ├── mod.rs │ │ └── split │ │ │ ├── central.rs │ │ │ ├── mod.rs │ │ │ ├── packet.rs │ │ │ └── peripheral.rs │ │ ├── split │ │ ├── mod.rs │ │ ├── uart_full_duplex.rs │ │ └── uart_half_duplex.rs │ │ └── system.rs ├── rktk-drivers-rp │ ├── .cargo │ │ └── config.toml │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── display.rs │ │ ├── flash.rs │ │ ├── keyscan │ │ ├── flex_pin.rs │ │ └── mod.rs │ │ ├── lib.rs │ │ ├── mouse.rs │ │ ├── rgb │ │ ├── mod.rs │ │ └── ws2812_pio.rs │ │ ├── split │ │ ├── mod.rs │ │ └── pio_half_duplex.rs │ │ └── system.rs ├── rktk-log │ ├── .gitignore │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── helper.rs │ │ ├── lib.rs │ │ └── macros.rs ├── rktk-rrp │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── client.rs │ │ ├── endpoints.rs │ │ ├── lib.rs │ │ ├── macros │ │ ├── client.rs │ │ ├── mod.rs │ │ └── server.rs │ │ ├── server.rs │ │ ├── tests │ │ ├── mod.rs │ │ ├── test_server.rs │ │ └── test_transport.rs │ │ └── transport │ │ ├── error.rs │ │ ├── mod.rs │ │ ├── read.rs │ │ └── write.rs ├── rktk.dev.json ├── rktk │ ├── Cargo.toml │ ├── build │ │ ├── main.rs │ │ └── schema │ │ │ ├── constant.rs │ │ │ ├── dynamic │ │ │ ├── key_manager.rs │ │ │ ├── keyboard.rs │ │ │ ├── mod.rs │ │ │ └── rktk.rs │ │ │ └── mod.rs │ ├── schema.json │ └── src │ │ ├── config │ │ ├── keymap.rs │ │ ├── mod.rs │ │ ├── rgb.rs │ │ └── storage │ │ │ ├── mod.rs │ │ │ ├── read.rs │ │ │ └── write.rs │ │ ├── dongle_task.rs │ │ ├── drivers │ │ ├── dummy.rs │ │ ├── interface │ │ │ ├── debounce.rs │ │ │ ├── display.rs │ │ │ ├── dongle.rs │ │ │ ├── encoder.rs │ │ │ ├── keyscan.rs │ │ │ ├── mod.rs │ │ │ ├── mouse.rs │ │ │ ├── reporter.rs │ │ │ ├── rgb.rs │ │ │ ├── split.rs │ │ │ ├── storage.rs │ │ │ ├── system.rs │ │ │ ├── usb.rs │ │ │ └── wireless.rs │ │ └── mod.rs │ │ ├── hooks │ │ ├── interface │ │ │ ├── dongle.rs │ │ │ ├── master.rs │ │ │ ├── mod.rs │ │ │ └── rgb.rs │ │ └── mod.rs │ │ ├── lib.rs │ │ ├── task │ │ ├── channels.rs │ │ ├── display │ │ │ ├── default_display.rs │ │ │ ├── default_display │ │ │ │ └── images.rs │ │ │ ├── mod.rs │ │ │ └── utils.rs │ │ ├── initializers.rs │ │ ├── logger.rs │ │ ├── master │ │ │ ├── handle_keyboard.rs │ │ │ ├── handle_mouse.rs │ │ │ ├── handle_slave.rs │ │ │ ├── mod.rs │ │ │ ├── report.rs │ │ │ ├── rrp_server.rs │ │ │ └── utils.rs │ │ ├── mod.rs │ │ ├── rgb.rs │ │ ├── slave.rs │ │ └── split_handler.rs │ │ └── utils.rs └── xtask │ ├── Cargo.toml │ ├── res │ ├── doc_after.html │ └── doc_before.html │ └── src │ ├── check.rs │ ├── config.rs │ ├── doc.rs │ ├── main.rs │ ├── publish.rs │ ├── stats.rs │ ├── test.rs │ └── utils.rs ├── crates_config.json ├── keyboards ├── .cargo │ └── config.toml ├── corne │ ├── .cargo │ │ └── config.toml │ ├── crates │ │ ├── corne-nrf │ │ │ ├── .cargo │ │ │ │ └── config.toml │ │ │ ├── Cargo.toml │ │ │ ├── build.rs │ │ │ ├── memory.x │ │ │ └── src │ │ │ │ ├── keymap.rs │ │ │ │ └── main.rs │ │ └── corne-rp │ │ │ ├── .cargo │ │ │ └── config.toml │ │ │ ├── Cargo.toml │ │ │ ├── README.md │ │ │ ├── build.rs │ │ │ ├── memory.x │ │ │ └── src │ │ │ ├── keymap.rs │ │ │ └── main.rs │ └── rktk.json ├── dummy │ ├── .cargo │ │ └── config.toml │ └── crates │ │ └── dummy-rp │ │ ├── .cargo │ │ └── config.toml │ │ ├── Cargo.toml │ │ └── src │ │ ├── keymap.rs │ │ └── main.rs ├── keyball61 │ ├── .cargo │ │ └── config.toml │ ├── crates │ │ ├── keyball61-common │ │ │ ├── Cargo.toml │ │ │ └── src │ │ │ │ └── lib.rs │ │ ├── keyball61-nrf │ │ │ ├── .cargo │ │ │ │ └── config.toml │ │ │ ├── Cargo.toml │ │ │ ├── build.rs │ │ │ ├── memory.x │ │ │ └── src │ │ │ │ ├── bin │ │ │ │ └── keyball61.rs │ │ │ │ └── lib.rs │ │ └── keyball61-rp │ │ │ ├── .cargo │ │ │ └── config.toml │ │ │ ├── Cargo.toml │ │ │ ├── build.rs │ │ │ ├── memory.x │ │ │ └── src │ │ │ ├── bin │ │ │ └── keyball61.rs │ │ │ └── lib.rs │ └── rktk.json └── neg │ ├── .cargo │ └── config.toml │ ├── crates │ └── neg-nrf │ │ ├── .cargo │ │ └── config.toml │ │ ├── Cargo.toml │ │ ├── build.rs │ │ ├── memory.x │ │ └── src │ │ ├── bin │ │ ├── neg_nrf_master.rs │ │ └── neg_nrf_slave.rs │ │ ├── common.rs │ │ ├── drivers.rs │ │ ├── lib.rs │ │ ├── master.rs │ │ ├── misc.rs │ │ └── slave.rs │ └── rktk.json ├── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── rust-toolchain.toml └── site ├── .gitignore ├── README.md ├── app ├── (home) │ ├── layout.tsx │ └── page.tsx ├── api │ └── search │ │ └── route.ts ├── docs │ ├── [[...slug]] │ │ └── page.tsx │ └── layout.tsx ├── global.css ├── layout.config.tsx ├── layout.tsx └── not-found.tsx ├── content └── docs │ ├── customize-keymap │ ├── action │ │ ├── oneshot.mdx │ │ └── tap-dance.mdx │ └── index.mdx │ ├── guide-driver.mdx │ ├── guide-first-keyboard.mdx │ ├── index.mdx │ ├── meta.json │ ├── reference-device │ ├── display.mdx │ ├── dongle.mdx │ ├── encoder.mdx │ ├── index.mdx │ ├── keyscan.mdx │ ├── mouse.mdx │ ├── reporter.mdx │ ├── split.mdx │ └── storage.mdx │ ├── reference-features │ ├── index.mdx │ └── rrp.mdx │ ├── reference-platform │ ├── index.mdx │ ├── nrf.mdx │ └── rp.mdx │ ├── reference-project-overview.mdx │ ├── reference-tips │ ├── index.mdx │ └── optimize-binary-size.mdx │ └── rktk-client.mdx ├── lib ├── remarkDirectiveDriverTable.ts └── source.ts ├── next.config.mjs ├── package.json ├── postcss.config.mjs ├── public └── logo.webp ├── source.config.ts └── tsconfig.json /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [profile] 2 | 3 | [profile.dioxus-wasm] 4 | inherits = "dev" 5 | opt-level = 2 6 | 7 | [profile.dioxus-server] 8 | inherits = "dev" 9 | 10 | [profile.dioxus-android] 11 | inherits = "dev" 12 | 13 | [profile.wasm-dev] 14 | inherits = "dev" 15 | opt-level = 1 16 | 17 | [profile.server-dev] 18 | inherits = "dev" 19 | 20 | [profile.android-dev] 21 | inherits = "dev" 22 | 23 | [alias] 24 | xtask = "run -p xtask --" 25 | 26 | [build] 27 | rustflags = ["--cfg=web_sys_unstable_apis"] 28 | 29 | [doc.extern-map.registries] 30 | crates-io = "https://docs.rs/" 31 | -------------------------------------------------------------------------------- /.github/workflows/doc.yml: -------------------------------------------------------------------------------- 1 | name: doc 2 | 3 | on: 4 | push: 5 | branches: ["master"] 6 | paths: 7 | - ".github/workflows/doc.yml" 8 | - ".github/workflows/setup-rust/action.yaml" 9 | - "crates/rktk/wrangler.jsonc" 10 | - "**.toml" 11 | - "**.rs" 12 | - "**.lock" 13 | - "**.md" 14 | workflow_dispatch: 15 | 16 | env: 17 | CARGO_TERM_COLOR: always 18 | CLICOLOR_FORCE: 1 19 | 20 | jobs: 21 | build-and-deploy-docs: 22 | name: Build and deploy rustdoc 23 | runs-on: ubuntu-latest 24 | steps: 25 | - name: Checkout 26 | uses: actions/checkout@v4 27 | 28 | - name: Setup rust 29 | uses: ./.github/workflows/setup-rust 30 | with: 31 | target: "thumbv6m-none-eabi,thumbv7em-none-eabihf" 32 | 33 | - uses: r7kamura/rust-problem-matchers@v1 34 | 35 | - name: Build docs 36 | run: cargo xtask doc 37 | 38 | - name: Setup pnpm 39 | uses: ./.github/workflows/setup-pnpm 40 | 41 | - name: Deploy 42 | uses: cloudflare/wrangler-action@v3 43 | with: 44 | apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} 45 | command: deploy --assets ./target/doc.merged/ --name rktk-rust-docs --compatibility-date 2025-06-07 46 | -------------------------------------------------------------------------------- /.github/workflows/rktk-client.yml: -------------------------------------------------------------------------------- 1 | name: rktk-client 2 | 3 | on: 4 | push: 5 | branches: ["master"] 6 | paths: 7 | - "crates/rktk-client/**" 8 | - "crates/kmsm/**.rs" 9 | - "crates/rktk-rrp/**.rs" 10 | - ".github/workflows/rktk-client.yml" 11 | - ".github/workflows/setup-rust/action.yaml" 12 | workflow_dispatch: 13 | 14 | env: 15 | CARGO_TERM_COLOR: always 16 | 17 | jobs: 18 | build-and-deploy-rktk-client: 19 | name: Build and deploy rktk-client 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@v4 24 | 25 | - name: Setup rust 26 | uses: ./.github/workflows/setup-rust 27 | with: 28 | components: "rust-src" 29 | target: "wasm32-unknown-unknown" 30 | 31 | - uses: taiki-e/install-action@v2 32 | with: 33 | tool: wasm-pack,dioxus-cli@0.7.0-rc.0 34 | 35 | - name: Setup pnpm 36 | uses: ./.github/workflows/setup-pnpm 37 | 38 | - name: Build 39 | run: cd crates/rktk-client && pnpm build 40 | 41 | - name: Deploy 42 | uses: cloudflare/wrangler-action@v3 43 | with: 44 | apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} 45 | command: deploy --assets ./target/dx/rktk-client/release/web/public --name rktk-client --compatibility-date 2025-06-07 46 | -------------------------------------------------------------------------------- /.github/workflows/setup-pnpm/action.yaml: -------------------------------------------------------------------------------- 1 | name: Setup pnpm 2 | 3 | description: "Setup node,pnpm and install deps" 4 | 5 | runs: 6 | using: "composite" 7 | steps: 8 | - uses: pnpm/action-setup@v4 9 | name: Install pnpm 10 | 11 | - name: Setup Node 12 | uses: actions/setup-node@v4 13 | with: 14 | node-version: 20 15 | cache: "pnpm" 16 | 17 | - name: pnpm install 18 | shell: bash 19 | run: pnpm install -r 20 | -------------------------------------------------------------------------------- /.github/workflows/setup-rust/action.yaml: -------------------------------------------------------------------------------- 1 | name: Setup Rust 2 | 3 | description: "Setup Rust toolchain" 4 | 5 | inputs: 6 | components: 7 | description: "rustup components to install" 8 | required: true 9 | target: 10 | description: "rustup target to install" 11 | required: true 12 | 13 | runs: 14 | using: "composite" 15 | steps: 16 | - uses: dtolnay/rust-toolchain@master 17 | id: toolchain 18 | with: 19 | toolchain: "nightly-2025-10-31" 20 | components: "${{ inputs.components }}" 21 | targets: "${{ inputs.target }}" 22 | 23 | - id: rust-override 24 | shell: bash 25 | run: "rustup override set ${{steps.toolchain.outputs.name}}" 26 | 27 | - uses: Swatinem/rust-cache@v2 28 | -------------------------------------------------------------------------------- /.github/workflows/site.yml: -------------------------------------------------------------------------------- 1 | name: site 2 | 3 | on: 4 | push: 5 | branches: ["master"] 6 | paths: 7 | - "site/**" 8 | - ".github/workflows/site.yml" 9 | workflow_dispatch: 10 | 11 | jobs: 12 | build-and-deploy-site: 13 | name: Build and deploy site 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | with: 19 | fetch-depth: 0 # Required to generate update time 20 | 21 | - name: Setup pnpm 22 | uses: ./.github/workflows/setup-pnpm 23 | 24 | - name: Next build cache 25 | uses: actions/cache@v4 26 | with: 27 | path: | 28 | site/.next/cache 29 | key: ${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx', 'content/**.mdx', 'content/**.json') }} 30 | restore-keys: | 31 | ${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}- 32 | 33 | - name: Build with Next.js 34 | run: cd site && pnpm build 35 | 36 | - name: Deploy 37 | uses: cloudflare/wrangler-action@v3 38 | with: 39 | apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} 40 | command: deploy --assets ./site/out/ --name rktk-site --compatibility-date 2025-06-07 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | node_modules 3 | .next 4 | .wrangler 5 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "rust-analyzer.check.overrideCommand": [ 3 | "cargo", 4 | "check-delta", 5 | "-l", 6 | "file", 7 | "-s", 8 | "clippy", 9 | "--message-format=json", 10 | "--features", 11 | "_check", 12 | "--all-targets" 13 | ], 14 | "rust-analyzer.cargo.features": ["_check"], 15 | "rust-analyzer.check.extraEnv": { 16 | "DEFMT_LOG": "trace" 17 | }, 18 | "evenBetterToml.schema.enabled": false 19 | } 20 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "check all", 6 | "type": "shell", 7 | "command": "cargo xtask check all" 8 | }, 9 | { 10 | "label": "test all", 11 | "type": "shell", 12 | "command": "cargo xtask test all" 13 | }, 14 | { 15 | "label": "start rktk-client", 16 | "type": "shell", 17 | "command": "cd crates/rktk-client && pnpm dev" 18 | }, 19 | { 20 | "label": "clean", 21 | "type": "shell", 22 | "command": "cargo clean" 23 | }, 24 | { 25 | "label": "doc", 26 | "type": "shell", 27 | "command": "cargo xtask doc" 28 | }, 29 | { 30 | "label": "site dev", 31 | "type": "shell", 32 | "command": "cd site && pnpm dev" 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 nazo6 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/2.1.3/schema.json", 3 | "vcs": { 4 | "enabled": true, 5 | "clientKind": "git", 6 | "useIgnoreFile": true 7 | }, 8 | "files": { 9 | "includes": ["**", "!crates/rktk/schema.json"], 10 | "ignoreUnknown": false 11 | }, 12 | "formatter": { 13 | "enabled": true, 14 | "indentStyle": "tab", 15 | "lineWidth": 80 16 | }, 17 | "linter": { 18 | "enabled": true, 19 | "rules": { 20 | "recommended": true 21 | } 22 | }, 23 | "javascript": { 24 | "formatter": { 25 | "quoteStyle": "double" 26 | } 27 | }, 28 | "assist": { 29 | "enabled": true, 30 | "actions": { 31 | "source": { 32 | "organizeImports": "on" 33 | } 34 | } 35 | }, 36 | "overrides": [ 37 | { 38 | "includes": ["**/rktk.json"], 39 | "formatter": { 40 | "lineWidth": 320 41 | } 42 | } 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /crates/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [env] 2 | RKTK_CONFIG_PATH = { value = "rktk.dev.json", relative = true } 3 | -------------------------------------------------------------------------------- /crates/kmsm-rktk/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "kmsm-rktk" 3 | authors.workspace = true 4 | license.workspace = true 5 | version.workspace = true 6 | edition.workspace = true 7 | repository.workspace = true 8 | 9 | [dependencies] 10 | kmsm = { workspace = true } 11 | strum = { workspace = true, features = ["derive"] } 12 | paste = { workspace = true } 13 | 14 | [features] 15 | _check = [] 16 | -------------------------------------------------------------------------------- /crates/kmsm-rktk/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | 3 | use kmsm::keycode::prelude::*; 4 | 5 | macro_rules! gen_rktk_keys { 6 | ($($name:ident = $value:literal),* $(,)?) => { 7 | /// Custom key id of kmsm which is specific to rktk. 8 | /// It must be used with `Custom1` variant of [`kmsm::keycode::KeyCode`] 9 | /// 10 | /// Custom2 and Custom3 can be used by user. 11 | #[derive( 12 | Debug, Clone, Copy, PartialEq, Eq, strum::EnumIter, strum::IntoStaticStr, strum::FromRepr, 13 | )] 14 | #[repr(u8)] 15 | pub enum RktkKeys { 16 | $( 17 | $name = $value, 18 | )* 19 | } 20 | 21 | paste::paste! { 22 | $(pub const [<$name:snake:upper>] : KeyAction = KeyAction::Normal(KeyCode::Custom1($value));)* 23 | } 24 | }; 25 | } 26 | 27 | gen_rktk_keys! { 28 | FlashClear = 0, 29 | OutputBle = 1, 30 | OutputUsb = 2, 31 | BleBondClear = 3, 32 | Bootloader = 4, 33 | PowerOff = 5, 34 | RgbOff = 6, 35 | RgbBrightnessUp = 7, 36 | RgbBrightnessDown = 8, 37 | RgbPatternRainbow = 9, 38 | } 39 | 40 | use core::fmt::{self, Display, Formatter}; 41 | impl Display for RktkKeys { 42 | fn fmt(&self, f: &mut Formatter) -> fmt::Result { 43 | let s: &'static str = self.into(); 44 | write!(f, "{s}") 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /crates/kmsm/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "kmsm" 3 | description = "Keymap/state manager for self-made keyboard firmware" 4 | authors.workspace = true 5 | license.workspace = true 6 | version.workspace = true 7 | edition.workspace = true 8 | repository.workspace = true 9 | 10 | [package.metadata.docs.rs] 11 | features = ["state", "postcard"] 12 | 13 | [dependencies] 14 | heapless = { workspace = true } 15 | usbd-hid = { workspace = true, optional = true } 16 | 17 | paste = { workspace = true } 18 | macro_rules_attribute = { workspace = true } 19 | 20 | strum = { workspace = true, features = ["derive"] } 21 | serde = { workspace = true, optional = true, features = ["derive"] } 22 | serde_with = { workspace = true, features = ["macros"], optional = true } 23 | postcard = { workspace = true, optional = true, features = [ 24 | "experimental-derive", 25 | ] } 26 | 27 | ssmarshal = { workspace = true, optional = true } 28 | defmt = { workspace = true, optional = true } 29 | 30 | document-features = { workspace = true } 31 | 32 | [dev-dependencies] 33 | critical-section = { workspace = true, features = ["std"] } 34 | usbd-hid = { workspace = true } 35 | pretty_assertions = { workspace = true } 36 | 37 | [features] 38 | _check = ["state", "ssmarshal/std", "defmt"] 39 | 40 | ## Enables state management. 41 | state = ["dep:usbd-hid"] 42 | 43 | ## Enables serialization using serde. 44 | serde = ["dep:serde", "dep:serde_with"] 45 | ## Derives postcard's MaxSize trait 46 | postcard = ["dep:postcard", "serde"] 47 | 48 | ## Derives defmt's Format trait 49 | defmt = ["dep:defmt"] 50 | -------------------------------------------------------------------------------- /crates/kmsm/README.md: -------------------------------------------------------------------------------- 1 | # kmsm 2 | 3 | Keymap/state management library for keyboard firmware. 4 | 5 | For more detail, see [RKTK project README](https://github.com/nazo6/rktk) 6 | -------------------------------------------------------------------------------- /crates/kmsm/src/interface/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::macros::common_derive; 2 | 3 | pub mod state; 4 | -------------------------------------------------------------------------------- /crates/kmsm/src/keycode/layer.rs: -------------------------------------------------------------------------------- 1 | //! Layer operation keys 2 | 3 | #![allow(non_snake_case)] 4 | 5 | use macro_rules_attribute::apply; 6 | 7 | use super::{KeyAction, KeyCode}; 8 | use crate::macros::common_derive; 9 | 10 | /// Keycode for layer operations. 11 | #[apply(common_derive)] 12 | #[derive(Copy, strum::EnumIter, strum::IntoStaticStr)] 13 | pub enum LayerOp { 14 | /// Momentary activates the specified layer. 15 | Momentary(u8), 16 | /// Toggles the state of the specified layer. 17 | Toggle(u8), 18 | } 19 | 20 | pub const fn MO(n: u8) -> KeyAction { 21 | KeyAction::Normal(KeyCode::Layer(LayerOp::Momentary(n))) 22 | } 23 | 24 | pub const fn TG(n: u8) -> KeyAction { 25 | KeyAction::Normal(KeyCode::Layer(LayerOp::Toggle(n))) 26 | } 27 | -------------------------------------------------------------------------------- /crates/kmsm/src/keycode/media.rs: -------------------------------------------------------------------------------- 1 | //! Media keys. 2 | 3 | use macro_rules_attribute::apply; 4 | 5 | use crate::macros::{common_derive, impl_display, normal, with_consts}; 6 | 7 | /// Represents `media key` which is used for media control. 8 | /// 9 | /// These keys are sent using a different descriptor than normal keys. 10 | #[apply(with_consts)] 11 | #[apply(common_derive)] 12 | #[derive(Copy, strum::EnumIter, strum::IntoStaticStr)] 13 | pub enum Media { 14 | Zero = 0x00, 15 | Play = 0xB0, 16 | MPause = 0xB1, 17 | Record = 0xB2, 18 | NextTrack = 0xB5, 19 | PrevTrack = 0xB6, 20 | MStop = 0xB7, 21 | RandomPlay = 0xB9, 22 | Repeat = 0xBC, 23 | PlayPause = 0xCD, 24 | MMute = 0xE2, 25 | VolumeIncrement = 0xE9, 26 | VolumeDecrement = 0xEA, 27 | Reserved = 0xEB, 28 | } 29 | 30 | impl_display!(Media); 31 | 32 | normal!(VOLUP, Media, VolumeIncrement); 33 | normal!(VOLDN, Media, VolumeDecrement); 34 | -------------------------------------------------------------------------------- /crates/kmsm/src/keycode/modifier.rs: -------------------------------------------------------------------------------- 1 | //! Modifier keys 2 | 3 | use macro_rules_attribute::apply; 4 | 5 | use crate::macros::{impl_display, with_consts}; 6 | 7 | use super::common_derive; 8 | 9 | #[apply(with_consts)] 10 | #[apply(common_derive)] 11 | #[derive(Copy, strum::EnumIter, strum::IntoStaticStr)] 12 | pub enum Modifier { 13 | LCtrl = 0x01, 14 | LShft = 0x02, 15 | LAlt = 0x04, 16 | LGui = 0x08, 17 | RCtrl = 0x10, 18 | RShft = 0x20, 19 | RAlt = 0x40, 20 | RGui = 0x80, 21 | } 22 | 23 | impl_display!(Modifier); 24 | -------------------------------------------------------------------------------- /crates/kmsm/src/keycode/mouse.rs: -------------------------------------------------------------------------------- 1 | use macro_rules_attribute::apply; 2 | 3 | use crate::macros::{impl_display, with_consts}; 4 | 5 | use super::common_derive; 6 | 7 | #[apply(with_consts)] 8 | #[apply(common_derive)] 9 | #[derive(Copy, strum::EnumIter, strum::IntoStaticStr)] 10 | pub enum Mouse { 11 | MLeft = 0b0000_0001, 12 | MRight = 0b0000_0010, 13 | MMiddle = 0b0000_0100, 14 | MBack = 0b0000_1000, 15 | MForward = 0b0001_0000, 16 | } 17 | 18 | impl_display!(Mouse); 19 | -------------------------------------------------------------------------------- /crates/kmsm/src/keycode/special.rs: -------------------------------------------------------------------------------- 1 | //! Special keys. 2 | 3 | use macro_rules_attribute::apply; 4 | 5 | use crate::macros::{common_derive, impl_display, with_consts}; 6 | 7 | /// Represents special keys. 8 | /// 9 | /// "Special Key" is a key that is not intended to be sent externally. There are two types of keys in this category: 10 | /// 1. "Transparent" keys 11 | /// These keys have no meaning in this crate. If these keys are pressed, its information are 12 | /// passed through transparent report to caller. 13 | /// 2. Keys that determine the behavior of kmsm 14 | /// These keys are used to control the behavior of kmsm. 15 | /// For example, while `MoScrl` key is pressed, mouse event is converted to scroll event. 16 | #[apply(with_consts)] 17 | #[apply(common_derive)] 18 | #[derive(Copy, strum::EnumIter, strum::IntoStaticStr)] 19 | pub enum Special { 20 | MoScrl, 21 | AmlReset, 22 | LockTg, 23 | } 24 | 25 | impl_display!(Special); 26 | -------------------------------------------------------------------------------- /crates/kmsm/src/keycode/utils.rs: -------------------------------------------------------------------------------- 1 | //! Utility functions to define keymap. 2 | 3 | use super::{modifier::Modifier, KeyAction, KeyCode}; 4 | 5 | /// Press key with shift 6 | #[allow(non_snake_case)] 7 | pub const fn SF(k: KeyAction) -> KeyAction { 8 | if let KeyAction::Normal(KeyCode::Key(key)) = k { 9 | KeyAction::Normal2(KeyCode::Modifier(Modifier::LShft), KeyCode::Key(key)) 10 | } else { 11 | panic!("Unsupported key type") 12 | } 13 | } 14 | 15 | /// Tap dance 16 | #[allow(non_snake_case)] 17 | pub const fn TD(id: u8) -> KeyAction { 18 | KeyAction::TapDance(id) 19 | } 20 | -------------------------------------------------------------------------------- /crates/kmsm/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | //! 3 | //! This crate consists of the following modules: 4 | //! - [`keycode`]: Keycode definitions 5 | //! - [`keymap`]: Keymap definition 6 | //! - [`state`]: State management 7 | //! 8 | //! To know how to define keymap, see `keycode` and `keymap` modules. 9 | //! 10 | //! ## Feature flags 11 | #![doc = document_features::document_features!()] 12 | #![cfg_attr(doc, feature(doc_cfg))] 13 | #![allow(non_snake_case)] 14 | #![cfg_attr(not(test), no_std)] 15 | 16 | pub mod interface; 17 | pub mod keycode; 18 | pub mod keymap; 19 | mod macros; 20 | mod time; 21 | 22 | #[cfg(any(test, feature = "state"))] 23 | pub mod state; 24 | -------------------------------------------------------------------------------- /crates/kmsm/src/macros.rs: -------------------------------------------------------------------------------- 1 | use macro_rules_attribute::attribute_alias; 2 | 3 | macro_rules! normal { 4 | ($name:ident, $type:ident, $variant:ident) => { 5 | pub const $name: crate::keycode::KeyAction = 6 | crate::keycode::KeyAction::Normal(crate::keycode::KeyCode::$type($type::$variant)); 7 | }; 8 | } 9 | 10 | macro_rules! with_consts { 11 | // for enum with value 12 | { 13 | $(#[$($attr:tt)*])* 14 | $vis:vis enum $name:ident { 15 | $($variant:ident = $val:literal,)* 16 | } 17 | } => { 18 | $(#[$($attr)*])* 19 | $vis enum $name { $($variant = $val,)* } 20 | 21 | paste::paste!{ 22 | $(pub const [<$variant:snake:upper>] : crate::keycode::KeyAction = crate::keycode::KeyAction::Normal(crate::keycode::KeyCode::$name($name::$variant));)* 23 | } 24 | }; 25 | // for enum without value 26 | { 27 | $(#[$($attr:tt)*])* 28 | $vis:vis enum $name:ident { 29 | $($variant:ident,)* 30 | } 31 | } => { 32 | $(#[$($attr)*])* 33 | $vis enum $name { $($variant,)* } 34 | 35 | paste::paste!{ 36 | $(pub const [<$variant:snake:upper>] : crate::keycode::KeyAction = crate::keycode::KeyAction::Normal(crate::keycode::KeyCode::$name($name::$variant));)* 37 | } 38 | } 39 | } 40 | 41 | macro_rules! impl_display { 42 | ($type:ty) => { 43 | use core::fmt::{self, Display, Formatter}; 44 | impl Display for $type { 45 | fn fmt(&self, f: &mut Formatter) -> fmt::Result { 46 | let s: &'static str = self.into(); 47 | write!(f, "{s}") 48 | } 49 | } 50 | }; 51 | } 52 | 53 | attribute_alias! { 54 | #[apply(common_derive)] = 55 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 56 | #[cfg_attr( 57 | feature = "postcard", 58 | derive(postcard::experimental::max_size::MaxSize) 59 | )] 60 | #[derive(PartialEq, Eq, Clone, Debug)] 61 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 62 | ; 63 | } 64 | 65 | pub(super) use impl_display; 66 | pub(super) use normal; 67 | pub(super) use with_consts; 68 | -------------------------------------------------------------------------------- /crates/kmsm/src/state/key_resolver/oneshot.rs: -------------------------------------------------------------------------------- 1 | use crate::{interface::state::input_event::KeyChangeEvent, keycode::KeyCode}; 2 | 3 | use super::EventType; 4 | 5 | #[derive(Debug)] 6 | struct OneshotKeyState { 7 | pub key: KeyCode, 8 | // When active, this is some and contains the location of the key that activated this oneshot 9 | // key. 10 | pub active: Option<(u8, u8)>, 11 | } 12 | 13 | pub struct OneshotState { 14 | oneshot: heapless::Vec, 15 | } 16 | 17 | impl OneshotState { 18 | pub fn new() -> Self { 19 | Self { 20 | oneshot: heapless::Vec::new(), 21 | } 22 | } 23 | 24 | pub fn pre_resolve( 25 | &mut self, 26 | event: Option<&KeyChangeEvent>, 27 | mut cb: impl FnMut(EventType, KeyCode), 28 | ) { 29 | self.oneshot.retain_mut(|oneshot| { 30 | if let Some(event) = event { 31 | if event.pressed { 32 | if oneshot.active.is_none() { 33 | oneshot.active = Some((event.row, event.col)); 34 | cb(EventType::Pressed, oneshot.key); 35 | return true; 36 | } 37 | } else if oneshot.active == Some((event.row, event.col)) { 38 | cb(EventType::Released, oneshot.key); 39 | return false; 40 | } 41 | } 42 | 43 | if oneshot.active.is_some() { 44 | cb(EventType::Pressing, oneshot.key); 45 | } 46 | 47 | true 48 | }); 49 | } 50 | 51 | pub fn process_keycode(&mut self, kc: &KeyCode, pressed: bool) { 52 | if pressed { 53 | let _ = self.oneshot.push(OneshotKeyState { 54 | key: *kc, 55 | active: None, 56 | }); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /crates/kmsm/src/state/shared.rs: -------------------------------------------------------------------------------- 1 | use crate::time::{Duration, Instant}; 2 | 3 | use crate::keymap::Keymap; 4 | 5 | pub(super) type LayerActive = [bool; LAYER]; 6 | 7 | pub(super) struct SharedState< 8 | const LAYER: usize, 9 | const ROW: usize, 10 | const COL: usize, 11 | const ENCODER_COUNT: usize, 12 | const TAP_DANCE_MAX_DEFINITIONS: usize, 13 | const TAP_DANCE_MAX_REPEATS: usize, 14 | const COMBO_KEY_MAX_DEFINITIONS: usize, 15 | const COMBO_KEY_MAX_SOURCES: usize, 16 | > { 17 | pub keymap: Keymap< 18 | LAYER, 19 | ROW, 20 | COL, 21 | ENCODER_COUNT, 22 | TAP_DANCE_MAX_DEFINITIONS, 23 | TAP_DANCE_MAX_REPEATS, 24 | COMBO_KEY_MAX_DEFINITIONS, 25 | COMBO_KEY_MAX_SOURCES, 26 | >, 27 | pub layer_active: LayerActive, 28 | pub now: Instant, 29 | pub locked: bool, 30 | } 31 | 32 | impl< 33 | const LAYER: usize, 34 | const ROW: usize, 35 | const COL: usize, 36 | const ENCODER_COUNT: usize, 37 | const TAP_DANCE_MAX_DEFINITIONS: usize, 38 | const TAP_DANCE_MAX_REPEATS: usize, 39 | const COMBO_KEY_MAX_DEFINITIONS: usize, 40 | const COMBO_KEY_MAX_SOURCES: usize, 41 | > 42 | SharedState< 43 | LAYER, 44 | ROW, 45 | COL, 46 | ENCODER_COUNT, 47 | TAP_DANCE_MAX_DEFINITIONS, 48 | TAP_DANCE_MAX_REPEATS, 49 | COMBO_KEY_MAX_DEFINITIONS, 50 | COMBO_KEY_MAX_SOURCES, 51 | > 52 | { 53 | pub fn new( 54 | keymap: Keymap< 55 | LAYER, 56 | ROW, 57 | COL, 58 | ENCODER_COUNT, 59 | TAP_DANCE_MAX_DEFINITIONS, 60 | TAP_DANCE_MAX_REPEATS, 61 | COMBO_KEY_MAX_DEFINITIONS, 62 | COMBO_KEY_MAX_SOURCES, 63 | >, 64 | ) -> Self { 65 | Self { 66 | keymap, 67 | layer_active: [false; LAYER], 68 | now: Instant::from_start(Duration::from_millis(0)), 69 | locked: false, 70 | } 71 | } 72 | 73 | pub fn highest_layer(&self) -> usize { 74 | self.layer_active.iter().rposition(|&x| x).unwrap_or(0) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /crates/kmsm/src/state/tests/basic.rs: -------------------------------------------------------------------------------- 1 | use super::prelude::*; 2 | use pretty_assertions::assert_eq; 3 | 4 | #[test] 5 | pub fn first_empty_second_none() { 6 | let mut state = new_state(EMPTY_KEYMAP); 7 | 8 | let report = update!(state, time(0)); 9 | assert_eq!(report, NONE_REPORT, "Nothing happens"); 10 | 11 | let report = update!(state, time(0)); 12 | assert_eq!(report, NONE_REPORT, "Nothing happens, second"); 13 | } 14 | 15 | #[test] 16 | pub fn key_press_release() { 17 | let mut keymap = EMPTY_KEYMAP; 18 | keymap.layers[0].keymap[0][0] = KeyAction::Normal(KeyCode::Key(Key::A)); 19 | let mut state = new_state(keymap); 20 | let _ = update!(state, time(0)); 21 | 22 | let report = update!(state, time(0), (0, 0, true)); 23 | assert_eq!( 24 | report, 25 | report_with_keycodes([0x04, 0, 0, 0, 0, 0]), 26 | "Key 'a' pressed" 27 | ); 28 | 29 | let report = update!(state, time(0), (0, 0, false)); 30 | assert_eq!(report, KEYBOARD_ONLY_REPORT, "Key 'a' released"); 31 | } 32 | -------------------------------------------------------------------------------- /crates/kmsm/src/state/tests/encoder.rs: -------------------------------------------------------------------------------- 1 | use crate::{interface::state::input_event::EncoderDirection, keymap::Keymap}; 2 | 3 | use super::prelude::*; 4 | use pretty_assertions::assert_eq; 5 | 6 | const ENCODER_KEYMAP: Keymap = const { 7 | let mut keymap = EMPTY_KEYMAP; 8 | keymap.layers[0].encoder_keys[0] = (Some(KeyCode::Key(Key::B)), Some(KeyCode::Key(Key::A))); 9 | keymap 10 | }; 11 | 12 | #[test] 13 | pub fn encoder_clockwise() { 14 | let mut state = new_state(ENCODER_KEYMAP); 15 | 16 | let _ = update!(state, time(0)); 17 | 18 | let report = state.update( 19 | InputEvent::Encoder((0, EncoderDirection::Clockwise)), 20 | time(0), 21 | ); 22 | 23 | assert_eq!( 24 | report, 25 | report_with_keycodes([0x04, 0, 0, 0, 0, 0]), 26 | "In first report, key `A` should be sent" 27 | ); 28 | 29 | let report = update!(state, time(10)); 30 | assert_eq!( 31 | report, KEYBOARD_ONLY_REPORT, 32 | "In second send, empty should not be sent" 33 | ); 34 | } 35 | 36 | #[test] 37 | pub fn encoder_counterclockwise() { 38 | let mut state = new_state(ENCODER_KEYMAP); 39 | 40 | let _ = update!(state, time(0)); 41 | 42 | let report = state.update( 43 | InputEvent::Encoder((0, EncoderDirection::CounterClockwise)), 44 | time(0), 45 | ); 46 | 47 | assert_eq!( 48 | report, 49 | report_with_keycodes([0x05, 0, 0, 0, 0, 0]), 50 | "In first report, key `B` should be sent" 51 | ); 52 | 53 | let report = update!(state, time(0)); 54 | assert_eq!( 55 | report, KEYBOARD_ONLY_REPORT, 56 | "In second send, empty report should be sent" 57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /crates/kmsm/src/state/tests/layer.rs: -------------------------------------------------------------------------------- 1 | use super::prelude::*; 2 | use pretty_assertions::assert_eq; 3 | 4 | #[test] 5 | fn layer_change_during_press() { 6 | let mut keymap = EMPTY_KEYMAP; 7 | keymap.layers[0].keymap[0][0] = KeyAction::Normal(KeyCode::Key(Key::A)); 8 | keymap.layers[1].keymap[0][0] = KeyAction::Normal(KeyCode::Mouse(Mouse::MLeft)); 9 | 10 | let mut state = new_state(keymap); 11 | let _ = update!(state, time(0)); 12 | 13 | let report = update!(state, time(0), (0, 0, true)); 14 | assert_eq!( 15 | report, 16 | report_with_keycodes([0x04, 0, 0, 0, 0, 0]), 17 | "Key 'A' at layer 0 is pressed" 18 | ); 19 | 20 | let _report = state.update(InputEvent::Mouse((20, 10)), time(50)); 21 | assert_eq!( 22 | state.inner().shared.layer_active, 23 | [false, true, false, false, false], 24 | "Aml activated" 25 | ); 26 | 27 | let report = update!(state, time(200), (0, 0, false)); 28 | assert_eq!( 29 | report.keyboard_report.unwrap().keycodes, 30 | [0, 0, 0, 0, 0, 0], 31 | "Key released" 32 | ); 33 | 34 | let _report = state.update(InputEvent::None, time(1000)); 35 | assert_eq!( 36 | state.inner().shared.layer_active, 37 | [false, false, false, false, false], 38 | "Aml deactivated" 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /crates/kmsm/src/state/tests/special.rs: -------------------------------------------------------------------------------- 1 | use super::prelude::*; 2 | use pretty_assertions::assert_eq; 3 | 4 | #[test] 5 | pub fn lock() { 6 | let mut keymap = EMPTY_KEYMAP; 7 | keymap.layers[0].keymap[0][0] = KeyAction::Normal(KeyCode::Special(Special::LockTg)); 8 | keymap.layers[0].keymap[0][1] = KeyAction::Normal(KeyCode::Key(Key::A)); 9 | 10 | let mut state = new_state(keymap); 11 | 12 | dbg!("T1"); 13 | let report = update!(state, time(0), (0, 1, true)); 14 | assert_eq!( 15 | report, 16 | report_with_keycodes([0x04, 0, 0, 0, 0, 0]), 17 | "Not locked. Key 'a' pressed" 18 | ); 19 | 20 | dbg!("T2"); 21 | let report = update!(state, time(10), (0, 0, true)); 22 | assert_eq!(report, NONE_REPORT, "Lock pressed. Still Key 'a' pressed"); 23 | dbg!("T3"); 24 | let report = update!(state, time(20), (0, 0, false)); 25 | assert_eq!(report, NONE_REPORT, "Lock released. Still Key 'a' pressed"); 26 | 27 | dbg!("T4"); 28 | let report = update!(state, time(30), (0, 1, false)); 29 | assert_eq!(report, KEYBOARD_ONLY_REPORT, "Locked. Key 'a' released"); 30 | 31 | dbg!("T5"); 32 | let report = update!(state, time(40), (0, 1, true)); 33 | assert_eq!(report, NONE_REPORT, "Locked. Key 'a' pressed, but not sent"); 34 | dbg!("T6"); 35 | let report = update!(state, time(50), (0, 1, false)); 36 | assert_eq!(report, KEYBOARD_ONLY_REPORT, "Locked. Key 'a' released."); 37 | 38 | dbg!("T7"); 39 | let report = update!(state, time(60), (0, 0, true)); 40 | assert_eq!(report, NONE_REPORT, "Lock pressed"); 41 | dbg!("T8"); 42 | let report = update!(state, time(70), (0, 0, false)); 43 | assert_eq!(report, NONE_REPORT, "Lock released"); 44 | 45 | let report = update!(state, time(80), (0, 1, true)); 46 | assert_eq!( 47 | report, 48 | report_with_keycodes([0x04, 0, 0, 0, 0, 0]), 49 | "Unlocked. Key 'a' pressed" 50 | ); 51 | let report = update!(state, time(90), (0, 1, false)); 52 | assert_eq!(report, KEYBOARD_ONLY_REPORT, "Unlocked. Key 'a' released"); 53 | } 54 | -------------------------------------------------------------------------------- /crates/kmsm/src/state/updater/layer.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | interface::state::output_event::EventType, 3 | keycode::{layer::LayerOp, KeyCode}, 4 | }; 5 | 6 | pub fn update_layer_by_keycode( 7 | layer_active: &mut [bool; LAYER], 8 | keycode: &KeyCode, 9 | event: EventType, 10 | ) { 11 | match (event, keycode) { 12 | (EventType::Released, KeyCode::Layer(LayerOp::Momentary(l))) => { 13 | layer_active[*l as usize] = false; 14 | } 15 | (_, KeyCode::Layer(LayerOp::Momentary(l))) => { 16 | layer_active[*l as usize] = true; 17 | } 18 | (EventType::Pressed, KeyCode::Layer(LayerOp::Toggle(l))) => { 19 | layer_active[*l as usize] = !layer_active[*l as usize]; 20 | } 21 | _ => {} 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /crates/kmsm/src/state/updater/mouse/aml.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::collapsible_if)] 2 | 3 | use crate::time::{Duration, Instant}; 4 | 5 | pub enum AmlState { 6 | /// Represents the active state of the AML. 7 | /// Contains u8 which is the mouse movement to track aml threshold. 8 | Inactive(u8), 9 | Active(Instant), 10 | } 11 | 12 | pub struct Aml { 13 | state: AmlState, 14 | auto_mouse_duration: Duration, 15 | auto_mouse_threshold: u8, 16 | } 17 | 18 | impl Aml { 19 | pub fn new(auto_mouse_duration: Duration, auto_mouse_threshold: u8) -> Self { 20 | Self { 21 | state: AmlState::Inactive(0), 22 | auto_mouse_duration, 23 | auto_mouse_threshold, 24 | } 25 | } 26 | 27 | pub fn enabled_changed( 28 | &mut self, 29 | now: Instant, 30 | mouse_event: (i8, i8), 31 | // If true, continues aml ignoring other conditions. 32 | // Typically used when mouse button is pressed. 33 | // This takes precedence over `force_disable_aml`. 34 | force_continue_aml: bool, 35 | // If true, disables aml ignoring other conditions. 36 | // Typically used when other key is pressed to acheive `HOLD_ON_OTHER_KEY_PRESS`. 37 | force_disable_aml: bool, 38 | ) -> (bool, bool) { 39 | let mut changed = false; 40 | 41 | match &mut self.state { 42 | AmlState::Active(start_time) => { 43 | if mouse_event != (0, 0) || force_continue_aml { 44 | *start_time = now; 45 | } else if (now - *start_time) > self.auto_mouse_duration || force_disable_aml { 46 | changed = true; 47 | self.state = AmlState::Inactive(0); 48 | } 49 | } 50 | AmlState::Inactive(movement) => { 51 | *movement += mouse_event.0.unsigned_abs() + mouse_event.1.unsigned_abs(); 52 | if *movement > self.auto_mouse_threshold { 53 | changed = true; 54 | self.state = AmlState::Active(now); 55 | } 56 | } 57 | } 58 | 59 | (matches!(self.state, AmlState::Active(_)), changed) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /crates/kmsm/src/time.rs: -------------------------------------------------------------------------------- 1 | use core::ops::{Add, Sub}; 2 | 3 | #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] 4 | pub struct Instant { 5 | /// Time from start in milliseconds. 6 | from_start: u32, 7 | } 8 | 9 | impl Instant { 10 | #[allow(dead_code)] 11 | pub const fn from_start(from_start: Duration) -> Self { 12 | Self { 13 | from_start: from_start.millis, 14 | } 15 | } 16 | } 17 | 18 | impl Add for Instant { 19 | type Output = Self; 20 | fn add(self, rhs: Duration) -> Self { 21 | Self { 22 | from_start: self.from_start + rhs.millis, 23 | } 24 | } 25 | } 26 | 27 | impl Sub for Instant { 28 | type Output = Duration; 29 | fn sub(self, rhs: Instant) -> Duration { 30 | Duration { 31 | millis: self.from_start - rhs.from_start, 32 | } 33 | } 34 | } 35 | 36 | #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] 37 | pub struct Duration { 38 | millis: u32, 39 | } 40 | 41 | impl Duration { 42 | #[allow(dead_code)] 43 | pub const fn from_millis(millis: u32) -> Self { 44 | Self { millis } 45 | } 46 | } 47 | 48 | impl From for Duration { 49 | fn from(d: core::time::Duration) -> Self { 50 | Self { 51 | millis: d.as_millis() as u32, 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /crates/rktk-client/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /crates/rktk-client/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rktk-client" 3 | authors.workspace = true 4 | license.workspace = true 5 | version.workspace = true 6 | edition.workspace = true 7 | repository.workspace = true 8 | 9 | [dependencies] 10 | kmsm = { workspace = true } 11 | kmsm-rktk = { workspace = true } 12 | rktk-rrp = { workspace = true, features = ["client"] } 13 | 14 | dioxus = { workspace = true } 15 | 16 | serde = { workspace = true } 17 | serde_json = { workspace = true } 18 | ssmarshal = { workspace = true, features = ["std"] } 19 | anyhow = { workspace = true } 20 | futures = { workspace = true, features = ["async-await"] } 21 | strum = { workspace = true } 22 | jiff = { workspace = true } 23 | 24 | log = { workspace = true } 25 | tracing-log = { workspace = true, features = ["log-tracer"] } 26 | 27 | kle-serial = { workspace = true } 28 | 29 | # web deps 30 | web-sys = { workspace = true, optional = true, features = [ 31 | "Navigator", 32 | "Window", 33 | "ResizeObserver", 34 | "Hid", 35 | "HidCollectionInfo", 36 | "HidConnectionEvent", 37 | "HidConnectionEventInit", 38 | "HidDevice", 39 | "HidDeviceFilter", 40 | "HidDeviceRequestOptions", 41 | "HidInputReportEvent", 42 | "HidInputReportEventInit", 43 | "HidReportInfo", 44 | "HidReportItem", 45 | "HiddenPluginEventInit", 46 | "HidUnitSystem", 47 | ] } 48 | js-sys = { workspace = true, optional = true } 49 | wasm-bindgen = { workspace = true, optional = true } 50 | wasm-bindgen-futures = { workspace = true, optional = true } 51 | serde-wasm-bindgen = { workspace = true, optional = true } 52 | async-channel = { workspace = true } 53 | 54 | # native deps 55 | smol = { workspace = true, optional = true } 56 | async-hid = { workspace = true, optional = true } 57 | 58 | [features] 59 | _check = ["web"] 60 | 61 | web = [ 62 | "dioxus/web", 63 | "dep:web-sys", 64 | "dep:js-sys", 65 | "dep:wasm-bindgen-futures", 66 | "dep:serde-wasm-bindgen", 67 | "dep:wasm-bindgen", 68 | "jiff/js", 69 | ] 70 | native = ["dioxus/desktop", "dep:smol", "dep:async-hid"] 71 | -------------------------------------------------------------------------------- /crates/rktk-client/Dioxus.toml: -------------------------------------------------------------------------------- 1 | [application] 2 | 3 | # App (Project) Name 4 | name = "rktk-client" 5 | 6 | [web.app] 7 | 8 | # HTML title tag content 9 | title = "rktk-client" 10 | 11 | # include `assets` in web platform 12 | [web.resource] 13 | # Additional JavaScript files 14 | script = [] 15 | 16 | [web.resource.dev] 17 | 18 | # Javascript code file 19 | # serve: [dev-server] only 20 | script = [] 21 | 22 | [web.watcher] 23 | watch_path = ["src", "assets", "assets/tailwind.css"] 24 | -------------------------------------------------------------------------------- /crates/rktk-client/assets/.gitignore: -------------------------------------------------------------------------------- 1 | tailwind.css 2 | -------------------------------------------------------------------------------- /crates/rktk-client/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nazo6/rktk/530c16515b51fb7975354ca708b4fc1420d8e10d/crates/rktk-client/assets/favicon.ico -------------------------------------------------------------------------------- /crates/rktk-client/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | if !std::path::Path::new("assets/tailwind.css").exists() { 3 | std::fs::write("assets/tailwind.css", "").unwrap(); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /crates/rktk-client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {app_title} 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /crates/rktk-client/input.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | @plugin "daisyui" { 3 | themes: 4 | cupcake --default, 5 | dark --prefersdark; 6 | } 7 | 8 | .join-item { 9 | outline: none !important; 10 | } 11 | -------------------------------------------------------------------------------- /crates/rktk-client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rktk-client", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "dev:tailwind": "tailwindcss -i ./input.css -o ./assets/tailwind.css --watch", 7 | "dev:web:dioxus": "dx serve -i true --platform web --features web", 8 | "dev": "conc -r -c auto npm:dev:tailwind npm:dev:web:dioxus", 9 | "dev:desktop:dioxus": "dx serve -i true --platform desktop --features native", 10 | "dev:desktop": "conc -r -c auto npm:dev:tailwind npm:dev:desktop:dioxus", 11 | "build": "pnpm tailwindcss -i ./input.css -o ./assets/tailwind.css && dx build --release --platform web --features web" 12 | }, 13 | "author": "nazo6", 14 | "license": "MIT", 15 | "devDependencies": { 16 | "@tailwindcss/cli": "^4.1.13", 17 | "concurrently": "^9.2.1", 18 | "daisyui": "^5.1.25", 19 | "tailwindcss": "^4.1.13", 20 | "wrangler": "^4.40.2" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /crates/rktk-client/src/app/cache.rs: -------------------------------------------------------------------------------- 1 | use std::{any::Any, cell::RefCell, collections::HashMap, future::Future, rc::Rc}; 2 | 3 | use dioxus::hooks::{use_context, use_context_provider}; 4 | 5 | #[derive(Clone)] 6 | pub struct Cache(Rc>>>); 7 | 8 | pub async fn with_cache>>( 9 | cache: Cache, 10 | key: &'static str, 11 | fut: F, 12 | ) -> Result { 13 | if let Some(val) = cache.0.borrow_mut().get(key) 14 | && let Some(val) = val.downcast_ref::() { 15 | return Ok(val.clone()); 16 | } 17 | 18 | let res = fut.await; 19 | if let Ok(val) = &res { 20 | cache.0.borrow_mut().insert(key, Box::new(val.clone())); 21 | } 22 | 23 | res 24 | } 25 | 26 | pub fn invalidate_cache(cache: Cache, key: &'static str) { 27 | cache.0.borrow_mut().remove(key); 28 | } 29 | 30 | pub fn use_cache_context_provider() -> Cache { 31 | use_context_provider(|| Cache(Rc::new(RefCell::new(HashMap::new())))) 32 | } 33 | 34 | pub fn use_cache() -> Cache { 35 | use_context::() 36 | } 37 | -------------------------------------------------------------------------------- /crates/rktk-client/src/app/components/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod notification; 2 | pub mod selector; 3 | pub mod topbar; 4 | -------------------------------------------------------------------------------- /crates/rktk-client/src/app/components/selector/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod key; 2 | pub mod key_action; 3 | pub mod key_code; 4 | -------------------------------------------------------------------------------- /crates/rktk-client/src/app/disconnect.rs: -------------------------------------------------------------------------------- 1 | use dioxus::prelude::*; 2 | 3 | use crate::backend::RrpHidDevice as _; 4 | 5 | use super::{ 6 | components::notification::{push_notification, Notification, NotificationLevel}, 7 | state::CONN, 8 | }; 9 | 10 | pub async fn disconnect() -> anyhow::Result<()> { 11 | { 12 | let Some(state) = &*CONN.read() else { 13 | return Err(anyhow::anyhow!("State is None")); 14 | }; 15 | state 16 | .device 17 | .lock() 18 | .await 19 | .close() 20 | .await 21 | .map_err(|e| anyhow::anyhow!("Failed to close device: {:?}", e))?; 22 | } 23 | 24 | push_notification(Notification { 25 | message: "Disconnected from device".to_string(), 26 | level: NotificationLevel::Info, 27 | ..Default::default() 28 | }); 29 | 30 | *CONN.write() = None; 31 | 32 | Ok(()) 33 | } 34 | -------------------------------------------------------------------------------- /crates/rktk-client/src/app/mod.rs: -------------------------------------------------------------------------------- 1 | use std::sync::LazyLock; 2 | 3 | use dioxus::{core::spawn_forever, prelude::*}; 4 | 5 | use crate::{ 6 | TAILWIND_CSS, 7 | backend::{Backend, RrpHidBackend as _}, 8 | }; 9 | 10 | mod cache; 11 | mod components; 12 | mod disconnect; 13 | mod page; 14 | mod state; 15 | 16 | const FAVICON: Asset = asset!("/assets/favicon.ico"); 17 | 18 | static BACKEND: LazyLock<(Backend, async_channel::Receiver<()>)> = LazyLock::new(Backend::new); 19 | 20 | #[component] 21 | pub fn App() -> Element { 22 | rsx! { 23 | document::Link { rel: "icon", href: FAVICON } 24 | document::Link { rel: "stylesheet", href: TAILWIND_CSS } 25 | document::Title { "RKTK Client" } 26 | components::notification::NotificationProvider {} 27 | div { class: "h-full bg-base flex flex-col", 28 | components::topbar::Topbar {} 29 | Home {} 30 | } 31 | } 32 | } 33 | 34 | #[component] 35 | fn Home() -> Element { 36 | use_effect(move || { 37 | let rx = BACKEND.1.clone(); 38 | spawn_forever(async move { 39 | while (rx.recv().await).is_ok() { 40 | let _ = disconnect::disconnect().await; 41 | } 42 | }); 43 | }); 44 | 45 | rsx! { 46 | if Backend::available() { 47 | if state::CONN.read().is_some() { 48 | page::connected::Connected {} 49 | } else { 50 | page::connect::Connect {} 51 | } 52 | } else { 53 | h1 { "WebHID not supported" } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /crates/rktk-client/src/app/page/connected/mod.rs: -------------------------------------------------------------------------------- 1 | use dioxus::prelude::*; 2 | 3 | use crate::app::cache::use_cache_context_provider; 4 | 5 | mod config; 6 | mod log; 7 | mod remap; 8 | 9 | #[derive(PartialEq, Eq)] 10 | enum Tabs { 11 | Remap, 12 | Config, 13 | Log, 14 | } 15 | 16 | #[component] 17 | pub fn Connected() -> Element { 18 | use_cache_context_provider(); 19 | 20 | let mut tab = use_signal(|| Tabs::Remap); 21 | 22 | rsx! { 23 | div { class: "flex flex-col h-full", 24 | div { 25 | role: "tablist", 26 | class: "tabs tabs-boxed ml-auto mr-auto my-2", 27 | a { 28 | role: "tab", 29 | class: "tab", 30 | class: if *tab.read() == Tabs::Remap { "tab-active" }, 31 | onclick: move |_| tab.set(Tabs::Remap), 32 | "Remap" 33 | } 34 | a { 35 | role: "tab", 36 | class: "tab", 37 | class: if *tab.read() == Tabs::Config { "tab-active" }, 38 | onclick: move |_| tab.set(Tabs::Config), 39 | "Config" 40 | } 41 | a { 42 | role: "tab", 43 | class: "tab", 44 | class: if *tab.read() == Tabs::Log { "tab-active" }, 45 | onclick: move |_| tab.set(Tabs::Log), 46 | "Log" 47 | } 48 | } 49 | div { class: "grow", 50 | match *tab.read() { 51 | Tabs::Remap => rsx! { 52 | remap::Remap {} 53 | }, 54 | Tabs::Config => rsx! { 55 | config::Config {} 56 | }, 57 | Tabs::Log => rsx! { 58 | log::Log {} 59 | }, 60 | } 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /crates/rktk-client/src/app/page/connected/remap/bar.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use dioxus::prelude::*; 4 | use kmsm::keycode::KeyAction; 5 | 6 | #[component] 7 | pub fn Bar( 8 | changes: HashMap<(u8, u8, u8), KeyAction>, 9 | apply: Callback<()>, 10 | discard_all: Callback<()>, 11 | ) -> Element { 12 | rsx! { 13 | div { class: "bg-base-300 text-secondary-content flex w-full h-10 items-center px-2 gap-2", 14 | div { class: "ml-auto p-2" } 15 | if !changes.is_empty() { 16 | div { "Pending changes: {changes.len()}" } 17 | } 18 | button { 19 | disabled: changes.is_empty(), 20 | class: "btn btn-sm btn-primary", 21 | onclick: move |_| apply(()), 22 | "Apply" 23 | } 24 | button { 25 | disabled: changes.is_empty(), 26 | class: "btn btn-sm btn-secondary", 27 | onclick: move |_| discard_all(()), 28 | "Discard" 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /crates/rktk-client/src/app/page/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod connect; 2 | pub mod connected; 3 | -------------------------------------------------------------------------------- /crates/rktk-client/src/app/state.rs: -------------------------------------------------------------------------------- 1 | use dioxus::prelude::*; 2 | use futures::lock::Mutex; 3 | use rktk_rrp::endpoints::get_keyboard_info::KeyboardInfo; 4 | 5 | use crate::backend::{Backend, RrpHidBackend}; 6 | 7 | pub struct ConnectedState { 8 | pub device: Mutex<::HidDevice>, 9 | pub keyboard: KeyboardInfo, 10 | } 11 | 12 | pub static CONN: GlobalSignal> = GlobalSignal::new(|| None); 13 | -------------------------------------------------------------------------------- /crates/rktk-client/src/backend/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "web")] 2 | pub mod web; 3 | 4 | #[cfg(feature = "native")] 5 | pub mod native; 6 | 7 | #[cfg(feature = "web")] 8 | pub type Backend = web::WebHidBackend; 9 | 10 | #[cfg(feature = "native")] 11 | pub type Backend = native::NativeBackend; 12 | 13 | pub trait RrpHidBackend: Sized { 14 | type Error: std::fmt::Display + std::fmt::Debug; 15 | type HidDevice: RrpHidDevice; 16 | 17 | /// Creates a new backend instance. 18 | /// 19 | /// Returns a tuple containing the backend instance and a stream that emits when device is 20 | /// disconnected. 21 | fn new() -> (Self, async_channel::Receiver<()>); 22 | 23 | fn available() -> bool { 24 | true 25 | } 26 | 27 | async fn open_device( 28 | &self, 29 | usage_page: u16, 30 | usage: u16, 31 | ) -> Result; 32 | } 33 | 34 | pub trait RrpHidDevice { 35 | type Error: std::fmt::Display + std::fmt::Debug; 36 | type ReadTransport: rktk_rrp::transport::ReadTransport + Unpin; 37 | type WriteTransport: rktk_rrp::transport::WriteTransport + Unpin; 38 | 39 | async fn close(&mut self) -> Result<(), Self::Error>; 40 | 41 | fn get_client( 42 | &mut self, 43 | ) -> &mut rktk_rrp::client::Client; 44 | } 45 | -------------------------------------------------------------------------------- /crates/rktk-client/src/main.rs: -------------------------------------------------------------------------------- 1 | mod app; 2 | mod backend; 3 | mod utils; 4 | 5 | use dioxus::prelude::*; 6 | 7 | const TAILWIND_CSS: Asset = asset!("/assets/tailwind.css"); 8 | 9 | fn main() { 10 | dioxus::logger::init(dioxus::logger::tracing::Level::INFO).unwrap(); 11 | 12 | tracing_log::LogTracer::builder() 13 | .with_max_level(log::LevelFilter::Info) 14 | .init() 15 | .unwrap(); 16 | 17 | dioxus::launch(app::App); 18 | } 19 | -------------------------------------------------------------------------------- /crates/rktk-client/src/utils.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | // NOTE: If [this](https://github.com/smol-rs/async-io/issues/89) issue is resolved, platform 4 | // specific implementations can be removed. 5 | 6 | #[cfg(feature = "web")] 7 | pub async fn sleep(delay: Duration) { 8 | let mut cb = |resolve: js_sys::Function, _reject: js_sys::Function| { 9 | web_sys::window() 10 | .unwrap() 11 | .set_timeout_with_callback_and_timeout_and_arguments_0( 12 | &resolve, 13 | delay.as_millis() as i32, 14 | ) 15 | .unwrap(); 16 | }; 17 | 18 | let p = js_sys::Promise::new(&mut cb); 19 | 20 | wasm_bindgen_futures::JsFuture::from(p).await.unwrap(); 21 | } 22 | 23 | #[cfg(feature = "native")] 24 | pub async fn sleep(delay: Duration) { 25 | smol::Timer::after(delay).await; 26 | } 27 | -------------------------------------------------------------------------------- /crates/rktk-defmt-print/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rktk-defmt-print" 3 | description = "Small convenient defmt-print wrapper for rktk keyboards" 4 | version.workspace = true 5 | license.workspace = true 6 | edition.workspace = true 7 | authors.workspace = true 8 | repository.workspace = true 9 | 10 | [dependencies] 11 | anyhow = { workspace = true } 12 | dialoguer = { workspace = true } 13 | dirs = { workspace = true } 14 | rusb = { workspace = true } 15 | serialport = { workspace = true } 16 | -------------------------------------------------------------------------------- /crates/rktk-drivers-common/README.md: -------------------------------------------------------------------------------- 1 | # rktk-drivers-common 2 | 3 | `rktk-drivers-common` is a collection of drivers for rktk, which is common to 4 | all platforms. 5 | 6 | This library is the basis of `rktk-drivers-nrf` and `nrf-drivers-rp`, and by 7 | utilizing traits defined by embassy etc., you can create drivers with only a 8 | very thin wrapper for MCUs that have embassy hal. 9 | -------------------------------------------------------------------------------- /crates/rktk-drivers-common/src/display/mod.rs: -------------------------------------------------------------------------------- 1 | //! Display drivers 2 | 3 | pub mod ssd1306; 4 | -------------------------------------------------------------------------------- /crates/rktk-drivers-common/src/encoder.rs: -------------------------------------------------------------------------------- 1 | //! Encoder driver implementations. 2 | 3 | use embassy_futures::select::{Either, select, select_slice}; 4 | use embassy_time::Timer; 5 | use embedded_hal::digital::InputPin; 6 | use embedded_hal_async::digital::Wait; 7 | use rktk::drivers::interface::encoder::{EncoderDirection, EncoderDriver}; 8 | 9 | /// General encoder driver that can be used with any digital input pin. 10 | pub struct GeneralEncoder { 11 | encoders: [(PIN, PIN); ENCODER_COUNT], 12 | } 13 | 14 | impl GeneralEncoder { 15 | pub fn new(encoders: [(PIN, PIN); ENCODER_COUNT]) -> Self { 16 | Self { encoders } 17 | } 18 | } 19 | 20 | impl EncoderDriver 21 | for GeneralEncoder 22 | { 23 | async fn read_wait(&mut self) -> (u8, EncoderDirection) { 24 | let mut futures = self 25 | .encoders 26 | .iter_mut() 27 | .enumerate() 28 | .map(|(i, (a, b))| async move { 29 | let dir = match select(a.wait_for_any_edge(), b.wait_for_any_edge()).await { 30 | Either::First(_) => { 31 | Timer::after_ticks(100).await; 32 | if a.is_high().unwrap() ^ b.is_high().unwrap() { 33 | EncoderDirection::Clockwise 34 | } else { 35 | EncoderDirection::CounterClockwise 36 | } 37 | } 38 | Either::Second(_) => { 39 | Timer::after_ticks(100).await; 40 | if a.is_high().unwrap() ^ b.is_high().unwrap() { 41 | EncoderDirection::CounterClockwise 42 | } else { 43 | EncoderDirection::Clockwise 44 | } 45 | } 46 | }; 47 | (i as u8, dir) 48 | }) 49 | .collect::>(); 50 | 51 | select_slice(core::pin::pin!(&mut futures)).await.0 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /crates/rktk-drivers-common/src/keyscan/flex_pin.rs: -------------------------------------------------------------------------------- 1 | pub enum Pull { 2 | Up, 3 | Down, 4 | } 5 | 6 | #[allow(async_fn_in_trait)] 7 | pub trait FlexPin { 8 | fn set_as_input(&mut self); 9 | fn set_as_output(&mut self); 10 | fn set_low(&mut self); 11 | fn set_high(&mut self); 12 | fn is_high(&self) -> bool; 13 | fn is_low(&self) -> bool; 14 | async fn wait_for_high(&mut self); 15 | async fn wait_for_low(&mut self); 16 | fn set_pull(&mut self, pull: Pull); 17 | } 18 | -------------------------------------------------------------------------------- /crates/rktk-drivers-common/src/keyscan/mod.rs: -------------------------------------------------------------------------------- 1 | //! Key scanning driver implementations. 2 | 3 | use embassy_time::Duration; 4 | use embedded_hal::digital::{InputPin, OutputPin}; 5 | use rktk::config::Hand; 6 | 7 | pub mod duplex_matrix; 8 | pub mod flex_pin; 9 | pub mod matrix; 10 | mod pressed; 11 | pub mod shift_register_matrix; 12 | 13 | /// Utility function that takes an output pin and an input pin and determines the move based on the result of the input pin when the output pin is high. 14 | /// 15 | /// * `output`: Output pin 16 | /// * `input`: Input pin 17 | /// * `input_delay`: Time from output pin high to read (default: 10ms) 18 | /// * `high_hand`: Which move is judged when the input pin is high? (default: Left) 19 | pub async fn detect_hand_from_matrix, I: InputPin>( 20 | mut output: O, 21 | mut input: I, 22 | input_delay: Option, 23 | high_hand: Option, 24 | ) -> Result { 25 | let high_hand = high_hand.unwrap_or(Hand::Left); 26 | 27 | output.set_high()?; 28 | embassy_time::Timer::after(input_delay.unwrap_or(Duration::from_millis(10))).await; 29 | let hand = if input.is_high()? { 30 | high_hand 31 | } else { 32 | high_hand.other() 33 | }; 34 | output.set_low()?; 35 | 36 | Ok(hand) 37 | } 38 | -------------------------------------------------------------------------------- /crates/rktk-drivers-common/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | //! ## Feature flags 3 | #![doc = document_features::document_features!()] 4 | #![no_std] 5 | #![cfg_attr(doc, feature(doc_cfg))] 6 | 7 | pub mod debounce; 8 | pub mod display; 9 | pub mod encoder; 10 | pub mod keyscan; 11 | pub mod mouse; 12 | pub mod panic_utils; 13 | pub mod storage; 14 | #[cfg(feature = "trouble")] 15 | pub mod trouble; 16 | pub mod usb; 17 | 18 | #[cfg(feature = "defmt-timestamp")] 19 | defmt::timestamp!("{=u64:us}", embassy_time::Instant::now().as_micros()); 20 | -------------------------------------------------------------------------------- /crates/rktk-drivers-common/src/mouse/mod.rs: -------------------------------------------------------------------------------- 1 | //! Mouse driver implementations. 2 | 3 | pub mod paw3395; 4 | pub mod pmw3360; 5 | -------------------------------------------------------------------------------- /crates/rktk-drivers-common/src/mouse/paw3395/error.rs: -------------------------------------------------------------------------------- 1 | use core::fmt::Debug; 2 | 3 | use rktk::drivers::interface::{Error, ErrorKind}; 4 | 5 | #[derive(Debug)] 6 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 7 | pub enum Paw3395Error { 8 | InvalidSignature, 9 | Spi(#[cfg_attr(feature = "defmt", defmt(Debug2Format))] SpiError), 10 | General(&'static str), 11 | NotSupported, 12 | } 13 | 14 | impl Error for Paw3395Error { 15 | fn kind(&self) -> ErrorKind { 16 | match self { 17 | Self::NotSupported => ErrorKind::NotSupported, 18 | _ => ErrorKind::Other, 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /crates/rktk-drivers-common/src/mouse/paw3395/registers.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | pub const PRODUCT_ID: u8 = 0x00; 3 | pub const REVISION_ID: u8 = 0x01; 4 | pub const MOTION: u8 = 0x02; 5 | pub const DELTA_X_L: u8 = 0x03; 6 | pub const DELTA_X_H: u8 = 0x04; 7 | pub const DELTA_Y_L: u8 = 0x05; 8 | pub const DELTA_Y_H: u8 = 0x06; 9 | pub const SQUAL: u8 = 0x07; 10 | pub const RAW_DATA_SUM: u8 = 0x08; 11 | pub const MAXIMUM_RAW_DATA: u8 = 0x09; 12 | pub const MINIMUM_RAW_DATA: u8 = 0x0A; 13 | pub const SHUTTER_LOWER: u8 = 0x0B; 14 | pub const SHUTTER_UPPER: u8 = 0x0C; 15 | pub const OBSERVATION: u8 = 0x15; 16 | pub const MOTION_BURST: u8 = 0x16; 17 | pub const POWER_UP_RESET: u8 = 0x3A; 18 | pub const SHUTDOWN: u8 = 0x3B; 19 | pub const PERFORMANCE: u8 = 0x40; 20 | pub const SET_RESOLUTION: u8 = 0x47; 21 | pub const RESOLUTION_X_LOW: u8 = 0x48; 22 | pub const RESOLUTION_X_HIGH: u8 = 0x49; 23 | pub const RESOLUTION_Y_LOW: u8 = 0x4A; 24 | pub const RESOLUTION_Y_HIGH: u8 = 0x4B; 25 | pub const ANGLE_SNAP: u8 = 0x56; 26 | pub const RAWDATA_OUTPUT: u8 = 0x58; 27 | pub const RAWDATA_STATUS: u8 = 0x59; 28 | pub const RIPPLE_CONTROL: u8 = 0x5A; 29 | pub const AXIS_CONTROL: u8 = 0x5B; 30 | pub const MOTION_CTRL: u8 = 0x5C; 31 | pub const INV_PRODUCT_ID: u8 = 0x5F; 32 | pub const RUN_DOWNSHIFT: u8 = 0x77; 33 | pub const REST1_PERIOD: u8 = 0x78; 34 | pub const REST1_DOWNSHIFT: u8 = 0x79; 35 | pub const REST2_PERIOD: u8 = 0x7A; 36 | pub const REST2_DOWNSHIFT: u8 = 0x7B; 37 | pub const REST3_PERIOD: u8 = 0x7C; 38 | pub const RUN_DOWNSHIFT_MULT: u8 = 0x7D; 39 | pub const REST_DOWNSHIFT_MULT: u8 = 0x7E; 40 | // pub const ANGLE_TUNE1: u8 = 0x0577; 41 | // pub const ANGLE_TUNE2: u8 = 0x0578; 42 | // pub const LIFT_CONFIG: u8 = 0x0C4E; 43 | -------------------------------------------------------------------------------- /crates/rktk-drivers-common/src/mouse/pmw3360/error.rs: -------------------------------------------------------------------------------- 1 | use core::fmt::Debug; 2 | 3 | use rktk::drivers::interface::{Error, ErrorKind}; 4 | 5 | #[derive(Debug)] 6 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 7 | pub enum Pmw3360Error { 8 | InvalidSignature, 9 | Spi(#[cfg_attr(feature = "defmt", defmt(Debug2Format))] SpiError), 10 | NotSupported, 11 | } 12 | 13 | impl Error for Pmw3360Error { 14 | fn kind(&self) -> ErrorKind { 15 | match self { 16 | Self::NotSupported => ErrorKind::NotSupported, 17 | _ => ErrorKind::Other, 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /crates/rktk-drivers-common/src/mouse/pmw3360/registers.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | pub const PRODUCT_ID: u8 = 0x00; 3 | pub const REVISION_ID: u8 = 0x01; 4 | pub const MOTION: u8 = 0x02; 5 | pub const DELTA_X_L: u8 = 0x03; 6 | pub const DELTA_X_H: u8 = 0x04; 7 | pub const DELTA_Y_L: u8 = 0x05; 8 | pub const DELTA_Y_H: u8 = 0x06; 9 | pub const SQUAL: u8 = 0x07; 10 | pub const RAW_DATA_SUM: u8 = 0x08; 11 | pub const MAXIMUM_RAW_DATA: u8 = 0x09; 12 | pub const MINIMUM_RAW_DATA: u8 = 0x0A; 13 | pub const SHUTTER_LOWER: u8 = 0x0B; 14 | pub const SHUTTER_UPPER: u8 = 0x0C; 15 | pub const CONTROL: u8 = 0x0D; 16 | pub const CONFIG_1: u8 = 0x0F; 17 | pub const CONFIG_2: u8 = 0x10; 18 | pub const ANGLE_TUNE: u8 = 0x11; 19 | pub const FRAME_CAPTURE: u8 = 0x12; 20 | pub const SROM_ENABLE: u8 = 0x13; 21 | pub const RUN_DOWNSHIFT: u8 = 0x14; 22 | pub const REST_1_RATE_LOWER: u8 = 0x15; 23 | pub const REST_1_RATE_UPPER: u8 = 0x16; 24 | pub const REST_1_DOWNSHIFT: u8 = 0x17; 25 | pub const REST_2_RATE_LOWER: u8 = 0x18; 26 | pub const REST_2_RATE_UPPER: u8 = 0x19; 27 | pub const REST_2_DOWNSHIFT: u8 = 0x1A; 28 | pub const REST_3_RATE_LOWER: u8 = 0x1B; 29 | pub const REST_3_RATE_UPPER: u8 = 0x1C; 30 | pub const OBSERVATION: u8 = 0x24; 31 | pub const DATA_OUT_LOWER: u8 = 0x25; 32 | pub const DATA_OUT_UPPER: u8 = 0x26; 33 | pub const RAW_DATA_DUMP: u8 = 0x29; 34 | pub const SROM_ID: u8 = 0x2A; 35 | pub const MIN_SQ_RUN: u8 = 0x2B; 36 | pub const RAW_DATA_THRESHOLD: u8 = 0x2C; 37 | pub const CONFIG_5: u8 = 0x2F; 38 | pub const POWER_UP_RESET: u8 = 0x3A; 39 | pub const SHUTDOWN: u8 = 0x3B; 40 | pub const INVERSE_PRODUCT_ID: u8 = 0x3F; 41 | pub const LIFTCUTOFF_TUNE_3: u8 = 0x41; 42 | pub const ANGLE_SNAP: u8 = 0x42; 43 | pub const LIFTCUTOFF_TUNE_1: u8 = 0x4A; 44 | pub const MOTION_BURST: u8 = 0x50; 45 | pub const LIFTCUTOFF_TUNE_TIMEOUT: u8 = 0x58; 46 | pub const LIFTCUTOFF_TUNE_MIN_LENGTH: u8 = 0x5A; 47 | pub const SROM_LOAD_BURST: u8 = 0x62; 48 | pub const LIFT_CONFIG: u8 = 0x63; 49 | pub const RAW_DATA_BURST: u8 = 0x64; 50 | pub const LIFTCUTOFF_TUNE_2: u8 = 0x65; 51 | -------------------------------------------------------------------------------- /crates/rktk-drivers-common/src/storage/mod.rs: -------------------------------------------------------------------------------- 1 | //! Storage driver implementations. 2 | 3 | pub mod flash_sequential_map; 4 | -------------------------------------------------------------------------------- /crates/rktk-drivers-common/src/trouble/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "reporter-trouble")] 2 | pub mod reporter; 3 | -------------------------------------------------------------------------------- /crates/rktk-drivers-common/src/trouble/reporter/driver.rs: -------------------------------------------------------------------------------- 1 | use core::convert::Infallible; 2 | 3 | use rktk::{ 4 | drivers::interface::{reporter::ReporterDriver, wireless::WirelessReporterDriver}, 5 | utils::Sender, 6 | }; 7 | 8 | use super::Report; 9 | 10 | pub struct TroubleReporter { 11 | pub(super) output_tx: Sender<'static, Report, 4>, 12 | } 13 | 14 | impl ReporterDriver for TroubleReporter { 15 | type Error = Infallible; 16 | 17 | fn try_send_keyboard_report( 18 | &self, 19 | report: usbd_hid::descriptor::KeyboardReport, 20 | ) -> Result<(), Self::Error> { 21 | let _ = self.output_tx.try_send(Report::Keyboard(report)); 22 | Ok(()) 23 | } 24 | 25 | fn try_send_media_keyboard_report( 26 | &self, 27 | report: usbd_hid::descriptor::MediaKeyboardReport, 28 | ) -> Result<(), Self::Error> { 29 | let _ = self.output_tx.try_send(Report::MediaKeyboard(report)); 30 | Ok(()) 31 | } 32 | 33 | fn try_send_mouse_report( 34 | &self, 35 | report: usbd_hid::descriptor::MouseReport, 36 | ) -> Result<(), Self::Error> { 37 | let _ = self.output_tx.try_send(Report::Mouse(report)); 38 | Ok(()) 39 | } 40 | 41 | async fn send_rrp_data(&self, _data: &[u8]) -> Result<(), Self::Error> { 42 | Ok(()) 43 | } 44 | 45 | fn wakeup(&self) -> Result { 46 | Ok(false) 47 | } 48 | } 49 | impl WirelessReporterDriver for TroubleReporter { 50 | type Error = Infallible; 51 | 52 | async fn clear_bond_data(&self) -> Result<(), ::Error> { 53 | Ok(()) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /crates/rktk-drivers-common/src/usb/defmt_logger/mod.rs: -------------------------------------------------------------------------------- 1 | #![allow(static_mut_refs)] 2 | 3 | mod task; 4 | 5 | use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, mutex::Mutex, signal::Signal}; 6 | pub use task::logger; 7 | 8 | /// The restore state of the critical section. 9 | static mut RESTORE: critical_section::RestoreState = critical_section::RestoreState::invalid(); 10 | 11 | /// Indicates if the logger is already taken to avoid reentries. 12 | static mut TAKEN: bool = false; 13 | 14 | /// The `defmt` encoder. 15 | static mut ENCODER: defmt::Encoder = defmt::Encoder::new(); 16 | 17 | static QUEUE: Mutex> = 18 | Mutex::new(heapless::Vec::new()); 19 | static LOG_SIGNAL: Signal = Signal::new(); 20 | 21 | /// The logger implementation. 22 | #[defmt::global_logger] 23 | pub struct USBLogger; 24 | 25 | unsafe impl defmt::Logger for USBLogger { 26 | fn acquire() { 27 | unsafe { 28 | let restore = critical_section::acquire(); 29 | if TAKEN { 30 | defmt::error!("defmt logger taken reentrantly"); 31 | defmt::panic!(); 32 | } 33 | TAKEN = true; 34 | RESTORE = restore; 35 | ENCODER.start_frame(inner); 36 | } 37 | } 38 | 39 | unsafe fn release() { 40 | unsafe { 41 | ENCODER.end_frame(inner); 42 | TAKEN = false; 43 | let restore = RESTORE; 44 | critical_section::release(restore); 45 | } 46 | } 47 | 48 | unsafe fn flush() {} 49 | 50 | unsafe fn write(bytes: &[u8]) { 51 | unsafe { 52 | ENCODER.write(bytes, inner); 53 | } 54 | } 55 | } 56 | 57 | fn inner(bytes: &[u8]) { 58 | let Ok(mut q) = QUEUE.try_lock() else { 59 | return; 60 | }; 61 | if q.extend_from_slice(bytes).is_err() { 62 | return; 63 | } 64 | 65 | LOG_SIGNAL.signal(()); 66 | } 67 | -------------------------------------------------------------------------------- /crates/rktk-drivers-common/src/usb/defmt_logger/task.rs: -------------------------------------------------------------------------------- 1 | //! Main task that runs the USB transport layer. 2 | 3 | use embassy_usb::{ 4 | class::cdc_acm::Sender, 5 | driver::{Driver, EndpointError}, 6 | }; 7 | 8 | use super::{LOG_SIGNAL, QUEUE}; 9 | 10 | /// Runs the logger task. 11 | pub async fn logger<'d, D: Driver<'d>>(mut sender: Sender<'d, D>, size: usize, use_dtr: bool) { 12 | sender.wait_connection().await; 13 | 14 | loop { 15 | if use_dtr { 16 | loop { 17 | if sender.dtr() { 18 | break; 19 | } 20 | embassy_time::Timer::after_millis(500).await; 21 | } 22 | } 23 | 24 | LOG_SIGNAL.wait().await; 25 | embassy_time::Timer::after_millis(100).await; 26 | LOG_SIGNAL.reset(); 27 | 28 | let q = core::mem::take(&mut *QUEUE.lock().await); 29 | 30 | for chunk in q.chunks(size) { 31 | if let Err(EndpointError::Disabled) = sender.write_packet(chunk).await { 32 | sender.wait_connection().await; 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /crates/rktk-drivers-common/src/usb/handler.rs: -------------------------------------------------------------------------------- 1 | use core::sync::atomic::Ordering; 2 | use embassy_usb::Handler; 3 | 4 | use super::SUSPENDED; 5 | 6 | pub struct UsbDeviceHandler {} 7 | 8 | impl UsbDeviceHandler { 9 | pub fn new() -> Self { 10 | UsbDeviceHandler {} 11 | } 12 | } 13 | 14 | // 参考: https://www.itf.co.jp/tech/road-to-usb-master/usb-status 15 | impl Handler for UsbDeviceHandler { 16 | fn enabled(&mut self, _enabled: bool) { 17 | SUSPENDED.store(false, Ordering::Release); 18 | } 19 | 20 | fn suspended(&mut self, suspended: bool) { 21 | if suspended { 22 | SUSPENDED.store(true, Ordering::Release); 23 | } else { 24 | SUSPENDED.store(false, Ordering::Release); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /crates/rktk-drivers-common/src/usb/mod.rs: -------------------------------------------------------------------------------- 1 | //! USB reporter implementation using [`embassy_usb`]. 2 | 3 | use core::sync::atomic::AtomicBool; 4 | 5 | mod builder; 6 | #[cfg(feature = "defmt-usb")] 7 | mod defmt_logger; 8 | mod driver; 9 | mod handler; 10 | mod raw_hid; 11 | mod rrp; 12 | mod task; 13 | 14 | #[cfg(feature = "usb-remote-wakeup")] 15 | type RemoteWakeupSignal = rktk::utils::Signal<()>; 16 | type ReadySignal = rktk::utils::Signal<()>; 17 | static SUSPENDED: AtomicBool = AtomicBool::new(false); 18 | 19 | pub use builder::CommonUsbReporterBuilder; 20 | /// Re-export of underlying embassy-usb driver's config type 21 | pub use embassy_usb::Config as UsbDriverConfig; 22 | 23 | /// Options for the [`CommonUsbReporterDriverBuilder`]. 24 | pub struct CommonUsbDriverConfig> { 25 | /// embassy-usb driver instance. 26 | pub driver: D, 27 | /// Config for underlying embassy-usb driver. 28 | pub driver_config: UsbDriverConfig<'static>, 29 | /// USB Poll interval for mouse in ms. 30 | pub mouse_poll_interval: u8, 31 | /// USB Poll interval for keyboard in ms. 32 | pub keyboard_poll_interval: u8, 33 | /// If this is set to true, defmt-usb logger waits for DTR signal before log output. 34 | /// This allows you to view logs recorded before the logger client is started. 35 | #[cfg(feature = "defmt-usb")] 36 | pub defmt_usb_use_dtr: bool, 37 | } 38 | 39 | impl> CommonUsbDriverConfig { 40 | /// Create usb options for the driver with default options. 41 | /// 42 | /// * `driver`: embassy-usb driver instance 43 | /// * `vid`: USB vendor ID 44 | /// * `pid`: USB product ID 45 | pub fn new(driver: D, mut driver_config: UsbDriverConfig<'static>) -> Self { 46 | driver_config.supports_remote_wakeup = cfg!(feature = "usb-remote-wakeup"); 47 | Self { 48 | driver_config, 49 | mouse_poll_interval: 1, 50 | keyboard_poll_interval: 1, 51 | driver, 52 | #[cfg(feature = "defmt-usb")] 53 | defmt_usb_use_dtr: true, 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /crates/rktk-drivers-common/src/usb/raw_hid.rs: -------------------------------------------------------------------------------- 1 | pub const RAW_HID_BUFFER_SIZE: usize = 32; 2 | 3 | use usbd_hid::descriptor::generator_prelude::*; 4 | 5 | #[gen_hid_descriptor( 6 | // same as QMK's raw hid 7 | (collection = APPLICATION, usage_page = 0xFF60, usage = 0x61) = { 8 | (usage = 0x62, logical_min = 0x0) = { 9 | #[item_settings data,variable,absolute] input_data=input; 10 | }; 11 | (usage = 0x63, logical_min = 0x0) = { 12 | #[item_settings data,variable,absolute] output_data=output; 13 | }; 14 | } 15 | )] 16 | pub struct RawHidReport { 17 | pub input_data: [u8; 32], 18 | pub output_data: [u8; 32], 19 | } 20 | -------------------------------------------------------------------------------- /crates/rktk-drivers-common/src/usb/rrp.rs: -------------------------------------------------------------------------------- 1 | pub const RRP_HID_BUFFER_SIZE: usize = 32; 2 | 3 | use usbd_hid::descriptor::generator_prelude::*; 4 | 5 | #[gen_hid_descriptor( 6 | (collection = APPLICATION, usage_page = 0xFF70, usage = 0x71) = { 7 | (usage = 0x72, logical_min = 0x0) = { 8 | #[item_settings data,variable,absolute] input_data=input; 9 | }; 10 | (usage = 0x73, logical_min = 0x0) = { 11 | #[item_settings data,variable,absolute] output_data=output; 12 | }; 13 | } 14 | )] 15 | pub struct RrpReport { 16 | pub input_data: [u8; 32], 17 | pub output_data: [u8; 32], 18 | } 19 | -------------------------------------------------------------------------------- /crates/rktk-drivers-nrf/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "thumbv7em-none-eabihf" 3 | -------------------------------------------------------------------------------- /crates/rktk-drivers-nrf/README.md: -------------------------------------------------------------------------------- 1 | # rktk-driver-nrf 2 | 3 | rktk drivers for nrf chip 4 | 5 | For more detail, see [RKTK project README](https://github.com/nazo6/rktk) 6 | 7 | ## Dependencies 8 | 9 | 'libclang' must be installed to use `sdc` feature. 10 | 11 | ## Note about `sdc` feature 12 | 13 | The `sdc` feature is not available in the crates.io version of rktk-drivers-nrf. 14 | It fails to compile when the feature is enabled. If you want to use the `sdc` 15 | feature, use the git version of this crate. 16 | -------------------------------------------------------------------------------- /crates/rktk-drivers-nrf/src/display.rs: -------------------------------------------------------------------------------- 1 | pub mod ssd1306 { 2 | use embassy_nrf::twim::{Config, Frequency}; 3 | 4 | /// Returns a recommended TWIM configuration for SSD1306. 5 | pub fn recommended_i2c_config() -> Config { 6 | let mut i2c_config = Config::default(); 7 | i2c_config.frequency = Frequency::K400; 8 | i2c_config 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /crates/rktk-drivers-nrf/src/esb/mod.rs: -------------------------------------------------------------------------------- 1 | pub use esb_ng::{Addresses, ConfigBuilder, Error}; 2 | 3 | pub mod dongle; 4 | pub mod reporter; 5 | 6 | #[derive(Default)] 7 | pub struct Config { 8 | pub addresses: Addresses, 9 | pub config: ConfigBuilder, 10 | } 11 | 12 | pub fn create_address(channel: u8) -> Result { 13 | Addresses::new( 14 | [0xE7, 0xE7, 0xE7, 0xE7], 15 | [0xC2, 0xC2, 0xC2, 0xC2], 16 | [0xE7, 0xC2, 0xC3, 0xC4], 17 | [0xC5, 0xC6, 0xC7, 0xC8], 18 | channel, 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /crates/rktk-drivers-nrf/src/keyscan/flex_pin.rs: -------------------------------------------------------------------------------- 1 | use embassy_nrf::{ 2 | Peri, 3 | gpio::{Flex, OutputDrive, Pin, Pull as NrfPull}, 4 | }; 5 | pub use rktk_drivers_common::keyscan::duplex_matrix::ScanDir; 6 | use rktk_drivers_common::keyscan::flex_pin::{FlexPin, Pull}; 7 | 8 | /// Wrapper over flex pin that implements rktk_drivers_common's [`FlexPin`] trait. 9 | pub struct NrfFlexPin<'a> { 10 | pin: Flex<'a>, 11 | pull: NrfPull, 12 | drive: OutputDrive, 13 | } 14 | 15 | impl<'a> NrfFlexPin<'a> { 16 | pub fn new(pin: Peri<'a, impl Pin>) -> Self { 17 | Self { 18 | pin: Flex::new(pin), 19 | pull: NrfPull::None, 20 | drive: OutputDrive::Standard, 21 | } 22 | } 23 | } 24 | 25 | impl FlexPin for NrfFlexPin<'_> { 26 | fn set_as_input(&mut self) { 27 | #[allow(clippy::needless_match)] 28 | let pull = match self.pull { 29 | NrfPull::Up => NrfPull::Up, 30 | NrfPull::Down => NrfPull::Down, 31 | NrfPull::None => NrfPull::None, 32 | }; 33 | self.pin.set_as_input(pull); 34 | } 35 | 36 | fn set_as_output(&mut self) { 37 | self.pin.set_as_output(self.drive); 38 | } 39 | 40 | fn set_low(&mut self) { 41 | self.pin.set_low(); 42 | } 43 | 44 | fn set_high(&mut self) { 45 | self.pin.set_high(); 46 | } 47 | 48 | fn is_high(&self) -> bool { 49 | self.pin.is_high() 50 | } 51 | 52 | fn is_low(&self) -> bool { 53 | self.pin.is_low() 54 | } 55 | 56 | async fn wait_for_high(&mut self) { 57 | self.pin.wait_for_high().await; 58 | } 59 | 60 | async fn wait_for_low(&mut self) { 61 | self.pin.wait_for_low().await; 62 | } 63 | 64 | fn set_pull(&mut self, pull: Pull) { 65 | self.pull = match pull { 66 | Pull::Up => NrfPull::Up, 67 | Pull::Down => NrfPull::Down, 68 | }; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /crates/rktk-drivers-nrf/src/keyscan/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod flex_pin; 2 | -------------------------------------------------------------------------------- /crates/rktk-drivers-nrf/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | //! 3 | //! This crate provides the rktk drivers for the nRF series microcontrollers. Currently, only nRF52840 4 | //! is enabled, but it should be easy to add support for other nRF series microcontrollers. 5 | //! 6 | //! Many drivers is in this crate are just convenient wrappers over [`rktk_drivers_common`], but 7 | //! implements some original drivers like BLE and Uart split driver. 8 | //! 9 | //! NOTE: This crate uses unreleased version of `nrf-softdevice` and such dependency is not accepted by crates.io. 10 | //! So if you want to use `softdevice` feature (needed for BLE), use git version of this crate. 11 | //! 12 | //! ## Feature flags 13 | #![doc = document_features::document_features!()] 14 | #![no_std] 15 | #![cfg_attr(doc, feature(doc_cfg))] 16 | 17 | pub mod display; 18 | #[cfg(feature = "esb")] 19 | pub mod esb; 20 | pub mod keyscan; 21 | pub mod mouse; 22 | pub mod rgb; 23 | #[cfg(feature = "sdc")] 24 | pub mod sdc; 25 | #[cfg(feature = "softdevice")] 26 | pub mod softdevice; 27 | pub mod split; 28 | pub mod system; 29 | -------------------------------------------------------------------------------- /crates/rktk-drivers-nrf/src/mouse.rs: -------------------------------------------------------------------------------- 1 | pub mod paw3395 { 2 | use embassy_nrf::spim; 3 | 4 | pub fn recommended_spi_config() -> spim::Config { 5 | let mut config = spim::Config::default(); 6 | config.frequency = spim::Frequency::M8; 7 | config.mode.polarity = spim::Polarity::IdleHigh; 8 | config.mode.phase = spim::Phase::CaptureOnSecondTransition; 9 | config 10 | } 11 | } 12 | pub mod pmw3360 { 13 | use embassy_nrf::spim; 14 | 15 | pub fn recommended_spi_config() -> spim::Config { 16 | let mut config = spim::Config::default(); 17 | config.frequency = spim::Frequency::M8; 18 | config.mode.polarity = spim::Polarity::IdleHigh; 19 | config.mode.phase = spim::Phase::CaptureOnSecondTransition; 20 | config 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /crates/rktk-drivers-nrf/src/rgb/mod.rs: -------------------------------------------------------------------------------- 1 | // pub mod ws2812_bitbang; 2 | pub mod ws2812_pwm; 3 | -------------------------------------------------------------------------------- /crates/rktk-drivers-nrf/src/softdevice/ble/constant.rs: -------------------------------------------------------------------------------- 1 | use nrf_softdevice::ble::Uuid; 2 | 3 | pub const DEVICE_INFORMATION: Uuid = Uuid::new_16(0x180a); 4 | pub const BATTERY_SERVICE: Uuid = Uuid::new_16(0x180f); 5 | 6 | pub const BATTERY_LEVEL: Uuid = Uuid::new_16(0x2a19); 7 | pub const MODEL_NUMBER: Uuid = Uuid::new_16(0x2a24); 8 | pub const SERIAL_NUMBER: Uuid = Uuid::new_16(0x2a25); 9 | pub const FIRMWARE_REVISION: Uuid = Uuid::new_16(0x2a26); 10 | pub const HARDWARE_REVISION: Uuid = Uuid::new_16(0x2a27); 11 | pub const SOFTWARE_REVISION: Uuid = Uuid::new_16(0x2a28); 12 | pub const MANUFACTURER_NAME: Uuid = Uuid::new_16(0x2a29); 13 | pub const PNP_ID: Uuid = Uuid::new_16(0x2a50); 14 | -------------------------------------------------------------------------------- /crates/rktk-drivers-nrf/src/softdevice/ble/server.rs: -------------------------------------------------------------------------------- 1 | use nrf_softdevice::{ 2 | ble::{ 3 | gatt_server::{self, RegisterError, WriteOp}, 4 | Connection, 5 | }, 6 | Softdevice, 7 | }; 8 | 9 | use super::services::{ 10 | battery::BatteryService, 11 | device_information::{DeviceInformation, DeviceInformationService, PnPID, VidSource}, 12 | hid::HidService, 13 | }; 14 | 15 | pub struct Server { 16 | pub _dis: DeviceInformationService, 17 | pub bas: BatteryService, 18 | pub hid: HidService, 19 | } 20 | 21 | impl Server { 22 | pub fn new( 23 | sd: &mut Softdevice, 24 | device_information: DeviceInformation, 25 | ) -> Result { 26 | let dis = DeviceInformationService::new( 27 | sd, 28 | &PnPID { 29 | vid_source: VidSource::UsbIF, 30 | vendor_id: 0xDEAD, 31 | product_id: 0xBEEF, 32 | product_version: 0x0000, 33 | }, 34 | device_information, 35 | )?; 36 | 37 | let bas = BatteryService::new(sd)?; 38 | let _ = bas.battery_level_set(sd, 100); 39 | 40 | let hid = HidService::new(sd)?; 41 | 42 | Ok(Self { 43 | _dis: dis, 44 | bas, 45 | hid, 46 | }) 47 | } 48 | } 49 | 50 | impl gatt_server::Server for Server { 51 | type Event = (); 52 | 53 | fn on_write( 54 | &self, 55 | conn: &Connection, 56 | handle: u16, 57 | _op: WriteOp, 58 | _offset: usize, 59 | data: &[u8], 60 | ) -> Option { 61 | self.hid.on_write(conn, handle, data); 62 | self.bas.on_write(handle, data); 63 | None 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /crates/rktk-drivers-nrf/src/softdevice/ble/services/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod battery; 2 | pub mod device_information; 3 | pub mod hid; 4 | -------------------------------------------------------------------------------- /crates/rktk-drivers-nrf/src/softdevice/flash.rs: -------------------------------------------------------------------------------- 1 | use nrf_softdevice::{Flash, Softdevice}; 2 | use rktk::utils::Mutex; 3 | use rktk_drivers_common::storage::flash_sequential_map::{ 4 | sequential_storage::cache::NoCache, FlashSequentialMapStorage, 5 | }; 6 | use static_cell::StaticCell; 7 | 8 | pub type SharedFlash = Mutex; 9 | 10 | static FLASH: StaticCell = StaticCell::new(); 11 | 12 | /// Get steal from softdevice instance. 13 | /// 14 | /// This function must be called only once. Otherwise, it will panic. 15 | pub fn get_flash(sd: &Softdevice) -> (&'static SharedFlash, Mutex) { 16 | ( 17 | FLASH.init(Mutex::new(nrf_softdevice::Flash::take(sd))), 18 | Mutex::new(NoCache::new()), 19 | ) 20 | } 21 | 22 | // 4kb * 160 = 640kb 23 | const FLASH_START: u32 = 4096 * 160; 24 | // 4kb * 3 = 12kb 25 | const FLASH_END: u32 = FLASH_START + 4096 * 3; 26 | 27 | pub fn create_storage_driver<'a>( 28 | flash: &'a SharedFlash, 29 | cache: &'a Mutex, 30 | ) -> FlashSequentialMapStorage<'a, Flash> { 31 | FlashSequentialMapStorage { 32 | flash, 33 | flash_range: FLASH_START..FLASH_END, 34 | cache, 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /crates/rktk-drivers-nrf/src/softdevice/split/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod central; 2 | mod packet; 3 | pub mod peripheral; 4 | 5 | const RKTK_SPLIT_SERVICE_ID: [u8; 16] = [ 6 | 0xeb, 0x04, 0x8b, 0xfd, 0x5b, 0x03, 0x21, 0xb5, 0xeb, 0x11, 0x65, 0x2f, 0x18, 0xce, 0x9c, 0x82, 7 | ]; 8 | const PSM: u16 = 0x2349; 9 | -------------------------------------------------------------------------------- /crates/rktk-drivers-nrf/src/softdevice/split/packet.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::manual_div_ceil)] 2 | 3 | use core::ptr::NonNull; 4 | 5 | use atomic_pool::{Box, pool}; 6 | use nrf_softdevice::ble::l2cap; 7 | 8 | pool!(pub PacketPool: [[u8; 64]; 10]); 9 | 10 | #[derive(Debug)] 11 | pub struct Packet { 12 | len: u8, 13 | buf: Box, 14 | } 15 | 16 | impl Packet { 17 | pub fn new(data: &[u8]) -> Self { 18 | let Some(mut buf) = Box::::new([0; 64]) else { 19 | panic!("PacketPool allocation failed"); 20 | }; 21 | buf[..data.len()].copy_from_slice(data); 22 | Packet { 23 | len: data.len() as u8, 24 | buf, 25 | } 26 | } 27 | 28 | pub fn as_bytes(&self) -> &[u8] { 29 | &self.buf[..self.len as usize] 30 | } 31 | } 32 | 33 | impl l2cap::Packet for Packet { 34 | const MTU: usize = 64; 35 | 36 | fn allocate() -> Option> { 37 | if let Some(buf) = Box::::new([0; 64]) { 38 | let ptr = Box::into_raw(buf).cast::(); 39 | // info!("allocate {}", ptr.as_ptr() as u32); 40 | Some(ptr) 41 | } else { 42 | None 43 | } 44 | } 45 | 46 | fn into_raw_parts(self) -> (NonNull, usize) { 47 | let ptr = Box::into_raw(self.buf).cast::(); 48 | let len = self.len; 49 | // info!("into_raw_parts {}", ptr.as_ptr() as u32); 50 | (ptr, len as usize) 51 | } 52 | 53 | unsafe fn from_raw_parts(ptr: NonNull, len: usize) -> Self { 54 | // info!("from_raw_parts {}", ptr.as_ptr() as u32); 55 | Self { 56 | len: len as u8, 57 | buf: unsafe { Box::from_raw(ptr.cast::<[u8; 64]>()) }, 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /crates/rktk-drivers-nrf/src/split/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod uart_full_duplex; 2 | pub mod uart_half_duplex; 3 | -------------------------------------------------------------------------------- /crates/rktk-drivers-nrf/src/split/uart_full_duplex.rs: -------------------------------------------------------------------------------- 1 | use embassy_nrf::buffered_uarte::BufferedUarte; 2 | use embedded_io_async::Write as _; 3 | use rktk::drivers::interface::split::SplitDriver; 4 | 5 | #[derive(Debug)] 6 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 7 | pub enum UartFullDuplexSplitDriverError { 8 | GeneralError(&'static str), 9 | } 10 | 11 | impl rktk::drivers::interface::Error for UartFullDuplexSplitDriverError {} 12 | 13 | pub struct UartFullDuplexSplitDriver { 14 | uarte: BufferedUarte<'static>, 15 | } 16 | 17 | impl UartFullDuplexSplitDriver { 18 | pub fn new(uarte: BufferedUarte<'static>) -> Self { 19 | Self { uarte } 20 | } 21 | } 22 | 23 | impl SplitDriver for UartFullDuplexSplitDriver { 24 | type Error = UartFullDuplexSplitDriverError; 25 | 26 | async fn recv(&mut self, buf: &mut [u8], _is_master: bool) -> Result { 27 | let size = self 28 | .uarte 29 | .read(buf) 30 | .await 31 | .map_err(|_| UartFullDuplexSplitDriverError::GeneralError("Read error"))?; 32 | Ok(size) 33 | } 34 | 35 | async fn send_all(&mut self, buf: &[u8], _is_master: bool) -> Result<(), Self::Error> { 36 | self.uarte 37 | .write_all(buf) 38 | .await 39 | .map_err(|_| UartFullDuplexSplitDriverError::GeneralError("Write error")) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /crates/rktk-drivers-nrf/src/system.rs: -------------------------------------------------------------------------------- 1 | use embassy_nrf::gpio::{Level, Output}; 2 | use rktk::{drivers::interface::system::SystemDriver, utils::Mutex}; 3 | 4 | pub struct NrfSystemDriver<'d> { 5 | vcc_cutoff: Option, Level)>>, 6 | } 7 | 8 | impl<'d> NrfSystemDriver<'d> { 9 | pub fn new(vcc_cutoff: Option<(Output<'d>, Level)>) -> Self { 10 | Self { 11 | vcc_cutoff: vcc_cutoff.map(|(pin, level)| Mutex::new((pin, level))), 12 | } 13 | } 14 | } 15 | 16 | impl SystemDriver for NrfSystemDriver<'_> { 17 | #[cfg(feature = "power")] 18 | async fn power_off(&self) { 19 | { 20 | if let Some(vcc_cutoff) = &self.vcc_cutoff { 21 | let mut out = vcc_cutoff.lock().await; 22 | let level = out.1; 23 | out.0.set_level(level); 24 | embassy_time::Timer::after_millis(50).await; 25 | } 26 | } 27 | 28 | embassy_nrf::power::set_system_off(); 29 | cortex_m::asm::udf(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /crates/rktk-drivers-rp/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "thumbv6m-none-eabi" 3 | -------------------------------------------------------------------------------- /crates/rktk-drivers-rp/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rktk-drivers-rp" 3 | description = "rktk drivers for RP chip" 4 | version.workspace = true 5 | license.workspace = true 6 | edition.workspace = true 7 | authors.workspace = true 8 | repository.workspace = true 9 | 10 | [package.metadata.docs.rs] 11 | features = ["embassy-rp/rp2040"] 12 | 13 | [dependencies] 14 | rktk = { workspace = true } 15 | rktk-drivers-common = { workspace = true } 16 | 17 | rktk-log = { workspace = true } 18 | log = { workspace = true, optional = true } 19 | defmt = { workspace = true, optional = true } 20 | 21 | embedded-storage-async = { workspace = true } 22 | 23 | embassy-embedded-hal = { workspace = true } 24 | embassy-time = { workspace = true } 25 | embassy-usb = { workspace = true } 26 | embassy-executor = { workspace = true } 27 | embassy-sync = { workspace = true } 28 | embassy-futures = { workspace = true } 29 | embassy-rp = { workspace = true, features = [ 30 | "time-driver", 31 | "critical-section-impl", 32 | ] } 33 | portable-atomic = { workspace = true } 34 | 35 | ssd1306 = { workspace = true } 36 | display-interface = { workspace = true } 37 | 38 | pio = { workspace = true } 39 | 40 | fixed = { workspace = true } 41 | fixed-macro = { workspace = true } 42 | static_cell = { workspace = true } 43 | 44 | [features] 45 | _check = ["embassy-rp/rp2040", "single-core"] 46 | 47 | default = ["single-core"] 48 | 49 | single-core = ["portable-atomic/unsafe-assume-single-core"] 50 | 51 | defmt = [ 52 | "dep:defmt", 53 | "rktk/defmt", 54 | "rktk-log/defmt", 55 | "rktk-drivers-common/defmt", 56 | ] 57 | log = ["dep:log"] 58 | -------------------------------------------------------------------------------- /crates/rktk-drivers-rp/README.md: -------------------------------------------------------------------------------- 1 | # rktk-driver-rp 2 | 3 | rktk drivers for RP2040 4 | 5 | For more detail, see [RKTK project README](https://github.com/nazo6/rktk) 6 | -------------------------------------------------------------------------------- /crates/rktk-drivers-rp/src/display.rs: -------------------------------------------------------------------------------- 1 | pub mod ssd1306 { 2 | use embassy_rp::i2c::Config; 3 | 4 | pub fn recommended_i2c_config() -> Config { 5 | let mut i2c_config = Config::default(); 6 | i2c_config.frequency = 400_000; 7 | i2c_config 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /crates/rktk-drivers-rp/src/flash.rs: -------------------------------------------------------------------------------- 1 | pub use embassy_sync; 2 | pub use rktk_drivers_common::storage::flash_sequential_map::FlashSequentialMapStorage; 3 | pub use rktk_drivers_common::storage::flash_sequential_map::sequential_storage; 4 | 5 | #[macro_export] 6 | macro_rules! init_storage { 7 | ($storage:ident, $flash:expr, $dma:expr, $size:expr) => { 8 | let flash = ::embassy_rp::flash::Flash::<_, _, $size>::new($flash, $dma); 9 | let flash = $crate::flash::embassy_sync::mutex::Mutex::new(flash); 10 | let cache = $crate::flash::embassy_sync::mutex::Mutex::new( 11 | $crate::flash::sequential_storage::cache::NoCache::new(), 12 | ); 13 | 14 | const FLASH_START: u32 = 1024 * 1024; 15 | const FLASH_END: u32 = 3 * 1024 * 1024; 16 | 17 | let $storage = $crate::flash::FlashSequentialMapStorage { 18 | flash: &flash, 19 | flash_range: FLASH_START..FLASH_END, 20 | cache: &cache, 21 | }; 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /crates/rktk-drivers-rp/src/keyscan/flex_pin.rs: -------------------------------------------------------------------------------- 1 | use embassy_rp::{ 2 | Peri, 3 | gpio::{Flex, Pin}, 4 | }; 5 | use rktk_drivers_common::keyscan::flex_pin::{FlexPin, Pull}; 6 | 7 | /// Wrapper over flex pin that implements rktk_drivers_common's [`FlexPin`] trait. 8 | pub struct RpFlexPin<'a>(Flex<'a>); 9 | 10 | impl<'a> RpFlexPin<'a> { 11 | pub fn new(pin: Peri<'a, impl Pin>) -> Self { 12 | Self(Flex::new(pin)) 13 | } 14 | } 15 | 16 | impl FlexPin for RpFlexPin<'_> { 17 | fn set_as_input(&mut self) { 18 | self.0.set_as_input(); 19 | } 20 | 21 | fn set_as_output(&mut self) { 22 | self.0.set_as_output(); 23 | } 24 | 25 | fn set_low(&mut self) { 26 | self.0.set_low(); 27 | } 28 | 29 | fn set_high(&mut self) { 30 | self.0.set_high(); 31 | } 32 | 33 | fn is_high(&self) -> bool { 34 | self.0.is_high() 35 | } 36 | 37 | fn is_low(&self) -> bool { 38 | self.0.is_low() 39 | } 40 | 41 | async fn wait_for_high(&mut self) { 42 | self.0.wait_for_high().await; 43 | } 44 | 45 | async fn wait_for_low(&mut self) { 46 | self.0.wait_for_low().await; 47 | } 48 | 49 | fn set_pull(&mut self, pull: Pull) { 50 | self.0.set_pull(match pull { 51 | Pull::Up => embassy_rp::gpio::Pull::Up, 52 | Pull::Down => embassy_rp::gpio::Pull::Down, 53 | }); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /crates/rktk-drivers-rp/src/keyscan/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod flex_pin; 2 | -------------------------------------------------------------------------------- /crates/rktk-drivers-rp/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | //! This crate provides the rktk drivers for the RP2040 chip. 3 | //! 4 | //! Many drivers is in this crate are just convenient wrappers over [`rktk_drivers_common`], but 5 | //! implements some original drivers like [`split::pio_half_duplex`]. 6 | #![no_std] 7 | 8 | pub mod display; 9 | pub mod flash; 10 | pub mod keyscan; 11 | pub mod mouse; 12 | pub mod rgb; 13 | pub mod split; 14 | pub mod system; 15 | -------------------------------------------------------------------------------- /crates/rktk-drivers-rp/src/mouse.rs: -------------------------------------------------------------------------------- 1 | pub mod paw3395 { 2 | use embassy_rp::spi::{Config, Phase, Polarity}; 3 | 4 | pub fn recommended_spi_config() -> Config { 5 | let mut spi_config = Config::default(); 6 | spi_config.frequency = 7_000_000; 7 | spi_config.polarity = Polarity::IdleHigh; 8 | spi_config.phase = Phase::CaptureOnSecondTransition; 9 | 10 | spi_config 11 | } 12 | } 13 | pub mod pmw3360 { 14 | use embassy_rp::spi::{Config, Phase, Polarity}; 15 | 16 | pub fn recommended_spi_config() -> Config { 17 | let mut spi_config = Config::default(); 18 | spi_config.frequency = 7_000_000; 19 | spi_config.polarity = Polarity::IdleHigh; 20 | spi_config.phase = Phase::CaptureOnSecondTransition; 21 | 22 | spi_config 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /crates/rktk-drivers-rp/src/rgb/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod ws2812_pio; 2 | -------------------------------------------------------------------------------- /crates/rktk-drivers-rp/src/split/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod pio_half_duplex; 2 | -------------------------------------------------------------------------------- /crates/rktk-drivers-rp/src/system.rs: -------------------------------------------------------------------------------- 1 | use core::{mem::MaybeUninit, ptr::write_volatile}; 2 | 3 | use embassy_time::{Duration, Timer}; 4 | use rktk::drivers::interface::system::SystemDriver; 5 | 6 | const BOOTLOADER_MAGIC: u32 = 0xABCD_EF01; 7 | 8 | #[unsafe(link_section = ".uninit")] 9 | static mut FLAG: MaybeUninit = MaybeUninit::uninit(); 10 | 11 | pub struct RpSystemDriver; 12 | 13 | impl SystemDriver for RpSystemDriver { 14 | async fn double_reset_usb_boot(&self, timeout: Duration) { 15 | unsafe { 16 | let flag = core::ptr::read(&raw const FLAG); 17 | let flag = flag.assume_init(); 18 | 19 | if flag == BOOTLOADER_MAGIC { 20 | // double reset is triggered. rebooting to bootloader. 21 | write_volatile(&raw mut FLAG, MaybeUninit::new(0)); 22 | embassy_rp::rom_data::reset_to_usb_boot(0, 0); 23 | } 24 | 25 | // write flag and wait for double reset 26 | write_volatile(&raw mut FLAG, MaybeUninit::new(BOOTLOADER_MAGIC)); 27 | Timer::after(timeout).await; 28 | // double-tap reset is not performed. reset flag and normal start 29 | write_volatile(&raw mut FLAG, MaybeUninit::new(0)); 30 | } 31 | } 32 | 33 | fn reset(&self) { 34 | // not supported 35 | } 36 | 37 | fn reset_to_bootloader(&self) { 38 | embassy_rp::rom_data::reset_to_usb_boot(0, 0); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /crates/rktk-log/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /crates/rktk-log/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rktk-log" 3 | description = "rktk log management" 4 | authors.workspace = true 5 | license.workspace = true 6 | version.workspace = true 7 | edition.workspace = true 8 | repository.workspace = true 9 | 10 | [dependencies] 11 | defmt = { workspace = true, optional = true } 12 | 13 | [features] 14 | _check = ["defmt", "log"] 15 | defmt = ["dep:defmt"] 16 | log = [] 17 | -------------------------------------------------------------------------------- /crates/rktk-log/README.md: -------------------------------------------------------------------------------- 1 | # rktk-log 2 | 3 | logger for rktk 4 | 5 | ## Credits 6 | 7 | - Most code are from [defmt-or-log](https://github.com/t-moe/defmt-or-log). 8 | 9 | ## About 10 | 11 | crate which uses macro in this crate must add below deps and features to 12 | Cargo.toml to work correctly. 13 | 14 | ```toml 15 | [dependencies] 16 | rktk-log = { version = "" } 17 | log = { version="", optional = true } 18 | defmt = { version="", optional = true } 19 | 20 | [features] 21 | defmt = ["dep:defmt", "rktk-log/defmt"] 22 | log = ["dep:log", "rktk-log/log"] 23 | ``` 24 | 25 | Also you should consider to modify defmt to feature if your depedency provides 26 | feature for defmt. 27 | 28 | ## Viewing defmt-usb log 29 | 30 | By using 31 | [my defmt-print fork](https://github.com/nazo6/defmt/tree/defmt-print-serial), 32 | you can print log from usb (serialport). 33 | 34 | ### Usage 35 | 36 | ```sh 37 | # Install 38 | cargo install --git https://github.com/nazo6/defmt --branch defmt-print-serial defmt-print 39 | 40 | # Connect 41 | defmt-print serial COM1 # Change COM1 to your serial port 42 | ``` 43 | 44 | To find com port, you can use software such as 45 | [USB Device Tree Viewer](https://www.uwe-sieber.de/usbtreeview_e.html) on 46 | windows. 47 | -------------------------------------------------------------------------------- /crates/rktk-log/src/helper.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug)] 2 | pub struct Debug2Format<'a, T: core::fmt::Debug + ?Sized>(pub &'a T); 3 | 4 | #[cfg(feature = "defmt")] 5 | impl defmt::Format for Debug2Format<'_, T> { 6 | fn format(&self, f: defmt::Formatter<'_>) { 7 | defmt::Debug2Format(self.0).format(f) 8 | } 9 | } 10 | 11 | pub struct Display2Format<'a, T: core::fmt::Display + ?Sized>(pub &'a T); 12 | 13 | impl core::fmt::Display for Display2Format<'_, T> { 14 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 15 | core::fmt::Display::fmt(&self.0, f) 16 | } 17 | } 18 | 19 | #[cfg(feature = "defmt")] 20 | impl defmt::Format for Display2Format<'_, T> { 21 | fn format(&self, f: defmt::Formatter<'_>) { 22 | defmt::Display2Format(self.0).format(f) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /crates/rktk-log/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | 3 | pub mod helper; 4 | #[doc(hidden)] 5 | pub mod macros; 6 | 7 | #[cfg(not(feature = "defmt"))] 8 | pub trait MaybeFormat: core::fmt::Debug {} 9 | #[cfg(not(feature = "defmt"))] 10 | impl MaybeFormat for T where T: core::fmt::Debug {} 11 | 12 | #[cfg(feature = "defmt")] 13 | pub trait MaybeFormat: core::fmt::Debug + defmt::Format {} 14 | #[cfg(feature = "defmt")] 15 | impl MaybeFormat for T where T: core::fmt::Debug + defmt::Format {} 16 | -------------------------------------------------------------------------------- /crates/rktk-rrp/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rktk-rrp" 3 | description = "rrp - RKTK Remote Protocol" 4 | authors.workspace = true 5 | license.workspace = true 6 | version.workspace = true 7 | edition.workspace = true 8 | repository.workspace = true 9 | 10 | [package.metadata.docs.rs] 11 | all-features = true 12 | 13 | [dependencies] 14 | futures = { workspace = true } 15 | kmsm = { workspace = true, features = ["postcard"] } 16 | postcard = { workspace = true, features = ["experimental-derive"] } 17 | 18 | serde = { workspace = true, features = ["derive"] } 19 | serde_with = { workspace = true } 20 | macro_rules_attribute = { workspace = true } 21 | thiserror = { workspace = true } 22 | 23 | # server deps 24 | heapless = { workspace = true, optional = true } 25 | embedded-io-async = { workspace = true, optional = true } 26 | 27 | # client deps 28 | wasm-bindgen = { workspace = true, optional = true } 29 | 30 | [dev-dependencies] 31 | tokio = { workspace = true, features = ["io-std", "io-util", "macros", "rt"] } 32 | 33 | [features] 34 | default = [] 35 | 36 | server = ["dep:heapless", "dep:embedded-io-async"] 37 | client = ["std"] 38 | 39 | std = ["postcard/use-std", "futures/std"] 40 | 41 | _check = ["server", "client"] 42 | -------------------------------------------------------------------------------- /crates/rktk-rrp/README.md: -------------------------------------------------------------------------------- 1 | # rrp - rktk remap protocol 2 | -------------------------------------------------------------------------------- /crates/rktk-rrp/src/client.rs: -------------------------------------------------------------------------------- 1 | //! rrp client. uses std. 2 | 3 | use core::fmt::Display; 4 | 5 | use crate::transport::{ReadTransport, TransportError, WriteTransport}; 6 | 7 | /// Client to make requests to the rrp server. 8 | pub struct Client { 9 | pub(crate) reader: RT, 10 | pub(crate) writer: WT, 11 | } 12 | 13 | impl Client { 14 | pub fn new(reader: RT, writer: WT) -> Self { 15 | Self { reader, writer } 16 | } 17 | } 18 | 19 | #[derive(Debug, thiserror::Error)] 20 | pub enum ClientError { 21 | #[error(transparent)] 22 | Transport(#[from] TransportError), 23 | #[error("failed: status={status}, message={message}")] 24 | Failed { status: u8, message: String }, 25 | } 26 | -------------------------------------------------------------------------------- /crates/rktk-rrp/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | #![cfg_attr(not(feature = "std"), no_std)] 3 | #![cfg_attr(doc, feature(doc_cfg))] 4 | 5 | #[cfg(all(not(feature = "client"), not(feature = "server")))] 6 | compile_error!("At least, one of the `client` or `server` features should be enabled"); 7 | 8 | #[cfg(feature = "client")] 9 | pub mod client; 10 | pub mod endpoints; 11 | #[cfg(feature = "server")] 12 | pub mod server; 13 | pub mod transport; 14 | 15 | mod macros; 16 | 17 | #[cfg(test)] 18 | mod tests; 19 | -------------------------------------------------------------------------------- /crates/rktk-rrp/src/macros/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "client")] 2 | mod client; 3 | #[cfg(feature = "server")] 4 | mod server; 5 | 6 | macro_rules! generate_impls { 7 | ($($endpoint_id:tt: $endpoint_name:ident($req_kind:tt) -> $res_kind:tt;)*) => { 8 | #[cfg(feature = "server")] 9 | pub mod server_generated { 10 | $crate::macros::server::generate_server_handlers! { 11 | $($endpoint_id: $endpoint_name($req_kind: $crate::endpoints::$endpoint_name::Request) -> $res_kind: $crate::endpoints::$endpoint_name::Response;)* 12 | } 13 | } 14 | 15 | #[cfg(feature = "client")] 16 | pub mod client_generated { 17 | $crate::macros::client::generate_client! { 18 | $($endpoint_id: $endpoint_name($req_kind: $crate::endpoints::$endpoint_name::Request) -> $res_kind: $crate::endpoints::$endpoint_name::Response;)* 19 | } 20 | } 21 | }; 22 | } 23 | 24 | #[cfg(not(test))] 25 | generate_impls!( 26 | 0: get_keyboard_info(normal) -> normal; 27 | 1: get_layout_json(normal) -> stream; 28 | 2: get_keymaps(normal) -> stream; 29 | 3: set_keymaps(stream) -> normal; 30 | 4: get_keymap_config(normal) -> normal; 31 | 5: set_keymap_config(normal) -> normal; 32 | 6: get_now(normal) -> normal; 33 | 7: get_log(normal) -> stream; 34 | ); 35 | 36 | #[cfg(test)] 37 | generate_impls!( 38 | 0: get_keyboard_info(normal) -> normal; 39 | 1: get_layout_json(normal) -> stream; 40 | 2: get_keymaps(normal) -> stream; 41 | 3: set_keymaps(stream) -> normal; 42 | 4: get_keymap_config(normal) -> normal; 43 | 5: set_keymap_config(normal) -> normal; 44 | 6: get_now(normal) -> normal; 45 | 7: get_log(normal) -> stream; 46 | 8: test_normal_normal(normal) -> normal; 47 | 9: test_stream_normal(stream) -> normal; 48 | 10: test_normal_stream(normal) -> stream; 49 | 11: test_stream_stream(stream) -> stream; 50 | ); 51 | -------------------------------------------------------------------------------- /crates/rktk-rrp/src/server.rs: -------------------------------------------------------------------------------- 1 | pub use crate::macros::server_generated::ServerHandlers; 2 | use crate::transport::read::ReadTransportExt as _; 3 | use crate::transport::*; 4 | 5 | pub struct Server> { 6 | pub(crate) reader: RT, 7 | pub(crate) writer: WT, 8 | pub(crate) handlers: H, 9 | } 10 | 11 | impl> 12 | Server 13 | { 14 | pub fn new(reader: RT, writer: WT, handlers: H) -> Self { 15 | Self { 16 | reader, 17 | writer, 18 | handlers, 19 | } 20 | } 21 | 22 | pub async fn start(&mut self) { 23 | loop { 24 | let _ = self.process_request::().await; 25 | } 26 | } 27 | 28 | async fn process_request( 29 | &mut self, 30 | ) -> Result<(), TransportError> { 31 | let req_header = self.reader.recv_request_header().await?; 32 | 33 | self.handle::(req_header).await?; 34 | 35 | Ok(()) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /crates/rktk-rrp/src/tests/test_server.rs: -------------------------------------------------------------------------------- 1 | use core::fmt::Display; 2 | 3 | use futures::StreamExt as _; 4 | use futures::{stream, Stream}; 5 | 6 | use crate::macros::server_generated::ServerHandlers; 7 | use crate::transport::error::ReceiveError; 8 | 9 | pub struct Handlers; 10 | 11 | impl ServerHandlers for Handlers { 12 | type Error = &'static str; 13 | 14 | async fn test_normal_normal(&mut self, req: String) -> Result { 15 | Ok(req) 16 | } 17 | 18 | async fn test_normal_stream( 19 | &mut self, 20 | req: Vec, 21 | ) -> Result, Self::Error> { 22 | Ok(stream::iter(req)) 23 | } 24 | 25 | async fn test_stream_normal( 26 | &mut self, 27 | req: impl Stream>>, 28 | ) -> Result, Self::Error> { 29 | Ok(req.filter_map(|x| async move { x.ok() }).collect().await) 30 | } 31 | 32 | async fn test_stream_stream( 33 | &mut self, 34 | req: impl Stream>>, 35 | ) -> Result, Self::Error> { 36 | Ok(req.filter_map(|x| async move { x.ok() })) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /crates/rktk-rrp/src/tests/test_transport.rs: -------------------------------------------------------------------------------- 1 | use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _, DuplexStream}; 2 | 3 | use crate::transport::{ReadTransport, WriteTransport}; 4 | 5 | pub struct TestReader(pub DuplexStream); 6 | impl ReadTransport for TestReader { 7 | type Error = std::io::Error; 8 | 9 | async fn read(&mut self, buf: &mut [u8]) -> Result { 10 | self.0.read(buf).await 11 | } 12 | } 13 | 14 | pub struct TestWriter(pub DuplexStream); 15 | impl WriteTransport for TestWriter { 16 | type Error = std::io::Error; 17 | 18 | async fn write(&mut self, buf: &[u8]) -> Result { 19 | self.0.write(buf).await 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /crates/rktk-rrp/src/transport/error.rs: -------------------------------------------------------------------------------- 1 | use core::fmt::Display; 2 | 3 | #[derive(Debug, thiserror::Error)] 4 | pub enum TransportError { 5 | #[error(transparent)] 6 | RecvError(#[from] ReceiveError), 7 | #[error(transparent)] 8 | SendError(#[from] SendError), 9 | } 10 | 11 | #[derive(Debug, thiserror::Error)] 12 | pub enum ReceiveError { 13 | #[error("frame error: {0}")] 14 | FrameError(&'static str), 15 | #[error("io error: {0}")] 16 | Read(RE), 17 | #[error("deserialization error")] 18 | Deserialization(#[from] postcard::Error), 19 | #[error("buffer too small")] 20 | BufferTooSmall, 21 | } 22 | 23 | #[derive(Debug, thiserror::Error)] 24 | pub enum SendError { 25 | #[error("serialization error")] 26 | Serialization(#[from] postcard::Error), 27 | #[error("io error")] 28 | Write(WE), 29 | } 30 | -------------------------------------------------------------------------------- /crates/rktk-rrp/src/transport/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod error; 2 | 3 | pub(crate) mod read; 4 | pub(crate) mod write; 5 | 6 | pub use error::TransportError; 7 | pub use read::ReadTransport; 8 | pub use write::WriteTransport; 9 | 10 | // NOTE: First, I tried to use embedded_io_async::Read and embedded_io_async::Write instead of 11 | // defining ReadTransport and WriteTransport. However, it's error type doesn't require Display trait, 12 | // so I gave up using it. 13 | 14 | #[derive(Debug, PartialEq, Eq)] 15 | pub enum Indicator { 16 | Start = 0x55, 17 | Continue = 0xFF, 18 | End = 0x00, 19 | } 20 | 21 | impl TryFrom for Indicator { 22 | type Error = &'static str; 23 | 24 | fn try_from(value: u8) -> Result { 25 | match value { 26 | 0x00 => Ok(Self::End), 27 | 0x55 => Ok(Self::Start), 28 | 0xFF => Ok(Self::Continue), 29 | _ => Err("Invalid indicator"), 30 | } 31 | } 32 | } 33 | 34 | #[derive(Debug)] 35 | pub struct RequestHeader { 36 | pub request_id: u8, 37 | pub endpoint_id: u8, 38 | } 39 | 40 | #[derive(Debug)] 41 | pub struct ResponseHeader { 42 | pub request_id: u8, 43 | /// 0: OK, 1: Error 44 | pub status: u8, 45 | } 46 | -------------------------------------------------------------------------------- /crates/rktk.dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./crates/rktk/schema.json", 3 | "constant": { 4 | "keyboard": { 5 | "cols": 14, 6 | "rows": 5 7 | } 8 | }, 9 | "dynamic": { 10 | "keyboard": { 11 | "name": "test-keyboard" 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /crates/rktk/build/schema/dynamic/key_manager.rs: -------------------------------------------------------------------------------- 1 | // Private struct available in this module is copied from kmsm::interface::state::config::* to avoid foreign impl problem. 2 | // `use keymanager::...::config::*` is inserted in top of generated code and types in this module 3 | // will not be used. 4 | 5 | use smart_default::SmartDefault; 6 | 7 | #[doc = r#" 8 | Config for key manager. 9 | "#] 10 | #[macro_rules_attribute::apply(crate::schema::common_derive)] 11 | #[derive(Default)] 12 | #[serde(default)] 13 | pub struct KeyManagerConfig { 14 | pub mouse: MouseConfig, 15 | pub key_resolver: KeyResolverConfig, 16 | } 17 | 18 | #[macro_rules_attribute::apply(crate::schema::common_derive)] 19 | #[derive(SmartDefault)] 20 | #[serde(default)] 21 | struct MouseConfig { 22 | #[default(1)] 23 | pub auto_mouse_layer: u8, 24 | 25 | #[default(500)] 26 | pub auto_mouse_duration: u32, 27 | 28 | #[default(0)] 29 | pub auto_mouse_threshold: u8, 30 | 31 | #[default(20)] 32 | pub scroll_divider_x: i8, 33 | 34 | #[default(-12)] 35 | pub scroll_divider_y: i8, 36 | } 37 | 38 | #[macro_rules_attribute::apply(crate::schema::common_derive)] 39 | #[derive(SmartDefault)] 40 | #[serde(default)] 41 | struct KeyResolverConfig { 42 | pub tap_hold: TapHoldConfig, 43 | pub tap_dance: TapDanceConfig, 44 | pub combo: ComboConfig, 45 | } 46 | 47 | #[macro_rules_attribute::apply(crate::schema::common_derive)] 48 | #[derive(SmartDefault)] 49 | #[serde(default)] 50 | struct TapHoldConfig { 51 | #[default(200)] 52 | pub threshold: u32, 53 | 54 | #[default(true)] 55 | pub hold_on_other_key: bool, 56 | } 57 | 58 | #[macro_rules_attribute::apply(crate::schema::common_derive)] 59 | #[derive(SmartDefault)] 60 | #[serde(default)] 61 | struct TapDanceConfig { 62 | #[default(200)] 63 | pub threshold: u32, 64 | } 65 | 66 | #[macro_rules_attribute::apply(crate::schema::common_derive)] 67 | #[derive(SmartDefault)] 68 | #[serde(default)] 69 | struct ComboConfig { 70 | #[default(50)] 71 | pub threshold: u32, 72 | } 73 | -------------------------------------------------------------------------------- /crates/rktk/build/schema/dynamic/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod key_manager; 2 | pub mod keyboard; 3 | pub mod rktk; 4 | 5 | /// Root struct of the "dynamic" config 6 | #[derive(serde::Deserialize, schemars::JsonSchema, const_gen::CompileConst)] 7 | #[inherit_docs] 8 | pub struct DynamicConfig { 9 | pub keyboard: keyboard::KeyboardConfig, 10 | #[serde(default)] 11 | pub rktk: rktk::RktkConfig, 12 | #[serde(default)] 13 | pub key_manager: key_manager::KeyManagerConfig, 14 | } 15 | -------------------------------------------------------------------------------- /crates/rktk/build/schema/mod.rs: -------------------------------------------------------------------------------- 1 | #![allow(private_interfaces)] 2 | 3 | use macro_rules_attribute::attribute_alias; 4 | 5 | pub mod constant; 6 | pub mod dynamic; 7 | 8 | attribute_alias! { 9 | #[apply(common_derive)] = 10 | #[derive(serde::Deserialize, schemars::JsonSchema, const_gen::CompileConst)] 11 | #[serde(deny_unknown_fields)] 12 | #[inherit_docs] 13 | ; 14 | } 15 | 16 | #[derive(serde::Deserialize, schemars::JsonSchema)] 17 | pub struct Config { 18 | pub constant: constant::ConstantConfig, 19 | pub dynamic: dynamic::DynamicConfig, 20 | } 21 | -------------------------------------------------------------------------------- /crates/rktk/src/config/keymap.rs: -------------------------------------------------------------------------------- 1 | //! Keymap related configs. 2 | 3 | use super::CONST_CONFIG; 4 | 5 | /// Re-exports of raw [`kmsm`] types. 6 | /// 7 | /// Use parent module's type if available. 8 | pub mod keymanager { 9 | pub use kmsm::keycode; 10 | pub use kmsm::keymap; 11 | } 12 | 13 | pub mod prelude { 14 | pub use kmsm::keycode::prelude::*; 15 | pub use kmsm_rktk::*; 16 | } 17 | 18 | pub type Keymap = kmsm::keymap::Keymap< 19 | { CONST_CONFIG.key_manager.layer_count as usize }, 20 | { CONST_CONFIG.keyboard.rows as usize }, 21 | { CONST_CONFIG.keyboard.cols as usize }, 22 | { CONST_CONFIG.keyboard.encoder_count as usize }, 23 | { CONST_CONFIG.key_manager.tap_dance_max_definitions }, 24 | { CONST_CONFIG.key_manager.tap_dance_max_repeats }, 25 | { CONST_CONFIG.key_manager.combo_key_max_definitions }, 26 | { CONST_CONFIG.key_manager.combo_key_max_sources }, 27 | >; 28 | 29 | pub type Layer = kmsm::keymap::Layer< 30 | { CONST_CONFIG.keyboard.rows as usize }, 31 | { CONST_CONFIG.keyboard.cols as usize }, 32 | { CONST_CONFIG.keyboard.encoder_count as usize }, 33 | >; 34 | 35 | pub type LayerKeymap = kmsm::keymap::LayerKeymap< 36 | { CONST_CONFIG.keyboard.rows as usize }, 37 | { CONST_CONFIG.keyboard.cols as usize }, 38 | >; 39 | -------------------------------------------------------------------------------- /crates/rktk/src/config/rgb.rs: -------------------------------------------------------------------------------- 1 | pub use blinksy::layout; 2 | pub use blinksy::layout2d; 3 | 4 | pub struct DummyLayout; 5 | 6 | impl layout::Layout2d for DummyLayout { 7 | const PIXEL_COUNT: usize = 0; 8 | 9 | fn shapes() -> impl Iterator { 10 | [].into_iter() 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /crates/rktk/src/config/storage/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::drivers::interface::storage::StorageDriver; 2 | 3 | mod read; 4 | mod write; 5 | 6 | pub struct StorageConfigManager { 7 | pub storage: S, 8 | } 9 | 10 | pub enum ConfigKey { 11 | Version = 0, 12 | StateConfig = 1, 13 | StateKeymap = 2, 14 | } 15 | 16 | impl StorageConfigManager { 17 | pub fn new(storage: S) -> Self { 18 | Self { storage } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /crates/rktk/src/config/storage/read.rs: -------------------------------------------------------------------------------- 1 | use core::fmt::Debug; 2 | use postcard::experimental::max_size::MaxSize as _; 3 | use kmsm::interface::state::config::StateConfig; 4 | 5 | use crate::{config::keymap::Layer, drivers::interface::storage::StorageDriver}; 6 | 7 | use super::{ConfigKey, StorageConfigManager}; 8 | 9 | #[derive(Debug)] 10 | pub enum ConfigReadError { 11 | ReadError(E), 12 | DecodeError(postcard::Error), 13 | } 14 | 15 | impl From for ConfigReadError { 16 | fn from(e: E) -> Self { 17 | ConfigReadError::ReadError(e) 18 | } 19 | } 20 | 21 | impl StorageConfigManager { 22 | pub async fn read_version(&self) -> Result> { 23 | let mut buf = [0; 2]; 24 | let key = u64::from_le_bytes([ConfigKey::Version as u8, 0, 0, 0, 0, 0, 0, 0]); 25 | self.storage.read::<2>(key, &mut buf).await?; 26 | Ok(u16::from_le_bytes(buf)) 27 | } 28 | 29 | pub async fn read_state_config(&self) -> Result> { 30 | let mut buf = [0; StateConfig::POSTCARD_MAX_SIZE]; 31 | let key = u64::from_le_bytes([ConfigKey::StateConfig as u8, 0, 0, 0, 0, 0, 0, 0]); 32 | self.storage 33 | .read::<{ StateConfig::POSTCARD_MAX_SIZE }>(key, &mut buf) 34 | .await?; 35 | let res = postcard::from_bytes(&buf).map_err(ConfigReadError::DecodeError)?; 36 | Ok(res) 37 | } 38 | 39 | pub async fn read_keymap(&self, layer: u8) -> Result> { 40 | let mut buf = [0; Layer::POSTCARD_MAX_SIZE]; 41 | let key = u64::from_le_bytes([ConfigKey::StateKeymap as u8, layer, 0, 0, 0, 0, 0, 0]); 42 | self.storage 43 | .read::<{ Layer::POSTCARD_MAX_SIZE }>(key, &mut buf) 44 | .await?; 45 | let res = postcard::from_bytes(&buf).map_err(ConfigReadError::DecodeError)?; 46 | Ok(res) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /crates/rktk/src/config/storage/write.rs: -------------------------------------------------------------------------------- 1 | use core::fmt::Debug; 2 | use postcard::experimental::max_size::MaxSize as _; 3 | use kmsm::interface::state::config::StateConfig; 4 | 5 | use crate::{config::keymap::Layer, drivers::interface::storage::StorageDriver}; 6 | 7 | use super::{ConfigKey, StorageConfigManager}; 8 | 9 | #[derive(Debug)] 10 | pub enum ConfigWriteError { 11 | WriteError(E), 12 | EncodeError(postcard::Error), 13 | } 14 | 15 | impl From for ConfigWriteError { 16 | fn from(e: E) -> Self { 17 | ConfigWriteError::WriteError(e) 18 | } 19 | } 20 | 21 | impl StorageConfigManager { 22 | pub async fn write_version(&self, version: u16) -> Result<(), ConfigWriteError> { 23 | let key = u64::from_le_bytes([ConfigKey::Version as u8, 0, 0, 0, 0, 0, 0, 0]); 24 | 25 | self.storage.write::<2>(key, &version.to_le_bytes()).await?; 26 | Ok(()) 27 | } 28 | 29 | pub async fn write_state_config( 30 | &self, 31 | data: &StateConfig, 32 | ) -> Result<(), ConfigWriteError> { 33 | let key = u64::from_le_bytes([ConfigKey::StateConfig as u8, 0, 0, 0, 0, 0, 0, 0]); 34 | 35 | let mut buf = [0; StateConfig::POSTCARD_MAX_SIZE]; 36 | let _slice = postcard::to_slice(data, &mut buf).map_err(ConfigWriteError::EncodeError)?; 37 | self.storage 38 | .write::<{ StateConfig::POSTCARD_MAX_SIZE }>(key, &buf) 39 | .await?; 40 | Ok(()) 41 | } 42 | 43 | pub async fn write_keymap( 44 | &self, 45 | layer: u8, 46 | data: &Layer, 47 | ) -> Result<(), ConfigWriteError> { 48 | let key = u64::from_le_bytes([ConfigKey::StateKeymap as u8, layer, 0, 0, 0, 0, 0, 0]); 49 | 50 | let mut buf = [0; Layer::POSTCARD_MAX_SIZE]; 51 | let _slice = postcard::to_slice(data, &mut buf).map_err(ConfigWriteError::EncodeError)?; 52 | self.storage 53 | .write::<{ Layer::POSTCARD_MAX_SIZE }>(key, &buf) 54 | .await?; 55 | Ok(()) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /crates/rktk/src/drivers/interface/debounce.rs: -------------------------------------------------------------------------------- 1 | //! Debounce driver type 2 | //! 3 | //! `debounce` is way to reduce chatter or noise this can be achieved by ignoring events that are too close to each other in time. 4 | 5 | pub use kmsm::interface::state::input_event::KeyChangeEvent; 6 | 7 | /// Debounce driver interface 8 | pub trait DebounceDriver { 9 | /// Determines whether events occurring at a certain time should be ignored. 10 | fn should_ignore_event(&mut self, event: &KeyChangeEvent, now: embassy_time::Instant) -> bool; 11 | } 12 | -------------------------------------------------------------------------------- /crates/rktk/src/drivers/interface/display.rs: -------------------------------------------------------------------------------- 1 | use display_interface::DisplayError; 2 | use embedded_graphics::{pixelcolor::BinaryColor, prelude::*}; 3 | 4 | /// Interface for display drivers. 5 | /// 6 | /// TODO: Allow sync-only drivers? 7 | pub trait DisplayDriver: AsRef + AsMut + 'static { 8 | type Display: DrawTarget; 9 | 10 | /// Called when the display is initialized. 11 | /// 12 | /// It is guaranteed that: 13 | /// - No other function is called before this function. 14 | /// - If this function returns an error, other functions will not be called. 15 | /// 16 | /// Default implementation returns `Ok(())`. 17 | async fn init(&mut self) -> Result<(), DisplayError> { 18 | Ok(()) 19 | } 20 | 21 | async fn flush(&mut self) -> Result<(), DisplayError> { 22 | Ok(()) 23 | } 24 | 25 | /// Sets brightness of the display. 26 | /// 27 | /// 0 is off, 255 is full brightness. 28 | async fn set_brightness(&mut self, _brightness: u8) -> Result<(), DisplayError> { 29 | Err(DisplayError::DataFormatNotImplemented) 30 | } 31 | 32 | async fn set_display_on(&mut self, on: bool) -> Result<(), DisplayError> { 33 | if on { 34 | self.set_brightness(255).await 35 | } else { 36 | self.set_brightness(0).await 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /crates/rktk/src/drivers/interface/encoder.rs: -------------------------------------------------------------------------------- 1 | pub use kmsm::interface::state::input_event::EncoderDirection; 2 | 3 | pub trait EncoderDriver { 4 | async fn read_wait(&mut self) -> (u8, EncoderDirection); 5 | } 6 | -------------------------------------------------------------------------------- /crates/rktk/src/drivers/interface/keyscan.rs: -------------------------------------------------------------------------------- 1 | pub use kmsm::interface::state::input_event::KeyChangeEvent; 2 | 3 | /// Key scanner driver interface. 4 | /// 5 | /// The keyscan driver has two roles: 6 | /// - Scanning the keys 7 | /// - Determining which hand is currently using the keyboard on a split keyboard 8 | /// 9 | /// This is because the key scanning circuit often includes a left/right determination circuit. 10 | pub trait KeyscanDriver { 11 | /// Scans a key and returns the delta from the previous key scan 12 | async fn scan(&mut self, callback: impl FnMut(KeyChangeEvent)); 13 | } 14 | -------------------------------------------------------------------------------- /crates/rktk/src/drivers/interface/mod.rs: -------------------------------------------------------------------------------- 1 | //! Driver interface types 2 | #![allow(async_fn_in_trait)] 3 | 4 | pub mod debounce; 5 | pub mod display; 6 | pub mod dongle; 7 | pub mod encoder; 8 | pub mod keyscan; 9 | pub mod mouse; 10 | pub mod reporter; 11 | pub mod rgb; 12 | pub mod split; 13 | pub mod storage; 14 | pub mod system; 15 | pub mod usb; 16 | pub mod wireless; 17 | 18 | pub trait Error: core::fmt::Debug + rktk_log::MaybeFormat { 19 | fn kind(&self) -> ErrorKind { 20 | ErrorKind::Other 21 | } 22 | } 23 | 24 | #[non_exhaustive] 25 | pub enum ErrorKind { 26 | NotSupported, 27 | Other, 28 | } 29 | 30 | impl Error for core::convert::Infallible { 31 | fn kind(&self) -> ErrorKind { 32 | unreachable!() 33 | } 34 | } 35 | 36 | macro_rules! generate_builder { 37 | ($driver:ident) => { 38 | paste::paste! { 39 | pub trait [<$driver Builder>] { 40 | type Output: $driver; 41 | type Error: rktk_log::MaybeFormat; 42 | async fn build(self) -> Result<(Self::Output, impl Future + 'static), Self::Error>; 43 | } 44 | } 45 | }; 46 | } 47 | pub(crate) use generate_builder; 48 | -------------------------------------------------------------------------------- /crates/rktk/src/drivers/interface/mouse.rs: -------------------------------------------------------------------------------- 1 | pub trait MouseDriver { 2 | type Error: super::Error; 3 | 4 | async fn init(&mut self) -> Result<(), Self::Error> { 5 | Ok(()) 6 | } 7 | async fn read(&mut self) -> Result<(i8, i8), Self::Error>; 8 | async fn set_cpi(&mut self, _cpi: u16) -> Result<(), Self::Error>; 9 | async fn get_cpi(&mut self) -> Result; 10 | } 11 | -------------------------------------------------------------------------------- /crates/rktk/src/drivers/interface/reporter.rs: -------------------------------------------------------------------------------- 1 | use usbd_hid::descriptor::{KeyboardReport, MediaKeyboardReport, MouseReport}; 2 | 3 | pub trait ReporterDriver { 4 | type Error: super::Error; 5 | 6 | async fn wait_ready(&self) {} 7 | 8 | /// Read a keyboard report from the device and return leds data. 9 | async fn recv_keyboard_report(&self) -> Result { 10 | let _: () = core::future::pending().await; 11 | unreachable!() 12 | } 13 | 14 | fn try_send_keyboard_report(&self, _report: KeyboardReport) -> Result<(), Self::Error>; 15 | 16 | fn try_send_media_keyboard_report( 17 | &self, 18 | _report: MediaKeyboardReport, 19 | ) -> Result<(), Self::Error>; 20 | fn try_send_mouse_report(&self, _report: MouseReport) -> Result<(), Self::Error>; 21 | 22 | async fn send_rrp_data(&self, _data: &[u8]) -> Result<(), Self::Error>; 23 | async fn recv_rrp_data(&self, _buf: &mut [u8]) -> Result { 24 | let _: () = core::future::pending().await; 25 | unreachable!() 26 | } 27 | 28 | async fn send_raw_hid_data(&self, _data: &[u8]) -> Result<(), Self::Error> { 29 | Ok(()) 30 | } 31 | async fn recv_raw_hid_data(&self, _buf: &mut [u8]) -> Result { 32 | let _: () = core::future::pending().await; 33 | unreachable!() 34 | } 35 | 36 | /// Wake up the device. 37 | /// This is used to wake up the device from suspend mode. 38 | /// 39 | /// # Returns 40 | /// - `Ok(true)`: Woke up signal sent successfully. 41 | /// - `Ok(false)`: The device is already awake. 42 | /// - `Err(_)`: Failed to send the wake up signal or not supported. 43 | fn wakeup(&self) -> Result; 44 | } 45 | 46 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 47 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 48 | pub enum Output { 49 | Usb, 50 | Ble, 51 | } 52 | 53 | super::generate_builder!(ReporterDriver); 54 | -------------------------------------------------------------------------------- /crates/rktk/src/drivers/interface/split.rs: -------------------------------------------------------------------------------- 1 | use postcard::experimental::max_size::MaxSize; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use super::rgb::RgbCommand; 5 | 6 | pub trait SplitDriver: 'static { 7 | type Error: super::Error; 8 | 9 | async fn init(&mut self) -> Result<(), Self::Error> { 10 | Ok(()) 11 | } 12 | 13 | /// Receive data from the other side and return the number of bytes received. 14 | /// 15 | /// If there is no data, this function should wait until data is received. 16 | async fn recv(&mut self, buf: &mut [u8], is_master: bool) -> Result; 17 | 18 | /// Send data to the other side. 19 | /// 20 | /// Implemention should wait until the *all* data is sent. 21 | async fn send_all(&mut self, buf: &[u8], is_master: bool) -> Result<(), Self::Error>; 22 | } 23 | 24 | #[derive(Debug, Deserialize, Serialize, MaxSize)] 25 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 26 | pub enum MasterToSlave { 27 | Rgb(RgbCommand), 28 | Message(u8), 29 | } 30 | 31 | #[derive(Debug, Deserialize, Serialize, MaxSize)] 32 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 33 | pub enum SlaveToMaster { 34 | Pressed(u8, u8), 35 | Released(u8, u8), 36 | Mouse { x: i8, y: i8 }, 37 | Message(u8), 38 | } 39 | -------------------------------------------------------------------------------- /crates/rktk/src/drivers/interface/storage.rs: -------------------------------------------------------------------------------- 1 | /// Storage driver interface 2 | pub trait StorageDriver { 3 | type Error: super::Error; 4 | 5 | async fn format(&self) -> Result<(), Self::Error>; 6 | async fn read(&self, key: u64, buf: &mut [u8]) -> Result<(), Self::Error>; 7 | async fn write(&self, key: u64, buf: &[u8]) -> Result<(), Self::Error>; 8 | } 9 | -------------------------------------------------------------------------------- /crates/rktk/src/drivers/interface/system.rs: -------------------------------------------------------------------------------- 1 | use embassy_time::Duration; 2 | 3 | /// Driver to interact with the system 4 | pub trait SystemDriver { 5 | /// Reboot the system if double-reset is detected. 6 | /// 7 | /// Implement this only for chips that require this feature (e.g. RP2040). 8 | /// There is no need to implement this if the feature is already implemented, such as the nRF52840 uf2 bootloader. 9 | async fn double_reset_usb_boot(&self, _timeout: Duration) {} 10 | 11 | fn reset(&self) {} 12 | 13 | /// Reset to the bootloader (typically uf2 flash mode) 14 | fn reset_to_bootloader(&self) {} 15 | 16 | async fn power_off(&self) {} 17 | } 18 | -------------------------------------------------------------------------------- /crates/rktk/src/drivers/interface/usb.rs: -------------------------------------------------------------------------------- 1 | use super::reporter::ReporterDriver; 2 | 3 | pub trait UsbReporterDriver: ReporterDriver { 4 | type Error: super::Error; 5 | 6 | async fn vbus_detect(&self) -> Result::Error>; 7 | } 8 | 9 | super::generate_builder!(UsbReporterDriver); 10 | -------------------------------------------------------------------------------- /crates/rktk/src/drivers/interface/wireless.rs: -------------------------------------------------------------------------------- 1 | //! Bluetooth Low Energy (BLE) driver type. 2 | 3 | use super::reporter::ReporterDriver; 4 | 5 | /// BLE driver type. 6 | pub trait WirelessReporterDriver: ReporterDriver { 7 | type Error: super::Error; 8 | 9 | /// Clears all bond data 10 | async fn clear_bond_data(&self) -> Result<(), ::Error>; 11 | } 12 | 13 | super::generate_builder!(WirelessReporterDriver); 14 | -------------------------------------------------------------------------------- /crates/rktk/src/drivers/mod.rs: -------------------------------------------------------------------------------- 1 | //! Drivers for the keyboard. 2 | 3 | use interface::{ 4 | display::DisplayDriver, mouse::MouseDriver, split::SplitDriver, system::SystemDriver, 5 | usb::UsbReporterDriverBuilder, wireless::WirelessReporterDriverBuilder, 6 | }; 7 | 8 | use crate::drivers::interface::{ 9 | debounce::DebounceDriver, encoder::EncoderDriver, keyscan::KeyscanDriver, rgb::RgbDriver, 10 | storage::StorageDriver, 11 | }; 12 | 13 | pub mod dummy; 14 | pub mod interface; 15 | 16 | /// All drivers required to run the keyboard. 17 | /// 18 | /// Only the `key_scanner` and `usb` drivers are required. 19 | /// For other drivers, if the value is None, it will be handled appropriately. 20 | pub struct Drivers< 21 | System: SystemDriver, 22 | KeyScan: KeyscanDriver, 23 | Debounce: DebounceDriver, 24 | Encoder: EncoderDriver, 25 | Rgb: RgbDriver, 26 | Storage: StorageDriver, 27 | Split: SplitDriver, 28 | Ble: WirelessReporterDriverBuilder, 29 | Usb: UsbReporterDriverBuilder, 30 | Display: DisplayDriver, 31 | Mouse: MouseDriver, 32 | > { 33 | pub system: System, 34 | pub keyscan: KeyScan, 35 | pub debounce: Option, 36 | pub encoder: Option, 37 | pub rgb: Option, 38 | pub storage: Option, 39 | pub split: Option, 40 | pub ble_builder: Option, 41 | pub usb_builder: Option, 42 | pub mouse: Option, 43 | pub display: Option, 44 | } 45 | -------------------------------------------------------------------------------- /crates/rktk/src/hooks/interface/dongle.rs: -------------------------------------------------------------------------------- 1 | use crate::drivers::interface::dongle::DongleData; 2 | 3 | pub trait DongleHooks { 4 | async fn on_dongle_data(&mut self, _data: &mut DongleData) -> bool { 5 | true 6 | } 7 | } 8 | 9 | pub struct DummyDongleHooks {} 10 | impl DongleHooks for DummyDongleHooks {} 11 | 12 | pub fn default_dongle_hooks() -> impl DongleHooks { 13 | DummyDongleHooks {} 14 | } 15 | -------------------------------------------------------------------------------- /crates/rktk/src/hooks/interface/mod.rs: -------------------------------------------------------------------------------- 1 | //! Hooks traits 2 | 3 | #![allow(async_fn_in_trait)] 4 | 5 | pub mod dongle; 6 | pub mod master; 7 | pub mod rgb; 8 | 9 | pub use common::CommonHooks; 10 | pub use master::MasterHooks; 11 | pub use rgb::RgbHooks; 12 | pub use slave::SlaveHooks; 13 | 14 | pub mod common { 15 | use crate::{ 16 | config::Hand, 17 | drivers::interface::{keyscan::KeyscanDriver, mouse::MouseDriver, storage::StorageDriver}, 18 | }; 19 | 20 | /// Hooks common for both master and slave side 21 | pub trait CommonHooks { 22 | async fn on_init( 23 | &mut self, 24 | _hand: Hand, 25 | _key_scanner: &mut impl KeyscanDriver, 26 | _mouse: Option<&mut impl MouseDriver>, 27 | _storage: Option<&mut impl StorageDriver>, 28 | ) { 29 | } 30 | } 31 | } 32 | 33 | pub mod slave { 34 | use crate::drivers::interface::{keyscan::KeyscanDriver, mouse::MouseDriver}; 35 | 36 | pub trait SlaveHooks { 37 | /// Called after slave side initialization. 38 | async fn on_slave_init( 39 | &mut self, 40 | _key_scanner: &mut impl KeyscanDriver, 41 | _mouse: Option<&mut impl MouseDriver>, 42 | ) { 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /crates/rktk/src/hooks/interface/rgb.rs: -------------------------------------------------------------------------------- 1 | use crate::drivers::interface::rgb::*; 2 | 3 | /// Hooks related to RGB functionality. 4 | /// 5 | /// This trait allows for custom RGB behavior to be implemented. 6 | /// These hooks are called in both master and slave sides of the keyboard. 7 | pub trait RgbHooks: 'static { 8 | /// Invoked after the RGB driver is initialized. 9 | /// 10 | /// * `_driver`: [`RgbDriver`] instance to control RGB. 11 | /// * `_is_master`: If true, this is the master side of the keyboard. 12 | async fn on_rgb_init(&mut self, _driver: &mut impl RgbDriver, _is_master: bool) {} 13 | 14 | /// Invoked 15 | /// - after the [`RgbCommand`] is send and RGB task received it 16 | /// - before the RGB task processes the command. 17 | /// 18 | /// You can use this hook to modify the RGB mode before the RGB task processes. 19 | /// 20 | /// * `_driver`: [`RgbDriver`] instance to control RGB. 21 | /// * `_rgb_mode`: [`RgbMode`] to be processed. 22 | async fn on_rgb_process(&mut self, _driver: &mut impl RgbDriver, _rgb_mode: &mut RgbMode) {} 23 | async fn custom_rgb(&mut self, _driver: &mut impl RgbDriver, _brightness: f32) {} 24 | } 25 | -------------------------------------------------------------------------------- /crates/rktk/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # rktk 2 | //! ## Overview 3 | //! `rktk` is a framework to build keyboard firmware. Using rktk, you can easily make feature-rich 4 | //! highly customizable keyboard firmware. 5 | //! 6 | //! ## `rktk` crate 7 | //! 8 | //! This `rktk` crate is the main crate of the project. It contains the main logic of the 9 | //! keyboard firmware and does not depend on any specific hardware. 10 | //! 11 | //! This crate consists of the following modules: 12 | //! - [`task`]: The main task that runs the keyboard firmware. 13 | //! - [`drivers`]: Drivers that are used by the task. 14 | //! - [`hooks`]: Hooks that can be used to customize the behavior of the application. 15 | //! - [`config`]: Configuration of the keyboard. 16 | //! 17 | //! Basically, by passing [`drivers::Drivers`], [`hooks::Hooks`] and [`config::keymap::Keymap`] to [`task::start`], you can start the keyboard firmware. 18 | //! 19 | //! ## Feature flags 20 | #![doc = document_features::document_features!()] 21 | //! 22 | //! ### `alloc` feature 23 | //! Embassy has the limitation that tasks with generics cannot be spawned. 24 | //! For this reason, rktk, which makes heavy use of generics, uses the `join` method of embassy-sync to execute all tasks instead of spawning them. 25 | //! However, this may be inferior to using spawning in terms of performance and power consumption. 26 | //! 27 | //! So if we enable `alloc` feature and provide an allocator, we can remove this limitation by spawning tasks in the heap. 28 | //! 29 | //! ## Note about statically configured value 30 | //! You may see hard-coded values is used in some places (ex: [`config::keymap::Keymap`]). 31 | //! These types are actually not hardcoded, but are configurable using json file. 32 | //! Just a random value is provided because it is required to generate docs. 33 | //! 34 | //! For more detail, see [`config`]. 35 | //! 36 | #![no_std] 37 | 38 | #[cfg(feature = "alloc")] 39 | extern crate alloc; 40 | 41 | pub mod config; 42 | pub mod dongle_task; 43 | pub mod drivers; 44 | pub mod hooks; 45 | pub mod task; 46 | pub mod utils; 47 | 48 | #[doc(hidden)] 49 | pub mod reexports { 50 | pub use heapless; 51 | pub use static_cell; 52 | } 53 | -------------------------------------------------------------------------------- /crates/rktk/src/task/display/mod.rs: -------------------------------------------------------------------------------- 1 | use embedded_graphics::{draw_target::DrawTarget, pixelcolor::BinaryColor}; 2 | use rktk_log::error; 3 | 4 | use crate::{ 5 | config::CONST_CONFIG, 6 | config::Hand, 7 | drivers::interface::{display::DisplayDriver, reporter::Output}, 8 | utils::{Channel, Signal}, 9 | }; 10 | 11 | pub mod default_display; 12 | pub mod utils; 13 | 14 | pub enum DisplayMessage { 15 | Clear, 16 | Message(&'static str), 17 | Master(Option), 18 | MouseAvailable(bool), 19 | MouseMove((i8, i8)), 20 | Output(Output), 21 | LayerState([bool; CONST_CONFIG.key_manager.layer_count as usize]), 22 | Hand(Option), 23 | NumLock(bool), 24 | CapsLock(bool), 25 | Brightness(u8), 26 | On(bool), 27 | } 28 | 29 | #[allow(async_fn_in_trait)] 30 | pub trait DisplayConfig { 31 | async fn start( 32 | &mut self, 33 | display: &mut D, 34 | display_controller: &Channel, 35 | display_dynamic_message_controller: &Signal>, 36 | ); 37 | } 38 | 39 | pub static DISPLAY_CONTROLLER: Channel = Channel::new(); 40 | pub static DISPLAY_DYNAMIC_MESSAGE_CONTROLLER: Signal> = Signal::new(); 41 | 42 | pub(crate) async fn start(display: &mut D, config: &mut C) { 43 | if display.init().await.is_err() { 44 | error!("Failed to initialize display"); 45 | return; 46 | } 47 | 48 | let _ = display.as_mut().clear(BinaryColor::Off); 49 | let _ = display.flush().await; 50 | 51 | config 52 | .start( 53 | display, 54 | &DISPLAY_CONTROLLER, 55 | &DISPLAY_DYNAMIC_MESSAGE_CONTROLLER, 56 | ) 57 | .await; 58 | } 59 | -------------------------------------------------------------------------------- /crates/rktk/src/task/display/utils.rs: -------------------------------------------------------------------------------- 1 | use embedded_graphics::{prelude::*, primitives::Rectangle}; 2 | 3 | #[derive(Debug, Clone, Copy)] 4 | pub enum Rotation { 5 | Rotate0, 6 | Rotate90, 7 | } 8 | 9 | #[derive(Debug)] 10 | pub struct RotatedDrawTarget<'a, T> 11 | where 12 | T: DrawTarget, 13 | { 14 | parent: &'a mut T, 15 | rotation: Rotation, 16 | } 17 | 18 | impl<'a, T> RotatedDrawTarget<'a, T> 19 | where 20 | T: DrawTarget, 21 | { 22 | pub fn new(parent: &'a mut T, rotation: Rotation) -> Self { 23 | Self { parent, rotation } 24 | } 25 | } 26 | 27 | impl DrawTarget for RotatedDrawTarget<'_, T> 28 | where 29 | T: DrawTarget, 30 | { 31 | type Color = T::Color; 32 | type Error = T::Error; 33 | 34 | fn draw_iter(&mut self, pixels: I) -> Result<(), Self::Error> 35 | where 36 | I: IntoIterator>, 37 | { 38 | match self.rotation { 39 | Rotation::Rotate0 => self.draw_iter(pixels), 40 | Rotation::Rotate90 => { 41 | let parent_height = self.parent.bounding_box().size.height as i32; 42 | 43 | self.parent.draw_iter( 44 | pixels 45 | .into_iter() 46 | .map(|Pixel(p, c)| Pixel(Point::new(p.y, parent_height - p.x), c)), 47 | ) 48 | } 49 | } 50 | } 51 | } 52 | 53 | impl Dimensions for RotatedDrawTarget<'_, T> 54 | where 55 | T: DrawTarget, 56 | { 57 | fn bounding_box(&self) -> Rectangle { 58 | match self.rotation { 59 | Rotation::Rotate0 => self.parent.bounding_box(), 60 | Rotation::Rotate90 => { 61 | let parent_bb = self.parent.bounding_box(); 62 | Rectangle::new( 63 | parent_bb.top_left, 64 | Size::new(parent_bb.size.height, parent_bb.size.width), 65 | ) 66 | } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /crates/rktk/src/task/logger.rs: -------------------------------------------------------------------------------- 1 | use core::fmt::Write; 2 | 3 | use rktk_rrp::endpoints::get_log::{self, LogChunk}; 4 | 5 | use crate::{config::CONST_CONFIG, utils::Channel}; 6 | 7 | pub struct LogWriter; 8 | 9 | impl Write for LogWriter { 10 | fn write_str(&mut self, s: &str) -> core::fmt::Result { 11 | for chunk in s.as_bytes().chunks(32) { 12 | let mut buf = [0; 32]; 13 | buf[..chunk.len()].copy_from_slice(chunk); 14 | 15 | if let Err(_e) = LOG_CHANNEL.try_send(LogChunk::Bytes { 16 | bytes: buf, 17 | len: chunk.len() as u8, 18 | }) { 19 | return Ok(()); 20 | } 21 | } 22 | 23 | Ok(()) 24 | } 25 | } 26 | 27 | pub static LOG_CHANNEL: Channel = Channel::new(); 28 | 29 | pub struct RrpLogger; 30 | 31 | pub static RRP_LOGGER: RrpLogger = RrpLogger; 32 | 33 | impl log::Log for RrpLogger { 34 | fn enabled(&self, _metadata: &log::Metadata) -> bool { 35 | !LOG_CHANNEL.is_full() 36 | } 37 | 38 | fn log(&self, record: &log::Record) { 39 | if !self.enabled(record.metadata()) { 40 | return; 41 | } 42 | 43 | let _ = LOG_CHANNEL.try_send(LogChunk::Start { 44 | time: embassy_time::Instant::now().as_millis(), 45 | level: match record.level() { 46 | log::Level::Error => get_log::LogLevel::Error, 47 | log::Level::Warn => get_log::LogLevel::Warn, 48 | log::Level::Info => get_log::LogLevel::Info, 49 | log::Level::Debug => get_log::LogLevel::Debug, 50 | log::Level::Trace => get_log::LogLevel::Trace, 51 | }, 52 | line: record.line(), 53 | }); 54 | 55 | if record.level() == log::Level::Error { 56 | crate::print!("{}", record.args()); 57 | } 58 | 59 | write!( 60 | &mut LogWriter, 61 | "{}\t{}", 62 | record.module_path().unwrap_or_default(), 63 | record.args() 64 | ) 65 | .unwrap(); 66 | 67 | let _ = LOG_CHANNEL.try_send(LogChunk::End); 68 | } 69 | fn flush(&self) {} 70 | } 71 | -------------------------------------------------------------------------------- /crates/rktk/src/task/master/handle_keyboard.rs: -------------------------------------------------------------------------------- 1 | use rktk_log::{debug, warn}; 2 | 3 | use super::utils::get_split_right_shift; 4 | use crate::{ 5 | config::Hand, 6 | config::schema::DynamicConfig, 7 | drivers::interface::{debounce::DebounceDriver, keyscan::KeyscanDriver}, 8 | task::channels::report::KEYBOARD_EVENT_REPORT_CHANNEL, 9 | }; 10 | 11 | use super::utils::resolve_entire_key_pos; 12 | 13 | pub async fn start( 14 | config: &'static DynamicConfig, 15 | hand: Hand, 16 | mut keyscan: impl KeyscanDriver, 17 | debounce: &mut Option, 18 | ) { 19 | debug!("keyscan start"); 20 | let mut latest = embassy_time::Instant::from_millis(0); 21 | let interval = embassy_time::Duration::from_millis(config.rktk.scan_interval_keyboard); 22 | let shift = get_split_right_shift(config); 23 | loop { 24 | let elapsed = latest.elapsed(); 25 | if elapsed < interval { 26 | embassy_time::Timer::after(interval - elapsed).await; 27 | } 28 | 29 | keyscan 30 | .scan(|mut event| { 31 | if let Some(debounce) = debounce.as_mut() 32 | && debounce.should_ignore_event(&event, embassy_time::Instant::now()) 33 | { 34 | debug!("Debounced"); 35 | return; 36 | } 37 | resolve_entire_key_pos(&mut event, hand, shift); 38 | 39 | if KEYBOARD_EVENT_REPORT_CHANNEL.try_send(event).is_err() { 40 | warn!("Keyboard full"); 41 | } 42 | }) 43 | .await; 44 | 45 | latest = embassy_time::Instant::now(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /crates/rktk/src/task/master/handle_mouse.rs: -------------------------------------------------------------------------------- 1 | use embassy_time::Duration; 2 | 3 | use crate::{ 4 | config::schema::DynamicConfig, drivers::interface::mouse::MouseDriver, 5 | task::channels::report::update_mouse, 6 | }; 7 | 8 | pub async fn start(config: &'static DynamicConfig, mut mouse: Option) { 9 | if let Some(mouse) = &mut mouse { 10 | let mut latest = embassy_time::Instant::from_millis(0); 11 | let interval = Duration::from_millis(config.rktk.scan_interval_mouse); 12 | loop { 13 | let elapsed = latest.elapsed(); 14 | if elapsed < interval { 15 | embassy_time::Timer::after(interval - elapsed).await; 16 | } 17 | 18 | let mouse_move = match mouse.read().await { 19 | Ok(m) => { 20 | if config.rktk.swap_mouse_x_y { 21 | (m.1, m.0) 22 | } else { 23 | m 24 | } 25 | } 26 | Err(e) => { 27 | rktk_log::warn!( 28 | "Failed to read mouse: {:?}", 29 | rktk_log::helper::Debug2Format(&e) 30 | ); 31 | crate::print!("{:?}", e); 32 | continue; 33 | } 34 | }; 35 | 36 | if mouse_move == (0, 0) { 37 | continue; 38 | } 39 | 40 | update_mouse(mouse_move.0, mouse_move.1); 41 | 42 | latest = embassy_time::Instant::now(); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /crates/rktk/src/task/master/handle_slave.rs: -------------------------------------------------------------------------------- 1 | use kmsm::interface::state::input_event::KeyChangeEvent; 2 | use rktk_log::debug; 3 | 4 | use super::utils::get_split_right_shift; 5 | use crate::{ 6 | config::Hand, 7 | config::schema::DynamicConfig, 8 | drivers::interface::split::SlaveToMaster, 9 | task::channels::{ 10 | report::{KEYBOARD_EVENT_REPORT_CHANNEL, update_mouse}, 11 | split::S2mRx, 12 | }, 13 | }; 14 | 15 | use super::utils::resolve_entire_key_pos; 16 | 17 | pub async fn start(config: &'static DynamicConfig, hand: Hand, s2m_rx: S2mRx<'_>) { 18 | debug!("split recv start"); 19 | 20 | let slave_hand = hand.other(); 21 | let shift = get_split_right_shift(config); 22 | loop { 23 | s2m_rx.ready_to_receive().await; 24 | while let Ok(cmd_from_slave) = s2m_rx.try_receive() { 25 | match cmd_from_slave { 26 | SlaveToMaster::Pressed(row, col) => { 27 | let mut ev = KeyChangeEvent { 28 | col, 29 | row, 30 | pressed: true, 31 | }; 32 | resolve_entire_key_pos(&mut ev, slave_hand, shift); 33 | KEYBOARD_EVENT_REPORT_CHANNEL.send(ev).await; 34 | } 35 | SlaveToMaster::Released(row, col) => { 36 | let mut ev = KeyChangeEvent { 37 | col, 38 | row, 39 | pressed: false, 40 | }; 41 | resolve_entire_key_pos(&mut ev, slave_hand, shift); 42 | KEYBOARD_EVENT_REPORT_CHANNEL.send(ev).await; 43 | } 44 | SlaveToMaster::Mouse { x, y } => { 45 | update_mouse(x, y); 46 | } 47 | SlaveToMaster::Message(_) => {} 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /crates/rktk/src/task/master/mod.rs: -------------------------------------------------------------------------------- 1 | use kmsm::state::hid_report::HidReportState; 2 | 3 | use crate::{config::CONST_CONFIG, utils::Mutex}; 4 | 5 | pub(super) mod handle_keyboard; 6 | pub(super) mod handle_mouse; 7 | pub(super) mod handle_slave; 8 | pub(super) mod report; 9 | #[cfg(feature = "rrp")] 10 | pub(super) mod rrp_server; 11 | pub(super) mod utils; 12 | 13 | pub(super) mod handle_encoder { 14 | use rktk_log::warn; 15 | 16 | use crate::{ 17 | drivers::interface::encoder::EncoderDriver, 18 | task::channels::report::ENCODER_EVENT_REPORT_CHANNEL, 19 | }; 20 | 21 | pub async fn start(enc: &mut Option) { 22 | if let Some(encoder) = enc.as_mut() { 23 | loop { 24 | let (id, dir) = encoder.read_wait().await; 25 | if ENCODER_EVENT_REPORT_CHANNEL.try_send((id, dir)).is_err() { 26 | warn!("enc full"); 27 | } 28 | } 29 | } 30 | } 31 | } 32 | 33 | type ConfiguredState = HidReportState< 34 | { CONST_CONFIG.key_manager.layer_count as usize }, 35 | { CONST_CONFIG.keyboard.rows as usize }, 36 | { CONST_CONFIG.keyboard.cols as usize }, 37 | { CONST_CONFIG.keyboard.encoder_count as usize }, 38 | { CONST_CONFIG.key_manager.normal_max_pressed_keys }, 39 | { CONST_CONFIG.key_manager.oneshot_state_size }, 40 | { CONST_CONFIG.key_manager.tap_dance_max_definitions }, 41 | { CONST_CONFIG.key_manager.tap_dance_max_repeats }, 42 | { CONST_CONFIG.key_manager.combo_key_max_definitions }, 43 | { CONST_CONFIG.key_manager.combo_key_max_sources }, 44 | >; 45 | 46 | type SharedState = Mutex; 47 | -------------------------------------------------------------------------------- /crates/xtask/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "xtask" 3 | description = "Internal xtask command for rktk" 4 | publish = false 5 | authors.workspace = true 6 | license.workspace = true 7 | version.workspace = true 8 | edition.workspace = true 9 | repository.workspace = true 10 | 11 | [dependencies] 12 | clap = { workspace = true, features = ["derive"] } 13 | anyhow = { workspace = true } 14 | duct = { workspace = true } 15 | colored = { workspace = true } 16 | cargo_metadata = { workspace = true } 17 | serde = { workspace = true } 18 | serde_json = { workspace = true } 19 | dircpy = { workspace = true, default-features = false } 20 | human_bytes = { workspace = true } 21 | 22 | [features] 23 | _check = [] 24 | -------------------------------------------------------------------------------- /crates/xtask/res/doc_after.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /crates/xtask/src/config.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, sync::LazyLock}; 2 | 3 | #[derive(serde::Deserialize)] 4 | pub struct InternalCmdConfig { 5 | pub crates: HashMap, 6 | pub check_skip_global: Option>, 7 | pub check_env: HashMap, 8 | 9 | pub test_features_global: Option>, 10 | 11 | pub doc_features_global: Option>, 12 | 13 | pub publish_order: Vec, 14 | } 15 | 16 | #[derive(Default, serde::Deserialize, Clone)] 17 | #[serde(default)] 18 | pub struct CrateConfig { 19 | // Check configuration 20 | /// Disables feature powerset check 21 | pub check_no_powerset: bool, 22 | /// Features to check (these features will be always added). 23 | pub check_features: Option>, 24 | pub check_at_least_one_of: Option>>, 25 | pub check_group_features: Option>>, 26 | pub check_mutually_exclusive_features: Option>>, 27 | /// Features to skip check (these features will be never added.). This overrides global skip. 28 | pub check_skip: Option>, 29 | /// If false, check will be performed through `cargo clippy`. 30 | /// If true, `cargo build` will be used instead.` 31 | pub check_build: bool, 32 | 33 | // Test configuration 34 | pub test_enabled: bool, 35 | pub test_features: Option>, 36 | 37 | // Doc configuration 38 | pub doc_enabled: bool, 39 | 40 | // Publish configuration 41 | /// If false, `_check` feature is used when publishing crate. 42 | /// If true, `_release` feature is used instead. 43 | pub use_release_feature: bool, 44 | } 45 | 46 | pub static CRATES_CONFIG: LazyLock = LazyLock::new(|| { 47 | serde_json::from_str(include_str!("../../../crates_config.json")) 48 | .expect("Failed to parse crate_config.json") 49 | }); 50 | -------------------------------------------------------------------------------- /crates/xtask/src/utils.rs: -------------------------------------------------------------------------------- 1 | macro_rules! xprintln { 2 | ($($arg:tt)*) => {{ 3 | use colored::*; 4 | 5 | eprint!("{} ", " rktk ".on_blue(),); 6 | eprintln!($($arg)*); 7 | }}; 8 | } 9 | use std::sync::LazyLock; 10 | 11 | use cargo_metadata::Metadata; 12 | pub(crate) use xprintln; 13 | 14 | /// Metadata for the current (dir) workspace. 15 | pub(crate) static METADATA: LazyLock> = 16 | LazyLock::new(|| cargo_metadata::MetadataCommand::new().no_deps().exec().ok()); 17 | -------------------------------------------------------------------------------- /keyboards/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | # Configuration to minimize binary size 2 | 3 | [profile.release] 4 | debug = true 5 | opt-level = "z" 6 | lto = "fat" 7 | codegen-units = 1 8 | panic = "immediate-abort" 9 | rustflags = ["-Zlocation-detail=none"] 10 | 11 | [unstable] 12 | build-std = ["core", "alloc"] 13 | build-std-features = ["optimize_for_size"] 14 | panic-immediate-abort = true 15 | -------------------------------------------------------------------------------- /keyboards/corne/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [env] 2 | RKTK_CONFIG_PATH = { value = "rktk.json", relative = true } 3 | -------------------------------------------------------------------------------- /keyboards/corne/crates/corne-nrf/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "thumbv7em-none-eabihf" 3 | -------------------------------------------------------------------------------- /keyboards/corne/crates/corne-nrf/Cargo.toml: -------------------------------------------------------------------------------- 1 | # You can remove this line and `forced-target` property and use `.cargo/config.toml` if your crate isn't in workspace. 2 | cargo-features = ["panic-immediate-abort"] 3 | 4 | [[bin]] 5 | name = "corne-nrf" 6 | path = "src/main.rs" 7 | test = false 8 | doctest = false 9 | bench = false 10 | 11 | [package] 12 | name = "corne-nrf" 13 | authors.workspace = true 14 | license.workspace = true 15 | version.workspace = true 16 | edition.workspace = true 17 | repository.workspace = true 18 | publish = false 19 | 20 | [dependencies] 21 | rktk = { workspace = true } 22 | rktk-drivers-nrf = { workspace = true, features = ["softdevice-ble"] } 23 | rktk-drivers-common = { workspace = true } 24 | 25 | cortex-m = { workspace = true } 26 | cortex-m-rt = { workspace = true } 27 | embassy-executor = { workspace = true, features = [ 28 | "arch-cortex-m", 29 | "executor-thread", 30 | "executor-interrupt", 31 | ] } 32 | 33 | embassy-nrf = { workspace = true, features = [ 34 | "gpiote", 35 | "time-driver-rtc1", 36 | "nrf52840", 37 | "nfc-pins-as-gpio", 38 | ] } 39 | nrf-softdevice = { workspace = true, features = [ 40 | "critical-section-impl", 41 | "s140", 42 | "nrf52840", 43 | ] } 44 | 45 | [features] 46 | default = ["right"] 47 | 48 | right = [] 49 | left = [] 50 | 51 | # Remove this line in your project. It's just used in this workspace for check. 52 | _check = [] 53 | -------------------------------------------------------------------------------- /keyboards/corne/crates/corne-nrf/build.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::fs::File; 3 | use std::io::Write; 4 | use std::path::PathBuf; 5 | 6 | fn main() { 7 | let out = &PathBuf::from(env::var_os("OUT_DIR").unwrap()); 8 | File::create(out.join("memory.x")) 9 | .unwrap() 10 | .write_all(include_bytes!("memory.x")) 11 | .unwrap(); 12 | println!("cargo:rustc-link-search={}", out.display()); 13 | 14 | println!("cargo:rerun-if-changed=memory.x"); 15 | 16 | println!("cargo:rustc-link-arg-bins=--nmagic"); 17 | println!("cargo:rustc-link-arg-bins=-Tlink.x"); 18 | } 19 | -------------------------------------------------------------------------------- /keyboards/corne/crates/corne-nrf/memory.x: -------------------------------------------------------------------------------- 1 | MEMORY 2 | { 3 | /* for softdevice v6 */ 4 | FLASH : ORIGIN = 0x00026000, LENGTH = 828K 5 | /* RAM MAX: 256K (0x40000) */ 6 | RAM : ORIGIN = 0x20008000, LENGTH = 200K 7 | } 8 | -------------------------------------------------------------------------------- /keyboards/corne/crates/corne-rp/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "thumbv6m-none-eabi" 3 | -------------------------------------------------------------------------------- /keyboards/corne/crates/corne-rp/Cargo.toml: -------------------------------------------------------------------------------- 1 | cargo-features = ["panic-immediate-abort"] 2 | 3 | [[bin]] 4 | name = "corne-rp" 5 | path = "src/main.rs" 6 | test = false 7 | doctest = false 8 | bench = false 9 | 10 | [package] 11 | name = "corne-rp" 12 | authors.workspace = true 13 | license.workspace = true 14 | version.workspace = true 15 | edition.workspace = true 16 | repository.workspace = true 17 | publish = false 18 | 19 | [dependencies] 20 | rktk = { workspace = true } 21 | rktk-drivers-rp = { workspace = true } 22 | rktk-drivers-common = { workspace = true } 23 | 24 | cortex-m = { workspace = true } 25 | cortex-m-rt = { workspace = true } 26 | embassy-rp = { workspace = true, features = [ 27 | "time-driver", 28 | "critical-section-impl", 29 | "rp2040", 30 | ] } 31 | embassy-executor = { workspace = true, features = [ 32 | "arch-cortex-m", 33 | "executor-thread", 34 | "executor-interrupt", 35 | ] } 36 | 37 | [features] 38 | default = ["right"] 39 | 40 | right = [] 41 | left = [] 42 | 43 | _check = [] 44 | # Remove this line in your project. It's just used in this workspace for check. 45 | -------------------------------------------------------------------------------- /keyboards/corne/crates/corne-rp/README.md: -------------------------------------------------------------------------------- 1 | # corne v3 example 2 | 3 | RKTK firmware example for corne v3 with ProMicro RP2040. To make example simple, 4 | only keyboard feature is enabled (OLED, RGB is disabled). 5 | 6 | Actually, I don't have a corne keyboard, so please report any issues you 7 | encounter. 8 | 9 | ## Building 10 | 11 | 1. Clone this repository and move here 12 | ```bash 13 | git clone https://github.com/nazo6/rktk 14 | cd rktk/examples/corne 15 | ``` 16 | 17 | 2. Install `uf2deploy` 18 | ```bash 19 | cargo install uf2deploy 20 | ``` 21 | 22 | 3. Build and deploy the firmware 23 | ```bash 24 | # By running cargo run, uf2deploy will be executed automatically. uf2deploy converts elf to uf2 and copies uf2 to attached device. 25 | # Use feature `left` or `right` to build firmware for left or right half. 26 | cargo run --release --features left 27 | ``` 28 | -------------------------------------------------------------------------------- /keyboards/corne/crates/corne-rp/build.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::fs::File; 3 | use std::io::Write; 4 | use std::path::PathBuf; 5 | 6 | fn main() { 7 | let out = &PathBuf::from(env::var_os("OUT_DIR").unwrap()); 8 | File::create(out.join("memory.x")) 9 | .unwrap() 10 | .write_all(include_bytes!("memory.x")) 11 | .unwrap(); 12 | println!("cargo:rustc-link-search={}", out.display()); 13 | 14 | println!("cargo:rerun-if-changed=memory.x"); 15 | 16 | println!("cargo:rustc-link-arg-bins=--nmagic"); 17 | println!("cargo:rustc-link-arg-bins=-Tlink.x"); 18 | println!("cargo:rustc-link-arg-bins=-Tlink-rp.x"); 19 | } 20 | -------------------------------------------------------------------------------- /keyboards/corne/crates/corne-rp/memory.x: -------------------------------------------------------------------------------- 1 | MEMORY { 2 | BOOT2 : ORIGIN = 0x10000000, LENGTH = 0x100 3 | FLASH : ORIGIN = 0x10000100, LENGTH = 2048K - 0x100 4 | 5 | /* Pick one of the two options for RAM layout */ 6 | 7 | /* OPTION A: Use all RAM banks as one big block */ 8 | /* Reasonable, unless you are doing something */ 9 | /* really particular with DMA or other concurrent */ 10 | /* access that would benefit from striping */ 11 | RAM : ORIGIN = 0x20000000, LENGTH = 264K 12 | 13 | /* OPTION B: Keep the unstriped sections separate */ 14 | /* RAM: ORIGIN = 0x20000000, LENGTH = 256K */ 15 | /* SCRATCH_A: ORIGIN = 0x20040000, LENGTH = 4K */ 16 | /* SCRATCH_B: ORIGIN = 0x20041000, LENGTH = 4K */ 17 | } 18 | -------------------------------------------------------------------------------- /keyboards/corne/rktk.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../crates/rktk/schema.json", 3 | "constant": { 4 | "keyboard": { 5 | "cols": 12, 6 | "rows": 4 7 | } 8 | }, 9 | "dynamic": { 10 | "keyboard": { 11 | "name": "corne" 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /keyboards/dummy/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [env] 2 | RKTK_CONFIG_PATH = { value = "../corne/rktk.json", relative = true } 3 | -------------------------------------------------------------------------------- /keyboards/dummy/crates/dummy-rp/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "thumbv6m-none-eabi" 3 | -------------------------------------------------------------------------------- /keyboards/dummy/crates/dummy-rp/Cargo.toml: -------------------------------------------------------------------------------- 1 | cargo-features = ["panic-immediate-abort"] 2 | 3 | [[bin]] 4 | name = "dummy-rp" 5 | path = "src/main.rs" 6 | test = false 7 | doctest = false 8 | bench = false 9 | 10 | [package] 11 | name = "dummy-rp" 12 | authors.workspace = true 13 | license.workspace = true 14 | version.workspace = true 15 | edition.workspace = true 16 | repository.workspace = true 17 | publish = false 18 | 19 | [dependencies] 20 | rktk-drivers-rp = { workspace = true } 21 | rktk = { workspace = true } 22 | cortex-m = { workspace = true, features = ["inline-asm"] } 23 | cortex-m-rt = { workspace = true } 24 | embassy-rp = { workspace = true, features = [ 25 | "time-driver", 26 | "critical-section-impl", 27 | "rp2040", 28 | ] } 29 | embassy-executor = { workspace = true, features = [ 30 | "arch-cortex-m", 31 | "executor-thread", 32 | "executor-interrupt", 33 | ] } 34 | 35 | [features] 36 | _check = [] 37 | -------------------------------------------------------------------------------- /keyboards/dummy/crates/dummy-rp/src/keymap.rs: -------------------------------------------------------------------------------- 1 | use rktk::config::keymap::{Keymap, Layer, LayerKeymap, keymanager::keycode::_____}; 2 | 3 | #[rustfmt::skip] 4 | const L0: LayerKeymap = [ 5 | [ _____ , _____ , _____ , _____ , _____ , _____ , /**/ _____ , _____ , _____ , _____ , _____ , _____ ], 6 | [ _____ , _____ , _____ , _____ , _____ , _____ , /**/ _____ , _____ , _____ , _____ , _____ , _____ ], 7 | [ _____ , _____ , _____ , _____ , _____ , _____ , /**/ _____ , _____ , _____ , _____ , _____ , _____ ], 8 | [ _____ , _____ , _____ , _____ , _____ , _____ , /**/ _____ , _____ , _____ , _____ , _____ , _____ ], 9 | ]; 10 | 11 | pub const KEYMAP: Keymap = Keymap { 12 | layers: [ 13 | Layer { 14 | keymap: L0, 15 | ..Layer::const_default() 16 | }, 17 | Layer { 18 | keymap: L0, 19 | ..Layer::const_default() 20 | }, 21 | Layer { 22 | keymap: L0, 23 | ..Layer::const_default() 24 | }, 25 | Layer { 26 | keymap: L0, 27 | ..Layer::const_default() 28 | }, 29 | Layer { 30 | keymap: L0, 31 | ..Layer::const_default() 32 | }, 33 | ], 34 | ..Keymap::const_default() 35 | }; 36 | -------------------------------------------------------------------------------- /keyboards/dummy/crates/dummy-rp/src/main.rs: -------------------------------------------------------------------------------- 1 | //! Dummy keyboard to test dummy drivers. 2 | 3 | #![no_std] 4 | #![no_main] 5 | 6 | mod keymap; 7 | 8 | use core::panic::PanicInfo; 9 | 10 | use embassy_executor::Spawner; 11 | use rktk::{ 12 | config::new_rktk_opts, 13 | drivers::{ 14 | Drivers, dummy, 15 | interface::keyscan::{KeyChangeEvent, KeyscanDriver}, 16 | }, 17 | hooks::empty_hooks::create_empty_hooks, 18 | }; 19 | 20 | use rktk_drivers_rp::system::RpSystemDriver; 21 | 22 | pub struct DummyKeyscanDriver; 23 | impl KeyscanDriver for DummyKeyscanDriver { 24 | async fn scan(&mut self, _cb: impl FnMut(KeyChangeEvent)) { 25 | let _: () = core::future::pending().await; 26 | } 27 | } 28 | 29 | #[embassy_executor::main] 30 | async fn main(spawner: Spawner) { 31 | let _p = embassy_rp::init(Default::default()); 32 | 33 | let drivers = Drivers { 34 | keyscan: DummyKeyscanDriver, 35 | system: RpSystemDriver, 36 | mouse: dummy::mouse(), 37 | usb_builder: dummy::usb_builder(), 38 | display: dummy::display(), 39 | split: dummy::split(), 40 | rgb: dummy::rgb(), 41 | ble_builder: dummy::ble_builder(), 42 | storage: dummy::storage(), 43 | debounce: dummy::debounce(), 44 | encoder: dummy::encoder(), 45 | }; 46 | 47 | rktk::task::start( 48 | spawner, 49 | drivers, 50 | create_empty_hooks(), 51 | new_rktk_opts(&keymap::KEYMAP, None), 52 | ) 53 | .await; 54 | } 55 | 56 | #[panic_handler] 57 | fn panic(_info: &PanicInfo) -> ! { 58 | loop {} 59 | } 60 | -------------------------------------------------------------------------------- /keyboards/keyball61/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [env] 2 | RKTK_CONFIG_PATH = { value = "rktk.json", relative = true } 3 | -------------------------------------------------------------------------------- /keyboards/keyball61/crates/keyball61-common/Cargo.toml: -------------------------------------------------------------------------------- 1 | cargo-features = ["panic-immediate-abort"] 2 | 3 | [package] 4 | name = "keyball61-common" 5 | version.workspace = true 6 | license.workspace = true 7 | edition.workspace = true 8 | authors.workspace = true 9 | repository.workspace = true 10 | 11 | [dependencies] 12 | rktk = { workspace = true } 13 | rktk-drivers-common = { workspace = true } 14 | 15 | [features] 16 | _check = [] 17 | -------------------------------------------------------------------------------- /keyboards/keyball61/crates/keyball61-nrf/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "thumbv7em-none-eabihf" 3 | -------------------------------------------------------------------------------- /keyboards/keyball61/crates/keyball61-nrf/Cargo.toml: -------------------------------------------------------------------------------- 1 | cargo-features = ["panic-immediate-abort"] 2 | 3 | [package] 4 | name = "keyball61-nrf" 5 | version.workspace = true 6 | license.workspace = true 7 | edition.workspace = true 8 | authors.workspace = true 9 | repository.workspace = true 10 | 11 | [lib] 12 | path = "src/lib.rs" 13 | test = false 14 | doctest = false 15 | bench = false 16 | 17 | [[bin]] 18 | name = "keyball61" 19 | test = false 20 | doctest = false 21 | bench = false 22 | 23 | [dependencies] 24 | keyball61-common = { path = "../keyball61-common/" } 25 | 26 | rktk = { workspace = true } 27 | rktk-drivers-nrf = { workspace = true } 28 | rktk-drivers-common = { workspace = true } 29 | 30 | embassy-executor = { workspace = true, features = [ 31 | "arch-cortex-m", 32 | "executor-thread", 33 | "executor-interrupt", 34 | ] } 35 | embassy-sync = { workspace = true } 36 | embassy-time = { workspace = true } 37 | embassy-embedded-hal = { workspace = true } 38 | 39 | embassy-nrf = { workspace = true, features = [ 40 | "gpiote", 41 | "time-driver-rtc1", 42 | "nrf52840", 43 | "nfc-pins-as-gpio", 44 | "unstable-pac", 45 | ] } 46 | nrf-softdevice = { workspace = true, features = [ 47 | "ble-peripheral", 48 | "ble-central", 49 | "critical-section-impl", 50 | "s140", 51 | "nrf52840", 52 | ] } 53 | cortex-m = { workspace = true } 54 | cortex-m-rt = { workspace = true } 55 | 56 | once_cell = { workspace = true, features = ["critical-section"] } 57 | 58 | [features] 59 | _check = [] 60 | 61 | usb = ["rktk-drivers-nrf/softdevice"] 62 | ble = ["rktk-drivers-nrf/softdevice-ble"] 63 | default = ["ble", "usb"] 64 | -------------------------------------------------------------------------------- /keyboards/keyball61/crates/keyball61-nrf/build.rs: -------------------------------------------------------------------------------- 1 | //! This build script copies the `memory.x` file from the crate root into 2 | //! a directory where the linker can always find it at build time. 3 | //! For many projects this is optional, as the linker always searches the 4 | //! project root directory -- wherever `Cargo.toml` is. However, if you 5 | //! are using a workspace or have a more complicated build setup, this 6 | //! build script becomes required. Additionally, by requesting that 7 | //! Cargo re-run the build script whenever `memory.x` is changed, 8 | //! updating `memory.x` ensures a rebuild of the application with the 9 | //! new memory settings. 10 | 11 | use std::env; 12 | use std::fs::File; 13 | use std::io::Write; 14 | use std::path::PathBuf; 15 | 16 | fn main() { 17 | // Put `memory.x` in our output directory and ensure it's 18 | // on the linker search path. 19 | let out = &PathBuf::from(env::var_os("OUT_DIR").unwrap()); 20 | File::create(out.join("memory.x")) 21 | .unwrap() 22 | .write_all(include_bytes!("memory.x")) 23 | .unwrap(); 24 | println!("cargo:rustc-link-search={}", out.display()); 25 | 26 | // By default, Cargo will re-run a build script whenever 27 | // any file in the project changes. By specifying `memory.x` 28 | // here, we ensure the build script is only re-run when 29 | // `memory.x` is changed. 30 | println!("cargo:rerun-if-changed=memory.x"); 31 | 32 | println!("cargo:rustc-link-arg-bins=--nmagic"); 33 | println!("cargo:rustc-link-arg-bins=-Tlink.x"); 34 | } 35 | -------------------------------------------------------------------------------- /keyboards/keyball61/crates/keyball61-nrf/memory.x: -------------------------------------------------------------------------------- 1 | MEMORY 2 | { 3 | /* for softdevice v6 */ 4 | FLASH : ORIGIN = 0x00026000, LENGTH = 828K 5 | /* RAM MAX: 256K (0x40000) */ 6 | RAM : ORIGIN = 0x20008000, LENGTH = 200K 7 | } 8 | -------------------------------------------------------------------------------- /keyboards/keyball61/crates/keyball61-nrf/src/bin/keyball61.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![no_main] 3 | #![feature(impl_trait_in_assoc_type)] 4 | 5 | use embassy_executor::Spawner; 6 | use keyball61_nrf::start; 7 | use rktk::config::keymap::Keymap; 8 | 9 | static KM: Keymap = Keymap::const_default(); 10 | 11 | #[embassy_executor::main] 12 | async fn main(spawner: Spawner) { 13 | start(spawner, &KM).await; 14 | } 15 | -------------------------------------------------------------------------------- /keyboards/keyball61/crates/keyball61-rp/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "thumbv6m-none-eabi" 3 | -------------------------------------------------------------------------------- /keyboards/keyball61/crates/keyball61-rp/Cargo.toml: -------------------------------------------------------------------------------- 1 | cargo-features = ["panic-immediate-abort"] 2 | 3 | [package] 4 | name = "keyball61-rp" 5 | authors.workspace = true 6 | license.workspace = true 7 | version.workspace = true 8 | edition.workspace = true 9 | repository.workspace = true 10 | 11 | [lib] 12 | path = "src/lib.rs" 13 | test = false 14 | doctest = false 15 | bench = false 16 | 17 | [[bin]] 18 | name = "keyball61" 19 | test = false 20 | doctest = false 21 | bench = false 22 | 23 | [dependencies] 24 | keyball61-common = { path = "../keyball61-common/" } 25 | 26 | rktk = { workspace = true, features = ["rrp"] } 27 | rktk-drivers-rp = { workspace = true } 28 | rktk-drivers-common = { workspace = true } 29 | 30 | embassy-executor = { workspace = true, features = [ 31 | "arch-cortex-m", 32 | "executor-thread", 33 | "executor-interrupt", 34 | ] } 35 | embassy-sync = { workspace = true } 36 | embassy-time = { workspace = true } 37 | embassy-embedded-hal = { workspace = true } 38 | 39 | embassy-rp = { workspace = true, features = [ 40 | "time-driver", 41 | "critical-section-impl", 42 | "rp2040", 43 | ] } 44 | 45 | cortex-m = { workspace = true } 46 | cortex-m-rt = { workspace = true } 47 | 48 | portable-atomic = { workspace = true } 49 | 50 | defmt = { workspace = true, optional = true } 51 | 52 | [features] 53 | _check = [] 54 | 55 | default = ["defmt", "single-core"] 56 | 57 | defmt = [ 58 | "dep:defmt", 59 | "rktk/defmt", 60 | "rktk-drivers-common/defmt-usb", 61 | "rktk-drivers-common/defmt-timestamp", 62 | "rktk-drivers-rp/defmt", 63 | ] 64 | 65 | single-core = ["portable-atomic/unsafe-assume-single-core"] 66 | -------------------------------------------------------------------------------- /keyboards/keyball61/crates/keyball61-rp/build.rs: -------------------------------------------------------------------------------- 1 | //! This build script copies the `memory.x` file from the crate root into 2 | //! a directory where the linker can always find it at build time. 3 | //! For many projects this is optional, as the linker always searches the 4 | //! project root directory -- wherever `Cargo.toml` is. However, if you 5 | //! are using a workspace or have a more complicated build setup, this 6 | //! build script becomes required. Additionally, by requesting that 7 | //! Cargo re-run the build script whenever `memory.x` is changed, 8 | //! updating `memory.x` ensures a rebuild of the application with the 9 | //! new memory settings. 10 | 11 | use std::env; 12 | use std::fs::File; 13 | use std::io::Write; 14 | use std::path::PathBuf; 15 | 16 | fn main() { 17 | // Put `memory.x` in our output directory and ensure it's 18 | // on the linker search path. 19 | let out = &PathBuf::from(env::var_os("OUT_DIR").unwrap()); 20 | File::create(out.join("memory.x")) 21 | .unwrap() 22 | .write_all(include_bytes!("memory.x")) 23 | .unwrap(); 24 | println!("cargo:rustc-link-search={}", out.display()); 25 | 26 | // By default, Cargo will re-run a build script whenever 27 | // any file in the project changes. By specifying `memory.x` 28 | // here, we ensure the build script is only re-run when 29 | // `memory.x` is changed. 30 | println!("cargo:rerun-if-changed=memory.x"); 31 | 32 | println!("cargo:rustc-link-arg-bins=--nmagic"); 33 | println!("cargo:rustc-link-arg-bins=-Tlink.x"); 34 | println!("cargo:rustc-link-arg-bins=-Tlink-rp.x"); 35 | 36 | println!("cargo:rerun-if-env-changed=CARGO_FEATURE_DEFMT"); 37 | if std::env::var("CARGO_FEATURE_DEFMT").is_ok() { 38 | println!("cargo:rustc-link-arg-bins=-Tdefmt.x"); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /keyboards/keyball61/crates/keyball61-rp/memory.x: -------------------------------------------------------------------------------- 1 | MEMORY { 2 | BOOT2 : ORIGIN = 0x10000000, LENGTH = 0x100 3 | FLASH : ORIGIN = 0x10000100, LENGTH = 2048K - 0x100 4 | 5 | /* Pick one of the two options for RAM layout */ 6 | 7 | /* OPTION A: Use all RAM banks as one big block */ 8 | /* Reasonable, unless you are doing something */ 9 | /* really particular with DMA or other concurrent */ 10 | /* access that would benefit from striping */ 11 | RAM : ORIGIN = 0x20000000, LENGTH = 256K 12 | 13 | /* OPTION B: Keep the unstriped sections separate */ 14 | /* RAM: ORIGIN = 0x20000000, LENGTH = 256K */ 15 | /* SCRATCH_A: ORIGIN = 0x20040000, LENGTH = 4K */ 16 | /* SCRATCH_B: ORIGIN = 0x20041000, LENGTH = 4K */ 17 | } 18 | 19 | -------------------------------------------------------------------------------- /keyboards/keyball61/crates/keyball61-rp/src/bin/keyball61.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![no_main] 3 | #![feature(impl_trait_in_assoc_type)] 4 | 5 | use embassy_executor::Spawner; 6 | use keyball61_rp::start; 7 | use rktk::config::keymap::Keymap; 8 | 9 | static KM: Keymap = Keymap::const_default(); 10 | 11 | #[embassy_executor::main] 12 | async fn main(spawner: Spawner) { 13 | start(spawner, &KM).await; 14 | } 15 | -------------------------------------------------------------------------------- /keyboards/neg/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [env] 2 | RKTK_CONFIG_PATH = { value = "rktk.json", relative = true } 3 | -------------------------------------------------------------------------------- /keyboards/neg/crates/neg-nrf/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "thumbv7em-none-eabihf" 3 | -------------------------------------------------------------------------------- /keyboards/neg/crates/neg-nrf/build.rs: -------------------------------------------------------------------------------- 1 | //! This build script copies the `memory.x` file from the crate root into 2 | //! a directory where the linker can always find it at build time. 3 | //! For many projects this is optional, as the linker always searches the 4 | //! project root directory -- wherever `Cargo.toml` is. However, if you 5 | //! are using a workspace or have a more complicated build setup, this 6 | //! build script becomes required. Additionally, by requesting that 7 | //! Cargo re-run the build script whenever `memory.x` is changed, 8 | //! updating `memory.x` ensures a rebuild of the application with the 9 | //! new memory settings. 10 | 11 | use std::env; 12 | use std::fs::File; 13 | use std::io::Write; 14 | use std::path::PathBuf; 15 | 16 | fn main() { 17 | // Put `memory.x` in our output directory and ensure it's 18 | // on the linker search path. 19 | let out = &PathBuf::from(env::var_os("OUT_DIR").unwrap()); 20 | File::create(out.join("memory.x")) 21 | .unwrap() 22 | .write_all(include_bytes!("memory.x")) 23 | .unwrap(); 24 | println!("cargo:rustc-link-search={}", out.display()); 25 | 26 | // By default, Cargo will re-run a build script whenever 27 | // any file in the project changes. By specifying `memory.x` 28 | // here, we ensure the build script is only re-run when 29 | // `memory.x` is changed. 30 | println!("cargo:rerun-if-changed=memory.x"); 31 | 32 | println!("cargo:rustc-link-arg-bins=--nmagic"); 33 | println!("cargo:rustc-link-arg-bins=-Tlink.x"); 34 | 35 | println!("cargo:rerun-if-env-changed=CARGO_FEATURE_DEFMT"); 36 | if std::env::var("CARGO_FEATURE_DEFMT").is_ok() { 37 | println!("cargo:rustc-link-arg-bins=-Tdefmt.x"); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /keyboards/neg/crates/neg-nrf/memory.x: -------------------------------------------------------------------------------- 1 | MEMORY 2 | { 3 | /* for softdevice v6 */ 4 | FLASH : ORIGIN = 0x00026000, LENGTH = 828K 5 | /* RAM MAX: 256K (0x40000) */ 6 | RAM : ORIGIN = 0x20008000, LENGTH = 0x38000 7 | } 8 | -------------------------------------------------------------------------------- /keyboards/neg/crates/neg-nrf/src/bin/neg_nrf_master.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![no_main] 3 | #![feature(impl_trait_in_assoc_type)] 4 | 5 | use embassy_executor::Spawner; 6 | use neg_nrf::{init_peri, start_master}; 7 | use rktk::{config::keymap::Keymap, hooks::create_empty_hooks}; 8 | 9 | // Empty keymap for demo. 10 | // Please replace with your own keymap configuration. 11 | static KM: Keymap = Keymap::const_default(); 12 | 13 | #[embassy_executor::main] 14 | async fn main(spawner: Spawner) { 15 | let p = init_peri(); 16 | start_master(spawner, p, create_empty_hooks(), &KM).await; 17 | } 18 | -------------------------------------------------------------------------------- /keyboards/neg/crates/neg-nrf/src/bin/neg_nrf_slave.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![no_main] 3 | #![feature(impl_trait_in_assoc_type)] 4 | 5 | use embassy_executor::Spawner; 6 | use neg_nrf::{init_peri, start_slave}; 7 | use rktk::hooks::create_empty_hooks; 8 | 9 | #[embassy_executor::main] 10 | async fn main(spawner: Spawner) { 11 | let p = init_peri(); 12 | start_slave(spawner, p, create_empty_hooks()).await; 13 | } 14 | -------------------------------------------------------------------------------- /keyboards/neg/crates/neg-nrf/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | 3 | use core::panic::PanicInfo; 4 | 5 | use embassy_nrf::bind_interrupts; 6 | use rktk_drivers_common::panic_utils; 7 | 8 | mod common; 9 | mod drivers; 10 | mod master; 11 | mod misc; 12 | mod slave; 13 | 14 | // ===== Global linkages ===== 15 | 16 | #[cfg(feature = "alloc")] 17 | extern crate alloc; 18 | #[cfg(feature = "alloc")] 19 | use embedded_alloc::LlffHeap as Heap; 20 | 21 | #[cfg(feature = "alloc")] 22 | #[global_allocator] 23 | static HEAP: Heap = Heap::empty(); 24 | 25 | #[cfg(feature = "sd")] 26 | use nrf_softdevice as _; 27 | 28 | #[panic_handler] 29 | fn panic(info: &PanicInfo) -> ! { 30 | cortex_m::interrupt::disable(); 31 | panic_utils::save_panic_info(info); 32 | cortex_m::peripheral::SCB::sys_reset() 33 | } 34 | 35 | // ===== Irq definitions ===== 36 | 37 | bind_interrupts!(pub struct Irqs { 38 | USBD => embassy_nrf::usb::InterruptHandler; 39 | SPI2 => embassy_nrf::spim::InterruptHandler; 40 | TWISPI0 => embassy_nrf::twim::InterruptHandler; 41 | UARTE0 => embassy_nrf::buffered_uarte::InterruptHandler; 42 | #[cfg(feature = "trouble")] 43 | RNG => embassy_nrf::rng::InterruptHandler; 44 | #[cfg(feature = "trouble")] 45 | EGU0_SWI0 => nrf_sdc::mpsl::LowPrioInterruptHandler; 46 | #[cfg(feature = "trouble")] 47 | CLOCK_POWER => nrf_sdc::mpsl::ClockInterruptHandler; 48 | #[cfg(feature = "trouble")] 49 | RADIO => nrf_sdc::mpsl::HighPrioInterruptHandler; 50 | #[cfg(feature = "trouble")] 51 | TIMER0 => nrf_sdc::mpsl::HighPrioInterruptHandler; 52 | #[cfg(feature = "trouble")] 53 | RTC0 => nrf_sdc::mpsl::HighPrioInterruptHandler; 54 | #[cfg(not(feature = "software-vbus"))] 55 | CLOCK_POWER =>embassy_nrf::usb::vbus_detect::InterruptHandler; 56 | }); 57 | 58 | pub use common::init_peri; 59 | pub use master::start_master; 60 | pub use slave::start_slave; 61 | -------------------------------------------------------------------------------- /keyboards/neg/crates/neg-nrf/src/slave.rs: -------------------------------------------------------------------------------- 1 | use embassy_nrf::Peripherals; 2 | use rktk::{ 3 | config::keymap::Keymap, 4 | drivers::{Drivers, dummy}, 5 | hooks::AllHooks, 6 | }; 7 | 8 | #[cfg(feature = "sd")] 9 | use crate::common::init_sd; 10 | use crate::*; 11 | 12 | const EMPTY_KM: Keymap = Keymap::const_default(); 13 | 14 | pub async fn start_slave(spawner: embassy_executor::Spawner, p: Peripherals, hooks: impl AllHooks) { 15 | #[cfg(feature = "sd")] 16 | let _ = init_sd(spawner).await; 17 | 18 | let spi = create_spi!(p); 19 | 20 | let drivers = Drivers { 21 | keyscan: driver_keyscan!(p, spi), 22 | system: driver_system!(p), 23 | mouse: Some(driver_mouse!(p, spi)), 24 | usb_builder: dummy::usb_builder(), 25 | display: Some(driver_display!(p)), 26 | split: Some(driver_split!(p)), 27 | rgb: Some(driver_rgb!(p)), 28 | storage: dummy::storage(), 29 | ble_builder: dummy::ble_builder(), 30 | debounce: Some(driver_debounce!()), 31 | encoder: Some(driver_encoder!(p)), 32 | }; 33 | rktk::task::start(spawner, drivers, hooks, misc::get_opts(&EMPTY_KM)).await; 34 | } 35 | -------------------------------------------------------------------------------- /keyboards/neg/rktk.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../rktk/crates/rktk/schema.json", 3 | "constant": { 4 | "keyboard": { 5 | "cols": 16, 6 | "rows": 5, 7 | "encoder_count": 1 8 | }, 9 | "key_manager": { 10 | "combo_key_max_definitions": 0, 11 | "combo_key_max_sources": 2, 12 | "oneshot_state_size": 5, 13 | "tap_dance_max_definitions": 1, 14 | "tap_dance_max_repeats": 4 15 | } 16 | }, 17 | "dynamic": { 18 | "rktk": { 19 | "scan_interval_keyboard": 5, 20 | "scan_interval_mouse": 5, 21 | "display_timeout": 100000, 22 | "swap_mouse_x_y": true 23 | }, 24 | "key_manager": { 25 | "mouse": { 26 | "auto_mouse_threshold": 10 27 | } 28 | }, 29 | "keyboard": { 30 | "name": "negL", 31 | "layout": { 32 | "keymap": [ 33 | [{ "x": 2 }, "0,2", "0,3", "0,4", "0,5", { "x": 5 }, "0,10", "0,11", "0,12", "0,13"], 34 | [{ "y": -0.75 }, "0,0", "0,1", { "x": 13 }, "0,14", "0,15"], 35 | [{ "y": -0.25, "x": 2 }, "1,2", "1,3", "1,4", "1,5", { "x": 5 }, "1,10", "1,11", "1,12", "1,13"], 36 | [{ "y": -0.75 }, "1,0", "1,1", { "x": 13 }, "1,14", "1,15"], 37 | [{ "y": -0.25, "x": 2 }, "2,2", "2,3", "2,4", "2,5", { "x": 5 }, "2,10", "2,11", "2,12", "2,13"], 38 | [{ "y": -0.75 }, "2,0", "2,1", { "x": 13 }, "2,14", "2,15"], 39 | [{ "y": -0.25, "x": 2 }, "3,2", "3,3", "3,4", "3,5", { "x": 5 }, "3,10", "3,11", "3,12", "3,13"], 40 | [{ "y": -0.9, "x": 6 }, "3,6", { "x": 3 }, "3,9"], 41 | [{ "y": -0.85 }, "3,0", "3,1", { "x": 13 }, "3,14", "3,15"], 42 | [{ "y": -0.25, "x": 2 }, "4,2", "4,3"], 43 | [{ "y": -0.75 }, "4,0", "4,1", { "x": 2.1 }, "4,4", { "x": 9.9 }, "4,14", "4,15"], 44 | [{ "r": 5, "rx": 5.75, "ry": 4.75, "y": -0.4, "x": -0.55 }, "4,5"], 45 | [{ "r": 10, "rx": 6.75, "ry": 5, "y": -0.5, "x": -0.5 }, "4,6"], 46 | [{ "r": 15, "rx": 7.75, "ry": 5.5, "y": -0.75, "x": -0.5 }, "4,7"], 47 | [{ "r": -15, "rx": 9.25, "y": -0.75, "x": -0.25 }, "4,8"], 48 | [{ "r": -10, "rx": 10.25, "ry": 5, "y": -0.5, "x": -0.25 }, "4,9"], 49 | [{ "r": -5, "rx": 11, "y": -0.65, "x": 0.1 }, "4,10"] 50 | ] 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rktk", 3 | "packageManager": "pnpm@10.6.3", 4 | "devDependencies": { 5 | "@biomejs/biome": "^2.2.4", 6 | "wrangler": "^4.40.2" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "crates/rktk-client" 3 | - "site" 4 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "nightly-2025-10-31" 3 | components = ["clippy", "rust-std", "rust-src"] 4 | targets = [ 5 | "thumbv6m-none-eabi", 6 | "thumbv7em-none-eabihf", 7 | "wasm32-unknown-unknown", 8 | ] 9 | -------------------------------------------------------------------------------- /site/.gitignore: -------------------------------------------------------------------------------- 1 | # deps 2 | /node_modules 3 | 4 | # generated content 5 | .contentlayer 6 | .content-collections 7 | .source 8 | 9 | # test & build 10 | /coverage 11 | /.next/ 12 | /out/ 13 | /build 14 | *.tsbuildinfo 15 | 16 | # misc 17 | .DS_Store 18 | *.pem 19 | /.pnp 20 | .pnp.js 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | # others 26 | .env*.local 27 | .vercel 28 | next-env.d.ts -------------------------------------------------------------------------------- /site/README.md: -------------------------------------------------------------------------------- 1 | # rktk-site 2 | -------------------------------------------------------------------------------- /site/app/(home)/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from "react"; 2 | import { HomeLayout } from "fumadocs-ui/layouts/home"; 3 | import { baseOptions } from "@/app/layout.config"; 4 | 5 | export default function Layout({ children }: { children: ReactNode }) { 6 | return {children}; 7 | } 8 | -------------------------------------------------------------------------------- /site/app/(home)/page.tsx: -------------------------------------------------------------------------------- 1 | import { Callout } from "fumadocs-ui/components/callout"; 2 | import Link from "next/link"; 3 | import { AiFillGithub } from "react-icons/ai"; 4 | import { BiSolidRightArrowAlt } from "react-icons/bi"; 5 | 6 | export default function HomePage() { 7 | return ( 8 |
9 |
10 | This document is in construction 11 |
12 |

13 | RKTK 14 |

15 |

16 | Rust keyboard firmware framework 17 |

18 |
19 | 23 | Get started 24 | 25 | 26 | 31 | View on Github 32 | 33 | 34 |
35 |
36 |
37 |
38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /site/app/api/search/route.ts: -------------------------------------------------------------------------------- 1 | import { source } from "@/lib/source"; 2 | import { createFromSource } from "fumadocs-core/search/server"; 3 | 4 | // it should be cached forever 5 | export const revalidate = false; 6 | 7 | export const { staticGET: GET } = createFromSource(source); 8 | -------------------------------------------------------------------------------- /site/app/docs/[[...slug]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { source } from "@/lib/source"; 2 | import { 3 | DocsBody, 4 | DocsDescription, 5 | DocsPage, 6 | DocsTitle, 7 | } from "fumadocs-ui/page"; 8 | import { notFound } from "next/navigation"; 9 | import defaultMdxComponents, { createRelativeLink } from "fumadocs-ui/mdx"; 10 | import { BiLinkExternal } from "react-icons/bi"; 11 | import clsx from "clsx"; 12 | import { ComponentProps } from "react"; 13 | 14 | function createLink(Base: React.FC>) { 15 | return function Link(props: React.ComponentPropsWithoutRef<"a">) { 16 | const isExternal = props.href ? /^https?:\/\//.test(props.href) : false; 17 | 18 | return ( 19 | 23 | {props.children} 24 | {isExternal && } 25 | 26 | ); 27 | }; 28 | } 29 | 30 | export default async function Page(props: { 31 | params: Promise<{ slug?: string[] }>; 32 | }) { 33 | const params = await props.params; 34 | const page = source.getPage(params.slug); 35 | if (!page) notFound(); 36 | 37 | const MDXContent = page.data.body; 38 | 39 | return ( 40 | 51 | {page.data.title} 52 | {page.data.description} 53 | 54 | 60 | 61 | 62 | ); 63 | } 64 | 65 | export async function generateStaticParams() { 66 | return source.generateParams(); 67 | } 68 | 69 | export async function generateMetadata(props: { 70 | params: Promise<{ slug?: string[] }>; 71 | }) { 72 | const params = await props.params; 73 | const page = source.getPage(params.slug); 74 | if (!page) notFound(); 75 | 76 | return { 77 | title: page.data.title, 78 | description: page.data.description, 79 | }; 80 | } 81 | -------------------------------------------------------------------------------- /site/app/docs/layout.tsx: -------------------------------------------------------------------------------- 1 | import { DocsLayout, type DocsLayoutProps } from "fumadocs-ui/layouts/notebook"; 2 | import type { ReactNode } from "react"; 3 | import { baseOptions } from "@/app/layout.config"; 4 | import { source } from "@/lib/source"; 5 | import { GithubInfo } from "fumadocs-ui/components/github-info"; 6 | 7 | const docsOptions: DocsLayoutProps = { 8 | ...baseOptions, 9 | tree: source.pageTree, 10 | links: [ 11 | { 12 | type: "custom", 13 | children: , 14 | }, 15 | ], 16 | }; 17 | 18 | export default function Layout({ children }: { children: ReactNode }) { 19 | return {children}; 20 | } 21 | -------------------------------------------------------------------------------- /site/app/global.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | @import "fumadocs-ui/css/neutral.css"; 3 | @import "fumadocs-ui/css/preset.css"; 4 | 5 | @source '../node_modules/fumadocs-ui/dist/**/*.js'; 6 | 7 | :root { 8 | --fd-layout-width: 1400px; 9 | 10 | --color-fd-primary: hsl(340, 40%, 48%); 11 | } 12 | 13 | .dark { 14 | --color-fd-background: hsl(0, 0%, 0%); 15 | } 16 | -------------------------------------------------------------------------------- /site/app/layout.config.tsx: -------------------------------------------------------------------------------- 1 | import type { BaseLayoutProps } from "fumadocs-ui/layouts/shared"; 2 | import { AiFillGithub } from "react-icons/ai"; 3 | import { BiLinkExternal } from "react-icons/bi"; 4 | 5 | /** 6 | * Shared layout configurations 7 | * 8 | * you can customise layouts individually from: 9 | * Home Layout: app/(home)/layout.tsx 10 | * Docs Layout: app/docs/layout.tsx 11 | */ 12 | export const baseOptions: BaseLayoutProps = { 13 | nav: { 14 | title: ( 15 |
16 | logo 21 |

RKTK

22 |
23 | ), 24 | }, 25 | links: [ 26 | { 27 | text: "Documentation", 28 | url: "/docs", 29 | active: "nested-url", 30 | }, 31 | { 32 | text: ( 33 |

34 | API Docs 35 | 36 |

37 | ), 38 | url: "https://rktk-docs.nazo6.dev", 39 | }, 40 | { 41 | text: ( 42 |

43 | RKTK Client 44 | 45 |

46 | ), 47 | url: "https://rktk-client.nazo6.dev", 48 | }, 49 | { 50 | type: "icon", 51 | url: "https://github.com/nazo6/rktk", 52 | text: "Github", 53 | icon: , 54 | external: true, 55 | }, 56 | ], 57 | }; 58 | -------------------------------------------------------------------------------- /site/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import "./global.css"; 2 | import { RootProvider } from "fumadocs-ui/provider"; 3 | import { Inter } from "next/font/google"; 4 | import type { ReactNode } from "react"; 5 | 6 | const inter = Inter({ 7 | subsets: ["latin"], 8 | }); 9 | 10 | export default function Layout({ children }: { children: ReactNode }) { 11 | return ( 12 | 13 | 14 | 21 | {children} 22 | 23 | 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /site/app/not-found.tsx: -------------------------------------------------------------------------------- 1 | import { HomeLayout } from "fumadocs-ui/layouts/home"; 2 | import { baseOptions } from "@/app/layout.config"; 3 | import Link from "next/link"; 4 | 5 | export default function NotFound() { 6 | return ( 7 | 8 |
9 |

Not Found

10 |

Could not find requested resource

11 | Return Home 12 |
13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /site/content/docs/customize-keymap/action/oneshot.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Oneshot 3 | --- 4 | -------------------------------------------------------------------------------- /site/content/docs/customize-keymap/action/tap-dance.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Tap Dance 3 | --- 4 | -------------------------------------------------------------------------------- /site/content/docs/guide-driver.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Driver 3 | --- 4 | -------------------------------------------------------------------------------- /site/content/docs/guide-first-keyboard.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Create keyboard 3 | --- 4 | 5 | 6 | Follow the [Setup section](./) to set up your environment before proceeding. 7 | 8 | 9 | The fastest way to develop keyboard firmware using rktk is to use templates. 10 | To use a template, you need [cargo-generate](https://github.com/cargo-generate/cargo-generate). Install it with the following command. 11 | 12 | ```sh 13 | cargo install cargo-generate 14 | ``` 15 | 16 | Then, you can create a new keyboard firmware project with the following command. 17 | 18 | ```sh 19 | cargo generate https://github.com/nazo6/rktk-template 20 | ``` 21 | 22 | When executed, you will be asked about the crate name, MCU to build, etc. After you answer, a crate will be created for your keyboard in a new directory. 23 | Now let's go to this directory and build the firmware. 24 | 25 | ```sh 26 | cd 27 | cargo run --release --features right 28 | ``` 29 | 30 | Using the `cargo run` command will build the firmware into a UF2 file, and if your microcontroller is connected to a PC in bootloader mode, the firmware will be copied automatically. 31 | 32 | Note that the `--release` option is used here. Without it, unoptimized binaries will be generated. 33 | 34 | The template comes with default settings for corne keyboard. Since this is a split keyboard, left and right must be specified. This can be specified with `--features right`. 35 | This feature is one example of following the Rust way. This behavior can be easily customized. 36 | -------------------------------------------------------------------------------- /site/content/docs/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Setup 3 | --- 4 | 5 | First, let's build an environment for using RKTK. 6 | 7 | ## 1. Install rust 8 | 9 | RKTK is built with Rust, so Rust is required; there are various ways to install 10 | Rust, but using [rustup](https://rustup.rs/) is recommended. 11 | 12 | After install rust, make sure that the `cargo` command is available. 13 | 14 | ## 2. Install `uf2deploy` 15 | 16 | [`uf2deploy`](https://github.com/nazo6/uf2deploy) is a command line tool to 17 | convert ELF binary which is produced by rust compiler to UF2 file, which is 18 | common for flashing. 19 | 20 | Also, this tools can be set as 21 | [cargo runner](https://doc.rust-lang.org/cargo/reference/config.html#targettriplerunner). 22 | By setting this, you can just `cargo run` to compile and deploy to device. 23 | 24 | You can install `uf2deploy` with the following command: 25 | 26 | ```sh 27 | cargo install uf2deploy 28 | ``` 29 | -------------------------------------------------------------------------------- /site/content/docs/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "RKTK", 3 | "root": true, 4 | "pages": [ 5 | "---Guide---", 6 | "index", 7 | "guide-first-keyboard", 8 | "guide-driver", 9 | "---Customization---", 10 | "customize-keymap", 11 | "---Reference---", 12 | "reference-project-overview", 13 | "reference-features", 14 | "reference-device", 15 | "reference-platform", 16 | "reference-tips" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /site/content/docs/reference-device/display.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Display 3 | --- 4 | 5 | Display drivers are used to control various types of displays, such as OLED, LCD, and e-paper displays. They provide an interface for sending data and commands to the display, allowing you to render graphics, text, and other visual elements. 6 | 7 | ## Driver list 8 | 9 | ### Common 10 | 11 | :::drivers_table 12 | 13 | | name | crate | path | description | 14 | | ------- | ------------------- | --------------- | --------------------------- | 15 | | SSD1306 | rktk-drivers-common | display/ssd1306 | SSD1306 OLED display driver | 16 | 17 | ::: 18 | -------------------------------------------------------------------------------- /site/content/docs/reference-device/dongle.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Dongle 3 | --- 4 | 5 | Dongle is usually connected to a computer via USB. It receives data from the wireless keyboard and sends it to the host. It can also be used to send data from the host to the keyboard. 6 | 7 | ## Driver list 8 | 9 | ### nRF 10 | 11 | :::drivers_table 12 | 13 | | name | crate | path | description | 14 | | ---------- | ---------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | 15 | | 2.4GHz ESB | rktk-drivers-nrf | esb/dongle | Received 2.4GHz ESB Reporter data from the keyboard and sends it to the host. It is intended to use with keyboard that has ESB reporter driver. | 16 | 17 | ::: 18 | -------------------------------------------------------------------------------- /site/content/docs/reference-device/encoder.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Encoder 3 | --- 4 | 5 | Encoder or rotary encoder is a device that converts the angular position of a shaft or axle to an analog or digital signal. It is commonly used in keyboards and other input devices to provide a way to adjust settings such as volume, brightness, or scrolling. 6 | 7 | ## Driver list 8 | 9 | ### Common 10 | 11 | :::drivers_table 12 | 13 | | name | crate | path | description | 14 | | ------- | ------------------- | ------- | ----------------------------- | 15 | | General | rktk-drivers-common | encoder | General rotary encoder driver | 16 | 17 | ::: 18 | -------------------------------------------------------------------------------- /site/content/docs/reference-device/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Device 3 | --- 4 | 5 | ## Features 6 | 7 | Each function consists of an interface that abstracts it and a driver that 8 | implements it. 9 | 10 | This document provides an overview of each function and the drivers that are 11 | easily available in the official version. Drivers can be easily implemented by 12 | yourself. 13 | 14 | ### About drivers 15 | 16 | A list of drivers is shown on each feature page, some of which are only 17 | available on certain platforms. For example, SoftDevice, which provides BLE 18 | functionality, is proprietary to nordic and can only be used on nrf platforms. 19 | 20 | On the other hand, some drivers are available for all platforms. These drivers 21 | are compatible with [embassy](https://github.com/embassy-rs/embassy) and the 22 | rest of the Rust ecosystem, and can be found in the “Common” section. 23 | 24 | For a detailed look at how to use the driver, see the [Guide](./guide-driver). 25 | -------------------------------------------------------------------------------- /site/content/docs/reference-device/keyscan.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Keyscan 3 | --- 4 | 5 | The key scanner is responsible for detecting key press information from GPIOs 6 | and other devices. 7 | 8 | Typical keyscanners include matrix, matrix with shift register, and duplex 9 | matrix, all of which are provided as official drivers. 10 | 11 | ## Driver list 12 | 13 | ### Common 14 | 15 | :::drivers_table 16 | 17 | | name | crate | path | description | 18 | | -------------------------- | ------------------- | ----------------------------- | -------------------------- | 19 | | Matrix | rktk-drivers-common | keyscan/matrix | Normal matrix | 20 | | Duplex-Matrix | rktk-drivers-common | keyscan/duplex_matrix | Duplex matrix | 21 | | Matrix with shift register | rktk-drivers-common | keyscan/shift_register_matrix | Matrix with shift register | 22 | 23 | ::: 24 | -------------------------------------------------------------------------------- /site/content/docs/reference-device/mouse.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Mouse 3 | --- 4 | 5 | Mouse drivers are used to control optical mice. They provide an interface for reading the mouse's position and button states, allowing you to implement mouse functionality in your applications. 6 | 7 | ## Driver list 8 | 9 | ### Common 10 | 11 | :::drivers_table 12 | 13 | | name | crate | path | description | 14 | | ------- | ------------------- | ------------- | ----------------------------------- | 15 | | PMW3360 | rktk-drivers-common | mouse/pmw3360 | Pixart PMW3360 optical mouse driver | 16 | | PAW3395 | rktk-drivers-common | mouse/paw3395 | Pixart PAW3395 optical mouse driver | 17 | 18 | ::: 19 | -------------------------------------------------------------------------------- /site/content/docs/reference-device/reporter.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Reporter 3 | --- 4 | 5 | Reporter drivers are used to send data from the device to the host. This can be done over USB, BLE, or other protocols. 6 | 7 | ## Driver list 8 | 9 | ### Common 10 | 11 | :::drivers_table 12 | 13 | | name | crate | path | description | 14 | | ------------- | ------------------- | ---- | ----------------------------------------------------------------------------------------------------------------- | 15 | | USB (embassy) | rktk-drivers-common | usb | USB reporter Driver based on embassy-usb. Can be used with any platform that embassy-usb's driver is implemented. | 16 | 17 | ::: 18 | 19 | ### nRF 20 | 21 | :::drivers_table 22 | 23 | | name | crate | path | description | 24 | | ---------------- | ---------------- | ------------ | ----------------------------------------------------------------------------------- | 25 | | BLE (softdevice) | rktk-drivers-nrf | softdevice | BLE reporter driver based on the nRF softdevice. Can be used with any nRF platform. | 26 | | 2.4GHz ESB | rktk-drivers-nrf | esb/reporter | Reports to other nRF device connected to PC using proprietary ESB protocol. | 27 | 28 | ::: 29 | -------------------------------------------------------------------------------- /site/content/docs/reference-device/split.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Split 3 | --- 4 | 5 | Split drivers are used to control devices that require a split connection, such as TRRS or TRS connections. 6 | 7 | ## Driver list 8 | 9 | ### nRF 10 | 11 | :::drivers_table 12 | 13 | | name | crate | path | description | 14 | | ---------------- | ---------------- | ---------------------- | ------------------------------- | 15 | | UART full-duplex | rktk-drivers-nrf | split/uart_full_duplex | Uses two pins and TRRS cable | 16 | | UART half-duplex | rktk-drivers-nrf | split/uart_half_duplex | Uses only one pin and TRS cable | 17 | 18 | ::: 19 | 20 | ### RP 21 | 22 | :::drivers_table 23 | 24 | | name | crate | path | description | 25 | | --------------- | --------------- | --------------------- | ------------------------------- | 26 | | PIO half-duplex | rktk-drivers-rp | split/pio_half_duplex | Uses only one pin and TRS cable | 27 | 28 | ::: 29 | -------------------------------------------------------------------------------- /site/content/docs/reference-device/storage.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Storage 3 | --- 4 | 5 | Storage drivers are used to manage data storage on devices. They provide an interface for reading and writing data to various types of storage media, such as flash memory, SD cards, and other forms of non-volatile memory. 6 | 7 | ## Driver list 8 | 9 | ### Common 10 | 11 | :::drivers_table 12 | 13 | | name | crate | path | description | 14 | | -------------------------- | ------------------- | ---------------------------- | --------------------------------------------------------------------------------------------------------- | 15 | | Flash (sequential-storage) | rktk-drivers-common | storage/flash_sequential_map | General storage driver that can be used with any platform that has embedded_storage_async implementation. | 16 | 17 | ::: 18 | -------------------------------------------------------------------------------- /site/content/docs/reference-platform/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Platform 3 | --- 4 | 5 | Some features/drivers are platform-specific. 6 | -------------------------------------------------------------------------------- /site/content/docs/reference-platform/rp.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: RP series 3 | --- 4 | 5 | RP chip such as RP2040 and RP2350 is used by microcontroller such as ProMicro 6 | RP2040. 7 | 8 | ## RP2040 and CAS 9 | 10 | When compiling rktk for RP2040, you may see errors related to Atomic. This is because RP2040 has old architecture (thumbv6) and this architecture does not support feature called atomic CAS. 11 | 12 | Fortunately, polyfill for such old architecture is provided by [portable-atomic](https://github.com/taiki-e/portable-atomic). But to use this, you have to manually add `portable-atomic` as dependency with `unsafe-assume-single-core` or `critical-section` feature. 13 | 14 | Usually, `unsafe-assume-single-core` should be OK as rktk does not use multiple core. Although, if you have other dependencies that utilize multiple core, you have to use `critical-section` feature instead. 15 | -------------------------------------------------------------------------------- /site/content/docs/reference-project-overview.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Project overview 3 | --- 4 | -------------------------------------------------------------------------------- /site/content/docs/reference-tips/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Tips 3 | --- 4 | -------------------------------------------------------------------------------- /site/content/docs/reference-tips/optimize-binary-size.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Optimize binary size 3 | --- 4 | 5 | ## Adjust build config (`.cargo/config.toml`) 6 | 7 | Here is an example of a binary size-optimized `.cargo/config.toml`. If you use [rktk-template](https://github.com/nazo6/rktk-template), you don't need modify it, as it is already included in the template. 8 | 9 | ```toml title=".cargo/config.toml" 10 | [profile.release] 11 | debug = true 12 | opt-level = "z" 13 | lto = "fat" 14 | panic = "abort" 15 | rustflags = ["-Zlocation-detail=none"] 16 | codegen-units = 1 17 | 18 | [unstable] 19 | build-std = ["core", "alloc"] 20 | build-std-features = ["optimize_for_size"] 21 | ``` 22 | 23 | - `debug = true` 24 | 25 | This enables debug info. This make _ELF_ binary big, but the final binary flashed to mcu (UF2 file) is not affected. So it is recommended to enable this. 26 | 27 | - `opt-level = "z"` 28 | 29 | This enables size optimization of compiler. This is the most important option to reduce binary size. For other options, see [cargo book](https://doc.rust-lang.org/cargo/reference/profiles.html#opt-level). 30 | The documents suggets that `"s"` sometimes produces smaller binaries than `"z"`, but in my experience, `"z"` is always smaller than `"s"`. 31 | 32 | - `lto = "fat"` 33 | 34 | This enables link time optimization. This is also important to reduce binary size. 35 | 36 | - `panic = "abort"` and `rustflags = ["-Zlocation-detail=none"]` 37 | 38 | These two lines allow the program to simply stop at panic. This reduces the binary size. 39 | However, you will no longer receive messages in case of panic. 40 | 41 | Note that second line is only available in nightly rust. 42 | 43 | - `build-std` and `build-std-features` 44 | 45 | By default, program will be compiled with precompiled `core` and `alloc` crates. By enabling this, these crates are compiled from source code with size optimization. 46 | 47 | These options are only available in nightly rust. Note that [these options cannot be used with `per-package-target` unstable feature](https://github.com/rust-lang/cargo/issues/11898) 48 | -------------------------------------------------------------------------------- /site/content/docs/rktk-client.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: RKTK Client 3 | --- 4 | 5 | [RKTK Client](https://rktk-client.nazo6.dev/) is the VIA equivalent of QMK, 6 | allowing keyboard remapping and configuration changes in the browser. 7 | 8 | > [!NOTE] 9 | > 10 | > RKTK Client can only be used with browsers that support WebHID (e.g. Chrome). 11 | -------------------------------------------------------------------------------- /site/lib/source.ts: -------------------------------------------------------------------------------- 1 | import { docs } from "@/.source"; 2 | import { loader } from "fumadocs-core/source"; 3 | 4 | // `loader()` also assign a URL to your pages 5 | // See https://fumadocs.vercel.app/docs/headless/source-api for more info 6 | export const source = loader({ 7 | baseUrl: "/docs", 8 | source: docs.toFumadocsSource(), 9 | }); 10 | -------------------------------------------------------------------------------- /site/next.config.mjs: -------------------------------------------------------------------------------- 1 | import { createMDX } from "fumadocs-mdx/next"; 2 | 3 | const withMDX = createMDX(); 4 | 5 | /** @type {import('next').NextConfig} */ 6 | const config = { 7 | reactStrictMode: true, 8 | output: "export", 9 | }; 10 | 11 | export default withMDX(config); 12 | -------------------------------------------------------------------------------- /site/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rktk-site", 3 | "private": true, 4 | "scripts": { 5 | "build": "next build", 6 | "dev": "next dev", 7 | "start": "next start", 8 | "postinstall": "fumadocs-mdx" 9 | }, 10 | "dependencies": { 11 | "clsx": "^2.1.1", 12 | "fumadocs-core": "^15.8.1", 13 | "fumadocs-mdx": "^12.0.1", 14 | "fumadocs-ui": "^15.8.1", 15 | "next": "15.5.4", 16 | "react": "^19.1.1", 17 | "react-dom": "^19.1.1", 18 | "react-icons": "^5.5.0", 19 | "remark-directive": "^4.0.0", 20 | "unist-util-visit": "^5.0.0" 21 | }, 22 | "devDependencies": { 23 | "@tailwindcss/postcss": "^4.1.13", 24 | "@types/mdast": "^4.0.4", 25 | "@types/mdx": "^2.0.13", 26 | "@types/node": "^24.5.2", 27 | "@types/react": "^19.1.15", 28 | "@types/react-dom": "^19.1.9", 29 | "postcss": "^8.5.6", 30 | "tailwindcss": "^4.1.13", 31 | "typescript": "^5.9.2" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /site/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | "@tailwindcss/postcss": {}, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /site/public/logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nazo6/rktk/530c16515b51fb7975354ca708b4fc1420d8e10d/site/public/logo.webp -------------------------------------------------------------------------------- /site/source.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, defineDocs } from "fumadocs-mdx/config"; 2 | import remarkDirective from "remark-directive"; 3 | import { remarkDirectiveDriverTable } from "./lib/remarkDirectiveDriverTable"; 4 | // import mdxMermaid from "mdx-mermaid"; 5 | 6 | export const docs = defineDocs({ 7 | dir: "content/docs", 8 | }); 9 | 10 | export default defineConfig({ 11 | mdxOptions: { 12 | remarkPlugins: (v) => [ 13 | // [mdxMermaid, { output: "svg" }], 14 | remarkDirective, 15 | remarkDirectiveDriverTable, 16 | ...v, 17 | ], 18 | }, 19 | lastModifiedTime: "git", 20 | }); 21 | -------------------------------------------------------------------------------- /site/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "target": "ESNext", 5 | "lib": ["dom", "dom.iterable", "esnext"], 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "strict": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "noEmit": true, 11 | "esModuleInterop": true, 12 | "module": "esnext", 13 | "moduleResolution": "bundler", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "jsx": "preserve", 17 | "incremental": true, 18 | "paths": { 19 | "@/.source": ["./.source/index.ts"], 20 | "@/*": ["./*"] 21 | }, 22 | "plugins": [ 23 | { 24 | "name": "next" 25 | } 26 | ] 27 | }, 28 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 29 | "exclude": ["node_modules"] 30 | } 31 | --------------------------------------------------------------------------------