Skip to content

Commit

Permalink
docs: added a page on dongles.
Browse files Browse the repository at this point in the history
Co-authored-by: rasmuskoit <[email protected]>
Co-authored-by: Cem Aksoylar <[email protected]>
  • Loading branch information
3 people committed Jan 2, 2025
1 parent d0016b3 commit 5d59cdc
Show file tree
Hide file tree
Showing 3 changed files with 313 additions and 1 deletion.
311 changes: 311 additions & 0 deletions docs/docs/development/hardware-integration/dongle.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,311 @@
---
title: Keyboard Dongle
sidebar_label: Keyboard Dongle
---

import Tabs from "@theme/Tabs";
import TabItem from "@theme/TabItem";

A bluetooth dongle can be added to any wireless keyboard running ZMK. The result is a [split keyboard](../../features/split-keyboards.md) with the dongle as ["central"](../../features/split-keyboards.md#central-and-peripheral-roles). There are a number of advantages to adding a dongle, but also some disadvantages:

Benefits:

- For split keyboards, having the dongle for the "central" role results in [improved battery life](/power-profiler) for the former central part, as it is now a peripheral.
- It is easier to connect to the host device since dongles are typically connected via USB.

Disadvantages:

- An extra [board](../../faq.md#what-is-a-board) is needed (any BLE-capable board that ZMK supports will work).
- The keyboard becomes unusable without the dongle.

Depending on how the dongle is used, there are some additional [latency considerations](../../features/split-keyboards.md#latency-considerations) to keep in mind.
The addition of the dongle adds an extra "hop" for the former central, increasing its latency to that of a peripheral. The other parts are unchanged latency-wise. There is also a commonly occurring case where the peripherals benefit.
Assuming the dongle is connected to USB and the former central would have been connected via bluetooth to the host if the dongle wasn't present:

- The former central will have its latency increased by about 1ms from the extra USB "hop"
- The other parts will have their average latency _decreased_ by 6.5ms from the replacement of a BLE "hop" with a "USB" hop.

As a result, for this common use case the average latency of the keyboard decreases.

## Adding a Dongle

The approach taken to adding a dongle is _generally_ the same[^1].
Whether your keyboard consists of a [board and a shield](index.mdx#boards--shields) or is just a board is irrelevant.

To add a dongle, you will create a simplified form of a [new shield](new-shield.mdx).
The approach described below assumes the dongle will not have any keys assigned to itself, so it will not work for that scenario.
If you want to understand the details of how it all works better, please read through the [new shield guide](new-shield.mdx).

As there are a very large number of possible devices that could be used as a dongle, you will be defining your dongle as a personal shield intended for your exclusive use.

Prior to adding a dongle to your keyboard, please test its functionality without a dongle. The below guide will assume that your keyboard is named `my_keyboard`, replace accordingly.

### Dongle Folder

First, make sure that your `zmk-config` matches the folder structure found in the [unified ZMK config template](https://github.com/zmkfirmware/unified-zmk-config-template) (extra files and folders are fine, but none should be missing).

Next, navigate to the `zmk-config/boards/shields` directory. Create a subdirectory called `my_keyboard`, if one doesn't already exist. Unless otherwise specified, the below files should all be placed in said folder.

### Kconfig Files

#### Kconfig.shield

Make a file called `Kconfig.shield`, if one does not exist already. Add the following lines to it, replacing `SHIELD_MY_KEYBOARD_DONGLE` and `my_keyboard_dongle` according to your keyboard:

```kconfig title="Kconfig.shield"
# No whitespace after the comma or in the keyboard name!
config SHIELD_MY_KEYBOARD_DONGLE
def_bool $(shields_list_contains,my_keyboard_dongle)
```

#### Kconfig.defconfig

Make a file called `Kconfig.defconfig`, if one does not exist already. Add the following lines to it where `SHIELD_MY_KEYBOARD_DONGLE` should match the previous section:

```kconfig title="Kconfig.defconfig"
if SHIELD_MY_KEYBOARD_DONGLE
# Max 16 characters in keyboard name
config ZMK_KEYBOARD_NAME
default "My Board"
config ZMK_SPLIT_ROLE_CENTRAL
default y
config ZMK_SPLIT
default y
# Set this to the number of peripherals your dongle will have.
# For a unibody, this would be 1. If you have left and right halves, set it to 2, etc.
config ZMK_SPLIT_BLE_CENTRAL_PERIPHERALS
default 1
# Set this to ZMK_SPLIT_BLE_CENTRAL_PERIPHERALS + your desired number of BT profiles (default is 5)
config BT_MAX_CONN
default 6
# Set this to the same number as BT_MAX_CONN
config BT_MAX_PAIRED
default 6
endif
```

### Dongle Overlay File

Create a file called `my_keyboard_dongle.overlay` (renamed as appropriate).
This file will include your keyboard's matrix transform and physical layout, allowing it to map key press events from the peripherals to behaviors.

Add the following lines to the file you created, introducing a mock kscan for the dongle since it has no keys itself:

```dts title="my_keyboard_dongle.overlay"
#include <dt-bindings/zmk/matrix_transform.h>
/ {
chosen {
zmk,kscan = &mock_kscan;
};
mock_kscan: mock_kscan_0 {
compatible = "zmk,kscan-mock";
columns = <0>;
rows = <0>;
events = <0>;
};
};
```

You will now need to find and copy your keyboard's matrix transform into the `my_keyboard_dongle.overlay` file.

#### Matrix transform

Navigate to the directory defining your keyboard (in-tree keyboards found [here](https://github.com/zmkfirmware/zmk/tree/main/app/boards), if your keyboard is a shield look under the `shields` subdirectory) and look through the [devicetree files](../../config/index.md) for nodes with `compatible = "zmk,matrix-transform";`.
This should look something like this:

```dts
default_transform: keymap_transform_0 {
compatible = "zmk,matrix-transform";
columns = <14>;
rows = <5>;
map = <
// Lots of RC(r,c) macros
>;
};
```

Copy this node into your `my_keyboard_dongle.overlay` file. It should be a child node of the root node (put it inside the `/ { ... };`).
If there are multiple matrix transformations, copy the one that matches your keyboard's layout.

Make a note of the label that the transform has, it will be used later. In the example the label is `default_transform`.

#### Physical layout

A full physical layout is necessary to allow your dongle to be used with [ZMK Studio](../../features/studio.md).
If your keyboard is not Studio-ready or you have no interest in using ZMK Studio with your dongle, then this section is simplified significantly.

<Tabs
groupId="studio-ready"
defaultValue="studio"
values={[
{ label: "Dongle with Studio", value: "studio" },
{ label: "Dongle without Studio", value: "nostudio" }
]}
>
<TabItem value="studio">

You will need to find your keyboard's physical layout, much like finding the matrix transform.
Depending on your keyboard, the physical layout could be found in a variety of locations. Look for a node with `compatible = "zmk,physical-layout";`.
There are three commonly found possibilities:

- The physical layout is found in a file called `my_keyboard-layouts.dtsi`
- The physical layout is found in the same file as the matrix transform and chosen node
- The physical layout is imported from ZMK's shared layouts - `#include <layouts/<layout_name>.dtsi>` can be found at the top of one of the devicetree files.

<Tabs
groupId="layout-locations"
defaultValue="layouts"
values={[
{ label: "Layouts File", value: "layouts" },
{ label: "Base File", value: "base" },
{ label: "Imported", value: "imported" }
]}
>
<TabItem value="layouts">
Copy the file into your dongle's folder. Then import the file, assign the matrix transform to it, and select it in the `chosen` node:

```dts
#import "my_keyboard-layouts.dtsi"
&physical_layout0 {
transform = <&default_transform>;
};
/ {
chosen {
zmk,kscan = &mock_kscan;
zmk,physical-layout = &physical_layout0;
};
};
```

Make sure that the labels `physical_layout0` and `default_transform` match those of the physical layout node defined in the file and the matrix transform respectively.

:::note
If there are multiple physical layouts in the file, you will need to copy over all of the remaining matrix transformations and assign them to their corresponding physical layout.
:::

</TabItem>
<TabItem value="base">
Copy the physical layout node into your `my_keyboard_dongle.overlay` file. Make sure the matrix transform is assigned to it, and select it in the `chosen` node.

```dts
/ {
chosen {
zmk,kscan = &mock_kscan;
zmk,physical-layout = &physical_layout0;
};
physical_layout0: physical_layout_0 {
compatible = "zmk,physical-layout";
display-name = "Default Layout";
transform = <&default_transform>;
keys = <...>; // Long list of key positions
};
};
```

Make sure that the labels `physical_layout0` and `default_transform` match those of the physical layout node and the matrix transform respectively.

:::note
If there are multiple physical layouts, you need only copy the one that applies to your keyboard.
:::

</TabItem>
<TabItem value="imported">
Include your desired layout at the top of the file. Then assign your matrix transform to it, taking care to match up the node labels correctly.

```dts
#include <layouts/<layout_name>.dtsi>
&physical_layout0 {
transform = <&default_transform>;
};
```

In this case your node label will _not_ be `physical_layout0`, make sure to adjust it accordingly.

Finally, select your physical layout in the chosen node.

```dts
/ {
chosen {
zmk,kscan = &kscan0;
zmk,physical-layout = &physical_layout0;
};
};
```

</TabItem>
</Tabs>

</TabItem>
<TabItem value="nostudio">
Add a barebones [physical layout](physical-layouts.md) without the `keys` property to your `my_keyboard_dongle.overlay` file.

```dts
/ {
physical_layout0: physical_layout_0 {
compatible = "zmk,physical-layout";
display-name = "Default Layout";
transform = <&default_transform>;
};
};
```

Make sure that the `transform` property uses the same label as your matrix transform. Then select the physical layout in the same `chosen` node that was previously created:

```dts
/ {
chosen {
zmk,kscan = &mock_kscan;
zmk,physical-layout = &physical_layout0; // New item added
};
};
```

</TabItem>
</Tabs>

## Building the Firmware

Add the appropriate lines to your `build.yml` file to build the firmware for your dongle. Also add some CMake arguments using `cmake-args` to the existing parts of your keyboard, turning them into peripherals for your dongle.[^2]

```yaml
include:
# -----------------------------------------
# Your other keyboard parts here
# -----------------------------------------
# Change the board appropriately, you can use any board
- board: nice_nano_v2
shield: my_keyboard_dongle
- board: nice_nano_v2
shield: settings_reset
# Add these cmake-args to the peripherals you wish to use with the dongle
- board: nice_nano_v2
shield: my_keyboard
cmake-args: -DCONFIG_ZMK_SPLIT=y -DCONFIG_ZMK_SPLIT_ROLE_CENTRAL=n
```
You can then flash the firmware to your device as detailed in our [user setup](../../user-setup.mdx#installing-the-firmware) and [split keyboard](../../features/split-keyboards.md#building-and-flashing-firmware) pages.
:::warning
Before flashing your new firmware, you need to flash `settings_reset` [firmware](../../troubleshooting/connection-issues.mdx#acquiring-a-reset-uf2) on all devices to ensure they can pair to each other.
:::

To use your dongled keyboard with [ZMK Studio](../../features/studio.md), apply the instructions for [building with Studio](../../features/studio.md#building) to the dongle.

If you ever want to "undongle" your keyboard, simply remove these CMake arguments and flash the resulting firmware (after a `settings_reset`).

[^1]: If you have a custom dongle that uses an onboard MCU, then you will need to take a slightly different approach that isn't currently documented.
[^2]: If you are building locally, you can append these flags to the end of the [build command](../local-toolchain/build-flash.mdx#cmake-arguments).
2 changes: 1 addition & 1 deletion docs/docs/features/split-keyboards.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Supporting split communication over wired protocols is planned, allowing for ZMK
## Central and Peripheral Roles

In split keyboards running ZMK, one part is assigned the "central" role which receives key position and sensor events from the other parts that are called "peripherals."
The central runs the necessary keymap logic to convert received events into HID events such as keycodes and then communicates with the connected host devices, e.g. over USB or bluetooth.
The central runs the necessary keymap logic to convert received events into HID events such as keycodes and then communicates with the connected host devices, e.g. over USB or bluetooth. If the keyboard makes use of a [dongle](../development/hardware-integration/dongle.mdx), then the dongle takes on the role of central.

The internal keyboard state (like active layers) is handled exclusively by the central.
Peripherals _cannot_ communicate with host devices on their own, since they can only communicate with the central.
Expand Down
1 change: 1 addition & 0 deletions docs/sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ module.exports = {
"development/hardware-integration/physical-layouts",
"development/hardware-integration/hardware-metadata-files",
"development/hardware-integration/pinctrl",
"development/hardware-integration/dongle",
"development/hardware-integration/shift-registers",
"development/hardware-integration/encoders",
"development/hardware-integration/soft-off-setup",
Expand Down

0 comments on commit 5d59cdc

Please sign in to comment.