From 75544d91273d1ae9f0255556dc5170478a1a6e49 Mon Sep 17 00:00:00 2001 From: Ryan Date: Mon, 7 Mar 2022 22:22:22 +1100 Subject: Move Doxygen docs to subdirectory (#16561) --- docs/_summary.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) (limited to 'docs/_summary.md') diff --git a/docs/_summary.md b/docs/_summary.md index 74203aa0f8..250a48fe6b 100644 --- a/docs/_summary.md +++ b/docs/_summary.md @@ -147,7 +147,7 @@ * [EEPROM Driver](eeprom_driver.md) * ['serial' Driver](serial_driver.md) * [UART Driver](uart_driver.md) - * [GPIO Controls](internals_gpio_control.md) + * [GPIO Controls](gpio_control.md) * [Keyboard Guidelines](hardware_keyboard_guidelines.md) * Python Development @@ -183,10 +183,10 @@ * [Understanding QMK](understanding_qmk.md) * QMK Internals (In Progress) - * [Defines](internals_defines.md) - * [Input Callback Reg](internals_input_callback_reg.md) - * [Midi Device](internals_midi_device.md) - * [Midi Device Setup Process](internals_midi_device_setup_process.md) - * [Midi Util](internals_midi_util.md) - * [Send Functions](internals_send_functions.md) - * [Sysex Tools](internals_sysex_tools.md) + * [Defines](internals/defines.md) + * [Input Callback Reg](internals/input_callback_reg.md) + * [Midi Device](internals/midi_device.md) + * [Midi Device Setup Process](internals/midi_device_setup_process.md) + * [Midi Util](internals/midi_util.md) + * [Send Functions](internals/send_functions.md) + * [Sysex Tools](internals/sysex_tools.md) -- cgit v1.2.3 From 9ee2effe8e61d32524b11092a0d04a96972c1493 Mon Sep 17 00:00:00 2001 From: Ryan Date: Sat, 19 Mar 2022 17:57:22 +1100 Subject: HD44780 driver rework (#16370) --- builddefs/common_features.mk | 3 +- docs/_summary.md | 2 +- docs/feature_hd44780.md | 345 +++++++++++++++++++++---- drivers/lcd/hd44780.c | 284 +++++++++++++++++++++ drivers/lcd/hd44780.h | 220 ++++++++++++++++ platforms/avr/drivers/hd44780.c | 542 ---------------------------------------- platforms/avr/drivers/hd44780.h | 348 -------------------------- 7 files changed, 800 insertions(+), 944 deletions(-) create mode 100644 drivers/lcd/hd44780.c create mode 100644 drivers/lcd/hd44780.h delete mode 100644 platforms/avr/drivers/hd44780.c delete mode 100644 platforms/avr/drivers/hd44780.h (limited to 'docs/_summary.md') diff --git a/builddefs/common_features.mk b/builddefs/common_features.mk index 08d186d656..a1793f91a5 100644 --- a/builddefs/common_features.mk +++ b/builddefs/common_features.mk @@ -646,8 +646,9 @@ ifeq ($(strip $(HAPTIC_ENABLE)),yes) endif ifeq ($(strip $(HD44780_ENABLE)), yes) - SRC += platforms/avr/drivers/hd44780.c OPT_DEFS += -DHD44780_ENABLE + COMMON_VPATH += $(DRIVER_PATH)/lcd + SRC += hd44780.c endif VALID_OLED_DRIVER_TYPES := SSD1306 custom diff --git a/docs/_summary.md b/docs/_summary.md index 250a48fe6b..249bfcd9ed 100644 --- a/docs/_summary.md +++ b/docs/_summary.md @@ -94,7 +94,7 @@ * Hardware Features * Displays - * [HD44780 LCD Controller](feature_hd44780.md) + * [HD44780 LCD Driver](feature_hd44780.md) * [ST7565 LCD Driver](feature_st7565.md) * [OLED Driver](feature_oled_driver.md) * Lighting diff --git a/docs/feature_hd44780.md b/docs/feature_hd44780.md index dc476c734f..4ade640baa 100644 --- a/docs/feature_hd44780.md +++ b/docs/feature_hd44780.md @@ -1,57 +1,298 @@ -# HD44780 LCD Displays - -This is an integration of Peter Fleury's LCD library. This page will explain the basics. [For in depth documentation visit his page.](http://www.peterfleury.epizy.com/doxygen/avr-gcc-libraries/group__pfleury__lcd.html) - -You can enable support for HD44780 Displays by setting the `HD44780_ENABLE` flag in your keyboards `rules.mk` to yes. - -## Configuration - -You will need to configure the pins used by your display, and its number of lines and columns in your keyboard's `config.h`. - - -Uncomment the section labled HD44780 and change the parameters as needed. -```` -/* - * HD44780 LCD Display Configuration - */ - -#define LCD_LINES 2 //< number of visible lines of the display -#define LCD_DISP_LENGTH 16 //< visibles characters per line of the display -#define LCD_IO_MODE 1 //< 0: memory mapped mode, 1: IO port mode -#if LCD_IO_MODE -#define LCD_PORT PORTB //< port for the LCD lines -#define LCD_DATA0_PORT LCD_PORT //< port for 4bit data bit 0 -#define LCD_DATA1_PORT LCD_PORT //< port for 4bit data bit 1 -#define LCD_DATA2_PORT LCD_PORT //< port for 4bit data bit 2 -#define LCD_DATA3_PORT LCD_PORT //< port for 4bit data bit 3 -#define LCD_DATA0_PIN 4 //< pin for 4bit data bit 0 -#define LCD_DATA1_PIN 5 //< pin for 4bit data bit 1 -#define LCD_DATA2_PIN 6 //< pin for 4bit data bit 2 -#define LCD_DATA3_PIN 7 //< pin for 4bit data bit 3 -#define LCD_RS_PORT LCD_PORT //< port for RS line -#define LCD_RS_PIN 3 //< pin for RS line -#define LCD_RW_PORT LCD_PORT //< port for RW line -#define LCD_RW_PIN 2 //< pin for RW line -#define LCD_E_PORT LCD_PORT //< port for Enable line -#define LCD_E_PIN 1 //< pin for Enable line -#endif -```` - -Should you need to configure other properties you can copy them from `quantum/hd44780.h` and set them in your `config.h` +# HD44780 LCD Driver + +## Supported Hardware + +LCD modules using [HD44780U](https://www.sparkfun.com/datasheets/LCD/HD44780.pdf) IC or equivalent, communicating in 4-bit mode. + +|Module|Size |Notes | +|------|--------------|---------------------------------| +|1602A |16x2, 5x8 dots| | +|2004A |20x4, 5x8 dots|Untested, not currently supported| + +To run these modules at 3.3V, an additional MAX660 voltage converter IC must be soldered on, along with two 10µF capacitors. See [this page](https://www.codrey.com/electronic-circuits/hack-your-16x2-lcd/) for more details. ## Usage -To initialize your display, call `lcd_init()` with one of these parameters: -```` -LCD_DISP_OFF : display off -LCD_DISP_ON : display on, cursor off -LCD_DISP_ON_CURSOR : display on, cursor on -LCD_DISP_ON_CURSOR_BLINK : display on, cursor on flashing -```` -This is best done in your keyboards `matrix_init_kb` or your keymaps `matrix_init_user`. -It is advised to clear the display before use. -To do so call `lcd_clrscr()`. +Add the following to your `rules.mk`: + +```make +HD44780_ENABLE = yes +``` + +## Basic Configuration + +Add the following to your `config.h`: + +|Define |Default |Description | +|-----------------------|--------------|-----------------------------------------------------------------------------------------------------| +|`HD44780_DATA_PINS` |*Not defined* |(Required) An array of four GPIO pins connected to the display's D4-D7 pins, eg. `{ B1, B3, B2, B6 }`| +|`HD44780_RS_PIN` |*Not defined* |(Required) The GPIO connected to the display's RS pin | +|`HD44780_RW_PIN` |*Not defined* |(Required) The GPIO connected to the display's RW pin | +|`HD44780_E_PIN` |*Not defined* |(Required) The GPIO connected to the display's E pin | +|`HD44780_DISPLAY_COLS` |`16` |The number of visible characters on a single line of the display | +|`HD44780_DISPLAY_LINES`|`2` |The number of visible lines on the display | +|`HD44780_WRAP_LINES` |*Not defined* |If defined, input characters will wrap to the next line | + +## Examples + +### Hello World + +Add the following to your `keymap.c`: + +```c +void keyboard_post_init_user(void) { + hd44780_init(true, true); // Show blinking cursor + hd44780_puts_P(PSTR("Hello, world!\n")); +} +``` + +### Custom Character Definition + +Up to eight custom characters can be defined. This data is stored in the Character Generator RAM (CGRAM), and is not persistent across power cycles. + +This example defines the QMK Psi as the first custom character. The first 16 positions in the character set are reserved for the eight custom characters duplicated. + +``` +Byte | 16 8 4 2 1 + 1 | x x x ■ □ ■ □ ■ + 2 | x x x ■ □ ■ □ ■ + 3 | x x x ■ □ ■ □ ■ + 4 | x x x □ ■ ■ ■ □ + 5 | x x x □ □ ■ □ □ + 6 | x x x □ □ ■ □ □ + 7 | x x x □ □ ■ □ □ + 8 | x x x □ □ □ □ □ +``` + +```c +const uint8_t PROGMEM psi[8] = { 0x15, 0x15, 0x15, 0x0E, 0x04, 0x04, 0x04, 0x00 }; + +void keyboard_post_init_user(void) { + hd44780_init(false, false); + hd44780_define_char_P(0, psi); + // Cursor is incremented while defining characters so must be reset + hd44780_home(); + // 0x08 to avoid null terminator + hd44780_puts_P(PSTR("\x08 QMK Firmware")); +} +``` + +## API + +### `void hd44780_init(bool cursor, bool blink)` + +Initialize the display. + +This function should be called only once, before any of the other functions can be called. + +#### Arguments + + - `bool cursor` + Whether to show the cursor. + - `bool blink` + Whether to blink the cursor, if shown. + +--- + +### `void hd44780_clear(void)` + +Clear the display. + +This function is called on init. + +--- + +### `void hd44780_home(void)` + +Move the cursor to the home position. + +This function is called on init. + +--- + +### `void hd44780_on(bool cursor, bool blink)` + +Turn the display on, and/or set the cursor properties. + +This function is called on init. + +#### Arguments + + - `bool cursor` + Whether to show the cursor. + - `bool blink` + Whether to blink the cursor, if shown. + +--- + +### `void hd44780_off(void)` + +Turn the display off. + +--- + +### `void hd44780_set_cursor(uint8_t col, uint8_t line)` + +Move the cursor to the specified position on the display. + +#### Arguments + + - `uint8_t col` + The column number to move to, from 0 to 15 on 16x2 displays. + - `bool line` + The line number to move to, either 0 or 1 on 16x2 displays. + +--- + +### `void hd44780_putc(char c)` + +Print a character to the display. The newline character `\n` will move the cursor to the start of the next line. + +The exact character shown may depend on the ROM code of your particular display - refer to the datasheet for the full character set. + +#### Arguments + + - `char c` + The character to print. + +--- + +### `void hd44780_puts(const char *s)` + +Print a string of characters to the display. + +#### Arguments + + - `const char *s` + The string to print. + +--- + +### `void hd44780_puts_P(const char *s)` + +Print a string of characters from PROGMEM to the display. + +On ARM devices, this function is simply an alias of `hd44780_puts()`. + +#### Arguments + + - `const char *s` + The PROGMEM string to print (ie. `PSTR("Hello")`). + +--- + +### `void hd44780_define_char(uint8_t index, uint8_t *data)` + +Define a custom character. + +#### Arguments + + - `uint8_t index` + The index of the custom character to define, from 0 to 7. + - `uint8_t *data` + An array of 8 bytes containing the 5-bit row data of the character, where the first byte is the topmost row, and the least significant bit of each byte is the rightmost column. + +--- + +### `void hd44780_define_char_P(uint8_t index, const uint8_t *data)` + +Define a custom character from PROGMEM. + +On ARM devices, this function is simply an alias of `hd44780_define_char()`. + +#### Arguments + + - `uint8_t index` + The index of the custom character to define, from 0 to 7. + - `const uint8_t *data` + A PROGMEM array of 8 bytes containing the 5-bit row data of the character, where the first byte is the topmost row, and the least significant bit of each byte is the rightmost column. + +--- + +### `bool hd44780_busy(void)` + +Indicates whether the display is currently processing, and cannot accept instructions. + +#### Return Value + +`true` if the display is busy. + +--- + +### `void hd44780_write(uint8_t data, bool isData)` + +Write a byte to the display. + +#### Arguments + + - `uint8_t data` + The byte to send to the display. + - `bool isData` + Whether the byte is an instruction or character data. + +--- + +### `uint8_t hd44780_read(bool isData)` + +Read a byte from the display. + +#### Arguments + + - `bool isData` + Whether to read the current cursor position, or the character at the cursor. + +#### Return Value + +If `isData` is `true`, the returned byte will be the character at the current DDRAM address. Otherwise, it will be the current DDRAM address and the busy flag. + +--- + +### `void hd44780_command(uint8_t command)` + +Send a command to the display. Refer to the datasheet and `hd44780.h` for the valid commands and defines. + +This function waits for the display to clear the busy flag before sending the command. + +#### Arguments + + - `uint8_t command` + The command to send. + +--- + +### `void hd44780_data(uint8_t data)` + +Send a byte of data to the display. + +This function waits for the display to clear the busy flag before sending the data. + +#### Arguments + + - `uint8_t data` + The byte of data to send. + +--- + +### `void hd44780_set_cgram_address(uint8_t address)` + +Set the CGRAM address. + +This function is used when defining custom characters. + +#### Arguments + + - `uint8_t address` + The CGRAM address to move to, from `0x00` to `0x3F`. + +--- + +### `void hd44780_set_ddram_address(uint8_t address)` + +Set the DDRAM address. + +This function is used when printing characters to the display, and setting the cursor. -To now print something to your Display you first call `lcd_gotoxy(column, line)`. To go to the start of the first line you would call `lcd_gotoxy(0, 0)` and then print a string with `lcd_puts("example string")`. +#### Arguments -There are more methods available to control the display. [For in depth documentation please visit the linked page.](http://www.peterfleury.epizy.com/doxygen/avr-gcc-libraries/group__pfleury__lcd.html) + - `uint8_t address` + The DDRAM address to move to, from `0x00` to `0x7F`. diff --git a/drivers/lcd/hd44780.c b/drivers/lcd/hd44780.c new file mode 100644 index 0000000000..c988ebe56c --- /dev/null +++ b/drivers/lcd/hd44780.c @@ -0,0 +1,284 @@ +/* +Copyright 2022 + +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 . +*/ + +#include "hd44780.h" +#include "gpio.h" +#include "progmem.h" +#include "wait.h" + +#ifndef HD44780_DATA_PINS +# error hd44780: no data pins defined! +#endif + +#ifndef HD44780_RS_PIN +# error hd44780: no RS pin defined! +#endif + +#ifndef HD44780_RW_PIN +# error hd44780: no R/W pin defined! +#endif + +#ifndef HD44780_E_PIN +# error hd44780: no E pin defined! +#endif + +static const pin_t data_pins[4] = HD44780_DATA_PINS; + +#ifndef HD44780_DISPLAY_COLS +# define HD44780_DISPLAY_COLS 16 +#endif + +#ifndef HD44780_DISPLAY_LINES +# define HD44780_DISPLAY_LINES 2 +#endif + +#ifndef HD44780_DDRAM_LINE0_ADDR +# define HD44780_DDRAM_LINE0_ADDR 0x00 +#endif +#ifndef HD44780_DDRAM_LINE1_ADDR +# define HD44780_DDRAM_LINE1_ADDR 0x40 +#endif + +#define HD44780_INIT_DELAY_MS 16 +#define HD44780_ENABLE_DELAY_US 1 + +static void hd44780_latch(void) { + writePinHigh(HD44780_E_PIN); + wait_us(HD44780_ENABLE_DELAY_US); + writePinLow(HD44780_E_PIN); +} + +void hd44780_write(uint8_t data, bool isData) { + writePin(HD44780_RS_PIN, isData); + writePinLow(HD44780_RW_PIN); + + for (int i = 0; i < 4; i++) { + setPinOutput(data_pins[i]); + } + + // Write high nibble + for (int i = 0; i < 4; i++) { + writePin(data_pins[i], (data >> 4) & (1 << i)); + } + hd44780_latch(); + + // Write low nibble + for (int i = 0; i < 4; i++) { + writePin(data_pins[i], data & (1 << i)); + } + hd44780_latch(); + + for (int i = 0; i < 4; i++) { + writePinHigh(data_pins[i]); + } +} + +uint8_t hd44780_read(bool isData) { + uint8_t data = 0; + + writePin(HD44780_RS_PIN, isData); + writePinHigh(HD44780_RW_PIN); + + for (int i = 0; i < 4; i++) { + setPinInput(data_pins[i]); + } + + writePinHigh(HD44780_E_PIN); + wait_us(HD44780_ENABLE_DELAY_US); + + // Read high nibble + for (int i = 0; i < 4; i++) { + data |= (readPin(data_pins[i]) << i); + } + + data <<= 4; + + writePinLow(HD44780_E_PIN); + wait_us(HD44780_ENABLE_DELAY_US); + writePinHigh(HD44780_E_PIN); + wait_us(HD44780_ENABLE_DELAY_US); + + // Read low nibble + for (int i = 0; i < 4; i++) { + data |= (readPin(data_pins[i]) << i); + } + + writePinLow(HD44780_E_PIN); + + return data; +} + +bool hd44780_busy(void) { + return hd44780_read(false) & HD44780_BUSY_FLAG; +} + +void hd44780_command(uint8_t command) { + while (hd44780_busy()) + ; + hd44780_write(command, false); +} + +void hd44780_data(uint8_t data) { + while (hd44780_busy()) + ; + hd44780_write(data, true); +} + +void hd44780_clear(void) { + hd44780_command(HD44780_CMD_CLEAR_DISPLAY); +} + +void hd44780_home(void) { + hd44780_command(HD44780_CMD_RETURN_HOME); +} + +void hd44780_on(bool cursor, bool blink) { + if (cursor) { + if (blink) { + hd44780_command(HD44780_CMD_DISPLAY | HD44780_DISPLAY_ON | HD44780_DISPLAY_CURSOR | HD44780_DISPLAY_BLINK); + } else { + hd44780_command(HD44780_CMD_DISPLAY | HD44780_DISPLAY_ON | HD44780_DISPLAY_CURSOR); + } + } else { + hd44780_command(HD44780_CMD_DISPLAY | HD44780_DISPLAY_ON); + } +} + +void hd44780_off() { + hd44780_command(HD44780_CMD_DISPLAY); +} + +void hd44780_set_cgram_address(uint8_t address) { + hd44780_command(HD44780_CMD_SET_CGRAM_ADDRESS + (address & 0x3F)); +} + +void hd44780_set_ddram_address(uint8_t address) { + hd44780_command(HD44780_CMD_SET_DDRAM_ADDRESS + (address & 0x7F)); +} + +void hd44780_init(bool cursor, bool blink) { + setPinOutput(HD44780_RS_PIN); + setPinOutput(HD44780_RW_PIN); + setPinOutput(HD44780_E_PIN); + + for (int i = 0; i < 4; i++) { + setPinOutput(data_pins[i]); + } + + wait_ms(HD44780_INIT_DELAY_MS); + + // Manually configure for 4-bit mode - can't use hd44780_command() yet + // HD44780U datasheet, Fig. 24 (p46) + writePinHigh(data_pins[0]); // Function set + writePinHigh(data_pins[1]); // DL = 1 + hd44780_latch(); + wait_ms(5); + // Send again + hd44780_latch(); + wait_us(64); + // And again (?) + hd44780_latch(); + wait_us(64); + + writePinLow(data_pins[0]); // DL = 0 + hd44780_latch(); + wait_us(64); + +#if HD44780_DISPLAY_LINES == 1 + hd44780_command(HD44780_CMD_FUNCTION); // 4 bit, 1 line, 5x8 dots +#else + hd44780_command(HD44780_CMD_FUNCTION | HD44780_FUNCTION_2_LINES); // 4 bit, 2 lines, 5x8 dots +#endif + hd44780_on(cursor, blink); + hd44780_clear(); + hd44780_home(); + hd44780_command(HD44780_CMD_ENTRY_MODE | HD44780_ENTRY_MODE_INC); +} + +void hd44780_set_cursor(uint8_t col, uint8_t line) { + register uint8_t address = col; + +#if HD44780_DISPLAY_LINES == 1 + address += HD44780_DDRAM_LINE0_ADDR; +#elif HD44780_DISPLAY_LINES == 2 + if (line == 0) { + address += HD44780_DDRAM_LINE0_ADDR; + } else { + address += HD44780_DDRAM_LINE1_ADDR; + } +#endif + + hd44780_set_ddram_address(address); +} + +void hd44780_define_char(uint8_t index, uint8_t *data) { + hd44780_set_cgram_address((index & 0x7) << 3); + for (uint8_t i = 0; i < 8; i++) { + hd44780_data(data[i]); + } +} + +void hd44780_putc(char c) { + while (hd44780_busy()) + ; + uint8_t current_position = hd44780_read(false); + + if (c == '\n') { + hd44780_set_cursor(0, current_position < HD44780_DDRAM_LINE1_ADDR ? 1 : 0); + } else { +#if defined(HD44780_WRAP_LINES) +# if HD44780_DISPLAY_LINES == 1 + if (current_position == HD44780_DDRAM_LINE0_ADDR + HD44780_DISPLAY_COLS) { + // Go to start of line + hd44780_set_cursor(0, 0); + } +# elif HD44780_DISPLAY_LINES == 2 + if (current_position == HD44780_DDRAM_LINE0_ADDR + HD44780_DISPLAY_COLS) { + // Go to start of second line + hd44780_set_cursor(0, 1); + } else if (current_position == HD44780_DDRAM_LINE1_ADDR + HD44780_DISPLAY_COLS) { + // Go to start of first line + hd44780_set_cursor(0, 0); + } +# endif +#endif + hd44780_data(c); + } +} + +void hd44780_puts(const char *s) { + register char c; + while ((c = *s++)) { + hd44780_putc(c); + } +} + +#if defined(__AVR__) +void hd44780_define_char_P(uint8_t index, const uint8_t *data) { + hd44780_set_cgram_address(index << 3); + for (uint8_t i = 0; i < 8; i++) { + hd44780_data(pgm_read_byte(data++)); + } +} + +void hd44780_puts_P(const char *s) { + register char c; + while ((c = pgm_read_byte(s++))) { + hd44780_putc(c); + } +} +#endif diff --git a/drivers/lcd/hd44780.h b/drivers/lcd/hd44780.h new file mode 100644 index 0000000000..d4b8f9ea76 --- /dev/null +++ b/drivers/lcd/hd44780.h @@ -0,0 +1,220 @@ +/* +Copyright 2022 + +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 . +*/ + +#pragma once + +#include +#include + +/** + * \defgroup hd44780 + * + * HD44780 Character LCD Driver + * \{ + */ + +/* + * HD44780 instructions + * https://www.sparkfun.com/datasheets/LCD/HD44780.pdf + * Table 6 (p24) + */ +// Clear display +#define HD44780_CMD_CLEAR_DISPLAY 0x01 +// Return home +#define HD44780_CMD_RETURN_HOME 0x02 +// Entry mode set +#define HD44780_CMD_ENTRY_MODE 0x04 +#define HD44780_ENTRY_MODE_INC 0x02 // I/D +#define HD44780_ENTRY_MODE_SHIFT 0x01 // S +// Display on/off control +#define HD44780_CMD_DISPLAY 0x08 +#define HD44780_DISPLAY_ON 0x04 // D +#define HD44780_DISPLAY_CURSOR 0x02 // C +#define HD44780_DISPLAY_BLINK 0x01 // B +// Cursor or display shift +#define HD44780_CMD_MOVE 0x10 +#define HD44780_MOVE_DISPLAY 0x08 // S/C +#define HD44780_MOVE_RIGHT 0x04 // R/L +// Function set +#define HD44780_CMD_FUNCTION 0x20 +#define HD44780_FUNCTION_8_BIT 0x10 // DL +#define HD44780_FUNCTION_2_LINES 0x08 // N +#define HD44780_FUNCTION_5X10_DOTS 0x04 // F +// Set CGRAM address +#define HD44780_CMD_SET_CGRAM_ADDRESS 0x40 +// Set DDRAM address +#define HD44780_CMD_SET_DDRAM_ADDRESS 0x80 + +// Bitmask for busy flag when reading +#define HD44780_BUSY_FLAG 0x80 + +/** + * \brief Write a byte to the display. + * + * \param data The byte to send to the display. + * \param isData Whether the byte is an instruction or character data. + */ +void hd44780_write(uint8_t data, bool isData); + +/** + * \brief Read a byte from the display. + * + * \param isData Whether to read the current cursor position, or the character at the cursor. + * + * \return If `isData` is `true`, the returned byte will be the character at the current DDRAM address. Otherwise, it will be the current DDRAM address and the busy flag. + */ +uint8_t hd44780_read(bool isData); + +/** + * \brief Indicates whether the display is currently processing, and cannot accept instructions. + * + * \return `true` if the display is busy. + */ +bool hd44780_busy(void); + +/** + * \brief Send a command to the display. Refer to the datasheet for the valid commands. + * + * This function waits for the display to clear the busy flag before sending the command. + * + * \param command The command to send. + */ +void hd44780_command(uint8_t command); + +/** + * \brief Send a byte of data to the display. + * + * This function waits for the display to clear the busy flag before sending the data. + * + * \param data The byte of data to send. + */ +void hd44780_data(uint8_t data); + +/** + * \brief Clear the display. + * + * This function is called on init. + */ +void hd44780_clear(void); + +/** + * \brief Move the cursor to the home position. + * + * This function is called on init. + */ +void hd44780_home(void); + +/** + * \brief Turn the display on, and/or set the cursor position. + * + * This function is called on init. + * + * \param cursor Whether to show the cursor. + * \param blink Whether to blink the cursor, if shown. + */ +void hd44780_on(bool cursor, bool blink); + +/** + * \brief Turn the display off. + */ +void hd44780_off(void); + +/** + * \brief Set the CGRAM address. + * + * This function is used when defining custom characters. + * + * \param address The CGRAM address to move to, from `0x00` to `0x3F`. + */ +void hd44780_set_cgram_address(uint8_t address); + +/** + * \brief Set the DDRAM address. + * + * This function is used when printing characters to the display, and setting the cursor. + * + * \param address The DDRAM address to move to, from `0x00` to `0x7F`. + */ +void hd44780_set_ddram_address(uint8_t address); + +/** + * \brief Initialize the display. + * + * This function should be called only once, before any of the other functions can be called. + * + * \param cursor Whether to show the cursor. + * \param blink Whether to blink the cursor, if shown. + */ +void hd44780_init(bool cursor, bool blink); + +/** + * \brief Move the cursor to the specified position on the display. + * + * \param col The column number to move to, from 0 to 15 on 16x2 displays. + * \param line The line number to move to, either 0 or 1 on 16x2 displays. + */ +void hd44780_set_cursor(uint8_t col, uint8_t line); + +/** + * \brief Define a custom character. + * + * \param index The index of the custom character to define, from 0 to 7. + * \param data An array of 8 bytes containing the 5-bit row data of the character, where the first byte is the topmost row, and the least significant bit of each byte is the rightmost column. + */ +void hd44780_define_char(uint8_t index, uint8_t *data); + +/** + * \brief Print a character to the display. The newline character will move the cursor to the start of the next line. + * + * The exact character shown may depend on the ROM code of your particular display - refer to the datasheet for the full character set. + * + * \param c The character to print. + */ +void hd44780_putc(char c); + +/** + * \brief Print a string of characters to the display. + * + * \param s The string to print. + */ +void hd44780_puts(const char *s); + +#if defined(__AVR__) || defined(__DOXYGEN__) +/** + * \brief Define a custom character from PROGMEM. + * + * On ARM devices, this function is simply an alias of hd44780_define_char(). + * + * \param index The index of the custom character to define, from 0 to 7. + * \param data A PROGMEM array of 8 bytes containing the 5-bit row data of the character, where the first byte is the topmost row, and the least significant bit of each byte is the rightmost column. + */ +void hd44780_define_char_P(uint8_t index, const uint8_t *data); + +/** + * \brief Print a string of characters from PROGMEM to the display. + * + * On ARM devices, this function is simply an alias of hd44780_puts(). + * + * \param s The PROGMEM string to print. + */ +void hd44780_puts_P(const char *s); +#else +# define hd44780_define_char_P(index, data) hd44780_define_char(index, data) +# define hd44780_puts_P(s) hd44780_puts(s) +#endif + +/** \} */ diff --git a/platforms/avr/drivers/hd44780.c b/platforms/avr/drivers/hd44780.c deleted file mode 100644 index f15d7d0da8..0000000000 --- a/platforms/avr/drivers/hd44780.c +++ /dev/null @@ -1,542 +0,0 @@ -/**************************************************************************** - Title: HD44780U LCD library - Author: Peter Fleury http://tinyurl.com/peterfleury - License: GNU General Public License Version 3 - File: $Id: lcd.c,v 1.15.2.2 2015/01/17 12:16:05 peter Exp $ - Software: AVR-GCC 3.3 - Target: any AVR device, memory mapped mode only for AT90S4414/8515/Mega - - DESCRIPTION - Basic routines for interfacing a HD44780U-based text lcd display - - Originally based on Volker Oth's lcd library, - changed lcd_init(), added additional constants for lcd_command(), - added 4-bit I/O mode, improved and optimized code. - - Library can be operated in memory mapped mode (LCD_IO_MODE=0) or in - 4-bit IO port mode (LCD_IO_MODE=1). 8-bit IO port mode not supported. - - Memory mapped mode compatible with Kanda STK200, but supports also - generation of R/W signal through A8 address line. - - USAGE - See the C include lcd.h file for a description of each function - -*****************************************************************************/ -#include -#include -#include -#include -#include "hd44780.h" - -/* -** constants/macros -*/ -#define DDR(x) (*(&x - 1)) /* address of data direction register of port x */ -#if defined(__AVR_ATmega64__) || defined(__AVR_ATmega128__) -/* on ATmega64/128 PINF is on port 0x00 and not 0x60 */ -# define PIN(x) (&PORTF == &(x) ? _SFR_IO8(0x00) : (*(&x - 2))) -#else -# define PIN(x) (*(&x - 2)) /* address of input register of port x */ -#endif - -#if LCD_IO_MODE -# define lcd_e_delay() _delay_us(LCD_DELAY_ENABLE_PULSE) -# define lcd_e_high() LCD_E_PORT |= _BV(LCD_E_PIN); -# define lcd_e_low() LCD_E_PORT &= ~_BV(LCD_E_PIN); -# define lcd_e_toggle() toggle_e() -# define lcd_rw_high() LCD_RW_PORT |= _BV(LCD_RW_PIN) -# define lcd_rw_low() LCD_RW_PORT &= ~_BV(LCD_RW_PIN) -# define lcd_rs_high() LCD_RS_PORT |= _BV(LCD_RS_PIN) -# define lcd_rs_low() LCD_RS_PORT &= ~_BV(LCD_RS_PIN) -#endif - -#if LCD_IO_MODE -# if LCD_LINES == 1 -# define LCD_FUNCTION_DEFAULT LCD_FUNCTION_4BIT_1LINE -# else -# define LCD_FUNCTION_DEFAULT LCD_FUNCTION_4BIT_2LINES -# endif -#else -# if LCD_LINES == 1 -# define LCD_FUNCTION_DEFAULT LCD_FUNCTION_8BIT_1LINE -# else -# define LCD_FUNCTION_DEFAULT LCD_FUNCTION_8BIT_2LINES -# endif -#endif - -#if LCD_CONTROLLER_KS0073 -# if LCD_LINES == 4 - -# define KS0073_EXTENDED_FUNCTION_REGISTER_ON 0x2C /* |0|010|1100 4-bit mode, extension-bit RE = 1 */ -# define KS0073_EXTENDED_FUNCTION_REGISTER_OFF 0x28 /* |0|010|1000 4-bit mode, extension-bit RE = 0 */ -# define KS0073_4LINES_MODE 0x09 /* |0|000|1001 4 lines mode */ - -# endif -#endif - -/* -** function prototypes -*/ -#if LCD_IO_MODE -static void toggle_e(void); -#endif - -/* -** local functions -*/ - -/************************************************************************* -delay for a minimum of microseconds -the number of loops is calculated at compile-time from MCU clock frequency -*************************************************************************/ -#define delay(us) _delay_us(us) - -#if LCD_IO_MODE -/* toggle Enable Pin to initiate write */ -static void toggle_e(void) { - lcd_e_high(); - lcd_e_delay(); - lcd_e_low(); -} -#endif - -/************************************************************************* -Low-level function to write byte to LCD controller -Input: data byte to write to LCD - rs 1: write data - 0: write instruction -Returns: none -*************************************************************************/ -#if LCD_IO_MODE -static void lcd_write(uint8_t data, uint8_t rs) { - unsigned char dataBits; - - if (rs) { /* write data (RS=1, RW=0) */ - lcd_rs_high(); - } else { /* write instruction (RS=0, RW=0) */ - lcd_rs_low(); - } - lcd_rw_low(); /* RW=0 write mode */ - - if ((&LCD_DATA0_PORT == &LCD_DATA1_PORT) && (&LCD_DATA1_PORT == &LCD_DATA2_PORT) && (&LCD_DATA2_PORT == &LCD_DATA3_PORT) && (LCD_DATA0_PIN == 0) && (LCD_DATA1_PIN == 1) && (LCD_DATA2_PIN == 2) && (LCD_DATA3_PIN == 3)) { - /* configure data pins as output */ - DDR(LCD_DATA0_PORT) |= 0x0F; - - /* output high nibble first */ - dataBits = LCD_DATA0_PORT & 0xF0; - LCD_DATA0_PORT = dataBits | ((data >> 4) & 0x0F); - lcd_e_toggle(); - - /* output low nibble */ - LCD_DATA0_PORT = dataBits | (data & 0x0F); - lcd_e_toggle(); - - /* all data pins high (inactive) */ - LCD_DATA0_PORT = dataBits | 0x0F; - } else { - /* configure data pins as output */ - DDR(LCD_DATA0_PORT) |= _BV(LCD_DATA0_PIN); - DDR(LCD_DATA1_PORT) |= _BV(LCD_DATA1_PIN); - DDR(LCD_DATA2_PORT) |= _BV(LCD_DATA2_PIN); - DDR(LCD_DATA3_PORT) |= _BV(LCD_DATA3_PIN); - - /* output high nibble first */ - LCD_DATA3_PORT &= ~_BV(LCD_DATA3_PIN); - LCD_DATA2_PORT &= ~_BV(LCD_DATA2_PIN); - LCD_DATA1_PORT &= ~_BV(LCD_DATA1_PIN); - LCD_DATA0_PORT &= ~_BV(LCD_DATA0_PIN); - if (data & 0x80) LCD_DATA3_PORT |= _BV(LCD_DATA3_PIN); - if (data & 0x40) LCD_DATA2_PORT |= _BV(LCD_DATA2_PIN); - if (data & 0x20) LCD_DATA1_PORT |= _BV(LCD_DATA1_PIN); - if (data & 0x10) LCD_DATA0_PORT |= _BV(LCD_DATA0_PIN); - lcd_e_toggle(); - - /* output low nibble */ - LCD_DATA3_PORT &= ~_BV(LCD_DATA3_PIN); - LCD_DATA2_PORT &= ~_BV(LCD_DATA2_PIN); - LCD_DATA1_PORT &= ~_BV(LCD_DATA1_PIN); - LCD_DATA0_PORT &= ~_BV(LCD_DATA0_PIN); - if (data & 0x08) LCD_DATA3_PORT |= _BV(LCD_DATA3_PIN); - if (data & 0x04) LCD_DATA2_PORT |= _BV(LCD_DATA2_PIN); - if (data & 0x02) LCD_DATA1_PORT |= _BV(LCD_DATA1_PIN); - if (data & 0x01) LCD_DATA0_PORT |= _BV(LCD_DATA0_PIN); - lcd_e_toggle(); - - /* all data pins high (inactive) */ - LCD_DATA0_PORT |= _BV(LCD_DATA0_PIN); - LCD_DATA1_PORT |= _BV(LCD_DATA1_PIN); - LCD_DATA2_PORT |= _BV(LCD_DATA2_PIN); - LCD_DATA3_PORT |= _BV(LCD_DATA3_PIN); - } -} -#else -# define lcd_write(d, rs) \ - if (rs) \ - *(volatile uint8_t *)(LCD_IO_DATA) = d; \ - else \ - *(volatile uint8_t *)(LCD_IO_FUNCTION) = d; -/* rs==0 -> write instruction to LCD_IO_FUNCTION */ -/* rs==1 -> write data to LCD_IO_DATA */ -#endif - -/************************************************************************* -Low-level function to read byte from LCD controller -Input: rs 1: read data - 0: read busy flag / address counter -Returns: byte read from LCD controller -*************************************************************************/ -#if LCD_IO_MODE -static uint8_t lcd_read(uint8_t rs) { - uint8_t data; - - if (rs) - lcd_rs_high(); /* RS=1: read data */ - else - lcd_rs_low(); /* RS=0: read busy flag */ - lcd_rw_high(); /* RW=1 read mode */ - - if ((&LCD_DATA0_PORT == &LCD_DATA1_PORT) && (&LCD_DATA1_PORT == &LCD_DATA2_PORT) && (&LCD_DATA2_PORT == &LCD_DATA3_PORT) && (LCD_DATA0_PIN == 0) && (LCD_DATA1_PIN == 1) && (LCD_DATA2_PIN == 2) && (LCD_DATA3_PIN == 3)) { - DDR(LCD_DATA0_PORT) &= 0xF0; /* configure data pins as input */ - - lcd_e_high(); - lcd_e_delay(); - data = PIN(LCD_DATA0_PORT) << 4; /* read high nibble first */ - lcd_e_low(); - - lcd_e_delay(); /* Enable 500ns low */ - - lcd_e_high(); - lcd_e_delay(); - data |= PIN(LCD_DATA0_PORT) & 0x0F; /* read low nibble */ - lcd_e_low(); - } else { - /* configure data pins as input */ - DDR(LCD_DATA0_PORT) &= ~_BV(LCD_DATA0_PIN); - DDR(LCD_DATA1_PORT) &= ~_BV(LCD_DATA1_PIN); - DDR(LCD_DATA2_PORT) &= ~_BV(LCD_DATA2_PIN); - DDR(LCD_DATA3_PORT) &= ~_BV(LCD_DATA3_PIN); - - /* read high nibble first */ - lcd_e_high(); - lcd_e_delay(); - data = 0; - if (PIN(LCD_DATA0_PORT) & _BV(LCD_DATA0_PIN)) data |= 0x10; - if (PIN(LCD_DATA1_PORT) & _BV(LCD_DATA1_PIN)) data |= 0x20; - if (PIN(LCD_DATA2_PORT) & _BV(LCD_DATA2_PIN)) data |= 0x40; - if (PIN(LCD_DATA3_PORT) & _BV(LCD_DATA3_PIN)) data |= 0x80; - lcd_e_low(); - - lcd_e_delay(); /* Enable 500ns low */ - - /* read low nibble */ - lcd_e_high(); - lcd_e_delay(); - if (PIN(LCD_DATA0_PORT) & _BV(LCD_DATA0_PIN)) data |= 0x01; - if (PIN(LCD_DATA1_PORT) & _BV(LCD_DATA1_PIN)) data |= 0x02; - if (PIN(LCD_DATA2_PORT) & _BV(LCD_DATA2_PIN)) data |= 0x04; - if (PIN(LCD_DATA3_PORT) & _BV(LCD_DATA3_PIN)) data |= 0x08; - lcd_e_low(); - } - return data; -} -#else -# define lcd_read(rs) (rs) ? *(volatile uint8_t *)(LCD_IO_DATA + LCD_IO_READ) : *(volatile uint8_t *)(LCD_IO_FUNCTION + LCD_IO_READ) -/* rs==0 -> read instruction from LCD_IO_FUNCTION */ -/* rs==1 -> read data from LCD_IO_DATA */ -#endif - -/************************************************************************* -loops while lcd is busy, returns address counter -*************************************************************************/ -static uint8_t lcd_waitbusy(void) - -{ - register uint8_t c; - - /* wait until busy flag is cleared */ - while ((c = lcd_read(0)) & (1 << LCD_BUSY)) { - } - - /* the address counter is updated 4us after the busy flag is cleared */ - delay(LCD_DELAY_BUSY_FLAG); - - /* now read the address counter */ - return (lcd_read(0)); // return address counter - -} /* lcd_waitbusy */ - -/************************************************************************* -Move cursor to the start of next line or to the first line if the cursor -is already on the last line. -*************************************************************************/ -static inline void lcd_newline(uint8_t pos) { - register uint8_t addressCounter; - -#if LCD_LINES == 1 - addressCounter = 0; -#endif -#if LCD_LINES == 2 - if (pos < (LCD_START_LINE2)) - addressCounter = LCD_START_LINE2; - else - addressCounter = LCD_START_LINE1; -#endif -#if LCD_LINES == 4 -# if KS0073_4LINES_MODE - if (pos < LCD_START_LINE2) - addressCounter = LCD_START_LINE2; - else if ((pos >= LCD_START_LINE2) && (pos < LCD_START_LINE3)) - addressCounter = LCD_START_LINE3; - else if ((pos >= LCD_START_LINE3) && (pos < LCD_START_LINE4)) - addressCounter = LCD_START_LINE4; - else - addressCounter = LCD_START_LINE1; -# else - if (pos < LCD_START_LINE3) - addressCounter = LCD_START_LINE2; - else if ((pos >= LCD_START_LINE2) && (pos < LCD_START_LINE4)) - addressCounter = LCD_START_LINE3; - else if ((pos >= LCD_START_LINE3) && (pos < LCD_START_LINE2)) - addressCounter = LCD_START_LINE4; - else - addressCounter = LCD_START_LINE1; -# endif -#endif - lcd_command((1 << LCD_DDRAM) + addressCounter); - -} /* lcd_newline */ - -/* -** PUBLIC FUNCTIONS -*/ - -/************************************************************************* -Send LCD controller instruction command -Input: instruction to send to LCD controller, see HD44780 data sheet -Returns: none -*************************************************************************/ -void lcd_command(uint8_t cmd) { - lcd_waitbusy(); - lcd_write(cmd, 0); -} - -/************************************************************************* -Send data byte to LCD controller -Input: data to send to LCD controller, see HD44780 data sheet -Returns: none -*************************************************************************/ -void lcd_data(uint8_t data) { - lcd_waitbusy(); - lcd_write(data, 1); -} - -/************************************************************************* -Set cursor to specified position -Input: x horizontal position (0: left most position) - y vertical position (0: first line) -Returns: none -*************************************************************************/ -void lcd_gotoxy(uint8_t x, uint8_t y) { -#if LCD_LINES == 1 - lcd_command((1 << LCD_DDRAM) + LCD_START_LINE1 + x); -#endif -#if LCD_LINES == 2 - if (y == 0) - lcd_command((1 << LCD_DDRAM) + LCD_START_LINE1 + x); - else - lcd_command((1 << LCD_DDRAM) + LCD_START_LINE2 + x); -#endif -#if LCD_LINES == 4 - if (y == 0) - lcd_command((1 << LCD_DDRAM) + LCD_START_LINE1 + x); - else if (y == 1) - lcd_command((1 << LCD_DDRAM) + LCD_START_LINE2 + x); - else if (y == 2) - lcd_command((1 << LCD_DDRAM) + LCD_START_LINE3 + x); - else /* y==3 */ - lcd_command((1 << LCD_DDRAM) + LCD_START_LINE4 + x); -#endif - -} /* lcd_gotoxy */ - -/************************************************************************* -*************************************************************************/ -int lcd_getxy(void) { - return lcd_waitbusy(); -} - -/************************************************************************* -Clear display and set cursor to home position -*************************************************************************/ -void lcd_clrscr(void) { - lcd_command(1 << LCD_CLR); -} - -/************************************************************************* -Set cursor to home position -*************************************************************************/ -void lcd_home(void) { - lcd_command(1 << LCD_HOME); -} - -/************************************************************************* -Display character at current cursor position -Input: character to be displayed -Returns: none -*************************************************************************/ -void lcd_putc(char c) { - uint8_t pos; - - pos = lcd_waitbusy(); // read busy-flag and address counter - if (c == '\n') { - lcd_newline(pos); - } else { -#if LCD_WRAP_LINES == 1 -# if LCD_LINES == 1 - if (pos == LCD_START_LINE1 + LCD_DISP_LENGTH) { - lcd_write((1 << LCD_DDRAM) + LCD_START_LINE1, 0); - } -# elif LCD_LINES == 2 - if (pos == LCD_START_LINE1 + LCD_DISP_LENGTH) { - lcd_write((1 << LCD_DDRAM) + LCD_START_LINE2, 0); - } else if (pos == LCD_START_LINE2 + LCD_DISP_LENGTH) { - lcd_write((1 << LCD_DDRAM) + LCD_START_LINE1, 0); - } -# elif LCD_LINES == 4 - if (pos == LCD_START_LINE1 + LCD_DISP_LENGTH) { - lcd_write((1 << LCD_DDRAM) + LCD_START_LINE2, 0); - } else if (pos == LCD_START_LINE2 + LCD_DISP_LENGTH) { - lcd_write((1 << LCD_DDRAM) + LCD_START_LINE3, 0); - } else if (pos == LCD_START_LINE3 + LCD_DISP_LENGTH) { - lcd_write((1 << LCD_DDRAM) + LCD_START_LINE4, 0); - } else if (pos == LCD_START_LINE4 + LCD_DISP_LENGTH) { - lcd_write((1 << LCD_DDRAM) + LCD_START_LINE1, 0); - } -# endif - lcd_waitbusy(); -#endif - lcd_write(c, 1); - } - -} /* lcd_putc */ - -/************************************************************************* -Display string without auto linefeed -Input: string to be displayed -Returns: none -*************************************************************************/ -void lcd_puts(const char *s) -/* print string on lcd (no auto linefeed) */ -{ - register char c; - - while ((c = *s++)) { - lcd_putc(c); - } - -} /* lcd_puts */ - -/************************************************************************* -Display string from program memory without auto linefeed -Input: string from program memory be be displayed -Returns: none -*************************************************************************/ -void lcd_puts_p(const char *progmem_s) -/* print string from program memory on lcd (no auto linefeed) */ -{ - register char c; - - while ((c = pgm_read_byte(progmem_s++))) { - lcd_putc(c); - } - -} /* lcd_puts_p */ - -/************************************************************************* -Initialize display and select type of cursor -Input: dispAttr LCD_DISP_OFF display off - LCD_DISP_ON display on, cursor off - LCD_DISP_ON_CURSOR display on, cursor on - LCD_DISP_CURSOR_BLINK display on, cursor on flashing -Returns: none -*************************************************************************/ -void lcd_init(uint8_t dispAttr) { -#if LCD_IO_MODE - /* - * Initialize LCD to 4 bit I/O mode - */ - - if ((&LCD_DATA0_PORT == &LCD_DATA1_PORT) && (&LCD_DATA1_PORT == &LCD_DATA2_PORT) && (&LCD_DATA2_PORT == &LCD_DATA3_PORT) && (&LCD_RS_PORT == &LCD_DATA0_PORT) && (&LCD_RW_PORT == &LCD_DATA0_PORT) && (&LCD_E_PORT == &LCD_DATA0_PORT) && (LCD_DATA0_PIN == 0) && (LCD_DATA1_PIN == 1) && (LCD_DATA2_PIN == 2) && (LCD_DATA3_PIN == 3) && (LCD_RS_PIN == 4) && (LCD_RW_PIN == 5) && (LCD_E_PIN == 6)) { - /* configure all port bits as output (all LCD lines on same port) */ - DDR(LCD_DATA0_PORT) |= 0x7F; - } else if ((&LCD_DATA0_PORT == &LCD_DATA1_PORT) && (&LCD_DATA1_PORT == &LCD_DATA2_PORT) && (&LCD_DATA2_PORT == &LCD_DATA3_PORT) && (LCD_DATA0_PIN == 0) && (LCD_DATA1_PIN == 1) && (LCD_DATA2_PIN == 2) && (LCD_DATA3_PIN == 3)) { - /* configure all port bits as output (all LCD data lines on same port, but control lines on different ports) */ - DDR(LCD_DATA0_PORT) |= 0x0F; - DDR(LCD_RS_PORT) |= _BV(LCD_RS_PIN); - DDR(LCD_RW_PORT) |= _BV(LCD_RW_PIN); - DDR(LCD_E_PORT) |= _BV(LCD_E_PIN); - } else { - /* configure all port bits as output (LCD data and control lines on different ports */ - DDR(LCD_RS_PORT) |= _BV(LCD_RS_PIN); - DDR(LCD_RW_PORT) |= _BV(LCD_RW_PIN); - DDR(LCD_E_PORT) |= _BV(LCD_E_PIN); - DDR(LCD_DATA0_PORT) |= _BV(LCD_DATA0_PIN); - DDR(LCD_DATA1_PORT) |= _BV(LCD_DATA1_PIN); - DDR(LCD_DATA2_PORT) |= _BV(LCD_DATA2_PIN); - DDR(LCD_DATA3_PORT) |= _BV(LCD_DATA3_PIN); - } - delay(LCD_DELAY_BOOTUP); /* wait 16ms or more after power-on */ - - /* initial write to lcd is 8bit */ - LCD_DATA1_PORT |= _BV(LCD_DATA1_PIN); // LCD_FUNCTION>>4; - LCD_DATA0_PORT |= _BV(LCD_DATA0_PIN); // LCD_FUNCTION_8BIT>>4; - lcd_e_toggle(); - delay(LCD_DELAY_INIT); /* delay, busy flag can't be checked here */ - - /* repeat last command */ - lcd_e_toggle(); - delay(LCD_DELAY_INIT_REP); /* delay, busy flag can't be checked here */ - - /* repeat last command a third time */ - lcd_e_toggle(); - delay(LCD_DELAY_INIT_REP); /* delay, busy flag can't be checked here */ - - /* now configure for 4bit mode */ - LCD_DATA0_PORT &= ~_BV(LCD_DATA0_PIN); // LCD_FUNCTION_4BIT_1LINE>>4 - lcd_e_toggle(); - delay(LCD_DELAY_INIT_4BIT); /* some displays need this additional delay */ - - /* from now the LCD only accepts 4 bit I/O, we can use lcd_command() */ -#else - /* - * Initialize LCD to 8 bit memory mapped mode - */ - - /* enable external SRAM (memory mapped lcd) and one wait state */ - MCUCR = _BV(SRE) | _BV(SRW); - - /* reset LCD */ - delay(LCD_DELAY_BOOTUP); /* wait 16ms after power-on */ - lcd_write(LCD_FUNCTION_8BIT_1LINE, 0); /* function set: 8bit interface */ - delay(LCD_DELAY_INIT); /* wait 5ms */ - lcd_write(LCD_FUNCTION_8BIT_1LINE, 0); /* function set: 8bit interface */ - delay(LCD_DELAY_INIT_REP); /* wait 64us */ - lcd_write(LCD_FUNCTION_8BIT_1LINE, 0); /* function set: 8bit interface */ - delay(LCD_DELAY_INIT_REP); /* wait 64us */ -#endif - -#if KS0073_4LINES_MODE - /* Display with KS0073 controller requires special commands for enabling 4 line mode */ - lcd_command(KS0073_EXTENDED_FUNCTION_REGISTER_ON); - lcd_command(KS0073_4LINES_MODE); - lcd_command(KS0073_EXTENDED_FUNCTION_REGISTER_OFF); -#else - lcd_command(LCD_FUNCTION_DEFAULT); /* function set: display lines */ -#endif - lcd_command(LCD_DISP_OFF); /* display off */ - lcd_clrscr(); /* display clear */ - lcd_command(LCD_MODE_DEFAULT); /* set entry mode */ - lcd_command(dispAttr); /* display/cursor control */ - -} /* lcd_init */ diff --git a/platforms/avr/drivers/hd44780.h b/platforms/avr/drivers/hd44780.h deleted file mode 100644 index 08e60f8a44..0000000000 --- a/platforms/avr/drivers/hd44780.h +++ /dev/null @@ -1,348 +0,0 @@ -/************************************************************************* - Title : C include file for the HD44780U LCD library (lcd.c) - Author: Peter Fleury http://tinyurl.com/peterfleury - License: GNU General Public License Version 3 - File: $Id: lcd.h,v 1.14.2.4 2015/01/20 17:16:07 peter Exp $ - Software: AVR-GCC 4.x - Hardware: any AVR device, memory mapped mode only for AVR with - memory mapped interface (AT90S8515/ATmega8515/ATmega128) -***************************************************************************/ - -/** - @mainpage - Collection of libraries for AVR-GCC - @author Peter Fleury pfleury@gmx.ch http://tinyurl.com/peterfleury - @copyright (C) 2015 Peter Fleury, GNU General Public License Version 3 - - @file - @defgroup pfleury_lcd LCD library - @code #include @endcode - - @brief Basic routines for interfacing a HD44780U-based character LCD display - - LCD character displays can be found in many devices, like espresso machines, laser printers. - The Hitachi HD44780 controller and its compatible controllers like Samsung KS0066U have become an industry standard for these types of displays. - - This library allows easy interfacing with a HD44780 compatible display and can be - operated in memory mapped mode (LCD_IO_MODE defined as 0 in the include file lcd.h.) or in - 4-bit IO port mode (LCD_IO_MODE defined as 1). 8-bit IO port mode is not supported. - - Memory mapped mode is compatible with old Kanda STK200 starter kit, but also supports - generation of R/W signal through A8 address line. - - @see The chapter Interfacing a HD44780 Based LCD to an AVR - on my home page, which shows example circuits how to connect an LCD to an AVR controller. - - @author Peter Fleury pfleury@gmx.ch http://tinyurl.com/peterfleury - - @version 2.0 - - @copyright (C) 2015 Peter Fleury, GNU General Public License Version 3 - -*/ - -#pragma once - -#include -#include - -#if (__GNUC__ * 100 + __GNUC_MINOR__) < 405 -# error "This library requires AVR-GCC 4.5 or later, update to newer AVR-GCC compiler !" -#endif - -/**@{*/ - -/* - * LCD and target specific definitions below can be defined in a separate include file with name lcd_definitions.h instead modifying this file - * by adding -D_LCD_DEFINITIONS_FILE to the CDEFS section in the Makefile - * All definitions added to the file lcd_definitions.h will override the default definitions from lcd.h - */ -#ifdef _LCD_DEFINITIONS_FILE -# include "lcd_definitions.h" -#endif - -/** - * @name Definition for LCD controller type - * Use 0 for HD44780 controller, change to 1 for displays with KS0073 controller. - */ -#ifndef LCD_CONTROLLER_KS0073 -# define LCD_CONTROLLER_KS0073 0 /**< Use 0 for HD44780 controller, 1 for KS0073 controller */ -#endif - -/** - * @name Definitions for Display Size - * Change these definitions to adapt setting to your display - * - * These definitions can be defined in a separate include file \b lcd_definitions.h instead modifying this file by - * adding -D_LCD_DEFINITIONS_FILE to the CDEFS section in the Makefile. - * All definitions added to the file lcd_definitions.h will override the default definitions from lcd.h - * - */ -#ifndef LCD_LINES -# define LCD_LINES 2 /**< number of visible lines of the display */ -#endif -#ifndef LCD_DISP_LENGTH -# define LCD_DISP_LENGTH 16 /**< visibles characters per line of the display */ -#endif -#ifndef LCD_LINE_LENGTH -# define LCD_LINE_LENGTH 0x40 /**< internal line length of the display */ -#endif -#ifndef LCD_START_LINE1 -# define LCD_START_LINE1 0x00 /**< DDRAM address of first char of line 1 */ -#endif -#ifndef LCD_START_LINE2 -# define LCD_START_LINE2 0x40 /**< DDRAM address of first char of line 2 */ -#endif -#ifndef LCD_START_LINE3 -# define LCD_START_LINE3 0x14 /**< DDRAM address of first char of line 3 */ -#endif -#ifndef LCD_START_LINE4 -# define LCD_START_LINE4 0x54 /**< DDRAM address of first char of line 4 */ -#endif -#ifndef LCD_WRAP_LINES -# define LCD_WRAP_LINES 0 /**< 0: no wrap, 1: wrap at end of visibile line */ -#endif - -/** - * @name Definitions for 4-bit IO mode - * - * The four LCD data lines and the three control lines RS, RW, E can be on the - * same port or on different ports. - * Change LCD_RS_PORT, LCD_RW_PORT, LCD_E_PORT if you want the control lines on - * different ports. - * - * Normally the four data lines should be mapped to bit 0..3 on one port, but it - * is possible to connect these data lines in different order or even on different - * ports by adapting the LCD_DATAx_PORT and LCD_DATAx_PIN definitions. - * - * Adjust these definitions to your target.\n - * These definitions can be defined in a separate include file \b lcd_definitions.h instead modifying this file by - * adding \b -D_LCD_DEFINITIONS_FILE to the \b CDEFS section in the Makefile. - * All definitions added to the file lcd_definitions.h will override the default definitions from lcd.h - * - */ -#define LCD_IO_MODE 1 /**< 0: memory mapped mode, 1: IO port mode */ - -#if LCD_IO_MODE - -# ifndef LCD_PORT -# define LCD_PORT PORTA /**< port for the LCD lines */ -# endif -# ifndef LCD_DATA0_PORT -# define LCD_DATA0_PORT LCD_PORT /**< port for 4bit data bit 0 */ -# endif -# ifndef LCD_DATA1_PORT -# define LCD_DATA1_PORT LCD_PORT /**< port for 4bit data bit 1 */ -# endif -# ifndef LCD_DATA2_PORT -# define LCD_DATA2_PORT LCD_PORT /**< port for 4bit data bit 2 */ -# endif -# ifndef LCD_DATA3_PORT -# define LCD_DATA3_PORT LCD_PORT /**< port for 4bit data bit 3 */ -# endif -# ifndef LCD_DATA0_PIN -# define LCD_DATA0_PIN 4 /**< pin for 4bit data bit 0 */ -# endif -# ifndef LCD_DATA1_PIN -# define LCD_DATA1_PIN 5 /**< pin for 4bit data bit 1 */ -# endif -# ifndef LCD_DATA2_PIN -# define LCD_DATA2_PIN 6 /**< pin for 4bit data bit 2 */ -# endif -# ifndef LCD_DATA3_PIN -# define LCD_DATA3_PIN 7 /**< pin for 4bit data bit 3 */ -# endif -# ifndef LCD_RS_PORT -# define LCD_RS_PORT LCD_PORT /**< port for RS line */ -# endif -# ifndef LCD_RS_PIN -# define LCD_RS_PIN 3 /**< pin for RS line */ -# endif -# ifndef LCD_RW_PORT -# define LCD_RW_PORT LCD_PORT /**< port for RW line */ -# endif -# ifndef LCD_RW_PIN -# define LCD_RW_PIN 2 /**< pin for RW line */ -# endif -# ifndef LCD_E_PORT -# define LCD_E_PORT LCD_PORT /**< port for Enable line */ -# endif -# ifndef LCD_E_PIN -# define LCD_E_PIN 1 /**< pin for Enable line */ -# endif - -#elif defined(__AVR_AT90S4414__) || defined(__AVR_AT90S8515__) || defined(__AVR_ATmega64__) || defined(__AVR_ATmega8515__) || defined(__AVR_ATmega103__) || defined(__AVR_ATmega128__) || defined(__AVR_ATmega161__) || defined(__AVR_ATmega162__) -/* - * memory mapped mode is only supported when the device has an external data memory interface - */ -# define LCD_IO_DATA 0xC000 /* A15=E=1, A14=RS=1 */ -# define LCD_IO_FUNCTION 0x8000 /* A15=E=1, A14=RS=0 */ -# define LCD_IO_READ 0x0100 /* A8 =R/W=1 (R/W: 1=Read, 0=Write */ - -#else -# error "external data memory interface not available for this device, use 4-bit IO port mode" - -#endif - -/** - * @name Definitions of delays - * Used to calculate delay timers. - * Adapt the F_CPU define in the Makefile to the clock frequency in Hz of your target - * - * These delay times can be adjusted, if some displays require different delays.\n - * These definitions can be defined in a separate include file \b lcd_definitions.h instead modifying this file by - * adding \b -D_LCD_DEFINITIONS_FILE to the \b CDEFS section in the Makefile. - * All definitions added to the file lcd_definitions.h will override the default definitions from lcd.h - */ -#ifndef LCD_DELAY_BOOTUP -# define LCD_DELAY_BOOTUP 16000 /**< delay in micro seconds after power-on */ -#endif -#ifndef LCD_DELAY_INIT -# define LCD_DELAY_INIT 5000 /**< delay in micro seconds after initialization command sent */ -#endif -#ifndef LCD_DELAY_INIT_REP -# define LCD_DELAY_INIT_REP 64 /**< delay in micro seconds after initialization command repeated */ -#endif -#ifndef LCD_DELAY_INIT_4BIT -# define LCD_DELAY_INIT_4BIT 64 /**< delay in micro seconds after setting 4-bit mode */ -#endif -#ifndef LCD_DELAY_BUSY_FLAG -# define LCD_DELAY_BUSY_FLAG 4 /**< time in micro seconds the address counter is updated after busy flag is cleared */ -#endif -#ifndef LCD_DELAY_ENABLE_PULSE -# define LCD_DELAY_ENABLE_PULSE 1 /**< enable signal pulse width in micro seconds */ -#endif - -/** - * @name Definitions for LCD command instructions - * The constants define the various LCD controller instructions which can be passed to the - * function lcd_command(), see HD44780 data sheet for a complete description. - */ - -/* instruction register bit positions, see HD44780U data sheet */ -#define LCD_CLR 0 /* DB0: clear display */ -#define LCD_HOME 1 /* DB1: return to home position */ -#define LCD_ENTRY_MODE 2 /* DB2: set entry mode */ -#define LCD_ENTRY_INC 1 /* DB1: 1=increment, 0=decrement */ -#define LCD_ENTRY_SHIFT 0 /* DB2: 1=display shift on */ -#define LCD_ON 3 /* DB3: turn lcd/cursor on */ -#define LCD_ON_DISPLAY 2 /* DB2: turn display on */ -#define LCD_ON_CURSOR 1 /* DB1: turn cursor on */ -#define LCD_ON_BLINK 0 /* DB0: blinking cursor ? */ -#define LCD_MOVE 4 /* DB4: move cursor/display */ -#define LCD_MOVE_DISP 3 /* DB3: move display (0-> cursor) ? */ -#define LCD_MOVE_RIGHT 2 /* DB2: move right (0-> left) ? */ -#define LCD_FUNCTION 5 /* DB5: function set */ -#define LCD_FUNCTION_8BIT 4 /* DB4: set 8BIT mode (0->4BIT mode) */ -#define LCD_FUNCTION_2LINES 3 /* DB3: two lines (0->one line) */ -#define LCD_FUNCTION_10DOTS 2 /* DB2: 5x10 font (0->5x7 font) */ -#define LCD_CGRAM 6 /* DB6: set CG RAM address */ -#define LCD_DDRAM 7 /* DB7: set DD RAM address */ -#define LCD_BUSY 7 /* DB7: LCD is busy */ - -/* set entry mode: display shift on/off, dec/inc cursor move direction */ -#define LCD_ENTRY_DEC 0x04 /* display shift off, dec cursor move dir */ -#define LCD_ENTRY_DEC_SHIFT 0x05 /* display shift on, dec cursor move dir */ -#define LCD_ENTRY_INC_ 0x06 /* display shift off, inc cursor move dir */ -#define LCD_ENTRY_INC_SHIFT 0x07 /* display shift on, inc cursor move dir */ - -/* display on/off, cursor on/off, blinking char at cursor position */ -#define LCD_DISP_OFF 0x08 /* display off */ -#define LCD_DISP_ON 0x0C /* display on, cursor off */ -#define LCD_DISP_ON_BLINK 0x0D /* display on, cursor off, blink char */ -#define LCD_DISP_ON_CURSOR 0x0E /* display on, cursor on */ -#define LCD_DISP_ON_CURSOR_BLINK 0x0F /* display on, cursor on, blink char */ - -/* move cursor/shift display */ -#define LCD_MOVE_CURSOR_LEFT 0x10 /* move cursor left (decrement) */ -#define LCD_MOVE_CURSOR_RIGHT 0x14 /* move cursor right (increment) */ -#define LCD_MOVE_DISP_LEFT 0x18 /* shift display left */ -#define LCD_MOVE_DISP_RIGHT 0x1C /* shift display right */ - -/* function set: set interface data length and number of display lines */ -#define LCD_FUNCTION_4BIT_1LINE 0x20 /* 4-bit interface, single line, 5x7 dots */ -#define LCD_FUNCTION_4BIT_2LINES 0x28 /* 4-bit interface, dual line, 5x7 dots */ -#define LCD_FUNCTION_8BIT_1LINE 0x30 /* 8-bit interface, single line, 5x7 dots */ -#define LCD_FUNCTION_8BIT_2LINES 0x38 /* 8-bit interface, dual line, 5x7 dots */ - -#define LCD_MODE_DEFAULT ((1 << LCD_ENTRY_MODE) | (1 << LCD_ENTRY_INC)) - -/** - * @name Functions - */ - -/** - @brief Initialize display and select type of cursor - @param dispAttr \b LCD_DISP_OFF display off\n - \b LCD_DISP_ON display on, cursor off\n - \b LCD_DISP_ON_CURSOR display on, cursor on\n - \b LCD_DISP_ON_CURSOR_BLINK display on, cursor on flashing - @return none -*/ -extern void lcd_init(uint8_t dispAttr); - -/** - @brief Clear display and set cursor to home position - @return none -*/ -extern void lcd_clrscr(void); - -/** - @brief Set cursor to home position - @return none -*/ -extern void lcd_home(void); - -/** - @brief Set cursor to specified position - - @param x horizontal position\n (0: left most position) - @param y vertical position\n (0: first line) - @return none -*/ -extern void lcd_gotoxy(uint8_t x, uint8_t y); - -/** - @brief Display character at current cursor position - @param c character to be displayed - @return none -*/ -extern void lcd_putc(char c); - -/** - @brief Display string without auto linefeed - @param s string to be displayed - @return none -*/ -extern void lcd_puts(const char *s); - -/** - @brief Display string from program memory without auto linefeed - @param progmem_s string from program memory be be displayed - @return none - @see lcd_puts_P -*/ -extern void lcd_puts_p(const char *progmem_s); - -/** - @brief Send LCD controller instruction command - @param cmd instruction to send to LCD controller, see HD44780 data sheet - @return none -*/ -extern void lcd_command(uint8_t cmd); - -/** - @brief Send data byte to LCD controller - - Similar to lcd_putc(), but without interpreting LF - @param data byte to send to LCD controller, see HD44780 data sheet - @return none -*/ -extern void lcd_data(uint8_t data); - -/** - @brief macros for automatically storing string constant in program memory -*/ -#define lcd_puts_P(__s) lcd_puts_p(PSTR(__s)) - -/**@}*/ -- cgit v1.2.3 From 1f2b1dedccdf21b629c45ece80b4ca32f6653296 Mon Sep 17 00:00:00 2001 From: Nick Brassel Date: Wed, 13 Apr 2022 18:00:18 +1000 Subject: Quantum Painter (#10174) * Install dependencies before executing unit tests. * Split out UTF-8 decoder. * Fixup python formatting rules. * Add documentation for QGF/QFF and the RLE format used. * Add CLI commands for converting images and fonts. * Add stub rules.mk for QP. * Add stream type. * Add base driver and comms interfaces. * Add support for SPI, SPI+D/C comms drivers. * Include when enabled. * Add base support for SPI+D/C+RST panels, as well as concrete implementation of ST7789. * Add support for GC9A01. * Add support for ILI9341. * Add support for ILI9163. * Add support for SSD1351. * Implement qp_setpixel, including pixdata buffer management. * Implement qp_line. * Implement qp_rect. * Implement qp_circle. * Implement qp_ellipse. * Implement palette interpolation. * Allow for streams to work with either flash or RAM. * Image loading. * Font loading. * QGF palette loading. * Progressive decoder of pixel data supporting Raw+RLE, 1-,2-,4-,8-bpp monochrome and palette-based images. * Image drawing. * Animations. * Font rendering. * Check against 256 colours, dump out the loaded palette if debugging enabled. * Fix build. * AVR is not the intended audience. * `qmk format-c` * Generation fix. * First batch of docs. * More docs and examples. * Review comments. * Public API documentation. --- .github/workflows/unit_test.yml | 2 + builddefs/common_features.mk | 8 +- docs/_summary.md | 1 + docs/cli_commands.md | 12 + docs/quantum_painter.md | 705 +++++++++++++++++++++++ docs/quantum_painter_qff.md | 103 ++++ docs/quantum_painter_qgf.md | 178 ++++++ docs/quantum_painter_rle.md | 29 + drivers/painter/comms/qp_comms_spi.c | 137 +++++ drivers/painter/comms/qp_comms_spi.h | 51 ++ drivers/painter/gc9a01/qp_gc9a01.c | 150 +++++ drivers/painter/gc9a01/qp_gc9a01.h | 37 ++ drivers/painter/gc9a01/qp_gc9a01_opcodes.h | 78 +++ drivers/painter/ili9xxx/qp_ili9163.c | 121 ++++ drivers/painter/ili9xxx/qp_ili9163.h | 37 ++ drivers/painter/ili9xxx/qp_ili9341.c | 128 ++++ drivers/painter/ili9xxx/qp_ili9341.h | 37 ++ drivers/painter/ili9xxx/qp_ili9xxx_opcodes.h | 100 ++++ drivers/painter/ssd1351/qp_ssd1351.c | 125 ++++ drivers/painter/ssd1351/qp_ssd1351.h | 37 ++ drivers/painter/ssd1351/qp_ssd1351_opcodes.h | 48 ++ drivers/painter/st77xx/qp_st7789.c | 144 +++++ drivers/painter/st77xx/qp_st7789.h | 44 ++ drivers/painter/st77xx/qp_st7789_opcodes.h | 64 ++ drivers/painter/st77xx/qp_st77xx_opcodes.h | 51 ++ drivers/painter/tft_panel/qp_tft_panel.c | 130 +++++ drivers/painter/tft_panel/qp_tft_panel.h | 67 +++ lib/python/qmk/cli/__init__.py | 4 +- lib/python/qmk/cli/painter/__init__.py | 2 + lib/python/qmk/cli/painter/convert_graphics.py | 86 +++ lib/python/qmk/cli/painter/make_font.py | 87 +++ lib/python/qmk/painter.py | 268 +++++++++ lib/python/qmk/painter_qff.py | 401 +++++++++++++ lib/python/qmk/painter_qgf.py | 408 +++++++++++++ quantum/main.c | 11 +- quantum/painter/qff.c | 137 +++++ quantum/painter/qff.h | 88 +++ quantum/painter/qgf.c | 292 ++++++++++ quantum/painter/qgf.h | 136 +++++ quantum/painter/qp.c | 228 ++++++++ quantum/painter/qp.h | 453 +++++++++++++++ quantum/painter/qp_comms.c | 72 +++ quantum/painter/qp_comms.h | 25 + quantum/painter/qp_draw.h | 85 +++ quantum/painter/qp_draw_circle.c | 172 ++++++ quantum/painter/qp_draw_codec.c | 142 +++++ quantum/painter/qp_draw_core.c | 294 ++++++++++ quantum/painter/qp_draw_ellipse.c | 116 ++++ quantum/painter/qp_draw_image.c | 382 ++++++++++++ quantum/painter/qp_draw_text.c | 444 ++++++++++++++ quantum/painter/qp_internal.h | 33 ++ quantum/painter/qp_internal_driver.h | 82 +++ quantum/painter/qp_internal_formats.h | 49 ++ quantum/painter/qp_stream.c | 171 ++++++ quantum/painter/qp_stream.h | 82 +++ quantum/painter/rules.mk | 116 ++++ quantum/process_keycode/process_unicode_common.c | 30 +- quantum/quantum.h | 4 + quantum/utf8.c | 46 ++ quantum/utf8.h | 21 + requirements.txt | 1 + setup.cfg | 4 + 62 files changed, 7561 insertions(+), 35 deletions(-) create mode 100644 docs/quantum_painter.md create mode 100644 docs/quantum_painter_qff.md create mode 100644 docs/quantum_painter_qgf.md create mode 100644 docs/quantum_painter_rle.md create mode 100644 drivers/painter/comms/qp_comms_spi.c create mode 100644 drivers/painter/comms/qp_comms_spi.h create mode 100644 drivers/painter/gc9a01/qp_gc9a01.c create mode 100644 drivers/painter/gc9a01/qp_gc9a01.h create mode 100644 drivers/painter/gc9a01/qp_gc9a01_opcodes.h create mode 100644 drivers/painter/ili9xxx/qp_ili9163.c create mode 100644 drivers/painter/ili9xxx/qp_ili9163.h create mode 100644 drivers/painter/ili9xxx/qp_ili9341.c create mode 100644 drivers/painter/ili9xxx/qp_ili9341.h create mode 100644 drivers/painter/ili9xxx/qp_ili9xxx_opcodes.h create mode 100644 drivers/painter/ssd1351/qp_ssd1351.c create mode 100644 drivers/painter/ssd1351/qp_ssd1351.h create mode 100644 drivers/painter/ssd1351/qp_ssd1351_opcodes.h create mode 100644 drivers/painter/st77xx/qp_st7789.c create mode 100644 drivers/painter/st77xx/qp_st7789.h create mode 100644 drivers/painter/st77xx/qp_st7789_opcodes.h create mode 100644 drivers/painter/st77xx/qp_st77xx_opcodes.h create mode 100644 drivers/painter/tft_panel/qp_tft_panel.c create mode 100644 drivers/painter/tft_panel/qp_tft_panel.h create mode 100644 lib/python/qmk/cli/painter/__init__.py create mode 100644 lib/python/qmk/cli/painter/convert_graphics.py create mode 100644 lib/python/qmk/cli/painter/make_font.py create mode 100644 lib/python/qmk/painter.py create mode 100644 lib/python/qmk/painter_qff.py create mode 100644 lib/python/qmk/painter_qgf.py create mode 100644 quantum/painter/qff.c create mode 100644 quantum/painter/qff.h create mode 100644 quantum/painter/qgf.c create mode 100644 quantum/painter/qgf.h create mode 100644 quantum/painter/qp.c create mode 100644 quantum/painter/qp.h create mode 100644 quantum/painter/qp_comms.c create mode 100644 quantum/painter/qp_comms.h create mode 100644 quantum/painter/qp_draw.h create mode 100644 quantum/painter/qp_draw_circle.c create mode 100644 quantum/painter/qp_draw_codec.c create mode 100644 quantum/painter/qp_draw_core.c create mode 100644 quantum/painter/qp_draw_ellipse.c create mode 100644 quantum/painter/qp_draw_image.c create mode 100644 quantum/painter/qp_draw_text.c create mode 100644 quantum/painter/qp_internal.h create mode 100644 quantum/painter/qp_internal_driver.h create mode 100644 quantum/painter/qp_internal_formats.h create mode 100644 quantum/painter/qp_stream.c create mode 100644 quantum/painter/qp_stream.h create mode 100644 quantum/painter/rules.mk create mode 100644 quantum/utf8.c create mode 100644 quantum/utf8.h (limited to 'docs/_summary.md') diff --git a/.github/workflows/unit_test.yml b/.github/workflows/unit_test.yml index 26bcb2f511..726ce19f0c 100644 --- a/.github/workflows/unit_test.yml +++ b/.github/workflows/unit_test.yml @@ -26,5 +26,7 @@ jobs: - uses: actions/checkout@v2 with: submodules: recursive + - name: Install dependencies + run: pip3 install -r requirements-dev.txt - name: Run tests run: make test:all diff --git a/builddefs/common_features.mk b/builddefs/common_features.mk index a1793f91a5..c976b8296d 100644 --- a/builddefs/common_features.mk +++ b/builddefs/common_features.mk @@ -149,6 +149,11 @@ ifeq ($(strip $(POINTING_DEVICE_ENABLE)), yes) endif endif +QUANTUM_PAINTER_ENABLE ?= no +ifeq ($(strip $(QUANTUM_PAINTER_ENABLE)), yes) + include $(QUANTUM_DIR)/painter/rules.mk +endif + VALID_EEPROM_DRIVER_TYPES := vendor custom transient i2c spi EEPROM_DRIVER ?= vendor ifeq ($(filter $(EEPROM_DRIVER),$(VALID_EEPROM_DRIVER_TYPES)),) @@ -696,7 +701,8 @@ endif ifeq ($(strip $(UNICODE_COMMON)), yes) OPT_DEFS += -DUNICODE_COMMON_ENABLE - SRC += $(QUANTUM_DIR)/process_keycode/process_unicode_common.c + SRC += $(QUANTUM_DIR)/process_keycode/process_unicode_common.c \ + $(QUANTUM_DIR)/utf8.c endif MAGIC_ENABLE ?= yes diff --git a/docs/_summary.md b/docs/_summary.md index 249bfcd9ed..786685eba4 100644 --- a/docs/_summary.md +++ b/docs/_summary.md @@ -94,6 +94,7 @@ * Hardware Features * Displays + * [Quantum Painter](quantum_painter.md) * [HD44780 LCD Driver](feature_hd44780.md) * [ST7565 LCD Driver](feature_st7565.md) * [OLED Driver](feature_oled_driver.md) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 463abcef12..a380d3eb2f 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -515,3 +515,15 @@ Run single test: qmk pytest -t qmk.tests.test_cli_commands.test_c2json qmk pytest -t qmk.tests.test_qmk_path + +## `qmk painter-convert-graphics` + +This command converts images to a format usable by QMK, i.e. the QGF File Format. See the [Quantum Painter](quantum_painter.md?id=quantum-painter-cli) documentation for more information on this command. + +## `qmk painter-make-font-image` + +This command converts a TTF font to an intermediate format for editing, before converting to the QFF File Format. See the [Quantum Painter](quantum_painter.md?id=quantum-painter-cli) documentation for more information on this command. + +## `qmk painter-convert-font-image` + +This command converts an intermediate font image to the QFF File Format. See the [Quantum Painter](quantum_painter.md?id=quantum-painter-cli) documentation for more information on this command. diff --git a/docs/quantum_painter.md b/docs/quantum_painter.md new file mode 100644 index 0000000000..a3705b62ce --- /dev/null +++ b/docs/quantum_painter.md @@ -0,0 +1,705 @@ +# Quantum Painter :id=quantum-painter + +Quantum Painter is the standardised API for graphical displays. It currently includes support for basic drawing primitives, as well as custom images, animations, and fonts. + +Due to the complexity, there is no support for Quantum Painter on AVR-based boards. + +To enable overall Quantum Painter to be built into your firmware, add the following to `rules.mk`: + +```make +QUANTUM_PAINTER_ENABLE = yes +QUANTUM_PAINTER_DRIVERS = ...... +``` + +You will also likely need to select an appropriate driver in `rules.mk`, which is listed below. + +!> Quantum Painter is not currently integrated with system-level operations such as disabling displays after a configurable timeout, or when the keyboard goes into suspend. Users will need to handle this manually at the current time. + +The QMK CLI can be used to convert from normal images such as PNG files or animated GIFs, as well as fonts from TTF files. + +Hardware supported: + +| Display Panel | Panel Type | Size | Comms Transport | Driver | +|---------------|--------------------|------------------|-----------------|-----------------------------------------| +| GC9A01 | RGB LCD (circular) | 240x240 | SPI + D/C + RST | `QUANTUM_PAINTER_DRIVERS = gc9a01_spi` | +| ILI9163 | RGB LCD | 128x128 | SPI + D/C + RST | `QUANTUM_PAINTER_DRIVERS = ili9163_spi` | +| ILI9341 | RGB LCD | 240x320 | SPI + D/C + RST | `QUANTUM_PAINTER_DRIVERS = ili9341_spi` | +| SSD1351 | RGB OLED | 128x128 | SPI + D/C + RST | `QUANTUM_PAINTER_DRIVERS = ssd1351_spi` | +| ST7789 | RGB LCD | 240x320, 240x240 | SPI + D/C + RST | `QUANTUM_PAINTER_DRIVERS = st7789_spi` | + +## Quantum Painter Configuration :id=quantum-painter-config + +| Option | Default | Purpose | +|-----------------------------------------|---------|---------------------------------------------------------------------------------------------------------------------------------------------| +| `QUANTUM_PAINTER_NUM_IMAGES` | `8` | The maximum number of images/animations that can be loaded at any one time. | +| `QUANTUM_PAINTER_NUM_FONTS` | `4` | The maximum number of fonts that can be loaded at any one time. | +| `QUANTUM_PAINTER_CONCURRENT_ANIMATIONS` | `4` | The maximum number of animations that can be executed at the same time. | +| `QUANTUM_PAINTER_LOAD_FONTS_TO_RAM` | `FALSE` | Whether or not fonts should be loaded to RAM. Relevant for fonts stored in off-chip persistent storage, such as external flash. | +| `QUANTUM_PAINTER_PIXDATA_BUFFER_SIZE` | `32` | The limit of the amount of pixel data that can be transmitted in one transaction to the display. Higher values require more RAM on the MCU. | +| `QUANTUM_PAINTER_SUPPORTS_256_PALETTE` | `FALSE` | If 256-color palettes are supported. Requires significantly more RAM on the MCU. | +| `QUANTUM_PAINTER_DEBUG` | _unset_ | Prints out significant amounts of debugging information to CONSOLE output. Significant performance degradation, use only for debugging. | + +Drivers have their own set of configurable options, and are described in their respective sections. + +## Quantum Painter CLI Commands :id=quantum-painter-cli + +### `qmk painter-convert-graphics` + +This command converts images to a format usable by QMK, i.e. the QGF File Format. + +**Usage**: + +``` +usage: qmk painter-convert-graphics [-h] [-d] [-r] -f FORMAT [-o OUTPUT] -i INPUT [-v] + +optional arguments: + -h, --help show this help message and exit + -d, --no-deltas Disables the use of delta frames when encoding animations. + -r, --no-rle Disables the use of RLE when encoding images. + -f FORMAT, --format FORMAT + Output format, valid types: pal256, pal16, pal4, pal2, mono256, mono16, mono4, mono2 + -o OUTPUT, --output OUTPUT + Specify output directory. Defaults to same directory as input. + -i INPUT, --input INPUT + Specify input graphic file. + -v, --verbose Turns on verbose output. +``` + +The `INPUT` argument can be any image file loadable by Python's Pillow module. Common formats include PNG, or Animated GIF. + +The `OUTPUT` argument needs to be a directory, and will default to the same directory as the input argument. + +The `FORMAT` argument can be any of the following: + +| Format | Meaning | +|-----------|-----------------------------------------------------------------------| +| `pal256` | 256-color palette (requires `QUANTUM_PAINTER_SUPPORTS_256_PALETTE`) | +| `pal16` | 16-color palette | +| `pal4` | 4-color palette | +| `pal2` | 2-color palette | +| `mono256` | 256-shade grayscale (requires `QUANTUM_PAINTER_SUPPORTS_256_PALETTE`) | +| `mono16` | 16-shade grayscale | +| `mono4` | 4-shade grayscale | +| `mono2` | 2-shade grayscale | + +**Examples**: + +``` +$ cd /home/qmk/qmk_firmware/keyboards/my_keeb +$ qmk painter-convert-graphics -f mono16 -i my_image.gif -o ./generated/ +Writing /home/qmk/qmk_firmware/keyboards/my_keeb/generated/my_image.qgf.h... +Writing /home/qmk/qmk_firmware/keyboards/my_keeb/generated/my_image.qgf.c... +``` + +### `qmk painter-make-font-image` + +This command converts a TTF font to an intermediate format for editing, before converting to the QFF File Format. + +**Usage**: + +``` +usage: qmk painter-make-font-image [-h] [-a] [-u UNICODE_GLYPHS] [-n] [-s SIZE] -o OUTPUT -f FONT + +optional arguments: + -h, --help show this help message and exit + -a, --no-aa Disable anti-aliasing on fonts. + -u UNICODE_GLYPHS, --unicode-glyphs UNICODE_GLYPHS + Also generate the specified unicode glyphs. + -n, --no-ascii Disables output of the full ASCII character set (0x20..0x7E), exporting only the glyphs specified. + -s SIZE, --size SIZE Specify font size. Default 12. + -o OUTPUT, --output OUTPUT + Specify output image path. + -f FONT, --font FONT Specify input font file. +``` + +The `FONT` argument is generally a TrueType Font file (TTF). + +The `OUTPUT` argument is the output image to generate, generally something like `my_font.png`. + +The `UNICODE_GLYPHS` argument allows for specifying extra unicode glyphs to generate, and accepts a string. + +**Examples**: + +``` +$ qmk painter-make-font-image --font NotoSans-ExtraCondensedBold.ttf --size 11 -o noto11.png --unicode-glyphs "ĄȽɂɻɣɈʣ" +``` + +### `qmk painter-convert-font-image` + +This command converts an intermediate font image to the QFF File Format. + +This command expects an image that conforms to the following format: + +* Top-left pixel (at `0,0`) is the "delimiter" color: + * Each glyph in the font starts when a pixel of this color is found on the first row + * The first row is discarded when converting to the QFF format +* The number of delimited glyphs must match the supplied arguments to the command: + * The full ASCII set `0x20..0x7E` (if `--no-ascii` was not specified) + * The corresponding number of unicode glyphs if any were specified with `--unicode-glyphs` +* The order of the glyphs matches the ASCII set, if any, followed by the Unicode glyph set, if any. + +**Usage**: + +``` +usage: qmk painter-convert-font-image [-h] [-r] -f FORMAT [-u UNICODE_GLYPHS] [-n] [-o OUTPUT] [-i INPUT] + +optional arguments: + -h, --help show this help message and exit + -r, --no-rle Disable the use of RLE to minimise converted image size. + -f FORMAT, --format FORMAT + Output format, valid types: pal256, pal16, pal4, pal2, mono256, mono16, mono4, mono2 + -u UNICODE_GLYPHS, --unicode-glyphs UNICODE_GLYPHS + Also generate the specified unicode glyphs. + -n, --no-ascii Disables output of the full ASCII character set (0x20..0x7E), exporting only the glyphs specified. + -o OUTPUT, --output OUTPUT + Specify output directory. Defaults to same directory as input. + -i INPUT, --input INPUT + Specify input graphic file. +``` + +The same arguments for `--no-ascii` and `--unicode-glyphs` need to be specified, as per `qmk painter-make-font-image`. + +**Examples**: + +``` +$ cd /home/qmk/qmk_firmware/keyboards/my_keeb +$ qmk painter-convert-font-image --input noto11.png -f mono4 --unicode-glyphs "ĄȽɂɻɣɈʣ" +Writing /home/qmk/qmk_firmware/keyboards/my_keeb/generated/noto11.qff.h... +Writing /home/qmk/qmk_firmware/keyboards/my_keeb/generated/noto11.qff.c... +``` + +## Quantum Painter Drawing API :id=quantum-painter-api + +All APIs require a `painter_device_t` object as their first parameter -- this object comes from the specific device initialisation, and instructions on creating it can be found in each driver's respective section. + +To use any of the APIs, you need to include `qp.h`: +```c +#include +``` + +### General Notes :id=quantum-painter-api-general + +The coordinate system used in Quantum Painter generally accepts `left`, `top`, `right`, and `bottom` instead of x/y/width/height, and each coordinate is inclusive of where pixels should be drawn. This is required as some datatypes used by display panels have a maximum value of `255` -- for any value or geometry extent that matches `256`, this would be represented as a `0`, instead. + +?> Drawing a horizontal line 8 pixels long, starting from 4 pixels inside the left side of the display, will need `left=4`, `right=11`. + +All color data matches the standard QMK HSV triplet definitions: + +* Hue is of the range `0...255` and is internally mapped to 0...360 degrees. +* Saturation is of the range `0...255` and is internally mapped to 0...100% saturation. +* Value is of the range `0...255` and is internally mapped to 0...100% brightness. + +?> Colors used in Quantum Painter are not subject to the RGB lighting CIE curve, if it is enabled. + +### Device Control :id=quantum-painter-api-device-control + +#### Display Initialisation :id=quantum-painter-api-init + +```c +bool qp_init(painter_device_t device, painter_rotation_t rotation); +``` + +The `qp_init` function is used to initialise a display device after it has been created. This accepts a rotation parameter (`QP_ROTATION_0`, `QP_ROTATION_90`, `QP_ROTATION_180`, `QP_ROTATION_270`), which makes sure that the orientation of what's drawn on the display is correct. + +```c +static painter_device_t display; +void keyboard_post_init_kb(void) { + display = qp_make_.......; // Create the display + qp_init(display, QP_ROTATION_0); // Initialise the display +} +``` + +#### Display Power :id=quantum-painter-api-power + +```c +bool qp_power(painter_device_t device, bool power_on); +``` + +The `qp_power` function instructs the display whether or not the display panel should be on or off. + +!> If there is a separate backlight controlled through the normal QMK backlight API, this is not controlled by the `qp_power` function and needs to be manually handled elsewhere. + +```c +static uint8_t last_backlight = 255; +void suspend_power_down_user(void) { + if (last_backlight == 255) { + last_backlight = get_backlight_level(); + } + backlight_set(0); + rgb_matrix_set_suspend_state(true); + qp_power(display, false); +} + +void suspend_wakeup_init_user(void) { + qp_power(display, true); + rgb_matrix_set_suspend_state(false); + if (last_backlight != 255) { + backlight_set(last_backlight); + } + last_backlight = 255; +} +``` + +#### Display Clear :id=quantum-painter-api-clear + +```c +bool qp_clear(painter_device_t device); +``` + +The `qp_clear` function clears the display's screen. + +#### Display Flush :id=quantum-painter-api-flush + +```c +bool qp_flush(painter_device_t device); +``` + +The `qp_flush` function ensures that all drawing operations are "pushed" to the display. This should be done as the last operation whenever a sequence of draws occur, and guarantees that any changes are applied. + +!> Some display panels may seem to work even without a call to `qp_flush` -- this may be because the driver cannot queue drawing operations and needs to display them immediately when invoked. In general, calling `qp_flush` at the end is still considered "best practice". + +```c +void housekeeping_task_user(void) { + static uint32_t last_draw = 0; + if (timer_elapsed32(last_draw) > 33) { // Throttle to 30fps + last_draw = timer_read32(); + // Draw a rect based off the current RGB color + qp_rect(display, 0, 7, 0, 239, rgb_matrix_get_hue(), 255, 255); + qp_flush(display); + } +} +``` + +### Drawing Primitives :id=quantum-painter-api-primitives + +#### Set Pixel :id=quantum-painter-api-setpixel + +```c +bool qp_setpixel(painter_device_t device, uint16_t x, uint16_t y, uint8_t hue, uint8_t sat, uint8_t val); +``` + +The `qp_setpixel` can be used to set a specific pixel on the screen to the supplied color. + +?> Using `qp_setpixel` for large amounts of drawing operations is inefficient and should be avoided unless they cannot be achieved with other drawing APIs. + +```c +void housekeeping_task_user(void) { + static uint32_t last_draw = 0; + if (timer_elapsed32(last_draw) > 33) { // Throttle to 30fps + last_draw = timer_read32(); + // Draw a 240px high vertical rainbow line on X=0: + for (int i = 0; i < 239; ++i) { + qp_setpixel(display, 0, i, i, 255, 255); + } + qp_flush(display); + } +} +``` + +#### Draw Line :id=quantum-painter-api-line + +```c +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); +``` + +The `qp_line` can be used to draw lines on the screen with the supplied color. + +```c +void housekeeping_task_user(void) { + static uint32_t last_draw = 0; + if (timer_elapsed32(last_draw) > 33) { // Throttle to 30fps + last_draw = timer_read32(); + // Draw 8px-wide rainbow down the left side of the display + for (int i = 0; i < 239; ++i) { + qp_line(display, 0, i, 7, i, i, 255, 255); + } + qp_flush(display); + } +} +``` + +#### Draw Rect :id=quantum-painter-api-rect + +```c +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); +``` + +The `qp_rect` can be used to draw rectangles on the screen with the supplied color, with or without a background fill. If not filled, any pixels inside the rectangle will be left as-is. + +```c +void housekeeping_task_user(void) { + static uint32_t last_draw = 0; + if (timer_elapsed32(last_draw) > 33) { // Throttle to 30fps + last_draw = timer_read32(); + // Draw 8px-wide rainbow filled rectangles down the left side of the display + for (int i = 0; i < 239; i+=8) { + qp_rect(display, 0, i, 7, i+7, i, 255, 255, true); + } + qp_flush(display); + } +} +``` + +#### Draw Circle :id=quantum-painter-api-circle + +```c +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); +``` + +The `qp_circle` can be used to draw circles on the screen with the supplied color, with or without a background fill. If not filled, any pixels inside the circle will be left as-is. + +```c +void housekeeping_task_user(void) { + static uint32_t last_draw = 0; + if (timer_elapsed32(last_draw) > 33) { // Throttle to 30fps + last_draw = timer_read32(); + // Draw r=4 filled circles down the left side of the display + for (int i = 0; i < 239; i+=8) { + qp_circle(display, 4, 4+i, 4, i, 255, 255, true); + } + qp_flush(display); + } +} +``` + +#### Draw Ellipse :id=quantum-painter-api-ellipse + +```c +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); +``` + +The `qp_ellipse` can be used to draw ellipses on the screen with the supplied color, with or without a background fill. If not filled, any pixels inside the ellipses will be left as-is. + +```c +void housekeeping_task_user(void) { + static uint32_t last_draw = 0; + if (timer_elapsed32(last_draw) > 33) { // Throttle to 30fps + last_draw = timer_read32(); + // Draw 16x8 filled ellipses down the left side of the display + for (int i = 0; i < 239; i+=8) { + qp_ellipse(display, 8, 4+i, 16, 8, i, 255, 255, true); + } + qp_flush(display); + } +} +``` + +### Image Functions :id=quantum-painter-api-images + +#### Load Image :id=quantum-painter-api-load-image + +```c +painter_image_handle_t qp_load_image_mem(const void *buffer); +``` + +The `qp_load_image_mem` function loads a QGF image from memory or flash. + +`qp_load_image_mem` returns a handle to the loaded image, which can then be used to draw to the screen using `qp_drawimage`, `qp_drawimage_recolor`, `qp_animate`, or `qp_animate_recolor`. If an image is no longer required, it can be unloaded by calling `qp_close_image` below. + +See the [CLI Commands](quantum_painter.md?id=quantum-painter-cli) for instructions on how to convert images to [QGF](quantum_painter_qgf.md). + +?> The total number of images available to load at any one time is controlled by the configurable option `QUANTUM_PAINTER_NUM_IMAGES` in the table above. If more images are required, the number should be increased in `config.h`. + +Image information is available through accessing the handle: + +| Property | Accessor | +|-------------|----------------------| +| Width | `image->width` | +| Height | `image->height` | +| Frame Count | `image->frame_count` | + +#### Unload Image :id=quantum-painter-api-close-image + +```c +bool qp_close_image(painter_image_handle_t image); +``` + +The `qp_close_image` function releases resources related to the loading of the supplied image. + +#### Draw image :id=quantum-painter-api-draw-image + +```c +bool qp_drawimage(painter_device_t device, uint16_t x, uint16_t y, painter_image_handle_t image); +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); +``` + +The `qp_drawimage` and `qp_drawimage_recolor` functions draw the supplied image to the screen at the supplied location, with the latter function allowing for monochrome-based images to be recolored. + +```c +// Draw an image on the bottom-right of the 240x320 display on initialisation +static painter_image_handle_t my_image; +void keyboard_post_init_kb(void) { + my_image = qp_load_image_mem(gfx_my_image); + if (my_image != NULL) { + qp_drawimage(display, (239 - my_image->width), (319 - my_image->height), my_image); + } +} +``` + +#### Animate Image :id=quantum-painter-api-animate-image + +```c +deferred_token qp_animate(painter_device_t device, uint16_t x, uint16_t y, painter_image_handle_t image); +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); +``` + +The `qp_animate` and `qp_animate_recolor` functions draw the supplied image to the screen at the supplied location, with the latter function allowing for monochrome-based animations to be recolored. They also set up internal timing such that each frame is rendered at the correct time as per the animated image. + +Once an image has been set to animate, it will loop indefinitely until stopped, with no user intervention required. + +Both functions return a `deferred_token`, which can then be used to stop the animation, using `qp_stop_animation` below. + +```c +// Animate an image on the bottom-right of the 240x320 display on initialisation +static painter_image_handle_t my_image; +static deferred_token my_anim; +void keyboard_post_init_kb(void) { + my_image = qp_load_image_mem(gfx_my_image); + if (my_image != NULL) { + my_anim = qp_animate(display, (239 - my_image->width), (319 - my_image->height), my_image); + } +} +``` + +#### Stop Animation :id=quantum-painter-api-stop-animation + +```c +void qp_stop_animation(deferred_token anim_token); +``` + +The `qp_stop_animation` function stops the previously-started animation. +```c +void housekeeping_task_user(void) { + if (some_random_stop_reason) { + qp_stop_animation(my_anim); + } +} +``` + +### Font Functions :id=quantum-painter-api-fonts + +#### Load Font :id=quantum-painter-api-load-font + +```c +painter_font_handle_t qp_load_font_mem(const void *buffer); +``` + +The `qp_load_font_mem` function loads a QFF font from memory or flash. + +`qp_load_font_mem` returns a handle to the loaded font, which can then be measured using `qp_textwidth`, or drawn to the screen using `qp_drawtext`, or `qp_drawtext_recolor`. If a font is no longer required, it can be unloaded by calling `qp_close_font` below. + +See the [CLI Commands](quantum_painter.md?id=quantum-painter-cli) for instructions on how to convert TTF fonts to [QFF](quantum_painter_qff.md). + +?> The total number of fonts available to load at any one time is controlled by the configurable option `QUANTUM_PAINTER_NUM_FONTS` in the table above. If more fonts are required, the number should be increased in `config.h`. + +Font information is available through accessing the handle: + +| Property | Accessor | +|-------------|----------------------| +| Line Height | `image->line_height` | + +#### Unload Font :id=quantum-painter-api-close-font + +```c +bool qp_close_font(painter_font_handle_t font); +``` + +The `qp_close_font` function releases resources related to the loading of the supplied font. + +#### Measure Text :id=quantum-painter-api-textwidth + +```c +int16_t qp_textwidth(painter_font_handle_t font, const char *str); +``` + +The `qp_textwidth` function allows measurement of how many pixels wide the supplied string would result in, for the given font. + +#### Draw Text :id=quantum-painter-api-drawtext + +```c +int16_t qp_drawtext(painter_device_t device, uint16_t x, uint16_t y, painter_font_handle_t font, const char *str); +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); +``` + +The `qp_drawtext` and `qp_drawtext_recolor` functions draw the supplied string to the screen at the given location using the font supplied, with the latter function allowing for monochrome-based fonts to be recolored. + +```c +// Draw a text message on the bottom-right of the 240x320 display on initialisation +static painter_font_handle_t my_font; +void keyboard_post_init_kb(void) { + my_font = qp_load_font_mem(font_opensans); + if (my_font != NULL) { + static const char *text = "Hello from QMK!"; + int16_t width = qp_textwidth(my_font, text); + qp_drawtext(display, (239 - width), (319 - my_font->line_height), my_font, text); + } +} +``` + +### Advanced Functions :id=quantum-painter-api-advanced + +#### Get Geometry :id=quantum-painter-api-get-geometry + +```c +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); +``` + +The `qp_get_geometry` function allows external code to retrieve the current width, height, rotation, and drawing offsets. + +#### Set Viewport Offsets :id=quantum-painter-api-set-viewport + +```c +void qp_set_viewport_offsets(painter_device_t device, uint16_t offset_x, uint16_t offset_y); +``` + +The `qp_set_viewport_offsets` function can be used to offset all subsequent drawing operations. For example, if a display controller is internally 240x320, but the display panel is 240x240 and has a Y offset of 80 pixels, you could invoke `qp_set_viewport_offsets(display, 0, 80);` and the drawing positioning would be corrected. + +#### Set Viewport :id=quantum-painter-api-viewport + +```c +bool qp_viewport(painter_device_t device, uint16_t left, uint16_t top, uint16_t right, uint16_t bottom); +``` + +The `qp_viewport` function controls where raw pixel data is written to. + +#### Stream Pixel Data :id=quantum-painter-api-pixdata + +```c +bool qp_pixdata(painter_device_t device, const void *pixel_data, uint32_t native_pixel_count); +``` + +The `qp_pixdata` function allows raw pixel data to be streamed to the display. It requires a native pixel count rather than the number of bytes to transfer, to ensure display panel data alignment is respected. E.g. for display panels using RGB565 internal format, sending 10 pixels will result in 20 bytes of transfer. + +!> Under normal circumstances, users will not need to manually call either `qp_viewport` or `qp_pixdata`. These allow for writing of raw pixel information, in the display panel's native format, to the area defined by the viewport. + +## Quantum Painter Display Drivers :id=quantum-painter-drivers + +### Common: Standard TFT (SPI + D/C + RST) + +Most TFT display panels use a 5-pin interface -- SPI SCK, SPI MOSI, SPI CS, D/C, and RST pins. + +For these displays, QMK's `spi_master` must already be correctly configured for the platform you're building for. + +The pin assignments for SPI CS, D/C, and RST are specified during device construction. + +### GC9A01 :id=qp-driver-gc9a01 + +Enabling support for the GC9A01 in Quantum Painter is done by adding the following to `rules.mk`: + +```make +QUANTUM_PAINTER_ENABLE = yes +QUANTUM_PAINTER_DRIVERS = gc9a01_spi +``` + +Creating a GC9A01 device in firmware can then be done with the following API: + +```c +painter_device_t qp_gc9a01_make_spi_device(uint16_t panel_width, uint16_t panel_height, pin_t chip_select_pin, pin_t dc_pin, pin_t reset_pin, uint16_t spi_divisor, int spi_mode); +``` + +The device handle returned from the `qp_gc9a01_make_spi_device` function can be used to perform all other drawing operations. + +The maximum number of displays can be configured by changing the following in your `config.h` (default is 1): + +```c +// 3 displays: +#define GC9A01_NUM_DEVICES 3 +``` + +### ILI9163 :id=qp-driver-ili9163 + +Enabling support for the ILI9163 in Quantum Painter is done by adding the following to `rules.mk`: + +```make +QUANTUM_PAINTER_ENABLE = yes +QUANTUM_PAINTER_DRIVERS = ili9163_spi +``` + +Creating a ILI9163 device in firmware can then be done with the following API: + +```c +painter_device_t qp_ili9163_make_spi_device(uint16_t panel_width, uint16_t panel_height, pin_t chip_select_pin, pin_t dc_pin, pin_t reset_pin, uint16_t spi_divisor, int spi_mode); +``` + +The device handle returned from the `qp_ili9163_make_spi_device` function can be used to perform all other drawing operations. + +The maximum number of displays can be configured by changing the following in your `config.h` (default is 1): + +```c +// 3 displays: +#define ILI9163_NUM_DEVICES 3 +``` + +### ILI9341 :id=qp-driver-ili9341 + +Enabling support for the ILI9341 in Quantum Painter is done by adding the following to `rules.mk`: + +```make +QUANTUM_PAINTER_ENABLE = yes +QUANTUM_PAINTER_DRIVERS = ili9341_spi +``` + +Creating a ILI9341 device in firmware can then be done with the following API: + +```c +painter_device_t qp_ili9341_make_spi_device(uint16_t panel_width, uint16_t panel_height, pin_t chip_select_pin, pin_t dc_pin, pin_t reset_pin, uint16_t spi_divisor, int spi_mode); +``` + +The device handle returned from the `qp_ili9341_make_spi_device` function can be used to perform all other drawing operations. + +The maximum number of displays can be configured by changing the following in your `config.h` (default is 1): + +```c +// 3 displays: +#define ILI9341_NUM_DEVICES 3 +``` + +### SSD1351 :id=qp-driver-ssd1351 + +Enabling support for the SSD1351 in Quantum Painter is done by adding the following to `rules.mk`: + +```make +QUANTUM_PAINTER_ENABLE = yes +QUANTUM_PAINTER_DRIVERS = ssd1351_spi +``` + +Creating a SSD1351 device in firmware can then be done with the following API: + +```c +painter_device_t qp_ssd1351_make_spi_device(uint16_t panel_width, uint16_t panel_height, pin_t chip_select_pin, pin_t dc_pin, pin_t reset_pin, uint16_t spi_divisor, int spi_mode); +``` + +The device handle returned from the `qp_ssd1351_make_spi_device` function can be used to perform all other drawing operations. + +The maximum number of displays can be configured by changing the following in your `config.h` (default is 1): + +```c +// 3 displays: +#define SSD1351_NUM_DEVICES 3 +``` + +### ST7789 :id=qp-driver-st7789 + +Enabling support for the ST7789 in Quantum Painter is done by adding the following to `rules.mk`: + +```make +QUANTUM_PAINTER_ENABLE = yes +QUANTUM_PAINTER_DRIVERS = st7789_spi +``` + +Creating a ST7789 device in firmware can then be done with the following API: + +```c +painter_device_t qp_st7789_make_spi_device(uint16_t panel_width, uint16_t panel_height, pin_t chip_select_pin, pin_t dc_pin, pin_t reset_pin, uint16_t spi_divisor, int spi_mode); +``` + +The device handle returned from the `qp_st7789_make_spi_device` function can be used to perform all other drawing operations. + +The maximum number of displays can be configured by changing the following in your `config.h` (default is 1): + +```c +// 3 displays: +#define ST7789_NUM_DEVICES 3 +``` + +!> Some ST7789 devices are known to have different drawing offsets -- despite being a 240x320 pixel display controller internally, some display panels are only 240x240, or smaller. These may require an offset to be applied; see `qp_set_viewport_offsets` above for information on how to override the offsets if they aren't correctly rendered. \ No newline at end of file diff --git a/docs/quantum_painter_qff.md b/docs/quantum_painter_qff.md new file mode 100644 index 0000000000..f62d59bdcb --- /dev/null +++ b/docs/quantum_painter_qff.md @@ -0,0 +1,103 @@ +# QMK Font Format :id=qmk-font-format + +QMK uses a font format _("Quantum Font Format" - QFF)_ specifically for resource-constrained systems. + +This format is capable of encoding 1-, 2-, 4-, and 8-bit-per-pixel greyscale- and palette-based images into a font. It also includes RLE for pixel data for some basic compression. + +All integer values are in little-endian format. + +The QFF is defined in terms of _blocks_ -- each _block_ contains a _header_ and an optional _blob_ of data. The _header_ contains the block's _typeid_, and the length of the _blob_ that follows. Each block type is denoted by a different _typeid_ has its own block definition below. All blocks are defined as packed structs, containing zero padding between fields. + +The general structure of the file is: + +* _Font descriptor block_ +* _ASCII glyph block_ (optional, only if ASCII glyphs are included) +* _Unicode glyph block_ (optional, only if Unicode glyphs are included) +* _Font palette block_ (optional, depending on frame format) +* _Font data block_ + +## Block Header :id=qff-block-header + +The block header is identical to [QGF's block header](quantum_painter_qgf.md#qgf-block-header), and is present for all blocks, including the font descriptor. + +## Font descriptor block :id=qff-font-descriptor + +* _typeid_ = 0x00 +* _length_ = 20 + +This block must be located at the start of the file contents, and can exist a maximum of once in an entire QGF file. It is always followed by either the _ASCII glyph table_ or the _Unicode glyph table_, depending on which glyphs are included in the font. + +_Block_ format: + +```c +typedef struct __attribute__((packed)) qff_font_descriptor_v1_t { + qgf_block_header_v1_t header; // = { .type_id = 0x00, .neg_type_id = (~0x00), .length = 20 } + uint24_t magic; // 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 + uint8_t format; // frame format, see below. + 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"); +``` + +The values for `format`, `flags`, `compression_scheme`, and `transparency_index` match [QGF's frame descriptor block](quantum_painter_qgf.md#qgf-frame-descriptor), with the exception that the `delta` flag is ignored by QFF. + +## ASCII glyph table :id=qff-ascii-table + +* _typeid_ = 0x01 +* _length_ = 290 + +If the font contains ascii characters, the _ASCII glyph block_ must be located directly after the _font descriptor block_. + +```c +#define QFF_GLYPH_WIDTH_BITS 6 +#define QFF_GLYPH_WIDTH_MASK ((1<= 128 + length = marker - 128 + for i = 0 ... length-1 + c = READ_OCTET() + WRITE_OCTET(c) + + else + length = marker + c = READ_OCTET() + for i = 0 ... length-1 + WRITE_OCTET(c) + +``` diff --git a/drivers/painter/comms/qp_comms_spi.c b/drivers/painter/comms/qp_comms_spi.c new file mode 100644 index 0000000000..e644ba9f84 --- /dev/null +++ b/drivers/painter/comms/qp_comms_spi.c @@ -0,0 +1,137 @@ +// Copyright 2021 Nick Brassel (@tzarc) +// SPDX-License-Identifier: GPL-2.0-or-later + +#ifdef QUANTUM_PAINTER_SPI_ENABLE + +# include "spi_master.h" +# include "qp_comms_spi.h" + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Base SPI support + +bool qp_comms_spi_init(painter_device_t device) { + struct painter_driver_t * driver = (struct painter_driver_t *)device; + struct qp_comms_spi_config_t *comms_config = (struct qp_comms_spi_config_t *)driver->comms_config; + + // Initialize the SPI peripheral + spi_init(); + + // Set up CS as output high + setPinOutput(comms_config->chip_select_pin); + writePinHigh(comms_config->chip_select_pin); + + return true; +} + +bool qp_comms_spi_start(painter_device_t device) { + struct painter_driver_t * driver = (struct painter_driver_t *)device; + struct qp_comms_spi_config_t *comms_config = (struct qp_comms_spi_config_t *)driver->comms_config; + + return spi_start(comms_config->chip_select_pin, comms_config->lsb_first, comms_config->mode, comms_config->divisor); +} + +uint32_t qp_comms_spi_send_data(painter_device_t device, const void *data, uint32_t byte_count) { + uint32_t bytes_remaining = byte_count; + const uint8_t *p = (const uint8_t *)data; + while (bytes_remaining > 0) { + uint32_t bytes_this_loop = bytes_remaining < 1024 ? bytes_remaining : 1024; + spi_transmit(p, bytes_this_loop); + p += bytes_this_loop; + bytes_remaining -= bytes_this_loop; + } + + return byte_count - bytes_remaining; +} + +void qp_comms_spi_stop(painter_device_t device) { + struct painter_driver_t * driver = (struct painter_driver_t *)device; + struct qp_comms_spi_config_t *comms_config = (struct qp_comms_spi_config_t *)driver->comms_config; + spi_stop(); + writePinHigh(comms_config->chip_select_pin); +} + +const struct painter_comms_vtable_t spi_comms_vtable = { + .comms_init = qp_comms_spi_init, + .comms_start = qp_comms_spi_start, + .comms_send = qp_comms_spi_send_data, + .comms_stop = qp_comms_spi_stop, +}; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// SPI with D/C and RST pins + +# ifdef QUANTUM_PAINTER_SPI_DC_RESET_ENABLE + +bool qp_comms_spi_dc_reset_init(painter_device_t device) { + if (!qp_comms_spi_init(device)) { + return false; + } + + struct painter_driver_t * driver = (struct painter_driver_t *)device; + struct qp_comms_spi_dc_reset_config_t *comms_config = (struct qp_comms_spi_dc_reset_config_t *)driver->comms_config; + + // Set up D/C as output low, if specified + if (comms_config->dc_pin != NO_PIN) { + setPinOutput(comms_config->dc_pin); + writePinLow(comms_config->dc_pin); + } + + // Set up RST as output, if specified, performing a reset in the process + if (comms_config->reset_pin != NO_PIN) { + setPinOutput(comms_config->reset_pin); + writePinLow(comms_config->reset_pin); + wait_ms(20); + writePinHigh(comms_config->reset_pin); + wait_ms(20); + } + + return true; +} + +uint32_t qp_comms_spi_dc_reset_send_data(painter_device_t device, const void *data, uint32_t byte_count) { + struct painter_driver_t * driver = (struct painter_driver_t *)device; + struct qp_comms_spi_dc_reset_config_t *comms_config = (struct qp_comms_spi_dc_reset_config_t *)driver->comms_config; + writePinHigh(comms_config->dc_pin); + return qp_comms_spi_send_data(device, data, byte_count); +} + +void qp_comms_spi_dc_reset_send_command(painter_device_t device, uint8_t cmd) { + struct painter_driver_t * driver = (struct painter_driver_t *)device; + struct qp_comms_spi_dc_reset_config_t *comms_config = (struct qp_comms_spi_dc_reset_config_t *)driver->comms_config; + writePinLow(comms_config->dc_pin); + spi_write(cmd); +} + +void qp_comms_spi_dc_reset_bulk_command_sequence(painter_device_t device, const uint8_t *sequence, size_t sequence_len) { + for (size_t i = 0; i < sequence_len;) { + uint8_t command = sequence[i]; + uint8_t delay = sequence[i + 1]; + uint8_t num_bytes = sequence[i + 2]; + qp_comms_spi_dc_reset_send_command(device, command); + if (num_bytes > 0) { + qp_comms_spi_dc_reset_send_data(device, &sequence[i + 3], num_bytes); + } + if (delay > 0) { + wait_ms(delay); + } + i += (3 + num_bytes); + } +} + +const struct painter_comms_with_command_vtable_t spi_comms_with_dc_vtable = { + .base = + { + .comms_init = qp_comms_spi_dc_reset_init, + .comms_start = qp_comms_spi_start, + .comms_send = qp_comms_spi_dc_reset_send_data, + .comms_stop = qp_comms_spi_stop, + }, + .send_command = qp_comms_spi_dc_reset_send_command, + .bulk_command_sequence = qp_comms_spi_dc_reset_bulk_command_sequence, +}; + +# endif // QUANTUM_PAINTER_SPI_DC_RESET_ENABLE + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#endif // QUANTUM_PAINTER_SPI_ENABLE diff --git a/drivers/painter/comms/qp_comms_spi.h b/drivers/painter/comms/qp_comms_spi.h new file mode 100644 index 0000000000..9989987327 --- /dev/null +++ b/drivers/painter/comms/qp_comms_spi.h @@ -0,0 +1,51 @@ +// Copyright 2021 Nick Brassel (@tzarc) +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#ifdef QUANTUM_PAINTER_SPI_ENABLE + +# include + +# include "gpio.h" +# include "qp_internal.h" + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Base SPI support + +struct qp_comms_spi_config_t { + pin_t chip_select_pin; + uint16_t divisor; + bool lsb_first; + int8_t mode; +}; + +bool qp_comms_spi_init(painter_device_t device); +bool qp_comms_spi_start(painter_device_t device); +uint32_t qp_comms_spi_send_data(painter_device_t device, const void* data, uint32_t byte_count); +void qp_comms_spi_stop(painter_device_t device); + +extern const struct painter_comms_vtable_t spi_comms_vtable; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// SPI with D/C and RST pins + +# ifdef QUANTUM_PAINTER_SPI_DC_RESET_ENABLE + +struct qp_comms_spi_dc_reset_config_t { + struct qp_comms_spi_config_t spi_config; + pin_t dc_pin; + pin_t reset_pin; +}; + +void qp_comms_spi_dc_reset_send_command(painter_device_t device, uint8_t cmd); +uint32_t qp_comms_spi_dc_reset_send_data(painter_device_t device, const void* data, uint32_t byte_count); +void qp_comms_spi_dc_reset_bulk_command_sequence(painter_device_t device, const uint8_t* sequence, size_t sequence_len); + +extern const struct painter_comms_with_command_vtable_t spi_comms_with_dc_vtable; + +# endif // QUANTUM_PAINTER_SPI_DC_RESET_ENABLE + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#endif // QUANTUM_PAINTER_SPI_ENABLE diff --git a/drivers/painter/gc9a01/qp_gc9a01.c b/drivers/painter/gc9a01/qp_gc9a01.c new file mode 100644 index 0000000000..ad76d58b07 --- /dev/null +++ b/drivers/painter/gc9a01/qp_gc9a01.c @@ -0,0 +1,150 @@ +// Copyright 2021 Paul Cotter (@gr1mr3aver) +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include "qp_internal.h" +#include "qp_comms.h" +#include "qp_gc9a01.h" +#include "qp_gc9a01_opcodes.h" +#include "qp_tft_panel.h" + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Driver storage +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +tft_panel_dc_reset_painter_device_t gc9a01_drivers[GC9A01_NUM_DEVICES] = {0}; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Initialization +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +bool qp_gc9a01_init(painter_device_t device, painter_rotation_t rotation) { + // A lot of these "unknown" opcodes are sourced from other OSS projects and are seemingly required for this display to function. + // clang-format off + const uint8_t gc9a01_init_sequence[] = { + // Command, Delay, N, Data[N] + GC9A01_SET_INTER_REG_ENABLE2, 0, 0, + 0xEB, 0, 1, 0x14, + GC9A01_SET_INTER_REG_ENABLE1, 0, 0, + GC9A01_SET_INTER_REG_ENABLE2, 0, 0, + 0xEB, 0, 1, 0x14, + 0x84, 0, 1, 0x40, + 0x85, 0, 1, 0xFF, + 0x86, 0, 1, 0xFF, + 0x87, 0, 1, 0xFF, + 0x88, 0, 1, 0x0A, + 0x89, 0, 1, 0x21, + 0x8a, 0, 1, 0x00, + 0x8b, 0, 1, 0x80, + 0x8c, 0, 1, 0x01, + 0x8d, 0, 1, 0x01, + 0x8e, 0, 1, 0xFF, + 0x8f, 0, 1, 0xFF, + GC9A01_SET_FUNCTION_CTL, 0, 2, 0x00, 0x20, + GC9A01_SET_PIX_FMT, 0, 1, 0x55, + 0x90, 0, 4, 0x08, 0x08, 0x08, 0x08, + 0xBD, 0, 1, 0x06, + 0xBC, 0, 1, 0x00, + 0xFF, 0, 3, 0x60, 0x01, 0x04, + GC9A01_SET_POWER_CTL_2, 0, 1, 0x13, + GC9A01_SET_POWER_CTL_3, 0, 1, 0x13, + GC9A01_SET_POWER_CTL_4, 0, 1, 0x22, + 0xBE, 0, 1, 0x11, + 0xE1, 0, 2, 0x10, 0x0E, + 0xDF, 0, 3, 0x21, 0x0C, 0x02, + GC9A01_SET_GAMMA1, 0, 6, 0x45, 0x09, 0x08, 0x08, 0x26, 0x2A, + GC9A01_SET_GAMMA2, 0, 6, 0x43, 0x70, 0x72, 0x36, 0x37, 0x6F, + GC9A01_SET_GAMMA3, 0, 6, 0x45, 0x09, 0x08, 0x08, 0x26, 0x2A, + GC9A01_SET_GAMMA4, 0, 6, 0x43, 0x70, 0x72, 0x36, 0x37, 0x6F, + 0xED, 0, 2, 0x1B, 0x0B, + 0xAE, 0, 1, 0x77, + 0xCD, 0, 1, 0x63, + 0x70, 0, 9, 0x07, 0x07, 0x04, 0x0E, 0x0F, 0x09, 0x07, 0x08, 0x03, + GC9A01_SET_FRAME_RATE, 0, 1, 0x34, + 0x62, 0, 12, 0x18, 0x0D, 0x71, 0xED, 0x70, 0x70, 0x18, 0x0F, 0x71, 0xEF, 0x70, 0x70, + 0x63, 0, 12, 0x18, 0x11, 0x71, 0xF1, 0x70, 0x70, 0x18, 0x13, 0x71, 0xF3, 0x70, 0x70, + 0x64, 0, 7, 0x28, 0x29, 0xF1, 0x01, 0xF1, 0x00, 0x07, + 0x66, 0, 10, 0x3C, 0x00, 0xCD, 0x67, 0x45, 0x45, 0x10, 0x00, 0x00, 0x00, + 0x67, 0, 10, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x01, 0x54, 0x10, 0x32, 0x98, + 0x74, 0, 7, 0x10, 0x85, 0x80, 0x00, 0x00, 0x4E, 0x00, + 0x98, 0, 2, 0x3E, 0x07, + GC9A01_CMD_TEARING_OFF, 0, 0, + GC9A01_CMD_INVERT_OFF, 0, 0, + GC9A01_CMD_SLEEP_OFF, 120, 0, + GC9A01_CMD_DISPLAY_ON, 20, 0 + }; + // clang-format on + + // clang-format on + qp_comms_bulk_command_sequence(device, gc9a01_init_sequence, sizeof(gc9a01_init_sequence)); + + // Configure the rotation (i.e. the ordering and direction of memory writes in GRAM) + const uint8_t madctl[] = { + [QP_ROTATION_0] = GC9A01_MADCTL_BGR, + [QP_ROTATION_90] = GC9A01_MADCTL_BGR | GC9A01_MADCTL_MX | GC9A01_MADCTL_MV, + [QP_ROTATION_180] = GC9A01_MADCTL_BGR | GC9A01_MADCTL_MX | GC9A01_MADCTL_MY, + [QP_ROTATION_270] = GC9A01_MADCTL_BGR | GC9A01_MADCTL_MV | GC9A01_MADCTL_MY, + }; + qp_comms_command_databyte(device, GC9A01_SET_MEM_ACS_CTL, madctl[rotation]); + + return true; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Driver vtable +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +const struct tft_panel_dc_reset_painter_driver_vtable_t gc9a01_driver_vtable = { + .base = + { + .init = qp_gc9a01_init, + .power = qp_tft_panel_power, + .clear = qp_tft_panel_clear, + .flush = qp_tft_panel_flush, + .pixdata = qp_tft_panel_pixdata, + .viewport = qp_tft_panel_viewport, + .palette_convert = qp_tft_panel_palette_convert, + .append_pixels = qp_tft_panel_append_pixels, + }, + .rgb888_to_native16bit = qp_rgb888_to_rgb565_swapped, + .num_window_bytes = 2, + .swap_window_coords = false, + .opcodes = + { + .display_on = GC9A01_CMD_DISPLAY_ON, + .display_off = GC9A01_CMD_DISPLAY_OFF, + .set_column_address = GC9A01_SET_COL_ADDR, + .set_row_address = GC9A01_SET_PAGE_ADDR, + .enable_writes = GC9A01_SET_MEM, + }, +}; + +#ifdef QUANTUM_PAINTER_GC9A01_SPI_ENABLE +// Factory function for creating a handle to the ILI9341 device +painter_device_t qp_gc9a01_make_spi_device(uint16_t panel_width, uint16_t panel_height, pin_t chip_select_pin, pin_t dc_pin, pin_t reset_pin, uint16_t spi_divisor, int spi_mode) { + for (uint32_t i = 0; i < GC9A01_NUM_DEVICES; ++i) { + tft_panel_dc_reset_painter_device_t *driver = &gc9a01_drivers[i]; + if (!driver->base.driver_vtable) { + driver->base.driver_vtable = (const struct painter_driver_vtable_t *)&gc9a01_driver_vtable; + driver->base.comms_vtable = (const struct painter_comms_vtable_t *)&spi_comms_with_dc_vtable; + driver->base.native_bits_per_pixel = 16; // RGB565 + driver->base.panel_width = panel_width; + driver->base.panel_height = panel_height; + driver->base.rotation = QP_ROTATION_0; + driver->base.offset_x = 0; + driver->base.offset_y = 0; + + // SPI and other pin configuration + driver->base.comms_config = &driver->spi_dc_reset_config; + driver->spi_dc_reset_config.spi_config.chip_select_pin = chip_select_pin; + driver->spi_dc_reset_config.spi_config.divisor = spi_divisor; + driver->spi_dc_reset_config.spi_config.lsb_first = false; + driver->spi_dc_reset_config.spi_config.mode = spi_mode; + driver->spi_dc_reset_config.dc_pin = dc_pin; + driver->spi_dc_reset_config.reset_pin = reset_pin; + return (painter_device_t)driver; + } + } + return NULL; +} + +#endif // QUANTUM_PAINTER_GC9A01_SPI_ENABLE diff --git a/drivers/painter/gc9a01/qp_gc9a01.h b/drivers/painter/gc9a01/qp_gc9a01.h new file mode 100644 index 0000000000..e2b1939564 --- /dev/null +++ b/drivers/painter/gc9a01/qp_gc9a01.h @@ -0,0 +1,37 @@ +// Copyright 2021 Paul Cotter (@gr1mr3aver) +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "gpio.h" +#include "qp_internal.h" + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Quantum Painter GC9A01 configurables (add to your keyboard's config.h) + +#ifndef GC9A01_NUM_DEVICES +/** + * @def This controls the maximum number of GC9A01 devices that Quantum Painter can communicate with at any one time. + * Increasing this number allows for multiple displays to be used. + */ +# define GC9A01_NUM_DEVICES 1 +#endif + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Quantum Painter GC9A01 device factories + +#ifdef QUANTUM_PAINTER_GC9A01_SPI_ENABLE +/** + * Factory method for an GC9A01 SPI LCD device. + * + * @param panel_width[in] the width of the display panel + * @param panel_height[in] the height of the display panel + * @param chip_select_pin[in] the GPIO pin used for SPI chip select + * @param dc_pin[in] the GPIO pin used for D/C control + * @param reset_pin[in] the GPIO pin used for RST + * @param spi_divisor[in] the SPI divisor to use when communicating with the display + * @param spi_mode[in] the SPI mode to use when communicating with the display + * @return the device handle used with all drawing routines in Quantum Painter + */ +painter_device_t qp_gc9a01_make_spi_device(uint16_t panel_width, uint16_t panel_height, pin_t chip_select_pin, pin_t dc_pin, pin_t reset_pin, uint16_t spi_divisor, int spi_mode); +#endif // QUANTUM_PAINTER_GC9A01_SPI_ENABLE diff --git a/drivers/painter/gc9a01/qp_gc9a01_opcodes.h b/drivers/painter/gc9a01/qp_gc9a01_opcodes.h new file mode 100644 index 0000000000..6ff4efe7a8 --- /dev/null +++ b/drivers/painter/gc9a01/qp_gc9a01_opcodes.h @@ -0,0 +1,78 @@ +// Copyright 2021 Paul Cotter (@gr1mr3aver) +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Quantum Painter GC9A01 command opcodes +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Level 1 command opcodes + +#define GC9A01_GET_ID_INFO 0x04 // Get ID information +#define GC9A01_GET_STATUS 0x09 // Get status +#define GC9A01_CMD_SLEEP_ON 0x10 // Enter sleep mode +#define GC9A01_CMD_SLEEP_OFF 0x11 // Exit sleep mode +#define GC9A01_CMD_PARTIAL_ON 0x12 // Enter partial mode +#define GC9A01_CMD_PARTIAL_OFF 0x13 // Exit partial mode +#define GC9A01_CMD_INVERT_ON 0x20 // Enter inverted mode +#define GC9A01_CMD_INVERT_OFF 0x21 // Exit inverted mode +#define GC9A01_CMD_DISPLAY_OFF 0x28 // Disable display +#define GC9A01_CMD_DISPLAY_ON 0x29 // Enable display +#define GC9A01_SET_COL_ADDR 0x2A // Set column address +#define GC9A01_SET_PAGE_ADDR 0x2B // Set page address +#define GC9A01_SET_MEM 0x2C // Set memory +#define GC9A01_SET_PARTIAL_AREA 0x30 // Set partial area +#define GC9A01_SET_VSCROLL 0x33 // Set vertical scroll def +#define GC9A01_CMD_TEARING_ON 0x34 // Tearing line enabled +#define GC9A01_CMD_TEARING_OFF 0x35 // Tearing line disabled +#define GC9A01_SET_MEM_ACS_CTL 0x36 // Set mem access ctl +#define GC9A01_SET_VSCROLL_ADDR 0x37 // Set vscroll start addr +#define GC9A01_CMD_IDLE_OFF 0x38 // Exit idle mode +#define GC9A01_CMD_IDLE_ON 0x39 // Enter idle mode +#define GC9A01_SET_PIX_FMT 0x3A // Set pixel format +#define GC9A01_SET_MEM_CONT 0x3C // Set memory continue +#define GC9A01_SET_TEAR_SCANLINE 0x44 // Set tearing scanline +#define GC9A01_GET_TEAR_SCANLINE 0x45 // Get tearing scanline +#define GC9A01_SET_BRIGHTNESS 0x51 // Set brightness +#define GC9A01_SET_DISPLAY_CTL 0x53 // Set display ctl +#define GC9A01_GET_ID1 0xDA // Get ID1 +#define GC9A01_GET_ID2 0xDB // Get ID2 +#define GC9A01_GET_ID3 0xDC // Get ID3 + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Level 2 command opcodes + +#define GC9A01_SET_RGB_IF_SIG_CTL 0xB0 // RGB IF signal ctl +#define GC9A01_SET_BLANKING_PORCH_CTL 0xB5 // Set blanking porch ctl +#define GC9A01_SET_FUNCTION_CTL 0xB6 // Set function ctl +#define GC9A01_SET_TEARING_EFFECT 0xBA // Set backlight ctl 3 +#define GC9A01_SET_IF_CTL 0xF6 // Set interface control + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Level 3 command opcodes + +#define GC9A01_SET_FRAME_RATE 0xE8 // Set frame rate +#define GC9A01_SET_SPI_2DATA 0xE9 // Set frame rate +#define GC9A01_SET_POWER_CTL_1 0xC1 // Set power ctl 1 +#define GC9A01_SET_POWER_CTL_2 0xC3 // Set power ctl 2 +#define GC9A01_SET_POWER_CTL_3 0xC4 // Set power ctl 3 +#define GC9A01_SET_POWER_CTL_4 0xC9 // Set power ctl 4 +#define GC9A01_SET_POWER_CTL_7 0xA7 // Set power ctl 7 +#define GC9A01_SET_INTER_REG_ENABLE1 0xFE // Enable Inter Register 1 +#define GC9A01_SET_INTER_REG_ENABLE2 0xEF // Enable Inter Register 2 +#define GC9A01_SET_GAMMA1 0xF0 // +#define GC9A01_SET_GAMMA2 0xF1 +#define GC9A01_SET_GAMMA3 0xF2 +#define GC9A01_SET_GAMMA4 0xF3 + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// MADCTL Flags +#define GC9A01_MADCTL_MY 0b10000000 +#define GC9A01_MADCTL_MX 0b01000000 +#define GC9A01_MADCTL_MV 0b00100000 +#define GC9A01_MADCTL_ML 0b00010000 +#define GC9A01_MADCTL_RGB 0b00000000 +#define GC9A01_MADCTL_BGR 0b00001000 +#define GC9A01_MADCTL_MH 0b00000100 diff --git a/drivers/painter/ili9xxx/qp_ili9163.c b/drivers/painter/ili9xxx/qp_ili9163.c new file mode 100644 index 0000000000..beaac0fbb5 --- /dev/null +++ b/drivers/painter/ili9xxx/qp_ili9163.c @@ -0,0 +1,121 @@ +// Copyright 2021 Nick Brassel (@tzarc) +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "qp_internal.h" +#include "qp_comms.h" +#include "qp_ili9163.h" +#include "qp_ili9xxx_opcodes.h" +#include "qp_tft_panel.h" + +#ifdef QUANTUM_PAINTER_ILI9163_SPI_ENABLE +# include "qp_comms_spi.h" +#endif // QUANTUM_PAINTER_ILI9163_SPI_ENABLE + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Common + +// Driver storage +tft_panel_dc_reset_painter_device_t ili9163_drivers[ILI9163_NUM_DEVICES] = {0}; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Initialization + +bool qp_ili9163_init(painter_device_t device, painter_rotation_t rotation) { + // clang-format off + const uint8_t ili9163_init_sequence[] = { + // Command, Delay, N, Data[N] + ILI9XXX_CMD_RESET, 120, 0, + ILI9XXX_CMD_SLEEP_OFF, 5, 0, + ILI9XXX_SET_PIX_FMT, 0, 1, 0x55, + ILI9XXX_SET_GAMMA, 0, 1, 0x04, + ILI9XXX_ENABLE_3_GAMMA, 0, 1, 0x01, + ILI9XXX_SET_FUNCTION_CTL, 0, 2, 0xFF, 0x06, + ILI9XXX_SET_PGAMMA, 0, 15, 0x36, 0x29, 0x12, 0x22, 0x1C, 0x15, 0x42, 0xB7, 0x2F, 0x13, 0x12, 0x0A, 0x11, 0x0B, 0x06, + ILI9XXX_SET_NGAMMA, 0, 15, 0x09, 0x16, 0x2D, 0x0D, 0x13, 0x15, 0x40, 0x48, 0x53, 0x0C, 0x1D, 0x25, 0x2E, 0x34, 0x39, + ILI9XXX_SET_FRAME_CTL_NORMAL, 0, 2, 0x08, 0x02, + ILI9XXX_SET_POWER_CTL_1, 0, 2, 0x0A, 0x02, + ILI9XXX_SET_POWER_CTL_2, 0, 1, 0x02, + ILI9XXX_SET_VCOM_CTL_1, 0, 2, 0x50, 0x63, + ILI9XXX_SET_VCOM_CTL_2, 0, 1, 0x00, + ILI9XXX_CMD_PARTIAL_OFF, 0, 0, + ILI9XXX_CMD_DISPLAY_ON, 20, 0 + }; + // clang-format on + qp_comms_bulk_command_sequence(device, ili9163_init_sequence, sizeof(ili9163_init_sequence)); + + // Configure the rotation (i.e. the ordering and direction of memory writes in GRAM) + const uint8_t madctl[] = { + [QP_ROTATION_0] = ILI9XXX_MADCTL_BGR, + [QP_ROTATION_90] = ILI9XXX_MADCTL_BGR | ILI9XXX_MADCTL_MX | ILI9XXX_MADCTL_MV, + [QP_ROTATION_180] = ILI9XXX_MADCTL_BGR | ILI9XXX_MADCTL_MX | ILI9XXX_MADCTL_MY, + [QP_ROTATION_270] = ILI9XXX_MADCTL_BGR | ILI9XXX_MADCTL_MV | ILI9XXX_MADCTL_MY, + }; + qp_comms_command_databyte(device, ILI9XXX_SET_MEM_ACS_CTL, madctl[rotation]); + + return true; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Driver vtable + +const struct tft_panel_dc_reset_painter_driver_vtable_t ili9163_driver_vtable = { + .base = + { + .init = qp_ili9163_init, + .power = qp_tft_panel_power, + .clear = qp_tft_panel_clear, + .flush = qp_tft_panel_flush, + .pixdata = qp_tft_panel_pixdata, + .viewport = qp_tft_panel_viewport, + .palette_convert = qp_tft_panel_palette_convert, + .append_pixels = qp_tft_panel_append_pixels, + }, + .rgb888_to_native16bit = qp_rgb888_to_rgb565_swapped, + .num_window_bytes = 2, + .swap_window_coords = false, + .opcodes = + { + .display_on = ILI9XXX_CMD_DISPLAY_ON, + .display_off = ILI9XXX_CMD_DISPLAY_OFF, + .set_column_address = ILI9XXX_SET_COL_ADDR, + .set_row_address = ILI9XXX_SET_PAGE_ADDR, + .enable_writes = ILI9XXX_SET_MEM, + }, +}; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// SPI + +#ifdef QUANTUM_PAINTER_ILI9163_SPI_ENABLE + +// Factory function for creating a handle to the ILI9163 device +painter_device_t qp_ili9163_make_spi_device(uint16_t panel_width, uint16_t panel_height, pin_t chip_select_pin, pin_t dc_pin, pin_t reset_pin, uint16_t spi_divisor, int spi_mode) { + for (uint32_t i = 0; i < ILI9163_NUM_DEVICES; ++i) { + tft_panel_dc_reset_painter_device_t *driver = &ili9163_drivers[i]; + if (!driver->base.driver_vtable) { + driver->base.driver_vtable = (const struct painter_driver_vtable_t *)&ili9163_driver_vtable; + driver->base.comms_vtable = (const struct painter_comms_vtable_t *)&spi_comms_with_dc_vtable; + driver->base.panel_width = panel_width; + driver->base.panel_height = panel_height; + driver->base.rotation = QP_ROTATION_0; + driver->base.offset_x = 0; + driver->base.offset_y = 0; + driver->base.native_bits_per_pixel = 16; // RGB565 + + // SPI and other pin configuration + driver->base.comms_config = &driver->spi_dc_reset_config; + driver->spi_dc_reset_config.spi_config.chip_select_pin = chip_select_pin; + driver->spi_dc_reset_config.spi_config.divisor = spi_divisor; + driver->spi_dc_reset_config.spi_config.lsb_first = false; + driver->spi_dc_reset_config.spi_config.mode = spi_mode; + driver->spi_dc_reset_config.dc_pin = dc_pin; + driver->spi_dc_reset_config.reset_pin = reset_pin; + return (painter_device_t)driver; + } + } + return NULL; +} + +#endif // QUANTUM_PAINTER_ILI9163_SPI_ENABLE + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/drivers/painter/ili9xxx/qp_ili9163.h b/drivers/painter/ili9xxx/qp_ili9163.h new file mode 100644 index 0000000000..88d23629a9 --- /dev/null +++ b/drivers/painter/ili9xxx/qp_ili9163.h @@ -0,0 +1,37 @@ +// Copyright 2021 Nick Brassel (@tzarc) +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "gpio.h" +#include "qp_internal.h" + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Quantum Painter ILI9163 configurables (add to your keyboard's config.h) + +#ifndef ILI9163_NUM_DEVICES +/** + * @def This controls the maximum number of ILI9163 devices that Quantum Painter can communicate with at any one time. + * Increasing this number allows for multiple displays to be used. + */ +# define ILI9163_NUM_DEVICES 1 +#endif + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Quantum Painter ILI9163 device factories + +#ifdef QUANTUM_PAINTER_ILI9163_SPI_ENABLE +/** + * Factory method for an ILI9163 SPI LCD device. + * + * @param panel_width[in] the width of the display panel + * @param panel_height[in] the height of the display panel + * @param chip_select_pin[in] the GPIO pin used for SPI chip select + * @param dc_pin[in] the GPIO pin used for D/C control + * @param reset_pin[in] the GPIO pin used for RST + * @param spi_divisor[in] the SPI divisor to use when communicating with the display + * @param spi_mode[in] the SPI mode to use when communicating with the display + * @return the device handle used with all drawing routines in Quantum Painter + */ +painter_device_t qp_ili9163_make_spi_device(uint16_t panel_width, uint16_t panel_height, pin_t chip_select_pin, pin_t dc_pin, pin_t reset_pin, uint16_t spi_divisor, int spi_mode); +#endif // QUANTUM_PAINTER_ILI9163_SPI_ENABLE diff --git a/drivers/painter/ili9xxx/qp_ili9341.c b/drivers/painter/ili9xxx/qp_ili9341.c new file mode 100644 index 0000000000..1f41dcfc0b --- /dev/null +++ b/drivers/painter/ili9xxx/qp_ili9341.c @@ -0,0 +1,128 @@ +// Copyright 2021 Nick Brassel (@tzarc) +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "qp_internal.h" +#include "qp_comms.h" +#include "qp_ili9341.h" +#include "qp_ili9xxx_opcodes.h" +#include "qp_tft_panel.h" + +#ifdef QUANTUM_PAINTER_ILI9341_SPI_ENABLE +# include +#endif // QUANTUM_PAINTER_ILI9341_SPI_ENABLE + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Common + +// Driver storage +tft_panel_dc_reset_painter_device_t ili9341_drivers[ILI9341_NUM_DEVICES] = {0}; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Initialization + +bool qp_ili9341_init(painter_device_t device, painter_rotation_t rotation) { + // clang-format off + const uint8_t ili9341_init_sequence[] = { + // Command, Delay, N, Data[N] + ILI9XXX_CMD_RESET, 120, 0, + ILI9XXX_CMD_SLEEP_OFF, 5, 0, + ILI9XXX_POWER_CTL_A, 0, 5, 0x39, 0x2C, 0x00, 0x34, 0x02, + ILI9XXX_POWER_CTL_B, 0, 3, 0x00, 0xD9, 0x30, + ILI9XXX_POWER_ON_SEQ_CTL, 0, 4, 0x64, 0x03, 0x12, 0x81, + ILI9XXX_SET_PUMP_RATIO_CTL, 0, 1, 0x20, + ILI9XXX_SET_POWER_CTL_1, 0, 1, 0x26, + ILI9XXX_SET_POWER_CTL_2, 0, 1, 0x11, + ILI9XXX_SET_VCOM_CTL_1, 0, 2, 0x35, 0x3E, + ILI9XXX_SET_VCOM_CTL_2, 0, 1, 0xBE, + ILI9XXX_DRV_TIMING_CTL_A, 0, 3, 0x85, 0x10, 0x7A, + ILI9XXX_DRV_TIMING_CTL_B, 0, 2, 0x00, 0x00, + ILI9XXX_SET_BRIGHTNESS, 0, 1, 0xFF, + ILI9XXX_ENABLE_3_GAMMA, 0, 1, 0x00, + ILI9XXX_SET_GAMMA, 0, 1, 0x01, + ILI9XXX_SET_PGAMMA, 0, 15, 0x0F, 0x29, 0x24, 0x0C, 0x0E, 0x09, 0x4E, 0x78, 0x3C, 0x09, 0x13, 0x05, 0x17, 0x11, 0x00, + ILI9XXX_SET_NGAMMA, 0, 15, 0x00, 0x16, 0x1B, 0x04, 0x11, 0x07, 0x31, 0x33, 0x42, 0x05, 0x0C, 0x0A, 0x28, 0x2F, 0x0F, + ILI9XXX_SET_PIX_FMT, 0, 1, 0x05, + ILI9XXX_SET_FRAME_CTL_NORMAL, 0, 2, 0x00, 0x1B, + ILI9XXX_SET_FUNCTION_CTL, 0, 2, 0x0A, 0xA2, + ILI9XXX_CMD_PARTIAL_OFF, 0, 0, + ILI9XXX_CMD_DISPLAY_ON, 20, 0 + }; + // clang-format on + qp_comms_bulk_command_sequence(device, ili9341_init_sequence, sizeof(ili9341_init_sequence)); + + // Configure the rotation (i.e. the ordering and direction of memory writes in GRAM) + const uint8_t madctl[] = { + [QP_ROTATION_0] = ILI9XXX_MADCTL_BGR, + [QP_ROTATION_90] = ILI9XXX_MADCTL_BGR | ILI9XXX_MADCTL_MX | ILI9XXX_MADCTL_MV, + [QP_ROTATION_180] = ILI9XXX_MADCTL_BGR | ILI9XXX_MADCTL_MX | ILI9XXX_MADCTL_MY, + [QP_ROTATION_270] = ILI9XXX_MADCTL_BGR | ILI9XXX_MADCTL_MV | ILI9XXX_MADCTL_MY, + }; + qp_comms_command_databyte(device, ILI9XXX_SET_MEM_ACS_CTL, madctl[rotation]); + + return true; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Driver vtable + +const struct tft_panel_dc_reset_painter_driver_vtable_t ili9341_driver_vtable = { + .base = + { + .init = qp_ili9341_init, + .power = qp_tft_panel_power, + .clear = qp_tft_panel_clear, + .flush = qp_tft_panel_flush, + .pixdata = qp_tft_panel_pixdata, + .viewport = qp_tft_panel_viewport, + .palette_convert = qp_tft_panel_palette_convert, + .append_pixels = qp_tft_panel_append_pixels, + }, + .rgb888_to_native16bit = qp_rgb888_to_rgb565_swapped, + .num_window_bytes = 2, + .swap_window_coords = false, + .opcodes = + { + .display_on = ILI9XXX_CMD_DISPLAY_ON, + .display_off = ILI9XXX_CMD_DISPLAY_OFF, + .set_column_address = ILI9XXX_SET_COL_ADDR, + .set_row_address = ILI9XXX_SET_PAGE_ADDR, + .enable_writes = ILI9XXX_SET_MEM, + }, +}; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// SPI + +#ifdef QUANTUM_PAINTER_ILI9341_SPI_ENABLE + +// Factory function for creating a handle to the ILI9341 device +painter_device_t qp_ili9341_make_spi_device(uint16_t panel_width, uint16_t panel_height, pin_t chip_select_pin, pin_t dc_pin, pin_t reset_pin, uint16_t spi_divisor, int spi_mode) { + for (uint32_t i = 0; i < ILI9341_NUM_DEVICES; ++i) { + tft_panel_dc_reset_painter_device_t *driver = &ili9341_drivers[i]; + if (!driver->base.driver_vtable) { + driver->base.driver_vtable = (const struct painter_driver_vtable_t *)&ili9341_driver_vtable; + driver->base.comms_vtable = (const struct painter_comms_vtable_t *)&spi_comms_with_dc_vtable; + driver->base.native_bits_per_pixel = 16; // RGB565 + driver->base.panel_width = panel_width; + driver->base.panel_height = panel_height; + driver->base.rotation = QP_ROTATION_0; + driver->base.offset_x = 0; + driver->base.offset_y = 0; + + // SPI and other pin configuration + driver->base.comms_config = &driver->spi_dc_reset_config; + driver->spi_dc_reset_config.spi_config.chip_select_pin = chip_select_pin; + driver->spi_dc_reset_config.spi_config.divisor = spi_divisor; + driver->spi_dc_reset_config.spi_config.lsb_first = false; + driver->spi_dc_reset_config.spi_config.mode = spi_mode; + driver->spi_dc_reset_config.dc_pin = dc_pin; + driver->spi_dc_reset_config.reset_pin = reset_pin; + return (painter_device_t)driver; + } + } + return NULL; +} + +#endif // QUANTUM_PAINTER_ILI9341_SPI_ENABLE + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/drivers/painter/ili9xxx/qp_ili9341.h b/drivers/painter/ili9xxx/qp_ili9341.h new file mode 100644 index 0000000000..28b0152a84 --- /dev/null +++ b/drivers/painter/ili9xxx/qp_ili9341.h @@ -0,0 +1,37 @@ +// Copyright 2021 Nick Brassel (@tzarc) +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "gpio.h" +#include "qp_internal.h" + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Quantum Painter ILI9341 configurables (add to your keyboard's config.h) + +#ifndef ILI9341_NUM_DEVICES +/** + * @def This controls the maximum number of ILI9341 devices that Quantum Painter can communicate with at any one time. + * Increasing this number allows for multiple displays to be used. + */ +# define ILI9341_NUM_DEVICES 1 +#endif + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Quantum Painter ILI9341 device factories + +#ifdef QUANTUM_PAINTER_ILI9341_SPI_ENABLE +/** + * Factory method for an ILI9341 SPI LCD device. + * + * @param panel_width[in] the width of the display panel + * @param panel_height[in] the height of the display panel + * @param chip_select_pin[in] the GPIO pin used for SPI chip select + * @param dc_pin[in] the GPIO pin used for D/C control + * @param reset_pin[in] the GPIO pin used for RST + * @param spi_divisor[in] the SPI divisor to use when communicating with the display + * @param spi_mode[in] the SPI mode to use when communicating with the display + * @return the device handle used with all drawing routines in Quantum Painter + */ +painter_device_t qp_ili9341_make_spi_device(uint16_t panel_width, uint16_t panel_height, pin_t chip_select_pin, pin_t dc_pin, pin_t reset_pin, uint16_t spi_divisor, int spi_mode); +#endif // QUANTUM_PAINTER_ILI9341_SPI_ENABLE diff --git a/drivers/painter/ili9xxx/qp_ili9xxx_opcodes.h b/drivers/painter/ili9xxx/qp_ili9xxx_opcodes.h new file mode 100644 index 0000000000..1fa395cb89 --- /dev/null +++ b/drivers/painter/ili9xxx/qp_ili9xxx_opcodes.h @@ -0,0 +1,100 @@ +// Copyright 2021 Nick Brassel (@tzarc) +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Quantum Painter ILI9xxx command opcodes +#define ILI9XXX_CMD_NOP 0x00 // No operation +#define ILI9XXX_CMD_RESET 0x01 // Software reset +#define ILI9XXX_GET_ID_INFO 0x04 // Get ID information +#define ILI9XXX_GET_STATUS 0x09 // Get status +#define ILI9XXX_GET_PWR_MODE 0x0A // Get power mode +#define ILI9XXX_GET_MADCTL 0x0B // Get MADCTL +#define ILI9XXX_GET_PIX_FMT 0x0C // Get pixel format +#define ILI9XXX_GET_IMG_FMT 0x0D // Get image format +#define ILI9XXX_GET_SIG_MODE 0x0E // Get signal mode +#define ILI9XXX_GET_SELF_DIAG 0x0F // Get self-diagnostics +#define ILI9XXX_CMD_SLEEP_ON 0x10 // Enter sleep mode +#define ILI9XXX_CMD_SLEEP_OFF 0x11 // Exist sleep mode +#define ILI9XXX_CMD_PARTIAL_ON 0x12 // Enter partial mode +#define ILI9XXX_CMD_PARTIAL_OFF 0x13 // Exit partial mode +#define ILI9XXX_CMD_INVERT_ON 0x20 // Enter inverted mode +#define ILI9XXX_CMD_INVERT_OFF 0x21 // Exit inverted mode +#define ILI9XXX_SET_GAMMA 0x26 // Set gamma params +#define ILI9XXX_CMD_DISPLAY_OFF 0x28 // Disable display +#define ILI9XXX_CMD_DISPLAY_ON 0x29 // Enable display +#define ILI9XXX_SET_COL_ADDR 0x2A // Set column address +#define ILI9XXX_SET_PAGE_ADDR 0x2B // Set page address +#define ILI9XXX_SET_MEM 0x2C // Set memory +#define ILI9XXX_SET_COLOR 0x2D // Set color +#define ILI9XXX_GET_MEM 0x2E // Get memory +#define ILI9XXX_SET_PARTIAL_AREA 0x30 // Set partial area +#define ILI9XXX_SET_VSCROLL 0x33 // Set vertical scroll def +#define ILI9XXX_CMD_TEARING_ON 0x34 // Tearing line enabled +#define ILI9XXX_CMD_TEARING_OFF 0x35 // Tearing line disabled +#define ILI9XXX_SET_MEM_ACS_CTL 0x36 // Set mem access ctl +#define ILI9XXX_SET_VSCROLL_ADDR 0x37 // Set vscroll start addr +#define ILI9XXX_CMD_IDLE_OFF 0x38 // Exit idle mode +#define ILI9XXX_CMD_IDLE_ON 0x39 // Enter idle mode +#define ILI9XXX_SET_PIX_FMT 0x3A // Set pixel format +#define ILI9XXX_SET_MEM_CONT 0x3C // Set memory continue +#define ILI9XXX_GET_MEM_CONT 0x3E // Get memory continue +#define ILI9XXX_SET_TEAR_SCANLINE 0x44 // Set tearing scanline +#define ILI9XXX_GET_TEAR_SCANLINE 0x45 // Get tearing scanline +#define ILI9XXX_SET_BRIGHTNESS 0x51 // Set brightness +#define ILI9XXX_GET_BRIGHTNESS 0x52 // Get brightness +#define ILI9XXX_SET_DISPLAY_CTL 0x53 // Set display ctl +#define ILI9XXX_GET_DISPLAY_CTL 0x54 // Get display ctl +#define ILI9XXX_SET_CABC 0x55 // Set CABC +#define ILI9XXX_GET_CABC 0x56 // Get CABC +#define ILI9XXX_SET_CABC_MIN 0x5E // Set CABC min +#define ILI9XXX_GET_CABC_MIN 0x5F // Set CABC max +#define ILI9XXX_GET_ID1 0xDA // Get ID1 +#define ILI9XXX_GET_ID2 0xDB // Get ID2 +#define ILI9XXX_GET_ID3 0xDC // Get ID3 +#define ILI9XXX_SET_RGB_IF_SIG_CTL 0xB0 // RGB IF signal ctl +#define ILI9XXX_SET_FRAME_CTL_NORMAL 0xB1 // Set frame ctl (normal) +#define ILI9XXX_SET_FRAME_CTL_IDLE 0xB2 // Set frame ctl (idle) +#define ILI9XXX_SET_FRAME_CTL_PARTIAL 0xB3 // Set frame ctl (partial) +#define ILI9XXX_SET_INVERSION_CTL 0xB4 // Set inversion ctl +#define ILI9XXX_SET_BLANKING_PORCH_CTL 0xB5 // Set blanking porch ctl +#define ILI9XXX_SET_FUNCTION_CTL 0xB6 // Set function ctl +#define ILI9XXX_SET_ENTRY_MODE 0xB7 // Set entry mode +#define ILI9XXX_SET_LIGHT_CTL_1 0xB8 // Set backlight ctl 1 +#define ILI9XXX_SET_LIGHT_CTL_2 0xB9 // Set backlight ctl 2 +#define ILI9XXX_SET_LIGHT_CTL_3 0xBA // Set backlight ctl 3 +#define ILI9XXX_SET_LIGHT_CTL_4 0xBB // Set backlight ctl 4 +#define ILI9XXX_SET_LIGHT_CTL_5 0xBC // Set backlight ctl 5 +#define ILI9XXX_SET_LIGHT_CTL_7 0xBE // Set backlight ctl 7 +#define ILI9XXX_SET_LIGHT_CTL_8 0xBF // Set backlight ctl 8 +#define ILI9XXX_SET_POWER_CTL_1 0xC0 // Set power ctl 1 +#define ILI9XXX_SET_POWER_CTL_2 0xC1 // Set power ctl 2 +#define ILI9XXX_SET_VCOM_CTL_1 0xC5 // Set VCOM ctl 1 +#define ILI9XXX_SET_VCOM_CTL_2 0xC7 // Set VCOM ctl 2 +#define ILI9XXX_POWER_CTL_A 0xCB // Set power control A +#define ILI9XXX_POWER_CTL_B 0xCF // Set power control B +#define ILI9XXX_DRV_TIMING_CTL_A 0xE8 // Set driver timing control A +#define ILI9XXX_DRV_TIMING_CTL_B 0xEA // Set driver timing control B +#define ILI9XXX_POWER_ON_SEQ_CTL 0xED // Set Power on sequence control +#define ILI9XXX_SET_NVMEM 0xD0 // Set NVMEM data +#define ILI9XXX_GET_NVMEM_KEY 0xD1 // Get NVMEM protect key +#define ILI9XXX_GET_NVMEM_STATUS 0xD2 // Get NVMEM status +#define ILI9XXX_GET_ID4 0xD3 // Get ID4 +#define ILI9XXX_SET_PGAMMA 0xE0 // Set positive gamma +#define ILI9XXX_SET_NGAMMA 0xE1 // Set negative gamma +#define ILI9XXX_SET_DGAMMA_CTL_1 0xE2 // Set digital gamma ctl 1 +#define ILI9XXX_SET_DGAMMA_CTL_2 0xE3 // Set digital gamma ctl 2 +#define ILI9XXX_ENABLE_3_GAMMA 0xF2 // Enable 3 gamma +#define ILI9XXX_SET_IF_CTL 0xF6 // Set interface control +#define ILI9XXX_SET_PUMP_RATIO_CTL 0xF7 // Set pump ratio control + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// MADCTL Flags +#define ILI9XXX_MADCTL_MY 0b10000000 +#define ILI9XXX_MADCTL_MX 0b01000000 +#define ILI9XXX_MADCTL_MV 0b00100000 +#define ILI9XXX_MADCTL_ML 0b00010000 +#define ILI9XXX_MADCTL_RGB 0b00000000 +#define ILI9XXX_MADCTL_BGR 0b00001000 +#define ILI9XXX_MADCTL_MH 0b00000100 diff --git a/drivers/painter/ssd1351/qp_ssd1351.c b/drivers/painter/ssd1351/qp_ssd1351.c new file mode 100644 index 0000000000..970e7e67f3 --- /dev/null +++ b/drivers/painter/ssd1351/qp_ssd1351.c @@ -0,0 +1,125 @@ +// Copyright 2021 Nick Brassel (@tzarc) +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "qp_internal.h" +#include "qp_comms.h" +#include "qp_ssd1351.h" +#include "qp_ssd1351_opcodes.h" +#include "qp_tft_panel.h" + +#ifdef QUANTUM_PAINTER_SSD1351_SPI_ENABLE +# include "qp_comms_spi.h" +#endif // QUANTUM_PAINTER_SSD1351_SPI_ENABLE + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Common + +// Driver storage +tft_panel_dc_reset_painter_device_t ssd1351_drivers[SSD1351_NUM_DEVICES] = {0}; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Initialization + +bool qp_ssd1351_init(painter_device_t device, painter_rotation_t rotation) { + tft_panel_dc_reset_painter_device_t *driver = (tft_panel_dc_reset_painter_device_t *)device; + + // clang-format off + const uint8_t ssd1351_init_sequence[] = { + // Command, Delay, N, Data[N] + SSD1351_COMMANDLOCK, 5, 1, 0x12, + SSD1351_COMMANDLOCK, 5, 1, 0xB1, + SSD1351_DISPLAYOFF, 5, 0, + SSD1351_CLOCKDIV, 5, 1, 0xF1, + SSD1351_MUXRATIO, 5, 1, 0x7F, + SSD1351_DISPLAYOFFSET, 5, 1, 0x00, + SSD1351_SETGPIO, 5, 1, 0x00, + SSD1351_FUNCTIONSELECT, 5, 1, 0x01, + SSD1351_PRECHARGE, 5, 1, 0x32, + SSD1351_VCOMH, 5, 1, 0x05, + SSD1351_NORMALDISPLAY, 5, 0, + SSD1351_CONTRASTABC, 5, 3, 0xC8, 0x80, 0xC8, + SSD1351_CONTRASTMASTER, 5, 1, 0x0F, + SSD1351_SETVSL, 5, 3, 0xA0, 0xB5, 0x55, + SSD1351_PRECHARGE2, 5, 1, 0x01, + SSD1351_DISPLAYON, 5, 0, + }; + // clang-format on + qp_comms_bulk_command_sequence(device, ssd1351_init_sequence, sizeof(ssd1351_init_sequence)); + + // Configure the rotation (i.e. the ordering and direction of memory writes in GRAM) + const uint8_t madctl[] = { + [QP_ROTATION_0] = SSD1351_MADCTL_BGR | SSD1351_MADCTL_MY, + [QP_ROTATION_90] = SSD1351_MADCTL_BGR | SSD1351_MADCTL_MX | SSD1351_MADCTL_MY | SSD1351_MADCTL_MV, + [QP_ROTATION_180] = SSD1351_MADCTL_BGR | SSD1351_MADCTL_MX, + [QP_ROTATION_270] = SSD1351_MADCTL_BGR | SSD1351_MADCTL_MV, + }; + qp_comms_command_databyte(device, SSD1351_SETREMAP, madctl[rotation]); + qp_comms_command_databyte(device, SSD1351_STARTLINE, (rotation == QP_ROTATION_0 || rotation == QP_ROTATION_90) ? driver->base.panel_height : 0); + + return true; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Driver vtable + +const struct tft_panel_dc_reset_painter_driver_vtable_t ssd1351_driver_vtable = { + .base = + { + .init = qp_ssd1351_init, + .power = qp_tft_panel_power, + .clear = qp_tft_panel_clear, + .flush = qp_tft_panel_flush, + .pixdata = qp_tft_panel_pixdata, + .viewport = qp_tft_panel_viewport, + .palette_convert = qp_tft_panel_palette_convert, + .append_pixels = qp_tft_panel_append_pixels, + }, + .rgb888_to_native16bit = qp_rgb888_to_rgb565_swapped, + .num_window_bytes = 1, + .swap_window_coords = true, + .opcodes = + { + .display_on = SSD1351_DISPLAYON, + .display_off = SSD1351_DISPLAYOFF, + .set_column_address = SSD1351_SETCOLUMN, + .set_row_address = SSD1351_SETROW, + .enable_writes = SSD1351_WRITERAM, + }, +}; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// SPI + +#ifdef QUANTUM_PAINTER_SSD1351_SPI_ENABLE + +// Factory function for creating a handle to the SSD1351 device +painter_device_t qp_ssd1351_make_spi_device(uint16_t panel_width, uint16_t panel_height, pin_t chip_select_pin, pin_t dc_pin, pin_t reset_pin, uint16_t spi_divisor, int spi_mode) { + for (uint32_t i = 0; i < SSD1351_NUM_DEVICES; ++i) { + tft_panel_dc_reset_painter_device_t *driver = &ssd1351_drivers[i]; + if (!driver->base.driver_vtable) { + driver->base.driver_vtable = (const struct painter_driver_vtable_t *)&ssd1351_driver_vtable; + driver->base.comms_vtable = (const struct painter_comms_vtable_t *)&spi_comms_with_dc_vtable; + driver->base.panel_width = panel_width; + driver->base.panel_height = panel_height; + driver->base.rotation = QP_ROTATION_0; + driver->base.offset_x = 0; + driver->base.offset_y = 0; + driver->base.native_bits_per_pixel = 16; // RGB565 + + // SPI and other pin configuration + driver->base.comms_config = &driver->spi_dc_reset_config; + driver->spi_dc_reset_config.spi_config.chip_select_pin = chip_select_pin; + driver->spi_dc_reset_config.spi_config.divisor = spi_divisor; + driver->spi_dc_reset_config.spi_config.lsb_first = false; + driver->spi_dc_reset_config.spi_config.mode = spi_mode; + driver->spi_dc_reset_config.dc_pin = dc_pin; + driver->spi_dc_reset_config.reset_pin = reset_pin; + return (painter_device_t)driver; + } + } + return NULL; +} + +#endif // QUANTUM_PAINTER_SSD1351_SPI_ENABLE + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/drivers/painter/ssd1351/qp_ssd1351.h b/drivers/painter/ssd1351/qp_ssd1351.h new file mode 100644 index 0000000000..0df34f204d --- /dev/null +++ b/drivers/painter/ssd1351/qp_ssd1351.h @@ -0,0 +1,37 @@ +// Copyright 2021 Nick Brassel (@tzarc) +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "gpio.h" +#include "qp_internal.h" + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Quantum Painter SSD1351 configurables (add to your keyboard's config.h) + +#ifndef SSD1351_NUM_DEVICES +/** + * @def This controls the maximum number of SSD1351 devices that Quantum Painter can communicate with at any one time. + * Increasing this number allows for multiple displays to be used. + */ +# define SSD1351_NUM_DEVICES 1 +#endif + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Quantum Painter SSD1351 device factories + +#ifdef QUANTUM_PAINTER_SSD1351_SPI_ENABLE +/** + * Factory method for an SSD1351 SPI OLED device. + * + * @param panel_width[in] the width of the display panel + * @param panel_height[in] the height of the display panel + * @param chip_select_pin[in] the GPIO pin used for SPI chip select + * @param dc_pin[in] the GPIO pin used for D/C control + * @param reset_pin[in] the GPIO pin used for RST + * @param spi_divisor[in] the SPI divisor to use when communicating with the display + * @param spi_mode[in] the SPI mode to use when communicating with the display + * @return the device handle used with all drawing routines in Quantum Painter + */ +painter_device_t qp_ssd1351_make_spi_device(uint16_t panel_width, uint16_t panel_height, pin_t chip_select_pin, pin_t dc_pin, pin_t reset_pin, uint16_t spi_divisor, int spi_mode); +#endif // QUANTUM_PAINTER_SSD1351_SPI_ENABLE diff --git a/drivers/painter/ssd1351/qp_ssd1351_opcodes.h b/drivers/painter/ssd1351/qp_ssd1351_opcodes.h new file mode 100644 index 0000000000..48ed2a3a7c --- /dev/null +++ b/drivers/painter/ssd1351/qp_ssd1351_opcodes.h @@ -0,0 +1,48 @@ +// Copyright 2021 Nick Brassel (@tzarc) +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Quantum Painter SSD1351 command opcodes + +// System function commands +#define SSD1351_SETCOLUMN 0x15 +#define SSD1351_SETROW 0x75 +#define SSD1351_WRITERAM 0x5C +#define SSD1351_READRAM 0x5D +#define SSD1351_SETREMAP 0xA0 +#define SSD1351_STARTLINE 0xA1 +#define SSD1351_DISPLAYOFFSET 0xA2 +#define SSD1351_DISPLAYALLOFF 0xA4 +#define SSD1351_DISPLAYALLON 0xA5 +#define SSD1351_NORMALDISPLAY 0xA6 +#define SSD1351_INVERTDISPLAY 0xA7 +#define SSD1351_FUNCTIONSELECT 0xAB +#define SSD1351_DISPLAYOFF 0xAE +#define SSD1351_DISPLAYON 0xAF +#define SSD1351_PRECHARGE 0xB1 +#define SSD1351_DISPLAYENHANCE 0xB2 +#define SSD1351_CLOCKDIV 0xB3 +#define SSD1351_SETVSL 0xB4 +#define SSD1351_SETGPIO 0xB5 +#define SSD1351_PRECHARGE2 0xB6 +#define SSD1351_SETGRAY 0xB8 +#define SSD1351_USELUT 0xB9 +#define SSD1351_PRECHARGELEVEL 0xBB +#define SSD1351_VCOMH 0xBE +#define SSD1351_CONTRASTABC 0xC1 +#define SSD1351_CONTRASTMASTER 0xC7 +#define SSD1351_MUXRATIO 0xCA +#define SSD1351_COMMANDLOCK 0xFD +#define SSD1351_HORIZSCROLL 0x96 +#define SSD1351_STOPSCROLL 0x9E +#define SSD1351_STARTSCROLL 0x9F + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// SETREMAP (MADCTL) Flags +#define SSD1351_MADCTL_MY 0b00010000 +#define SSD1351_MADCTL_MX 0b00000010 +#define SSD1351_MADCTL_MV 0b00000001 +#define SSD1351_MADCTL_RGB 0b01100000 +#define SSD1351_MADCTL_BGR 0b01100100 diff --git a/drivers/painter/st77xx/qp_st7789.c b/drivers/painter/st77xx/qp_st7789.c new file mode 100644 index 0000000000..d005ece050 --- /dev/null +++ b/drivers/painter/st77xx/qp_st7789.c @@ -0,0 +1,144 @@ +// 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_st7789.h" +#include "qp_st77xx_opcodes.h" +#include "qp_st7789_opcodes.h" +#include "qp_tft_panel.h" + +#ifdef QUANTUM_PAINTER_ST7789_SPI_ENABLE +# include "qp_comms_spi.h" +#endif // QUANTUM_PAINTER_ST7789_SPI_ENABLE + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Common + +// Driver storage +tft_panel_dc_reset_painter_device_t st7789_drivers[ST7789_NUM_DEVICES] = {0}; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Automatic viewport offsets + +#ifndef ST7789_NO_AUTOMATIC_OFFSETS +static inline void st7789_automatic_viewport_offsets(painter_device_t device, painter_rotation_t rotation) { + struct painter_driver_t *driver = (struct painter_driver_t *)device; + + // clang-format off + const struct { + uint16_t offset_x; + uint16_t offset_y; + } rotation_offsets_240x240[] = { + [QP_ROTATION_0] = { .offset_x = 0, .offset_y = 0 }, + [QP_ROTATION_90] = { .offset_x = 0, .offset_y = 0 }, + [QP_ROTATION_180] = { .offset_x = 0, .offset_y = 80 }, + [QP_ROTATION_270] = { .offset_x = 80, .offset_y = 0 }, + }; + // clang-format on + + if (driver->panel_width == 240 && driver->panel_height == 240) { + driver->offset_x = rotation_offsets_240x240[rotation].offset_x; + driver->offset_y = rotation_offsets_240x240[rotation].offset_y; + } +} +#endif // ST7789_NO_AUTOMATIC_OFFSETS + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Initialization + +bool qp_st7789_init(painter_device_t device, painter_rotation_t rotation) { + // clang-format off + const uint8_t st7789_init_sequence[] = { + // Command, Delay, N, Data[N] + ST77XX_CMD_RESET, 120, 0, + ST77XX_CMD_SLEEP_OFF, 5, 0, + ST77XX_SET_PIX_FMT, 0, 1, 0x55, + ST77XX_CMD_INVERT_ON, 0, 0, + ST77XX_CMD_NORMAL_ON, 0, 0, + ST77XX_CMD_DISPLAY_ON, 20, 0 + }; + // clang-format on + qp_comms_bulk_command_sequence(device, st7789_init_sequence, sizeof(st7789_init_sequence)); + + // Configure the rotation (i.e. the ordering and direction of memory writes in GRAM) + const uint8_t madctl[] = { + [QP_ROTATION_0] = ST77XX_MADCTL_RGB, + [QP_ROTATION_90] = ST77XX_MADCTL_RGB | ST77XX_MADCTL_MX | ST77XX_MADCTL_MV, + [QP_ROTATION_180] = ST77XX_MADCTL_RGB | ST77XX_MADCTL_MX | ST77XX_MADCTL_MY, + [QP_ROTATION_270] = ST77XX_MADCTL_RGB | ST77XX_MADCTL_MV | ST77XX_MADCTL_MY, + }; + qp_comms_command_databyte(device, ST77XX_SET_MADCTL, madctl[rotation]); + +#ifndef ST7789_NO_AUTOMATIC_VIEWPORT_OFFSETS + st7789_automatic_viewport_offsets(device, rotation); +#endif // ST7789_NO_AUTOMATIC_VIEWPORT_OFFSETS + + return true; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Driver vtable + +const struct tft_panel_dc_reset_painter_driver_vtable_t st7789_driver_vtable = { + .base = + { + .init = qp_st7789_init, + .power = qp_tft_panel_power, + .clear = qp_tft_panel_clear, + .flush = qp_tft_panel_flush, + .pixdata = qp_tft_panel_pixdata, + .viewport = qp_tft_panel_viewport, + .palette_convert = qp_tft_panel_palette_convert, + .append_pixels = qp_tft_panel_append_pixels, + }, + .rgb888_to_native16bit = qp_rgb888_to_rgb565_swapped, + .num_window_bytes = 2, + .swap_window_coords = false, + .opcodes = + { + .display_on = ST77XX_CMD_DISPLAY_ON, + .display_off = ST77XX_CMD_DISPLAY_OFF, + .set_column_address = ST77XX_SET_COL_ADDR, + .set_row_address = ST77XX_SET_ROW_ADDR, + .enable_writes = ST77XX_SET_MEM, + }, +}; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// SPI + +#ifdef QUANTUM_PAINTER_ST7789_SPI_ENABLE + +// Factory function for creating a handle to the ST7789 device +painter_device_t qp_st7789_make_spi_device(uint16_t panel_width, uint16_t panel_height, pin_t chip_select_pin, pin_t dc_pin, pin_t reset_pin, uint16_t spi_divisor, int spi_mode) { + for (uint32_t i = 0; i < ST7789_NUM_DEVICES; ++i) { + tft_panel_dc_reset_painter_device_t *driver = &st7789_drivers[i]; + if (!driver->base.driver_vtable) { + driver->base.driver_vtable = (const struct painter_driver_vtable_t *)&st7789_driver_vtable; + driver->base.comms_vtable = (const struct painter_comms_vtable_t *)&spi_comms_with_dc_vtable; + driver->base.panel_width = panel_width; + driver->base.panel_height = panel_height; + driver->base.rotation = QP_ROTATION_0; + driver->base.offset_x = 0; + driver->base.offset_y = 0; + driver->base.native_bits_per_pixel = 16; // RGB565 + + // SPI and other pin configuration + driver->base.comms_config = &driver->spi_dc_reset_config; + driver->spi_dc_reset_config.spi_config.chip_select_pin = chip_select_pin; + driver->spi_dc_reset_config.spi_config.divisor = spi_divisor; + driver->spi_dc_reset_config.spi_config.lsb_first = false; + driver->spi_dc_reset_config.spi_config.mode = spi_mode; + driver->spi_dc_reset_config.dc_pin = dc_pin; + driver->spi_dc_reset_config.reset_pin = reset_pin; + return (painter_device_t)driver; + } + } + return NULL; +} + +#endif // QUANTUM_PAINTER_ST7789_SPI_ENABLE + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/drivers/painter/st77xx/qp_st7789.h b/drivers/painter/st77xx/qp_st7789.h new file mode 100644 index 0000000000..ec61f5d70b --- /dev/null +++ b/drivers/painter/st77xx/qp_st7789.h @@ -0,0 +1,44 @@ +// Copyright 2021 Paul Cotter (@gr1mr3aver) +// Copyright 2021 Nick Brassel (@tzarc) +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "gpio.h" +#include "qp_internal.h" + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Quantum Painter ST7789 configurables (add to your keyboard's config.h) + +#ifndef ST7789_NUM_DEVICES +/** + * @def This controls the maximum number of ST7789 devices that Quantum Painter can communicate with at any one time. + * Increasing this number allows for multiple displays to be used. + */ +# define ST7789_NUM_DEVICES 1 +#endif + +// Additional configuration options to be copied to your keyboard's config.h (don't change here): + +// If you know exactly which offsets should be used on your panel with respect to selected rotation, then this config +// option allows you to save some flash space -- you'll need to invoke qp_set_viewport_offsets() instead from your keyboard. +// #define ST7789_NO_AUTOMATIC_VIEWPORT_OFFSETS + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Quantum Painter ST7789 device factories + +#ifdef QUANTUM_PAINTER_ST7789_SPI_ENABLE +/** + * Factory method for an ST7789 SPI LCD device. + * + * @param panel_width[in] the width of the display panel + * @param panel_height[in] the height of the display panel + * @param chip_select_pin[in] the GPIO pin used for SPI chip select + * @param dc_pin[in] the GPIO pin used for D/C control + * @param reset_pin[in] the GPIO pin used for RST + * @param spi_divisor[in] the SPI divisor to use when communicating with the display + * @param spi_mode[in] the SPI mode to use when communicating with the display + * @return the device handle used with all drawing routines in Quantum Painter + */ +painter_device_t qp_st7789_make_spi_device(uint16_t panel_width, uint16_t panel_height, pin_t chip_select_pin, pin_t dc_pin, pin_t reset_pin, uint16_t spi_divisor, int spi_mode); +#endif // QUANTUM_PAINTER_ST7789_SPI_ENABLE diff --git a/drivers/painter/st77xx/qp_st7789_opcodes.h b/drivers/painter/st77xx/qp_st7789_opcodes.h new file mode 100644 index 0000000000..b5baba7184 --- /dev/null +++ b/drivers/painter/st77xx/qp_st7789_opcodes.h @@ -0,0 +1,64 @@ +// Copyright 2021 Paul Cotter (@gr1mr3aver) +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Quantum Painter ST7789 additional command opcodes + +// System function commands +#define ST7789_GET_SELF_DIAG 0x0F // Get self-diagnostic result +#define ST7789_SET_VERT_SCRL 0x33 // Set vertical scroll definition +#define ST7789_SET_VERT_SCRL_ADDR 0x37 // SEt Vertical scroll start address +#define ST7789_SET_MEM_CONT 0x3C // Memory Write continue +#define ST7789_GET_MEM_CONT 0x3E // Memory Read continue +#define ST7789_SET_TEAR_LINE 0x44 // Set tear scanline +#define ST7789_GET_TEAR_LINE 0x45 // Get tear scanline +#define ST7789_SET_BRIGHTNESS 0x51 // Set display brightness +#define ST7789_GET_BRIGHTNESS 0x52 // Get display brightness +#define ST7789_SET_CTRL 0x53 // Set CTRL display +#define ST7789_GET_CTRL 0x54 // Get CTRL display value +#define ST7789_SET_CAB_COLOR 0x55 // Set content adaptive brightness control and color enhancement +#define ST7789_GET_CAB_COLOR 0x56 // Get content adaptive brightness control and color enhancement +#define ST7789_SET_CAB_BRIGHTNESS 0x5E // Set content adaptive minimum brightness +#define ST7789_GET_CAB_BRIGHTNESS 0x5F // Get content adaptive minimum brightness +#define ST7789_GET_ABC_SELF_DIAG 0x68 // Get Auto brightness control self diagnostics + +// Panel Function Commands +#define ST7789_SET_RAM_CTL 0xB0 // Set RAM control +#define ST7789_SET_RGB_CTL 0xB1 // Set RGB control +#define ST7789_SET_PORCH_CTL 0xB2 // Set Porch control +#define ST7789_SET_FRAME_RATE_CTL_1 0xB3 // Set frame rate control 1 +#define ST7789_SET_PARTIAL_CTL 0xB5 // Set Partial control +#define ST7789_SET_GATE_CTL 0xB7 // Set gate control +#define ST7789_SET_GATE_ON_TIMING 0xB8 // Set gate on timing adjustment +#define ST7789_SET_DIGITAL_GAMMA_ON 0xBA // Enable digital gamma +#define ST7789_SET_VCOM 0xBB // Set VCOM +#define ST7789_SET_POWER_SAVE 0xBC // Set power saving mode +#define ST7789_SET_DISP_OFF_POWER 0xBD // Set display off power saving +#define ST7789_SET_LCM_CTL 0xC0 // Set LCM control +#define ST7789_SET_IDS 0xC1 // Set IDs +#define ST7789_SET_VDV_VRH_ON 0xC2 // Set VDV and VRH command enable +#define ST7789_SET_VRH 0xC3 // Set VRH +#define ST7789_SET_VDV 0xC4 // Set VDV +#define ST7789_SET_VCOM_OFFSET 0xC5 // Set VCOM offset ctl +#define ST7789_SET_FRAME_RATE_CTL_2 0xC6 // Set frame rate control 2 +#define ST7789_SET_CABC_CTL 0xC7 // Set CABC Control +#define ST7789_GET_REG_1 0xC8 // Get register value selection1 +#define ST7789_GET_REG_2 0xCA // Get register value selection2 +#define ST7789_SET_PWM_FREQ 0xCC // Set PWM frequency +#define ST7789_SET_POWER_CTL_1 0xD0 // Set power ctl 1 +#define ST7789_SET_VAP_VAN_ON 0xD2 // Enable VAP/VAN signal output +#define ST7789_SET_CMD2_ENABLE 0xDF // Enable command 2 +#define ST7789_SET_PGAMMA 0xE0 // Set positive gamma +#define ST7789_SET_NGAMMA 0xE1 // Set negative gamma +#define ST7789_SET_DIGITAL_GAMMA_RED 0xE2 // Set digital gamma lookup table for red +#define ST7789_SET_DIGITAL_GAMMA_BLUE 0xE3 // Get digital gamma lookup table for blue +#define ST7789_SET_GATE_CTL_2 0xE4 // Set gate control 2 +#define ST7789_SET_SPI2_ENABLE 0xE7 // Enable SPI2 +#define ST7789_SET_POWER_CTL_2 0xE8 // Set power ctl 2 +#define ST7789_SET_EQ_TIME_CTL 0xE9 // Set equalize time control +#define ST7789_SET_PROG_CTL 0xEC // Set program control +#define ST7789_SET_PROG_MODE_ENABLE 0xFA // Set program mode enable +#define ST7789_SET_NVMEM 0xFC // Set NVMEM data +#define ST7789_SET_PROG_ACTION 0xFE // Set program action diff --git a/drivers/painter/st77xx/qp_st77xx_opcodes.h b/drivers/painter/st77xx/qp_st77xx_opcodes.h new file mode 100644 index 0000000000..131378d832 --- /dev/null +++ b/drivers/painter/st77xx/qp_st77xx_opcodes.h @@ -0,0 +1,51 @@ +// Copyright 2021 Paul Cotter (@gr1mr3aver) +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Quantum Painter ST77XX command opcodes + +// System function commands +#define ST77XX_CMD_NOP 0x00 // No operation +#define ST77XX_CMD_RESET 0x01 // Software reset +#define ST77XX_GET_ID_INFO 0x04 // Get ID information +#define ST77XX_GET_STATUS 0x09 // Get status +#define ST77XX_GET_PWR_MODE 0x0A // Get power mode +#define ST77XX_GET_MADCTL 0x0B // Get mem access ctl +#define ST77XX_GET_PIX_FMT 0x0C // Get pixel format +#define ST77XX_GET_IMG_FMT 0x0D // Get image format +#define ST77XX_GET_SIG_MODE 0x0E // Get signal mode +#define ST77XX_CMD_SLEEP_ON 0x10 // Enter sleep mode +#define ST77XX_CMD_SLEEP_OFF 0x11 // Exist sleep mode +#define ST77XX_CMD_PARTIAL_ON 0x12 // Enter partial mode +#define ST77XX_CMD_NORMAL_ON 0x13 // Exit partial mode +#define ST77XX_CMD_INVERT_OFF 0x20 // Exit inverted mode +#define ST77XX_CMD_INVERT_ON 0x21 // Enter inverted mode +#define ST77XX_SET_GAMMA 0x26 // Set gamma params +#define ST77XX_CMD_DISPLAY_OFF 0x28 // Disable display +#define ST77XX_CMD_DISPLAY_ON 0x29 // Enable display +#define ST77XX_SET_COL_ADDR 0x2A // Set column address +#define ST77XX_SET_ROW_ADDR 0x2B // Set page (row) address +#define ST77XX_SET_MEM 0x2C // Set memory +#define ST77XX_GET_MEM 0x2E // Get memory +#define ST77XX_SET_PARTIAL_AREA 0x30 // Set partial area +#define ST77XX_CMD_TEARING_OFF 0x34 // Tearing line disabled +#define ST77XX_CMD_TEARING_ON 0x35 // Tearing line enabled +#define ST77XX_SET_MADCTL 0x36 // Set mem access ctl +#define ST77XX_CMD_IDLE_OFF 0x38 // Exit idle mode +#define ST77XX_CMD_IDLE_ON 0x39 // Enter idle mode +#define ST77XX_SET_PIX_FMT 0x3A // Set pixel format +#define ST77XX_GET_ID1 0xDA // Get ID1 +#define ST77XX_GET_ID2 0xDB // Get ID2 +#define ST77XX_GET_ID3 0xDC // Get ID3 + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// MADCTL Flags +#define ST77XX_MADCTL_MY 0b10000000 +#define ST77XX_MADCTL_MX 0b01000000 +#define ST77XX_MADCTL_MV 0b00100000 +#define ST77XX_MADCTL_ML 0b00010000 +#define ST77XX_MADCTL_RGB 0b00000000 +#define ST77XX_MADCTL_BGR 0b00001000 +#define ST77XX_MADCTL_MH 0b00000100 diff --git a/drivers/painter/tft_panel/qp_tft_panel.c b/drivers/painter/tft_panel/qp_tft_panel.c new file mode 100644 index 0000000000..4d636c9509 --- /dev/null +++ b/drivers/painter/tft_panel/qp_tft_panel.c @@ -0,0 +1,130 @@ +// Copyright 2021 Nick Brassel (@tzarc) +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "color.h" +#include "qp_internal.h" +#include "qp_comms.h" +#include "qp_draw.h" +#include "qp_tft_panel.h" + +#define BYTE_SWAP(x) (((((uint16_t)(x)) >> 8) & 0x00FF) | ((((uint16_t)(x)) << 8) & 0xFF00)) + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Native pixel format conversion + +uint16_t qp_rgb888_to_rgb565(uint8_t r, uint8_t g, uint8_t b) { + uint16_t rgb565 = (((uint16_t)r) >> 3) << 11 | (((uint16_t)g) >> 2) << 5 | (((uint16_t)b) >> 3); + return rgb565; +} + +uint16_t qp_rgb888_to_rgb565_swapped(uint8_t r, uint8_t g, uint8_t b) { + uint16_t rgb565 = (((uint16_t)r) >> 3) << 11 | (((uint16_t)g) >> 2) << 5 | (((uint16_t)b) >> 3); + return BYTE_SWAP(rgb565); +} + +uint16_t qp_rgb888_to_bgr565(uint8_t r, uint8_t g, uint8_t b) { + uint16_t bgr565 = (((uint16_t)b) >> 3) << 11 | (((uint16_t)g) >> 2) << 5 | (((uint16_t)r) >> 3); + return bgr565; +} + +uint16_t qp_rgb888_to_bgr565_swapped(uint8_t r, uint8_t g, uint8_t b) { + uint16_t bgr565 = (((uint16_t)b) >> 3) << 11 | (((uint16_t)g) >> 2) << 5 | (((uint16_t)r) >> 3); + return BYTE_SWAP(bgr565); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Quantum Painter API implementations + +// Power control +bool qp_tft_panel_power(painter_device_t device, bool power_on) { + struct painter_driver_t * driver = (struct painter_driver_t *)device; + struct tft_panel_dc_reset_painter_driver_vtable_t *vtable = (struct tft_panel_dc_reset_painter_driver_vtable_t *)driver->driver_vtable; + qp_comms_command(device, power_on ? vtable->opcodes.display_on : vtable->opcodes.display_off); + return true; +} + +// Screen clear +bool qp_tft_panel_clear(painter_device_t device) { + struct painter_driver_t *driver = (struct painter_driver_t *)device; + driver->driver_vtable->init(device, driver->rotation); // Re-init the LCD + return true; +} + +// Screen flush +bool qp_tft_panel_flush(painter_device_t device) { + // No-op, as there's no framebuffer in RAM for this device. + return true; +} + +// Viewport to draw to +bool qp_tft_panel_viewport(painter_device_t device, uint16_t left, uint16_t top, uint16_t right, uint16_t bottom) { + struct painter_driver_t * driver = (struct painter_driver_t *)device; + struct tft_panel_dc_reset_painter_driver_vtable_t *vtable = (struct tft_panel_dc_reset_painter_driver_vtable_t *)driver->driver_vtable; + + // Fix up the drawing location if required + left += driver->offset_x; + right += driver->offset_x; + top += driver->offset_y; + bottom += driver->offset_y; + + // Check if we need to manually swap the window coordinates based on whether or not we're in a sideways rotation + if (vtable->swap_window_coords && (driver->rotation == QP_ROTATION_90 || driver->rotation == QP_ROTATION_270)) { + uint16_t temp; + + temp = left; + left = top; + top = temp; + + temp = right; + right = bottom; + bottom = temp; + } + + if (vtable->num_window_bytes == 1) { + // Set up the x-window + uint8_t xbuf[2] = {left & 0xFF, right & 0xFF}; + qp_comms_command_databuf(device, vtable->opcodes.set_column_address, xbuf, sizeof(xbuf)); + + // Set up the y-window + uint8_t ybuf[2] = {top & 0xFF, bottom & 0xFF}; + qp_comms_command_databuf(device, vtable->opcodes.set_row_address, ybuf, sizeof(ybuf)); + } else if (vtable->num_window_bytes == 2) { + // Set up the x-window + uint8_t xbuf[4] = {left >> 8, left & 0xFF, right >> 8, right & 0xFF}; + qp_comms_command_databuf(device, vtable->opcodes.set_column_address, xbuf, sizeof(xbuf)); + + // Set up the y-window + uint8_t ybuf[4] = {top >> 8, top & 0xFF, bottom >> 8, bottom & 0xFF}; + qp_comms_command_databuf(device, vtable->opcodes.set_row_address, ybuf, sizeof(ybuf)); + } + + // Lock in the window + qp_comms_command(device, vtable->opcodes.enable_writes); + return true; +} + +// Stream pixel data to the current write position in GRAM +bool qp_tft_panel_pixdata(painter_device_t device, const void *pixel_data, uint32_t native_pixel_count) { + qp_comms_send(device, pixel_data, native_pixel_count * sizeof(uint16_t)); + return true; +} + +// Convert supplied palette entries into their native equivalents +bool qp_tft_panel_palette_convert(painter_device_t device, int16_t palette_size, qp_pixel_t *palette) { + struct painter_driver_t * driver = (struct painter_driver_t *)device; + struct tft_panel_dc_reset_painter_driver_vtable_t *vtable = (struct tft_panel_dc_reset_painter_driver_vtable_t *)driver->driver_vtable; + for (int16_t i = 0; i < palette_size; ++i) { + RGB rgb = hsv_to_rgb_nocie((HSV){palette[i].hsv888.h, palette[i].hsv888.s, palette[i].hsv888.v}); + palette[i].rgb565 = vtable->rgb888_to_native16bit(rgb.r, rgb.g, rgb.b); + } + return true; +} + +// Append pixels to the target location, keyed by the pixel index +bool qp_tft_panel_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) { + uint16_t *buf = (uint16_t *)target_buffer; + for (uint32_t i = 0; i < pixel_count; ++i) { + buf[pixel_offset + i] = palette[palette_indices[i]].rgb565; + } + return true; +} diff --git a/drivers/painter/tft_panel/qp_tft_panel.h b/drivers/painter/tft_panel/qp_tft_panel.h new file mode 100644 index 0000000000..6eddfc503d --- /dev/null +++ b/drivers/painter/tft_panel/qp_tft_panel.h @@ -0,0 +1,67 @@ +// Copyright 2021 Nick Brassel (@tzarc) +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "color.h" +#include "qp_internal.h" + +#ifdef QUANTUM_PAINTER_SPI_ENABLE +# include "qp_comms_spi.h" +#endif // QUANTUM_PAINTER_SPI_ENABLE + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Common TFT panel implementation using D/C, and RST pins. + +typedef uint16_t (*rgb888_to_native_uint16_t)(uint8_t r, uint8_t g, uint8_t b); + +// Driver vtable with extras +struct tft_panel_dc_reset_painter_driver_vtable_t { + struct painter_driver_vtable_t base; // must be first, so it can be cast to/from the painter_driver_vtable_t* type + + // Conversion function for palette entries + rgb888_to_native_uint16_t rgb888_to_native16bit; + + // Number of bytes for transmitting x/y coordinates + uint8_t num_window_bytes; + + // Whether or not the x/y coords should be swapped on 90/270 rotation + bool swap_window_coords; + + // Opcodes for normal display operation + struct { + uint8_t display_on; + uint8_t display_off; + uint8_t set_column_address; + uint8_t set_row_address; + uint8_t enable_writes; + } opcodes; +}; + +// Device definition +typedef struct tft_panel_dc_reset_painter_device_t { + struct painter_driver_t base; // must be first, so it can be cast to/from the painter_device_t* type + + union { +#ifdef QUANTUM_PAINTER_SPI_ENABLE + // SPI-based configurables + struct qp_comms_spi_dc_reset_config_t spi_dc_reset_config; +#endif // QUANTUM_PAINTER_SPI_ENABLE + + // TODO: I2C/parallel etc. + }; +} tft_panel_dc_reset_painter_device_t; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Forward declarations for injecting into concrete driver vtables + +bool qp_tft_panel_power(painter_device_t device, bool power_on); +bool qp_tft_panel_clear(painter_device_t device); +bool qp_tft_panel_flush(painter_device_t device); +bool qp_tft_panel_viewport(painter_device_t device, uint16_t left, uint16_t top, uint16_t right, uint16_t bottom); +bool qp_tft_panel_pixdata(painter_device_t device, const void *pixel_data, uint32_t native_pixel_count); +bool qp_tft_panel_palette_convert(painter_device_t device, int16_t palette_size, qp_pixel_t *palette); +bool qp_tft_panel_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); + +uint16_t qp_rgb888_to_rgb565(uint8_t r, uint8_t g, uint8_t b); +uint16_t qp_rgb888_to_rgb565_swapped(uint8_t r, uint8_t g, uint8_t b); +uint16_t qp_rgb888_to_bgr565(uint8_t r, uint8_t g, uint8_t b); +uint16_t qp_rgb888_to_bgr565_swapped(uint8_t r, uint8_t g, uint8_t b); diff --git a/lib/python/qmk/cli/__init__.py b/lib/python/qmk/cli/__init__.py index 5f65e677e5..85baa238a8 100644 --- a/lib/python/qmk/cli/__init__.py +++ b/lib/python/qmk/cli/__init__.py @@ -16,7 +16,8 @@ import_names = { # A mapping of package name to importable name 'pep8-naming': 'pep8ext_naming', 'pyusb': 'usb.core', - 'qmk-dotty-dict': 'dotty_dict' + 'qmk-dotty-dict': 'dotty_dict', + 'pillow': 'PIL' } safe_commands = [ @@ -67,6 +68,7 @@ subcommands = [ 'qmk.cli.multibuild', 'qmk.cli.new.keyboard', 'qmk.cli.new.keymap', + 'qmk.cli.painter', 'qmk.cli.pyformat', 'qmk.cli.pytest', 'qmk.cli.via2json', diff --git a/lib/python/qmk/cli/painter/__init__.py b/lib/python/qmk/cli/painter/__init__.py new file mode 100644 index 0000000000..d1a225346c --- /dev/null +++ b/lib/python/qmk/cli/painter/__init__.py @@ -0,0 +1,2 @@ +from . import convert_graphics +from . import make_font diff --git a/lib/python/qmk/cli/painter/convert_graphics.py b/lib/python/qmk/cli/painter/convert_graphics.py new file mode 100644 index 0000000000..bbc30d26ff --- /dev/null +++ b/lib/python/qmk/cli/painter/convert_graphics.py @@ -0,0 +1,86 @@ +"""This script tests QGF functionality. +""" +import re +import datetime +from io import BytesIO +from qmk.path import normpath +from qmk.painter import render_header, render_source, render_license, render_bytes, valid_formats +from milc import cli +from PIL import Image + + +@cli.argument('-v', '--verbose', arg_only=True, action='store_true', help='Turns on verbose output.') +@cli.argument('-i', '--input', required=True, help='Specify input graphic file.') +@cli.argument('-o', '--output', default='', help='Specify output directory. Defaults to same directory as input.') +@cli.argument('-f', '--format', required=True, help='Output format, valid types: %s' % (', '.join(valid_formats.keys()))) +@cli.argument('-r', '--no-rle', arg_only=True, action='store_true', help='Disables the use of RLE when encoding images.') +@cli.argument('-d', '--no-deltas', arg_only=True, action='store_true', help='Disables the use of delta frames when encoding animations.') +@cli.subcommand('Converts an input image to something QMK understands') +def painter_convert_graphics(cli): + """Converts an image file to a format that Quantum Painter understands. + + This command uses the `qmk.painter` module to generate a Quantum Painter image defintion from an image. The generated definitions are written to a files next to the input -- `INPUT.c` and `INPUT.h`. + """ + # Work out the input file + if cli.args.input != '-': + cli.args.input = normpath(cli.args.input) + + # Error checking + if not cli.args.input.exists(): + cli.log.error('Input image file does not exist!') + cli.print_usage() + return False + + # Work out the output directory + if len(cli.args.output) == 0: + cli.args.output = cli.args.input.parent + cli.args.output = normpath(cli.args.output) + + # Ensure we have a valid format + if cli.args.format not in valid_formats.keys(): + cli.log.error('Output format %s is invalid. Allowed values: %s' % (cli.args.format, ', '.join(valid_formats.keys()))) + cli.print_usage() + return False + + # Work out the encoding parameters + format = valid_formats[cli.args.format] + + # Load the input image + input_img = Image.open(cli.args.input) + + # Convert the image to QGF using PIL + out_data = BytesIO() + input_img.save(out_data, "QGF", use_deltas=(not cli.args.no_deltas), use_rle=(not cli.args.no_rle), qmk_format=format, verbose=cli.args.verbose) + out_bytes = out_data.getvalue() + + # Work out the text substitutions for rendering the output data + subs = { + 'generated_type': 'image', + 'var_prefix': 'gfx', + 'generator_command': f'qmk painter-convert-graphics -i {cli.args.input.name} -f {cli.args.format}', + 'year': datetime.date.today().strftime("%Y"), + 'input_file': cli.args.input.name, + 'sane_name': re.sub(r"[^a-zA-Z0-9]", "_", cli.args.input.stem), + 'byte_count': len(out_bytes), + 'bytes_lines': render_bytes(out_bytes), + 'format': cli.args.format, + } + + # Render the license + subs.update({'license': render_license(subs)}) + + # Render and write the header file + header_text = render_header(subs) + header_file = cli.args.output / (cli.args.input.stem + ".qgf.h") + with open(header_file, 'w') as header: + print(f"Writing {header_file}...") + header.write(header_text) + header.close() + + # Render and write the source file + source_text = render_source(subs) + source_file = cli.args.output / (cli.args.input.stem + ".qgf.c") + with open(source_file, 'w') as source: + print(f"Writing {source_file}...") + source.write(source_text) + source.close() diff --git a/lib/python/qmk/cli/painter/make_font.py b/lib/python/qmk/cli/painter/make_font.py new file mode 100644 index 0000000000..0762843fd3 --- /dev/null +++ b/lib/python/qmk/cli/painter/make_font.py @@ -0,0 +1,87 @@ +"""This script automates the conversion of font files into a format QMK firmware understands. +""" + +import re +import datetime +from io import BytesIO +from qmk.path import normpath +from qmk.painter_qff import QFFFont +from qmk.painter import render_header, render_source, render_license, render_bytes, valid_formats +from milc import cli + + +@cli.argument('-f', '--font', required=True, help='Specify input font file.') +@cli.argument('-o', '--output', required=True, help='Specify output image path.') +@cli.argument('-s', '--size', default=12, help='Specify font size. Default 12.') +@cli.argument('-n', '--no-ascii', arg_only=True, action='store_true', help='Disables output of the full ASCII character set (0x20..0x7E), exporting only the glyphs specified.') +@cli.argument('-u', '--unicode-glyphs', default='', help='Also generate the specified unicode glyphs.') +@cli.argument('-a', '--no-aa', arg_only=True, action='store_true', help='Disable anti-aliasing on fonts.') +@cli.subcommand('Converts an input font to something QMK understands') +def painter_make_font_image(cli): + # Create the font object + font = QFFFont(cli) + # Read from the input file + cli.args.font = normpath(cli.args.font) + font.generate_image(cli.args.font, cli.args.size, include_ascii_glyphs=(not cli.args.no_ascii), unicode_glyphs=cli.args.unicode_glyphs, use_aa=(False if cli.args.no_aa else True)) + # Render out the data + font.save_to_image(normpath(cli.args.output)) + + +@cli.argument('-i', '--input', help='Specify input graphic file.') +@cli.argument('-o', '--output', default='', help='Specify output directory. Defaults to same directory as input.') +@cli.argument('-n', '--no-ascii', arg_only=True, action='store_true', help='Disables output of the full ASCII character set (0x20..0x7E), exporting only the glyphs specified.') +@cli.argument('-u', '--unicode-glyphs', default='', help='Also generate the specified unicode glyphs.') +@cli.argument('-f', '--format', required=True, help='Output format, valid types: %s' % (', '.join(valid_formats.keys()))) +@cli.argument('-r', '--no-rle', arg_only=True, action='store_true', help='Disable the use of RLE to minimise converted image size.') +@cli.subcommand('Converts an input font image to something QMK firmware understands') +def painter_convert_font_image(cli): + # Work out the format + format = valid_formats[cli.args.format] + + # Create the font object + font = QFFFont(cli.log) + + # Read from the input file + cli.args.input = normpath(cli.args.input) + font.read_from_image(cli.args.input, include_ascii_glyphs=(not cli.args.no_ascii), unicode_glyphs=cli.args.unicode_glyphs) + + # Work out the output directory + if len(cli.args.output) == 0: + cli.args.output = cli.args.input.parent + cli.args.output = normpath(cli.args.output) + + # Render out the data + out_data = BytesIO() + font.save_to_qff(format, (False if cli.args.no_rle else True), out_data) + + # Work out the text substitutions for rendering the output data + subs = { + 'generated_type': 'font', + 'var_prefix': 'font', + 'generator_command': f'qmk painter-convert-font-image -i {cli.args.input.name} -f {cli.args.format}', + 'year': datetime.date.today().strftime("%Y"), + 'input_file': cli.args.input.name, + 'sane_name': re.sub(r"[^a-zA-Z0-9]", "_", cli.args.input.stem), + 'byte_count': out_data.getbuffer().nbytes, + 'bytes_lines': render_bytes(out_data.getbuffer().tobytes()), + 'format': cli.args.format, + } + + # Render the license + subs.update({'license': render_license(subs)}) + + # Render and write the header file + header_text = render_header(subs) + header_file = cli.args.output / (cli.args.input.stem + ".qff.h") + with open(header_file, 'w') as header: + print(f"Writing {header_file}...") + header.write(header_text) + header.close() + + # Render and write the source file + source_text = render_source(subs) + source_file = cli.args.output / (cli.args.input.stem + ".qff.c") + with open(source_file, 'w') as source: + print(f"Writing {source_file}...") + source.write(source_text) + source.close() diff --git a/lib/python/qmk/painter.py b/lib/python/qmk/painter.py new file mode 100644 index 0000000000..d0cc1dddec --- /dev/null +++ b/lib/python/qmk/painter.py @@ -0,0 +1,268 @@ +"""Functions that help us work with Quantum Painter's file formats. +""" +import math +import re +from string import Template +from PIL import Image, ImageOps + +# The list of valid formats Quantum Painter supports +valid_formats = { + 'pal256': { + 'image_format': 'IMAGE_FORMAT_PALETTE', + 'bpp': 8, + 'has_palette': True, + 'num_colors': 256, + 'image_format_byte': 0x07, # see qp_internal_formats.h + }, + 'pal16': { + 'image_format': 'IMAGE_FORMAT_PALETTE', + 'bpp': 4, + 'has_palette': True, + 'num_colors': 16, + 'image_format_byte': 0x06, # see qp_internal_formats.h + }, + 'pal4': { + 'image_format': 'IMAGE_FORMAT_PALETTE', + 'bpp': 2, + 'has_palette': True, + 'num_colors': 4, + 'image_format_byte': 0x05, # see qp_internal_formats.h + }, + 'pal2': { + 'image_format': 'IMAGE_FORMAT_PALETTE', + 'bpp': 1, + 'has_palette': True, + 'num_colors': 2, + 'image_format_byte': 0x04, # see qp_internal_formats.h + }, + 'mono256': { + 'image_format': 'IMAGE_FORMAT_GRAYSCALE', + 'bpp': 8, + 'has_palette': False, + 'num_colors': 256, + 'image_format_byte': 0x03, # see qp_internal_formats.h + }, + 'mono16': { + 'image_format': 'IMAGE_FORMAT_GRAYSCALE', + 'bpp': 4, + 'has_palette': False, + 'num_colors': 16, + 'image_format_byte': 0x02, # see qp_internal_formats.h + }, + 'mono4': { + 'image_format': 'IMAGE_FORMAT_GRAYSCALE', + 'bpp': 2, + 'has_palette': False, + 'num_colors': 4, + 'image_format_byte': 0x01, # see qp_internal_formats.h + }, + 'mono2': { + 'image_format': 'IMAGE_FORMAT_GRAYSCALE', + 'bpp': 1, + 'has_palette': False, + 'num_colors': 2, + 'image_format_byte': 0x00, # see qp_internal_formats.h + } +} + +license_template = """\ +// Copyright ${year} QMK -- generated source code only, ${generated_type} retains original copyright +// SPDX-License-Identifier: GPL-2.0-or-later + +// This file was auto-generated by `${generator_command}` +""" + + +def render_license(subs): + license_txt = Template(license_template) + return license_txt.substitute(subs) + + +header_file_template = """\ +${license} +#pragma once + +#include + +extern const uint32_t ${var_prefix}_${sane_name}_length; +extern const uint8_t ${var_prefix}_${sane_name}[${byte_count}]; +""" + + +def render_header(subs): + header_txt = Template(header_file_template) + return header_txt.substitute(subs) + + +source_file_template = """\ +${license} +#include + +const uint32_t ${var_prefix}_${sane_name}_length = ${byte_count}; + +// clang-format off +const uint8_t ${var_prefix}_${sane_name}[${byte_count}] = { +${bytes_lines} +}; +// clang-format on +""" + + +def render_source(subs): + source_txt = Template(source_file_template) + return source_txt.substitute(subs) + + +def render_bytes(bytes, newline_after=16): + lines = '' + for n in range(len(bytes)): + if n % newline_after == 0 and n > 0 and n != len(bytes): + lines = lines + "\n " + elif n == 0: + lines = lines + " " + lines = lines + " 0x{0:02X},".format(bytes[n]) + return lines.rstrip() + + +def clean_output(str): + str = re.sub(r'\r', '', str) + str = re.sub(r'[\n]{3,}', r'\n\n', str) + return str + + +def rescale_byte(val, maxval): + """Rescales a byte value to the supplied range, i.e. [0,255] -> [0,maxval]. + """ + return int(round(val * maxval / 255.0)) + + +def convert_requested_format(im, format): + """Convert an image to the requested format. + """ + + # Work out the requested format + ncolors = format["num_colors"] + image_format = format["image_format"] + + # Ensure we have a valid number of colors for the palette + if ncolors <= 0 or ncolors > 256 or (ncolors & (ncolors - 1) != 0): + raise ValueError("Number of colors must be 2, 4, 16, or 256.") + + # Work out where we're getting the bytes from + if image_format == 'IMAGE_FORMAT_GRAYSCALE': + # If mono, convert input to grayscale, then to RGB, then grab the raw bytes corresponding to the intensity of the red channel + im = ImageOps.grayscale(im) + im = im.convert("RGB") + elif image_format == 'IMAGE_FORMAT_PALETTE': + # If color, convert input to RGB, palettize based on the supplied number of colors, then get the raw palette bytes + im = im.convert("RGB") + im = im.convert("P", palette=Image.ADAPTIVE, colors=ncolors) + + return im + + +def convert_image_bytes(im, format): + """Convert the supplied image to the equivalent bytes required by the QMK firmware. + """ + + # Work out the requested format + ncolors = format["num_colors"] + image_format = format["image_format"] + shifter = int(math.log2(ncolors)) + pixels_per_byte = int(8 / math.log2(ncolors)) + (width, height) = im.size + expected_byte_count = ((width * height) + (pixels_per_byte - 1)) // pixels_per_byte + + if image_format == 'IMAGE_FORMAT_GRAYSCALE': + # Take the red channel + image_bytes = im.tobytes("raw", "R") + image_bytes_len = len(image_bytes) + + # No palette + palette = None + + bytearray = [] + for x in range(expected_byte_count): + byte = 0 + for n in range(pixels_per_byte): + byte_offset = x * pixels_per_byte + n + if byte_offset < image_bytes_len: + # If mono, each input byte is a grayscale [0,255] pixel -- rescale to the range we want then pack together + byte = byte | (rescale_byte(image_bytes[byte_offset], ncolors - 1) << int(n * shifter)) + bytearray.append(byte) + + elif image_format == 'IMAGE_FORMAT_PALETTE': + # Convert each pixel to the palette bytes + image_bytes = im.tobytes("raw", "P") + image_bytes_len = len(image_bytes) + + # Export the palette + palette = [] + pal = im.getpalette() + for n in range(0, ncolors * 3, 3): + palette.append((pal[n + 0], pal[n + 1], pal[n + 2])) + + bytearray = [] + for x in range(expected_byte_count): + byte = 0 + for n in range(pixels_per_byte): + byte_offset = x * pixels_per_byte + n + if byte_offset < image_bytes_len: + # If color, each input byte is the index into the color palette -- pack them together + byte = byte | ((image_bytes[byte_offset] & (ncolors - 1)) << int(n * shifter)) + bytearray.append(byte) + + if len(bytearray) != expected_byte_count: + raise Exception(f"Wrong byte count, was {len(bytearray)}, expected {expected_byte_count}") + + return (palette, bytearray) + + +def compress_bytes_qmk_rle(bytearray): + debug_dump = False + output = [] + temp = [] + repeat = False + + def append_byte(c): + if debug_dump: + print('Appending byte:', '0x{0:02X}'.format(int(c)), '=', c) + output.append(c) + + def append_range(r): + append_byte(127 + len(r)) + if debug_dump: + print('Appending {0} byte(s):'.format(len(r)), '[', ', '.join(['{0:02X}'.format(e) for e in r]), ']') + output.extend(r) + + for n in range(0, len(bytearray) + 1): + end = True if n == len(bytearray) else False + if not end: + c = bytearray[n] + temp.append(c) + if len(temp) <= 1: + continue + + if debug_dump: + print('Temp buffer state {0:3d} bytes:'.format(len(temp)), '[', ', '.join(['{0:02X}'.format(e) for e in temp]), ']') + + if repeat: + if temp[-1] != temp[-2]: + repeat = False + if not repeat or len(temp) == 128 or end: + append_byte(len(temp) if end else len(temp) - 1) + append_byte(temp[0]) + temp = [temp[-1]] + repeat = False + else: + if len(temp) >= 2 and temp[-1] == temp[-2]: + repeat = True + if len(temp) > 2: + append_range(temp[0:(len(temp) - 2)]) + temp = [temp[-1], temp[-1]] + continue + if len(temp) == 128 or end: + append_range(temp) + temp = [] + repeat = False + return output diff --git a/lib/python/qmk/painter_qff.py b/lib/python/qmk/painter_qff.py new file mode 100644 index 0000000000..746bb166e5 --- /dev/null +++ b/lib/python/qmk/painter_qff.py @@ -0,0 +1,401 @@ +# Copyright 2021 Nick Brassel (@tzarc) +# SPDX-License-Identifier: GPL-2.0-or-later + +# Quantum Font File "QFF" Font File Format. +# See https://docs.qmk.fm/#/quantum_painter_qff for more information. + +from pathlib import Path +from typing import Dict, Any +from colorsys import rgb_to_hsv +from PIL import Image, ImageDraw, ImageFont, ImageChops +from PIL._binary import o8, o16le as o16, o32le as o32 +from qmk.painter_qgf import QGFBlockHeader, QGFFramePaletteDescriptorV1 +from milc.attrdict import AttrDict +import qmk.painter + + +def o24(i): + return o16(i & 0xFFFF) + o8((i & 0xFF0000) >> 16) + + +######################################################################################################################## + + +class QFFGlyphInfo(AttrDict): + def __init__(self, *args, **kwargs): + super().__init__() + + for n, value in enumerate(args): + self[f'arg:{n}'] = value + + for key, value in kwargs.items(): + self[key] = value + + def write(self, fp, include_code_point): + if include_code_point is True: + fp.write(o24(ord(self.code_point))) + + value = ((self.data_offset << 6) & 0xFFFFC0) | (self.w & 0x3F) + fp.write(o24(value)) + + +######################################################################################################################## + + +class QFFFontDescriptor: + type_id = 0x00 + length = 20 + magic = 0x464651 + + def __init__(self): + self.header = QGFBlockHeader() + self.header.type_id = QFFFontDescriptor.type_id + self.header.length = QFFFontDescriptor.length + self.version = 1 + self.total_file_size = 0 + self.line_height = 0 + self.has_ascii_table = False + self.unicode_glyph_count = 0 + self.format = 0xFF + self.flags = 0 + self.compression = 0xFF + self.transparency_index = 0xFF # TODO: Work out how to retrieve the transparent palette entry from the PIL gif loader + + def write(self, fp): + self.header.write(fp) + fp.write( + b'' # start off with empty bytes... + + o24(QFFFontDescriptor.magic) # magic + + o8(self.version) # version + + o32(self.total_file_size) # file size + + o32((~self.total_file_size) & 0xFFFFFFFF) # negated file size + + o8(self.line_height) # line height + + o8(1 if self.has_ascii_table is True else 0) # whether or not we have an ascii table present + + o16(self.unicode_glyph_count & 0xFFFF) # number of unicode glyphs present + + o8(self.format) # format + + o8(self.flags) # flags + + o8(self.compression) # compression + + o8(self.transparency_index) # transparency index + ) + + @property + def is_transparent(self): + return (self.flags & 0x01) == 0x01 + + @is_transparent.setter + def is_transparent(self, val): + if val: + self.flags |= 0x01 + else: + self.flags &= ~0x01 + + +######################################################################################################################## + + +class QFFAsciiGlyphTableV1: + type_id = 0x01 + length = 95 * 3 # We have 95 glyphs: [0x20...0x7E] + + def __init__(self): + self.header = QGFBlockHeader() + self.header.type_id = QFFAsciiGlyphTableV1.type_id + self.header.length = QFFAsciiGlyphTableV1.length + + # Each glyph is key=code_point, value=QFFGlyphInfo + self.glyphs = {} + + def add_glyph(self, glyph: QFFGlyphInfo): + self.glyphs[ord(glyph.code_point)] = glyph + + def write(self, fp): + self.header.write(fp) + + for n in range(0x20, 0x7F): + self.glyphs[n].write(fp, False) + + +######################################################################################################################## + + +class QFFUnicodeGlyphTableV1: + type_id = 0x02 + + def __init__(self): + self.header = QGFBlockHeader() + self.header.type_id = QFFUnicodeGlyphTableV1.type_id + self.header.length = 0 + + # Each glyph is key=code_point, value=QFFGlyphInfo + self.glyphs = {} + + def add_glyph(self, glyph: QFFGlyphInfo): + self.glyphs[ord(glyph.code_point)] = glyph + + def write(self, fp): + self.header.length = len(self.glyphs.keys()) * 6 + self.header.write(fp) + + for n in sorted(self.glyphs.keys()): + self.glyphs[n].write(fp, True) + + +######################################################################################################################## + + +class QFFFontDataDescriptorV1: + type_id = 0x04 + + def __init__(self): + self.header = QGFBlockHeader() + self.header.type_id = QFFFontDataDescriptorV1.type_id + self.data = [] + + def write(self, fp): + self.header.length = len(self.data) + self.header.write(fp) + fp.write(bytes(self.data)) + + +######################################################################################################################## + + +def _generate_font_glyphs_list(use_ascii, unicode_glyphs): + # The set of glyphs that we want to generate images for + glyphs = {} + + # Add ascii charset if requested + if use_ascii is True: + for c in range(0x20, 0x7F): # does not include 0x7F! + glyphs[chr(c)] = True + + # Append any extra unicode glyphs + unicode_glyphs = list(unicode_glyphs) + for c in unicode_glyphs: + glyphs[c] = True + + return sorted(glyphs.keys()) + + +class QFFFont: + def __init__(self, logger): + self.logger = logger + self.image = None + self.glyph_data = {} + self.glyph_height = 0 + return + + def _extract_glyphs(self, format): + total_data_size = 0 + total_rle_data_size = 0 + + converted_img = qmk.painter.convert_requested_format(self.image, format) + (self.palette, _) = qmk.painter.convert_image_bytes(converted_img, format) + + # Work out how many bytes used for RLE vs. non-RLE + for _, glyph_entry in self.glyph_data.items(): + glyph_img = converted_img.crop((glyph_entry.x, 1, glyph_entry.x + glyph_entry.w, 1 + self.glyph_height)) + (_, this_glyph_image_bytes) = qmk.painter.convert_image_bytes(glyph_img, format) + this_glyph_rle_bytes = qmk.painter.compress_bytes_qmk_rle(this_glyph_image_bytes) + total_data_size += len(this_glyph_image_bytes) + total_rle_data_size += len(this_glyph_rle_bytes) + glyph_entry['image_uncompressed_bytes'] = this_glyph_image_bytes + glyph_entry['image_compressed_bytes'] = this_glyph_rle_bytes + + return (total_data_size, total_rle_data_size) + + def _parse_image(self, img, include_ascii_glyphs: bool = True, unicode_glyphs: str = ''): + # Clear out any existing font metadata + self.image = None + # Each glyph is key=code_point, value={ x: ?, w: ? } + self.glyph_data = {} + self.glyph_height = 0 + + # Work out the list of glyphs required + glyphs = _generate_font_glyphs_list(include_ascii_glyphs, unicode_glyphs) + + # Work out the geometry + (width, height) = img.size + + # Work out the glyph offsets/widths + glyph_pixel_offsets = [] + glyph_pixel_widths = [] + pixels = img.load() + + # Run through the markers and work out where each glyph starts/stops + glyph_split_color = pixels[0, 0] # top left pixel is the marker color we're going to use to split each glyph + glyph_pixel_offsets.append(0) + last_offset = 0 + for x in range(1, width): + if pixels[x, 0] == glyph_split_color: + glyph_pixel_offsets.append(x) + glyph_pixel_widths.append(x - last_offset) + last_offset = x + glyph_pixel_widths.append(width - last_offset) + + # Make sure the number of glyphs we're attempting to generate matches the input image + if len(glyph_pixel_offsets) != len(glyphs): + self.logger.error('The number of glyphs to generate doesn\'t match the number of detected glyphs in the input image.') + return + + # Set up the required metadata for each glyph + for n in range(0, len(glyph_pixel_offsets)): + self.glyph_data[glyphs[n]] = QFFGlyphInfo(code_point=glyphs[n], x=glyph_pixel_offsets[n], w=glyph_pixel_widths[n]) + + # Parsing was successful, keep the image in this instance + self.image = img + self.glyph_height = height - 1 # subtract the line with the markers + + def generate_image(self, ttf_file: Path, font_size: int, include_ascii_glyphs: bool = True, unicode_glyphs: str = '', include_before_left: bool = False, use_aa: bool = True): + # Load the font + font = ImageFont.truetype(str(ttf_file), int(font_size)) + # Work out the max font size + max_font_size = font.font.ascent + abs(font.font.descent) + # Work out the list of glyphs required + glyphs = _generate_font_glyphs_list(include_ascii_glyphs, unicode_glyphs) + + baseline_offset = 9999999 + total_glyph_width = 0 + max_glyph_height = -1 + + # Measure each glyph to determine the overall baseline offset required + for glyph in glyphs: + (ls_l, ls_t, ls_r, ls_b) = font.getbbox(glyph, anchor='ls') + glyph_width = (ls_r - ls_l) if include_before_left else (ls_r) + glyph_height = font.getbbox(glyph, anchor='la')[3] + if max_glyph_height < glyph_height: + max_glyph_height = glyph_height + total_glyph_width += glyph_width + if baseline_offset > ls_t: + baseline_offset = ls_t + + # Create the output image + img = Image.new("RGB", (total_glyph_width + 1, max_font_size * 2 + 1), (0, 0, 0, 255)) + cur_x_pos = 0 + + # Loop through each glyph... + for glyph in glyphs: + # Work out this glyph's bounding box + (ls_l, ls_t, ls_r, ls_b) = font.getbbox(glyph, anchor='ls') + glyph_width = (ls_r - ls_l) if include_before_left else (ls_r) + glyph_height = ls_b - ls_t + x_offset = -ls_l + y_offset = ls_t - baseline_offset + + # Draw each glyph to its own image so we don't get anti-aliasing applied to the final image when straddling edges + glyph_img = Image.new("RGB", (glyph_width, max_font_size), (0, 0, 0, 255)) + glyph_draw = ImageDraw.Draw(glyph_img) + if not use_aa: + glyph_draw.fontmode = "1" + glyph_draw.text((x_offset, y_offset), glyph, font=font, anchor='lt') + + # Place the glyph-specific image in the correct location overall + img.paste(glyph_img, (cur_x_pos, 1)) + + # Set up the marker for start of each glyph + pixels = img.load() + pixels[cur_x_pos, 0] = (255, 0, 255) + + # Increment for the next glyph's position + cur_x_pos += glyph_width + + # Add the ending marker so that the difference/crop works + pixels = img.load() + pixels[cur_x_pos, 0] = (255, 0, 255) + + # Determine the usable font area + dummy_img = Image.new("RGB", (total_glyph_width + 1, max_font_size + 1), (0, 0, 0, 255)) + bbox = ImageChops.difference(img, dummy_img).getbbox() + bbox = (bbox[0], bbox[1], bbox[2] - 1, bbox[3]) # remove the unused end-marker + + # Crop and re-parse the resulting image to ensure we're generating the correct format + self._parse_image(img.crop(bbox), include_ascii_glyphs, unicode_glyphs) + + def save_to_image(self, img_file: Path): + # Drop out if there's no image loaded + if self.image is None: + self.logger.error('No image is loaded.') + return + + # Save the image to the supplied file + self.image.save(str(img_file)) + + def read_from_image(self, img_file: Path, include_ascii_glyphs: bool = True, unicode_glyphs: str = ''): + # Load and parse the supplied image file + self._parse_image(Image.open(str(img_file)), include_ascii_glyphs, unicode_glyphs) + return + + def save_to_qff(self, format: Dict[str, Any], use_rle: bool, fp): + # Drop out if there's no image loaded + if self.image is None: + self.logger.error('No image is loaded.') + return + + # Work out if we want to use RLE at all, skipping it if it's not any smaller (it's applied per-glyph) + (total_data_size, total_rle_data_size) = self._extract_glyphs(format) + if use_rle: + use_rle = (total_rle_data_size < total_data_size) + + # For each glyph, work out which image data we want to use and append it to the image buffer, recording the byte-wise offset + img_buffer = bytes() + for _, glyph_entry in self.glyph_data.items(): + glyph_entry['data_offset'] = len(img_buffer) + glyph_img_bytes = glyph_entry.image_compressed_bytes if use_rle else glyph_entry.image_uncompressed_bytes + img_buffer += bytes(glyph_img_bytes) + + font_descriptor = QFFFontDescriptor() + ascii_table = QFFAsciiGlyphTableV1() + unicode_table = QFFUnicodeGlyphTableV1() + data_descriptor = QFFFontDataDescriptorV1() + data_descriptor.data = img_buffer + + # Check if we have all the ASCII glyphs present + include_ascii_glyphs = all([chr(n) in self.glyph_data for n in range(0x20, 0x7F)]) + + # Helper for populating the blocks + for code_point, glyph_entry in self.glyph_data.items(): + if ord(code_point) >= 0x20 and ord(code_point) <= 0x7E and include_ascii_glyphs: + ascii_table.add_glyph(glyph_entry) + else: + unicode_table.add_glyph(glyph_entry) + + # Configure the font descriptor + font_descriptor.line_height = self.glyph_height + font_descriptor.has_ascii_table = include_ascii_glyphs + font_descriptor.unicode_glyph_count = len(unicode_table.glyphs.keys()) + font_descriptor.is_transparent = False + font_descriptor.format = format['image_format_byte'] + font_descriptor.compression = 0x01 if use_rle else 0x00 + + # Write a dummy font descriptor -- we'll have to come back and write it properly once we've rendered out everything else + font_descriptor_location = fp.tell() + font_descriptor.write(fp) + + # Write out the ASCII table if required + if font_descriptor.has_ascii_table: + ascii_table.write(fp) + + # Write out the unicode table if required + if font_descriptor.unicode_glyph_count > 0: + unicode_table.write(fp) + + # Write out the palette if required + if format['has_palette']: + palette_descriptor = QGFFramePaletteDescriptorV1() + + # Helper to convert from RGB888 to the QMK "dialect" of HSV888 + def rgb888_to_qmk_hsv888(e): + hsv = rgb_to_hsv(e[0] / 255.0, e[1] / 255.0, e[2] / 255.0) + return (int(hsv[0] * 255.0), int(hsv[1] * 255.0), int(hsv[2] * 255.0)) + + # Convert all palette entries to HSV888 and write to the output + palette_descriptor.palette_entries = list(map(rgb888_to_qmk_hsv888, self.palette)) + palette_descriptor.write(fp) + + # Write out the image data + data_descriptor.write(fp) + + # Now fix up the overall font descriptor, then write it in the correct location + font_descriptor.total_file_size = fp.tell() + fp.seek(font_descriptor_location, 0) + font_descriptor.write(fp) diff --git a/lib/python/qmk/painter_qgf.py b/lib/python/qmk/painter_qgf.py new file mode 100644 index 0000000000..71ce1f5a02 --- /dev/null +++ b/lib/python/qmk/painter_qgf.py @@ -0,0 +1,408 @@ +# Copyright 2021 Nick Brassel (@tzarc) +# SPDX-License-Identifier: GPL-2.0-or-later + +# Quantum Graphics File "QGF" Image File Format. +# See https://docs.qmk.fm/#/quantum_painter_qgf for more information. + +from colorsys import rgb_to_hsv +from types import FunctionType +from PIL import Image, ImageFile, ImageChops +from PIL._binary import o8, o16le as o16, o32le as o32 +import qmk.painter + + +def o24(i): + return o16(i & 0xFFFF) + o8((i & 0xFF0000) >> 16) + + +######################################################################################################################## + + +class QGFBlockHeader: + block_size = 5 + + def write(self, fp): + fp.write(b'' # start off with empty bytes... + + o8(self.type_id) # block type id + + o8((~self.type_id) & 0xFF) # negated block type id + + o24(self.length) # blob length + ) + + +######################################################################################################################## + + +class QGFGraphicsDescriptor: + type_id = 0x00 + length = 18 + magic = 0x464751 + + def __init__(self): + self.header = QGFBlockHeader() + self.header.type_id = QGFGraphicsDescriptor.type_id + self.header.length = QGFGraphicsDescriptor.length + self.version = 1 + self.total_file_size = 0 + self.image_width = 0 + self.image_height = 0 + self.frame_count = 0 + + def write(self, fp): + self.header.write(fp) + fp.write( + b'' # start off with empty bytes... + + o24(QGFGraphicsDescriptor.magic) # magic + + o8(self.version) # version + + o32(self.total_file_size) # file size + + o32((~self.total_file_size) & 0xFFFFFFFF) # negated file size + + o16(self.image_width) # width + + o16(self.image_height) # height + + o16(self.frame_count) # frame count + ) + + +######################################################################################################################## + + +class QGFFrameOffsetDescriptorV1: + type_id = 0x01 + + def __init__(self, frame_count): + self.header = QGFBlockHeader() + self.header.type_id = QGFFrameOffsetDescriptorV1.type_id + self.frame_offsets = [0xFFFFFFFF] * frame_count + self.frame_count = frame_count + + def write(self, fp): + self.header.length = len(self.frame_offsets) * 4 + self.header.write(fp) + for offset in self.frame_offsets: + fp.write(b'' # start off with empty bytes... + + o32(offset) # offset + ) + + +######################################################################################################################## + + +class QGFFrameDescriptorV1: + type_id = 0x02 + length = 6 + + def __init__(self): + self.header = QGFBlockHeader() + self.header.type_id = QGFFrameDescriptorV1.type_id + self.header.length = QGFFrameDescriptorV1.length + self.format = 0xFF + self.flags = 0 + self.compression = 0xFF + self.transparency_index = 0xFF # TODO: Work out how to retrieve the transparent palette entry from the PIL gif loader + self.delay = 1000 # Placeholder until it gets read from the animation + + def write(self, fp): + self.header.write(fp) + fp.write(b'' # start off with empty bytes... + + o8(self.format) # format + + o8(self.flags) # flags + + o8(self.compression) # compression + + o8(self.transparency_index) # transparency index + + o16(self.delay) # delay + ) + + @property + def is_transparent(self): + return (self.flags & 0x01) == 0x01 + + @is_transparent.setter + def is_transparent(self, val): + if val: + self.flags |= 0x01 + else: + self.flags &= ~0x01 + + @property + def is_delta(self): + return (self.flags & 0x02) == 0x02 + + @is_delta.setter + def is_delta(self, val): + if val: + self.flags |= 0x02 + else: + self.flags &= ~0x02 + + +######################################################################################################################## + + +class QGFFramePaletteDescriptorV1: + type_id = 0x03 + + def __init__(self): + self.header = QGFBlockHeader() + self.header.type_id = QGFFramePaletteDescriptorV1.type_id + self.header.length = 0 + self.palette_entries = [(0xFF, 0xFF, 0xFF)] * 4 + + def write(self, fp): + self.header.length = len(self.palette_entries) * 3 + self.header.write(fp) + for entry in self.palette_entries: + fp.write(b'' # start off with empty bytes... + + o8(entry[0]) # h + + o8(entry[1]) # s + + o8(entry[2]) # v + ) + + +######################################################################################################################## + + +class QGFFrameDeltaDescriptorV1: + type_id = 0x04 + length = 8 + + def __init__(self): + self.header = QGFBlockHeader() + self.header.type_id = QGFFrameDeltaDescriptorV1.type_id + self.header.length = QGFFrameDeltaDescriptorV1.length + self.left = 0 + self.top = 0 + self.right = 0 + self.bottom = 0 + + def write(self, fp): + self.header.write(fp) + fp.write(b'' # start off with empty bytes... + + o16(self.left) # left + + o16(self.top) # top + + o16(self.right) # right + + o16(self.bottom) # bottom + ) + + +######################################################################################################################## + + +class QGFFrameDataDescriptorV1: + type_id = 0x05 + + def __init__(self): + self.header = QGFBlockHeader() + self.header.type_id = QGFFrameDataDescriptorV1.type_id + self.data = [] + + def write(self, fp): + self.header.length = len(self.data) + self.header.write(fp) + fp.write(bytes(self.data)) + + +######################################################################################################################## + + +class QGFImageFile(ImageFile.ImageFile): + + format = "QGF" + format_description = "Quantum Graphics File Format" + + def _open(self): + raise NotImplementedError("Reading QGF files is not supported") + + +######################################################################################################################## + + +def _accept(prefix): + """Helper method used by PIL to work out if it can parse an input file. + + Currently unimplemented. + """ + return False + + +def _save(im, fp, filename): + """Helper method used by PIL to write to an output file. + """ + # Work out from the parameters if we need to do anything special + encoderinfo = im.encoderinfo.copy() + append_images = list(encoderinfo.get("append_images", [])) + verbose = encoderinfo.get("verbose", False) + use_deltas = encoderinfo.get("use_deltas", True) + use_rle = encoderinfo.get("use_rle", True) + + # Helper for inline verbose prints + def vprint(s): + if verbose: + print(s) + + # Helper to iterate through all frames in the input image + def _for_all_frames(x: FunctionType): + frame_num = 0 + last_frame = None + for frame in [im] + append_images: + # Get number of of frames in this image + nfr = getattr(frame, "n_frames", 1) + for idx in range(nfr): + frame.seek(idx) + frame.load() + copy = frame.copy().convert("RGB") + x(frame_num, copy, last_frame) + last_frame = copy + frame_num += 1 + + # Collect all the frame sizes + frame_sizes = [] + _for_all_frames(lambda idx, frame, last_frame: frame_sizes.append(frame.size)) + + # Make sure all frames are the same size + if len(list(set(frame_sizes))) != 1: + raise ValueError("Mismatching sizes on frames") + + # Write out the initial graphics descriptor (and write a dummy value), so that we can come back and fill in the + # correct values once we've written all the frames to the output + graphics_descriptor_location = fp.tell() + graphics_descriptor = QGFGraphicsDescriptor() + graphics_descriptor.frame_count = len(frame_sizes) + graphics_descriptor.image_width = frame_sizes[0][0] + graphics_descriptor.image_height = frame_sizes[0][1] + vprint(f'{"Graphics descriptor block":26s} {fp.tell():5d}d / {fp.tell():04X}h') + graphics_descriptor.write(fp) + + # Work out the frame offset descriptor location (and write a dummy value), so that we can come back and fill in the + # correct offsets once we've written all the frames to the output + frame_offset_location = fp.tell() + frame_offsets = QGFFrameOffsetDescriptorV1(graphics_descriptor.frame_count) + vprint(f'{"Frame offsets block":26s} {fp.tell():5d}d / {fp.tell():04X}h') + frame_offsets.write(fp) + + # Helper function to save each frame to the output file + def _write_frame(idx, frame, last_frame): + # If we replace the frame we're going to output with a delta, we can override it here + this_frame = frame + location = (0, 0) + size = frame.size + + # Work out the format we're going to use + format = encoderinfo["qmk_format"] + + # Convert the original frame so we can do comparisons + converted = qmk.painter.convert_requested_format(this_frame, format) + graphic_data = qmk.painter.convert_image_bytes(converted, format) + + # Convert the raw data to RLE-encoded if requested + raw_data = graphic_data[1] + if use_rle: + rle_data = qmk.painter.compress_bytes_qmk_rle(graphic_data[1]) + use_raw_this_frame = not use_rle or len(raw_data) <= len(rle_data) + image_data = raw_data if use_raw_this_frame else rle_data + + # Work out if a delta frame is smaller than injecting it directly + use_delta_this_frame = False + if use_deltas and last_frame is not None: + # If we want to use deltas, then find the difference + diff = ImageChops.difference(frame, last_frame) + + # Get the bounding box of those differences + bbox = diff.getbbox() + + # If we have a valid bounding box... + if bbox: + # ...create the delta frame by cropping the original. + delta_frame = frame.crop(bbox) + delta_location = (bbox[0], bbox[1]) + delta_size = (bbox[2] - bbox[0], bbox[3] - bbox[1]) + + # Convert the delta frame to the requested format + delta_converted = qmk.painter.convert_requested_format(delta_frame, format) + delta_graphic_data = qmk.painter.convert_image_bytes(delta_converted, format) + + # Work out how large the delta frame is going to be with compression etc. + delta_raw_data = delta_graphic_data[1] + if use_rle: + delta_rle_data = qmk.painter.compress_bytes_qmk_rle(delta_graphic_data[1]) + delta_use_raw_this_frame = not use_rle or len(delta_raw_data) <= len(delta_rle_data) + delta_image_data = delta_raw_data if delta_use_raw_this_frame else delta_rle_data + + # If the size of the delta frame (plus delta descriptor) is smaller than the original, use that instead + # This ensures that if a non-delta is overall smaller in size, we use that in preference due to flash + # sizing constraints. + if (len(delta_image_data) + QGFFrameDeltaDescriptorV1.length) < len(image_data): + # Copy across all the delta equivalents so that the rest of the processing acts on those + this_frame = delta_frame + location = delta_location + size = delta_size + converted = delta_converted + graphic_data = delta_graphic_data + raw_data = delta_raw_data + rle_data = delta_rle_data + use_raw_this_frame = delta_use_raw_this_frame + image_data = delta_image_data + use_delta_this_frame = True + + # Write out the frame descriptor + frame_offsets.frame_offsets[idx] = fp.tell() + vprint(f'{f"Frame {idx:3d} base":26s} {fp.tell():5d}d / {fp.tell():04X}h') + frame_descriptor = QGFFrameDescriptorV1() + frame_descriptor.is_delta = use_delta_this_frame + frame_descriptor.is_transparent = False + frame_descriptor.format = format['image_format_byte'] + frame_descriptor.compression = 0x00 if use_raw_this_frame else 0x01 # See qp.h, painter_compression_t + frame_descriptor.delay = frame.info['duration'] if 'duration' in frame.info else 1000 # If we're not an animation, just pretend we're delaying for 1000ms + frame_descriptor.write(fp) + + # Write out the palette if required + if format['has_palette']: + palette = graphic_data[0] + palette_descriptor = QGFFramePaletteDescriptorV1() + + # Helper to convert from RGB888 to the QMK "dialect" of HSV888 + def rgb888_to_qmk_hsv888(e): + hsv = rgb_to_hsv(e[0] / 255.0, e[1] / 255.0, e[2] / 255.0) + return (int(hsv[0] * 255.0), int(hsv[1] * 255.0), int(hsv[2] * 255.0)) + + # Convert all palette entries to HSV888 and write to the output + palette_descriptor.palette_entries = list(map(rgb888_to_qmk_hsv888, palette)) + vprint(f'{f"Frame {idx:3d} palette":26s} {fp.tell():5d}d / {fp.tell():04X}h') + palette_descriptor.write(fp) + + # Write out the delta info if required + if use_delta_this_frame: + # Set up the rendering location of where the delta frame should be situated + delta_descriptor = QGFFrameDeltaDescriptorV1() + delta_descriptor.left = location[0] + delta_descriptor.top = location[1] + delta_descriptor.right = location[0] + size[0] + delta_descriptor.bottom = location[1] + size[1] + + # Write the delta frame to the output + vprint(f'{f"Frame {idx:3d} delta":26s} {fp.tell():5d}d / {fp.tell():04X}h') + delta_descriptor.write(fp) + + # Write out the data for this frame to the output + data_descriptor = QGFFrameDataDescriptorV1() + data_descriptor.data = image_data + vprint(f'{f"Frame {idx:3d} data":26s} {fp.tell():5d}d / {fp.tell():04X}h') + data_descriptor.write(fp) + + # Iterate over each if the input frames, writing it to the output in the process + _for_all_frames(_write_frame) + + # Go back and update the graphics descriptor now that we can determine the final file size + graphics_descriptor.total_file_size = fp.tell() + fp.seek(graphics_descriptor_location, 0) + graphics_descriptor.write(fp) + + # Go back and update the frame offsets now that they're written to the file + fp.seek(frame_offset_location, 0) + frame_offsets.write(fp) + + +######################################################################################################################## + +# Register with PIL so that it knows about the QGF format +Image.register_open(QGFImageFile.format, QGFImageFile, _accept) +Image.register_save(QGFImageFile.format, _save) +Image.register_save_all(QGFImageFile.format, _save) +Image.register_extension(QGFImageFile.format, f".{QGFImageFile.format.lower()}") +Image.register_mime(QGFImageFile.format, f"image/{QGFImageFile.format.lower()}") 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/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 +#include + +#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 +#include + +#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 +#include + +#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 +#include + +#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 +#include + +#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..5134ae7e99 --- /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(); + + // 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 (!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; + } + + 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 +#include + +#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 +# include +# define qp_dprintf(...) dprintf(__VA_ARGS__) +#else +# define qp_dprintf(...) \ + do { \ + } while (0) +#endif + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Specific internal definitions + +#include +#include 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 . + */ + +#pragma once + +#include +#include +#include +#include + +#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/process_keycode/process_unicode_common.c b/quantum/process_keycode/process_unicode_common.c index 2606ea1f37..652becbc9a 100644 --- a/quantum/process_keycode/process_unicode_common.c +++ b/quantum/process_keycode/process_unicode_common.c @@ -16,6 +16,7 @@ #include "process_unicode_common.h" #include "eeprom.h" +#include "utf8.h" unicode_config_t unicode_config; uint8_t unicode_saved_mods; @@ -229,35 +230,6 @@ void register_unicode(uint32_t code_point) { unicode_input_finish(); } -// 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/quantum.h b/quantum/quantum.h index f87e5f1916..9ce3c1f5d6 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 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 . + */ + +#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 . + */ + +#pragma once + +#include + +const char *decode_utf8(const char *str, int32_t *code_point); \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 6d338ae1cb..e09d58d829 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,3 +9,4 @@ milc>=1.4.2 pygments pyusb qmk-dotty-dict +pillow diff --git a/setup.cfg b/setup.cfg index c7d7952098..6cbe1a616d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -6,7 +6,11 @@ ignore = # Conflicts with our yapf config E231 per_file_ignores = + # Module imported but unused **/__init__.py:F401 + # Quantum Painter also outputs append data using bytes object arithmetic on multiple lines + **/painter_qgf.py:W503 + **/painter_qff.py:W503 # Let's slowly crank this down max_complexity=16 -- cgit v1.2.3 From 68b16bba68d79c63bbd649a5be58756e937c96da Mon Sep 17 00:00:00 2001 From: Pascal Getreuer <50221757+getreuer@users.noreply.github.com> Date: Fri, 13 May 2022 23:00:32 -0700 Subject: [Core] Add Caps Word feature to core (#16588) Co-authored-by: precondition <57645186+precondition@users.noreply.github.com> Co-authored-by: Drashna Jaelre --- builddefs/generic_features.mk | 1 + builddefs/show_options.mk | 3 +- data/mappings/info_config.json | 3 + data/mappings/info_rules.json | 1 + data/schemas/keyboard.jsonschema | 10 + docs/_summary.md | 1 + docs/feature_caps_word.md | 156 ++++++++++ docs/keycodes.md | 8 + quantum/caps_word.c | 80 ++++++ quantum/caps_word.h | 43 +++ quantum/keyboard.c | 7 + quantum/process_keycode/process_caps_word.c | 160 +++++++++++ quantum/process_keycode/process_caps_word.h | 37 +++ quantum/quantum.c | 3 + quantum/quantum.h | 5 + quantum/quantum_keycodes.h | 3 + tests/caps_word/config.h | 21 ++ tests/caps_word/test.mk | 19 ++ tests/caps_word/test_caps_word.cpp | 423 ++++++++++++++++++++++++++++ 19 files changed, 983 insertions(+), 1 deletion(-) create mode 100644 docs/feature_caps_word.md create mode 100644 quantum/caps_word.c create mode 100644 quantum/caps_word.h create mode 100644 quantum/process_keycode/process_caps_word.c create mode 100644 quantum/process_keycode/process_caps_word.h create mode 100644 tests/caps_word/config.h create mode 100644 tests/caps_word/test.mk create mode 100644 tests/caps_word/test_caps_word.cpp (limited to 'docs/_summary.md') diff --git a/builddefs/generic_features.mk b/builddefs/generic_features.mk index 0475a2ff09..c3f1ec0f72 100644 --- a/builddefs/generic_features.mk +++ b/builddefs/generic_features.mk @@ -17,6 +17,7 @@ SPACE_CADET_ENABLE ?= yes GRAVE_ESC_ENABLE ?= yes GENERIC_FEATURES = \ + CAPS_WORD \ COMBO \ COMMAND \ DEFERRED_EXEC \ diff --git a/builddefs/show_options.mk b/builddefs/show_options.mk index 16b69ef0ea..f67d009191 100644 --- a/builddefs/show_options.mk +++ b/builddefs/show_options.mk @@ -81,7 +81,8 @@ OTHER_OPTION_NAMES = \ RGBLIGHT_FULL_POWER \ LTO_ENABLE \ PROGRAMMABLE_BUTTON_ENABLE \ - SECURE_ENABLE + SECURE_ENABLE \ + CAPS_WORD_ENABLE define NAME_ECHO @printf " %-30s = %-16s # %s\\n" "$1" "$($1)" "$(origin $1)" diff --git a/data/mappings/info_config.json b/data/mappings/info_config.json index 02ad3226c2..d9f96b5892 100644 --- a/data/mappings/info_config.json +++ b/data/mappings/info_config.json @@ -11,6 +11,8 @@ "BACKLIGHT_BREATHING": {"info_key": "backlight.breathing", "value_type": "bool"}, "BREATHING_PERIOD": {"info_key": "backlight.breathing_period", "value_type": "int"}, "BACKLIGHT_PIN": {"info_key": "backlight.pin"}, + "BOTH_SHIFTS_TURNS_ON_CAPS_WORD": {"info_key": "caps_word.both_shifts_turns_on", "value_type": "bool"}, + "CAPS_WORD_IDLE_TIMEOUT": {"info_key": "caps_word.idle_timeout", "value_type": "int"}, "COMBO_COUNT": {"info_key": "combo.count", "value_type": "int"}, "COMBO_TERM": {"info_key": "combo.term", "value_type": "int"}, "DEBOUNCE": {"info_key": "debounce", "value_type": "int"}, @@ -19,6 +21,7 @@ #"DEVICE_VER": {"info_key": "usb.device_version", "value_type": "bcd_version"}, "DESCRIPTION": {"info_key": "keyboard_folder", "value_type": "str", "to_json": false}, "DIODE_DIRECTION": {"info_key": "diode_direction"}, + "DOUBLE_TAP_SHIFT_TURNS_ON_CAPS_WORD": {"info_key": "caps_word.double_tap_shift_turns_on", "value_type": "bool"}, "FORCE_NKRO": {"info_key": "usb.force_nkro", "value_type": "bool"}, "DYNAMIC_KEYMAP_EEPROM_MAX_ADDR": {"info_key": "dynamic_keymap.eeprom_max_addr", "value_type": "int"}, "DYNAMIC_KEYMAP_LAYER_COUNT": {"info_key": "dynamic_keymap.layer_count", "value_type": "int"}, diff --git a/data/mappings/info_rules.json b/data/mappings/info_rules.json index 4b0fde5629..a8b39afbd1 100644 --- a/data/mappings/info_rules.json +++ b/data/mappings/info_rules.json @@ -10,6 +10,7 @@ "BOARD": {"info_key": "board"}, "BOOTLOADER": {"info_key": "bootloader", "warn_duplicate": false}, "BLUETOOTH": {"info_key": "bluetooth.driver"}, + "CAPS_WORD_ENABLE": {"info_key": "caps_word.enabled", "value_type": "bool"}, "FIRMWARE_FORMAT": {"info_key": "build.firmware_format"}, "KEYBOARD_SHARED_EP": {"info_key": "usb.shared_endpoint.keyboard", "value_type": "bool"}, "MOUSE_SHARED_EP": {"info_key": "usb.shared_endpoint.mouse", "value_type": "bool"}, diff --git a/data/schemas/keyboard.jsonschema b/data/schemas/keyboard.jsonschema index 6b1e543e48..aaf6f887d5 100644 --- a/data/schemas/keyboard.jsonschema +++ b/data/schemas/keyboard.jsonschema @@ -92,6 +92,16 @@ "enum": ["COL2ROW", "ROW2COL"] }, "debounce": {"$ref": "qmk.definitions.v1#/unsigned_int"}, + "caps_word": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": {"type": "boolean"}, + "both_shifts_turns_on": {"type": "boolean"}, + "double_tap_shift_turns_on": {"type": "boolean"}, + "idle_timeout": {"$ref": "qmk.definitions.v1#/unsigned_int"}, + }, + }, "combo": { "type": "object", "properties": { diff --git a/docs/_summary.md b/docs/_summary.md index 786685eba4..fed75196b4 100644 --- a/docs/_summary.md +++ b/docs/_summary.md @@ -75,6 +75,7 @@ * Software Features * [Auto Shift](feature_auto_shift.md) + * [Caps Word](feature_caps_word.md) * [Combos](feature_combo.md) * [Debounce API](feature_debounce_type.md) * [Key Lock](feature_key_lock.md) diff --git a/docs/feature_caps_word.md b/docs/feature_caps_word.md new file mode 100644 index 0000000000..0b71119917 --- /dev/null +++ b/docs/feature_caps_word.md @@ -0,0 +1,156 @@ +# Caps Word + +It is often useful to type a single word in all capitals, for instance +abbreviations like "QMK", or in code, identifiers like `KC_SPC`. "Caps Word" is +a modern alternative to Caps Lock: + +* Letters are capitalized while active, and Caps Word automatically disables + itself at the end of the word. That is, it stops by default once a space or + any key other than `a`--`z`, `0`--`9`, `-`, `_`, delete, or backspace is + pressed. Caps Word also disables itself if the keyboard is idle for 5 seconds. + This is configurable, see below. + +* To avoid requiring a dedicated key for Caps Word, there is an option + (`BOTH_SHIFTS_TURNS_ON_CAPS_WORD`) to activate Caps Word by simultaneously + pressing both shift keys. See below for other options. + +* The implementation does not use the Caps Lock (`KC_CAPS`) keycode. Caps Word + works even if you're remapping Caps Lock at the OS level to Ctrl or something + else, as Emacs and Vim users often do. + + +## How do I enable Caps Word :id=how-do-i-enable-caps-word + +In your `rules.mk`, add: + +```make +CAPS_WORD_ENABLE = yes +``` + +Next, use one the following methods to activate Caps Word: + +* **Activate by pressing a key**: Use the `CAPS_WORD` keycode (short + alias `CAPSWRD`) in your keymap. + +* **Activate by pressing Left Shift + Right Shift**: Add `#define + BOTH_SHIFTS_TURNS_ON_CAPS_WORD` to config.h. You may also need to disable or + reconfigure Command, details below. Then, simultaneously pressing both left + and right shifts turns on Caps Word. This method works with the plain + `KC_LSFT` and `KC_RSFT` keycodes as well as one-shot shifts and Space Cadet + shifts. If your shift keys are mod-taps, hold both shift mod-tap keys until + the tapping term, then release them. + +* **Activate by double tapping Left Shift**: Add `#define + DOUBLE_TAP_SHIFT_TURNS_ON_CAPS_WORD` config.h. Then, double tapping Left Shift + turns on Caps Word. This method works with `KC_LSFT` or one-shot Left Shift + `OSM(MOD_LSFT)`. To count as a double tap, the maximum time in milliseconds + between taps is `TAPPING_TERM`, or if using `TAPPING_TERM_PER_KEY`, the time + returned by `get_tapping_term()` for the shift keycode being tapped. + +* **Custom activation**: You can activate Caps Word from code by calling + `caps_word_on()`. This may be used to activate Caps Word through [a + combo](feature_combo.md) or [tap dance](feature_tap_dance.md) or any means + you like. + +### Troubleshooting: Command :id=troubleshooting-command + +When using `BOTH_SHIFTS_TURNS_ON_CAPS_WORD`, you might see a compile message +**"BOTH_SHIFTS_TURNS_ON_CAPS_WORD and Command should not be enabled at the same +time, since both use the Left Shift + Right Shift key combination."** + +Many keyboards enable the [Command feature](feature_command.md), which by +default is also activated using the Left Shift + Right Shift key combination. To +fix this conflict, please disable Command by adding in rules.mk: + +```make +COMMAND_ENABLE = no +``` + +Or configure Command to use another key combination like Left Ctrl + Right Ctrl +by defining `IS_COMMAND()` in config.h: + +```c +// Activate Command with Left Ctrl + Right Ctrl. +#define IS_COMMAND() (get_mods() == MOD_MASK_CTRL) +``` + + +## Customizing Caps Word :id=customizing-caps-word + +### Idle timeout :id=idle-timeout + +Caps Word turns off automatically if no keys are pressed for +`CAPS_WORD_IDLE_TIMEOUT` milliseconds. The default is 5000 (5 seconds). +Configure the timeout duration in config.h, for instance + +```c +#define CAPS_WORD_IDLE_TIMEOUT 3000 // 3 seconds. +``` + +Setting `CAPS_WORD_IDLE_TIMEOUT` to 0 configures Caps Word to never time out. +Caps Word then remains active indefinitely until a word breaking key is pressed. + + +### Functions :id=functions + +Functions to manipulate Caps Word: + +| Function | Description | +|-------------------------|------------------------------------------------| +| `caps_word_on()` | Turns Caps Word on. | +| `caps_word_off()` | Turns Caps Word off. | +| `caps_word_toggle()` | Toggles Caps Word. | +| `is_caps_word_on()` | Returns true if Caps Word is currently on. | + + +### Configure which keys are "word breaking" :id=configure-which-keys-are-word-breaking + +You can define the `caps_word_press_user(uint16_t keycode)` callback to +configure which keys should be shifted and which keys are considered "word +breaking" and stop Caps Word. + +The callback is called on every key press while Caps Word is active. When the +key should be shifted (that is, a letter key), the callback should call +`add_weak_mods(MOD_BIT(KC_LSFT))` to shift the key. Returning true continues the +current "word," while returning false is "word breaking" and deactivates Caps +Word. The default callback is + +```c +bool caps_word_press_user(uint16_t keycode) { + switch (keycode) { + // Keycodes that continue Caps Word, with shift applied. + case KC_A ... KC_Z: + case KC_MINS: + add_weak_mods(MOD_BIT(KC_LSFT)); // Apply shift to next key. + return true; + + // Keycodes that continue Caps Word, without shifting. + case KC_1 ... KC_0: + case KC_BSPC: + case KC_DEL: + case KC_UNDS: + return true; + + default: + return false; // Deactivate Caps Word. + } +} +``` + + +### Representing Caps Word state :id=representing-caps-word-state + +Define `caps_word_set_user(bool active)` to get callbacks when Caps Word turns +on or off. This is useful to represent the current Caps Word state, e.g. by +setting an LED or playing a sound. In your keymap, define + +```c +void caps_word_set_user(bool active) { + if (active) { + // Do something when Caps Word activates. + } else { + // Do something when Caps Word deactivates. + } +} +``` + diff --git a/docs/keycodes.md b/docs/keycodes.md index 10652bdb2f..bd5af32dd3 100644 --- a/docs/keycodes.md +++ b/docs/keycodes.md @@ -269,6 +269,14 @@ See also: [Bluetooth](feature_bluetooth.md) |`OUT_USB` |USB only | |`OUT_BT` |Bluetooth only | +## Caps Word :id=caps-word + +See also: [Caps Word](feature_caps_word.md) + +|Key |Aliases |Description | +|-----------|---------|------------------------------| +|`CAPS_WORD`|`CAPSWRD`|Toggles Caps Word | + ## Dynamic Macros :id=dynamic-macros See also: [Dynamic Macros](feature_dynamic_macros.md) diff --git a/quantum/caps_word.c b/quantum/caps_word.c new file mode 100644 index 0000000000..5b83659f28 --- /dev/null +++ b/quantum/caps_word.c @@ -0,0 +1,80 @@ +// Copyright 2021-2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "caps_word.h" + +/** @brief True when Caps Word is active. */ +static bool caps_word_active = false; + +#if CAPS_WORD_IDLE_TIMEOUT > 0 +// Constrain timeout to a sensible range. With 16-bit timers, the longest +// timeout possible is 32768 ms, rounded here to 30000 ms = half a minute. +# if CAPS_WORD_IDLE_TIMEOUT < 100 || CAPS_WORD_IDLE_TIMEOUT > 30000 +# error "CAPS_WORD_IDLE_TIMEOUT must be between 100 and 30000 ms" +# endif + +/** @brief Deadline for idle timeout. */ +static uint16_t idle_timer = 0; + +void caps_word_task(void) { + if (caps_word_active && timer_expired(timer_read(), idle_timer)) { + caps_word_off(); + } +} + +void caps_word_reset_idle_timer(void) { + idle_timer = timer_read() + CAPS_WORD_IDLE_TIMEOUT; +} +#endif // CAPS_WORD_IDLE_TIMEOUT > 0 + +void caps_word_on(void) { + if (caps_word_active) { + return; + } + + clear_mods(); +#ifndef NO_ACTION_ONESHOT + clear_oneshot_mods(); +#endif // NO_ACTION_ONESHOT +#if CAPS_WORD_IDLE_TIMEOUT > 0 + caps_word_reset_idle_timer(); +#endif // CAPS_WORD_IDLE_TIMEOUT > 0 + + caps_word_active = true; + caps_word_set_user(true); +} + +void caps_word_off(void) { + if (!caps_word_active) { + return; + } + + unregister_weak_mods(MOD_MASK_SHIFT); // Make sure weak shift is off. + caps_word_active = false; + caps_word_set_user(false); +} + +void caps_word_toggle(void) { + if (caps_word_active) { + caps_word_off(); + } else { + caps_word_on(); + } +} + +bool is_caps_word_on(void) { + return caps_word_active; +} + +__attribute__((weak)) void caps_word_set_user(bool active) {} diff --git a/quantum/caps_word.h b/quantum/caps_word.h new file mode 100644 index 0000000000..b83f73371e --- /dev/null +++ b/quantum/caps_word.h @@ -0,0 +1,43 @@ +// Copyright 2021-2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include "quantum.h" + +#ifndef CAPS_WORD_IDLE_TIMEOUT +# define CAPS_WORD_IDLE_TIMEOUT 5000 // Default timeout of 5 seconds. +#endif // CAPS_WORD_IDLE_TIMEOUT + +#if CAPS_WORD_IDLE_TIMEOUT > 0 +/** @brief Matrix scan task for Caps Word feature */ +void caps_word_task(void); + +/** @brief Resets timer for Caps Word idle timeout. */ +void caps_word_reset_idle_timer(void); +#else +static inline void caps_word_task(void) {} +#endif // CAPS_WORD_IDLE_TIMEOUT > 0 + +void caps_word_on(void); /**< Activates Caps Word. */ +void caps_word_off(void); /**< Deactivates Caps Word. */ +void caps_word_toggle(void); /**< Toggles Caps Word. */ +bool is_caps_word_on(void); /**< Gets whether currently active. */ + +/** + * @brief Caps Word set callback. + * + * @param active True if Caps Word is active, false otherwise + */ +void caps_word_set_user(bool active); diff --git a/quantum/keyboard.c b/quantum/keyboard.c index 1dccd48e2f..a65f9d6d18 100644 --- a/quantum/keyboard.c +++ b/quantum/keyboard.c @@ -108,6 +108,9 @@ along with this program. If not, see . #ifdef BLUETOOTH_ENABLE # include "outputselect.h" #endif +#ifdef CAPS_WORD_ENABLE +# include "caps_word.h" +#endif static uint32_t last_input_modification_time = 0; uint32_t last_input_activity_time(void) { @@ -549,6 +552,10 @@ void quantum_task(void) { autoshift_matrix_scan(); #endif +#ifdef CAPS_WORD_ENABLE + caps_word_task(); +#endif + #ifdef SECURE_ENABLE secure_task(); #endif diff --git a/quantum/process_keycode/process_caps_word.c b/quantum/process_keycode/process_caps_word.c new file mode 100644 index 0000000000..15238f04a1 --- /dev/null +++ b/quantum/process_keycode/process_caps_word.c @@ -0,0 +1,160 @@ +// Copyright 2021-2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "process_caps_word.h" + +bool process_caps_word(uint16_t keycode, keyrecord_t* record) { + if (keycode == CAPSWRD) { // Pressing CAPSWRD toggles Caps Word. + if (record->event.pressed) { + caps_word_toggle(); + } + return false; + } + +#ifndef NO_ACTION_ONESHOT + const uint8_t mods = get_mods() | get_oneshot_mods(); +#else + const uint8_t mods = get_mods(); +#endif // NO_ACTION_ONESHOT + + if (!is_caps_word_on()) { + // The following optionally turns on Caps Word by holding left and + // right shifts or by double tapping left shift. This way Caps Word + // may be used without needing a dedicated key and also without + // needing combos or tap dance. + +#ifdef BOTH_SHIFTS_TURNS_ON_CAPS_WORD + // Many keyboards enable the Command feature by default, which also + // uses left+right shift. It can be configured to use a different + // key combination by defining IS_COMMAND(). We make a non-fatal + // warning if Command is enabled but IS_COMMAND() is *not* defined. +# if defined(COMMAND_ENABLE) && !defined(IS_COMMAND) +# pragma message "BOTH_SHIFTS_TURNS_ON_CAPS_WORD and Command should not be enabled at the same time, since both use the Left Shift + Right Shift key combination. Please disable Command, or ensure that `IS_COMMAND` is not set to (get_mods() == MOD_MASK_SHIFT)." +# else + if (mods == MOD_MASK_SHIFT +# ifdef COMMAND_ENABLE + // Don't activate Caps Word at the same time as Command. + && !(IS_COMMAND()) +# endif // COMMAND_ENABLE + ) { + caps_word_on(); + } +# endif // defined(COMMAND_ENABLE) && !defined(IS_COMMAND) +#endif // BOTH_SHIFTS_TURNS_ON_CAPS_WORD + +#ifdef DOUBLE_TAP_SHIFT_TURNS_ON_CAPS_WORD + // Double tapping left shift turns on Caps Word. + // + // NOTE: This works with KC_LSFT and one-shot left shift. It + // wouldn't make sense with mod-tap or Space Cadet shift since + // double tapping would of course trigger the tapping action. + if (record->event.pressed) { + static bool tapped = false; + static uint16_t timer = 0; + if (keycode == KC_LSFT || keycode == OSM(MOD_LSFT)) { + if (tapped && !timer_expired(record->event.time, timer)) { + // Left shift was double tapped, activate Caps Word. + caps_word_on(); + } + tapped = true; + timer = record->event.time + GET_TAPPING_TERM(keycode, record); + } else { + tapped = false; // Reset when any other key is pressed. + } + } +#endif // DOUBLE_TAP_SHIFT_TURNS_ON_CAPS_WORD + + return true; + } + +#if CAPS_WORD_IDLE_TIMEOUT > 0 + caps_word_reset_idle_timer(); +#endif // CAPS_WORD_IDLE_TIMEOUT > 0 + + // From here on, we only take action on press events. + if (!record->event.pressed) { + return true; + } + + if (!(mods & ~MOD_MASK_SHIFT)) { + switch (keycode) { + // Ignore MO, TO, TG, TT, and OSL layer switch keys. + case QK_MOMENTARY ... QK_MOMENTARY_MAX: + case QK_TO ... QK_TO_MAX: + case QK_TOGGLE_LAYER ... QK_TOGGLE_LAYER_MAX: + case QK_LAYER_TAP_TOGGLE ... QK_LAYER_TAP_TOGGLE_MAX: + case QK_ONE_SHOT_LAYER ... QK_ONE_SHOT_LAYER_MAX: + return true; + +#ifndef NO_ACTION_TAPPING + case QK_MOD_TAP ... QK_MOD_TAP_MAX: + if (record->tap.count == 0) { + // Deactivate if a mod becomes active through holding + // a mod-tap key. + caps_word_off(); + return true; + } + keycode &= 0xff; + break; + +# ifndef NO_ACTION_LAYER + case QK_LAYER_TAP ... QK_LAYER_TAP_MAX: +# endif // NO_ACTION_LAYER + if (record->tap.count == 0) { + return true; + } + keycode &= 0xff; + break; +#endif // NO_ACTION_TAPPING + +#ifdef SWAP_HANDS_ENABLE + case QK_SWAP_HANDS ... QK_SWAP_HANDS_MAX: + if (keycode > 0x56F0 || record->tap.count == 0) { + return true; + } + keycode &= 0xff; + break; +#endif // SWAP_HANDS_ENABLE + } + + clear_weak_mods(); + if (caps_word_press_user(keycode)) { + send_keyboard_report(); + return true; + } + } + + caps_word_off(); + return true; +} + +__attribute__((weak)) bool caps_word_press_user(uint16_t keycode) { + switch (keycode) { + // Keycodes that continue Caps Word, with shift applied. + case KC_A ... KC_Z: + case KC_MINS: + add_weak_mods(MOD_BIT(KC_LSFT)); // Apply shift to next key. + return true; + + // Keycodes that continue Caps Word, without shifting. + case KC_1 ... KC_0: + case KC_BSPC: + case KC_DEL: + case KC_UNDS: + return true; + + default: + return false; // Deactivate Caps Word. + } +} diff --git a/quantum/process_keycode/process_caps_word.h b/quantum/process_keycode/process_caps_word.h new file mode 100644 index 0000000000..f215bbc3a3 --- /dev/null +++ b/quantum/process_keycode/process_caps_word.h @@ -0,0 +1,37 @@ +// Copyright 2021-2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include "quantum.h" +#include "caps_word.h" + +/** + * @brief Process handler for Caps Word feature. + * + * @param keycode Keycode registered by matrix press, per keymap + * @param record keyrecord_t structure + * @return true Continue processing keycodes, and send to host + * @return false Stop processing keycodes, and don't send to host + */ +bool process_caps_word(uint16_t keycode, keyrecord_t* record); + +/** + * @brief Weak function for user-level Caps Word press modification. + * + * @param keycode Keycode registered by matrix press, per keymap + * @return true Continue Caps Word + * @return false Stop Caps Word + */ +bool caps_word_press_user(uint16_t keycode); diff --git a/quantum/quantum.c b/quantum/quantum.c index c0e801a4bb..b54b46760c 100644 --- a/quantum/quantum.c +++ b/quantum/quantum.c @@ -307,6 +307,9 @@ bool process_record_quantum(keyrecord_t *record) { #ifdef TERMINAL_ENABLE process_terminal(keycode, record) && #endif +#ifdef CAPS_WORD_ENABLE + process_caps_word(keycode, record) && +#endif #ifdef SPACE_CADET_ENABLE process_space_cadet(keycode, record) && #endif diff --git a/quantum/quantum.h b/quantum/quantum.h index 7829ec7e0c..92e1af1c40 100644 --- a/quantum/quantum.h +++ b/quantum/quantum.h @@ -233,6 +233,11 @@ extern layer_state_t layer_state; # include "pointing_device.h" #endif +#ifdef CAPS_WORD_ENABLE +# include "caps_word.h" +# include "process_caps_word.h" +#endif + // For tri-layer void update_tri_layer(uint8_t layer1, uint8_t layer2, uint8_t layer3); layer_state_t update_tri_layer_state(layer_state_t state, uint8_t layer1, uint8_t layer2, uint8_t layer3); diff --git a/quantum/quantum_keycodes.h b/quantum/quantum_keycodes.h index 5d5c4ed8c4..2f8ee2322e 100644 --- a/quantum/quantum_keycodes.h +++ b/quantum/quantum_keycodes.h @@ -602,6 +602,8 @@ enum quantum_keycodes { SECURE_UNLOCK, SECURE_TOGGLE, + CAPS_WORD, + // Start of custom keycode range for keyboards and keymaps - always leave at the end SAFE_RANGE }; @@ -964,5 +966,6 @@ enum quantum_keycodes { #define PB_32 PROGRAMMABLE_BUTTON_32 #define PROGRAMMABLE_BUTTON_MIN PROGRAMMABLE_BUTTON_1 #define PROGRAMMABLE_BUTTON_MAX PROGRAMMABLE_BUTTON_32 +#define CAPSWRD CAPS_WORD #include "quantum_keycodes_legacy.h" diff --git a/tests/caps_word/config.h b/tests/caps_word/config.h new file mode 100644 index 0000000000..0d5cebd778 --- /dev/null +++ b/tests/caps_word/config.h @@ -0,0 +1,21 @@ +// Copyright 2022 Google LLC +// +// 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 . + +#pragma once + +#include "test_common.h" + +#define BOTH_SHIFTS_TURNS_ON_CAPS_WORD +#define DOUBLE_TAP_SHIFT_TURNS_ON_CAPS_WORD diff --git a/tests/caps_word/test.mk b/tests/caps_word/test.mk new file mode 100644 index 0000000000..2509b01858 --- /dev/null +++ b/tests/caps_word/test.mk @@ -0,0 +1,19 @@ +# Copyright 2022 Google LLC +# +# 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 . + +CAPS_WORD_ENABLE = yes +COMMAND_ENABLE = no +SPACE_CADET_ENABLE = yes + diff --git a/tests/caps_word/test_caps_word.cpp b/tests/caps_word/test_caps_word.cpp new file mode 100644 index 0000000000..bcc8c53326 --- /dev/null +++ b/tests/caps_word/test_caps_word.cpp @@ -0,0 +1,423 @@ +// Copyright 2022 Google LLC +// +// 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 . + +#include "keyboard_report_util.hpp" +#include "keycode.h" +#include "test_common.hpp" +#include "test_fixture.hpp" +#include "test_keymap_key.hpp" + +using ::testing::_; +using ::testing::AnyNumber; +using ::testing::AnyOf; +using ::testing::InSequence; +using ::testing::TestParamInfo; + +class CapsWord : public TestFixture { + public: + void SetUp() override { + caps_word_off(); + } + + // Convenience function to tap `key`. + void TapKey(KeymapKey key) { + key.press(); + run_one_scan_loop(); + key.release(); + run_one_scan_loop(); + } + + // Taps in order each key in `keys`. + template + void TapKeys(Ts... keys) { + for (KeymapKey key : {keys...}) { + TapKey(key); + } + } +}; + +// Tests caps_word_on(), _off(), and _toggle() functions. +TEST_F(CapsWord, OnOffToggleFuns) { + TestDriver driver; + + EXPECT_EQ(is_caps_word_on(), false); + + caps_word_on(); + EXPECT_EQ(is_caps_word_on(), true); + caps_word_on(); + EXPECT_EQ(is_caps_word_on(), true); + + caps_word_off(); + EXPECT_EQ(is_caps_word_on(), false); + caps_word_off(); + EXPECT_EQ(is_caps_word_on(), false); + + caps_word_toggle(); + EXPECT_EQ(is_caps_word_on(), true); + caps_word_toggle(); + EXPECT_EQ(is_caps_word_on(), false); + + testing::Mock::VerifyAndClearExpectations(&driver); +} + +// Tests the default `caps_word_press_user()` function. +TEST_F(CapsWord, DefaultCapsWordPressUserFun) { + // Spot check some keycodes that continue Caps Word, with shift applied. + for (uint16_t keycode : {KC_A, KC_B, KC_Z, KC_MINS}) { + SCOPED_TRACE("keycode: " + testing::PrintToString(keycode)); + clear_weak_mods(); + EXPECT_TRUE(caps_word_press_user(keycode)); + EXPECT_EQ(get_weak_mods(), MOD_BIT(KC_LSFT)); + } + + // Some keycodes that continue Caps Word, without shifting. + for (uint16_t keycode : {KC_1, KC_9, KC_0, KC_BSPC, KC_DEL}) { + SCOPED_TRACE("keycode: " + testing::PrintToString(keycode)); + clear_weak_mods(); + EXPECT_TRUE(caps_word_press_user(keycode)); + EXPECT_EQ(get_weak_mods(), 0); + } + + // Some keycodes that turn off Caps Word. + for (uint16_t keycode : {KC_SPC, KC_DOT, KC_COMM, KC_TAB, KC_ESC, KC_ENT}) { + SCOPED_TRACE("keycode: " + testing::PrintToString(keycode)); + EXPECT_FALSE(caps_word_press_user(keycode)); + } +} + +// Tests that `CAPSWRD` key toggles Caps Word. +TEST_F(CapsWord, CapswrdKey) { + TestDriver driver; + KeymapKey key_capswrd(0, 0, 0, CAPSWRD); + set_keymap({key_capswrd}); + + // No keyboard reports should be sent. + EXPECT_CALL(driver, send_keyboard_mock(_)).Times(0); + + TapKey(key_capswrd); // Tap the CAPSWRD key. + EXPECT_EQ(is_caps_word_on(), true); + + TapKey(key_capswrd); // Tap the CAPSWRD key again. + EXPECT_EQ(is_caps_word_on(), false); + + testing::Mock::VerifyAndClearExpectations(&driver); +} + +// Tests that being idle for CAPS_WORD_IDLE_TIMEOUT turns off Caps Word. +TEST_F(CapsWord, IdleTimeout) { + TestDriver driver; + KeymapKey key_a(0, 0, 0, KC_A); + set_keymap({key_a}); + + // Allow any number of reports with no keys or only KC_LSFT. + // clang-format off + EXPECT_CALL(driver, send_keyboard_mock(AnyOf( + KeyboardReport(), + KeyboardReport(KC_LSFT)))) + .Times(AnyNumber()); + // clang-format on + + // Expect "Shift+A". + EXPECT_CALL(driver, send_keyboard_mock(KeyboardReport(KC_LSFT, KC_A))); + + // Turn on Caps Word and tap "A". + caps_word_on(); + TapKey(key_a); + + testing::Mock::VerifyAndClearExpectations(&driver); + + idle_for(CAPS_WORD_IDLE_TIMEOUT); + run_one_scan_loop(); + + // Caps Word should be off and mods should be clear. + EXPECT_EQ(is_caps_word_on(), false); + EXPECT_EQ(get_mods() | get_weak_mods(), 0); + + EXPECT_CALL(driver, send_keyboard_mock(KeyboardReport())).Times(AnyNumber()); + // Expect unshifted "A". + EXPECT_CALL(driver, send_keyboard_mock(KeyboardReport(KC_A))); + TapKey(key_a); + + testing::Mock::VerifyAndClearExpectations(&driver); +} + +// Tests that typing "A, 4, A, 4" produces "Shift+A, 4, Shift+A, 4". +TEST_F(CapsWord, ShiftsLettersButNotDigits) { + TestDriver driver; + KeymapKey key_a(0, 0, 0, KC_A); + KeymapKey key_4(0, 1, 0, KC_4); + set_keymap({key_a, key_4}); + + // Allow any number of reports with no keys or only KC_LSFT. + // clang-format off + EXPECT_CALL(driver, send_keyboard_mock(AnyOf( + KeyboardReport(), + KeyboardReport(KC_LSFT)))) + .Times(AnyNumber()); + // clang-format on + + { // Expect: "Shift+A, 4, Shift+A, 4". + InSequence s; + EXPECT_CALL(driver, send_keyboard_mock(KeyboardReport(KC_LSFT, KC_A))); + EXPECT_CALL(driver, send_keyboard_mock(KeyboardReport(KC_4))); + EXPECT_CALL(driver, send_keyboard_mock(KeyboardReport(KC_LSFT, KC_A))); + EXPECT_CALL(driver, send_keyboard_mock(KeyboardReport(KC_4))); + } + + // Turn on Caps Word and tap "A, 4, A, 4". + caps_word_on(); + TapKeys(key_a, key_4, key_a, key_4); + + testing::Mock::VerifyAndClearExpectations(&driver); +} + +// Tests that typing "A, Space, A" produces "Shift+A, Space, A". +TEST_F(CapsWord, SpaceTurnsOffCapsWord) { + TestDriver driver; + KeymapKey key_a(0, 0, 0, KC_A); + KeymapKey key_spc(0, 1, 0, KC_SPC); + set_keymap({key_a, key_spc}); + + // Allow any number of reports with no keys or only KC_LSFT. + // clang-format off + EXPECT_CALL(driver, send_keyboard_mock(AnyOf( + KeyboardReport(), + KeyboardReport(KC_LSFT)))) + .Times(AnyNumber()); + // clang-format on + + { // Expect: "Shift+A, Space, A". + InSequence seq; + EXPECT_CALL(driver, send_keyboard_mock(KeyboardReport(KC_LSFT, KC_A))); + EXPECT_CALL(driver, send_keyboard_mock(KeyboardReport(KC_SPC))); + EXPECT_CALL(driver, send_keyboard_mock(KeyboardReport(KC_A))); + } + + // Turn on Caps Word and tap "A, Space, A". + caps_word_on(); + TapKeys(key_a, key_spc, key_a); + + testing::Mock::VerifyAndClearExpectations(&driver); +} + +struct CapsWordBothShiftsParams { + std::string name; + uint16_t left_shift_keycode; + uint16_t right_shift_keycode; + + static const std::string& GetName(const TestParamInfo& info) { + return info.param.name; + } +}; + +// Tests the BOTH_SHIFTS_TURNS_ON_CAPS_WORD method to turn on Caps Word. +class CapsWordBothShifts : public ::testing::WithParamInterface, public CapsWord {}; + +// Pressing shifts as "Left down, Right down, Left up, Right up". +TEST_P(CapsWordBothShifts, PressLRLR) { + TestDriver driver; + KeymapKey left_shift(0, 0, 0, GetParam().left_shift_keycode); + KeymapKey right_shift(0, 1, 0, GetParam().right_shift_keycode); + set_keymap({left_shift, right_shift}); + + // clang-format off + EXPECT_CALL(driver, send_keyboard_mock(AnyOf( + KeyboardReport(), + KeyboardReport(KC_LSFT), + KeyboardReport(KC_RSFT), + KeyboardReport(KC_LSFT, KC_RSFT)))) + .Times(AnyNumber()); + // clang-format on + + EXPECT_EQ(is_caps_word_on(), false); + + left_shift.press(); // Press both shifts. + run_one_scan_loop(); + right_shift.press(); + + // For mod-tap and Space Cadet keys, wait for the tapping term. + if (left_shift.code == LSFT_T(KC_A) || left_shift.code == KC_LSPO) { + idle_for(TAPPING_TERM); + } + + run_one_scan_loop(); + left_shift.release(); // Release both. + run_one_scan_loop(); + right_shift.release(); + run_one_scan_loop(); + + EXPECT_EQ(is_caps_word_on(), true); + + testing::Mock::VerifyAndClearExpectations(&driver); +} + +// Pressing shifts as "Left down, Right down, Right up, Left up". +TEST_P(CapsWordBothShifts, PressLRRL) { + TestDriver driver; + KeymapKey left_shift(0, 0, 0, GetParam().left_shift_keycode); + KeymapKey right_shift(0, 1, 0, GetParam().right_shift_keycode); + set_keymap({left_shift, right_shift}); + + // clang-format off + EXPECT_CALL(driver, send_keyboard_mock(AnyOf( + KeyboardReport(), + KeyboardReport(KC_LSFT), + KeyboardReport(KC_RSFT), + KeyboardReport(KC_LSFT, KC_RSFT)))) + .Times(AnyNumber()); + // clang-format on + + EXPECT_EQ(is_caps_word_on(), false); + + left_shift.press(); // Press both shifts. + run_one_scan_loop(); + right_shift.press(); + + if (left_shift.code == LSFT_T(KC_A) || left_shift.code == KC_LSPO) { + idle_for(TAPPING_TERM); + } + run_one_scan_loop(); + + right_shift.release(); // Release both. + run_one_scan_loop(); + left_shift.release(); + run_one_scan_loop(); + + EXPECT_EQ(is_caps_word_on(), true); + + testing::Mock::VerifyAndClearExpectations(&driver); +} + +// clang-format off +INSTANTIATE_TEST_CASE_P( + ShiftPairs, + CapsWordBothShifts, + ::testing::Values( + CapsWordBothShiftsParams{ + "PlainShifts", KC_LSFT, KC_RSFT}, + CapsWordBothShiftsParams{ + "OneshotShifts", OSM(MOD_LSFT), OSM(MOD_RSFT)}, + CapsWordBothShiftsParams{ + "SpaceCadetShifts", KC_LSPO, KC_RSPC}, + CapsWordBothShiftsParams{ + "ModTapShifts", LSFT_T(KC_A), RSFT_T(KC_B)} + ), + CapsWordBothShiftsParams::GetName + ); +// clang-format on + +struct CapsWordDoubleTapShiftParams { + std::string name; + uint16_t left_shift_keycode; + + static const std::string& GetName(const TestParamInfo& info) { + return info.param.name; + } +}; + +// Tests the DOUBLE_TAP_SHIFT_TURNS_ON_CAPS_WORD method to turn on Caps Word. +class CapsWordDoubleTapShift : public ::testing::WithParamInterface, public CapsWord {}; + +// Tests that double tapping activates Caps Word. +TEST_P(CapsWordDoubleTapShift, Activation) { + TestDriver driver; + KeymapKey left_shift(0, 0, 0, GetParam().left_shift_keycode); + set_keymap({left_shift}); + + // clang-format off + EXPECT_CALL(driver, send_keyboard_mock(AnyOf( + KeyboardReport(), + KeyboardReport(KC_LSFT)))) + .Times(AnyNumber()); + // clang-format on + + EXPECT_EQ(is_caps_word_on(), false); + + // Tapping shift twice within the tapping term turns on Caps Word. + TapKey(left_shift); + idle_for(TAPPING_TERM - 10); + TapKey(left_shift); + + EXPECT_EQ(is_caps_word_on(), true); + + testing::Mock::VerifyAndClearExpectations(&driver); +} + +// Double tap doesn't count if another key is pressed between the taps. +TEST_P(CapsWordDoubleTapShift, Interrupted) { + TestDriver driver; + KeymapKey left_shift(0, 0, 0, GetParam().left_shift_keycode); + KeymapKey key_a(0, 1, 0, KC_A); + set_keymap({left_shift, key_a}); + + // clang-format off + EXPECT_CALL(driver, send_keyboard_mock(AnyOf( + KeyboardReport(), + KeyboardReport(KC_LSFT), + KeyboardReport(KC_LSFT, KC_A)))) + .Times(AnyNumber()); + // clang-format on + + left_shift.press(); + run_one_scan_loop(); + + TapKey(key_a); // 'A' key interrupts the double tap. + + left_shift.release(); + run_one_scan_loop(); + + idle_for(TAPPING_TERM - 10); + TapKey(left_shift); + + EXPECT_EQ(is_caps_word_on(), false); // Caps Word is still off. + clear_oneshot_mods(); + + testing::Mock::VerifyAndClearExpectations(&driver); +} + +// Double tap doesn't count if taps are more than tapping term apart. +TEST_P(CapsWordDoubleTapShift, SlowTaps) { + TestDriver driver; + KeymapKey left_shift(0, 0, 0, GetParam().left_shift_keycode); + set_keymap({left_shift}); + + // clang-format off + EXPECT_CALL(driver, send_keyboard_mock(AnyOf( + KeyboardReport(), + KeyboardReport(KC_LSFT)))) + .Times(AnyNumber()); + // clang-format on + + TapKey(left_shift); + idle_for(TAPPING_TERM + 1); + TapKey(left_shift); + + EXPECT_EQ(is_caps_word_on(), false); // Caps Word is still off. + clear_oneshot_mods(); + + testing::Mock::VerifyAndClearExpectations(&driver); +} + +// clang-format off +INSTANTIATE_TEST_CASE_P( + Shifts, + CapsWordDoubleTapShift, + ::testing::Values( + CapsWordDoubleTapShiftParams{"PlainShift", KC_LSFT}, + CapsWordDoubleTapShiftParams{"OneshotShift", OSM(MOD_LSFT)} + ), + CapsWordDoubleTapShiftParams::GetName + ); +// clang-format on -- cgit v1.2.3 From de6360feca6ecea4ca7a409d93b8fc347323409c Mon Sep 17 00:00:00 2001 From: zvecr Date: Tue, 24 May 2022 01:55:34 +0100 Subject: stub changelog --- docs/ChangeLog/20220528.md | 161 +++++++++++++++++++++++++++++++++++++++ docs/_summary.md | 2 +- docs/breaking_changes.md | 1 + docs/breaking_changes_history.md | 1 + 4 files changed, 164 insertions(+), 1 deletion(-) create mode 100644 docs/ChangeLog/20220528.md (limited to 'docs/_summary.md') diff --git a/docs/ChangeLog/20220528.md b/docs/ChangeLog/20220528.md new file mode 100644 index 0000000000..bac681df4b --- /dev/null +++ b/docs/ChangeLog/20220528.md @@ -0,0 +1,161 @@ +# QMK Breaking Changes - 2022 May 28 Changelog + +## Notable Features :id=notable-features + +### TODO + +### Updated Keyboard Codebases :id=updated-keyboard-codebases + +The following keyboards have had their source moved within QMK: + +| Old Keyboard Name | New Keyboard Name | +|-----------------------|--------------------| +| absinthe | keyhive/absinthe | +| amj40 | amjkeyboard/amj40 | +| amj60 | amjkeyboard/amj60 | +| amj96 | amjkeyboard/amj96 | +| amjpad | amjkeyboard/amjpad | +| at101_bh | viktus/at101_bh | +| ergosaurus | keyhive/ergosaurus | +| gmmk/pro/ansi | gmmk/pro/rev1/ansi | +| gmmk/pro/iso | gmmk/pro/rev1/iso | +| honeycomb | keyhive/honeycomb | +| lattice60 | keyhive/lattice60 | +| melody96 | ymdk/melody96 | +| mt40 | mt/mt40 | +| mt64rgb | mt/mt64rgb | +| mt84 | mt/mt84 | +| mt980 | mt/mt980 | +| navi10 | keyhive/navi10 | +| omnikey_bh | viktus/omnikey_bh | +| opus | keyhive/opus | +| smallice | keyhive/smallice | +| southpole | keyhive/southpole | +| uno | keyhive/uno | +| ut472 | keyhive/ut472 | +| wheatfield/blocked65 | mt/blocked65 | +| wheatfield/split75 | mt/split75 | +| z150_bh | viktus/z150_bh | + +## Notable core changes :id=notable-core + +### TODO + +--- + +## Full changelist + +Core: +* Quantum Painter ([#10174](https://github.com/qmk/qmk_firmware/pull/10174)) +* Add support for encoder mapping. ([#13286](https://github.com/qmk/qmk_firmware/pull/13286)) +* Add support for multiple switchs/solenoids to Haptic Feedback engine ([#15657](https://github.com/qmk/qmk_firmware/pull/15657)) +* Add compile/make macro to core ([#15959](https://github.com/qmk/qmk_firmware/pull/15959)) +* Add Reboot keycode to core ([#15990](https://github.com/qmk/qmk_firmware/pull/15990)) +* Add support for multiple sensors to pmw3360 ([#15996](https://github.com/qmk/qmk_firmware/pull/15996)) +* Asymmetric encoders, encoder tests. ([#16068](https://github.com/qmk/qmk_firmware/pull/16068)) +* Add hacky via support for RGB Matrix ([#16086](https://github.com/qmk/qmk_firmware/pull/16086)) +* Allow usage of AVRs minimal printf library ([#16266](https://github.com/qmk/qmk_firmware/pull/16266)) +* Squeeze AVR some more with `-mrelax` and `-mcall-prologues` ([#16269](https://github.com/qmk/qmk_firmware/pull/16269)) +* Heatmap incorrect matrix effect workaround ([#16315](https://github.com/qmk/qmk_firmware/pull/16315)) +* Add SN74x154 driver and convert AL1 custom matrix ([#16331](https://github.com/qmk/qmk_firmware/pull/16331)) +* Add customizable snake and knight animation increments ([#16337](https://github.com/qmk/qmk_firmware/pull/16337)) +* Chibios USB protocol: allow overriding RAW Capacity ([#16339](https://github.com/qmk/qmk_firmware/pull/16339)) +* HD44780 driver rework ([#16370](https://github.com/qmk/qmk_firmware/pull/16370)) +* Update wb32-dfu ([#16438](https://github.com/qmk/qmk_firmware/pull/16438)) +* Remove `send_unicode_hex_string()` ([#16518](https://github.com/qmk/qmk_firmware/pull/16518)) +* Add :flash target for UF2 bootloaders ([#16525](https://github.com/qmk/qmk_firmware/pull/16525)) +* Move `has_mouse_report_changed` function to `report.c` ([#16543](https://github.com/qmk/qmk_firmware/pull/16543)) +* Move Doxygen docs to subdirectory ([#16561](https://github.com/qmk/qmk_firmware/pull/16561)) +* Add Caps Word feature to core ([#16588](https://github.com/qmk/qmk_firmware/pull/16588)) +* Add non blackpill F4x1 config files ([#16600](https://github.com/qmk/qmk_firmware/pull/16600)) +* Force platform pin defs to be included ([#16611](https://github.com/qmk/qmk_firmware/pull/16611)) +* Refactor CTPC logic to allow future converters ([#16621](https://github.com/qmk/qmk_firmware/pull/16621)) +* Use a mutex guard for split shared memory ([#16647](https://github.com/qmk/qmk_firmware/pull/16647)) +* Rename TICK to TICK_EVENT ([#16649](https://github.com/qmk/qmk_firmware/pull/16649)) +* Add GET_TAPPING_TERM macro to reduce duplicate code ([#16681](https://github.com/qmk/qmk_firmware/pull/16681)) +* add the ability to change the pwm frequency for the IS31FL3737B ([#16718](https://github.com/qmk/qmk_firmware/pull/16718)) +* Joystick feature updates ([#16732](https://github.com/qmk/qmk_firmware/pull/16732)) +* Add emulated eeprom support for STM32F303xE ([#16737](https://github.com/qmk/qmk_firmware/pull/16737)) +* Refactor writePin to work with statements ([#16738](https://github.com/qmk/qmk_firmware/pull/16738)) +* Add mechanism to limit available converters ([#16783](https://github.com/qmk/qmk_firmware/pull/16783)) +* Implement XAP 'secure' core requirements ([#16843](https://github.com/qmk/qmk_firmware/pull/16843)) +* rgblight: Add functions to stop blinking one or all but one layer ([#16859](https://github.com/qmk/qmk_firmware/pull/16859)) +* Expose API for hardware unique ID ([#16869](https://github.com/qmk/qmk_firmware/pull/16869)) +* Added support for Wb32fq95 ([#16871](https://github.com/qmk/qmk_firmware/pull/16871)) +* Provide better config defaults for bluepill boards ([#16909](https://github.com/qmk/qmk_firmware/pull/16909)) +* Joystick: Simplify report descriptor and clean up error messages ([#16926](https://github.com/qmk/qmk_firmware/pull/16926)) +* Rename keymap_extras headers for consistency ([#16939](https://github.com/qmk/qmk_firmware/pull/16939)) +* Sendstring keycode overhaul ([#16941](https://github.com/qmk/qmk_firmware/pull/16941)) +* Move disable_jtag to platforms ([#16960](https://github.com/qmk/qmk_firmware/pull/16960)) +* Remove ARM pgm_read_word workaround in rgblight ([#16961](https://github.com/qmk/qmk_firmware/pull/16961)) +* Warn about LTO with arm_atsam, not ChibiOS. ([#17106](https://github.com/qmk/qmk_firmware/pull/17106)) + +CLI: +* Rework generate-api CLI command to use .build directory ([#16441](https://github.com/qmk/qmk_firmware/pull/16441)) +* Change data driven "str" type to represent a quoted string literal ([#16516](https://github.com/qmk/qmk_firmware/pull/16516)) +* Bump the 'jsonschema' version ([#16635](https://github.com/qmk/qmk_firmware/pull/16635)) +* Add frameworking for development board presets ([#16637](https://github.com/qmk/qmk_firmware/pull/16637)) +* Extend 'qmk info' to handle keymap level overrides ([#16702](https://github.com/qmk/qmk_firmware/pull/16702)) +* Data driven `g_led_config` ([#16728](https://github.com/qmk/qmk_firmware/pull/16728)) +* Allow new-keyboard to use development_board presets ([#16785](https://github.com/qmk/qmk_firmware/pull/16785)) +* Also format *.hpp files. ([#16997](https://github.com/qmk/qmk_firmware/pull/16997)) + +Submodule updates: +* ChibiOS 21.11.1 update. ([#16251](https://github.com/qmk/qmk_firmware/pull/16251)) +* Update ChibiOS-Contrib ([#16915](https://github.com/qmk/qmk_firmware/pull/16915)) + +Keyboards: +* chore: Add personal GMMK Pro keymap ([#15320](https://github.com/qmk/qmk_firmware/pull/15320)) +* move melody96 to ymdk vendor folder ([#15680](https://github.com/qmk/qmk_firmware/pull/15680)) +* move amj keyboards into amjkeyboard vendor folder ([#15733](https://github.com/qmk/qmk_firmware/pull/15733)) +* move z150_bh at101_bh omnikey_bh to viktus/ ([#16004](https://github.com/qmk/qmk_firmware/pull/16004)) +* MS Sculpt Mobile refactor ([#16038](https://github.com/qmk/qmk_firmware/pull/16038)) +* move keyhive exclusive boards into /keyhive ([#16084](https://github.com/qmk/qmk_firmware/pull/16084)) +* move 麦田 boards into /mt ([#16095](https://github.com/qmk/qmk_firmware/pull/16095)) +* Convert Wasdat Code custom matrix to SN74x138 driver ([#16257](https://github.com/qmk/qmk_firmware/pull/16257)) +* Move GMMK Pro to allow for multiple revisions ([#16423](https://github.com/qmk/qmk_firmware/pull/16423)) +* Updated pin mapping and readme. ([#16505](https://github.com/qmk/qmk_firmware/pull/16505)) +* Map data driven `DESCRIPTION` as string literal ([#16523](https://github.com/qmk/qmk_firmware/pull/16523)) +* remove unecessary layers ([#16559](https://github.com/qmk/qmk_firmware/pull/16559)) +* Helix/rev2 move to split common ([#16723](https://github.com/qmk/qmk_firmware/pull/16723)) +* Remove some layout exceptions ([#16957](https://github.com/qmk/qmk_firmware/pull/16957)) +* Refactor legacy quantum keycodes in default-ish keymaps ([#17037](https://github.com/qmk/qmk_firmware/pull/17037)) + +Keyboard fixes: +* gboards/gergoplex: move `COMBO_ENABLE` to keymap level ([#16667](https://github.com/qmk/qmk_firmware/pull/16667)) +* usb-usb converter: community layout support ([#16773](https://github.com/qmk/qmk_firmware/pull/16773)) +* Fix build of `keyhive/uno`. ([#16891](https://github.com/qmk/qmk_firmware/pull/16891)) +* Fix uno ([#16892](https://github.com/qmk/qmk_firmware/pull/16892)) +* converter/usb_usb: remove surplus commas ([#17024](https://github.com/qmk/qmk_firmware/pull/17024)) +* Various fixes for g_led_config lint warnings ([#17104](https://github.com/qmk/qmk_firmware/pull/17104)) + +Others: +* Add warning for CTPC/CONVERT_TO_PROTON_C. ([#16782](https://github.com/qmk/qmk_firmware/pull/16782)) +* Add bluepill/blackpill development board presets ([#16806](https://github.com/qmk/qmk_firmware/pull/16806)) +* Recommend pillow as part of manual MSYS install ([#17133](https://github.com/qmk/qmk_firmware/pull/17133)) + +Bugs: +* Fix one-shot locked modifiers ([#16114](https://github.com/qmk/qmk_firmware/pull/16114)) +* Fix missing definition for non-encoder case. ([#16593](https://github.com/qmk/qmk_firmware/pull/16593)) +* Fixup builds. ([#16596](https://github.com/qmk/qmk_firmware/pull/16596)) +* Missed some erroneous prints. ([#16597](https://github.com/qmk/qmk_firmware/pull/16597)) +* Workaround for pin_def errors on KINETIS based builds ([#16614](https://github.com/qmk/qmk_firmware/pull/16614)) +* Fix flipped logic bug with One Shot `OS_ON` / `OS_OFF` keys ([#16617](https://github.com/qmk/qmk_firmware/pull/16617)) +* Redo workaround for pin_def errors on KINETIS ([#16620](https://github.com/qmk/qmk_firmware/pull/16620)) +* Fix oneshot toggle logic ([#16630](https://github.com/qmk/qmk_firmware/pull/16630)) +* Mousekeys fix ([#16640](https://github.com/qmk/qmk_firmware/pull/16640)) +* Ignore transport defaults if SPLIT_KEYBOARD is unset ([#16706](https://github.com/qmk/qmk_firmware/pull/16706)) +* Fixes #16705 : digital rain follows val ([#16716](https://github.com/qmk/qmk_firmware/pull/16716)) +* Fix AVR backlight breathing: low brightness limit & exceeding breathing table max index ([#16770](https://github.com/qmk/qmk_firmware/pull/16770)) +* Fixed usb read loops not reading until timeout ([#16827](https://github.com/qmk/qmk_firmware/pull/16827)) +* [QP] Check BPP capabilities before loading the palette ([#16863](https://github.com/qmk/qmk_firmware/pull/16863)) +* Fix #16859. ([#16865](https://github.com/qmk/qmk_firmware/pull/16865)) +* Preinstall python dependencies before executing `qmk`. ([#16874](https://github.com/qmk/qmk_firmware/pull/16874)) +* Fixup AVR builds. ([#16875](https://github.com/qmk/qmk_firmware/pull/16875)) +* Fix kinetic mouse mode ([#16951](https://github.com/qmk/qmk_firmware/pull/16951)) +* Enhancement and fixes of "Secure" feature ([#16958](https://github.com/qmk/qmk_firmware/pull/16958)) +* Check for ongoing transfers on the OUT endpoint ([#16974](https://github.com/qmk/qmk_firmware/pull/16974)) +* MSYS2 install: add some Python dependencies through Pacman ([#17025](https://github.com/qmk/qmk_firmware/pull/17025)) +* Revert "Fix kinetic mouse mode (#16951)" ([#17095](https://github.com/qmk/qmk_firmware/pull/17095)) +* Workaround for recent -Werror=array-bounds AVR issues ([#17136](https://github.com/qmk/qmk_firmware/pull/17136)) +* Bug fix: Continue Caps Word when AltGr (right Alt) is held. ([#17156](https://github.com/qmk/qmk_firmware/pull/17156)) diff --git a/docs/_summary.md b/docs/_summary.md index fed75196b4..11f5e1dd51 100644 --- a/docs/_summary.md +++ b/docs/_summary.md @@ -133,7 +133,7 @@ * Breaking Changes * [Overview](breaking_changes.md) * [My Pull Request Was Flagged](breaking_changes_instructions.md) - * [Most Recent ChangeLog](ChangeLog/20220226.md "QMK v0.16.0 - 2022 Feb 26") + * [Most Recent ChangeLog](ChangeLog/20220528.md "QMK v0.17.0 - 2022 May 28") * [Past Breaking Changes](breaking_changes_history.md) * C Development diff --git a/docs/breaking_changes.md b/docs/breaking_changes.md index 3fed268b98..375defe87d 100644 --- a/docs/breaking_changes.md +++ b/docs/breaking_changes.md @@ -8,6 +8,7 @@ The breaking change period is when we will merge PR's that change QMK in dangero ## What has been included in past Breaking Changes? +* [2022 May 28](ChangeLog/20220528.md) * [2022 Feb 26](ChangeLog/20220226.md) * [2021 Nov 27](ChangeLog/20211127.md) * [2021 Aug 28](ChangeLog/20210828.md) diff --git a/docs/breaking_changes_history.md b/docs/breaking_changes_history.md index c40989fb5e..4c38456e94 100644 --- a/docs/breaking_changes_history.md +++ b/docs/breaking_changes_history.md @@ -2,6 +2,7 @@ This page links to all previous changelogs from the QMK Breaking Changes process. +* [2022 May 28](ChangeLog/20220528.md) - version 0.17.0 * [2022 Feb 26](ChangeLog/20220226.md) - version 0.16.0 * [2021 Nov 27](ChangeLog/20211127.md) - version 0.15.0 * [2021 Aug 28](ChangeLog/20210828.md) - version 0.14.0 -- cgit v1.2.3