Compare commits

..

78 Commits

Author SHA1 Message Date
dependabot[bot]
f40e7a4441 chore(deps): bump actions/github-script from 7 to 8
Bumps [actions/github-script](https://github.com/actions/github-script) from 7 to 8.
- [Release notes](https://github.com/actions/github-script/releases)
- [Commits](https://github.com/actions/github-script/compare/v7...v8)

---
updated-dependencies:
- dependency-name: actions/github-script
  dependency-version: '8'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-05 05:01:14 +00:00
Chris Helming
9a8fdd66ea docs: Add missing semicolon to kscan.md composite example (#3052) 2025-09-02 20:42:30 -07:00
Pete Johanson
0b5a103c18 fix(core): Generate correct keymap layer names for all builds (#3047)
Properly account for studio and non-studio builds for the generation of
the layer names stored within the keymap system.

Fixes #3045
2025-08-26 03:26:20 -06:00
Cem Aksoylar
f09e551929 docs: Add warning box for clearing settings for splits, generalize troubleshooting procedure (#3039)
Co-authored-by: Nicolas Munnich <98408764+nmunnich@users.noreply.github.com>
2025-08-19 17:27:21 -07:00
Nicolas Munnich
ee69b9e3c7 docs: Add a dedicated page on ZMK events (#2815)
* docs: Added a dedicated page on ZMK events

* docs: Apply suggestions from code review

Co-authored-by: Cem Aksoylar <caksoylar@users.noreply.github.com>

* docs: Apply suggestions from code review

Bring the code snipper in new-behavior back, touchups on the page

* docs: clarify "calling" hold tap

Adjustment after feedback from code review

---------

Co-authored-by: Cem Aksoylar <caksoylar@users.noreply.github.com>
2025-08-18 23:45:16 +02:00
Cem Aksoylar
5144de677d docs: Document usage of persistent settings explicitly (#3036) 2025-08-18 12:04:35 -07:00
Kurtis Lew
5225952f96 chore(docs): Typo in CMake Arguments example code blocks (#3038) 2025-08-18 09:15:58 +01:00
Cem Aksoylar
9fcce45cb5 docs: Fix remaining typos for requirements extras (#3035) 2025-08-16 22:29:11 -07:00
Nicolas Munnich
62007e500b docs: Add devicetree primer (#2715)
* docs: Add devicetree primer

* docs: Apply suggestions from code review

Co-authored-by: Cem Aksoylar <caksoylar@users.noreply.github.com>

* docs: Move devicetree property types into primer and link to primer

* docs: Changes from code review

Co-authored-by: Cem Aksoylar <caksoylar@users.noreply.github.com>

---------

Co-authored-by: Cem Aksoylar <caksoylar@users.noreply.github.com>
2025-08-16 10:05:27 +02:00
Nicolas Munnich
ccf20a1f5c feat(boards/shields): Add settings for Rpi Pico and add tester_rpi_pico shield (#2900)
* feat(boards): Properly support settings for RPi Pico board.

* feat(shields): Add a tester_rpi_pico shield

---------

Co-authored-by: Peter Johanson <peter@peterjohanson.com>
2025-08-10 22:00:35 +01:00
Marius Petcu
c7fae18ae1 feat(boards): add support for nrf52840dongle_nrf52840 (#3011)
This adds additional required configs for the Nordic NRF52840 Dongle (PCA10059),
which was already supported by Zephyr, but didn't have flash settings
and Bluetooth enabled.
2025-08-08 23:47:03 +01:00
Nicolas Munnich
61da930ed5 chore(ci): Set days before issue stale to 1000 (#3022) 2025-08-08 13:13:57 -04:00
Maxim
3609ac6fc8 docs: fix typo for pip requirements (#3029) 2025-08-07 23:33:38 +02:00
Peter Cock
45700887ba Suggest using cu for USB log viewing on macOS (#3027)
* Suggest using cu for USB log viewing on macOS

It may not be as friendly, but cu should be available while tio could be hard to install.

* Polish wording

Co-authored-by: Nicolas Munnich <98408764+nmunnich@users.noreply.github.com>

* Suggest "man cu" for how to use this tool

---------

Co-authored-by: Nicolas Munnich <98408764+nmunnich@users.noreply.github.com>
2025-08-07 23:26:17 +02:00
Artem
a8a392807e chore(docs): correct a misleading _defconfig description (#3015)
* docs: correct a misleading _defconfig description

* chore(docs): improve _defconfig description

Co-authored-by: Joel Spadin <joelspadin@gmail.com>

---------

Co-authored-by: Joel Spadin <joelspadin@gmail.com>
2025-08-05 00:20:39 +01:00
Nicolas Munnich
f3233c1b60 fix(ci): Fix release please template bump (#3021) 2025-08-04 18:54:03 -04:00
Nicolas Munnich
919bce7962 fix(docs): Tweak layer tap example to be clearer (#3023) 2025-08-04 23:44:16 +01:00
dependabot[bot]
cc19ff7c5b chore(deps): bump http-proxy-middleware from 2.0.7 to 2.0.9 in /docs (#2929)
Bumps [http-proxy-middleware](https://github.com/chimurai/http-proxy-middleware) from 2.0.7 to 2.0.9.
- [Release notes](https://github.com/chimurai/http-proxy-middleware/releases)
- [Changelog](https://github.com/chimurai/http-proxy-middleware/blob/v2.0.9/CHANGELOG.md)
- [Commits](https://github.com/chimurai/http-proxy-middleware/compare/v2.0.7...v2.0.9)

---
updated-dependencies:
- dependency-name: http-proxy-middleware
  dependency-version: 2.0.9
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-04 22:54:01 +01:00
dependabot[bot]
90363719a2 chore(deps): bump on-headers and compression in /docs (#2999)
---
updated-dependencies:
- dependency-name: on-headers
  dependency-version: 1.1.0
  dependency-type: indirect
- dependency-name: compression
  dependency-version: 1.8.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-04 22:32:36 +01:00
dependabot[bot]
e57bf5af37 chore(deps): bump the docusaurus-minor-patch group across 1 directory with 3 updates (#2973)
Bumps the docusaurus-minor-patch group with 3 updates in the /docs directory: [@docusaurus/core](https://github.com/facebook/docusaurus/tree/HEAD/packages/docusaurus), [@docusaurus/preset-classic](https://github.com/facebook/docusaurus/tree/HEAD/packages/docusaurus-preset-classic) and [@docusaurus/theme-mermaid](https://github.com/facebook/docusaurus/tree/HEAD/packages/docusaurus-theme-mermaid).


Updates `@docusaurus/core` from 3.8.0 to 3.8.1
- [Release notes](https://github.com/facebook/docusaurus/releases)
- [Changelog](https://github.com/facebook/docusaurus/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/docusaurus/commits/v3.8.1/packages/docusaurus)

Updates `@docusaurus/preset-classic` from 3.8.0 to 3.8.1
- [Release notes](https://github.com/facebook/docusaurus/releases)
- [Changelog](https://github.com/facebook/docusaurus/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/docusaurus/commits/v3.8.1/packages/docusaurus-preset-classic)

Updates `@docusaurus/theme-mermaid` from 3.8.0 to 3.8.1
- [Release notes](https://github.com/facebook/docusaurus/releases)
- [Changelog](https://github.com/facebook/docusaurus/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/docusaurus/commits/v3.8.1/packages/docusaurus-theme-mermaid)

---
updated-dependencies:
- dependency-name: "@docusaurus/core"
  dependency-version: 3.8.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: docusaurus-minor-patch
- dependency-name: "@docusaurus/preset-classic"
  dependency-version: 3.8.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: docusaurus-minor-patch
- dependency-name: "@docusaurus/theme-mermaid"
  dependency-version: 3.8.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: docusaurus-minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-04 22:29:08 +01:00
Léon Hagenaars-Keus
9aaf87c6d2 docs: Updated putty link (#3020)
Solves issue #3019 (incorrect putty link)

- putty.org is not affiliated with the putty software
- the site now hosts links to interviews about a recent pandemic
- this pull request updates the link to the actual project page

Co-authored-by: Léon Hagenaars-Keus <lhagenaars@sogyo.nl>
2025-08-04 13:16:38 +01:00
Pete Johanson
edf5c0814f chore(main): release 0.3.0 (#2858) 2025-08-01 18:44:20 -04:00
Nicolas Munnich
6c266b7123 chore(ci): Automatically bump version in user config template when releasing a new version (#3016) 2025-08-01 18:27:45 -04:00
Robert U
c25e927a2f chore(ci): Optimize build workflow (#2757)
* Use treeless clones in workflows
* Replace yaml2json with yq

Github's ubuntu runners come with `yq` installed, which can replace
piping `yaml2json` to `jq`. It also saves installing `yaml2json`.

It's worth noting that the `yq` version installed is the `go` version
(which has a slightly different syntax than the competing `python`
version).
2025-07-31 18:36:04 -04:00
Nicolas Munnich
c86f0d6ff4 chore: Add some basic encoder tests (#2845)
Adds some encoder tests for rotating, and basic layers.
Mock Kscans don't seem to interact nicely, slightly on the hacky side.
However, better to have hacky tests than no tests.
2025-07-31 18:29:59 -04:00
Genteure
af967667b0 fix(core): Correctly sync BAS battery level (#2977)
Fix BAS battery level showing 100% if controller boots up with 0% battery charge.

Closes zmkfirmware/zmk#2972
2025-07-31 17:51:21 -04:00
Pete Johanson
2ae5185419 fix(split): Compile and run properly in wired polling mode. (#3012)
Fixes for regressions from split refactors that broke polling mode
specifically.

Co-authored-by: honorless <86894501+lesshonor@users.noreply.github.com>
Co-authored-by: Cem Aksoylar <caksoylar@users.noreply.github.com>
2025-07-31 17:09:25 -04:00
Pete Johanson
1530ae36c2 fix(split): Enable wired split by default if DTS is set (#3010)
Remove the previous condition on ZMK_SPLIT_WIRED that only enabled it if
!ZMK_BLE. We'll defer to the `depends on` to ensure the DTS exists to
use wired split to make that decision on enabling the feature.
2025-07-30 13:43:46 -04:00
Cem Aksoylar
1bac680c4f fix(behaviors): Correct macro release state for parametrized macros (#2942)
test(behaviors): Add parametrized macro test that fails

fix(behaviors): Correct macro release state for parametrized
2025-07-29 18:39:26 -04:00
snoyer
d09087f4dc feat(display): nice!view individual profile status (#2265)
Allow the nice!view widget to display the status of each profile by:

- not adding a circle around the number if the profile is not bound
- drawing a dashed circle around the number if a profile is bound but not connected
2025-07-26 18:46:51 +02:00
Nicolas Munnich
6d7bbc8670 feat(ci): Add stale GitHub Action to automatically close stale PRs (#2924)
Co-authored-by: Cem Aksoylar <caksoylar@users.noreply.github.com>
2025-07-21 19:11:56 +02:00
Joel Spadin
8059e671b2 fix(boards): Disable high voltage DC-DC by default (#2995)
Given a typical setup of a battery at ~4 V and a nice!nano v2 running at
3.3 V, I measured a small increase in power usage (using a Nordic PPK2)
when SOC_DCDC_NRF52X_HV was enabled. This changes the nice_nano_v2 and
mikoto boards so this option is disabled by default.

Fixes #2990
2025-07-21 17:14:29 +02:00
Cem Aksoylar
61325ee82c docs: Document requirements-extra.txt and endpoint selection for Studio (#3001)
* docs: Document requirements-extra.txt for Studio builds

* docs: Note endpoint selection requirement for Studio

* docs: Fix indents in list items in native setup page

Add this file to .prettierignore because it messes all the additional indents.
There is also no way to make prettier ignore a range of lines in MDX files AFAICT.

* Revert "docs: Fix indents in list items in native setup page"

This reverts commit 6d8aeba801.
2025-07-21 17:11:06 +02:00
Tobias Adolph
342d838913 fix(ble,hid): Fix smooth scrolling over BLE (#2998)
- Use correct MIN16 value in hid report. -32768 in 2's compliment is 8000(hex)
- Initialize resolution multiplier array
- Properly implement `read_hids_mouse_feature_report`

Fixes: #2957

Co-authored-by: Tobias Adolph <43353209+adolto@users.noreply.github.comgit>
2025-07-20 09:13:18 -06:00
Dimitri Krassovski
7292df02d4 feat(shields): Add a physical layout for a_dux (#3000) 2025-07-19 21:44:05 -07:00
snoyer
fe91cc6625 refactor(ble): add functions to check if profile is open/connected by address (#2993)
Helper functions for BLE profile statuses.
2025-07-19 07:51:53 -06:00
Joel Spadin
cef7af4408 fix: Fix build with Studio and USB but not UART (#2996)
Changed CONFIG_ZMK_STUDIO_TRANSPORT_UART to automatically enable itself
whenever a zmk,studio-rpc-uart chosen node is specified. The previous
behavior of enabling if CONFIG_ZMK_USB was enabled broke builds when
CONFIG_ZMK_STUDIO was enabled but the chosen node wasn't specified.
2025-07-11 22:07:38 -06:00
snoyer
9e905d6593 feat(ble): Add function to get profile address by index (#2992) 2025-07-09 20:25:11 -06:00
Nicolas Munnich
e93cd31a58 blog: Add blog post on pinning ZMK version (#2974)
* blog: Add blog post on pinning ZMK version

* docs: apply suggestions from code review

Co-authored-by: Cem Aksoylar <caksoylar@users.noreply.github.com>

* docs: bump date for correctly pinned template

---------

Co-authored-by: Cem Aksoylar <caksoylar@users.noreply.github.com>
2025-07-03 17:27:11 +02:00
Pete Johanson
6b44d33db2 feat(split): Runtime selection of split transport (#2886)
feat(split): Runtime selection of split transport

Allow building multiple split transports, and select an active
one based on the transport availability. Wired split availability
depends on additional `detect-gpios` which must be a GPIO pin
that goes active when a wired connection is present.

feat(split): Suspend/resume wired UART devices.

To better support runtime split support, suspend/resume the UART
as necessary to save power when not using the UART.

docs(split): Document adjusting nRF52 UART interrupt priorities

For wired split on nRF52, you may need to adjust the priority for UART
interrupts lower, to ensure the interrupts used for timing sensitive
BT operations can run when needed, so document this in our pinctrl docs.

refactor(split): Restore use of aync UART on nRF52.

With fixes for Zephyr UART driver, re-enable using async API on nRF52.

fix(split): Minor wired split fixes.

Various minor fixes for wired split to avoid spurious TX in half duplex,
etc.

fix: Unconditionally define HID payloads to avoid error.

Don't conditionally define HID indicator payload, to avoid compilation
errors.

docs(split): Expand on details of split transports.

Expand the split keyboard documentation with a more fleshed out section
on the available split trasnports, and what is and isn't supported by
each, including the runtime selection functionality.

---------

Co-authored-by: Nicolas Munnich <98408764+nmunnich@users.noreply.github.com>
2025-06-16 03:45:25 -04:00
badjeff
462d48b78e feat(pointing): Allow peripheral input processing to stop propagation (#2844)
Allow peripheral input processors to prevent event propagation to centrals.

---------

Co-authored-by: Pete Johanson <peter@peterjohanson.com>
2025-06-16 02:23:25 -04:00
ouj4k2q5
eb99b4ede0 fix(docs): Fix soft off waker configuration example (#2960) 2025-06-08 17:24:21 +02:00
George Norton
239baa4875 feat(metadata): Add metadata to the mouse_key_press behavior (#2950)
Adds metadata to the mouse key press behavior so that mouse buttons can be
configured through studio
2025-06-02 14:32:18 -04:00
Cem Aksoylar
9da5d3ba82 fix(display): Make stock battery widget depend on the right symbol (#2953) 2025-06-02 07:25:08 -06:00
dependabot[bot]
f568b25e56 chore(deps): bump the docusaurus-minor-patch group (#2948) 2025-06-01 23:21:14 -07:00
Pete Johanson
cb00077273 chore(docs): Fix prettier warnings (#2952)
* chore(docs): Fix prettier warnings
2025-06-01 12:02:39 -07:00
cormoran
461f5c832f fix(pointing): Avoids mutex leak for default layer toggle event (#2934)
Remove unnecessary special check for toggle of layer 0,
to avoid mutex leak.
2025-05-09 20:01:17 -04:00
Steven Sheldon
879cff7595 docs: Document the default flavor of layer-tap (#2937) 2025-05-08 11:43:48 -07:00
FearlessSpiff
ad6a181d7e feat(shield): Add underglow for reviung5 (#2191) 2025-05-05 20:46:21 +02:00
Omar L.
f1b944b1ef docs: Add secondary functions for the keypad number keys (#2933)
* docs: Add secondary functions for the keypad number keys

* docs: Change descriptions of secondary functions for the keypad number keys

The descriptions were updated to match those of the USB HID usage tables.
2025-04-30 17:32:22 +01:00
Aasim-A
4235c8b491 chore: Add typescript support to eslint, fix errors (#2923) 2025-04-22 20:51:41 -07:00
Pete Johanson
e3030bfcc8 fix(combos): Properly clean up all old candidates. (#2928) 2025-04-22 01:17:12 -04:00
Pete Johanson
00ff486931 fix(combos): Restore prompts for two deprecated Kconfigs (#2926)
Restore prompts for deprecated combo Kconfig symbols, to avoid errors
for existing builds that set them explicitly.
2025-04-21 02:47:39 -04:00
Pete Johanson
c4ee8ab86b refactor(combos): Reduce RAM usage, simplify config (#2849)
* Reference combos by index, not 32-bit pointers, and store bitfields
instead of arrays in several places, to bring down our flash/RAM usage.
* Use bit field to track candidate combos, to avoid needing an explicit
`ZMK_COMBO_MAX_COMBOS_PER_KEY` setting.
* Determine the max keys per combo automatically from the devicetree,
so we remove the ZMK_COMBO_MAX_KEYS_PER_COMBO Kconfig symbol.
2025-04-20 05:01:22 -04:00
romil-soni
d9576c5534 fix(docs): remove title as alt text (#2922) 2025-04-20 10:41:13 +02:00
Xudong Zheng
c6738ce2e5 refactor(split): use LOG_HEXDUMP_DBG() to print position state data (#2854)
The previous code prints one line per byte, making debugging difficult.
2025-04-20 04:36:52 -04:00
Joel Spadin
2a7ab8ed0a Improve VS Code Python settings (#2860)
chore: Fix deprecated vscode Python formatter

The python.formatting.provider setting was deprecated in favor of
having a separate extension for each Python formatter and using
editor.defaultFormatter instead. This adds a recommendation for the
Black formatter extension and selects it for Python files.

chore: Limit vscode Python analysis paths

This limits vscode's Python analysis to the scripts folders so it
doesn't slow down trying to scan all of Zephyr.
2025-04-20 04:34:15 -04:00
Tobias Adolph
2c0e7daced fix(hid): Fix scroll value truncation (#2865)
Fix 8 bit truncation of 16 bit scroll values when passed into
functions `zmk_hid_mouse_scroll_set` and
`zmk_hid_mouse_scroll_update`.

Fixes: #2864

Co-authored-by: Tobias Adolph <43353209+adolto@users.noreply.github.comgit>
2025-04-20 04:28:51 -04:00
Tygo van den Hurk
84772ebf14 fix: changed shebang to make scripts more platform independent (#2893)
See for example this thread:

https://stackoverflow.com/questions/21612980/why-is-usr-bin-env-bash-superior-to-bin-bash

on why its better to use '#!/usr/bin/env bash' instead.
Without this change these scripts will not run on some
platforms. This is not a breaking change for the
platforms it already works on.
2025-04-20 04:23:31 -04:00
Genteure
7823a43f62 docs: fix path typo in module-creation.md (#2920) 2025-04-16 09:53:56 -07:00
dependabot[bot]
90bca78300 chore(deps): bump estree-util-value-to-estree in /docs (#2914) 2025-04-07 21:54:32 -07:00
Will Hack
a34839f001 chore(eslint): upgrade to eslint v9 and flat config (#2909) 2025-04-06 18:16:03 -07:00
Maximilian Engl
6f85f48b19 fix(split): add source to battery event (#2901) 2025-04-04 10:43:51 -07:00
dependabot[bot]
9aadc3e5ab chore(deps): bump image-size from 1.2.0 to 1.2.1 in /docs (#2903) 2025-04-03 22:54:30 -07:00
Nicolas Munnich
1c76bcb0a1 blog: Add meeting notes from February (#2890)
Co-authored-by: Cem Aksoylar <caksoylar@users.noreply.github.com>
2025-03-25 19:20:50 +01:00
Cem Aksoylar
d733fbafa5 docs: Document missing user-defined input processors (#2895) 2025-03-25 14:35:38 +01:00
Nicolas Munnich
49f86f7ed0 docs: Update hold-tap page (#2888)
Co-authored-by: Cem Aksoylar <caksoylar@users.noreply.github.com>
2025-03-23 18:12:50 -07:00
Pete Johanson
5bb39ec3ea fix(split): Conditionally build all split code (#2884)
Don't add the split CMake subdirectory unless the feature is enabled.
2025-03-18 18:49:59 -04:00
Pete Johanson
147c340c6e Feature: Full-Duplex Wired Split (#2766)
refactor(split): Refactor split code for extension

Extract central/peripheral code to allow for plugging in alternate
transports, instead of tying all split logic to BT.

feat(split): Add full-duplex wired split support

* Depends on full-duplex hardware UART for communication.
* Supports all existing central commands/peripheral events, including
  sensors/inputs from peripherals.
* Only one wired split peripheral supported (for now)
* Relies on chosen `zmk,split-uart` referencing the UART device.

docs: Add wired split config docs.

Migrate split to its own dedicated config file, and add details
on wired split config.

Co-authored-by: Nicolas Munnich <98408764+Nick-Munnich@users.noreply.github.com>

fix: Properly override stack size on RP2040

Move the system work queue stack size override on RP2040 ouf of
a `ZMK_BLE` conditional so it is properly applied generally for that
SoC.

---------

Co-authored-by: Nicolas Munnich <98408764+Nick-Munnich@users.noreply.github.com>
2025-03-18 02:48:32 -04:00
Genteure
5ba7e260f4 docs: update name of XIAO boards (#2869) 2025-03-17 15:15:57 -07:00
Cem Aksoylar
bffbccc748 chore(deps): npm update on docs (#2878) 2025-03-16 20:47:42 -07:00
Cem Aksoylar
b366df8b17 docs: Fix powershell tab in user setup (#2879) 2025-03-16 20:14:48 -07:00
JJGadgets
4da89bd997 fix(ci): pin tj-actions/changed-files due to compromise (#2874)
Ideally it's be swapped out for an alternative but for now this is to mitigate.
2025-03-15 02:25:01 -04:00
idesignstuff
5d9920406c Update container.mdx for clarity about ZMK repo folder location (#2868) 2025-03-10 21:52:30 -07:00
Cem Aksoylar
f5a838b4bd docs: Fix wakeup-source property location for composite kscan (#2861) 2025-03-09 14:59:57 -07:00
Allister MacLeod
eb170c930f feat(shields): Add physical layouts for tester_xiao and tester_pro_micro (#2852)
feat(shields): Add tester_xiao layouts

Added two layouts for the XIAO tester shield:

 - Keys arranged to match the XIAO pinout, viewed from the front
 - Single row of eleven keys

These layouts are abstractions since the tester_xiao shield may be
used in a situation where the "keys" are actually jumper wires on a
breadboard or the like.

feat(shields): Add tester_pro_micro layouts

Added two layouts for the Pro Micro tester shield:

 - Keys arranged to match the Pro Micro pinout, viewed from the front
 - Single row of eighteen keys
2025-03-07 19:22:25 -07:00
Pete Johanson
241ff39556 chore(main): release 0.2.1 (#2851) 2025-03-02 07:45:12 -07:00
Pete Johanson
f20e6ea759 fix(behaviors): Proper comma separated device list (#2850)
Properly generate the comma separated list of devs in caps word/
key repeat.
2025-03-02 06:49:07 -07:00
187 changed files with 11931 additions and 7470 deletions

View File

@@ -61,7 +61,7 @@ jobs:
- name: Enable babblesim group filter
run: west config manifest.group-filter -- +babblesim
- name: Update modules (west update)
run: west update
run: west update --fetch-opt=--filter=tree:0
- name: Export Zephyr CMake package (west zephyr-export)
run: west zephyr-export
- name: Build BabbleSim components

View File

@@ -34,13 +34,10 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- name: Install yaml2json
run: python3 -m pip install remarshal
- name: Fetch Build Matrix
run: |
echo "build_matrix=$(yaml2json '${{ inputs.build_matrix_path }}' | jq -c .)" >> $GITHUB_ENV
yaml2json "${{ inputs.build_matrix_path }}" | jq
echo "build_matrix=$(yq -oj -I0 '${{ inputs.build_matrix_path }}')" >> $GITHUB_ENV
yq -oj "${{ inputs.build_matrix_path }}"
build:
runs-on: ubuntu-latest
@@ -117,7 +114,7 @@ jobs:
- name: West Update
working-directory: ${{ env.base_dir }}
run: west update
run: west update --fetch-opt=--filter=tree:0
- name: West Zephyr export
working-directory: ${{ env.base_dir }}

View File

@@ -53,7 +53,7 @@ jobs:
- name: Initialize workspace (west init)
run: west init -l app
- name: Update modules (west update)
run: west update
run: west update --fetch-opt=--filter=tree:0
- name: Export Zephyr CMake package (west zephyr-export)
run: west zephyr-export
- name: Use Node.js
@@ -63,7 +63,7 @@ jobs:
- name: Install @actions/artifact
run: npm install @actions/artifact
- name: Build
uses: actions/github-script@v7
uses: actions/github-script@v8
id: boards-list
with:
script: |
@@ -95,7 +95,7 @@ jobs:
throw new Error('Failed to build one or more configurations');
}
- name: Upload artifacts
uses: actions/github-script@v7
uses: actions/github-script@v8
continue-on-error: ${{ github.event_name == 'pull_request' }}
id: boards-upload
with:
@@ -146,7 +146,7 @@ jobs:
include-list: ${{ steps.compile-list.outputs.result }}
steps:
- name: Join build lists
uses: actions/github-script@v7
uses: actions/github-script@v8
id: compile-list
with:
script: |
@@ -196,7 +196,7 @@ jobs:
node-version: "14.x"
- name: Install js-yaml
run: npm install js-yaml
- uses: actions/github-script@v7
- uses: actions/github-script@v8
id: core-list
with:
script: |
@@ -225,7 +225,7 @@ jobs:
node-version: "14.x"
- name: Install js-yaml
run: npm install js-yaml
- uses: actions/github-script@v7
- uses: actions/github-script@v8
id: boards-list
with:
script: |
@@ -302,7 +302,7 @@ jobs:
nightly-include: ${{ steps.nightly-list.outputs.result }}
steps:
- name: Create nightly list
uses: actions/github-script@v7
uses: actions/github-script@v8
id: nightly-list
with:
script: |
@@ -355,7 +355,7 @@ jobs:
- name: Install js-yaml
run: npm install js-yaml
- name: Aggregate Metadata
uses: actions/github-script@v7
uses: actions/github-script@v8
id: aggregate-metadata
with:
script: |
@@ -373,7 +373,7 @@ jobs:
result-encoding: string
- name: Organize Metadata
uses: actions/github-script@v7
uses: actions/github-script@v8
id: organize-metadata
with:
script: |
@@ -430,12 +430,12 @@ jobs:
uses: actions/checkout@v4
with:
persist-credentials: false
- uses: tj-actions/changed-files@v45
- uses: tj-actions/changed-files@9200e69727eb73eb060652b19946b8a2fdfb654b # pin to v45.0.8 due to https://github.com/tj-actions/changed-files/issues/2463 https://www.stepsecurity.io/blog/harden-runner-detection-tj-actions-changed-files-action-is-compromised
id: changed-files
with:
json: true
escape_json: false
- uses: actions/github-script@v7
- uses: actions/github-script@v8
id: board-changes
with:
script: |
@@ -443,7 +443,7 @@ jobs:
const boardChanges = changedFiles.filter(f => f.startsWith('app/boards'));
return boardChanges.length ? 'true' : 'false';
result-encoding: string
- uses: actions/github-script@v7
- uses: actions/github-script@v8
id: core-changes
with:
script: |

View File

@@ -11,28 +11,70 @@ permissions:
name: release-please
jobs:
release-please:
handle-commit:
name: Handle new commit
runs-on: ubuntu-latest
outputs:
release_created: ${{ steps.release.outputs.release_created }}
major: ${{ steps.release.outputs.major }}
minor: ${{ steps.release.outputs.minor }}
patch: ${{ steps.release.outputs.patch }}
steps:
- uses: googleapis/release-please-action@v4
id: release
with:
token: ${{ secrets.ZMK_RELEASE_PLEASE_TOKEN }}
target-branch: ${{ github.ref_name }}
release-new-version:
name: Release new version
needs: handle-commit
runs-on: ubuntu-latest
if: ${{ needs.handle-commit.outputs.release_created }}
env:
ZMK_RELEASE_PLEASE_TOKEN: ${{ secrets.ZMK_RELEASE_PLEASE_TOKEN }}
VERSION: v${{ needs.handle-commit.outputs.major }}.${{ needs.handle-commit.outputs.minor }}
steps:
- uses: actions/checkout@v4
if: ${{ steps.release.outputs.release_created }}
- name: create major, minor branch
if: ${{ steps.release.outputs.release_created && steps.release.outputs.patch == '0' }}
- name: Create major.minor branch
if: ${{ needs.handle-commit.outputs.patch == '0' }}
run: |
git remote add gh-token-branch "https://x-access-token:${{ secrets.ZMK_RELEASE_PLEASE_TOKEN }}@github.com/${{ github.repository }}.git"
git checkout -b v${{ steps.release.outputs.major }}.${{ steps.release.outputs.minor }}-branch
git push gh-token-branch v${{ steps.release.outputs.major }}.${{ steps.release.outputs.minor }}-branch
git remote add gh-token-branch "https://x-access-token:$ZMK_RELEASE_PLEASE_TOKEN@github.com/${{ github.repository }}.git"
git checkout -b $VERSION-branch
git push gh-token-branch $VERSION-branch
- name: tag major and minor versions
if: ${{ steps.release.outputs.release_created }}
run: |
git config user.name github-actions[bot]
git config user.email 41898282+github-actions[bot]@users.noreply.github.com
git remote add gh-token "https://x-access-token:${{ secrets.ZMK_RELEASE_PLEASE_TOKEN }}@github.com/${{ github.repository }}.git"
git tag -d v${{ steps.release.outputs.major }}.${{ steps.release.outputs.minor }} || true
git tag -a v${{ steps.release.outputs.major }}.${{ steps.release.outputs.minor }} -m "Release v${{ steps.release.outputs.major }}.${{ steps.release.outputs.minor }}"
git push --force gh-token v${{ steps.release.outputs.major }}.${{ steps.release.outputs.minor }}
git remote add gh-token "https://x-access-token:$ZMK_RELEASE_PLEASE_TOKEN@github.com/${{ github.repository }}.git"
git tag -d $VERSION || true
git tag -a $VERSION -m "Release $VERSION"
git push --force gh-token $VERSION
bump-user-config-template-version:
name: Bump user config template version
needs: handle-commit
runs-on: ubuntu-latest
if: ${{ needs.handle-commit.outputs.release_created }}
env:
ZMK_RELEASE_PLEASE_TOKEN: ${{ secrets.ZMK_RELEASE_PLEASE_TOKEN }}
VERSION: v${{ needs.handle-commit.outputs.major }}.${{ needs.handle-commit.outputs.minor }}
steps:
- name: Bump user config template
run: |
if [ -z "$VERSION" ]; then
echo "VERSION is not set, exiting."
exit 1
fi
git clone "https://x-access-token:$ZMK_RELEASE_PLEASE_TOKEN@github.com/zmkfirmware/unified-zmk-config-template.git"
cd unified-zmk-config-template
git config user.name github-actions[bot]
git config user.email 41898282+github-actions[bot]@users.noreply.github.com
sed -i 's/^\(\s*\)revision: .*/\1revision: '"$VERSION"'/' config/west.yml
sed -i 's|uses: zmkfirmware/zmk/.github/workflows/build-user-config.yml@.*|uses: zmkfirmware/zmk/.github/workflows/build-user-config.yml@'"$VERSION"'|' .github/workflows/build.yml
git add .
git commit -m "Version bump to $VERSION"
git push origin main

28
.github/workflows/stale.yml vendored Normal file
View File

@@ -0,0 +1,28 @@
name: "Mark and close stale PRs in the repo"
on:
schedule:
- cron: "00 14 * * *" # runs daily at 14:00 https://crontab.guru/#00_14_*_*_*
jobs:
stale:
runs-on: ubuntu-24.04
steps:
- uses: actions/stale@v9.1.0
with:
days-before-pr-stale: 300 # ~10 months
stale-pr-label: "stale"
stale-pr-message: >
This PR has been automatically marked as stale because it has not
had activity in 10 months. It will be closed in 14 days if no
further activity occurs. Feel free to give a status update or
re-open when it has been rebased and is ready for review (again).
Thanks!
days-before-pr-close: 14
close-pr-message: >
This PR was closed because it had no activity for over 10 months.
Feel free to give a status update or re-open when it has been
rebased and is ready for review (again).
days-before-issue-stale: 1000 # ~3 years
days-before-issue-close: -1
ascending: true # Process older PRs first
operations-per-run: 30 # Default value, listed here again to make it explicit

View File

@@ -61,7 +61,7 @@ jobs:
- name: Initialize workspace (west init)
run: west init -l app
- name: Update modules (west update)
run: west update
run: west update --fetch-opt=--filter=tree:0
- name: Export Zephyr CMake package (west zephyr-export)
run: west zephyr-export
- name: Test ${{ matrix.test }}

View File

@@ -12,8 +12,8 @@ repos:
types_or: [c++, c]
args:
- -i
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v2.7.1
- repo: https://github.com/rbubley/mirrors-prettier
rev: 787fb9f542b140ba0b2aced38e6a3e68021647a3
hooks:
- id: prettier
exclude: |
@@ -22,9 +22,6 @@ repos:
CHANGELOG.md|
.release-please-manifest.json
)$
# Workaround for https://github.com/pre-commit/mirrors-prettier/issues/29
additional_dependencies:
- prettier@2.8.7
- repo: https://github.com/jorisroovers/gitlint
rev: v0.19.1
hooks:

View File

@@ -1 +1 @@
{".":"0.2.0"}
{".":"0.3.0"}

View File

@@ -1,6 +1,7 @@
{
"recommendations": [
"esbenp.prettier-vscode",
"ms-python.black-formatter",
"ms-python.python",
"ms-vscode.cpptools",
"plorefice.devicetree",

View File

@@ -3,7 +3,7 @@
"*.overlay": "dts",
"*.keymap": "dts"
},
"python.formatting.provider": "black",
"python.analysis.include": ["app/scripts", "zephyr/scripts"],
"[c]": {
"editor.formatOnSave": true
},
@@ -13,7 +13,7 @@
},
"[python]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "ms-python.python"
"editor.defaultFormatter": "ms-python.black-formatter"
},
"[css][json][jsonc][html][markdown][yaml]": {
"editor.formatOnSave": true,

View File

@@ -1,5 +1,59 @@
# Changelog
## [0.3.0](https://github.com/zmkfirmware/zmk/compare/v0.2.1...v0.3.0) (2025-08-01)
### Features
* **ble:** Add function to get profile address by index ([#2992](https://github.com/zmkfirmware/zmk/issues/2992)) ([9e905d6](https://github.com/zmkfirmware/zmk/commit/9e905d65936348824588dc3f424755353ac61186))
* **ci:** Add stale GitHub Action to automatically close stale PRs ([#2924](https://github.com/zmkfirmware/zmk/issues/2924)) ([6d7bbc8](https://github.com/zmkfirmware/zmk/commit/6d7bbc8670d175fd63e8c834feb41f80e7b52e74))
* **display:** nice!view individual profile status ([#2265](https://github.com/zmkfirmware/zmk/issues/2265)) ([d09087f](https://github.com/zmkfirmware/zmk/commit/d09087f4dc280b8fdb1d32d63b03cc10162b89ce))
* Full-Duplex Wired Split ([#2766](https://github.com/zmkfirmware/zmk/issues/2766)) ([147c340](https://github.com/zmkfirmware/zmk/commit/147c340c6e8d377304acfdd64dc86cf83ebdfef2))
* **metadata:** Add metadata to the mouse_key_press behavior ([#2950](https://github.com/zmkfirmware/zmk/issues/2950)) ([239baa4](https://github.com/zmkfirmware/zmk/commit/239baa487509ace108d36f0e5c627d61a3d95f53))
* **pointing:** Allow peripheral input processing to stop propagation ([#2844](https://github.com/zmkfirmware/zmk/issues/2844)) ([462d48b](https://github.com/zmkfirmware/zmk/commit/462d48b78edac8bedb75666699ea4fa446d2152c))
* **shield:** Add underglow for reviung5 ([#2191](https://github.com/zmkfirmware/zmk/issues/2191)) ([ad6a181](https://github.com/zmkfirmware/zmk/commit/ad6a181d7ec34fb6e31134f6bb991a9b2d0b8f78))
* **shields:** Add a physical layout for a_dux ([#3000](https://github.com/zmkfirmware/zmk/issues/3000)) ([7292df0](https://github.com/zmkfirmware/zmk/commit/7292df02d4b05d783f432f8658de22d940909fe4))
* **shields:** Add physical layouts for tester_xiao and tester_pro_micro ([#2852](https://github.com/zmkfirmware/zmk/issues/2852)) ([eb170c9](https://github.com/zmkfirmware/zmk/commit/eb170c930f56e3fb3df0b813d987abfd1dc31b9a))
* **shields:** Add tester_pro_micro layouts ([eb170c9](https://github.com/zmkfirmware/zmk/commit/eb170c930f56e3fb3df0b813d987abfd1dc31b9a))
* **shields:** Add tester_xiao layouts ([eb170c9](https://github.com/zmkfirmware/zmk/commit/eb170c930f56e3fb3df0b813d987abfd1dc31b9a))
* **split:** Add full-duplex wired split support ([147c340](https://github.com/zmkfirmware/zmk/commit/147c340c6e8d377304acfdd64dc86cf83ebdfef2))
* **split:** Runtime selection of split transport ([6b44d33](https://github.com/zmkfirmware/zmk/commit/6b44d33db2f4bad7d98e475e6f7968493b05af73))
* **split:** Runtime selection of split transport ([#2886](https://github.com/zmkfirmware/zmk/issues/2886)) ([6b44d33](https://github.com/zmkfirmware/zmk/commit/6b44d33db2f4bad7d98e475e6f7968493b05af73))
* **split:** Suspend/resume wired UART devices. ([6b44d33](https://github.com/zmkfirmware/zmk/commit/6b44d33db2f4bad7d98e475e6f7968493b05af73))
### Bug Fixes
* **behaviors:** Correct macro release state for parametrized ([1bac680](https://github.com/zmkfirmware/zmk/commit/1bac680c4fb4f07e43c01754b6f1e72dab455e50))
* **behaviors:** Correct macro release state for parametrized macros ([#2942](https://github.com/zmkfirmware/zmk/issues/2942)) ([1bac680](https://github.com/zmkfirmware/zmk/commit/1bac680c4fb4f07e43c01754b6f1e72dab455e50))
* **ble,hid:** Fix smooth scrolling over BLE ([#2998](https://github.com/zmkfirmware/zmk/issues/2998)) ([342d838](https://github.com/zmkfirmware/zmk/commit/342d83891301b1be53233a12c7723bb99cbe5ff6)), closes [#2957](https://github.com/zmkfirmware/zmk/issues/2957)
* **boards:** Disable high voltage DC-DC by default ([#2995](https://github.com/zmkfirmware/zmk/issues/2995)) ([8059e67](https://github.com/zmkfirmware/zmk/commit/8059e671b24a261939401afb5a65c4fa756adc2d)), closes [#2990](https://github.com/zmkfirmware/zmk/issues/2990)
* changed shebang to make scripts more platform independent ([#2893](https://github.com/zmkfirmware/zmk/issues/2893)) ([84772eb](https://github.com/zmkfirmware/zmk/commit/84772ebf14e5a7c67ba573a61f0a50048802c799))
* **ci:** pin tj-actions/changed-files due to compromise ([#2874](https://github.com/zmkfirmware/zmk/issues/2874)) ([4da89bd](https://github.com/zmkfirmware/zmk/commit/4da89bd99716bf6c1d7d788f3cdaec4cee7403e9))
* **combos:** Properly clean up all old candidates. ([#2928](https://github.com/zmkfirmware/zmk/issues/2928)) ([e3030bf](https://github.com/zmkfirmware/zmk/commit/e3030bfcc87b7f511b0ebe993fb1f1f06215982e))
* **combos:** Restore prompts for two deprecated Kconfigs ([#2926](https://github.com/zmkfirmware/zmk/issues/2926)) ([00ff486](https://github.com/zmkfirmware/zmk/commit/00ff48693113ed74a3345aa1ac81fdea302b3a09))
* **core:** Correctly sync BAS battery level ([#2977](https://github.com/zmkfirmware/zmk/issues/2977)) ([af96766](https://github.com/zmkfirmware/zmk/commit/af967667b0e139a963178e63028c7be341cade9e))
* **display:** Make stock battery widget depend on the right symbol ([#2953](https://github.com/zmkfirmware/zmk/issues/2953)) ([9da5d3b](https://github.com/zmkfirmware/zmk/commit/9da5d3ba82b38b74ad798a82a838d84c52220bbe))
* **docs:** Fix soft off waker configuration example ([#2960](https://github.com/zmkfirmware/zmk/issues/2960)) ([eb99b4e](https://github.com/zmkfirmware/zmk/commit/eb99b4ede06bc01674ce16217ebbad40bc11ec50))
* **docs:** remove title as alt text ([#2922](https://github.com/zmkfirmware/zmk/issues/2922)) ([d9576c5](https://github.com/zmkfirmware/zmk/commit/d9576c55346acfc8eed36709aaae28f91e0d06ad))
* Fix build with Studio and USB but not UART ([#2996](https://github.com/zmkfirmware/zmk/issues/2996)) ([cef7af4](https://github.com/zmkfirmware/zmk/commit/cef7af4408cc44c20fab93a0b2e20b3429d0a98e))
* **hid:** Fix scroll value truncation ([#2865](https://github.com/zmkfirmware/zmk/issues/2865)) ([2c0e7da](https://github.com/zmkfirmware/zmk/commit/2c0e7daced1421c34a9d417b7d3e9bccbf0ebd7f)), closes [#2864](https://github.com/zmkfirmware/zmk/issues/2864)
* **pointing:** Avoids mutex leak for default layer toggle event ([#2934](https://github.com/zmkfirmware/zmk/issues/2934)) ([461f5c8](https://github.com/zmkfirmware/zmk/commit/461f5c832fb8854d87dca54d113d306323697219))
* Properly override stack size on RP2040 ([147c340](https://github.com/zmkfirmware/zmk/commit/147c340c6e8d377304acfdd64dc86cf83ebdfef2))
* **split:** add source to battery event ([#2901](https://github.com/zmkfirmware/zmk/issues/2901)) ([6f85f48](https://github.com/zmkfirmware/zmk/commit/6f85f48b19afae04f98e9abacb36ce1425b61f78))
* **split:** Compile and run properly in wired polling mode. ([#3012](https://github.com/zmkfirmware/zmk/issues/3012)) ([2ae5185](https://github.com/zmkfirmware/zmk/commit/2ae51854192aed92af95536f79547e2928cd1bf5))
* **split:** Conditionally build all split code ([#2884](https://github.com/zmkfirmware/zmk/issues/2884)) ([5bb39ec](https://github.com/zmkfirmware/zmk/commit/5bb39ec3eae23055593350cb3689a8240028181e))
* **split:** Enable wired split by default if DTS is set ([#3010](https://github.com/zmkfirmware/zmk/issues/3010)) ([1530ae3](https://github.com/zmkfirmware/zmk/commit/1530ae36c22e3e2285e895737c74de5d960a5ae4))
* **split:** Minor wired split fixes. ([6b44d33](https://github.com/zmkfirmware/zmk/commit/6b44d33db2f4bad7d98e475e6f7968493b05af73))
* Unconditionally define HID payloads to avoid error. ([6b44d33](https://github.com/zmkfirmware/zmk/commit/6b44d33db2f4bad7d98e475e6f7968493b05af73))
## [0.2.1](https://github.com/zmkfirmware/zmk/compare/v0.2.0...v0.2.1) (2025-03-02)
### Bug Fixes
* **behaviors:** Proper comma separated device list ([#2850](https://github.com/zmkfirmware/zmk/issues/2850)) ([f20e6ea](https://github.com/zmkfirmware/zmk/commit/f20e6ea7594b49eef1e3acc017529073a0409962))
## [0.2.0](https://github.com/zmkfirmware/zmk/compare/v0.1.0...v0.2.0) (2025-03-01)

View File

@@ -1,3 +1,4 @@
module.exports = {
endOfLine: "auto",
trailingComma: "es5",
};

View File

@@ -96,7 +96,7 @@ target_sources_ifdef(CONFIG_ZMK_BATTERY_REPORTING app PRIVATE src/battery.c)
target_sources_ifdef(CONFIG_ZMK_HID_INDICATORS app PRIVATE src/events/hid_indicators_changed.c)
target_sources_ifdef(CONFIG_ZMK_SPLIT app PRIVATE src/events/split_peripheral_status_changed.c)
add_subdirectory(src/split)
add_subdirectory_ifdef(CONFIG_ZMK_SPLIT src/split)
target_sources_ifdef(CONFIG_USB_DEVICE_STACK app PRIVATE src/usb.c)
target_sources_ifdef(CONFIG_ZMK_USB app PRIVATE src/usb_hid.c)

View File

@@ -189,10 +189,6 @@ config BT_CTLR_PHY_2M
config BT_TINYCRYPT_ECC
default y if BT_HCI && !BT_CTLR
config SYSTEM_WORKQUEUE_STACK_SIZE
default 4096 if SOC_RP2040
default 2048
config ZMK_BLE_THREAD_STACK_SIZE
int "BLE notify thread stack size"
default 768
@@ -448,12 +444,16 @@ config ZMK_COMBO_MAX_PRESSED_COMBOS
default 4
config ZMK_COMBO_MAX_COMBOS_PER_KEY
int "Maximum number of combos per key"
default 5
int "Deprecated: Max combos per key"
default 0
help
Deprecated: Storage for combos is now determined automatically
config ZMK_COMBO_MAX_KEYS_PER_COMBO
int "Maximum number of keys per combo"
default 4
int "Deprecated: Max keys per combo"
default 0
help
Deprecated: This is now auto-calculated based on `key-positions` in devicetree
# Combo options
endmenu

View File

@@ -1,6 +1,10 @@
# Copyright (c) 2024 The ZMK Contributors
# SPDX-License-Identifier: MIT
config SYSTEM_WORKQUEUE_STACK_SIZE
default 2048 if SOC_RP2040
default 2048 if ZMK_BLE
# HID
if ZMK_HID_REPORT_TYPE_HKRO

View File

@@ -3,7 +3,7 @@ VERSION_MAJOR = 0
# x-release-please-end
# x-release-please-start-minor
VERSION_MINOR = 2
VERSION_MINOR = 3
# x-release-please-end
# x-release-please-start-patch

View File

@@ -6,7 +6,7 @@ if BOARD_GLOVE80_LH
config BOARD
default "glove80 lh"
config ZMK_SPLIT_BLE_ROLE_CENTRAL
config ZMK_SPLIT_ROLE_CENTRAL
default y
endif # BOARD_GLOVE80_LH

View File

@@ -7,7 +7,7 @@ config BOARD_ENABLE_DCDC
config BOARD_ENABLE_DCDC_HV
bool "High voltage DCDC converter"
select SOC_DCDC_NRF52X_HV
default y
default n
depends on (BOARD_MIKOTO)
choice BOARD_MIKOTO_CHARGER_CURRENT

View File

@@ -9,5 +9,5 @@ config BOARD_ENABLE_DCDC
config BOARD_ENABLE_DCDC_HV
bool "High voltage DCDC converter"
select SOC_DCDC_NRF52X_HV
default y
default n
depends on (BOARD_NICE_NANO_V2)

View File

@@ -1,6 +1,6 @@
file_format: "1"
id: seeeduino_xiao
name: Seeeduino XIAO
name: Seeed Studio XIAO SAMD21
type: board
arch: arm
outputs:

View File

@@ -1,6 +1,6 @@
file_format: "1"
id: seeeduino_xiao_ble
name: Seeeduino XIAO BLE
name: Seeed Studio XIAO nRF52840
type: board
arch: arm
outputs:

View File

@@ -1,6 +1,6 @@
file_format: "1"
id: seeeduino_xiao_rp2040
name: Seeeduino XIAO RP2040
name: Seeed Studio XIAO RP2040
type: board
arch: arm
outputs:

View File

@@ -5,9 +5,9 @@ type: interconnect
url: https://wiki.seeedstudio.com/Seeeduino-XIAO/
manufacturer: Seeed
description: |
The Seeed(uino) XIAO is a popular smaller format micro-controller, that has gained popularity as an alternative
The Seeed Studio XIAO is a popular smaller format micro-controller, that has gained popularity as an alternative
to the SparkFun Pro Micro. Since its creation, several pin compatible controllers, such
as the Seeeduino XIAO BLE, Adafruit QT Py and Adafruit QT Py RP2040, have become available.
as the Seeed Studio XIAO nRF52840 (also known as XIAO BLE), Adafruit QT Py and Adafruit QT Py RP2040, have become available.
node_labels:
gpio: xiao_d
i2c: xiao_i2c

View File

@@ -0,0 +1,12 @@
# Copyright (c) 2025 The ZMK Contributors
# SPDX-License-Identifier: MIT
CONFIG_ZMK_BLE=y
CONFIG_ZMK_USB=y
CONFIG_MPU_ALLOW_FLASH_WRITE=y
CONFIG_NVS=y
CONFIG_SETTINGS_NVS=y
CONFIG_FLASH=y
CONFIG_FLASH_PAGE_LAYOUT=y
CONFIG_FLASH_MAP=y

View File

@@ -3,3 +3,10 @@ CONFIG_SERIAL=n
CONFIG_UART_CONSOLE=n
CONFIG_UART_INTERRUPT_DRIVEN=n
CONFIG_ZMK_USB=y
CONFIG_MPU_ALLOW_FLASH_WRITE=y
CONFIG_NVS=y
CONFIG_SETTINGS_NVS=y
CONFIG_FLASH=y
CONFIG_FLASH_PAGE_LAYOUT=y
CONFIG_FLASH_MAP=y

View File

@@ -0,0 +1,15 @@
&uart0 { status = "disabled"; };
&code_partition {
reg = <0x100 (DT_SIZE_M(2) - 0x100 - DT_SIZE_K(512))>;
};
&flash0 {
partitions {
storage_partition: partition@180000 {
reg = <0x180000 DT_SIZE_K(512)>;
read-only;
};
};
};

View File

@@ -0,0 +1,50 @@
/*
* Copyright (c) 2025 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#include <physical_layouts.dtsi>
/ {
physical_layout0: physical_layout_0 {
compatible = "zmk,physical-layout";
display-name = "Default";
keys // w h x y rot rx ry
= <&key_physical_attrs 100 100 0 133 0 0 0>
, <&key_physical_attrs 100 100 100 31 0 0 0>
, <&key_physical_attrs 100 100 200 0 0 0 0>
, <&key_physical_attrs 100 100 300 28 0 0 0>
, <&key_physical_attrs 100 100 400 42 0 0 0>
, <&key_physical_attrs 100 100 800 42 0 0 0>
, <&key_physical_attrs 100 100 900 28 0 0 0>
, <&key_physical_attrs 100 100 1000 0 0 0 0>
, <&key_physical_attrs 100 100 1100 31 0 0 0>
, <&key_physical_attrs 100 100 1200 133 0 0 0>
, <&key_physical_attrs 100 100 0 233 0 0 0>
, <&key_physical_attrs 100 100 100 131 0 0 0>
, <&key_physical_attrs 100 100 200 100 0 0 0>
, <&key_physical_attrs 100 100 300 128 0 0 0>
, <&key_physical_attrs 100 100 400 142 0 0 0>
, <&key_physical_attrs 100 100 800 142 0 0 0>
, <&key_physical_attrs 100 100 900 128 0 0 0>
, <&key_physical_attrs 100 100 1000 100 0 0 0>
, <&key_physical_attrs 100 100 1100 131 0 0 0>
, <&key_physical_attrs 100 100 1200 233 0 0 0>
, <&key_physical_attrs 100 100 0 333 0 0 0>
, <&key_physical_attrs 100 100 100 231 0 0 0>
, <&key_physical_attrs 100 100 200 200 0 0 0>
, <&key_physical_attrs 100 100 300 228 0 0 0>
, <&key_physical_attrs 100 100 400 242 0 0 0>
, <&key_physical_attrs 100 100 800 242 0 0 0>
, <&key_physical_attrs 100 100 900 228 0 0 0>
, <&key_physical_attrs 100 100 1000 200 0 0 0>
, <&key_physical_attrs 100 100 1100 231 0 0 0>
, <&key_physical_attrs 100 100 1200 333 0 0 0>
, <&key_physical_attrs 100 100 400 375 0 0 0>
, <&key_physical_attrs 100 100 500 400 0 0 0>
, <&key_physical_attrs 100 100 700 400 0 0 0>
, <&key_physical_attrs 100 100 800 375 0 0 0>
;
};
};

View File

@@ -5,12 +5,18 @@
*/
#include <dt-bindings/zmk/matrix_transform.h>
#include "a_dux-layouts.dtsi"
&physical_layout0 {
transform = <&default_transform>;
};
/ {
chosen {
zmk,kscan = &kscan0;
zmk,matrix-transform = &default_transform;
zmk,physical-layout = &physical_layout0;
};
default_transform: keymap_transform_0 {
@@ -49,5 +55,4 @@
<&pro_micro 9 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>
;
};
};

View File

@@ -4,9 +4,21 @@ The nice!view is a low-power, high refresh rate display meant to replace I2C OLE
This shield requires that an `&nice_view_spi` labeled SPI bus is provided with _at least_ MOSI, SCK, and CS pins defined.
## Custom widget
The nice!view shield includes a custom vertical widget.
Profile indicators show the status of the first five BLE profiles using numbers from 1 to 5.
The number corresponding to the currently selected profile is drawn in a filled disk
and the circle outline around each profile number correspond to the following states:
- solid outline: connected
- dashed outline: not connected
- no outline: not bound
## Disable custom widget
The nice!view shield includes a custom vertical widget. To use the built-in ZMK one, add the following item to your `.conf` file:
To use the built-in ZMK widget instead of the custom nice!view one, add the following item to your `.conf` file:
```
CONFIG_ZMK_DISPLAY_STATUS_SCREEN_BUILT_IN=y

View File

@@ -1,6 +1,6 @@
/*
*
* Copyright (c) 2023 The ZMK Contributors
* Copyright (c) 2025 The ZMK Contributors
* SPDX-License-Identifier: MIT
*
*/
@@ -33,6 +33,8 @@ struct output_status_state {
int active_profile_index;
bool active_profile_connected;
bool active_profile_bonded;
bool profiles_connected[NICEVIEW_PROFILE_COUNT];
bool profiles_bonded[NICEVIEW_PROFILE_COUNT];
};
struct layer_status_state {
@@ -142,15 +144,24 @@ static void draw_middle(lv_obj_t *widget, lv_color_t cbuf[], const struct status
lv_canvas_draw_rect(canvas, 0, 0, CANVAS_SIZE, CANVAS_SIZE, &rect_black_dsc);
// Draw circles
int circle_offsets[5][2] = {
int circle_offsets[NICEVIEW_PROFILE_COUNT][2] = {
{13, 13}, {55, 13}, {34, 34}, {13, 55}, {55, 55},
};
for (int i = 0; i < 5; i++) {
for (int i = 0; i < NICEVIEW_PROFILE_COUNT; i++) {
bool selected = i == state->active_profile_index;
lv_canvas_draw_arc(canvas, circle_offsets[i][0], circle_offsets[i][1], 13, 0, 360,
&arc_dsc);
if (state->profiles_connected[i]) {
lv_canvas_draw_arc(canvas, circle_offsets[i][0], circle_offsets[i][1], 13, 0, 360,
&arc_dsc);
} else if (state->profiles_bonded[i]) {
const int segments = 8;
const int gap = 20;
for (int j = 0; j < segments; ++j)
lv_canvas_draw_arc(canvas, circle_offsets[i][0], circle_offsets[i][1], 13,
360. / segments * j + gap / 2.0,
360. / segments * (j + 1) - gap / 2.0, &arc_dsc);
}
if (selected) {
lv_canvas_draw_arc(canvas, circle_offsets[i][0], circle_offsets[i][1], 9, 0, 359,
@@ -234,6 +245,10 @@ static void set_output_status(struct zmk_widget_status *widget,
widget->state.active_profile_index = state->active_profile_index;
widget->state.active_profile_connected = state->active_profile_connected;
widget->state.active_profile_bonded = state->active_profile_bonded;
for (int i = 0; i < NICEVIEW_PROFILE_COUNT; ++i) {
widget->state.profiles_connected[i] = state->profiles_connected[i];
widget->state.profiles_bonded[i] = state->profiles_bonded[i];
}
draw_top(widget->obj, widget->cbuf, &widget->state);
draw_middle(widget->obj, widget->cbuf2, &widget->state);
@@ -245,12 +260,17 @@ static void output_status_update_cb(struct output_status_state state) {
}
static struct output_status_state output_status_get_state(const zmk_event_t *_eh) {
return (struct output_status_state){
struct output_status_state state = {
.selected_endpoint = zmk_endpoints_selected(),
.active_profile_index = zmk_ble_active_profile_index(),
.active_profile_connected = zmk_ble_active_profile_is_connected(),
.active_profile_bonded = !zmk_ble_active_profile_is_open(),
};
for (int i = 0; i < MIN(NICEVIEW_PROFILE_COUNT, ZMK_BLE_PROFILE_COUNT); ++i) {
state.profiles_connected[i] = zmk_ble_profile_is_connected(i);
state.profiles_bonded[i] = !zmk_ble_profile_is_open(i);
}
return state;
}
ZMK_DISPLAY_WIDGET_LISTENER(widget_output_status, struct output_status_state,

View File

@@ -1,6 +1,6 @@
/*
*
* Copyright (c) 2023 The ZMK Contributors
* Copyright (c) 2025 The ZMK Contributors
* SPDX-License-Identifier: MIT
*
*/
@@ -8,6 +8,8 @@
#include <lvgl.h>
#include <zmk/endpoints.h>
#define NICEVIEW_PROFILE_COUNT 5
#define CANVAS_SIZE 68
#define LVGL_BACKGROUND \
@@ -23,6 +25,8 @@ struct status_state {
int active_profile_index;
bool active_profile_connected;
bool active_profile_bonded;
bool profiles_connected[NICEVIEW_PROFILE_COUNT];
bool profiles_bonded[NICEVIEW_PROFILE_COUNT];
uint8_t layer_index;
const char *layer_label;
uint8_t wpm[10];

View File

@@ -0,0 +1,52 @@
/*
* Copyright (c) 2024 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#include <dt-bindings/led/led.h>
&pinctrl {
spi3_default: spi3_default {
group1 {
psels = <NRF_PSEL(SPIM_MOSI, 0, 6)>;
};
};
spi3_sleep: spi3_sleep {
group1 {
psels = <NRF_PSEL(SPIM_MOSI, 0, 6)>;
low-power-enable;
};
};
};
&spi3 {
compatible = "nordic,nrf-spim";
status = "okay";
pinctrl-0 = <&spi3_default>;
pinctrl-1 = <&spi3_sleep>;
pinctrl-names = "default", "sleep";
led_strip: ws2812@0 {
compatible = "worldsemi,ws2812-spi";
/* SPI */
reg = <0>; /* ignored, but necessary for SPI bindings */
spi-max-frequency = <4000000>;
/* WS2812 */
chain-length = <4>;
spi-one-frame = <0x70>;
spi-zero-frame = <0x40>;
color-mapping = <LED_COLOR_ID_GREEN LED_COLOR_ID_RED LED_COLOR_ID_BLUE>;
};
};
/ {
chosen {
zmk,underglow = &led_strip;
};
};

View File

@@ -1,3 +1,7 @@
# Encoder support. Uncomment to enable.
# Uncomment the following two lines to add support for encoders
# CONFIG_EC11=y
# CONFIG_EC11_TRIGGER_GLOBAL_THREAD=y
# Uncomment the following two lines to enable RGB underglow
# CONFIG_ZMK_RGB_UNDERGLOW=y
# CONFIG_WS2812_STRIP=y

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2022 The ZMK Contributors
* Copyright (c) 2024 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
@@ -8,20 +8,31 @@
#include <dt-bindings/zmk/keys.h>
#include <dt-bindings/zmk/bt.h>
#include <dt-bindings/zmk/outputs.h>
#include <dt-bindings/zmk/rgb.h>
#define BASE 0
#define BLE 1
#define RGB 2
/ {
behaviors {
rgb_encoder: rgb_encoder {
compatible = "zmk,behavior-sensor-rotate";
#sensor-binding-cells = <0>;
bindings = <&rgb_ug RGB_BRI>, <&rgb_ug RGB_BRD>;
};
};
keymap {
compatible = "zmk,keymap";
base_layer {
display-name = "BASE";
bindings = <
// ╭───────────────────────────┬──────────────────┬────────────┬─────────────╮
&mo BLE &kp C_PREVIOUS &kp C_PLAY_PAUSE &kp C_NEXT &kp C_MUTE
// ╰───────────────────────────┴──────────────────┴────────────┴─────────────╯
// ╭─────────────────────────┬──────────────────┬────────────┬────────────────╮
&mo BLE &kp C_PREVIOUS &kp C_PLAY_PAUSE &kp C_NEXT &lt RGB C_MUTE
// ╰─────────────────────────┴──────────────────┴────────────┴────────────────╯
>;
sensor-bindings = <&inc_dec_kp C_VOL_UP C_VOL_DN>;
};
@@ -29,10 +40,20 @@
ble_layer {
display-name = "BLE";
bindings = <
// ╭─────────────┬─────────────┬─────────────┬────────────┬─────────────╮
&trans &out OUT_TOG &bt BT_PRV &bt BT_NXT &bt BT_CLR
// ╰─────────────┴─────────────┴─────────────┴────────────┴─────────────╯
// ╭────────┬─────────────┬────────────┬────────────┬────────────╮
&trans &out OUT_TOG &bt BT_PRV &bt BT_NXT &bt BT_CLR
// ╰────────┴─────────────┴────────────┴────────────┴────────────╯
>;
};
rgb_layer {
display-name = "RGB";
bindings = <
// ╭──────────────────┬─────────────────┬─────────────────┬──────────────────────────────────┬────────╮
&rgb_ug RGB_TOG &rgb_ug RGB_EFR &rgb_ug RGB_EFF &rgb_ug RGB_COLOR_HSB(307,89,98) &trans
// ╰──────────────────┴─────────────────┴─────────────────┴──────────────────────────────────┴────────╯
>;
sensor-bindings = <&rgb_encoder>;
};
};
};
};

View File

@@ -7,3 +7,4 @@ requires: [pro_micro]
features:
- keys
- encoder
- underglow

View File

@@ -0,0 +1,80 @@
/*
* Copyright (c) 2025 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#include <physical_layouts.dtsi>
/ {
tester_position_map {
compatible = "zmk,physical-layout-position-map";
complete;
pinout_map: pinout_positions {
physical-layout = <&physical_layout0>;
positions = <0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17>;
};
inline_map: single_row_positions {
physical-layout = <&physical_layout1>;
positions = <0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17>;
};
};
physical_layout0: physical_layout_0 {
compatible = "zmk,physical-layout";
display-name = "Pro Micro Pinout";
// Map key positions to the Pro Micro pinout. The coordinate
// deltas jump around a little because some of the Pro Micro
// pins are out of order: D0 is "after" D1 and D16 is "before"
// D14.
keys // w h x y rot rx ry
= <&key_physical_attrs 100 100 0 100 0 0 0> // D0
, <&key_physical_attrs 100 100 0 0 0 0 0> // D1
, <&key_physical_attrs 100 100 0 400 0 0 0> // D2
, <&key_physical_attrs 100 100 0 500 0 0 0> // D3
, <&key_physical_attrs 100 100 0 600 0 0 0> // D4
, <&key_physical_attrs 100 100 0 700 0 0 0> // D5
, <&key_physical_attrs 100 100 0 800 0 0 0> // D6
, <&key_physical_attrs 100 100 0 900 0 0 0> // D7
, <&key_physical_attrs 100 100 0 1000 0 0 0> // D8
, <&key_physical_attrs 100 100 0 1100 0 0 0> // D9
, <&key_physical_attrs 100 100 600 1100 0 0 0> // D10
, <&key_physical_attrs 100 100 600 900 0 0 0> // D14
, <&key_physical_attrs 100 100 600 800 0 0 0> // D15
, <&key_physical_attrs 100 100 600 1000 0 0 0> // D16
, <&key_physical_attrs 100 100 600 700 0 0 0> // D18
, <&key_physical_attrs 100 100 600 600 0 0 0> // D19
, <&key_physical_attrs 100 100 600 500 0 0 0> // D20
, <&key_physical_attrs 100 100 600 400 0 0 0> // D21
;
};
physical_layout1: physical_layout_1 {
compatible = "zmk,physical-layout";
display-name = "Single Row";
// Single row of eighteen "keys", in ascending "Arduino" order.
keys // w h x y rot rx ry
= <&key_physical_attrs 100 100 0 0 0 0 0>
, <&key_physical_attrs 100 100 100 0 0 0 0>
, <&key_physical_attrs 100 100 200 0 0 0 0>
, <&key_physical_attrs 100 100 300 0 0 0 0>
, <&key_physical_attrs 100 100 400 0 0 0 0>
, <&key_physical_attrs 100 100 500 0 0 0 0>
, <&key_physical_attrs 100 100 600 0 0 0 0>
, <&key_physical_attrs 100 100 700 0 0 0 0>
, <&key_physical_attrs 100 100 800 0 0 0 0>
, <&key_physical_attrs 100 100 900 0 0 0 0>
, <&key_physical_attrs 100 100 1000 0 0 0 0>
, <&key_physical_attrs 100 100 1100 0 0 0 0>
, <&key_physical_attrs 100 100 1200 0 0 0 0>
, <&key_physical_attrs 100 100 1300 0 0 0 0>
, <&key_physical_attrs 100 100 1400 0 0 0 0>
, <&key_physical_attrs 100 100 1500 0 0 0 0>
, <&key_physical_attrs 100 100 1600 0 0 0 0>
, <&key_physical_attrs 100 100 1700 0 0 0 0>
;
};
};

View File

@@ -1,9 +1,18 @@
#include <dt-bindings/zmk/matrix_transform.h>
#include "tester_pro_micro-layouts.dtsi"
&physical_layout0 {
transform = <&transform0>;
};
&physical_layout1 {
transform = <&transform0>;
};
/ {
chosen {
zmk,kscan = &kscan0;
zmk,matrix-transform = &transform0;
zmk,physical-layout = &physical_layout0;
};
kscan0: kscan {
@@ -41,4 +50,4 @@
RC(0,0) RC(0,1) RC(0,2) RC(0,3) RC(0,4) RC(0,5) RC(0,6) RC(0,7) RC(0,8) RC(0,9) RC(0,10) RC(0,11) RC(0,12) RC(0,13) RC(0,14) RC(0,15) RC(0,16) RC(0,17)
>;
};
};
};

View File

@@ -0,0 +1,12 @@
if SHIELD_TESTER_RPI_PICO
config ZMK_KEYBOARD_NAME
default "ZMK Tester"
config ZMK_BLE
def_bool n
config SETTINGS
def_bool n
endif

View File

@@ -0,0 +1,2 @@
config SHIELD_TESTER_RPI_PICO
def_bool $(shields_list_contains,tester_rpi_pico)

View File

@@ -0,0 +1,95 @@
/*
* Copyright (c) 2025 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#include <physical_layouts.dtsi>
/ {
tester_position_map {
compatible = "zmk,physical-layout-position-map";
complete;
pinout_map: pinout_positions {
physical-layout = <&physical_layout0>;
positions = <0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25>;
};
inline_map: single_row_positions {
physical-layout = <&physical_layout1>;
positions = <0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25>;
};
};
physical_layout0: physical_layout_0 {
compatible = "zmk,physical-layout";
display-name = "Rpi Pico Pinout";
kscan = <&kscan0>;
transform = <&matrix_transform0>;
keys // w h x y rot rx ry
= <&key_physical_attrs 100 100 0 0 0 0 0>
, <&key_physical_attrs 100 100 0 100 0 0 0>
, <&key_physical_attrs 100 100 0 300 0 0 0>
, <&key_physical_attrs 100 100 0 400 0 0 0>
, <&key_physical_attrs 100 100 0 500 0 0 0>
, <&key_physical_attrs 100 100 0 600 0 0 0>
, <&key_physical_attrs 100 100 0 800 0 0 0>
, <&key_physical_attrs 100 100 0 900 0 0 0>
, <&key_physical_attrs 100 100 0 1000 0 0 0>
, <&key_physical_attrs 100 100 0 1100 0 0 0>
, <&key_physical_attrs 100 100 0 1300 0 0 0>
, <&key_physical_attrs 100 100 0 1400 0 0 0>
, <&key_physical_attrs 100 100 0 1500 0 0 0>
, <&key_physical_attrs 100 100 0 1600 0 0 0>
, <&key_physical_attrs 100 100 0 1800 0 0 0>
, <&key_physical_attrs 100 100 0 1900 0 0 0>
, <&key_physical_attrs 100 100 600 1900 0 0 0>
, <&key_physical_attrs 100 100 600 1800 0 0 0>
, <&key_physical_attrs 100 100 600 1600 0 0 0>
, <&key_physical_attrs 100 100 600 1500 0 0 0>
, <&key_physical_attrs 100 100 600 1400 0 0 0>
, <&key_physical_attrs 100 100 600 1300 0 0 0>
, <&key_physical_attrs 100 100 600 1100 0 0 0>
, <&key_physical_attrs 100 100 600 900 0 0 0>
, <&key_physical_attrs 100 100 600 800 0 0 0>
, <&key_physical_attrs 100 100 600 600 0 0 0>
;
};
physical_layout1: physical_layout_1 {
compatible = "zmk,physical-layout";
display-name = "Single Row";
// Single row of eighteen "keys", in ascending "Arduino" order.
keys // w h x y rot rx ry
= <&key_physical_attrs 100 100 0 0 0 0 0>
, <&key_physical_attrs 100 100 100 0 0 0 0>
, <&key_physical_attrs 100 100 200 0 0 0 0>
, <&key_physical_attrs 100 100 300 0 0 0 0>
, <&key_physical_attrs 100 100 400 0 0 0 0>
, <&key_physical_attrs 100 100 500 0 0 0 0>
, <&key_physical_attrs 100 100 600 0 0 0 0>
, <&key_physical_attrs 100 100 700 0 0 0 0>
, <&key_physical_attrs 100 100 800 0 0 0 0>
, <&key_physical_attrs 100 100 900 0 0 0 0>
, <&key_physical_attrs 100 100 1000 0 0 0 0>
, <&key_physical_attrs 100 100 1100 0 0 0 0>
, <&key_physical_attrs 100 100 1200 0 0 0 0>
, <&key_physical_attrs 100 100 1300 0 0 0 0>
, <&key_physical_attrs 100 100 1400 0 0 0 0>
, <&key_physical_attrs 100 100 1500 0 0 0 0>
, <&key_physical_attrs 100 100 1600 0 0 0 0>
, <&key_physical_attrs 100 100 1700 0 0 0 0>
, <&key_physical_attrs 100 100 1800 0 0 0 0>
, <&key_physical_attrs 100 100 1900 0 0 0 0>
, <&key_physical_attrs 100 100 2000 0 0 0 0>
, <&key_physical_attrs 100 100 2100 0 0 0 0>
, <&key_physical_attrs 100 100 2200 0 0 0 0>
, <&key_physical_attrs 100 100 2300 0 0 0 0>
, <&key_physical_attrs 100 100 2400 0 0 0 0>
, <&key_physical_attrs 100 100 2500 0 0 0 0>
;
};
};

View File

@@ -0,0 +1,77 @@
#include <behaviors.dtsi>
#include <dt-bindings/zmk/keys.h>
#define PIN_MACRO(name, pin) \
/ { \
macros { \
name: name { \
compatible = "zmk,behavior-macro"; \
wait-ms = <5>; \
tap-ms = <5>; \
#binding-cells = <0>; \
bindings = <&kp P &kp I &kp N &kp SPACE>, pin, <&kp ENTER>; \
}; \
}; \
};
PIN_MACRO(pin0, <&kp N0>)
PIN_MACRO(pin1, <&kp N1>)
PIN_MACRO(pin2, <&kp N2>)
PIN_MACRO(pin3, <&kp N3>)
PIN_MACRO(pin4, <&kp N4>)
PIN_MACRO(pin5, <&kp N5>)
PIN_MACRO(pin6, <&kp N6>)
PIN_MACRO(pin7, <&kp N7>)
PIN_MACRO(pin8, <&kp N8>)
PIN_MACRO(pin9, <&kp N9>)
PIN_MACRO(pin10, <&kp N1 &kp N0>)
PIN_MACRO(pin11, <&kp N1 &kp N1>)
PIN_MACRO(pin12, <&kp N1 &kp N2>)
PIN_MACRO(pin13, <&kp N1 &kp N3>)
PIN_MACRO(pin14, <&kp N1 &kp N4>)
PIN_MACRO(pin15, <&kp N1 &kp N5>)
PIN_MACRO(pin16, <&kp N1 &kp N6>)
PIN_MACRO(pin17, <&kp N1 &kp N7>)
PIN_MACRO(pin18, <&kp N1 &kp N8>)
PIN_MACRO(pin19, <&kp N1 &kp N9>)
PIN_MACRO(pin20, <&kp N2 &kp N0>)
PIN_MACRO(pin21, <&kp N2 &kp N1>)
PIN_MACRO(pin22, <&kp N2 &kp N2>)
PIN_MACRO(pin26, <&kp N2 &kp N6>)
PIN_MACRO(pin27, <&kp N2 &kp N7>)
PIN_MACRO(pin28, <&kp N2 &kp N8>)
/ {
keymap {
compatible = "zmk,keymap";
default_layer {
bindings = <&pin0
&pin1
&pin2
&pin3
&pin4
&pin5
&pin6
&pin7
&pin8
&pin9
&pin10
&pin11
&pin12
&pin13
&pin14
&pin15
&pin16
&pin17
&pin18
&pin19
&pin20
&pin21
&pin22
&pin26
&pin27
&pin28>;
};
};
};

View File

@@ -0,0 +1,63 @@
#include <dt-bindings/zmk/matrix_transform.h>
#include "tester_rpi_pico-layouts.dtsi"
&physical_layout0 {
transform = <&transform0>;
};
&physical_layout1 {
transform = <&transform0>;
};
/ {
chosen {
zmk,kscan = &kscan0;
zmk,physical-layout = &physical_layout0;
};
kscan0: kscan {
compatible = "zmk,kscan-gpio-direct";
wakeup-source;
debounce-press-ms = <10>;
debounce-release-ms = <10>;
input-gpios
= <&pico_header 0 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>
, <&pico_header 1 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>
, <&pico_header 2 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>
, <&pico_header 3 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>
, <&pico_header 4 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>
, <&pico_header 5 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>
, <&pico_header 6 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>
, <&pico_header 7 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>
, <&pico_header 8 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>
, <&pico_header 9 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>
, <&pico_header 10 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>
, <&pico_header 11 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>
, <&pico_header 12 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>
, <&pico_header 13 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>
, <&pico_header 14 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>
, <&pico_header 15 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>
, <&pico_header 16 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>
, <&pico_header 17 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>
, <&pico_header 18 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>
, <&pico_header 19 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>
, <&pico_header 20 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>
, <&pico_header 21 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>
, <&pico_header 22 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>
, <&pico_header 26 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>
, <&pico_header 27 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>
, <&pico_header 28 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>
;
};
transform0: keymap_transform {
compatible = "zmk,matrix-transform";
columns = <26>;
rows = <1>;
map = <
RC(0,0) RC(0,1) RC(0,2) RC(0,3) RC(0,4) RC(0,5) RC(0,6) RC(0,7) RC(0,8) RC(0,9)
RC(0,10) RC(0,11) RC(0,12) RC(0,13) RC(0,14) RC(0,15) RC(0,16) RC(0,17) RC(0,18) RC(0,19)
RC(0,20) RC(0,21) RC(0,22) RC(0,23) RC(0,24) RC(0,25)
>;
};
};

View File

@@ -0,0 +1,6 @@
file_format: "1"
id: tester_rpi_pico
name: TesterRpiPico
type: shield
url: https://zmk.dev/docs/troubleshooting/hardware-issues
requires: [rpi_pico]

View File

@@ -0,0 +1,66 @@
/*
* Copyright (c) 2025 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#include <physical_layouts.dtsi>
// The tester might not have any physical keys at all, so these
// physical layouts are pretty much imaginary.
/ {
tester_position_map {
compatible = "zmk,physical-layout-position-map";
complete;
pinout_map: pinout_positions {
physical-layout = <&physical_layout0>;
positions = <0 1 2 3 4 5 6 7 8 9 10>;
};
inline_map: single_row_positions {
physical-layout = <&physical_layout1>;
positions = <0 1 2 3 4 5 6 7 8 9 10>;
};
};
physical_layout0: physical_layout_0 {
compatible = "zmk,physical-layout";
display-name = "XIAO Pinout";
// Map key positions to the XIAO pinout.
keys // w h x y rot rx ry
= <&key_physical_attrs 100 100 0 0 0 0 0>
, <&key_physical_attrs 100 100 0 100 0 0 0>
, <&key_physical_attrs 100 100 0 200 0 0 0>
, <&key_physical_attrs 100 100 0 300 0 0 0>
, <&key_physical_attrs 100 100 0 400 0 0 0>
, <&key_physical_attrs 100 100 0 500 0 0 0>
, <&key_physical_attrs 100 100 0 600 0 0 0>
, <&key_physical_attrs 100 100 600 600 0 0 0>
, <&key_physical_attrs 100 100 600 500 0 0 0>
, <&key_physical_attrs 100 100 600 400 0 0 0>
, <&key_physical_attrs 100 100 600 300 0 0 0>
;
};
physical_layout1: physical_layout_1 {
compatible = "zmk,physical-layout";
display-name = "Single Row";
// Single row of eleven "keys".
keys // w h x y rot rx ry
= <&key_physical_attrs 100 100 0 0 0 0 0>
, <&key_physical_attrs 100 100 100 0 0 0 0>
, <&key_physical_attrs 100 100 200 0 0 0 0>
, <&key_physical_attrs 100 100 300 0 0 0 0>
, <&key_physical_attrs 100 100 400 0 0 0 0>
, <&key_physical_attrs 100 100 500 0 0 0 0>
, <&key_physical_attrs 100 100 600 0 0 0 0>
, <&key_physical_attrs 100 100 700 0 0 0 0>
, <&key_physical_attrs 100 100 800 0 0 0 0>
, <&key_physical_attrs 100 100 900 0 0 0 0>
, <&key_physical_attrs 100 100 1000 0 0 0 0>
;
};
};

View File

@@ -1,9 +1,18 @@
#include <dt-bindings/zmk/matrix_transform.h>
#include "tester_xiao-layouts.dtsi"
&physical_layout0 {
transform = <&transform0>;
};
&physical_layout1 {
transform = <&transform0>;
};
/ {
chosen {
zmk,kscan = &kscan0;
zmk,matrix-transform = &transform0;
zmk,physical-layout = &physical_layout0;
};
kscan0: kscan {
@@ -34,4 +43,4 @@
RC(0,0) RC(0,1) RC(0,2) RC(0,3) RC(0,4) RC(0,5) RC(0,6) RC(0,7) RC(0,8) RC(0,9) RC(0,10)
>;
};
};
};

View File

@@ -11,11 +11,20 @@ left_encoder: &encoder {
status = "disabled";
};
&arduino_serial {
status = "okay";
};
/ {
chosen {
zmk,physical-layout = &split_matrix_physical_layout;
};
wired_split {
compatible = "zmk,wired-split";
device = <&arduino_serial>;
};
split_matrix_transform: split_matrix_transform {
compatible = "zmk,matrix-transform";
rows = <4>;

View File

@@ -14,6 +14,7 @@
mkp: mouse_key_press {
compatible = "zmk,behavior-mouse-key-press";
#binding-cells = <1>;
display-name = "Mouse Key Press";
};
};

View File

@@ -25,4 +25,3 @@ child-binding:
type: boolean
layers:
type: array
default: [-1]

View File

@@ -0,0 +1,27 @@
# Copyright (c) 2025 The ZMK Contributors
# SPDX-License-Identifier: MIT
description: |
Complete specification of wired split connection
compatible: "zmk,wired-split"
properties:
device:
type: phandle
required: true
description: The UART device for wired split communication
detect-gpios:
type: phandle-array
description: |
If your split includes support for an extra GPIO to detect presence of the split cable, set
this to the GPIO pin used to detect the connection.
half-duplex:
type: boolean
description: "Experimental: Enable half-duplex protocol mode"
dir-gpios:
type: phandle-array
description: "Experimental: Set the communication direction. Used for RS-422 style comms."

View File

@@ -0,0 +1,9 @@
/*
* Copyright (c) 2025 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#include <zephyr/linker/linker-defs.h>
ITERABLE_SECTION_ROM(zmk_split_transport_central, 4)

View File

@@ -0,0 +1,9 @@
/*
* Copyright (c) 2025 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#include <zephyr/linker/linker-defs.h>
ITERABLE_SECTION_ROM(zmk_split_transport_peripheral, 4)

View File

@@ -10,8 +10,7 @@
#include <zmk/ble/profile.h>
#define ZMK_BLE_IS_CENTRAL \
(IS_ENABLED(CONFIG_ZMK_SPLIT) && IS_ENABLED(CONFIG_ZMK_BLE) && \
IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL))
(IS_ENABLED(CONFIG_ZMK_SPLIT_BLE) && IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL))
#if ZMK_BLE_IS_CENTRAL
#define ZMK_BLE_PROFILE_COUNT (CONFIG_BT_MAX_PAIRED - CONFIG_ZMK_SPLIT_BLE_CENTRAL_PERIPHERALS)
@@ -29,10 +28,14 @@ int zmk_ble_prof_disconnect(uint8_t index);
int zmk_ble_active_profile_index(void);
int zmk_ble_profile_index(const bt_addr_le_t *addr);
bt_addr_le_t *zmk_ble_profile_address(uint8_t index);
bt_addr_le_t *zmk_ble_active_profile_addr(void);
struct bt_conn *zmk_ble_active_profile_conn(void);
bool zmk_ble_profile_is_connected(uint8_t index);
bool zmk_ble_profile_is_open(uint8_t index);
bool zmk_ble_active_profile_is_open(void);
bool zmk_ble_active_profile_is_connected(void);
char *zmk_ble_active_profile_name(void);

View File

@@ -208,7 +208,7 @@ static const uint8_t zmk_hid_report_desc[] = {
HID_USAGE_PAGE(HID_USAGE_GEN_DESKTOP),
HID_USAGE(HID_USAGE_GD_X),
HID_USAGE(HID_USAGE_GD_Y),
HID_LOGICAL_MIN16(0xFF, -0x7F),
HID_LOGICAL_MIN16(0x00, 0x80),
HID_LOGICAL_MAX16(0xFF, 0x7F),
HID_REPORT_SIZE(0x10),
HID_REPORT_COUNT(0x02),
@@ -226,7 +226,7 @@ static const uint8_t zmk_hid_report_desc[] = {
HID_FEATURE(ZMK_HID_MAIN_VAL_DATA | ZMK_HID_MAIN_VAL_VAR | ZMK_HID_MAIN_VAL_ABS),
#endif // IS_ENABLED(CONFIG_ZMK_POINTING_SMOOTH_SCROLLING)
HID_USAGE(HID_USAGE_GD_WHEEL),
HID_LOGICAL_MIN16(0xFF, -0x7F),
HID_LOGICAL_MIN16(0x00, 0x80),
HID_LOGICAL_MAX16(0xFF, 0x7F),
HID_PHYSICAL_MIN8(0x00),
HID_PHYSICAL_MAX8(0x00),
@@ -242,7 +242,7 @@ static const uint8_t zmk_hid_report_desc[] = {
#endif // IS_ENABLED(CONFIG_ZMK_POINTING_SMOOTH_SCROLLING)
HID_USAGE_PAGE(HID_USAGE_CONSUMER),
HID_USAGE16_SINGLE(HID_USAGE_CONSUMER_AC_PAN),
HID_LOGICAL_MIN16(0xFF, -0x7F),
HID_LOGICAL_MIN16(0x00, 0x80),
HID_LOGICAL_MAX16(0xFF, 0x7F),
HID_PHYSICAL_MIN8(0x00),
HID_PHYSICAL_MAX8(0x00),
@@ -289,8 +289,6 @@ struct zmk_hid_keyboard_report {
struct zmk_hid_keyboard_report_body body;
} __packed;
#if IS_ENABLED(CONFIG_ZMK_HID_INDICATORS)
struct zmk_hid_led_report_body {
uint8_t leds;
} __packed;
@@ -300,8 +298,6 @@ struct zmk_hid_led_report {
struct zmk_hid_led_report_body body;
} __packed;
#endif // IS_ENABLED(CONFIG_ZMK_HID_INDICATORS)
struct zmk_hid_consumer_report_body {
#if IS_ENABLED(CONFIG_ZMK_HID_CONSUMER_REPORT_USAGES_BASIC)
uint8_t keys[CONFIG_ZMK_HID_CONSUMER_REPORT_SIZE];
@@ -377,9 +373,9 @@ int zmk_hid_mouse_button_release(zmk_mouse_button_t button);
int zmk_hid_mouse_buttons_press(zmk_mouse_button_flags_t buttons);
int zmk_hid_mouse_buttons_release(zmk_mouse_button_flags_t buttons);
void zmk_hid_mouse_movement_set(int16_t x, int16_t y);
void zmk_hid_mouse_scroll_set(int8_t x, int8_t y);
void zmk_hid_mouse_scroll_set(int16_t x, int16_t y);
void zmk_hid_mouse_movement_update(int16_t x, int16_t y);
void zmk_hid_mouse_scroll_update(int8_t x, int8_t y);
void zmk_hid_mouse_scroll_update(int16_t x, int16_t y);
void zmk_hid_mouse_clear(void);
#endif // IS_ENABLED(CONFIG_ZMK_POINTING)

View File

@@ -7,13 +7,20 @@
#pragma once
#include <zmk/events/position_state_changed.h>
#include <zephyr/sys/util.h>
#include <zephyr/devicetree.h>
#define ZMK_KEYMAP_LAYERS_FOREACH(_fn) \
COND_CODE_1(IS_ENABLED(CONFIG_ZMK_STUDIO), (DT_FOREACH_CHILD(DT_INST(0, zmk_keymap), _fn)), \
(DT_FOREACH_CHILD_STATUS_OKAY(DT_INST(0, zmk_keymap), _fn)))
#define ZMK_KEYMAP_LAYERS_FOREACH_SEP(_fn, _sep) \
COND_CODE_1(IS_ENABLED(CONFIG_ZMK_STUDIO), \
(DT_FOREACH_CHILD_SEP(DT_INST(0, zmk_keymap), _fn, _sep)), \
(DT_FOREACH_CHILD_STATUS_OKAY_SEP(DT_INST(0, zmk_keymap), _fn, _sep)))
#define ZMK_LAYER_CHILD_LEN_PLUS_ONE(node) 1 +
#define ZMK_KEYMAP_LAYERS_LEN \
(COND_CODE_1( \
IS_ENABLED(CONFIG_ZMK_STUDIO), \
(DT_FOREACH_CHILD(DT_INST(0, zmk_keymap), ZMK_LAYER_CHILD_LEN_PLUS_ONE)), \
(DT_FOREACH_CHILD_STATUS_OKAY(DT_INST(0, zmk_keymap), ZMK_LAYER_CHILD_LEN_PLUS_ONE))) 0)
#define ZMK_KEYMAP_LAYERS_LEN (ZMK_KEYMAP_LAYERS_FOREACH(ZMK_LAYER_CHILD_LEN_PLUS_ONE) 0)
/**
* @brief A layer ID is a stable identifier to refer to a layer, regardless of ordering.

View File

@@ -1,24 +0,0 @@
#pragma once
#include <zephyr/bluetooth/addr.h>
#include <zmk/behavior.h>
#if IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS)
#include <zmk/hid_indicators_types.h>
#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS)
int zmk_split_bt_invoke_behavior(uint8_t source, struct zmk_behavior_binding *binding,
struct zmk_behavior_binding_event event, bool state);
#if IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS)
int zmk_split_bt_update_hid_indicator(zmk_hid_indicators_t indicators);
#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS)
#if IS_ENABLED(CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING)
int zmk_split_get_peripheral_battery_level(uint8_t source, uint8_t *level);
#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING)

View File

@@ -37,11 +37,3 @@ struct zmk_split_input_event_payload {
uint32_t value;
uint8_t sync;
} __packed;
int zmk_split_bt_position_pressed(uint8_t position);
int zmk_split_bt_position_released(uint8_t position);
int zmk_split_bt_sensor_triggered(uint8_t sensor_index,
const struct zmk_sensor_channel_data channel_data[],
size_t channel_data_size);
int zmk_split_bt_report_input(uint8_t reg, uint8_t type, uint16_t code, int32_t value, bool sync);

View File

@@ -0,0 +1,48 @@
/*
* Copyright (c) 2025 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#pragma once
#include <zephyr/bluetooth/addr.h>
#include <zmk/behavior.h>
#if IS_ENABLED(CONFIG_ZMK_SPLIT_BLE)
#include <zmk/ble.h>
#define BLE_PERIPHERAL_COUNT ZMK_SPLIT_BLE_PERIPHERAL_COUNT
#else
#define BLE_PERIPHERAL_COUNT 0
#endif
#if IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED)
#define WIRED_PERIPHERAL_COUNT 1
#else
#define WIRED_PERIPHERAL_COUNT 0
#endif
#define ZMK_SPLIT_CENTRAL_PERIPHERAL_COUNT MAX(BLE_PERIPHERAL_COUNT, WIRED_PERIPHERAL_COUNT)
#if IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS)
#include <zmk/hid_indicators_types.h>
#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS)
int zmk_split_central_invoke_behavior(uint8_t source, struct zmk_behavior_binding *binding,
struct zmk_behavior_binding_event event, bool state);
#if IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS)
int zmk_split_central_update_hid_indicator(zmk_hid_indicators_t indicators);
#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS)
#if IS_ENABLED(CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING)
int zmk_split_central_get_peripheral_battery_level(uint8_t source, uint8_t *level);
#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING)

View File

@@ -0,0 +1,11 @@
/*
* Copyright (c) 2025 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#pragma once
#include <zmk/split/transport/types.h>
int zmk_split_peripheral_report_event(const struct zmk_split_transport_peripheral_event *event);

View File

@@ -0,0 +1,44 @@
/*
* Copyright (c) 2025 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#pragma once
#include <zephyr/types.h>
#include <zmk/split/transport/types.h>
struct zmk_split_transport_central;
typedef int (*zmk_split_transport_central_status_changed_cb_t)(
const struct zmk_split_transport_central *transport, struct zmk_split_transport_status status);
typedef int (*zmk_split_transport_central_send_command_t)(
uint8_t source, struct zmk_split_transport_central_command cmd);
typedef int (*zmk_split_transport_central_get_available_source_ids_t)(uint8_t *sources);
typedef int (*zmk_split_transport_central_set_status_callback_t)(
zmk_split_transport_central_status_changed_cb_t cb);
struct zmk_split_transport_central_api {
zmk_split_transport_central_send_command_t send_command;
zmk_split_transport_central_get_available_source_ids_t get_available_source_ids;
zmk_split_transport_set_enabled_t set_enabled;
zmk_split_transport_get_status_t get_status;
zmk_split_transport_central_set_status_callback_t set_status_callback;
};
struct zmk_split_transport_central {
const struct zmk_split_transport_central_api *api;
};
int zmk_split_transport_central_peripheral_event_handler(
const struct zmk_split_transport_central *transport, uint8_t source,
struct zmk_split_transport_peripheral_event ev);
#define ZMK_SPLIT_TRANSPORT_CENTRAL_REGISTER(name, _api, priority) \
STRUCT_SECTION_ITERABLE_NAMED(zmk_split_transport_central, _CONCAT(priority, _##name), \
name) = { \
.api = _api, \
};

View File

@@ -0,0 +1,43 @@
/*
* Copyright (c) 2025 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#pragma once
#include <zephyr/types.h>
#include <zmk/split/transport/types.h>
struct zmk_split_transport_peripheral;
typedef int (*zmk_split_transport_peripheral_status_changed_cb_t)(
const struct zmk_split_transport_peripheral *transport,
struct zmk_split_transport_status status);
typedef int (*zmk_split_transport_peripheral_report_event_callback_t)(
const struct zmk_split_transport_peripheral_event *event);
typedef int (*zmk_split_transport_peripheral_set_status_callback_t)(
zmk_split_transport_peripheral_status_changed_cb_t cb);
struct zmk_split_transport_peripheral_api {
zmk_split_transport_peripheral_report_event_callback_t report_event;
zmk_split_transport_set_enabled_t set_enabled;
zmk_split_transport_get_status_t get_status;
zmk_split_transport_peripheral_set_status_callback_t set_status_callback;
};
struct zmk_split_transport_peripheral {
const struct zmk_split_transport_peripheral_api *api;
};
int zmk_split_transport_peripheral_command_handler(
const struct zmk_split_transport_peripheral *transport,
struct zmk_split_transport_central_command cmd);
#define ZMK_SPLIT_TRANSPORT_PERIPHERAL_REGISTER(name, _api, priority) \
STRUCT_SECTION_ITERABLE_NAMED(zmk_split_transport_peripheral, _CONCAT(priority, _##name), \
name) = { \
.api = _api, \
};

View File

@@ -0,0 +1,91 @@
/*
* Copyright (c) 2025 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#pragma once
#include <zmk/hid_indicators_types.h>
#include <zmk/sensors.h>
#include <zephyr/sys/util.h>
enum zmk_split_transport_connections_status {
ZMK_SPLIT_TRANSPORT_CONNECTIONS_STATUS_DISCONNECTED = 0,
ZMK_SPLIT_TRANSPORT_CONNECTIONS_STATUS_SOME_CONNECTED,
ZMK_SPLIT_TRANSPORT_CONNECTIONS_STATUS_ALL_CONNECTED,
};
struct zmk_split_transport_status {
bool available;
bool enabled;
enum zmk_split_transport_connections_status connections;
};
typedef struct zmk_split_transport_status (*zmk_split_transport_get_status_t)(void);
typedef int (*zmk_split_transport_set_enabled_t)(bool enabled);
enum zmk_split_transport_peripheral_event_type {
ZMK_SPLIT_TRANSPORT_PERIPHERAL_EVENT_TYPE_KEY_POSITION_EVENT,
ZMK_SPLIT_TRANSPORT_PERIPHERAL_EVENT_TYPE_SENSOR_EVENT,
ZMK_SPLIT_TRANSPORT_PERIPHERAL_EVENT_TYPE_INPUT_EVENT,
ZMK_SPLIT_TRANSPORT_PERIPHERAL_EVENT_TYPE_BATTERY_EVENT,
};
struct zmk_split_transport_peripheral_event {
enum zmk_split_transport_peripheral_event_type type;
union {
struct {
uint8_t position;
uint8_t pressed;
} key_position_event;
struct {
struct zmk_sensor_channel_data channel_data;
uint8_t sensor_index;
} sensor_event;
struct {
uint8_t reg;
uint8_t sync;
uint8_t type;
uint16_t code;
int32_t value;
} input_event;
struct {
uint8_t level;
} battery_event;
} data;
} __packed;
enum zmk_split_transport_central_command_type {
ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_POLL_EVENTS,
ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_INVOKE_BEHAVIOR,
ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_SET_PHYSICAL_LAYOUT,
ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_SET_HID_INDICATORS,
} __packed;
struct zmk_split_transport_central_command {
enum zmk_split_transport_central_command_type type;
union {
struct {
char behavior_dev[16];
uint32_t param1, param2;
uint32_t position;
uint8_t event_source;
uint8_t state;
} invoke_behavior;
struct {
uint8_t layout_idx;
} set_physical_layout;
struct {
zmk_hid_indicators_t indicators;
} set_hid_indicators;
} data;
} __packed;

View File

@@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
# Copyright (c) 2023 The ZMK Contributors
# SPDX-License-Identifier: MIT

View File

@@ -93,7 +93,18 @@ static int zmk_battery_update(const struct device *battery) {
if (last_state_of_charge != state_of_charge.val1) {
last_state_of_charge = state_of_charge.val1;
rc = raise_zmk_battery_state_changed(
(struct zmk_battery_state_changed){.state_of_charge = last_state_of_charge});
if (rc != 0) {
LOG_ERR("Failed to raise battery state changed event: %d", rc);
return rc;
}
}
#if IS_ENABLED(CONFIG_BT_BAS)
if (bt_bas_get_battery_level() != last_state_of_charge) {
LOG_DBG("Setting BAS GATT battery level to %d.", last_state_of_charge);
rc = bt_bas_set_battery_level(last_state_of_charge);
@@ -102,10 +113,8 @@ static int zmk_battery_update(const struct device *battery) {
LOG_WRN("Failed to set BAS GATT battery level (err %d)", rc);
return rc;
}
#endif
rc = raise_zmk_battery_state_changed(
(struct zmk_battery_state_changed){.state_of_charge = last_state_of_charge});
}
#endif
return rc;
}

View File

@@ -17,9 +17,8 @@
#endif
#include <zmk/ble.h>
#if ZMK_BLE_IS_CENTRAL
#include <zmk/split/bluetooth/central.h>
#if IS_ENABLED(CONFIG_ZMK_SPLIT) && IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL)
#include <zmk/split/central.h>
#endif
#include <drivers/behavior.h>
@@ -95,19 +94,19 @@ int zmk_behavior_invoke_binding(const struct zmk_behavior_binding *src_binding,
case BEHAVIOR_LOCALITY_CENTRAL:
return invoke_locally(&binding, event, pressed);
case BEHAVIOR_LOCALITY_EVENT_SOURCE:
#if ZMK_BLE_IS_CENTRAL // source is a member of event because CONFIG_ZMK_SPLIT is enabled
#if IS_ENABLED(CONFIG_ZMK_SPLIT) && IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL)
if (event.source == ZMK_POSITION_STATE_CHANGE_SOURCE_LOCAL) {
return invoke_locally(&binding, event, pressed);
} else {
return zmk_split_bt_invoke_behavior(event.source, &binding, event, pressed);
return zmk_split_central_invoke_behavior(event.source, &binding, event, pressed);
}
#else
return invoke_locally(&binding, event, pressed);
#endif
case BEHAVIOR_LOCALITY_GLOBAL:
#if ZMK_BLE_IS_CENTRAL
for (int i = 0; i < ZMK_SPLIT_BLE_PERIPHERAL_COUNT; i++) {
zmk_split_bt_invoke_behavior(i, &binding, event, pressed);
#if IS_ENABLED(CONFIG_ZMK_SPLIT) && IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL)
for (int i = 0; i < ZMK_SPLIT_CENTRAL_PERIPHERAL_COUNT; i++) {
zmk_split_central_invoke_behavior(i, &binding, event, pressed);
}
#endif
return invoke_locally(&binding, event, pressed);

View File

@@ -84,7 +84,8 @@ static int caps_word_keycode_state_changed_listener(const zmk_event_t *eh);
ZMK_LISTENER(behavior_caps_word, caps_word_keycode_state_changed_listener);
ZMK_SUBSCRIPTION(behavior_caps_word, zmk_keycode_state_changed);
static const struct device *devs[] = {DT_INST_FOREACH_STATUS_OKAY(DEVICE_DT_INST_GET)};
#define GET_DEV(inst) DEVICE_DT_INST_GET(inst),
static const struct device *devs[] = {DT_INST_FOREACH_STATUS_OKAY(GET_DEV)};
static bool caps_word_is_caps_includelist(const struct behavior_caps_word_config *config,
uint16_t usage_page, uint8_t usage_id,

View File

@@ -77,7 +77,8 @@ static int key_repeat_keycode_state_changed_listener(const zmk_event_t *eh);
ZMK_LISTENER(behavior_key_repeat, key_repeat_keycode_state_changed_listener);
ZMK_SUBSCRIPTION(behavior_key_repeat, zmk_keycode_state_changed);
static const struct device *devs[] = {DT_INST_FOREACH_STATUS_OKAY(DEVICE_DT_INST_GET)};
#define GET_DEV(inst) DEVICE_DT_INST_GET(inst),
static const struct device *devs[] = {DT_INST_FOREACH_STATUS_OKAY(GET_DEV)};
static int key_repeat_keycode_state_changed_listener(const zmk_event_t *eh) {
struct zmk_keycode_state_changed *ev = as_zmk_keycode_state_changed(eh);

View File

@@ -129,7 +129,9 @@ static int behavior_macro_init(const struct device *dev) {
LOG_DBG("Release will resume at %d", state->release_state.start_index);
break;
} else {
// Ignore regular invokable bindings
// Mostly ignore regular invokable bindings, except they will consume macro parameters
state->release_state.param1_source = PARAM_SOURCE_BINDING;
state->release_state.param2_source = PARAM_SOURCE_BINDING;
}
}

View File

@@ -19,6 +19,27 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
#if DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT)
#if IS_ENABLED(CONFIG_ZMK_BEHAVIOR_METADATA)
static const struct behavior_parameter_value_metadata param_values[] = {
{.display_name = "MB1", .type = BEHAVIOR_PARAMETER_VALUE_TYPE_VALUE, .value = MB1},
{.display_name = "MB2", .type = BEHAVIOR_PARAMETER_VALUE_TYPE_VALUE, .value = MB2},
{.display_name = "MB3", .type = BEHAVIOR_PARAMETER_VALUE_TYPE_VALUE, .value = MB3},
{.display_name = "MB4", .type = BEHAVIOR_PARAMETER_VALUE_TYPE_VALUE, .value = MB4},
{.display_name = "MB5", .type = BEHAVIOR_PARAMETER_VALUE_TYPE_VALUE, .value = MB5}};
static const struct behavior_parameter_metadata_set param_metadata_set[] = {{
.param1_values = param_values,
.param1_values_len = ARRAY_SIZE(param_values),
}};
static const struct behavior_parameter_metadata metadata = {
.sets_len = ARRAY_SIZE(param_metadata_set),
.sets = param_metadata_set,
};
#endif
static void process_key_state(const struct device *dev, int32_t val, bool pressed) {
for (int i = 0; i < ZMK_HID_MOUSE_NUM_BUTTONS; i++) {
if (val & BIT(i)) {
@@ -47,7 +68,12 @@ static int on_keymap_binding_released(struct zmk_behavior_binding *binding,
}
static const struct behavior_driver_api behavior_mouse_key_press_driver_api = {
.binding_pressed = on_keymap_binding_pressed, .binding_released = on_keymap_binding_released};
.binding_pressed = on_keymap_binding_pressed,
.binding_released = on_keymap_binding_released,
#if IS_ENABLED(CONFIG_ZMK_BEHAVIOR_METADATA)
.parameter_metadata = &metadata,
#endif // IS_ENABLED(CONFIG_ZMK_BEHAVIOR_METADATA)
};
#define MKP_INST(n) \
BEHAVIOR_DT_INST_DEFINE(n, NULL, NULL, NULL, NULL, POST_KERNEL, \

View File

@@ -78,7 +78,7 @@ static struct bt_data zmk_ble_ad[] = {
),
};
#if IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL)
#if IS_ENABLED(CONFIG_ZMK_SPLIT_BLE) && IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL)
static bt_addr_le_t peripheral_addrs[ZMK_SPLIT_BLE_PERIPHERAL_COUNT];
@@ -95,8 +95,13 @@ static void raise_profile_changed_event_callback(struct k_work *work) {
K_WORK_DEFINE(raise_profile_changed_event_work, raise_profile_changed_event_callback);
bool zmk_ble_active_profile_is_open(void) {
return !bt_addr_le_cmp(&profiles[active_profile].peer, BT_ADDR_LE_ANY);
bool zmk_ble_active_profile_is_open(void) { return zmk_ble_profile_is_open(active_profile); }
bool zmk_ble_profile_is_open(uint8_t index) {
if (index >= ZMK_BLE_PROFILE_COUNT) {
return false;
}
return !bt_addr_le_cmp(&profiles[index].peer, BT_ADDR_LE_ANY);
}
void set_profile_address(uint8_t index, const bt_addr_le_t *addr) {
@@ -115,9 +120,16 @@ void set_profile_address(uint8_t index, const bt_addr_le_t *addr) {
}
bool zmk_ble_active_profile_is_connected(void) {
return zmk_ble_profile_is_connected(active_profile);
}
bool zmk_ble_profile_is_connected(uint8_t index) {
if (index >= ZMK_BLE_PROFILE_COUNT) {
return false;
}
struct bt_conn *conn;
struct bt_conn_info info;
bt_addr_le_t *addr = zmk_ble_active_profile_addr();
bt_addr_le_t *addr = &profiles[index].peer;
if (!bt_addr_le_cmp(addr, BT_ADDR_LE_ANY)) {
return false;
} else if ((conn = bt_conn_lookup_addr_le(BT_ID_DEFAULT, addr)) == NULL) {
@@ -250,6 +262,13 @@ int zmk_ble_profile_index(const bt_addr_le_t *addr) {
return -ENODEV;
}
bt_addr_le_t *zmk_ble_profile_address(uint8_t index) {
if (index >= ZMK_BLE_PROFILE_COUNT) {
return (bt_addr_le_t *)(BT_ADDR_LE_NONE);
}
return &profiles[index].peer;
}
#if IS_ENABLED(CONFIG_SETTINGS)
static void ble_save_profile_work(struct k_work *work) {
settings_save_one("ble/active_profile", &active_profile, sizeof(active_profile));
@@ -357,7 +376,7 @@ int zmk_ble_set_device_name(char *name) {
return update_advertising();
}
#if IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL)
#if IS_ENABLED(CONFIG_ZMK_SPLIT_BLE) && IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL)
int zmk_ble_put_peripheral_addr(const bt_addr_le_t *addr) {
for (int i = 0; i < ZMK_SPLIT_BLE_PERIPHERAL_COUNT; i++) {
@@ -446,7 +465,7 @@ static int ble_profiles_handle_set(const char *name, size_t len, settings_read_c
return err;
}
}
#if IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL)
#if IS_ENABLED(CONFIG_ZMK_SPLIT_BLE) && IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL)
else if (settings_name_steq(name, "peripheral_addresses", &next) && next) {
if (len != sizeof(bt_addr_le_t)) {
return -EINVAL;

View File

@@ -9,6 +9,7 @@
#include <zephyr/device.h>
#include <zephyr/logging/log.h>
#include <zephyr/sys/dlist.h>
#include <zephyr/sys/util.h>
#include <zephyr/kernel.h>
#include <drivers/behavior.h>
@@ -26,53 +27,99 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
#if DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT)
#if CONFIG_ZMK_COMBO_MAX_KEYS_PER_COMBO > 0
#warning \
"CONFIG_ZMK_COMBO_MAX_KEYS_PER_COMBO is deprecated, and is auto-calculated from the devicetree now."
#endif
#if CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY > 0
#warning "CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY is deprecated, and is auto-calculated."
#endif
#define COMBOS_KEYS_BYTE_ARRAY(node_id) \
uint8_t _CONCAT(combo_prop_, node_id)[DT_PROP_LEN(node_id, key_positions)];
#define MAX_COMBO_KEYS sizeof(union {DT_INST_FOREACH_CHILD(0, COMBOS_KEYS_BYTE_ARRAY)})
struct combo_cfg {
int32_t key_positions[CONFIG_ZMK_COMBO_MAX_KEYS_PER_COMBO];
int32_t key_position_len;
struct zmk_behavior_binding behavior;
int32_t key_positions[MAX_COMBO_KEYS];
int16_t key_position_len;
int16_t require_prior_idle_ms;
int32_t timeout_ms;
int32_t require_prior_idle_ms;
uint32_t layer_mask;
struct zmk_behavior_binding behavior;
// if slow release is set, the combo releases when the last key is released.
// otherwise, the combo releases when the first key is released.
bool slow_release;
// the virtual key position is a key position outside the range used by the keyboard.
// it is necessary so hold-taps can uniquely identify a behavior.
int32_t virtual_key_position;
int32_t layers_len;
int8_t layers[];
};
struct active_combo {
const struct combo_cfg *combo;
uint16_t combo_idx;
// key_positions_pressed is filled with key_positions when the combo is pressed.
// The keys are removed from this array when they are released.
// Once this array is empty, the behavior is released.
uint32_t key_positions_pressed_count;
struct zmk_position_state_changed_event
key_positions_pressed[CONFIG_ZMK_COMBO_MAX_KEYS_PER_COMBO];
uint16_t key_positions_pressed_count;
struct zmk_position_state_changed_event key_positions_pressed[MAX_COMBO_KEYS];
};
struct combo_candidate {
const struct combo_cfg *combo;
// the time after which this behavior should be removed from candidates.
// by keeping track of when the candidate should be cleared there is no
// possibility of accidental releases.
int64_t timeout_at;
};
#define PROP_BIT_AT_IDX(n, prop, idx) BIT(DT_PROP_BY_IDX(n, prop, idx))
uint32_t pressed_keys_count = 0;
#define NODE_PROP_BITMASK(n, prop) \
COND_CODE_1(DT_NODE_HAS_PROP(n, prop), \
(DT_FOREACH_PROP_ELEM_SEP(n, prop, PROP_BIT_AT_IDX, (|))), (0))
#define GET_KEY_POSITION_MASK_PORTION(idx, n) ((NODE_PROP_BITMASK(n, key_positions) >> idx) & 0xFF)
#define COMBO_INST(n, positions) \
COND_CODE_1(IS_EQ(DT_PROP_LEN(n, key_positions), positions), \
( \
{ \
.timeout_ms = DT_PROP(n, timeout_ms), \
.require_prior_idle_ms = DT_PROP(n, require_prior_idle_ms), \
.key_positions = DT_PROP(n, key_positions), \
.key_position_len = DT_PROP_LEN(n, key_positions), \
.behavior = ZMK_KEYMAP_EXTRACT_BINDING(0, n), \
.slow_release = DT_PROP(n, slow_release), \
.layer_mask = NODE_PROP_BITMASK(n, layers), \
}, ), \
())
#define COMBO_CONFIGS_WITH_MATCHING_POSITIONS_LEN(positions, _ignore) \
DT_INST_FOREACH_CHILD_VARGS(0, COMBO_INST, positions)
// We do some magic here to generate the `combos` array by "key position length", looping
// by key position length and on each iteration, only include entries where the `key-positions`
// length matches.
// Doing so allows our bitmasks to be "shorted key positions list first" when searching for matches.
// `20` is chosen as a reasonable limit, since the theoretical maximum number of keys you might
// reasonably press simultaneously with 10 fingers is 20 keys, two keys per finger.
static const struct combo_cfg combos[] = {
LISTIFY(20, COMBO_CONFIGS_WITH_MATCHING_POSITIONS_LEN, (), 0)};
#define COMBO_ONE(n) +1
#define COMBO_CHILDREN_COUNT (0 DT_INST_FOREACH_CHILD(0, COMBO_ONE))
// We need at least 4 bytes to avoid alignment issues
#define BYTES_FOR_COMBOS_MASK DIV_ROUND_UP(COMBO_CHILDREN_COUNT, 32)
uint8_t pressed_keys_count = 0;
// set of keys pressed
struct zmk_position_state_changed_event pressed_keys[CONFIG_ZMK_COMBO_MAX_KEYS_PER_COMBO] = {};
struct zmk_position_state_changed_event pressed_keys[MAX_COMBO_KEYS] = {};
// the set of candidate combos based on the currently pressed_keys
struct combo_candidate candidates[CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY];
uint32_t candidates[BYTES_FOR_COMBOS_MASK];
// the last candidate that was completely pressed
const struct combo_cfg *fully_pressed_combo = NULL;
int16_t fully_pressed_combo = INT16_MAX;
// a lookup dict that maps a key position to all combos on that position
const struct combo_cfg *combo_lookup[ZMK_KEYMAP_LEN][CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY] = {NULL};
uint32_t combo_lookup[ZMK_KEYMAP_LEN][BYTES_FOR_COMBOS_MASK] = {};
// combos that have been activated and still have (some) keys pressed
// this array is always contiguous from 0.
struct active_combo active_combos[CONFIG_ZMK_COMBO_MAX_PRESSED_COMBOS] = {NULL};
int active_combo_count = 0;
struct active_combo active_combos[CONFIG_ZMK_COMBO_MAX_PRESSED_COMBOS] = {};
uint8_t active_combo_count = 0;
struct k_work_delayable timeout_task;
int64_t timeout_task_timeout_at;
@@ -90,52 +137,22 @@ static void store_last_tapped(int64_t timestamp) {
// Store the combo key pointer in the combos array, one pointer for each key position
// The combos are sorted shortest-first, then by virtual-key-position.
static int initialize_combo(const struct combo_cfg *new_combo) {
for (int i = 0; i < new_combo->key_position_len; i++) {
int32_t position = new_combo->key_positions[i];
if (position >= ZMK_KEYMAP_LEN) {
LOG_ERR("Unable to initialize combo, key position %d does not exist", position);
return -EINVAL;
}
static int initialize_combo(size_t index) {
const struct combo_cfg *new_combo = &combos[index];
const struct combo_cfg *insert_combo = new_combo;
bool set = false;
for (int j = 0; j < CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY; j++) {
const struct combo_cfg *combo_at_j = combo_lookup[position][j];
if (combo_at_j == NULL) {
combo_lookup[position][j] = insert_combo;
set = true;
break;
}
if (combo_at_j->key_position_len < insert_combo->key_position_len ||
(combo_at_j->key_position_len == insert_combo->key_position_len &&
combo_at_j->virtual_key_position < insert_combo->virtual_key_position)) {
continue;
}
// put insert_combo in this spot, move all other combos up.
combo_lookup[position][j] = insert_combo;
insert_combo = combo_at_j;
}
if (!set) {
LOG_ERR("Too many combos for key position %d, CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY %d.",
position, CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY);
return -ENOMEM;
}
for (size_t kp = 0; kp < new_combo->key_position_len; kp++) {
sys_bitfield_set_bit((mem_addr_t)&combo_lookup[new_combo->key_positions[kp]], index);
}
return 0;
}
static bool combo_active_on_layer(const struct combo_cfg *combo, uint8_t layer) {
if (combo->layers[0] == -1) {
// -1 in the first layer position is global layer scope
if (!combo->layer_mask) {
return true;
}
for (int j = 0; j < combo->layers_len; j++) {
if (combo->layers[j] == layer) {
return true;
}
}
return false;
return combo->layer_mask & BIT(layer);
}
static bool is_quick_tap(const struct combo_cfg *combo, int64_t timestamp) {
@@ -145,66 +162,58 @@ static bool is_quick_tap(const struct combo_cfg *combo, int64_t timestamp) {
static int setup_candidates_for_first_keypress(int32_t position, int64_t timestamp) {
int number_of_combo_candidates = 0;
uint8_t highest_active_layer = zmk_keymap_highest_layer_active();
for (int i = 0; i < CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY; i++) {
const struct combo_cfg *combo = combo_lookup[position][i];
if (combo == NULL) {
return number_of_combo_candidates;
for (size_t i = 0; i < ARRAY_SIZE(combos); i++) {
if (sys_bitfield_test_bit((mem_addr_t)&combo_lookup[position], i)) {
const struct combo_cfg *combo = &combos[i];
if (combo_active_on_layer(combo, highest_active_layer) &&
!is_quick_tap(combo, timestamp)) {
sys_bitfield_set_bit((mem_addr_t)&candidates, i);
number_of_combo_candidates++;
}
// LOG_DBG("combo timeout %d %d %d", position, i, candidates[i].timeout_at);
}
if (combo_active_on_layer(combo, highest_active_layer) && !is_quick_tap(combo, timestamp)) {
candidates[number_of_combo_candidates].combo = combo;
candidates[number_of_combo_candidates].timeout_at = timestamp + combo->timeout_ms;
number_of_combo_candidates++;
}
// LOG_DBG("combo timeout %d %d %d", position, i, candidates[i].timeout_at);
}
return number_of_combo_candidates;
}
static inline uint8_t zero_one_or_more_bits(uint32_t field) {
if (field == 0) {
return 0;
}
if ((field & (field - 1)) == 0) {
return 1;
}
return 2;
}
static int filter_candidates(int32_t position) {
// this code iterates over candidates and the lookup together to filter in O(n)
// assuming they are both sorted on key_position_len, virtual_key_position
int matches = 0, lookup_idx = 0, candidate_idx = 0;
while (lookup_idx < CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY &&
candidate_idx < CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY) {
const struct combo_cfg *candidate = candidates[candidate_idx].combo;
const struct combo_cfg *lookup = combo_lookup[position][lookup_idx];
if (candidate == NULL || lookup == NULL) {
break;
}
if (candidate->virtual_key_position == lookup->virtual_key_position) {
candidates[matches] = candidates[candidate_idx];
matches++;
candidate_idx++;
lookup_idx++;
} else if (candidate->key_position_len > lookup->key_position_len) {
lookup_idx++;
} else if (candidate->key_position_len < lookup->key_position_len) {
candidate_idx++;
} else if (candidate->virtual_key_position > lookup->virtual_key_position) {
lookup_idx++;
} else if (candidate->virtual_key_position < lookup->virtual_key_position) {
candidate_idx++;
int matches = 0;
for (int i = 0; i < BYTES_FOR_COMBOS_MASK; i++) {
candidates[i] &= combo_lookup[position][i];
if (matches < 2) {
matches += zero_one_or_more_bits(candidates[i]);
}
}
// clear unmatched candidates
for (int i = matches; i < CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY; i++) {
candidates[i].combo = NULL;
}
// LOG_DBG("combo matches after filter %d", matches);
LOG_DBG("combo matches after filter %d", matches);
return matches;
}
static int64_t first_candidate_timeout() {
if (pressed_keys_count == 0) {
return LONG_MAX;
}
int64_t first_timeout = LONG_MAX;
for (int i = 0; i < CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY; i++) {
if (candidates[i].combo == NULL) {
break;
}
if (candidates[i].timeout_at < first_timeout) {
first_timeout = candidates[i].timeout_at;
for (int i = 0; i < ARRAY_SIZE(combos); i++) {
if (sys_bitfield_test_bit((mem_addr_t)&candidates, i)) {
first_timeout = MIN(first_timeout, combos[i].timeout_ms);
}
}
return first_timeout;
return pressed_keys[0].data.timestamp + first_timeout;
}
static inline bool candidate_is_completely_pressed(const struct combo_cfg *candidate) {
@@ -219,26 +228,17 @@ static inline bool candidate_is_completely_pressed(const struct combo_cfg *candi
static int cleanup();
static int filter_timed_out_candidates(int64_t timestamp) {
int remaining_candidates = 0;
for (int i = 0; i < CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY; i++) {
struct combo_candidate *candidate = &candidates[i];
if (candidate->combo == NULL) {
break;
}
if (candidate->timeout_at > timestamp) {
bool need_to_bubble_up = remaining_candidates != i;
if (need_to_bubble_up) {
// bubble up => reorder candidates so they're contiguous
candidates[remaining_candidates].combo = candidate->combo;
candidates[remaining_candidates].timeout_at = candidate->timeout_at;
// clear the previous location
candidates[i].combo = NULL;
candidates[i].timeout_at = 0;
}
__ASSERT(pressed_keys_count > 0, "Searching for a candidate timeout with no keys pressed");
remaining_candidates++;
} else {
candidate->combo = NULL;
int remaining_candidates = 0;
for (int i = 0; i < ARRAY_SIZE(combos); i++) {
if (sys_bitfield_test_bit((mem_addr_t)&candidates, i)) {
if (pressed_keys[0].data.timestamp + combos[i].timeout_ms > timestamp) {
remaining_candidates++;
} else {
sys_bitfield_clear_bit((mem_addr_t)&candidates, i);
}
}
}
@@ -249,18 +249,8 @@ static int filter_timed_out_candidates(int64_t timestamp) {
return remaining_candidates;
}
static int clear_candidates() {
for (int i = 0; i < CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY; i++) {
if (candidates[i].combo == NULL) {
return i;
}
candidates[i].combo = NULL;
}
return CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY;
}
static int capture_pressed_key(const struct zmk_position_state_changed *ev) {
if (pressed_keys_count == CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY) {
if (pressed_keys_count == MAX_COMBO_KEYS) {
return ZMK_EV_EVENT_BUBBLE;
}
@@ -271,7 +261,7 @@ static int capture_pressed_key(const struct zmk_position_state_changed *ev) {
const struct zmk_listener zmk_listener_combo;
static int release_pressed_keys() {
uint32_t count = pressed_keys_count;
uint8_t count = pressed_keys_count;
pressed_keys_count = 0;
for (int i = 0; i < count; i++) {
struct zmk_position_state_changed_event *ev = &pressed_keys[i];
@@ -288,9 +278,10 @@ static int release_pressed_keys() {
return count;
}
static inline int press_combo_behavior(const struct combo_cfg *combo, int32_t timestamp) {
static inline int press_combo_behavior(int combo_idx, const struct combo_cfg *combo,
int32_t timestamp) {
struct zmk_behavior_binding_event event = {
.position = combo->virtual_key_position,
.position = ZMK_VIRTUAL_KEY_POSITION_COMBO(combo_idx),
.timestamp = timestamp,
#if IS_ENABLED(CONFIG_ZMK_SPLIT)
.source = ZMK_POSITION_STATE_CHANGE_SOURCE_LOCAL,
@@ -302,9 +293,10 @@ static inline int press_combo_behavior(const struct combo_cfg *combo, int32_t ti
return zmk_behavior_invoke_binding(&combo->behavior, event, true);
}
static inline int release_combo_behavior(const struct combo_cfg *combo, int32_t timestamp) {
static inline int release_combo_behavior(int combo_idx, const struct combo_cfg *combo,
int32_t timestamp) {
struct zmk_behavior_binding_event event = {
.position = combo->virtual_key_position,
.position = ZMK_VIRTUAL_KEY_POSITION_COMBO(combo_idx),
.timestamp = timestamp,
#if IS_ENABLED(CONFIG_ZMK_SPLIT)
.source = ZMK_POSITION_STATE_CHANGE_SOURCE_LOCAL,
@@ -316,7 +308,7 @@ static inline int release_combo_behavior(const struct combo_cfg *combo, int32_t
static void move_pressed_keys_to_active_combo(struct active_combo *active_combo) {
int combo_length = MIN(pressed_keys_count, active_combo->combo->key_position_len);
int combo_length = MIN(pressed_keys_count, combos[active_combo->combo_idx].key_position_len);
for (int i = 0; i < combo_length; i++) {
active_combo->key_positions_pressed[i] = pressed_keys[i];
}
@@ -330,10 +322,10 @@ static void move_pressed_keys_to_active_combo(struct active_combo *active_combo)
pressed_keys_count -= combo_length;
}
static struct active_combo *store_active_combo(const struct combo_cfg *combo) {
static struct active_combo *store_active_combo(int32_t combo_idx) {
for (int i = 0; i < CONFIG_ZMK_COMBO_MAX_PRESSED_COMBOS; i++) {
if (active_combos[i].combo == NULL) {
active_combos[i].combo = combo;
if (active_combos[i].combo_idx == UINT16_MAX) {
active_combos[i].combo_idx = combo_idx;
active_combo_count++;
return &active_combos[i];
}
@@ -344,15 +336,16 @@ static struct active_combo *store_active_combo(const struct combo_cfg *combo) {
return NULL;
}
static void activate_combo(const struct combo_cfg *combo) {
struct active_combo *active_combo = store_active_combo(combo);
static void activate_combo(int combo_idx) {
struct active_combo *active_combo = store_active_combo(combo_idx);
if (active_combo == NULL) {
// unable to store combo
release_pressed_keys();
return;
}
move_pressed_keys_to_active_combo(active_combo);
press_combo_behavior(combo, active_combo->key_positions_pressed[0].data.timestamp);
press_combo_behavior(combo_idx, &combos[combo_idx],
active_combo->key_positions_pressed[0].data.timestamp);
}
static void deactivate_combo(int active_combo_index) {
@@ -361,8 +354,8 @@ static void deactivate_combo(int active_combo_index) {
memcpy(&active_combos[active_combo_index], &active_combos[active_combo_count],
sizeof(struct active_combo));
}
active_combos[active_combo_count].combo = NULL;
active_combos[active_combo_count] = (struct active_combo){0};
active_combos[active_combo_count].combo_idx = UINT16_MAX;
}
/* returns true if a key was released. */
@@ -371,8 +364,8 @@ static bool release_combo_key(int32_t position, int64_t timestamp) {
struct active_combo *active_combo = &active_combos[combo_idx];
bool key_released = false;
bool all_keys_pressed =
active_combo->key_positions_pressed_count == active_combo->combo->key_position_len;
bool all_keys_pressed = active_combo->key_positions_pressed_count ==
combos[active_combo->combo_idx].key_position_len;
bool all_keys_released = true;
for (int i = 0; i < active_combo->key_positions_pressed_count; i++) {
if (key_released) {
@@ -387,9 +380,9 @@ static bool release_combo_key(int32_t position, int64_t timestamp) {
if (key_released) {
active_combo->key_positions_pressed_count--;
if ((active_combo->combo->slow_release && all_keys_released) ||
(!active_combo->combo->slow_release && all_keys_pressed)) {
release_combo_behavior(active_combo->combo, timestamp);
const struct combo_cfg *c = &combos[active_combo->combo_idx];
if ((c->slow_release && all_keys_released) || (!c->slow_release && all_keys_pressed)) {
release_combo_behavior(active_combo->combo_idx, c, timestamp);
}
if (all_keys_released) {
deactivate_combo(combo_idx);
@@ -402,10 +395,10 @@ static bool release_combo_key(int32_t position, int64_t timestamp) {
static int cleanup() {
k_work_cancel_delayable(&timeout_task);
clear_candidates();
if (fully_pressed_combo != NULL) {
memset(candidates, 0, BYTES_FOR_COMBOS_MASK * sizeof(uint32_t));
if (fully_pressed_combo != INT16_MAX) {
activate_combo(fully_pressed_combo);
fully_pressed_combo = NULL;
fully_pressed_combo = INT16_MAX;
}
return release_pressed_keys();
}
@@ -427,7 +420,7 @@ static void update_timeout_task() {
static int position_state_down(const zmk_event_t *ev, struct zmk_position_state_changed *data) {
int num_candidates;
if (candidates[0].combo == NULL) {
if (!pressed_keys_count) {
num_candidates = setup_candidates_for_first_keypress(data->position, data->timestamp);
if (num_candidates == 0) {
return ZMK_EV_EVENT_BUBBLE;
@@ -436,27 +429,31 @@ static int position_state_down(const zmk_event_t *ev, struct zmk_position_state_
filter_timed_out_candidates(data->timestamp);
num_candidates = filter_candidates(data->position);
}
update_timeout_task();
const struct combo_cfg *candidate_combo = candidates[0].combo;
LOG_DBG("combo: capturing position event %d", data->position);
int ret = capture_pressed_key(data);
switch (num_candidates) {
case 0:
update_timeout_task();
if (num_candidates) {
for (int i = 0; i < ARRAY_SIZE(combos); i++) {
if (sys_bitfield_test_bit((mem_addr_t)&candidates, i)) {
const struct combo_cfg *candidate_combo = &combos[i];
if (candidate_is_completely_pressed(candidate_combo)) {
fully_pressed_combo = i;
if (num_candidates == 1) {
cleanup();
}
}
return ret;
}
}
} else {
cleanup();
return ret;
case 1:
if (candidate_is_completely_pressed(candidate_combo)) {
fully_pressed_combo = candidate_combo;
cleanup();
}
return ret;
default:
if (candidate_is_completely_pressed(candidate_combo)) {
fully_pressed_combo = candidate_combo;
}
return ret;
}
return -EINVAL;
}
static int position_state_up(const zmk_event_t *ev, struct zmk_position_state_changed *data) {
@@ -481,8 +478,11 @@ static void combo_timeout_handler(struct k_work *item) {
return;
}
if (filter_timed_out_candidates(timeout_task_timeout_at) == 0) {
LOG_DBG("CLEANUP!");
cleanup();
}
LOG_DBG("ABOUT TO UPDATE IN TIMEOUT");
update_timeout_task();
}
@@ -520,26 +520,16 @@ ZMK_LISTENER(combo, behavior_combo_listener);
ZMK_SUBSCRIPTION(combo, zmk_position_state_changed);
ZMK_SUBSCRIPTION(combo, zmk_keycode_state_changed);
#define COMBO_INST(n) \
static const struct combo_cfg combo_config_##n = { \
.timeout_ms = DT_PROP(n, timeout_ms), \
.require_prior_idle_ms = DT_PROP(n, require_prior_idle_ms), \
.key_positions = DT_PROP(n, key_positions), \
.key_position_len = DT_PROP_LEN(n, key_positions), \
.behavior = ZMK_KEYMAP_EXTRACT_BINDING(0, n), \
.virtual_key_position = ZMK_VIRTUAL_KEY_POSITION_COMBO(__COUNTER__), \
.slow_release = DT_PROP(n, slow_release), \
.layers = DT_PROP(n, layers), \
.layers_len = DT_PROP_LEN(n, layers), \
};
#define INITIALIZE_COMBO(n) initialize_combo(&combo_config_##n);
DT_INST_FOREACH_CHILD(0, COMBO_INST)
static int combo_init(void) {
for (size_t i = 0; i < CONFIG_ZMK_COMBO_MAX_PRESSED_COMBOS; i++) {
active_combos[i].combo_idx = UINT16_MAX;
}
k_work_init_delayable(&timeout_task, combo_timeout_handler);
DT_INST_FOREACH_CHILD(0, INITIALIZE_COMBO);
LOG_WRN("Have %d combos!", ARRAY_SIZE(combos));
for (int i = 0; i < ARRAY_SIZE(combos); i++) {
initialize_combo(i);
}
return 0;
}

View File

@@ -10,7 +10,7 @@ config ZMK_WIDGET_LAYER_STATUS
config ZMK_WIDGET_BATTERY_STATUS
bool "Widget for battery charge information, using small icons"
depends on BT
depends on ZMK_BATTERY_REPORTING
select LV_USE_LABEL
if ZMK_WIDGET_BATTERY_STATUS

View File

@@ -445,7 +445,7 @@ void zmk_hid_mouse_movement_update(int16_t hwheel, int16_t wheel) {
LOG_DBG("Mouse movement updated to %d/%d", mouse_report.body.d_x, mouse_report.body.d_y);
}
void zmk_hid_mouse_scroll_set(int8_t hwheel, int8_t wheel) {
void zmk_hid_mouse_scroll_set(int16_t hwheel, int16_t wheel) {
mouse_report.body.d_scroll_x = hwheel;
mouse_report.body.d_scroll_y = wheel;
@@ -453,7 +453,7 @@ void zmk_hid_mouse_scroll_set(int8_t hwheel, int8_t wheel) {
mouse_report.body.d_scroll_y);
}
void zmk_hid_mouse_scroll_update(int8_t hwheel, int8_t wheel) {
void zmk_hid_mouse_scroll_update(int16_t hwheel, int16_t wheel) {
mouse_report.body.d_scroll_x += hwheel;
mouse_report.body.d_scroll_y += wheel;

View File

@@ -12,7 +12,7 @@
#include <zmk/hid_indicators.h>
#include <zmk/events/hid_indicators_changed.h>
#include <zmk/events/endpoint_changed.h>
#include <zmk/split/bluetooth/central.h>
#include <zmk/split/central.h>
LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
@@ -32,8 +32,8 @@ static void raise_led_changed_event(struct k_work *_work) {
raise_zmk_hid_indicators_changed((struct zmk_hid_indicators_changed){.indicators = indicators});
#if IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS) && IS_ENABLED(CONFIG_ZMK_SPLIT_BLE)
zmk_split_bt_update_hid_indicator(indicators);
#if IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS) && IS_ENABLED(CONFIG_ZMK_SPLIT)
zmk_split_central_update_hid_indicator(indicators);
#endif
}

View File

@@ -168,9 +168,28 @@ static ssize_t read_hids_mouse_input_report(struct bt_conn *conn, const struct b
static ssize_t read_hids_mouse_feature_report(struct bt_conn *conn, const struct bt_gatt_attr *attr,
void *buf, uint16_t len, uint16_t offset) {
struct zmk_hid_mouse_report_body *report_body = &zmk_hid_get_mouse_report()->body;
return bt_gatt_attr_read(conn, attr, buf, len, offset, report_body,
sizeof(struct zmk_hid_mouse_report_body));
int profile = zmk_ble_profile_index(bt_conn_get_dst(conn));
if (profile < 0) {
LOG_DBG(" BT_ATT_ERR_UNLIKELY");
return BT_GATT_ERR(BT_ATT_ERR_UNLIKELY);
}
struct zmk_endpoint_instance endpoint = {
.transport = ZMK_TRANSPORT_BLE,
.ble = {.profile_index = profile},
};
struct zmk_pointing_resolution_multipliers mult =
zmk_pointing_resolution_multipliers_get_profile(endpoint);
struct zmk_hid_mouse_resolution_feature_report_body report = {
.wheel_res = mult.wheel,
.hwheel_res = mult.hor_wheel,
};
return bt_gatt_attr_read(conn, attr, buf, len, offset, &report,
sizeof(struct zmk_hid_mouse_resolution_feature_report_body));
}
static ssize_t write_hids_mouse_feature_report(struct bt_conn *conn,

View File

@@ -74,12 +74,7 @@ static uint8_t keymap_layer_orders[ZMK_KEYMAP_LAYERS_LEN];
#define KEYMAP_VAR(_name, _opts, no_init) \
static _opts struct zmk_behavior_binding _name[ZMK_KEYMAP_LAYERS_LEN][ZMK_KEYMAP_LEN] = { \
COND_CODE_0( \
no_init, \
(COND_CODE_1(IS_ENABLED(CONFIG_ZMK_STUDIO), \
(DT_INST_FOREACH_CHILD_SEP(0, TRANSFORMED_LAYER, (, ))), \
(DT_INST_FOREACH_CHILD_STATUS_OKAY_SEP(0, TRANSFORMED_LAYER, (, ))))), \
(0))};
COND_CODE_0(no_init, (ZMK_KEYMAP_LAYERS_FOREACH_SEP(TRANSFORMED_LAYER, (, ))), (0))};
KEYMAP_VAR(zmk_keymap, COND_CODE_1(IS_ENABLED(CONFIG_ZMK_KEYMAP_SETTINGS_STORAGE), (), (const)),
IS_ENABLED(CONFIG_ZMK_STUDIO))
@@ -89,14 +84,14 @@ KEYMAP_VAR(zmk_keymap, COND_CODE_1(IS_ENABLED(CONFIG_ZMK_KEYMAP_SETTINGS_STORAGE
KEYMAP_VAR(zmk_stock_keymap, const, 0)
static char zmk_keymap_layer_names[ZMK_KEYMAP_LAYERS_LEN][CONFIG_ZMK_KEYMAP_LAYER_NAME_MAX_LEN] = {
DT_INST_FOREACH_CHILD_SEP(0, LAYER_NAME, (, ))};
ZMK_KEYMAP_LAYERS_FOREACH_SEP(LAYER_NAME, (, ))};
static uint32_t changed_layer_names = 0;
#else
static const char *zmk_keymap_layer_names[ZMK_KEYMAP_LAYERS_LEN] = {
DT_INST_FOREACH_CHILD_SEP(0, LAYER_NAME, (, ))};
ZMK_KEYMAP_LAYERS_FOREACH_SEP(LAYER_NAME, (, ))};
#endif

View File

@@ -134,9 +134,6 @@ static int handle_layer_state_changed(const struct device *dev, const zmk_event_
if (ret < 0) {
return ret;
}
if (data->state.toggle_layer == 0) {
return ZMK_EV_EVENT_BUBBLE;
}
if (!zmk_keymap_layer_active(zmk_keymap_layer_index_to_id(data->state.toggle_layer))) {
LOG_DBG("Deactivating layer that was activated by this processor");
data->state.is_active = false;

View File

@@ -44,7 +44,7 @@ int zmk_input_split_report_peripheral_event(uint8_t reg, uint8_t type, uint16_t
#else
#include <zmk/split/bluetooth/service.h>
#include <zmk/split/peripheral.h>
#define ZIS_INST(n) \
static const struct zmk_input_processor_entry processors_##n[] = \
@@ -56,11 +56,23 @@ int zmk_input_split_report_peripheral_event(uint8_t reg, uint8_t type, uint16_t
"Peripheral input splits need an `input` property set"); \
void split_input_handler_##n(struct input_event *evt) { \
for (size_t i = 0; i < ARRAY_SIZE(processors_##n); i++) { \
zmk_input_processor_handle_event(processors_##n[i].dev, evt, processors_##n[i].param1, \
processors_##n[i].param2, NULL); \
int ret = zmk_input_processor_handle_event(processors_##n[i].dev, evt, \
processors_##n[i].param1, \
processors_##n[i].param2, NULL); \
if (ret != ZMK_INPUT_PROC_CONTINUE) { \
return; \
} \
} \
zmk_split_bt_report_input(DT_INST_REG_ADDR(n), evt->type, evt->code, evt->value, \
evt->sync); \
struct zmk_split_transport_peripheral_event ev = { \
.type = ZMK_SPLIT_TRANSPORT_PERIPHERAL_EVENT_TYPE_INPUT_EVENT, \
.data = {.input_event = { \
.reg = DT_INST_REG_ADDR(n), \
.type = evt->type, \
.code = evt->code, \
.value = evt->value, \
.sync = evt->sync, \
}}}; \
zmk_split_peripheral_report_event(&ev); \
} \
INPUT_CALLBACK_DEFINE(DEVICE_DT_GET(DT_INST_PHANDLE(n, device)), split_input_handler_##n);

View File

@@ -13,7 +13,13 @@
LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
static struct zmk_pointing_resolution_multipliers multipliers[ZMK_ENDPOINT_COUNT];
static struct zmk_pointing_resolution_multipliers multipliers[ZMK_ENDPOINT_COUNT] = {
[0 ... ZMK_ENDPOINT_COUNT - 1] =
{
.wheel = 15,
.hor_wheel = 15,
},
};
struct zmk_pointing_resolution_multipliers
zmk_pointing_resolution_multipliers_get_current_profile(void) {

View File

@@ -3,4 +3,16 @@
if (CONFIG_ZMK_SPLIT_BLE)
add_subdirectory(bluetooth)
endif()
if (CONFIG_ZMK_SPLIT_WIRED)
add_subdirectory(wired)
endif()
if (CONFIG_ZMK_SPLIT_ROLE_CENTRAL)
target_sources(app PRIVATE central.c)
zephyr_linker_sources(SECTIONS ../../include/linker/zmk-split-transport-central.ld)
else()
target_sources(app PRIVATE peripheral.c)
zephyr_linker_sources(SECTIONS ../../include/linker/zmk-split-transport-peripheral.ld)
endif()

View File

@@ -9,16 +9,20 @@ if ZMK_SPLIT
config ZMK_SPLIT_ROLE_CENTRAL
bool "Split central device"
choice ZMK_SPLIT_TRANSPORT
prompt "Split transport"
config ZMK_SPLIT_BLE
bool "BLE"
bool "BLE Split"
default y
depends on ZMK_BLE
select BT_USER_PHY_UPDATE
select BT_AUTO_PHY_UPDATE
endchoice
config ZMK_SPLIT_WIRED
bool "Wired Split"
default y
depends on DT_HAS_ZMK_WIRED_SPLIT_ENABLED
select SERIAL
select RING_BUFFER
select CRC
config ZMK_SPLIT_PERIPHERAL_HID_INDICATORS
bool "Peripheral HID Indicators"
@@ -29,3 +33,4 @@ config ZMK_SPLIT_PERIPHERAL_HID_INDICATORS
endif # ZMK_SPLIT
rsource "bluetooth/Kconfig"
rsource "wired/Kconfig"

View File

@@ -2,3 +2,5 @@
# SPDX-License-Identifier: MIT
rsource "bluetooth/Kconfig.defaults"
rsource "wired/Kconfig.defaults"

View File

@@ -2,7 +2,6 @@
# SPDX-License-Identifier: MIT
if (NOT CONFIG_ZMK_SPLIT_ROLE_CENTRAL)
target_sources(app PRIVATE split_listener.c)
target_sources(app PRIVATE service.c)
target_sources(app PRIVATE peripheral.c)
endif()

View File

@@ -5,6 +5,11 @@ if ZMK_SPLIT && ZMK_SPLIT_BLE
menu "BLE Transport"
config ZMK_SPLIT_BLE_PRIORITY
int "BLE transport priority"
help
Lower number priorities transports are favored over higher numbers.
# Added for backwards compatibility. New shields / board should set `ZMK_SPLIT_ROLE_CENTRAL` only.
config ZMK_SPLIT_BLE_ROLE_CENTRAL
bool

View File

@@ -3,7 +3,12 @@
if ZMK_BLE
if ZMK_SPLIT_BLE && ZMK_SPLIT_ROLE_CENTRAL
if ZMK_SPLIT_BLE
config ZMK_SPLIT_BLE_PRIORITY
default 1
if ZMK_SPLIT_ROLE_CENTRAL
config ZMK_SPLIT_BLE_CENTRAL_PERIPHERALS
default 1
@@ -17,6 +22,8 @@ config BT_MAX_PAIRED
#ZMK_SPLIT_BLE && ZMK_SPLIT_ROLE_CENTRAL
endif
endif
if !ZMK_SPLIT_BLE
config BT_MAX_CONN

View File

@@ -23,6 +23,7 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
#include <zmk/ble.h>
#include <zmk/behavior.h>
#include <zmk/sensors.h>
#include <zmk/split/transport/central.h>
#include <zmk/split/bluetooth/uuid.h>
#include <zmk/split/bluetooth/service.h>
#include <zmk/event_manager.h>
@@ -128,22 +129,24 @@ void release_peripheral_input_subs(struct bt_conn *conn) {
#endif // IS_ENABLED(CONFIG_ZMK_INPUT_SPLIT)
static zmk_split_transport_central_status_changed_cb_t transport_status_cb;
static bool is_enabled;
static struct peripheral_slot peripherals[ZMK_SPLIT_BLE_PERIPHERAL_COUNT];
static bool is_scanning = false;
static const struct bt_uuid_128 split_service_uuid = BT_UUID_INIT_128(ZMK_SPLIT_BT_SERVICE_UUID);
K_MSGQ_DEFINE(peripheral_event_msgq, sizeof(struct zmk_position_state_changed),
struct peripheral_event_wrapper {
uint8_t source;
struct zmk_split_transport_peripheral_event event;
};
K_MSGQ_DEFINE(peripheral_event_msgq, sizeof(struct peripheral_event_wrapper),
CONFIG_ZMK_SPLIT_BLE_CENTRAL_POSITION_QUEUE_SIZE, 4);
void peripheral_event_work_callback(struct k_work *work) {
struct zmk_position_state_changed ev;
while (k_msgq_get(&peripheral_event_msgq, &ev, K_NO_WAIT) == 0) {
LOG_DBG("Trigger key position state change for %d", ev.position);
raise_zmk_position_state_changed(ev);
}
}
void peripheral_event_work_callback(struct k_work *work);
K_WORK_DEFINE(peripheral_event_work, peripheral_event_work_callback);
@@ -190,10 +193,13 @@ int release_peripheral_slot(int index) {
for (int j = 0; j < 8; j++) {
if (slot->position_state[i] & BIT(j)) {
uint32_t position = (i * 8) + j;
struct zmk_position_state_changed ev = {.source = index,
.position = position,
.state = false,
.timestamp = k_uptime_get()};
struct peripheral_event_wrapper ev = {
.source = index,
.event = {.type = ZMK_SPLIT_TRANSPORT_PERIPHERAL_EVENT_TYPE_KEY_POSITION_EVENT,
.data = {.key_position_event = {
.position = position,
.pressed = false,
}}}};
k_msgq_put(&peripheral_event_msgq, &ev, K_NO_WAIT);
k_work_submit(&peripheral_event_work);
@@ -250,19 +256,13 @@ int confirm_peripheral_slot_conn(struct bt_conn *conn) {
return 0;
}
static void notify_transport_status(void);
static void notify_status_work_cb(struct k_work *_work) { notify_transport_status(); }
static K_WORK_DEFINE(notify_status_work, notify_status_work_cb);
#if ZMK_KEYMAP_HAS_SENSORS
K_MSGQ_DEFINE(peripheral_sensor_event_msgq, sizeof(struct zmk_sensor_event),
CONFIG_ZMK_SPLIT_BLE_CENTRAL_POSITION_QUEUE_SIZE, 4);
void peripheral_sensor_event_work_callback(struct k_work *work) {
struct zmk_sensor_event ev;
while (k_msgq_get(&peripheral_sensor_event_msgq, &ev, K_NO_WAIT) == 0) {
LOG_DBG("Trigger sensor change for %d", ev.sensor_index);
raise_zmk_sensor_event(ev);
}
}
K_WORK_DEFINE(peripheral_sensor_event_work, peripheral_sensor_event_work_callback);
static uint8_t split_central_sensor_notify_func(struct bt_conn *conn,
struct bt_gatt_subscribe_params *params,
@@ -282,15 +282,20 @@ static uint8_t split_central_sensor_notify_func(struct bt_conn *conn,
struct sensor_event sensor_event;
memcpy(&sensor_event, data, MIN(length, sizeof(sensor_event)));
struct zmk_sensor_event ev = {
.sensor_index = sensor_event.sensor_index,
.channel_data_size = MIN(sensor_event.channel_data_size, ZMK_SENSOR_EVENT_MAX_CHANNELS),
.timestamp = k_uptime_get()};
if (sensor_event.channel_data_size != 1) {
return BT_GATT_ITER_STOP;
}
memcpy(ev.channel_data, sensor_event.channel_data,
sizeof(struct zmk_sensor_channel_data) * sensor_event.channel_data_size);
k_msgq_put(&peripheral_sensor_event_msgq, &ev, K_NO_WAIT);
k_work_submit(&peripheral_sensor_event_work);
struct peripheral_event_wrapper event_wrapper = {
.source = peripheral_slot_index_for_conn(conn),
.event = {.type = ZMK_SPLIT_TRANSPORT_PERIPHERAL_EVENT_TYPE_SENSOR_EVENT,
.data = {.sensor_event = {
.channel_data = sensor_event.channel_data[0],
.sensor_index = sensor_event.sensor_index,
}}}};
k_msgq_put(&peripheral_event_msgq, &event_wrapper, K_NO_WAIT);
k_work_submit(&peripheral_event_work);
return BT_GATT_ITER_CONTINUE;
}
@@ -298,27 +303,6 @@ static uint8_t split_central_sensor_notify_func(struct bt_conn *conn,
#if IS_ENABLED(CONFIG_ZMK_INPUT_SPLIT)
struct zmk_input_event_msg {
uint8_t reg;
struct zmk_split_input_event_payload payload;
};
K_MSGQ_DEFINE(peripheral_input_event_msgq, sizeof(struct zmk_input_event_msg), 5, 4);
// CONFIG_ZMK_SPLIT_BLE_CENTRAL_INPUT_QUEUE_SIZE, 4);
void peripheral_input_event_work_callback(struct k_work *work) {
struct zmk_input_event_msg msg;
while (k_msgq_get(&peripheral_input_event_msgq, &msg, K_NO_WAIT) == 0) {
int ret = zmk_input_split_report_peripheral_event(
msg.reg, msg.payload.type, msg.payload.code, msg.payload.value, msg.payload.sync);
if (ret < 0) {
LOG_WRN("Failed to report peripheral event %d", ret);
}
}
}
K_WORK_DEFINE(input_event_work, peripheral_input_event_work_callback);
static uint8_t peripheral_input_event_notify_cb(struct bt_conn *conn,
struct bt_gatt_subscribe_params *params,
const void *data, uint16_t length) {
@@ -335,18 +319,25 @@ static uint8_t peripheral_input_event_notify_cb(struct bt_conn *conn,
return BT_GATT_ITER_STOP;
}
struct zmk_input_event_msg msg;
memcpy(&msg.payload, data, MIN(length, sizeof(struct zmk_split_input_event_payload)));
LOG_DBG("Got an input event with type %d, code %d, value %d, sync %d", msg.payload.type,
msg.payload.code, msg.payload.value, msg.payload.sync);
struct zmk_split_input_event_payload payload;
memcpy(&payload, data, MIN(length, sizeof(struct zmk_split_input_event_payload)));
for (size_t i = 0; i < ARRAY_SIZE(peripheral_input_slots); i++) {
if (&peripheral_input_slots[i].sub == params) {
msg.reg = peripheral_input_slots[i].reg;
k_msgq_put(&peripheral_input_event_msgq, &msg, K_NO_WAIT);
k_work_submit(&input_event_work);
struct peripheral_event_wrapper event_wrapper = {
.source = peripheral_slot_index_for_conn(conn),
.event = {.type = ZMK_SPLIT_TRANSPORT_PERIPHERAL_EVENT_TYPE_INPUT_EVENT,
.data = {.input_event = {
.reg = peripheral_input_slots[i].reg,
.sync = payload.sync,
.code = payload.code,
.type = payload.type,
.value = payload.value,
}}}};
k_msgq_put(&peripheral_event_msgq, &event_wrapper, K_NO_WAIT);
k_work_submit(&peripheral_event_work);
break;
}
}
@@ -376,20 +367,21 @@ static uint8_t split_central_notify_func(struct bt_conn *conn,
for (int i = 0; i < POSITION_STATE_DATA_LEN; i++) {
slot->changed_positions[i] = ((uint8_t *)data)[i] ^ slot->position_state[i];
slot->position_state[i] = ((uint8_t *)data)[i];
LOG_DBG("data: %d", slot->position_state[i]);
}
LOG_HEXDUMP_DBG(slot->position_state, POSITION_STATE_DATA_LEN, "data");
for (int i = 0; i < POSITION_STATE_DATA_LEN; i++) {
for (int j = 0; j < 8; j++) {
if (slot->changed_positions[i] & BIT(j)) {
uint32_t position = (i * 8) + j;
bool pressed = slot->position_state[i] & BIT(j);
struct zmk_position_state_changed ev = {.source =
peripheral_slot_index_for_conn(conn),
.position = position,
.state = pressed,
.timestamp = k_uptime_get()};
struct peripheral_event_wrapper ev = {
.source = peripheral_slot_index_for_conn(conn),
.event = {.type = ZMK_SPLIT_TRANSPORT_PERIPHERAL_EVENT_TYPE_KEY_POSITION_EVENT,
.data = {.key_position_event = {
.position = position,
.pressed = pressed,
}}}};
k_msgq_put(&peripheral_event_msgq, &ev, K_NO_WAIT);
k_work_submit(&peripheral_event_work);
}
@@ -401,35 +393,6 @@ static uint8_t split_central_notify_func(struct bt_conn *conn,
#if IS_ENABLED(CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING)
static uint8_t peripheral_battery_levels[ZMK_SPLIT_BLE_PERIPHERAL_COUNT] = {0};
int zmk_split_get_peripheral_battery_level(uint8_t source, uint8_t *level) {
if (source >= ARRAY_SIZE(peripheral_battery_levels)) {
return -EINVAL;
}
if (peripherals[source].state != PERIPHERAL_SLOT_STATE_CONNECTED) {
return -ENOTCONN;
}
*level = peripheral_battery_levels[source];
return 0;
}
K_MSGQ_DEFINE(peripheral_batt_lvl_msgq, sizeof(struct zmk_peripheral_battery_state_changed),
CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_QUEUE_SIZE, 4);
void peripheral_batt_lvl_change_callback(struct k_work *work) {
struct zmk_peripheral_battery_state_changed ev;
while (k_msgq_get(&peripheral_batt_lvl_msgq, &ev, K_NO_WAIT) == 0) {
LOG_DBG("Triggering peripheral battery level change %u", ev.state_of_charge);
peripheral_battery_levels[ev.source] = ev.state_of_charge;
raise_zmk_peripheral_battery_state_changed(ev);
}
}
K_WORK_DEFINE(peripheral_batt_lvl_work, peripheral_batt_lvl_change_callback);
static uint8_t split_central_battery_level_notify_func(struct bt_conn *conn,
struct bt_gatt_subscribe_params *params,
const void *data, uint16_t length) {
@@ -454,10 +417,16 @@ static uint8_t split_central_battery_level_notify_func(struct bt_conn *conn,
LOG_DBG("[BATTERY LEVEL NOTIFICATION] data %p length %u", data, length);
uint8_t battery_level = ((uint8_t *)data)[0];
LOG_DBG("Battery level: %u", battery_level);
struct zmk_peripheral_battery_state_changed ev = {
.source = peripheral_slot_index_for_conn(conn), .state_of_charge = battery_level};
k_msgq_put(&peripheral_batt_lvl_msgq, &ev, K_NO_WAIT);
k_work_submit(&peripheral_batt_lvl_work);
struct peripheral_event_wrapper ev = {
.source = peripheral_slot_index_for_conn(conn),
.event = {.type = ZMK_SPLIT_TRANSPORT_PERIPHERAL_EVENT_TYPE_BATTERY_EVENT,
.data = {.battery_event = {
.level = battery_level,
}}}};
k_msgq_put(&peripheral_event_msgq, &ev, K_NO_WAIT);
k_work_submit(&peripheral_event_work);
return BT_GATT_ITER_CONTINUE;
}
@@ -493,10 +462,15 @@ static uint8_t split_central_battery_level_read_func(struct bt_conn *conn, uint8
LOG_DBG("Battery level: %u", battery_level);
struct zmk_peripheral_battery_state_changed ev = {
.source = peripheral_slot_index_for_conn(conn), .state_of_charge = battery_level};
k_msgq_put(&peripheral_batt_lvl_msgq, &ev, K_NO_WAIT);
k_work_submit(&peripheral_batt_lvl_work);
struct peripheral_event_wrapper ev = {
.source = peripheral_slot_index_for_conn(conn),
.event = {.type = ZMK_SPLIT_TRANSPORT_PERIPHERAL_EVENT_TYPE_BATTERY_EVENT,
.data = {.battery_event = {
.level = battery_level,
}}}};
k_msgq_put(&peripheral_event_msgq, &ev, K_NO_WAIT);
k_work_submit(&peripheral_event_work);
return BT_GATT_ITER_CONTINUE;
}
@@ -909,6 +883,11 @@ static void split_central_device_found(const bt_addr_le_t *addr, int8_t rssi, ui
}
static int start_scanning(void) {
if (!is_enabled) {
LOG_DBG("Not scanning, we're disabled");
return 0;
}
// No action is necessary if central is already scanning.
if (is_scanning) {
LOG_DBG("Scanning already running");
@@ -966,6 +945,7 @@ static void split_central_connected(struct bt_conn *conn, uint8_t conn_err) {
confirm_peripheral_slot_conn(conn);
split_central_process_connection(conn);
k_work_submit(&notify_status_work);
}
static void split_central_disconnected(struct bt_conn *conn, uint8_t reason) {
@@ -977,10 +957,19 @@ static void split_central_disconnected(struct bt_conn *conn, uint8_t reason) {
LOG_DBG("Disconnected: %s (reason %d)", addr, reason);
#if IS_ENABLED(CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING)
struct zmk_peripheral_battery_state_changed ev = {
.source = peripheral_slot_index_for_conn(conn), .state_of_charge = 0};
k_msgq_put(&peripheral_batt_lvl_msgq, &ev, K_NO_WAIT);
k_work_submit(&peripheral_batt_lvl_work);
struct peripheral_event_wrapper ev = {
.source = peripheral_slot_index_for_conn(conn),
.event = {.type = ZMK_SPLIT_TRANSPORT_PERIPHERAL_EVENT_TYPE_BATTERY_EVENT,
.data = {.battery_event = {
.level = 0,
}}}};
k_msgq_put(&peripheral_event_msgq, &ev, K_NO_WAIT);
k_work_submit(&peripheral_event_work);
// struct zmk_peripheral_battery_state_changed ev = {
// .source = peripheral_slot_index_for_conn(conn), .state_of_charge = 0};
// k_msgq_put(&peripheral_batt_lvl_msgq, &ev, K_NO_WAIT);
// k_work_submit(&peripheral_batt_lvl_work);
#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING)
#if IS_ENABLED(CONFIG_ZMK_INPUT_SPLIT)
@@ -990,9 +979,11 @@ static void split_central_disconnected(struct bt_conn *conn, uint8_t reason) {
err = release_peripheral_slot_for_conn(conn);
if (err < 0) {
return;
LOG_WRN("Failed to release peripheral slot (%d)", err);
}
k_work_submit(&notify_status_work);
start_scanning();
}
@@ -1027,17 +1018,16 @@ K_THREAD_STACK_DEFINE(split_central_split_run_q_stack,
struct k_work_q split_central_split_run_q;
struct zmk_split_run_behavior_payload_wrapper {
struct central_cmd_wrapper {
uint8_t source;
struct zmk_split_run_behavior_payload payload;
struct zmk_split_transport_central_command cmd;
};
K_MSGQ_DEFINE(zmk_split_central_split_run_msgq,
sizeof(struct zmk_split_run_behavior_payload_wrapper),
K_MSGQ_DEFINE(zmk_split_central_split_run_msgq, sizeof(struct central_cmd_wrapper),
CONFIG_ZMK_SPLIT_BLE_CENTRAL_SPLIT_RUN_QUEUE_SIZE, 4);
void split_central_split_run_callback(struct k_work *work) {
struct zmk_split_run_behavior_payload_wrapper payload_wrapper;
struct central_cmd_wrapper payload_wrapper;
LOG_DBG("");
@@ -1046,34 +1036,85 @@ void split_central_split_run_callback(struct k_work *work) {
LOG_ERR("Source not connected");
continue;
}
if (!peripherals[payload_wrapper.source].run_behavior_handle) {
LOG_ERR("Run behavior handle not found");
continue;
switch (payload_wrapper.cmd.type) {
case ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_INVOKE_BEHAVIOR: {
if (!peripherals[payload_wrapper.source].run_behavior_handle) {
LOG_ERR("Run behavior handle not found");
continue;
}
struct zmk_split_run_behavior_payload payload = {
.data = {
.param1 = payload_wrapper.cmd.data.invoke_behavior.param1,
.param2 = payload_wrapper.cmd.data.invoke_behavior.param2,
.position = payload_wrapper.cmd.data.invoke_behavior.position,
.source = payload_wrapper.cmd.data.invoke_behavior.event_source,
.state = payload_wrapper.cmd.data.invoke_behavior.state ? 1 : 0,
}};
const size_t payload_dev_size = sizeof(payload.behavior_dev);
if (strlcpy(payload.behavior_dev, payload_wrapper.cmd.data.invoke_behavior.behavior_dev,
payload_dev_size) >= payload_dev_size) {
LOG_ERR("Truncated behavior label %s to %s before invoking peripheral behavior",
payload_wrapper.cmd.data.invoke_behavior.behavior_dev,
payload.behavior_dev);
}
int err = bt_gatt_write_without_response(
peripherals[payload_wrapper.source].conn,
peripherals[payload_wrapper.source].run_behavior_handle, &payload,
sizeof(struct zmk_split_run_behavior_payload), true);
if (err) {
LOG_ERR("Failed to write the behavior characteristic (err %d)", err);
}
break;
}
case ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_SET_PHYSICAL_LAYOUT:
update_peripheral_selected_layout(
&peripherals[payload_wrapper.source],
payload_wrapper.cmd.data.set_physical_layout.layout_idx);
break;
#if IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS)
case ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_SET_HID_INDICATORS:
LOG_WRN("do the indicators dance");
if (peripherals[payload_wrapper.source].update_hid_indicators == 0) {
// It appears that sometimes the peripheral is considered connected
// before the GATT characteristics have been discovered. If this is
// the case, the update_hid_indicators handle will not yet be set.
LOG_WRN("NO HANDLE TO SET ON PERIPHERAL");
break;
}
int err = bt_gatt_write_without_response(
peripherals[payload_wrapper.source].conn,
peripherals[payload_wrapper.source].run_behavior_handle, &payload_wrapper.payload,
sizeof(struct zmk_split_run_behavior_payload), true);
int err = bt_gatt_write_without_response(
peripherals[payload_wrapper.source].conn,
peripherals[payload_wrapper.source].update_hid_indicators,
&payload_wrapper.cmd.data.set_hid_indicators.indicators,
sizeof(payload_wrapper.cmd.data.set_hid_indicators.indicators), true);
if (err) {
LOG_ERR("Failed to write the behavior characteristic (err %d)", err);
if (err) {
LOG_ERR("Failed to write HID indicator characteristic (err %d)", err);
}
break;
#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS)
default:
LOG_WRN("Unsupported wrapped central command type %d", payload_wrapper.cmd.type);
return;
}
}
}
K_WORK_DEFINE(split_central_split_run_work, split_central_split_run_callback);
static int
split_bt_invoke_behavior_payload(struct zmk_split_run_behavior_payload_wrapper payload_wrapper) {
static int split_bt_invoke_behavior_payload(struct central_cmd_wrapper payload_wrapper) {
LOG_DBG("");
int err = k_msgq_put(&zmk_split_central_split_run_msgq, &payload_wrapper, K_MSEC(100));
if (err) {
switch (err) {
case -EAGAIN: {
LOG_WRN("Consumer message queue full, popping first message and queueing again");
struct zmk_split_run_behavior_payload_wrapper discarded_report;
LOG_WRN("Run command message queue full, popping first message and queueing again");
struct central_cmd_wrapper discarded_report;
k_msgq_get(&zmk_split_central_split_run_msgq, &discarded_report, K_NO_WAIT);
return split_bt_invoke_behavior_payload(payload_wrapper);
}
@@ -1088,66 +1129,9 @@ split_bt_invoke_behavior_payload(struct zmk_split_run_behavior_payload_wrapper p
return 0;
};
int zmk_split_bt_invoke_behavior(uint8_t source, struct zmk_behavior_binding *binding,
struct zmk_behavior_binding_event event, bool state) {
struct zmk_split_run_behavior_payload payload = {.data = {
.param1 = binding->param1,
.param2 = binding->param2,
.position = event.position,
.source = event.source,
.state = state ? 1 : 0,
}};
const size_t payload_dev_size = sizeof(payload.behavior_dev);
if (strlcpy(payload.behavior_dev, binding->behavior_dev, payload_dev_size) >=
payload_dev_size) {
LOG_ERR("Truncated behavior label %s to %s before invoking peripheral behavior",
binding->behavior_dev, payload.behavior_dev);
}
static int finish_init();
struct zmk_split_run_behavior_payload_wrapper wrapper = {.source = source, .payload = payload};
return split_bt_invoke_behavior_payload(wrapper);
}
#if IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS)
static zmk_hid_indicators_t hid_indicators = 0;
static void split_central_update_indicators_callback(struct k_work *work) {
zmk_hid_indicators_t indicators = hid_indicators;
for (int i = 0; i < ZMK_SPLIT_BLE_PERIPHERAL_COUNT; i++) {
if (peripherals[i].state != PERIPHERAL_SLOT_STATE_CONNECTED) {
continue;
}
if (peripherals[i].update_hid_indicators == 0) {
// It appears that sometimes the peripheral is considered connected
// before the GATT characteristics have been discovered. If this is
// the case, the update_hid_indicators handle will not yet be set.
continue;
}
int err = bt_gatt_write_without_response(peripherals[i].conn,
peripherals[i].update_hid_indicators, &indicators,
sizeof(indicators), true);
if (err) {
LOG_ERR("Failed to write HID indicator characteristic (err %d)", err);
}
}
}
static K_WORK_DEFINE(split_central_update_indicators, split_central_update_indicators_callback);
int zmk_split_bt_update_hid_indicator(zmk_hid_indicators_t indicators) {
hid_indicators = indicators;
return k_work_submit_to_queue(&split_central_split_run_q, &split_central_update_indicators);
}
#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS)
static int finish_init() {
return IS_ENABLED(CONFIG_ZMK_BLE_CLEAR_BONDS_ON_START) ? 0 : start_scanning();
}
static bool settings_loaded = false;
#if IS_ENABLED(CONFIG_SETTINGS)
@@ -1185,4 +1169,127 @@ static int zmk_split_bt_central_listener_cb(const zmk_event_t *eh) {
}
ZMK_LISTENER(zmk_split_bt_central, zmk_split_bt_central_listener_cb);
ZMK_SUBSCRIPTION(zmk_split_bt_central, zmk_physical_layout_selection_changed);
ZMK_SUBSCRIPTION(zmk_split_bt_central, zmk_physical_layout_selection_changed);
static int split_central_bt_send_command(uint8_t source,
struct zmk_split_transport_central_command cmd) {
if (source >= ARRAY_SIZE(peripherals)) {
return -EINVAL;
}
switch (cmd.type) {
case ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_SET_HID_INDICATORS:
case ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_SET_PHYSICAL_LAYOUT:
case ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_INVOKE_BEHAVIOR: {
struct central_cmd_wrapper wrapper = {.source = source, .cmd = cmd};
return split_bt_invoke_behavior_payload(wrapper);
}
case ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_POLL_EVENTS:
return -ENOTSUP;
default:
return -ENOTSUP;
}
return 0;
}
static int split_central_bt_get_available_source_ids(uint8_t *sources) {
int count = 0;
for (int i = 0; i < ZMK_SPLIT_BLE_PERIPHERAL_COUNT; i++) {
if (peripherals[i].state != PERIPHERAL_SLOT_STATE_CONNECTED) {
continue;
}
sources[count++] = i;
}
return count;
}
static int split_central_bt_set_enabled(bool enabled) {
is_enabled = enabled;
if (enabled) {
return start_scanning();
} else {
int err = stop_scanning();
if (err < 0) {
LOG_WRN("Failed to stop scanning for peripherals (%d)", err);
}
for (int i = 0; i < ZMK_SPLIT_BLE_PERIPHERAL_COUNT; i++) {
if (peripherals[i].state != PERIPHERAL_SLOT_STATE_CONNECTED) {
continue;
}
err = bt_conn_disconnect(peripherals[i].conn, BT_HCI_ERR_REMOTE_USER_TERM_CONN);
if (err < 0) {
LOG_WRN("Failed to disconnect a peripheral (%d)", err);
}
}
return 0;
}
}
static int
split_central_bt_set_status_callback(zmk_split_transport_central_status_changed_cb_t cb) {
transport_status_cb = cb;
return 0;
}
static struct zmk_split_transport_status split_central_bt_get_status() {
uint8_t _source_ids[ZMK_SPLIT_BLE_PERIPHERAL_COUNT];
int count = split_central_bt_get_available_source_ids(_source_ids);
enum zmk_split_transport_connections_status conn_status;
if (count == 0) {
conn_status = ZMK_SPLIT_TRANSPORT_CONNECTIONS_STATUS_DISCONNECTED;
} else if (count == ZMK_SPLIT_BLE_PERIPHERAL_COUNT) {
conn_status = ZMK_SPLIT_TRANSPORT_CONNECTIONS_STATUS_ALL_CONNECTED;
} else {
conn_status = ZMK_SPLIT_TRANSPORT_CONNECTIONS_STATUS_SOME_CONNECTED;
}
return (struct zmk_split_transport_status){
.available = !IS_ENABLED(CONFIG_ZMK_BLE_CLEAR_BONDS_ON_START) && settings_loaded,
.enabled = is_enabled,
.connections = conn_status,
};
}
static const struct zmk_split_transport_central_api central_api = {
.send_command = split_central_bt_send_command,
.get_available_source_ids = split_central_bt_get_available_source_ids,
.set_enabled = split_central_bt_set_enabled,
.set_status_callback = split_central_bt_set_status_callback,
.get_status = split_central_bt_get_status,
};
ZMK_SPLIT_TRANSPORT_CENTRAL_REGISTER(bt_central, &central_api, CONFIG_ZMK_SPLIT_BLE_PRIORITY);
static void notify_transport_status(void) {
if (transport_status_cb) {
transport_status_cb(&bt_central, split_central_bt_get_status());
}
}
static int finish_init() {
settings_loaded = true;
if (!transport_status_cb) {
return 0;
}
return transport_status_cb(&bt_central, split_central_bt_get_status());
}
void peripheral_event_work_callback(struct k_work *work) {
struct peripheral_event_wrapper ev;
while (k_msgq_get(&peripheral_event_msgq, &ev, K_NO_WAIT) == 0) {
LOG_DBG("Trigger key position state change for %d",
ev.event.data.key_position_event.position);
zmk_split_transport_central_peripheral_event_handler(&bt_central, ev.source, ev.event);
}
}

View File

@@ -18,7 +18,7 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
#include <zmk/event_manager.h>
#include <zmk/battery.h>
#include <zmk/events/battery_state_changed.h>
#include <zmk/split/bluetooth/central.h>
#include <zmk/split/central.h>
static void blvl_ccc_cfg_changed(const struct bt_gatt_attr *attr, uint16_t value) {
ARG_UNUSED(attr);
@@ -32,7 +32,7 @@ static ssize_t read_blvl(struct bt_conn *conn, const struct bt_gatt_attr *attr,
uint16_t len, uint16_t offset) {
const uint8_t source = (uint8_t)(uint32_t)attr->user_data;
uint8_t level = 0;
int rc = zmk_split_get_peripheral_battery_level(source, &level);
int rc = zmk_split_central_get_peripheral_battery_level(source, &level);
if (rc == -EINVAL) {
LOG_ERR("Invalid peripheral index requested for battery level read: %d", source);

View File

@@ -20,6 +20,9 @@
#include <zephyr/bluetooth/gatt.h>
#include <zephyr/bluetooth/hci_types.h>
#include "peripheral.h"
#include "service.h"
#if IS_ENABLED(CONFIG_SETTINGS)
#include <zephyr/settings/settings.h>
@@ -70,6 +73,7 @@ static int start_advertising(bool low_duty) {
};
static bool low_duty_advertising = false;
static bool enabled = false;
static void advertising_cb(struct k_work *work) {
const int err = start_advertising(low_duty_advertising);
@@ -86,7 +90,7 @@ static void connected(struct bt_conn *conn, uint8_t err) {
raise_zmk_split_peripheral_status_changed(
(struct zmk_split_peripheral_status_changed){.connected = is_connected});
if (err == BT_HCI_ERR_ADV_TIMEOUT) {
if (err == BT_HCI_ERR_ADV_TIMEOUT && enabled) {
low_duty_advertising = true;
k_work_submit(&advertising_work);
}
@@ -104,8 +108,10 @@ static void disconnected(struct bt_conn *conn, uint8_t reason) {
raise_zmk_split_peripheral_status_changed(
(struct zmk_split_peripheral_status_changed){.connected = is_connected});
low_duty_advertising = false;
k_work_submit(&advertising_work);
if (enabled) {
low_duty_advertising = false;
k_work_submit(&advertising_work);
}
}
static void security_changed(struct bt_conn *conn, bt_security_t level, enum bt_security_err err) {
@@ -146,6 +152,85 @@ bool zmk_split_bt_peripheral_is_connected(void) { return is_connected; }
bool zmk_split_bt_peripheral_is_bonded(void) { return is_bonded; }
static zmk_split_transport_peripheral_status_changed_cb_t transport_status_cb;
static int
split_peripheral_bt_set_status_callback(zmk_split_transport_peripheral_status_changed_cb_t cb) {
transport_status_cb = cb;
return 0;
}
static void find_first_conn(struct bt_conn *conn, void *data) {
struct bt_conn **cp = (struct bt_conn **)data;
*cp = conn;
}
static int split_peripheral_bt_set_enabled(bool en) {
int err;
enabled = en;
if (en) {
k_work_submit(&advertising_work);
return 0;
} else {
struct bt_conn *conn = NULL;
bt_conn_foreach(BT_CONN_TYPE_LE, find_first_conn, &conn);
if (conn) {
err = bt_conn_disconnect(conn, BT_HCI_ERR_REMOTE_USER_TERM_CONN);
if (err < 0) {
LOG_WRN("Failed to disconnect connection to central (%d)", err);
}
}
err = bt_le_adv_stop();
if (err < 0) {
LOG_WRN("Failed to stop advertising (%d)", err);
}
return 0;
}
}
static void notify_transport_status(void);
static void notify_status_work_cb(struct k_work *_work) { notify_transport_status(); }
static K_WORK_DEFINE(notify_status_work, notify_status_work_cb);
static bool settings_loaded = false;
static struct zmk_split_transport_status split_peripheral_bt_get_status(void) {
return (struct zmk_split_transport_status){
.available = !IS_ENABLED(CONFIG_ZMK_BLE_CLEAR_BONDS_ON_START) && settings_loaded,
.enabled = enabled,
.connections = zmk_split_bt_peripheral_is_connected()
? ZMK_SPLIT_TRANSPORT_CONNECTIONS_STATUS_ALL_CONNECTED
: ZMK_SPLIT_TRANSPORT_CONNECTIONS_STATUS_DISCONNECTED,
};
}
static const struct zmk_split_transport_peripheral_api peripheral_api = {
.report_event = zmk_split_transport_peripheral_bt_report_event,
.set_enabled = split_peripheral_bt_set_enabled,
.set_status_callback = split_peripheral_bt_set_status_callback,
.get_status = split_peripheral_bt_get_status,
};
ZMK_SPLIT_TRANSPORT_PERIPHERAL_REGISTER(bt_peripheral, &peripheral_api,
CONFIG_ZMK_SPLIT_BLE_PRIORITY);
struct zmk_split_transport_peripheral *zmk_split_transport_peripheral_bt(void) {
return &bt_peripheral;
}
static void notify_transport_status(void) {
if (transport_status_cb) {
transport_status_cb(&bt_peripheral, split_peripheral_bt_get_status());
}
}
static int zmk_peripheral_ble_complete_startup(void) {
#if IS_ENABLED(CONFIG_ZMK_BLE_CLEAR_BONDS_ON_START)
LOG_WRN("Clearing all existing BLE bond information from the keyboard");
@@ -156,7 +241,9 @@ static int zmk_peripheral_ble_complete_startup(void) {
bt_conn_auth_info_cb_register(&zmk_peripheral_ble_auth_info_cb);
low_duty_advertising = false;
k_work_submit(&advertising_work);
settings_loaded = true;
k_work_submit(&notify_status_work);
#endif
return 0;

View File

@@ -0,0 +1,12 @@
/*
* Copyright (c) 2025 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#pragma once
#include <zmk/split/transport/types.h>
#include <zmk/split/transport/peripheral.h>
struct zmk_split_transport_peripheral *zmk_split_transport_peripheral_bt(void);

View File

@@ -18,12 +18,16 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
#include <zephyr/bluetooth/uuid.h>
#include <drivers/behavior.h>
#include <zmk/stdlib.h>
#include <zmk/behavior.h>
#include <zmk/matrix.h>
#include <zmk/physical_layouts.h>
#include <zmk/split/transport/peripheral.h>
#include <zmk/split/bluetooth/uuid.h>
#include <zmk/split/bluetooth/service.h>
#include "peripheral.h"
#if IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS)
#include <zmk/events/hid_indicators_changed.h>
#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS)
@@ -60,48 +64,7 @@ static ssize_t split_svc_pos_state(struct bt_conn *conn, const struct bt_gatt_at
static ssize_t split_svc_run_behavior(struct bt_conn *conn, const struct bt_gatt_attr *attrs,
const void *buf, uint16_t len, uint16_t offset,
uint8_t flags) {
struct zmk_split_run_behavior_payload *payload = attrs->user_data;
uint16_t end_addr = offset + len;
LOG_DBG("offset %d len %d", offset, len);
if (end_addr > sizeof(struct zmk_split_run_behavior_payload)) {
return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET);
}
memcpy(payload + offset, buf, len);
// We run if:
// 1: We've gotten all the position/state/param data.
// 2: We have a null terminated string for the behavior device label.
const size_t behavior_dev_offset =
offsetof(struct zmk_split_run_behavior_payload, behavior_dev);
if ((end_addr > sizeof(struct zmk_split_run_behavior_data)) &&
payload->behavior_dev[end_addr - behavior_dev_offset - 1] == '\0') {
struct zmk_behavior_binding binding = {
.param1 = payload->data.param1,
.param2 = payload->data.param2,
.behavior_dev = payload->behavior_dev,
};
LOG_DBG("%s with params %d %d: pressed? %d", binding.behavior_dev, binding.param1,
binding.param2, payload->data.state);
struct zmk_behavior_binding_event event = {.position = payload->data.position,
.timestamp = k_uptime_get()};
int err;
if (payload->data.state > 0) {
err = behavior_keymap_binding_pressed(&binding, event);
} else {
err = behavior_keymap_binding_released(&binding, event);
}
if (err) {
LOG_ERR("Failed to invoke behavior %s: %d", binding.behavior_dev, err);
}
}
return len;
}
uint8_t flags);
static ssize_t split_svc_num_of_positions(struct bt_conn *conn, const struct bt_gatt_attr *attrs,
void *buf, uint16_t len, uint16_t offset) {
@@ -285,12 +248,12 @@ int send_position_state() {
return 0;
}
int zmk_split_bt_position_pressed(uint8_t position) {
static int zmk_split_bt_position_pressed(uint8_t position) {
WRITE_BIT(position_state[position / 8], position % 8, true);
return send_position_state();
}
int zmk_split_bt_position_released(uint8_t position) {
static int zmk_split_bt_position_released(uint8_t position) {
WRITE_BIT(position_state[position / 8], position % 8, false);
return send_position_state();
}
@@ -332,9 +295,9 @@ int send_sensor_state(struct sensor_event ev) {
return 0;
}
int zmk_split_bt_sensor_triggered(uint8_t sensor_index,
const struct zmk_sensor_channel_data channel_data[],
size_t channel_data_size) {
static int zmk_split_bt_sensor_triggered(uint8_t sensor_index,
const struct zmk_sensor_channel_data channel_data[],
size_t channel_data_size) {
if (channel_data_size > ZMK_SENSOR_EVENT_MAX_CHANNELS) {
return -EINVAL;
}
@@ -349,7 +312,8 @@ int zmk_split_bt_sensor_triggered(uint8_t sensor_index,
#if IS_ENABLED(CONFIG_ZMK_INPUT_SPLIT)
int zmk_split_bt_report_input(uint8_t reg, uint8_t type, uint16_t code, int32_t value, bool sync) {
static int zmk_split_bt_report_input(uint8_t reg, uint8_t type, uint16_t code, int32_t value,
bool sync) {
for (size_t i = 0; i < split_svc.attr_count; i++) {
if (bt_uuid_cmp(split_svc.attrs[i].uuid,
@@ -380,3 +344,94 @@ static int service_init(void) {
}
SYS_INIT(service_init, APPLICATION, CONFIG_ZMK_BLE_INIT_PRIORITY);
int zmk_split_transport_peripheral_bt_report_event(
const struct zmk_split_transport_peripheral_event *ev) {
switch (ev->type) {
case ZMK_SPLIT_TRANSPORT_PERIPHERAL_EVENT_TYPE_KEY_POSITION_EVENT:
if (ev->data.key_position_event.pressed) {
zmk_split_bt_position_pressed(ev->data.key_position_event.position);
} else {
zmk_split_bt_position_released(ev->data.key_position_event.position);
}
break;
#if ZMK_KEYMAP_HAS_SENSORS
case ZMK_SPLIT_TRANSPORT_PERIPHERAL_EVENT_TYPE_SENSOR_EVENT:
zmk_split_bt_sensor_triggered(ev->data.sensor_event.sensor_index,
&ev->data.sensor_event.channel_data, 1);
break;
#endif
#if IS_ENABLED(CONFIG_ZMK_INPUT_SPLIT)
case ZMK_SPLIT_TRANSPORT_PERIPHERAL_EVENT_TYPE_INPUT_EVENT:
return zmk_split_bt_report_input(ev->data.input_event.reg, ev->data.input_event.type,
ev->data.input_event.code, ev->data.input_event.value,
ev->data.input_event.sync);
#endif
#if IS_ENABLED(CONFIG_ZMK_BATTERY_REPORTING)
case ZMK_SPLIT_TRANSPORT_PERIPHERAL_EVENT_TYPE_BATTERY_EVENT:
// The BLE transport uses standard BAS service for propagation, so just return success here.
return 0;
#endif
default:
LOG_WRN("Unhandled event type %d", ev->type);
return -ENOTSUP;
}
return 0;
}
static ssize_t split_svc_run_behavior(struct bt_conn *conn, const struct bt_gatt_attr *attrs,
const void *buf, uint16_t len, uint16_t offset,
uint8_t flags) {
struct zmk_split_run_behavior_payload *payload = attrs->user_data;
uint16_t end_addr = offset + len;
LOG_DBG("offset %d len %d", offset, len);
if (end_addr > sizeof(struct zmk_split_run_behavior_payload)) {
return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET);
}
memcpy(payload + offset, buf, len);
// We run if:
// 1: We've gotten all the position/state/param data.
// 2: We have a null terminated string for the behavior device label.
const size_t behavior_dev_offset =
offsetof(struct zmk_split_run_behavior_payload, behavior_dev);
if ((end_addr > sizeof(struct zmk_split_run_behavior_data)) &&
payload->behavior_dev[end_addr - behavior_dev_offset - 1] == '\0') {
struct zmk_split_transport_central_command cmd = {
.type = ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_INVOKE_BEHAVIOR,
.data = {.invoke_behavior = {
.param1 = payload->data.param1,
.param2 = payload->data.param2,
.position = payload->data.position,
.state = payload->data.state,
}}};
const size_t payload_dev_size = sizeof(cmd.data.invoke_behavior.behavior_dev);
if (strlcpy(cmd.data.invoke_behavior.behavior_dev, payload->behavior_dev,
payload_dev_size) >= payload_dev_size) {
LOG_ERR("Truncated behavior label %s to %s before invoking peripheral behavior",
payload->behavior_dev, cmd.data.invoke_behavior.behavior_dev);
}
LOG_DBG("%s with params %d %d: pressed? %d", cmd.data.invoke_behavior.behavior_dev,
cmd.data.invoke_behavior.param1, cmd.data.invoke_behavior.param2,
cmd.data.invoke_behavior.state);
int err = zmk_split_transport_peripheral_command_handler(
zmk_split_transport_peripheral_bt(), cmd);
if (err) {
LOG_ERR("Failed to invoke behavior %s: %d", payload->behavior_dev, err);
}
}
return len;
}

View File

@@ -0,0 +1,12 @@
/*
* Copyright (c) 2025 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#pragma once
#include <zmk/split/transport/types.h>
int zmk_split_transport_peripheral_bt_report_event(
const struct zmk_split_transport_peripheral_event *ev);

View File

@@ -1,47 +0,0 @@
/*
* Copyright (c) 2020 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#include <zephyr/device.h>
#include <zephyr/logging/log.h>
#include <zmk/split/bluetooth/service.h>
LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
#include <zmk/event_manager.h>
#include <zmk/events/position_state_changed.h>
#include <zmk/events/sensor_event.h>
#include <zmk/hid.h>
#include <zmk/sensors.h>
#include <zmk/endpoints.h>
int split_listener(const zmk_event_t *eh) {
LOG_DBG("");
const struct zmk_position_state_changed *pos_ev;
if ((pos_ev = as_zmk_position_state_changed(eh)) != NULL) {
if (pos_ev->state) {
return zmk_split_bt_position_pressed(pos_ev->position);
} else {
return zmk_split_bt_position_released(pos_ev->position);
}
}
#if ZMK_KEYMAP_HAS_SENSORS
const struct zmk_sensor_event *sensor_ev;
if ((sensor_ev = as_zmk_sensor_event(eh)) != NULL) {
return zmk_split_bt_sensor_triggered(sensor_ev->sensor_index, sensor_ev->channel_data,
sensor_ev->channel_data_size);
}
#endif /* ZMK_KEYMAP_HAS_SENSORS */
return ZMK_EV_EVENT_BUBBLE;
}
ZMK_LISTENER(split_listener, split_listener);
ZMK_SUBSCRIPTION(split_listener, zmk_position_state_changed);
#if ZMK_KEYMAP_HAS_SENSORS
ZMK_SUBSCRIPTION(split_listener, zmk_sensor_event);
#endif /* ZMK_KEYMAP_HAS_SENSORS */

227
app/src/split/central.c Normal file
View File

@@ -0,0 +1,227 @@
/*
* Copyright (c) 2025 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#include <errno.h>
#include <zmk/stdlib.h>
#include <zmk/split/transport/central.h>
#include <zmk/split/central.h>
#include <zmk/hid_indicators_types.h>
#include <zmk/pointing/input_split.h>
#include <zephyr/logging/log.h>
#include <zmk/event_manager.h>
#include <zmk/events/battery_state_changed.h>
#include <zmk/events/position_state_changed.h>
#include <zmk/events/sensor_event.h>
LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
const struct zmk_split_transport_central *active_transport;
#if IS_ENABLED(CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING)
static uint8_t peripheral_battery_levels[ZMK_SPLIT_CENTRAL_PERIPHERAL_COUNT] = {0};
#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING)
int zmk_split_transport_central_peripheral_event_handler(
const struct zmk_split_transport_central *transport, uint8_t source,
struct zmk_split_transport_peripheral_event ev) {
if (transport != active_transport) {
// Ignoring events from non-active transport
LOG_WRN("Ignoring peripheral event from non-active transport");
return -EINVAL;
}
switch (ev.type) {
case ZMK_SPLIT_TRANSPORT_PERIPHERAL_EVENT_TYPE_KEY_POSITION_EVENT: {
struct zmk_position_state_changed state_ev = {.source = source,
.position =
ev.data.key_position_event.position,
.state = ev.data.key_position_event.pressed,
.timestamp = k_uptime_get()};
return raise_zmk_position_state_changed(state_ev);
}
#if IS_ENABLED(CONFIG_ZMK_INPUT_SPLIT)
case ZMK_SPLIT_TRANSPORT_PERIPHERAL_EVENT_TYPE_INPUT_EVENT: {
return zmk_input_split_report_peripheral_event(
ev.data.input_event.reg, ev.data.input_event.type, ev.data.input_event.code,
ev.data.input_event.value, ev.data.input_event.sync);
}
#endif // IS_ENABLED(CONFIG_ZMK_POINTING)
#if IS_ENABLED(CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING)
case ZMK_SPLIT_TRANSPORT_PERIPHERAL_EVENT_TYPE_BATTERY_EVENT: {
struct zmk_peripheral_battery_state_changed battery_ev = {
.source = source,
.state_of_charge = ev.data.battery_event.level,
};
peripheral_battery_levels[source] = ev.data.battery_event.level;
return raise_zmk_peripheral_battery_state_changed(battery_ev);
}
#endif
case ZMK_SPLIT_TRANSPORT_PERIPHERAL_EVENT_TYPE_SENSOR_EVENT: {
struct zmk_sensor_event sensor_ev = {.sensor_index = ev.data.sensor_event.sensor_index,
.channel_data_size = 1,
.timestamp = k_uptime_get()};
sensor_ev.channel_data[0] = ev.data.sensor_event.channel_data;
return raise_zmk_sensor_event(sensor_ev);
}
default:
LOG_WRN("GOT AN UNKNOWN EVENT TYPE %d", ev.type);
return -ENOTSUP;
}
}
int zmk_split_central_invoke_behavior(uint8_t source, struct zmk_behavior_binding *binding,
struct zmk_behavior_binding_event event, bool state) {
if (!active_transport || !active_transport->api || !active_transport->api->send_command) {
return -ENODEV;
}
struct zmk_split_transport_central_command command =
(struct zmk_split_transport_central_command){
.type = ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_INVOKE_BEHAVIOR,
.data =
{
.invoke_behavior =
{
.param1 = binding->param1,
.param2 = binding->param2,
.position = event.position,
.event_source = event.source,
.state = state ? 1 : 0,
},
},
};
const size_t payload_dev_size = sizeof(command.data.invoke_behavior.behavior_dev);
if (strlcpy(command.data.invoke_behavior.behavior_dev, binding->behavior_dev,
payload_dev_size) >= payload_dev_size) {
LOG_ERR("Truncated behavior label %s to %s before invoking peripheral behavior",
binding->behavior_dev, command.data.invoke_behavior.behavior_dev);
}
return active_transport->api->send_command(source, command);
};
#if IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS)
int zmk_split_central_update_hid_indicator(zmk_hid_indicators_t indicators) {
if (!active_transport || !active_transport->api ||
!active_transport->api->get_available_source_ids || !active_transport->api->send_command) {
return -ENODEV;
}
uint8_t source_ids[ZMK_SPLIT_CENTRAL_PERIPHERAL_COUNT];
int ret = active_transport->api->get_available_source_ids(source_ids);
if (ret < 0) {
return ret;
}
struct zmk_split_transport_central_command command =
(struct zmk_split_transport_central_command){
.type = ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_SET_HID_INDICATORS,
.data =
{
.set_hid_indicators =
{
.indicators = indicators,
},
},
};
for (size_t i = 0; i < ret; i++) {
ret = active_transport->api->send_command(source_ids[i], command);
if (ret < 0) {
return ret;
}
}
return 0;
}
#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS)
#if IS_ENABLED(CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING)
int zmk_split_central_get_peripheral_battery_level(uint8_t source, uint8_t *level) {
if (source >= ARRAY_SIZE(peripheral_battery_levels)) {
return -EINVAL;
}
*level = peripheral_battery_levels[source];
return 0;
}
#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING)
static int select_first_available_transport(void) {
// Transports are sorted by priority, so find the first
// One that's available, and enable it. Any transport that
// Doesn't support `get_status` is assumed to be always
// available and fully connected.
STRUCT_SECTION_FOREACH(zmk_split_transport_central, t) {
if (!t->api->get_status || t->api->get_status().available) {
if (active_transport == t) {
LOG_DBG("First available is already selected, moving on");
return 0;
}
if (active_transport && active_transport->api->set_enabled) {
int err = active_transport->api->set_enabled(false);
if (err < 0) {
LOG_WRN("Error disabling previously selected split transport (%d)", err);
}
}
active_transport = t;
int err = 0;
if (active_transport->api->set_enabled) {
err = active_transport->api->set_enabled(true);
}
return err;
}
}
return -ENODEV;
}
static int transport_status_changed_cb(const struct zmk_split_transport_central *central,
struct zmk_split_transport_status status) {
if (central == active_transport) {
LOG_DBG("Central at %p changed status: enabled %d, available %d, connections %d", central,
status.enabled, status.available, status.connections);
if (status.connections == ZMK_SPLIT_TRANSPORT_CONNECTIONS_STATUS_DISCONNECTED) {
return select_first_available_transport();
}
} else {
// Just to be sure, in case a higher priority transport becomes available
select_first_available_transport();
}
return 0;
}
static int central_init(void) {
STRUCT_SECTION_FOREACH(zmk_split_transport_central, t) {
if (!t->api->set_status_callback) {
continue;
}
t->api->set_status_callback(transport_status_changed_cb);
}
return select_first_available_transport();
}
SYS_INIT(central_init, APPLICATION, CONFIG_KERNEL_INIT_PRIORITY_DEFAULT);

190
app/src/split/peripheral.c Normal file
View File

@@ -0,0 +1,190 @@
/*
* Copyright (c) 2025 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#include <errno.h>
#include <zmk/stdlib.h>
#include <zmk/split/transport/peripheral.h>
#include <drivers/behavior.h>
#include <zmk/behavior.h>
#include <zmk/event_manager.h>
#include <zmk/events/position_state_changed.h>
#include <zmk/events/sensor_event.h>
#include <zmk/events/battery_state_changed.h>
#include <zephyr/init.h>
#include <zephyr/logging/log.h>
LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
const struct zmk_split_transport_peripheral *active_transport;
int zmk_split_transport_peripheral_command_handler(
const struct zmk_split_transport_peripheral *transport,
struct zmk_split_transport_central_command cmd) {
LOG_DBG("");
switch (cmd.type) {
case ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_INVOKE_BEHAVIOR: {
struct zmk_behavior_binding binding = {
.param1 = cmd.data.invoke_behavior.param1,
.param2 = cmd.data.invoke_behavior.param2,
.behavior_dev = cmd.data.invoke_behavior.behavior_dev,
};
LOG_DBG("%s with params %d %d: pressed? %d", binding.behavior_dev, binding.param1,
binding.param2, cmd.data.invoke_behavior.state);
struct zmk_behavior_binding_event event = {.position = cmd.data.invoke_behavior.position,
.timestamp = k_uptime_get()};
int err;
if (cmd.data.invoke_behavior.state > 0) {
err = behavior_keymap_binding_pressed(&binding, event);
} else {
err = behavior_keymap_binding_released(&binding, event);
}
if (err) {
LOG_ERR("Failed to invoke behavior %s: %d", binding.behavior_dev, err);
}
}
default:
LOG_WRN("Unhandled command type %d", cmd.type);
return -ENOTSUP;
}
return 0;
}
int zmk_split_peripheral_report_event(const struct zmk_split_transport_peripheral_event *event) {
if (!active_transport || !active_transport->api || !active_transport->api->report_event) {
LOG_WRN("No active transport that supports reporting events!");
return -ENODEV;
}
return active_transport->api->report_event(event);
}
static int select_first_available_transport(void) {
// Transports are sorted by priority, so find the first
// One that's available, and enable it. Any transport that
// Doesn't support `get_status` is assumed to be always
// available and fully connected.
STRUCT_SECTION_FOREACH(zmk_split_transport_peripheral, t) {
if (!t->api->get_status || t->api->get_status().available) {
if (active_transport == t) {
LOG_DBG("First available is already selected, moving on");
return 0;
}
if (active_transport && active_transport->api->set_enabled) {
int err = active_transport->api->set_enabled(false);
if (err < 0) {
LOG_WRN("Error disabling previously selected split transport (%d)", err);
}
}
active_transport = t;
int err = 0;
if (active_transport->api->set_enabled) {
err = active_transport->api->set_enabled(true);
}
return err;
}
}
return -ENODEV;
}
static int transport_status_changed_cb(const struct zmk_split_transport_peripheral *p,
struct zmk_split_transport_status status) {
if (p == active_transport) {
LOG_DBG("Peripheral at %p changed status: enabled %d, available %d, connections %d", p,
status.enabled, status.available, status.connections);
if (status.connections == ZMK_SPLIT_TRANSPORT_CONNECTIONS_STATUS_DISCONNECTED) {
LOG_DBG("Find us a new active transport!");
return select_first_available_transport();
}
} else {
select_first_available_transport();
}
return 0;
}
static int peripheral_init(void) {
STRUCT_SECTION_FOREACH(zmk_split_transport_peripheral, t) {
if (!t->api->set_status_callback) {
continue;
}
t->api->set_status_callback(transport_status_changed_cb);
}
return select_first_available_transport();
}
SYS_INIT(peripheral_init, APPLICATION, CONFIG_KERNEL_INIT_PRIORITY_DEFAULT);
int split_peripheral_listener(const zmk_event_t *eh) {
LOG_DBG("");
const struct zmk_position_state_changed *pos_ev;
if ((pos_ev = as_zmk_position_state_changed(eh)) != NULL) {
struct zmk_split_transport_peripheral_event ev = {
.type = ZMK_SPLIT_TRANSPORT_PERIPHERAL_EVENT_TYPE_KEY_POSITION_EVENT,
.data = {.key_position_event = {
.position = pos_ev->position,
.pressed = pos_ev->state,
}}};
zmk_split_peripheral_report_event(&ev);
}
#if ZMK_KEYMAP_HAS_SENSORS
const struct zmk_sensor_event *sensor_ev;
if ((sensor_ev = as_zmk_sensor_event(eh)) != NULL) {
if (sensor_ev->channel_data_size != 1) {
return -ENOTSUP;
}
struct zmk_split_transport_peripheral_event ev = {
.type = ZMK_SPLIT_TRANSPORT_PERIPHERAL_EVENT_TYPE_SENSOR_EVENT,
.data = {.sensor_event = {
.channel_data = sensor_ev->channel_data[0],
.sensor_index = sensor_ev->sensor_index,
}}};
zmk_split_peripheral_report_event(&ev);
}
#endif /* ZMK_KEYMAP_HAS_SENSORS */
#if IS_ENABLED(CONFIG_ZMK_BATTERY_REPORTING)
const struct zmk_battery_state_changed *battery_ev;
if ((battery_ev = as_zmk_battery_state_changed(eh)) != NULL) {
struct zmk_split_transport_peripheral_event ev = {
.type = ZMK_SPLIT_TRANSPORT_PERIPHERAL_EVENT_TYPE_BATTERY_EVENT,
.data = {.battery_event = {
.level = battery_ev->state_of_charge,
}}};
zmk_split_peripheral_report_event(&ev);
}
#endif // IS_ENABLED(CONFIG_ZMK_BATTERY_REPORTING)
return ZMK_EV_EVENT_BUBBLE;
}
ZMK_LISTENER(split_peripheral, split_peripheral_listener);
ZMK_SUBSCRIPTION(split_peripheral, zmk_position_state_changed);
#if ZMK_KEYMAP_HAS_SENSORS
ZMK_SUBSCRIPTION(split_peripheral, zmk_sensor_event);
#endif /* ZMK_KEYMAP_HAS_SENSORS */
#if IS_ENABLED(CONFIG_ZMK_BATTERY_REPORTING)
ZMK_SUBSCRIPTION(split_peripheral, zmk_battery_state_changed);
#endif

View File

@@ -0,0 +1,6 @@
# Copyright (c) 2025 The ZMK Contributors
# SPDX-License-Identifier: MIT
target_sources(app PRIVATE wired.c)
target_sources_ifdef(CONFIG_ZMK_SPLIT_ROLE_CENTRAL app PRIVATE central.c)
target_sources_ifndef(CONFIG_ZMK_SPLIT_ROLE_CENTRAL app PRIVATE peripheral.c)

View File

@@ -0,0 +1,57 @@
# Copyright (c) 2025 The ZMK Contributors
# SPDX-License-Identifier: MIT
if ZMK_SPLIT_WIRED
config ZMK_SPLIT_WIRED_PRIORITY
int "Wired transport priority"
help
Lower number priorities transports are favored over higher numbers.
choice
prompt "UART Mode"
config ZMK_SPLIT_WIRED_UART_MODE_ASYNC
bool "Async (DMA) Mode"
# For now, don't use async/DMA on nRF52 due to RX bug (fixed
# in newer Zephyr version?)
depends on SERIAL_SUPPORT_ASYNC
select UART_ASYNC_API
config ZMK_SPLIT_WIRED_UART_MODE_INTERRUPT
bool "Interrupt Mode"
depends on SERIAL_SUPPORT_INTERRUPT
select UART_INTERRUPT_DRIVEN
config ZMK_SPLIT_WIRED_UART_MODE_POLLING
bool "Polling Mode"
endchoice
if ZMK_SPLIT_WIRED_UART_MODE_POLLING
config ZMK_SPLIT_WIRED_POLLING_RX_PERIOD
int "Ticks between RX polls"
endif
if ZMK_SPLIT_WIRED_UART_MODE_ASYNC
config ZMK_SPLIT_WIRED_ASYNC_RX_TIMEOUT
int "RX Timeout (in microseconds) before reporting received data"
endif
config ZMK_SPLIT_WIRED_CMD_BUFFER_ITEMS
int "Number of central commands to buffer for TX/RX"
config ZMK_SPLIT_WIRED_EVENT_BUFFER_ITEMS
int "Number of peripheral events to buffer for TX/RX"
config ZMK_SPLIT_WIRED_HALF_DUPLEX_RX_TIMEOUT
int "RX timeout (in ms) when polling peripheral(s) and waiting for any response"
config ZMK_SPLIT_WIRED_HALF_DUPLEX_RX_COMPLETE_TIMEOUT
int "RX complete timeout (in ticks) when polling peripheral(s) after receiving some response data"
endif

View File

@@ -0,0 +1,37 @@
# Copyright (c) 2025 The ZMK Contributors
# SPDX-License-Identifier: MIT
if ZMK_SPLIT_WIRED
config ZMK_SPLIT_WIRED_PRIORITY
default 0
config ZMK_SPLIT_WIRED_CMD_BUFFER_ITEMS
default 4
config ZMK_SPLIT_WIRED_EVENT_BUFFER_ITEMS
default 16
if ZMK_SPLIT_WIRED_UART_MODE_POLLING
config ZMK_SPLIT_WIRED_POLLING_RX_PERIOD
default 10
endif
if ZMK_SPLIT_WIRED_UART_MODE_ASYNC
config ZMK_SPLIT_WIRED_ASYNC_RX_TIMEOUT
default 20
endif
config ZMK_SPLIT_WIRED_HALF_DUPLEX_RX_TIMEOUT
default 15
config ZMK_SPLIT_WIRED_HALF_DUPLEX_RX_COMPLETE_TIMEOUT
default 20
endif

View File

@@ -0,0 +1,487 @@
/*
* Copyright (c) 2025 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#include <zephyr/types.h>
#include <zephyr/init.h>
#include <zephyr/pm/device.h>
#include <zephyr/pm/device_runtime.h>
#include <zephyr/settings/settings.h>
#include <zephyr/sys/crc.h>
#include <zephyr/sys/ring_buffer.h>
#include <zephyr/logging/log.h>
LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
#include <zephyr/drivers/gpio.h>
#include <zephyr/drivers/uart.h>
#include <zmk/stdlib.h>
#include <zmk/behavior.h>
#include <zmk/sensors.h>
#include <zmk/split/transport/central.h>
#include <zmk/event_manager.h>
#include <zmk/events/position_state_changed.h>
#include <zmk/events/sensor_event.h>
#include <zmk/pointing/input_split.h>
#include <zmk/hid_indicators_types.h>
#include <zmk/physical_layouts.h>
#include "wired.h"
#define DT_DRV_COMPAT zmk_wired_split
#define IS_HALF_DUPLEX_MODE \
(DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) && DT_INST_PROP_OR(0, half_duplex, false))
#define RX_BUFFER_SIZE \
((sizeof(struct event_envelope) + sizeof(struct msg_postfix)) * \
CONFIG_ZMK_SPLIT_WIRED_EVENT_BUFFER_ITEMS)
#define TX_BUFFER_SIZE \
((sizeof(struct command_envelope) + sizeof(struct msg_postfix)) * \
CONFIG_ZMK_SPLIT_WIRED_CMD_BUFFER_ITEMS)
#if IS_HALF_DUPLEX_MODE
static K_SEM_DEFINE(tx_sem, 0, 1);
#endif
RING_BUF_DECLARE(rx_buf, RX_BUFFER_SIZE);
RING_BUF_DECLARE(tx_buf, TX_BUFFER_SIZE);
#if DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT)
static const struct device *uart = DEVICE_DT_GET(DT_INST_PHANDLE(0, device));
#define HAS_DIR_GPIO (IS_HALF_DUPLEX_MODE && DT_INST_NODE_HAS_PROP(0, dir_gpios))
#if HAS_DIR_GPIO
static const struct gpio_dt_spec dir_gpio = GPIO_DT_SPEC_INST_GET(0, dir_gpios);
#endif
#define HAS_DETECT_GPIO DT_INST_NODE_HAS_PROP(0, detect_gpios)
#if HAS_DETECT_GPIO
static const struct gpio_dt_spec detect_gpio = GPIO_DT_SPEC_INST_GET(0, detect_gpios);
#endif
#else
#error \
"Need to create a node with compatible of 'zmk,wired-split` with a `device` property set to an enabled UART. See http://zmk.dev/docs/development/hardware-integration/new-shield#wired-split"
#endif
static void publish_events_work(struct k_work *work);
K_WORK_DEFINE(publish_events, publish_events_work);
#if IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_ASYNC)
uint8_t async_rx_buf[RX_BUFFER_SIZE / 2][2];
static struct zmk_split_wired_async_state async_state = {
.process_tx_work = &publish_events,
.rx_bufs = {async_rx_buf[0], async_rx_buf[1]},
.rx_bufs_len = RX_BUFFER_SIZE / 2,
.rx_size_process_trigger = MSG_EXTRA_SIZE + 1,
.rx_buf = &rx_buf,
.tx_buf = &tx_buf,
#if HAS_DIR_GPIO
.dir_gpio = &dir_gpio,
#endif
};
#endif
#if IS_HALF_DUPLEX_MODE
static int can_tx(void) { return k_sem_take(&tx_sem, K_NO_WAIT); }
#else
static inline int can_tx(void) { return 0; }
#endif
#if IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_POLLING)
static void send_pending_tx_work_cb(struct k_work *work);
static K_WORK_DEFINE(wired_central_tx_work, send_pending_tx_work_cb);
static void read_timer_cb(struct k_timer *_timer) {
zmk_split_wired_poll_in(&rx_buf, uart, &publish_events, NULL);
// Check if we found any bytes, read some, or read all the bytes in the RX
}
static K_TIMER_DEFINE(wired_central_read_timer, read_timer_cb, NULL);
#endif
static void begin_tx(void) {
#if IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_INTERRUPT)
uart_irq_tx_enable(uart);
#elif IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_ASYNC)
zmk_split_wired_async_tx(&async_state);
#else
k_work_submit(&wired_central_tx_work);
#endif
}
static void begin_rx(void) {
#if IS_ENABLED(CONFIG_PM_DEVICE_RUNTIME)
pm_device_runtime_get(uart);
#elif IS_ENABLED(CONFIG_PM_DEVICE)
pm_device_action_run(uart, PM_DEVICE_ACTION_RESUME);
#endif // IS_ENABLED(CONFIG_PM_DEVICE)
#if IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_INTERRUPT)
uart_irq_rx_enable(uart);
#elif IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_ASYNC)
zmk_split_wired_async_rx(&async_state);
#else
k_timer_start(&wired_central_read_timer, K_TICKS(CONFIG_ZMK_SPLIT_WIRED_POLLING_RX_PERIOD),
K_TICKS(CONFIG_ZMK_SPLIT_WIRED_POLLING_RX_PERIOD));
#endif
}
#if HAS_DETECT_GPIO
static void stop_rx(void) {
#if IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_INTERRUPT)
uart_irq_rx_disable(uart);
#elif IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_ASYNC)
zmk_split_wired_async_rx_cancel(&async_state);
#else
k_timer_stop(&wired_central_read_timer);
#endif
#if IS_ENABLED(CONFIG_PM_DEVICE_RUNTIME)
pm_device_runtime_put(uart);
#elif IS_ENABLED(CONFIG_PM_DEVICE)
pm_device_action_run(uart, PM_DEVICE_ACTION_SUSPEND);
#endif // IS_ENABLED(CONFIG_PM_DEVICE)
}
#endif // HAS_DETECT_GPIO
static ssize_t get_payload_data_size(const struct zmk_split_transport_central_command *cmd) {
switch (cmd->type) {
case ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_POLL_EVENTS:
return 0;
case ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_INVOKE_BEHAVIOR:
return sizeof(cmd->data.invoke_behavior);
case ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_SET_PHYSICAL_LAYOUT:
return sizeof(cmd->data.set_physical_layout);
case ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_SET_HID_INDICATORS:
return sizeof(cmd->data.set_hid_indicators);
default:
return -ENOTSUP;
}
}
static int split_central_wired_send_command(uint8_t source,
struct zmk_split_transport_central_command cmd) {
if (source != 0) {
return -EINVAL;
}
ssize_t data_size = get_payload_data_size(&cmd);
if (data_size < 0) {
LOG_WRN("Failed to determine payload data size %d", data_size);
return data_size;
}
// Data + type + source
size_t payload_size =
data_size + sizeof(source) + sizeof(enum zmk_split_transport_central_command_type);
if (ring_buf_space_get(&tx_buf) < MSG_EXTRA_SIZE + payload_size) {
LOG_WRN("No room to send command to the peripheral %d", source);
return -ENOSPC;
}
struct command_envelope env = {.prefix =
{
.magic_prefix = ZMK_SPLIT_WIRED_ENVELOPE_MAGIC_PREFIX,
.payload_size = payload_size,
},
.payload = {
.source = source,
.cmd = cmd,
}};
struct msg_postfix postfix = {.crc =
crc32_ieee((void *)&env, sizeof(env.prefix) + payload_size)};
ring_buf_put(&tx_buf, (uint8_t *)&env, sizeof(env.prefix) + payload_size);
ring_buf_put(&tx_buf, (uint8_t *)&postfix, sizeof(postfix));
if (can_tx() >= 0) {
begin_tx();
}
return 0;
}
#if IS_HALF_DUPLEX_MODE
void rx_done_cb(struct k_work *work);
static K_WORK_DELAYABLE_DEFINE(rx_done_work, rx_done_cb);
void rx_done_cb(struct k_work *work) {
k_sem_give(&tx_sem);
// Poll for the next event data!
split_central_wired_send_command(0,
(struct zmk_split_transport_central_command){
.type = ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_POLL_EVENTS,
});
k_work_reschedule(&rx_done_work, K_MSEC(CONFIG_ZMK_SPLIT_WIRED_HALF_DUPLEX_RX_TIMEOUT));
}
#endif
#if IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_INTERRUPT)
static void serial_cb(const struct device *dev, void *user_data) {
while (uart_irq_update(dev) && uart_irq_is_pending(dev)) {
if (uart_irq_rx_ready(dev)) {
zmk_split_wired_fifo_read(dev, &rx_buf, &publish_events, NULL);
#if IS_HALF_DUPLEX_MODE
k_work_reschedule(&rx_done_work,
K_TICKS(CONFIG_ZMK_SPLIT_WIRED_HALF_DUPLEX_RX_COMPLETE_TIMEOUT));
#endif
}
if (uart_irq_tx_complete(dev)) {
if (ring_buf_size_get(&tx_buf) == 0) {
uart_irq_tx_disable(dev);
#if HAS_DIR_GPIO
gpio_pin_set_dt(&dir_gpio, 0);
#endif
}
}
if (uart_irq_tx_ready(dev)) {
#if HAS_DIR_GPIO
gpio_pin_set_dt(&dir_gpio, 1);
#endif
zmk_split_wired_fifo_fill(dev, &tx_buf);
}
}
}
#elif IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_POLLING)
static void send_pending_tx_work_cb(struct k_work *work) {
zmk_split_wired_poll_out(&tx_buf, uart);
}
#endif
#if HAS_DETECT_GPIO
static void notify_transport_status(void);
static struct gpio_callback detect_callback;
static void notify_status_work_cb(struct k_work *_work) { notify_transport_status(); }
static K_WORK_DEFINE(notify_status_work, notify_status_work_cb);
static void detect_pin_irq_callback_handler(const struct device *port, struct gpio_callback *cb,
const gpio_port_pins_t pin) {
k_work_submit(&notify_status_work);
}
#endif
static int zmk_split_wired_central_init(void) {
if (!device_is_ready(uart)) {
return -ENODEV;
}
#if IS_ENABLED(CONFIG_PM_DEVICE_RUNTIME)
pm_device_runtime_put(uart);
#elif IS_ENABLED(CONFIG_PM_DEVICE)
pm_device_action_run(uart, PM_DEVICE_ACTION_SUSPEND);
#endif // IS_ENABLED(CONFIG_PM_DEVICE)
#if IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_INTERRUPT)
int ret = uart_irq_callback_user_data_set(uart, serial_cb, NULL);
if (ret < 0) {
if (ret == -ENOTSUP) {
LOG_ERR("Interrupt-driven UART API support not enabled");
} else if (ret == -ENOSYS) {
LOG_ERR("UART device does not support interrupt-driven API");
} else {
LOG_ERR("Error setting UART callback: %d\n", ret);
}
return ret;
}
uart_irq_rx_enable(uart);
#elif IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_ASYNC)
async_state.uart = uart;
int ret = zmk_split_wired_async_init(&async_state);
if (ret < 0) {
LOG_ERR("Failed to set up async wired split UART (%d)", ret);
return ret;
}
#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_*)
#if IS_HALF_DUPLEX_MODE
#if HAS_DIR_GPIO
gpio_pin_configure_dt(&dir_gpio, GPIO_OUTPUT_INACTIVE);
#endif
#endif // IS_HALF_DUPLEX_MODE
#if HAS_DETECT_GPIO
gpio_pin_configure_dt(&detect_gpio, GPIO_INPUT);
gpio_init_callback(&detect_callback, detect_pin_irq_callback_handler, BIT(detect_gpio.pin));
int err = gpio_add_callback(detect_gpio.port, &detect_callback);
if (err) {
LOG_ERR("Error adding the callback to the detect pin: %i", err);
return err;
}
err = gpio_pin_interrupt_configure_dt(&detect_gpio, GPIO_INT_EDGE_BOTH);
if (err < 0) {
LOG_WRN("Failed to so configure interrupt for detection pin (%d)", err);
return err;
}
#endif // HAS_DETECT_GPIO
return 0;
}
SYS_INIT(zmk_split_wired_central_init, APPLICATION, CONFIG_KERNEL_INIT_PRIORITY_DEFAULT);
static int split_central_wired_get_available_source_ids(uint8_t *sources) {
sources[0] = 0;
return 1;
}
static int split_central_wired_set_enabled(bool enabled) {
if (enabled) {
begin_rx();
#if IS_HALF_DUPLEX_MODE
k_work_schedule(&rx_done_work, K_MSEC(CONFIG_ZMK_SPLIT_WIRED_HALF_DUPLEX_RX_TIMEOUT));
#endif
return 0;
#if HAS_DETECT_GPIO
} else {
#if IS_HALF_DUPLEX_MODE
k_work_cancel_delayable(&rx_done_work);
#endif
stop_rx();
return 0;
#endif
}
return -ENOTSUP;
}
#if HAS_DETECT_GPIO
static zmk_split_transport_central_status_changed_cb_t transport_status_cb;
static int
split_central_wired_set_status_callback(zmk_split_transport_central_status_changed_cb_t cb) {
transport_status_cb = cb;
return 0;
}
static struct zmk_split_transport_status split_central_wired_get_status() {
int detected = gpio_pin_get_dt(&detect_gpio);
if (detected > 0) {
return (struct zmk_split_transport_status){
.available = true,
.enabled = true, // Track this
.connections = ZMK_SPLIT_TRANSPORT_CONNECTIONS_STATUS_ALL_CONNECTED,
};
} else {
return (struct zmk_split_transport_status){
.available = false,
.enabled = true, // Track this
.connections = ZMK_SPLIT_TRANSPORT_CONNECTIONS_STATUS_DISCONNECTED,
};
}
}
#endif // HAS_DETECT_GPIO
static const struct zmk_split_transport_central_api central_api = {
.send_command = split_central_wired_send_command,
.get_available_source_ids = split_central_wired_get_available_source_ids,
.set_enabled = split_central_wired_set_enabled,
#if HAS_DETECT_GPIO
.set_status_callback = split_central_wired_set_status_callback,
.get_status = split_central_wired_get_status,
#endif // HAS_DETECT_GPIO
};
ZMK_SPLIT_TRANSPORT_CENTRAL_REGISTER(wired_central, &central_api, CONFIG_ZMK_SPLIT_WIRED_PRIORITY);
#if HAS_DETECT_GPIO
static void notify_transport_status(void) {
if (transport_status_cb) {
transport_status_cb(&wired_central, split_central_wired_get_status());
}
}
#endif
static void publish_events_work(struct k_work *work) {
#if IS_HALF_DUPLEX_MODE
k_work_reschedule(&rx_done_work,
K_MSEC(CONFIG_ZMK_SPLIT_WIRED_HALF_DUPLEX_RX_COMPLETE_TIMEOUT));
#endif // IS_HALF_DUPLEX_MODE
while (ring_buf_size_get(&rx_buf) > MSG_EXTRA_SIZE) {
struct event_envelope env;
int item_err =
zmk_split_wired_get_item(&rx_buf, (uint8_t *)&env, sizeof(struct event_envelope));
switch (item_err) {
case 0:
zmk_split_transport_central_peripheral_event_handler(&wired_central, env.payload.source,
env.payload.event);
break;
case -EAGAIN:
return;
default:
LOG_WRN("Issue fetching an item from the RX buffer: %d", item_err);
return;
}
}
}

View File

@@ -0,0 +1,456 @@
/*
* Copyright (c) 2025 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#include <zephyr/types.h>
#include <zephyr/init.h>
#include <zephyr/pm/device.h>
#include <zephyr/pm/device_runtime.h>
#include <zephyr/settings/settings.h>
#include <zephyr/sys/crc.h>
#include <zephyr/sys/ring_buffer.h>
#include <zephyr/logging/log.h>
LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
#include <zephyr/drivers/gpio.h>
#include <zephyr/drivers/uart.h>
#include <zmk/stdlib.h>
#include <zmk/behavior.h>
#include <zmk/sensors.h>
#include <zmk/split/transport/peripheral.h>
#include <zmk/split/transport/types.h>
#include <zmk/event_manager.h>
#include <zmk/events/position_state_changed.h>
#include <zmk/events/sensor_event.h>
#include <zmk/pointing/input_split.h>
#include <zmk/hid_indicators_types.h>
#include <zmk/physical_layouts.h>
#include "wired.h"
#define DT_DRV_COMPAT zmk_wired_split
#define IS_HALF_DUPLEX_MODE \
(DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) && DT_INST_PROP_OR(0, half_duplex, false))
#define TX_BUFFER_SIZE \
((sizeof(struct event_envelope) + sizeof(struct msg_postfix)) * \
CONFIG_ZMK_SPLIT_WIRED_EVENT_BUFFER_ITEMS)
#define RX_BUFFER_SIZE \
((sizeof(struct command_envelope) + sizeof(struct msg_postfix)) * \
CONFIG_ZMK_SPLIT_WIRED_CMD_BUFFER_ITEMS)
RING_BUF_DECLARE(chosen_rx_buf, RX_BUFFER_SIZE);
RING_BUF_DECLARE(chosen_tx_buf, TX_BUFFER_SIZE);
static const uint8_t peripheral_id = 0;
K_SEM_DEFINE(tx_sem, 0, 1);
#if DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT)
static const struct device *uart = DEVICE_DT_GET(DT_INST_PHANDLE(0, device));
#define HAS_DIR_GPIO (DT_INST_NODE_HAS_PROP(0, dir_gpios))
#if HAS_DIR_GPIO
static const struct gpio_dt_spec dir_gpio = GPIO_DT_SPEC_INST_GET(0, dir_gpios);
#endif
#define HAS_DETECT_GPIO DT_INST_NODE_HAS_PROP(0, detect_gpios)
#if HAS_DETECT_GPIO
static const struct gpio_dt_spec detect_gpio = GPIO_DT_SPEC_INST_GET(0, detect_gpios);
#endif
#else
#error \
"Need to create a node with compatible of 'zmk,wired-split` with a `device` property set to an enabled UART. See http://zmk.dev/docs/development/hardware-integration/new-shield#wired-split"
#endif
static void publish_commands_work(struct k_work *work);
K_WORK_DEFINE(publish_commands, publish_commands_work);
static void process_tx_cb(void);
K_MSGQ_DEFINE(cmd_msg_queue, sizeof(struct zmk_split_transport_central_command), 3, 4);
#if IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_ASYNC)
uint8_t async_rx_buf[RX_BUFFER_SIZE / 2][2];
static struct zmk_split_wired_async_state async_state = {
.rx_bufs = {async_rx_buf[0], async_rx_buf[1]},
.rx_bufs_len = RX_BUFFER_SIZE / 2,
.rx_size_process_trigger = sizeof(struct command_envelope),
.process_tx_callback = process_tx_cb,
.rx_buf = &chosen_rx_buf,
.tx_buf = &chosen_tx_buf,
#if HAS_DIR_GPIO
.dir_gpio = &dir_gpio,
#endif
};
#endif
#if IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_POLLING)
static void wired_peripheral_read_tick_cb(struct k_timer *timer) {
zmk_split_wired_poll_in(&chosen_rx_buf, uart, NULL, process_tx_cb);
}
static K_TIMER_DEFINE(wired_peripheral_read_timer, wired_peripheral_read_tick_cb, NULL);
#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_POLLING)
static void begin_rx(void) {
#if IS_ENABLED(CONFIG_PM_DEVICE_RUNTIME)
pm_device_runtime_get(uart);
#elif IS_ENABLED(CONFIG_PM_DEVICE)
pm_device_action_run(uart, PM_DEVICE_ACTION_RESUME);
#endif // IS_ENABLED(CONFIG_PM_DEVICE)
#if IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_INTERRUPT)
uart_irq_rx_enable(uart);
#elif IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_ASYNC)
zmk_split_wired_async_rx(&async_state);
#else
k_timer_start(&wired_peripheral_read_timer, K_TICKS(CONFIG_ZMK_SPLIT_WIRED_POLLING_RX_PERIOD),
K_TICKS(CONFIG_ZMK_SPLIT_WIRED_POLLING_RX_PERIOD));
#endif
}
#if HAS_DETECT_GPIO
static void stop_rx(void) {
#if IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_INTERRUPT)
uart_irq_rx_disable(uart);
#elif IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_ASYNC)
zmk_split_wired_async_rx_cancel(&async_state);
#else
k_timer_stop(&wired_peripheral_read_timer);
#endif
#if IS_ENABLED(CONFIG_PM_DEVICE_RUNTIME)
pm_device_runtime_put(uart);
#elif IS_ENABLED(CONFIG_PM_DEVICE)
pm_device_action_run(uart, PM_DEVICE_ACTION_SUSPEND);
#endif // IS_ENABLED(CONFIG_PM_DEVICE)
}
#endif // HAS_DETECT_GPIO
#if IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_INTERRUPT)
static void serial_cb(const struct device *dev, void *user_data) {
while (uart_irq_update(dev) && uart_irq_is_pending(dev)) {
if (uart_irq_rx_ready(dev)) {
zmk_split_wired_fifo_read(dev, &chosen_rx_buf, NULL, process_tx_cb);
}
if (uart_irq_tx_complete(dev)) {
if (ring_buf_size_get(&chosen_tx_buf) == 0) {
uart_irq_tx_disable(dev);
}
#if HAS_DIR_GPIO
gpio_pin_set_dt(&dir_gpio, 0);
#endif
}
if (uart_irq_tx_ready(dev)) {
#if HAS_DIR_GPIO
gpio_pin_set_dt(&dir_gpio, 1);
#endif
zmk_split_wired_fifo_fill(dev, &chosen_tx_buf);
}
}
}
#elif IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_POLLING)
static void send_pending_tx_work_cb(struct k_work *work) {
zmk_split_wired_poll_out(&chosen_tx_buf, uart);
}
static K_WORK_DEFINE(send_pending_tx, send_pending_tx_work_cb);
#endif
#if HAS_DETECT_GPIO
static void notify_transport_status(void);
static struct gpio_callback detect_callback;
static void notify_status_work_cb(struct k_work *_work) { notify_transport_status(); }
static K_WORK_DEFINE(notify_status_work, notify_status_work_cb);
static void detect_pin_irq_callback_handler(const struct device *port, struct gpio_callback *cb,
const gpio_port_pins_t pin) {
k_work_submit(&notify_status_work);
}
#endif
static int zmk_split_wired_peripheral_init(void) {
if (!device_is_ready(uart)) {
return -ENODEV;
}
#if IS_ENABLED(CONFIG_PM_DEVICE_RUNTIME)
pm_device_runtime_put(uart);
#elif IS_ENABLED(CONFIG_PM_DEVICE)
pm_device_action_run(uart, PM_DEVICE_ACTION_SUSPEND);
#endif // IS_ENABLED(CONFIG_PM_DEVICE)
#if HAS_DIR_GPIO
gpio_pin_configure_dt(&dir_gpio, GPIO_OUTPUT_INACTIVE);
#endif
#if IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_INTERRUPT)
/* configure interrupt and callback to receive data */
int ret = uart_irq_callback_user_data_set(uart, serial_cb, NULL);
if (ret < 0) {
if (ret == -ENOTSUP) {
LOG_ERR("Interrupt-driven UART API support not enabled");
} else if (ret == -ENOSYS) {
LOG_ERR("UART device does not support interrupt-driven API");
} else {
LOG_ERR("Error setting UART callback: %d\n", ret);
}
return ret;
}
#elif IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_ASYNC)
async_state.uart = uart;
int ret = zmk_split_wired_async_init(&async_state);
if (ret < 0) {
LOG_ERR("Failed to set up async wired split UART (%d)", ret);
return ret;
}
#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_ASYNC)
#if HAS_DETECT_GPIO
gpio_pin_configure_dt(&detect_gpio, GPIO_INPUT);
gpio_init_callback(&detect_callback, detect_pin_irq_callback_handler, BIT(detect_gpio.pin));
int err = gpio_add_callback(detect_gpio.port, &detect_callback);
if (err) {
LOG_ERR("Error adding the callback to the detect pin: %i", err);
return err;
}
err = gpio_pin_interrupt_configure_dt(&detect_gpio, GPIO_INT_EDGE_BOTH);
if (err < 0) {
LOG_WRN("Failed to so configure interrupt for detection pin (%d)", err);
return err;
}
#endif // HAS_DETECT_GPIO
return 0;
}
SYS_INIT(zmk_split_wired_peripheral_init, APPLICATION, CONFIG_KERNEL_INIT_PRIORITY_DEFAULT);
static void begin_tx(void) {
#if IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_INTERRUPT)
uart_irq_tx_enable(uart);
#elif IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_ASYNC)
zmk_split_wired_async_tx(&async_state);
#elif IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_POLLING)
k_work_submit(&send_pending_tx);
#endif
}
static ssize_t get_payload_data_size(const struct zmk_split_transport_peripheral_event *evt) {
switch (evt->type) {
case ZMK_SPLIT_TRANSPORT_PERIPHERAL_EVENT_TYPE_INPUT_EVENT:
return sizeof(evt->data.input_event);
case ZMK_SPLIT_TRANSPORT_PERIPHERAL_EVENT_TYPE_KEY_POSITION_EVENT:
return sizeof(evt->data.key_position_event);
case ZMK_SPLIT_TRANSPORT_PERIPHERAL_EVENT_TYPE_SENSOR_EVENT:
return sizeof(evt->data.sensor_event);
case ZMK_SPLIT_TRANSPORT_PERIPHERAL_EVENT_TYPE_BATTERY_EVENT:
return sizeof(evt->data.battery_event);
default:
return -ENOTSUP;
}
}
static int
split_peripheral_wired_report_event(const struct zmk_split_transport_peripheral_event *event) {
ssize_t data_size = get_payload_data_size(event);
if (data_size < 0) {
LOG_WRN("Failed to determine payload data size %d", data_size);
return data_size;
}
// Data + type + source
size_t payload_size =
data_size + sizeof(peripheral_id) + sizeof(enum zmk_split_transport_peripheral_event_type);
if (ring_buf_space_get(&chosen_tx_buf) < MSG_EXTRA_SIZE + payload_size) {
LOG_WRN("No room to send peripheral to the central (have %d but only space for %d)",
MSG_EXTRA_SIZE + payload_size, ring_buf_space_get(&chosen_tx_buf));
return -ENOSPC;
}
struct event_envelope env = {.prefix =
{
.magic_prefix = ZMK_SPLIT_WIRED_ENVELOPE_MAGIC_PREFIX,
.payload_size = payload_size,
},
.payload = {
.source = peripheral_id,
.event = *event,
}};
struct msg_postfix postfix = {.crc =
crc32_ieee((void *)&env, sizeof(env.prefix) + payload_size)};
LOG_HEXDUMP_DBG(&env, sizeof(env.prefix) + payload_size, "Payload");
size_t put = ring_buf_put(&chosen_tx_buf, (uint8_t *)&env, sizeof(env.prefix) + payload_size);
if (put != sizeof(env.prefix) + payload_size) {
LOG_WRN("Failed to put the whole message (%d vs %d)", put,
sizeof(env.prefix) + payload_size);
}
put = ring_buf_put(&chosen_tx_buf, (uint8_t *)&postfix, sizeof(postfix));
if (put != sizeof(postfix)) {
LOG_WRN("Failed to put the whole message (%d vs %d)", put, sizeof(postfix));
}
#if !IS_HALF_DUPLEX_MODE
begin_tx();
#endif
return 0;
}
static bool is_enabled;
static int split_peripheral_wired_set_enabled(bool enabled) {
if (is_enabled == enabled) {
return 0;
}
is_enabled = enabled;
if (enabled) {
begin_rx();
return 0;
#if HAS_DETECT_GPIO
} else {
stop_rx();
return 0;
#endif
}
return -ENOTSUP;
}
#if HAS_DETECT_GPIO
static zmk_split_transport_peripheral_status_changed_cb_t transport_status_cb;
static int
split_peripheral_wired_set_status_callback(zmk_split_transport_peripheral_status_changed_cb_t cb) {
transport_status_cb = cb;
return 0;
}
static struct zmk_split_transport_status split_peripheral_wired_get_status() {
int detected = gpio_pin_get_dt(&detect_gpio);
if (detected > 0) {
return (struct zmk_split_transport_status){
.available = true,
.enabled = true, // Track this
.connections = ZMK_SPLIT_TRANSPORT_CONNECTIONS_STATUS_ALL_CONNECTED,
};
} else {
return (struct zmk_split_transport_status){
.available = false,
.enabled = true, // Track this
.connections = ZMK_SPLIT_TRANSPORT_CONNECTIONS_STATUS_DISCONNECTED,
};
}
}
#endif // HAS_DETECT_GPIO
static const struct zmk_split_transport_peripheral_api peripheral_api = {
.report_event = split_peripheral_wired_report_event,
.set_enabled = split_peripheral_wired_set_enabled,
#if HAS_DETECT_GPIO
.set_status_callback = split_peripheral_wired_set_status_callback,
.get_status = split_peripheral_wired_get_status,
#endif // HAS_DETECT_GPIO
};
ZMK_SPLIT_TRANSPORT_PERIPHERAL_REGISTER(wired_peripheral, &peripheral_api,
CONFIG_ZMK_SPLIT_WIRED_PRIORITY);
#if HAS_DETECT_GPIO
static void notify_transport_status(void) {
if (transport_status_cb) {
LOG_DBG("Invoking the status CB");
transport_status_cb(&wired_peripheral, split_peripheral_wired_get_status());
}
}
#endif // HAS_DETECT_GPIO
static void process_tx_cb(void) {
while (ring_buf_size_get(&chosen_rx_buf) > MSG_EXTRA_SIZE) {
struct command_envelope env;
int item_err = zmk_split_wired_get_item(&chosen_rx_buf, (uint8_t *)&env,
sizeof(struct command_envelope));
switch (item_err) {
case 0:
if (env.payload.cmd.type == ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_POLL_EVENTS) {
begin_tx();
} else {
int ret = k_msgq_put(&cmd_msg_queue, &env.payload.cmd, K_NO_WAIT);
if (ret < 0) {
LOG_WRN("Failed to queue command for processing (%d)", ret);
return;
}
k_work_submit(&publish_commands);
}
break;
case -EAGAIN:
return;
default:
LOG_WRN("Issue fetching an item from the RX buffer: %d", item_err);
return;
}
}
}
static void publish_commands_work(struct k_work *work) {
struct zmk_split_transport_central_command cmd;
while (k_msgq_get(&cmd_msg_queue, &cmd, K_NO_WAIT) >= 0) {
zmk_split_transport_peripheral_command_handler(&wired_peripheral, cmd);
}
}

327
app/src/split/wired/wired.c Normal file
View File

@@ -0,0 +1,327 @@
/*
* Copyright (c) 2025 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#include "wired.h"
#include <zephyr/sys/crc.h>
#include <zephyr/drivers/uart.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/logging/log.h>
LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
#if IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_POLLING)
void zmk_split_wired_poll_out(struct ring_buf *tx_buf, const struct device *uart) {
uint8_t *buf;
uint32_t claim_len;
while ((claim_len = ring_buf_get_claim(tx_buf, &buf, MIN(32, tx_buf->size))) > 0) {
LOG_HEXDUMP_DBG(buf, claim_len, "TX Bytes");
for (int i = 0; i < claim_len; i++) {
uart_poll_out(uart, buf[i]);
}
ring_buf_get_finish(tx_buf, claim_len);
}
}
int zmk_split_wired_poll_in(struct ring_buf *rx_buf, const struct device *uart,
struct k_work *process_data_work,
zmk_split_wired_process_tx_callback_t process_data_cb) {
uint8_t *buf;
uint32_t read = 0;
uint32_t claim_len = ring_buf_put_claim(rx_buf, &buf, ring_buf_space_get(rx_buf));
if (claim_len < 1) {
LOG_WRN("No room available for reading in from the serial port");
return -ENOSPC;
}
bool all_read = false;
while (read < claim_len) {
if (uart_poll_in(uart, buf + read) < 0) {
all_read = true;
break;
}
read++;
}
ring_buf_put_finish(rx_buf, read);
if (ring_buf_size_get(rx_buf) > 0) {
if (process_data_work) {
k_work_submit(process_data_work);
} else if (process_data_cb) {
process_data_cb();
}
}
// TODO: Also indicate if no bytes read at all?
return (all_read ? 1 : 0);
}
#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_POLLING)
#if IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_INTERRUPT)
void zmk_split_wired_fifo_read(const struct device *dev, struct ring_buf *buf,
struct k_work *process_work,
zmk_split_wired_process_tx_callback_t process_cb) {
// TODO: Add error checking on platforms that support it
uint32_t last_read = 0, len = 0;
do {
uint8_t *buffer;
len = ring_buf_put_claim(buf, &buffer, buf->size);
if (len > 0) {
last_read = uart_fifo_read(dev, buffer, len);
ring_buf_put_finish(buf, last_read);
} else {
LOG_ERR("Dropping incoming RPC byte, insufficient room in the RX buffer. Bump "
"CONFIG_ZMK_STUDIO_RPC_RX_BUF_SIZE.");
uint8_t dummy;
last_read = uart_fifo_read(dev, &dummy, 1);
}
} while (last_read && last_read == len);
if (process_work) {
k_work_submit(process_work);
} else if (process_cb) {
process_cb();
}
}
void zmk_split_wired_fifo_fill(const struct device *dev, struct ring_buf *tx_buf) {
uint32_t len;
while ((len = ring_buf_size_get(tx_buf)) > 0) {
uint8_t *buf;
uint32_t claim_len = ring_buf_get_claim(tx_buf, &buf, tx_buf->size);
if (claim_len <= 0) {
break;
}
int sent = uart_fifo_fill(dev, buf, claim_len);
ring_buf_get_finish(tx_buf, MAX(sent, 0));
if (sent <= 0) {
break;
}
}
// if (ring_buf_size_get(tx_buf) == 0) {
// uart_irq_tx_disable(dev);
// }
}
#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_INTERRUPT)
#if IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_ASYNC)
enum ASYNC_STATE_BITS {
ASYNC_STATE_BIT_RXBUF0_USED = 0,
ASYNC_STATE_BIT_RXBUF1_USED,
};
void zmk_split_wired_async_tx(struct zmk_split_wired_async_state *state) {
uint8_t *buf;
uint32_t claim_len = ring_buf_get_claim(state->tx_buf, &buf, ring_buf_size_get(state->tx_buf));
if (claim_len <= 0) {
return;
}
if (state->dir_gpio) {
gpio_pin_set_dt(state->dir_gpio, 1);
}
#if !IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL)
LOG_DBG("Sending %d", claim_len);
#endif
int err = uart_tx(state->uart, buf, claim_len, SYS_FOREVER_US);
if (err < 0) {
LOG_DBG("NO TX %d", err);
}
}
int zmk_split_wired_async_rx(struct zmk_split_wired_async_state *state) {
atomic_set_bit(&state->state, ASYNC_STATE_BIT_RXBUF0_USED);
atomic_clear_bit(&state->state, ASYNC_STATE_BIT_RXBUF1_USED);
int ret = uart_rx_enable(state->uart, state->rx_bufs[0], state->rx_bufs_len,
CONFIG_ZMK_SPLIT_WIRED_ASYNC_RX_TIMEOUT);
if (ret < 0) {
LOG_ERR("Failed to enable RX (%d)", ret);
}
return ret;
}
int zmk_split_wired_async_rx_cancel(struct zmk_split_wired_async_state *state) {
return uart_rx_disable(state->uart);
}
static void restart_rx_work_cb(struct k_work *work) {
struct k_work_delayable *dwork = k_work_delayable_from_work(work);
struct zmk_split_wired_async_state *state =
CONTAINER_OF(dwork, struct zmk_split_wired_async_state, restart_rx_work);
zmk_split_wired_async_rx(state);
}
static void async_uart_cb(const struct device *dev, struct uart_event *ev, void *user_data) {
struct zmk_split_wired_async_state *state = (struct zmk_split_wired_async_state *)user_data;
switch (ev->type) {
case UART_TX_ABORTED:
// This can only really occur for a TX timeout for a HW flow control UART setup. What to do
// here in practice?
LOG_WRN("TX Aborted");
break;
case UART_TX_DONE:
LOG_DBG("TX Done %d", ev->data.tx.len);
ring_buf_get_finish(state->tx_buf, ev->data.tx.len);
if (ring_buf_size_get(state->tx_buf) > 0) {
zmk_split_wired_async_tx(state);
} else {
if (state->dir_gpio) {
gpio_pin_set_dt(state->dir_gpio, 0);
}
}
break;
case UART_RX_RDY: {
size_t received =
ring_buf_put(state->rx_buf, &ev->data.rx.buf[ev->data.rx.offset], ev->data.rx.len);
if (received < ev->data.rx.len) {
LOG_ERR("RX overrun!");
break;
}
// LOG_DBG("RX %d and now buffer is %d", received, ring_buf_size_get(state->rx_buf));
if (state->process_tx_callback) {
state->process_tx_callback();
} else if (state->process_tx_work) {
k_work_submit(state->process_tx_work);
}
break;
}
case UART_RX_BUF_RELEASED:
if (ev->data.rx_buf.buf == state->rx_bufs[0]) {
atomic_clear_bit(&state->state, ASYNC_STATE_BIT_RXBUF0_USED);
} else if (ev->data.rx_buf.buf == state->rx_bufs[1]) {
atomic_clear_bit(&state->state, ASYNC_STATE_BIT_RXBUF1_USED);
}
break;
case UART_RX_BUF_REQUEST:
if (!atomic_test_and_set_bit(&state->state, ASYNC_STATE_BIT_RXBUF0_USED)) {
uart_rx_buf_rsp(state->uart, state->rx_bufs[0], state->rx_bufs_len);
break;
}
if (!atomic_test_and_set_bit(&state->state, ASYNC_STATE_BIT_RXBUF1_USED)) {
uart_rx_buf_rsp(state->uart, state->rx_bufs[1], state->rx_bufs_len);
break;
}
LOG_WRN("No RX buffers available!");
break;
case UART_RX_STOPPED:
// LOG_WRN("UART RX Stopped %d with state %ld", ev->data.rx_stop.reason, state->state);
break;
case UART_RX_DISABLED: {
k_work_schedule(&state->restart_rx_work, K_MSEC(1));
break;
}
default:
break;
}
}
int zmk_split_wired_async_init(struct zmk_split_wired_async_state *state) {
__ASSERT(state != NULL, "State is null");
k_work_init_delayable(&state->restart_rx_work, restart_rx_work_cb);
int ret = uart_callback_set(state->uart, async_uart_cb, state);
if (ret < 0) {
LOG_ERR("Failed to set up async callback on UART (%d)", ret);
return ret;
}
// atomic_set_bit(&state->state, ASYNC_STATE_BIT_RXBUF0_USED);
// ret = uart_rx_enable(state->uart, state->rx_bufs[0], state->rx_bufs_len,
// CONFIG_ZMK_SPLIT_WIRED_ASYNC_RX_TIMEOUT);
// if (ret < 0) {
// LOG_ERR("Failed to enable RX (%d)", ret);
// return ret;
// }
return 0;
}
#endif
int zmk_split_wired_get_item(struct ring_buf *rx_buf, uint8_t *env, size_t env_size) {
while (ring_buf_size_get(rx_buf) > sizeof(struct msg_prefix) + sizeof(struct msg_postfix)) {
struct msg_prefix prefix;
__ASSERT_EVAL(
(void)ring_buf_peek(rx_buf, (uint8_t *)&prefix, sizeof(prefix)),
uint32_t peek_read = ring_buf_peek(rx_buf, (uint8_t *)&prefix, sizeof(prefix)),
peek_read == sizeof(prefix), "Somehow read less than we expect from the RX buffer");
if (memcmp(&prefix.magic_prefix, &ZMK_SPLIT_WIRED_ENVELOPE_MAGIC_PREFIX,
sizeof(prefix.magic_prefix)) != 0) {
uint8_t discarded_byte;
ring_buf_get(rx_buf, &discarded_byte, 1);
LOG_WRN("Prefix mismatch, discarding byte %0x", discarded_byte);
continue;
}
size_t payload_to_read = sizeof(prefix) + prefix.payload_size;
if (payload_to_read > env_size) {
LOG_WRN("Invalid message with payload %d bigger than expected max %d", payload_to_read,
env_size);
return -EINVAL;
}
if (ring_buf_size_get(rx_buf) < payload_to_read + sizeof(struct msg_postfix)) {
return -EAGAIN;
}
// Now that prefix matches, read it out so we can read the rest of the payload.
__ASSERT_EVAL((void)ring_buf_get(rx_buf, env, payload_to_read),
uint32_t read = ring_buf_get(rx_buf, env, payload_to_read),
read == payload_to_read,
"Somehow read less than we expect from the RX buffer");
struct msg_postfix postfix;
__ASSERT_EVAL((void)ring_buf_get(rx_buf, (uint8_t *)&postfix, sizeof(postfix)),
uint32_t read = ring_buf_get(rx_buf, (uint8_t *)&postfix, sizeof(postfix)),
read == sizeof(postfix),
"Somehow read less of the postfix than we expect from the RX buffer");
uint32_t crc = crc32_ieee(env, payload_to_read);
if (crc != postfix.crc) {
LOG_WRN("Data corruption in received peripheral event, ignoring %d vs %d", crc,
postfix.crc);
return -EINVAL;
}
return 0;
}
return -EAGAIN;
}

View File

@@ -0,0 +1,96 @@
/*
* Copyright (c) 2025 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#pragma once
#include <zephyr/sys/ring_buffer.h>
#include <zephyr/device.h>
#include <zmk/split/transport/types.h>
#define ZMK_SPLIT_WIRED_ENVELOPE_MAGIC_PREFIX "ZmKw"
struct msg_prefix {
uint8_t magic_prefix[sizeof(ZMK_SPLIT_WIRED_ENVELOPE_MAGIC_PREFIX) - 1];
uint8_t payload_size;
} __packed;
struct command_payload {
uint8_t source;
struct zmk_split_transport_central_command cmd;
} __packed;
struct command_envelope {
struct msg_prefix prefix;
struct command_payload payload;
} __packed;
struct event_payload {
uint8_t source;
struct zmk_split_transport_peripheral_event event;
} __packed;
struct event_envelope {
struct msg_prefix prefix;
struct event_payload payload;
} __packed;
struct msg_postfix {
uint32_t crc;
} __packed;
#define MSG_EXTRA_SIZE (sizeof(struct msg_prefix) + sizeof(struct msg_postfix))
typedef void (*zmk_split_wired_process_tx_callback_t)(void);
#if IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_POLLING)
void zmk_split_wired_poll_out(struct ring_buf *tx_buf, const struct device *uart);
int zmk_split_wired_poll_in(struct ring_buf *rx_buf, const struct device *uart,
struct k_work *process_data_work,
zmk_split_wired_process_tx_callback_t process_data_cb);
#endif
#if IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_INTERRUPT)
void zmk_split_wired_fifo_read(const struct device *dev, struct ring_buf *buf,
struct k_work *process_work,
zmk_split_wired_process_tx_callback_t process_cb);
void zmk_split_wired_fifo_fill(const struct device *dev, struct ring_buf *tx_buf);
#endif
#if IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_ASYNC)
struct zmk_split_wired_async_state {
atomic_t state;
uint8_t *rx_bufs[2];
size_t rx_bufs_len;
size_t rx_size_process_trigger;
struct ring_buf *tx_buf;
struct ring_buf *rx_buf;
zmk_split_wired_process_tx_callback_t process_tx_callback;
const struct device *uart;
struct k_work_delayable restart_rx_work;
struct k_work *process_tx_work;
const struct gpio_dt_spec *dir_gpio;
};
int zmk_split_wired_async_init(struct zmk_split_wired_async_state *state);
void zmk_split_wired_async_tx(struct zmk_split_wired_async_state *state);
int zmk_split_wired_async_rx(struct zmk_split_wired_async_state *state);
int zmk_split_wired_async_rx_cancel(struct zmk_split_wired_async_state *state);
#endif
int zmk_split_wired_get_item(struct ring_buf *rx_buf, uint8_t *env, size_t env_size);

Some files were not shown because too many files have changed in this diff Show More