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:
		| @@ -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 | ||||
|  * it under the terms of the GNU General Public License as published by | ||||
| @@ -15,77 +15,118 @@ | ||||
|  */ | ||||
| #include "process_steno.h" | ||||
| #include "quantum_keycodes.h" | ||||
| #include "eeprom.h" | ||||
| #include "keymap_steno.h" | ||||
| #include "virtser.h" | ||||
| #include <string.h> | ||||
| #ifdef VIRTSER_ENABLE | ||||
| #    include "virtser.h" | ||||
| #endif | ||||
| #ifdef STENO_ENABLE_ALL | ||||
| #    include "eeprom.h" | ||||
| #endif | ||||
|  | ||||
| // 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 | ||||
| // All steno keys that have been pressed to form this chord, | ||||
| // stored in MAX_STROKE_SIZE groups of 8-bit arrays. | ||||
| static uint8_t chord[MAX_STROKE_SIZE] = {0}; | ||||
| // The number of physical keys actually being held down. | ||||
| // This is not always equal to the number of 1 bits 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. | ||||
| static int8_t n_pressed_keys = 0; | ||||
|  | ||||
| #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) | ||||
|  | ||||
| #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; | ||||
| #ifdef STENO_ENABLE_ALL | ||||
| 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}; | ||||
|  | ||||
| #    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 | ||||
| /* 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_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 | ||||
|  | ||||
| static void steno_clear_state(void) { | ||||
|     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 | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #ifdef STENO_ENABLE_ALL | ||||
| void steno_init() { | ||||
|     if (!eeconfig_is_enabled()) { | ||||
|         eeconfig_init(); | ||||
| @@ -94,19 +135,20 @@ void steno_init() { | ||||
| } | ||||
|  | ||||
| void steno_set_mode(steno_mode_t new_mode) { | ||||
|     steno_clear_state(); | ||||
|     steno_clear_chord(); | ||||
|     mode = new_mode; | ||||
|     eeprom_update_byte(EECONFIG_STENOMODE, mode); | ||||
| } | ||||
| #endif // STENO_ENABLE_ALL | ||||
|  | ||||
| /* override to intercept chords right before they get sent. | ||||
|  * 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; | ||||
| } | ||||
|  | ||||
| __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; | ||||
| } | ||||
|  | ||||
| @@ -114,108 +156,94 @@ __attribute__((weak)) bool process_steno_user(uint16_t keycode, keyrecord_t *rec | ||||
|     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) { | ||||
|     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) { | ||||
| #ifdef STENO_ENABLE_ALL | ||||
|         case QK_STENO_BOLT: | ||||
|             if (!process_steno_user(keycode, record)) { | ||||
|                 return false; | ||||
|             } | ||||
|             if (IS_PRESSED(record->event)) { | ||||
|                 steno_set_mode(STENO_MODE_BOLT); | ||||
|             } | ||||
|             return false; | ||||
|  | ||||
|         case QK_STENO_GEMINI: | ||||
|             if (!process_steno_user(keycode, record)) { | ||||
|                 return false; | ||||
|             } | ||||
|             if (IS_PRESSED(record->event)) { | ||||
|                 steno_set_mode(STENO_MODE_GEMINI); | ||||
|             } | ||||
|             return false; | ||||
| #endif // STENO_ENABLE_ALL | ||||
|  | ||||
| #ifdef STENO_COMBINEDMAP | ||||
|         case QK_STENO_COMB ... QK_STENO_COMB_MAX: { | ||||
|             uint8_t result; | ||||
|             result = process_steno(combinedmap_first[keycode - QK_STENO_COMB], record); | ||||
|             result &= process_steno(combinedmap_second[keycode - QK_STENO_COMB], record); | ||||
|             return result; | ||||
|             bool first_result  = process_steno(combinedmap_first[keycode - QK_STENO_COMB], record); | ||||
|             bool second_result = process_steno(combinedmap_second[keycode - QK_STENO_COMB], record); | ||||
|             return first_result && second_result; | ||||
|         } | ||||
| #endif | ||||
| #endif // STENO_COMBINEDMAP | ||||
|         case STN__MIN ... STN__MAX: | ||||
|             if (!process_steno_user(keycode, record)) { | ||||
|                 return false; | ||||
|             } | ||||
|             switch (mode) { | ||||
|                 case STENO_MODE_BOLT: | ||||
|                     update_state_bolt(keycode - QK_STENO, IS_PRESSED(record->event)); | ||||
|                     break; | ||||
|                 case STENO_MODE_GEMINI: | ||||
|                     update_state_gemini(keycode - QK_STENO, IS_PRESSED(record->event)); | ||||
|                     break; | ||||
|             } | ||||
|             // allow postprocessing hooks | ||||
|             if (postprocess_steno_user(keycode, record, mode, chord, pressed)) { | ||||
|                 if (IS_PRESSED(record->event)) { | ||||
|                     ++pressed; | ||||
|                 } else { | ||||
|                     --pressed; | ||||
|                     if (pressed <= 0) { | ||||
|                         pressed = 0; | ||||
|                         send_steno_chord(); | ||||
|                     } | ||||
|             if (IS_PRESSED(record->event)) { | ||||
|                 n_pressed_keys++; | ||||
|                 switch (mode) { | ||||
| #ifdef STENO_ENABLE_BOLT | ||||
|                     case STENO_MODE_BOLT: | ||||
|                         add_bolt_key_to_chord(keycode - QK_STENO); | ||||
|                         break; | ||||
| #endif // STENO_ENABLE_BOLT | ||||
| #ifdef STENO_ENABLE_GEMINI | ||||
|                     case STENO_MODE_GEMINI: | ||||
|                         add_gemini_key_to_chord(keycode - QK_STENO); | ||||
|                         break; | ||||
| #endif // STENO_ENABLE_GEMINI | ||||
|                     default: | ||||
|                         return false; | ||||
|                 } | ||||
|                 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; | ||||
| } | ||||
|   | ||||
| @@ -18,10 +18,22 @@ | ||||
|  | ||||
| #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); | ||||
| void     steno_init(void); | ||||
| void     steno_set_mode(steno_mode_t mode); | ||||
| uint8_t *steno_get_state(void); | ||||
| uint8_t *steno_get_chord(void); | ||||
| #ifdef STENO_ENABLE_GEMINI | ||||
| #    define MAX_STROKE_SIZE GEMINI_STROKE_SIZE | ||||
| #else | ||||
| #    define MAX_STROKE_SIZE BOLT_STROKE_SIZE | ||||
| #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 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user