diff --git a/.github/workflows/build-container.yml b/.github/workflows/build-container.yml index faf379d36c6..3dd5a95c01d 100644 --- a/.github/workflows/build-container.yml +++ b/.github/workflows/build-container.yml @@ -99,6 +99,7 @@ jobs: --arg branch "$VERSION_BRANCH" \ --arg digest "$digest" \ --arg api_version "$api_version" \ + --argjson boards '["glove80", "go60"]' \ > "/tmp/$VERSION_NAME.json" - name: Upload image metadata file to versions bucket run: aws s3 cp "/tmp/$VERSION_NAME.json" "s3://$VERSIONS_BUCKET/images/$VERSION_NAME.json" diff --git a/.github/workflows/nix-build.yml b/.github/workflows/nix-build.yml index 345a6bdcf3d..d8f6d37be25 100644 --- a/.github/workflows/nix-build.yml +++ b/.github/workflows/nix-build.yml @@ -1,4 +1,4 @@ -name: Build Glove80 Firmware +name: Build Firmware on: push: @@ -20,8 +20,13 @@ on: jobs: build: - name: Build Glove80 Firmware + name: Build Firmware runs-on: ubuntu-latest + strategy: + matrix: + board: + - glove80 + - go60 steps: - uses: actions/checkout@v4 - uses: cachix/install-nix-action@v27 @@ -32,15 +37,15 @@ jobs: name: moergo-glove80-zmk-dev authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" skipPush: "${{ github.repository != 'moergo-sc/zmk' }}" - - name: Build Glove80 combined firmware - run: nix-build -A glove80_combined -o combined + - name: Build ${{ matrix.board }} combined firmware + run: nix-build -A ${{matrix.board}}_combined -o combined - name: Copy result out of nix store - run: cp combined/glove80.uf2 glove80.uf2 + run: cp combined/${{matrix.board}}.uf2 ${{matrix.board}}.uf2 - name: Upload result uses: actions/upload-artifact@v4 with: - name: glove80.uf2 - path: glove80.uf2 + name: ${{matrix.board}}.uf2 + path: ${{matrix.board}}.uf2 release: name: Create Release for Tag if: >- @@ -50,13 +55,17 @@ jobs: needs: build runs-on: ubuntu-latest steps: - - name: Download compiled firmware artifact + - name: Download Glove80 firmware artifact uses: actions/download-artifact@v4 with: name: glove80.uf2 + - name: Download Go60 firmware artifact + uses: actions/download-artifact@v4 + with: + name: go60.uf2 - name: Create Release for Tag uses: ncipollo/release-action@v1 with: - artifacts: "glove80.uf2" + artifacts: "glove80.uf2,go60.uf2" artifactErrorsFailBuild: true generateReleaseNotes: true diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 60c502fcd2a..cabd8505e38 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -41,6 +41,7 @@ target_sources(app PRIVATE src/events/sensor_event.c) target_sources_ifdef(CONFIG_ZMK_WPM app PRIVATE src/events/wpm_state_changed.c) target_sources_ifdef(CONFIG_USB_DEVICE_STACK app PRIVATE src/events/usb_conn_state_changed.c) target_sources(app PRIVATE src/behaviors/behavior_reset.c) +target_sources(app PRIVATE src/behaviors/behavior_transparent.c) target_sources_ifdef(CONFIG_ZMK_EXT_POWER app PRIVATE src/behaviors/behavior_ext_power.c) target_sources_ifdef(CONFIG_ZMK_BEHAVIOR_SOFT_OFF app PRIVATE src/behaviors/behavior_soft_off.c) add_subdirectory_ifdef(CONFIG_ZMK_POINTING src/pointing/) @@ -58,7 +59,6 @@ if ((NOT CONFIG_ZMK_SPLIT) OR CONFIG_ZMK_SPLIT_ROLE_CENTRAL) target_sources(app PRIVATE src/behaviors/behavior_outputs.c) target_sources(app PRIVATE src/behaviors/behavior_toggle_layer.c) target_sources(app PRIVATE src/behaviors/behavior_to_layer.c) - target_sources(app PRIVATE src/behaviors/behavior_transparent.c) target_sources(app PRIVATE src/behaviors/behavior_none.c) target_sources_ifdef(CONFIG_ZMK_BEHAVIOR_SENSOR_ROTATE app PRIVATE src/behaviors/behavior_sensor_rotate.c) target_sources_ifdef(CONFIG_ZMK_BEHAVIOR_SENSOR_ROTATE_VAR app PRIVATE src/behaviors/behavior_sensor_rotate_var.c) @@ -71,16 +71,12 @@ if ((NOT CONFIG_ZMK_SPLIT) OR CONFIG_ZMK_SPLIT_ROLE_CENTRAL) target_sources(app PRIVATE src/behavior_queue.c) target_sources(app PRIVATE src/conditional_layer.c) target_sources(app PRIVATE src/endpoints.c) - target_sources(app PRIVATE src/events/endpoint_changed.c) target_sources(app PRIVATE src/hid_listener.c) target_sources(app PRIVATE src/keymap.c) - target_sources(app PRIVATE src/events/layer_state_changed.c) - target_sources(app PRIVATE src/events/modifiers_state_changed.c) target_sources(app PRIVATE src/events/keycode_state_changed.c) target_sources_ifdef(CONFIG_ZMK_HID_INDICATORS app PRIVATE src/hid_indicators.c) if (CONFIG_ZMK_BLE) - target_sources(app PRIVATE src/events/ble_active_profile_changed.c) target_sources(app PRIVATE src/behaviors/behavior_bt.c) target_sources(app PRIVATE src/ble.c) target_sources(app PRIVATE src/hog.c) @@ -88,6 +84,12 @@ if ((NOT CONFIG_ZMK_SPLIT) OR CONFIG_ZMK_SPLIT_ROLE_CENTRAL) endif() target_sources_ifdef(CONFIG_ZMK_RGB_UNDERGLOW app PRIVATE src/behaviors/behavior_rgb_underglow.c) +target_sources_ifdef(CONFIG_ZMK_RGB_UNDERGLOW app PRIVATE src/behaviors/behavior_underglow_color.c) +if (CONFIG_ZMK_HID_INDICATORS) + target_sources_ifdef(CONFIG_ZMK_RGB_UNDERGLOW app PRIVATE src/behaviors/behavior_underglow_indicators.c) +endif() +target_sources_ifdef(CONFIG_ZMK_RGB_UNDERGLOW app PRIVATE src/behaviors/behavior_underglow_battery.c) +target_sources_ifdef(CONFIG_ZMK_RGB_UNDERGLOW app PRIVATE src/events/underglow_color_changed.c) target_sources_ifdef(CONFIG_ZMK_BACKLIGHT app PRIVATE src/behaviors/behavior_backlight.c) target_sources_ifdef(CONFIG_ZMK_BATTERY_REPORTING app PRIVATE src/events/battery_state_changed.c) @@ -95,12 +97,21 @@ target_sources_ifdef(CONFIG_ZMK_BATTERY_REPORTING app PRIVATE src/battery.c) target_sources_ifdef(CONFIG_ZMK_HID_INDICATORS app PRIVATE src/events/hid_indicators_changed.c) +target_sources_ifdef(CONFIG_ZMK_BLE app PRIVATE src/events/ble_active_profile_changed.c) + + +target_sources(app PRIVATE src/events/layer_state_changed.c) +target_sources(app PRIVATE src/events/modifiers_state_changed.c) +target_sources(app PRIVATE src/events/endpoint_changed.c) + target_sources_ifdef(CONFIG_ZMK_SPLIT app PRIVATE src/events/split_peripheral_status_changed.c) +target_sources_ifdef(CONFIG_ZMK_SPLIT app PRIVATE src/events/split_peripheral_layer_changed.c) add_subdirectory_ifdef(CONFIG_ZMK_SPLIT src/split) target_sources_ifdef(CONFIG_USB_DEVICE_STACK app PRIVATE src/usb.c) target_sources_ifdef(CONFIG_ZMK_USB app PRIVATE src/usb_hid.c) target_sources_ifdef(CONFIG_ZMK_RGB_UNDERGLOW app PRIVATE src/rgb_underglow.c) +target_sources_ifdef(CONFIG_ZMK_RGB_UNDERGLOW app PRIVATE src/rgb_underglow_layer.c) target_sources_ifdef(CONFIG_ZMK_BACKLIGHT app PRIVATE src/backlight.c) target_sources_ifdef(CONFIG_ZMK_LOW_PRIORITY_WORK_QUEUE app PRIVATE src/workqueue.c) target_sources(app PRIVATE src/main.c) diff --git a/app/Kconfig b/app/Kconfig index 01de7dcdcf5..73042f16fd9 100644 --- a/app/Kconfig +++ b/app/Kconfig @@ -345,6 +345,10 @@ config ZMK_RGB_UNDERGLOW_AUTO_OFF_USB bool "Turn off RGB underglow when USB is disconnected" depends on USB_DEVICE_STACK +config EXPERIMENTAL_RGB_LAYER + bool "Experimental per-key per-layer RGB underglow" + default n + endif # ZMK_RGB_UNDERGLOW menuconfig ZMK_BACKLIGHT diff --git a/app/boards/arm/glove80/glove80_lh.dts b/app/boards/arm/glove80/glove80_lh.dts index f37661c68a4..51978846a7e 100644 --- a/app/boards/arm/glove80/glove80_lh.dts +++ b/app/boards/arm/glove80/glove80_lh.dts @@ -20,6 +20,15 @@ zmk,underglow-indicators = &underglow_indicators; }; + underglow-layer { + compatible = "zmk,underglow-layer"; + pixel-lookup = + <52>, <53>, <54>, <69>, <70>, <71>, <15>, <27>, <39>, <51>, <4>, <14>, <26>, <38>, + <50>, <68>, <3>, <13>, <25>, <37>, <49>, <67>, <2>, <12>, <24>, <36>, <48>, <66>, + <1>, <11>, <23>, <35>, <47>, <65>, <0>, <10>, <22>, <34>, <46>, <64>; + }; + + back_led_backlight: pwmleds { compatible = "pwm-leds"; pwm_led_0 { diff --git a/app/boards/arm/glove80/glove80_lh_defconfig b/app/boards/arm/glove80/glove80_lh_defconfig index 57c72c8be9d..d36173bb1d8 100644 --- a/app/boards/arm/glove80/glove80_lh_defconfig +++ b/app/boards/arm/glove80/glove80_lh_defconfig @@ -102,6 +102,9 @@ CONFIG_ZMK_HID_CONSUMER_REPORT_USAGES_BASIC=y CONFIG_ZMK_USB_BOOT=y CONFIG_ZMK_HID_INDICATORS=y +# Send HID indicator to peripherals +CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS=y + # Turn on debugging to disable optimization. Debug messages can result in larger # stacks, so enable stack protection and particularly a larger BLE peripheral stack. # CONFIG_DEBUG=y diff --git a/app/boards/arm/glove80/glove80_rh.dts b/app/boards/arm/glove80/glove80_rh.dts index 7b54f62c858..bcb6a41b753 100644 --- a/app/boards/arm/glove80/glove80_rh.dts +++ b/app/boards/arm/glove80/glove80_rh.dts @@ -20,6 +20,15 @@ zmk,battery = &vbatt; }; + underglow-layer { + compatible = "zmk,underglow-layer"; + pixel-lookup = + <57>, <56>, <55>, <74>, <73>, <72>, <16>, <28>, <40>, <58>, <5>, <17>, <29>, <41>, + <59>, <75>, <6>, <18>, <30>, <42>, <60>, <76>, <7>, <19>, <31>, <43>, <61>, <77>, + <8>, <20>, <32>, <44>, <62>, <78>, <9>, <21>, <33>, <45>, <63>, <79>; + }; + + back_led_backlight: pwmleds { compatible = "pwm-leds"; pwm_led_0 { diff --git a/app/boards/arm/glove80/glove80_rh_defconfig b/app/boards/arm/glove80/glove80_rh_defconfig index 4394bee2ad2..ebceb6c4530 100644 --- a/app/boards/arm/glove80/glove80_rh_defconfig +++ b/app/boards/arm/glove80/glove80_rh_defconfig @@ -69,6 +69,10 @@ CONFIG_ZMK_RGB_UNDERGLOW_HUE_START=285 CONFIG_ZMK_RGB_UNDERGLOW_SAT_START=75 CONFIG_ZMK_RGB_UNDERGLOW_BRT_START=16 +# Enable HID indicators on peripheral +CONFIG_ZMK_HID_INDICATORS=y +CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS=y + # The power LED is implemented as a backlight # For now, the power LED is acting as a "USB connected" indicator CONFIG_ZMK_BACKLIGHT=y diff --git a/app/boards/arm/go60/CMakeLists.txt b/app/boards/arm/go60/CMakeLists.txt new file mode 100644 index 00000000000..3eb2cd276ea --- /dev/null +++ b/app/boards/arm/go60/CMakeLists.txt @@ -0,0 +1,3 @@ +zephyr_library() +zephyr_library_sources(usb_serial_number.c) +zephyr_library_include_directories(${ZEPHYR_BASE}/drivers) diff --git a/app/boards/arm/go60/Kconfig b/app/boards/arm/go60/Kconfig new file mode 100644 index 00000000000..c0ba7e5c526 --- /dev/null +++ b/app/boards/arm/go60/Kconfig @@ -0,0 +1,8 @@ +# Copyright (c) 2021 The ZMK Contributors +# SPDX-License-Identifier: MIT + +config BOARD_ENABLE_DCDC + bool "Enable DCDC mode" + select SOC_DCDC_NRF52X + default y + depends on (BOARD_GO60_LH || BOARD_GO60_RH) diff --git a/app/boards/arm/go60/Kconfig.board b/app/boards/arm/go60/Kconfig.board new file mode 100644 index 00000000000..351f51b0f4b --- /dev/null +++ b/app/boards/arm/go60/Kconfig.board @@ -0,0 +1,10 @@ +# Copyright (c) 2021 The ZMK Contributors +# SPDX-License-Identifier: MIT + +config BOARD_GO60_LH + bool "Go60 LH" + depends on SOC_NRF52840_QIAA + +config BOARD_GO60_RH + bool "Go60 RH" + depends on SOC_NRF52840_QIAA diff --git a/app/boards/arm/go60/Kconfig.defconfig b/app/boards/arm/go60/Kconfig.defconfig new file mode 100644 index 00000000000..eb2ab28753f --- /dev/null +++ b/app/boards/arm/go60/Kconfig.defconfig @@ -0,0 +1,77 @@ +# Copyright (c) 2021 The ZMK Contributors +# SPDX-License-Identifier: MIT + +if BOARD_GO60_LH + +config BOARD + default "go60 lh" + +config ZMK_SPLIT_ROLE_CENTRAL + default y + +endif # BOARD_GO60_LH + +if BOARD_GO60_RH + +config BOARD + default "go60 rh" + +endif # BOARD_GO60_RH + +if BOARD_GO60_LH || BOARD_GO60_RH + +config UART_0_INTERRUPT_DRIVEN + depends on !ZMK_SPLIT_WIRED_UART_MODE_ASYNC + +config ZMK_SPLIT + default y + +config ZMK_POINTING + default y + +config ZMK_POINTING_SMOOTH_SCROLLING + default n + +config ZMK_IDLE_TIMEOUT + default 120000 + +config BT_CTLR + default BT + +config ZMK_KSCAN_MATRIX_WAIT_BETWEEN_OUTPUTS + default 5 + +config PINCTRL + default y + +if USB + +config USB_NRFX + default y + +config USB_DEVICE_STACK + default y + +endif # USB + +if ZMK_BACKLIGHT + +config PWM + default y + +config LED_PWM + default y + +endif # ZMK_BACKLIGHT + +if ZMK_RGB_UNDERGLOW + +config SPI + default y + +config WS2812_STRIP + default y + +endif # ZMK_RGB_UNDERGLOW + +endif # BOARD_GO60_LH || BOARD_GO60_RH diff --git a/app/boards/arm/go60/board.cmake b/app/boards/arm/go60/board.cmake new file mode 100644 index 00000000000..ed0e07a508f --- /dev/null +++ b/app/boards/arm/go60/board.cmake @@ -0,0 +1,6 @@ +# Copyright (c) 2021 The ZMK Contributors +# SPDX-License-Identifier: MIT + +board_runner_args(nrfjprog "--nrf-family=NRF52" "--softreset") +include(${ZEPHYR_BASE}/boards/common/uf2.board.cmake) +include(${ZEPHYR_BASE}/boards/common/nrfjprog.board.cmake) diff --git a/app/boards/arm/go60/go60-layouts.dtsi b/app/boards/arm/go60/go60-layouts.dtsi new file mode 100644 index 00000000000..c9f49263d9e --- /dev/null +++ b/app/boards/arm/go60/go60-layouts.dtsi @@ -0,0 +1,74 @@ +#include + +/ { + physical_layout0: physical_layout_0 { + compatible = "zmk,physical-layout"; + display-name = "Default"; + + kscan = <&kscan0>; + transform = <&matrix_transform0>; + + keys // w h x y rot rx ry + = <&key_physical_attrs 100 100 0 50 0 0 0> + , <&key_physical_attrs 100 100 100 50 0 0 0> + , <&key_physical_attrs 100 100 200 0 0 0 0> + , <&key_physical_attrs 100 100 300 0 0 0 0> + , <&key_physical_attrs 100 100 400 0 0 0 0> + , <&key_physical_attrs 100 100 500 0 0 0 0> + , <&key_physical_attrs 100 100 1150 0 0 0 0> + , <&key_physical_attrs 100 100 1250 0 0 0 0> + , <&key_physical_attrs 100 100 1350 0 0 0 0> + , <&key_physical_attrs 100 100 1450 0 0 0 0> + , <&key_physical_attrs 100 100 1550 50 0 0 0> + , <&key_physical_attrs 100 100 1650 50 0 0 0> + , <&key_physical_attrs 100 100 0 150 0 0 0> + , <&key_physical_attrs 100 100 100 150 0 0 0> + , <&key_physical_attrs 100 100 200 100 0 0 0> + , <&key_physical_attrs 100 100 300 100 0 0 0> + , <&key_physical_attrs 100 100 400 100 0 0 0> + , <&key_physical_attrs 100 100 500 100 0 0 0> + , <&key_physical_attrs 100 100 1150 100 0 0 0> + , <&key_physical_attrs 100 100 1250 100 0 0 0> + , <&key_physical_attrs 100 100 1350 100 0 0 0> + , <&key_physical_attrs 100 100 1450 100 0 0 0> + , <&key_physical_attrs 100 100 1550 150 0 0 0> + , <&key_physical_attrs 100 100 1650 150 0 0 0> + , <&key_physical_attrs 100 100 0 250 0 0 0> + , <&key_physical_attrs 100 100 100 250 0 0 0> + , <&key_physical_attrs 100 100 200 200 0 0 0> + , <&key_physical_attrs 100 100 300 200 0 0 0> + , <&key_physical_attrs 100 100 400 200 0 0 0> + , <&key_physical_attrs 100 100 500 200 0 0 0> + , <&key_physical_attrs 100 100 1150 200 0 0 0> + , <&key_physical_attrs 100 100 1250 200 0 0 0> + , <&key_physical_attrs 100 100 1350 200 0 0 0> + , <&key_physical_attrs 100 100 1450 200 0 0 0> + , <&key_physical_attrs 100 100 1550 250 0 0 0> + , <&key_physical_attrs 100 100 1650 250 0 0 0> + , <&key_physical_attrs 100 100 0 350 0 0 0> + , <&key_physical_attrs 100 100 100 350 0 0 0> + , <&key_physical_attrs 100 100 200 300 0 0 0> + , <&key_physical_attrs 100 100 300 300 0 0 0> + , <&key_physical_attrs 100 100 400 300 0 0 0> + , <&key_physical_attrs 100 100 500 300 0 0 0> + , <&key_physical_attrs 100 100 1150 300 0 0 0> + , <&key_physical_attrs 100 100 1250 300 0 0 0> + , <&key_physical_attrs 100 100 1350 300 0 0 0> + , <&key_physical_attrs 100 100 1450 300 0 0 0> + , <&key_physical_attrs 100 100 1550 350 0 0 0> + , <&key_physical_attrs 100 100 1650 350 0 0 0> + , <&key_physical_attrs 100 100 200 400 0 0 0> + , <&key_physical_attrs 100 100 300 400 0 0 0> + , <&key_physical_attrs 100 100 400 400 0 0 0> + , <&key_physical_attrs 100 100 520 410 1250 520 410> + , <&key_physical_attrs 100 100 640 440 2500 640 440> + , <&key_physical_attrs 100 100 750 495 3700 750 495> + , <&key_physical_attrs 100 100 915 550 (-3700) 915 550> + , <&key_physical_attrs 100 100 1015 480 (-2500) 1015 480> + , <&key_physical_attrs 100 100 1130 430 (-1250) 1130 430> + , <&key_physical_attrs 100 100 1250 400 0 0 0> + , <&key_physical_attrs 100 100 1350 400 0 0 0> + , <&key_physical_attrs 100 100 1450 400 0 0 0> + ; + }; +}; diff --git a/app/boards/arm/go60/go60.dtsi b/app/boards/arm/go60/go60.dtsi new file mode 100644 index 00000000000..12877602edd --- /dev/null +++ b/app/boards/arm/go60/go60.dtsi @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2021 The ZMK Contributors + * SPDX-License-Identifier: MIT + */ + +/dts-v1/; +#include + +#include +#include "go60-layouts.dtsi" + +&physical_layout0 { + transform = <&matrix_transform0>; +}; + +/ { + chosen { + zmk,kscan = &kscan0; + zmk,physical-layout = &physical_layout0; + zephyr,code-partition = &code_partition; + zephyr,sram = &sram0; + zephyr,flash = &flash0; + }; + + matrix_transform0: keymap_transform_0 { + compatible = "zmk,matrix-transform"; + columns = <14>; + rows = <4>; + map = < + RC(0,0) RC(0,1) RC(0,2) RC(0,3) RC(0,4) RC(0,5) RC(0,8) RC(0,9) RC(0,10) RC(0,11) RC(0,12) RC(0,13) + RC(1,0) RC(1,1) RC(1,2) RC(1,3) RC(1,4) RC(1,5) RC(1,8) RC(1,9) RC(1,10) RC(1,11) RC(1,12) RC(1,13) + RC(2,0) RC(2,1) RC(2,2) RC(2,3) RC(2,4) RC(2,5) RC(2,8) RC(2,9) RC(2,10) RC(2,11) RC(2,12) RC(2,13) + RC(3,0) RC(3,1) RC(3,2) RC(3,3) RC(3,4) RC(3,5) RC(3,8) RC(3,9) RC(3,10) RC(3,11) RC(3,12) RC(3,13) + RC(4,2) RC(4,3) RC(4,4) RC(4,9) RC(4,10) RC(4,11) + RC(0,6) RC(1,6) RC(2,6) RC(2,7) RC(1,7) RC(0,7) + >; + }; + + kscan0: kscan { + compatible = "zmk,kscan-gpio-matrix"; + wakeup-source; + + diode-direction = "row2col"; + debounce-press-ms = <4>; + debounce-release-ms = <20>; + }; + + split_config { + compatible = "zmk,wired-split"; + device = <&uart0>; + half-duplex; + dir-gpios = <&gpio0 11 GPIO_ACTIVE_HIGH>; + detect-gpios = <&gpio1 10 GPIO_ACTIVE_HIGH>; + }; +}; + +&adc { + status = "okay"; +}; + +&gpiote { + status = "okay"; +}; + +&gpio0 { + status = "okay"; +}; + +&gpio1 { + status = "okay"; +}; + +zephyr_udc0: &usbd { + status = "okay"; +}; + +&flash0 { + /* + * For more information, see: + * http://docs.zephyrproject.org/latest/devices/dts/flash_partitions.html + */ + partitions { + compatible = "fixed-partitions"; + #address-cells = <1>; + #size-cells = <1>; + + sd_partition: partition@0 { + reg = <0x00000000 0x00026000>; + }; + code_partition: partition@26000 { + reg = <0x00026000 0x000c6000>; + }; + + /* + * The flash starting at 0x000ec000 and ending at + * 0x000f3fff is reserved for use by the application. + */ + + /* + * Storage partition will be used by FCB/LittleFS/NVS + * if enabled. + */ + storage_partition: partition@ec000 { + reg = <0x000ec000 0x00008000>; + }; + + boot_partition: partition@f4000 { + reg = <0x000f4000 0x0000c000>; + }; + }; +}; diff --git a/app/boards/arm/go60/go60.keymap b/app/boards/arm/go60/go60.keymap new file mode 100644 index 00000000000..7a921236b5d --- /dev/null +++ b/app/boards/arm/go60/go60.keymap @@ -0,0 +1,196 @@ +/* + * Copyright (c) 2020 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +// layers +#define LAYER_Base 0 +#define LAYER_Keypad 1 +#define LAYER_SymbolNav 2 +#define LAYER_Magic 3 +#define LAYER_Factory 4 + +/ { + input_processors { + zip_click_to_right_click_mapper: zip_click_to_right_click_mapper { + compatible = "zmk,input-processor-code-mapper"; + #input-processor-cells = <0>; + type = ; + map = ; + }; + }; + + behaviors { + magic: magic { + compatible = "zmk,behavior-hold-tap"; + #binding-cells = <2>; + flavor = "tap-preferred"; + tapping-term-ms = <200>; + bindings = <&mo>, <&rgb_ug_status_macro>; + }; + + keypad_td: keypad_td { + compatible = "zmk,behavior-tap-dance"; + #binding-cells = <0>; + tapping-term-ms = <200>; + bindings = <&mo LAYER_Keypad>, <&to LAYER_Keypad>; + }; + + symbol_nav_td: symbol_nav_td { + compatible = "zmk,behavior-tap-dance"; + #binding-cells = <0>; + tapping-term-ms = <200>; + bindings = <&mo LAYER_SymbolNav>, <&to LAYER_SymbolNav>; + }; + + bt_0: bt_0 { + compatible = "zmk,behavior-tap-dance"; + #binding-cells = <0>; + tapping-term-ms = <200>; + bindings = <&bt_select_0>, <&bt BT_DISC 0>; + }; + bt_1: bt_1 { + compatible = "zmk,behavior-tap-dance"; + #binding-cells = <0>; + tapping-term-ms = <200>; + bindings = <&bt_select_1>, <&bt BT_DISC 1>; + }; + bt_2: bt_2 { + compatible = "zmk,behavior-tap-dance"; + #binding-cells = <0>; + tapping-term-ms = <200>; + bindings = <&bt_select_2>, <&bt BT_DISC 2>; + }; + bt_3: bt_3 { + compatible = "zmk,behavior-tap-dance"; + #binding-cells = <0>; + tapping-term-ms = <200>; + bindings = <&bt_select_3>, <&bt BT_DISC 3>; + }; + }; + + macros { + rgb_ug_status_macro: rgb_ug_status_macro { + compatible = "zmk,behavior-macro"; + #binding-cells = <0>; + bindings = <&rgb_ug RGB_STATUS>; + }; + + bt_select_0: bt_select_0 { + compatible = "zmk,behavior-macro"; + #binding-cells = <0>; + bindings + = <&out OUT_BLE>, + <&bt BT_SEL 0>; + }; + bt_select_1: bt_select_1 { + compatible = "zmk,behavior-macro"; + #binding-cells = <0>; + bindings + = <&out OUT_BLE>, + <&bt BT_SEL 1>; + }; + bt_select_2: bt_select_2 { + compatible = "zmk,behavior-macro"; + #binding-cells = <0>; + bindings + = <&out OUT_BLE>, + <&bt BT_SEL 2>; + }; + bt_select_3: bt_select_3 { + compatible = "zmk,behavior-macro"; + #binding-cells = <0>; + bindings + = <&out OUT_BLE>, + <&bt BT_SEL 3>; + }; + }; + + keymap { + compatible = "zmk,keymap"; + layer_Base { + bindings = < + &kp EQUAL &kp N1 &kp N2 &kp N3 &kp N4 &kp N5 &kp N6 &kp N7 &kp N8 &kp N9 &kp N0 &kp MINUS + &kp TAB &kp Q &kp W &kp E &kp R &kp T &kp Y &kp U &kp I &kp O &kp P &kp BSLH + &kp ESC &kp A &kp S &kp D &kp F &kp G &kp H &kp J &kp K &kp L &kp SEMI &kp SQT + &magic LAYER_Magic 0 &kp Z &kp X &kp C &kp V &kp B &kp N &kp M &kp COMMA &kp DOT &kp FSLH &keypad_td + &kp GRAVE &kp DEL &kp BSPC &kp LGUI &kp LBKT &kp RBKT + &symbol_nav_td &kp LSHFT &kp LCTRL &kp LALT &kp SPACE &mt RALT RET + >; + }; + + layer_Keypad { + bindings = < + &none &kp F1 &kp F2 &kp F3 &kp F4 &kp F5 &kp F6 &kp F7 &kp F8 &kp F9 &kp F10 &kp F11 + &trans &none &kp HOME &kp UP &kp END &kp PG_UP &kp KP_N7 &kp KP_N8 &kp KP_N9 &kp KP_MINUS &kp KP_SLASH &kp F12 + &trans &kp LC(Y) &kp LEFT &kp DOWN &kp RIGHT &kp PG_DN &kp KP_N4 &kp KP_N5 &kp KP_N6 &kp KP_PLUS &kp KP_MULTIPLY &kp KP_NUM + &magic LAYER_Magic 0 &kp LC(Z) &kp LC(X) &kp LC(C) &kp LC(V) &kp LS(LC(V)) &kp KP_N1 &kp KP_N2 &kp KP_N3 &kp KP_ENTER &kp EQUAL &to LAYER_Base + &none &trans &trans &kp KP_N0 &kp KP_DOT &kp BSPC + &mo LAYER_SymbolNav &trans &trans &trans &trans &kp KP_N0 + >; + }; + + layer_SymbolNav { + bindings = < + &kp INS &kp F1 &kp F2 &kp F3 &kp F4 &kp F5 &kp F6 &kp F7 &kp F8 &kp F9 &kp F10 &kp F11 + &kp SLCK &kp EXCL &kp HOME &kp UP &kp END &kp PG_UP &kp LPAR &kp RPAR &kp UP &kp PRCNT &kp K_APP &kp F12 + &kp CAPS &kp AT &kp LEFT &kp DOWN &kp RIGHT &kp PG_DN &kp DLLR &kp LEFT &kp DOWN &kp RIGHT &kp PAUSE_BREAK &kp PSCRN + &magic LAYER_Magic 0 &kp HASH &kp LBKT &kp RBKT &kp LBRC &kp RBRC &kp AMPS &kp STAR &kp UNDER &kp PLUS &kp LSHFT &kp LCTRL + &kp CARET &trans &trans &trans &kp LC(LS(TAB)) &kp LC(TAB) + &to LAYER_Base &trans &trans &trans &trans &trans + >; + }; + + layer_Magic { + bindings = < + &bt BT_CLR &kp C_BRI_DN &kp C_BRI_UP &kp C_PREV &kp C_NEXT &kp C_PP &kp C_MUTE &kp C_VOL_DN &kp C_VOL_UP &none &none &bt BT_CLR_ALL + &bootloader &rgb_ug RGB_SPI &rgb_ug RGB_SAI &rgb_ug RGB_HUI &rgb_ug RGB_BRI &rgb_ug RGB_TOG &none &none &none &none &none &bootloader + &sys_reset &rgb_ug RGB_SPD &rgb_ug RGB_SAD &rgb_ug RGB_HUD &rgb_ug RGB_BRD &rgb_ug RGB_EFF &none &none &none &none &none &sys_reset + &none &none &bt_0 &bt_1 &bt_2 &bt_3 &none &none &none &none &none &to LAYER_Factory + &out OUT_USB &none &none &none &none &none + &none &none &none &none &none &none + >; + }; + + layer_Factory { + bindings = < + &kp N0 &kp N4 &kp N8 &kp N3 &kp N8 &kp N3 &kp N3 &kp N8 &kp N3 &kp N8 &kp N4 &kp N0 + &kp N1 &kp N5 &kp N9 &kp N4 &kp N9 &kp N4 &kp N4 &kp N9 &kp N4 &kp N9 &kp N5 &kp N1 + &kp N2 &kp N6 &kp N0 &kp N5 &kp N0 &kp N5 &kp N5 &kp N0 &kp N5 &kp N0 &kp N6 &kp N2 + &kp N3 &kp N7 &kp N1 &kp N6 &kp N1 &kp N6 &kp N6 &kp N1 &kp N6 &kp N1 &kp N7 &kp N3 + &kp N2 &kp N7 &kp N2 &kp N2 &kp N7 &kp N2 + &kp N7 &kp N8 &kp N9 &kp N9 &kp N8 &kp N7 + >; + }; + }; +}; + +&cirque_rh_listener { + input-processors = <&zip_xy_scaler 3 1>; + layer_2 { + layers = <2>; + input-processors = <&zip_xy_scaler 9 1>; + }; +}; + +&cirque_lh_listener { + input-processors = <&zip_xy_to_scroll_mapper>, <&zip_xy_scaler 1 8>, <&zip_click_to_right_click_mapper>; + layer_4 { + layers = <4>; + input-processors = <&zip_xy_scaler 3 1>; + }; +}; diff --git a/app/boards/arm/go60/go60.yaml b/app/boards/arm/go60/go60.yaml new file mode 100644 index 00000000000..a71c12f9168 --- /dev/null +++ b/app/boards/arm/go60/go60.yaml @@ -0,0 +1,19 @@ +identifier: go60 +name: Go60 +url: https://www.moergo.com/ +type: mcu +arch: arm +toolchain: + - zephyr + - gnuarmemb + - xtools +supported: + - adc + - usb_device + - ble + - ieee802160 + - pwm + - watchdog + - gpio + - i2c + - spi diff --git a/app/boards/arm/go60/go60.zmk.yml b/app/boards/arm/go60/go60.zmk.yml new file mode 100644 index 00000000000..66455ab637b --- /dev/null +++ b/app/boards/arm/go60/go60.zmk.yml @@ -0,0 +1,17 @@ +file_format: "1" +id: go60 +name: Go60 +type: board +arch: arm +url: https://www.moergo.com/ +features: + - keys + - underglow + - backlight + - studio +outputs: + - usb + - ble +siblings: + - go60_lh + - go60_rh diff --git a/app/boards/arm/go60/go60_lh-pinctrl.dtsi b/app/boards/arm/go60/go60_lh-pinctrl.dtsi new file mode 100644 index 00000000000..24c289cee83 --- /dev/null +++ b/app/boards/arm/go60/go60_lh-pinctrl.dtsi @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2021 The ZMK Contributors + * SPDX-License-Identifier: MIT + */ + +&pinctrl { + // SPI1 for Cirque module + spi1_default: spi1_default { + group1 { + psels = , // EXT1 + , // EXT3 + ; // EXT2 + }; + }; + + spi1_sleep: spi1_sleep { + group1 { + psels = , + , + ; + low-power-enable; + }; + }; + + // SPI3 for WS2812 backlight + spi3_default: spi3_default { + group1 { + psels = ; // WS2812_VEXT_DATA + }; + }; + + spi3_sleep: spi3_sleep { + group1 { + psels = ; + low-power-enable; + }; + }; + + pwm0_default: pwm0_default { + group1 { + psels = ; // rear LED + }; + }; + + pwm0_sleep: pwm0_sleep { + group1 { + psels = ; + bias-pull-down; + }; + }; + + uart0_default: uart0_default { + group1 { + psels = , // RS422_TXD + ; // RS422_RXD + }; + }; + + uart0_sleep: uart0_sleep { + group1 { + psels = , + ; + low-power-enable; + }; + }; +}; diff --git a/app/boards/arm/go60/go60_lh.dts b/app/boards/arm/go60/go60_lh.dts new file mode 100644 index 00000000000..e2a1ea1721d --- /dev/null +++ b/app/boards/arm/go60/go60_lh.dts @@ -0,0 +1,190 @@ +/* + * Copyright (c) 2021 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include "go60.dtsi" +#include "go60_lh-pinctrl.dtsi" + +#include + +/ { + model = "go60_lh"; + compatible = "go60_lh"; + + chosen { + zmk,underglow = &led_strip; + zmk,backlight = &back_led_backlight; + zmk,battery = &vbatt; + zmk,underglow-indicators = &underglow_indicators; + }; + + underglow-layer { + compatible = "zmk,underglow-layer"; + pixel-lookup = + <54>, <55>, <56>, <5>, <17>, <29>, <41>, <4>, <16>, <28>, + <40>, <50>, <3>, <15>, <27>, <39>, <49>, <2>, <14>, <26>, + <38>, <48>, <1>, <13>, <25>, <37>, <0>, <12>, <24>, <36>; + }; + + back_led_backlight: pwmleds { + compatible = "pwm-leds"; + pwm_led_0 { + pwms = <&pwm0 0 PWM_USEC(20) PWM_POLARITY_NORMAL>; + }; + }; + + // Node name must match original "EXT_POWER" label to preserve user settings. + EXT_POWER { + compatible = "zmk,ext-power-generic"; + control-gpios = <&gpio1 11 GPIO_ACTIVE_HIGH>; /* WS2812_CE */ + init-delay-ms = <100>; + }; + + vbatt: vbatt { + compatible = "zmk,battery-nrf-vddh"; + }; + + /* + Go60 LEDs + 26 22 17 12 7 3 + 27 23 18 13 8 4 + 28 24 19 14 9 5 + 29 25 20 15 10 6 + 21 16 11 0 1 2 + */ + underglow_indicators: underglow-indicators { + compatible = "zmk,underglow-indicators"; + layer-state = <26 22 17 12 7 3>; + bat-lhs = <27 23 18 13 8 4>; + bat-rhs = <28 24 19 14 9 5>; + capslock = <0>; + numlock = <1>; + scrolllock = <2>; + output-fallback = <11>; + ble-state = <20 15 10 6>; + usb-state = <21>; + }; + + go60_ext: connector { + compatible = "moergo,go60-ext"; + #gpio-cells = <2>; + gpio-map-mask = <0xffffffff 0xffffffc0>; + gpio-map-pass-thru = <0 0x3f>; + gpio-map + = <1 0 &gpio0 19 0> /* EXT1 */ + , <2 0 &gpio0 21 0> /* EXT2 */ + , <3 0 &gpio0 22 0> /* EXT3 */ + , <4 0 &gpio0 23 0> /* EXT4 */ + , <5 0 &gpio0 25 0> /* EXT5 */ + ; + }; + + // Left hand Cirque + cirque_lh_listener: cirque_lh_listener { + compatible = "zmk,input-listener"; + device = <&glidepoint>; + }; + + // Right hand Cirque + inputs { + #address-cells = <1>; + #size-cells = <0>; + cirque_split: cirque_split@0 { + compatible = "zmk,input-split"; + reg = <0>; + }; + }; + + cirque_rh_listener: cirque_rh_listener { + compatible = "zmk,input-listener"; + device = <&cirque_split>; + }; +}; + +&spi1 { + status = "okay"; + + compatible = "nordic,nrf-spim"; + pinctrl-0 = <&spi1_default>; + pinctrl-1 = <&spi1_sleep>; + pinctrl-names = "default", "sleep"; + + cs-gpios = <&go60_ext 5 GPIO_ACTIVE_LOW>; + + glidepoint: glidepoint@0 { + compatible = "cirque,pinnacle"; + reg = <0>; + spi-max-frequency = <1000000>; + status = "okay"; + dr-gpios = <&go60_ext 4 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)>; + + sensitivity = "1x"; + rotate-90; + y-invert; + no-secondary-tap; + }; +}; + +&spi3 { + compatible = "nordic,nrf-spim"; + status = "okay"; + + pinctrl-0 = <&spi3_default>; + pinctrl-1 = <&spi3_sleep>; + pinctrl-names = "default", "sleep"; + + led_strip: ws2812@0 { + compatible = "worldsemi,ws2812-spi"; + + /* SPI */ + reg = <0>; /* ignored, but necessary for SPI bindings */ + spi-max-frequency = <4000000>; + + /* WS2812 */ + chain-length = <30>; + spi-one-frame = <0x70>; + spi-zero-frame = <0x40>; + + color-mapping = ; + }; +}; + +&pwm0 { + status = "okay"; + pinctrl-0 = <&pwm0_default>; + pinctrl-1 = <&pwm0_sleep>; + pinctrl-names = "default", "sleep"; +}; + +&uart0 { + status = "okay"; + compatible = "nordic,nrf-uarte"; + // Max stable speed with half duplex on nRF52 + current-speed = <921600>; + /* current-speed = <460800>; */ + /* current-speed = <230400>; */ + pinctrl-0 = <&uart0_default>; + pinctrl-1 = <&uart0_sleep>; + pinctrl-names = "default", "sleep"; +}; + +&kscan0 { + row-gpios + = <&gpio1 1 GPIO_ACTIVE_HIGH> // LH ROW1 (KEYSCAN_OUT1) + , <&gpio1 3 GPIO_ACTIVE_HIGH> // LH ROW2 (KEYSCAN_OUT2) + , <&gpio1 5 GPIO_ACTIVE_HIGH> // LH ROW3 (KEYSCAN_OUT3) + , <&gpio1 7 GPIO_ACTIVE_HIGH> // LH ROW4 (KEYSCAN_OUT4) + , <&gpio1 6 GPIO_ACTIVE_HIGH> // LH ROW5 (KEYSCAN_OUT5) + ; + col-gpios + = <&gpio0 24 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)> // LH COL6 (KEYSCAN_IN7) + , <&gpio0 20 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)> // LH COL5 (KEYSCAN_IN6) + , <&gpio0 17 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)> // LH COL4 (KEYSCAN_IN5) + , <&gpio0 15 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)> // LH COL3 (KEYSCAN_IN4) + , <&gpio0 16 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)> // LH COL2 (KEYSCAN_IN3) + , <&gpio0 13 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)> // LH COL1 (KEYSCAN_IN2) + , <&gpio0 14 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)> // LH Thumb (KEYSCAN_IN1) + ; +}; diff --git a/app/boards/arm/go60/go60_lh.keymap b/app/boards/arm/go60/go60_lh.keymap new file mode 100644 index 00000000000..177b240aab9 --- /dev/null +++ b/app/boards/arm/go60/go60_lh.keymap @@ -0,0 +1,7 @@ +/* + * Copyright (c) 2020 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include "go60.keymap" diff --git a/app/boards/arm/go60/go60_lh_defconfig b/app/boards/arm/go60/go60_lh_defconfig new file mode 100644 index 00000000000..d6f3ebcc282 --- /dev/null +++ b/app/boards/arm/go60/go60_lh_defconfig @@ -0,0 +1,115 @@ +# Copyright (c) 2021 The ZMK Contributors +# SPDX-License-Identifier: MIT + +CONFIG_SOC_SERIES_NRF52X=y +CONFIG_SOC_NRF52840_QIAA=y +CONFIG_BOARD_GO60_LH=y + +# Enable both USB and BLE +CONFIG_ZMK_USB=y +CONFIG_ZMK_BLE=y + +# Keyboard IDs +CONFIG_ZMK_KEYBOARD_NAME="Go60 Left" +CONFIG_USB_DEVICE_PID=0x27db +CONFIG_USB_DEVICE_VID=0x16c0 +CONFIG_USB_DEVICE_MANUFACTURER="MoErgo" +CONFIG_USB_DEVICE_SN="moergo.com:GO60-0123456789ABCDEF" + +CONFIG_BT_DEVICE_NAME="Go60" + +CONFIG_BT_DIS_PNP_PID=0x27db +CONFIG_BT_DIS_PNP_VID=0x16c0 +CONFIG_BT_DIS_MANUF="MoErgo" +CONFIG_BT_DIS_MODEL="Go60" + +CONFIG_BT_CTLR_TX_PWR_PLUS_8=y + +# Work-around for Windows bug with battery notifications +CONFIG_BT_GATT_ENFORCE_SUBSCRIPTION=n + +# Allow unauthenticated re-pairing for already paired hosts. This would permit +# an attacker that can spoof the host's peer address to "steal" the keyboard +# pairing by overwriting it, but without access to the previous keys it can't +# establish a MITM, and the sudden loss of the keyboard would be very obvious to +# the previously-connected host. +CONFIG_BT_SMP_ALLOW_UNAUTH_OVERWRITE=y +CONFIG_ZMK_BLE_PASSKEY_ENTRY=n + +# Fetch peripheral battery level for status display reporting +CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING=y + +# Enable MPU +CONFIG_ARM_MPU=y + +# Enable GPIO +CONFIG_GPIO=y + +# Build configurations +CONFIG_BUILD_OUTPUT_UF2=y +CONFIG_BUILD_OUTPUT_UF2_FAMILY_ID="0x9809B007" +CONFIG_USE_DT_CODE_PARTITION=y + +# Flash configuration +CONFIG_MPU_ALLOW_FLASH_WRITE=y +CONFIG_NVS=y +CONFIG_SETTINGS_NVS=y +CONFIG_FLASH=y +CONFIG_FLASH_PAGE_LAYOUT=y +CONFIG_FLASH_MAP=y + +# Enable 32kHz crystal +CONFIG_CLOCK_CONTROL_NRF_K32SRC_XTAL=y + +# Enable RGB underglow +CONFIG_ZMK_RGB_UNDERGLOW=y + +# disable EXT_POWER until underglow gets turned on +CONFIG_ZMK_EXT_POWER_START=n + +CONFIG_ZMK_RGB_UNDERGLOW_EXT_POWER=y +CONFIG_ZMK_RGB_UNDERGLOW_ON_START=n +CONFIG_ZMK_RGB_UNDERGLOW_BRT_STEP=2 +CONFIG_ZMK_RGB_UNDERGLOW_BRT_MIN=2 + +# DO NOT CHANGE CONFIG_ZMK_RGB_UNDERGLOW_BRT_MAX TO ABOVE 40. Configuring +# BRT_MAX above 40% can draw more than 500mA current, which can potentially +# damage some computers. WARRANTY IS VOID IF BRT_MAX SET ABOVE 40. +CONFIG_ZMK_RGB_UNDERGLOW_BRT_MAX=40 + +CONFIG_ZMK_RGB_UNDERGLOW_EFF_START=3 +CONFIG_ZMK_RGB_UNDERGLOW_HUE_START=285 +CONFIG_ZMK_RGB_UNDERGLOW_SAT_START=75 +CONFIG_ZMK_RGB_UNDERGLOW_BRT_START=16 + +# The power LED is implemented as a backlight +# For now, the power LED is acting as a "USB connected" indicator +CONFIG_ZMK_BACKLIGHT=y +CONFIG_ZMK_BACKLIGHT_ON_START=y +CONFIG_ZMK_BACKLIGHT_BRT_START=5 +CONFIG_ZMK_BACKLIGHT_AUTO_OFF_IDLE=y +CONFIG_ZMK_BACKLIGHT_AUTO_OFF_USB=y + +# The full two-byte consumer report space has compatibility issues with some +# operating systems, most notably macOS. Use the more basic single-byte usage +# space. +CONFIG_ZMK_HID_CONSUMER_REPORT_USAGES_BASIC=y + +# Enable USB boot protocol support +CONFIG_ZMK_USB_BOOT=y +CONFIG_ZMK_HID_INDICATORS=y + +# Send HID indicator to peripherals +CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS=y + +# Turn on debugging to disable optimization. Debug messages can result in larger +# stacks, so enable stack protection and particularly a larger BLE peripheral stack. +# CONFIG_DEBUG=y +# CONFIG_DEBUG_THREAD_INFO=y +# CONFIG_EXCEPTION_STACK_TRACE=y +# CONFIG_HW_STACK_PROTECTION=y +# CONFIG_ZMK_SPLIT_BLE_PERIPHERAL_STACK_SIZE=1300 + +# Log via USB or Segger RTT +CONFIG_ZMK_USB_LOGGING=n +CONFIG_ZMK_RTT_LOGGING=n diff --git a/app/boards/arm/go60/go60_rh-pinctrl.dtsi b/app/boards/arm/go60/go60_rh-pinctrl.dtsi new file mode 100644 index 00000000000..cb7d22b6c27 --- /dev/null +++ b/app/boards/arm/go60/go60_rh-pinctrl.dtsi @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2021 The ZMK Contributors + * SPDX-License-Identifier: MIT + */ + +&pinctrl { + // SPI1 for Cirque module + spi1_default: spi1_default { + group1 { + psels = , // EXT1 + , // EXT3 + ; // EXT2 + }; + }; + + spi1_sleep: spi1_sleep { + group1 { + psels = , + , + ; + low-power-enable; + }; + }; + + // SPI3 for WS2812 backlight + spi3_default: spi3_default { + group1 { + psels = ; // WS2812_VEXT_DATA + }; + }; + + spi3_sleep: spi3_sleep { + group1 { + psels = ; + low-power-enable; + }; + }; + + pwm0_default: pwm0_default { + group1 { + psels = ; // Rear LED + }; + }; + + pwm0_sleep: pwm0_sleep { + group1 { + psels = ; + bias-pull-down; + }; + }; + + uart0_default: uart0_default { + group1 { + psels = , // RS422_TXD + ; // RS422_RXD + }; + }; + + uart0_sleep: uart0_sleep { + group1 { + psels = , + ; + low-power-enable; + }; + }; +}; diff --git a/app/boards/arm/go60/go60_rh.dts b/app/boards/arm/go60/go60_rh.dts new file mode 100644 index 00000000000..3fee2ac88dc --- /dev/null +++ b/app/boards/arm/go60/go60_rh.dts @@ -0,0 +1,174 @@ +/* + * Copyright (c) 2021 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + + +#include "go60.dtsi" +#include "go60_rh-pinctrl.dtsi" + +#include + +/ { + model = "go60_rh"; + compatible = "go60_rh"; + + chosen { + zmk,underglow = &led_strip; + zmk,backlight = &back_led_backlight; + zmk,battery = &vbatt; + }; + + underglow-layer { + compatible = "zmk,underglow-layer"; + pixel-lookup = + <59>, <58>, <57>, <6>, <18>, <30>, <42>, <7>, <19>, <31>, + <43>, <51>, <8>, <20>, <32>, <44>, <52>, <9>, <21>, <33>, + <45>, <53>, <10>, <22>, <34>, <46>, <11>, <23>, <35>, <47>; + }; + + back_led_backlight: pwmleds { + compatible = "pwm-leds"; + pwm_led_0 { + pwms = <&pwm0 0 PWM_USEC(20) PWM_POLARITY_NORMAL>; + }; + }; + + // Node name must match original "EXT_POWER" label to preserve user settings. + EXT_POWER { + compatible = "zmk,ext-power-generic"; + control-gpios = <&gpio1 11 GPIO_ACTIVE_HIGH>; /* WS2812_CE */ + init-delay-ms = <100>; + }; + + vbatt: vbatt { + compatible = "zmk,battery-nrf-vddh"; + }; + + go60_ext: connector { + compatible = "moergo,go60-ext"; + #gpio-cells = <2>; + gpio-map-mask = <0xffffffff 0xffffffc0>; + gpio-map-pass-thru = <0 0x3f>; + gpio-map + = <1 0 &gpio0 19 0> /* EXT1 */ + , <2 0 &gpio0 21 0> /* EXT2 */ + , <3 0 &gpio0 22 0> /* EXT3 */ + , <4 0 &gpio0 23 0> /* EXT4 */ + , <5 0 &gpio0 25 0> /* EXT5 */ + ; + }; + inputs { + #address-cells = <1>; + #size-cells = <0>; + cirque_split: cirque_split@0 { + compatible = "zmk,input-split"; + reg = <0>; + device = <&glidepoint>; + }; + }; + + // Disabled input listeners for the Cirques, to permit references in the keymap + cirque_lh_listener: cirque_lh_listener { + compatible = "zmk,input-listener"; + status = "disabled"; + }; + + cirque_rh_listener: cirque_rh_listener { + compatible = "zmk,input-listener"; + status = "disabled"; + }; +}; + +&spi1 { + status = "okay"; + + compatible = "nordic,nrf-spim"; + pinctrl-0 = <&spi1_default>; + pinctrl-1 = <&spi1_sleep>; + pinctrl-names = "default", "sleep"; + + cs-gpios = <&go60_ext 5 GPIO_ACTIVE_LOW>; + + glidepoint: glidepoint@0 { + compatible = "cirque,pinnacle"; + reg = <0>; + spi-max-frequency = <1000000>; + status = "okay"; + dr-gpios = <&go60_ext 4 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)>; + + sensitivity = "1x"; + rotate-90; + y-invert; + no-secondary-tap; + }; +}; + +&spi3 { + compatible = "nordic,nrf-spim"; + status = "okay"; + + pinctrl-0 = <&spi3_default>; + pinctrl-1 = <&spi3_sleep>; + pinctrl-names = "default", "sleep"; + + led_strip: ws2812@0 { + compatible = "worldsemi,ws2812-spi"; + + /* SPI */ + reg = <0>; /* ignored, but necessary for SPI bindings */ + spi-max-frequency = <4000000>; + + /* WS2812 */ + chain-length = <30>; + spi-one-frame = <0x70>; + spi-zero-frame = <0x40>; + + color-mapping = ; + }; +}; + +&pwm0 { + status = "okay"; + pinctrl-0 = <&pwm0_default>; + pinctrl-1 = <&pwm0_sleep>; + pinctrl-names = "default", "sleep"; +}; + + +&uart0 { + status = "okay"; + compatible = "nordic,nrf-uarte"; + // Max stable speed with half duplex on nRF52 + current-speed = <921600>; + /* current-speed = <460800>; */ + /* current-speed = <230400>; */ + pinctrl-0 = <&uart0_default>; + pinctrl-1 = <&uart0_sleep>; + pinctrl-names = "default", "sleep"; +}; + +/* For right hand, the columns are offset by 7 */ +&matrix_transform0 { + col-offset = <7>; +}; + +&kscan0 { + row-gpios + = <&gpio1 1 GPIO_ACTIVE_HIGH> // LH ROW1 (KEYSCAN_OUT1) + , <&gpio1 3 GPIO_ACTIVE_HIGH> // LH ROW2 (KEYSCAN_OUT2) + , <&gpio1 5 GPIO_ACTIVE_HIGH> // LH ROW3 (KEYSCAN_OUT3) + , <&gpio1 7 GPIO_ACTIVE_HIGH> // LH ROW4 (KEYSCAN_OUT4) + , <&gpio1 6 GPIO_ACTIVE_HIGH> // LH ROW5 (KEYSCAN_OUT5) + ; + col-gpios + = <&gpio0 14 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)> // RH Thumb (KEYSCAN_IN1) + , <&gpio0 13 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)> // RH COL1 (KEYSCAN_IN2) + , <&gpio0 16 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)> // RH COL2 (KEYSCAN_IN3) + , <&gpio0 15 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)> // RH COL3 (KEYSCAN_IN4) + , <&gpio0 17 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)> // RH COL4 (KEYSCAN_IN5) + , <&gpio0 20 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)> // RH COL5 (KEYSCAN_IN6) + , <&gpio0 24 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)> // RH COL6 (KEYSCAN_IN7) + ; +}; diff --git a/app/boards/arm/go60/go60_rh.keymap b/app/boards/arm/go60/go60_rh.keymap new file mode 100644 index 00000000000..177b240aab9 --- /dev/null +++ b/app/boards/arm/go60/go60_rh.keymap @@ -0,0 +1,7 @@ +/* + * Copyright (c) 2020 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include "go60.keymap" diff --git a/app/boards/arm/go60/go60_rh_defconfig b/app/boards/arm/go60/go60_rh_defconfig new file mode 100644 index 00000000000..a6555841b38 --- /dev/null +++ b/app/boards/arm/go60/go60_rh_defconfig @@ -0,0 +1,96 @@ +# Copyright (c) 2021 The ZMK Contributors +# SPDX-License-Identifier: MIT + +CONFIG_SOC_SERIES_NRF52X=y +CONFIG_SOC_NRF52840_QIAA=y +CONFIG_BOARD_GO60_RH=y + +# Enable BLE for split peripheral +CONFIG_ZMK_USB=n +CONFIG_ZMK_BLE=y + +# Keyboard IDs +CONFIG_ZMK_KEYBOARD_NAME="Go60 Right" +CONFIG_USB_DEVICE_PID=0x27d9 +CONFIG_USB_DEVICE_VID=0x16c0 +CONFIG_USB_DEVICE_MANUFACTURER="MoErgo" +CONFIG_USB_DEVICE_SN="moergo.com:GO60-0123456789ABCDEF" + +CONFIG_BT_DIS_PNP_PID=0x27d9 +CONFIG_BT_DIS_PNP_VID=0x16c0 +CONFIG_BT_DIS_MANUF="MoErgo" +CONFIG_BT_DIS_MODEL="Go60 Right" + +CONFIG_BT_CTLR_TX_PWR_PLUS_8=y + +# Enable MPU +CONFIG_ARM_MPU=y + +# Enable GPIO +CONFIG_GPIO=y + +# Build configurations +CONFIG_BUILD_OUTPUT_UF2=y +CONFIG_BUILD_OUTPUT_UF2_FAMILY_ID="0x980AB007" +CONFIG_USE_DT_CODE_PARTITION=y + +# Flash configuration +CONFIG_MPU_ALLOW_FLASH_WRITE=y +CONFIG_NVS=y +CONFIG_SETTINGS_NVS=y +CONFIG_FLASH=y +CONFIG_FLASH_PAGE_LAYOUT=y +CONFIG_FLASH_MAP=y + +# Enable 32kHz crystal +CONFIG_CLOCK_CONTROL_NRF_K32SRC_XTAL=y + +# Enable RGB underglow +CONFIG_ZMK_RGB_UNDERGLOW=y + +# disable EXT_POWER until underglow gets turned on +CONFIG_ZMK_EXT_POWER_START=n + +CONFIG_ZMK_RGB_UNDERGLOW_EXT_POWER=y +CONFIG_ZMK_RGB_UNDERGLOW_ON_START=n +CONFIG_ZMK_RGB_UNDERGLOW_BRT_STEP=2 +CONFIG_ZMK_RGB_UNDERGLOW_BRT_MIN=2 + +# DO NOT CHANGE CONFIG_ZMK_RGB_UNDERGLOW_BRT_MAX TO ABOVE 40. Configuring +# BRT_MAX above 40% can draw more than 500mA current, which can potentially +# damage some computers. WARRANTY IS VOID IF BRT_MAX SET ABOVE 40. +CONFIG_ZMK_RGB_UNDERGLOW_BRT_MAX=40 + +CONFIG_ZMK_RGB_UNDERGLOW_EFF_START=3 +CONFIG_ZMK_RGB_UNDERGLOW_HUE_START=285 +CONFIG_ZMK_RGB_UNDERGLOW_SAT_START=75 +CONFIG_ZMK_RGB_UNDERGLOW_BRT_START=16 + +# Enable HID indicators on peripheral +CONFIG_ZMK_HID_INDICATORS=y +CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS=y + +# The power LED is implemented as a backlight +# For now, the power LED is acting as a "USB connected" indicator +CONFIG_ZMK_BACKLIGHT=y +CONFIG_ZMK_BACKLIGHT_ON_START=y +CONFIG_ZMK_BACKLIGHT_BRT_START=5 +CONFIG_ZMK_BACKLIGHT_AUTO_OFF_IDLE=y +CONFIG_ZMK_BACKLIGHT_AUTO_OFF_USB=y + +# The full two-byte consumer report space has compatibility issues with some +# operating systems, most notably macOS. Use the more basic single-byte usage +# space. +CONFIG_ZMK_HID_CONSUMER_REPORT_USAGES_BASIC=y + +# Turn on debugging to disable optimization. Debug messages can result in larger +# stacks, so enable stack protection and particularly a larger BLE peripheral stack. +# CONFIG_DEBUG=y +# CONFIG_DEBUG_THREAD_INFO=y +# CONFIG_EXCEPTION_STACK_TRACE=y +# CONFIG_HW_STACK_PROTECTION=y +# CONFIG_ZMK_SPLIT_BLE_PERIPHERAL_STACK_SIZE=1300 + +# Log via USB or Segger RTT +CONFIG_ZMK_USB_LOGGING=n +CONFIG_ZMK_RTT_LOGGING=n diff --git a/app/boards/arm/go60/pre_dt_board.cmake b/app/boards/arm/go60/pre_dt_board.cmake new file mode 100644 index 00000000000..05b0efe5f04 --- /dev/null +++ b/app/boards/arm/go60/pre_dt_board.cmake @@ -0,0 +1,9 @@ +# +# Copyright (c) 2024 The ZMK Contributors +# SPDX-License-Identifier: MIT +# + +# Suppresses duplicate unit-address warning at build time for power, clock, acl and flash-controller +# https://docs.zephyrproject.org/latest/build/dts/intro-input-output.html + +list(APPEND EXTRA_DTC_FLAGS "-Wno-unique_unit_address_if_enabled") \ No newline at end of file diff --git a/app/boards/arm/go60/readme.md b/app/boards/arm/go60/readme.md new file mode 100644 index 00000000000..8345306b97f --- /dev/null +++ b/app/boards/arm/go60/readme.md @@ -0,0 +1,13 @@ +## MoErgo Go60 + +This board definition provides ZMK support for the [MoErgo Go60](https://www.moergo.com) +keyboard. + +MoErgo additionally offers a customized version of ZMK which adds additional functionality such as +RGB status indicators, available on GitHub at [moergo-sc/zmk](https://github.com/moergo-sc/zmk). The +MoErgo customized ZMK fork is regularly updated to include the latest changes from mainline ZMK, but +will not always be completely up-to-date. MoErgo also offers an online layout configurator and +firmware builder application using the customized fork at [my.go60.com](https://my.go60.com). + +While mainline ZMK is expected to work well with Go60, MoErgo only provides support for use of +their customized fork. Likewise, the ZMK community cannot directly provide support for MoErgo's fork. diff --git a/app/boards/arm/go60/usb_serial_number.c b/app/boards/arm/go60/usb_serial_number.c new file mode 100644 index 00000000000..92a5ba7cabe --- /dev/null +++ b/app/boards/arm/go60/usb_serial_number.c @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2020 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include +#include "usb_descriptor.h" + +#define LOG_LEVEL CONFIG_USB_DEVICE_LOG_LEVEL +#include +LOG_MODULE_DECLARE(usb_descriptor); + +int base16_encode(const uint8_t *data, int length, uint8_t *result, int bufSize); + +uint8_t *usb_update_sn_string_descriptor(void) { + /* + * nrf52840 hwinfo returns a 64-bit hardware id. Go60 uses this as a + * serial number, encoded as base16 into the last 16 characters of the + * CONFIG_USB_DEVICE_SN template. If insufficient template space is + * available, instead return the static serial number string. + */ + const uint8_t template_len = sizeof(CONFIG_USB_DEVICE_SN); + const uint8_t sn_len = 16; + + if (template_len < sn_len + 1) { + LOG_DBG("Serial number template too short"); + return CONFIG_USB_DEVICE_SN; + } + + static uint8_t serial[sizeof(CONFIG_USB_DEVICE_SN)]; + strncpy(serial, CONFIG_USB_DEVICE_SN, template_len); + + uint8_t hwid[8]; + memset(hwid, 0, sizeof(hwid)); + uint8_t hwlen = hwinfo_get_device_id(hwid, sizeof(hwid)); + + if (hwlen > 0) { + const uint8_t offset = template_len - sn_len - 1; + LOG_HEXDUMP_DBG(&hwid, sn_len, "Serial Number"); + base16_encode(hwid, hwlen, serial + offset, sn_len + 1); + } + + return serial; +} + +int base16_encode(const uint8_t *data, int length, uint8_t *result, int bufSize) { + const char hex[] = "0123456789ABCDEF"; + + int i = 0; + while (i < bufSize && i < length * 2) { + uint8_t nibble; + if (i % 2 == 0) { + nibble = data[i / 2] >> 4; + } else { + nibble = data[i / 2] & 0xF; + } + result[i] = hex[nibble]; + ++i; + } + if (i < bufSize) { + result[i] = '\0'; + } + return i; +} diff --git a/app/dts/behaviors.dtsi b/app/dts/behaviors.dtsi index 653b085d5c5..1f87ef1a7fe 100644 --- a/app/dts/behaviors.dtsi +++ b/app/dts/behaviors.dtsi @@ -28,3 +28,6 @@ #include #include #include +#include +#include +#include diff --git a/app/dts/behaviors/ug_battery.dtsi b/app/dts/behaviors/ug_battery.dtsi new file mode 100644 index 00000000000..3e5f75cc65e --- /dev/null +++ b/app/dts/behaviors/ug_battery.dtsi @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2024 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +/ { + behaviors { + ug_b2: ugbat20 { + compatible = "zmk,behavior-underglow-battery"; + threshold = <20>; + #binding-cells = <2>; + display-name = "Underglow Battery level 20%"; + }; + ug_b4: ugbat40 { + compatible = "zmk,behavior-underglow-battery"; + threshold = <40>; + #binding-cells = <2>; + display-name = "Underglow Battery level 40%"; + }; + ug_b6: ugbat60 { + compatible = "zmk,behavior-underglow-battery"; + threshold = <60>; + #binding-cells = <2>; + display-name = "Underglow Battery level 60%"; + }; + ug_b8: ugbat80 { + compatible = "zmk,behavior-underglow-battery"; + threshold = <80>; + #binding-cells = <2>; + display-name = "Underglow Battery level 80%"; + }; + }; +}; diff --git a/app/dts/behaviors/ug_color.dtsi b/app/dts/behaviors/ug_color.dtsi new file mode 100644 index 00000000000..15d6c8630f2 --- /dev/null +++ b/app/dts/behaviors/ug_color.dtsi @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2024 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +/ { + behaviors { + ug: ugcolor { + compatible = "zmk,behavior-underglow-color"; + #binding-cells = <1>; + display-name = "Underglow Color"; + }; + }; +}; diff --git a/app/dts/behaviors/ug_indicators.dtsi b/app/dts/behaviors/ug_indicators.dtsi new file mode 100644 index 00000000000..b62c79b7795 --- /dev/null +++ b/app/dts/behaviors/ug_indicators.dtsi @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2024 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include + +/ { + behaviors { + ug_nl: ugnumlk { + compatible = "zmk,behavior-underglow-indicators"; + indicator = ; + #binding-cells = <2>; + display-name = "Underglow NumLock indicator"; + }; + + ug_cl: ugcapslk { + compatible = "zmk,behavior-underglow-indicators"; + indicator = ; + #binding-cells = <2>; + display-name = "Underglow CapsLock indicator"; + }; + + ug_sl: ugscrllk { + compatible = "zmk,behavior-underglow-indicators"; + indicator = ; + #binding-cells = <2>; + display-name = "Underglow ScrollLock indicator"; + }; + + }; +}; diff --git a/app/dts/bindings/behaviors/zmk,behavior-underglow-battery.yaml b/app/dts/bindings/behaviors/zmk,behavior-underglow-battery.yaml new file mode 100644 index 00000000000..4c535ea5505 --- /dev/null +++ b/app/dts/bindings/behaviors/zmk,behavior-underglow-battery.yaml @@ -0,0 +1,12 @@ +# Copyright (c) 2024, The ZMK Contributors +# SPDX-License-Identifier: MIT + +description: Set underglow color based on battery level + +compatible: "zmk,behavior-underglow-battery" + +include: two_param.yaml + +properties: + threshold: + type: int diff --git a/app/dts/bindings/behaviors/zmk,behavior-underglow-color.yaml b/app/dts/bindings/behaviors/zmk,behavior-underglow-color.yaml new file mode 100644 index 00000000000..b3e902787cc --- /dev/null +++ b/app/dts/bindings/behaviors/zmk,behavior-underglow-color.yaml @@ -0,0 +1,8 @@ +# Copyright (c) 2024, The ZMK Contributors +# SPDX-License-Identifier: MIT + +description: Set underglow to specified color + +compatible: "zmk,behavior-underglow-color" + +include: one_param.yaml diff --git a/app/dts/bindings/behaviors/zmk,behavior-underglow-indicators.yaml b/app/dts/bindings/behaviors/zmk,behavior-underglow-indicators.yaml new file mode 100644 index 00000000000..3bd19081fb8 --- /dev/null +++ b/app/dts/bindings/behaviors/zmk,behavior-underglow-indicators.yaml @@ -0,0 +1,13 @@ +# Copyright (c) 2024, The ZMK Contributors +# SPDX-License-Identifier: MIT + +description: Set underglow for HID indicators + +compatible: "zmk,behavior-underglow-indicators" + +include: two_param.yaml + +properties: + indicator: + type: int + default: 0 diff --git a/app/dts/bindings/zmk,underglow-layer.yaml b/app/dts/bindings/zmk,underglow-layer.yaml new file mode 100644 index 00000000000..28221039267 --- /dev/null +++ b/app/dts/bindings/zmk,underglow-layer.yaml @@ -0,0 +1,24 @@ +description: | + Allows defining a rgbmap composed of multiple layers + +compatible: "zmk,underglow-layer" + +properties: + pixel-lookup: + type: array + required: true + +child-binding: + description: "A layer to be used in a rgbmap" + + properties: + bindings: + type: phandle-array + required: true + layer-id: + type: int + required: true + fade-delay: + type: int + required: false + default: -1 diff --git a/app/include/dt-bindings/zmk/hid_indicators.h b/app/include/dt-bindings/zmk/hid_indicators.h new file mode 100644 index 00000000000..860c81dbdcd --- /dev/null +++ b/app/include/dt-bindings/zmk/hid_indicators.h @@ -0,0 +1,9 @@ +/* + * Copyright (c) 2020 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#define NUM_LOCK 0 +#define CAPS_LOCK 1 +#define SCROLL_LOCK 2 diff --git a/app/include/dt-bindings/zmk/rgb_colors.h b/app/include/dt-bindings/zmk/rgb_colors.h new file mode 100644 index 00000000000..edaa976685f --- /dev/null +++ b/app/include/dt-bindings/zmk/rgb_colors.h @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2021 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#define GREEN 0x00ff00 +#define RED 0xff0000 +#define BLUE 0x0000ff +#define TEAL 0x008080 +#define ORANGE 0xffa500 +#define YELLOW 0xffff00 +#define GOLD 0xffd700 +#define PURPLE 0x800080 +#define PINK 0xffc0cb +#define WHITE 0xffffff +#define BLACK 0x000000 diff --git a/app/include/zmk/events/split_peripheral_layer_changed.h b/app/include/zmk/events/split_peripheral_layer_changed.h new file mode 100644 index 00000000000..2445c164f5c --- /dev/null +++ b/app/include/zmk/events/split_peripheral_layer_changed.h @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2022 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#pragma once + +#include +#include + +struct zmk_split_peripheral_layer_changed { + uint32_t layers; +}; + +ZMK_EVENT_DECLARE(zmk_split_peripheral_layer_changed); diff --git a/app/include/zmk/events/underglow_color_changed.h b/app/include/zmk/events/underglow_color_changed.h new file mode 100644 index 00000000000..0d0ddf37dc1 --- /dev/null +++ b/app/include/zmk/events/underglow_color_changed.h @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2024 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#pragma once + +#include + +struct zmk_underglow_color_changed { + uint32_t layers; + bool wakeup; +}; + +ZMK_EVENT_DECLARE(zmk_underglow_color_changed); diff --git a/app/include/zmk/rgb_underglow.h b/app/include/zmk/rgb_underglow.h index 0c45e1c68f7..f00dcd5e257 100644 --- a/app/include/zmk/rgb_underglow.h +++ b/app/include/zmk/rgb_underglow.h @@ -16,6 +16,8 @@ int zmk_rgb_underglow_toggle(void); int zmk_rgb_underglow_get_state(bool *state); int zmk_rgb_underglow_on(void); int zmk_rgb_underglow_off(void); +int zmk_rgb_underglow_transient_on(void); +int zmk_rgb_underglow_transient_off(void); int zmk_rgb_underglow_cycle_effect(int direction); int zmk_rgb_underglow_calc_effect(int direction); int zmk_rgb_underglow_select_effect(int effect); diff --git a/app/include/zmk/rgb_underglow_layer.h b/app/include/zmk/rgb_underglow_layer.h new file mode 100644 index 00000000000..bee13de2e15 --- /dev/null +++ b/app/include/zmk/rgb_underglow_layer.h @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2020 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#pragma once +#include + +#define ZMK_RGB_CHILD_LEN_PLUS_ONE(node) 1 + + +#define ZMK_RGBMAP_LAYERS_LEN \ + (DT_FOREACH_CHILD(DT_INST(0, zmk_underglow_layer), ZMK_RGB_CHILD_LEN_PLUS_ONE) 0) + +#define ZMK_RGBMAP_EXTRACT_BINDING(idx, drv_inst) \ + { \ + .behavior_dev = DEVICE_DT_NAME(DT_PHANDLE_BY_IDX(drv_inst, bindings, idx)), \ + .param1 = COND_CODE_0(DT_PHA_HAS_CELL_AT_IDX(drv_inst, bindings, idx, param1), (0), \ + (DT_PHA_BY_IDX(drv_inst, bindings, idx, param1))), \ + .param2 = COND_CODE_0(DT_PHA_HAS_CELL_AT_IDX(drv_inst, bindings, idx, param2), (0), \ + (DT_PHA_BY_IDX(drv_inst, bindings, idx, param2))), \ + } + +const int rgb_pixel_lookup(int idx); +const int zmk_rgbmap_id(uint8_t layer); +const int zmk_rgbmap_fade_delay(uint8_t layer); + +const struct zmk_behavior_binding *rgb_underglow_get_bindings(uint8_t layer); + +uint8_t rgb_underglow_top_layer_with_state(uint32_t state_to_test); +uint8_t rgb_underglow_top_layer(void); +uint32_t rgb_underglow_layers_state(void); diff --git a/app/include/zmk/split/bluetooth/uuid.h b/app/include/zmk/split/bluetooth/uuid.h index c9a63efa702..6380e08f231 100644 --- a/app/include/zmk/split/bluetooth/uuid.h +++ b/app/include/zmk/split/bluetooth/uuid.h @@ -20,3 +20,4 @@ #define ZMK_SPLIT_BT_UPDATE_HID_INDICATORS_UUID ZMK_BT_SPLIT_UUID(0x00000004) #define ZMK_SPLIT_BT_SELECT_PHYS_LAYOUT_UUID ZMK_BT_SPLIT_UUID(0x00000005) #define ZMK_SPLIT_BT_INPUT_EVENT_UUID ZMK_BT_SPLIT_UUID(0x00000006) +#define ZMK_SPLIT_BT_UPDATE_LAYERS_UUID ZMK_BT_SPLIT_UUID(0x00000007) diff --git a/app/include/zmk/split/central.h b/app/include/zmk/split/central.h index ff971bfc607..3fcf17f23b9 100644 --- a/app/include/zmk/split/central.h +++ b/app/include/zmk/split/central.h @@ -46,3 +46,5 @@ int zmk_split_central_update_hid_indicator(zmk_hid_indicators_t indicators); int zmk_split_central_get_peripheral_battery_level(uint8_t source, uint8_t *level); #endif // IS_ENABLED(CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING) + +int zmk_split_central_update_layers(uint32_t layers); diff --git a/app/include/zmk/split/peripheral_layers.h b/app/include/zmk/split/peripheral_layers.h new file mode 100644 index 00000000000..b00e66d9119 --- /dev/null +++ b/app/include/zmk/split/peripheral_layers.h @@ -0,0 +1,6 @@ +#pragma once + +void set_peripheral_layers_state(uint32_t new_layers); +bool peripheral_layer_active(uint8_t layer); +uint8_t peripheral_highest_layer_active(void); +uint32_t peripheral_layers_state(void); diff --git a/app/include/zmk/split/transport/types.h b/app/include/zmk/split/transport/types.h index 1d6eb734c65..1ce264673d1 100644 --- a/app/include/zmk/split/transport/types.h +++ b/app/include/zmk/split/transport/types.h @@ -66,6 +66,7 @@ enum zmk_split_transport_central_command_type { ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_INVOKE_BEHAVIOR, ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_SET_PHYSICAL_LAYOUT, ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_SET_HID_INDICATORS, + ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_SET_RGB_LAYERS, } __packed; struct zmk_split_transport_central_command { @@ -87,5 +88,9 @@ struct zmk_split_transport_central_command { struct { zmk_hid_indicators_t indicators; } set_hid_indicators; + + struct { + uint32_t layers; + } set_rgb_layers; } data; } __packed; \ No newline at end of file diff --git a/app/snippets/cirque-lh-sensitivity-high/cirque-lh-sensitivity.overlay b/app/snippets/cirque-lh-sensitivity-high/cirque-lh-sensitivity.overlay new file mode 100644 index 00000000000..057a9d010aa --- /dev/null +++ b/app/snippets/cirque-lh-sensitivity-high/cirque-lh-sensitivity.overlay @@ -0,0 +1,3 @@ +&glidepoint { + sensitivity = "1x"; +}; diff --git a/app/snippets/cirque-lh-sensitivity-high/snippet.yml b/app/snippets/cirque-lh-sensitivity-high/snippet.yml new file mode 100644 index 00000000000..639fc6bb2ec --- /dev/null +++ b/app/snippets/cirque-lh-sensitivity-high/snippet.yml @@ -0,0 +1,8 @@ +name: cirque-lh-sensitivity-high +boards: + go60_lh: + append: + EXTRA_DTC_OVERLAY_FILE: cirque-lh-sensitivity.overlay + glove80_lh: + append: + EXTRA_DTC_OVERLAY_FILE: cirque-lh-sensitivity.overlay diff --git a/app/snippets/cirque-lh-sensitivity-low/cirque-lh-sensitivity.overlay b/app/snippets/cirque-lh-sensitivity-low/cirque-lh-sensitivity.overlay new file mode 100644 index 00000000000..6f99ed7828a --- /dev/null +++ b/app/snippets/cirque-lh-sensitivity-low/cirque-lh-sensitivity.overlay @@ -0,0 +1,3 @@ +&glidepoint { + sensitivity = "3x"; +}; diff --git a/app/snippets/cirque-lh-sensitivity-low/snippet.yml b/app/snippets/cirque-lh-sensitivity-low/snippet.yml new file mode 100644 index 00000000000..d379001fc14 --- /dev/null +++ b/app/snippets/cirque-lh-sensitivity-low/snippet.yml @@ -0,0 +1,8 @@ +name: cirque-lh-sensitivity-low +boards: + go60_lh: + append: + EXTRA_DTC_OVERLAY_FILE: cirque-lh-sensitivity.overlay + glove80_lh: + append: + EXTRA_DTC_OVERLAY_FILE: cirque-lh-sensitivity.overlay diff --git a/app/snippets/cirque-lh-sensitivity-medium/cirque-lh-sensitivity.overlay b/app/snippets/cirque-lh-sensitivity-medium/cirque-lh-sensitivity.overlay new file mode 100644 index 00000000000..53b3dd017e8 --- /dev/null +++ b/app/snippets/cirque-lh-sensitivity-medium/cirque-lh-sensitivity.overlay @@ -0,0 +1,3 @@ +&glidepoint { + sensitivity = "2x"; +}; diff --git a/app/snippets/cirque-lh-sensitivity-medium/snippet.yml b/app/snippets/cirque-lh-sensitivity-medium/snippet.yml new file mode 100644 index 00000000000..1652017fc7c --- /dev/null +++ b/app/snippets/cirque-lh-sensitivity-medium/snippet.yml @@ -0,0 +1,8 @@ +name: cirque-lh-sensitivity-medium +boards: + go60_lh: + append: + EXTRA_DTC_OVERLAY_FILE: cirque-lh-sensitivity.overlay + glove80_lh: + append: + EXTRA_DTC_OVERLAY_FILE: cirque-lh-sensitivity.overlay diff --git a/app/snippets/cirque-rh-sensitivity-high/cirque-rh-sensitivity.overlay b/app/snippets/cirque-rh-sensitivity-high/cirque-rh-sensitivity.overlay new file mode 100644 index 00000000000..057a9d010aa --- /dev/null +++ b/app/snippets/cirque-rh-sensitivity-high/cirque-rh-sensitivity.overlay @@ -0,0 +1,3 @@ +&glidepoint { + sensitivity = "1x"; +}; diff --git a/app/snippets/cirque-rh-sensitivity-high/snippet.yml b/app/snippets/cirque-rh-sensitivity-high/snippet.yml new file mode 100644 index 00000000000..5ddc9e2ff81 --- /dev/null +++ b/app/snippets/cirque-rh-sensitivity-high/snippet.yml @@ -0,0 +1,8 @@ +name: cirque-rh-sensitivity-high +boards: + go60_rh: + append: + EXTRA_DTC_OVERLAY_FILE: cirque-rh-sensitivity.overlay + glove80_rh: + append: + EXTRA_DTC_OVERLAY_FILE: cirque-rh-sensitivity.overlay diff --git a/app/snippets/cirque-rh-sensitivity-low/cirque-rh-sensitivity.overlay b/app/snippets/cirque-rh-sensitivity-low/cirque-rh-sensitivity.overlay new file mode 100644 index 00000000000..6f99ed7828a --- /dev/null +++ b/app/snippets/cirque-rh-sensitivity-low/cirque-rh-sensitivity.overlay @@ -0,0 +1,3 @@ +&glidepoint { + sensitivity = "3x"; +}; diff --git a/app/snippets/cirque-rh-sensitivity-low/snippet.yml b/app/snippets/cirque-rh-sensitivity-low/snippet.yml new file mode 100644 index 00000000000..21786f9d531 --- /dev/null +++ b/app/snippets/cirque-rh-sensitivity-low/snippet.yml @@ -0,0 +1,8 @@ +name: cirque-rh-sensitivity-low +boards: + go60_rh: + append: + EXTRA_DTC_OVERLAY_FILE: cirque-rh-sensitivity.overlay + glove80_rh: + append: + EXTRA_DTC_OVERLAY_FILE: cirque-rh-sensitivity.overlay diff --git a/app/snippets/cirque-rh-sensitivity-medium/cirque-rh-sensitivity.overlay b/app/snippets/cirque-rh-sensitivity-medium/cirque-rh-sensitivity.overlay new file mode 100644 index 00000000000..53b3dd017e8 --- /dev/null +++ b/app/snippets/cirque-rh-sensitivity-medium/cirque-rh-sensitivity.overlay @@ -0,0 +1,3 @@ +&glidepoint { + sensitivity = "2x"; +}; diff --git a/app/snippets/cirque-rh-sensitivity-medium/snippet.yml b/app/snippets/cirque-rh-sensitivity-medium/snippet.yml new file mode 100644 index 00000000000..faeca4f81cf --- /dev/null +++ b/app/snippets/cirque-rh-sensitivity-medium/snippet.yml @@ -0,0 +1,8 @@ +name: cirque-rh-sensitivity-medium +boards: + go60_rh: + append: + EXTRA_DTC_OVERLAY_FILE: cirque-rh-sensitivity.overlay + glove80_rh: + append: + EXTRA_DTC_OVERLAY_FILE: cirque-rh-sensitivity.overlay diff --git a/app/src/activity.c b/app/src/activity.c index b109d46d841..bd367723c15 100644 --- a/app/src/activity.c +++ b/app/src/activity.c @@ -16,6 +16,7 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); #include #include #include +#include #include #include @@ -109,6 +110,9 @@ static int activity_init(void) { ZMK_LISTENER(activity, activity_event_listener); ZMK_SUBSCRIPTION(activity, zmk_position_state_changed); ZMK_SUBSCRIPTION(activity, zmk_sensor_event); +#if IS_ENABLED(CONFIG_ZMK_SPLIT) +ZMK_SUBSCRIPTION(activity, zmk_split_peripheral_layer_changed); +#endif #if IS_ENABLED(CONFIG_ZMK_POINTING) diff --git a/app/src/behaviors/behavior_underglow_battery.c b/app/src/behaviors/behavior_underglow_battery.c new file mode 100644 index 00000000000..e369f8428d2 --- /dev/null +++ b/app/src/behaviors/behavior_underglow_battery.c @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2024 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#define DT_DRV_COMPAT zmk_behavior_underglow_battery + +// Dependencies +#include +#include +#include +#include +#include +#include +#include + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +#if DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) + +struct underglow_battery_data { + uint32_t layers; +}; + +struct underglow_battery_config { + int threshold; +}; + +static struct underglow_battery_data underglow_battery_data = {.layers = 0}; + +static int underglow_battery_init(const struct device *dev) { return 0; }; + +static int underglow_battery_process(struct zmk_behavior_binding *binding, + struct zmk_behavior_binding_event event) { + const struct device *dev = zmk_behavior_get_binding(binding->behavior_dev); + const struct underglow_battery_config *config = dev->config; + struct underglow_battery_data *data = dev->data; + data->layers |= BIT(event.layer); + int bat = zmk_battery_state_of_charge(); + + if (bat >= config->threshold) + return binding->param2; + else + return binding->param1; +} + +static const struct behavior_driver_api underglow_battery_driver_api = { + .binding_pressed = underglow_battery_process, + .locality = BEHAVIOR_LOCALITY_GLOBAL, +#if IS_ENABLED(CONFIG_ZMK_BEHAVIOR_METADATA) + .get_parameter_metadata = zmk_behavior_get_empty_param_metadata, +#endif // IS_ENABLED(CONFIG_ZMK_BEHAVIOR_METADATA) +}; + +static int underglow_battery_listener(const zmk_event_t *eh); + +ZMK_LISTENER(behavior_underglow_battery, underglow_battery_listener); +ZMK_SUBSCRIPTION(behavior_underglow_battery, zmk_battery_state_changed); + +static int underglow_battery_listener(const zmk_event_t *eh) { + raise_zmk_underglow_color_changed((struct zmk_underglow_color_changed){ + .layers = underglow_battery_data.layers, .wakeup = false}); + + return ZMK_EV_EVENT_BUBBLE; +} + +#define KP_INST(n) \ + static struct underglow_battery_config underglow_battery_config_##n = { \ + .threshold = DT_INST_PROP(n, threshold)}; \ + BEHAVIOR_DT_INST_DEFINE(n, underglow_battery_init, NULL, &underglow_battery_data, \ + &underglow_battery_config_##n, POST_KERNEL, \ + CONFIG_KERNEL_INIT_PRIORITY_DEFAULT, &underglow_battery_driver_api); + +DT_INST_FOREACH_STATUS_OKAY(KP_INST) + +#endif /* DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) */ diff --git a/app/src/behaviors/behavior_underglow_color.c b/app/src/behaviors/behavior_underglow_color.c new file mode 100644 index 00000000000..29000aab661 --- /dev/null +++ b/app/src/behaviors/behavior_underglow_color.c @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2024 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#define DT_DRV_COMPAT zmk_behavior_underglow_color + +// Dependencies +#include +#include +#include + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +#if DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) + +// Initialization Function +static int underglow_color_init(const struct device *dev) { return 0; }; + +static int underglow_color_process(struct zmk_behavior_binding *binding, + struct zmk_behavior_binding_event event) { + return binding->param1; +} + +// API Structure +static const struct behavior_driver_api underglow_color_driver_api = { + .binding_pressed = underglow_color_process, + .locality = BEHAVIOR_LOCALITY_GLOBAL, +#if IS_ENABLED(CONFIG_ZMK_BEHAVIOR_METADATA) + .get_parameter_metadata = zmk_behavior_get_empty_param_metadata, +#endif // IS_ENABLED(CONFIG_ZMK_BEHAVIOR_METADATA) + +}; + +BEHAVIOR_DT_INST_DEFINE(0, underglow_color_init, NULL, NULL, NULL, POST_KERNEL, + CONFIG_KERNEL_INIT_PRIORITY_DEFAULT, &underglow_color_driver_api); + +#endif /* DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) */ diff --git a/app/src/behaviors/behavior_underglow_indicators.c b/app/src/behaviors/behavior_underglow_indicators.c new file mode 100644 index 00000000000..913e7f07087 --- /dev/null +++ b/app/src/behaviors/behavior_underglow_indicators.c @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2024 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#define DT_DRV_COMPAT zmk_behavior_underglow_indicators + +// Dependencies +#include +#include +#include +#include +#include +#include +#include + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +#if DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) + +struct underglow_indicators_data { + zmk_hid_indicators_t indicators; + uint32_t layers; +}; + +struct underglow_indicators_config { + int indicator; +}; + +static int underglow_indicators_init(const struct device *dev) { return 0; }; + +static int underglow_indicators_process(struct zmk_behavior_binding *binding, + struct zmk_behavior_binding_event event) { + const struct device *dev = zmk_behavior_get_binding(binding->behavior_dev); + if (dev == NULL) { + return binding->param1; + } + struct underglow_indicators_data *data = dev->data; + const struct underglow_indicators_config *config = dev->config; + data->layers |= BIT(event.layer); + + if (data->indicators & BIT(config->indicator)) + return binding->param2; + else + return binding->param1; +} + +static const struct behavior_driver_api underglow_indicators_driver_api = { + .binding_pressed = underglow_indicators_process, + .locality = BEHAVIOR_LOCALITY_GLOBAL, +#if IS_ENABLED(CONFIG_ZMK_BEHAVIOR_METADATA) + .get_parameter_metadata = zmk_behavior_get_empty_param_metadata, +#endif // IS_ENABLED(CONFIG_ZMK_BEHAVIOR_METADATA) +}; + +static int underglow_indicators_listener(const zmk_event_t *eh); + +ZMK_LISTENER(behavior_underglow_indicators, underglow_indicators_listener); +ZMK_SUBSCRIPTION(behavior_underglow_indicators, zmk_hid_indicators_changed); + +static struct underglow_indicators_data underglow_indicators_data = {.indicators = 0, .layers = 0}; + +static int underglow_indicators_listener(const zmk_event_t *eh) { + const struct zmk_hid_indicators_changed *ev = as_zmk_hid_indicators_changed(eh); + underglow_indicators_data.indicators = ev->indicators; + raise_zmk_underglow_color_changed((struct zmk_underglow_color_changed){ + .layers = underglow_indicators_data.layers, .wakeup = true}); + + return ZMK_EV_EVENT_BUBBLE; +} + +#define KP_INST(n) \ + static struct underglow_indicators_config underglow_indicators_config_##n = { \ + .indicator = DT_INST_PROP(n, indicator)}; \ + BEHAVIOR_DT_INST_DEFINE(n, underglow_indicators_init, NULL, &underglow_indicators_data, \ + &underglow_indicators_config_##n, POST_KERNEL, \ + CONFIG_KERNEL_INIT_PRIORITY_DEFAULT, \ + &underglow_indicators_driver_api); + +DT_INST_FOREACH_STATUS_OKAY(KP_INST) + +#endif /* DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) */ diff --git a/app/src/events/split_peripheral_layer_changed.c b/app/src/events/split_peripheral_layer_changed.c new file mode 100644 index 00000000000..81f2ab8de00 --- /dev/null +++ b/app/src/events/split_peripheral_layer_changed.c @@ -0,0 +1,10 @@ +/* + * Copyright (c) 2022 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include +#include + +ZMK_EVENT_IMPL(zmk_split_peripheral_layer_changed); \ No newline at end of file diff --git a/app/src/events/underglow_color_changed.c b/app/src/events/underglow_color_changed.c new file mode 100644 index 00000000000..468ce83d812 --- /dev/null +++ b/app/src/events/underglow_color_changed.c @@ -0,0 +1,10 @@ +/* + * Copyright (c) 2020 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include +#include + +ZMK_EVENT_IMPL(zmk_underglow_color_changed); diff --git a/app/src/keymap.c b/app/src/keymap.c index 762dd4f4112..3dafad92608 100644 --- a/app/src/keymap.c +++ b/app/src/keymap.c @@ -17,10 +17,14 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); #include #include #include +#if IS_ENABLED(CONFIG_ZMK_SPLIT_BLE) +#include +#endif #include #include #include +#include #include static zmk_keymap_layers_state_t _zmk_keymap_layer_state = 0; @@ -155,6 +159,11 @@ static inline int set_layer_state(zmk_keymap_layer_id_t layer_id, bool state) { if (ret < 0) { LOG_WRN("Failed to raise layer state changed (%d)", ret); } +#if IS_ENABLED(CONFIG_ZMK_SPLIT_BLE) + zmk_split_central_update_layers(_zmk_keymap_layer_state); + raise_zmk_split_peripheral_layer_changed( + (struct zmk_split_peripheral_layer_changed){.layers = _zmk_keymap_layer_state}); +#endif } return ret; diff --git a/app/src/rgb_underglow.c b/app/src/rgb_underglow.c index e93cda91b9f..25779bc0264 100644 --- a/app/src/rgb_underglow.c +++ b/app/src/rgb_underglow.c @@ -16,6 +16,9 @@ #include #include #include +#include +#include + #include #include @@ -23,19 +26,28 @@ #include #include +#include #include +#include #include #include #include #include +#include + #include +#include #if IS_ENABLED(CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING) #include #endif +#if !IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL) +#include +#endif + LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); #if !DT_HAS_CHOSEN(zmk_underglow) @@ -47,6 +59,11 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); #define STRIP_CHOSEN DT_CHOSEN(zmk_underglow) #define STRIP_NUM_PIXELS DT_PROP(STRIP_CHOSEN, chain_length) +#if DT_HAS_COMPAT_STATUS_OKAY(zmk_underglow_layer) && IS_ENABLED(CONFIG_EXPERIMENTAL_RGB_LAYER) +#define UNDERGLOW_LAYER_ENABLED 1 +static void zmk_rgb_underglow_set_layer(uint8_t layer, bool wakeup); +#endif + #define HUE_MAX 360 #define SAT_MAX 100 #define BRT_MAX 100 @@ -59,6 +76,9 @@ enum rgb_underglow_effect { UNDERGLOW_EFFECT_BREATHE, UNDERGLOW_EFFECT_SPECTRUM, UNDERGLOW_EFFECT_SWIRL, +#if IS_ENABLED(UNDERGLOW_LAYER_ENABLED) + UNDERGLOW_EFFECT_LAYER_INDICATORS, +#endif UNDERGLOW_EFFECT_NUMBER // Used to track number of underglow effects }; @@ -69,6 +89,7 @@ struct rgb_underglow_state { uint16_t animation_step; bool on; bool status_active; + bool layer_enabled; uint16_t status_animation_step; }; @@ -190,6 +211,25 @@ static void zmk_rgb_underglow_effect_swirl(void) { state.animation_step = state.animation_step % HUE_MAX; } +#if IS_ENABLED(UNDERGLOW_LAYER_ENABLED) +static void zmk_rgb_underglow_effect_layer(void) { + bool active = false; + for (int i = 0; i < STRIP_NUM_PIXELS; i++) { + pixels[i].r -= state.animation_speed < pixels[i].r ? state.animation_speed : pixels[i].r; + pixels[i].g -= state.animation_speed < pixels[i].g ? state.animation_speed : pixels[i].g; + pixels[i].b -= state.animation_speed < pixels[i].b ? state.animation_speed : pixels[i].b; + if (pixels[i].r || pixels[i].g || pixels[i].b) { + active = true; + } + } + state.animation_step += state.animation_speed; + + if (state.animation_step > 255 || !active) { + zmk_rgb_underglow_transient_off(); + } +} +#endif // IS_ENABLED(UNDERGLOW_LAYER_ENABLED) + static int zmk_led_generate_status(void); static void zmk_led_write_pixels(void) { @@ -419,6 +459,13 @@ static int zmk_led_generate_status(void) { } #endif // underglow_indicators exists +static inline struct led_rgb hue_sat(int hue, int sat) { + struct zmk_led_hsb hsb = state.color; + hsb.h = hue; + hsb.s = sat; + return hsb_to_rgb(hsb_scale_min_max(hsb)); +} + static void zmk_rgb_underglow_tick(struct k_work *work) { switch (state.current_effect) { case UNDERGLOW_EFFECT_SOLID: @@ -433,6 +480,11 @@ static void zmk_rgb_underglow_tick(struct k_work *work) { case UNDERGLOW_EFFECT_SWIRL: zmk_rgb_underglow_effect_swirl(); break; +#if IS_ENABLED(UNDERGLOW_LAYER_ENABLED) + case UNDERGLOW_EFFECT_LAYER_INDICATORS: + zmk_rgb_underglow_effect_layer(); + break; +#endif } zmk_led_write_pixels(); @@ -463,9 +515,15 @@ static int rgb_settings_set(const char *name, size_t len, settings_read_cb read_ rc = read_cb(cb_arg, &state, sizeof(state)); if (rc >= 0) { if (state.on) { +#if IS_ENABLED(UNDERGLOW_LAYER_ENABLED) + if (state.layer_enabled) { + zmk_rgb_underglow_transient_on(); + zmk_rgb_underglow_set_layer(rgb_underglow_top_layer(), true); + } +#else k_timer_start(&underglow_tick, K_NO_WAIT, K_MSEC(50)); +#endif } - return 0; } @@ -503,7 +561,8 @@ static int zmk_rgb_underglow_init(void) { animation_speed : CONFIG_ZMK_RGB_UNDERGLOW_SPD_START, current_effect : CONFIG_ZMK_RGB_UNDERGLOW_EFF_START, animation_step : 0, - on : IS_ENABLED(CONFIG_ZMK_RGB_UNDERGLOW_ON_START) + on : IS_ENABLED(CONFIG_ZMK_RGB_UNDERGLOW_ON_START), + layer_enabled : false }; #if IS_ENABLED(CONFIG_SETTINGS) @@ -517,7 +576,11 @@ static int zmk_rgb_underglow_init(void) { if (state.on) { k_timer_start(&underglow_tick, K_NO_WAIT, K_MSEC(25)); } - +#if IS_ENABLED(UNDERGLOW_LAYER_ENABLED) + if (state.layer_enabled) { + zmk_rgb_underglow_set_layer(rgb_underglow_top_layer(), true); + } +#endif return 0; } @@ -534,11 +597,12 @@ int zmk_rgb_underglow_get_state(bool *on_off) { if (!led_strip) return -ENODEV; - *on_off = state.on; + *on_off = state.on || state.layer_enabled; return 0; } void zmk_rgb_set_ext_power(void) { + LOG_DBG("setting ext power"); #if IS_ENABLED(CONFIG_ZMK_RGB_UNDERGLOW_EXT_POWER) if (ext_power == NULL) return; @@ -573,16 +637,27 @@ void zmk_rgb_set_ext_power(void) { } int zmk_rgb_underglow_on(void) { +#if IS_ENABLED(UNDERGLOW_LAYER_ENABLED) + if (state.current_effect == UNDERGLOW_EFFECT_LAYER_INDICATORS) { + state.layer_enabled = true; + } +#endif + zmk_rgb_underglow_transient_on(); + return zmk_rgb_underglow_save_state(); +} + +int zmk_rgb_underglow_transient_on(void) { if (!led_strip) return -ENODEV; state.on = true; zmk_rgb_set_ext_power(); - state.animation_step = 0; - k_timer_start(&underglow_tick, K_NO_WAIT, K_MSEC(25)); - - return zmk_rgb_underglow_save_state(); + if (!state.layer_enabled) { + state.animation_step = 0; + k_timer_start(&underglow_tick, K_NO_WAIT, K_MSEC(25)); + } + return 0; } static void zmk_rgb_underglow_off_handler(struct k_work *work) { @@ -596,6 +671,12 @@ static void zmk_rgb_underglow_off_handler(struct k_work *work) { K_WORK_DEFINE(underglow_off_work, zmk_rgb_underglow_off_handler); int zmk_rgb_underglow_off(void) { + zmk_rgb_underglow_transient_off(); + state.layer_enabled = false; + return zmk_rgb_underglow_save_state(); +} + +int zmk_rgb_underglow_transient_off(void) { if (!led_strip) return -ENODEV; @@ -605,7 +686,7 @@ int zmk_rgb_underglow_off(void) { state.on = false; zmk_rgb_set_ext_power(); - return zmk_rgb_underglow_save_state(); + return 0; } int zmk_rgb_underglow_calc_effect(int direction) { @@ -622,7 +703,9 @@ int zmk_rgb_underglow_select_effect(int effect) { state.current_effect = effect; state.animation_step = 0; - +#if IS_ENABLED(UNDERGLOW_LAYER_ENABLED) + state.layer_enabled = (effect == UNDERGLOW_EFFECT_LAYER_INDICATORS); +#endif return zmk_rgb_underglow_save_state(); } @@ -634,6 +717,152 @@ int zmk_rgb_underglow_toggle(void) { return state.on ? zmk_rgb_underglow_off() : zmk_rgb_underglow_on(); } +#if IS_ENABLED(UNDERGLOW_LAYER_ENABLED) + +static struct led_rgb hex_to_rgb(uint8_t r, uint8_t g, uint8_t b) { + struct zmk_led_hsb hsb = state.color; + return (struct led_rgb){ + r : (hsb.b * (r)) / 0xff, + g : (hsb.b * (g)) / 0xff, + b : (hsb.b * (b)) / 0xff + }; +} + +static int zmk_rgb_underglow_apply_merged_rgbmap() { + LOG_DBG("applying merged rgbmap"); + int rc = 0; + size_t len = 0; + uint8_t active_layers[ZMK_KEYMAP_LAYERS_LEN]; + uint32_t layer_state = rgb_underglow_layers_state(); + LOG_DBG("layer state: %08x", layer_state); + for (uint8_t layer = ZMK_KEYMAP_LAYERS_LEN - 1; layer > 0; layer--) { + if ((layer_state & (BIT(layer))) == (BIT(layer)) || layer == 0) { + active_layers[len] = layer; + len++; + LOG_DBG("active layer: %d", layer); + } + } + active_layers[len++] = 0; // add default layer (0) + // fast track if no bindings on top layer + if (rgb_underglow_get_bindings(active_layers[0]) == NULL) { + LOG_DBG("no rgb bindings found for active layer: %d", active_layers[0]); + return 0; + } + + for (int pixel = 0; pixel < STRIP_NUM_PIXELS; pixel++) { + uint8_t midx = rgb_pixel_lookup(pixel); + int color = 0; + if (midx >= ZMK_KEYMAP_LEN) { + LOG_DBG("out of range"); + } else { + + for (int layer = 0; layer < len; layer++) { + // LOG_DBG("getting rgb bindings for active layer: %d", active_layers[layer]); + const struct zmk_behavior_binding *bindings = + rgb_underglow_get_bindings(active_layers[layer]); + if (bindings != NULL) { + const struct device *dev = + zmk_behavior_get_binding(bindings[midx].behavior_dev); + if (dev != NULL) { + const struct behavior_driver_api *api = + (const struct behavior_driver_api *)dev->api; + if (api->binding_pressed != NULL) { + struct zmk_behavior_binding_event event = {.position = midx, + .layer = + active_layers[layer], + .timestamp = k_uptime_get()}; + + color = api->binding_pressed( + (struct zmk_behavior_binding *)&bindings[midx], event); + if (color == ZMK_BEHAVIOR_TRANSPARENT) { + color = 0; + continue; + } + } // end if binding_pressed != NULL + } // end if dev != NULL + } // end if bindings != NULL + + break; + } // end for each active layer + + // set pixel color + pixels[pixel] = + hex_to_rgb((color & 0xFF0000) >> 16, (color & 0xFF00) >> 8, color & 0xFF); + + if (color > 0) { + rc = 1; // layer has at least one pixel up + } + } // end for each pixel + } + return rc; +} + +static int zmk_rgb_underglow_apply_rgbmap(const struct zmk_behavior_binding *bindings, + size_t rgbmap_len, uint8_t layer) { + LOG_DBG("applying rgbmap for layer: %d", layer); + return zmk_rgb_underglow_apply_merged_rgbmap(); + int rc = 0; + for (int i = 0; i < STRIP_NUM_PIXELS; i++) { + uint8_t midx = rgb_pixel_lookup(i); + if (midx >= ZMK_KEYMAP_LEN) { + LOG_DBG("out of range"); + } else { + const struct device *dev = zmk_behavior_get_binding(bindings[midx].behavior_dev); + + if (dev == NULL) { + continue; + } + + const struct behavior_driver_api *api = (const struct behavior_driver_api *)dev->api; + + if (api->binding_pressed == NULL) { + continue; + } + struct zmk_behavior_binding_event event = { + .position = midx, .layer = layer, .timestamp = k_uptime_get()}; + + int color = api->binding_pressed((struct zmk_behavior_binding *)&bindings[midx], event); + + if (color > 0) { + pixels[i] = + hex_to_rgb((color & 0xFF0000) >> 16, (color & 0xFF00) >> 8, color & 0xFF); + rc = 1; + } else { + pixels[i] = (struct led_rgb){r : 0, g : 0, b : 0}; + } + } + } + return rc; +} + +static void zmk_rgb_underglow_set_layer(uint8_t layer, bool wakeup) { + LOG_DBG("state.layer: %d state.on: %d", state.layer_enabled, state.on); + if (!state.layer_enabled) + return; + + if (zmk_rgb_underglow_apply_merged_rgbmap()) { + if (!state.on) { + if (!wakeup) { + LOG_DBG("rgb off and no wakeup, abort refresh"); + return; + } + zmk_rgb_underglow_transient_on(); + } + k_timer_stop(&underglow_tick); + state.animation_step = 0; + int fade_delay = zmk_rgbmap_fade_delay(layer); + if (fade_delay >= 0) { + k_timer_start(&underglow_tick, K_SECONDS(fade_delay), K_MSEC(50)); + } + LOG_DBG("write pixels"); + zmk_led_write_pixels(); + } else { + if (state.on) + zmk_rgb_underglow_transient_off(); + } +} +#endif /* IS_ENABLED(UNDERGLOW_LAYER_ENABLED) */ + static void zmk_led_write_pixels_work(struct k_work *work); static void zmk_rgb_underglow_status_update(struct k_timer *timer); @@ -763,7 +992,7 @@ int zmk_rgb_underglow_change_spd(int direction) { } #if IS_ENABLED(CONFIG_ZMK_RGB_UNDERGLOW_AUTO_OFF_IDLE) || \ - IS_ENABLED(CONFIG_ZMK_RGB_UNDERGLOW_AUTO_OFF_USB) + IS_ENABLED(CONFIG_ZMK_RGB_UNDERGLOW_AUTO_OFF_USB) || IS_ENABLED(UNDERGLOW_LAYER_ENABLED) struct rgb_underglow_sleep_state { bool is_awake; bool rgb_state_before_sleeping; @@ -782,14 +1011,20 @@ static int rgb_underglow_auto_state(bool target_wake_state) { sleep_state.is_awake = target_wake_state; if (sleep_state.is_awake) { +#if IS_ENABLED(UNDERGLOW_LAYER_ENABLED) + if (state.layer_enabled) { + zmk_rgb_underglow_set_layer(rgb_underglow_top_layer(), true); + return 0; + } +#endif if (sleep_state.rgb_state_before_sleeping) { - return zmk_rgb_underglow_on(); + return zmk_rgb_underglow_transient_on(); } else { - return zmk_rgb_underglow_off(); + return zmk_rgb_underglow_transient_off(); } } else { sleep_state.rgb_state_before_sleeping = state.on; - return zmk_rgb_underglow_off(); + return zmk_rgb_underglow_transient_off(); } } @@ -801,6 +1036,30 @@ static int rgb_underglow_event_listener(const zmk_event_t *eh) { } #endif +#if IS_ENABLED(UNDERGLOW_LAYER_ENABLED) + if (as_zmk_split_peripheral_layer_changed(eh)) { + const struct zmk_split_peripheral_layer_changed *ev = + as_zmk_split_peripheral_layer_changed(eh); + LOG_DBG("zmk_split_peripheral_layer_changed: %08x", ev->layers); +#if !IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL) + set_peripheral_layers_state(ev->layers); +#endif + uint8_t layer = rgb_underglow_top_layer(); + LOG_DBG("top layer: %d", layer); + zmk_rgb_underglow_set_layer(layer, true); + return 0; + } + if (as_zmk_underglow_color_changed(eh)) { + const struct zmk_underglow_color_changed *ev = as_zmk_underglow_color_changed(eh); + uint8_t layer = rgb_underglow_top_layer(); + LOG_DBG("refresh layers %d, current: %d, wakeup: %d", ev->layers, layer, ev->wakeup); + if ((ev->layers & (BIT(layer))) == BIT(layer)) { + zmk_rgb_underglow_set_layer(rgb_underglow_top_layer(), ev->wakeup); + } + return 0; + } +#endif /* UNDERGLOW_LAYER_ENABLED */ + #if IS_ENABLED(CONFIG_ZMK_RGB_UNDERGLOW_AUTO_OFF_USB) if (as_zmk_usb_conn_state_changed(eh)) { return rgb_underglow_auto_state(zmk_usb_is_powered()); @@ -812,7 +1071,8 @@ static int rgb_underglow_event_listener(const zmk_event_t *eh) { ZMK_LISTENER(rgb_underglow, rgb_underglow_event_listener); #endif // IS_ENABLED(CONFIG_ZMK_RGB_UNDERGLOW_AUTO_OFF_IDLE) || - // IS_ENABLED(CONFIG_ZMK_RGB_UNDERGLOW_AUTO_OFF_USB) + // IS_ENABLED(CONFIG_ZMK_RGB_UNDERGLOW_AUTO_OFF_USB) || + // IS_ENABLED(UNDERGLOW_LAYER_ENABLED) #if IS_ENABLED(CONFIG_ZMK_RGB_UNDERGLOW_AUTO_OFF_IDLE) ZMK_SUBSCRIPTION(rgb_underglow, zmk_activity_state_changed); @@ -822,4 +1082,9 @@ ZMK_SUBSCRIPTION(rgb_underglow, zmk_activity_state_changed); ZMK_SUBSCRIPTION(rgb_underglow, zmk_usb_conn_state_changed); #endif +#if IS_ENABLED(UNDERGLOW_LAYER_ENABLED) +ZMK_SUBSCRIPTION(rgb_underglow, zmk_split_peripheral_layer_changed); +ZMK_SUBSCRIPTION(rgb_underglow, zmk_underglow_color_changed); +#endif + SYS_INIT(zmk_rgb_underglow_init, APPLICATION, CONFIG_APPLICATION_INIT_PRIORITY); diff --git a/app/src/rgb_underglow_layer.c b/app/src/rgb_underglow_layer.c new file mode 100644 index 00000000000..6751b8c2de3 --- /dev/null +++ b/app/src/rgb_underglow_layer.c @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2020 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include +#include +#include +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +#include +#include +#include + +#if !IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL) +#include +#endif + +#define DT_DRV_COMPAT zmk_underglow_layer +#if DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) + +#define UNDERGLOW_LAYER_ENABLED +#define LAYER_ID(node) DT_PROP(node, layer_id) +#define FADE_DELAY(node) DT_PROP(node, fade_delay) + +#define TRANSFORMED_RGB_LAYER(node) \ + {COND_CODE_1(DT_NODE_HAS_PROP(node, bindings), \ + (LISTIFY(DT_PROP_LEN(node, bindings), ZMK_RGBMAP_EXTRACT_BINDING, (, ), node)), \ + ())} + +#define RGBMAP_VAR(_name, _opts) \ + static _opts struct zmk_behavior_binding _name[ZMK_RGBMAP_LAYERS_LEN][ZMK_KEYMAP_LEN] = { \ + DT_INST_FOREACH_CHILD_STATUS_OKAY_SEP(0, TRANSFORMED_RGB_LAYER, (, ))}; + +RGBMAP_VAR(zmk_rgbmap, COND_CODE_1(IS_ENABLED(CONFIG_ZMK_KEYMAP_SETTINGS_STORAGE), (), (const))) + +const int pixel_lookup_table[] = DT_INST_PROP(0, pixel_lookup); + +static int zmk_rgbmap_ids[ZMK_RGBMAP_LAYERS_LEN] = {DT_INST_FOREACH_CHILD_SEP(0, LAYER_ID, (, ))}; +static int zmk_rgbmap_fds[ZMK_RGBMAP_LAYERS_LEN] = {DT_INST_FOREACH_CHILD_SEP(0, FADE_DELAY, (, ))}; + +const int rgb_pixel_lookup(int idx) { return pixel_lookup_table[idx]; }; + +const int zmk_rgbmap_id(uint8_t layer) { + for (uint8_t i = 0; i < ZMK_RGBMAP_LAYERS_LEN; i++) { + if (zmk_rgbmap_ids[i] == layer) { + return i; + } + } + return -1; +} + +const int zmk_rgbmap_fade_delay(uint8_t layer) { return zmk_rgbmap_fds[zmk_rgbmap_id(layer)]; } + +const struct zmk_behavior_binding *rgb_underglow_get_bindings(uint8_t layer) { + int rgblayer = zmk_rgbmap_id(layer); + if (rgblayer == -1) { + return NULL; + } else { + return zmk_rgbmap[rgblayer]; + } +} + +bool rgb_underglow_layer_active_with_state(uint8_t layer, uint32_t state_to_test) { + return (state_to_test & (BIT(layer))) == (BIT(layer)); +}; + +uint8_t rgb_underglow_top_layer_with_state(uint32_t state_to_test) { + for (uint8_t layer = ZMK_KEYMAP_LAYERS_LEN - 1; layer > 0; layer--) { + if ((state_to_test & (BIT(layer))) == (BIT(layer)) || layer == 0) { + return layer; + } + } + // return default layer (0) + return 0; +} + +uint32_t rgb_underglow_layers_state(void) { +#if IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL) + return zmk_keymap_layer_state(); +#else + return peripheral_layers_state(); +#endif +} + +uint8_t rgb_underglow_top_layer(void) { +#if IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL) + return zmk_keymap_highest_layer_active(); +#else + return peripheral_highest_layer_active(); +#endif +} +#endif /* DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) */ diff --git a/app/src/split/CMakeLists.txt b/app/src/split/CMakeLists.txt index 20c73089220..2d6ee8e1f27 100644 --- a/app/src/split/CMakeLists.txt +++ b/app/src/split/CMakeLists.txt @@ -14,5 +14,6 @@ if (CONFIG_ZMK_SPLIT_ROLE_CENTRAL) zephyr_linker_sources(SECTIONS ../../include/linker/zmk-split-transport-central.ld) else() target_sources(app PRIVATE peripheral.c) + target_sources(app PRIVATE peripheral_layers.c) zephyr_linker_sources(SECTIONS ../../include/linker/zmk-split-transport-peripheral.ld) endif() \ No newline at end of file diff --git a/app/src/split/bluetooth/CMakeLists.txt b/app/src/split/bluetooth/CMakeLists.txt index f4e12a9d097..e54eda99424 100644 --- a/app/src/split/bluetooth/CMakeLists.txt +++ b/app/src/split/bluetooth/CMakeLists.txt @@ -7,6 +7,7 @@ if (NOT CONFIG_ZMK_SPLIT_ROLE_CENTRAL) endif() if (CONFIG_ZMK_SPLIT_ROLE_CENTRAL) target_sources(app PRIVATE central.c) + target_sources_ifdef(CONFIG_ZMK_SPLIT_BLE_PREF_IDLE app PRIVATE central_listener.c) endif() if (CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_PROXY) diff --git a/app/src/split/bluetooth/Kconfig b/app/src/split/bluetooth/Kconfig index 5f4a782f772..c5f11945da5 100644 --- a/app/src/split/bluetooth/Kconfig +++ b/app/src/split/bluetooth/Kconfig @@ -74,6 +74,26 @@ config ZMK_SPLIT_BLE_PREF_TIMEOUT int "Supervision timeout to use for split central/peripheral connection" default 400 +config ZMK_SPLIT_BLE_PREF_IDLE + bool "Set slower split peripheral BLE params on idle to save power" + default y + +if ZMK_SPLIT_BLE_PREF_IDLE + +config ZMK_SPLIT_BLE_PREF_IDLE_INT + int "Peripheral idle connection interval in 1.25ms units" + default 18 + +config ZMK_SPLIT_BLE_PREF_IDLE_LATENCY + int "Peripheral idle latency in Connection Intervals" + default 10 + +config ZMK_SPLIT_BLE_PREF_IDLE_TIMEOUT + int "Peripheral idle supervision timeout in 10ms units" + default 400 + +endif # ZMK_SPLIT_BLE_PREF_IDLE + endif # ZMK_SPLIT_ROLE_CENTRAL if !ZMK_SPLIT_ROLE_CENTRAL diff --git a/app/src/split/bluetooth/central.c b/app/src/split/bluetooth/central.c index 66b8219db8f..ede95d3b723 100644 --- a/app/src/split/bluetooth/central.c +++ b/app/src/split/bluetooth/central.c @@ -33,6 +33,7 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); #include #include #include +#include static int start_scanning(void); @@ -60,6 +61,8 @@ struct peripheral_slot { uint16_t update_hid_indicators; #endif // IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS) uint16_t selected_physical_layout_handle; + uint16_t update_layers_handle; + uint8_t position_state[POSITION_STATE_DATA_LEN]; uint8_t changed_positions[POSITION_STATE_DATA_LEN]; }; @@ -219,6 +222,7 @@ int release_peripheral_slot(int index) { #if IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS) slot->update_hid_indicators = 0; #endif // IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS) + slot->update_layers_handle = 0; return 0; } @@ -620,6 +624,10 @@ static uint8_t split_central_chrc_discovery_func(struct bt_conn *conn, LOG_DBG("Found update HID indicators handle"); slot->update_hid_indicators = bt_gatt_attr_value_handle(attr); #endif // IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS) + } else if (!bt_uuid_cmp(((struct bt_gatt_chrc *)attr->user_data)->uuid, + BT_UUID_DECLARE_128(ZMK_SPLIT_BT_UPDATE_LAYERS_UUID))) { + LOG_DBG("Found update Layers handle"); + slot->update_layers_handle = bt_gatt_attr_value_handle(attr); #if IS_ENABLED(CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING) } else if (!bt_uuid_cmp(((struct bt_gatt_chrc *)attr->user_data)->uuid, BT_UUID_BAS_BATTERY_LEVEL)) { @@ -707,6 +715,8 @@ static uint8_t split_central_chrc_discovery_func(struct bt_conn *conn, } #endif // IS_ENABLED(CONFIG_ZMK_INPUT_SPLIT) + subscribed = subscribed && slot->update_layers_handle; + return subscribed ? BT_GATT_ITER_STOP : BT_GATT_ITER_CONTINUE; } @@ -1028,6 +1038,7 @@ K_MSGQ_DEFINE(zmk_split_central_split_run_msgq, sizeof(struct central_cmd_wrappe void split_central_split_run_callback(struct k_work *work) { struct central_cmd_wrapper payload_wrapper; + int err; LOG_DBG(""); @@ -1060,7 +1071,7 @@ void split_central_split_run_callback(struct k_work *work) { payload.behavior_dev); } - int err = bt_gatt_write_without_response( + err = bt_gatt_write_without_response( peripherals[payload_wrapper.source].conn, peripherals[payload_wrapper.source].run_behavior_handle, &payload, sizeof(struct zmk_split_run_behavior_payload), true); @@ -1086,7 +1097,7 @@ void split_central_split_run_callback(struct k_work *work) { break; } - int err = bt_gatt_write_without_response( + err = bt_gatt_write_without_response( peripherals[payload_wrapper.source].conn, peripherals[payload_wrapper.source].update_hid_indicators, &payload_wrapper.cmd.data.set_hid_indicators.indicators, @@ -1097,6 +1108,18 @@ void split_central_split_run_callback(struct k_work *work) { } break; #endif // IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS) + case ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_SET_RGB_LAYERS: + err = bt_gatt_write_without_response( + peripherals[payload_wrapper.source].conn, + peripherals[payload_wrapper.source].update_layers_handle, + &payload_wrapper.cmd.data.set_rgb_layers.layers, + sizeof(payload_wrapper.cmd.data.set_rgb_layers.layers), true); + + if (err) { + LOG_ERR("Failed to send layers to peripheral (err %d)", err); + } + break; + default: LOG_WRN("Unsupported wrapped central command type %d", payload_wrapper.cmd.type); return; @@ -1178,6 +1201,7 @@ static int split_central_bt_send_command(uint8_t source, } switch (cmd.type) { + case ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_SET_RGB_LAYERS: case ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_SET_HID_INDICATORS: case ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_SET_PHYSICAL_LAYOUT: case ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_INVOKE_BEHAVIOR: { diff --git a/app/src/split/bluetooth/central_listener.c b/app/src/split/bluetooth/central_listener.c new file mode 100644 index 00000000000..87e471cb776 --- /dev/null +++ b/app/src/split/bluetooth/central_listener.c @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2022 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +#include +#include + +#include +#include + +static void set_sleep_params(struct bt_conn *conn, void *data) { + struct bt_conn_info info; + + bt_conn_get_info(conn, &info); + + if (info.role == BT_CONN_ROLE_CENTRAL) { + int err = + bt_conn_le_param_update(conn, BT_LE_CONN_PARAM(CONFIG_ZMK_SPLIT_BLE_PREF_IDLE_INT, + CONFIG_ZMK_SPLIT_BLE_PREF_IDLE_INT, + CONFIG_ZMK_SPLIT_BLE_PREF_IDLE_LATENCY, + CONFIG_ZMK_SPLIT_BLE_PREF_IDLE_TIMEOUT)); + + if (err) { + LOG_DBG("Failed to sleep split connection: %d", err); + } + } +} + +static void set_wake_params(struct bt_conn *conn, void *data) { + struct bt_conn_info info; + + bt_conn_get_info(conn, &info); + + if (info.role == BT_CONN_ROLE_CENTRAL) { + int err = bt_conn_le_param_update( + conn, + BT_LE_CONN_PARAM(CONFIG_ZMK_SPLIT_BLE_PREF_INT, CONFIG_ZMK_SPLIT_BLE_PREF_INT, + CONFIG_ZMK_SPLIT_BLE_PREF_LATENCY, CONFIG_ZMK_SPLIT_BLE_PREF_TIMEOUT)); + + if (err) { + LOG_DBG("Failed to wake up split connection: %d", err); + } + } +} + +static void sleep_all() { + LOG_DBG("Setting idle connection parameters on peripherals"); + + bt_conn_foreach(BT_CONN_TYPE_LE, set_sleep_params, NULL); +} + +static void wake_all() { + LOG_DBG("Waking up from idle connection parameters on peripherals"); + + bt_conn_foreach(BT_CONN_TYPE_LE, set_wake_params, NULL); +} + +int central_event_handler(const zmk_event_t *eh) { + struct zmk_activity_state_changed *ev = as_zmk_activity_state_changed(eh); + if (ev == NULL) { + return -ENOTSUP; + } + + switch (ev->state) { + case ZMK_ACTIVITY_ACTIVE: + wake_all(); + break; + case ZMK_ACTIVITY_IDLE: + sleep_all(); + break; + case ZMK_ACTIVITY_SLEEP: + break; + default: + LOG_WRN("Unhandled activity state: %d", ev->state); + return -EINVAL; + } + return 0; +} + +ZMK_LISTENER(central, central_event_handler); +ZMK_SUBSCRIPTION(central, zmk_activity_state_changed); diff --git a/app/src/split/bluetooth/service.c b/app/src/split/bluetooth/service.c index 5bbed1373e6..e83a6ea046c 100644 --- a/app/src/split/bluetooth/service.c +++ b/app/src/split/bluetooth/service.c @@ -31,9 +31,11 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); #if IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS) #include #endif // IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS) +#include #include #include +#include #if ZMK_KEYMAP_HAS_SENSORS static struct sensor_event last_sensor_event; @@ -139,6 +141,31 @@ static ssize_t split_svc_get_selected_phys_layout(struct bt_conn *conn, return bt_gatt_attr_read(conn, attrs, buf, len, offset, &selected, sizeof(selected)); } +static uint32_t layers = 0; + +static void split_svc_update_layers_callback(struct k_work *work) { + LOG_DBG("Setting peripheral layers: %x", layers); + // set_peripheral_layers_state(layers); + raise_zmk_split_peripheral_layer_changed( + (struct zmk_split_peripheral_layer_changed){.layers = layers}); +} + +static K_WORK_DEFINE(split_svc_update_layers_work, split_svc_update_layers_callback); + +static ssize_t split_svc_update_layers(struct bt_conn *conn, const struct bt_gatt_attr *attr, + const void *buf, uint16_t len, uint16_t offset, + uint8_t flags) { + if (offset + len > sizeof(uint32_t)) { + return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET); + } + + memcpy((uint8_t *)&layers + offset, buf, len); + + k_work_submit(&split_svc_update_layers_work); + + return len; +} + #if IS_ENABLED(CONFIG_ZMK_INPUT_SPLIT) static void split_input_events_ccc(const struct bt_gatt_attr *attr, uint16_t value) { @@ -204,8 +231,11 @@ BT_GATT_SERVICE_DEFINE( BT_GATT_CHARACTERISTIC(BT_UUID_DECLARE_128(ZMK_SPLIT_BT_SELECT_PHYS_LAYOUT_UUID), BT_GATT_CHRC_WRITE | BT_GATT_CHRC_READ, BT_GATT_PERM_WRITE_ENCRYPT | BT_GATT_PERM_READ_ENCRYPT, - split_svc_get_selected_phys_layout, split_svc_select_phys_layout, - NULL), ); + split_svc_get_selected_phys_layout, split_svc_select_phys_layout, NULL), + + BT_GATT_CHARACTERISTIC(BT_UUID_DECLARE_128(ZMK_SPLIT_BT_UPDATE_LAYERS_UUID), + BT_GATT_CHRC_WRITE_WITHOUT_RESP, BT_GATT_PERM_WRITE_ENCRYPT, NULL, + split_svc_update_layers, NULL), ); K_THREAD_STACK_DEFINE(service_q_stack, CONFIG_ZMK_SPLIT_BLE_PERIPHERAL_STACK_SIZE); diff --git a/app/src/split/central.c b/app/src/split/central.c index e2b8064370c..5503b85271a 100644 --- a/app/src/split/central.c +++ b/app/src/split/central.c @@ -150,6 +150,42 @@ int zmk_split_central_update_hid_indicator(zmk_hid_indicators_t indicators) { #endif // IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS) +int zmk_split_central_update_layers(uint32_t new_layers) { + if (!active_transport || !active_transport->api || + !active_transport->api->get_available_source_ids || !active_transport->api->send_command) { + return -ENODEV; + } + + uint8_t source_ids[ZMK_SPLIT_CENTRAL_PERIPHERAL_COUNT]; + + int ret = active_transport->api->get_available_source_ids(source_ids); + + if (ret < 0) { + return ret; + } + + struct zmk_split_transport_central_command command = + (struct zmk_split_transport_central_command){ + .type = ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_SET_RGB_LAYERS, + .data = + { + .set_rgb_layers = + { + .layers = new_layers, + }, + }, + }; + + for (size_t i = 0; i < ret; i++) { + ret = active_transport->api->send_command(source_ids[i], command); + if (ret < 0) { + return ret; + } + } + + return 0; +} + #if IS_ENABLED(CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING) int zmk_split_central_get_peripheral_battery_level(uint8_t source, uint8_t *level) { diff --git a/app/src/split/peripheral.c b/app/src/split/peripheral.c index 4157ffa2205..527e0dac0a2 100644 --- a/app/src/split/peripheral.c +++ b/app/src/split/peripheral.c @@ -11,12 +11,18 @@ #include #include +#include #include #include #include #include +#if IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS) +#include +#endif +#include + #include #include @@ -50,6 +56,20 @@ int zmk_split_transport_peripheral_command_handler( if (err) { LOG_ERR("Failed to invoke behavior %s: %d", binding.behavior_dev, err); } + return err; + } + case ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_SET_PHYSICAL_LAYOUT: { + return zmk_physical_layouts_select(cmd.data.set_physical_layout.layout_idx); + } +#if IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS) + case ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_SET_HID_INDICATORS: { + return raise_zmk_hid_indicators_changed((struct zmk_hid_indicators_changed){ + .indicators = cmd.data.set_hid_indicators.indicators}); + } +#endif + case ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_SET_RGB_LAYERS: { + return raise_zmk_split_peripheral_layer_changed( + (struct zmk_split_peripheral_layer_changed){.layers = cmd.data.set_rgb_layers.layers}); } default: LOG_WRN("Unhandled command type %d", cmd.type); diff --git a/app/src/split/peripheral_layers.c b/app/src/split/peripheral_layers.c new file mode 100644 index 00000000000..c150bc4a952 --- /dev/null +++ b/app/src/split/peripheral_layers.c @@ -0,0 +1,27 @@ + +#include +#include + +#include +#include + +static uint32_t peripheral_layers = 0; + +void set_peripheral_layers_state(uint32_t new_layers) { peripheral_layers = new_layers; } + +uint32_t peripheral_layers_state(void) { return peripheral_layers; } + +bool peripheral_layer_active(uint8_t layer) { + return (peripheral_layers & (BIT(layer))) == (BIT(layer)); +}; + +uint8_t peripheral_highest_layer_active(void) { + if (peripheral_layers > 0) { + for (uint8_t layer = ZMK_KEYMAP_LAYERS_LEN - 1; layer > 0; layer--) { + if ((peripheral_layers & (BIT(layer))) == (BIT(layer)) || layer == 0) { + return layer; + } + } + } + return 0; +} diff --git a/app/src/split/wired/central.c b/app/src/split/wired/central.c index 157092f7836..ac26f06a43b 100644 --- a/app/src/split/wired/central.c +++ b/app/src/split/wired/central.c @@ -185,6 +185,8 @@ static ssize_t get_payload_data_size(const struct zmk_split_transport_central_co return sizeof(cmd->data.set_physical_layout); case ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_SET_HID_INDICATORS: return sizeof(cmd->data.set_hid_indicators); + case ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_SET_RGB_LAYERS: + return sizeof(cmd->data.set_rgb_layers); default: return -ENOTSUP; } diff --git a/default.nix b/default.nix index 57d15753a44..78746af8e53 100644 --- a/default.nix +++ b/default.nix @@ -43,6 +43,16 @@ makeScope newScope (self: with self; { glove80_combined = combine_uf2 glove80_left glove80_right "glove80"; + go60_left = zmk.override { + board = "go60_lh"; + }; + + go60_right = zmk.override { + board = "go60_rh"; + }; + + go60_combined = combine_uf2 go60_left go60_right "go60"; + glove80_v0_left = zmk.override { board = "glove80_v0_lh"; }; diff --git a/lambda/api_version.txt b/lambda/api_version.txt index 00750edc07d..b8626c4cff2 100644 --- a/lambda/api_version.txt +++ b/lambda/api_version.txt @@ -1 +1 @@ -3 +4 diff --git a/lambda/app.rb b/lambda/app.rb index 7318124a6ef..9da78e7a400 100644 --- a/lambda/app.rb +++ b/lambda/app.rb @@ -3,60 +3,61 @@ require 'stringio' require 'digest' require 'json' +require './param_parser' +require './param_serializer' require './compiler' module LambdaFunction # Handle a non-HTTP compile request, returning a JSON body of either the # compiled result or an error. class Handler + include ParamParser + REVISION = ENV.fetch('REVISION', 'unknown') def self.process(event:, context:) return { type: 'keep_alive' } if event.has_key?('keep_alive') - parse_base64_param = ->(param, required: true) do - if event.include?(param) - Base64.strict_decode64(event.fetch(param)) - elsif required - return error(status: 400, message: "Missing required argument: #{param}") - end - rescue ArgumentError - return error(status: 400, message: "Invalid Base64 in #{param} input") - end + self.new(event).process + end - board = event.fetch('board', 'glove80') - keymap_data = parse_base64_param.('keymap') - kconfig_data = parse_base64_param.('kconfig', required: false) + attr_reader :params + def initialize(event) + @params = event + end - # Including kconfig settings that affect the RHS require building both - # firmware images, doubling compile time. Clients should omit rhs_kconfig - # where possible. - rhs_kconfig_data = parse_base64_param.('rhs_kconfig', required: false) + def process + board = parse_param('board', with: ParamSerializer::String, default: 'glove80') + keymap_data = parse_param('keymap', with: ParamSerializer::Base64) + snippets = parse_array_param('snippets', with: ParamSerializer::String, default: []) + lhs_kconfig_data = parse_param('kconfig', with: ParamSerializer::Base64, default: nil) + rhs_kconfig_data = parse_param('rhs_kconfig', with: ParamSerializer::Base64, default: nil) result, log = begin - log_compile(board, keymap_data, kconfig_data, rhs_kconfig_data) - - Compiler.new.compile(board, keymap_data, kconfig_data, rhs_kconfig_data) + log_compile(board, keymap_data, lhs_kconfig_data, rhs_kconfig_data, snippets) + Compiler.new.compile(board, keymap_data, lhs_kconfig_data, rhs_kconfig_data, snippets) rescue Compiler::CompileError => e - return error(status: e.status, message: e.message, detail: e.log) + return error(status: e.status, message: e.message, detail: e.log, exception: e) end result = Base64.strict_encode64(result) { type: 'result', result: result, log: log, revision: REVISION } + rescue ParamParser::ParseError => e + error(status: 400, message: "Error parsing #{e.param}", detail: [e.message], exception: e) rescue StandardError => e error(status: 500, message: "Unexpected error: #{e.class}", detail: [e.message], exception: e) end - def self.log_compile(board, keymap_data, kconfig_data, rhs_kconfig_data) + def log_compile(board, keymap_data, kconfig_data, rhs_kconfig_data, snippets) keymap = Digest::SHA1.base64digest(keymap_data) kconfig = kconfig_data ? Digest::SHA1.base64digest(kconfig_data) : 'nil' rhs_kconfig = rhs_kconfig_data ? Digest::SHA1.base64digest(rhs_kconfig_data) : 'nil' - puts("Compiling with board: #{board}; keymap: #{keymap}; kconfig: #{kconfig}; rhs_kconfig: #{rhs_kconfig}") + puts("Compiling with board: #{board}; keymap: #{keymap}; kconfig: #{kconfig}; rhs_kconfig: #{rhs_kconfig}; snippets: #{snippets.inspect}") end - def self.error(status:, message:, detail: nil, exception: nil) + def error(status:, message:, detail: nil, exception: nil) reported_error = { type: 'error', status:, message:, detail:, revision: REVISION } exception_detail = { class: exception.class, backtrace: exception.backtrace } if exception diff --git a/lambda/compiler.rb b/lambda/compiler.rb index fe9e91f2a5b..4cb7fbbde8b 100644 --- a/lambda/compiler.rb +++ b/lambda/compiler.rb @@ -18,13 +18,13 @@ def initialize(message, status: 400, log:) end end - def compile(board, keymap_data, lhs_kconfig_data, rhs_kconfig_data) + def compile(board, keymap_data, lhs_kconfig_data, rhs_kconfig_data, snippets) board_lh = board + '_lh' board_rh = board + '_rh' - if rhs_kconfig_data && !rhs_kconfig_data.empty? - lhs_result, lhs_output = compile_board(board_lh, keymap_data:, kconfig_data: lhs_kconfig_data, include_static_rhs: false) - rhs_result, rhs_output = compile_board(board_rh, keymap_data: nil, kconfig_data: rhs_kconfig_data, include_static_rhs: false) + if rhs_kconfig_data || !snippets.empty? + lhs_result, lhs_output = compile_board(board_lh, keymap_data:, kconfig_data: lhs_kconfig_data, snippets:, include_static_rhs: false) + rhs_result, rhs_output = compile_board(board_rh, keymap_data:, kconfig_data: rhs_kconfig_data, snippets:, include_static_rhs: false) [ lhs_result.concat(rhs_result), ["LHS Output:", *lhs_output, "RHS Output:", *rhs_output], @@ -34,12 +34,13 @@ def compile(board, keymap_data, lhs_kconfig_data, rhs_kconfig_data) end end - def compile_board(board, keymap_data:, kconfig_data:, include_static_rhs: false) + def compile_board(board, keymap_data:, kconfig_data:, snippets: [], include_static_rhs: false) in_build_dir do compile_command = ['compileZmk', '-b', board] if keymap_data dts_parse_errors = validate_devicetree!(keymap_data) + File.open('build.keymap', 'w') { |io| io.write(keymap_data) } compile_command << '-k' << './build.keymap' end @@ -51,10 +52,14 @@ def compile_board(board, keymap_data:, kconfig_data:, include_static_rhs: false) # If requesting USB logging, include the corresponding snippet that also # enables necessary devicetree nodes if kconfig_data =~ /CONFIG_ZMK_USB_LOGGING\s*=\s*y/ - compile_command << '-s' << 'zmk-usb-logging' + snippets += ['zmk-usb-logging'] end end + unless snippets.empty? + compile_command << '-s' << snippets.uniq.join(';') + end + if include_static_rhs # Concatenate the pre-compiled glove80_rh image to the resulting uf2 compile_command << '-m' @@ -91,7 +96,8 @@ def compile_board(board, keymap_data:, kconfig_data:, include_static_rhs: false) end PERMITTED_DTS_SECTIONS = %w[ - behaviors macros combos conditional_layers keymap underglow-indicators + behaviors macros combos conditional_layers keymap + underglow-indicators underglow-layer cirque_lh_listener cirque_rh_listener input_processors mkp_input_listener mmv_input_listener msc_input_listener @@ -105,6 +111,7 @@ def compile_board(board, keymap_data:, kconfig_data:, include_static_rhs: false) /dts-v1/; / { underglow_indicators: underglow-indicators {}; + underglow_layer: underglow-layer {}; cirque_lh_listener: cirque_lh_listener {}; cirque_rh_listener: cirque_rh_listener {}; }; diff --git a/lambda/param_parser.rb b/lambda/param_parser.rb new file mode 100644 index 00000000000..afca34b8165 --- /dev/null +++ b/lambda/param_parser.rb @@ -0,0 +1,143 @@ +# frozen_string_literal: true + +# Copyright 2016 DMM.com LLC + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the “Software”), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +require './param_serializer' + +module ParamParser + class ParseError < RuntimeError + attr_accessor :param, :value + + def initialize(message, param, value) + super(message) + self.param = param + self.value = value + end + end + + PARAM_REQUIRED = Object.new + BLANK = Object.new + + # Parse the specified parameter, optionally deserializing with the provided + # ParamSerializer. If the parameter is missing and no default is + # provided, raises a ParseError. + # + # If `BLANK` is provided as a default, return a placeholder object that can be + # later stripped out with `remove_blanks` + # + # If `dump` is true, use the serializer to re-serialize any successfully + # parsed argument back to a canonical string. This can be useful to validate + # and normalize the input to another service without parsing it. A serializer + # must be passed to use this option. + def parse_param(param, with: nil, default: PARAM_REQUIRED, dump: false) + serializer = + case with + when String, Symbol + ParamSerializer.for!(with) + else + with + end + + parse = + if !params.has_key?(param) + raise ParseError.new("Required parameter '#{param}' missing", param, nil) if default == PARAM_REQUIRED + default + else + val = params[param] + if !serializer.nil? + begin + serializer.load(val) + rescue ParamSerializer::LoadError => ex + raise ParseError.new("Invalid parameter '#{param}': '#{val.inspect}' - #{ex.message}", param, val) + end + else + val + end + end + + if dump && parse != BLANK + begin + parse = serializer.dump(parse) + rescue NoMethodError => ex + raise ParseError.new("Serializer '#{serializer}' can't dump param '#{param}' #{val.inspect} - #{ex.message}", param, val) + end + end + + parse + end + + # Parse an array-typed param using the provided serializer for each member element. + def parse_array_param(param, with: nil, default: PARAM_REQUIRED, dump: false) + serializer = + case with + when String, Symbol + ParamSerializer.for!(with) + else + with + end + + vals = params[param] + + parses = + if vals.nil? + raise ParseError.new("Required parameter '#{param}' missing", param, nil) if default == PARAM_REQUIRED + default + elsif !vals.is_a?(Array) + raise ParseError.new("Invalid type for parameter '#{param}': '#{vals.class.name}'", param, vals) + elsif !serializer.nil? + vals.map do |val| + begin + serializer.load(val) + rescue ParamSerializer::LoadError => ex + raise ParseError.new("Invalid member in array parameter '#{param}': '#{val.inspect}' - #{ex.message}", param, val) + end + end + else + vals + end + + if dump && parses != BLANK + parses.map! { |v| serializer.dump(v) } + end + + parses + end + + # Convenience method to make it simpler to build a hash structure with + # optional members from parsed data. This method recursively traverses the + # provided structure and removes any instances of the sentinel value + # Parser::BLANK. + def remove_blanks(arg) + case arg + when Hash + arg.each do |k, v| + if v == BLANK + arg.delete(k) + else + remove_blanks(v) + end + end + when Array + arg.delete(BLANK) + arg.each { |e| remove_blanks(e) } + end + end +end diff --git a/lambda/param_serializer.rb b/lambda/param_serializer.rb new file mode 100644 index 00000000000..6042c0b5ca4 --- /dev/null +++ b/lambda/param_serializer.rb @@ -0,0 +1,290 @@ +# frozen_string_literal: true + +# Copyright 2016 DMM.com LLC + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the “Software”), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +require 'json' +require 'date' +require 'digest' + +class ParamSerializer + class LoadError < ArgumentError; end + class DumpError < ArgumentError; end + + attr_reader :clazz + + def initialize(clazz) + @clazz = clazz + end + + def dump(val, json: false) + matches_type!(val) + if json && self.class.json_value? + val + else + val.to_s + end + end + + def load(_val) + raise StandardError.new('unimplemented') + end + + def matches_type?(val) + val.is_a?(clazz) + end + + def matches_type!(val, err: DumpError) + unless matches_type?(val) + raise err.new("Incorrect type for #{self.class.name}: #{val.inspect}:#{val.class.name}") + end + true + end + + @registry = {} + class << self + def load(...) + singleton.load(...) + end + + def dump(...) + singleton.dump(...) + end + + def matches_type?(...) + singleton.matches_type?(...) + end + + def matches_type!(...) + singleton.matches_type!(...) + end + + def singleton + raise ArgumentError.new("Singleton instance not defined for abstract serializer '#{self.name}'") + end + + def json_value? + false + end + + def for(name) + @registry[name.to_s] + end + + def for!(name) + s = self.for(name) + raise ArgumentError.new("No serializer registered with name: '#{name}'") if s.nil? + s + end + + private + + def set_singleton! + instance = self.new + define_singleton_method(:singleton) { instance } + end + + def json_value! + define_singleton_method(:json_value?) { true } + end + end + + class String < ParamSerializer + def initialize + super(::String) + end + + def load(str) + matches_type!(str, err: LoadError) + str + end + + set_singleton! + json_value! + end + + class Integer < ParamSerializer + def initialize + super(::Integer) + end + + # JSON only supports floats, so we have to accept a value + # which may have already been parsed into a Ruby Float or Integer. + def load(str_or_num) + raise LoadError.new("Invalid integer: #{str_or_num}") unless [::String, ::Integer].any? { |t| str_or_num.is_a?(t) } + Integer(str_or_num) + rescue ArgumentError => e + raise LoadError.new(e.message) + end + + set_singleton! + json_value! + end + + class Float < ParamSerializer + def initialize + super(::Float) + end + + def load(str) + Float(str) + rescue TypeError, ArgumentError => _e + raise LoadError.new("Invalid type for conversion to Float") + end + + set_singleton! + json_value! + end + + class Boolean < ParamSerializer + def initialize + super(nil) + end + + def load(str) + str = str.downcase if str.is_a?(::String) + + if ['false', 'no', 'off', false, '0', 0].include?(str) + false + elsif ['true', 'yes', 'on', true, '1', 1].include?(str) + true + else + raise LoadError.new("Invalid boolean: #{str.inspect}") + end + end + + def matches_type?(val) + [true, false].include?(val) + end + + set_singleton! + json_value! + end + + class Numeric < ParamSerializer + def initialize + super(::Numeric) + end + + def load(str) + Float(str) + rescue TypeError, ArgumentError => _e + raise LoadError.new("Invalid type for conversion to Numeric") + end + + set_singleton! + json_value! + end + + # Abstract serializer for ISO8601 dates and times + class ISO8601 < ParamSerializer + def load(str) + raise TypeError.new unless str.is_a?(::String) + + clazz.parse(str) + rescue TypeError, ArgumentError => _e + raise LoadError.new("Invalid type for conversion to #{clazz}") + end + + def dump(val, json: nil) + matches_type!(val) + val.iso8601 + end + end + + class Date < ISO8601 + def initialize + super(::Date) + end + + set_singleton! + end + + class Time < ISO8601 + def initialize + super(::Time) + end + + set_singleton! + end + + class UUID < ParamSerializer::String + def load(str) + matches_type!(str, err: LoadError) + super + end + + def matches_type?(str) + super && /[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}/i.match?(str) + end + + set_singleton! + json_value! + end + + # Abstract serializer for members of a fixed set of lowercase strings, + # case-normalized on parse. + class StringEnum < ParamSerializer + def initialize(*members) + @member_set = members.map { |s| normalize(s) }.to_set.freeze + super(nil) + end + + def load(str) + val = normalize(str.to_s) + matches_type!(val, err: LoadError) + val + end + + def matches_type?(str) + str.is_a?(::String) && @member_set.include?(str) + end + + def normalize(str) + str.downcase + end + end + + class CaseSensitiveStringEnum < StringEnum + def normalize(str) + str + end + end + + class Base64 < ParamSerializer + def initialize + super(::String) + end + + def load(base64) + ::Base64.strict_decode64(base64) + rescue ArgumentError => _e + raise LoadError.new('Invalid Base64') + end + + def dump(str, json: nil) + matches_type!(str) + ::Base64.strict_encode64(str) + end + + set_singleton! + json_value! + end +end diff --git a/release.nix b/release.nix index b1b1c39967e..8af9b5a6dfb 100644 --- a/release.nix +++ b/release.nix @@ -67,6 +67,7 @@ let }; }; zmk_glove80_rh = zmk.override { board = "glove80_rh"; }; + zmk_go60_rh = zmk.override { board = "go60_rh"; }; realpath_coreutils = if pkgs.stdenv.isDarwin then pkgs.coreutils else pkgs.busybox; in pkgs.writeShellScriptBin "compileZmk" '' set -eo pipefail @@ -122,6 +123,9 @@ let glove80_lh) merge_rhs_firmware="${zmk_glove80_rh}/zmk.uf2" ;; + go60_lh) + merge_rhs_firmware="${zmk_go60_rh}/zmk.uf2" + ;; *) echo "No pre-built RHS exists to merge with specified board '$board'" >&2 exit 2 @@ -195,6 +199,18 @@ let cd /tmp/build compileZmk -b glove80_rh -k ${zmk.src}/app/boards/arm/glove80/glove80.keymap + + rm -fr /tmp/build + mkdir /tmp/build + cd /tmp/build + + compileZmk -b go60_lh -k ${zmk.src}/app/boards/arm/go60/go60.keymap + + rm -fr /tmp/build + mkdir /tmp/build + cd /tmp/build + + compileZmk -b go60_rh -k ${zmk.src}/app/boards/arm/go60/go60.keymap ''; entrypoint = pkgs.writeShellScriptBin "entrypoint" ''