diff options
Diffstat (limited to 'quantum')
82 files changed, 5251 insertions, 387 deletions
| diff --git a/quantum/action.c b/quantum/action.c index 3efed443a3..ef059f0e2a 100644 --- a/quantum/action.c +++ b/quantum/action.c @@ -14,9 +14,18 @@ GNU General Public License for more details.  You should have received a copy of the GNU General Public License  along with this program.  If not, see <http://www.gnu.org/licenses/>.  */ +#include <limits.h> + +#ifdef DEBUG_ACTION +#    include "debug.h" +#else +#    include "nodebug.h" +#endif +  #include "host.h"  #include "keycode.h"  #include "keyboard.h" +#include "keymap.h"  #include "mousekey.h"  #include "programmable_button.h"  #include "command.h" @@ -32,12 +41,6 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.  #    include "backlight.h"  #endif -#ifdef DEBUG_ACTION -#    include "debug.h" -#else -#    include "nodebug.h" -#endif -  #ifdef POINTING_DEVICE_ENABLE  #    include "pointing_device.h"  #endif @@ -89,6 +92,7 @@ void action_exec(keyevent_t event) {      }  #ifdef SWAP_HANDS_ENABLE +    // Swap hands handles both keys and encoders, if ENCODER_MAP_ENABLE is defined.      if (!IS_NOEVENT(event)) {          process_hand_swap(&event);      } @@ -97,7 +101,7 @@ void action_exec(keyevent_t event) {      keyrecord_t record = {.event = event};  #ifndef NO_ACTION_ONESHOT -    if (!keymap_config.oneshot_disable) { +    if (keymap_config.oneshot_enable) {  #    if (defined(ONESHOT_TIMEOUT) && (ONESHOT_TIMEOUT > 0))          if (has_oneshot_layer_timed_out()) {              clear_oneshot_layer_state(ONESHOT_OTHER_KEY_PRESSED); @@ -136,27 +140,65 @@ void action_exec(keyevent_t event) {  }  #ifdef SWAP_HANDS_ENABLE +extern const keypos_t PROGMEM hand_swap_config[MATRIX_ROWS][MATRIX_COLS]; +#    ifdef ENCODER_MAP_ENABLE +extern const uint8_t PROGMEM encoder_hand_swap_config[NUM_ENCODERS]; +#    endif // ENCODER_MAP_ENABLE +  bool swap_hands = false;  bool swap_held  = false; +bool should_swap_hands(size_t index, uint8_t *swap_state, bool pressed) { +    size_t  array_index = index / (CHAR_BIT); +    size_t  bit_index   = index % (CHAR_BIT); +    uint8_t bit_val     = 1 << bit_index; +    bool    do_swap     = pressed ? swap_hands : swap_state[array_index] & bit_val; +    return do_swap; +} + +void set_swap_hands_state(size_t index, uint8_t *swap_state, bool on) { +    size_t  array_index = index / (CHAR_BIT); +    size_t  bit_index   = index % (CHAR_BIT); +    uint8_t bit_val     = 1 << bit_index; +    if (on) { +        swap_state[array_index] |= bit_val; +    } else { +        swap_state[array_index] &= ~bit_val; +    } +} +  /** \brief Process Hand Swap   *   * FIXME: Needs documentation.   */  void process_hand_swap(keyevent_t *event) { -    static swap_state_row_t swap_state[MATRIX_ROWS]; - -    keypos_t         pos     = event->key; -    swap_state_row_t col_bit = (swap_state_row_t)1 << pos.col; -    bool             do_swap = event->pressed ? swap_hands : swap_state[pos.row] & (col_bit); - -    if (do_swap) { -        event->key.row = pgm_read_byte(&hand_swap_config[pos.row][pos.col].row); -        event->key.col = pgm_read_byte(&hand_swap_config[pos.row][pos.col].col); -        swap_state[pos.row] |= col_bit; -    } else { -        swap_state[pos.row] &= ~(col_bit); +    keypos_t pos = event->key; +    if (pos.row < MATRIX_ROWS && pos.col < MATRIX_COLS) { +        static uint8_t matrix_swap_state[((MATRIX_ROWS * MATRIX_COLS) + (CHAR_BIT)-1) / (CHAR_BIT)]; +        size_t         index   = (size_t)(pos.row * MATRIX_COLS) + pos.col; +        bool           do_swap = should_swap_hands(index, matrix_swap_state, event->pressed); +        if (do_swap) { +            event->key.row = pgm_read_byte(&hand_swap_config[pos.row][pos.col].row); +            event->key.col = pgm_read_byte(&hand_swap_config[pos.row][pos.col].col); +            set_swap_hands_state(index, matrix_swap_state, true); +        } else { +            set_swap_hands_state(index, matrix_swap_state, false); +        } +    } +#    ifdef ENCODER_MAP_ENABLE +    else if (pos.row == KEYLOC_ENCODER_CW || pos.row == KEYLOC_ENCODER_CCW) { +        static uint8_t encoder_swap_state[((NUM_ENCODERS) + (CHAR_BIT)-1) / (CHAR_BIT)]; +        size_t         index   = pos.col; +        bool           do_swap = should_swap_hands(index, encoder_swap_state, event->pressed); +        if (do_swap) { +            event->key.row = pos.row; +            event->key.col = pgm_read_byte(&encoder_hand_swap_config[pos.col]); +            set_swap_hands_state(index, encoder_swap_state, true); +        } else { +            set_swap_hands_state(index, encoder_swap_state, false); +        }      } +#    endif // ENCODER_MAP_ENABLE  }  #endif @@ -216,7 +258,7 @@ void process_record(keyrecord_t *record) {      if (!process_record_quantum(record)) {  #ifndef NO_ACTION_ONESHOT -        if (is_oneshot_layer_active() && record->event.pressed && !keymap_config.oneshot_disable) { +        if (is_oneshot_layer_active() && record->event.pressed && keymap_config.oneshot_enable) {              clear_oneshot_layer_state(ONESHOT_OTHER_KEY_PRESSED);          }  #endif @@ -281,7 +323,7 @@ void process_action(keyrecord_t *record, action_t action) {  #    ifdef SWAP_HANDS_ENABLE          && !(action.kind.id == ACT_SWAP_HANDS && action.swap.code == OP_SH_ONESHOT)  #    endif -        && !keymap_config.oneshot_disable) { +        && keymap_config.oneshot_enable) {          clear_oneshot_layer_state(ONESHOT_OTHER_KEY_PRESSED);          do_release_oneshot = !is_oneshot_layer_active();      } @@ -325,7 +367,7 @@ void process_action(keyrecord_t *record, action_t action) {  #    ifndef NO_ACTION_ONESHOT                  case MODS_ONESHOT:                      // Oneshot modifier -                    if (keymap_config.oneshot_disable) { +                    if (!keymap_config.oneshot_enable) {                          if (event.pressed) {                              if (mods) {                                  if (IS_MOD(action.key.code) || action.key.code == KC_NO) { @@ -571,7 +613,7 @@ void process_action(keyrecord_t *record, action_t action) {  #        ifndef NO_ACTION_ONESHOT                  case OP_ONESHOT:                      // Oneshot modifier -                    if (keymap_config.oneshot_disable) { +                    if (!keymap_config.oneshot_enable) {                          if (event.pressed) {                              layer_on(action.layer_tap.val);                          } else { diff --git a/quantum/action_layer.c b/quantum/action_layer.c index e20eedee40..473e0e948d 100644 --- a/quantum/action_layer.c +++ b/quantum/action_layer.c @@ -1,8 +1,5 @@ +#include <limits.h>  #include <stdint.h> -#include "keyboard.h" -#include "action.h" -#include "util.h" -#include "action_layer.h"  #ifdef DEBUG_ACTION  #    include "debug.h" @@ -10,6 +7,12 @@  #    include "nodebug.h"  #endif +#include "keyboard.h" +#include "keymap.h" +#include "action.h" +#include "util.h" +#include "action_layer.h" +  /** \brief Default Layer State   */  layer_state_t default_layer_state = 0; @@ -223,19 +226,20 @@ void layer_debug(void) {  /** \brief source layer cache   */ -uint8_t source_layers_cache[(MATRIX_ROWS * MATRIX_COLS + 7) / 8][MAX_LAYER_BITS] = {{0}}; +uint8_t source_layers_cache[((MATRIX_ROWS * MATRIX_COLS) + (CHAR_BIT)-1) / (CHAR_BIT)][MAX_LAYER_BITS] = {{0}}; +#    ifdef ENCODER_MAP_ENABLE +uint8_t encoder_source_layers_cache[(NUM_ENCODERS + (CHAR_BIT)-1) / (CHAR_BIT)][MAX_LAYER_BITS] = {{0}}; +#    endif // ENCODER_MAP_ENABLE -/** \brief update source layers cache +/** \brief update source layers cache impl   * - * Updates the cached keys when changing layers + * Updates the supplied cache when changing layers   */ -void update_source_layers_cache(keypos_t key, uint8_t layer) { -    const uint8_t key_number  = key.col + (key.row * MATRIX_COLS); -    const uint8_t storage_row = key_number / 8; -    const uint8_t storage_bit = key_number % 8; - +void update_source_layers_cache_impl(uint8_t layer, uint16_t entry_number, uint8_t cache[][MAX_LAYER_BITS]) { +    const uint16_t storage_idx = entry_number / (CHAR_BIT); +    const uint8_t  storage_bit = entry_number % (CHAR_BIT);      for (uint8_t bit_number = 0; bit_number < MAX_LAYER_BITS; bit_number++) { -        source_layers_cache[storage_row][bit_number] ^= (-((layer & (1U << bit_number)) != 0) ^ source_layers_cache[storage_row][bit_number]) & (1U << storage_bit); +        cache[storage_idx][bit_number] ^= (-((layer & (1U << bit_number)) != 0) ^ cache[storage_idx][bit_number]) & (1U << storage_bit);      }  } @@ -243,18 +247,52 @@ void update_source_layers_cache(keypos_t key, uint8_t layer) {   *   * reads the cached keys stored when the layer was changed   */ -uint8_t read_source_layers_cache(keypos_t key) { -    const uint8_t key_number  = key.col + (key.row * MATRIX_COLS); -    const uint8_t storage_row = key_number / 8; -    const uint8_t storage_bit = key_number % 8; -    uint8_t       layer       = 0; +uint8_t read_source_layers_cache_impl(uint16_t entry_number, uint8_t cache[][MAX_LAYER_BITS]) { +    const uint16_t storage_idx = entry_number / (CHAR_BIT); +    const uint8_t  storage_bit = entry_number % (CHAR_BIT); +    uint8_t        layer       = 0;      for (uint8_t bit_number = 0; bit_number < MAX_LAYER_BITS; bit_number++) { -        layer |= ((source_layers_cache[storage_row][bit_number] & (1U << storage_bit)) != 0) << bit_number; +        layer |= ((cache[storage_idx][bit_number] & (1U << storage_bit)) != 0) << bit_number;      }      return layer;  } + +/** \brief update encoder source layers cache + * + * Updates the cached encoders when changing layers + */ +void update_source_layers_cache(keypos_t key, uint8_t layer) { +    if (key.row < MATRIX_ROWS && key.col < MATRIX_COLS) { +        const uint16_t entry_number = (uint16_t)(key.row * MATRIX_COLS) + key.col; +        update_source_layers_cache_impl(layer, entry_number, source_layers_cache); +    } +#    ifdef ENCODER_MAP_ENABLE +    else if (key.row == KEYLOC_ENCODER_CW || key.row == KEYLOC_ENCODER_CCW) { +        const uint16_t entry_number = key.col; +        update_source_layers_cache_impl(layer, entry_number, encoder_source_layers_cache); +    } +#    endif // ENCODER_MAP_ENABLE +} + +/** \brief read source layers cache + * + * reads the cached keys stored when the layer was changed + */ +uint8_t read_source_layers_cache(keypos_t key) { +    if (key.row < MATRIX_ROWS && key.col < MATRIX_COLS) { +        const uint16_t entry_number = (uint16_t)(key.row * MATRIX_COLS) + key.col; +        return read_source_layers_cache_impl(entry_number, source_layers_cache); +    } +#    ifdef ENCODER_MAP_ENABLE +    else if (key.row == KEYLOC_ENCODER_CW || key.row == KEYLOC_ENCODER_CCW) { +        const uint16_t entry_number = key.col; +        return read_source_layers_cache_impl(entry_number, encoder_source_layers_cache); +    } +#    endif // ENCODER_MAP_ENABLE +    return 0; +}  #endif  /** \brief Store or get action (FIXME: Needs better summary) diff --git a/quantum/action_tapping.c b/quantum/action_tapping.c index 6f8b4f8c56..3c8b5678b7 100644 --- a/quantum/action_tapping.c +++ b/quantum/action_tapping.c @@ -1,10 +1,5 @@  #include <stdint.h>  #include <stdbool.h> -#include "action.h" -#include "action_layer.h" -#include "action_tapping.h" -#include "keycode.h" -#include "timer.h"  #ifdef DEBUG_ACTION  #    include "debug.h" @@ -12,6 +7,12 @@  #    include "nodebug.h"  #endif +#include "action.h" +#include "action_layer.h" +#include "action_tapping.h" +#include "keycode.h" +#include "timer.h" +  #ifndef NO_ACTION_TAPPING  #    define IS_TAPPING() !IS_NOEVENT(tapping_key.event) @@ -23,17 +24,20 @@  #    else  #        define IS_TAPPING_RECORD(r) (IS_TAPPING() && KEYEQ(tapping_key.event.key, (r->event.key)) && tapping_key.keycode == r->keycode)  #    endif +#    define WITHIN_TAPPING_TERM(e) (TIMER_DIFF_16(e.time, tapping_key.event.time) < GET_TAPPING_TERM(get_record_keycode(&tapping_key, false), &tapping_key)) +#    ifdef DYNAMIC_TAPPING_TERM_ENABLE  uint16_t g_tapping_term = TAPPING_TERM; +#    endif +#    ifdef TAPPING_TERM_PER_KEY  __attribute__((weak)) uint16_t get_tapping_term(uint16_t keycode, keyrecord_t *record) { +#        ifdef DYNAMIC_TAPPING_TERM_ENABLE      return g_tapping_term; +#        else +    return TAPPING_TERM; +#        endif  } - -#    ifdef TAPPING_TERM_PER_KEY -#        define WITHIN_TAPPING_TERM(e) (TIMER_DIFF_16(e.time, tapping_key.event.time) < get_tapping_term(get_record_keycode(&tapping_key, false), &tapping_key)) -#    else -#        define WITHIN_TAPPING_TERM(e) (TIMER_DIFF_16(e.time, tapping_key.event.time) < g_tapping_term)  #    endif  #    ifdef TAPPING_FORCE_HOLD_PER_KEY @@ -164,15 +168,7 @@ bool process_tapping(keyrecord_t *keyp) {                  else if (                      (                          ( -                            ( -#        ifdef TAPPING_TERM_PER_KEY -                                get_tapping_term(tapping_keycode, &tapping_key) -#        else -                                g_tapping_term -#        endif -                                >= 500 -                            ) - +                            GET_TAPPING_TERM(tapping_keycode, &tapping_key) >= 500  #        ifdef PERMISSIVE_HOLD_PER_KEY                              || get_permissive_hold(tapping_keycode, &tapping_key)  #        elif defined(PERMISSIVE_HOLD) diff --git a/quantum/action_tapping.h b/quantum/action_tapping.h index b2feb6850c..9b64c93120 100644 --- a/quantum/action_tapping.h +++ b/quantum/action_tapping.h @@ -44,3 +44,11 @@ bool     get_retro_tapping(uint16_t keycode, keyrecord_t *record);  #ifdef DYNAMIC_TAPPING_TERM_ENABLE  extern uint16_t g_tapping_term;  #endif + +#ifdef TAPPING_TERM_PER_KEY +#    define GET_TAPPING_TERM(keycode, record) get_tapping_term(keycode, record) +#elif defined(DYNAMIC_TAPPING_TERM_ENABLE) +#    define GET_TAPPING_TERM(keycode, record) g_tapping_term +#else +#    define GET_TAPPING_TERM(keycode, record) (TAPPING_TERM) +#endif diff --git a/quantum/action_util.c b/quantum/action_util.c index 4ea0bf61fb..738410a4ac 100644 --- a/quantum/action_util.c +++ b/quantum/action_util.c @@ -155,7 +155,7 @@ void clear_oneshot_swaphands(void) {   * FIXME: needs doc   */  void set_oneshot_layer(uint8_t layer, uint8_t state) { -    if (!keymap_config.oneshot_disable) { +    if (keymap_config.oneshot_enable) {          oneshot_layer_data = layer << 3 | state;          layer_on(layer);  #    if (defined(ONESHOT_TIMEOUT) && (ONESHOT_TIMEOUT > 0)) @@ -184,7 +184,7 @@ void reset_oneshot_layer(void) {  void clear_oneshot_layer_state(oneshot_fullfillment_t state) {      uint8_t start_state = oneshot_layer_data;      oneshot_layer_data &= ~state; -    if ((!get_oneshot_layer_state() && start_state != oneshot_layer_data) && !keymap_config.oneshot_disable) { +    if ((!get_oneshot_layer_state() && start_state != oneshot_layer_data) && keymap_config.oneshot_enable) {          layer_off(get_oneshot_layer());          reset_oneshot_layer();      } @@ -202,8 +202,8 @@ bool is_oneshot_layer_active(void) {   * FIXME: needs doc   */  void oneshot_set(bool active) { -    if (keymap_config.oneshot_disable != active) { -        keymap_config.oneshot_disable = active; +    if (keymap_config.oneshot_enable != active) { +        keymap_config.oneshot_enable = active;          eeconfig_update_keymap(keymap_config.raw);          clear_oneshot_layer_state(ONESHOT_OTHER_KEY_PRESSED);          dprintf("Oneshot: active: %d\n", active); @@ -215,7 +215,7 @@ void oneshot_set(bool active) {   * FIXME: needs doc   */  void oneshot_toggle(void) { -    oneshot_set(!keymap_config.oneshot_disable); +    oneshot_set(!keymap_config.oneshot_enable);  }  /** \brief enable oneshot @@ -235,7 +235,7 @@ void oneshot_disable(void) {  }  bool is_oneshot_enabled(void) { -    return keymap_config.oneshot_disable; +    return keymap_config.oneshot_enable;  }  #endif @@ -413,7 +413,7 @@ void del_oneshot_mods(uint8_t mods) {   * FIXME: needs doc   */  void set_oneshot_mods(uint8_t mods) { -    if (!keymap_config.oneshot_disable) { +    if (keymap_config.oneshot_enable) {          if (oneshot_mods != mods) {  #    if (defined(ONESHOT_TIMEOUT) && (ONESHOT_TIMEOUT > 0))              oneshot_time = timer_read(); diff --git a/quantum/dynamic_keymap.c b/quantum/dynamic_keymap.c index f070375ff3..fc1c55784d 100644 --- a/quantum/dynamic_keymap.c +++ b/quantum/dynamic_keymap.c @@ -21,6 +21,12 @@  #include "dynamic_keymap.h"  #include "via.h" // for default VIA_EEPROM_ADDR_END +#ifdef ENCODER_ENABLE +#    include "encoder.h" +#else +#    define NUM_ENCODERS 0 +#endif +  #ifndef DYNAMIC_KEYMAP_LAYER_COUNT  #    define DYNAMIC_KEYMAP_LAYER_COUNT 4  #endif @@ -58,20 +64,28 @@  #    endif  #endif -// Dynamic macro starts after dynamic keymaps -#ifndef DYNAMIC_KEYMAP_MACRO_EEPROM_ADDR -#    define DYNAMIC_KEYMAP_MACRO_EEPROM_ADDR (DYNAMIC_KEYMAP_EEPROM_ADDR + (DYNAMIC_KEYMAP_LAYER_COUNT * MATRIX_ROWS * MATRIX_COLS * 2)) +// Dynamic encoders starts after dynamic keymaps +#ifndef DYNAMIC_KEYMAP_ENCODER_EEPROM_ADDR +#    define DYNAMIC_KEYMAP_ENCODER_EEPROM_ADDR (DYNAMIC_KEYMAP_EEPROM_ADDR + (DYNAMIC_KEYMAP_LAYER_COUNT * MATRIX_ROWS * MATRIX_COLS * 2))  #endif +// Dynamic macro starts after dynamic encoders, but only when using ENCODER_MAP +#ifdef ENCODER_MAP_ENABLE +#    ifndef DYNAMIC_KEYMAP_MACRO_EEPROM_ADDR +#        define DYNAMIC_KEYMAP_MACRO_EEPROM_ADDR (DYNAMIC_KEYMAP_ENCODER_EEPROM_ADDR + (DYNAMIC_KEYMAP_LAYER_COUNT * NUM_ENCODERS * 2 * 2)) +#    endif // DYNAMIC_KEYMAP_MACRO_EEPROM_ADDR +#else      // ENCODER_MAP_ENABLE +#    ifndef DYNAMIC_KEYMAP_MACRO_EEPROM_ADDR +#        define DYNAMIC_KEYMAP_MACRO_EEPROM_ADDR (DYNAMIC_KEYMAP_ENCODER_EEPROM_ADDR) +#    endif // DYNAMIC_KEYMAP_MACRO_EEPROM_ADDR +#endif     // ENCODER_MAP_ENABLE +  // Sanity check that dynamic keymaps fit in available EEPROM  // If there's not 100 bytes available for macros, then something is wrong.  // The keyboard should override DYNAMIC_KEYMAP_LAYER_COUNT to reduce it,  // or DYNAMIC_KEYMAP_EEPROM_MAX_ADDR to increase it, *only if* the microcontroller has  // more than the default. -#if DYNAMIC_KEYMAP_EEPROM_MAX_ADDR - DYNAMIC_KEYMAP_MACRO_EEPROM_ADDR < 100 -#    pragma message STR(DYNAMIC_KEYMAP_EEPROM_MAX_ADDR - DYNAMIC_KEYMAP_MACRO_EEPROM_ADDR) " < 100" -#    error Dynamic keymaps are configured to use more EEPROM than is available. -#endif +_Static_assert((DYNAMIC_KEYMAP_EEPROM_MAX_ADDR) - (DYNAMIC_KEYMAP_MACRO_EEPROM_ADDR) >= 100, "Dynamic keymaps are configured to use more EEPROM than is available.");  // Dynamic macros are stored after the keymaps and use what is available  // up to and including DYNAMIC_KEYMAP_EEPROM_MAX_ADDR. @@ -89,6 +103,7 @@ void *dynamic_keymap_key_to_eeprom_address(uint8_t layer, uint8_t row, uint8_t c  }  uint16_t dynamic_keymap_get_keycode(uint8_t layer, uint8_t row, uint8_t column) { +    if (layer >= DYNAMIC_KEYMAP_LAYER_COUNT || row >= MATRIX_ROWS || column >= MATRIX_COLS) return KC_NO;      void *address = dynamic_keymap_key_to_eeprom_address(layer, row, column);      // Big endian, so we can read/write EEPROM directly from host if we want      uint16_t keycode = eeprom_read_byte(address) << 8; @@ -97,12 +112,36 @@ uint16_t dynamic_keymap_get_keycode(uint8_t layer, uint8_t row, uint8_t column)  }  void dynamic_keymap_set_keycode(uint8_t layer, uint8_t row, uint8_t column, uint16_t keycode) { +    if (layer >= DYNAMIC_KEYMAP_LAYER_COUNT || row >= MATRIX_ROWS || column >= MATRIX_COLS) return;      void *address = dynamic_keymap_key_to_eeprom_address(layer, row, column);      // Big endian, so we can read/write EEPROM directly from host if we want      eeprom_update_byte(address, (uint8_t)(keycode >> 8));      eeprom_update_byte(address + 1, (uint8_t)(keycode & 0xFF));  } +#ifdef ENCODER_MAP_ENABLE +void *dynamic_keymap_encoder_to_eeprom_address(uint8_t layer, uint8_t encoder_id) { +    return ((void *)DYNAMIC_KEYMAP_ENCODER_EEPROM_ADDR) + (layer * NUM_ENCODERS * 2 * 2) + (encoder_id * 2 * 2); +} + +uint16_t dynamic_keymap_get_encoder(uint8_t layer, uint8_t encoder_id, bool clockwise) { +    if (layer >= DYNAMIC_KEYMAP_LAYER_COUNT || encoder_id >= NUM_ENCODERS) return KC_NO; +    void *address = dynamic_keymap_encoder_to_eeprom_address(layer, encoder_id); +    // Big endian, so we can read/write EEPROM directly from host if we want +    uint16_t keycode = ((uint16_t)eeprom_read_byte(address + (clockwise ? 0 : 2))) << 8; +    keycode |= eeprom_read_byte(address + (clockwise ? 0 : 2) + 1); +    return keycode; +} + +void dynamic_keymap_set_encoder(uint8_t layer, uint8_t encoder_id, bool clockwise, uint16_t keycode) { +    if (layer >= DYNAMIC_KEYMAP_LAYER_COUNT || encoder_id >= NUM_ENCODERS) return; +    void *address = dynamic_keymap_encoder_to_eeprom_address(layer, encoder_id); +    // Big endian, so we can read/write EEPROM directly from host if we want +    eeprom_update_byte(address + (clockwise ? 0 : 2), (uint8_t)(keycode >> 8)); +    eeprom_update_byte(address + (clockwise ? 0 : 2) + 1, (uint8_t)(keycode & 0xFF)); +} +#endif // ENCODER_MAP_ENABLE +  void dynamic_keymap_reset(void) {      // Reset the keymaps in EEPROM to what is in flash.      // All keyboards using dynamic keymaps should define a layout @@ -113,6 +152,12 @@ void dynamic_keymap_reset(void) {                  dynamic_keymap_set_keycode(layer, row, column, pgm_read_word(&keymaps[layer][row][column]));              }          } +#ifdef ENCODER_MAP_ENABLE +        for (int encoder = 0; encoder < NUM_ENCODERS; encoder++) { +            dynamic_keymap_set_encoder(layer, encoder, true, pgm_read_word(&encoder_map[layer][encoder][0])); +            dynamic_keymap_set_encoder(layer, encoder, false, pgm_read_word(&encoder_map[layer][encoder][1])); +        } +#endif // ENCODER_MAP_ENABLE      }  } @@ -148,9 +193,15 @@ void dynamic_keymap_set_buffer(uint16_t offset, uint16_t size, uint8_t *data) {  uint16_t keymap_key_to_keycode(uint8_t layer, keypos_t key) {      if (layer < DYNAMIC_KEYMAP_LAYER_COUNT && key.row < MATRIX_ROWS && key.col < MATRIX_COLS) {          return dynamic_keymap_get_keycode(layer, key.row, key.col); -    } else { -        return KC_NO;      } +#ifdef ENCODER_MAP_ENABLE +    else if (layer < DYNAMIC_KEYMAP_LAYER_COUNT && key.row == KEYLOC_ENCODER_CW && key.col < NUM_ENCODERS) { +        return dynamic_keymap_get_encoder(layer, key.col, true); +    } else if (layer < DYNAMIC_KEYMAP_LAYER_COUNT && key.row == KEYLOC_ENCODER_CCW && key.col < NUM_ENCODERS) { +        return dynamic_keymap_get_encoder(layer, key.col, false); +    } +#endif // ENCODER_MAP_ENABLE +    return KC_NO;  }  uint8_t dynamic_keymap_macro_get_count(void) { diff --git a/quantum/dynamic_keymap.h b/quantum/dynamic_keymap.h index 55676172b6..459b48d07a 100644 --- a/quantum/dynamic_keymap.h +++ b/quantum/dynamic_keymap.h @@ -22,7 +22,11 @@ uint8_t  dynamic_keymap_get_layer_count(void);  void *   dynamic_keymap_key_to_eeprom_address(uint8_t layer, uint8_t row, uint8_t column);  uint16_t dynamic_keymap_get_keycode(uint8_t layer, uint8_t row, uint8_t column);  void     dynamic_keymap_set_keycode(uint8_t layer, uint8_t row, uint8_t column, uint16_t keycode); -void     dynamic_keymap_reset(void); +#ifdef ENCODER_MAP_ENABLE +uint16_t dynamic_keymap_get_encoder(uint8_t layer, uint8_t encoder_id, bool clockwise); +void     dynamic_keymap_set_encoder(uint8_t layer, uint8_t encoder_id, bool clockwise, uint16_t keycode); +#endif // ENCODER_MAP_ENABLE +void dynamic_keymap_reset(void);  // These get/set the keycodes as stored in the EEPROM buffer  // Data is big-endian 16-bit values (the keycodes)  // Order is by layer/row/column diff --git a/quantum/eeconfig.c b/quantum/eeconfig.c index 14cd5887f4..0ff9996ca4 100644 --- a/quantum/eeconfig.c +++ b/quantum/eeconfig.c @@ -46,7 +46,7 @@ void eeconfig_init_quantum(void) {      eeprom_update_byte(EECONFIG_DEFAULT_LAYER, 0);      default_layer_state = 0;      eeprom_update_byte(EECONFIG_KEYMAP_LOWER_BYTE, 0); -    eeprom_update_byte(EECONFIG_KEYMAP_UPPER_BYTE, 0); +    eeprom_update_byte(EECONFIG_KEYMAP_UPPER_BYTE, 0x4);      eeprom_update_byte(EECONFIG_MOUSEKEY_ACCEL, 0);      eeprom_update_byte(EECONFIG_BACKLIGHT, 0);      eeprom_update_byte(EECONFIG_AUDIO, 0xFF); // On by default diff --git a/quantum/eeconfig.h b/quantum/eeconfig.h index f3cd1867ab..565a0dbe5b 100644 --- a/quantum/eeconfig.h +++ b/quantum/eeconfig.h @@ -21,7 +21,7 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.  #include <stdbool.h>  #ifndef EECONFIG_MAGIC_NUMBER -#    define EECONFIG_MAGIC_NUMBER (uint16_t)0xFEE9 // When changing, decrement this value to avoid future re-init issues +#    define EECONFIG_MAGIC_NUMBER (uint16_t)0xFEE8 // When changing, decrement this value to avoid future re-init issues  #endif  #define EECONFIG_MAGIC_NUMBER_OFF (uint16_t)0xFFFF diff --git a/quantum/encoder.c b/quantum/encoder.c index 438c7d8564..105bed0147 100644 --- a/quantum/encoder.c +++ b/quantum/encoder.c @@ -23,6 +23,10 @@  // for memcpy  #include <string.h> +#ifndef ENCODER_MAP_KEY_DELAY +#    define ENCODER_MAP_KEY_DELAY 2 +#endif +  #if !defined(ENCODER_RESOLUTIONS) && !defined(ENCODER_RESOLUTION)  #    define ENCODER_RESOLUTION 4  #endif @@ -31,11 +35,13 @@  #    error "No encoder pads defined by ENCODERS_PAD_A and ENCODERS_PAD_B"  #endif -#define NUMBER_OF_ENCODERS (sizeof(encoders_pad_a) / sizeof(pin_t)) -static pin_t encoders_pad_a[] = ENCODERS_PAD_A; -static pin_t encoders_pad_b[] = ENCODERS_PAD_B; +extern volatile bool isLeftHand; + +static pin_t encoders_pad_a[NUM_ENCODERS_MAX_PER_SIDE] = ENCODERS_PAD_A; +static pin_t encoders_pad_b[NUM_ENCODERS_MAX_PER_SIDE] = ENCODERS_PAD_B; +  #ifdef ENCODER_RESOLUTIONS -static uint8_t encoder_resolutions[] = ENCODER_RESOLUTIONS; +static uint8_t encoder_resolutions[NUM_ENCODERS] = ENCODER_RESOLUTIONS;  #endif  #ifndef ENCODER_DIRECTION_FLIP @@ -47,18 +53,20 @@ static uint8_t encoder_resolutions[] = ENCODER_RESOLUTIONS;  #endif  static int8_t encoder_LUT[] = {0, -1, 1, 0, 1, 0, 0, -1, -1, 0, 0, 1, 0, 1, -1, 0}; -static uint8_t encoder_state[NUMBER_OF_ENCODERS]  = {0}; -static int8_t  encoder_pulses[NUMBER_OF_ENCODERS] = {0}; +static uint8_t encoder_state[NUM_ENCODERS]  = {0}; +static int8_t  encoder_pulses[NUM_ENCODERS] = {0}; +// encoder counts +static uint8_t thisCount;  #ifdef SPLIT_KEYBOARD -// right half encoders come over as second set of encoders -static uint8_t encoder_value[NUMBER_OF_ENCODERS * 2] = {0}; -// row offsets for each hand +// encoder offsets for each hand  static uint8_t thisHand, thatHand; -#else -static uint8_t encoder_value[NUMBER_OF_ENCODERS] = {0}; +// encoder counts for each hand +static uint8_t thatCount;  #endif +static uint8_t encoder_value[NUM_ENCODERS] = {0}; +  __attribute__((weak)) void encoder_wait_pullup_charge(void) {      wait_us(100);  } @@ -72,46 +80,83 @@ __attribute__((weak)) bool encoder_update_kb(uint8_t index, bool clockwise) {  }  void encoder_init(void) { +#ifdef SPLIT_KEYBOARD +    thisHand  = isLeftHand ? 0 : NUM_ENCODERS_LEFT; +    thatHand  = NUM_ENCODERS_LEFT - thisHand; +    thisCount = isLeftHand ? NUM_ENCODERS_LEFT : NUM_ENCODERS_RIGHT; +    thatCount = isLeftHand ? NUM_ENCODERS_RIGHT : NUM_ENCODERS_LEFT; +#else // SPLIT_KEYBOARD +    thisCount                = NUM_ENCODERS; +#endif + +#ifdef ENCODER_TESTS +    // Annoying that we have to clear out values during initialisation here, but +    // because all the arrays are static locals, rerunning tests in the same +    // executable doesn't reset any of these. Kinda crappy having test-only code +    // here, but it's the simplest solution. +    memset(encoder_value, 0, sizeof(encoder_value)); +    memset(encoder_state, 0, sizeof(encoder_state)); +    memset(encoder_pulses, 0, sizeof(encoder_pulses)); +    static const pin_t encoders_pad_a_left[] = ENCODERS_PAD_A; +    static const pin_t encoders_pad_b_left[] = ENCODERS_PAD_B; +    for (uint8_t i = 0; i < thisCount; i++) { +        encoders_pad_a[i] = encoders_pad_a_left[i]; +        encoders_pad_b[i] = encoders_pad_b_left[i]; +    } +#endif +  #if defined(SPLIT_KEYBOARD) && defined(ENCODERS_PAD_A_RIGHT) && defined(ENCODERS_PAD_B_RIGHT) +    // Re-initialise the pads if it's the right-hand side      if (!isLeftHand) { -        const pin_t encoders_pad_a_right[] = ENCODERS_PAD_A_RIGHT; -        const pin_t encoders_pad_b_right[] = ENCODERS_PAD_B_RIGHT; -#    if defined(ENCODER_RESOLUTIONS_RIGHT) -        const uint8_t encoder_resolutions_right[] = ENCODER_RESOLUTIONS_RIGHT; -#    endif -        for (uint8_t i = 0; i < NUMBER_OF_ENCODERS; i++) { +        static const pin_t encoders_pad_a_right[] = ENCODERS_PAD_A_RIGHT; +        static const pin_t encoders_pad_b_right[] = ENCODERS_PAD_B_RIGHT; +        for (uint8_t i = 0; i < thisCount; i++) {              encoders_pad_a[i] = encoders_pad_a_right[i];              encoders_pad_b[i] = encoders_pad_b_right[i]; -#    if defined(ENCODER_RESOLUTIONS_RIGHT) -            encoder_resolutions[i] = encoder_resolutions_right[i]; -#    endif          }      } -#endif +#endif // defined(SPLIT_KEYBOARD) && defined(ENCODERS_PAD_A_RIGHT) && defined(ENCODERS_PAD_B_RIGHT) -    for (int i = 0; i < NUMBER_OF_ENCODERS; i++) { +    // Encoder resolutions is handled purely master-side, so concatenate the two arrays +#if defined(SPLIT_KEYBOARD) && defined(ENCODER_RESOLUTIONS) +#    if defined(ENCODER_RESOLUTIONS_RIGHT) +    static const uint8_t encoder_resolutions_right[NUM_ENCODERS_RIGHT] = ENCODER_RESOLUTIONS_RIGHT; +#    else  // defined(ENCODER_RESOLUTIONS_RIGHT) +    static const uint8_t encoder_resolutions_right[NUM_ENCODERS_RIGHT] = ENCODER_RESOLUTIONS; +#    endif // defined(ENCODER_RESOLUTIONS_RIGHT) +    for (uint8_t i = 0; i < NUM_ENCODERS_RIGHT; i++) { +        encoder_resolutions[NUM_ENCODERS_LEFT + i] = encoder_resolutions_right[i]; +    } +#endif // defined(SPLIT_KEYBOARD) && defined(ENCODER_RESOLUTIONS) + +    for (uint8_t i = 0; i < thisCount; i++) {          setPinInputHigh(encoders_pad_a[i]);          setPinInputHigh(encoders_pad_b[i]);      }      encoder_wait_pullup_charge(); -    for (int i = 0; i < NUMBER_OF_ENCODERS; i++) { +    for (uint8_t i = 0; i < thisCount; i++) {          encoder_state[i] = (readPin(encoders_pad_a[i]) << 0) | (readPin(encoders_pad_b[i]) << 1);      } +} -#ifdef SPLIT_KEYBOARD -    thisHand = isLeftHand ? 0 : NUMBER_OF_ENCODERS; -    thatHand = NUMBER_OF_ENCODERS - thisHand; -#endif +#ifdef ENCODER_MAP_ENABLE +static void encoder_exec_mapping(uint8_t index, bool clockwise) { +    // The delays below cater for Windows and its wonderful requirements. +    action_exec(clockwise ? ENCODER_CW_EVENT(index, true) : ENCODER_CCW_EVENT(index, true)); +    wait_ms(ENCODER_MAP_KEY_DELAY); +    action_exec(clockwise ? ENCODER_CW_EVENT(index, false) : ENCODER_CCW_EVENT(index, false)); +    wait_ms(ENCODER_MAP_KEY_DELAY);  } +#endif // ENCODER_MAP_ENABLE  static bool encoder_update(uint8_t index, uint8_t state) {      bool    changed = false;      uint8_t i       = index;  #ifdef ENCODER_RESOLUTIONS -    uint8_t resolution = encoder_resolutions[i]; +    const uint8_t resolution = encoder_resolutions[i];  #else -    uint8_t resolution = ENCODER_RESOLUTION; +    const uint8_t resolution = ENCODER_RESOLUTION;  #endif  #ifdef SPLIT_KEYBOARD @@ -121,12 +166,20 @@ static bool encoder_update(uint8_t index, uint8_t state) {      if (encoder_pulses[i] >= resolution) {          encoder_value[index]++;          changed = true; +#ifdef ENCODER_MAP_ENABLE +        encoder_exec_mapping(index, ENCODER_COUNTER_CLOCKWISE); +#else  // ENCODER_MAP_ENABLE          encoder_update_kb(index, ENCODER_COUNTER_CLOCKWISE); +#endif // ENCODER_MAP_ENABLE      }      if (encoder_pulses[i] <= -resolution) { // direction is arbitrary here, but this clockwise          encoder_value[index]--;          changed = true; +#ifdef ENCODER_MAP_ENABLE +        encoder_exec_mapping(index, ENCODER_CLOCKWISE); +#else  // ENCODER_MAP_ENABLE          encoder_update_kb(index, ENCODER_CLOCKWISE); +#endif // ENCODER_MAP_ENABLE      }      encoder_pulses[i] %= resolution;  #ifdef ENCODER_DEFAULT_POS @@ -139,10 +192,13 @@ static bool encoder_update(uint8_t index, uint8_t state) {  bool encoder_read(void) {      bool changed = false; -    for (uint8_t i = 0; i < NUMBER_OF_ENCODERS; i++) { -        encoder_state[i] <<= 2; -        encoder_state[i] |= (readPin(encoders_pad_a[i]) << 0) | (readPin(encoders_pad_b[i]) << 1); -        changed |= encoder_update(i, encoder_state[i]); +    for (uint8_t i = 0; i < thisCount; i++) { +        uint8_t new_status = (readPin(encoders_pad_a[i]) << 0) | (readPin(encoders_pad_b[i]) << 1); +        if ((encoder_state[i] & 0x3) != new_status) { +            encoder_state[i] <<= 2; +            encoder_state[i] |= new_status; +            changed |= encoder_update(i, encoder_state[i]); +        }      }      return changed;  } @@ -150,26 +206,34 @@ bool encoder_read(void) {  #ifdef SPLIT_KEYBOARD  void last_encoder_activity_trigger(void); -void encoder_state_raw(uint8_t* slave_state) { -    memcpy(slave_state, &encoder_value[thisHand], sizeof(uint8_t) * NUMBER_OF_ENCODERS); +void encoder_state_raw(uint8_t *slave_state) { +    memcpy(slave_state, &encoder_value[thisHand], sizeof(uint8_t) * thisCount);  } -void encoder_update_raw(uint8_t* slave_state) { +void encoder_update_raw(uint8_t *slave_state) {      bool changed = false; -    for (uint8_t i = 0; i < NUMBER_OF_ENCODERS; i++) { -        uint8_t index = i + thatHand; -        int8_t  delta = slave_state[i] - encoder_value[index]; +    for (uint8_t i = 0; i < thatCount; i++) { // Note inverted logic -- we want the opposite side +        const uint8_t index = i + thatHand; +        int8_t        delta = slave_state[i] - encoder_value[index];          while (delta > 0) {              delta--;              encoder_value[index]++;              changed = true; +#    ifdef ENCODER_MAP_ENABLE +            encoder_exec_mapping(index, ENCODER_COUNTER_CLOCKWISE); +#    else  // ENCODER_MAP_ENABLE              encoder_update_kb(index, ENCODER_COUNTER_CLOCKWISE); +#    endif // ENCODER_MAP_ENABLE          }          while (delta < 0) {              delta++;              encoder_value[index]--;              changed = true; +#    ifdef ENCODER_MAP_ENABLE +            encoder_exec_mapping(index, ENCODER_CLOCKWISE); +#    else  // ENCODER_MAP_ENABLE              encoder_update_kb(index, ENCODER_CLOCKWISE); +#    endif // ENCODER_MAP_ENABLE          }      } diff --git a/quantum/encoder.h b/quantum/encoder.h index 25dc77721d..82f95b4931 100644 --- a/quantum/encoder.h +++ b/quantum/encoder.h @@ -18,6 +18,7 @@  #pragma once  #include "quantum.h" +#include "util.h"  void encoder_init(void);  bool encoder_read(void); @@ -26,6 +27,37 @@ bool encoder_update_kb(uint8_t index, bool clockwise);  bool encoder_update_user(uint8_t index, bool clockwise);  #ifdef SPLIT_KEYBOARD +  void encoder_state_raw(uint8_t* slave_state);  void encoder_update_raw(uint8_t* slave_state); -#endif + +#    if defined(ENCODERS_PAD_A_RIGHT) +#        define NUM_ENCODERS_LEFT (sizeof(((pin_t[])ENCODERS_PAD_A)) / sizeof(pin_t)) +#        define NUM_ENCODERS_RIGHT (sizeof(((pin_t[])ENCODERS_PAD_A_RIGHT)) / sizeof(pin_t)) +#    else +#        define NUM_ENCODERS_LEFT (sizeof(((pin_t[])ENCODERS_PAD_A)) / sizeof(pin_t)) +#        define NUM_ENCODERS_RIGHT NUM_ENCODERS_LEFT +#    endif +#    define NUM_ENCODERS (NUM_ENCODERS_LEFT + NUM_ENCODERS_RIGHT) + +#else // SPLIT_KEYBOARD + +#    define NUM_ENCODERS (sizeof(((pin_t[])ENCODERS_PAD_A)) / sizeof(pin_t)) +#    define NUM_ENCODERS_LEFT NUM_ENCODERS +#    define NUM_ENCODERS_RIGHT 0 + +#endif // SPLIT_KEYBOARD + +#ifndef NUM_ENCODERS +#    define NUM_ENCODERS 0 +#    define NUM_ENCODERS_LEFT 0 +#    define NUM_ENCODERS_RIGHT 0 +#endif // NUM_ENCODERS + +#define NUM_ENCODERS_MAX_PER_SIDE MAX(NUM_ENCODERS_LEFT, NUM_ENCODERS_RIGHT) + +#ifdef ENCODER_MAP_ENABLE +#    define ENCODER_CCW_CW(ccw, cw) \ +        { (cw), (ccw) } +extern const uint16_t encoder_map[][NUM_ENCODERS][2]; +#endif // ENCODER_MAP_ENABLE diff --git a/quantum/encoder/tests/config_mock.h b/quantum/encoder/tests/config_mock.h new file mode 100644 index 0000000000..703dcaf103 --- /dev/null +++ b/quantum/encoder/tests/config_mock.h @@ -0,0 +1,22 @@ +// Copyright 2022 Nick Brassel (@tzarc) +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#define MATRIX_ROWS 1 +#define MATRIX_COLS 1 + +/* Here, "pins" from 0 to 31 are allowed. */ +#define ENCODERS_PAD_A \ +    { 0 } +#define ENCODERS_PAD_B \ +    { 1 } + +#ifdef __cplusplus +extern "C" { +#endif + +#include "mock.h" + +#ifdef __cplusplus +}; +#endif diff --git a/quantum/encoder/tests/config_mock_split_left_eq_right.h b/quantum/encoder/tests/config_mock_split_left_eq_right.h new file mode 100644 index 0000000000..c80ac4d519 --- /dev/null +++ b/quantum/encoder/tests/config_mock_split_left_eq_right.h @@ -0,0 +1,26 @@ +// Copyright 2022 Nick Brassel (@tzarc) +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#define MATRIX_ROWS 1 +#define MATRIX_COLS 1 + +/* Here, "pins" from 0 to 31 are allowed. */ +#define ENCODERS_PAD_A \ +    { 0, 2 } +#define ENCODERS_PAD_B \ +    { 1, 3 } +#define ENCODERS_PAD_A_RIGHT \ +    { 4, 6 } +#define ENCODERS_PAD_B_RIGHT \ +    { 5, 7 } + +#ifdef __cplusplus +extern "C" { +#endif + +#include "mock_split.h" + +#ifdef __cplusplus +}; +#endif diff --git a/quantum/encoder/tests/config_mock_split_left_gt_right.h b/quantum/encoder/tests/config_mock_split_left_gt_right.h new file mode 100644 index 0000000000..91d5f3d605 --- /dev/null +++ b/quantum/encoder/tests/config_mock_split_left_gt_right.h @@ -0,0 +1,26 @@ +// Copyright 2022 Nick Brassel (@tzarc) +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#define MATRIX_ROWS 1 +#define MATRIX_COLS 1 + +/* Here, "pins" from 0 to 31 are allowed. */ +#define ENCODERS_PAD_A \ +    { 0, 2, 4 } +#define ENCODERS_PAD_B \ +    { 1, 3, 5 } +#define ENCODERS_PAD_A_RIGHT \ +    { 6, 8 } +#define ENCODERS_PAD_B_RIGHT \ +    { 7, 9 } + +#ifdef __cplusplus +extern "C" { +#endif + +#include "mock_split.h" + +#ifdef __cplusplus +}; +#endif diff --git a/quantum/encoder/tests/config_mock_split_left_lt_right.h b/quantum/encoder/tests/config_mock_split_left_lt_right.h new file mode 100644 index 0000000000..4108a184a6 --- /dev/null +++ b/quantum/encoder/tests/config_mock_split_left_lt_right.h @@ -0,0 +1,26 @@ +// Copyright 2022 Nick Brassel (@tzarc) +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#define MATRIX_ROWS 1 +#define MATRIX_COLS 1 + +/* Here, "pins" from 0 to 31 are allowed. */ +#define ENCODERS_PAD_A \ +    { 0, 2 } +#define ENCODERS_PAD_B \ +    { 1, 3 } +#define ENCODERS_PAD_A_RIGHT \ +    { 4, 6, 8 } +#define ENCODERS_PAD_B_RIGHT \ +    { 5, 7, 9 } + +#ifdef __cplusplus +extern "C" { +#endif + +#include "mock_split.h" + +#ifdef __cplusplus +}; +#endif diff --git a/quantum/encoder/tests/config_mock_split_no_left.h b/quantum/encoder/tests/config_mock_split_no_left.h new file mode 100644 index 0000000000..9db7fa7e41 --- /dev/null +++ b/quantum/encoder/tests/config_mock_split_no_left.h @@ -0,0 +1,26 @@ +// Copyright 2022 Nick Brassel (@tzarc) +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#define MATRIX_ROWS 1 +#define MATRIX_COLS 1 + +/* Here, "pins" from 0 to 31 are allowed. */ +#define ENCODERS_PAD_A \ +    {} +#define ENCODERS_PAD_B \ +    {} +#define ENCODERS_PAD_A_RIGHT \ +    { 0, 2 } +#define ENCODERS_PAD_B_RIGHT \ +    { 1, 3 } + +#ifdef __cplusplus +extern "C" { +#endif + +#include "mock_split.h" + +#ifdef __cplusplus +}; +#endif diff --git a/quantum/encoder/tests/config_mock_split_no_right.h b/quantum/encoder/tests/config_mock_split_no_right.h new file mode 100644 index 0000000000..14f18015e6 --- /dev/null +++ b/quantum/encoder/tests/config_mock_split_no_right.h @@ -0,0 +1,26 @@ +// Copyright 2022 Nick Brassel (@tzarc) +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#define MATRIX_ROWS 1 +#define MATRIX_COLS 1 + +/* Here, "pins" from 0 to 31 are allowed. */ +#define ENCODERS_PAD_A \ +    { 0, 2 } +#define ENCODERS_PAD_B \ +    { 1, 3 } +#define ENCODERS_PAD_A_RIGHT \ +    {} +#define ENCODERS_PAD_B_RIGHT \ +    {} + +#ifdef __cplusplus +extern "C" { +#endif + +#include "mock_split.h" + +#ifdef __cplusplus +}; +#endif diff --git a/quantum/encoder/tests/encoder_tests.cpp b/quantum/encoder/tests/encoder_tests.cpp index 1888fdab8d..b7c18aeec0 100644 --- a/quantum/encoder/tests/encoder_tests.cpp +++ b/quantum/encoder/tests/encoder_tests.cpp @@ -30,12 +30,12 @@ struct update {      bool   clockwise;  }; -uint8_t uidx = 0; +uint8_t updates_array_idx = 0;  update  updates[32];  bool encoder_update_kb(uint8_t index, bool clockwise) { -    updates[uidx % 32] = {index, clockwise}; -    uidx++; +    updates[updates_array_idx % 32] = {index, clockwise}; +    updates_array_idx++;      return true;  } @@ -47,15 +47,15 @@ bool setAndRead(pin_t pin, bool val) {  class EncoderTest : public ::testing::Test {};  TEST_F(EncoderTest, TestInit) { -    uidx = 0; +    updates_array_idx = 0;      encoder_init();      EXPECT_EQ(pinIsInputHigh[0], true);      EXPECT_EQ(pinIsInputHigh[1], true); -    EXPECT_EQ(uidx, 0); +    EXPECT_EQ(updates_array_idx, 0);  }  TEST_F(EncoderTest, TestOneClockwise) { -    uidx = 0; +    updates_array_idx = 0;      encoder_init();      // send 4 pulses. with resolution 4, that's one step and we should get 1 update.      setAndRead(0, false); @@ -63,26 +63,26 @@ TEST_F(EncoderTest, TestOneClockwise) {      setAndRead(0, true);      setAndRead(1, true); -    EXPECT_EQ(uidx, 1); +    EXPECT_EQ(updates_array_idx, 1);      EXPECT_EQ(updates[0].index, 0);      EXPECT_EQ(updates[0].clockwise, true);  }  TEST_F(EncoderTest, TestOneCounterClockwise) { -    uidx = 0; +    updates_array_idx = 0;      encoder_init();      setAndRead(1, false);      setAndRead(0, false);      setAndRead(1, true);      setAndRead(0, true); -    EXPECT_EQ(uidx, 1); +    EXPECT_EQ(updates_array_idx, 1);      EXPECT_EQ(updates[0].index, 0);      EXPECT_EQ(updates[0].clockwise, false);  }  TEST_F(EncoderTest, TestTwoClockwiseOneCC) { -    uidx = 0; +    updates_array_idx = 0;      encoder_init();      setAndRead(0, false);      setAndRead(1, false); @@ -97,7 +97,7 @@ TEST_F(EncoderTest, TestTwoClockwiseOneCC) {      setAndRead(1, true);      setAndRead(0, true); -    EXPECT_EQ(uidx, 3); +    EXPECT_EQ(updates_array_idx, 3);      EXPECT_EQ(updates[0].index, 0);      EXPECT_EQ(updates[0].clockwise, true);      EXPECT_EQ(updates[1].index, 0); @@ -107,38 +107,38 @@ TEST_F(EncoderTest, TestTwoClockwiseOneCC) {  }  TEST_F(EncoderTest, TestNoEarly) { -    uidx = 0; +    updates_array_idx = 0;      encoder_init();      // send 3 pulses. with resolution 4, that's not enough for a step.      setAndRead(0, false);      setAndRead(1, false);      setAndRead(0, true); -    EXPECT_EQ(uidx, 0); +    EXPECT_EQ(updates_array_idx, 0);      // now send last pulse      setAndRead(1, true); -    EXPECT_EQ(uidx, 1); +    EXPECT_EQ(updates_array_idx, 1);      EXPECT_EQ(updates[0].index, 0);      EXPECT_EQ(updates[0].clockwise, true);  }  TEST_F(EncoderTest, TestHalfway) { -    uidx = 0; +    updates_array_idx = 0;      encoder_init();      // go halfway      setAndRead(0, false);      setAndRead(1, false); -    EXPECT_EQ(uidx, 0); +    EXPECT_EQ(updates_array_idx, 0);      // back off      setAndRead(1, true);      setAndRead(0, true); -    EXPECT_EQ(uidx, 0); +    EXPECT_EQ(updates_array_idx, 0);      // go all the way      setAndRead(0, false);      setAndRead(1, false);      setAndRead(0, true);      setAndRead(1, true);      // should result in 1 update -    EXPECT_EQ(uidx, 1); +    EXPECT_EQ(updates_array_idx, 1);      EXPECT_EQ(updates[0].index, 0);      EXPECT_EQ(updates[0].clockwise, true);  } diff --git a/quantum/encoder/tests/encoder_tests_split_left_eq_right.cpp b/quantum/encoder/tests/encoder_tests_split_left_eq_right.cpp new file mode 100644 index 0000000000..916e47b185 --- /dev/null +++ b/quantum/encoder/tests/encoder_tests_split_left_eq_right.cpp @@ -0,0 +1,135 @@ +/* Copyright 2021 Balz Guenat + * + * 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 + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program.  If not, see <http://www.gnu.org/licenses/>. + */ + +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include <vector> +#include <algorithm> +#include <stdio.h> + +extern "C" { +#include "encoder.h" +#include "encoder/tests/mock_split.h" +} + +struct update { +    int8_t index; +    bool   clockwise; +}; + +uint8_t updates_array_idx = 0; +update  updates[32]; + +bool isLeftHand; + +bool encoder_update_kb(uint8_t index, bool clockwise) { +    if (!isLeftHand) { +        // this method has no effect on slave half +        printf("ignoring update on right hand (%d,%s)\n", index, clockwise ? "CW" : "CC"); +        return true; +    } +    updates[updates_array_idx % 32] = {index, clockwise}; +    updates_array_idx++; +    return true; +} + +bool setAndRead(pin_t pin, bool val) { +    setPin(pin, val); +    return encoder_read(); +} + +class EncoderSplitTestLeftEqRight : public ::testing::Test { +   protected: +    void SetUp() override { +        updates_array_idx = 0; +        for (int i = 0; i < 32; i++) { +            pinIsInputHigh[i] = 0; +            pins[i]           = 0; +        } +    } +}; + +TEST_F(EncoderSplitTestLeftEqRight, TestInitLeft) { +    isLeftHand = true; +    encoder_init(); +    EXPECT_EQ(pinIsInputHigh[0], true); +    EXPECT_EQ(pinIsInputHigh[1], true); +    EXPECT_EQ(pinIsInputHigh[2], true); +    EXPECT_EQ(pinIsInputHigh[3], true); +    EXPECT_EQ(pinIsInputHigh[4], false); +    EXPECT_EQ(pinIsInputHigh[5], false); +    EXPECT_EQ(pinIsInputHigh[6], false); +    EXPECT_EQ(pinIsInputHigh[7], false); +    EXPECT_EQ(updates_array_idx, 0); // no updates received +} + +TEST_F(EncoderSplitTestLeftEqRight, TestInitRight) { +    isLeftHand = false; +    encoder_init(); +    EXPECT_EQ(pinIsInputHigh[0], false); +    EXPECT_EQ(pinIsInputHigh[1], false); +    EXPECT_EQ(pinIsInputHigh[2], false); +    EXPECT_EQ(pinIsInputHigh[3], false); +    EXPECT_EQ(pinIsInputHigh[4], true); +    EXPECT_EQ(pinIsInputHigh[5], true); +    EXPECT_EQ(pinIsInputHigh[6], true); +    EXPECT_EQ(pinIsInputHigh[7], true); +    EXPECT_EQ(updates_array_idx, 0); // no updates received +} + +TEST_F(EncoderSplitTestLeftEqRight, TestOneClockwiseLeft) { +    isLeftHand = true; +    encoder_init(); +    // send 4 pulses. with resolution 4, that's one step and we should get 1 update. +    setAndRead(0, false); +    setAndRead(1, false); +    setAndRead(0, true); +    setAndRead(1, true); + +    EXPECT_EQ(updates_array_idx, 1); // one update received +    EXPECT_EQ(updates[0].index, 0); +    EXPECT_EQ(updates[0].clockwise, true); +} + +TEST_F(EncoderSplitTestLeftEqRight, TestOneClockwiseRightSent) { +    isLeftHand = false; +    encoder_init(); +    // send 4 pulses. with resolution 4, that's one step and we should get 1 update. +    setAndRead(6, false); +    setAndRead(7, false); +    setAndRead(6, true); +    setAndRead(7, true); + +    uint8_t slave_state[32] = {0}; +    encoder_state_raw(slave_state); + +    EXPECT_EQ(slave_state[0], 0); +    EXPECT_EQ(slave_state[1], 0xFF); +} + +TEST_F(EncoderSplitTestLeftEqRight, TestMultipleEncodersRightReceived) { +    isLeftHand = true; +    encoder_init(); + +    uint8_t slave_state[32] = {1, 0xFF}; // First right encoder is CCW, Second right encoder CW +    encoder_update_raw(slave_state); + +    EXPECT_EQ(updates_array_idx, 2); // two updates received, one for each changed item on the right side +    EXPECT_EQ(updates[0].index, 2); +    EXPECT_EQ(updates[0].clockwise, false); +    EXPECT_EQ(updates[1].index, 3); +    EXPECT_EQ(updates[1].clockwise, true); +} diff --git a/quantum/encoder/tests/encoder_tests_split_left_gt_right.cpp b/quantum/encoder/tests/encoder_tests_split_left_gt_right.cpp new file mode 100644 index 0000000000..7b64bb2981 --- /dev/null +++ b/quantum/encoder/tests/encoder_tests_split_left_gt_right.cpp @@ -0,0 +1,139 @@ +/* Copyright 2021 Balz Guenat + * + * 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 + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program.  If not, see <http://www.gnu.org/licenses/>. + */ + +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include <vector> +#include <algorithm> +#include <stdio.h> + +extern "C" { +#include "encoder.h" +#include "encoder/tests/mock_split.h" +} + +struct update { +    int8_t index; +    bool   clockwise; +}; + +uint8_t updates_array_idx = 0; +update  updates[32]; + +bool isLeftHand; + +bool encoder_update_kb(uint8_t index, bool clockwise) { +    if (!isLeftHand) { +        // this method has no effect on slave half +        printf("ignoring update on right hand (%d,%s)\n", index, clockwise ? "CW" : "CC"); +        return true; +    } +    updates[updates_array_idx % 32] = {index, clockwise}; +    updates_array_idx++; +    return true; +} + +bool setAndRead(pin_t pin, bool val) { +    setPin(pin, val); +    return encoder_read(); +} + +class EncoderSplitTestLeftGreaterThanRight : public ::testing::Test { +   protected: +    void SetUp() override { +        updates_array_idx = 0; +        for (int i = 0; i < 32; i++) { +            pinIsInputHigh[i] = 0; +            pins[i]           = 0; +        } +    } +}; + +TEST_F(EncoderSplitTestLeftGreaterThanRight, TestInitLeft) { +    isLeftHand = true; +    encoder_init(); +    EXPECT_EQ(pinIsInputHigh[0], true); +    EXPECT_EQ(pinIsInputHigh[1], true); +    EXPECT_EQ(pinIsInputHigh[2], true); +    EXPECT_EQ(pinIsInputHigh[3], true); +    EXPECT_EQ(pinIsInputHigh[4], true); +    EXPECT_EQ(pinIsInputHigh[5], true); +    EXPECT_EQ(pinIsInputHigh[6], false); +    EXPECT_EQ(pinIsInputHigh[7], false); +    EXPECT_EQ(pinIsInputHigh[8], false); +    EXPECT_EQ(pinIsInputHigh[9], false); +    EXPECT_EQ(updates_array_idx, 0); // no updates received +} + +TEST_F(EncoderSplitTestLeftGreaterThanRight, TestInitRight) { +    isLeftHand = false; +    encoder_init(); +    EXPECT_EQ(pinIsInputHigh[0], false); +    EXPECT_EQ(pinIsInputHigh[1], false); +    EXPECT_EQ(pinIsInputHigh[2], false); +    EXPECT_EQ(pinIsInputHigh[3], false); +    EXPECT_EQ(pinIsInputHigh[4], false); +    EXPECT_EQ(pinIsInputHigh[5], false); +    EXPECT_EQ(pinIsInputHigh[6], true); +    EXPECT_EQ(pinIsInputHigh[7], true); +    EXPECT_EQ(pinIsInputHigh[8], true); +    EXPECT_EQ(pinIsInputHigh[9], true); +    EXPECT_EQ(updates_array_idx, 0); // no updates received +} + +TEST_F(EncoderSplitTestLeftGreaterThanRight, TestOneClockwiseLeft) { +    isLeftHand = true; +    encoder_init(); +    // send 4 pulses. with resolution 4, that's one step and we should get 1 update. +    setAndRead(0, false); +    setAndRead(1, false); +    setAndRead(0, true); +    setAndRead(1, true); + +    EXPECT_EQ(updates_array_idx, 1); // one update received +    EXPECT_EQ(updates[0].index, 0); +    EXPECT_EQ(updates[0].clockwise, true); +} + +TEST_F(EncoderSplitTestLeftGreaterThanRight, TestOneClockwiseRightSent) { +    isLeftHand = false; +    encoder_init(); +    // send 4 pulses. with resolution 4, that's one step and we should get 1 update. +    setAndRead(6, false); +    setAndRead(7, false); +    setAndRead(6, true); +    setAndRead(7, true); + +    uint8_t slave_state[32] = {0}; +    encoder_state_raw(slave_state); + +    EXPECT_EQ(slave_state[0], 0xFF); +    EXPECT_EQ(slave_state[1], 0); +} + +TEST_F(EncoderSplitTestLeftGreaterThanRight, TestMultipleEncodersRightReceived) { +    isLeftHand = true; +    encoder_init(); + +    uint8_t slave_state[32] = {1, 0xFF}; // First right encoder is CCW, Second right encoder no change, third right encoder CW +    encoder_update_raw(slave_state); + +    EXPECT_EQ(updates_array_idx, 2); // two updates received, one for each changed item on the right side +    EXPECT_EQ(updates[0].index, 3); +    EXPECT_EQ(updates[0].clockwise, false); +    EXPECT_EQ(updates[1].index, 4); +    EXPECT_EQ(updates[1].clockwise, true); +} diff --git a/quantum/encoder/tests/encoder_tests_split_left_lt_right.cpp b/quantum/encoder/tests/encoder_tests_split_left_lt_right.cpp new file mode 100644 index 0000000000..a6519c5762 --- /dev/null +++ b/quantum/encoder/tests/encoder_tests_split_left_lt_right.cpp @@ -0,0 +1,139 @@ +/* Copyright 2021 Balz Guenat + * + * 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 + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program.  If not, see <http://www.gnu.org/licenses/>. + */ + +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include <vector> +#include <algorithm> +#include <stdio.h> + +extern "C" { +#include "encoder.h" +#include "encoder/tests/mock_split.h" +} + +struct update { +    int8_t index; +    bool   clockwise; +}; + +uint8_t updates_array_idx = 0; +update  updates[32]; + +bool isLeftHand; + +bool encoder_update_kb(uint8_t index, bool clockwise) { +    if (!isLeftHand) { +        // this method has no effect on slave half +        printf("ignoring update on right hand (%d,%s)\n", index, clockwise ? "CW" : "CC"); +        return true; +    } +    updates[updates_array_idx % 32] = {index, clockwise}; +    updates_array_idx++; +    return true; +} + +bool setAndRead(pin_t pin, bool val) { +    setPin(pin, val); +    return encoder_read(); +} + +class EncoderSplitTestLeftLessThanRight : public ::testing::Test { +   protected: +    void SetUp() override { +        updates_array_idx = 0; +        for (int i = 0; i < 32; i++) { +            pinIsInputHigh[i] = 0; +            pins[i]           = 0; +        } +    } +}; + +TEST_F(EncoderSplitTestLeftLessThanRight, TestInitLeft) { +    isLeftHand = true; +    encoder_init(); +    EXPECT_EQ(pinIsInputHigh[0], true); +    EXPECT_EQ(pinIsInputHigh[1], true); +    EXPECT_EQ(pinIsInputHigh[2], true); +    EXPECT_EQ(pinIsInputHigh[3], true); +    EXPECT_EQ(pinIsInputHigh[4], false); +    EXPECT_EQ(pinIsInputHigh[5], false); +    EXPECT_EQ(pinIsInputHigh[6], false); +    EXPECT_EQ(pinIsInputHigh[7], false); +    EXPECT_EQ(pinIsInputHigh[8], false); +    EXPECT_EQ(pinIsInputHigh[9], false); +    EXPECT_EQ(updates_array_idx, 0); // no updates received +} + +TEST_F(EncoderSplitTestLeftLessThanRight, TestInitRight) { +    isLeftHand = false; +    encoder_init(); +    EXPECT_EQ(pinIsInputHigh[0], false); +    EXPECT_EQ(pinIsInputHigh[1], false); +    EXPECT_EQ(pinIsInputHigh[2], false); +    EXPECT_EQ(pinIsInputHigh[3], false); +    EXPECT_EQ(pinIsInputHigh[4], true); +    EXPECT_EQ(pinIsInputHigh[5], true); +    EXPECT_EQ(pinIsInputHigh[6], true); +    EXPECT_EQ(pinIsInputHigh[7], true); +    EXPECT_EQ(pinIsInputHigh[8], true); +    EXPECT_EQ(pinIsInputHigh[9], true); +    EXPECT_EQ(updates_array_idx, 0); // no updates received +} + +TEST_F(EncoderSplitTestLeftLessThanRight, TestOneClockwiseLeft) { +    isLeftHand = true; +    encoder_init(); +    // send 4 pulses. with resolution 4, that's one step and we should get 1 update. +    setAndRead(0, false); +    setAndRead(1, false); +    setAndRead(0, true); +    setAndRead(1, true); + +    EXPECT_EQ(updates_array_idx, 1); // one update received +    EXPECT_EQ(updates[0].index, 0); +    EXPECT_EQ(updates[0].clockwise, true); +} + +TEST_F(EncoderSplitTestLeftLessThanRight, TestOneClockwiseRightSent) { +    isLeftHand = false; +    encoder_init(); +    // send 4 pulses. with resolution 4, that's one step and we should get 1 update. +    setAndRead(6, false); +    setAndRead(7, false); +    setAndRead(6, true); +    setAndRead(7, true); + +    uint8_t slave_state[32] = {0}; +    encoder_state_raw(slave_state); + +    EXPECT_EQ(slave_state[0], 0); +    EXPECT_EQ(slave_state[1], 0xFF); +} + +TEST_F(EncoderSplitTestLeftLessThanRight, TestMultipleEncodersRightReceived) { +    isLeftHand = true; +    encoder_init(); + +    uint8_t slave_state[32] = {1, 0, 0xFF}; // First right encoder is CCW, Second right encoder no change, third right encoder CW +    encoder_update_raw(slave_state); + +    EXPECT_EQ(updates_array_idx, 2); // two updates received, one for each changed item on the right side +    EXPECT_EQ(updates[0].index, 2); +    EXPECT_EQ(updates[0].clockwise, false); +    EXPECT_EQ(updates[1].index, 4); +    EXPECT_EQ(updates[1].clockwise, true); +} diff --git a/quantum/encoder/tests/encoder_tests_split.cpp b/quantum/encoder/tests/encoder_tests_split_no_left.cpp index 25e52c83f9..b6b2d7e2d1 100644 --- a/quantum/encoder/tests/encoder_tests_split.cpp +++ b/quantum/encoder/tests/encoder_tests_split_no_left.cpp @@ -30,7 +30,7 @@ struct update {      bool   clockwise;  }; -uint8_t uidx = 0; +uint8_t updates_array_idx = 0;  update  updates[32];  bool isLeftHand; @@ -41,8 +41,8 @@ bool encoder_update_kb(uint8_t index, bool clockwise) {          printf("ignoring update on right hand (%d,%s)\n", index, clockwise ? "CW" : "CC");          return true;      } -    updates[uidx % 32] = {index, clockwise}; -    uidx++; +    updates[updates_array_idx % 32] = {index, clockwise}; +    updates_array_idx++;      return true;  } @@ -51,10 +51,10 @@ bool setAndRead(pin_t pin, bool val) {      return encoder_read();  } -class EncoderTest : public ::testing::Test { +class EncoderSplitTestNoLeft : public ::testing::Test {     protected:      void SetUp() override { -        uidx = 0; +        updates_array_idx = 0;          for (int i = 0; i < 32; i++) {              pinIsInputHigh[i] = 0;              pins[i]           = 0; @@ -62,27 +62,27 @@ class EncoderTest : public ::testing::Test {      }  }; -TEST_F(EncoderTest, TestInitLeft) { +TEST_F(EncoderSplitTestNoLeft, TestInitLeft) {      isLeftHand = true;      encoder_init(); -    EXPECT_EQ(pinIsInputHigh[0], true); -    EXPECT_EQ(pinIsInputHigh[1], true); +    EXPECT_EQ(pinIsInputHigh[0], false); +    EXPECT_EQ(pinIsInputHigh[1], false);      EXPECT_EQ(pinIsInputHigh[2], false);      EXPECT_EQ(pinIsInputHigh[3], false); -    EXPECT_EQ(uidx, 0); +    EXPECT_EQ(updates_array_idx, 0); // no updates received  } -TEST_F(EncoderTest, TestInitRight) { +TEST_F(EncoderSplitTestNoLeft, TestInitRight) {      isLeftHand = false;      encoder_init(); -    EXPECT_EQ(pinIsInputHigh[0], false); -    EXPECT_EQ(pinIsInputHigh[1], false); +    EXPECT_EQ(pinIsInputHigh[0], true); +    EXPECT_EQ(pinIsInputHigh[1], true);      EXPECT_EQ(pinIsInputHigh[2], true);      EXPECT_EQ(pinIsInputHigh[3], true); -    EXPECT_EQ(uidx, 0); +    EXPECT_EQ(updates_array_idx, 0); // no updates received  } -TEST_F(EncoderTest, TestOneClockwiseLeft) { +TEST_F(EncoderSplitTestNoLeft, TestOneClockwiseLeft) {      isLeftHand = true;      encoder_init();      // send 4 pulses. with resolution 4, that's one step and we should get 1 update. @@ -91,12 +91,10 @@ TEST_F(EncoderTest, TestOneClockwiseLeft) {      setAndRead(0, true);      setAndRead(1, true); -    EXPECT_EQ(uidx, 1); -    EXPECT_EQ(updates[0].index, 0); -    EXPECT_EQ(updates[0].clockwise, true); +    EXPECT_EQ(updates_array_idx, 0); // no updates received  } -TEST_F(EncoderTest, TestOneClockwiseRightSent) { +TEST_F(EncoderSplitTestNoLeft, TestOneClockwiseRightSent) {      isLeftHand = false;      encoder_init();      // send 4 pulses. with resolution 4, that's one step and we should get 1 update. @@ -105,39 +103,23 @@ TEST_F(EncoderTest, TestOneClockwiseRightSent) {      setAndRead(2, true);      setAndRead(3, true); -    uint8_t slave_state[2] = {0}; +    uint8_t slave_state[32] = {0};      encoder_state_raw(slave_state); -    EXPECT_EQ((int8_t)slave_state[0], -1); +    EXPECT_EQ(slave_state[0], 0); +    EXPECT_EQ(slave_state[1], 0xFF);  } -/* this test will not work after the previous test. - * this is due to encoder_value[1] already being set to -1 when simulating the right half. - * When we now receive this update acting as the left half, there is no change. - * This is hard to mock, as the static values inside encoder.c normally exist twice, once on each half, - * but here, they only exist once. - */ - -// TEST_F(EncoderTest, TestOneClockwiseRightReceived) { -//     isLeftHand = true; -//     encoder_init(); - -//     uint8_t slave_state[2] = {255, 0}; -//     encoder_update_raw(slave_state); - -//     EXPECT_EQ(uidx, 1); -//     EXPECT_EQ(updates[0].index, 1); -//     EXPECT_EQ(updates[0].clockwise, true); -// } - -TEST_F(EncoderTest, TestOneCounterClockwiseRightReceived) { +TEST_F(EncoderSplitTestNoLeft, TestMultipleEncodersRightReceived) {      isLeftHand = true;      encoder_init(); -    uint8_t slave_state[2] = {0, 0}; +    uint8_t slave_state[32] = {1, 0xFF}; // First right encoder is CCW, Second right encoder no change, third right encoder CW      encoder_update_raw(slave_state); -    EXPECT_EQ(uidx, 1); -    EXPECT_EQ(updates[0].index, 1); +    EXPECT_EQ(updates_array_idx, 2); // two updates received, one for each changed item on the right side +    EXPECT_EQ(updates[0].index, 0);      EXPECT_EQ(updates[0].clockwise, false); +    EXPECT_EQ(updates[1].index, 1); +    EXPECT_EQ(updates[1].clockwise, true);  } diff --git a/quantum/encoder/tests/encoder_tests_split_no_right.cpp b/quantum/encoder/tests/encoder_tests_split_no_right.cpp new file mode 100644 index 0000000000..fa0a7c18a8 --- /dev/null +++ b/quantum/encoder/tests/encoder_tests_split_no_right.cpp @@ -0,0 +1,118 @@ +/* Copyright 2021 Balz Guenat + * + * 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 + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program.  If not, see <http://www.gnu.org/licenses/>. + */ + +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include <vector> +#include <algorithm> +#include <stdio.h> + +extern "C" { +#include "encoder.h" +#include "encoder/tests/mock_split.h" +} + +struct update { +    int8_t index; +    bool   clockwise; +}; + +uint8_t updates_array_idx = 0; +update  updates[32]; + +bool isLeftHand; + +bool encoder_update_kb(uint8_t index, bool clockwise) { +    if (!isLeftHand) { +        // this method has no effect on slave half +        printf("ignoring update on right hand (%d,%s)\n", index, clockwise ? "CW" : "CC"); +        return true; +    } +    updates[updates_array_idx % 32] = {index, clockwise}; +    updates_array_idx++; +    return true; +} + +bool setAndRead(pin_t pin, bool val) { +    setPin(pin, val); +    return encoder_read(); +} + +class EncoderSplitTestNoRight : public ::testing::Test { +   protected: +    void SetUp() override { +        updates_array_idx = 0; +        for (int i = 0; i < 32; i++) { +            pinIsInputHigh[i] = 0; +            pins[i]           = 0; +        } +    } +}; + +TEST_F(EncoderSplitTestNoRight, TestInitLeft) { +    isLeftHand = true; +    encoder_init(); +    EXPECT_EQ(pinIsInputHigh[0], true); +    EXPECT_EQ(pinIsInputHigh[1], true); +    EXPECT_EQ(pinIsInputHigh[2], true); +    EXPECT_EQ(pinIsInputHigh[3], true); +    EXPECT_EQ(updates_array_idx, 0); // no updates received +} + +TEST_F(EncoderSplitTestNoRight, TestInitRight) { +    isLeftHand = false; +    encoder_init(); +    EXPECT_EQ(pinIsInputHigh[0], false); +    EXPECT_EQ(pinIsInputHigh[1], false); +    EXPECT_EQ(pinIsInputHigh[2], false); +    EXPECT_EQ(pinIsInputHigh[3], false); +    EXPECT_EQ(updates_array_idx, 0); // no updates received +} + +TEST_F(EncoderSplitTestNoRight, TestOneClockwiseLeft) { +    isLeftHand = true; +    encoder_init(); +    // send 4 pulses. with resolution 4, that's one step and we should get 1 update. +    setAndRead(0, false); +    setAndRead(1, false); +    setAndRead(0, true); +    setAndRead(1, true); + +    EXPECT_EQ(updates_array_idx, 1); // one updates received +    EXPECT_EQ(updates[0].index, 0); +    EXPECT_EQ(updates[0].clockwise, true); +} + +TEST_F(EncoderSplitTestNoRight, TestOneClockwiseRightSent) { +    isLeftHand = false; +    encoder_init(); + +    uint8_t slave_state[32] = {0xAA, 0xAA}; +    encoder_state_raw(slave_state); + +    EXPECT_EQ(slave_state[0], 0xAA); +    EXPECT_EQ(slave_state[1], 0xAA); +} + +TEST_F(EncoderSplitTestNoRight, TestMultipleEncodersRightReceived) { +    isLeftHand = true; +    encoder_init(); + +    uint8_t slave_state[32] = {1, 0xFF}; // These values would trigger updates if there were encoders on the other side +    encoder_update_raw(slave_state); + +    EXPECT_EQ(updates_array_idx, 0); // no updates received -- no right-hand encoders +} diff --git a/quantum/encoder/tests/mock.h b/quantum/encoder/tests/mock.h index dbc25a0846..80c336b5ef 100644 --- a/quantum/encoder/tests/mock.h +++ b/quantum/encoder/tests/mock.h @@ -19,12 +19,6 @@  #include <stdint.h>  #include <stdbool.h> -/* Here, "pins" from 0 to 31 are allowed. */ -#define ENCODERS_PAD_A \ -    { 0 } -#define ENCODERS_PAD_B \ -    { 1 } -  typedef uint8_t pin_t;  extern bool pins[]; diff --git a/quantum/encoder/tests/mock_split.h b/quantum/encoder/tests/mock_split.h index 0ae62652f9..2fc12f1830 100644 --- a/quantum/encoder/tests/mock_split.h +++ b/quantum/encoder/tests/mock_split.h @@ -20,20 +20,10 @@  #include <stdbool.h>  #define SPLIT_KEYBOARD -/* Here, "pins" from 0 to 31 are allowed. */ -#define ENCODERS_PAD_A \ -    { 0 } -#define ENCODERS_PAD_B \ -    { 1 } -#define ENCODERS_PAD_A_RIGHT \ -    { 2 } -#define ENCODERS_PAD_B_RIGHT \ -    { 3 } -  typedef uint8_t pin_t; -extern bool     isLeftHand; -void            encoder_state_raw(uint8_t* slave_state); -void            encoder_update_raw(uint8_t* slave_state); + +void encoder_state_raw(uint8_t* slave_state); +void encoder_update_raw(uint8_t* slave_state);  extern bool pins[];  extern bool pinIsInputHigh[]; diff --git a/quantum/encoder/tests/rules.mk b/quantum/encoder/tests/rules.mk index b826ce3aed..6a2611952c 100644 --- a/quantum/encoder/tests/rules.mk +++ b/quantum/encoder/tests/rules.mk @@ -1,13 +1,58 @@ -encoder_DEFS := -DENCODER_MOCK_SINGLE +encoder_DEFS := -DENCODER_TESTS -DENCODER_ENABLE -DENCODER_MOCK_SINGLE +encoder_CONFIG := $(QUANTUM_PATH)/encoder/tests/config_mock.h  encoder_SRC := \ +	platforms/test/timer.c \  	$(QUANTUM_PATH)/encoder/tests/mock.c \  	$(QUANTUM_PATH)/encoder/tests/encoder_tests.cpp \  	$(QUANTUM_PATH)/encoder.c -encoder_split_DEFS := -DENCODER_MOCK_SPLIT +encoder_split_left_eq_right_DEFS := -DENCODER_TESTS -DENCODER_ENABLE -DENCODER_MOCK_SPLIT +encoder_split_left_eq_right_INC := $(QUANTUM_PATH)/split_common +encoder_split_left_eq_right_CONFIG := $(QUANTUM_PATH)/encoder/tests/config_mock_split_left_eq_right.h -encoder_split_SRC := \ +encoder_split_left_eq_right_SRC := \ +	platforms/test/timer.c \  	$(QUANTUM_PATH)/encoder/tests/mock_split.c \ -	$(QUANTUM_PATH)/encoder/tests/encoder_tests_split.cpp \ +	$(QUANTUM_PATH)/encoder/tests/encoder_tests_split_left_eq_right.cpp \ +	$(QUANTUM_PATH)/encoder.c + +encoder_split_left_gt_right_DEFS := -DENCODER_TESTS -DENCODER_ENABLE -DENCODER_MOCK_SPLIT +encoder_split_left_gt_right_INC := $(QUANTUM_PATH)/split_common +encoder_split_left_gt_right_CONFIG := $(QUANTUM_PATH)/encoder/tests/config_mock_split_left_gt_right.h + +encoder_split_left_gt_right_SRC := \ +	platforms/test/timer.c \ +	$(QUANTUM_PATH)/encoder/tests/mock_split.c \ +	$(QUANTUM_PATH)/encoder/tests/encoder_tests_split_left_gt_right.cpp \ +	$(QUANTUM_PATH)/encoder.c + +encoder_split_left_lt_right_DEFS := -DENCODER_TESTS -DENCODER_ENABLE -DENCODER_MOCK_SPLIT +encoder_split_left_lt_right_INC := $(QUANTUM_PATH)/split_common +encoder_split_left_lt_right_CONFIG := $(QUANTUM_PATH)/encoder/tests/config_mock_split_left_lt_right.h + +encoder_split_left_lt_right_SRC := \ +	platforms/test/timer.c \ +	$(QUANTUM_PATH)/encoder/tests/mock_split.c \ +	$(QUANTUM_PATH)/encoder/tests/encoder_tests_split_left_lt_right.cpp \ +	$(QUANTUM_PATH)/encoder.c + +encoder_split_no_left_DEFS := -DENCODER_TESTS -DENCODER_ENABLE -DENCODER_MOCK_SPLIT +encoder_split_no_left_INC := $(QUANTUM_PATH)/split_common +encoder_split_no_left_CONFIG := $(QUANTUM_PATH)/encoder/tests/config_mock_split_no_left.h + +encoder_split_no_left_SRC := \ +	platforms/test/timer.c \ +	$(QUANTUM_PATH)/encoder/tests/mock_split.c \ +	$(QUANTUM_PATH)/encoder/tests/encoder_tests_split_no_left.cpp \ +	$(QUANTUM_PATH)/encoder.c + +encoder_split_no_right_DEFS := -DENCODER_TESTS -DENCODER_ENABLE -DENCODER_MOCK_SPLIT +encoder_split_no_right_INC := $(QUANTUM_PATH)/split_common +encoder_split_no_right_CONFIG := $(QUANTUM_PATH)/encoder/tests/config_mock_split_no_right.h + +encoder_split_no_right_SRC := \ +	platforms/test/timer.c \ +	$(QUANTUM_PATH)/encoder/tests/mock_split.c \ +	$(QUANTUM_PATH)/encoder/tests/encoder_tests_split_no_right.cpp \  	$(QUANTUM_PATH)/encoder.c diff --git a/quantum/encoder/tests/testlist.mk b/quantum/encoder/tests/testlist.mk index 1be9f4a054..6b2fd84d96 100644 --- a/quantum/encoder/tests/testlist.mk +++ b/quantum/encoder/tests/testlist.mk @@ -1,3 +1,7 @@  TEST_LIST += \  	encoder \ -	encoder_split +	encoder_split_left_eq_right \ +	encoder_split_left_gt_right \ +	encoder_split_left_lt_right \ +	encoder_split_no_left \ +	encoder_split_no_right diff --git a/quantum/joystick.c b/quantum/joystick.c index 7b87201aef..86b2c64036 100644 --- a/quantum/joystick.c +++ b/quantum/joystick.c @@ -1,13 +1,38 @@  #include "joystick.h" -joystick_t joystick_status = {.buttons = {0}, -                              .axes = -                                  { +// clang-format off +joystick_t joystick_status = { +    .buttons = {0}, +    .axes = {  #if JOYSTICK_AXES_COUNT > 0 -                                      0 +        0  #endif -                                  }, -                              .status = 0}; +    }, +    .status = 0 +}; +// clang-format on  // array defining the reading of analog values for each axis  __attribute__((weak)) joystick_config_t joystick_axes[JOYSTICK_AXES_COUNT] = {}; + +// to be implemented in the hid protocol library +void send_joystick_packet(joystick_t *joystick); + +void joystick_flush(void) { +    if ((joystick_status.status & JS_UPDATED) > 0) { +        send_joystick_packet(&joystick_status); +        joystick_status.status &= ~JS_UPDATED; +    } +} + +void register_joystick_button(uint8_t button) { +    joystick_status.buttons[button / 8] |= 1 << (button % 8); +    joystick_status.status |= JS_UPDATED; +    joystick_flush(); +} + +void unregister_joystick_button(uint8_t button) { +    joystick_status.buttons[button / 8] &= ~(1 << (button % 8)); +    joystick_status.status |= JS_UPDATED; +    joystick_flush(); +} diff --git a/quantum/joystick.h b/quantum/joystick.h index 9156491aca..002df3a6d9 100644 --- a/quantum/joystick.h +++ b/quantum/joystick.h @@ -1,8 +1,7 @@  #pragma once -#include "quantum.h" -  #include <stdint.h> +#include "gpio.h"  #ifndef JOYSTICK_BUTTON_COUNT  #    define JOYSTICK_BUTTON_COUNT 8 @@ -58,5 +57,7 @@ typedef struct {  extern joystick_t joystick_status; -// to be implemented in the hid protocol library -void send_joystick_packet(joystick_t *joystick); +void joystick_flush(void); + +void register_joystick_button(uint8_t button); +void unregister_joystick_button(uint8_t button); diff --git a/quantum/keyboard.c b/quantum/keyboard.c index ce4f06ae69..63236f0b20 100644 --- a/quantum/keyboard.c +++ b/quantum/keyboard.c @@ -489,7 +489,7 @@ bool matrix_scan_task(void) {      // we can get here with some keys processed now.      if (!keys_processed)  #endif -        action_exec(TICK); +        action_exec(TICK_EVENT);  MATRIX_LOOP_END: @@ -562,6 +562,10 @@ void quantum_task(void) {  #ifdef AUTO_SHIFT_ENABLE      autoshift_matrix_scan();  #endif + +#ifdef SECURE_ENABLE +    secure_task(); +#endif  }  /** \brief Keyboard task: Do keyboard routine jobs diff --git a/quantum/keyboard.h b/quantum/keyboard.h index e122b38264..fe0736a515 100644 --- a/quantum/keyboard.h +++ b/quantum/keyboard.h @@ -40,25 +40,47 @@ typedef struct {  /* equivalent test of keypos_t */  #define KEYEQ(keya, keyb) ((keya).row == (keyb).row && (keya).col == (keyb).col) +/* special keypos_t entries */ +#define KEYLOC_TICK 255 +#define KEYLOC_COMBO 254 +#define KEYLOC_ENCODER_CW 253 +#define KEYLOC_ENCODER_CCW 252 +  /* Rules for No Event:   * 1) (time == 0) to handle (keyevent_t){} as empty event   * 2) Matrix(255, 255) to make TICK event available   */  static inline bool IS_NOEVENT(keyevent_t event) { -    return event.time == 0 || (event.key.row == 255 && event.key.col == 255); +    return event.time == 0 || (event.key.row == KEYLOC_TICK && event.key.col == KEYLOC_TICK); +} +static inline bool IS_KEYEVENT(keyevent_t event) { +    return event.key.row < MATRIX_ROWS && event.key.col < MATRIX_COLS; +} +static inline bool IS_COMBOEVENT(keyevent_t event) { +    return event.key.row == KEYLOC_COMBO; +} +static inline bool IS_ENCODEREVENT(keyevent_t event) { +    return event.key.row == KEYLOC_ENCODER_CW || event.key.row == KEYLOC_ENCODER_CCW;  }  static inline bool IS_PRESSED(keyevent_t event) { -    return (!IS_NOEVENT(event) && event.pressed); +    return !IS_NOEVENT(event) && event.pressed;  }  static inline bool IS_RELEASED(keyevent_t event) { -    return (!IS_NOEVENT(event) && !event.pressed); +    return !IS_NOEVENT(event) && !event.pressed;  } +/* Common keyevent object factory */ +#define MAKE_KEYPOS(row_num, col_num) ((keypos_t){.row = (row_num), .col = (col_num)}) +#define MAKE_KEYEVENT(row_num, col_num, press) ((keyevent_t){.key = MAKE_KEYPOS((row_num), (col_num)), .pressed = (press), .time = (timer_read() | 1)}) +  /* Tick event */ -#define TICK                                                                                    \ -    (keyevent_t) {                                                                              \ -        .key = (keypos_t){.row = 255, .col = 255}, .pressed = false, .time = (timer_read() | 1) \ -    } +#define TICK_EVENT MAKE_KEYEVENT(KEYLOC_TICK, KEYLOC_TICK, false) + +#ifdef ENCODER_MAP_ENABLE +/* Encoder events */ +#    define ENCODER_CW_EVENT(enc_id, press) MAKE_KEYEVENT(KEYLOC_ENCODER_CW, (enc_id), (press)) +#    define ENCODER_CCW_EVENT(enc_id, press) MAKE_KEYEVENT(KEYLOC_ENCODER_CCW, (enc_id), (press)) +#endif // ENCODER_MAP_ENABLE  /* it runs once at early stage of startup before keyboard_init. */  void keyboard_setup(void); diff --git a/quantum/keycode_config.h b/quantum/keycode_config.h index d7e334fdc8..a2cb025ed2 100644 --- a/quantum/keycode_config.h +++ b/quantum/keycode_config.h @@ -37,7 +37,7 @@ typedef union {          bool nkro : 1;          bool swap_lctl_lgui : 1;          bool swap_rctl_rgui : 1; -        bool oneshot_disable : 1; +        bool oneshot_enable : 1;      };  } keymap_config_t; diff --git a/quantum/keymap.h b/quantum/keymap.h index 2ee2e1b576..d64b271efb 100644 --- a/quantum/keymap.h +++ b/quantum/keymap.h @@ -32,6 +32,7 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.  // #include "print.h"  #include "debug.h"  #include "keycode_config.h" +#include "gpio.h" // for pin_t  // ChibiOS uses RESET in its FlagStatus enumeration  // Therefore define it as QK_BOOTLOADER here, to avoid name collision @@ -49,3 +50,8 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.  uint16_t keymap_key_to_keycode(uint8_t layer, keypos_t key);  extern const uint16_t keymaps[][MATRIX_ROWS][MATRIX_COLS]; + +#ifdef ENCODER_MAP_ENABLE +// Ensure we have a forward declaration for the encoder map +#    include "encoder.h" +#endif diff --git a/quantum/keymap_common.c b/quantum/keymap_common.c index a91b2a0b36..c1940f0fd3 100644 --- a/quantum/keymap_common.c +++ b/quantum/keymap_common.c @@ -148,6 +148,15 @@ action_t action_for_keycode(uint16_t keycode) {  // translates key to keycode  __attribute__((weak)) uint16_t keymap_key_to_keycode(uint8_t layer, keypos_t key) { -    // Read entire word (16bits) -    return pgm_read_word(&keymaps[(layer)][(key.row)][(key.col)]); +    if (key.row < MATRIX_ROWS && key.col < MATRIX_COLS) { +        return pgm_read_word(&keymaps[layer][key.row][key.col]); +    } +#ifdef ENCODER_MAP_ENABLE +    else if (key.row == KEYLOC_ENCODER_CW && key.col < NUM_ENCODERS) { +        return pgm_read_word(&encoder_map[layer][key.col][0]); +    } else if (key.row == KEYLOC_ENCODER_CCW && key.col < NUM_ENCODERS) { +        return pgm_read_word(&encoder_map[layer][key.col][1]); +    } +#endif // ENCODER_MAP_ENABLE +    return KC_NO;  } diff --git a/quantum/main.c b/quantum/main.c index faba668056..2d5911b708 100644 --- a/quantum/main.c +++ b/quantum/main.c @@ -43,10 +43,6 @@ void protocol_task(void) {      protocol_post_task();  } -#ifdef DEFERRED_EXEC_ENABLE -void deferred_exec_task(void); -#endif // DEFERRED_EXEC_ENABLE -  /** \brief Main   *   * FIXME: Needs doc @@ -63,8 +59,15 @@ int main(void) {      while (true) {          protocol_task(); +#ifdef QUANTUM_PAINTER_ENABLE +        // Run Quantum Painter animations +        void qp_internal_animation_tick(void); +        qp_internal_animation_tick(); +#endif +  #ifdef DEFERRED_EXEC_ENABLE          // Run deferred executions +        void deferred_exec_task(void);          deferred_exec_task();  #endif // DEFERRED_EXEC_ENABLE diff --git a/quantum/mousekey.c b/quantum/mousekey.c index 8bafbf977a..64d0e66682 100644 --- a/quantum/mousekey.c +++ b/quantum/mousekey.c @@ -16,6 +16,7 @@   */  #include <stdint.h> +#include <string.h>  #include "keycode.h"  #include "host.h"  #include "timer.h" @@ -209,7 +210,7 @@ static uint8_t wheel_unit(void) {  void mousekey_task(void) {      // report cursor and scroll movement independently -    report_mouse_t const tmpmr = mouse_report; +    report_mouse_t tmpmr = mouse_report;      mouse_report.x = 0;      mouse_report.y = 0; @@ -251,8 +252,10 @@ void mousekey_task(void) {          }      } -    if (mouse_report.x || mouse_report.y || mouse_report.v || mouse_report.h) mousekey_send(); -    mouse_report = tmpmr; +    if (has_mouse_report_changed(&mouse_report, &tmpmr)) { +        mousekey_send(); +    } +    memcpy(&mouse_report, &tmpmr, sizeof(tmpmr));  }  void mousekey_on(uint8_t code) { @@ -340,11 +343,11 @@ uint16_t        w_intervals[mkspd_COUNT] = {MK_W_INTERVAL_UNMOD, MK_W_INTERVAL_0  void mousekey_task(void) {      // report cursor and scroll movement independently -    report_mouse_t const tmpmr = mouse_report; -    mouse_report.x             = 0; -    mouse_report.y             = 0; -    mouse_report.v             = 0; -    mouse_report.h             = 0; +    report_mouse_t tmpmr = mouse_report; +    mouse_report.x       = 0; +    mouse_report.y       = 0; +    mouse_report.v       = 0; +    mouse_report.h       = 0;      if ((tmpmr.x || tmpmr.y) && timer_elapsed(last_timer_c) > c_intervals[mk_speed]) {          mouse_report.x = tmpmr.x; @@ -355,8 +358,10 @@ void mousekey_task(void) {          mouse_report.h = tmpmr.h;      } -    if (mouse_report.x || mouse_report.y || mouse_report.v || mouse_report.h) mousekey_send(); -    mouse_report = tmpmr; +    if (has_mouse_report_changed(&mouse_report, &tmpmr)) { +        mousekey_send(); +    } +    memcpy(&mouse_report, &tmpmr, sizeof(tmpmr));  }  void adjust_speed(void) { diff --git a/quantum/painter/qff.c b/quantum/painter/qff.c new file mode 100644 index 0000000000..cd6af788f9 --- /dev/null +++ b/quantum/painter/qff.c @@ -0,0 +1,137 @@ +// Copyright 2021 Nick Brassel (@tzarc) +// SPDX-License-Identifier: GPL-2.0-or-later + +// Quantum Font File "QFF" File Format. +// See https://docs.qmk.fm/#/quantum_painter_qff for more information. + +#include "qff.h" +#include "qp_draw.h" + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// QFF API + +bool qff_read_font_descriptor(qp_stream_t *stream, uint8_t *line_height, bool *has_ascii_table, uint16_t *num_unicode_glyphs, uint8_t *bpp, bool *has_palette, painter_compression_t *compression_scheme, uint32_t *total_bytes) { +    // Seek to the start +    qp_stream_setpos(stream, 0); + +    // Read and validate the font descriptor +    qff_font_descriptor_v1_t font_descriptor; +    if (qp_stream_read(&font_descriptor, sizeof(qff_font_descriptor_v1_t), 1, stream) != 1) { +        qp_dprintf("Failed to read font_descriptor, expected length was not %d\n", (int)sizeof(qff_font_descriptor_v1_t)); +        return false; +    } + +    // Make sure this block is valid +    if (!qgf_validate_block_header(&font_descriptor.header, QFF_FONT_DESCRIPTOR_TYPEID, (sizeof(qff_font_descriptor_v1_t) - sizeof(qgf_block_header_v1_t)))) { +        return false; +    } + +    // Make sure the magic and version are correct +    if (font_descriptor.magic != QFF_MAGIC || font_descriptor.qff_version != 0x01) { +        qp_dprintf("Failed to validate font_descriptor, expected magic 0x%06X was 0x%06X, expected version = 0x%02X was 0x%02X\n", (int)QFF_MAGIC, (int)font_descriptor.magic, (int)0x01, (int)font_descriptor.qff_version); +        return false; +    } + +    // Make sure the file length is valid +    if (font_descriptor.neg_total_file_size != ~font_descriptor.total_file_size) { +        qp_dprintf("Failed to validate font_descriptor, expected negated length 0x%08X was 0x%08X\n", (int)(~font_descriptor.total_file_size), (int)font_descriptor.neg_total_file_size); +        return false; +    } + +    // Copy out the required info +    if (line_height) { +        *line_height = font_descriptor.line_height; +    } +    if (has_ascii_table) { +        *has_ascii_table = font_descriptor.has_ascii_table; +    } +    if (num_unicode_glyphs) { +        *num_unicode_glyphs = font_descriptor.num_unicode_glyphs; +    } +    if (bpp || has_palette) { +        if (!qgf_parse_format(font_descriptor.format, bpp, has_palette)) { +            return false; +        } +    } +    if (compression_scheme) { +        *compression_scheme = font_descriptor.compression_scheme; +    } +    if (total_bytes) { +        *total_bytes = font_descriptor.total_file_size; +    } + +    return true; +} + +static bool qff_validate_ascii_descriptor(qp_stream_t *stream) { +    // Read the raw descriptor +    qff_ascii_glyph_table_v1_t ascii_descriptor; +    if (qp_stream_read(&ascii_descriptor, sizeof(qff_ascii_glyph_table_v1_t), 1, stream) != 1) { +        qp_dprintf("Failed to read ascii_descriptor, expected length was not %d\n", (int)sizeof(qff_ascii_glyph_table_v1_t)); +        return false; +    } + +    // Make sure this block is valid +    if (!qgf_validate_block_header(&ascii_descriptor.header, QFF_ASCII_GLYPH_DESCRIPTOR_TYPEID, (sizeof(qff_ascii_glyph_table_v1_t) - sizeof(qgf_block_header_v1_t)))) { +        return false; +    } + +    return true; +} + +static bool qff_validate_unicode_descriptor(qp_stream_t *stream, uint16_t num_unicode_glyphs) { +    // Read the raw descriptor +    qff_unicode_glyph_table_v1_t unicode_descriptor; +    if (qp_stream_read(&unicode_descriptor, sizeof(qff_unicode_glyph_table_v1_t), 1, stream) != 1) { +        qp_dprintf("Failed to read unicode_descriptor, expected length was not %d\n", (int)sizeof(qff_unicode_glyph_table_v1_t)); +        return false; +    } + +    // Make sure this block is valid +    if (!qgf_validate_block_header(&unicode_descriptor.header, QFF_UNICODE_GLYPH_DESCRIPTOR_TYPEID, num_unicode_glyphs * 6)) { +        return false; +    } + +    // Skip the necessary amount of data to get to the next block +    qp_stream_seek(stream, num_unicode_glyphs * sizeof(qff_unicode_glyph_v1_t), SEEK_CUR); + +    return true; +} + +bool qff_validate_stream(qp_stream_t *stream) { +    bool     has_ascii_table; +    uint16_t num_unicode_glyphs; + +    if (!qff_read_font_descriptor(stream, NULL, &has_ascii_table, &num_unicode_glyphs, NULL, NULL, NULL, NULL)) { +        return false; +    } + +    if (has_ascii_table) { +        if (!qff_validate_ascii_descriptor(stream)) { +            return false; +        } +    } + +    if (num_unicode_glyphs > 0) { +        if (!qff_validate_unicode_descriptor(stream, num_unicode_glyphs)) { +            return false; +        } +    } + +    return true; +} + +uint32_t qff_get_total_size(qp_stream_t *stream) { +    // Get the original location +    uint32_t oldpos = qp_stream_tell(stream); + +    // Read the font descriptor, grabbing the size +    uint32_t total_size; +    if (!qff_read_font_descriptor(stream, NULL, NULL, NULL, NULL, NULL, NULL, &total_size)) { +        return false; +    } + +    // Restore the original location +    qp_stream_setpos(stream, oldpos); +    return total_size; +} diff --git a/quantum/painter/qff.h b/quantum/painter/qff.h new file mode 100644 index 0000000000..6f1a1fd815 --- /dev/null +++ b/quantum/painter/qff.h @@ -0,0 +1,88 @@ +// Copyright 2021 Nick Brassel (@tzarc) +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +// Quantum Font File "QFF" File Format. +// See https://docs.qmk.fm/#/quantum_painter_qff for more information. + +#include <stdint.h> +#include <stdbool.h> + +#include "qp_stream.h" +#include "qp_internal.h" +#include "qgf.h" + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// QFF structures + +///////////////////////////////////////// +// Font descriptor + +#define QFF_FONT_DESCRIPTOR_TYPEID 0x00 + +typedef struct __attribute__((packed)) qff_font_descriptor_v1_t { +    qgf_block_header_v1_t header;              // = { .type_id = 0x00, .neg_type_id = (~0x00), .length = 20 } +    uint32_t              magic : 24;          // constant, equal to 0x464651 ("QFF") +    uint8_t               qff_version;         // constant, equal to 0x01 +    uint32_t              total_file_size;     // total size of the entire file, starting at offset zero +    uint32_t              neg_total_file_size; // negated value of total_file_size, used for detecting parsing errors +    uint8_t               line_height;         // glyph height in pixels +    bool                  has_ascii_table;     // whether the font has an ascii table of glyphs (0x20...0x7E) +    uint16_t              num_unicode_glyphs;  // the number of glyphs in the unicode table -- no table specified if zero +    qp_image_format_t     format : 8;          // Frame format, see qp.h. +    uint8_t               flags;               // frame flags, see below. +    uint8_t               compression_scheme;  // compression scheme, see below. +    uint8_t               transparency_index;  // palette index used for transparent pixels (not yet implemented) +} qff_font_descriptor_v1_t; + +_Static_assert(sizeof(qff_font_descriptor_v1_t) == (sizeof(qgf_block_header_v1_t) + 20), "qff_font_descriptor_v1_t must be 25 bytes in v1 of QFF"); + +#define QFF_MAGIC 0x464651 + +///////////////////////////////////////// +// ASCII glyph table descriptor + +#define QFF_ASCII_GLYPH_DESCRIPTOR_TYPEID 0x01 + +#define QFF_GLYPH_WIDTH_BITS 6 +#define QFF_GLYPH_WIDTH_MASK ((1 << QFF_GLYPH_WIDTH_BITS) - 1) +#define QFF_GLYPH_OFFSET_BITS 18 +#define QFF_GLYPH_OFFSET_MASK (((1 << QFF_GLYPH_OFFSET_BITS) - 1) << QFF_GLYPH_WIDTH_BITS) + +typedef struct __attribute__((packed)) qff_ascii_glyph_v1_t { +    uint32_t value : 24; // Uses QFF_GLYPH_*_(BITS|MASK) as bitfield ordering is compiler-defined +} qff_ascii_glyph_v1_t; + +_Static_assert(sizeof(qff_ascii_glyph_v1_t) == 3, "qff_ascii_glyph_v1_t must be 3 bytes in v1 of QFF"); + +typedef struct __attribute__((packed)) qff_ascii_glyph_table_v1_t { +    qgf_block_header_v1_t header;    // = { .type_id = 0x01, .neg_type_id = (~0x01), .length = 285 } +    qff_ascii_glyph_v1_t  glyph[95]; // 95 glyphs, 0x20..0x7E +} qff_ascii_glyph_table_v1_t; + +_Static_assert(sizeof(qff_ascii_glyph_table_v1_t) == (sizeof(qgf_block_header_v1_t) + (95 * sizeof(qff_ascii_glyph_v1_t))), "qff_ascii_glyph_table_v1_t must be 290 bytes in v1 of QFF"); + +///////////////////////////////////////// +// Unicode glyph table descriptor + +#define QFF_UNICODE_GLYPH_DESCRIPTOR_TYPEID 0x02 + +typedef struct __attribute__((packed)) qff_unicode_glyph_v1_t { +    uint32_t code_point : 24; +    uint32_t value : 24; // Uses QFF_GLYPH_*_(BITS|MASK) as bitfield ordering is compiler-defined +} qff_unicode_glyph_v1_t; + +_Static_assert(sizeof(qff_unicode_glyph_v1_t) == 6, "qff_unicode_glyph_v1_t must be 6 bytes in v1 of QFF"); + +typedef struct __attribute__((packed)) qff_unicode_glyph_table_v1_t { +    qgf_block_header_v1_t  header;   // = { .type_id = 0x02, .neg_type_id = (~0x02), .length = (N * 6) } +    qff_unicode_glyph_v1_t glyph[0]; // Extent of '0' signifies that this struct is immediately followed by the glyph data +} qff_unicode_glyph_table_v1_t; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// QFF API + +bool     qff_validate_stream(qp_stream_t *stream); +uint32_t qff_get_total_size(qp_stream_t *stream); +bool     qff_read_font_descriptor(qp_stream_t *stream, uint8_t *line_height, bool *has_ascii_table, uint16_t *num_unicode_glyphs, uint8_t *bpp, bool *has_palette, painter_compression_t *compression_scheme, uint32_t *total_bytes); diff --git a/quantum/painter/qgf.c b/quantum/painter/qgf.c new file mode 100644 index 0000000000..834837105b --- /dev/null +++ b/quantum/painter/qgf.c @@ -0,0 +1,292 @@ +// Copyright 2021 Nick Brassel (@tzarc) +// SPDX-License-Identifier: GPL-2.0-or-later + +// Quantum Graphics File "QGF" File Format. +// See https://docs.qmk.fm/#/quantum_painter_qgf for more information. + +#include "qgf.h" +#include "qp_draw.h" + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// QGF API + +bool qgf_validate_block_header(qgf_block_header_v1_t *desc, uint8_t expected_typeid, int32_t expected_length) { +    if (desc->type_id != expected_typeid || desc->neg_type_id != ((~expected_typeid) & 0xFF)) { +        qp_dprintf("Failed to validate header, expected typeid 0x%02X, was 0x%02X, expected negated typeid 0x%02X, was 0x%02X\n", (int)expected_typeid, (int)desc->type_id, (int)((~desc->type_id) & 0xFF), (int)desc->neg_type_id); +        return false; +    } + +    if (expected_length >= 0 && desc->length != expected_length) { +        qp_dprintf("Failed to validate header (typeid 0x%02X), expected length %d, was %d\n", (int)desc->type_id, (int)expected_length, (int)desc->length); +        return false; +    } + +    return true; +} + +bool qgf_parse_format(qp_image_format_t format, uint8_t *bpp, bool *has_palette) { +    // clang-format off +    static const struct  QP_PACKED { +        uint8_t bpp; +        bool    has_palette; +    } formats[] = { +        [GRAYSCALE_1BPP] = {.bpp = 1, .has_palette = false}, +        [GRAYSCALE_2BPP] = {.bpp = 2, .has_palette = false}, +        [GRAYSCALE_4BPP] = {.bpp = 4, .has_palette = false}, +        [GRAYSCALE_8BPP] = {.bpp = 8, .has_palette = false}, +        [PALETTE_1BPP] = {.bpp = 1, .has_palette = true}, +        [PALETTE_2BPP] = {.bpp = 2, .has_palette = true}, +        [PALETTE_4BPP] = {.bpp = 4, .has_palette = true}, +        [PALETTE_8BPP] = {.bpp = 8, .has_palette = true}, +    }; +    // clang-format on + +    // Copy out the required info +    if (format > PALETTE_8BPP) { +        qp_dprintf("Failed to parse frame_descriptor, invalid format 0x%02X\n", (int)format); +        return false; +    } + +    // Copy out the required info +    if (bpp) { +        *bpp = formats[format].bpp; +    } +    if (has_palette) { +        *has_palette = formats[format].has_palette; +    } + +    return true; +} + +bool qgf_parse_frame_descriptor(qgf_frame_v1_t *frame_descriptor, uint8_t *bpp, bool *has_palette, bool *is_delta, painter_compression_t *compression_scheme, uint16_t *delay) { +    // Decode the format +    qgf_parse_format(frame_descriptor->format, bpp, has_palette); + +    // Copy out the required info +    if (is_delta) { +        *is_delta = (frame_descriptor->flags & QGF_FRAME_FLAG_DELTA) == QGF_FRAME_FLAG_DELTA; +    } +    if (compression_scheme) { +        *compression_scheme = frame_descriptor->compression_scheme; +    } +    if (delay) { +        *delay = frame_descriptor->delay; +    } + +    return true; +} + +bool qgf_read_graphics_descriptor(qp_stream_t *stream, uint16_t *image_width, uint16_t *image_height, uint16_t *frame_count, uint32_t *total_bytes) { +    // Seek to the start +    qp_stream_setpos(stream, 0); + +    // Read and validate the graphics descriptor +    qgf_graphics_descriptor_v1_t graphics_descriptor; +    if (qp_stream_read(&graphics_descriptor, sizeof(qgf_graphics_descriptor_v1_t), 1, stream) != 1) { +        qp_dprintf("Failed to read graphics_descriptor, expected length was not %d\n", (int)sizeof(qgf_graphics_descriptor_v1_t)); +        return false; +    } + +    // Make sure this block is valid +    if (!qgf_validate_block_header(&graphics_descriptor.header, QGF_GRAPHICS_DESCRIPTOR_TYPEID, (sizeof(qgf_graphics_descriptor_v1_t) - sizeof(qgf_block_header_v1_t)))) { +        return false; +    } + +    // Make sure the magic and version are correct +    if (graphics_descriptor.magic != QGF_MAGIC || graphics_descriptor.qgf_version != 0x01) { +        qp_dprintf("Failed to validate graphics_descriptor, expected magic 0x%06X was 0x%06X, expected version = 0x%02X was 0x%02X\n", (int)QGF_MAGIC, (int)graphics_descriptor.magic, (int)0x01, (int)graphics_descriptor.qgf_version); +        return false; +    } + +    // Make sure the file length is valid +    if (graphics_descriptor.neg_total_file_size != ~graphics_descriptor.total_file_size) { +        qp_dprintf("Failed to validate graphics_descriptor, expected negated length 0x%08X was 0x%08X\n", (int)(~graphics_descriptor.total_file_size), (int)graphics_descriptor.neg_total_file_size); +        return false; +    } + +    // Copy out the required info +    if (image_width) { +        *image_width = graphics_descriptor.image_width; +    } +    if (image_height) { +        *image_height = graphics_descriptor.image_height; +    } +    if (frame_count) { +        *frame_count = graphics_descriptor.frame_count; +    } +    if (total_bytes) { +        *total_bytes = graphics_descriptor.total_file_size; +    } + +    return true; +} + +static bool qgf_read_frame_offset(qp_stream_t *stream, uint16_t frame_number, uint32_t *frame_offset) { +    uint16_t frame_count; +    if (!qgf_read_graphics_descriptor(stream, NULL, NULL, &frame_count, NULL)) { +        return false; +    } + +    // Read the frame offsets descriptor +    qgf_frame_offsets_v1_t frame_offsets; +    if (qp_stream_read(&frame_offsets, sizeof(qgf_frame_offsets_v1_t), 1, stream) != 1) { +        qp_dprintf("Failed to read frame_offsets, expected length was not %d\n", (int)sizeof(qgf_frame_offsets_v1_t)); +        return false; +    } + +    // Make sure this block is valid +    if (!qgf_validate_block_header(&frame_offsets.header, QGF_FRAME_OFFSET_DESCRIPTOR_TYPEID, (frame_count * sizeof(uint32_t)))) { +        return false; +    } + +    if (frame_number >= frame_count) { +        qp_dprintf("Invalid frame number, was %d but only %d frames in image\n", (int)frame_number, (int)frame_count); +        return false; +    } + +    // Skip the necessary amount of data to get to the requested frame offset +    qp_stream_seek(stream, frame_number * sizeof(uint32_t), SEEK_CUR); + +    // Read the frame offset +    uint32_t offset = 0; +    if (qp_stream_read(&offset, sizeof(uint32_t), 1, stream) != 1) { +        qp_dprintf("Failed to read frame offset, expected length was not %d\n", (int)sizeof(uint32_t)); +        return false; +    } + +    // Copy out the required info +    if (frame_offset) { +        *frame_offset = offset; +    } + +    return true; +} + +void qgf_seek_to_frame_descriptor(qp_stream_t *stream, uint16_t frame_number) { +    // Read the offset +    uint32_t offset = 0; +    qgf_read_frame_offset(stream, frame_number, &offset); + +    // Move to the offset +    qp_stream_setpos(stream, offset); +} + +bool qgf_validate_frame_descriptor(qp_stream_t *stream, uint16_t frame_number, uint8_t *bpp, bool *has_palette, bool *is_delta) { +    // Seek to the correct location +    qgf_seek_to_frame_descriptor(stream, frame_number); + +    // Read the raw descriptor +    qgf_frame_v1_t frame_descriptor; +    if (qp_stream_read(&frame_descriptor, sizeof(qgf_frame_v1_t), 1, stream) != 1) { +        qp_dprintf("Failed to read frame_descriptor, expected length was not %d\n", (int)sizeof(qgf_frame_v1_t)); +        return false; +    } + +    // Make sure this block is valid +    if (!qgf_validate_block_header(&frame_descriptor.header, QGF_FRAME_DESCRIPTOR_TYPEID, (sizeof(qgf_frame_v1_t) - sizeof(qgf_block_header_v1_t)))) { +        return false; +    } + +    return qgf_parse_frame_descriptor(&frame_descriptor, bpp, has_palette, is_delta, NULL, NULL); +} + +bool qgf_validate_palette_descriptor(qp_stream_t *stream, uint16_t frame_number, uint8_t bpp) { +    // Read the palette descriptor +    qgf_palette_v1_t palette_descriptor; +    if (qp_stream_read(&palette_descriptor, sizeof(qgf_palette_v1_t), 1, stream) != 1) { +        qp_dprintf("Failed to read palette_descriptor, expected length was not %d\n", (int)sizeof(qgf_palette_v1_t)); +        return false; +    } + +    // Make sure this block is valid +    uint32_t expected_length = (1 << bpp) * 3 * sizeof(uint8_t); +    if (!qgf_validate_block_header(&palette_descriptor.header, QGF_FRAME_PALETTE_DESCRIPTOR_TYPEID, expected_length)) { +        return false; +    } + +    // Move forward in the stream to the next block +    qp_stream_seek(stream, expected_length, SEEK_CUR); +    return true; +} + +bool qgf_validate_delta_descriptor(qp_stream_t *stream, uint16_t frame_number) { +    // Read the delta descriptor +    qgf_delta_v1_t delta_descriptor; +    if (qp_stream_read(&delta_descriptor, sizeof(qgf_delta_v1_t), 1, stream) != 1) { +        qp_dprintf("Failed to read delta_descriptor, expected length was not %d\n", (int)sizeof(qgf_delta_v1_t)); +        return false; +    } + +    // Make sure this block is valid +    if (!qgf_validate_block_header(&delta_descriptor.header, QGF_FRAME_DELTA_DESCRIPTOR_TYPEID, (sizeof(qgf_delta_v1_t) - sizeof(qgf_block_header_v1_t)))) { +        return false; +    } + +    return true; +} + +bool qgf_validate_frame_data_descriptor(qp_stream_t *stream, uint16_t frame_number) { +    // Read and validate the data block +    qgf_data_v1_t data_descriptor; +    if (qp_stream_read(&data_descriptor, sizeof(qgf_data_v1_t), 1, stream) != 1) { +        qp_dprintf("Failed to read data_descriptor, expected length was not %d\n", (int)sizeof(qgf_data_v1_t)); +        return false; +    } + +    if (!qgf_validate_block_header(&data_descriptor.header, QGF_FRAME_DATA_DESCRIPTOR_TYPEID, -1)) { +        return false; +    } + +    return true; +} + +bool qgf_validate_stream(qp_stream_t *stream) { +    uint16_t frame_count; +    if (!qgf_read_graphics_descriptor(stream, NULL, NULL, &frame_count, NULL)) { +        return false; +    } + +    // Read and validate all the frames (automatically validates the frame offset descriptor in the process) +    for (uint16_t i = 0; i < frame_count; ++i) { +        // Validate the frame descriptor block +        uint8_t bpp; +        bool    has_palette; +        bool    has_delta; +        if (!qgf_validate_frame_descriptor(stream, i, &bpp, &has_palette, &has_delta)) { +            return false; +        } + +        // If we've got a palette block, check it +        if (has_palette && !qgf_validate_palette_descriptor(stream, i, bpp)) { +            return false; +        } + +        // If we've got a delta block, check it +        if (has_delta && !qgf_validate_delta_descriptor(stream, i)) { +            return false; +        } + +        // Check the data block +        if (!qgf_validate_frame_data_descriptor(stream, i)) { +            return false; +        } +    } + +    return true; +} + +// Work out the total size of an image definition, assuming we can read far enough into the file +uint32_t qgf_get_total_size(qp_stream_t *stream) { +    // Get the original location +    uint32_t oldpos = qp_stream_tell(stream); + +    // Read the graphics descriptor, grabbing the size +    uint32_t total_size; +    if (!qgf_read_graphics_descriptor(stream, NULL, NULL, NULL, &total_size)) { +        return false; +    } + +    // Restore the original location +    qp_stream_setpos(stream, oldpos); +    return total_size; +} diff --git a/quantum/painter/qgf.h b/quantum/painter/qgf.h new file mode 100644 index 0000000000..54585edd04 --- /dev/null +++ b/quantum/painter/qgf.h @@ -0,0 +1,136 @@ +// Copyright 2021 Nick Brassel (@tzarc) +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +// Quantum Graphics File "QGF" File Format. +// See https://docs.qmk.fm/#/quantum_painter_qgf for more information. + +#include <stdint.h> +#include <stdbool.h> + +#include "qp_stream.h" +#include "qp_internal.h" + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// QGF structures + +///////////////////////////////////////// +// Common block header + +typedef struct QP_PACKED qgf_block_header_v1_t { +    uint8_t  type_id;     // See each respective block type below. +    uint8_t  neg_type_id; // Negated type ID, used for detecting parsing errors. +    uint32_t length : 24; // 24-bit blob length, allowing for block sizes of a maximum of 16MB. +} qgf_block_header_v1_t; + +_Static_assert(sizeof(qgf_block_header_v1_t) == 5, "qgf_block_header_v1_t must be 5 bytes in v1 of QGF"); + +///////////////////////////////////////// +// Graphics descriptor + +#define QGF_GRAPHICS_DESCRIPTOR_TYPEID 0x00 + +typedef struct QP_PACKED qgf_graphics_descriptor_v1_t { +    qgf_block_header_v1_t header;              // = { .type_id = 0x00, .neg_type_id = (~0x00), .length = 18 } +    uint32_t              magic : 24;          // constant, equal to 0x464751 ("QGF") +    uint8_t               qgf_version;         // constant, equal to 0x01 +    uint32_t              total_file_size;     // total size of the entire file, starting at offset zero +    uint32_t              neg_total_file_size; // negated value of total_file_size +    uint16_t              image_width;         // in pixels +    uint16_t              image_height;        // in pixels +    uint16_t              frame_count;         // minimum of 1 +} qgf_graphics_descriptor_v1_t; + +_Static_assert(sizeof(qgf_graphics_descriptor_v1_t) == (sizeof(qgf_block_header_v1_t) + 18), "qgf_graphics_descriptor_v1_t must be 23 bytes in v1 of QGF"); + +#define QGF_MAGIC 0x464751 + +///////////////////////////////////////// +// Frame offset descriptor + +#define QGF_FRAME_OFFSET_DESCRIPTOR_TYPEID 0x01 + +typedef struct QP_PACKED qgf_frame_offsets_v1_t { +    qgf_block_header_v1_t header;    // = { .type_id = 0x01, .neg_type_id = (~0x01), .length = (N * sizeof(uint32_t)) } +    uint32_t              offset[0]; // '0' signifies that this struct is immediately followed by the frame offsets +} qgf_frame_offsets_v1_t; + +_Static_assert(sizeof(qgf_frame_offsets_v1_t) == sizeof(qgf_block_header_v1_t), "qgf_frame_offsets_v1_t must only contain qgf_block_header_v1_t in v1 of QGF"); + +///////////////////////////////////////// +// Frame descriptor + +#define QGF_FRAME_DESCRIPTOR_TYPEID 0x02 + +typedef struct QP_PACKED qgf_frame_v1_t { +    qgf_block_header_v1_t header;                 // = { .type_id = 0x02, .neg_type_id = (~0x02), .length = 6 } +    qp_image_format_t     format : 8;             // Frame format, see qp.h. +    uint8_t               flags;                  // Frame flags, see below. +    painter_compression_t compression_scheme : 8; // Compression scheme, see qp.h. +    uint8_t               transparency_index;     // palette index used for transparent pixels (not yet implemented) +    uint16_t              delay;                  // frame delay time for animations (in units of milliseconds) +} qgf_frame_v1_t; + +_Static_assert(sizeof(qgf_frame_v1_t) == (sizeof(qgf_block_header_v1_t) + 6), "qgf_frame_v1_t must be 11 bytes in v1 of QGF"); + +#define QGF_FRAME_FLAG_DELTA 0x02 +#define QGF_FRAME_FLAG_TRANSPARENT 0x01 + +///////////////////////////////////////// +// Frame palette descriptor + +#define QGF_FRAME_PALETTE_DESCRIPTOR_TYPEID 0x03 + +typedef struct QP_PACKED qgf_palette_entry_v1_t { +    uint8_t h; // hue component: `[0,360)` degrees is mapped to `[0,255]` uint8_t. +    uint8_t s; // saturation component: `[0,1]` is mapped to `[0,255]` uint8_t. +    uint8_t v; // value component: `[0,1]` is mapped to `[0,255]` uint8_t. +} qgf_palette_entry_v1_t; + +_Static_assert(sizeof(qgf_palette_entry_v1_t) == 3, "Palette entry is not 3 bytes in size"); + +typedef struct QP_PACKED qgf_palette_v1_t { +    qgf_block_header_v1_t  header; // = { .type_id = 0x03, .neg_type_id = (~0x03), .length = (N * 3 * sizeof(uint8_t)) } +    qgf_palette_entry_v1_t hsv[0]; // N * hsv, where N is the number of palette entries depending on the frame format in the descriptor +} qgf_palette_v1_t; + +_Static_assert(sizeof(qgf_palette_v1_t) == sizeof(qgf_block_header_v1_t), "qgf_palette_v1_t must only contain qgf_block_header_v1_t in v1 of QGF"); + +///////////////////////////////////////// +// Frame delta descriptor + +#define QGF_FRAME_DELTA_DESCRIPTOR_TYPEID 0x04 + +typedef struct QP_PACKED qgf_delta_v1_t { +    qgf_block_header_v1_t header; // = { .type_id = 0x04, .neg_type_id = (~0x04), .length = 8 } +    uint16_t              left;   // The left pixel location to draw the delta image +    uint16_t              top;    // The top pixel location to draw the delta image +    uint16_t              right;  // The right pixel location to to draw the delta image +    uint16_t              bottom; // The bottom pixel location to to draw the delta image +} qgf_delta_v1_t; + +_Static_assert(sizeof(qgf_delta_v1_t) == (sizeof(qgf_block_header_v1_t) + 8), "qgf_delta_v1_t must be 13 bytes in v1 of QGF"); + +///////////////////////////////////////// +// Frame data descriptor + +#define QGF_FRAME_DATA_DESCRIPTOR_TYPEID 0x05 + +typedef struct QP_PACKED qgf_data_v1_t { +    qgf_block_header_v1_t header;  // = { .type_id = 0x05, .neg_type_id = (~0x05), .length = N } +    uint8_t               data[0]; // 0 signifies that this struct is immediately followed by the length of data specified in the header +} qgf_data_v1_t; + +_Static_assert(sizeof(qgf_data_v1_t) == sizeof(qgf_block_header_v1_t), "qgf_data_v1_t must only contain qgf_block_header_v1_t in v1 of QGF"); + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// QGF API + +uint32_t qgf_get_total_size(qp_stream_t *stream); +bool     qgf_validate_stream(qp_stream_t *stream); +bool     qgf_validate_block_header(qgf_block_header_v1_t *desc, uint8_t expected_typeid, int32_t expected_length); +bool     qgf_read_graphics_descriptor(qp_stream_t *stream, uint16_t *image_width, uint16_t *image_height, uint16_t *frame_count, uint32_t *total_bytes); +bool     qgf_parse_format(qp_image_format_t format, uint8_t *bpp, bool *has_palette); +void     qgf_seek_to_frame_descriptor(qp_stream_t *stream, uint16_t frame_number); +bool     qgf_parse_frame_descriptor(qgf_frame_v1_t *frame_descriptor, uint8_t *bpp, bool *has_palette, bool *is_delta, painter_compression_t *compression_scheme, uint16_t *delay); diff --git a/quantum/painter/qp.c b/quantum/painter/qp.c new file mode 100644 index 0000000000..e292ff6497 --- /dev/null +++ b/quantum/painter/qp.c @@ -0,0 +1,228 @@ +// Copyright 2021 Nick Brassel (@tzarc) +// SPDX-License-Identifier: GPL-2.0-or-later + +#include <quantum.h> +#include <utf8.h> + +#include "qp_internal.h" +#include "qp_comms.h" +#include "qp_draw.h" + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Internal driver validation + +static bool validate_driver_vtable(struct painter_driver_t *driver) { +    return (driver->driver_vtable && driver->driver_vtable->init && driver->driver_vtable->power && driver->driver_vtable->clear && driver->driver_vtable->viewport && driver->driver_vtable->pixdata && driver->driver_vtable->palette_convert && driver->driver_vtable->append_pixels) ? true : false; +} + +static bool validate_comms_vtable(struct painter_driver_t *driver) { +    return (driver->comms_vtable && driver->comms_vtable->comms_init && driver->comms_vtable->comms_start && driver->comms_vtable->comms_stop && driver->comms_vtable->comms_send) ? true : false; +} + +static bool validate_driver_integrity(struct painter_driver_t *driver) { +    return validate_driver_vtable(driver) && validate_comms_vtable(driver); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Quantum Painter External API: qp_init + +bool qp_init(painter_device_t device, painter_rotation_t rotation) { +    qp_dprintf("qp_init: entry\n"); +    struct painter_driver_t *driver = (struct painter_driver_t *)device; + +    driver->validate_ok = false; +    if (!validate_driver_integrity(driver)) { +        qp_dprintf("Failed to validate driver integrity in qp_init\n"); +        return false; +    } + +    driver->validate_ok = true; + +    if (!qp_comms_init(device)) { +        driver->validate_ok = false; +        qp_dprintf("qp_init: fail (could not init comms)\n"); +        return false; +    } + +    if (!qp_comms_start(device)) { +        qp_dprintf("qp_init: fail (could not start comms)\n"); +        return false; +    } + +    // Set the rotation before init +    driver->rotation = rotation; + +    // Invoke init +    bool ret = driver->driver_vtable->init(device, rotation); +    qp_comms_stop(device); +    qp_dprintf("qp_init: %s\n", ret ? "ok" : "fail"); +    return ret; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Quantum Painter External API: qp_power + +bool qp_power(painter_device_t device, bool power_on) { +    qp_dprintf("qp_power: entry\n"); +    struct painter_driver_t *driver = (struct painter_driver_t *)device; +    if (!driver->validate_ok) { +        qp_dprintf("qp_power: fail (validation_ok == false)\n"); +        return false; +    } + +    if (!qp_comms_start(device)) { +        qp_dprintf("qp_power: fail (could not start comms)\n"); +        return false; +    } + +    bool ret = driver->driver_vtable->power(device, power_on); +    qp_comms_stop(device); +    qp_dprintf("qp_power: %s\n", ret ? "ok" : "fail"); +    return ret; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Quantum Painter External API: qp_clear + +bool qp_clear(painter_device_t device) { +    qp_dprintf("qp_clear: entry\n"); +    struct painter_driver_t *driver = (struct painter_driver_t *)device; +    if (!driver->validate_ok) { +        qp_dprintf("qp_clear: fail (validation_ok == false)\n"); +        return false; +    } + +    if (!qp_comms_start(device)) { +        qp_dprintf("qp_clear: fail (could not start comms)\n"); +        return false; +    } + +    bool ret = driver->driver_vtable->clear(device); +    qp_comms_stop(device); +    qp_dprintf("qp_clear: %s\n", ret ? "ok" : "fail"); +    return ret; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Quantum Painter External API: qp_flush + +bool qp_flush(painter_device_t device) { +    qp_dprintf("qp_flush: entry\n"); +    struct painter_driver_t *driver = (struct painter_driver_t *)device; +    if (!driver->validate_ok) { +        qp_dprintf("qp_flush: fail (validation_ok == false)\n"); +        return false; +    } + +    if (!qp_comms_start(device)) { +        qp_dprintf("qp_flush: fail (could not start comms)\n"); +        return false; +    } + +    bool ret = driver->driver_vtable->flush(device); +    qp_comms_stop(device); +    qp_dprintf("qp_flush: %s\n", ret ? "ok" : "fail"); +    return ret; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Quantum Painter External API: qp_get_geometry + +void qp_get_geometry(painter_device_t device, uint16_t *width, uint16_t *height, painter_rotation_t *rotation, uint16_t *offset_x, uint16_t *offset_y) { +    qp_dprintf("qp_geometry: entry\n"); +    struct painter_driver_t *driver = (struct painter_driver_t *)device; + +    switch (driver->rotation) { +        default: +        case QP_ROTATION_0: +        case QP_ROTATION_180: +            if (width) { +                *width = driver->panel_width; +            } +            if (height) { +                *height = driver->panel_height; +            } +            break; +        case QP_ROTATION_90: +        case QP_ROTATION_270: +            if (width) { +                *width = driver->panel_height; +            } +            if (height) { +                *height = driver->panel_width; +            } +            break; +    } + +    if (rotation) { +        *rotation = driver->rotation; +    } + +    if (offset_x) { +        *offset_x = driver->offset_x; +    } + +    if (offset_y) { +        *offset_y = driver->offset_y; +    } + +    qp_dprintf("qp_geometry: ok\n"); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Quantum Painter External API: qp_set_viewport_offsets + +void qp_set_viewport_offsets(painter_device_t device, uint16_t offset_x, uint16_t offset_y) { +    qp_dprintf("qp_set_viewport_offsets: entry\n"); +    struct painter_driver_t *driver = (struct painter_driver_t *)device; + +    driver->offset_x = offset_x; +    driver->offset_y = offset_y; + +    qp_dprintf("qp_set_viewport_offsets: ok\n"); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Quantum Painter External API: qp_viewport + +bool qp_viewport(painter_device_t device, uint16_t left, uint16_t top, uint16_t right, uint16_t bottom) { +    qp_dprintf("qp_viewport: entry\n"); +    struct painter_driver_t *driver = (struct painter_driver_t *)device; +    if (!driver->validate_ok) { +        qp_dprintf("qp_viewport: fail (validation_ok == false)\n"); +        return false; +    } + +    if (!qp_comms_start(device)) { +        qp_dprintf("qp_viewport: fail (could not start comms)\n"); +        return false; +    } + +    // Set the viewport +    bool ret = driver->driver_vtable->viewport(device, left, top, right, bottom); +    qp_dprintf("qp_viewport: %s\n", ret ? "ok" : "fail"); +    qp_comms_stop(device); +    return ret; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Quantum Painter External API: qp_pixdata + +bool qp_pixdata(painter_device_t device, const void *pixel_data, uint32_t native_pixel_count) { +    qp_dprintf("qp_pixdata: entry\n"); +    struct painter_driver_t *driver = (struct painter_driver_t *)device; +    if (!driver->validate_ok) { +        qp_dprintf("qp_pixdata: fail (validation_ok == false)\n"); +        return false; +    } + +    if (!qp_comms_start(device)) { +        qp_dprintf("qp_pixdata: fail (could not start comms)\n"); +        return false; +    } + +    bool ret = driver->driver_vtable->pixdata(device, pixel_data, native_pixel_count); +    qp_dprintf("qp_pixdata: %s\n", ret ? "ok" : "fail"); +    qp_comms_stop(device); +    return ret; +} diff --git a/quantum/painter/qp.h b/quantum/painter/qp.h new file mode 100644 index 0000000000..e1c14d156c --- /dev/null +++ b/quantum/painter/qp.h @@ -0,0 +1,453 @@ +// Copyright 2021 Nick Brassel (@tzarc) +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include <stdint.h> +#include <stdbool.h> + +#include "deferred_exec.h" + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Quantum Painter global configurables (add to your keyboard's config.h) + +#ifndef QUANTUM_PAINTER_NUM_IMAGES +/** + * @def This controls the maximum number of images that Quantum Painter can load at any one time. Images can be loaded + *      using \ref qp_load_image_mem, and can be unloaded by calling \ref qp_close_image. Increasing this number in + *      order to load more images increases the amount of RAM required. Image data is not held in RAM, just metadata. + */ +#    define QUANTUM_PAINTER_NUM_IMAGES 8 +#endif // QUANTUM_PAINTER_NUM_IMAGES + +#ifndef QUANTUM_PAINTER_NUM_FONTS +/** + * @def This controls the maximum number of fonts that Quantum Painter can load. Fonts can be loaded using + *      \ref qp_load_font_mem, and can be unloaded by calling \ref qp_close_font. Increasing this number in order to + *      load more fonts increases the amount of RAM required. Font data is not held in RAM, unless + *      \ref QUANTUM_PAINTER_LOAD_FONTS_TO_RAM is set to TRUE. + */ +#    define QUANTUM_PAINTER_NUM_FONTS 4 +#endif // QUANTUM_PAINTER_NUM_FONTS + +#ifndef QUANTUM_PAINTER_LOAD_FONTS_TO_RAM +/** + * @def This controls whether or not fonts should be cached in RAM. Under normal circumstances, fonts can have quite + *      random access patterns, and due to timing of flash memory or external storage, it may be a significant speedup + *      moving the font into RAM before use. Defaults to "off", but if it's enabled it will fallback to reading from the + *      original location if corresponding RAM could not be allocated (such as being too large). + */ +#    define QUANTUM_PAINTER_LOAD_FONTS_TO_RAM FALSE +#endif + +#ifndef QUANTUM_PAINTER_CONCURRENT_ANIMATIONS +/** + * @def This controls the maximum number of animations that Quantum Painter can play simultaneously. Increasing this + *      number in order to play more animations at the same time increases the amount of RAM required. + */ +#    define QUANTUM_PAINTER_CONCURRENT_ANIMATIONS 4 +#endif // QUANTUM_PAINTER_CONCURRENT_ANIMATIONS + +#ifndef QUANTUM_PAINTER_PIXDATA_BUFFER_SIZE +/** + * @def This controls the maximum size of the pixel data buffer used for single blocks of transmission. Larger buffers + *      means more data is processed at one time, with less frequent transmissions, at the cost of RAM. + */ +#    define QUANTUM_PAINTER_PIXDATA_BUFFER_SIZE 32 +#endif + +#ifndef QUANTUM_PAINTER_SUPPORTS_256_PALETTE +/** + * @def This controls whether 256-color palettes are supported. This has relatively hefty requirements on RAM -- at + *      least 1kB extra is required just to store the palette information, with more required for other metadata. + */ +#    define QUANTUM_PAINTER_SUPPORTS_256_PALETTE FALSE +#endif + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Quantum Painter types + +/** + * @typedef A handle to a Quantum Painter device, such as an LCD or OLED. Most Quantum Painter APIs require this + *          argument in order to perform operations on the display. + */ +typedef const void *painter_device_t; + +/** + * @typedef The desired rotation of a panel. Used as a parameter to \ref qp_init, and can be queried by + *          \ref qp_get_geometry. + */ +typedef enum { QP_ROTATION_0, QP_ROTATION_90, QP_ROTATION_180, QP_ROTATION_270 } painter_rotation_t; + +/** + * @typedef A descriptor for a Quantum Painter image. + */ +typedef struct painter_image_desc_t { +    uint16_t width;       ///< Image width +    uint16_t height;      ///< Image height +    uint16_t frame_count; ///< Number of frames in this image +} painter_image_desc_t; + +/** + * @typedef A handle to a Quantum Painter image. + */ +typedef const painter_image_desc_t *painter_image_handle_t; + +/** + * @typedef A descriptor for a Quantum Painter font. + */ +typedef struct painter_font_desc_t { +    uint8_t line_height; ///< The number of pixels in height for each line +} painter_font_desc_t; + +/** + * @typedef A handle to a Quantum Painter font. + */ +typedef const painter_font_desc_t *painter_font_handle_t; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Quantum Painter External API + +/** + * Initialize a device and set its rotation. + * + * @param device[in] the handle of the device to initialize + * @param rotation[in] the rotation to use + * @return true if initialization succeeded + * @return false if initialization failed + */ +bool qp_init(painter_device_t device, painter_rotation_t rotation); + +/** + * Controls whether a display is on or off. + * + * @note If backlighting is used to control brightness (such as for an LCD), it will need to be handled external to + *       Quantum Painter. + * + * @param device[in] the handle of the device to control + * @param power_on[in] whether or not the device should be on + * @return true if controlling the power state succeeded + * @return false if controlling the power state failed + */ +bool qp_power(painter_device_t device, bool power_on); + +/** + * Clears a device's screen. + * + * @param device[in] the handle of the device to control + * @return true if clearing the screen succeeded + * @return false if clearing the screen failed + */ +bool qp_clear(painter_device_t device); + +/** + * Transmits any outstanding data to the screen in order to persist all changes to the display. + * + * @note Drivers without internal framebuffers will likely ignore this API. + * + * @param device[in] the handle of the device to control + * @return true if flushing changes to the screen succeeded + * @return false if flushing changes to the screen failed + */ +bool qp_flush(painter_device_t device); + +/** + * Retrieves the size, rotation, and offsets for the display. + * + * @note Any arguments of NULL will be ignored. + * + * @param device[in] the handle of the device to control + * @param width[out] the device's width + * @param height[out] the device's height + * @param rotation[out] the device's rotation + * @param offset_x[out] the device's x-offset applied while drawing + * @param offset_y[out] the device's y-offset applied while drawing + */ +void qp_get_geometry(painter_device_t device, uint16_t *width, uint16_t *height, painter_rotation_t *rotation, uint16_t *offset_x, uint16_t *offset_y); + +/** + * Allows repositioning of the viewport if the panel geometry offsets are non-zero. + * + * @param device[in] the handle of the device to control + * @param offset_x[in] the device's x-offset applied while drawing + * @param offset_y[in] the device's y-offset applied while drawing + */ +void qp_set_viewport_offsets(painter_device_t device, uint16_t offset_x, uint16_t offset_y); + +/** + * Sets a pixel to the specified color. + * + * @param device[in] the handle of the device to control + * @param x[in] the x-position to draw onto the device + * @param y[in] the y-position to draw onto the device + * @param hue[in] the hue to use, with 0-360 mapped to 0-255 + * @param sat[in] the saturation to use, with 0-100% mapped to 0-255 + * @param val[in] the value to use, with 0-100% mapped to 0-255 + * @return true if setting the pixel succeeded + * @return false if setting the pixel failed + */ +bool qp_setpixel(painter_device_t device, uint16_t x, uint16_t y, uint8_t hue, uint8_t sat, uint8_t val); + +/** + * Draws a line using the specified color. + * + * @param device[in] the handle of the device to control + * @param x0[in] the device's x-position to start + * @param y0[in] the device's y-position to start + * @param x1[in] the device's x-position to finish + * @param y1[in] the device's y-position to finish + * @param hue[in] the hue to use, with 0-360 mapped to 0-255 + * @param sat[in] the saturation to use, with 0-100% mapped to 0-255 + * @param val[in] the value to use, with 0-100% mapped to 0-255 + * @return true if drawing the line succeeded + * @return false if drawing the line failed + */ +bool qp_line(painter_device_t device, uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint8_t hue, uint8_t sat, uint8_t val); + +/** + * Draws a rectangle using the specified color, optionally filled. + * + * @param device[in] the handle of the device to control + * @param left[in] the device's x-position to start + * @param top[in] the device's y-position to start + * @param right[in] the device's x-position to finish + * @param bottom[in] the device's y-position to finish + * @param hue[in] the hue to use, with 0-360 mapped to 0-255 + * @param sat[in] the saturation to use, with 0-100% mapped to 0-255 + * @param val[in] the value to use, with 0-100% mapped to 0-255 + * @param filled[in] whether the rectangle should be filled + * @return true if drawing the rectangle succeeded + * @return false if drawing the rectangle failed + */ +bool qp_rect(painter_device_t device, uint16_t left, uint16_t top, uint16_t right, uint16_t bottom, uint8_t hue, uint8_t sat, uint8_t val, bool filled); + +/** + * Draws a circle using the specified color, optionally filled. + * + * @param device[in] the handle of the device to control + * @param x[in] the x-position of the centre of the circle to draw onto the device + * @param y[in] the y-position of the centre of the circle to draw onto the device + * @param radius[in] the radius of the circle to draw + * @param hue[in] the hue to use, with 0-360 mapped to 0-255 + * @param sat[in] the saturation to use, with 0-100% mapped to 0-255 + * @param val[in] the value to use, with 0-100% mapped to 0-255 + * @param filled[in] whether the circle should be filled + * @return true if drawing the circle succeeded + * @return false if drawing the circle failed + */ +bool qp_circle(painter_device_t device, uint16_t x, uint16_t y, uint16_t radius, uint8_t hue, uint8_t sat, uint8_t val, bool filled); + +/** + * Draws a ellipse using the specified color, optionally filled. + * + * @param device[in] the handle of the device to control + * @param x[in] the x-position of the centre of the ellipse to draw onto the device + * @param y[in] the y-position of the centre of the ellipse to draw onto the device + * @param sizex[in] the horizontal size of the ellipse + * @param sizey[in] the vertical size of the ellipse + * @param hue[in] the hue to use, with 0-360 mapped to 0-255 + * @param sat[in] the saturation to use, with 0-100% mapped to 0-255 + * @param val[in] the value to use, with 0-100% mapped to 0-255 + * @param filled[in] whether the ellipse should be filled + * @return true if drawing the ellipse succeeded + * @return false if drawing the ellipse failed + */ +bool qp_ellipse(painter_device_t device, uint16_t x, uint16_t y, uint16_t sizex, uint16_t sizey, uint8_t hue, uint8_t sat, uint8_t val, bool filled); + +/** + * Sets up the location on the display to stream raw pixel data to the display, using \ref qp_pixdata. + * + * @note This is for advanced uses only, and should not be required for normal Quantum Painter functionality. + * + * @param device[in] the handle of the device to control + * @param left[in] the device's x-position to start + * @param top[in] the device's y-position to start + * @param right[in] the device's x-position to finish + * @param bottom[in] the device's y-position to finish + * @return true if setting the viewport succeeded + * @return false if setting the viewport failed + */ +bool qp_viewport(painter_device_t device, uint16_t left, uint16_t top, uint16_t right, uint16_t bottom); + +/** + * Streams raw pixel data (in the native panel format) to the area previously set by \ref qp_viewport. + * + * @note This is for advanced uses only, and should not be required for normal Quantum Painter functionality. + * + * @param device[in] the handle of the device to control + * @param pixel_data[in] pointer to buffer data + * @param native_pixel_count[in] the number of pixels to transmit + * @return true if streaming of data succeeded + * @return false if streaming of data failed + */ +bool qp_pixdata(painter_device_t device, const void *pixel_data, uint32_t native_pixel_count); + +/** + * Loads an image into memory. + * + * @note Images can be unloaded by calling \ref qp_close_image. + * + * @param buffer[in] the image data to load + * @return an image handle usable with \ref qp_drawimage, \ref qp_drawimage_recolor, \ref qp_animate, and + *         \ref qp_animate_recolor. + * @return NULL if loading the image failed + */ +painter_image_handle_t qp_load_image_mem(const void *buffer); + +/** + * Closes an image handle when no longer in use. + * + * @param image[in] the handle of the image to unload + * @return true if unloading the image succeeded + * @return false if unloading the image failed + */ +bool qp_close_image(painter_image_handle_t image); + +/** + * Draws an image to the display. + * + * @param device[in] the handle of the device to control + * @param x[in] the x-position where the image should be drawn onto the device + * @param y[in] the y-position where the image should be drawn onto the device + * @param image[in] the handle of the image to draw + * @return true if drawing the image succeeded + * @return false if drawing the image failed + */ +bool qp_drawimage(painter_device_t device, uint16_t x, uint16_t y, painter_image_handle_t image); + +/** + * Draws an image to the display, recoloring monochrome images to the desired foreground/background. + * + * @param device[in] the handle of the device to control + * @param x[in] the x-position where the image should be drawn onto the device + * @param y[in] the y-position where the image should be drawn onto the device + * @param image[in] the handle of the image to draw + * @param hue_fg[in] the foreground hue to use, with 0-360 mapped to 0-255 + * @param sat_fg[in] the foreground saturation to use, with 0-100% mapped to 0-255 + * @param val_fg[in] the foreground value to use, with 0-100% mapped to 0-255 + * @param hue_bg[in] the background hue to use, with 0-360 mapped to 0-255 + * @param sat_bg[in] the background saturation to use, with 0-100% mapped to 0-255 + * @param val_bg[in] the background value to use, with 0-100% mapped to 0-255 + * @return true if drawing the image succeeded + * @return false if drawing the image failed + */ +bool qp_drawimage_recolor(painter_device_t device, uint16_t x, uint16_t y, painter_image_handle_t image, uint8_t hue_fg, uint8_t sat_fg, uint8_t val_fg, uint8_t hue_bg, uint8_t sat_bg, uint8_t val_bg); + +/** + * Draws an animation to the display. + * + * @param device[in] the handle of the device to control + * @param x[in] the x-position where the image should be drawn onto the device + * @param y[in] the y-position where the image should be drawn onto the device + * @param image[in] the handle of the image to draw + * @return the \ref deferred_token to use with \ref qp_stop_animation in order to stop animating + * @return INVALID_DEFERRED_TOKEN if animating the image failed + */ +deferred_token qp_animate(painter_device_t device, uint16_t x, uint16_t y, painter_image_handle_t image); + +/** + * Draws an animation to the display, recoloring monochrome images to the desired foreground/background. + * + * @param device[in] the handle of the device to control + * @param x[in] the x-position where the image should be drawn onto the device + * @param y[in] the y-position where the image should be drawn onto the device + * @param image[in] the handle of the image to draw + * @param hue_fg[in] the foreground hue to use, with 0-360 mapped to 0-255 + * @param sat_fg[in] the foreground saturation to use, with 0-100% mapped to 0-255 + * @param val_fg[in] the foreground value to use, with 0-100% mapped to 0-255 + * @param hue_bg[in] the background hue to use, with 0-360 mapped to 0-255 + * @param sat_bg[in] the background saturation to use, with 0-100% mapped to 0-255 + * @param val_bg[in] the background value to use, with 0-100% mapped to 0-255 + * @return the \ref deferred_token to use with \ref qp_stop_animation in order to stop animating + * @return INVALID_DEFERRED_TOKEN if animating the image failed + */ +deferred_token qp_animate_recolor(painter_device_t device, uint16_t x, uint16_t y, painter_image_handle_t image, uint8_t hue_fg, uint8_t sat_fg, uint8_t val_fg, uint8_t hue_bg, uint8_t sat_bg, uint8_t val_bg); + +/** + * Cancels a running animation. + * + * @param anim_token[in] the animation token returned by \ref qp_animate, or \ref qp_animate_recolor. + */ +void qp_stop_animation(deferred_token anim_token); + +/** + * Loads a font into memory. + * + * @note Fonts can be unloaded by calling \ref qp_close_font. + * + * @param buffer[in] the font data to load + * @return an image handle usable with \ref qp_textwidth, \ref qp_drawtext, and \ref qp_drawtext_recolor. + * @return NULL if loading the font failed + */ +painter_font_handle_t qp_load_font_mem(const void *buffer); + +/** + * Closes a font handle when no longer in use. + * + * @param font[in] the handle of the font to unload + * @return true if unloading the font succeeded + * @return false if unloading the font failed + */ +bool qp_close_font(painter_font_handle_t font); + +/** + * Measures the width (in pixels) of the supplied string, given the specified font. + * + * @param font[in] the handle of the font + * @param str[in] the string to measure + * @return the width (in pixels) needed to draw the specified string + */ +int16_t qp_textwidth(painter_font_handle_t font, const char *str); + +/** + * Draws text to the display. + * + * @param device[in] the handle of the device to control + * @param x[in] the x-position where the text should be drawn onto the device + * @param y[in] the y-position where the text should be drawn onto the device + * @param font[in] the handle of the font + * @param str[in] the string to draw + * @return the width (in pixels) used when drawing the specified string + */ +int16_t qp_drawtext(painter_device_t device, uint16_t x, uint16_t y, painter_font_handle_t font, const char *str); + +/** + * Draws text to the display, recoloring monochrome fonts to the desired foreground/background. + * + * @param device[in] the handle of the device to control + * @param x[in] the x-position where the text should be drawn onto the device + * @param y[in] the y-position where the text should be drawn onto the device + * @param font[in] the handle of the font + * @param str[in] the string to draw + * @param hue_fg[in] the foreground hue to use, with 0-360 mapped to 0-255 + * @param sat_fg[in] the foreground saturation to use, with 0-100% mapped to 0-255 + * @param val_fg[in] the foreground value to use, with 0-100% mapped to 0-255 + * @param hue_bg[in] the background hue to use, with 0-360 mapped to 0-255 + * @param sat_bg[in] the background saturation to use, with 0-100% mapped to 0-255 + * @param val_bg[in] the background value to use, with 0-100% mapped to 0-255 + * @return the width (in pixels) used when drawing the specified string + */ +int16_t qp_drawtext_recolor(painter_device_t device, uint16_t x, uint16_t y, painter_font_handle_t font, const char *str, uint8_t hue_fg, uint8_t sat_fg, uint8_t val_fg, uint8_t hue_bg, uint8_t sat_bg, uint8_t val_bg); + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Quantum Painter Drivers + +#ifdef QUANTUM_PAINTER_ILI9163_ENABLE +#    include "qp_ili9163.h" +#endif // QUANTUM_PAINTER_ILI9163_ENABLE + +#ifdef QUANTUM_PAINTER_ILI9341_ENABLE +#    include "qp_ili9341.h" +#endif // QUANTUM_PAINTER_ILI9341_ENABLE + +#ifdef QUANTUM_PAINTER_ST7789_ENABLE +#    include "qp_st7789.h" +#endif // QUANTUM_PAINTER_ST7789_ENABLE + +#ifdef QUANTUM_PAINTER_GC9A01_ENABLE +#    include "qp_gc9a01.h" +#endif // QUANTUM_PAINTER_GC9A01_ENABLE + +#ifdef QUANTUM_PAINTER_SSD1351_ENABLE +#    include "qp_ssd1351.h" +#endif // QUANTUM_PAINTER_SSD1351_ENABLE diff --git a/quantum/painter/qp_comms.c b/quantum/painter/qp_comms.c new file mode 100644 index 0000000000..dc17b49460 --- /dev/null +++ b/quantum/painter/qp_comms.c @@ -0,0 +1,72 @@ +// Copyright 2021 Nick Brassel (@tzarc) +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "qp_comms.h" + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Base comms APIs + +bool qp_comms_init(painter_device_t device) { +    struct painter_driver_t *driver = (struct painter_driver_t *)device; +    if (!driver->validate_ok) { +        qp_dprintf("qp_comms_init: fail (validation_ok == false)\n"); +        return false; +    } + +    return driver->comms_vtable->comms_init(device); +} + +bool qp_comms_start(painter_device_t device) { +    struct painter_driver_t *driver = (struct painter_driver_t *)device; +    if (!driver->validate_ok) { +        qp_dprintf("qp_comms_start: fail (validation_ok == false)\n"); +        return false; +    } + +    return driver->comms_vtable->comms_start(device); +} + +void qp_comms_stop(painter_device_t device) { +    struct painter_driver_t *driver = (struct painter_driver_t *)device; +    if (!driver->validate_ok) { +        qp_dprintf("qp_comms_stop: fail (validation_ok == false)\n"); +        return; +    } + +    driver->comms_vtable->comms_stop(device); +} + +uint32_t qp_comms_send(painter_device_t device, const void *data, uint32_t byte_count) { +    struct painter_driver_t *driver = (struct painter_driver_t *)device; +    if (!driver->validate_ok) { +        qp_dprintf("qp_comms_send: fail (validation_ok == false)\n"); +        return false; +    } + +    return driver->comms_vtable->comms_send(device, data, byte_count); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Comms APIs that use a D/C pin + +void qp_comms_command(painter_device_t device, uint8_t cmd) { +    struct painter_driver_t *                   driver       = (struct painter_driver_t *)device; +    struct painter_comms_with_command_vtable_t *comms_vtable = (struct painter_comms_with_command_vtable_t *)driver->comms_vtable; +    comms_vtable->send_command(device, cmd); +} + +void qp_comms_command_databyte(painter_device_t device, uint8_t cmd, uint8_t data) { +    qp_comms_command(device, cmd); +    qp_comms_send(device, &data, sizeof(data)); +} + +uint32_t qp_comms_command_databuf(painter_device_t device, uint8_t cmd, const void *data, uint32_t byte_count) { +    qp_comms_command(device, cmd); +    return qp_comms_send(device, data, byte_count); +} + +void qp_comms_bulk_command_sequence(painter_device_t device, const uint8_t *sequence, size_t sequence_len) { +    struct painter_driver_t *                   driver       = (struct painter_driver_t *)device; +    struct painter_comms_with_command_vtable_t *comms_vtable = (struct painter_comms_with_command_vtable_t *)driver->comms_vtable; +    comms_vtable->bulk_command_sequence(device, sequence, sequence_len); +} diff --git a/quantum/painter/qp_comms.h b/quantum/painter/qp_comms.h new file mode 100644 index 0000000000..8fbf25c201 --- /dev/null +++ b/quantum/painter/qp_comms.h @@ -0,0 +1,25 @@ +// Copyright 2021 Nick Brassel (@tzarc) +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include <stdbool.h> +#include <stdlib.h> + +#include "qp_internal.h" + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Base comms APIs + +bool     qp_comms_init(painter_device_t device); +bool     qp_comms_start(painter_device_t device); +void     qp_comms_stop(painter_device_t device); +uint32_t qp_comms_send(painter_device_t device, const void* data, uint32_t byte_count); + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Comms APIs that use a D/C pin + +void     qp_comms_command(painter_device_t device, uint8_t cmd); +void     qp_comms_command_databyte(painter_device_t device, uint8_t cmd, uint8_t data); +uint32_t qp_comms_command_databuf(painter_device_t device, uint8_t cmd, const void* data, uint32_t byte_count); +void     qp_comms_bulk_command_sequence(painter_device_t device, const uint8_t* sequence, size_t sequence_len); diff --git a/quantum/painter/qp_draw.h b/quantum/painter/qp_draw.h new file mode 100644 index 0000000000..7094d80eaa --- /dev/null +++ b/quantum/painter/qp_draw.h @@ -0,0 +1,85 @@ +// Copyright 2021 Nick Brassel (@tzarc) +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "qp_internal.h" +#include "qp_stream.h" + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Quantum Painter utility functions + +// Global variable used for native pixel data streaming. +extern uint8_t qp_internal_global_pixdata_buffer[QUANTUM_PAINTER_PIXDATA_BUFFER_SIZE]; + +// Check if the supplied bpp is capable of being rendered +bool qp_internal_bpp_capable(uint8_t bits_per_pixel); + +// Returns the number of pixels that can fit in the pixdata buffer +uint32_t qp_internal_num_pixels_in_buffer(painter_device_t device); + +// Fills the supplied buffer with equivalent native pixels matching the supplied HSV +void qp_internal_fill_pixdata(painter_device_t device, uint32_t num_pixels, uint8_t hue, uint8_t sat, uint8_t val); + +// qp_setpixel internal implementation, but uses the global pixdata buffer with pre-converted native pixel. Only the first pixel is used. +bool qp_internal_setpixel_impl(painter_device_t device, uint16_t x, uint16_t y); + +// qp_rect internal implementation, but uses the global pixdata buffer with pre-converted native pixels. +bool qp_internal_fillrect_helper_impl(painter_device_t device, uint16_t l, uint16_t t, uint16_t r, uint16_t b); + +// Convert from input pixel data + palette to equivalent pixels +typedef int16_t (*qp_internal_byte_input_callback)(void* cb_arg); +typedef bool (*qp_internal_pixel_output_callback)(qp_pixel_t* palette, uint8_t index, void* cb_arg); +bool qp_internal_decode_palette(painter_device_t device, uint32_t pixel_count, uint8_t bits_per_pixel, qp_internal_byte_input_callback input_callback, void* input_arg, qp_pixel_t* palette, qp_internal_pixel_output_callback output_callback, void* output_arg); +bool qp_internal_decode_grayscale(painter_device_t device, uint32_t pixel_count, uint8_t bits_per_pixel, qp_internal_byte_input_callback input_callback, void* input_arg, qp_internal_pixel_output_callback output_callback, void* output_arg); +bool qp_internal_decode_recolor(painter_device_t device, uint32_t pixel_count, uint8_t bits_per_pixel, qp_internal_byte_input_callback input_callback, void* input_arg, qp_pixel_t fg_hsv888, qp_pixel_t bg_hsv888, qp_internal_pixel_output_callback output_callback, void* output_arg); + +// Global variable used for interpolated pixel lookup table. +#if QUANTUM_PAINTER_SUPPORTS_256_PALETTE +extern qp_pixel_t qp_internal_global_pixel_lookup_table[256]; +#else +extern qp_pixel_t qp_internal_global_pixel_lookup_table[16]; +#endif + +// Generates a color-interpolated lookup table based off the number of items, from foreground to background, for use with monochrome image rendering. +// Returns true if a palette was created, false if the palette is reused. +// As this uses a global, this may present a problem if using the same parameters but a different screen converts pixels -- use qp_internal_invalidate_palette() below to reset. +bool qp_internal_interpolate_palette(qp_pixel_t fg_hsv888, qp_pixel_t bg_hsv888, int16_t steps); + +// Resets the global palette so that it can be regenerated. Only needed if the colors are identical, but a different display is used with a different internal pixel format. +void qp_internal_invalidate_palette(void); + +// Helper shared between image and font rendering -- sets up the global palette to match the palette block specified in the asset. Expects the stream to be positioned at the start of the block header. +bool qp_internal_load_qgf_palette(qp_stream_t* stream, uint8_t bpp); + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Quantum Painter codec functions + +enum qp_internal_rle_mode_t { +    MARKER_BYTE, +    REPEATING_RUN, +    NON_REPEATING_RUN, +}; + +struct qp_internal_byte_input_state { +    painter_device_t device; +    qp_stream_t*     src_stream; +    int16_t          curr; +    union { +        // RLE-specific +        struct { +            enum qp_internal_rle_mode_t mode; +            uint8_t                     remain; // number of bytes remaining in the current mode +        } rle; +    }; +}; + +struct qp_internal_pixel_output_state { +    painter_device_t device; +    uint32_t         pixel_write_pos; +    uint32_t         max_pixels; +}; + +bool qp_internal_pixel_appender(qp_pixel_t* palette, uint8_t index, void* cb_arg); + +qp_internal_byte_input_callback qp_internal_prepare_input_state(struct qp_internal_byte_input_state* input_state, painter_compression_t compression); diff --git a/quantum/painter/qp_draw_circle.c b/quantum/painter/qp_draw_circle.c new file mode 100644 index 0000000000..edaae35835 --- /dev/null +++ b/quantum/painter/qp_draw_circle.c @@ -0,0 +1,172 @@ +// Copyright 2021 Paul Cotter (@gr1mr3aver) +// Copyright 2021 Nick Brassel (@tzarc) +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "qp.h" +#include "qp_internal.h" +#include "qp_comms.h" +#include "qp_draw.h" + +// Utilize 8-way symmetry to draw circles +static bool qp_circle_helper_impl(painter_device_t device, uint16_t centerx, uint16_t centery, uint16_t offsetx, uint16_t offsety, bool filled) { +    /* +    Circles have the property of 8-way symmetry, so eight pixels can be drawn +    for each computed [offsetx,offsety] given the center coordinates +    represented by [centerx,centery]. + +    For filled circles, we can draw horizontal lines between each pair of +    pixels with the same final value of y. + +    Two special cases exist and have been optimized: +    1) offsetx == offsety (the final point), makes half the coordinates +    equivalent, so we can omit them (and the corresponding fill lines) +    2) offsetx == 0 (the starting point) means that some horizontal lines +    would be a single pixel in length, so we write individual pixels instead. +    This also makes half the symmetrical points identical to their twins, +    so we only need four points or two points and one line +    */ + +    int16_t xpx = ((int16_t)centerx) + ((int16_t)offsetx); +    int16_t xmx = ((int16_t)centerx) - ((int16_t)offsetx); +    int16_t xpy = ((int16_t)centerx) + ((int16_t)offsety); +    int16_t xmy = ((int16_t)centerx) - ((int16_t)offsety); +    int16_t ypx = ((int16_t)centery) + ((int16_t)offsetx); +    int16_t ymx = ((int16_t)centery) - ((int16_t)offsetx); +    int16_t ypy = ((int16_t)centery) + ((int16_t)offsety); +    int16_t ymy = ((int16_t)centery) - ((int16_t)offsety); + +    if (offsetx == 0) { +        if (!qp_internal_setpixel_impl(device, centerx, ypy)) { +            return false; +        } +        if (!qp_internal_setpixel_impl(device, centerx, ymy)) { +            return false; +        } +        if (filled) { +            if (!qp_internal_fillrect_helper_impl(device, xpy, centery, xmy, centery)) { +                return false; +            } +        } else { +            if (!qp_internal_setpixel_impl(device, xpy, centery)) { +                return false; +            } +            if (!qp_internal_setpixel_impl(device, xmy, centery)) { +                return false; +            } +        } +    } else if (offsetx == offsety) { +        if (filled) { +            if (!qp_internal_fillrect_helper_impl(device, xpy, ypy, xmy, ypy)) { +                return false; +            } +            if (!qp_internal_fillrect_helper_impl(device, xpy, ymy, xmy, ymy)) { +                return false; +            } +        } else { +            if (!qp_internal_setpixel_impl(device, xpy, ypy)) { +                return false; +            } +            if (!qp_internal_setpixel_impl(device, xmy, ypy)) { +                return false; +            } +            if (!qp_internal_setpixel_impl(device, xpy, ymy)) { +                return false; +            } +            if (!qp_internal_setpixel_impl(device, xmy, ymy)) { +                return false; +            } +        } + +    } else { +        if (filled) { +            if (!qp_internal_fillrect_helper_impl(device, xpx, ypy, xmx, ypy)) { +                return false; +            } +            if (!qp_internal_fillrect_helper_impl(device, xpx, ymy, xmx, ymy)) { +                return false; +            } +            if (!qp_internal_fillrect_helper_impl(device, xpy, ypx, xmy, ypx)) { +                return false; +            } +            if (!qp_internal_fillrect_helper_impl(device, xpy, ymx, xmy, ymx)) { +                return false; +            } +        } else { +            if (!qp_internal_setpixel_impl(device, xpx, ypy)) { +                return false; +            } +            if (!qp_internal_setpixel_impl(device, xmx, ypy)) { +                return false; +            } +            if (!qp_internal_setpixel_impl(device, xpx, ymy)) { +                return false; +            } +            if (!qp_internal_setpixel_impl(device, xmx, ymy)) { +                return false; +            } +            if (!qp_internal_setpixel_impl(device, xpy, ypx)) { +                return false; +            } +            if (!qp_internal_setpixel_impl(device, xmy, ypx)) { +                return false; +            } +            if (!qp_internal_setpixel_impl(device, xpy, ymx)) { +                return false; +            } +            if (!qp_internal_setpixel_impl(device, xmy, ymx)) { +                return false; +            } +        } +    } + +    return true; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Quantum Painter External API: qp_circle + +bool qp_circle(painter_device_t device, uint16_t x, uint16_t y, uint16_t radius, uint8_t hue, uint8_t sat, uint8_t val, bool filled) { +    qp_dprintf("qp_circle: entry\n"); +    struct painter_driver_t *driver = (struct painter_driver_t *)device; +    if (!driver->validate_ok) { +        qp_dprintf("qp_circle: fail (validation_ok == false)\n"); +        return false; +    } + +    // plot the initial set of points for x, y and r +    int16_t xcalc = 0; +    int16_t ycalc = (int16_t)radius; +    int16_t err   = ((5 - (radius >> 2)) >> 2); + +    qp_internal_fill_pixdata(device, (radius * 2) + 1, hue, sat, val); + +    if (!qp_comms_start(device)) { +        qp_dprintf("qp_circle: fail (could not start comms)\n"); +        return false; +    } + +    bool ret = true; +    if (!qp_circle_helper_impl(device, x, y, xcalc, ycalc, filled)) { +        ret = false; +    } + +    if (ret) { +        while (xcalc < ycalc) { +            xcalc++; +            if (err < 0) { +                err += (xcalc << 1) + 1; +            } else { +                ycalc--; +                err += ((xcalc - ycalc) << 1) + 1; +            } +            if (!qp_circle_helper_impl(device, x, y, xcalc, ycalc, filled)) { +                ret = false; +                break; +            } +        } +    } + +    qp_dprintf("qp_circle: %s\n", ret ? "ok" : "fail"); +    qp_comms_stop(device); +    return ret; +} diff --git a/quantum/painter/qp_draw_codec.c b/quantum/painter/qp_draw_codec.c new file mode 100644 index 0000000000..438dce3994 --- /dev/null +++ b/quantum/painter/qp_draw_codec.c @@ -0,0 +1,142 @@ +// Copyright 2021 Nick Brassel (@tzarc) +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "qp_internal.h" +#include "qp_draw.h" +#include "qp_comms.h" + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Palette / Monochrome-format decoder + +static const qp_pixel_t qp_pixel_white = {.hsv888 = {.h = 0, .s = 0, .v = 255}}; +static const qp_pixel_t qp_pixel_black = {.hsv888 = {.h = 0, .s = 0, .v = 0}}; + +bool qp_internal_bpp_capable(uint8_t bits_per_pixel) { +#if !(QUANTUM_PAINTER_SUPPORTS_256_PALETTE) +    if (bits_per_pixel > 4) { +        qp_dprintf("qp_internal_decode_palette: image bpp greater than 4\n"); +        return false; +    } +#endif + +    if (bits_per_pixel > 8) { +        qp_dprintf("qp_internal_decode_palette: image bpp greater than 8\n"); +        return false; +    } + +    return true; +} + +bool qp_internal_decode_palette(painter_device_t device, uint32_t pixel_count, uint8_t bits_per_pixel, qp_internal_byte_input_callback input_callback, void* input_arg, qp_pixel_t* palette, qp_internal_pixel_output_callback output_callback, void* output_arg) { +    const uint8_t pixel_bitmask    = (1 << bits_per_pixel) - 1; +    const uint8_t pixels_per_byte  = 8 / bits_per_pixel; +    uint32_t      remaining_pixels = pixel_count; // don't try to derive from byte_count, we may not use an entire byte +    while (remaining_pixels > 0) { +        uint8_t byteval = input_callback(input_arg); +        if (byteval < 0) { +            return false; +        } +        uint8_t loop_pixels = remaining_pixels < pixels_per_byte ? remaining_pixels : pixels_per_byte; +        for (uint8_t q = 0; q < loop_pixels; ++q) { +            if (!output_callback(palette, byteval & pixel_bitmask, output_arg)) { +                return false; +            } +            byteval >>= bits_per_pixel; +        } +        remaining_pixels -= loop_pixels; +    } +    return true; +} + +bool qp_internal_decode_grayscale(painter_device_t device, uint32_t pixel_count, uint8_t bits_per_pixel, qp_internal_byte_input_callback input_callback, void* input_arg, qp_internal_pixel_output_callback output_callback, void* output_arg) { +    return qp_internal_decode_recolor(device, pixel_count, bits_per_pixel, input_callback, input_arg, qp_pixel_white, qp_pixel_black, output_callback, output_arg); +} + +bool qp_internal_decode_recolor(painter_device_t device, uint32_t pixel_count, uint8_t bits_per_pixel, qp_internal_byte_input_callback input_callback, void* input_arg, qp_pixel_t fg_hsv888, qp_pixel_t bg_hsv888, qp_internal_pixel_output_callback output_callback, void* output_arg) { +    struct painter_driver_t* driver = (struct painter_driver_t*)device; +    int16_t                  steps  = 1 << bits_per_pixel; // number of items we need to interpolate +    if (qp_internal_interpolate_palette(fg_hsv888, bg_hsv888, steps)) { +        if (!driver->driver_vtable->palette_convert(device, steps, qp_internal_global_pixel_lookup_table)) { +            return false; +        } +    } + +    return qp_internal_decode_palette(device, pixel_count, bits_per_pixel, input_callback, input_arg, qp_internal_global_pixel_lookup_table, output_callback, output_arg); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Progressive pull of bytes, push of pixels + +static inline int16_t qp_drawimage_byte_uncompressed_decoder(void* cb_arg) { +    struct qp_internal_byte_input_state* state = (struct qp_internal_byte_input_state*)cb_arg; +    state->curr                                = qp_stream_get(state->src_stream); +    return state->curr; +} + +static inline int16_t qp_drawimage_byte_rle_decoder(void* cb_arg) { +    struct qp_internal_byte_input_state* state = (struct qp_internal_byte_input_state*)cb_arg; + +    // Work out if we're parsing the initial marker byte +    if (state->rle.mode == MARKER_BYTE) { +        uint8_t c = qp_stream_get(state->src_stream); +        if (c >= 128) { +            state->rle.mode   = NON_REPEATING_RUN; // non-repeated run +            state->rle.remain = c - 127; +        } else { +            state->rle.mode   = REPEATING_RUN; // repeated run +            state->rle.remain = c; +        } + +        state->curr = qp_stream_get(state->src_stream); +    } + +    // Work out which byte we're returning +    uint8_t c = state->curr; + +    // Decrement the counter of the bytes remaining +    state->rle.remain--; + +    if (state->rle.remain > 0) { +        // If we're in a non-repeating run, queue up the next byte +        if (state->rle.mode == NON_REPEATING_RUN) { +            state->curr = qp_stream_get(state->src_stream); +        } +    } else { +        // Swap back to querying the marker byte mode +        state->rle.mode = MARKER_BYTE; +    } + +    return c; +} + +bool qp_internal_pixel_appender(qp_pixel_t* palette, uint8_t index, void* cb_arg) { +    struct qp_internal_pixel_output_state* state  = (struct qp_internal_pixel_output_state*)cb_arg; +    struct painter_driver_t*               driver = (struct painter_driver_t*)state->device; + +    if (!driver->driver_vtable->append_pixels(state->device, qp_internal_global_pixdata_buffer, palette, state->pixel_write_pos++, 1, &index)) { +        return false; +    } + +    // If we've hit the transmit limit, send out the entire buffer and reset the write position +    if (state->pixel_write_pos == state->max_pixels) { +        if (!driver->driver_vtable->pixdata(state->device, qp_internal_global_pixdata_buffer, state->pixel_write_pos)) { +            return false; +        } +        state->pixel_write_pos = 0; +    } + +    return true; +} + +qp_internal_byte_input_callback qp_internal_prepare_input_state(struct qp_internal_byte_input_state* input_state, painter_compression_t compression) { +    switch (compression) { +        case IMAGE_UNCOMPRESSED: +            return qp_drawimage_byte_uncompressed_decoder; +        case IMAGE_COMPRESSED_RLE: +            input_state->rle.mode   = MARKER_BYTE; +            input_state->rle.remain = 0; +            return qp_drawimage_byte_rle_decoder; +        default: +            return NULL; +    } +} diff --git a/quantum/painter/qp_draw_core.c b/quantum/painter/qp_draw_core.c new file mode 100644 index 0000000000..c31c734132 --- /dev/null +++ b/quantum/painter/qp_draw_core.c @@ -0,0 +1,294 @@ +// Copyright 2021-2022 Nick Brassel (@tzarc) +// Copyright 2021 Paul Cotter (@gr1mr3aver) +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "qp_internal.h" +#include "qp_comms.h" +#include "qp_draw.h" +#include "qgf.h" + +_Static_assert((QUANTUM_PAINTER_PIXDATA_BUFFER_SIZE > 0) && (QUANTUM_PAINTER_PIXDATA_BUFFER_SIZE % 16) == 0, "QUANTUM_PAINTER_PIXDATA_BUFFER_SIZE needs to be a non-zero multiple of 16"); + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Global variables +// +// NOTE: The variables in this section are intentionally outside a stack frame. They are able to be defined with larger +//       sizes than the normal stack frames would allow, and as such need to be external. +// +//       **** DO NOT refactor this and decide to place the variables inside the function calling them -- you will **** +//       **** very likely get artifacts rendered to the screen as a result.                                       **** +// + +// Buffer used for transmitting native pixel data to the downstream device. +uint8_t qp_internal_global_pixdata_buffer[QUANTUM_PAINTER_PIXDATA_BUFFER_SIZE]; + +// Static buffer to contain a generated color palette +static bool       generated_palette = false; +static int16_t    generated_steps   = -1; +static qp_pixel_t interpolated_fg_hsv888; +static qp_pixel_t interpolated_bg_hsv888; +#if QUANTUM_PAINTER_SUPPORTS_256_PALETTE +qp_pixel_t qp_internal_global_pixel_lookup_table[256]; +#else +qp_pixel_t qp_internal_global_pixel_lookup_table[16]; +#endif + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Helpers + +uint32_t qp_internal_num_pixels_in_buffer(painter_device_t device) { +    struct painter_driver_t *driver = (struct painter_driver_t *)device; +    return ((QUANTUM_PAINTER_PIXDATA_BUFFER_SIZE * 8) / driver->native_bits_per_pixel); +} + +// qp_setpixel internal implementation, but accepts a buffer with pre-converted native pixel. Only the first pixel is used. +bool qp_internal_setpixel_impl(painter_device_t device, uint16_t x, uint16_t y) { +    struct painter_driver_t *driver = (struct painter_driver_t *)device; +    return driver->driver_vtable->viewport(device, x, y, x, y) && driver->driver_vtable->pixdata(device, qp_internal_global_pixdata_buffer, 1); +} + +// Fills the global native pixel buffer with equivalent pixels matching the supplied HSV +void qp_internal_fill_pixdata(painter_device_t device, uint32_t num_pixels, uint8_t hue, uint8_t sat, uint8_t val) { +    struct painter_driver_t *driver            = (struct painter_driver_t *)device; +    uint32_t                 pixels_in_pixdata = qp_internal_num_pixels_in_buffer(device); +    num_pixels                                 = QP_MIN(pixels_in_pixdata, num_pixels); + +    // Convert the color to native pixel format +    qp_pixel_t color = {.hsv888 = {.h = hue, .s = sat, .v = val}}; +    driver->driver_vtable->palette_convert(device, 1, &color); + +    // Append the required number of pixels +    uint8_t palette_idx = 0; +    for (uint32_t i = 0; i < num_pixels; ++i) { +        driver->driver_vtable->append_pixels(device, qp_internal_global_pixdata_buffer, &color, i, 1, &palette_idx); +    } +} + +// Resets the global palette so that it can be regenerated. Only needed if the colors are identical, but a different display is used with a different internal pixel format. +void qp_internal_invalidate_palette(void) { +    generated_palette = false; +    generated_steps   = -1; +} + +// Interpolates between two colors to generate a palette +bool qp_internal_interpolate_palette(qp_pixel_t fg_hsv888, qp_pixel_t bg_hsv888, int16_t steps) { +    // Check if we need to generate a new palette -- if the input parameters match then assume the palette can stay unchanged. +    // This may present a problem if using the same parameters but a different screen converts pixels -- use qp_internal_invalidate_palette() to reset. +    if (generated_palette == true && generated_steps == steps && memcmp(&interpolated_fg_hsv888, &fg_hsv888, sizeof(fg_hsv888)) == 0 && memcmp(&interpolated_bg_hsv888, &bg_hsv888, sizeof(bg_hsv888)) == 0) { +        // We already have the correct palette, no point regenerating it. +        return false; +    } + +    // Save the parameters so we know whether we can skip generation +    generated_palette      = true; +    generated_steps        = steps; +    interpolated_fg_hsv888 = fg_hsv888; +    interpolated_bg_hsv888 = bg_hsv888; + +    int16_t hue_fg = fg_hsv888.hsv888.h; +    int16_t hue_bg = bg_hsv888.hsv888.h; + +    // Make sure we take the "shortest" route from one hue to the other +    if ((hue_fg - hue_bg) >= 128) { +        hue_bg += 256; +    } else if ((hue_fg - hue_bg) <= -128) { +        hue_bg -= 256; +    } + +    // Interpolate each of the lookup table entries +    for (int16_t i = 0; i < steps; ++i) { +        qp_internal_global_pixel_lookup_table[i].hsv888.h = (uint8_t)((hue_fg - hue_bg) * i / (steps - 1) + hue_bg); +        qp_internal_global_pixel_lookup_table[i].hsv888.s = (uint8_t)((fg_hsv888.hsv888.s - bg_hsv888.hsv888.s) * i / (steps - 1) + bg_hsv888.hsv888.s); +        qp_internal_global_pixel_lookup_table[i].hsv888.v = (uint8_t)((fg_hsv888.hsv888.v - bg_hsv888.hsv888.v) * i / (steps - 1) + bg_hsv888.hsv888.v); + +        qp_dprintf("qp_internal_interpolate_palette: %3d of %d -- H: %3d, S: %3d, V: %3d\n", (int)(i + 1), (int)steps, (int)qp_internal_global_pixel_lookup_table[i].hsv888.h, (int)qp_internal_global_pixel_lookup_table[i].hsv888.s, (int)qp_internal_global_pixel_lookup_table[i].hsv888.v); +    } + +    return true; +} + +// Helper shared between image and font rendering -- sets up the global palette to match the palette block specified in the asset. Expects the stream to be positioned at the start of the block header. +bool qp_internal_load_qgf_palette(qp_stream_t *stream, uint8_t bpp) { +    qgf_palette_v1_t palette_descriptor; +    if (qp_stream_read(&palette_descriptor, sizeof(qgf_palette_v1_t), 1, stream) != 1) { +        qp_dprintf("Failed to read palette_descriptor, expected length was not %d\n", (int)sizeof(qgf_palette_v1_t)); +        return false; +    } + +    // BPP determines the number of palette entries, each entry is a HSV888 triplet. +    const uint16_t palette_entries = 1u << bpp; + +    // Ensure we aren't reusing any palette +    qp_internal_invalidate_palette(); + +    // Read the palette entries +    for (uint16_t i = 0; i < palette_entries; ++i) { +        // Read the palette entry +        qgf_palette_entry_v1_t entry; +        if (qp_stream_read(&entry, sizeof(qgf_palette_entry_v1_t), 1, stream) != 1) { +            return false; +        } + +        // Update the lookup table +        qp_internal_global_pixel_lookup_table[i].hsv888.h = entry.h; +        qp_internal_global_pixel_lookup_table[i].hsv888.s = entry.s; +        qp_internal_global_pixel_lookup_table[i].hsv888.v = entry.v; + +        qp_dprintf("qp_internal_load_qgf_palette: %3d of %d -- H: %3d, S: %3d, V: %3d\n", (int)(i + 1), (int)palette_entries, (int)qp_internal_global_pixel_lookup_table[i].hsv888.h, (int)qp_internal_global_pixel_lookup_table[i].hsv888.s, (int)qp_internal_global_pixel_lookup_table[i].hsv888.v); +    } + +    return true; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Quantum Painter External API: qp_setpixel + +bool qp_setpixel(painter_device_t device, uint16_t x, uint16_t y, uint8_t hue, uint8_t sat, uint8_t val) { +    struct painter_driver_t *driver = (struct painter_driver_t *)device; +    if (!driver->validate_ok) { +        qp_dprintf("qp_setpixel: fail (validation_ok == false)\n"); +        return false; +    } + +    if (!qp_comms_start(device)) { +        qp_dprintf("Failed to start comms in qp_setpixel\n"); +        return false; +    } + +    qp_internal_fill_pixdata(device, 1, hue, sat, val); +    bool ret = qp_internal_setpixel_impl(device, x, y); +    qp_comms_stop(device); +    qp_dprintf("qp_setpixel: %s\n", ret ? "ok" : "fail"); +    return ret; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Quantum Painter External API: qp_line + +bool qp_line(painter_device_t device, uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint8_t hue, uint8_t sat, uint8_t val) { +    if (x0 == x1 || y0 == y1) { +        qp_dprintf("qp_line(%d, %d, %d, %d): entry (deferring to qp_rect)\n", (int)x0, (int)y0, (int)x1, (int)y1); +        bool ret = qp_rect(device, x0, y0, x1, y1, hue, sat, val, true); +        qp_dprintf("qp_line(%d, %d, %d, %d): %s (deferred to qp_rect)\n", (int)x0, (int)y0, (int)x1, (int)y1, ret ? "ok" : "fail"); +        return ret; +    } + +    qp_dprintf("qp_line(%d, %d, %d, %d): entry\n", (int)x0, (int)y0, (int)x1, (int)y1); +    struct painter_driver_t *driver = (struct painter_driver_t *)device; +    if (!driver->validate_ok) { +        qp_dprintf("qp_line: fail (validation_ok == false)\n"); +        return false; +    } + +    if (!qp_comms_start(device)) { +        qp_dprintf("Failed to start comms in qp_line\n"); +        return false; +    } + +    qp_internal_fill_pixdata(device, 1, hue, sat, val); + +    // draw angled line using Bresenham's algo +    int16_t x      = ((int16_t)x0); +    int16_t y      = ((int16_t)y0); +    int16_t slopex = ((int16_t)x0) < ((int16_t)x1) ? 1 : -1; +    int16_t slopey = ((int16_t)y0) < ((int16_t)y1) ? 1 : -1; +    int16_t dx     = abs(((int16_t)x1) - ((int16_t)x0)); +    int16_t dy     = -abs(((int16_t)y1) - ((int16_t)y0)); + +    int16_t e  = dx + dy; +    int16_t e2 = 2 * e; + +    bool ret = true; +    while (x != x1 || y != y1) { +        if (!qp_internal_setpixel_impl(device, x, y)) { +            ret = false; +            break; +        } +        e2 = 2 * e; +        if (e2 >= dy) { +            e += dy; +            x += slopex; +        } +        if (e2 <= dx) { +            e += dx; +            y += slopey; +        } +    } +    // draw the last pixel +    if (!qp_internal_setpixel_impl(device, x, y)) { +        ret = false; +    } + +    qp_comms_stop(device); +    qp_dprintf("qp_line(%d, %d, %d, %d): %s\n", (int)x0, (int)y0, (int)x1, (int)y1, ret ? "ok" : "fail"); +    return ret; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Quantum Painter External API: qp_rect + +bool qp_internal_fillrect_helper_impl(painter_device_t device, uint16_t left, uint16_t top, uint16_t right, uint16_t bottom) { +    uint32_t                 pixels_in_pixdata = qp_internal_num_pixels_in_buffer(device); +    struct painter_driver_t *driver            = (struct painter_driver_t *)device; + +    uint16_t l = QP_MIN(left, right); +    uint16_t r = QP_MAX(left, right); +    uint16_t t = QP_MIN(top, bottom); +    uint16_t b = QP_MAX(top, bottom); +    uint16_t w = r - l + 1; +    uint16_t h = b - t + 1; + +    uint32_t remaining = w * h; +    driver->driver_vtable->viewport(device, l, t, r, b); +    while (remaining > 0) { +        uint32_t transmit = QP_MIN(remaining, pixels_in_pixdata); +        if (!driver->driver_vtable->pixdata(device, qp_internal_global_pixdata_buffer, transmit)) { +            return false; +        } +        remaining -= transmit; +    } +    return true; +} + +bool qp_rect(painter_device_t device, uint16_t left, uint16_t top, uint16_t right, uint16_t bottom, uint8_t hue, uint8_t sat, uint8_t val, bool filled) { +    qp_dprintf("qp_rect(%d, %d, %d, %d): entry\n", (int)left, (int)top, (int)right, (int)bottom); +    struct painter_driver_t *driver = (struct painter_driver_t *)device; +    if (!driver->validate_ok) { +        qp_dprintf("qp_rect: fail (validation_ok == false)\n"); +        return false; +    } + +    // Cater for cases where people have submitted the coordinates backwards +    uint16_t l = QP_MIN(left, right); +    uint16_t r = QP_MAX(left, right); +    uint16_t t = QP_MIN(top, bottom); +    uint16_t b = QP_MAX(top, bottom); +    uint16_t w = r - l + 1; +    uint16_t h = b - t + 1; + +    bool ret = true; +    if (!qp_comms_start(device)) { +        qp_dprintf("Failed to start comms in qp_rect\n"); +        return false; +    } + +    if (filled) { +        // Fill up the pixdata buffer with the required number of native pixels +        qp_internal_fill_pixdata(device, w * h, hue, sat, val); + +        // Perform the draw +        ret = qp_internal_fillrect_helper_impl(device, l, t, r, b); +    } else { +        // Fill up the pixdata buffer with the required number of native pixels +        qp_internal_fill_pixdata(device, QP_MAX(w, h), hue, sat, val); + +        // Draw 4x filled single-width rects to create an outline +        if (!qp_internal_fillrect_helper_impl(device, l, t, r, t) || !qp_internal_fillrect_helper_impl(device, l, b, r, b) || !qp_internal_fillrect_helper_impl(device, l, t + 1, l, b - 1) || !qp_internal_fillrect_helper_impl(device, r, t + 1, r, b - 1)) { +            ret = false; +        } +    } + +    qp_comms_stop(device); +    qp_dprintf("qp_rect(%d, %d, %d, %d): %s\n", (int)l, (int)t, (int)r, (int)b, ret ? "ok" : "fail"); +    return ret; +} diff --git a/quantum/painter/qp_draw_ellipse.c b/quantum/painter/qp_draw_ellipse.c new file mode 100644 index 0000000000..7f2f4abcfd --- /dev/null +++ b/quantum/painter/qp_draw_ellipse.c @@ -0,0 +1,116 @@ +// Copyright 2021 Paul Cotter (@gr1mr3aver) +// Copyright 2021 Nick Brassel (@tzarc) +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "qp_internal.h" +#include "qp_comms.h" +#include "qp_draw.h" + +// Utilize 4-way symmetry to draw an ellipse +static bool qp_ellipse_helper_impl(painter_device_t device, uint16_t centerx, uint16_t centery, uint16_t offsetx, uint16_t offsety, bool filled) { +    /* +    Ellipses have the property of 4-way symmetry, so four pixels can be drawn +    for each computed [offsetx,offsety] given the center coordinates +    represented by [centerx,centery]. + +    For filled ellipses, we can draw horizontal lines between each pair of +    pixels with the same final value of y. + +    When offsetx == 0 only two pixels can be drawn for filled or unfilled ellipses +    */ + +    int16_t xpx = ((int16_t)centerx) + ((int16_t)offsetx); +    int16_t xmx = ((int16_t)centerx) - ((int16_t)offsetx); +    int16_t ypy = ((int16_t)centery) + ((int16_t)offsety); +    int16_t ymy = ((int16_t)centery) - ((int16_t)offsety); + +    if (offsetx == 0) { +        if (!qp_internal_setpixel_impl(device, xpx, ypy)) { +            return false; +        } +        if (!qp_internal_setpixel_impl(device, xpx, ymy)) { +            return false; +        } +    } else if (filled) { +        if (!qp_internal_fillrect_helper_impl(device, xpx, ypy, xmx, ypy)) { +            return false; +        } +        if (offsety > 0 && !qp_internal_fillrect_helper_impl(device, xpx, ymy, xmx, ymy)) { +            return false; +        } +    } else { +        if (!qp_internal_setpixel_impl(device, xpx, ypy)) { +            return false; +        } +        if (!qp_internal_setpixel_impl(device, xpx, ymy)) { +            return false; +        } +        if (!qp_internal_setpixel_impl(device, xmx, ypy)) { +            return false; +        } +        if (!qp_internal_setpixel_impl(device, xmx, ymy)) { +            return false; +        } +    } + +    return true; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Quantum Painter External API: qp_ellipse + +bool qp_ellipse(painter_device_t device, uint16_t x, uint16_t y, uint16_t sizex, uint16_t sizey, uint8_t hue, uint8_t sat, uint8_t val, bool filled) { +    qp_dprintf("qp_ellipse: entry\n"); +    struct painter_driver_t *driver = (struct painter_driver_t *)device; +    if (!driver->validate_ok) { +        qp_dprintf("qp_ellipse: fail (validation_ok == false)\n"); +        return false; +    } + +    int16_t aa = ((int16_t)sizex) * ((int16_t)sizex); +    int16_t bb = ((int16_t)sizey) * ((int16_t)sizey); +    int16_t fa = 4 * ((int16_t)aa); +    int16_t fb = 4 * ((int16_t)bb); + +    int16_t dx = 0; +    int16_t dy = ((int16_t)sizey); + +    qp_internal_fill_pixdata(device, QP_MAX(sizex, sizey), hue, sat, val); + +    if (!qp_comms_start(device)) { +        qp_dprintf("qp_ellipse: fail (could not start comms)\n"); +        return false; +    } + +    bool ret = true; +    for (int16_t delta = (2 * bb) + (aa * (1 - (2 * sizey))); bb * dx <= aa * dy; dx++) { +        if (!qp_ellipse_helper_impl(device, x, y, dx, dy, filled)) { +            ret = false; +            break; +        } +        if (delta >= 0) { +            delta += fa * (1 - dy); +            dy--; +        } +        delta += bb * (4 * dx + 6); +    } + +    dx = sizex; +    dy = 0; + +    for (int16_t delta = (2 * aa) + (bb * (1 - (2 * sizex))); aa * dy <= bb * dx; dy++) { +        if (!qp_ellipse_helper_impl(device, x, y, dx, dy, filled)) { +            ret = false; +            break; +        } +        if (delta >= 0) { +            delta += fb * (1 - dx); +            dx--; +        } +        delta += aa * (4 * dy + 6); +    } + +    qp_dprintf("qp_ellipse: %s\n", ret ? "ok" : "fail"); +    qp_comms_stop(device); +    return ret; +} diff --git a/quantum/painter/qp_draw_image.c b/quantum/painter/qp_draw_image.c new file mode 100644 index 0000000000..5822758dce --- /dev/null +++ b/quantum/painter/qp_draw_image.c @@ -0,0 +1,382 @@ +// Copyright 2021 Nick Brassel (@tzarc) +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "qp_internal.h" +#include "qp_draw.h" +#include "qp_comms.h" +#include "qgf.h" +#include "deferred_exec.h" + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// QGF image handles + +typedef struct qgf_image_handle_t { +    painter_image_desc_t base; +    bool                 validate_ok; +    union { +        qp_stream_t        stream; +        qp_memory_stream_t mem_stream; +#ifdef QP_STREAM_HAS_FILE_IO +        qp_file_stream_t file_stream; +#endif // QP_STREAM_HAS_FILE_IO +    }; +} qgf_image_handle_t; + +static qgf_image_handle_t image_descriptors[QUANTUM_PAINTER_NUM_IMAGES] = {0}; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Quantum Painter External API: qp_load_image_mem + +painter_image_handle_t qp_load_image_mem(const void *buffer) { +    qp_dprintf("qp_load_image_mem: entry\n"); +    qgf_image_handle_t *image = NULL; + +    // Find a free slot +    for (int i = 0; i < QUANTUM_PAINTER_NUM_IMAGES; ++i) { +        if (!image_descriptors[i].validate_ok) { +            image = &image_descriptors[i]; +            break; +        } +    } + +    // Drop out if not found +    if (!image) { +        qp_dprintf("qp_load_image_mem: fail (no free slot)\n"); +        return NULL; +    } + +    // Assume we can read the graphics descriptor +    image->mem_stream = qp_make_memory_stream((void *)buffer, sizeof(qgf_graphics_descriptor_v1_t)); + +    // Update the length of the stream to match, and rewind to the start +    image->mem_stream.length   = qgf_get_total_size(&image->stream); +    image->mem_stream.position = 0; + +    // Now that we know the length, validate the input data +    if (!qgf_validate_stream(&image->stream)) { +        qp_dprintf("qp_load_image_mem: fail (failed validation)\n"); +        return NULL; +    } + +    // Fill out the QP image descriptor +    qgf_read_graphics_descriptor(&image->stream, &image->base.width, &image->base.height, &image->base.frame_count, NULL); + +    // Validation success, we can return the handle +    image->validate_ok = true; +    qp_dprintf("qp_load_image_mem: ok\n"); +    return (painter_image_handle_t)image; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Quantum Painter External API: qp_close_image + +bool qp_close_image(painter_image_handle_t image) { +    qgf_image_handle_t *qgf_image = (qgf_image_handle_t *)image; +    if (!qgf_image->validate_ok) { +        qp_dprintf("qp_close_image: fail (invalid image)\n"); +        return false; +    } + +    // Free up this image for use elsewhere. +    qgf_image->validate_ok = false; +    return true; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Quantum Painter External API: qp_drawimage + +bool qp_drawimage(painter_device_t device, uint16_t x, uint16_t y, painter_image_handle_t image) { +    return qp_drawimage_recolor(device, x, y, image, 0, 0, 255, 0, 0, 0); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Quantum Painter External API: qp_drawimage_recolor + +typedef struct qgf_frame_info_t { +    painter_compression_t compression_scheme; +    uint8_t               bpp; +    bool                  has_palette; +    bool                  is_delta; +    uint16_t              left; +    uint16_t              top; +    uint16_t              right; +    uint16_t              bottom; +    uint16_t              delay; +} qgf_frame_info_t; + +static bool qp_drawimage_prepare_frame_for_stream_read(painter_device_t device, qgf_image_handle_t *qgf_image, uint16_t frame_number, qp_pixel_t fg_hsv888, qp_pixel_t bg_hsv888, qgf_frame_info_t *info) { +    struct painter_driver_t *driver = (struct painter_driver_t *)device; + +    // Drop out if we can't actually place the data we read out anywhere +    if (!info) { +        qp_dprintf("Failed to prepare stream for read, output info buffer unavailable\n"); +        return false; +    } + +    // Seek to the frame +    qgf_seek_to_frame_descriptor(&qgf_image->stream, frame_number); + +    // Read the frame descriptor +    qgf_frame_v1_t frame_descriptor; +    if (qp_stream_read(&frame_descriptor, sizeof(qgf_frame_v1_t), 1, &qgf_image->stream) != 1) { +        qp_dprintf("Failed to read frame_descriptor, expected length was not %d\n", (int)sizeof(qgf_frame_v1_t)); +        return false; +    } + +    // Parse out the frame info +    if (!qgf_parse_frame_descriptor(&frame_descriptor, &info->bpp, &info->has_palette, &info->is_delta, &info->compression_scheme, &info->delay)) { +        return false; +    } + +    // Ensure we aren't reusing any palette +    qp_internal_invalidate_palette(); + +    if (!qp_internal_bpp_capable(info->bpp)) { +        qp_dprintf("qp_drawimage_recolor: fail (image bpp too high (%d), check QUANTUM_PAINTER_SUPPORTS_256_PALETTE)\n", (int)info->bpp); +        qp_comms_stop(device); +        return false; +    } + +    // Handle palette if needed +    const uint16_t palette_entries  = 1u << info->bpp; +    bool           needs_pixconvert = false; +    if (info->has_palette) { +        // Load the palette from the stream +        if (!qp_internal_load_qgf_palette((qp_stream_t *)&qgf_image->stream, info->bpp)) { +            return false; +        } + +        needs_pixconvert = true; +    } else { +        // Interpolate from fg/bg +        needs_pixconvert = qp_internal_interpolate_palette(fg_hsv888, bg_hsv888, palette_entries); +    } + +    if (needs_pixconvert) { +        // Convert the palette to native format +        if (!driver->driver_vtable->palette_convert(device, palette_entries, qp_internal_global_pixel_lookup_table)) { +            qp_dprintf("qp_drawimage_recolor: fail (could not convert pixels to native)\n"); +            qp_comms_stop(device); +            return false; +        } +    } + +    // Handle delta if needed +    if (info->is_delta) { +        qgf_delta_v1_t delta_descriptor; +        if (qp_stream_read(&delta_descriptor, sizeof(qgf_delta_v1_t), 1, &qgf_image->stream) != 1) { +            qp_dprintf("Failed to read delta_descriptor, expected length was not %d\n", (int)sizeof(qgf_delta_v1_t)); +            return false; +        } + +        info->left   = delta_descriptor.left; +        info->top    = delta_descriptor.top; +        info->right  = delta_descriptor.right; +        info->bottom = delta_descriptor.bottom; +    } + +    // Read the data block +    qgf_data_v1_t data_descriptor; +    if (qp_stream_read(&data_descriptor, sizeof(qgf_data_v1_t), 1, &qgf_image->stream) != 1) { +        qp_dprintf("Failed to read data_descriptor, expected length was not %d\n", (int)sizeof(qgf_data_v1_t)); +        return false; +    } + +    // Stream is now at the point of being able to read pixdata +    return true; +} + +static bool qp_drawimage_recolor_impl(painter_device_t device, uint16_t x, uint16_t y, painter_image_handle_t image, int frame_number, qgf_frame_info_t *frame_info, qp_pixel_t fg_hsv888, qp_pixel_t bg_hsv888) { +    qp_dprintf("qp_drawimage_recolor: entry\n"); +    struct painter_driver_t *driver = (struct painter_driver_t *)device; +    if (!driver->validate_ok) { +        qp_dprintf("qp_drawimage_recolor: fail (validation_ok == false)\n"); +        return false; +    } + +    qgf_image_handle_t *qgf_image = (qgf_image_handle_t *)image; +    if (!qgf_image->validate_ok) { +        qp_dprintf("qp_drawimage_recolor: fail (invalid image)\n"); +        return false; +    } + +    // Read the frame info +    if (!qp_drawimage_prepare_frame_for_stream_read(device, qgf_image, frame_number, fg_hsv888, bg_hsv888, frame_info)) { +        qp_dprintf("qp_drawimage_recolor: fail (could not read frame %d)\n", frame_number); +        return false; +    } + +    if (!qp_comms_start(device)) { +        qp_dprintf("qp_drawimage_recolor: fail (could not start comms)\n"); +        return false; +    } + +    uint16_t l, t, r, b; +    if (frame_info->is_delta) { +        l = x + frame_info->left; +        t = y + frame_info->top; +        r = x + frame_info->right - 1; +        b = y + frame_info->bottom - 1; +    } else { +        l = x; +        t = y; +        r = x + image->width - 1; +        b = y + image->height - 1; +    } +    uint32_t pixel_count = ((uint32_t)(r - l + 1)) * (b - t + 1); + +    // Configure where we're going to be rendering to +    if (!driver->driver_vtable->viewport(device, l, t, r, b)) { +        qp_dprintf("qp_drawimage_recolor: fail (could not set viewport)\n"); +        qp_comms_stop(device); +        return false; +    } + +    // Set up the input state +    struct qp_internal_byte_input_state input_state    = {.device = device, .src_stream = &qgf_image->stream}; +    qp_internal_byte_input_callback     input_callback = qp_internal_prepare_input_state(&input_state, frame_info->compression_scheme); +    if (input_callback == NULL) { +        qp_dprintf("qp_drawimage_recolor: fail (invalid image compression scheme)\n"); +        qp_comms_stop(device); +        return false; +    } + +    // Set up the output state +    struct qp_internal_pixel_output_state output_state = {.device = device, .pixel_write_pos = 0, .max_pixels = qp_internal_num_pixels_in_buffer(device)}; + +    // Decode the pixel data and stream to the display +    bool ret = qp_internal_decode_palette(device, pixel_count, frame_info->bpp, input_callback, &input_state, qp_internal_global_pixel_lookup_table, qp_internal_pixel_appender, &output_state); + +    // Any leftovers need transmission as well. +    if (ret && output_state.pixel_write_pos > 0) { +        ret &= driver->driver_vtable->pixdata(device, qp_internal_global_pixdata_buffer, output_state.pixel_write_pos); +    } + +    qp_dprintf("qp_drawimage_recolor: %s\n", ret ? "ok" : "fail"); +    qp_comms_stop(device); +    return ret; +} + +bool qp_drawimage_recolor(painter_device_t device, uint16_t x, uint16_t y, painter_image_handle_t image, uint8_t hue_fg, uint8_t sat_fg, uint8_t val_fg, uint8_t hue_bg, uint8_t sat_bg, uint8_t val_bg) { +    qgf_frame_info_t frame_info = {0}; +    qp_pixel_t       fg_hsv888  = {.hsv888 = {.h = hue_fg, .s = sat_fg, .v = val_fg}}; +    qp_pixel_t       bg_hsv888  = {.hsv888 = {.h = hue_bg, .s = sat_bg, .v = val_bg}}; +    return qp_drawimage_recolor_impl(device, x, y, image, 0, &frame_info, fg_hsv888, bg_hsv888); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Quantum Painter External API: qp_animate + +deferred_token qp_animate(painter_device_t device, uint16_t x, uint16_t y, painter_image_handle_t image) { +    return qp_animate_recolor(device, x, y, image, 0, 0, 255, 0, 0, 0); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Quantum Painter External API: qp_animate_recolor + +typedef struct animation_state_t { +    painter_device_t       device; +    uint16_t               x; +    uint16_t               y; +    painter_image_handle_t image; +    qp_pixel_t             fg_hsv888; +    qp_pixel_t             bg_hsv888; +    uint16_t               frame_number; +    deferred_token         defer_token; +} animation_state_t; + +static deferred_executor_t animation_executors[QUANTUM_PAINTER_CONCURRENT_ANIMATIONS] = {0}; +static animation_state_t   animation_states[QUANTUM_PAINTER_CONCURRENT_ANIMATIONS]    = {0}; + +static deferred_token qp_render_animation_state(animation_state_t *state, uint16_t *delay_ms) { +    qgf_frame_info_t frame_info = {0}; +    qp_dprintf("qp_render_animation_state: entry (frame #%d)\n", (int)state->frame_number); +    bool ret = qp_drawimage_recolor_impl(state->device, state->x, state->y, state->image, state->frame_number, &frame_info, state->fg_hsv888, state->bg_hsv888); +    if (ret) { +        ++state->frame_number; +        if (state->frame_number >= state->image->frame_count) { +            state->frame_number = 0; +        } +        *delay_ms = frame_info.delay; +    } +    qp_dprintf("qp_render_animation_state: %s (delay %dms)\n", ret ? "ok" : "fail", (int)(*delay_ms)); +    return ret; +} + +static uint32_t animation_callback(uint32_t trigger_time, void *cb_arg) { +    animation_state_t *state = (animation_state_t *)cb_arg; +    uint16_t           delay_ms; +    bool               ret = qp_render_animation_state(state, &delay_ms); +    if (!ret) { +        // Setting the device to NULL clears the animation slot +        state->device = NULL; +    } +    // If we're successful, keep animating -- returning 0 cancels the deferred execution +    return ret ? delay_ms : 0; +} + +deferred_token qp_animate_recolor(painter_device_t device, uint16_t x, uint16_t y, painter_image_handle_t image, uint8_t hue_fg, uint8_t sat_fg, uint8_t val_fg, uint8_t hue_bg, uint8_t sat_bg, uint8_t val_bg) { +    qp_dprintf("qp_animate_recolor: entry\n"); + +    animation_state_t *anim_state = NULL; +    for (int i = 0; i < QUANTUM_PAINTER_CONCURRENT_ANIMATIONS; ++i) { +        if (animation_states[i].device == NULL) { +            anim_state = &animation_states[i]; +            break; +        } +    } + +    if (!anim_state) { +        qp_dprintf("qp_animate_recolor: fail (could not find free animation slot)\n"); +        return INVALID_DEFERRED_TOKEN; +    } + +    // Prepare the animation state +    anim_state->device       = device; +    anim_state->x            = x; +    anim_state->y            = y; +    anim_state->image        = image; +    anim_state->fg_hsv888    = (qp_pixel_t){.hsv888 = {.h = hue_fg, .s = sat_fg, .v = val_fg}}; +    anim_state->bg_hsv888    = (qp_pixel_t){.hsv888 = {.h = hue_bg, .s = sat_bg, .v = val_bg}}; +    anim_state->frame_number = 0; + +    // Draw the first frame +    uint16_t delay_ms; +    if (!qp_render_animation_state(anim_state, &delay_ms)) { +        anim_state->device = NULL; // disregard the allocated animation slot +        qp_dprintf("qp_animate_recolor: fail (could not render first frame)\n"); +        return INVALID_DEFERRED_TOKEN; +    } + +    // Set up the timer +    anim_state->defer_token = defer_exec_advanced(animation_executors, QUANTUM_PAINTER_CONCURRENT_ANIMATIONS, delay_ms, animation_callback, anim_state); +    if (anim_state->defer_token == INVALID_DEFERRED_TOKEN) { +        anim_state->device = NULL; // disregard the allocated animation slot +        qp_dprintf("qp_animate_recolor: fail (could not set up animation executor)\n"); +        return INVALID_DEFERRED_TOKEN; +    } + +    qp_dprintf("qp_animate_recolor: ok (deferred token = %d)\n", (int)anim_state->defer_token); +    return anim_state->defer_token; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Quantum Painter External API: qp_stop_animation + +void qp_stop_animation(deferred_token anim_token) { +    for (int i = 0; i < QUANTUM_PAINTER_CONCURRENT_ANIMATIONS; ++i) { +        if (animation_states[i].defer_token == anim_token) { +            cancel_deferred_exec_advanced(animation_executors, QUANTUM_PAINTER_CONCURRENT_ANIMATIONS, anim_token); +            animation_states[i].device = NULL; +            return; +        } +    } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Quantum Painter Core API: qp_internal_animation_tick + +void qp_internal_animation_tick(void) { +    static uint32_t last_anim_exec = 0; +    deferred_exec_advanced_task(animation_executors, QUANTUM_PAINTER_CONCURRENT_ANIMATIONS, &last_anim_exec); +} diff --git a/quantum/painter/qp_draw_text.c b/quantum/painter/qp_draw_text.c new file mode 100644 index 0000000000..f99e082cad --- /dev/null +++ b/quantum/painter/qp_draw_text.c @@ -0,0 +1,444 @@ +// Copyright 2021 Nick Brassel (@tzarc) +// SPDX-License-Identifier: GPL-2.0-or-later + +#include <quantum.h> +#include <utf8.h> + +#include "qp_internal.h" +#include "qp_draw.h" +#include "qp_comms.h" +#include "qff.h" + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// QFF font handles + +typedef struct qff_font_handle_t { +    painter_font_desc_t   base; +    bool                  validate_ok; +    bool                  has_ascii_table; +    uint16_t              num_unicode_glyphs; +    uint8_t               bpp; +    bool                  has_palette; +    painter_compression_t compression_scheme; +    union { +        qp_stream_t        stream; +        qp_memory_stream_t mem_stream; +#ifdef QP_STREAM_HAS_FILE_IO +        qp_file_stream_t file_stream; +#endif // QP_STREAM_HAS_FILE_IO +    }; +#if QUANTUM_PAINTER_LOAD_FONTS_TO_RAM +    bool  owns_buffer; +    void *buffer; +#endif // QUANTUM_PAINTER_LOAD_FONTS_TO_RAM +} qff_font_handle_t; + +static qff_font_handle_t font_descriptors[QUANTUM_PAINTER_NUM_FONTS] = {0}; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Quantum Painter External API: qp_load_font_mem + +painter_font_handle_t qp_load_font_mem(const void *buffer) { +    qp_dprintf("qp_load_font_mem: entry\n"); +    qff_font_handle_t *font = NULL; + +    // Find a free slot +    for (int i = 0; i < QUANTUM_PAINTER_NUM_FONTS; ++i) { +        if (!font_descriptors[i].validate_ok) { +            font = &font_descriptors[i]; +            break; +        } +    } + +    // Drop out if not found +    if (!font) { +        qp_dprintf("qp_load_font_mem: fail (no free slot)\n"); +        return NULL; +    } + +    // Assume we can read the graphics descriptor +    font->mem_stream = qp_make_memory_stream((void *)buffer, sizeof(qff_font_descriptor_v1_t)); + +    // Update the length of the stream to match, and rewind to the start +    font->mem_stream.length   = qff_get_total_size(&font->stream); +    font->mem_stream.position = 0; + +    // Now that we know the length, validate the input data +    if (!qff_validate_stream(&font->stream)) { +        qp_dprintf("qp_load_font_mem: fail (failed validation)\n"); +        return NULL; +    } + +#if QUANTUM_PAINTER_LOAD_FONTS_TO_RAM +    // Clear out any existing data +    font->owns_buffer = false; +    font->buffer      = NULL; + +    void *ram_buffer = malloc(font->mem_stream.length); +    if (ram_buffer == NULL) { +        qp_dprintf("qp_load_font_mem: could not allocate enough RAM for font, falling back to original\n"); +    } else { +        do { +            // Copy the data into RAM +            if (qp_stream_read(ram_buffer, 1, font->mem_stream.length, &font->mem_stream) != font->mem_stream.length) { +                qp_dprintf("qp_load_font_mem: could not copy from flash to RAM, falling back to original\n"); +                break; +            } + +            // Create the new stream with the new buffer +            font->buffer      = ram_buffer; +            font->owns_buffer = true; +            font->mem_stream  = qp_make_memory_stream(font->buffer, font->mem_stream.length); +        } while (0); +    } + +    // Free the buffer if we were unable to recreate the RAM copy. +    if (ram_buffer != NULL && !font->owns_buffer) { +        free(ram_buffer); +    } +#endif // QUANTUM_PAINTER_LOAD_FONTS_TO_RAM + +    // Read the info (parsing already successful above, no need to check return value) +    qff_read_font_descriptor(&font->stream, &font->base.line_height, &font->has_ascii_table, &font->num_unicode_glyphs, &font->bpp, &font->has_palette, &font->compression_scheme, NULL); + +    if (!qp_internal_bpp_capable(font->bpp)) { +        qp_dprintf("qp_load_font_mem: fail (image bpp too high (%d), check QUANTUM_PAINTER_SUPPORTS_256_PALETTE)\n", (int)font->bpp); +        qp_close_font((painter_font_handle_t)font); +        return NULL; +    } + +    // Validation success, we can return the handle +    font->validate_ok = true; +    qp_dprintf("qp_load_font_mem: ok\n"); +    return (painter_font_handle_t)font; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Quantum Painter External API: qp_close_font + +bool qp_close_font(painter_font_handle_t font) { +    qff_font_handle_t *qff_font = (qff_font_handle_t *)font; +    if (!qff_font->validate_ok) { +        qp_dprintf("qp_close_font: fail (invalid font)\n"); +        return false; +    } + +#if QUANTUM_PAINTER_LOAD_FONTS_TO_RAM +    // Nuke the buffer, if required +    if (qff_font->owns_buffer) { +        free(qff_font->buffer); +        qff_font->buffer      = NULL; +        qff_font->owns_buffer = false; +    } +#endif // QUANTUM_PAINTER_LOAD_FONTS_TO_RAM + +    // Free up this font for use elsewhere. +    qff_font->validate_ok = false; +    return true; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Helpers + +// Callback to be invoked for each codepoint detected in the UTF8 input string +typedef bool (*code_point_handler)(qff_font_handle_t *qff_font, uint32_t code_point, uint8_t width, uint8_t height, void *cb_arg); + +// Helper that sets up the palette (if required) and returns the offset in the stream that the data starts +static inline bool qp_drawtext_prepare_font_for_render(painter_device_t device, qff_font_handle_t *qff_font, qp_pixel_t fg_hsv888, qp_pixel_t bg_hsv888, uint32_t *data_offset) { +    struct painter_driver_t *driver = (struct painter_driver_t *)device; + +    // Drop out if we can't actually place the data we read out anywhere +    if (!data_offset) { +        qp_dprintf("Failed to prepare stream for read, output info buffer unavailable\n"); +        return false; +    } + +    // Work out where we're reading from +    uint32_t offset = sizeof(qff_font_descriptor_v1_t); +    if (qff_font->has_ascii_table) { +        offset += sizeof(qff_ascii_glyph_table_v1_t); +    } +    if (qff_font->num_unicode_glyphs > 0) { +        offset += sizeof(qff_unicode_glyph_table_v1_t) + (qff_font->num_unicode_glyphs * 6); +    } + +    // Handle palette if needed +    const uint16_t palette_entries  = 1u << qff_font->bpp; +    bool           needs_pixconvert = false; +    if (qff_font->has_palette) { +        // If this font has a palette, we need to read it out and set up the pixel lookup table +        qp_stream_setpos(&qff_font->stream, offset); +        if (!qp_internal_load_qgf_palette(&qff_font->stream, qff_font->bpp)) { +            return false; +        } + +        // Skip this block, as far as offset calculations go +        offset += sizeof(qgf_palette_v1_t) + (palette_entries * 3); +        needs_pixconvert = true; +    } else { +        // Interpolate from fg/bg +        int16_t palette_entries = 1 << qff_font->bpp; +        needs_pixconvert        = qp_internal_interpolate_palette(fg_hsv888, bg_hsv888, palette_entries); +    } + +    if (needs_pixconvert) { +        // Convert the palette to native format +        if (!driver->driver_vtable->palette_convert(device, palette_entries, qp_internal_global_pixel_lookup_table)) { +            qp_dprintf("qp_drawtext_recolor: fail (could not convert pixels to native)\n"); +            qp_comms_stop(device); +            return false; +        } +    } + +    *data_offset = offset; +    return true; +} + +static inline bool qp_drawtext_prepare_glyph_for_render(qff_font_handle_t *qff_font, uint32_t code_point, uint8_t *width) { +    if (code_point >= 0x20 && code_point < 0x7F && qff_font->has_ascii_table) { +        // Do ascii table +        qff_ascii_glyph_v1_t glyph_info; +        uint32_t             glyph_info_offset = sizeof(qff_font_descriptor_v1_t)          // Skip the font descriptor +                                     + sizeof(qgf_block_header_v1_t)                       // Skip the ascii table header +                                     + (code_point - 0x20) * sizeof(qff_ascii_glyph_v1_t); // Jump direct to the data offset based on the glyph index +        if (qp_stream_setpos(&qff_font->stream, glyph_info_offset) < 0) { +            qp_dprintf("Failed to set stream position while reading ascii glyph info\n"); +            return false; +        } + +        if (qp_stream_read(&glyph_info, sizeof(qff_ascii_glyph_v1_t), 1, &qff_font->stream) != 1) { +            qp_dprintf("Failed to read glyph info\n"); +            return false; +        } + +        uint8_t  glyph_width  = (uint8_t)(glyph_info.value & QFF_GLYPH_WIDTH_MASK); +        uint32_t glyph_offset = ((glyph_info.value & QFF_GLYPH_OFFSET_MASK) >> QFF_GLYPH_WIDTH_BITS); +        uint32_t data_offset  = sizeof(qff_font_descriptor_v1_t)                                                                                                                   // Skip the font descriptor +                               + sizeof(qff_ascii_glyph_table_v1_t)                                                                                                                // Skip the ascii table +                               + (qff_font->num_unicode_glyphs > 0 ? (sizeof(qff_unicode_glyph_table_v1_t) + (qff_font->num_unicode_glyphs * sizeof(qff_unicode_glyph_v1_t))) : 0) // Skip the unicode table +                               + (qff_font->has_palette ? (sizeof(qgf_palette_v1_t) + ((1 << qff_font->bpp) * sizeof(qgf_palette_entry_v1_t))) : 0)                                // Skip the palette +                               + sizeof(qgf_block_header_v1_t)                                                                                                                     // Skip the data block header +                               + glyph_offset;                                                                                                                                     // Jump to the specified glyph offset + +        if (qp_stream_setpos(&qff_font->stream, data_offset) < 0) { +            qp_dprintf("Failed to set stream position while preparing ascii glyph data\n"); +            return false; +        } + +        *width = glyph_width; +        return true; +    } else { +        // Do unicode table, which may include singular ascii glyphs if full ascii table isn't specified +        uint32_t glyph_info_offset = sizeof(qff_font_descriptor_v1_t)                                       // Skip the font descriptor +                                     + (qff_font->has_ascii_table ? sizeof(qff_ascii_glyph_table_v1_t) : 0) // Skip the ascii table +                                     + sizeof(qgf_block_header_v1_t);                                       // Skip the unicode block header + +        if (qp_stream_setpos(&qff_font->stream, glyph_info_offset) < 0) { +            qp_dprintf("Failed to set stream position while preparing glyph data\n"); +            return false; +        } + +        qff_unicode_glyph_v1_t glyph_info; +        for (uint16_t i = 0; i < qff_font->num_unicode_glyphs; ++i) { +            if (qp_stream_read(&glyph_info, sizeof(qff_unicode_glyph_v1_t), 1, &qff_font->stream) != 1) { +                qp_dprintf("Failed to set stream position while reading unicode glyph info\n"); +                return false; +            } + +            if (glyph_info.code_point == code_point) { +                uint8_t  glyph_width  = (uint8_t)(glyph_info.value & QFF_GLYPH_WIDTH_MASK); +                uint32_t glyph_offset = ((glyph_info.value & QFF_GLYPH_OFFSET_MASK) >> QFF_GLYPH_WIDTH_BITS); +                uint32_t data_offset  = sizeof(qff_font_descriptor_v1_t)                                                                                                                   // Skip the font descriptor +                                       + sizeof(qff_ascii_glyph_table_v1_t)                                                                                                                // Skip the ascii table +                                       + (qff_font->num_unicode_glyphs > 0 ? (sizeof(qff_unicode_glyph_table_v1_t) + (qff_font->num_unicode_glyphs * sizeof(qff_unicode_glyph_v1_t))) : 0) // Skip the unicode table +                                       + (qff_font->has_palette ? (sizeof(qgf_palette_v1_t) + ((1 << qff_font->bpp) * sizeof(qgf_palette_entry_v1_t))) : 0)                                // Skip the palette +                                       + sizeof(qgf_block_header_v1_t)                                                                                                                     // Skip the data block header +                                       + glyph_offset;                                                                                                                                     // Jump to the specified glyph offset + +                if (qp_stream_setpos(&qff_font->stream, data_offset) < 0) { +                    qp_dprintf("Failed to set stream position while preparing unicode glyph data\n"); +                    return false; +                } + +                *width = glyph_width; +                return true; +            } +        } + +        // Not found +        qp_dprintf("Failed to find unicode glyph info\n"); +        return false; +    } +    return false; +} + +// Function to iterate over each UTF8 codepoint, invoking the callback for each decoded glyph +static inline bool qp_iterate_code_points(qff_font_handle_t *qff_font, const char *str, code_point_handler handler, void *cb_arg) { +    while (*str) { +        int32_t code_point = 0; +        str                = decode_utf8(str, &code_point); +        if (code_point < 0) { +            qp_dprintf("Invalid unicode code point decoded. Cannot render.\n"); +            return false; +        } + +        uint8_t width; +        if (!qp_drawtext_prepare_glyph_for_render(qff_font, code_point, &width)) { +            qp_dprintf("Failed to prepare glyph for rendering.\n"); +            return false; +        } + +        if (!handler(qff_font, code_point, width, qff_font->base.line_height, cb_arg)) { +            qp_dprintf("Failed to execute glyph handler.\n"); +            return false; +        } +    } +    return true; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// String width calculation + +// Callback state +struct code_point_iter_calcwidth_state { +    int16_t width; +}; + +// Codepoint handler callback: width calc +static inline bool qp_font_code_point_handler_calcwidth(qff_font_handle_t *qff_font, uint32_t code_point, uint8_t width, uint8_t height, void *cb_arg) { +    struct code_point_iter_calcwidth_state *state = (struct code_point_iter_calcwidth_state *)cb_arg; + +    // Increment the overall width by this glyph's width +    state->width += width; + +    return true; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// String drawing implementation + +// Callback state +struct code_point_iter_drawglyph_state { +    painter_device_t                       device; +    int16_t                                xpos; +    int16_t                                ypos; +    qp_internal_byte_input_callback        input_callback; +    struct qp_internal_byte_input_state *  input_state; +    struct qp_internal_pixel_output_state *output_state; +}; + +// Codepoint handler callback: drawing +static inline bool qp_font_code_point_handler_drawglyph(qff_font_handle_t *qff_font, uint32_t code_point, uint8_t width, uint8_t height, void *cb_arg) { +    struct code_point_iter_drawglyph_state *state  = (struct code_point_iter_drawglyph_state *)cb_arg; +    struct painter_driver_t *               driver = (struct painter_driver_t *)state->device; + +    // Reset the input state's RLE mode -- the stream should already be correctly positioned by qp_iterate_code_points() +    state->input_state->rle.mode = MARKER_BYTE; // ignored if not using RLE + +    // Reset the output state +    state->output_state->pixel_write_pos = 0; + +    // Configure where we're going to be rendering to +    driver->driver_vtable->viewport(state->device, state->xpos, state->ypos, state->xpos + width - 1, state->ypos + height - 1); + +    // Move the x-position for the next glyph +    state->xpos += width; + +    // Decode the pixel data for the glyph +    uint32_t pixel_count = ((uint32_t)width) * height; +    bool     ret         = qp_internal_decode_palette(state->device, pixel_count, qff_font->bpp, state->input_callback, state->input_state, qp_internal_global_pixel_lookup_table, qp_internal_pixel_appender, state->output_state); + +    // Any leftovers need transmission as well. +    if (ret && state->output_state->pixel_write_pos > 0) { +        ret &= driver->driver_vtable->pixdata(state->device, qp_internal_global_pixdata_buffer, state->output_state->pixel_write_pos); +    } + +    return ret; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Quantum Painter External API: qp_textwidth + +int16_t qp_textwidth(painter_font_handle_t font, const char *str) { +    qff_font_handle_t *qff_font = (qff_font_handle_t *)font; +    if (!qff_font->validate_ok) { +        qp_dprintf("qp_textwidth: fail (invalid font)\n"); +        return false; +    } + +    // Create the codepoint iterator state +    struct code_point_iter_calcwidth_state state = {.width = 0}; +    // Iterate each codepoint, return the calculated width if successful. +    return qp_iterate_code_points(qff_font, str, qp_font_code_point_handler_calcwidth, &state) ? state.width : 0; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Quantum Painter External API: qp_drawtext + +int16_t qp_drawtext(painter_device_t device, uint16_t x, uint16_t y, painter_font_handle_t font, const char *str) { +    // Offload to the recolor variant, substituting fg=white bg=black. +    // Traditional LCDs with those colors will need to manually invoke qp_drawtext_recolor with the colors reversed. +    return qp_drawtext_recolor(device, x, y, font, str, 0, 0, 255, 0, 0, 0); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Quantum Painter External API: qp_drawtext_recolor + +int16_t qp_drawtext_recolor(painter_device_t device, uint16_t x, uint16_t y, painter_font_handle_t font, const char *str, uint8_t hue_fg, uint8_t sat_fg, uint8_t val_fg, uint8_t hue_bg, uint8_t sat_bg, uint8_t val_bg) { +    qp_dprintf("qp_drawtext_recolor: entry\n"); +    struct painter_driver_t *driver = (struct painter_driver_t *)device; +    if (!driver->validate_ok) { +        qp_dprintf("qp_drawtext_recolor: fail (validation_ok == false)\n"); +        return 0; +    } + +    qff_font_handle_t *qff_font = (qff_font_handle_t *)font; +    if (!qff_font->validate_ok) { +        qp_dprintf("qp_drawtext_recolor: fail (invalid font)\n"); +        return false; +    } + +    if (!qp_comms_start(device)) { +        qp_dprintf("qp_drawtext_recolor: fail (could not start comms)\n"); +        return 0; +    } + +    // Set up the byte input state and input callback +    struct qp_internal_byte_input_state input_state    = {.device = device, .src_stream = &qff_font->stream}; +    qp_internal_byte_input_callback     input_callback = qp_internal_prepare_input_state(&input_state, qff_font->compression_scheme); +    if (input_callback == NULL) { +        qp_dprintf("qp_drawtext_recolor: fail (invalid font compression scheme)\n"); +        qp_comms_stop(device); +        return false; +    } + +    // Set up the pixel output state +    struct qp_internal_pixel_output_state output_state = {.device = device, .pixel_write_pos = 0, .max_pixels = qp_internal_num_pixels_in_buffer(device)}; + +    // Set up the codepoint iteration state +    struct code_point_iter_drawglyph_state state = {// Common +                                                    .device = device, +                                                    .xpos   = x, +                                                    .ypos   = y, +                                                    // Input +                                                    .input_callback = input_callback, +                                                    .input_state    = &input_state, +                                                    // Output +                                                    .output_state = &output_state}; + +    qp_pixel_t fg_hsv888 = {.hsv888 = {.h = hue_fg, .s = sat_fg, .v = val_fg}}; +    qp_pixel_t bg_hsv888 = {.hsv888 = {.h = hue_bg, .s = sat_bg, .v = val_bg}}; +    uint32_t   data_offset; +    if (!qp_drawtext_prepare_font_for_render(driver, qff_font, fg_hsv888, bg_hsv888, &data_offset)) { +        qp_dprintf("qp_drawtext_recolor: fail (failed to prepare font for rendering)\n"); +        qp_comms_stop(device); +        return false; +    } + +    // Iterate the codepoints with the drawglyph callback +    bool ret = qp_iterate_code_points(qff_font, str, qp_font_code_point_handler_drawglyph, &state); + +    qp_dprintf("qp_drawtext_recolor: %s\n", ret ? "ok" : "fail"); +    qp_comms_stop(device); +    return ret ? (state.xpos - x) : 0; +} diff --git a/quantum/painter/qp_internal.h b/quantum/painter/qp_internal.h new file mode 100644 index 0000000000..e7a6d113c5 --- /dev/null +++ b/quantum/painter/qp_internal.h @@ -0,0 +1,33 @@ +// Copyright 2021 Nick Brassel (@tzarc) +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "quantum.h" +#include "qp.h" + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Helpers + +// Mark certain types that there should be no padding bytes between members. +#define QP_PACKED __attribute__((packed)) + +// Min/max defines +#define QP_MIN(X, Y) (((X) < (Y)) ? (X) : (Y)) +#define QP_MAX(X, Y) (((X) > (Y)) ? (X) : (Y)) + +#ifdef QUANTUM_PAINTER_DEBUG +#    include <debug.h> +#    include <print.h> +#    define qp_dprintf(...) dprintf(__VA_ARGS__) +#else +#    define qp_dprintf(...) \ +        do {                \ +        } while (0) +#endif + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Specific internal definitions + +#include <qp_internal_formats.h> +#include <qp_internal_driver.h> diff --git a/quantum/painter/qp_internal_driver.h b/quantum/painter/qp_internal_driver.h new file mode 100644 index 0000000000..9e9d6bc848 --- /dev/null +++ b/quantum/painter/qp_internal_driver.h @@ -0,0 +1,82 @@ +// Copyright 2021 Nick Brassel (@tzarc) +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "qp_internal.h" + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Driver callbacks + +typedef bool (*painter_driver_init_func)(painter_device_t device, painter_rotation_t rotation); +typedef bool (*painter_driver_power_func)(painter_device_t device, bool power_on); +typedef bool (*painter_driver_clear_func)(painter_device_t device); +typedef bool (*painter_driver_flush_func)(painter_device_t device); +typedef bool (*painter_driver_viewport_func)(painter_device_t device, uint16_t left, uint16_t top, uint16_t right, uint16_t bottom); +typedef bool (*painter_driver_pixdata_func)(painter_device_t device, const void *pixel_data, uint32_t native_pixel_count); +typedef bool (*painter_driver_convert_palette_func)(painter_device_t device, int16_t palette_size, qp_pixel_t *palette); +typedef bool (*painter_driver_append_pixels)(painter_device_t device, uint8_t *target_buffer, qp_pixel_t *palette, uint32_t pixel_offset, uint32_t pixel_count, uint8_t *palette_indices); + +// Driver vtable definition +struct painter_driver_vtable_t { +    painter_driver_init_func            init; +    painter_driver_power_func           power; +    painter_driver_clear_func           clear; +    painter_driver_flush_func           flush; +    painter_driver_viewport_func        viewport; +    painter_driver_pixdata_func         pixdata; +    painter_driver_convert_palette_func palette_convert; +    painter_driver_append_pixels        append_pixels; +}; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Comms callbacks + +typedef bool (*painter_driver_comms_init_func)(painter_device_t device); +typedef bool (*painter_driver_comms_start_func)(painter_device_t device); +typedef void (*painter_driver_comms_stop_func)(painter_device_t device); +typedef uint32_t (*painter_driver_comms_send_func)(painter_device_t device, const void *data, uint32_t byte_count); + +struct painter_comms_vtable_t { +    painter_driver_comms_init_func  comms_init; +    painter_driver_comms_start_func comms_start; +    painter_driver_comms_stop_func  comms_stop; +    painter_driver_comms_send_func  comms_send; +}; + +typedef void (*painter_driver_comms_send_command_func)(painter_device_t device, uint8_t cmd); +typedef void (*painter_driver_comms_bulk_command_sequence)(painter_device_t device, const uint8_t *sequence, size_t sequence_len); + +struct painter_comms_with_command_vtable_t { +    struct painter_comms_vtable_t              base; // must be first, so this object can be cast from the painter_comms_vtable_t* type +    painter_driver_comms_send_command_func     send_command; +    painter_driver_comms_bulk_command_sequence bulk_command_sequence; +}; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Driver base definition + +struct painter_driver_t { +    const struct painter_driver_vtable_t *driver_vtable; +    const struct painter_comms_vtable_t * comms_vtable; + +    // Flag signifying if validation was successful +    bool validate_ok; + +    // Panel geometry +    uint16_t panel_width; +    uint16_t panel_height; + +    // Target drawing rotation +    painter_rotation_t rotation; + +    // Automated offsets for setting viewport +    uint16_t offset_x; +    uint16_t offset_y; + +    // Number of bits per pixel, used for determining how many pixels can be sent during a transmission of the pixdata buffer +    uint8_t native_bits_per_pixel; + +    // Comms config pointer -- needs to point to an appropriate comms config if the comms driver requires it. +    void *comms_config; +}; diff --git a/quantum/painter/qp_internal_formats.h b/quantum/painter/qp_internal_formats.h new file mode 100644 index 0000000000..a4a86f0345 --- /dev/null +++ b/quantum/painter/qp_internal_formats.h @@ -0,0 +1,49 @@ +// Copyright 2021 Nick Brassel (@tzarc) +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "qp_internal.h" + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Quantum Painter pixel formats + +// Datatype containing a pixel's color. The internal member used is dependent on the external context. +typedef union QP_PACKED qp_pixel_t { +    uint8_t mono; +    uint8_t palette_idx; + +    struct QP_PACKED { +        uint8_t h; +        uint8_t s; +        uint8_t v; +    } hsv888; + +    struct QP_PACKED { +        uint8_t r; +        uint8_t g; +        uint8_t b; +    } rgb888; + +    uint16_t rgb565; + +    uint32_t dummy; +} qp_pixel_t; +_Static_assert(sizeof(qp_pixel_t) == 4, "Invalid size for qp_pixel_t"); + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Quantum Painter image format + +typedef enum qp_image_format_t { +    // Pixel formats available in the QGF frame format +    GRAYSCALE_1BPP = 0x00, +    GRAYSCALE_2BPP = 0x01, +    GRAYSCALE_4BPP = 0x02, +    GRAYSCALE_8BPP = 0x03, +    PALETTE_1BPP   = 0x04, +    PALETTE_2BPP   = 0x05, +    PALETTE_4BPP   = 0x06, +    PALETTE_8BPP   = 0x07, +} qp_image_format_t; + +typedef enum painter_compression_t { IMAGE_UNCOMPRESSED, IMAGE_COMPRESSED_RLE } painter_compression_t; diff --git a/quantum/painter/qp_stream.c b/quantum/painter/qp_stream.c new file mode 100644 index 0000000000..f00ae5ed38 --- /dev/null +++ b/quantum/painter/qp_stream.c @@ -0,0 +1,171 @@ +// Copyright 2021 Nick Brassel (@tzarc) +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "qp_stream.h" + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Stream API + +uint32_t qp_stream_read_impl(void *output_buf, uint32_t member_size, uint32_t num_members, qp_stream_t *stream) { +    uint8_t *output_ptr = (uint8_t *)output_buf; + +    uint32_t i; +    for (i = 0; i < (num_members * member_size); ++i) { +        int16_t c = qp_stream_get(stream); +        if (c < 0) { +            break; +        } + +        output_ptr[i] = (uint8_t)(c & 0xFF); +    } + +    return i / member_size; +} + +uint32_t qp_stream_write_impl(const void *input_buf, uint32_t member_size, uint32_t num_members, qp_stream_t *stream) { +    uint8_t *input_ptr = (uint8_t *)input_buf; + +    uint32_t i; +    for (i = 0; i < (num_members * member_size); ++i) { +        if (!qp_stream_put(stream, input_ptr[i])) { +            break; +        } +    } + +    return i / member_size; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Memory streams + +int16_t mem_get(qp_stream_t *stream) { +    qp_memory_stream_t *s = (qp_memory_stream_t *)stream; +    if (s->position >= s->length) { +        s->is_eof = true; +        return STREAM_EOF; +    } +    return s->buffer[s->position++]; +} + +bool mem_put(qp_stream_t *stream, uint8_t c) { +    qp_memory_stream_t *s = (qp_memory_stream_t *)stream; +    if (s->position >= s->length) { +        s->is_eof = true; +        return false; +    } +    s->buffer[s->position++] = c; +    return true; +} + +int mem_seek(qp_stream_t *stream, int32_t offset, int origin) { +    qp_memory_stream_t *s = (qp_memory_stream_t *)stream; + +    // Handle as per fseek +    int32_t position = s->position; +    switch (origin) { +        case SEEK_SET: +            position = offset; +            break; +        case SEEK_CUR: +            position += offset; +            break; +        case SEEK_END: +            position = s->length + offset; +            break; +        default: +            return -1; +    } + +    // If we're before the start, ignore it. +    if (position < 0) { +        return -1; +    } + +    // If we're at the end it's okay, we only care if we're after the end for failure purposes -- as per lseek() +    if (position > s->length) { +        return -1; +    } + +    // Update the offset +    s->position = position; + +    // Successful invocation of fseek() results in clearing of the EOF flag by default, mirror the same functionality +    s->is_eof = false; + +    return 0; +} + +int32_t mem_tell(qp_stream_t *stream) { +    qp_memory_stream_t *s = (qp_memory_stream_t *)stream; +    return s->position; +} + +bool mem_is_eof(qp_stream_t *stream) { +    qp_memory_stream_t *s = (qp_memory_stream_t *)stream; +    return s->is_eof; +} + +qp_memory_stream_t qp_make_memory_stream(void *buffer, int32_t length) { +    qp_memory_stream_t stream = { +        .base = +            { +                .get    = mem_get, +                .put    = mem_put, +                .seek   = mem_seek, +                .tell   = mem_tell, +                .is_eof = mem_is_eof, +            }, +        .buffer   = (uint8_t *)buffer, +        .length   = length, +        .position = 0, +    }; +    return stream; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// FILE streams + +#ifdef QP_STREAM_HAS_FILE_IO + +int16_t file_get(qp_stream_t *stream) { +    qp_file_stream_t *s = (qp_file_stream_t *)stream; +    int               c = fgetc(s->file); +    if (c < 0 || feof(s->file)) return STREAM_EOF; +    return (uint16_t)c; +} + +bool file_put(qp_stream_t *stream, uint8_t c) { +    qp_file_stream_t *s = (qp_file_stream_t *)stream; +    return fputc(c, s->file) == c; +} + +int file_seek(qp_stream_t *stream, int32_t offset, int origin) { +    qp_file_stream_t *s = (qp_file_stream_t *)stream; +    return fseek(s->file, offset, origin); +} + +int32_t file_tell(qp_stream_t *stream) { +    qp_file_stream_t *s = (qp_file_stream_t *)stream; +    return (int32_t)ftell(s->file); +} + +bool file_is_eof(qp_stream_t *stream) { +    qp_file_stream_t *s = (qp_file_stream_t *)stream; +    return (bool)feof(s->file); +} + +qp_file_stream_t qp_make_file_stream(FILE *f) { +    qp_file_stream_t stream = { +        .base = +            { +                .get    = file_get, +                .put    = file_put, +                .seek   = file_seek, +                .tell   = file_tell, +                .is_eof = file_is_eof, +            }, +        .file = f, +    }; +    return stream; +} +#endif // QP_STREAM_HAS_FILE_IO diff --git a/quantum/painter/qp_stream.h b/quantum/painter/qp_stream.h new file mode 100644 index 0000000000..878b9bf530 --- /dev/null +++ b/quantum/painter/qp_stream.h @@ -0,0 +1,82 @@ +/* Copyright 2021 Nick Brassel (@tzarc) + * + * 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 + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program.  If not, see <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <stdbool.h> +#include <stdint.h> +#include <stdlib.h> +#include <stdio.h> + +#include "qp_internal.h" + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Stream API + +typedef struct qp_stream_t qp_stream_t; + +#define qp_stream_get(stream_ptr) (((qp_stream_t *)(stream_ptr))->get((qp_stream_t *)(stream_ptr))) +#define qp_stream_put(stream_ptr, c) (((qp_stream_t *)(stream_ptr))->put((qp_stream_t *)(stream_ptr), (c))) +#define qp_stream_seek(stream_ptr, offset, origin) (((qp_stream_t *)(stream_ptr))->seek((qp_stream_t *)(stream_ptr), (offset), (origin))) +#define qp_stream_tell(stream_ptr) (((qp_stream_t *)(stream_ptr))->tell((qp_stream_t *)(stream_ptr))) +#define qp_stream_eof(stream_ptr) (((qp_stream_t *)(stream_ptr))->is_eof((qp_stream_t *)(stream_ptr))) +#define qp_stream_setpos(stream_ptr, offset) qp_stream_seek((stream_ptr), (offset), SEEK_SET) +#define qp_stream_getpos(stream_ptr) qp_stream_tell((stream_ptr)) +#define qp_stream_read(output_buf, member_size, num_members, stream_ptr) qp_stream_read_impl((output_buf), (member_size), (num_members), (qp_stream_t *)(stream_ptr)) +#define qp_stream_write(input_buf, member_size, num_members, stream_ptr) qp_stream_write_impl((input_buf), (member_size), (num_members), (qp_stream_t *)(stream_ptr)) + +uint32_t qp_stream_read_impl(void *output_buf, uint32_t member_size, uint32_t num_members, qp_stream_t *stream); +uint32_t qp_stream_write_impl(const void *input_buf, uint32_t member_size, uint32_t num_members, qp_stream_t *stream); + +#define STREAM_EOF ((int16_t)(-1)) + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Stream definition + +struct qp_stream_t { +    int16_t (*get)(qp_stream_t *stream); +    bool (*put)(qp_stream_t *stream, uint8_t c); +    int (*seek)(qp_stream_t *stream, int32_t offset, int origin); +    int32_t (*tell)(qp_stream_t *stream); +    bool (*is_eof)(qp_stream_t *stream); +}; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Memory streams + +typedef struct qp_memory_stream_t { +    qp_stream_t base; +    uint8_t *   buffer; +    int32_t     length; +    int32_t     position; +    bool        is_eof; +} qp_memory_stream_t; + +qp_memory_stream_t qp_make_memory_stream(void *buffer, int32_t length); + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// FILE streams + +#ifdef QP_STREAM_HAS_FILE_IO + +typedef struct qp_file_stream_t { +    qp_stream_t base; +    FILE *      file; +} qp_file_stream_t; + +qp_file_stream_t qo_make_file_stream(FILE *f); + +#endif // QP_STREAM_HAS_FILE_IO diff --git a/quantum/painter/rules.mk b/quantum/painter/rules.mk new file mode 100644 index 0000000000..9115d3d406 --- /dev/null +++ b/quantum/painter/rules.mk @@ -0,0 +1,116 @@ +# Quantum Painter Configurables +QUANTUM_PAINTER_DRIVERS ?= +QUANTUM_PAINTER_ANIMATIONS_ENABLE ?= yes + +# The list of permissible drivers that can be listed in QUANTUM_PAINTER_DRIVERS +VALID_QUANTUM_PAINTER_DRIVERS := ili9163_spi ili9341_spi st7789_spi gc9a01_spi ssd1351_spi + +#------------------------------------------------------------------------------- + +OPT_DEFS += -DQUANTUM_PAINTER_ENABLE +COMMON_VPATH += $(QUANTUM_DIR)/painter +SRC += \ +    $(QUANTUM_DIR)/utf8.c \ +    $(QUANTUM_DIR)/color.c \ +    $(QUANTUM_DIR)/painter/qp.c \ +    $(QUANTUM_DIR)/painter/qp_stream.c \ +    $(QUANTUM_DIR)/painter/qgf.c \ +    $(QUANTUM_DIR)/painter/qff.c \ +    $(QUANTUM_DIR)/painter/qp_draw_core.c \ +    $(QUANTUM_DIR)/painter/qp_draw_codec.c \ +    $(QUANTUM_DIR)/painter/qp_draw_circle.c \ +    $(QUANTUM_DIR)/painter/qp_draw_ellipse.c \ +    $(QUANTUM_DIR)/painter/qp_draw_image.c \ +    $(QUANTUM_DIR)/painter/qp_draw_text.c + +# Check if people want animations... enable the defered exec if so. +ifeq ($(strip $(QUANTUM_PAINTER_ANIMATIONS_ENABLE)), yes) +    DEFERRED_EXEC_ENABLE := yes +    OPT_DEFS += -DQUANTUM_PAINTER_ANIMATIONS_ENABLE +endif + +# Comms flags +QUANTUM_PAINTER_NEEDS_COMMS_SPI ?= no + +# Handler for each driver +define handle_quantum_painter_driver +    CURRENT_PAINTER_DRIVER := $1 + +    ifeq ($$(filter $$(strip $$(CURRENT_PAINTER_DRIVER)),$$(VALID_QUANTUM_PAINTER_DRIVERS)),) +        $$(error "$$(CURRENT_PAINTER_DRIVER)" is not a valid Quantum Painter driver) + +    else ifeq ($$(strip $$(CURRENT_PAINTER_DRIVER)),ili9163_spi) +        QUANTUM_PAINTER_NEEDS_COMMS_SPI := yes +        QUANTUM_PAINTER_NEEDS_COMMS_SPI_DC_RESET := yes +        OPT_DEFS += -DQUANTUM_PAINTER_ILI9163_ENABLE -DQUANTUM_PAINTER_ILI9163_SPI_ENABLE +        COMMON_VPATH += \ +            $(DRIVER_PATH)/painter/tft_panel \ +            $(DRIVER_PATH)/painter/ili9xxx +        SRC += \ +            $(DRIVER_PATH)/painter/tft_panel/qp_tft_panel.c \ +            $(DRIVER_PATH)/painter/ili9xxx/qp_ili9163.c \ + +    else ifeq ($$(strip $$(CURRENT_PAINTER_DRIVER)),ili9341_spi) +        QUANTUM_PAINTER_NEEDS_COMMS_SPI := yes +        QUANTUM_PAINTER_NEEDS_COMMS_SPI_DC_RESET := yes +        OPT_DEFS += -DQUANTUM_PAINTER_ILI9341_ENABLE -DQUANTUM_PAINTER_ILI9341_SPI_ENABLE +        COMMON_VPATH += \ +            $(DRIVER_PATH)/painter/tft_panel \ +            $(DRIVER_PATH)/painter/ili9xxx +        SRC += \ +            $(DRIVER_PATH)/painter/tft_panel/qp_tft_panel.c \ +            $(DRIVER_PATH)/painter/ili9xxx/qp_ili9341.c \ + +    else ifeq ($$(strip $$(CURRENT_PAINTER_DRIVER)),st7789_spi) +        QUANTUM_PAINTER_NEEDS_COMMS_SPI := yes +        QUANTUM_PAINTER_NEEDS_COMMS_SPI_DC_RESET := yes +        OPT_DEFS += -DQUANTUM_PAINTER_ST7789_ENABLE -DQUANTUM_PAINTER_ST7789_SPI_ENABLE +        COMMON_VPATH += \ +            $(DRIVER_PATH)/painter/tft_panel \ +            $(DRIVER_PATH)/painter/st77xx +        SRC += \ +            $(DRIVER_PATH)/painter/tft_panel/qp_tft_panel.c \ +            $(DRIVER_PATH)/painter/st77xx/qp_st7789.c + +    else ifeq ($$(strip $$(CURRENT_PAINTER_DRIVER)),gc9a01_spi) +        QUANTUM_PAINTER_NEEDS_COMMS_SPI := yes +        QUANTUM_PAINTER_NEEDS_COMMS_SPI_DC_RESET := yes +        OPT_DEFS += -DQUANTUM_PAINTER_GC9A01_ENABLE -DQUANTUM_PAINTER_GC9A01_SPI_ENABLE +        COMMON_VPATH += \ +            $(DRIVER_PATH)/painter/tft_panel \ +            $(DRIVER_PATH)/painter/gc9a01 +        SRC += \ +            $(DRIVER_PATH)/painter/tft_panel/qp_tft_panel.c \ +            $(DRIVER_PATH)/painter/gc9a01/qp_gc9a01.c + +    else ifeq ($$(strip $$(CURRENT_PAINTER_DRIVER)),ssd1351_spi) +        QUANTUM_PAINTER_NEEDS_COMMS_SPI := yes +        QUANTUM_PAINTER_NEEDS_COMMS_SPI_DC_RESET := yes +        OPT_DEFS += -DQUANTUM_PAINTER_SSD1351_ENABLE -DQUANTUM_PAINTER_SSD1351_SPI_ENABLE +        COMMON_VPATH += \ +            $(DRIVER_PATH)/painter/tft_panel \ +            $(DRIVER_PATH)/painter/ssd1351 +        SRC += \ +            $(DRIVER_PATH)/painter/tft_panel/qp_tft_panel.c \ +            $(DRIVER_PATH)/painter/ssd1351/qp_ssd1351.c + +    endif +endef + +# Iterate through the listed drivers for the build, including what's necessary +$(foreach qp_driver,$(QUANTUM_PAINTER_DRIVERS),$(eval $(call handle_quantum_painter_driver,$(qp_driver)))) + +# If SPI comms is needed, set up the required files +ifeq ($(strip $(QUANTUM_PAINTER_NEEDS_COMMS_SPI)), yes) +    OPT_DEFS += -DQUANTUM_PAINTER_SPI_ENABLE +    QUANTUM_LIB_SRC += spi_master.c +    VPATH += $(DRIVER_PATH)/painter/comms +    SRC += \ +        $(QUANTUM_DIR)/painter/qp_comms.c \ +        $(DRIVER_PATH)/painter/comms/qp_comms_spi.c + +    ifeq ($(strip $(QUANTUM_PAINTER_NEEDS_COMMS_SPI_DC_RESET)), yes) +        OPT_DEFS += -DQUANTUM_PAINTER_SPI_DC_RESET_ENABLE +    endif +endif + diff --git a/quantum/pointing_device.c b/quantum/pointing_device.c index 47a0af45d2..a160647890 100644 --- a/quantum/pointing_device.c +++ b/quantum/pointing_device.c @@ -71,17 +71,6 @@ static report_mouse_t local_mouse_report = {};  extern const pointing_device_driver_t pointing_device_driver;  /** - * @brief Compares 2 mouse reports for difference and returns result - * - * @param[in] new_report report_mouse_t - * @param[in] old_report report_mouse_t - * @return bool result - */ -__attribute__((weak)) bool has_mouse_report_changed(report_mouse_t new_report, report_mouse_t old_report) { -    return memcmp(&new_report, &old_report, sizeof(new_report)); -} - -/**   * @brief Keyboard level code pointing device initialisation   *   */ @@ -165,7 +154,7 @@ __attribute__((weak)) void pointing_device_send(void) {      static report_mouse_t old_report = {};      // If you need to do other things, like debugging, this is the place to do it. -    if (has_mouse_report_changed(local_mouse_report, old_report)) { +    if (has_mouse_report_changed(&local_mouse_report, &old_report)) {          host_mouse_send(&local_mouse_report);      }      // send it and 0 it out except for buttons, so those stay until they are explicity over-ridden using update_pointing_device diff --git a/quantum/pointing_device.h b/quantum/pointing_device.h index a6bdbf120c..5c0eaeaf34 100644 --- a/quantum/pointing_device.h +++ b/quantum/pointing_device.h @@ -80,7 +80,6 @@ void           pointing_device_task(void);  void           pointing_device_send(void);  report_mouse_t pointing_device_get_report(void);  void           pointing_device_set_report(report_mouse_t mouse_report); -bool           has_mouse_report_changed(report_mouse_t new_report, report_mouse_t old_report);  uint16_t       pointing_device_get_cpi(void);  void           pointing_device_set_cpi(uint16_t cpi); diff --git a/quantum/pointing_device_drivers.c b/quantum/pointing_device_drivers.c index b8ef6e67e5..11cbf6594e 100644 --- a/quantum/pointing_device_drivers.c +++ b/quantum/pointing_device_drivers.c @@ -98,17 +98,9 @@ const pointing_device_driver_t pointing_device_driver = {  // clang-format on  #elif defined(POINTING_DEVICE_DRIVER_cirque_pinnacle_i2c) || defined(POINTING_DEVICE_DRIVER_cirque_pinnacle_spi)  #    ifndef CIRQUE_PINNACLE_TAPPING_TERM -#        ifdef TAPPING_TERM_PER_KEY -#            include "action.h" -#            include "action_tapping.h" -#            define CIRQUE_PINNACLE_TAPPING_TERM get_tapping_term(KC_BTN1, &(keyrecord_t){}) -#        else -#            ifdef TAPPING_TERM -#                define CIRQUE_PINNACLE_TAPPING_TERM TAPPING_TERM -#            else -#                define CIRQUE_PINNACLE_TAPPING_TERM 200 -#            endif -#        endif +#        include "action.h" +#        include "action_tapping.h" +#        define CIRQUE_PINNACLE_TAPPING_TERM GET_TAPPING_TERM(KC_BTN1, &(keyrecord_t){})  #    endif  #    ifndef CIRQUE_PINNACLE_TOUCH_DEBOUNCE  #        define CIRQUE_PINNACLE_TOUCH_DEBOUNCE (CIRQUE_PINNACLE_TAPPING_TERM * 8) diff --git a/quantum/process_keycode/process_auto_shift.c b/quantum/process_keycode/process_auto_shift.c index 2150edd7b2..e6a7c01f2a 100644 --- a/quantum/process_keycode/process_auto_shift.c +++ b/quantum/process_keycode/process_auto_shift.c @@ -182,12 +182,7 @@ static bool autoshift_press(uint16_t keycode, uint16_t now, keyrecord_t *record)  #            endif          ) &&  #        endif -        TIMER_DIFF_16(now, autoshift_time) < -#        ifdef TAPPING_TERM_PER_KEY -        get_tapping_term(autoshift_lastkey, record) -#        else -        TAPPING_TERM -#        endif +        TIMER_DIFF_16(now, autoshift_time) < GET_TAPPING_TERM(autoshift_lastkey, record)      ) {          // clang-format on          // Allow a tap-then-hold for keyrepeat. diff --git a/quantum/process_keycode/process_combo.c b/quantum/process_keycode/process_combo.c index efaf8fe0e9..d5a649adb3 100644 --- a/quantum/process_keycode/process_combo.c +++ b/quantum/process_keycode/process_combo.c @@ -88,8 +88,6 @@ static queued_combo_t combo_buffer[COMBO_BUFFER_LENGTH];  #define INCREMENT_MOD(i) i = (i + 1) % COMBO_BUFFER_LENGTH -#define COMBO_KEY_POS ((keypos_t){.col = 254, .row = 254}) -  #ifndef EXTRA_SHORT_COMBOS  /* flags are their own elements in combo_t struct. */  #    define COMBO_ACTIVE(combo) (combo->active) @@ -140,12 +138,7 @@ static queued_combo_t combo_buffer[COMBO_BUFFER_LENGTH];  static inline void release_combo(uint16_t combo_index, combo_t *combo) {      if (combo->keycode) {          keyrecord_t record = { -            .event = -                { -                    .key     = COMBO_KEY_POS, -                    .time    = timer_read() | 1, -                    .pressed = false, -                }, +            .event   = MAKE_KEYEVENT(KEYLOC_COMBO, KEYLOC_COMBO, false),              .keycode = combo->keycode,          };  #ifndef NO_ACTION_TAPPING @@ -325,7 +318,7 @@ void apply_combo(uint16_t combo_index, combo_t *combo) {          if (ALL_COMBO_KEYS_ARE_DOWN(state, key_count)) {              // this in the end executes the combo when the key_buffer is dumped.              record->keycode   = combo->keycode; -            record->event.key = COMBO_KEY_POS; +            record->event.key = MAKE_KEYPOS(KEYLOC_COMBO, KEYLOC_COMBO);              qrecord->combo_index = combo_index;              ACTIVATE_COMBO(combo); diff --git a/quantum/process_keycode/process_joystick.c b/quantum/process_keycode/process_joystick.c index 2fb092c573..8c3e71616f 100644 --- a/quantum/process_keycode/process_joystick.c +++ b/quantum/process_keycode/process_joystick.c @@ -6,41 +6,25 @@  #include <string.h>  #include <math.h> -bool process_joystick_buttons(uint16_t keycode, keyrecord_t *record); -  bool process_joystick(uint16_t keycode, keyrecord_t *record) { -    if (process_joystick_buttons(keycode, record) && (joystick_status.status & JS_UPDATED) > 0) { -        send_joystick_packet(&joystick_status); -        joystick_status.status &= ~JS_UPDATED; +    switch (keycode) { +        case JS_BUTTON0 ... JS_BUTTON_MAX: +            if (record->event.pressed) { +                register_joystick_button(keycode - JS_BUTTON0); +            } else { +                unregister_joystick_button(keycode - JS_BUTTON0); +            } +            return false;      } -      return true;  }  __attribute__((weak)) void joystick_task(void) { -    if (process_joystick_analogread() && (joystick_status.status & JS_UPDATED)) { -        send_joystick_packet(&joystick_status); -        joystick_status.status &= ~JS_UPDATED; +    if (process_joystick_analogread()) { +        joystick_flush();      }  } -bool process_joystick_buttons(uint16_t keycode, keyrecord_t *record) { -    if (keycode < JS_BUTTON0 || keycode > JS_BUTTON_MAX) { -        return true; -    } else { -        uint8_t button_idx = (keycode - JS_BUTTON0); -        if (record->event.pressed) { -            joystick_status.buttons[button_idx / 8] |= 1 << (button_idx % 8); -        } else { -            joystick_status.buttons[button_idx / 8] &= ~(1 << (button_idx % 8)); -        } - -        joystick_status.status |= JS_UPDATED; -    } - -    return true; -} -  uint16_t savePinState(pin_t pin) {  #ifdef __AVR__      uint8_t pinNumber = pin & 0xF; diff --git a/quantum/process_keycode/process_secure.c b/quantum/process_keycode/process_secure.c new file mode 100644 index 0000000000..827ace597a --- /dev/null +++ b/quantum/process_keycode/process_secure.c @@ -0,0 +1,39 @@ +// Copyright 2022 QMK +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "secure.h" +#include "process_secure.h" +#include "quantum_keycodes.h" + +bool preprocess_secure(uint16_t keycode, keyrecord_t *record) { +    if (secure_is_unlocking()) { +        if (!record->event.pressed) { +            secure_keypress_event(record->event.key.row, record->event.key.col); +        } + +        // Normal keypresses should be disabled until the sequence is completed +        return false; +    } + +    return true; +} + +bool process_secure(uint16_t keycode, keyrecord_t *record) { +#ifndef SECURE_DISABLE_KEYCODES +    if (!record->event.pressed) { +        if (keycode == SECURE_LOCK) { +            secure_lock(); +            return false; +        } +        if (keycode == SECURE_UNLOCK) { +            secure_unlock(); +            return false; +        } +        if (keycode == SECURE_TOGGLE) { +            secure_is_locked() ? secure_unlock() : secure_lock(); +            return false; +        } +    } +#endif +    return true; +}
\ No newline at end of file diff --git a/quantum/process_keycode/process_secure.h b/quantum/process_keycode/process_secure.h new file mode 100644 index 0000000000..2814264b92 --- /dev/null +++ b/quantum/process_keycode/process_secure.h @@ -0,0 +1,15 @@ +// Copyright 2022 QMK +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include <stdbool.h> +#include "action.h" + +/** \brief Intercept keycodes and detect unlock sequences + */ +bool preprocess_secure(uint16_t keycode, keyrecord_t *record); + +/** \brief Handle any secure specific keycodes + */ +bool process_secure(uint16_t keycode, keyrecord_t *record); diff --git a/quantum/process_keycode/process_space_cadet.c b/quantum/process_keycode/process_space_cadet.c index 46b2648c35..0997e7b7f3 100644 --- a/quantum/process_keycode/process_space_cadet.c +++ b/quantum/process_keycode/process_space_cadet.c @@ -93,12 +93,7 @@ void perform_space_cadet(keyrecord_t *record, uint16_t sc_keycode, uint8_t holdM              register_mods(MOD_BIT(holdMod));          }      } else { -#ifdef TAPPING_TERM_PER_KEY -        if (sc_last == holdMod && timer_elapsed(sc_timer) < get_tapping_term(sc_keycode, record)) -#else -        if (sc_last == holdMod && timer_elapsed(sc_timer) < TAPPING_TERM) -#endif -        { +        if (sc_last == holdMod && timer_elapsed(sc_timer) < GET_TAPPING_TERM(sc_keycode, record)) {              if (holdMod != tapMod) {                  if (IS_MOD(holdMod)) {                      unregister_mods(MOD_BIT(holdMod)); diff --git a/quantum/process_keycode/process_tap_dance.c b/quantum/process_keycode/process_tap_dance.c index e99119b2ae..db8df5f870 100644 --- a/quantum/process_keycode/process_tap_dance.c +++ b/quantum/process_keycode/process_tap_dance.c @@ -174,11 +174,7 @@ void tap_dance_task() {          if (action->custom_tapping_term > 0) {              tap_user_defined = action->custom_tapping_term;          } else { -#ifdef TAPPING_TERM_PER_KEY -            tap_user_defined = get_tapping_term(action->state.keycode, &(keyrecord_t){}); -#else -            tap_user_defined = TAPPING_TERM; -#endif +            tap_user_defined = GET_TAPPING_TERM(action->state.keycode, &(keyrecord_t){});          }          if (action->state.count && timer_elapsed(action->state.timer) > tap_user_defined) {              process_tap_dance_action_on_dance_finished(action); diff --git a/quantum/process_keycode/process_unicode_common.c b/quantum/process_keycode/process_unicode_common.c index 46b77e14ba..652becbc9a 100644 --- a/quantum/process_keycode/process_unicode_common.c +++ b/quantum/process_keycode/process_unicode_common.c @@ -16,8 +16,7 @@  #include "process_unicode_common.h"  #include "eeprom.h" -#include <ctype.h> -#include <string.h> +#include "utf8.h"  unicode_config_t unicode_config;  uint8_t          unicode_saved_mods; @@ -231,66 +230,6 @@ void register_unicode(uint32_t code_point) {      unicode_input_finish();  } -// clang-format off - -void send_unicode_hex_string(const char *str) { -    if (!str) { -        return; -    } - -    while (*str) { -        // Find the next code point (token) in the string -        for (; *str == ' '; str++);    // Skip leading spaces -        size_t n = strcspn(str, " ");  // Length of the current token -        char code_point[n+1]; -        strncpy(code_point, str, n);   // Copy token into buffer -        code_point[n] = '\0';          // Make sure it's null-terminated - -        // Normalize the code point: make all hex digits lowercase -        for (char *p = code_point; *p; p++) { -            *p = tolower((unsigned char)*p); -        } - -        // Send the code point as a Unicode input string -        unicode_input_start(); -        send_string(code_point); -        unicode_input_finish(); - -        str += n;  // Move to the first ' ' (or '\0') after the current token -    } -} - -// clang-format on - -// Borrowed from https://nullprogram.com/blog/2017/10/06/ -static const char *decode_utf8(const char *str, int32_t *code_point) { -    const char *next; - -    if (str[0] < 0x80) { // U+0000-007F -        *code_point = str[0]; -        next        = str + 1; -    } else if ((str[0] & 0xE0) == 0xC0) { // U+0080-07FF -        *code_point = ((int32_t)(str[0] & 0x1F) << 6) | ((int32_t)(str[1] & 0x3F) << 0); -        next        = str + 2; -    } else if ((str[0] & 0xF0) == 0xE0) { // U+0800-FFFF -        *code_point = ((int32_t)(str[0] & 0x0F) << 12) | ((int32_t)(str[1] & 0x3F) << 6) | ((int32_t)(str[2] & 0x3F) << 0); -        next        = str + 3; -    } else if ((str[0] & 0xF8) == 0xF0 && (str[0] <= 0xF4)) { // U+10000-10FFFF -        *code_point = ((int32_t)(str[0] & 0x07) << 18) | ((int32_t)(str[1] & 0x3F) << 12) | ((int32_t)(str[2] & 0x3F) << 6) | ((int32_t)(str[3] & 0x3F) << 0); -        next        = str + 4; -    } else { -        *code_point = -1; -        next        = str + 1; -    } - -    // part of a UTF-16 surrogate pair - invalid -    if (*code_point >= 0xD800 && *code_point <= 0xDFFF) { -        *code_point = -1; -    } - -    return next; -} -  void send_unicode_string(const char *str) {      if (!str) {          return; diff --git a/quantum/process_keycode/process_unicode_common.h b/quantum/process_keycode/process_unicode_common.h index 1a6607c757..8a4494c939 100644 --- a/quantum/process_keycode/process_unicode_common.h +++ b/quantum/process_keycode/process_unicode_common.h @@ -90,7 +90,6 @@ void register_hex(uint16_t hex);  void register_hex32(uint32_t hex);  void register_unicode(uint32_t code_point); -void send_unicode_hex_string(const char *str);  void send_unicode_string(const char *str);  bool process_unicode_common(uint16_t keycode, keyrecord_t *record); diff --git a/quantum/quantum.c b/quantum/quantum.c index ef6e5ac1df..673ea91b11 100644 --- a/quantum/quantum.c +++ b/quantum/quantum.c @@ -212,6 +212,12 @@ bool process_record_quantum(keyrecord_t *record) {      //   return false;      // } +#if defined(SECURE_ENABLE) +    if (!preprocess_secure(keycode, record)) { +        return false; +    } +#endif +  #ifdef VELOCIKEY_ENABLE      if (velocikey_enabled() && record->event.pressed) {          velocikey_accelerate(); @@ -247,6 +253,9 @@ bool process_record_quantum(keyrecord_t *record) {              process_record_via(keycode, record) &&  #endif              process_record_kb(keycode, record) && +#if defined(SECURE_ENABLE) +            process_secure(keycode, record) && +#endif  #if defined(SEQUENCER_ENABLE)              process_sequencer(keycode, record) &&  #endif @@ -358,6 +367,26 @@ bool process_record_quantum(keyrecord_t *record) {                  oneshot_disable();                  break;  #endif +#ifdef ENABLE_COMPILE_KEYCODE +            case QK_MAKE: // Compiles the firmware, and adds the flash command based on keyboard bootloader +            { +#    ifdef NO_ACTION_ONESHOT +                const uint8_t temp_mod = mod_config(get_mods()); +#    else +                const uint8_t temp_mod = mod_config(get_mods() | get_oneshot_mods()); +                clear_oneshot_mods(); +#    endif +                clear_mods(); + +                SEND_STRING_DELAY("qmk", TAP_CODE_DELAY); +                if (temp_mod & MOD_MASK_SHIFT) { // if shift is held, flash rather than compile +                    SEND_STRING_DELAY(" flash ", TAP_CODE_DELAY); +                } else { +                    SEND_STRING_DELAY(" compile ", TAP_CODE_DELAY); +                } +                SEND_STRING_DELAY("-kb " QMK_KEYBOARD " -km " QMK_KEYMAP SS_TAP(X_ENTER), TAP_CODE_DELAY); +            } +#endif          }      } diff --git a/quantum/quantum.h b/quantum/quantum.h index 020e455941..d021e7c05c 100644 --- a/quantum/quantum.h +++ b/quantum/quantum.h @@ -188,6 +188,10 @@ extern layer_state_t layer_state;  #    include "st7565.h"  #endif +#ifdef QUANTUM_PAINTER_ENABLE +#    include "qp.h" +#endif +  #ifdef DIP_SWITCH_ENABLE  #    include "dip_switch.h"  #endif @@ -196,10 +200,19 @@ extern layer_state_t layer_state;  #    include "process_dynamic_macro.h"  #endif +#ifdef SECURE_ENABLE +#    include "secure.h" +#    include "process_secure.h" +#endif +  #ifdef DYNAMIC_KEYMAP_ENABLE  #    include "dynamic_keymap.h"  #endif +#ifdef JOYSTICK_ENABLE +#    include "joystick.h" +#endif +  #ifdef VIA_ENABLE  #    include "via.h"  #endif diff --git a/quantum/quantum_keycodes.h b/quantum/quantum_keycodes.h index 2552c48165..c7b4ea593f 100644 --- a/quantum/quantum_keycodes.h +++ b/quantum/quantum_keycodes.h @@ -595,6 +595,12 @@ enum quantum_keycodes {      MAGIC_TOGGLE_CONTROL_CAPSLOCK, +    QK_MAKE, + +    SECURE_LOCK, +    SECURE_UNLOCK, +    SECURE_TOGGLE, +      // Start of custom keycode range for keyboards and keymaps - always leave at the end      SAFE_RANGE  }; diff --git a/quantum/rgb_matrix/animations/typing_heatmap_anim.h b/quantum/rgb_matrix/animations/typing_heatmap_anim.h index f3a94280c0..4b17c4c3ed 100644 --- a/quantum/rgb_matrix/animations/typing_heatmap_anim.h +++ b/quantum/rgb_matrix/animations/typing_heatmap_anim.h @@ -7,6 +7,10 @@ RGB_MATRIX_EFFECT(TYPING_HEATMAP)  #        endif  void process_rgb_matrix_typing_heatmap(uint8_t row, uint8_t col) { +#        ifdef RGB_MATRIX_TYPING_HEATMAP_SLIM +    // Limit effect to pressed keys +    g_rgb_frame_buffer[row][col] = qadd8(g_rgb_frame_buffer[row][col], 32); +#        else      uint8_t m_row = row - 1;      uint8_t p_row = row + 1;      uint8_t m_col = col - 1; @@ -27,6 +31,7 @@ void process_rgb_matrix_typing_heatmap(uint8_t row, uint8_t col) {          g_rgb_frame_buffer[m_row][col] = qadd8(g_rgb_frame_buffer[m_row][col], 16);          if (p_col < MATRIX_COLS) g_rgb_frame_buffer[m_row][p_col] = qadd8(g_rgb_frame_buffer[m_row][p_col], 13);      } +#        endif  }  // A timer to track the last time we decremented all heatmap values. diff --git a/quantum/rgblight/rgblight.c b/quantum/rgblight/rgblight.c index f4ddb81e92..dc5757cb00 100644 --- a/quantum/rgblight/rgblight.c +++ b/quantum/rgblight/rgblight.c @@ -826,6 +826,21 @@ void rgblight_blink_layer_repeat(uint8_t layer, uint16_t duration_ms, uint8_t ti      _repeat_timer = sync_timer_read() + duration_ms;  } +void rgblight_unblink_layer(uint8_t layer) { +    rgblight_set_layer_state(layer, false); +    _blinking_layer_mask &= ~((rgblight_layer_mask_t)1 << layer); +} + +void rgblight_unblink_all_but_layer(uint8_t layer) { +    for (uint8_t i = 0; i < RGBLIGHT_MAX_LAYERS; i++) { +        if (i != layer) { +            if ((_blinking_layer_mask & (rgblight_layer_mask_t)1 << i) != 0) { +                rgblight_unblink_layer(i); +            } +        } +    } +} +  void rgblight_blink_layer_repeat_helper(void) {      if (_blinking_layer_mask != 0 && timer_expired(sync_timer_read(), _repeat_timer)) {          for (uint8_t layer = 0; layer < RGBLIGHT_MAX_LAYERS; layer++) { @@ -1258,19 +1273,19 @@ void rgblight_effect_snake(animation_status_t *anim) {      }      rgblight_set();      if (increment == 1) { -        if (pos - 1 < 0) { +        if (pos - RGBLIGHT_EFFECT_SNAKE_INCREMENT < 0) {              pos = rgblight_ranges.effect_num_leds - 1;  #    if defined(RGBLIGHT_SPLIT) && !defined(RGBLIGHT_SPLIT_NO_ANIMATION_SYNC)              anim->pos = 0;  #    endif          } else { -            pos -= 1; +            pos -= RGBLIGHT_EFFECT_SNAKE_INCREMENT;  #    if defined(RGBLIGHT_SPLIT) && !defined(RGBLIGHT_SPLIT_NO_ANIMATION_SYNC)              anim->pos = 1;  #    endif          }      } else { -        pos = (pos + 1) % rgblight_ranges.effect_num_leds; +        pos = (pos + RGBLIGHT_EFFECT_SNAKE_INCREMENT) % rgblight_ranges.effect_num_leds;  #    if defined(RGBLIGHT_SPLIT) && !defined(RGBLIGHT_SPLIT_NO_ANIMATION_SYNC)          anim->pos = pos;  #    endif @@ -1284,7 +1299,7 @@ __attribute__((weak)) const uint8_t RGBLED_KNIGHT_INTERVALS[] PROGMEM = {127, 63  void rgblight_effect_knight(animation_status_t *anim) {      static int8_t low_bound  = 0;      static int8_t high_bound = RGBLIGHT_EFFECT_KNIGHT_LENGTH - 1; -    static int8_t increment  = 1; +    static int8_t increment  = RGBLIGHT_EFFECT_KNIGHT_INCREMENT;      uint8_t       i, cur;  #    if defined(RGBLIGHT_SPLIT) && !defined(RGBLIGHT_SPLIT_NO_ANIMATION_SYNC) diff --git a/quantum/rgblight/rgblight.h b/quantum/rgblight/rgblight.h index 7076dc41ac..a08b9a7b6b 100644 --- a/quantum/rgblight/rgblight.h +++ b/quantum/rgblight/rgblight.h @@ -126,10 +126,18 @@ enum RGBLIGHT_EFFECT_MODE {  #    define RGBLIGHT_EFFECT_SNAKE_LENGTH 4  #endif +#ifndef RGBLIGHT_EFFECT_SNAKE_INCREMENT +#    define RGBLIGHT_EFFECT_SNAKE_INCREMENT 1 +#endif +  #ifndef RGBLIGHT_EFFECT_KNIGHT_LENGTH  #    define RGBLIGHT_EFFECT_KNIGHT_LENGTH 3  #endif +#ifndef RGBLIGHT_EFFECT_KNIGHT_INCREMENT +#    define RGBLIGHT_EFFECT_KNIGHT_INCREMENT 1 +#endif +  #ifndef RGBLIGHT_EFFECT_KNIGHT_OFFSET  #    define RGBLIGHT_EFFECT_KNIGHT_OFFSET 0  #endif @@ -217,6 +225,24 @@ extern const rgblight_segment_t *const *rgblight_layers;  #        define RGBLIGHT_USE_TIMER  void rgblight_blink_layer(uint8_t layer, uint16_t duration_ms);  void rgblight_blink_layer_repeat(uint8_t layer, uint16_t duration_ms, uint8_t times); +/** + * \brief Stop blinking on one layer. + * + * Stop a layer that is blinking. If the layer is not blinking it will + * be unaffected. + * + * \param layer Layer number to stop blinking. + */ +void rgblight_unblink_layer(uint8_t layer); +/** + * \brief Stop blinking all layers except one. + * + * Stop all layers that are blinking except for one specific layer. + * Layers that are not blinking are unaffected. + * + * \param layer Layer number to keep blinking. + */ +void rgblight_unblink_all_but_layer(uint8_t layer);  #    endif  #endif diff --git a/quantum/secure.c b/quantum/secure.c new file mode 100644 index 0000000000..00048bd6dd --- /dev/null +++ b/quantum/secure.c @@ -0,0 +1,87 @@ +// Copyright 2022 QMK +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "secure.h" +#include "timer.h" + +#ifndef SECURE_UNLOCK_TIMEOUT +#    define SECURE_UNLOCK_TIMEOUT 5000 +#endif + +#ifndef SECURE_IDLE_TIMEOUT +#    define SECURE_IDLE_TIMEOUT 60000 +#endif + +#ifndef SECURE_UNLOCK_SEQUENCE +#    define SECURE_UNLOCK_SEQUENCE \ +        {                          \ +            { 0, 0 }               \ +        } +#endif + +static secure_status_t secure_status = SECURE_LOCKED; +static uint32_t        unlock_time   = 0; +static uint32_t        idle_time     = 0; + +secure_status_t secure_get_status(void) { +    return secure_status; +} + +void secure_lock(void) { +    secure_status = SECURE_LOCKED; +} + +void secure_unlock(void) { +    secure_status = SECURE_UNLOCKED; +    idle_time     = timer_read32(); +} + +void secure_request_unlock(void) { +    if (secure_status == SECURE_LOCKED) { +        secure_status = SECURE_PENDING; +        unlock_time   = timer_read32(); +    } +} + +void secure_activity_event(void) { +    if (secure_status == SECURE_UNLOCKED) { +        idle_time = timer_read32(); +    } +} + +void secure_keypress_event(uint8_t row, uint8_t col) { +    static const uint8_t sequence[][2] = SECURE_UNLOCK_SEQUENCE; +    static const uint8_t sequence_len  = sizeof(sequence) / sizeof(sequence[0]); + +    static uint8_t offset = 0; +    if ((sequence[offset][0] == row) && (sequence[offset][1] == col)) { +        offset++; +        if (offset == sequence_len) { +            offset = 0; +            secure_unlock(); +        } +    } else { +        offset = 0; +        secure_lock(); +    } +} + +void secure_task(void) { +#if SECURE_UNLOCK_TIMEOUT != 0 +    // handle unlock timeout +    if (secure_status == SECURE_PENDING) { +        if (timer_elapsed32(unlock_time) >= SECURE_UNLOCK_TIMEOUT) { +            secure_lock(); +        } +    } +#endif + +#if SECURE_IDLE_TIMEOUT != 0 +    // handle idle timeout +    if (secure_status == SECURE_UNLOCKED) { +        if (timer_elapsed32(idle_time) >= SECURE_IDLE_TIMEOUT) { +            secure_lock(); +        } +    } +#endif +} diff --git a/quantum/secure.h b/quantum/secure.h new file mode 100644 index 0000000000..04507fd5b1 --- /dev/null +++ b/quantum/secure.h @@ -0,0 +1,67 @@ +// Copyright 2022 QMK +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +/** \file + * + * Exposes a set of functionality to act as a virtual padlock for your device + * ... As long as that padlock is made of paper and its currently raining. + */ + +#include <stdint.h> +#include <stdbool.h> + +/** \brief Available secure states + */ +typedef enum { +    SECURE_LOCKED, +    SECURE_PENDING, +    SECURE_UNLOCKED, +} secure_status_t; + +/** \brief Query current secure state + */ +secure_status_t secure_get_status(void); + +/** \brief Helper to check if unlocking is currently locked + */ +#define secure_is_locked() (secure_get_status() == SECURE_LOCKED) + +/** \brief Helper to check if unlocking is currently in progress + */ +#define secure_is_unlocking() (secure_get_status() == SECURE_PENDING) + +/** \brief Helper to check if unlocking is currently unlocked + */ +#define secure_is_unlocked() (secure_get_status() == SECURE_UNLOCKED) + +/** \brief Lock down the device + */ +void secure_lock(void); + +/** \brief Force unlock the device + * + * \warning bypasses user unlock sequence + */ +void secure_unlock(void); + +/** \brief Begin listening for an unlock sequence + */ +void secure_request_unlock(void); + +/** \brief Flag to the secure subsystem that user activity has happened + * + * Call when some user activity has happened and the device should remain unlocked + */ +void secure_activity_event(void); + +/** \brief Flag to the secure subsystem that user has triggered a keypress + * + * Call to trigger processing of the unlock sequence + */ +void secure_keypress_event(uint8_t row, uint8_t col); + +/** \brief Handle various secure subsystem background tasks + */ +void secure_task(void); diff --git a/quantum/split_common/transactions.c b/quantum/split_common/transactions.c index cffbccaeee..105bf918cb 100644 --- a/quantum/split_common/transactions.c +++ b/quantum/split_common/transactions.c @@ -180,7 +180,7 @@ static void master_matrix_handlers_slave(matrix_row_t master_matrix[], matrix_ro  static bool encoder_handlers_master(matrix_row_t master_matrix[], matrix_row_t slave_matrix[]) {      static uint32_t last_update = 0; -    uint8_t         temp_state[NUMBER_OF_ENCODERS]; +    uint8_t         temp_state[NUM_ENCODERS_MAX_PER_SIDE];      bool okay = read_if_checksum_mismatch(GET_ENCODERS_CHECKSUM, GET_ENCODERS_DATA, &last_update, temp_state, split_shmem->encoders.state, sizeof(temp_state));      if (okay) encoder_update_raw(temp_state); @@ -188,7 +188,7 @@ static bool encoder_handlers_master(matrix_row_t master_matrix[], matrix_row_t s  }  static void encoder_handlers_slave(matrix_row_t master_matrix[], matrix_row_t slave_matrix[]) { -    uint8_t encoder_state[NUMBER_OF_ENCODERS]; +    uint8_t encoder_state[NUM_ENCODERS_MAX_PER_SIDE];      encoder_state_raw(encoder_state);      // Always prepare the encoder state for read.      memcpy(split_shmem->encoders.state, encoder_state, sizeof(encoder_state)); diff --git a/quantum/split_common/transport.h b/quantum/split_common/transport.h index 26bd136728..e62679990a 100644 --- a/quantum/split_common/transport.h +++ b/quantum/split_common/transport.h @@ -42,7 +42,6 @@ bool transport_execute_transaction(int8_t id, const void *initiator2target_buf,  #ifdef ENCODER_ENABLE  #    include "encoder.h" -#    define NUMBER_OF_ENCODERS (sizeof((pin_t[])ENCODERS_PAD_A) / sizeof(pin_t))  #endif // ENCODER_ENABLE  #ifdef BACKLIGHT_ENABLE @@ -67,7 +66,7 @@ typedef struct _split_master_matrix_sync_t {  #ifdef ENCODER_ENABLE  typedef struct _split_slave_encoder_sync_t {      uint8_t checksum; -    uint8_t state[NUMBER_OF_ENCODERS]; +    uint8_t state[NUM_ENCODERS_MAX_PER_SIDE];  } split_slave_encoder_sync_t;  #endif // ENCODER_ENABLE diff --git a/quantum/utf8.c b/quantum/utf8.c new file mode 100644 index 0000000000..4b2cd4d8d4 --- /dev/null +++ b/quantum/utf8.c @@ -0,0 +1,46 @@ +/* Copyright 2021 QMK + * + * 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 + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program.  If not, see <http://www.gnu.org/licenses/>. + */ + +#include "utf8.h" + +// Borrowed from https://nullprogram.com/blog/2017/10/06/ +const char *decode_utf8(const char *str, int32_t *code_point) { +    const char *next; + +    if (str[0] < 0x80) { // U+0000-007F +        *code_point = str[0]; +        next        = str + 1; +    } else if ((str[0] & 0xE0) == 0xC0) { // U+0080-07FF +        *code_point = ((int32_t)(str[0] & 0x1F) << 6) | ((int32_t)(str[1] & 0x3F) << 0); +        next        = str + 2; +    } else if ((str[0] & 0xF0) == 0xE0) { // U+0800-FFFF +        *code_point = ((int32_t)(str[0] & 0x0F) << 12) | ((int32_t)(str[1] & 0x3F) << 6) | ((int32_t)(str[2] & 0x3F) << 0); +        next        = str + 3; +    } else if ((str[0] & 0xF8) == 0xF0 && (str[0] <= 0xF4)) { // U+10000-10FFFF +        *code_point = ((int32_t)(str[0] & 0x07) << 18) | ((int32_t)(str[1] & 0x3F) << 12) | ((int32_t)(str[2] & 0x3F) << 6) | ((int32_t)(str[3] & 0x3F) << 0); +        next        = str + 4; +    } else { +        *code_point = -1; +        next        = str + 1; +    } + +    // part of a UTF-16 surrogate pair - invalid +    if (*code_point >= 0xD800 && *code_point <= 0xDFFF) { +        *code_point = -1; +    } + +    return next; +} diff --git a/quantum/utf8.h b/quantum/utf8.h new file mode 100644 index 0000000000..fb10910944 --- /dev/null +++ b/quantum/utf8.h @@ -0,0 +1,21 @@ +/* Copyright 2021 QMK + * + * 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 + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program.  If not, see <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <stdint.h> + +const char *decode_utf8(const char *str, int32_t *code_point);
\ No newline at end of file diff --git a/quantum/util.h b/quantum/util.h index bef3b9abe3..ab96ce4bde 100644 --- a/quantum/util.h +++ b/quantum/util.h @@ -24,3 +24,11 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.  // convert to string  #define STR(s) XSTR(s)  #define XSTR(s) #s + +#if !defined(MIN) +#    define MIN(x, y) (((x) < (y)) ? (x) : (y)) +#endif + +#if !defined(MAX) +#    define MAX(x, y) (((x) > (y)) ? (x) : (y)) +#endif | 
