Files
zmk/app/boards/shields/nice_view/widgets/status.c
Joel Spadin 6e7e0de2b6 feat(endpoints): add "no endpoint" value (#3140)
feat(endpoints): add "no endpoint" value

This adds ZMK_TRANSPORT_NONE, which can be set as the preferred
endpoint transport if you wish to prevent the keyboard from sending any
output. More usefully, it also is used to indicate that the preferred
endpoint is not available and it could not fall back to an available
one. To go along with this, many endpoint functions are renamed for
consistency, and a few new functions are added:

- zmk_endpoint_get_preferred_transport() returns the value that was set
  with zmk_endpoint_set_preferred_transport().

- zmk_endpoint_get_preferred() returns the endpoint that will be used
  if it is available. This endpoint always has the same transport as
  zmk_endpoint_get_preferred_transport().

- zmk_endpoint_is_connected() is a shortcut to check if the keyboard is
  actually connected to an endpoint.

This change is based on #2572 but without the option to disable endpoint
fallback. It does refactor code to allow adding that feature later.

fix(endpoints): Add endpoint setting upgrade

Adding ZMK_TRANSPORT_NONE at the start of enum zmk_transport results in
the preferred transport setting no longer representing the same values
when it is saved with earlier firmware and loaded with newer firmware.

To fix this, the "endpoints/preferred" setting is now deprecated and
replaced by "endpoints/preferred2". If the old setting is present, it
is interpreted as the old enum type, upgraded to the new type, and saved
immediately to the new setting. The old setting is then deleted.

To avoid this happening again in the future, enum zmk_transport now has
explicit values assigned to identifier, and a comment is also added to
explain that existing values must not be changed.

fix: Set default transport according to enabled transports

The default value for preferred_transport is now set to USB only if
CONFIG_ZMK_USB is enabled. If not, it falls back to BLE if
CONFIG_ZMK_BLE is enabled, then to "none" if nothing is enabled.
2026-02-12 01:51:42 -05:00

355 lines
12 KiB
C

/*
*
* Copyright (c) 2025 The ZMK Contributors
* SPDX-License-Identifier: MIT
*
*/
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
#include <zmk/battery.h>
#include <zmk/display.h>
#include "status.h"
#include <zmk/events/usb_conn_state_changed.h>
#include <zmk/event_manager.h>
#include <zmk/events/battery_state_changed.h>
#include <zmk/events/ble_active_profile_changed.h>
#include <zmk/events/endpoint_changed.h>
#include <zmk/events/wpm_state_changed.h>
#include <zmk/events/layer_state_changed.h>
#include <zmk/usb.h>
#include <zmk/ble.h>
#include <zmk/endpoints.h>
#include <zmk/keymap.h>
#include <zmk/wpm.h>
static sys_slist_t widgets = SYS_SLIST_STATIC_INIT(&widgets);
struct output_status_state {
struct zmk_endpoint_instance selected_endpoint;
int active_profile_index;
bool active_profile_connected;
bool active_profile_bonded;
bool profiles_connected[NICEVIEW_PROFILE_COUNT];
bool profiles_bonded[NICEVIEW_PROFILE_COUNT];
};
struct layer_status_state {
zmk_keymap_layer_index_t index;
const char *label;
};
struct wpm_status_state {
uint8_t wpm;
};
static void draw_top(lv_obj_t *widget, const struct status_state *state) {
lv_obj_t *canvas = lv_obj_get_child(widget, 0);
lv_draw_label_dsc_t label_dsc;
init_label_dsc(&label_dsc, LVGL_FOREGROUND, &lv_font_montserrat_16, LV_TEXT_ALIGN_RIGHT);
lv_draw_label_dsc_t label_dsc_wpm;
init_label_dsc(&label_dsc_wpm, LVGL_FOREGROUND, &lv_font_unscii_8, LV_TEXT_ALIGN_RIGHT);
lv_draw_rect_dsc_t rect_black_dsc;
init_rect_dsc(&rect_black_dsc, LVGL_BACKGROUND);
lv_draw_rect_dsc_t rect_white_dsc;
init_rect_dsc(&rect_white_dsc, LVGL_FOREGROUND);
lv_draw_line_dsc_t line_dsc;
init_line_dsc(&line_dsc, LVGL_FOREGROUND, 1);
// Fill background
lv_canvas_fill_bg(canvas, LVGL_BACKGROUND, LV_OPA_COVER);
// Draw battery
draw_battery(canvas, state);
// Draw output status
char output_text[10] = {};
switch (state->selected_endpoint.transport) {
case ZMK_TRANSPORT_USB:
strcat(output_text, LV_SYMBOL_USB);
break;
case ZMK_TRANSPORT_BLE:
if (state->active_profile_bonded) {
if (state->active_profile_connected) {
strcat(output_text, LV_SYMBOL_WIFI);
} else {
strcat(output_text, LV_SYMBOL_CLOSE);
}
} else {
strcat(output_text, LV_SYMBOL_SETTINGS);
}
break;
}
canvas_draw_text(canvas, 0, 0, CANVAS_SIZE, &label_dsc, output_text);
// Draw WPM
canvas_draw_rect(canvas, 0, 21, 68, 42, &rect_white_dsc);
canvas_draw_rect(canvas, 1, 22, 66, 40, &rect_black_dsc);
char wpm_text[6] = {};
snprintf(wpm_text, sizeof(wpm_text), "%d", state->wpm[9]);
canvas_draw_text(canvas, 42, 52, 24, &label_dsc_wpm, wpm_text);
int max = 0;
int min = 256;
for (int i = 0; i < 10; i++) {
if (state->wpm[i] > max) {
max = state->wpm[i];
}
if (state->wpm[i] < min) {
min = state->wpm[i];
}
}
int range = max - min;
if (range == 0) {
range = 1;
}
lv_point_t points[10];
for (int i = 0; i < 10; i++) {
points[i].x = 2 + i * 7;
points[i].y = 60 - (state->wpm[i] - min) * 36 / range;
}
canvas_draw_line(canvas, points, 10, &line_dsc);
// Rotate canvas
rotate_canvas(canvas);
}
static void draw_middle(lv_obj_t *widget, const struct status_state *state) {
lv_obj_t *canvas = lv_obj_get_child(widget, 1);
lv_draw_rect_dsc_t rect_black_dsc;
init_rect_dsc(&rect_black_dsc, LVGL_BACKGROUND);
lv_draw_rect_dsc_t rect_white_dsc;
init_rect_dsc(&rect_white_dsc, LVGL_FOREGROUND);
lv_draw_arc_dsc_t arc_dsc;
init_arc_dsc(&arc_dsc, LVGL_FOREGROUND, 2);
lv_draw_arc_dsc_t arc_dsc_filled;
init_arc_dsc(&arc_dsc_filled, LVGL_FOREGROUND, 9);
lv_draw_label_dsc_t label_dsc;
init_label_dsc(&label_dsc, LVGL_FOREGROUND, &lv_font_montserrat_18, LV_TEXT_ALIGN_CENTER);
lv_draw_label_dsc_t label_dsc_black;
init_label_dsc(&label_dsc_black, LVGL_BACKGROUND, &lv_font_montserrat_18, LV_TEXT_ALIGN_CENTER);
// Fill background
lv_canvas_fill_bg(canvas, LVGL_BACKGROUND, LV_OPA_COVER);
// Draw circles
int circle_offsets[NICEVIEW_PROFILE_COUNT][2] = {
{13, 13}, {55, 13}, {34, 34}, {13, 55}, {55, 55},
};
for (int i = 0; i < NICEVIEW_PROFILE_COUNT; i++) {
bool selected = i == state->active_profile_index;
if (state->profiles_connected[i]) {
canvas_draw_arc(canvas, circle_offsets[i][0], circle_offsets[i][1], 13, 0, 360,
&arc_dsc);
} else if (state->profiles_bonded[i]) {
const int segments = 8;
const int gap = 20;
for (int j = 0; j < segments; ++j)
canvas_draw_arc(canvas, circle_offsets[i][0], circle_offsets[i][1], 13,
360. / segments * j + gap / 2.0,
360. / segments * (j + 1) - gap / 2.0, &arc_dsc);
}
if (selected) {
canvas_draw_arc(canvas, circle_offsets[i][0], circle_offsets[i][1], 9, 0, 359,
&arc_dsc_filled);
}
char label[2];
snprintf(label, sizeof(label), "%d", i + 1);
canvas_draw_text(canvas, circle_offsets[i][0] - 8, circle_offsets[i][1] - 10, 16,
(selected ? &label_dsc_black : &label_dsc), label);
}
// Rotate canvas
rotate_canvas(canvas);
}
static void draw_bottom(lv_obj_t *widget, const struct status_state *state) {
lv_obj_t *canvas = lv_obj_get_child(widget, 2);
lv_draw_rect_dsc_t rect_black_dsc;
init_rect_dsc(&rect_black_dsc, LVGL_BACKGROUND);
lv_draw_label_dsc_t label_dsc;
init_label_dsc(&label_dsc, LVGL_FOREGROUND, &lv_font_montserrat_14, LV_TEXT_ALIGN_CENTER);
// Fill background
lv_canvas_fill_bg(canvas, LVGL_BACKGROUND, LV_OPA_COVER);
// Draw layer
if (state->layer_label == NULL || strlen(state->layer_label) == 0) {
char text[10] = {};
sprintf(text, "LAYER %i", state->layer_index);
canvas_draw_text(canvas, 0, 5, 68, &label_dsc, text);
} else {
canvas_draw_text(canvas, 0, 5, 68, &label_dsc, state->layer_label);
}
// Rotate canvas
rotate_canvas(canvas);
}
static void set_battery_status(struct zmk_widget_status *widget,
struct battery_status_state state) {
#if IS_ENABLED(CONFIG_USB_DEVICE_STACK)
widget->state.charging = state.usb_present;
#endif /* IS_ENABLED(CONFIG_USB_DEVICE_STACK) */
widget->state.battery = state.level;
draw_top(widget->obj, &widget->state);
}
static void battery_status_update_cb(struct battery_status_state state) {
struct zmk_widget_status *widget;
SYS_SLIST_FOR_EACH_CONTAINER(&widgets, widget, node) { set_battery_status(widget, state); }
}
static struct battery_status_state battery_status_get_state(const zmk_event_t *eh) {
const struct zmk_battery_state_changed *ev = as_zmk_battery_state_changed(eh);
return (struct battery_status_state){
.level = (ev != NULL) ? ev->state_of_charge : zmk_battery_state_of_charge(),
#if IS_ENABLED(CONFIG_USB_DEVICE_STACK)
.usb_present = zmk_usb_is_powered(),
#endif /* IS_ENABLED(CONFIG_USB_DEVICE_STACK) */
};
}
ZMK_DISPLAY_WIDGET_LISTENER(widget_battery_status, struct battery_status_state,
battery_status_update_cb, battery_status_get_state)
ZMK_SUBSCRIPTION(widget_battery_status, zmk_battery_state_changed);
#if IS_ENABLED(CONFIG_USB_DEVICE_STACK)
ZMK_SUBSCRIPTION(widget_battery_status, zmk_usb_conn_state_changed);
#endif /* IS_ENABLED(CONFIG_USB_DEVICE_STACK) */
static void set_output_status(struct zmk_widget_status *widget,
const struct output_status_state *state) {
widget->state.selected_endpoint = state->selected_endpoint;
widget->state.active_profile_index = state->active_profile_index;
widget->state.active_profile_connected = state->active_profile_connected;
widget->state.active_profile_bonded = state->active_profile_bonded;
for (int i = 0; i < NICEVIEW_PROFILE_COUNT; ++i) {
widget->state.profiles_connected[i] = state->profiles_connected[i];
widget->state.profiles_bonded[i] = state->profiles_bonded[i];
}
draw_top(widget->obj, &widget->state);
draw_middle(widget->obj, &widget->state);
}
static void output_status_update_cb(struct output_status_state state) {
struct zmk_widget_status *widget;
SYS_SLIST_FOR_EACH_CONTAINER(&widgets, widget, node) { set_output_status(widget, &state); }
}
static struct output_status_state output_status_get_state(const zmk_event_t *_eh) {
struct output_status_state state = {
.selected_endpoint = zmk_endpoint_get_selected(),
.active_profile_index = zmk_ble_active_profile_index(),
.active_profile_connected = zmk_ble_active_profile_is_connected(),
.active_profile_bonded = !zmk_ble_active_profile_is_open(),
};
for (int i = 0; i < MIN(NICEVIEW_PROFILE_COUNT, ZMK_BLE_PROFILE_COUNT); ++i) {
state.profiles_connected[i] = zmk_ble_profile_is_connected(i);
state.profiles_bonded[i] = !zmk_ble_profile_is_open(i);
}
return state;
}
ZMK_DISPLAY_WIDGET_LISTENER(widget_output_status, struct output_status_state,
output_status_update_cb, output_status_get_state)
ZMK_SUBSCRIPTION(widget_output_status, zmk_endpoint_changed);
#if IS_ENABLED(CONFIG_USB_DEVICE_STACK)
ZMK_SUBSCRIPTION(widget_output_status, zmk_usb_conn_state_changed);
#endif
#if defined(CONFIG_ZMK_BLE)
ZMK_SUBSCRIPTION(widget_output_status, zmk_ble_active_profile_changed);
#endif
static void set_layer_status(struct zmk_widget_status *widget, struct layer_status_state state) {
widget->state.layer_index = state.index;
widget->state.layer_label = state.label;
draw_bottom(widget->obj, &widget->state);
}
static void layer_status_update_cb(struct layer_status_state state) {
struct zmk_widget_status *widget;
SYS_SLIST_FOR_EACH_CONTAINER(&widgets, widget, node) { set_layer_status(widget, state); }
}
static struct layer_status_state layer_status_get_state(const zmk_event_t *eh) {
zmk_keymap_layer_index_t index = zmk_keymap_highest_layer_active();
return (struct layer_status_state){
.index = index, .label = zmk_keymap_layer_name(zmk_keymap_layer_index_to_id(index))};
}
ZMK_DISPLAY_WIDGET_LISTENER(widget_layer_status, struct layer_status_state, layer_status_update_cb,
layer_status_get_state)
ZMK_SUBSCRIPTION(widget_layer_status, zmk_layer_state_changed);
static void set_wpm_status(struct zmk_widget_status *widget, struct wpm_status_state state) {
for (int i = 0; i < 9; i++) {
widget->state.wpm[i] = widget->state.wpm[i + 1];
}
widget->state.wpm[9] = state.wpm;
draw_top(widget->obj, &widget->state);
}
static void wpm_status_update_cb(struct wpm_status_state state) {
struct zmk_widget_status *widget;
SYS_SLIST_FOR_EACH_CONTAINER(&widgets, widget, node) { set_wpm_status(widget, state); }
}
struct wpm_status_state wpm_status_get_state(const zmk_event_t *eh) {
return (struct wpm_status_state){.wpm = zmk_wpm_get_state()};
};
ZMK_DISPLAY_WIDGET_LISTENER(widget_wpm_status, struct wpm_status_state, wpm_status_update_cb,
wpm_status_get_state)
ZMK_SUBSCRIPTION(widget_wpm_status, zmk_wpm_state_changed);
int zmk_widget_status_init(struct zmk_widget_status *widget, lv_obj_t *parent) {
widget->obj = lv_obj_create(parent);
lv_obj_set_size(widget->obj, 160, 68);
lv_obj_t *top = lv_canvas_create(widget->obj);
lv_obj_align(top, LV_ALIGN_TOP_RIGHT, 0, 0);
lv_canvas_set_buffer(top, widget->cbuf, CANVAS_SIZE, CANVAS_SIZE, CANVAS_COLOR_FORMAT);
lv_obj_t *middle = lv_canvas_create(widget->obj);
lv_obj_align(middle, LV_ALIGN_TOP_LEFT, 24, 0);
lv_canvas_set_buffer(middle, widget->cbuf2, CANVAS_SIZE, CANVAS_SIZE, CANVAS_COLOR_FORMAT);
lv_obj_t *bottom = lv_canvas_create(widget->obj);
lv_obj_align(bottom, LV_ALIGN_TOP_LEFT, -44, 0);
lv_canvas_set_buffer(bottom, widget->cbuf3, CANVAS_SIZE, CANVAS_SIZE, CANVAS_COLOR_FORMAT);
sys_slist_append(&widgets, &widget->node);
widget_battery_status_init();
widget_output_status_init();
widget_layer_status_init();
widget_wpm_status_init();
return 0;
}
lv_obj_t *zmk_widget_status_obj(struct zmk_widget_status *widget) { return widget->obj; }