Merge branch 'zmkfirmware:main' into murphpad
This commit is contained in:
commit
8ed7588767
56 changed files with 4514 additions and 1525 deletions
2
.github/workflows/clang-format-lint.yml
vendored
2
.github/workflows/clang-format-lint.yml
vendored
|
@ -23,7 +23,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: DoozyX/clang-format-lint-action@v0.11
|
||||
- uses: DoozyX/clang-format-lint-action@v0.12
|
||||
with:
|
||||
source: "./app"
|
||||
extensions: "h,c"
|
||||
|
|
|
@ -42,7 +42,7 @@
|
|||
, <7 0 &gpio0 24 0> /* D6/A7 D7*/
|
||||
, <8 0 &gpio0 10 0> /* D8/A8 B4*/
|
||||
, <9 0 &gpio1 6 0> /* D9/A9 B5*/
|
||||
, <10 0 &gpio1 13 0> /* D10/A10 B6*/
|
||||
, <10 0 &gpio1 11 0> /* D10/A10 B6*/
|
||||
;
|
||||
};
|
||||
};
|
||||
|
|
|
@ -9,7 +9,6 @@ config ZMK_KEYBOARD_NAME
|
|||
config ZMK_USB
|
||||
default y
|
||||
|
||||
endif
|
||||
|
||||
if ZMK_DISPLAY
|
||||
|
||||
|
@ -46,3 +45,5 @@ choice LVGL_COLOR_DEPTH
|
|||
endchoice
|
||||
|
||||
endif # LVGL
|
||||
|
||||
endif
|
||||
|
|
|
@ -6,7 +6,6 @@ if SHIELD_TIDBIT
|
|||
config ZMK_KEYBOARD_NAME
|
||||
default "tidbit"
|
||||
|
||||
endif
|
||||
|
||||
if ZMK_DISPLAY
|
||||
|
||||
|
@ -43,3 +42,5 @@ choice LVGL_COLOR_DEPTH
|
|||
endchoice
|
||||
|
||||
endif # LVGL
|
||||
|
||||
endif
|
||||
|
|
|
@ -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);
|
||||
|
||||
if (err) {
|
||||
LOG_ERR("Unable to enable matrix GPIO interrupt");
|
||||
LOG_ERR("Unable to enable direct GPIO interrupt");
|
||||
return err;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -93,7 +93,7 @@ static int bvd_sample_fetch(const struct device *dev, enum sensor_channel chan)
|
|||
&val);
|
||||
|
||||
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);
|
||||
LOG_DBG("Percent: %d", percent);
|
||||
|
||||
|
|
9
app/include/zmk/battery.h
Normal file
9
app/include/zmk/battery.h
Normal file
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* Copyright (c) 2021 The ZMK Contributors
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
uint8_t zmk_battery_state_of_charge();
|
|
@ -17,7 +17,7 @@ testcases=$(find $path -name native_posix.keymap -exec dirname \{\} \;)
|
|||
num_cases=$(echo "$testcases" | wc -l)
|
||||
if [ $num_cases -gt 1 ]; then
|
||||
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=$?
|
||||
sort -k2 ./build/tests/pass-fail.log
|
||||
exit $err
|
||||
|
|
|
@ -15,10 +15,15 @@
|
|||
LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
|
||||
|
||||
#include <zmk/event_manager.h>
|
||||
#include <zmk/battery.h>
|
||||
#include <zmk/events/battery_state_changed.h>
|
||||
|
||||
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) {
|
||||
struct sensor_value state_of_charge;
|
||||
|
||||
|
@ -36,17 +41,23 @@ static int zmk_battery_update(const struct device *battery) {
|
|||
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);
|
||||
|
||||
rc = bt_bas_set_battery_level(last_state_of_charge);
|
||||
|
||||
if (rc != 0) {
|
||||
LOG_WRN("Failed to set BAS GATT battery level (err %d)", rc);
|
||||
return rc;
|
||||
}
|
||||
|
||||
return ZMK_EVENT_RAISE(new_zmk_battery_state_changed(
|
||||
(struct zmk_battery_state_changed){.state_of_charge = state_of_charge.val1}));
|
||||
rc = ZMK_EVENT_RAISE(new_zmk_battery_state_changed(
|
||||
(struct zmk_battery_state_changed){.state_of_charge = last_state_of_charge}));
|
||||
}
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
static void zmk_battery_work(struct k_work *work) {
|
||||
|
|
|
@ -177,6 +177,11 @@ static const struct behavior_driver_api behavior_sticky_key_driver_api = {
|
|||
.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) {
|
||||
struct zmk_keycode_state_changed *ev = as_zmk_keycode_state_changed(eh);
|
||||
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) {
|
||||
stop_timer(sticky_key);
|
||||
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);
|
||||
return ZMK_EV_EVENT_CAPTURED;
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
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) {
|
||||
struct active_sticky_key *sticky_key =
|
||||
CONTAINER_OF(item, struct active_sticky_key, release_timer);
|
||||
|
|
|
@ -192,7 +192,7 @@ static inline bool candidate_is_completely_pressed(struct combo_cfg *candidate)
|
|||
return pressed_keys[candidate->key_position_len - 1] != NULL;
|
||||
}
|
||||
|
||||
static void cleanup();
|
||||
static int cleanup();
|
||||
|
||||
static int filter_timed_out_candidates(int64_t timestamp) {
|
||||
int num_candidates = 0;
|
||||
|
@ -224,7 +224,7 @@ static int clear_candidates() {
|
|||
}
|
||||
|
||||
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) {
|
||||
continue;
|
||||
}
|
||||
|
@ -236,24 +236,26 @@ static int capture_pressed_key(const zmk_event_t *ev) {
|
|||
|
||||
const struct zmk_listener zmk_listener_combo;
|
||||
|
||||
static void release_pressed_keys() {
|
||||
// release the first key that was pressed
|
||||
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;
|
||||
}
|
||||
static int release_pressed_keys() {
|
||||
for (int i = 0; i < CONFIG_ZMK_COMBO_MAX_KEYS_PER_COMBO; i++) {
|
||||
const zmk_event_t *captured_event = pressed_keys[i];
|
||||
if (pressed_keys[i] == NULL) {
|
||||
return i;
|
||||
}
|
||||
pressed_keys[i] = NULL;
|
||||
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) {
|
||||
struct zmk_behavior_binding_event event = {
|
||||
|
@ -360,14 +362,14 @@ static bool release_combo_key(int32_t position, int64_t timestamp) {
|
|||
return false;
|
||||
}
|
||||
|
||||
static void cleanup() {
|
||||
static int cleanup() {
|
||||
k_delayed_work_cancel(&timeout_task);
|
||||
clear_candidates();
|
||||
if (fully_pressed_combo != NULL) {
|
||||
activate_combo(fully_pressed_combo);
|
||||
fully_pressed_combo = NULL;
|
||||
}
|
||||
release_pressed_keys();
|
||||
return release_pressed_keys();
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
struct combo_cfg *candidate_combo = candidates[0].combo;
|
||||
LOG_DBG("combo: capturing position event %d", data->position);
|
||||
int ret = capture_pressed_key(ev);
|
||||
switch (num_candidates) {
|
||||
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) {
|
||||
cleanup();
|
||||
if (release_combo_key(ev->position, ev->timestamp)) {
|
||||
static int position_state_up(const zmk_event_t *ev, struct zmk_position_state_changed *data) {
|
||||
int released_keys = cleanup();
|
||||
if (release_combo_key(data->position, data->timestamp)) {
|
||||
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) {
|
||||
|
@ -447,7 +455,7 @@ static int position_state_changed_listener(const zmk_event_t *ev) {
|
|||
if (data->state) { // keydown
|
||||
return position_state_down(ev, data);
|
||||
} else { // keyup
|
||||
return position_state_up(data);
|
||||
return position_state_up(ev, data);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -22,13 +22,13 @@ int zmk_event_manager_handle_from(zmk_event_t *event, uint8_t start_index) {
|
|||
uint8_t len = __event_subscriptions_end - __event_subscriptions_start;
|
||||
for (int i = start_index; i < len; 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) {
|
||||
continue;
|
||||
}
|
||||
ret = ev_sub->listener->callback(event);
|
||||
if (ret < 0) {
|
||||
LOG_DBG("Listener returned an error: %d", ret);
|
||||
goto release;
|
||||
} else if (ret > 0) {
|
||||
switch (ret) {
|
||||
case ZMK_EV_EVENT_BUBBLE:
|
||||
continue;
|
||||
case ZMK_EV_EVENT_HANDLED:
|
||||
LOG_DBG("Listener handled the event");
|
||||
ret = 0;
|
||||
|
@ -38,8 +38,9 @@ int zmk_event_manager_handle_from(zmk_event_t *event, uint8_t start_index) {
|
|||
event->last_listener_index = i;
|
||||
// Listeners are expected to free events they capture
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
default:
|
||||
LOG_DBG("Listener returned an error: %d", ret);
|
||||
goto release;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
bool pressed = (ev.state == ZMK_KSCAN_EVENT_STATE_PRESSED);
|
||||
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"));
|
||||
ZMK_EVENT_RAISE(new_zmk_position_state_changed((struct zmk_position_state_changed){
|
||||
.state = pressed, .position = position, .timestamp = k_uptime_get()}));
|
||||
|
|
|
@ -1,2 +1 @@
|
|||
s/.*hid_listener_keycode_//p
|
||||
s/.*combo//p
|
|
@ -1,2 +1 @@
|
|||
s/.*hid_listener_keycode_//p
|
||||
s/.*combo//p
|
|
@ -1,2 +1 @@
|
|||
s/.*hid_listener_keycode_//p
|
||||
s/.*combo//p
|
|
@ -1,2 +1 @@
|
|||
s/.*hid_listener_keycode_//p
|
||||
s/.*combo//p
|
|
@ -1,2 +1 @@
|
|||
s/.*hid_listener_keycode_//p
|
||||
s/.*combo//p
|
|
@ -1,2 +1 @@
|
|||
s/.*hid_listener_keycode_//p
|
||||
s/.*combo//p
|
|
@ -1,2 +1 @@
|
|||
s/.*hid_listener_keycode_//p
|
||||
s/.*combo//p
|
|
@ -1,2 +1 @@
|
|||
s/.*hid_listener_keycode_//p
|
||||
s/.*combo//p
|
|
@ -1,2 +1 @@
|
|||
s/.*hid_listener_keycode_//p
|
||||
s/.*combo//p
|
1
app/tests/combo/press-release-long-combo/events.patterns
Normal file
1
app/tests/combo/press-release-long-combo/events.patterns
Normal file
|
@ -0,0 +1 @@
|
|||
s/.*hid_listener_keycode_//p
|
|
@ -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
|
35
app/tests/combo/press-release-long-combo/native_posix.keymap
Normal file
35
app/tests/combo/press-release-long-combo/native_posix.keymap
Normal 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)
|
||||
>;
|
||||
};
|
|
@ -1,2 +1 @@
|
|||
s/.*hid_listener_keycode_//p
|
||||
s/.*combo/combo/p
|
|
@ -1,2 +1 @@
|
|||
s/.*hid_listener_keycode_//p
|
||||
s/.*combo/combo/p
|
||||
|
|
|
@ -1,2 +1 @@
|
|||
s/.*hid_listener_keycode_//p
|
||||
s/.*combo/combo/p
|
|
@ -0,0 +1 @@
|
|||
s/.*hid_listener_keycode_//p
|
|
@ -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
|
|
@ -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)
|
||||
>;
|
||||
};
|
|
@ -10,7 +10,6 @@ module.exports = {
|
|||
"plugin:react/recommended",
|
||||
"plugin:mdx/recommended",
|
||||
"prettier",
|
||||
"prettier/react",
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
|
|
BIN
docs/docs/assets/features/beta-testing/pr-repo-branch.png
Normal file
BIN
docs/docs/assets/features/beta-testing/pr-repo-branch.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 74 KiB |
BIN
docs/docs/assets/features/beta-testing/repo-branch.png
Normal file
BIN
docs/docs/assets/features/beta-testing/repo-branch.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 62 KiB |
BIN
docs/docs/assets/features/beta-testing/repo-url.png
Normal file
BIN
docs/docs/assets/features/beta-testing/repo-url.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 77 KiB |
|
@ -56,7 +56,7 @@ For example, if you press `&mt LEFT_SHIFT A` and then release it without pressin
|
|||
```
|
||||
&mt {
|
||||
retro-tap;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
#### Home row mods
|
||||
|
|
73
docs/docs/behaviors/mod-morph.md
Normal file
73
docs/docs/behaviors/mod-morph.md
Normal 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)>;
|
||||
```
|
|
@ -40,8 +40,8 @@ You can configure a different tapping term in your keymap:
|
|||
/ {
|
||||
keymap {
|
||||
...
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
### Additional information
|
||||
|
|
|
@ -38,8 +38,8 @@ You can configure a different `release-after-ms` in your keymap:
|
|||
/ {
|
||||
keymap {
|
||||
...
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
### Advanced usage
|
||||
|
|
|
@ -32,8 +32,8 @@ You can configure a different `release-after-ms` in your keymap:
|
|||
/ {
|
||||
keymap {
|
||||
...
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
### Advanced usage
|
||||
|
|
100
docs/docs/features/beta-testing.md
Normal file
100
docs/docs/features/beta-testing.md
Normal 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
|
||||
|
||||

|
||||
|
||||
### Finding the Repository URL
|
||||
|
||||

|
||||
|
||||
### Finding the Repository Branch
|
||||
|
||||

|
||||
|
||||
## 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>
|
|
@ -18,7 +18,6 @@ Combos configured in your `.keymap` file, but are separate from the `keymap` nod
|
|||
timeout-ms = <50>;
|
||||
key-positions = <0 1>;
|
||||
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.
|
||||
- `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.
|
||||
- `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.
|
||||
- (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.
|
||||
|
||||
|
|
|
@ -41,4 +41,4 @@ Here, the left encoder is configured to control volume up and down while the rig
|
|||
|
||||
## 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.
|
||||
|
|
|
@ -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.
|
||||
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.
|
||||
For other boards, you must select an SPI definition that has the `MOSI` pin as your data pin going to your LED strip.
|
||||
### nRF52-based boards
|
||||
|
||||
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 {
|
||||
|
@ -87,11 +93,15 @@ Here's an example of an nRF52 SPI definition:
|
|||
|
||||
:::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.
|
||||
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).
|
||||
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 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`:
|
||||
|
||||
```
|
||||
|
|
|
@ -10,6 +10,9 @@ module.exports = {
|
|||
projectName: "zmk", // Usually your repo name.
|
||||
plugins: [path.resolve(__dirname, "src/docusaurus-tree-sitter-plugin")],
|
||||
themeConfig: {
|
||||
colorMode: {
|
||||
respectPrefersColorScheme: true,
|
||||
},
|
||||
googleAnalytics: {
|
||||
trackingID: "UA-145201102-2",
|
||||
anonymizeIP: true,
|
||||
|
@ -29,6 +32,11 @@ module.exports = {
|
|||
position: "left",
|
||||
},
|
||||
{ to: "blog", label: "Blog", position: "left" },
|
||||
{
|
||||
to: "power-profiler",
|
||||
label: "Power Profiler",
|
||||
position: "left",
|
||||
},
|
||||
{
|
||||
href: "https://github.com/zmkfirmware/zmk",
|
||||
label: "GitHub",
|
||||
|
|
4463
docs/package-lock.json
generated
4463
docs/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -17,13 +17,13 @@
|
|||
"@docusaurus/core": "^2.0.0-alpha.66",
|
||||
"@docusaurus/preset-classic": "^2.0.0-alpha.66",
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.32",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.15.1",
|
||||
"@fortawesome/react-fontawesome": "^0.1.12",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.15.3",
|
||||
"@fortawesome/react-fontawesome": "^0.1.14",
|
||||
"classnames": "^2.2.6",
|
||||
"react": "^16.8.4",
|
||||
"react": "^16.14.0",
|
||||
"react-async": "^10.0.1",
|
||||
"react-copy-to-clipboard": "^5.0.2",
|
||||
"react-dom": "^16.8.4",
|
||||
"react-dom": "^16.14.0",
|
||||
"react-toastify": "^6.0.9",
|
||||
"web-tree-sitter": "^0.17.1"
|
||||
},
|
||||
|
@ -40,10 +40,10 @@
|
|||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^7.12.0",
|
||||
"eslint-config-prettier": "^6.14.0",
|
||||
"eslint-plugin-mdx": "^1.8.2",
|
||||
"eslint-plugin-react": "^7.21.5",
|
||||
"eslint": "^7.25.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-mdx": "^1.13.0",
|
||||
"eslint-plugin-react": "^7.23.2",
|
||||
"null-loader": "^3.0.0",
|
||||
"prettier": "2.1.2",
|
||||
"string-replace-loader": "2.3"
|
||||
|
|
|
@ -14,6 +14,7 @@ module.exports = {
|
|||
"features/displays",
|
||||
"features/encoders",
|
||||
"features/underglow",
|
||||
"features/beta-testing",
|
||||
],
|
||||
Behaviors: [
|
||||
"behaviors/key-press",
|
||||
|
@ -21,6 +22,7 @@ module.exports = {
|
|||
"behaviors/misc",
|
||||
"behaviors/hold-tap",
|
||||
"behaviors/mod-tap",
|
||||
"behaviors/mod-morph",
|
||||
"behaviors/sticky-key",
|
||||
"behaviors/sticky-layer",
|
||||
"behaviors/reset",
|
||||
|
|
100
docs/src/components/custom-board-form.js
Normal file
100
docs/src/components/custom-board-form.js
Normal 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;
|
266
docs/src/components/power-estimate.js
Normal file
266
docs/src/components/power-estimate.js
Normal 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;
|
|
@ -4,6 +4,24 @@
|
|||
* 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 {
|
||||
position: sticky;
|
||||
z-index: 1;
|
||||
|
@ -132,7 +150,7 @@ html[data-theme="light"] .codes.os.legend {
|
|||
}
|
||||
|
||||
.codes .os {
|
||||
color: black;
|
||||
color: var(--codes-os-fg);
|
||||
}
|
||||
|
||||
.codes td.os {
|
||||
|
@ -144,23 +162,23 @@ html[data-theme="light"] .codes.os.legend {
|
|||
}
|
||||
|
||||
.codes .os.windows {
|
||||
background: #caedfd;
|
||||
background: var(--codes-os-windows-bg);
|
||||
}
|
||||
|
||||
.codes .os.linux {
|
||||
background: #fff2ca;
|
||||
background: var(--codes-os-linux-bg);
|
||||
}
|
||||
|
||||
.codes .os.android {
|
||||
background: #d8eed9;
|
||||
background: var(--codes-os-android-bg);
|
||||
}
|
||||
|
||||
.codes .os.macos {
|
||||
background: #ececec;
|
||||
background: var(--codes-os-macos-bg);
|
||||
}
|
||||
|
||||
.codes .os.ios {
|
||||
background: #ffffff;
|
||||
background: var(--codes-os-ios-bg);
|
||||
}
|
||||
|
||||
.codes .footnotes {
|
||||
|
|
81
docs/src/css/power-estimate.css
Normal file
81
docs/src/css/power-estimate.css
Normal 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;
|
||||
}
|
195
docs/src/css/power-profiler.css
Normal file
195
docs/src/css/power-profiler.css
Normal 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
78
docs/src/data/power.js
Normal 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)
|
||||
},
|
||||
};
|
297
docs/src/pages/power-profiler.js
Normal file
297
docs/src/pages/power-profiler.js
Normal 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
23
docs/src/utils/hooks.js
Normal 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);
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
Loading…
Add table
Reference in a new issue