Merge branch 'zmkfirmware:main' into murphpad

This commit is contained in:
Kyle McCreery 2021-05-23 16:59:52 -10:00
commit 8ed7588767
56 changed files with 4514 additions and 1525 deletions

View file

@ -23,7 +23,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: DoozyX/clang-format-lint-action@v0.11 - uses: DoozyX/clang-format-lint-action@v0.12
with: with:
source: "./app" source: "./app"
extensions: "h,c" extensions: "h,c"

View file

@ -42,7 +42,7 @@
, <7 0 &gpio0 24 0> /* D6/A7 D7*/ , <7 0 &gpio0 24 0> /* D6/A7 D7*/
, <8 0 &gpio0 10 0> /* D8/A8 B4*/ , <8 0 &gpio0 10 0> /* D8/A8 B4*/
, <9 0 &gpio1 6 0> /* D9/A9 B5*/ , <9 0 &gpio1 6 0> /* D9/A9 B5*/
, <10 0 &gpio1 13 0> /* D10/A10 B6*/ , <10 0 &gpio1 11 0> /* D10/A10 B6*/
; ;
}; };
}; };

View file

@ -9,7 +9,6 @@ config ZMK_KEYBOARD_NAME
config ZMK_USB config ZMK_USB
default y default y
endif
if ZMK_DISPLAY if ZMK_DISPLAY
@ -46,3 +45,5 @@ choice LVGL_COLOR_DEPTH
endchoice endchoice
endif # LVGL endif # LVGL
endif

View file

@ -6,7 +6,6 @@ if SHIELD_TIDBIT
config ZMK_KEYBOARD_NAME config ZMK_KEYBOARD_NAME
default "tidbit" default "tidbit"
endif
if ZMK_DISPLAY if ZMK_DISPLAY
@ -43,3 +42,5 @@ choice LVGL_COLOR_DEPTH
endchoice endchoice
endif # LVGL endif # LVGL
endif

View file

@ -83,7 +83,7 @@ static int kscan_gpio_config_interrupts(const struct device *dev, gpio_flags_t f
int err = gpio_pin_interrupt_configure(dev, cfg->pin, flags); int err = gpio_pin_interrupt_configure(dev, cfg->pin, flags);
if (err) { if (err) {
LOG_ERR("Unable to enable matrix GPIO interrupt"); LOG_ERR("Unable to enable direct GPIO interrupt");
return err; return err;
} }
} }

View file

@ -93,7 +93,7 @@ static int bvd_sample_fetch(const struct device *dev, enum sensor_channel chan)
&val); &val);
uint16_t millivolts = val * (uint64_t)drv_cfg->full_ohm / drv_cfg->output_ohm; uint16_t millivolts = val * (uint64_t)drv_cfg->full_ohm / drv_cfg->output_ohm;
LOG_DBG("ADC raw %d ~ %d mV => %d mV\n", drv_data->adc_raw, val, millivolts); LOG_DBG("ADC raw %d ~ %d mV => %d mV", drv_data->adc_raw, val, millivolts);
uint8_t percent = lithium_ion_mv_to_pct(millivolts); uint8_t percent = lithium_ion_mv_to_pct(millivolts);
LOG_DBG("Percent: %d", percent); LOG_DBG("Percent: %d", percent);

View file

@ -0,0 +1,9 @@
/*
* Copyright (c) 2021 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#pragma once
uint8_t zmk_battery_state_of_charge();

View file

@ -17,7 +17,7 @@ testcases=$(find $path -name native_posix.keymap -exec dirname \{\} \;)
num_cases=$(echo "$testcases" | wc -l) num_cases=$(echo "$testcases" | wc -l)
if [ $num_cases -gt 1 ]; then if [ $num_cases -gt 1 ]; then
echo "" > ./build/tests/pass-fail.log echo "" > ./build/tests/pass-fail.log
echo "$testcases" | xargs -L 1 -P 4 ./run-test.sh echo "$testcases" | xargs -L 1 -P ${J:-4} ./run-test.sh
err=$? err=$?
sort -k2 ./build/tests/pass-fail.log sort -k2 ./build/tests/pass-fail.log
exit $err exit $err

View file

@ -15,10 +15,15 @@
LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
#include <zmk/event_manager.h> #include <zmk/event_manager.h>
#include <zmk/battery.h>
#include <zmk/events/battery_state_changed.h> #include <zmk/events/battery_state_changed.h>
const struct device *battery; const struct device *battery;
static uint8_t last_state_of_charge = 0;
uint8_t zmk_battery_state_of_charge() { return last_state_of_charge; }
static int zmk_battery_update(const struct device *battery) { static int zmk_battery_update(const struct device *battery) {
struct sensor_value state_of_charge; struct sensor_value state_of_charge;
@ -36,17 +41,23 @@ static int zmk_battery_update(const struct device *battery) {
return rc; return rc;
} }
LOG_DBG("Setting BAS GATT battery level to %d.", state_of_charge.val1); if (last_state_of_charge != state_of_charge.val1) {
last_state_of_charge = state_of_charge.val1;
rc = bt_bas_set_battery_level(state_of_charge.val1); LOG_DBG("Setting BAS GATT battery level to %d.", last_state_of_charge);
if (rc != 0) { rc = bt_bas_set_battery_level(last_state_of_charge);
LOG_WRN("Failed to set BAS GATT battery level (err %d)", rc);
return rc; if (rc != 0) {
LOG_WRN("Failed to set BAS GATT battery level (err %d)", rc);
return rc;
}
rc = ZMK_EVENT_RAISE(new_zmk_battery_state_changed(
(struct zmk_battery_state_changed){.state_of_charge = last_state_of_charge}));
} }
return ZMK_EVENT_RAISE(new_zmk_battery_state_changed( return rc;
(struct zmk_battery_state_changed){.state_of_charge = state_of_charge.val1}));
} }
static void zmk_battery_work(struct k_work *work) { static void zmk_battery_work(struct k_work *work) {

View file

@ -177,6 +177,11 @@ static const struct behavior_driver_api behavior_sticky_key_driver_api = {
.binding_released = on_sticky_key_binding_released, .binding_released = on_sticky_key_binding_released,
}; };
static int sticky_key_keycode_state_changed_listener(const zmk_event_t *eh);
ZMK_LISTENER(behavior_sticky_key, sticky_key_keycode_state_changed_listener);
ZMK_SUBSCRIPTION(behavior_sticky_key, zmk_keycode_state_changed);
static int sticky_key_keycode_state_changed_listener(const zmk_event_t *eh) { static int sticky_key_keycode_state_changed_listener(const zmk_event_t *eh) {
struct zmk_keycode_state_changed *ev = as_zmk_keycode_state_changed(eh); struct zmk_keycode_state_changed *ev = as_zmk_keycode_state_changed(eh);
if (ev == NULL) { if (ev == NULL) {
@ -212,7 +217,10 @@ static int sticky_key_keycode_state_changed_listener(const zmk_event_t *eh) {
if (sticky_key->timer_started) { if (sticky_key->timer_started) {
stop_timer(sticky_key); stop_timer(sticky_key);
if (sticky_key->config->quick_release) { if (sticky_key->config->quick_release) {
// continue processing the event. Release the sticky key afterwards.
ZMK_EVENT_RAISE_AFTER(eh, behavior_sticky_key);
release_sticky_key_behavior(sticky_key, ev->timestamp); release_sticky_key_behavior(sticky_key, ev->timestamp);
return ZMK_EV_EVENT_CAPTURED;
} }
} }
sticky_key->modified_key_usage_page = ev->usage_page; sticky_key->modified_key_usage_page = ev->usage_page;
@ -229,9 +237,6 @@ static int sticky_key_keycode_state_changed_listener(const zmk_event_t *eh) {
return ZMK_EV_EVENT_BUBBLE; return ZMK_EV_EVENT_BUBBLE;
} }
ZMK_LISTENER(behavior_sticky_key, sticky_key_keycode_state_changed_listener);
ZMK_SUBSCRIPTION(behavior_sticky_key, zmk_keycode_state_changed);
void behavior_sticky_key_timer_handler(struct k_work *item) { void behavior_sticky_key_timer_handler(struct k_work *item) {
struct active_sticky_key *sticky_key = struct active_sticky_key *sticky_key =
CONTAINER_OF(item, struct active_sticky_key, release_timer); CONTAINER_OF(item, struct active_sticky_key, release_timer);

View file

@ -192,7 +192,7 @@ static inline bool candidate_is_completely_pressed(struct combo_cfg *candidate)
return pressed_keys[candidate->key_position_len - 1] != NULL; return pressed_keys[candidate->key_position_len - 1] != NULL;
} }
static void cleanup(); static int cleanup();
static int filter_timed_out_candidates(int64_t timestamp) { static int filter_timed_out_candidates(int64_t timestamp) {
int num_candidates = 0; int num_candidates = 0;
@ -224,7 +224,7 @@ static int clear_candidates() {
} }
static int capture_pressed_key(const zmk_event_t *ev) { static int capture_pressed_key(const zmk_event_t *ev) {
for (int i = 0; i < CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY; i++) { for (int i = 0; i < CONFIG_ZMK_COMBO_MAX_KEYS_PER_COMBO; i++) {
if (pressed_keys[i] != NULL) { if (pressed_keys[i] != NULL) {
continue; continue;
} }
@ -236,23 +236,25 @@ static int capture_pressed_key(const zmk_event_t *ev) {
const struct zmk_listener zmk_listener_combo; const struct zmk_listener zmk_listener_combo;
static void release_pressed_keys() { static int release_pressed_keys() {
// release the first key that was pressed for (int i = 0; i < CONFIG_ZMK_COMBO_MAX_KEYS_PER_COMBO; i++) {
if (pressed_keys[0] == NULL) {
return;
}
ZMK_EVENT_RELEASE(pressed_keys[0])
pressed_keys[0] = NULL;
// reprocess events (see tests/combo/fully-overlapping-combos-3 for why this is needed)
for (int i = 1; i < CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY; i++) {
if (pressed_keys[i] == NULL) {
return;
}
const zmk_event_t *captured_event = pressed_keys[i]; const zmk_event_t *captured_event = pressed_keys[i];
if (pressed_keys[i] == NULL) {
return i;
}
pressed_keys[i] = NULL; pressed_keys[i] = NULL;
ZMK_EVENT_RAISE(captured_event); if (i == 0) {
LOG_DBG("combo: releasing position event %d",
as_zmk_position_state_changed(captured_event)->position);
ZMK_EVENT_RELEASE(captured_event)
} else {
// reprocess events (see tests/combo/fully-overlapping-combos-3 for why this is needed)
LOG_DBG("combo: reraising position event %d",
as_zmk_position_state_changed(captured_event)->position);
ZMK_EVENT_RAISE(captured_event);
}
} }
return CONFIG_ZMK_COMBO_MAX_KEYS_PER_COMBO;
} }
static inline int press_combo_behavior(struct combo_cfg *combo, int32_t timestamp) { static inline int press_combo_behavior(struct combo_cfg *combo, int32_t timestamp) {
@ -360,14 +362,14 @@ static bool release_combo_key(int32_t position, int64_t timestamp) {
return false; return false;
} }
static void cleanup() { static int cleanup() {
k_delayed_work_cancel(&timeout_task); k_delayed_work_cancel(&timeout_task);
clear_candidates(); clear_candidates();
if (fully_pressed_combo != NULL) { if (fully_pressed_combo != NULL) {
activate_combo(fully_pressed_combo); activate_combo(fully_pressed_combo);
fully_pressed_combo = NULL; fully_pressed_combo = NULL;
} }
release_pressed_keys(); return release_pressed_keys();
} }
static void update_timeout_task() { static void update_timeout_task() {
@ -399,6 +401,7 @@ static int position_state_down(const zmk_event_t *ev, struct zmk_position_state_
update_timeout_task(); update_timeout_task();
struct combo_cfg *candidate_combo = candidates[0].combo; struct combo_cfg *candidate_combo = candidates[0].combo;
LOG_DBG("combo: capturing position event %d", data->position);
int ret = capture_pressed_key(ev); int ret = capture_pressed_key(ev);
switch (num_candidates) { switch (num_candidates) {
case 0: case 0:
@ -418,13 +421,18 @@ static int position_state_down(const zmk_event_t *ev, struct zmk_position_state_
} }
} }
static int position_state_up(struct zmk_position_state_changed *ev) { static int position_state_up(const zmk_event_t *ev, struct zmk_position_state_changed *data) {
cleanup(); int released_keys = cleanup();
if (release_combo_key(ev->position, ev->timestamp)) { if (release_combo_key(data->position, data->timestamp)) {
return ZMK_EV_EVENT_HANDLED; return ZMK_EV_EVENT_HANDLED;
} else {
return 0;
} }
if (released_keys > 1) {
// The second and further key down events are re-raised. To preserve
// correct order for e.g. hold-taps, reraise the key up event too.
ZMK_EVENT_RAISE(ev);
return ZMK_EV_EVENT_CAPTURED;
}
return 0;
} }
static void combo_timeout_handler(struct k_work *item) { static void combo_timeout_handler(struct k_work *item) {
@ -447,7 +455,7 @@ static int position_state_changed_listener(const zmk_event_t *ev) {
if (data->state) { // keydown if (data->state) { // keydown
return position_state_down(ev, data); return position_state_down(ev, data);
} else { // keyup } else { // keyup
return position_state_up(data); return position_state_up(ev, data);
} }
} }

View file

@ -22,24 +22,25 @@ int zmk_event_manager_handle_from(zmk_event_t *event, uint8_t start_index) {
uint8_t len = __event_subscriptions_end - __event_subscriptions_start; uint8_t len = __event_subscriptions_end - __event_subscriptions_start;
for (int i = start_index; i < len; i++) { for (int i = start_index; i < len; i++) {
struct zmk_event_subscription *ev_sub = __event_subscriptions_start + i; struct zmk_event_subscription *ev_sub = __event_subscriptions_start + i;
if (ev_sub->event_type == event->event) { if (ev_sub->event_type != event->event) {
ret = ev_sub->listener->callback(event); continue;
if (ret < 0) { }
LOG_DBG("Listener returned an error: %d", ret); ret = ev_sub->listener->callback(event);
goto release; switch (ret) {
} else if (ret > 0) { case ZMK_EV_EVENT_BUBBLE:
switch (ret) { continue;
case ZMK_EV_EVENT_HANDLED: case ZMK_EV_EVENT_HANDLED:
LOG_DBG("Listener handled the event"); LOG_DBG("Listener handled the event");
ret = 0; ret = 0;
goto release; goto release;
case ZMK_EV_EVENT_CAPTURED: case ZMK_EV_EVENT_CAPTURED:
LOG_DBG("Listener captured the event"); LOG_DBG("Listener captured the event");
event->last_listener_index = i; event->last_listener_index = i;
// Listeners are expected to free events they capture // Listeners are expected to free events they capture
return 0; return 0;
} default:
} LOG_DBG("Listener returned an error: %d", ret);
goto release;
} }
} }

View file

@ -47,7 +47,7 @@ void zmk_kscan_process_msgq(struct k_work *item) {
while (k_msgq_get(&zmk_kscan_msgq, &ev, K_NO_WAIT) == 0) { while (k_msgq_get(&zmk_kscan_msgq, &ev, K_NO_WAIT) == 0) {
bool pressed = (ev.state == ZMK_KSCAN_EVENT_STATE_PRESSED); bool pressed = (ev.state == ZMK_KSCAN_EVENT_STATE_PRESSED);
uint32_t position = zmk_matrix_transform_row_column_to_position(ev.row, ev.column); uint32_t position = zmk_matrix_transform_row_column_to_position(ev.row, ev.column);
LOG_DBG("Row: %d, col: %d, position: %d, pressed: %s\n", ev.row, ev.column, position, LOG_DBG("Row: %d, col: %d, position: %d, pressed: %s", ev.row, ev.column, position,
(pressed ? "true" : "false")); (pressed ? "true" : "false"));
ZMK_EVENT_RAISE(new_zmk_position_state_changed((struct zmk_position_state_changed){ ZMK_EVENT_RAISE(new_zmk_position_state_changed((struct zmk_position_state_changed){
.state = pressed, .position = position, .timestamp = k_uptime_get()})); .state = pressed, .position = position, .timestamp = k_uptime_get()}));

View file

@ -1,2 +1 @@
s/.*hid_listener_keycode_//p s/.*hid_listener_keycode_//p
s/.*combo//p

View file

@ -1,2 +1 @@
s/.*hid_listener_keycode_//p s/.*hid_listener_keycode_//p
s/.*combo//p

View file

@ -1,2 +1 @@
s/.*hid_listener_keycode_//p s/.*hid_listener_keycode_//p
s/.*combo//p

View file

@ -1,2 +1 @@
s/.*hid_listener_keycode_//p s/.*hid_listener_keycode_//p
s/.*combo//p

View file

@ -1,2 +1 @@
s/.*hid_listener_keycode_//p s/.*hid_listener_keycode_//p
s/.*combo//p

View file

@ -1,2 +1 @@
s/.*hid_listener_keycode_//p s/.*hid_listener_keycode_//p
s/.*combo//p

View file

@ -1,2 +1 @@
s/.*hid_listener_keycode_//p s/.*hid_listener_keycode_//p
s/.*combo//p

View file

@ -1,2 +1 @@
s/.*hid_listener_keycode_//p s/.*hid_listener_keycode_//p
s/.*combo//p

View file

@ -1,2 +1 @@
s/.*hid_listener_keycode_//p s/.*hid_listener_keycode_//p
s/.*combo//p

View file

@ -0,0 +1 @@
s/.*hid_listener_keycode_//p

View file

@ -0,0 +1,4 @@
pressed: usage_page 0x07 keycode 0x07 implicit_mods 0x00 explicit_mods 0x00
pressed: usage_page 0x07 keycode 0x05 implicit_mods 0x00 explicit_mods 0x00
released: usage_page 0x07 keycode 0x05 implicit_mods 0x00 explicit_mods 0x00
released: usage_page 0x07 keycode 0x07 implicit_mods 0x00 explicit_mods 0x00

View file

@ -0,0 +1,35 @@
#include <dt-bindings/zmk/keys.h>
#include <behaviors.dtsi>
#include <dt-bindings/zmk/kscan-mock.h>
/ {
combos {
compatible = "zmk,combos";
combo_one {
timeout-ms = <80>;
key-positions = <0 1 2 3>;
bindings = <&kp Z>;
};
};
keymap {
compatible = "zmk,keymap";
label ="Default keymap";
default_layer {
bindings = <
&kp A &kp B
&kp C &kp D
>;
};
};
};
&kscan {
events = <
ZMK_MOCK_PRESS(1,1,10)
ZMK_MOCK_PRESS(0,1,10)
ZMK_MOCK_RELEASE(0,1,100)
ZMK_MOCK_RELEASE(1,1,100)
>;
};

View file

@ -1,2 +1 @@
s/.*hid_listener_keycode_//p s/.*hid_listener_keycode_//p
s/.*combo/combo/p

View file

@ -1,2 +1 @@
s/.*hid_listener_keycode_//p s/.*hid_listener_keycode_//p
s/.*combo/combo/p

View file

@ -1,2 +1 @@
s/.*hid_listener_keycode_//p s/.*hid_listener_keycode_//p
s/.*combo/combo/p

View file

@ -0,0 +1 @@
s/.*hid_listener_keycode_//p

View file

@ -0,0 +1,10 @@
pressed: usage_page 0x07 keycode 0x08 implicit_mods 0x00 explicit_mods 0x00
pressed: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00
released: usage_page 0x07 keycode 0x08 implicit_mods 0x00 explicit_mods 0x00
pressed: usage_page 0x07 keycode 0x05 implicit_mods 0x00 explicit_mods 0x00
released: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00
released: usage_page 0x07 keycode 0x05 implicit_mods 0x00 explicit_mods 0x00
pressed: usage_page 0x07 keycode 0x08 implicit_mods 0x00 explicit_mods 0x00
pressed: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00
released: usage_page 0x07 keycode 0x08 implicit_mods 0x00 explicit_mods 0x00
released: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00

View file

@ -0,0 +1,26 @@
#include <dt-bindings/zmk/keys.h>
#include <behaviors.dtsi>
#include <dt-bindings/zmk/kscan_mock.h>
#include "../behavior_keymap.dtsi"
&sk {
quick-release;
};
&kscan {
events = <
ZMK_MOCK_PRESS(0,0,10)
ZMK_MOCK_RELEASE(0,0,10)
ZMK_MOCK_PRESS(1,0,10)
/* second key is pressed shortly after the first. It should not be capitalized. */
ZMK_MOCK_PRESS(1,1,10)
ZMK_MOCK_RELEASE(1,0,10)
ZMK_MOCK_RELEASE(1,1,10)
/* repeat test to check if cleanup is done correctly */
ZMK_MOCK_PRESS(0,0,10)
ZMK_MOCK_RELEASE(0,0,10)
ZMK_MOCK_PRESS(1,0,10)
ZMK_MOCK_RELEASE(1,0,10)
>;
};

View file

@ -10,7 +10,6 @@ module.exports = {
"plugin:react/recommended", "plugin:react/recommended",
"plugin:mdx/recommended", "plugin:mdx/recommended",
"prettier", "prettier",
"prettier/react",
], ],
parserOptions: { parserOptions: {
ecmaFeatures: { ecmaFeatures: {

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

View file

@ -56,7 +56,7 @@ For example, if you press `&mt LEFT_SHIFT A` and then release it without pressin
``` ```
&mt { &mt {
retro-tap; retro-tap;
} };
``` ```
#### Home row mods #### Home row mods

View file

@ -0,0 +1,73 @@
---
title: Mod-Morph Behavior
sidebar_label: Mod-Morph
---
## Summary
The Mod-Morph behavior sends a different keypress, depending on whether a specified modifier is being held during the keypress.
- If you tap the key by itself, the first keycode is sent.
- If you tap the key while holding the specified modifier, the second keycode is sent.
## Mod-Morph
The Mod-Morph behavior acts as one of two keycodes, depending on if the required modifier is being held during the keypress.
When the modifier is being held it is sent along with the morphed keycode. This can cause problems when the morphed keycode and modifier have an existing relationship (such as `shift-delete` or `ctrl-v` on many operating systems).
### Configuration
An example of how to implement the mod-morph "Grave Escape":
```
/ {
behaviors {
gresc: grave_escape {
compatible = "zmk,behavior-mod-morph";
label = "GRAVE_ESCAPE";
#binding-cells = <0>;
bindings = <&kp ESC>, <&kp GRAVE>;
mods = <(MOD_LGUI|MOD_LSFT|MOD_RGUI|MOD_RSFT)>;
};
};
keymap {
...
};
};
```
Note that this specific mod-morph exists in ZMK by default using code `&gresc`.
### Behavior Binding
- Reference: `&gresc`
- Parameter: None
Example:
```
&gresc
```
### Mods
This is how you determine what modifiers will activate the morphed version of the keycode.
Available Modifiers:
- `MOD_LSFT`
- `MOD_RSFT`
- `MOD_LCTL`
- `MOD_RCTL`
- `MOD_LALT`
- `MOD_RALT`
- `MOD_LGUI`
- `MOD_RGUI`
Example:
```
mods = <(MOD_LGUI|MOD_LSFT|MOD_RGUI|MOD_RSFT)>;
```

View file

@ -40,8 +40,8 @@ You can configure a different tapping term in your keymap:
/ { / {
keymap { keymap {
... ...
} };
} };
``` ```
### Additional information ### Additional information

View file

@ -38,8 +38,8 @@ You can configure a different `release-after-ms` in your keymap:
/ { / {
keymap { keymap {
... ...
} };
} };
``` ```
### Advanced usage ### Advanced usage

View file

@ -32,8 +32,8 @@ You can configure a different `release-after-ms` in your keymap:
/ { / {
keymap { keymap {
... ...
} };
} };
``` ```
### Advanced usage ### Advanced usage

View file

@ -0,0 +1,100 @@
---
title: Beta Testing
sidebar_label: Beta Testing
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
You may find that ZMK does not support a feature or keyboard that you are interesting in using. You may find that someone
has already taken the time to submit the feature you need as a [Pull Request](https://github.com/zmkfirmware/zmk/pulls). If you find the feature you need as a pull request,
this page is for you!
## Developer Repositories and Branches
For a developer to submit a pull request to ZMK, they must first clone the original ZMK repository. After they have a copy
of the source code, they may create a feature branch to work within. When they have finished, they will publish the feature
branch and create the pull request.
### Finding the Repository Page from the Pull Request
![PR Repository](../assets/features/beta-testing/pr-repo-branch.png)
### Finding the Repository URL
![Repository URL](../assets/features/beta-testing/repo-url.png)
### Finding the Repository Branch
![Repository URL](../assets/features/beta-testing/repo-branch.png)
## Testing features
Testing features will require you to modify the `west.yml` file. You will need to add a new remote for the pull request you
would like to test, and change the selected remote and revision (or branch) for the `zmk` project.
### Examples
<Tabs
defaultValue="zmk"
values={[
{label: 'Default', value: 'zmk'},
{label: 'PR685: Macros', value: 'macros'},
{label: 'PR649: Add &sleep behavior', value: 'sleep'},
]}>
<TabItem value="zmk">
```
manifest:
remotes:
- name: zmkfirmware
url-base: https://github.com/zmkfirmware
projects:
- name: zmk
remote: zmkfirmware
revision: main
import: app/west.yml
self:
path: config
```
</TabItem>
<TabItem value="macros">
```
manifest:
remotes:
- name: zmkfirmware
url-base: https://github.com/zmkfirmware
- name: okke-formsma
url-base: https://github.com/okke-formsma
projects:
- name: zmk
remote: okke-formsma
revision: macros
import: app/west.yml
self:
path: config
```
</TabItem>
<TabItem value="sleep">
```
manifest:
remotes:
- name: zmkfirmware
url-base: https://github.com/zmkfirmware
- name: mcrosson
url-base: https://github.com/mcrosson
projects:
- name: zmk
remote: mcrosson
revision: feat-behavior-sleep
import: app/west.yml
self:
path: config
```
</TabItem>
</Tabs>

View file

@ -18,7 +18,6 @@ Combos configured in your `.keymap` file, but are separate from the `keymap` nod
timeout-ms = <50>; timeout-ms = <50>;
key-positions = <0 1>; key-positions = <0 1>;
bindings = <&kp ESC>; bindings = <&kp ESC>;
layers = <-1>;
}; };
}; };
}; };
@ -28,7 +27,7 @@ Combos configured in your `.keymap` file, but are separate from the `keymap` nod
- The `compatible` property should always be `"zmk,combos"` for combos. - The `compatible` property should always be `"zmk,combos"` for combos.
- `timeout-ms` is the number of milliseconds that all keys of the combo must be pressed. - `timeout-ms` is the number of milliseconds that all keys of the combo must be pressed.
- `key-positions` is an array of key positions. See the info section below about how to figure out the positions on your board. - `key-positions` is an array of key positions. See the info section below about how to figure out the positions on your board.
- `layers = <0 1...>` will allow limiting a combo to specific layers. this is an _optional_ parameter and defaults to `-1` which is global scope. - `layers = <0 1...>` will allow limiting a combo to specific layers. This is an _optional_ parameter, when omitted it defaults to global scope.
- `bindings` is the behavior that is activated when the behavior is pressed. - `bindings` is the behavior that is activated when the behavior is pressed.
- (advanced) you can specify `slow-release` if you want the combo binding to be released when all key-positions are released. The default is to release the combo as soon as any of the keys in the combo is released. - (advanced) you can specify `slow-release` if you want the combo binding to be released when all key-positions are released. The default is to release the combo as soon as any of the keys in the combo is released.

View file

@ -41,4 +41,4 @@ Here, the left encoder is configured to control volume up and down while the rig
## Adding Encoder Support ## Adding Encoder Support
See the [New Keyboard Shield](../development/new-shield#encoders) documentation for how to add or modify additional encoders to your shield. See the [New Keyboard Shield](/docs/development/new-shield#encoders) documentation for how to add or modify additional encoders to your shield.

View file

@ -55,10 +55,16 @@ If you have a shield with RGB underglow, you must add a `boards/` directory with
Inside the `boards/` folder, you define a `<board>.overlay` for each different board. Inside the `boards/` folder, you define a `<board>.overlay` for each different board.
For example, the Kyria shield has a `boards/nice_nano.overlay` file that defines the RGB underglow for the `nice_nano` board specifically. For example, the Kyria shield has a `boards/nice_nano.overlay` file that defines the RGB underglow for the `nice_nano` board specifically.
The first step to adding support for underglow is to select you SPI output. With nRF52 boards, you can just use `&spi1` and define the pins you want to use. ### nRF52-based boards
For other boards, you must select an SPI definition that has the `MOSI` pin as your data pin going to your LED strip.
Here's an example of an nRF52 SPI definition: With nRF52 boards, you can just use `&spi1` and define the pins you want to use.
To identify which pin number you need to put in the config you need do to a bit of math. You need the hardware port and run it through a function.
**32 \* X + Y** = `<Pin number>` where X is first part of the hardware port "PX.01" and Y is the second part of the hardware port "P1.Y".
(_P1.13_ would give you _32 \* 1 + 13_ = `<45>` and P0.15 would give you _32 \* 0 + 15_ = `<15>`)
Here's an example on a definition that uses P0.06:
``` ```
&spi1 { &spi1 {
@ -87,11 +93,15 @@ Here's an example of an nRF52 SPI definition:
:::info :::info
If you are configuring SPI for an nRF52840 (or other nRF52) based board, double check that you are using pins that aren't restricted to low frequency I/O. If you are configuring SPI for an nRF52 based board, double check that you are using pins that aren't restricted to low frequency I/O.
Ignoring these restrictions may result in poor wireless performance. You can find the list of low frequency I/O pins [here](https://infocenter.nordicsemi.com/index.jsp?topic=%2Fps_nrf52840%2Fpin.html&cp=4_0_0_6_0). Ignoring these restrictions may result in poor wireless performance. You can find the list of low frequency I/O pins for the nRF52840 [here](https://infocenter.nordicsemi.com/index.jsp?topic=%2Fps_nrf52840%2Fpin.html&cp=4_0_0_6_0).
::: :::
### Other boards
For other boards, you must select an SPI definition that has the `MOSI` pin as your data pin going to your LED strip.
Here's another example for a non-nRF52 board on `spi1`: Here's another example for a non-nRF52 board on `spi1`:
``` ```

View file

@ -10,6 +10,9 @@ module.exports = {
projectName: "zmk", // Usually your repo name. projectName: "zmk", // Usually your repo name.
plugins: [path.resolve(__dirname, "src/docusaurus-tree-sitter-plugin")], plugins: [path.resolve(__dirname, "src/docusaurus-tree-sitter-plugin")],
themeConfig: { themeConfig: {
colorMode: {
respectPrefersColorScheme: true,
},
googleAnalytics: { googleAnalytics: {
trackingID: "UA-145201102-2", trackingID: "UA-145201102-2",
anonymizeIP: true, anonymizeIP: true,
@ -29,6 +32,11 @@ module.exports = {
position: "left", position: "left",
}, },
{ to: "blog", label: "Blog", position: "left" }, { to: "blog", label: "Blog", position: "left" },
{
to: "power-profiler",
label: "Power Profiler",
position: "left",
},
{ {
href: "https://github.com/zmkfirmware/zmk", href: "https://github.com/zmkfirmware/zmk",
label: "GitHub", label: "GitHub",

4465
docs/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -17,13 +17,13 @@
"@docusaurus/core": "^2.0.0-alpha.66", "@docusaurus/core": "^2.0.0-alpha.66",
"@docusaurus/preset-classic": "^2.0.0-alpha.66", "@docusaurus/preset-classic": "^2.0.0-alpha.66",
"@fortawesome/fontawesome-svg-core": "^1.2.32", "@fortawesome/fontawesome-svg-core": "^1.2.32",
"@fortawesome/free-solid-svg-icons": "^5.15.1", "@fortawesome/free-solid-svg-icons": "^5.15.3",
"@fortawesome/react-fontawesome": "^0.1.12", "@fortawesome/react-fontawesome": "^0.1.14",
"classnames": "^2.2.6", "classnames": "^2.2.6",
"react": "^16.8.4", "react": "^16.14.0",
"react-async": "^10.0.1", "react-async": "^10.0.1",
"react-copy-to-clipboard": "^5.0.2", "react-copy-to-clipboard": "^5.0.2",
"react-dom": "^16.8.4", "react-dom": "^16.14.0",
"react-toastify": "^6.0.9", "react-toastify": "^6.0.9",
"web-tree-sitter": "^0.17.1" "web-tree-sitter": "^0.17.1"
}, },
@ -40,10 +40,10 @@
] ]
}, },
"devDependencies": { "devDependencies": {
"eslint": "^7.12.0", "eslint": "^7.25.0",
"eslint-config-prettier": "^6.14.0", "eslint-config-prettier": "^8.3.0",
"eslint-plugin-mdx": "^1.8.2", "eslint-plugin-mdx": "^1.13.0",
"eslint-plugin-react": "^7.21.5", "eslint-plugin-react": "^7.23.2",
"null-loader": "^3.0.0", "null-loader": "^3.0.0",
"prettier": "2.1.2", "prettier": "2.1.2",
"string-replace-loader": "2.3" "string-replace-loader": "2.3"

View file

@ -14,6 +14,7 @@ module.exports = {
"features/displays", "features/displays",
"features/encoders", "features/encoders",
"features/underglow", "features/underglow",
"features/beta-testing",
], ],
Behaviors: [ Behaviors: [
"behaviors/key-press", "behaviors/key-press",
@ -21,6 +22,7 @@ module.exports = {
"behaviors/misc", "behaviors/misc",
"behaviors/hold-tap", "behaviors/hold-tap",
"behaviors/mod-tap", "behaviors/mod-tap",
"behaviors/mod-morph",
"behaviors/sticky-key", "behaviors/sticky-key",
"behaviors/sticky-layer", "behaviors/sticky-layer",
"behaviors/reset", "behaviors/reset",

View file

@ -0,0 +1,100 @@
/*
* Copyright (c) 2021 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
import React from "react";
import PropTypes from "prop-types";
function CustomBoardForm({
bindPsuType,
bindOutputV,
bindEfficiency,
bindQuiescentMicroA,
bindOtherQuiescentMicroA,
}) {
return (
<div className="profilerSection">
<h3>Custom Board</h3>
<div className="row">
<div className="col col--4">
<div className="profilerInput">
<label>Power Supply Type</label>
<select {...bindPsuType}>
<option hidden value="">
Select a PSU type
</option>
<option value="LDO">LDO</option>
<option value="SWITCHING">Switching</option>
</select>
</div>
</div>
<div className="col col--4">
<div className="profilerInput">
<label>
Output Voltage{" "}
<span tooltip="Output Voltage of the PSU used by the system">
</span>
</label>
<input {...bindOutputV} type="range" min="1.8" step=".1" max="5" />
<span>{parseFloat(bindOutputV.value).toFixed(1)}V</span>
</div>
{bindPsuType.value === "SWITCHING" && (
<div className="profilerInput">
<label>
PSU Efficiency{" "}
<span tooltip="The estimated efficiency with a VIN of 3.8 and the output voltage entered above">
</span>
</label>
<input
{...bindEfficiency}
type="range"
min=".50"
step=".01"
max="1"
/>
<span>{Math.round(bindEfficiency.value * 100)}%</span>
</div>
)}
</div>
<div className="col col--4">
<div className="profilerInput">
<label>
PSU Quiescent{" "}
<span tooltip="The standby usage of the PSU"></span>
</label>
<div className="inputBox">
<input {...bindQuiescentMicroA} type="number" />
<span>µA</span>
</div>
</div>
<div className="profilerInput">
<label>
Other Quiescent{" "}
<span tooltip="Any other standby usage of the board (voltage dividers, extra ICs, etc)">
</span>
</label>
<div className="inputBox">
<input {...bindOtherQuiescentMicroA} type="number" />
<span>µA</span>
</div>
</div>
</div>
</div>
</div>
);
}
CustomBoardForm.propTypes = {
bindPsuType: PropTypes.Object,
bindOutputV: PropTypes.Object,
bindEfficiency: PropTypes.Object,
bindQuiescentMicroA: PropTypes.Object,
bindOtherQuiescentMicroA: PropTypes.Object,
};
export default CustomBoardForm;

View file

@ -0,0 +1,266 @@
/*
* Copyright (c) 2021 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
import React from "react";
import PropTypes from "prop-types";
import { displayPower, underglowPower, zmkBase } from "../data/power";
import "../css/power-estimate.css";
// Average monthly discharge percent
const lithiumIonMonthlyDischargePercent = 5;
// Average voltage of a lithium ion battery based of discharge graphs
const lithiumIonAverageVoltage = 3.8;
// Average discharge efficiency of li-ion https://en.wikipedia.org/wiki/Lithium-ion_battery
const lithiumIonDischargeEfficiency = 0.85;
// Range of the discharge efficiency
const lithiumIonDischargeEfficiencyRange = 0.05;
// Proportion of time spent typing (keys being pressed down and scanning). Estimated to 2%.
const timeSpentTyping = 0.02;
// Nordic power profiler kit accuracy
const measurementAccuracy = 0.2;
const batVolt = lithiumIonAverageVoltage;
const palette = [
"#bbdefb",
"#90caf9",
"#64b5f6",
"#42a5f5",
"#2196f3",
"#1e88e5",
"#1976d2",
];
function formatUsage(microWatts) {
if (microWatts > 1000) {
return (microWatts / 1000).toFixed(1) + "mW";
}
return Math.round(microWatts) + "µW";
}
function voltageEquivalentCalc(powerSupply) {
if (powerSupply.type === "LDO") {
return batVolt;
} else if (powerSupply.type === "SWITCHING") {
return powerSupply.outputVoltage / powerSupply.efficiency;
}
}
function formatMinutes(minutes, precision, floor) {
let message = "";
let count = 0;
let units = ["year", "month", "week", "day", "hour", "minute"];
let multiples = [60 * 24 * 365, 60 * 24 * 30, 60 * 24 * 7, 60 * 24, 60, 1];
for (let i = 0; i < units.length; i++) {
if (minutes >= multiples[i]) {
const timeCount = floor
? Math.floor(minutes / multiples[i])
: Math.ceil(minutes / multiples[i]);
minutes -= timeCount * multiples[i];
count++;
message +=
timeCount + (timeCount > 1 ? ` ${units[i]}s ` : ` ${units[i]} `);
}
if (count == precision) return message;
}
return message || "0 minutes";
}
function PowerEstimate({
board,
splitType,
batteryMilliAh,
usage,
underglow,
display,
}) {
if (!board || !board.powerSupply.type || !batteryMilliAh) {
return (
<div className="powerEstimate">
<h3>
<span>{splitType !== "standalone" ? splitType + ": " : " "}...</span>
</h3>
<div className="powerEstimateBar">
<div
className="powerEstimateBarSection"
style={{
width: "100%",
background: "#e0e0e0",
mixBlendMode: "overlay",
}}
></div>
</div>
</div>
);
}
const powerUsage = [];
let totalUsage = 0;
const voltageEquivalent = voltageEquivalentCalc(board.powerSupply);
// Lithium ion self discharge
const lithiumMonthlyDischargemAh =
parseInt(batteryMilliAh) * (lithiumIonMonthlyDischargePercent / 100);
const lithiumDischargeMicroA = (lithiumMonthlyDischargemAh * 1000) / 30 / 24;
const lithiumDischargeMicroW = lithiumDischargeMicroA * batVolt;
totalUsage += lithiumDischargeMicroW;
powerUsage.push({
title: "Battery Self Discharge",
usage: lithiumDischargeMicroW,
});
// Quiescent current
const quiescentMicroATotal =
parseInt(board.powerSupply.quiescentMicroA) +
parseInt(board.otherQuiescentMicroA);
const quiescentMicroW = quiescentMicroATotal * voltageEquivalent;
totalUsage += quiescentMicroW;
powerUsage.push({
title: "Board Quiescent Usage",
usage: quiescentMicroW,
});
// ZMK overall usage
const zmkMicroA =
zmkBase[splitType].idle +
(splitType !== "peripheral" ? zmkBase.hostConnection * usage.bondedQty : 0);
const zmkMicroW = zmkMicroA * voltageEquivalent;
const zmkUsage = zmkMicroW * (1 - usage.percentAsleep);
totalUsage += zmkUsage;
powerUsage.push({
title: "ZMK Base Usage",
usage: zmkUsage,
});
// ZMK typing usage
const zmkTypingMicroA = zmkBase[splitType].typing * timeSpentTyping;
const zmkTypingMicroW = zmkTypingMicroA * voltageEquivalent;
const zmkTypingUsage = zmkTypingMicroW * (1 - usage.percentAsleep);
totalUsage += zmkTypingUsage;
powerUsage.push({
title: "ZMK Typing Usage",
usage: zmkTypingUsage,
});
if (underglow.glowEnabled) {
const underglowAverageLedMicroA =
underglow.glowBrightness *
(underglowPower.ledOn - underglowPower.ledOff) +
underglowPower.ledOff;
const underglowMicroA =
underglowPower.firmware +
underglow.glowQuantity * underglowAverageLedMicroA;
const underglowMicroW = underglowMicroA * voltageEquivalent;
const underglowUsage = underglowMicroW * (1 - usage.percentAsleep);
totalUsage += underglowUsage;
powerUsage.push({
title: "RGB Underglow",
usage: underglowUsage,
});
}
if (display.displayEnabled && display.displayType) {
const { activePercent, active, sleep } = displayPower[display.displayType];
const displayMicroA = active * activePercent + sleep * (1 - activePercent);
const displayMicroW = displayMicroA * voltageEquivalent;
const displayUsage = displayMicroW * (1 - usage.percentAsleep);
totalUsage += displayUsage;
powerUsage.push({
title: "Display",
usage: displayUsage,
});
}
// Calculate the average minutes of use
const estimatedAvgEffectiveMicroWH =
batteryMilliAh * batVolt * lithiumIonDischargeEfficiency * 1000;
const estimatedAvgMinutes = Math.round(
(estimatedAvgEffectiveMicroWH / totalUsage) * 60
);
// Calculate worst case for battery life
const worstLithiumIonDischargeEfficiency =
lithiumIonDischargeEfficiency - lithiumIonDischargeEfficiencyRange;
const estimatedWorstEffectiveMicroWH =
batteryMilliAh * batVolt * worstLithiumIonDischargeEfficiency * 1000;
const highestTotalUsage = totalUsage * (1 + measurementAccuracy);
const estimatedWorstMinutes = Math.round(
(estimatedWorstEffectiveMicroWH / highestTotalUsage) * 60
);
// Calculate range (+-) of minutes using average - worst
const estimatedRange = estimatedAvgMinutes - estimatedWorstMinutes;
return (
<div className="powerEstimate">
<h3>
<span>{splitType !== "standalone" ? splitType + ": " : " "}</span>
{formatMinutes(estimatedAvgMinutes, 2, true)} (±
{formatMinutes(estimatedRange, 1, false).trim()})
</h3>
<div className="powerEstimateBar">
{powerUsage.map((p, i) => (
<div
key={p.title}
className={
"powerEstimateBarSection" + (i > 1 ? " rightSection" : "")
}
style={{
width: (p.usage / totalUsage) * 100 + "%",
background: palette[i],
}}
>
<div className="powerEstimateTooltipWrap">
<div className="powerEstimateTooltip">
<div>
{p.title} - {Math.round((p.usage / totalUsage) * 100)}%
</div>
<div style={{ fontSize: ".875rem" }}>
~{formatUsage(p.usage)} estimated avg. consumption
</div>
</div>
</div>
</div>
))}
</div>
</div>
);
}
PowerEstimate.propTypes = {
board: PropTypes.Object,
splitType: PropTypes.string,
batteryMilliAh: PropTypes.number,
usage: PropTypes.Object,
underglow: PropTypes.Object,
display: PropTypes.Object,
};
export default PowerEstimate;

View file

@ -4,6 +4,24 @@
* SPDX-License-Identifier: CC-BY-NC-SA-4.0 * SPDX-License-Identifier: CC-BY-NC-SA-4.0
*/ */
:root {
--codes-os-fg: black;
--codes-os-windows-bg: #caedfd;
--codes-os-linux-bg: #fff2ca;
--codes-os-android-bg: #d8eed9;
--codes-os-macos-bg: #ececec;
--codes-os-ios-bg: #ffffff;
}
html[data-theme="dark"] {
--codes-os-fg: #f5f6f7;
--codes-os-windows-bg: #032535;
--codes-os-linux-bg: #332600;
--codes-os-android-bg: #112712;
--codes-os-macos-bg: #121212;
--codes-os-ios-bg: #000000;
}
.codes.os.legend { .codes.os.legend {
position: sticky; position: sticky;
z-index: 1; z-index: 1;
@ -132,7 +150,7 @@ html[data-theme="light"] .codes.os.legend {
} }
.codes .os { .codes .os {
color: black; color: var(--codes-os-fg);
} }
.codes td.os { .codes td.os {
@ -144,23 +162,23 @@ html[data-theme="light"] .codes.os.legend {
} }
.codes .os.windows { .codes .os.windows {
background: #caedfd; background: var(--codes-os-windows-bg);
} }
.codes .os.linux { .codes .os.linux {
background: #fff2ca; background: var(--codes-os-linux-bg);
} }
.codes .os.android { .codes .os.android {
background: #d8eed9; background: var(--codes-os-android-bg);
} }
.codes .os.macos { .codes .os.macos {
background: #ececec; background: var(--codes-os-macos-bg);
} }
.codes .os.ios { .codes .os.ios {
background: #ffffff; background: var(--codes-os-ios-bg);
} }
.codes .footnotes { .codes .footnotes {

View file

@ -0,0 +1,81 @@
/*
* Copyright (c) 2021 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
.powerEstimate {
margin: 20px 0;
}
.powerEstimate > h3 > span {
text-transform: capitalize;
}
.powerEstimateBar {
height: 64px;
width: 100%;
box-shadow: rgba(0, 0, 0, 0.03) 0px 10px 20px 0px,
rgba(0, 0, 0, 0.1) 0px 1px 4px 0px;
border-radius: 64px;
display: flex;
justify-content: flex-start;
overflow: hidden;
}
.powerEstimateBarSection {
transition: all 0.2s ease;
flex-grow: 1;
}
.powerEstimateBarSection.rightSection {
display: flex;
justify-content: flex-end;
}
.powerEstimateTooltipWrap {
position: absolute;
visibility: hidden;
opacity: 0;
transform: translateY(calc(-100% - 8px));
transition: opacity 0.2s ease;
}
.powerEstimateBarSection:hover .powerEstimateTooltipWrap {
visibility: visible;
opacity: 1;
}
.powerEstimateTooltip {
display: block;
position: relative;
box-shadow: var(--ifm-global-shadow-tl);
width: 260px;
padding: 10px;
border-radius: 4px;
background: var(--ifm-background-surface-color);
transform: translateX(-15px);
}
.rightSection .powerEstimateTooltip {
transform: translateX(15px);
}
.powerEstimateTooltip:after {
content: "";
position: absolute;
top: 100%;
left: 27px;
margin-left: -8px;
width: 0;
height: 0;
border-top: 8px solid var(--ifm-background-surface-color);
border-right: 8px solid transparent;
border-left: 8px solid transparent;
}
.rightSection .powerEstimateTooltip:after {
left: unset;
right: 27px;
margin-right: -8px;
}

View file

@ -0,0 +1,195 @@
/*
* Copyright (c) 2021 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
.profilerSection {
margin: 10px 0;
padding: 10px 20px;
background: var(--ifm-background-surface-color);
border-radius: 4px;
box-shadow: rgba(0, 0, 0, 0.03) 0px 10px 20px 0px,
rgba(0, 0, 0, 0.1) 0px 1px 4px 0px;
}
.profilerInput {
margin-bottom: 12px;
}
.profilerInput label {
display: block;
}
.profilerDisclaimer {
padding: 20px 0;
font-size: 14px;
}
span[tooltip] {
position: relative;
}
span[tooltip]::before {
content: attr(tooltip);
font-size: 13px;
padding: 5px 10px;
position: absolute;
width: 220px;
border-radius: 4px;
background: var(--ifm-background-surface-color);
opacity: 0;
visibility: hidden;
box-shadow: rgba(0, 0, 0, 0.03) 0px 10px 20px 0px,
rgba(0, 0, 0, 0.1) 0px 1px 4px 0px;
transition: opacity 0.2s ease;
transform: translate(-50%, -100%);
left: 50%;
}
span[tooltip]::after {
content: "";
position: absolute;
border-top: 8px solid var(--ifm-background-surface-color);
border-right: 8px solid transparent;
border-left: 8px solid transparent;
width: 0;
height: 0;
opacity: 0;
visibility: hidden;
transition: opacity 0.2s ease;
transform: translateX(-50%);
left: 50%;
}
span[tooltip]:hover::before {
opacity: 1;
visibility: visible;
}
span[tooltip]:hover::after {
opacity: 1;
visibility: visible;
}
input[type="checkbox"].toggleInput {
display: none;
}
input[type="checkbox"] + .toggle {
margin: 6px 2px;
height: 20px;
width: 48px;
background: rgba(0, 0, 0, 0.5);
border-radius: 20px;
transition: all 0.2s ease;
user-select: none;
}
input[type="checkbox"] + .toggle > .toggleThumb {
height: 16px;
border-radius: 20px;
transform: translate(2px, 2px);
width: 16px;
background: var(--ifm-color-white);
box-shadow: var(--ifm-global-shadow-lw);
transition: all 0.2s ease;
}
input[type="checkbox"]:checked + .toggle {
background: var(--ifm-color-primary);
}
input[type="checkbox"]:checked + .toggle > .toggleThumb {
transform: translate(30px, 2px);
}
select {
border: solid 1px rgba(0, 0, 0, 0.5);
border-radius: 4px;
display: flex;
height: 34px;
width: 200px;
background: inherit;
color: inherit;
font-size: inherit;
line-height: inherit;
margin: 0;
padding: 3px 5px;
outline: none;
}
select > option {
background: var(--ifm-background-surface-color);
}
.inputBox {
border: solid 1px rgba(0, 0, 0, 0.5);
border-radius: 4px;
display: flex;
width: 200px;
}
.inputBox > input {
background: inherit;
color: inherit;
font-size: inherit;
line-height: inherit;
margin: 0;
padding: 3px 10px;
border: none;
width: 100%;
min-width: 0;
text-align: right;
outline: none;
}
.inputBox > span {
background: rgba(0, 0, 0, 0.05);
border-left: solid 1px rgba(0, 0, 0, 0.5);
padding: 3px 10px;
}
/* Chrome, Safari, Edge, Opera */
.inputBox > input::-webkit-outer-spin-button,
.inputBox > input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
/* Firefox */
.inputBox > input[type="number"] {
-moz-appearance: textfield;
}
.disclaimerHolder {
position: absolute;
width: 100vw;
height: 100vh;
top: 0;
left: 0;
z-index: 99;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
}
.disclaimer {
padding: 20px 20px;
background: var(--ifm-background-surface-color);
border-radius: 4px;
box-shadow: rgba(0, 0, 0, 0.03) 0px 10px 20px 0px,
rgba(0, 0, 0, 0.1) 0px 1px 4px 0px;
width: 500px;
}
.disclaimer > button {
border: none;
background: var(--ifm-color-primary);
color: var(--ifm-color-white);
cursor: pointer;
border-radius: 4px;
padding: 5px 15px;
}

78
docs/src/data/power.js Normal file
View file

@ -0,0 +1,78 @@
/*
* Copyright (c) 2021 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
/**
* This file holds all current measurements related to ZMK features and hardware
* All current measurements are in micro amps. Measurements were taken on a Nordic Power Profiler Kit
* The test device to get these values was three nice!nanos (nRF52840).
*/
export const zmkBase = {
hostConnection: 23, // How much current it takes to have an idle host connection
standalone: {
idle: 0, // No extra idle current
typing: 315, // Current while holding down a key. Represents polling+BLE notification power
},
central: {
idle: 490, // Idle current for connection to right half
typing: 380, // Current while holding down a key. Represents polling+BLE notification power
},
peripheral: {
idle: 20, // Idle current for connection to left half
typing: 365, // Current while holding down a key. Represents polling+BLE notification power
},
};
/**
* ZMK board power measurements
*
* Power supply can be an LDO or switching
* Quiescent and other quiescent are measured in micro amps
*
* Switching efficiency represents the efficiency of converting from
* 3.8V (average li-ion voltage) to the output voltage of the power supply
*/
export const zmkBoards = {
"nice!nano": {
name: "nice!nano",
powerSupply: {
type: "LDO",
outputVoltage: 3.3,
quiescentMicroA: 55,
},
otherQuiescentMicroA: 4,
},
"nice!60": {
powerSupply: {
type: "SWITCHING",
outputVoltage: 3.3,
efficiency: 0.95,
quiescentMicroA: 4,
},
otherQuiescentMicroA: 4,
},
};
export const underglowPower = {
firmware: 60, // ZMK power usage while underglow feature is turned on (SPIM mostly)
ledOn: 20000, // Estimated power consumption of a WS2812B at 100% (can be anywhere from 10mA to 30mA)
ledOff: 460, // Quiescent current of a WS2812B
};
export const displayPower = {
// Based on GoodDisplay's 1.02in epaper
EPAPER: {
activePercent: 0.05, // Estimated one refresh per minute taking three seconds
active: 1500, // Power draw during refresh
sleep: 5, // Idle power draw of an epaper
},
// 128x32 SSD1306
OLED: {
activePercent: 0.5, // Estimated sleeping half the time (based on idle)
active: 10000, // Estimated power draw when about half the pixels are on
sleep: 7, // Deep sleep power draw (display off)
},
};

View file

@ -0,0 +1,297 @@
/*
* Copyright (c) 2021 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
import React, { useState } from "react";
import classnames from "classnames";
import Layout from "@theme/Layout";
import styles from "./styles.module.css";
import PowerEstimate from "../components/power-estimate";
import CustomBoardForm from "../components/custom-board-form";
import { useInput } from "../utils/hooks";
import { zmkBoards } from "../data/power";
import "../css/power-profiler.css";
const Disclaimer = `This profiler makes many assumptions about typing
activity, battery characteristics, hardware behavior, and
doesn't account for error of user inputs. For example battery
mAh, which is often incorrectly advertised higher than it's actual capacity.
While it tries to estimate power usage using real power readings of ZMK,
every person will have different results that may be worse or even
better than the estimation given here.`;
function PowerProfiler() {
const { value: board, bind: bindBoard } = useInput("");
const { value: split, bind: bindSplit } = useInput(false);
const { value: batteryMilliAh, bind: bindBatteryMilliAh } = useInput(110);
const { value: psuType, bind: bindPsuType } = useInput("");
const { value: outputV, bind: bindOutputV } = useInput(3.3);
const { value: quiescentMicroA, bind: bindQuiescentMicroA } = useInput(55);
const {
value: otherQuiescentMicroA,
bind: bindOtherQuiescentMicroA,
} = useInput(0);
const { value: efficiency, bind: bindEfficiency } = useInput(0.9);
const { value: bondedQty, bind: bindBondedQty } = useInput(1);
const { value: percentAsleep, bind: bindPercentAsleep } = useInput(0.5);
const { value: glowEnabled, bind: bindGlowEnabled } = useInput(false);
const { value: glowQuantity, bind: bindGlowQuantity } = useInput(10);
const { value: glowBrightness, bind: bindGlowBrightness } = useInput(1);
const { value: displayEnabled, bind: bindDisplayEnabled } = useInput(false);
const { value: displayType, bind: bindDisplayType } = useInput("");
const [disclaimerAcknowledged, setDisclaimerAcknowledged] = useState(
typeof window !== "undefined"
? localStorage.getItem("zmkPowerProfilerDisclaimer") === "true"
: false
);
const currentBoard =
board === "custom"
? {
powerSupply: {
type: psuType,
outputVoltage: outputV,
quiescentMicroA: quiescentMicroA,
efficiency,
},
otherQuiescentMicroA: otherQuiescentMicroA,
}
: zmkBoards[board];
return (
<Layout
title={`ZMK Power Profiler`}
description="Estimate your keyboard's power usage and battery life on ZMK."
>
<header className={classnames("hero hero--primary", styles.heroBanner)}>
<div className="container">
<h1 className="hero__title">ZMK Power Profiler</h1>
<p className="hero__subtitle">
{"Estimate your keyboard's power usage and battery life on ZMK."}
</p>
</div>
</header>
<main>
<section className="container">
<div className="profilerSection">
<h3>Keyboard Specifications</h3>
<div className="row">
<div className="col col--4">
<div className="profilerInput">
<label>Board</label>
<select {...bindBoard}>
<option hidden value="">
Select a board
</option>
{Object.keys(zmkBoards).map((b) => (
<option key={b}>{b}</option>
))}
<option value="custom">Custom</option>
</select>
</div>
</div>
<div className="col col--4">
<div className="profilerInput">
<label>Split Keyboard</label>
<input
id="split"
checked={split}
{...bindSplit}
className="toggleInput"
type="checkbox"
/>
<label htmlFor="split" className="toggle">
<div className="toggleThumb" />
</label>
</div>
</div>
<div className="col col--4">
<div className="profilerInput">
<label>Battery Size</label>
<div className="inputBox">
<input {...bindBatteryMilliAh} type="number" />
<span>mAh</span>
</div>
</div>
</div>
</div>
</div>
{board === "custom" && (
<CustomBoardForm
bindPsuType={bindPsuType}
bindOutputV={bindOutputV}
bindEfficiency={bindEfficiency}
bindQuiescentMicroA={bindQuiescentMicroA}
bindOtherQuiescentMicroA={bindOtherQuiescentMicroA}
/>
)}
<div className="profilerSection">
<h3>Usage Values</h3>
<div className="row">
<div className="col col--4">
<div className="profilerInput">
<label>
Bonded Bluetooth Profiles{" "}
<span tooltip="The average number of host devices connected at once">
</span>
</label>
<input {...bindBondedQty} type="range" min="1" max="5" />
<span>{bondedQty}</span>
</div>
</div>
<div className="col col--4">
<div className="profilerInput">
<label>
Percentage Asleep{" "}
<span tooltip="How much time the keyboard is in deep sleep (15 min. default timeout)">
</span>
</label>
<input
{...bindPercentAsleep}
type="range"
min="0"
step=".1"
max="1"
/>
<span>{Math.round(percentAsleep * 100)}%</span>
</div>
</div>
</div>
</div>
<div className="profilerSection">
<h3>Features</h3>
<div className="row">
<div className="col col--4">
<div className="profilerInput">
<label>RGB Underglow</label>
<input
checked={glowEnabled}
id="glow"
{...bindGlowEnabled}
className="toggleInput"
type="checkbox"
/>
<label htmlFor="glow" className="toggle">
<div className="toggleThumb" />
</label>
</div>
{glowEnabled && (
<>
<div className="profilerInput">
<label>LED Quantity</label>
<div className="inputBox">
<input {...bindGlowQuantity} type="number" />
</div>
</div>
<div className="profilerInput">
<label>Brightness</label>
<input
{...bindGlowBrightness}
type="range"
min="0"
step=".01"
max="1"
/>
<span>{Math.round(glowBrightness * 100)}%</span>
</div>
</>
)}
</div>
<div className="col col--4">
<div className="profilerInput">
<label>Display</label>
<input
checked={displayEnabled}
id="display"
{...bindDisplayEnabled}
className="toggleInput"
type="checkbox"
/>
<label htmlFor="display" className="toggle">
<div className="toggleThumb" />
</label>
</div>
{displayEnabled && (
<div className="profilerInput">
<label>Display Type</label>
<select {...bindDisplayType}>
<option hidden selected>
Select type
</option>
<option value="EPAPER">ePaper</option>
<option value="OLED">OLED</option>
</select>
</div>
)}
</div>
</div>
</div>
{split ? (
<>
<PowerEstimate
board={currentBoard}
splitType="central"
batteryMilliAh={batteryMilliAh}
usage={{ bondedQty, percentAsleep }}
underglow={{ glowEnabled, glowBrightness, glowQuantity }}
display={{ displayEnabled, displayType }}
/>
<PowerEstimate
board={currentBoard}
splitType="peripheral"
batteryMilliAh={batteryMilliAh}
usage={{ bondedQty, percentAsleep }}
underglow={{ glowEnabled, glowBrightness, glowQuantity }}
display={{ displayEnabled, displayType }}
/>
</>
) : (
<PowerEstimate
board={currentBoard}
splitType="standalone"
batteryMilliAh={batteryMilliAh}
usage={{ bondedQty, percentAsleep }}
underglow={{ glowEnabled, glowBrightness, glowQuantity }}
display={{ displayEnabled, displayType }}
/>
)}
<div className="row">
<div className="col col--8 col--offset-2 profilerDisclaimer">
Disclaimer: {Disclaimer}
</div>
</div>
</section>
</main>
{!disclaimerAcknowledged && (
<div className="disclaimerHolder">
<div className="disclaimer">
<h3>Disclaimer</h3>
<p>{Disclaimer}</p>
<button
onClick={() => {
setDisclaimerAcknowledged(true);
localStorage.setItem("zmkPowerProfilerDisclaimer", true);
}}
>
I Understand
</button>
</div>
</div>
)}
</Layout>
);
}
export default PowerProfiler;

23
docs/src/utils/hooks.js Normal file
View file

@ -0,0 +1,23 @@
/*
* Copyright (c) 2021 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
import { useState } from "react";
export const useInput = (initialValue) => {
const [value, setValue] = useState(initialValue);
return {
value,
setValue,
bind: {
value,
onChange: (event) => {
const target = event.target;
setValue(target.type === "checkbox" ? target.checked : target.value);
},
},
};
};