Refactor steno and add STENO_PROTOCOL = [all|txbolt|geminipr] (#17065)

* Refactor steno into STENO_ENABLE_[ALL|GEMINI|BOLT]

* Update stenography documentation

* STENO_ENABLE_TXBOLT → STENO_ENABLE_BOLT

TXBOLT is a better name but BOLT is more consistent with the
pre-existing TX Bolt related constants, which all drop the "TX " prefix

* Comments

* STENO_ENABLE_[GEMINI|BOLT|ALL] → STENO_PROTOCOL = [geminipr|txbolt|all]

* Add note on lacking V-USB support

* Clear chord at the end of the switch(mode){send_steno_chord} block

* Return true if NOEVENT

* update_chord_xxx → add_xxx_key_to_chord

* Enable the defines for all the protocols if STENO_PROTOCOL = all

* Mention how to use `steno_set_mode`

* Set the default steno protocol to "all"

This is done so that existing keymaps invoking `steno_set_mode` don't
all suddenly break

* Add data driver equivalents for stenography feature

* Document format of serial steno packets

(Thanks dnaq)

* Add missing comma
This commit is contained in:
precondition 2022-06-23 20:43:24 +02:00 committed by GitHub
parent 2239527871
commit 7060cb7b26
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 353 additions and 183 deletions

View File

@ -92,10 +92,29 @@ ifeq ($(MUSIC_ENABLE), yes)
SRC += $(QUANTUM_DIR)/process_keycode/process_music.c SRC += $(QUANTUM_DIR)/process_keycode/process_music.c
endif endif
VALID_STENO_PROTOCOL_TYPES := geminipr txbolt all
STENO_PROTOCOL ?= all
ifeq ($(strip $(STENO_ENABLE)), yes) ifeq ($(strip $(STENO_ENABLE)), yes)
OPT_DEFS += -DSTENO_ENABLE ifeq ($(filter $(STENO_PROTOCOL),$(VALID_STENO_PROTOCOL_TYPES)),)
VIRTSER_ENABLE ?= yes $(call CATASTROPHIC_ERROR,Invalid STENO_PROTOCOL,STENO_PROTOCOL="$(STENO_PROTOCOL)" is not a valid stenography protocol)
SRC += $(QUANTUM_DIR)/process_keycode/process_steno.c else
OPT_DEFS += -DSTENO_ENABLE
VIRTSER_ENABLE ?= yes
ifeq ($(strip $(STENO_PROTOCOL)), geminipr)
OPT_DEFS += -DSTENO_ENABLE_GEMINI
endif
ifeq ($(strip $(STENO_PROTOCOL)), txbolt)
OPT_DEFS += -DSTENO_ENABLE_BOLT
endif
ifeq ($(strip $(STENO_PROTOCOL)), all)
OPT_DEFS += -DSTENO_ENABLE_ALL
OPT_DEFS += -DSTENO_ENABLE_GEMINI
OPT_DEFS += -DSTENO_ENABLE_BOLT
endif
SRC += $(QUANTUM_DIR)/process_keycode/process_steno.c
endif
endif endif
ifeq ($(strip $(VIRTSER_ENABLE)), yes) ifeq ($(strip $(VIRTSER_ENABLE)), yes)

View File

@ -45,6 +45,7 @@ OTHER_OPTION_NAMES = \
LEADER_ENABLE \ LEADER_ENABLE \
PRINTING_ENABLE \ PRINTING_ENABLE \
STENO_ENABLE \ STENO_ENABLE \
STENO_PROTOCOL \
TAP_DANCE_ENABLE \ TAP_DANCE_ENABLE \
VIRTSER_ENABLE \ VIRTSER_ENABLE \
OLED_ENABLE \ OLED_ENABLE \

View File

@ -28,6 +28,8 @@
"SPLIT_KEYBOARD": {"info_key": "split.enabled", "value_type": "bool"}, "SPLIT_KEYBOARD": {"info_key": "split.enabled", "value_type": "bool"},
"SPLIT_TRANSPORT": {"info_key": "split.transport.protocol", "to_c": false}, "SPLIT_TRANSPORT": {"info_key": "split.transport.protocol", "to_c": false},
"WAIT_FOR_USB": {"info_key": "usb.wait_for", "value_type": "bool"}, "WAIT_FOR_USB": {"info_key": "usb.wait_for", "value_type": "bool"},
"STENO_ENABLE": {"info_key": "stenography.enabled", "value_type": "bool"},
"STENO_PROTOCOL": {"info_key": "stenography.protocol"},
# Items we want flagged in lint # Items we want flagged in lint
"CTPC": {"info_key": "_deprecated.ctpc", "deprecated": true}, "CTPC": {"info_key": "_deprecated.ctpc", "deprecated": true},

View File

@ -361,6 +361,17 @@
} }
} }
}, },
"stenography": {
"type": "object",
"additionalProperties": false,
"properties": {
"enabled": {"type": "boolean"},
"protocol": {
"type": "string",
"enum": ["all", "geminipr", "txbolt"]
}
}
},
"split": { "split": {
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,

View File

@ -8,46 +8,107 @@ The [Open Steno Project](https://www.openstenoproject.org/) has built an open-so
Plover can work with any standard QWERTY keyboard, although it is more efficient if the keyboard supports NKRO (n-key rollover) to allow Plover to see all the pressed keys at once. An example keymap for Plover can be found in `planck/keymaps/default`. Switching to the `PLOVER` layer adjusts the position of the keyboard to support the number bar. Plover can work with any standard QWERTY keyboard, although it is more efficient if the keyboard supports NKRO (n-key rollover) to allow Plover to see all the pressed keys at once. An example keymap for Plover can be found in `planck/keymaps/default`. Switching to the `PLOVER` layer adjusts the position of the keyboard to support the number bar.
To use Plover with QMK just enable NKRO and optionally adjust your layout if you have anything other than a standard layout. You may also want to purchase some steno-friendly keycaps to make it easier to hit multiple keys. To enable NKRO, add `NKRO_ENABLE = yes` in your `rules.mk` and make sure to press `NK_ON` to turn it on because `NKRO_ENABLE = yes` merely adds the possibility of switching to NKRO mode but it doesn't automatically switch to it. If you want to automatically switch, add `#define FORCE_NKRO` in your `config.h`.
You may also need to adjust your layout, either in QMK or in Plover, if you have anything other than a standard layout. You may also want to purchase some steno-friendly keycaps to make it easier to hit multiple keys.
## Plover with Steno Protocol :id=plover-with-steno-protocol ## Plover with Steno Protocol :id=plover-with-steno-protocol
Plover also understands the language of several steno machines. QMK can speak a couple of these languages, TX Bolt and GeminiPR. An example layout can be found in `planck/keymaps/steno`. Plover also understands the language of several steno machines. QMK can speak a couple of these languages: TX Bolt and GeminiPR. An example layout can be found in `planck/keymaps/steno`.
When QMK speaks to Plover over a steno protocol Plover will not use the keyboard as input. This means that you can switch back and forth between a standard keyboard and your steno keyboard, or even switch layers from Plover to standard and back without needing to activate/deactivate Plover. When QMK speaks to Plover over a steno protocol, Plover will not use the keyboard as input. This means that you can switch back and forth between a standard keyboard and your steno keyboard, or even switch layers from Plover to standard and back without needing to activate/deactivate Plover.
In this mode Plover expects to speak with a steno machine over a serial port so QMK will present itself to the operating system as a virtual serial port in addition to a keyboard. By default QMK will speak the TX Bolt protocol but can be switched to GeminiPR; the last protocol used is stored in non-volatile memory so QMK will use the same protocol on restart. In this mode, Plover expects to speak with a steno machine over a serial port so QMK will present itself to the operating system as a virtual serial port in addition to a keyboard.
> Note: Due to hardware limitations you may not be able to run both a virtual serial port and mouse emulation at the same time. > Note: Due to hardware limitations, you might not be able to run both a virtual serial port and mouse emulation at the same time.
!> Serial stenography protocols are not supported on [V-USB keyboards](compatible_microcontrollers#atmel-avr).
To enable stenography protocols, add the following lines to your `rules.mk`:
```mk
STENO_ENABLE = yes
```
### TX Bolt :id=tx-bolt ### TX Bolt :id=tx-bolt
TX Bolt communicates the status of 24 keys over a very simple protocol in variable-sized (1-5 byte) packets. TX Bolt communicates the status of 24 keys over a simple protocol in variable-sized (1–4 bytes) packets.
To select TX Bolt, add the following lines to your `rules.mk`:
```mk
STENO_ENABLE = yes
STENO_PROTOCOL = txbolt
```
Each byte of the packet represents a different group of steno keys. Determining the group of a certain byte of the packet is done by checking the first two bits, the remaining bits are set if the corresponding steno key was pressed for the stroke. The last set of keys (as indicated by leading `11`) needs to keep track of less keys than there are bits so one of the bits is constantly 0.
The start of a new packet can be detected by comparing the group “ID” (the two MSBs) of the current byte to that of the previously received byte. If the group “ID” of the current byte is smaller or equal to that of the previous byte, it means that the current byte is the beginning of a new packet.
The format of TX Bolt packets is shown below.
```
00HWPKTS 01UE*OAR 10GLBPRF 110#ZDST
```
Examples of steno strokes and the associated packet:
- `EUBG` = `01110000 10101000`
- `WAZ` = `00010000 01000010 11001000`
- `PHAPBGS` = `00101000 01000010 10101100 11000010`
### GeminiPR :id=geminipr ### GeminiPR :id=geminipr
GeminiPR encodes 42 keys into a 6-byte packet. While TX Bolt contains everything that is necessary for standard stenography, GeminiPR opens up many more options, including supporting non-English theories. GeminiPR encodes 42 keys into a 6-byte packet. While TX Bolt contains everything that is necessary for standard stenography, GeminiPR opens up many more options, including differentiating between top and bottom `S-`, and supporting non-English theories.
To select GeminiPR, add the following lines to your `rules.mk`:
```mk
STENO_ENABLE = yes
STENO_PROTOCOL = geminipr
```
All packets in the GeminiPR protocol consist of exactly six bytes, used as bit-arrays for different groups of keys. The beginning of a packet is indicated by setting the most significant bit (MSB) to 1 while setting the MSB of the remaining five bytes to 0.
The format of GeminiPR packets is shown below.
```
1 Fn #1 #2 #3 #4 #5 #6
0 S1- S2- T- K- P- W- H-
0 R- A- O- *1 *2 res1 res2
0 pwr *3 *4 -E -U -F -R
0 -P -B -L -G -T -S -D
0 #7 #8 #9 #A #B #C -Z
```
Examples of steno strokes and the associated packet:
- `EUBG` = `10000000 00000000 00000000 00001100 00101000 00000000`
- `WAZ` = `10000000 00000010 00100000 00000000 00000000 00000001`
- `PHAPBGS` = `10000000 00000101 00100000 00000000 01101010 00000000`
### Switching protocols on the fly :id=switching-protocols-on-the-fly
If you wish to switch the serial protocol used to transfer the steno chords without having to recompile your keyboard firmware every time, you can press the `QK_STENO_BOLT` and `QK_STENO_GEMINI` keycodes in order to switch protocols on the fly.
To enable these special keycodes, add the following lines to your `rules.mk`:
```mk
STENO_ENABLE = yes
STENO_PROTOCOL = all
```
If you want to switch protocols programatically, as part of a custom macro for example, don't use `tap_code(QK_STENO_*)`, as `tap_code` only supports [basic keycodes](keycodes_basic). Instead, you should use `steno_set_mode(STENO_MODE_*)`, whose valid arguments are `STENO_MODE_BOLT` and `STENO_MODE_GEMINI`.
The default protocol is Gemini PR but the last protocol used is stored in non-volatile memory so QMK will remember your choice between reboots of your keyboard — assuming that your keyboard features (emulated) EEPROM.
Naturally, this option takes the most amount of firmware space as it needs to compile the code for all the available stenography protocols. In most cases, compiling a single stenography protocol is sufficient.
The default value for `STENO_PROTOCOL` is `all`.
## Configuring QMK for Steno :id=configuring-qmk-for-steno ## Configuring QMK for Steno :id=configuring-qmk-for-steno
Firstly, enable steno in your keymap's Makefile. You may also need disable mousekeys, extra keys, or another USB endpoint to prevent conflicts. The builtin USB stack for some processors only supports a certain number of USB endpoints and the virtual serial port needed for steno fills 3 of them. After enabling stenography and optionally selecting a protocol, you may also need disable mouse keys, extra keys, or another USB endpoint to prevent conflicts. The builtin USB stack for some processors only supports a certain number of USB endpoints and the virtual serial port needed for steno fills 3 of them.
```make !> If you had *explicitly* set `VIRSTER_ENABLE = no`, none of the serial stenography protocols (GeminiPR, TX Bolt) will work properly. You are expected to either set it to `yes`, remove the line from your `rules.mk` or send the steno chords yourself in an alternative way using the [provided interceptable hooks](#interfacing-with-the-code).
STENO_ENABLE = yes
MOUSEKEY_ENABLE = no
```
In your keymap create a new layer for Plover. You will need to include `keymap_steno.h`. See `planck/keymaps/steno/keymap.c` for an example. Remember to create a key to switch to the layer as well as a key for exiting the layer. If you would like to switch modes on the fly you can use the keycodes `QK_STENO_BOLT` and `QK_STENO_GEMINI`. If you only want to use one of the protocols you may set it up in your initialization function: In your keymap, create a new layer for Plover, that you can fill in with the [steno keycodes](#keycode-reference) (you will need to include `keymap_steno.h`, see `planck/keymaps/steno/keymap.c` for an example). Remember to create a key to switch to the layer as well as a key for exiting the layer.
```c Once you have your keyboard flashed, launch Plover. Click the 'Configure...' button. In the 'Machine' tab, select the Stenotype Machine that corresponds to your desired protocol. Click the 'Configure...' button on this tab and enter the serial port or click 'Scan'. Baud rate is fine at 9600 (although you should be able to set as high as 115200 with no issues). Use the default settings for everything else (Data Bits: 8, Stop Bits: 1, Parity: N, no flow control).
void eeconfig_init_user() {
steno_set_mode(STENO_MODE_GEMINI); // or STENO_MODE_BOLT
}
```
Once you have your keyboard flashed launch Plover. Click the 'Configure...' button. In the 'Machine' tab select the Stenotype Machine that corresponds to your desired protocol. Click the 'Configure...' button on this tab and enter the serial port or click 'Scan'. Baud rate is fine at 9600 (although you should be able to set as high as 115200 with no issues). Use the default settings for everything else (Data Bits: 8, Stop Bits: 1, Parity: N, no flow control). To test your keymap, you can chord keys on your keyboard and either look at the output of the 'paper tape' (Tools > Paper Tape) or that of the 'layout display' (Tools > Layout Display). If your strokes correctly show up, you are now ready to steno!
On the display tab click 'Open stroke display'. With Plover disabled you should be able to hit keys on your keyboard and see them show up in the stroke display window. Use this to make sure you have set up your keymap correctly. You are now ready to steno!
## Learning Stenography :id=learning-stenography ## Learning Stenography :id=learning-stenography
@ -60,7 +121,7 @@ On the display tab click 'Open stroke display'. With Plover disabled you should
The steno code has three interceptable hooks. If you define these functions, they will be called at certain points in processing; if they return true, processing continues, otherwise it's assumed you handled things. The steno code has three interceptable hooks. If you define these functions, they will be called at certain points in processing; if they return true, processing continues, otherwise it's assumed you handled things.
```c ```c
bool send_steno_chord_user(steno_mode_t mode, uint8_t chord[6]); bool send_steno_chord_user(steno_mode_t mode, uint8_t chord[MAX_STROKE_SIZE]);
``` ```
This function is called when a chord is about to be sent. Mode will be one of `STENO_MODE_BOLT` or `STENO_MODE_GEMINI`. This represents the actual chord that would be sent via whichever protocol. You can modify the chord provided to alter what gets sent. Remember to return true if you want the regular sending process to happen. This function is called when a chord is about to be sent. Mode will be one of `STENO_MODE_BOLT` or `STENO_MODE_GEMINI`. This represents the actual chord that would be sent via whichever protocol. You can modify the chord provided to alter what gets sent. Remember to return true if you want the regular sending process to happen.
@ -72,15 +133,23 @@ bool process_steno_user(uint16_t keycode, keyrecord_t *record) { return true; }
This function is called when a keypress has come in, before it is processed. The keycode should be one of `QK_STENO_BOLT`, `QK_STENO_GEMINI`, or one of the `STN_*` key values. This function is called when a keypress has come in, before it is processed. The keycode should be one of `QK_STENO_BOLT`, `QK_STENO_GEMINI`, or one of the `STN_*` key values.
```c ```c
bool postprocess_steno_user(uint16_t keycode, keyrecord_t *record, steno_mode_t mode, uint8_t chord[6], int8_t pressed); bool postprocess_steno_user(uint16_t keycode, keyrecord_t *record, steno_mode_t mode, uint8_t chord[MAX_STROKE_SIZE], int8_t n_pressed_keys);
``` ```
This function is called after a key has been processed, but before any decision about whether or not to send a chord. If `IS_PRESSED(record->event)` is false, and `pressed` is 0 or 1, the chord will be sent shortly, but has not yet been sent. This is where to put hooks for things like, say, live displays of steno chords or keys. This function is called after a key has been processed, but before any decision about whether or not to send a chord. This is where to put hooks for things like, say, live displays of steno chords or keys.
If `IS_PRESSED(record->event)` is false, and `n_pressed_keys` is 0 or 1, the chord will be sent shortly, but has not yet been sent. This relieves you of the need of keeping track of where a packet ends and another begins.
The `chord` argument contains the packet of the current chord as specified by the protocol in use. This is *NOT* simply a list of chorded steno keys of the form `[STN_E, STN_U, STN_BR, STN_GR]`. Refer to the appropriate protocol section of this document to learn more about the format of the packets in your steno protocol/mode of choice.
The `n_pressed_keys` argument is the number of physical keys actually being held down.
This is not always equal to the number of bits set to 1 (aka the [Hamming weight](https://en.wikipedia.org/wiki/Hamming_weight)) in `chord` because it is possible to simultaneously press down four keys, then release three of those four keys and then press yet another key while the fourth finger is still holding down its key.
At the end of this scenario given as an example, `chord` would have five bits set to 1 but
`n_pressed_keys` would be set to 2 because there are only two keys currently being pressed down.
## Keycode Reference :id=keycode-reference ## Keycode Reference :id=keycode-reference
As defined in `keymap_steno.h`. You must include `keymap_steno.h` to your `keymap.c` with `#include "keymap_steno.h"` before you can use these keycodes
> Note: TX Bolt does not support the full set of keys. The TX Bolt implementation in QMK will map the GeminiPR keys to the nearest TX Bolt key so that one key map will work for both. > Note: TX Bolt does not support the full set of keys. The TX Bolt implementation in QMK will map the GeminiPR keys to the nearest TX Bolt key so that one key map will work for both.
@ -124,10 +193,10 @@ As defined in `keymap_steno.h`.
|`STN_SR`|`STN_SR`| `-S`| |`STN_SR`|`STN_SR`| `-S`|
|`STN_DR`|`STN_DR`| `-D`| |`STN_DR`|`STN_DR`| `-D`|
|`STN_ZR`|`STN_ZR`| `-Z`| |`STN_ZR`|`STN_ZR`| `-Z`|
|`STN_FN`|| (GeminiPR only)| |`STN_FN`|| (Function)|
|`STN_RES1`||(GeminiPR only)| |`STN_RES1`||(Reset 1)|
|`STN_RES2`||(GeminiPR only)| |`STN_RES2`||(Reset 2)|
|`STN_PWR`||(GeminiPR only)| |`STN_PWR`||(Power)|
If you do not want to hit two keys with one finger combined keycodes can be used. These are also defined in `keymap_steno.h`, and causes both keys to be reported as pressed or released. To use these keycodes define `STENO_COMBINEDMAP` in your `config.h` file. If you do not want to hit two keys with one finger combined keycodes can be used. These are also defined in `keymap_steno.h`, and causes both keys to be reported as pressed or released. To use these keycodes define `STENO_COMBINEDMAP` in your `config.h` file.

View File

@ -381,7 +381,7 @@ void keyboard_init(void) {
#ifdef ENCODER_ENABLE #ifdef ENCODER_ENABLE
encoder_init(); encoder_init();
#endif #endif
#ifdef STENO_ENABLE #ifdef STENO_ENABLE_ALL
steno_init(); steno_init();
#endif #endif
#ifdef POINTING_DEVICE_ENABLE #ifdef POINTING_DEVICE_ENABLE

View File

@ -89,3 +89,31 @@ enum steno_combined_keycodes {
STN_COMB_MAX = STN_EU, STN_COMB_MAX = STN_EU,
}; };
#endif #endif
#ifdef STENO_ENABLE_BOLT
// TxBolt Codes
# define TXB_NUL 0
# define TXB_S_L 0b00000001
# define TXB_T_L 0b00000010
# define TXB_K_L 0b00000100
# define TXB_P_L 0b00001000
# define TXB_W_L 0b00010000
# define TXB_H_L 0b00100000
# define TXB_R_L 0b01000001
# define TXB_A_L 0b01000010
# define TXB_O_L 0b01000100
# define TXB_STR 0b01001000
# define TXB_E_R 0b01010000
# define TXB_U_R 0b01100000
# define TXB_F_R 0b10000001
# define TXB_R_R 0b10000010
# define TXB_P_R 0b10000100
# define TXB_B_R 0b10001000
# define TXB_L_R 0b10010000
# define TXB_G_R 0b10100000
# define TXB_T_R 0b11000001
# define TXB_S_R 0b11000010
# define TXB_D_R 0b11000100
# define TXB_Z_R 0b11001000
# define TXB_NUM 0b11010000
#endif // STENO_ENABLE_BOLT

View File

@ -1,4 +1,4 @@
/* Copyright 2017 Joseph Wasson /* Copyright 2017, 2022 Joseph Wasson, Vladislav Kucheriavykh
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -15,77 +15,118 @@
*/ */
#include "process_steno.h" #include "process_steno.h"
#include "quantum_keycodes.h" #include "quantum_keycodes.h"
#include "eeprom.h"
#include "keymap_steno.h" #include "keymap_steno.h"
#include "virtser.h"
#include <string.h> #include <string.h>
#ifdef VIRTSER_ENABLE
# include "virtser.h"
#endif
#ifdef STENO_ENABLE_ALL
# include "eeprom.h"
#endif
// TxBolt Codes // All steno keys that have been pressed to form this chord,
#define TXB_NUL 0 // stored in MAX_STROKE_SIZE groups of 8-bit arrays.
#define TXB_S_L 0b00000001 static uint8_t chord[MAX_STROKE_SIZE] = {0};
#define TXB_T_L 0b00000010 // The number of physical keys actually being held down.
#define TXB_K_L 0b00000100 // This is not always equal to the number of 1 bits in `chord` because it is possible to
#define TXB_P_L 0b00001000 // simultaneously press down four keys, then release three of those four keys and then press yet
#define TXB_W_L 0b00010000 // another key while the fourth finger is still holding down its key.
#define TXB_H_L 0b00100000 // At the end of this scenario given as an example, `chord` would have five bits set to 1 but
#define TXB_R_L 0b01000001 // `n_pressed_keys` would be set to 2 because there are only two keys currently being pressed down.
#define TXB_A_L 0b01000010 static int8_t n_pressed_keys = 0;
#define TXB_O_L 0b01000100
#define TXB_STR 0b01001000
#define TXB_E_R 0b01010000
#define TXB_U_R 0b01100000
#define TXB_F_R 0b10000001
#define TXB_R_R 0b10000010
#define TXB_P_R 0b10000100
#define TXB_B_R 0b10001000
#define TXB_L_R 0b10010000
#define TXB_G_R 0b10100000
#define TXB_T_R 0b11000001
#define TXB_S_R 0b11000010
#define TXB_D_R 0b11000100
#define TXB_Z_R 0b11001000
#define TXB_NUM 0b11010000
#define TXB_GRP0 0b00000000 #ifdef STENO_ENABLE_ALL
#define TXB_GRP1 0b01000000
#define TXB_GRP2 0b10000000
#define TXB_GRP3 0b11000000
#define TXB_GRPMASK 0b11000000
#define TXB_GET_GROUP(code) ((code & TXB_GRPMASK) >> 6)
#define BOLT_STATE_SIZE 4
#define GEMINI_STATE_SIZE 6
#define MAX_STATE_SIZE GEMINI_STATE_SIZE
static uint8_t state[MAX_STATE_SIZE] = {0};
static uint8_t chord[MAX_STATE_SIZE] = {0};
static int8_t pressed = 0;
static steno_mode_t mode; static steno_mode_t mode;
#elif defined(STENO_ENABLE_GEMINI)
static const steno_mode_t mode = STENO_MODE_GEMINI;
#elif defined(STENO_ENABLE_BOLT)
static const steno_mode_t mode = STENO_MODE_BOLT;
#endif
static inline void steno_clear_chord(void) {
memset(chord, 0, sizeof(chord));
}
#ifdef STENO_ENABLE_GEMINI
# ifdef VIRTSER_ENABLE
void send_steno_chord_gemini(void) {
// Set MSB to 1 to indicate the start of packet
chord[0] |= 0x80;
for (uint8_t i = 0; i < GEMINI_STROKE_SIZE; ++i) {
virtser_send(chord[i]);
}
}
# else
# pragma message "VIRTSER_ENABLE = yes is required for Gemini PR to work properly out of the box!"
# endif // VIRTSER_ENABLE
/**
* @precondition: `key` is pressed
*/
bool add_gemini_key_to_chord(uint8_t key) {
// Although each group of the packet is 8 bits long, the MSB is reserved
// to indicate whether that byte is the first byte of the packet (MSB=1)
// or one of the remaining five bytes of the packet (MSB=0).
// As a consequence, only 7 out of the 8 bits are left to be used as a bit array
// for the steno keys of that group.
const int group_idx = key / 7;
const int intra_group_idx = key - group_idx * 7;
// The 0th steno key of the group has bit=0b01000000, the 1st has bit=0b00100000, etc.
const uint8_t bit = 1 << (6 - intra_group_idx);
chord[group_idx] |= bit;
return false;
}
#endif // STENO_ENABLE_GEMINI
#ifdef STENO_ENABLE_BOLT
# define TXB_GRP0 0b00000000
# define TXB_GRP1 0b01000000
# define TXB_GRP2 0b10000000
# define TXB_GRP3 0b11000000
# define TXB_GRPMASK 0b11000000
# define TXB_GET_GROUP(code) ((code & TXB_GRPMASK) >> 6)
static const uint8_t boltmap[64] PROGMEM = {TXB_NUL, TXB_NUM, TXB_NUM, TXB_NUM, TXB_NUM, TXB_NUM, TXB_NUM, TXB_S_L, TXB_S_L, TXB_T_L, TXB_K_L, TXB_P_L, TXB_W_L, TXB_H_L, TXB_R_L, TXB_A_L, TXB_O_L, TXB_STR, TXB_STR, TXB_NUL, TXB_NUL, TXB_NUL, TXB_STR, TXB_STR, TXB_E_R, TXB_U_R, TXB_F_R, TXB_R_R, TXB_P_R, TXB_B_R, TXB_L_R, TXB_G_R, TXB_T_R, TXB_S_R, TXB_D_R, TXB_NUM, TXB_NUM, TXB_NUM, TXB_NUM, TXB_NUM, TXB_NUM, TXB_Z_R}; static const uint8_t boltmap[64] PROGMEM = {TXB_NUL, TXB_NUM, TXB_NUM, TXB_NUM, TXB_NUM, TXB_NUM, TXB_NUM, TXB_S_L, TXB_S_L, TXB_T_L, TXB_K_L, TXB_P_L, TXB_W_L, TXB_H_L, TXB_R_L, TXB_A_L, TXB_O_L, TXB_STR, TXB_STR, TXB_NUL, TXB_NUL, TXB_NUL, TXB_STR, TXB_STR, TXB_E_R, TXB_U_R, TXB_F_R, TXB_R_R, TXB_P_R, TXB_B_R, TXB_L_R, TXB_G_R, TXB_T_R, TXB_S_R, TXB_D_R, TXB_NUM, TXB_NUM, TXB_NUM, TXB_NUM, TXB_NUM, TXB_NUM, TXB_Z_R};
# ifdef VIRTSER_ENABLE
static void send_steno_chord_bolt(void) {
for (uint8_t i = 0; i < BOLT_STROKE_SIZE; ++i) {
// TX Bolt uses variable length packets where each byte corresponds to a bit array of certain keys.
// If a user chorded the keys of the first group with keys of the last group, for example, there
// would be bytes of 0x00 in `chord` for the middle groups which we mustn't send.
if (chord[i]) {
virtser_send(chord[i]);
}
}
// Sending a null packet is not always necessary, but it is simpler and more reliable
// to unconditionally send it every time instead of keeping track of more states and
// creating more branches in the execution of the program.
virtser_send(0);
}
# else
# pragma message "VIRTSER_ENABLE = yes is required for TX Bolt to work properly out of the box!"
# endif // VIRTSER_ENABLE
/**
* @precondition: `key` is pressed
*/
static bool add_bolt_key_to_chord(uint8_t key) {
uint8_t boltcode = pgm_read_byte(boltmap + key);
chord[TXB_GET_GROUP(boltcode)] |= boltcode;
return false;
}
#endif // STENO_ENABLE_BOLT
#ifdef STENO_COMBINEDMAP #ifdef STENO_COMBINEDMAP
/* Used to look up when pressing the middle row key to combine two consonant or vowel keys */ /* Used to look up when pressing the middle row key to combine two consonant or vowel keys */
static const uint16_t combinedmap_first[] PROGMEM = {STN_S1, STN_TL, STN_PL, STN_HL, STN_FR, STN_PR, STN_LR, STN_TR, STN_DR, STN_A, STN_E}; static const uint16_t combinedmap_first[] PROGMEM = {STN_S1, STN_TL, STN_PL, STN_HL, STN_FR, STN_PR, STN_LR, STN_TR, STN_DR, STN_A, STN_E};
static const uint16_t combinedmap_second[] PROGMEM = {STN_S2, STN_KL, STN_WL, STN_RL, STN_RR, STN_BR, STN_GR, STN_SR, STN_ZR, STN_O, STN_U}; static const uint16_t combinedmap_second[] PROGMEM = {STN_S2, STN_KL, STN_WL, STN_RL, STN_RR, STN_BR, STN_GR, STN_SR, STN_ZR, STN_O, STN_U};
#endif #endif
static void steno_clear_state(void) { #ifdef STENO_ENABLE_ALL
memset(state, 0, sizeof(state));
memset(chord, 0, sizeof(chord));
}
static void send_steno_state(uint8_t size, bool send_empty) {
for (uint8_t i = 0; i < size; ++i) {
if (chord[i] || send_empty) {
#ifdef VIRTSER_ENABLE
virtser_send(chord[i]);
#endif
}
}
}
void steno_init() { void steno_init() {
if (!eeconfig_is_enabled()) { if (!eeconfig_is_enabled()) {
eeconfig_init(); eeconfig_init();
@ -94,19 +135,20 @@ void steno_init() {
} }
void steno_set_mode(steno_mode_t new_mode) { void steno_set_mode(steno_mode_t new_mode) {
steno_clear_state(); steno_clear_chord();
mode = new_mode; mode = new_mode;
eeprom_update_byte(EECONFIG_STENOMODE, mode); eeprom_update_byte(EECONFIG_STENOMODE, mode);
} }
#endif // STENO_ENABLE_ALL
/* override to intercept chords right before they get sent. /* override to intercept chords right before they get sent.
* return zero to suppress normal sending behavior. * return zero to suppress normal sending behavior.
*/ */
__attribute__((weak)) bool send_steno_chord_user(steno_mode_t mode, uint8_t chord[6]) { __attribute__((weak)) bool send_steno_chord_user(steno_mode_t mode, uint8_t chord[MAX_STROKE_SIZE]) {
return true; return true;
} }
__attribute__((weak)) bool postprocess_steno_user(uint16_t keycode, keyrecord_t *record, steno_mode_t mode, uint8_t chord[6], int8_t pressed) { __attribute__((weak)) bool postprocess_steno_user(uint16_t keycode, keyrecord_t *record, steno_mode_t mode, uint8_t chord[MAX_STROKE_SIZE], int8_t n_pressed_keys) {
return true; return true;
} }
@ -114,108 +156,94 @@ __attribute__((weak)) bool process_steno_user(uint16_t keycode, keyrecord_t *rec
return true; return true;
} }
static void send_steno_chord(void) {
if (send_steno_chord_user(mode, chord)) {
switch (mode) {
case STENO_MODE_BOLT:
send_steno_state(BOLT_STATE_SIZE, false);
#ifdef VIRTSER_ENABLE
virtser_send(0); // terminating byte
#endif
break;
case STENO_MODE_GEMINI:
chord[0] |= 0x80; // Indicate start of packet
send_steno_state(GEMINI_STATE_SIZE, true);
break;
}
}
steno_clear_state();
}
uint8_t *steno_get_state(void) {
return &state[0];
}
uint8_t *steno_get_chord(void) {
return &chord[0];
}
static bool update_state_bolt(uint8_t key, bool press) {
uint8_t boltcode = pgm_read_byte(boltmap + key);
if (press) {
state[TXB_GET_GROUP(boltcode)] |= boltcode;
chord[TXB_GET_GROUP(boltcode)] |= boltcode;
} else {
state[TXB_GET_GROUP(boltcode)] &= ~boltcode;
}
return false;
}
static bool update_state_gemini(uint8_t key, bool press) {
int idx = key / 7;
uint8_t bit = 1 << (6 - (key % 7));
if (press) {
state[idx] |= bit;
chord[idx] |= bit;
} else {
state[idx] &= ~bit;
}
return false;
}
bool process_steno(uint16_t keycode, keyrecord_t *record) { bool process_steno(uint16_t keycode, keyrecord_t *record) {
if (keycode < QK_STENO || keycode > QK_STENO_MAX) {
return true; // Not a steno key, pass it further along the chain
/*
* Clearing or sending the chord state is not necessary as we intentionally ignore whatever
* normal keyboard keys the user may have tapped while chording steno keys.
*/
}
if (IS_NOEVENT(record->event)) {
return true;
}
if (!process_steno_user(keycode, record)) {
return false; // User fully processed the steno key themselves
}
switch (keycode) { switch (keycode) {
#ifdef STENO_ENABLE_ALL
case QK_STENO_BOLT: case QK_STENO_BOLT:
if (!process_steno_user(keycode, record)) {
return false;
}
if (IS_PRESSED(record->event)) { if (IS_PRESSED(record->event)) {
steno_set_mode(STENO_MODE_BOLT); steno_set_mode(STENO_MODE_BOLT);
} }
return false; return false;
case QK_STENO_GEMINI: case QK_STENO_GEMINI:
if (!process_steno_user(keycode, record)) {
return false;
}
if (IS_PRESSED(record->event)) { if (IS_PRESSED(record->event)) {
steno_set_mode(STENO_MODE_GEMINI); steno_set_mode(STENO_MODE_GEMINI);
} }
return false; return false;
#endif // STENO_ENABLE_ALL
#ifdef STENO_COMBINEDMAP #ifdef STENO_COMBINEDMAP
case QK_STENO_COMB ... QK_STENO_COMB_MAX: { case QK_STENO_COMB ... QK_STENO_COMB_MAX: {
uint8_t result; bool first_result = process_steno(combinedmap_first[keycode - QK_STENO_COMB], record);
result = process_steno(combinedmap_first[keycode - QK_STENO_COMB], record); bool second_result = process_steno(combinedmap_second[keycode - QK_STENO_COMB], record);
result &= process_steno(combinedmap_second[keycode - QK_STENO_COMB], record); return first_result && second_result;
return result;
} }
#endif #endif // STENO_COMBINEDMAP
case STN__MIN ... STN__MAX: case STN__MIN ... STN__MAX:
if (!process_steno_user(keycode, record)) { if (IS_PRESSED(record->event)) {
return false; n_pressed_keys++;
} switch (mode) {
switch (mode) { #ifdef STENO_ENABLE_BOLT
case STENO_MODE_BOLT: case STENO_MODE_BOLT:
update_state_bolt(keycode - QK_STENO, IS_PRESSED(record->event)); add_bolt_key_to_chord(keycode - QK_STENO);
break; break;
case STENO_MODE_GEMINI: #endif // STENO_ENABLE_BOLT
update_state_gemini(keycode - QK_STENO, IS_PRESSED(record->event)); #ifdef STENO_ENABLE_GEMINI
break; case STENO_MODE_GEMINI:
} add_gemini_key_to_chord(keycode - QK_STENO);
// allow postprocessing hooks break;
if (postprocess_steno_user(keycode, record, mode, chord, pressed)) { #endif // STENO_ENABLE_GEMINI
if (IS_PRESSED(record->event)) { default:
++pressed; return false;
} else {
--pressed;
if (pressed <= 0) {
pressed = 0;
send_steno_chord();
}
} }
if (!postprocess_steno_user(keycode, record, mode, chord, n_pressed_keys)) {
return false;
}
} else { // is released
n_pressed_keys--;
if (!postprocess_steno_user(keycode, record, mode, chord, n_pressed_keys)) {
return false;
}
if (n_pressed_keys > 0) {
// User hasn't released all keys yet,
// so the chord cannot be sent
return false;
}
n_pressed_keys = 0;
if (!send_steno_chord_user(mode, chord)) {
steno_clear_chord();
return false;
}
switch (mode) {
#if defined(STENO_ENABLE_BOLT) && defined(VIRTSER_ENABLE)
case STENO_MODE_BOLT:
send_steno_chord_bolt();
break;
#endif // STENO_ENABLE_BOLT && VIRTSER_ENABLE
#if defined(STENO_ENABLE_GEMINI) && defined(VIRTSER_ENABLE)
case STENO_MODE_GEMINI:
send_steno_chord_gemini();
break;
#endif // STENO_ENABLE_GEMINI && VIRTSER_ENABLE
default:
break;
}
steno_clear_chord();
} }
return false; break;
} }
return true; return false;
} }

View File

@ -18,10 +18,22 @@
#include "quantum.h" #include "quantum.h"
typedef enum { STENO_MODE_BOLT, STENO_MODE_GEMINI } steno_mode_t; #define BOLT_STROKE_SIZE 4
#define GEMINI_STROKE_SIZE 6
bool process_steno(uint16_t keycode, keyrecord_t *record); #ifdef STENO_ENABLE_GEMINI
void steno_init(void); # define MAX_STROKE_SIZE GEMINI_STROKE_SIZE
void steno_set_mode(steno_mode_t mode); #else
uint8_t *steno_get_state(void); # define MAX_STROKE_SIZE BOLT_STROKE_SIZE
uint8_t *steno_get_chord(void); #endif
typedef enum {
STENO_MODE_GEMINI,
STENO_MODE_BOLT,
} steno_mode_t;
bool process_steno(uint16_t keycode, keyrecord_t *record);
#ifdef STENO_ENABLE_ALL
void steno_init(void);
void steno_set_mode(steno_mode_t mode);
#endif // STENO_ENABLE_ALL