From f2007b7e9b622a5087584e51f9e1638dc0573364 Mon Sep 17 00:00:00 2001 From: Slavasil Date: Thu, 10 Sep 2026 14:35:16 +0300 Subject: [PATCH] initialize repository --- .gitignore | 1 + CMakeLists.txt | 21 ++ ds1302.c | 162 ++++++++++++ ds1302.h | 84 ++++++ font_ubuntu.h | 521 +++++++++++++++++++++++++++++++++++++ graphics.c | 225 ++++++++++++++++ graphics.h | 62 +++++ main.c | 589 ++++++++++++++++++++++++++++++++++++++++++ partition_table.json | 15 ++ pico_sdk_import.cmake | 121 +++++++++ pins.h | 28 ++ splash.png | Bin 0 -> 70325 bytes st7789.c | 214 +++++++++++++++ st7789.h | 33 +++ utf8.c | 59 +++++ utf8.h | 9 + 16 files changed, 2144 insertions(+) create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 ds1302.c create mode 100644 ds1302.h create mode 100644 font_ubuntu.h create mode 100644 graphics.c create mode 100644 graphics.h create mode 100644 main.c create mode 100644 partition_table.json create mode 100644 pico_sdk_import.cmake create mode 100644 pins.h create mode 100644 splash.png create mode 100644 st7789.c create mode 100644 st7789.h create mode 100644 utf8.c create mode 100644 utf8.h diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/build diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..12f7a37 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,21 @@ +cmake_minimum_required(VERSION 3.27) +set(PICO_SDK_PATH /home/sl/pico-sdk) +set(PICO_PLATFORM rp2350) +include(pico_sdk_import.cmake) + +project(GPIOTest) + +set(PICO_PSRAM_CS_PIN 47 CACHE STRING "GPIO used as Chip Select for PSRAM") +set(PICO_PSRAM_SIZE_BYTES 8388608 CACHE STRING "PSRAM size in bytes") +pico_sdk_init() + +add_executable(program main.c st7789.c graphics.c utf8.c ds1302.c beep.c) +target_link_libraries(program pico_stdlib pico_bootrom pico_rand hardware_gpio hardware_pwm hardware_spi hardware_regs hardware_psram pico_stdio_usb pico_time pico_sync) +#target_compile_definitions(program PRIVATE PICO_PSRAM_CS_PIN=${PICO_PSRAM_CS_PIN} PICO_PSRAM_SIZE_BYTES=${PICO_PSRAM_CS_PIN}) +target_compile_definitions(program PRIVATE PICO_PSRAM_CS_PIN=0 PICO_PSRAM_SIZE_BYTES=8388608) + +pico_enable_stdio_usb(program 1) + +pico_embed_pt_in_binary(program "${CMAKE_CURRENT_LIST_DIR}/partition_table.json") + +pico_add_extra_outputs(program) diff --git a/ds1302.c b/ds1302.c new file mode 100644 index 0000000..d13f2be --- /dev/null +++ b/ds1302.c @@ -0,0 +1,162 @@ +#include "ds1302.h" + +void _prepare_read(uint8_t address); +void _prepare_write(uint8_t address); +uint8_t _read_byte(); +void _write_byte(uint8_t value); +void _make_clock_cycle(); + +void _end_transfer(); + +#define REG_SECONDS 0x80 // SEC +#define REG_MINUTES 0x82 // MIN +#define REG_HOUR 0x84 // HR +#define REG_DATE 0x86 // DATE +#define REG_MONTH 0x88 // MONTH +#define REG_DAY 0x8A // DAY +#define REG_YEAR 0x8C // YEAR +#define REG_WP 0x8E // CONTROL +// TRICKLE CHARGER 1001000_ 0x90 +#define REG_BURST 0xBE // CLOCK BURST + +#define REG_RAM00 0xC0 +#define REG_RAM_BURST 0xFE + +void ds1302_init() { + gpio_init(PIN_RTC_RESET); + gpio_set_dir(PIN_RTC_RESET, GPIO_OUT); + gpio_put(PIN_RTC_RESET, LOW); + + gpio_init(PIN_RTC_CLOCK); + gpio_set_dir(PIN_RTC_CLOCK, GPIO_OUT); + gpio_put(PIN_RTC_CLOCK, LOW); + + gpio_init(PIN_RTC_IO); + gpio_set_dir(PIN_RTC_IO, GPIO_IN); +} + +int ds1302_is_clock_halted() { + _prepare_read(REG_SECONDS); + uint8_t seconds = _read_byte(); + _end_transfer(); + return (seconds & 0b10000000); +} + +void ds1302_get_datetime(DateTime *dt) { + _prepare_read(REG_BURST); + dt->second = ds1302_decode_bcd(_read_byte() & 0b01111111); + dt->minute = ds1302_decode_bcd(_read_byte() & 0b01111111); + dt->hour = ds1302_decode_bcd(_read_byte() & 0b00111111); + dt->day = ds1302_decode_bcd(_read_byte() & 0b00111111); + dt->month = ds1302_decode_bcd(_read_byte() & 0b00011111); + dt->dow = ds1302_decode_bcd(_read_byte() & 0b00000111); + dt->year = ds1302_decode_bcd(_read_byte() & 0b01111111); + _end_transfer(); +} + +void ds1302_set_datetime(DateTime *dt) { + // disable write protection + _prepare_write(REG_WP); + _write_byte(0b00000000); // 0, false + _end_transfer(); + // OR call writeProtect(0) + + _prepare_write(REG_BURST); + _write_byte(ds1302_encode_bcd(dt->second % 60)); + _write_byte(ds1302_encode_bcd(dt->minute % 60)); + _write_byte(ds1302_encode_bcd(dt->hour % 24)); + _write_byte(ds1302_encode_bcd(dt->day % 32)); + _write_byte(ds1302_encode_bcd(dt->month % 13)); + _write_byte(ds1302_encode_bcd(dt->dow % 8)); + _write_byte(ds1302_encode_bcd(dt->year % 100)); + _write_byte(0b10000000); + _end_transfer(); +} + +void ds1302_halt() { + _prepare_write(REG_SECONDS); + _write_byte(0b10000000); + _end_transfer(); +} + +void _prepare_read(uint8_t address) { + gpio_set_dir(PIN_RTC_IO, GPIO_OUT); + gpio_put(PIN_RTC_RESET, HIGH); + uint8_t command = 0b10000001 | address; + _write_byte(command); + gpio_set_dir(PIN_RTC_IO, GPIO_IN); +} + +void _prepare_write(uint8_t address) { + gpio_set_dir(PIN_RTC_IO, GPIO_OUT); + gpio_put(PIN_RTC_RESET, HIGH); + uint8_t command = 0b10000000 | address; + _write_byte(command); +} + +void _end_transfer() { gpio_put(PIN_RTC_RESET, LOW); } + +uint8_t _read_byte() { + uint8_t byte = 0; + for (uint8_t b = 0; b < 8; b++) { + if (gpio_get(PIN_RTC_IO) == HIGH) + byte |= 0x01 << b; + _make_clock_cycle(); + } + return byte; +} + +void _write_byte(uint8_t value) { + for (uint8_t b = 0; b < 8; b++) { + gpio_put(PIN_RTC_IO, (value & 0x01) ? HIGH : LOW); + _make_clock_cycle(); + value >>= 1; + } +} + +void _make_clock_cycle() { + gpio_put(PIN_RTC_CLOCK, HIGH); + sleep_us(1); + gpio_put(PIN_RTC_CLOCK, LOW); + sleep_us(1); +} + +void ds1302_get_ram_byte(uint8_t addr, uint8_t *data) { + _prepare_read(addr); + *data = _read_byte(); + _end_transfer(); +} + +void ds1302_set_ram_byte(uint8_t addr, uint8_t *data) { + _prepare_write(addr); + _write_byte(*data); + _end_transfer(); +} + +void ds1302_set_write_protection(int enable) { + _prepare_write(REG_WP); + _write_byte(enable); + _end_transfer(); +} + +void ds1302_write_ram_bulk(const uint8_t *data, int len) { + if (len <= 0 || len > 31) { + return; + } + _prepare_write(REG_RAM_BURST); + for (int i = 0; i < len; i++) { + _write_byte(data[i]); + } + _end_transfer(); +} + +void ds1302_read_ram_bulk(uint8_t *data, int len) { + if (len <= 0 || len > 31) { + return; + } + _prepare_read(REG_RAM_BURST); + for (int i = 0; i < len; i++) { + data[i] = _read_byte(); + } + _end_transfer(); +} diff --git a/ds1302.h b/ds1302.h new file mode 100644 index 0000000..c4c5b72 --- /dev/null +++ b/ds1302.h @@ -0,0 +1,84 @@ +/** + * Ds1302.h + * + * @version 1.0.3 + * @author Rafa Couto + * @license GNU Affero General Public License v3.0 + * @see https://github.com/Treboada/Ds1302 + * + * adapted for Pico by Ken Webb, 27 Oct 2022 + * + * 31 Oct 2022 + * I am using additional ideas and code on ds1302 Ram from : + * https://github.com/msparks/arduino-ds1302 Matt Sparks no license? + * "Arduino library for the DS1302 Real Time Clock chip" + * includes "Setting and accessing the 31 bytes of static RAM. Single-byte and multi-byte (burst) modes are supported." + * + */ + +#ifndef _DS_1302_H +#define _DS_1302_H + +#include "pico/stdlib.h" +#include "pins.h" +#include +#include +#include + +// the following defines are from Arduino.h +// these are needed in Ds1302.cpp +#define HIGH 0x1 +#define LOW 0x0 + +typedef struct { + uint8_t year; + uint8_t month; + uint8_t day; + uint8_t hour; + uint8_t minute; + uint8_t second; + uint8_t dow; +} DateTime; + +/** + * Months of year + */ +enum MONTH { + MONTH_JAN = 1, + MONTH_FEB = 2, + MONTH_MAR = 3, + MONTH_APR = 4, + MONTH_MAY = 5, + MONTH_JUN = 6, + MONTH_JUL = 7, + MONTH_AUG = 8, + MONTH_SET = 9, + MONTH_OCT = 10, + MONTH_NOV = 11, + MONTH_DEC = 12 +}; + +/** + * Days of week + */ +enum DOW { DOW_MON = 1, DOW_TUE = 2, DOW_WED = 3, DOW_THU = 4, DOW_FRI = 5, DOW_SAT = 6, DOW_SUN = 7 }; + +void ds1302_init(); +int ds1302_is_clock_halted(); +void ds1302_halt(); +void ds1302_get_datetime(DateTime *dt); +void ds1302_set_datetime(DateTime *dt); + +void ds1302_get_ram_byte(uint8_t addr, uint8_t *data); +void ds1302_set_ram_byte(uint8_t addr, uint8_t *data); + +void ds1302_set_write_protection(int enable); + +void ds1302_write_ram_bulk(const uint8_t *data, int len); +void ds1302_read_ram_bulk(uint8_t *data, int len); + +inline uint8_t ds1302_encode_bcd(uint8_t dec) { return ((dec / 10 * 16) + (dec % 10)); } + +inline uint8_t ds1302_decode_bcd(uint8_t bcd) { return ((bcd / 16 * 10) + (bcd % 16)); } + +#endif // _DS_1302_H diff --git a/font_ubuntu.h b/font_ubuntu.h new file mode 100644 index 0000000..2dd871c --- /dev/null +++ b/font_ubuntu.h @@ -0,0 +1,521 @@ +// Header File for SSD1306 characters +// Generated with TTF2BMH, edited manually +// Font Ubuntu Regular +// Font Size: 24 +const char GLYPH_MISSING[] = {254, 2, 194, 226, 98, 98, 226, 194, 130, 2, 254, 0, 255, 0, 0, 0, 192, 248, + 62, 15, 3, 0, 255, 0, 127, 64, 64, 64, 76, 76, 64, 64, 64, 64, 127, 0}; +const char GLYPH_EXCLAMATION_MARK[] = {254, 254, 63, 63, 7, 7}; +const char GLYPH_QUOTATION_MARK[] = {63, 63, 0, 0, 0, 63, 63, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; +const char GLYPH_NUMBER_SIGN[] = {192, 192, 192, 240, 254, 206, 192, 192, 192, 192, 192, 240, 254, 206, 192, + 48, 48, 252, 255, 51, 48, 48, 48, 48, 48, 252, 255, 51, 48, 48, + 0, 7, 7, 0, 0, 0, 0, 0, 0, 7, 7, 0, 0, 0, 0}; +const char GLYPH_DOLLAR_SIGN[] = {240, 248, 152, 12, 12, 15, 15, 12, 12, 24, 0, 0, 1, 3, 3, 7, 6, + 12, 12, 24, 240, 224, 3, 7, 6, 6, 6, 62, 62, 6, 7, 3, 1}; +const char GLYPH_PERCENT_SIGN[] = {248, 252, 14, 6, 6, 14, 252, 248, 0, 0, 128, 224, 120, 60, 14, 2, 0, + 1, 3, 7, 6, 6, 7, 195, 225, 120, 30, 7, 1, 248, 252, 14, 6, 6, + 0, 0, 0, 0, 4, 7, 3, 1, 0, 0, 0, 0, 1, 3, 7, 6, 6}; +const char GLYPH_AMPERSAND[] = {0, 0, 120, 252, 142, 6, 6, 134, 206, 252, 120, 0, 0, 0, 0, + 240, 248, 12, 7, 7, 7, 15, 29, 57, 112, 192, 192, 112, 60, 12, + 0, 3, 3, 7, 6, 6, 6, 6, 6, 3, 3, 1, 3, 6, 4}; +const char GLYPH_APOSTROPHE[] = {63, 63, 0, 0, 0, 0}; +const char GLYPH_LEFT_PARENTHESIS[] = {128, 224, 120, 14, 3, 1, 255, 255, 0, 0, 0, 0, 0, 3, 15, 56, 96, 192}; +const char GLYPH_RIGHT_PARENTHESIS[] = {1, 3, 14, 120, 224, 128, 0, 0, 0, 0, 255, 255, 192, 96, 56, 15, 3, 0}; +const char GLYPH_ASTERISK[] = {16, 56, 48, 176, 238, 126, 238, 176, 48, 56, 16, 0, 0, 2, 7, 3, 0, + 1, 3, 7, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; +const char GLYPH_PLUS_SIGN[] = {0, 0, 0, 0, 0, 224, 224, 0, 0, 0, 0, 0, 12, 12, 12, 12, 12, 255, + 255, 12, 12, 12, 12, 12, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0}; +const char GLYPH_COMMA[] = {0, 0, 0, 0, 0, 0, 96, 127, 15}; +const char GLYPH_HYPHEN_MINUS[] = {0, 0, 0, 0, 0, 0, 12, 12, 12, 12, 12, 12, 0, 0, 0, 0, 0, 0}; +const char GLYPH_FULL_STOP[] = {0, 0, 0, 0, 0, 0, 7, 7, 7}; +const char GLYPH_SOLIDUS[] = {0, 0, 0, 0, 0, 0, 224, 248, 31, 7, 0, 0, 128, 224, 124, + 31, 3, 0, 0, 0, 240, 124, 15, 3, 0, 0, 0, 0, 0, 0}; +const char GLYPH_DIGIT_ZERO[] = {192, 240, 60, 12, 6, 6, 6, 6, 6, 12, 60, 240, 192, 63, 255, 192, 0, 0, 0, 0, + 0, 0, 0, 192, 255, 63, 0, 0, 3, 3, 6, 6, 6, 6, 7, 3, 1, 0, 0}; +const char GLYPH_DIGIT_ONE[] = {32, 112, 48, 56, 28, 254, 254, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 7, 7}; +const char GLYPH_DIGIT_TWO[] = {8, 12, 14, 6, 6, 6, 6, 142, 252, 248, 0, 0, 192, 224, 112, 56, 12, + 6, 3, 1, 0, 0, 7, 7, 6, 6, 6, 6, 6, 6, 6, 6, 6}; +const char GLYPH_DIGIT_THREE[] = {4, 12, 6, 6, 6, 6, 6, 134, 140, 252, 120, 0, 0, 0, 0, 3, 3, 3, + 3, 7, 7, 140, 252, 240, 3, 7, 6, 6, 6, 6, 6, 6, 3, 3, 1, 0}; +const char GLYPH_DIGIT_FOUR[] = {0, 0, 0, 128, 192, 224, 56, 60, 30, 254, 254, 0, 0, 56, 60, 55, 51, 49, 48, 48, + 48, 48, 255, 255, 48, 48, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 7, 0, 0}; +const char GLYPH_DIGIT_FIVE[] = {0, 0, 254, 254, 6, 6, 6, 6, 6, 6, 0, 0, 3, 3, 3, 3, 3, + 7, 6, 14, 252, 248, 3, 7, 6, 6, 6, 6, 6, 7, 3, 1, 0}; +const char GLYPH_DIGIT_SIX[] = {0, 192, 240, 56, 24, 12, 12, 6, 6, 6, 6, 0, 127, 255, 134, 3, 3, 3, + 3, 3, 7, 142, 252, 248, 0, 1, 3, 7, 6, 6, 6, 6, 7, 3, 1, 0}; +const char GLYPH_DIGIT_SEVEN[] = {6, 6, 6, 6, 6, 6, 134, 230, 118, 30, 14, 0, 0, 0, 0, 240, 126, + 15, 1, 0, 0, 0, 0, 0, 0, 7, 7, 0, 0, 0, 0, 0, 0}; +const char GLYPH_DIGIT_EIGHT[] = {112, 248, 140, 142, 6, 6, 6, 6, 6, 14, 140, 252, 112, 240, 249, 141, 7, 7, 3, 2, + 6, 7, 15, 157, 252, 240, 0, 1, 3, 7, 6, 6, 6, 6, 6, 7, 3, 1, 0}; +const char GLYPH_DIGIT_NINE[] = {240, 248, 28, 14, 6, 6, 6, 6, 12, 28, 248, 224, 1, 7, 7, 14, 12, 12, + 12, 140, 204, 246, 127, 15, 0, 6, 6, 6, 6, 3, 3, 3, 1, 0, 0, 0}; +const char GLYPH_COLON[] = {192, 192, 192, 1, 1, 1, 7, 7, 7}; +const char GLYPH_SEMICOLON[] = {192, 192, 192, 1, 1, 1, 96, 127, 15}; +const char GLYPH_LESS_THAN_SIGN[] = {0, 0, 0, 0, 0, 0, 0, 0, 128, 128, 128, 192, 128, + 28, 28, 28, 54, 54, 119, 99, 99, 193, 193, 193, 128, 128, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0}; +const char GLYPH_EQUALS_SIGN[] = {128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 49, 49, 49, 49, 49, 49, + 49, 49, 49, 49, 49, 49, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; +const char GLYPH_GREATER_THAN_SIGN[] = {128, 192, 128, 128, 128, 0, 0, 0, 0, 0, 0, 0, 0, + 128, 128, 193, 193, 193, 99, 99, 119, 54, 54, 28, 28, 28, + 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; +const char GLYPH_QUESTION_MARK[] = {4, 6, 6, 6, 6, 6, 142, 252, 120, 0, 0, 0, 56, 62, + 7, 3, 1, 0, 0, 0, 0, 7, 7, 0, 0, 0, 0}; +const char GLYPH_COMMERCIAL_AT[] = {0, 192, 240, 120, 24, 12, 12, 142, 198, 198, 198, 198, 198, 198, 204, 12, + 255, 255, 129, 0, 0, 126, 255, 195, 129, 0, 0, 0, 0, 255, 255, 0, + 0, 3, 15, 30, 24, 56, 48, 113, 99, 99, 99, 99, 99, 97, 99, 3}; +const char GLYPH_LATIN_CAPITAL_LETTER_A[] = {0, 0, 0, 0, 0, 192, 240, 60, 14, 60, 240, 192, 0, 0, 0, 0, 0, + 0, 0, 224, 248, 62, 55, 49, 48, 48, 48, 49, 55, 62, 248, 224, 0, 0, + 4, 7, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 7, 4}; +const char GLYPH_LATIN_CAPITAL_LETTER_B[] = {254, 254, 6, 6, 6, 6, 6, 6, 6, 12, 156, 248, 240, 0, + 255, 255, 6, 6, 6, 6, 6, 6, 6, 7, 15, 141, 248, 240, + 7, 7, 6, 6, 6, 6, 6, 6, 6, 6, 3, 3, 1, 0}; +const char GLYPH_LATIN_CAPITAL_LETTER_C[] = {128, 240, 120, 28, 12, 14, 6, 6, 6, 6, 6, 12, 12, + 63, 255, 224, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 1, 3, 3, 6, 6, 6, 6, 6, 6, 7, 3}; +const char GLYPH_LATIN_CAPITAL_LETTER_D[] = {254, 254, 6, 6, 6, 6, 6, 6, 14, 12, 12, 24, 120, 224, 128, + 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 224, 127, 31, + 7, 7, 6, 6, 6, 6, 6, 6, 6, 3, 3, 1, 0, 0, 0}; +const char GLYPH_LATIN_CAPITAL_LETTER_E[] = {254, 254, 6, 6, 6, 6, 6, 6, 6, 6, 6, 0, 255, 255, 6, 6, 6, 6, + 6, 6, 6, 6, 0, 0, 7, 7, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6}; +const char GLYPH_LATIN_CAPITAL_LETTER_F[] = {254, 254, 6, 6, 6, 6, 6, 6, 6, 6, 6, 255, 255, 6, 6, 6, 6, + 6, 6, 6, 6, 0, 7, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0}; +const char GLYPH_LATIN_CAPITAL_LETTER_G[] = {128, 240, 120, 28, 12, 6, 6, 6, 6, 6, 6, 12, 4, + 63, 255, 224, 128, 0, 0, 0, 0, 0, 0, 0, 252, 252, + 0, 0, 1, 3, 3, 6, 6, 6, 6, 6, 6, 7, 3}; +const char GLYPH_LATIN_CAPITAL_LETTER_H[] = {254, 254, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 254, 254, + 255, 255, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 255, 255, + 7, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 7}; +const char GLYPH_LATIN_CAPITAL_LETTER_I[] = {254, 254, 255, 255, 7, 7}; +const char GLYPH_LATIN_CAPITAL_LETTER_J[] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 254, 254, 0, 0, 0, 0, 0, 0, + 0, 0, 128, 255, 255, 2, 3, 6, 6, 6, 6, 6, 7, 3, 1, 0}; +const char GLYPH_LATIN_CAPITAL_LETTER_K[] = {254, 254, 0, 0, 128, 192, 96, 48, 24, 12, 6, 2, 0, 0, + 255, 255, 6, 15, 29, 24, 56, 112, 224, 192, 128, 0, 0, 0, + 7, 7, 0, 0, 0, 0, 0, 0, 0, 1, 3, 7, 6, 4}; +const char GLYPH_LATIN_CAPITAL_LETTER_L[] = {254, 254, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 7, 7, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6}; +const char GLYPH_LATIN_CAPITAL_LETTER_M[] = {0, 254, 126, 60, 240, 192, 0, 0, 0, 0, 0, 0, 192, 240, 60, 126, + 255, 255, 0, 0, 0, 3, 15, 124, 240, 240, 124, 15, 3, 0, 0, 0, + 7, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; +const char GLYPH_LATIN_CAPITAL_LETTER_N[] = {254, 252, 28, 56, 112, 192, 128, 0, 0, 0, 0, 0, 0, 254, 254, + 255, 255, 0, 0, 0, 0, 1, 3, 14, 28, 56, 112, 192, 255, 255, + 7, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 7}; +const char GLYPH_LATIN_CAPITAL_LETTER_O[] = {192, 240, 120, 28, 12, 14, 6, 6, 6, 6, 14, 12, 28, 120, 240, 192, + 63, 255, 224, 128, 0, 0, 0, 0, 0, 0, 0, 0, 128, 224, 255, 31, + 0, 0, 1, 3, 3, 6, 6, 6, 6, 6, 6, 3, 3, 1, 0, 0}; +const char GLYPH_LATIN_CAPITAL_LETTER_P[] = {254, 254, 6, 6, 6, 6, 6, 6, 6, 12, 28, 248, 240, + 255, 255, 12, 12, 12, 12, 12, 12, 12, 6, 7, 3, 1, + 7, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; +const char GLYPH_LATIN_CAPITAL_LETTER_Q[] = {192, 240, 120, 28, 12, 14, 6, 6, 6, 6, 14, 12, 28, 120, 240, 192, + 63, 255, 224, 128, 0, 0, 0, 0, 0, 0, 0, 0, 128, 224, 127, 31, + 0, 0, 1, 3, 3, 6, 6, 30, 62, 118, 98, 99, 193, 193, 0, 0}; +const char GLYPH_LATIN_CAPITAL_LETTER_R[] = {254, 254, 6, 6, 6, 6, 6, 6, 6, 12, 28, 248, 240, 0, + 255, 255, 12, 12, 12, 12, 12, 12, 60, 118, 231, 195, 1, 0, + 7, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 7, 6}; +const char GLYPH_LATIN_CAPITAL_LETTER_S[] = {248, 252, 140, 6, 6, 6, 6, 6, 6, 14, 4, 0, 0, 1, 1, 3, 3, 6, + 6, 12, 12, 24, 248, 224, 3, 3, 6, 6, 6, 6, 6, 6, 6, 3, 3, 1}; +const char GLYPH_LATIN_CAPITAL_LETTER_T[] = {6, 6, 6, 6, 6, 6, 254, 254, 6, 6, 6, 6, 6, 6, 0, 0, 0, 0, 0, 0, 255, + 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 7, 0, 0, 0, 0, 0, 0}; +const char GLYPH_LATIN_CAPITAL_LETTER_U[] = {254, 254, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 254, 254, + 127, 255, 128, 0, 0, 0, 0, 0, 0, 0, 0, 128, 255, 127, + 0, 1, 3, 3, 6, 6, 6, 6, 6, 6, 3, 3, 1, 0}; +const char GLYPH_LATIN_CAPITAL_LETTER_V[] = {6, 30, 252, 224, 0, 0, 0, 0, 0, 0, 0, 0, 224, 252, 30, 2, + 0, 0, 0, 7, 31, 252, 224, 128, 128, 224, 252, 31, 7, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 3, 7, 7, 3, 0, 0, 0, 0, 0, 0}; +const char GLYPH_LATIN_CAPITAL_LETTER_W[] = { + 14, 254, 248, 0, 0, 0, 0, 0, 128, 248, 120, 224, 0, 0, 0, 0, 0, 0, 1, 31, 255, 240, 0, 224, 252, 31, + 3, 0, 3, 31, 252, 224, 0, 240, 0, 0, 0, 1, 7, 7, 7, 0, 0, 0, 0, 0, 0, 0, 3, 7, 7}; +const char GLYPH_LATIN_CAPITAL_LETTER_X[] = {2, 6, 28, 56, 224, 192, 0, 0, 192, 224, 56, 28, 6, 2, + 0, 0, 128, 224, 112, 57, 15, 15, 57, 112, 224, 128, 0, 0, + 4, 7, 3, 1, 0, 0, 0, 0, 0, 0, 1, 3, 7, 4}; +const char GLYPH_LATIN_CAPITAL_LETTER_Y[] = {2, 14, 60, 240, 192, 0, 0, 0, 0, 192, 240, 60, 14, 2, + 0, 0, 0, 0, 1, 7, 254, 254, 7, 1, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 7, 7, 0, 0, 0, 0, 0, 0}; +const char GLYPH_LATIN_CAPITAL_LETTER_Z[] = {6, 6, 6, 6, 6, 6, 6, 134, 198, 102, 54, 30, 14, + 0, 128, 224, 112, 24, 12, 7, 1, 0, 0, 0, 0, 0, + 7, 7, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6}; +const char GLYPH_LEFT_SQUARE_BRACKET[] = {255, 255, 1, 1, 1, 1, 255, 255, 0, 0, 0, 0, 255, 255, 192, 192, 192, 192}; +const char GLYPH_REVERSE_SOLIDUS[] = {7, 31, 248, 224, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 31, 124, + 224, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 15, 124, 240, 128}; +const char GLYPH_RIGHT_SQUARE_BRACKET[] = {1, 1, 1, 1, 255, 255, 0, 0, 0, 0, 255, 255, 192, 192, 192, 192, 255, 255}; +const char GLYPH_CIRCUMFLEX_ACCENT[] = {0, 128, 224, 240, 124, 62, 14, 62, 124, 240, 224, 128, 0, 2, 7, 3, 1, 0, 0, 0, + 0, 0, 1, 3, 7, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; +const char GLYPH_LOW_LINE[] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192}; +const char GLYPH_GRAVE_ACCENT[] = {1, 3, 7, 14, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; +const char GLYPH_LATIN_SMALL_LETTER_A[] = {0, 96, 96, 96, 96, 96, 96, 96, 192, 192, 128, 224, 240, 48, 24, 24, 24, + 24, 24, 24, 255, 255, 1, 3, 7, 6, 6, 6, 6, 6, 6, 7, 7}; +const char GLYPH_LATIN_SMALL_LETTER_B[] = {255, 255, 192, 96, 96, 96, 96, 96, 192, 192, 128, 0, + 255, 255, 0, 0, 0, 0, 0, 0, 0, 193, 255, 126, + 3, 7, 6, 6, 6, 6, 6, 6, 3, 3, 1, 0}; +const char GLYPH_LATIN_SMALL_LETTER_C[] = {0, 128, 192, 192, 96, 96, 96, 96, 96, 96, 126, 255, 129, 0, 0, + 0, 0, 0, 0, 0, 0, 1, 3, 3, 6, 6, 6, 6, 6, 7}; +const char GLYPH_LATIN_SMALL_LETTER_D[] = {0, 128, 192, 192, 96, 96, 96, 96, 96, 192, 255, 255, + 126, 255, 193, 0, 0, 0, 0, 0, 0, 0, 255, 255, + 0, 1, 3, 3, 7, 6, 6, 6, 6, 6, 7, 3}; +const char GLYPH_LATIN_SMALL_LETTER_E[] = {0, 128, 192, 192, 96, 96, 96, 96, 96, 224, 192, 128, 0, + 126, 255, 153, 24, 24, 24, 24, 24, 24, 24, 25, 31, 30, + 0, 1, 3, 3, 6, 6, 6, 6, 6, 6, 6, 3, 0}; +const char GLYPH_LATIN_SMALL_LETTER_F[] = {252, 255, 99, 97, 97, 97, 97, 1, 255, 255, 0, 0, + 0, 0, 0, 0, 7, 7, 0, 0, 0, 0, 0, 0}; +const char GLYPH_LATIN_SMALL_LETTER_G[] = {0, 128, 192, 192, 96, 96, 96, 96, 96, 96, 224, 192, + 62, 255, 193, 128, 0, 0, 0, 0, 0, 128, 255, 255, + 0, 96, 193, 193, 195, 195, 195, 195, 227, 113, 63, 31}; +const char GLYPH_LATIN_SMALL_LETTER_H[] = {255, 255, 96, 96, 96, 96, 96, 224, 192, 192, 0, 255, 255, 0, 0, 0, 0, + 0, 0, 1, 255, 255, 7, 7, 0, 0, 0, 0, 0, 0, 0, 7, 7}; +const char GLYPH_LATIN_SMALL_LETTER_I[] = {227, 227, 255, 255, 7, 7}; +const char GLYPH_LATIN_SMALL_LETTER_J[] = {0, 0, 227, 227, 0, 0, 255, 255, 192, 224, 127, 63}; +const char GLYPH_LATIN_SMALL_LETTER_K[] = {255, 255, 0, 0, 0, 0, 128, 192, 96, 32, 32, 0, 255, 255, 24, 60, 54, 115, + 225, 192, 128, 0, 0, 0, 7, 7, 0, 0, 0, 0, 0, 1, 3, 7, 6, 4}; +const char GLYPH_LATIN_SMALL_LETTER_L[] = {255, 255, 0, 0, 255, 255, 0, 0, 3, 7, 6, 6}; +const char GLYPH_LATIN_SMALL_LETTER_M[] = {192, 224, 96, 96, 96, 96, 96, 224, 192, 192, 64, 96, 96, 96, 96, 224, + 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 0, + 7, 7, 0, 0, 0, 0, 0, 0, 7, 7, 0, 0, 0, 0, 0, 0}; +const char GLYPH_LATIN_SMALL_LETTER_N[] = {192, 224, 96, 96, 96, 96, 96, 224, 192, 192, 0, 255, 255, 0, 0, 0, 0, + 0, 0, 1, 255, 255, 7, 7, 0, 0, 0, 0, 0, 0, 0, 7, 7}; +const char GLYPH_LATIN_SMALL_LETTER_O[] = {0, 128, 192, 192, 96, 96, 96, 96, 96, 192, 192, 128, 0, + 126, 255, 129, 0, 0, 0, 0, 0, 0, 0, 131, 255, 126, + 0, 1, 3, 3, 6, 6, 6, 6, 6, 3, 3, 1, 0}; +const char GLYPH_LATIN_SMALL_LETTER_P[] = {192, 224, 96, 96, 96, 96, 96, 224, 192, 192, 128, 0, + 255, 255, 0, 0, 0, 0, 0, 0, 0, 131, 255, 126, + 255, 255, 3, 6, 6, 6, 6, 6, 3, 3, 1, 0}; +const char GLYPH_LATIN_SMALL_LETTER_Q[] = {0, 128, 192, 192, 96, 96, 96, 96, 96, 96, 224, 192, + 126, 255, 129, 0, 0, 0, 0, 0, 0, 0, 255, 255, + 0, 1, 3, 3, 6, 6, 6, 6, 6, 3, 255, 255}; +const char GLYPH_LATIN_SMALL_LETTER_R[] = {192, 224, 96, 96, 96, 96, 96, 96, 255, 255, 0, 0, + 0, 0, 0, 0, 7, 7, 0, 0, 0, 0, 0, 0}; +const char GLYPH_LATIN_SMALL_LETTER_S[] = {128, 192, 224, 96, 96, 96, 96, 96, 224, 0, 7, 15, 12, 28, 24, + 24, 56, 48, 240, 224, 3, 6, 6, 6, 6, 6, 6, 7, 3, 1}; +const char GLYPH_LATIN_SMALL_LETTER_T[] = {254, 254, 96, 96, 96, 96, 96, 0, 255, 255, 0, 0, + 0, 0, 0, 0, 1, 3, 7, 6, 6, 6, 6, 3}; +const char GLYPH_LATIN_SMALL_LETTER_U[] = {224, 224, 0, 0, 0, 0, 0, 0, 0, 224, 224, 255, 255, 128, 0, 0, 0, + 0, 0, 0, 255, 255, 0, 3, 3, 7, 6, 6, 6, 6, 6, 7, 3}; +const char GLYPH_LATIN_SMALL_LETTER_V[] = {96, 224, 192, 0, 0, 0, 0, 0, 0, 192, 224, 96, 0, 3, 31, 252, 240, 128, + 128, 240, 252, 31, 3, 0, 0, 0, 0, 0, 3, 7, 7, 3, 0, 0, 0, 0}; +const char GLYPH_LATIN_SMALL_LETTER_W[] = { + 96, 224, 128, 0, 0, 0, 0, 0, 224, 224, 128, 0, 0, 0, 0, 0, 128, 224, 0, 7, 63, 252, 224, 0, 224, 254, 31, + 1, 31, 252, 224, 0, 224, 252, 63, 7, 0, 0, 0, 1, 7, 7, 7, 1, 0, 0, 0, 0, 7, 7, 7, 1, 0, 0}; +const char GLYPH_LATIN_SMALL_LETTER_X[] = {32, 96, 224, 128, 0, 0, 0, 0, 0, 128, 224, 96, 32, + 0, 0, 128, 195, 231, 126, 24, 126, 231, 195, 128, 0, 0, + 4, 6, 7, 1, 0, 0, 0, 0, 0, 1, 7, 6, 4}; +const char GLYPH_LATIN_SMALL_LETTER_Y[] = {32, 224, 192, 0, 0, 0, 0, 0, 0, 192, 224, 32, + 0, 1, 15, 62, 248, 192, 0, 224, 252, 31, 3, 0, + 192, 192, 192, 192, 97, 127, 31, 7, 0, 0, 0, 0}; +const char GLYPH_LATIN_SMALL_LETTER_Z[] = {96, 96, 96, 96, 96, 96, 96, 224, 224, 224, 0, 128, 224, 112, 56, + 12, 7, 3, 1, 0, 7, 7, 7, 6, 6, 6, 6, 6, 6, 6}; +const char GLYPH_LEFT_CURLY_BRACKET[] = {0, 0, 254, 255, 3, 1, 1, 24, 60, 247, 227, + 0, 0, 0, 0, 0, 63, 127, 224, 192, 192}; +const char GLYPH_VERTICAL_LINE[] = {255, 255, 255, 255, 255, 255}; +const char GLYPH_RIGHT_CURLY_BRACKET[] = {1, 1, 3, 255, 254, 0, 0, 0, 0, 0, 227, + 247, 60, 24, 192, 192, 224, 127, 63, 0, 0}; +const char GLYPH_NUMERO_SIGN[] = {254, 254, 28, 56, 224, 192, 0, 0, 0, 0, 0, 0, 254, 254, 255, 255, 0, 0, 0, 1, 7, + 14, 56, 112, 192, 128, 255, 255, 7, 7, 0, 0, 0, 0, 0, 0, 0, 0, 1, 3, 7, 7}; +const char GLYPH_CYRILLIC_CAPITAL_LETTER_BE[] = {254, 254, 6, 6, 6, 6, 6, 6, 6, 6, 0, 0, 255, 255, 3, 3, 3, 3, + 3, 3, 6, 142, 252, 248, 7, 7, 6, 6, 6, 6, 6, 6, 3, 3, 1, 0}; +const char GLYPH_CYRILLIC_CAPITAL_LETTER_GHE[] = {254, 254, 6, 6, 6, 6, 6, 6, 6, 6, 6, 255, 255, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 7, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0}; +const char GLYPH_CYRILLIC_CAPITAL_LETTER_DE[] = { + 0, 0, 0, 0, 0, 0, 254, 254, 6, 6, 6, 6, 6, 6, 254, 254, 0, 0, 0, 0, 0, 0, 192, 252, 63, 3, 0, + 0, 0, 0, 0, 0, 255, 255, 0, 0, 254, 254, 6, 7, 7, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 254, 254}; +const char GLYPH_CYRILLIC_CAPITAL_LETTER_IO[] = {254, 254, 6, 6, 6, 6, 6, 6, 6, 6, 6, 0, 255, 255, 6, 6, 6, 6, + 6, 6, 6, 6, 0, 0, 7, 7, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6}; +const char GLYPH_CYRILLIC_CAPITAL_LETTER_ZHE[] = {2, 6, 12, 24, 48, 96, 192, 128, 0, 0, 254, 254, 0, 0, 128, 192, 96, + 0, 0, 128, 192, 96, 56, 28, 7, 3, 2, 255, 255, 2, 3, 7, 28, 56, + 4, 7, 3, 1, 0, 0, 0, 0, 0, 0, 7, 7, 0, 0, 0, 0, 0}; +const char GLYPH_CYRILLIC_CAPITAL_LETTER_ZE[] = {0, 4, 14, 6, 6, 6, 6, 6, 14, 140, 252, 240, 0, + 0, 0, 0, 6, 6, 6, 6, 6, 7, 15, 29, 248, 240, + 3, 3, 7, 6, 6, 6, 6, 6, 6, 3, 3, 3, 0}; +const char GLYPH_CYRILLIC_CAPITAL_LETTER_I[] = {254, 254, 0, 0, 0, 0, 0, 0, 128, 192, 224, 112, 24, 252, 254, + 255, 255, 192, 96, 56, 28, 14, 7, 3, 1, 0, 0, 0, 255, 255, + 7, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 7}; +const char GLYPH_CYRILLIC_CAPITAL_LETTER_SHORT_I[] = { + 254, 254, 0, 0, 0, 0, 0, 0, 128, 192, 224, 112, 24, 252, 254, 255, 255, 192, 96, 56, 28, 14, 7, + 3, 1, 0, 0, 0, 255, 255, 7, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 7}; +const char GLYPH_CYRILLIC_CAPITAL_LETTER_KA[] = {254, 254, 0, 0, 128, 192, 96, 48, 24, 12, 6, 2, 0, 0, + 255, 255, 6, 15, 29, 24, 56, 112, 224, 192, 128, 0, 0, 0, + 7, 7, 0, 0, 0, 0, 0, 0, 0, 1, 3, 7, 6, 4}; +const char GLYPH_CYRILLIC_CAPITAL_LETTER_EL[] = {0, 0, 0, 0, 0, 0, 254, 254, 6, 6, 6, 6, 6, 6, 254, 254, + 0, 0, 0, 128, 192, 252, 63, 3, 0, 0, 0, 0, 0, 0, 255, 255, + 6, 6, 7, 3, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 7}; +const char GLYPH_CYRILLIC_CAPITAL_LETTER_PE[] = {254, 254, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 254, 254, + 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, + 7, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 7}; +const char GLYPH_CYRILLIC_CAPITAL_LETTER_U[] = {2, 14, 60, 240, 192, 0, 0, 0, 0, 0, 224, 252, 30, 2, + 0, 0, 0, 0, 3, 15, 188, 240, 124, 31, 3, 0, 0, 0, + 0, 6, 6, 6, 6, 7, 3, 1, 0, 0, 0, 0, 0, 0}; +const char GLYPH_CYRILLIC_CAPITAL_LETTER_EF[] = {128, 224, 112, 56, 24, 24, 12, 12, 12, 255, 255, 12, 12, + 12, 24, 24, 56, 15, 63, 112, 224, 192, 192, 128, 128, 128, + 255, 255, 128, 128, 128, 192, 192, 224, 0, 0, 0, 0, 0, + 0, 1, 1, 1, 7, 7, 1, 1, 1, 0, 0, 0}; +const char GLYPH_CYRILLIC_CAPITAL_LETTER_TSE[] = {254, 254, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 254, 254, 0, 0, + 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, + 7, 7, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 254, 254}; +const char GLYPH_CYRILLIC_CAPITAL_LETTER_CHE[] = {254, 254, 0, 0, 0, 0, 0, 0, 0, 0, 254, 254, + 3, 15, 14, 28, 24, 24, 24, 24, 24, 24, 255, 255, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 7}; +const char GLYPH_CYRILLIC_CAPITAL_LETTER_SHA[] = {254, 254, 0, 0, 0, 0, 0, 0, 0, 254, 254, 0, 0, 0, 0, 0, + 255, 255, 0, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, + 7, 7, 6, 6, 6, 6, 6, 6, 6, 7, 7, 6, 6, 6, 6, 6}; +const char GLYPH_CYRILLIC_CAPITAL_LETTER_SHCHA[] = {254, 254, 0, 0, 0, 0, 0, 0, 0, 0, 254, 254, 0, 0, 0, 0, + 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, + 7, 7, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 6, 6, 6, 6}; +const char GLYPH_CYRILLIC_CAPITAL_LETTER_HARD_SIGN[] = {6, 6, 6, 6, 6, 254, 254, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 255, 255, 3, 3, 3, 3, 3, 3, 6, 142, 252, 248, + 0, 0, 0, 0, 0, 7, 7, 6, 6, 6, 6, 6, 6, 3, 3, 1, 0}; +const char GLYPH_CYRILLIC_CAPITAL_LETTER_YERU[] = {254, 254, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 254, + 255, 255, 3, 3, 3, 3, 3, 3, 6, 142, 252, 248, 0, 0, 0, 255, + 7, 7, 6, 6, 6, 6, 6, 6, 3, 3, 1, 0, 0, 0, 0, 7}; +const char GLYPH_CYRILLIC_CAPITAL_LETTER_SOFT_SIGN[] = {254, 254, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 255, 255, 3, 3, 3, 3, 3, 3, 6, 142, 252, 248, + 7, 7, 6, 6, 6, 6, 6, 6, 3, 3, 1, 0}; +const char GLYPH_CYRILLIC_CAPITAL_LETTER_E[] = {12, 12, 6, 6, 6, 6, 6, 6, 12, 28, 120, 240, 192, + 0, 0, 0, 6, 6, 6, 6, 6, 6, 134, 230, 255, 63, + 3, 7, 6, 6, 6, 6, 6, 6, 3, 3, 1, 0, 0}; +const char GLYPH_CYRILLIC_CAPITAL_LETTER_YU[] = {254, 254, 0, 0, 0, 0, 128, 224, 120, 24, 12, 14, 6, 6, 6, 6, + 255, 255, 6, 6, 6, 6, 63, 255, 224, 128, 0, 0, 0, 0, 0, 0, + 7, 7, 0, 0, 0, 0, 0, 0, 1, 3, 3, 7, 6, 6, 6, 6}; +const char GLYPH_CYRILLIC_CAPITAL_LETTER_YA[] = {0, 240, 248, 28, 12, 6, 6, 6, 6, 6, 6, 6, 254, 254, + 0, 1, 131, 231, 126, 28, 12, 12, 12, 12, 12, 12, 255, 255, + 4, 7, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 7}; +const char GLYPH_CYRILLIC_SMALL_LETTER_BE[] = {192, 248, 220, 198, 102, 99, 99, 99, 97, 193, 193, 129, 0, + 127, 255, 192, 0, 0, 0, 0, 0, 0, 0, 129, 255, 126, + 0, 1, 3, 3, 6, 6, 6, 6, 6, 3, 3, 1, 0}; +const char GLYPH_CYRILLIC_SMALL_LETTER_VE[] = {224, 224, 96, 96, 96, 96, 224, 192, 128, 0, 255, 255, 24, 24, 24, + 24, 28, 63, 247, 224, 7, 7, 6, 6, 6, 6, 6, 7, 3, 1}; +const char GLYPH_CYRILLIC_SMALL_LETTER_GHE[] = {224, 224, 96, 96, 96, 96, 96, 96, 96, 255, 255, 0, 0, 0, + 0, 0, 0, 0, 7, 7, 0, 0, 0, 0, 0, 0, 0}; +const char GLYPH_CYRILLIC_SMALL_LETTER_DE[] = {0, 0, 0, 0, 0, 224, 224, 96, 96, 96, 96, 224, 224, 0, 0, + 0, 0, 0, 128, 240, 255, 15, 0, 0, 0, 0, 255, 255, 0, 0, + 126, 126, 6, 7, 7, 6, 6, 6, 6, 6, 6, 7, 7, 126, 126}; +const char GLYPH_CYRILLIC_SMALL_LETTER_IO[] = {0, 128, 192, 199, 103, 103, 96, 96, 103, 231, 199, 128, 0, + 126, 255, 153, 24, 24, 24, 24, 24, 24, 24, 25, 31, 30, + 0, 1, 3, 3, 6, 6, 6, 6, 6, 6, 6, 3, 0}; +const char GLYPH_CYRILLIC_SMALL_LETTER_ZHE[] = { + 0, 32, 96, 224, 192, 0, 0, 0, 0, 224, 224, 0, 0, 0, 0, 192, 224, 96, 0, 0, 0, 128, 193, 99, 54, 28, 8, + 255, 255, 8, 28, 54, 99, 193, 128, 0, 4, 6, 7, 1, 0, 0, 0, 0, 0, 7, 7, 0, 0, 0, 0, 0, 1, 7}; +const char GLYPH_CYRILLIC_SMALL_LETTER_ZE[] = {192, 96, 96, 96, 96, 96, 96, 96, 192, 192, 128, 0, 0, 0, 24, 24, 24, + 24, 24, 60, 255, 227, 3, 6, 6, 6, 6, 6, 6, 6, 7, 3, 1}; +const char GLYPH_CYRILLIC_SMALL_LETTER_I[] = {224, 224, 0, 0, 0, 0, 0, 0, 128, 192, 224, 224, + 255, 255, 192, 224, 112, 60, 30, 7, 3, 1, 255, 255, + 7, 7, 3, 1, 0, 0, 0, 0, 0, 0, 7, 7}; +const char GLYPH_CYRILLIC_SMALL_LETTER_SHORT_I[] = {224, 224, 1, 3, 7, 6, 6, 7, 131, 193, 224, 224, + 255, 255, 192, 224, 112, 60, 30, 7, 3, 1, 255, 255, + 7, 7, 3, 1, 0, 0, 0, 0, 0, 0, 7, 7}; +const char GLYPH_CYRILLIC_SMALL_LETTER_KA[] = {224, 224, 0, 0, 0, 0, 128, 192, 96, 32, 0, 0, + 255, 255, 24, 60, 54, 115, 225, 192, 128, 0, 0, 0, + 7, 7, 0, 0, 0, 0, 0, 1, 3, 7, 6, 4}; +const char GLYPH_CYRILLIC_SMALL_LETTER_EL[] = {0, 0, 0, 0, 224, 224, 96, 96, 96, 96, 96, 96, 224, 224, + 0, 0, 128, 224, 255, 31, 0, 0, 0, 0, 0, 0, 255, 255, + 6, 7, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 7, 7}; +const char GLYPH_CYRILLIC_SMALL_LETTER_EM[] = {192, 224, 96, 192, 0, 0, 0, 0, 0, 0, 0, 192, 96, 224, 224, + 255, 255, 0, 1, 15, 56, 224, 128, 224, 56, 15, 1, 0, 255, 255, + 7, 7, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 7, 7}; +const char GLYPH_CYRILLIC_SMALL_LETTER_EN[] = {224, 224, 0, 0, 0, 0, 0, 0, 0, 224, 224, 255, 255, 24, 24, 24, 24, + 24, 24, 24, 255, 255, 7, 7, 0, 0, 0, 0, 0, 0, 0, 7, 7}; +const char GLYPH_CYRILLIC_SMALL_LETTER_PE[] = {224, 224, 96, 96, 96, 96, 96, 96, 96, 224, 224, 255, 255, 0, 0, 0, 0, + 0, 0, 0, 255, 255, 7, 7, 0, 0, 0, 0, 0, 0, 0, 7, 7}; +const char GLYPH_CYRILLIC_SMALL_LETTER_TE[] = {96, 96, 96, 96, 224, 224, 96, 96, 96, 96, 0, 0, 0, 0, 255, + 255, 0, 0, 0, 0, 0, 0, 0, 0, 7, 7, 0, 0, 0, 0}; +const char GLYPH_CYRILLIC_SMALL_LETTER_EF[] = { + 0, 128, 128, 192, 224, 96, 96, 255, 255, 96, 96, 224, 192, 128, 128, 0, 126, 255, 195, 0, 0, 0, 0, 255, + 255, 0, 0, 0, 0, 195, 255, 126, 0, 1, 3, 3, 7, 6, 6, 255, 255, 6, 6, 7, 3, 1, 0, 0}; +const char GLYPH_CYRILLIC_SMALL_LETTER_TSE[] = {224, 224, 0, 0, 0, 0, 0, 0, 224, 224, 0, 0, + 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, + 7, 7, 6, 6, 6, 6, 6, 6, 7, 7, 126, 126}; +const char GLYPH_CYRILLIC_SMALL_LETTER_CHE[] = {224, 224, 0, 0, 0, 0, 0, 0, 224, 224, 15, 31, 56, 48, 48, + 48, 48, 48, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 7, 7}; +const char GLYPH_CYRILLIC_SMALL_LETTER_SHA[] = {224, 224, 0, 0, 0, 0, 0, 0, 224, 224, 0, 0, 0, 0, 0, 224, + 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 255, + 7, 7, 6, 6, 6, 6, 6, 6, 7, 7, 6, 6, 6, 6, 6, 7}; +const char GLYPH_CYRILLIC_SMALL_LETTER_SHCHA[] = {224, 224, 0, 0, 0, 0, 0, 0, 224, 224, 0, 0, 0, 0, 0, 224, + 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 255, + 7, 7, 6, 6, 6, 6, 6, 6, 7, 7, 6, 6, 6, 6, 6, 7}; +const char GLYPH_CYRILLIC_SMALL_LETTER_HARD_SIGN[] = {96, 96, 96, 224, 224, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 255, 255, 12, 12, 12, 12, 12, 12, 28, 24, 248, 240, + 0, 0, 0, 7, 7, 6, 6, 6, 6, 6, 6, 6, 3, 3, 1}; +const char GLYPH_CYRILLIC_SMALL_LETTER_YERU[] = {224, 224, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, + 255, 255, 12, 12, 12, 12, 12, 12, 24, 248, 240, 0, 0, 0, 255, 255, + 7, 7, 6, 6, 6, 6, 6, 6, 3, 3, 1, 0, 0, 0, 7, 7}; +const char GLYPH_CYRILLIC_SMALL_LETTER_SOFT_SIGN[] = {224, 224, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 12, 12, 12, + 12, 12, 28, 248, 240, 7, 7, 6, 6, 6, 6, 6, 7, 3, 1}; +const char GLYPH_CYRILLIC_SMALL_LETTER_E[] = {64, 96, 96, 96, 96, 96, 192, 192, 128, 0, 0, 0, 0, 24, 24, + 24, 24, 153, 255, 126, 7, 6, 6, 6, 6, 6, 7, 3, 1, 0}; +const char GLYPH_CYRILLIC_SMALL_LETTER_YU[] = {224, 224, 0, 0, 0, 0, 128, 192, 192, 96, 96, 96, 96, 96, 192, 192, + 255, 255, 24, 24, 24, 126, 255, 129, 0, 0, 0, 0, 0, 0, 0, 131, + 7, 7, 0, 0, 0, 0, 1, 3, 3, 6, 6, 6, 6, 6, 3, 3}; +const char GLYPH_CYRILLIC_SMALL_LETTER_YA[] = {0, 128, 192, 192, 96, 96, 96, 96, 96, 224, 224, + 0, 15, 159, 248, 112, 48, 48, 48, 48, 255, 255, + 4, 7, 3, 0, 0, 0, 0, 0, 0, 7, 7}; + +const char GLYPH_WIDTH[143] = { + 2, 7, 15, 11, 17, 15, 2, 6, 6, 11, 12, 3, 6, 3, 10, 13, 7, 11, 12, 13, 11, 12, 11, 13, 12, 3, 3, 13, 12, + 13, 9, 16, 17, 14, 13, 15, 12, 11, 13, 14, 2, 11, 14, 12, 16, 15, 16, 13, 16, 14, 12, 14, 14, 16, 17, 14, 14, 13, + 6, 11, 6, 13, 13, 5, 11, 12, 10, 12, 13, 8, 12, 11, 2, 4, 12, 4, 16, 11, 13, 12, 12, 8, 10, 8, 11, 12, 18, + 13, 12, 10, 7, 2, 7, 14, 12, 11, 18, 12, 17, 13, 15, 15, 14, 16, 14, 14, 17, 16, 12, 16, 16, 17, 16, 12, 13, 16, + 14, 13, 10, 9, 15, 13, 18, 11, 12, 12, 12, 14, 15, 11, 11, 10, 16, 12, 10, 16, 16, 15, 16, 10, 10, 16, 11}; + +const char *GLYPH_ADDR[143] = { + GLYPH_EXCLAMATION_MARK, + GLYPH_QUOTATION_MARK, + GLYPH_NUMBER_SIGN, + GLYPH_DOLLAR_SIGN, + GLYPH_PERCENT_SIGN, + GLYPH_AMPERSAND, + GLYPH_APOSTROPHE, + GLYPH_LEFT_PARENTHESIS, + GLYPH_RIGHT_PARENTHESIS, + GLYPH_ASTERISK, + GLYPH_PLUS_SIGN, + GLYPH_COMMA, + GLYPH_HYPHEN_MINUS, + GLYPH_FULL_STOP, + GLYPH_SOLIDUS, + GLYPH_DIGIT_ZERO, + GLYPH_DIGIT_ONE, + GLYPH_DIGIT_TWO, + GLYPH_DIGIT_THREE, + GLYPH_DIGIT_FOUR, + GLYPH_DIGIT_FIVE, + GLYPH_DIGIT_SIX, + GLYPH_DIGIT_SEVEN, + GLYPH_DIGIT_EIGHT, + GLYPH_DIGIT_NINE, + GLYPH_COLON, + GLYPH_SEMICOLON, + GLYPH_LESS_THAN_SIGN, + GLYPH_EQUALS_SIGN, + GLYPH_GREATER_THAN_SIGN, + GLYPH_QUESTION_MARK, + GLYPH_COMMERCIAL_AT, + GLYPH_LATIN_CAPITAL_LETTER_A, + GLYPH_LATIN_CAPITAL_LETTER_B, + GLYPH_LATIN_CAPITAL_LETTER_C, + GLYPH_LATIN_CAPITAL_LETTER_D, + GLYPH_LATIN_CAPITAL_LETTER_E, + GLYPH_LATIN_CAPITAL_LETTER_F, + GLYPH_LATIN_CAPITAL_LETTER_G, + GLYPH_LATIN_CAPITAL_LETTER_H, + GLYPH_LATIN_CAPITAL_LETTER_I, + GLYPH_LATIN_CAPITAL_LETTER_J, + GLYPH_LATIN_CAPITAL_LETTER_K, + GLYPH_LATIN_CAPITAL_LETTER_L, + GLYPH_LATIN_CAPITAL_LETTER_M, + GLYPH_LATIN_CAPITAL_LETTER_N, + GLYPH_LATIN_CAPITAL_LETTER_O, + GLYPH_LATIN_CAPITAL_LETTER_P, + GLYPH_LATIN_CAPITAL_LETTER_Q, + GLYPH_LATIN_CAPITAL_LETTER_R, + GLYPH_LATIN_CAPITAL_LETTER_S, + GLYPH_LATIN_CAPITAL_LETTER_T, + GLYPH_LATIN_CAPITAL_LETTER_U, + GLYPH_LATIN_CAPITAL_LETTER_V, + GLYPH_LATIN_CAPITAL_LETTER_W, + GLYPH_LATIN_CAPITAL_LETTER_X, + GLYPH_LATIN_CAPITAL_LETTER_Y, + GLYPH_LATIN_CAPITAL_LETTER_Z, + GLYPH_LEFT_SQUARE_BRACKET, + GLYPH_REVERSE_SOLIDUS, + GLYPH_RIGHT_SQUARE_BRACKET, + GLYPH_CIRCUMFLEX_ACCENT, + GLYPH_LOW_LINE, + GLYPH_GRAVE_ACCENT, + GLYPH_LATIN_SMALL_LETTER_A, + GLYPH_LATIN_SMALL_LETTER_B, + GLYPH_LATIN_SMALL_LETTER_C, + GLYPH_LATIN_SMALL_LETTER_D, + GLYPH_LATIN_SMALL_LETTER_E, + GLYPH_LATIN_SMALL_LETTER_F, + GLYPH_LATIN_SMALL_LETTER_G, + GLYPH_LATIN_SMALL_LETTER_H, + GLYPH_LATIN_SMALL_LETTER_I, + GLYPH_LATIN_SMALL_LETTER_J, + GLYPH_LATIN_SMALL_LETTER_K, + GLYPH_LATIN_SMALL_LETTER_L, + GLYPH_LATIN_SMALL_LETTER_M, + GLYPH_LATIN_SMALL_LETTER_N, + GLYPH_LATIN_SMALL_LETTER_O, + GLYPH_LATIN_SMALL_LETTER_P, + GLYPH_LATIN_SMALL_LETTER_Q, + GLYPH_LATIN_SMALL_LETTER_R, + GLYPH_LATIN_SMALL_LETTER_S, + GLYPH_LATIN_SMALL_LETTER_T, + GLYPH_LATIN_SMALL_LETTER_U, + GLYPH_LATIN_SMALL_LETTER_V, + GLYPH_LATIN_SMALL_LETTER_W, + GLYPH_LATIN_SMALL_LETTER_X, + GLYPH_LATIN_SMALL_LETTER_Y, + GLYPH_LATIN_SMALL_LETTER_Z, + GLYPH_LEFT_CURLY_BRACKET, + GLYPH_VERTICAL_LINE, + GLYPH_RIGHT_CURLY_BRACKET, + GLYPH_NUMERO_SIGN, + GLYPH_CYRILLIC_CAPITAL_LETTER_BE, + GLYPH_CYRILLIC_CAPITAL_LETTER_GHE, + GLYPH_CYRILLIC_CAPITAL_LETTER_DE, + GLYPH_CYRILLIC_CAPITAL_LETTER_IO, + GLYPH_CYRILLIC_CAPITAL_LETTER_ZHE, + GLYPH_CYRILLIC_CAPITAL_LETTER_ZE, + GLYPH_CYRILLIC_CAPITAL_LETTER_I, + GLYPH_CYRILLIC_CAPITAL_LETTER_SHORT_I, + GLYPH_CYRILLIC_CAPITAL_LETTER_KA, + GLYPH_CYRILLIC_CAPITAL_LETTER_EL, + GLYPH_CYRILLIC_CAPITAL_LETTER_PE, + GLYPH_CYRILLIC_CAPITAL_LETTER_U, + GLYPH_CYRILLIC_CAPITAL_LETTER_EF, + GLYPH_CYRILLIC_CAPITAL_LETTER_TSE, + GLYPH_CYRILLIC_CAPITAL_LETTER_CHE, + GLYPH_CYRILLIC_CAPITAL_LETTER_SHA, + GLYPH_CYRILLIC_CAPITAL_LETTER_SHCHA, + GLYPH_CYRILLIC_CAPITAL_LETTER_HARD_SIGN, + GLYPH_CYRILLIC_CAPITAL_LETTER_YERU, + GLYPH_CYRILLIC_CAPITAL_LETTER_SOFT_SIGN, + GLYPH_CYRILLIC_CAPITAL_LETTER_E, + GLYPH_CYRILLIC_CAPITAL_LETTER_YU, + GLYPH_CYRILLIC_CAPITAL_LETTER_YA, + GLYPH_CYRILLIC_SMALL_LETTER_BE, + GLYPH_CYRILLIC_SMALL_LETTER_VE, + GLYPH_CYRILLIC_SMALL_LETTER_GHE, + GLYPH_CYRILLIC_SMALL_LETTER_DE, + GLYPH_CYRILLIC_SMALL_LETTER_IO, + GLYPH_CYRILLIC_SMALL_LETTER_ZHE, + GLYPH_CYRILLIC_SMALL_LETTER_ZE, + GLYPH_CYRILLIC_SMALL_LETTER_I, + GLYPH_CYRILLIC_SMALL_LETTER_SHORT_I, + GLYPH_CYRILLIC_SMALL_LETTER_KA, + GLYPH_CYRILLIC_SMALL_LETTER_EL, + GLYPH_CYRILLIC_SMALL_LETTER_EM, + GLYPH_CYRILLIC_SMALL_LETTER_EN, + GLYPH_CYRILLIC_SMALL_LETTER_PE, + GLYPH_CYRILLIC_SMALL_LETTER_TE, + GLYPH_CYRILLIC_SMALL_LETTER_EF, + GLYPH_CYRILLIC_SMALL_LETTER_TSE, + GLYPH_CYRILLIC_SMALL_LETTER_CHE, + GLYPH_CYRILLIC_SMALL_LETTER_SHA, + GLYPH_CYRILLIC_SMALL_LETTER_SHCHA, + GLYPH_CYRILLIC_SMALL_LETTER_HARD_SIGN, + GLYPH_CYRILLIC_SMALL_LETTER_YERU, + GLYPH_CYRILLIC_SMALL_LETTER_SOFT_SIGN, + GLYPH_CYRILLIC_SMALL_LETTER_E, + GLYPH_CYRILLIC_SMALL_LETTER_YU, + GLYPH_CYRILLIC_SMALL_LETTER_YA, +}; + +inline int glyph_index_for_code_point(int codePoint) { + const int cyrillicTable[] = {32, 94, 33, 95, 96, 36, 98, 99, 100, 101, 102, 103, 44, 39, 46, 104, + 47, 34, 51, 105, 106, 55, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, + 64, 117, 118, 119, 120, 68, 122, 123, 124, 125, 126, 127, 128, 129, 78, 130, + 79, 66, 131, 88, 132, 87, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142}; + if (codePoint < 32) + return -1; + else if (codePoint < 126) + return codePoint - 33; + else if (codePoint < 0x401) + return -1; + else if (codePoint == 0x401) + return 97; + else if (codePoint < 0x410) + return -1; + else if (codePoint < 0x450) + return cyrillicTable[codePoint - 0x410]; + else + return -1; +} diff --git a/graphics.c b/graphics.c new file mode 100644 index 0000000..b5e2d4c --- /dev/null +++ b/graphics.c @@ -0,0 +1,225 @@ +#include "graphics.h" +#include "font_ubuntu.h" +#include "utf8.h" +#include + +void lcd_draw_horizontal_digit_segment(uint16_t x, uint16_t y, uint16_t color) { + x += LCD_OFFSET_X; + y += LCD_OFFSET_Y; + uint16_t y1 = y + 8; + uint16_t x0 = x + 7; + uint16_t w = 32; + for (int i = y; i < y1; ++i, w += 2, --x0) { + _draw_h_strip(x0, i, w, color); + } + x0 += 2; + w -= 4; + y = y1; + y1 = y + 7; + for (int i = y; i < y1; ++i, w -= 2, ++x0) { + _draw_h_strip(x0, i, w, color); + } +} + +void lcd_draw_vertical_digit_segment(uint16_t x, uint16_t y, uint16_t color) { + x += LCD_OFFSET_X; + y += LCD_OFFSET_Y; + uint16_t x1 = x + 8; + uint16_t y0 = y + 7; + uint16_t h = 32; + for (int i = x; i < x1; ++i, h += 2, --y0) { + _draw_v_strip(i, y0, h, color); + } + y0 += 2; + h -= 4; + x = x1; + x1 = x + 7; + for (int i = x; i < x1; ++i, h -= 2, ++y0) { + _draw_v_strip(i, y0, h, color); + } +} + +void lcd_draw_horizontal_small_digit_segment(uint16_t x, uint16_t y, uint16_t color) { + x += LCD_OFFSET_X; + y += LCD_OFFSET_Y; + uint16_t y1 = y + 6; + uint16_t x0 = x + 5; + uint16_t w = 24; + for (int i = y; i < y1; ++i, w += 2, --x0) { + _draw_h_strip(x0, i, w, color); + } + x0 += 2; + w -= 4; + y = y1; + y1 = y + 5; + for (int i = y; i < y1; ++i, w -= 2, ++x0) { + _draw_h_strip(x0, i, w, color); + } +} + +void lcd_draw_vertical_small_digit_segment(uint16_t x, uint16_t y, uint16_t color) { + x += LCD_OFFSET_X; + y += LCD_OFFSET_Y; + uint16_t x1 = x + 6; + uint16_t y0 = y + 5; + uint16_t h = 24; + for (int i = x; i < x1; ++i, h += 2, --y0) { + _draw_v_strip(i, y0, h, color); + } + y0 += 2; + h -= 4; + x = x1; + x1 = x + 5; + for (int i = x; i < x1; ++i, h -= 2, ++y0) { + _draw_v_strip(i, y0, h, color); + } +} + +void lcd_draw_horizontal_xsmall_digit_segment(uint16_t x, uint16_t y, uint16_t color) { + x += LCD_OFFSET_X; + y += LCD_OFFSET_Y; + uint16_t y1 = y + 4; + uint16_t x0 = x + 3; + uint16_t w = 16; + for (int i = y; i < y1; ++i, w += 2, --x0) { + _draw_h_strip(x0, i, w, color); + } + x0 += 2; + w -= 4; + y = y1; + y1 = y + 3; + for (int i = y; i < y1; ++i, w -= 2, ++x0) { + _draw_h_strip(x0, i, w, color); + } +} + +void lcd_draw_vertical_xsmall_digit_segment(uint16_t x, uint16_t y, uint16_t color) { + x += LCD_OFFSET_X; + y += LCD_OFFSET_Y; + uint16_t x1 = x + 4; + uint16_t y0 = y + 3; + uint16_t h = 16; + for (int i = x; i < x1; ++i, h += 2, --y0) { + _draw_v_strip(i, y0, h, color); + } + y0 += 2; + h -= 4; + x = x1; + x1 = x + 3; + for (int i = x; i < x1; ++i, h -= 2, ++y0) { + _draw_v_strip(i, y0, h, color); + } +} + +const uint8_t DIGIT_MASKS[10] = {0b1110111, 0b0100100, 0b1011101, 0b1101101, 0b0101110, + 0b1101011, 0b1111011, 0b0100101, 0b1111111, 0b1101111}; + +void lcd_draw_digit(uint8_t digit, uint16_t x, uint16_t y, uint16_t color, uint16_t bgColor) { + const uint16_t SEGMENT_OFFSETS[7][2] = {{9, 0}, {0, 9}, {49, 9}, {9, 49}, {0, 58}, {49, 58}, {9, 99}}; + for (int seg = 0; seg < 7; ++seg) { + uint16_t segColor = ((DIGIT_MASKS[digit] >> seg) & 1) ? color : bgColor; + if (seg == 0 || seg == 3 || seg == 6) + lcd_draw_horizontal_digit_segment(x + SEGMENT_OFFSETS[seg][0], y + SEGMENT_OFFSETS[seg][1], segColor); + else + lcd_draw_vertical_digit_segment(x + SEGMENT_OFFSETS[seg][0], y + SEGMENT_OFFSETS[seg][1], segColor); + } +} + +void lcd_draw_small_digit(uint8_t digit, uint16_t x, uint16_t y, uint16_t color, uint16_t bgColor) { + const uint16_t SEGMENT_OFFSETS[7][2] = {{7, 0}, {0, 7}, {37, 7}, {7, 37}, {0, 44}, {37, 44}, {7, 74}}; + for (int seg = 0; seg < 7; ++seg) { + uint16_t segColor = ((DIGIT_MASKS[digit] >> seg) & 1) ? color : bgColor; + if (seg == 0 || seg == 3 || seg == 6) + lcd_draw_horizontal_small_digit_segment(x + SEGMENT_OFFSETS[seg][0], y + SEGMENT_OFFSETS[seg][1], segColor); + else + lcd_draw_vertical_small_digit_segment(x + SEGMENT_OFFSETS[seg][0], y + SEGMENT_OFFSETS[seg][1], segColor); + } +} + +void lcd_draw_xsmall_digit(uint8_t digit, uint16_t x, uint16_t y, uint16_t color, uint16_t bgColor) { + const uint16_t SEGMENT_OFFSETS[7][2] = {{4, 0}, {0, 4}, {23, 4}, {4, 23}, {0, 27}, {23, 27}, {4, 46}}; + for (int seg = 0; seg < 7; ++seg) { + uint16_t segColor = ((DIGIT_MASKS[digit] >> seg) & 1) ? color : bgColor; + if (seg == 0 || seg == 3 || seg == 6) + lcd_draw_horizontal_xsmall_digit_segment(x + SEGMENT_OFFSETS[seg][0], y + SEGMENT_OFFSETS[seg][1], segColor); + else + lcd_draw_vertical_xsmall_digit_segment(x + SEGMENT_OFFSETS[seg][0], y + SEGMENT_OFFSETS[seg][1], segColor); + } +} + +void lcd_draw_decimal_point(uint16_t x, uint16_t y, uint16_t color) { + x += LCD_OFFSET_X; + y += LCD_OFFSET_Y; + for (int i = 0; i < 5; ++i) { + _draw_h_strip(x + 4 - i, y + i, 8 + i, color); + } + for (int i = 5; i < 9; ++i) { + _draw_h_strip(x, y + i, 12, color); + } + for (int i = 9; i < 13; ++i) { + _draw_h_strip(x, y + i, 20 - i, color); + } +} + +void lcd_write_string(const char *string, uint16_t x, uint16_t y, uint16_t color, uint16_t bgColor) { + x += LCD_OFFSET_X; + y += LCD_OFFSET_Y; + static uint16_t glyphPixels[24 * 24]; + while (*string) { + int character = decode_code_point(&string); + if (character != 32) { + int glyphIndex = glyph_index_for_code_point(character); + const uint8_t *glyph; + int8_t glyphWidth; + if (glyphIndex != -1) { + glyph = GLYPH_ADDR[glyphIndex]; + glyphWidth = GLYPH_WIDTH[glyphIndex]; + } else { + glyph = GLYPH_MISSING; + glyphWidth = 12; + } + int row = 0, col = 0; + for (int byte = 0; byte < glyphWidth * 3; ++byte) { + int rem = glyph[byte]; + for (int bit = 0; bit < 8; ++bit) { + glyphPixels[(row * 8 + bit) * (glyphWidth + LETTER_SPACING) + col] = (rem & 1) ? color : bgColor; + rem >>= 1; + } + if (++col == glyphWidth) { + col = 0; + ++row; + } + } + for (int j = 0; j < 24; ++j) { + for (int i = 0; i < LETTER_SPACING; ++i) + glyphPixels[j * (glyphWidth + LETTER_SPACING) + glyphWidth + i] = 0; + } + st7789_caset(x, x + glyphWidth + LETTER_SPACING - 1); + st7789_raset(y, y + 24 - 1); + st7789_write(glyphPixels, 24 * (glyphWidth + LETTER_SPACING) * sizeof(uint16_t)); + x += glyphWidth + LETTER_SPACING; + } else { + lcd_fill_rectangle(x, y, 12, 24, bgColor); + x += 12; + } + } +} + +int lcd_string_width(const char *string) { + int width = 0; + while (*string) { + int character = decode_code_point(&string); + if (character == 32) { + width += 12; + continue; + } else { + int glyphIndex = glyph_index_for_code_point(character); + if (glyphIndex != -1) { + width += GLYPH_WIDTH[glyphIndex] + 1; + } else { + width += 12; + } + } + } + return width; +} diff --git a/graphics.h b/graphics.h new file mode 100644 index 0000000..aed81bd --- /dev/null +++ b/graphics.h @@ -0,0 +1,62 @@ +#ifndef VELOMETER_GRAPHICS_H +#define VELOMETER_GRAPHICS_H +#include "st7789.h" + +#define LCD_OFFSET_X 2 +#define LCD_OFFSET_Y 34 +#define LCD_WIDTH 320 +#define LCD_HEIGHT 172 +#define LETTER_SPACING 2 + +static inline void _set_pixel(uint16_t x, uint16_t y, uint16_t color) { + st7789_set_cursor(x, y); + st7789_put(color); +} + +static inline void _draw_h_strip(uint16_t x, uint16_t y, uint16_t len, uint16_t color) { + st7789_caset(x, x + len - 1); + st7789_raset(y, y); + while (len--) { + st7789_put(color); + } +} + +static inline void _draw_v_strip(uint16_t x, uint16_t y, uint16_t len, uint16_t color) { + st7789_caset(x, x); + st7789_raset(y, y + len - 1); + while (len--) { + st7789_put(color); + } +} + +inline void lcd_set_pixel(uint16_t x, uint16_t y, uint16_t color) { + x += LCD_OFFSET_X; + y += LCD_OFFSET_Y; + st7789_set_cursor(x, y); + st7789_put(color); +} + +inline void lcd_fill_rectangle(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t color) { + x += LCD_OFFSET_X; + y += LCD_OFFSET_Y; + st7789_caset(x, x + w - 1); + st7789_raset(y, y + h - 1); + for (uint16_t i = 0; i < w * h; ++i) { + st7789_put(color); + } +} + +void lcd_draw_horizontal_digit_segment(uint16_t x, uint16_t y, uint16_t color); +void lcd_draw_vertical_digit_segment(uint16_t x, uint16_t y, uint16_t color); +void lcd_draw_horizontal_small_digit_segment(uint16_t x, uint16_t y, uint16_t color); +void lcd_draw_vertical_small_digit_segment(uint16_t x, uint16_t y, uint16_t color); +void lcd_draw_horizontal_xsmall_digit_segment(uint16_t x, uint16_t y, uint16_t color); +void lcd_draw_vertical_xsmall_digit_segment(uint16_t x, uint16_t y, uint16_t color); +void lcd_draw_digit(uint8_t digit, uint16_t x, uint16_t y, uint16_t color, uint16_t bgColor); +void lcd_draw_small_digit(uint8_t digit, uint16_t x, uint16_t y, uint16_t color, uint16_t bgColor); +void lcd_draw_xsmall_digit(uint8_t digit, uint16_t x, uint16_t y, uint16_t color, uint16_t bgColor); +void lcd_draw_decimal_point(uint16_t x, uint16_t y, uint16_t color); +void lcd_write_string(const char *string, uint16_t x, uint16_t y, uint16_t color, uint16_t bgColor); +int lcd_string_width(const char *string); + +#endif diff --git a/main.c b/main.c new file mode 100644 index 0000000..ac5e4b5 --- /dev/null +++ b/main.c @@ -0,0 +1,589 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "beep.h" +#include "ds1302.h" +#include "graphics.h" +#include "pins.h" +#include "st7789.h" + +#define LOGGING + +#define HALL_SENSOR_DEBOUNCE_DELAY_us 10000 +#define BUTTON_DEBOUNCE_DELAY_us 3000 +#define BUTTON_HOLD_TIME_ms 1000 +#define TICKS_PER_REVOLUTION 6 +#define SPEED_MEASUREMENT_INTERVAL_ms 100 +#define SPEED_MEASUREMENT_WINDOW_SIZE 16 + +#define SPEED_MEASUREMENT_WINDOW_INTERVAL (SPEED_MEASUREMENT_INTERVAL_ms * SPEED_MEASUREMENT_WINDOW_SIZE) + +enum GUIScreen { + MAIN_SCREEN, + SETTINGS_SCREEN, +}; + +enum DisplayMode { + DISPLAY_MODE_SPEED, + DISPLAY_MODE_DISTANCE, +}; + +struct GUIState { + uint screen; + union { + struct { + uint displayMode; + bool displayModeChanged; + } mainScreen; + struct { + //... + } settingsScreen; + } screenState; +}; + +void on_gpio_interrupt(uint pin, uint32_t eventMask); +int64_t hall_sensor_debounce_callback(alarm_id_t alarmId, void *userData); +int64_t button_debounce_callback(alarm_id_t alarmId, void *userData); +int64_t button_hold_callback(alarm_id_t alarmId, void *userData); +bool speed_measure_timer_callback(repeating_timer_t *rt); +void on_button_short_press(uint button); +void on_button_long_press(uint button); + +void update_lcd(); +void gui_init(struct GUIState *state); +void init_button_states(); +void save_to_rtc(); +bool load_from_rtc(); +void reset(); + +const uint8_t FRONT_BUTTON_PINS[4] = {PIN_BUTTON_0, PIN_BUTTON_1, PIN_BUTTON_2, PIN_BUTTON_3}; + +const uint16_t BRIGHTNESS_CONVERT_TABLE_X[] = {0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100}; +const uint16_t BRIGHTNESS_CONVERT_TABLE_Y[] = {0, 7, 12, 21, 37, 64, 111, 194, 338, 588, 1024}; +const size_t BRIGHTNESS_CONVERT_LABLE_LEN = 11; + +const uint millimetersPerRevolution = 1540; +const uint cmphPerTpw = 360000 * millimetersPerRevolution / SPEED_MEASUREMENT_WINDOW_INTERVAL / + TICKS_PER_REVOLUTION; // cm/hour per Ticks/window +const uint cmPerTick = millimetersPerRevolution / TICKS_PER_REVOLUTION / 10; // cm per tick + +int16_t convert_brightness(int16_t input) { + for (int i = 0; i < BRIGHTNESS_CONVERT_LABLE_LEN - 1; ++i) { + if (input < BRIGHTNESS_CONVERT_TABLE_X[i + 1]) { + int16_t x0 = BRIGHTNESS_CONVERT_TABLE_X[i]; + int16_t x1 = BRIGHTNESS_CONVERT_TABLE_X[i + 1]; + int16_t y0 = BRIGHTNESS_CONVERT_TABLE_Y[i]; + int16_t y1 = BRIGHTNESS_CONVERT_TABLE_Y[i + 1]; + return y0 + (y1 - y0) * (input - x0) / (x1 - x0); + } + } + return -1; +} + +// for debouncing +alarm_id_t hallSensorDebounceAlarm = 0; +bool hallSensorLastState = 0; +bool hallSensorPrevState = 0; + +struct { + alarm_id_t debounceAlarm, holdAlarm; + bool currentState, prevState, isHeld; +} buttonStates[4]; + +// for debug (and for the future) +absolute_time_t lastWheelTickTime = 0; + +// for speed measurement +uint8_t intervalWheelTicks[SPEED_MEASUREMENT_WINDOW_SIZE]; +uint8_t currentInterval = 0; +uint16_t ticksPerWindow = 0; +// critical_section_t intervalWheelTicksCritSec; +repeating_timer_t speedMeasureTimer; + +uint totalTicks = 0; + +DateTime currentDatetime; + +// unused currently +// repeating_timer_t audioPlaybackTimer; + +struct GUIState guiState; + +const uint16_t *find_splash_image_offset(const uint16_t *start) { + const uint16_t marker[3] = {0x6f66, 0x626f, 0x7261}; + for (int offs = 0; offs < 2 * 1024 * 1024; ++offs) { + if (start[offs + 0] == marker[0] && start[offs + 1] == marker[1] && start[offs + 2] == marker[2]) { + return start + offs; + } + } + return NULL; +} + +int main() { +#ifdef LOGGING + stdio_usb_init(); +#endif + + // display init + const struct st7789_config displayConfig = { + .spi = PICO_DEFAULT_SPI_INSTANCE(), + .gpio_din = PIN_LCD_SDA, + .gpio_clk = PIN_LCD_SCL, + .gpio_cs = PIN_LCD_CS, + .gpio_dc = PIN_LCD_DATA_COMMAND, + .gpio_rst = PIN_LCD_RESET, + .gpio_bl = PIN_LCD_BACKLIGHT, + }; + st7789_init(&displayConfig, LCD_WIDTH + LCD_OFFSET_X, LCD_HEIGHT + LCD_OFFSET_Y); + + // splash image (just for fun) + const uint16_t *splashImage = (const uint16_t *)(XIP_NOCACHE_NOALLOC_NOTRANSLATE_BASE + 1581056); + if (splashImage) { + st7789_caset(LCD_OFFSET_X, LCD_OFFSET_X + 317); + st7789_raset(LCD_OFFSET_Y, LCD_OFFSET_Y + 171); + st7789_write(splashImage, 318 * 172 * sizeof(uint16_t)); + } else { + rom_reset_usb_boot(0, 0); + } + sleep_ms(1000); + +#ifdef LOGGING + if (psram_is_available()) { + printf("PSRAM available. detecting size... "); + size_t psramSize = psram_detect_size(); + printf("%u\n", psramSize); + } +#endif + + // critical_section_init(&intervalWheelTicksCritSec); + add_repeating_timer_ms(SPEED_MEASUREMENT_INTERVAL_ms, speed_measure_timer_callback, NULL, &speedMeasureTimer); + + gpio_set_irq_callback(on_gpio_interrupt); + + gpio_init(PIN_BEEPER); + gpio_set_dir(PIN_BEEPER, GPIO_OUT); + + // hall sensor + gpio_init(PIN_MAIN_HALL_SENSOR); + gpio_pull_up(PIN_MAIN_HALL_SENSOR); + gpio_set_irq_enabled(PIN_MAIN_HALL_SENSOR, GPIO_IRQ_EDGE_RISE | GPIO_IRQ_EDGE_FALL, true); + + // on-board LED + gpio_init(25); + gpio_set_dir(25, true); + + // front buttons + init_button_states(); + gpio_init_mask(0b1111 << 15); + for (int pin = 15; pin <= 18; ++pin) { + gpio_pull_up(pin); + gpio_set_irq_enabled(pin, GPIO_IRQ_EDGE_RISE | GPIO_IRQ_EDGE_FALL, true); + } + + irq_set_enabled(IO_IRQ_BANK0, 1); + + ds1302_init(); + + DateTime tmp = {26, 9, 9, 15, 10, 0, DOW_WED}; + // ds1302_set_datetime(&tmp); + + load_from_rtc(); + + gpio_set_function(PIN_LCD_BACKLIGHT, GPIO_FUNC_PWM); + pwm_set_wrap(BACKLIGHT_PWM_SLICE, 1024); + pwm_set_chan_level(BACKLIGHT_PWM_SLICE, BACKLIGHT_PWM_CHANNEL, 1024); + pwm_set_enabled(BACKLIGHT_PWM_SLICE, true); + + gpio_set_function(7, GPIO_FUNC_PWM); + pwm_set_wrap(3, 256); + pwm_set_chan_level(3, PWM_CHAN_B, 0); + pwm_set_enabled(3, true); + + gui_init(&guiState); + + absolute_time_t nextLcdUpdateTime = delayed_by_ms(get_absolute_time(), 50); + absolute_time_t nextClockUpdateTime = get_absolute_time(); + absolute_time_t nextSaveToRtcTime = delayed_by_ms(get_absolute_time(), 10000); + + while (true) { + absolute_time_t t = get_absolute_time(); + if (t >= nextClockUpdateTime) { + nextClockUpdateTime = delayed_by_ms(t, 1000); + ds1302_get_datetime(¤tDatetime); + printf("time is %i %i %i %i %i %i %i\n", currentDatetime.year, currentDatetime.month, currentDatetime.day, + currentDatetime.dow, currentDatetime.hour, currentDatetime.minute, currentDatetime.second); + } + if (t >= nextLcdUpdateTime) { + nextLcdUpdateTime = delayed_by_ms(t, 50); + update_lcd(); + if (gpio_get(PIN_BUTTON_0) == 0 && gpio_get(PIN_BUTTON_3) == 0) { + rom_reset_usb_boot(0, 0); + } + } + if (t >= nextSaveToRtcTime) { + nextSaveToRtcTime = delayed_by_ms(t, 10000); + save_to_rtc(); + } + sleep_until(nextLcdUpdateTime); + } + + return 0; +} + +void on_gpio_interrupt(uint pin, uint32_t eventMask) { + if (pin == PIN_MAIN_HALL_SENSOR) { + if (eventMask & GPIO_IRQ_EDGE_RISE) + hallSensorLastState = 0; + if (eventMask & GPIO_IRQ_EDGE_FALL) + hallSensorLastState = 1; + if (hallSensorDebounceAlarm) + cancel_alarm(hallSensorDebounceAlarm); + hallSensorDebounceAlarm = + add_alarm_in_us(HALL_SENSOR_DEBOUNCE_DELAY_us, hall_sensor_debounce_callback, NULL, false); + gpio_acknowledge_irq(pin, eventMask); + } else if (pin == PIN_BUTTON_0 || pin == PIN_BUTTON_1 || pin == PIN_BUTTON_2 || pin == PIN_BUTTON_3) { + uint buttonIndex; + switch (pin) { + case PIN_BUTTON_0: + buttonIndex = 0; + break; + case PIN_BUTTON_1: + buttonIndex = 1; + break; + case PIN_BUTTON_2: + buttonIndex = 2; + break; + case PIN_BUTTON_3: + buttonIndex = 3; + } + auto buttonState = &buttonStates[buttonIndex]; + // if (eventMask & GPIO_IRQ_EDGE_RISE) + // buttonState->currentState = 0; + // if (eventMask & GPIO_IRQ_EDGE_FALL) + // buttonState->currentState = 1; + // buttonState->currentState = !gpio_get(pin); + if (buttonState->debounceAlarm != -1) + cancel_alarm(buttonState->debounceAlarm); + buttonState->debounceAlarm = + add_alarm_in_us(BUTTON_DEBOUNCE_DELAY_us, button_debounce_callback, (void *)buttonIndex, false); + gpio_acknowledge_irq(pin, eventMask); + } +} + +int64_t hall_sensor_debounce_callback(alarm_id_t alarmId, void *userData) { + (void)alarmId; + (void)userData; + if (hallSensorLastState != hallSensorPrevState) { + absolute_time_t now = get_absolute_time(); + lastWheelTickTime = now; + ++intervalWheelTicks[currentInterval]; + ++totalTicks; + + gpio_put(25, !gpio_get(25)); + hallSensorPrevState = hallSensorLastState; + } + return 0; +} + +int64_t button_debounce_callback(alarm_id_t alarmId, void *userData) { + (void)alarmId; + uint buttonIndex = (uint)userData; + auto buttonState = &buttonStates[buttonIndex]; + bool currentState = !gpio_get(FRONT_BUTTON_PINS[buttonIndex]); + if (currentState != buttonState->prevState) { + buttonState->prevState = currentState; + if (buttonState->holdAlarm != -1) { + cancel_alarm(buttonState->holdAlarm); + } + if (currentState == 1) { + buttonState->holdAlarm = add_alarm_in_ms(BUTTON_HOLD_TIME_ms, button_hold_callback, (void *)buttonIndex, false); + } else { + buttonState->holdAlarm = -1; + if (!buttonState->isHeld) + on_button_short_press(buttonIndex); + else + buttonState->isHeld = false; + } + } + return 0; +} + +int64_t button_hold_callback(alarm_id_t alarmId, void *userData) { + (void)alarmId; + uint buttonIndex = (uint)userData; + auto buttonState = &buttonStates[buttonIndex]; + buttonState->holdAlarm = -1; + buttonState->isHeld = true; + on_button_long_press(buttonIndex); + return 0; +} + +void on_button_short_press(uint button) { + // beep(60, 900); +#ifdef LOGGING + printf("button %i short press\n", button); +#endif + if (button == 3 && guiState.screen == MAIN_SCREEN) { + uint oldDispMode = guiState.screenState.mainScreen.displayMode; + if (oldDispMode == DISPLAY_MODE_SPEED) + guiState.screenState.mainScreen.displayMode = DISPLAY_MODE_DISTANCE; + else + guiState.screenState.mainScreen.displayMode = DISPLAY_MODE_SPEED; + guiState.screenState.mainScreen.displayModeChanged = true; + } +} + +void on_button_long_press(uint button) { + // beep(60, 1273); +#ifdef LOGGING + printf("button %i long press\n"); +#endif + if (button == 2 && buttonStates[3].isHeld || + button == 3 && buttonStates[2].isHeld) { // if buttons 2 & 3 pressed and held simultaneously... + reset(); + } +} + +bool speed_measure_timer_callback(repeating_timer_t *rt) { + (void)rt; + + uint16_t sum = 0; + /* + printf("window: "); + for (int i = 0; i < 16; ++i) { + sum += intervalWheelTicks[i]; + if (currentInterval == i) + printf("[%i] ", intervalWheelTicks[i]); + else + printf("%i ", intervalWheelTicks[i]); + } + printf("\n"); + */ + for (int i = 0; i < 16; ++i) { + sum += intervalWheelTicks[i]; + } + currentInterval = (currentInterval + 1) % 16; + intervalWheelTicks[currentInterval] = 0; + ticksPerWindow = sum; + // printf("Total %i \r", sum); + return true; +} + +// speed is in "2.1" decimal fixed-point, in km/h +void draw_speed(uint value) { +#define DIGITS_START_X 20 +#define DIGITS_START_Y 37 +#define DIGIT_WIDTH 64 +#define DIGIT_SPACING 8 + + if (value > 999) + value = 999; + + uint16_t color; + if (value >= 300) + color = 0xfd04; + else if (value >= 200) + color = 0xfe84; + else if (value >= 150) + color = 0xdfe4; + else if (value >= 100) + color = 0x6fe4; + else if (value >= 50) + color = 0x27fb; + else + color = 0x04ff; + + lcd_draw_digit(value / 100, DIGITS_START_X, DIGITS_START_Y, color, 0x0000); + lcd_draw_digit((value / 10) % 10, DIGITS_START_X + DIGIT_WIDTH + DIGIT_SPACING, DIGITS_START_Y, color, 0x0000); + lcd_draw_decimal_point(DIGITS_START_X + DIGIT_WIDTH * 2 + DIGIT_SPACING, DIGITS_START_Y + 101, color); + lcd_draw_digit(value % 10, DIGITS_START_X + DIGIT_WIDTH * 2 + DIGIT_SPACING * 2 + 5, DIGITS_START_Y, color, 0x0000); + +#undef DIGITS_START_X +#undef DIGITS_START_Y +#undef DIGIT_WIDTH +#undef DIGIT_SPACING +} + +// distance is in "2.3" decimal fixed-point, in kilometers +void draw_distance(uint value) { +#define DIGITS_START_X 20 +#define DIGITS_START_Y 37 +#define SMALL_DIGITS_Y_OFFSET 28 +#define XSMALL_DIGITS_Y_OFFSET 28 +#define DIGIT_WIDTH 64 +#define SMALL_DIGIT_WIDTH 48 +#define XSMALL_DIGIT_WIDTH 30 +#define DIGIT_SPACING 8 +#define SMALL_DIGIT_SPACING 6 +#define XSMALL_DIGIT_SPACING 4 + + if (value > 99999) + value = 99999; + + lcd_draw_digit(value / 10000, DIGITS_START_X, DIGITS_START_Y, 0xffff, 0x0000); + lcd_draw_digit((value / 1000) % 10, DIGITS_START_X + DIGIT_WIDTH + DIGIT_SPACING, DIGITS_START_Y, 0xffff, 0x0000); + lcd_draw_decimal_point(DIGITS_START_X + DIGIT_WIDTH * 2 + DIGIT_SPACING, DIGITS_START_Y + 101, 0xffff); + lcd_draw_small_digit((value / 100) % 10, DIGITS_START_X + DIGIT_WIDTH * 2 + DIGIT_SPACING * 2 + 10, + DIGITS_START_Y + SMALL_DIGITS_Y_OFFSET, 0xffff, 0x0000); + lcd_draw_small_digit((value / 10) % 10, + DIGITS_START_X + DIGIT_WIDTH * 2 + DIGIT_SPACING * 2 + SMALL_DIGIT_WIDTH + SMALL_DIGIT_SPACING + + 10, + DIGITS_START_Y + SMALL_DIGITS_Y_OFFSET, 0xffff, 0x0000); + lcd_draw_xsmall_digit(value % 10, + DIGITS_START_X + DIGIT_WIDTH * 2 + DIGIT_SPACING * 2 + SMALL_DIGIT_WIDTH * 2 + + SMALL_DIGIT_SPACING + XSMALL_DIGIT_SPACING + 10, + DIGITS_START_Y + XSMALL_DIGITS_Y_OFFSET, 0xffff, 0x0000); + +#undef DIGITS_START_X +#undef DIGITS_START_Y +#undef DIGIT_WIDTH +#undef DIGIT_SPACING +} + +void update_lcd() { + if (guiState.screen == MAIN_SCREEN) { + if (guiState.screenState.mainScreen.displayModeChanged) { + // lcd_fill_rectangle(49, 27, 213, 113, 0x0000); + st7789_fill(0x0000); + guiState.screenState.mainScreen.displayModeChanged = false; + } + + if (guiState.screenState.mainScreen.displayMode == DISPLAY_MODE_SPEED) { + uint convertedSpeed = ticksPerWindow * cmphPerTpw / 10000; + draw_speed(convertedSpeed); + lcd_write_string("Скорость", 20, 0, 0xffff, 0x0000); + lcd_write_string("км/ч", 241, 126, 0xffff, 0x0000); + } else if (guiState.screenState.mainScreen.displayMode == DISPLAY_MODE_DISTANCE) { + uint convertedDistance = totalTicks * cmPerTick / 100; + draw_distance(convertedDistance); + lcd_write_string("Расстояние", 20, 0, 0xffff, 0x0000); + lcd_write_string("км", 280, 126, 0xffff, 0x0000); + } + + char timeDisplay[6]; + memset(timeDisplay, ' ', sizeof(timeDisplay)); + timeDisplay[5] = 0; + sprintf(timeDisplay, "%i:%02i", currentDatetime.hour, currentDatetime.minute); + lcd_write_string(timeDisplay, 320 - 20 - lcd_string_width(timeDisplay), 0, 0xffff, 0x0000); + lcd_fill_rectangle(320 - 160, 0, 140 - lcd_string_width(timeDisplay), 24, 0x0000); + } else if (guiState.screen == SETTINGS_SCREEN) { + } +} + +void gui_init(struct GUIState *state) { + state->screen = 0; + state->screenState.mainScreen.displayMode = DISPLAY_MODE_DISTANCE; + state->screenState.mainScreen.displayModeChanged = true; +} + +void init_button_states() { + for (int i = 0; i < 4; ++i) { + buttonStates[i].debounceAlarm = -100; + buttonStates[i].holdAlarm = -1; + buttonStates[i].currentState = 0; + buttonStates[i].prevState = 0; + buttonStates[i].isHeld = 0; + } +} + +void reset() { + pwm_set_chan_level(BACKLIGHT_PWM_SLICE, BACKLIGHT_PWM_CHANNEL, 0); // turn off the screen + (*((volatile uint32_t *)(PPB_BASE + 0x0ED0C))) = 0x5FA0004; // weird trick to actually reset the controller +} + +void save_to_rtc() { + printf("saving distance to RTC... "); + static bool savedBefore = false; + static uint savedDistance; + uint8_t buffer[31]; + memset(buffer, 0, 31); + uint newDistance = totalTicks * cmPerTick / 10; + if (savedBefore && newDistance == savedDistance) { + printf("skipped saving\n"); + return; + } + + *(uint *)(buffer + 0) = newDistance; + + uint16_t checksum = 0; + for (int i = 0; i < 29; ++i) { + checksum += buffer[i]; + } + *(uint16_t *)(buffer + 29) = checksum; + + uint8_t checkBuffer[31]; + int limit = 10; + do { + printf("\nattempting to write...\n"); + ds1302_set_write_protection(0); + // for (int i = 0; i < 31; ++i) { + // ds1302_set_ram_byte(0xC0 + i * 2, buffer + i); + // sleep_us(2); + // } + ds1302_write_ram_bulk(buffer, 31); + for (int i = 0; i < 30; ++i) + printf("%02x ", buffer[i]); + printf("%02x\n", buffer[30]); + ds1302_read_ram_bulk(checkBuffer, 31); + printf("attempting to read back...\n"); + for (int i = 0; i < 30; ++i) + printf("%02x ", checkBuffer[i]); + printf("%02x\n", checkBuffer[30]); + if (limit-- == 0) + break; + } while (memcmp(buffer, checkBuffer, 31) != 0); + +#ifdef LOGGING + for (int i = 0; i < 30; ++i) + printf("%02x ", buffer[i]); + printf("%02x\n", buffer[30]); +#endif + + savedDistance = newDistance; + savedBefore = true; +#ifdef LOGGING + printf("saved\n"); +#endif +} + +bool load_from_rtc() { + uint8_t buffer[31]; + ds1302_read_ram_bulk(buffer, 31); + +#ifdef LOGGING + for (int i = 0; i < 30; ++i) + printf("%02x ", buffer[i]); + printf("%02x\n", buffer[30]); +#endif + + uint16_t checksum = *(uint16_t *)(buffer + 29); + uint16_t actualChecksum = 0; + for (int i = 0; i < 29; ++i) { + actualChecksum += buffer[i]; + } + if (actualChecksum != checksum) { +#ifdef LOGGING + printf("discarding invalid data in RTC memory\n"); +#endif + return false; + } + + uint savedDistance = *(uint *)(buffer + 0); + totalTicks = savedDistance * 10 / cmPerTick; +#ifdef LOGGING + printf("successfully loaded data from RTC memory\n"); +#endif + return true; +} diff --git a/partition_table.json b/partition_table.json new file mode 100644 index 0000000..04dc218 --- /dev/null +++ b/partition_table.json @@ -0,0 +1,15 @@ +{ + "version": [1, 0], + "unpartitioned": { + "families": ["absolute"], + "permissions": { + "secure": "rw", + "nonsecure": "rw", + "bootloader": "rw" + } + }, + "partitions": [ + {"name": "app", "id": 0, "size": "1536K", "families": ["rp2350-arm-s"], "permissions": {"secure": "rw", "nonsecure": "rw", "bootloader": "rw"}}, + {"name": "splash", "id": 1, "size": "112K", "families": ["data"], "permissions": {"secure": "rw", "nonsecure": "rw", "bootloader": "rw"}} + ] +} diff --git a/pico_sdk_import.cmake b/pico_sdk_import.cmake new file mode 100644 index 0000000..d493cc2 --- /dev/null +++ b/pico_sdk_import.cmake @@ -0,0 +1,121 @@ +# This is a copy of /external/pico_sdk_import.cmake + +# This can be dropped into an external project to help locate this SDK +# It should be include()ed prior to project() + +# Copyright 2020 (c) 2020 Raspberry Pi (Trading) Ltd. +# +# Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +# following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +# disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +if (DEFINED ENV{PICO_SDK_PATH} AND (NOT PICO_SDK_PATH)) + set(PICO_SDK_PATH $ENV{PICO_SDK_PATH}) + message("Using PICO_SDK_PATH from environment ('${PICO_SDK_PATH}')") +endif () + +if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT} AND (NOT PICO_SDK_FETCH_FROM_GIT)) + set(PICO_SDK_FETCH_FROM_GIT $ENV{PICO_SDK_FETCH_FROM_GIT}) + message("Using PICO_SDK_FETCH_FROM_GIT from environment ('${PICO_SDK_FETCH_FROM_GIT}')") +endif () + +if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT_PATH} AND (NOT PICO_SDK_FETCH_FROM_GIT_PATH)) + set(PICO_SDK_FETCH_FROM_GIT_PATH $ENV{PICO_SDK_FETCH_FROM_GIT_PATH}) + message("Using PICO_SDK_FETCH_FROM_GIT_PATH from environment ('${PICO_SDK_FETCH_FROM_GIT_PATH}')") +endif () + +if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT_TAG} AND (NOT PICO_SDK_FETCH_FROM_GIT_TAG)) + set(PICO_SDK_FETCH_FROM_GIT_TAG $ENV{PICO_SDK_FETCH_FROM_GIT_TAG}) + message("Using PICO_SDK_FETCH_FROM_GIT_TAG from environment ('${PICO_SDK_FETCH_FROM_GIT_TAG}')") +endif () + +if (PICO_SDK_FETCH_FROM_GIT AND NOT PICO_SDK_FETCH_FROM_GIT_TAG) + set(PICO_SDK_FETCH_FROM_GIT_TAG "master") + message("Using master as default value for PICO_SDK_FETCH_FROM_GIT_TAG") +endif() + +set(PICO_SDK_PATH "${PICO_SDK_PATH}" CACHE PATH "Path to the Raspberry Pi Pico SDK") +set(PICO_SDK_FETCH_FROM_GIT "${PICO_SDK_FETCH_FROM_GIT}" CACHE BOOL "Set to ON to fetch copy of SDK from git if not otherwise locatable") +set(PICO_SDK_FETCH_FROM_GIT_PATH "${PICO_SDK_FETCH_FROM_GIT_PATH}" CACHE FILEPATH "location to download SDK") +set(PICO_SDK_FETCH_FROM_GIT_TAG "${PICO_SDK_FETCH_FROM_GIT_TAG}" CACHE FILEPATH "release tag for SDK") + +if (NOT PICO_SDK_PATH) + if (PICO_SDK_FETCH_FROM_GIT) + include(FetchContent) + set(FETCHCONTENT_BASE_DIR_SAVE ${FETCHCONTENT_BASE_DIR}) + if (PICO_SDK_FETCH_FROM_GIT_PATH) + get_filename_component(FETCHCONTENT_BASE_DIR "${PICO_SDK_FETCH_FROM_GIT_PATH}" REALPATH BASE_DIR "${CMAKE_SOURCE_DIR}") + endif () + FetchContent_Declare( + pico_sdk + GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk + GIT_TAG ${PICO_SDK_FETCH_FROM_GIT_TAG} + ) + + if (NOT pico_sdk) + message("Downloading Raspberry Pi Pico SDK") + # GIT_SUBMODULES_RECURSE was added in 3.17 + if (${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.17.0") + FetchContent_Populate( + pico_sdk + QUIET + GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk + GIT_TAG ${PICO_SDK_FETCH_FROM_GIT_TAG} + GIT_SUBMODULES_RECURSE FALSE + + SOURCE_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-src + BINARY_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-build + SUBBUILD_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-subbuild + ) + else () + FetchContent_Populate( + pico_sdk + QUIET + GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk + GIT_TAG ${PICO_SDK_FETCH_FROM_GIT_TAG} + + SOURCE_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-src + BINARY_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-build + SUBBUILD_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-subbuild + ) + endif () + + set(PICO_SDK_PATH ${pico_sdk_SOURCE_DIR}) + endif () + set(FETCHCONTENT_BASE_DIR ${FETCHCONTENT_BASE_DIR_SAVE}) + else () + message(FATAL_ERROR + "SDK location was not specified. Please set PICO_SDK_PATH or set PICO_SDK_FETCH_FROM_GIT to on to fetch from git." + ) + endif () +endif () + +get_filename_component(PICO_SDK_PATH "${PICO_SDK_PATH}" REALPATH BASE_DIR "${CMAKE_BINARY_DIR}") +if (NOT EXISTS ${PICO_SDK_PATH}) + message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' not found") +endif () + +set(PICO_SDK_INIT_CMAKE_FILE ${PICO_SDK_PATH}/pico_sdk_init.cmake) +if (NOT EXISTS ${PICO_SDK_INIT_CMAKE_FILE}) + message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' does not appear to contain the Raspberry Pi Pico SDK") +endif () + +set(PICO_SDK_PATH ${PICO_SDK_PATH} CACHE PATH "Path to the Raspberry Pi Pico SDK" FORCE) + +include(${PICO_SDK_INIT_CMAKE_FILE}) diff --git a/pins.h b/pins.h new file mode 100644 index 0000000..075818c --- /dev/null +++ b/pins.h @@ -0,0 +1,28 @@ +#ifndef VELOMETER_PINS_H +#define VELOMETER_PINS_H + +#define PIN_LCD_CS 1 +#define PIN_LCD_SCL 2 +#define PIN_LCD_SDA 3 +#define PIN_LCD_DATA_COMMAND 4 +#define PIN_LCD_RESET 5 + +#define PIN_LCD_BACKLIGHT 6 +#define BACKLIGHT_PWM_SLICE 3 +#define BACKLIGHT_PWM_CHANNEL 0 + +#define PIN_RTC_RESET 9 +#define PIN_RTC_CLOCK 10 +#define PIN_RTC_IO 11 + +#define PIN_BEEPER 12 + +#define PIN_BUTTON_0 15 +#define PIN_BUTTON_1 16 +#define PIN_BUTTON_2 17 +#define PIN_BUTTON_3 18 + +#define PIN_MAIN_HALL_SENSOR 23 +#define PIN_AUX_HALL_SENSOR 24 + +#endif diff --git a/splash.png b/splash.png new file mode 100644 index 0000000000000000000000000000000000000000..8b86c29677c66a4b6eac7b4adfdda9bac781afdc GIT binary patch literal 70325 zcmV(^K-IsAP)jhv4CZ))Y8^gR$_nhTfc7ieB(=&8XB}%(A-aR zZM6l1L2Ib1v9hu3=dj~`i?~77L7)& zs-n{3v6xLyrHx?^hocq_My!8uz$Ow&tE{cJsi}-5CMIk;o3^Y@bfz?5Lm9K7iL{N3 zjoRqwuq7wQto_7MYpkuZP$*)_v}xZ4o6H34ikn_Y|aL zrBg{O$?94)G|9;+8%>N`MTPo_gjaSKW;A9xzhHo|)9C4V?zfKTx0xCKSMoS714mD6GK)JDY443h>dF8`g;c~SQ@dq`c`Xd*S~)xr`7w6MPgCiQJq65Y^D0)e3Hqe_dAug zbKPCmd9KR_2M29(a?*m~kfqbpmP(~`AEtCqMm45py%T1C^4V`%sJg`hkun<|8PiyX zbZyfX&~-7kJVP1{-81jmTC8#Gi=k!pyk6+G*tPMf_)XJj;kFbIt@dP>>T{`!=O!-J zn9vI~|3P2rpaMoKJr>*N59)G{$v)W`CTi9_q^0iI-h;qZC8tYl{Uy7te=McR7}6(c z1499wva$*{Nh&HTtg^Dw5{ZOWYMZns3P2ka!139r0JFNH+yNb+S6*H&pez;Wr9AM< z%(w{~34|>kjaeq0wM>TN1S}NR!SwHFG;9e?!iWYZkQ#SCah@E&x4y}eQUtaLIc=4Jw54W3mMkf?DGO=xW-KCLOKICkX^Djbg2L>yrfu57OvLG|Wi#G{ zFE5V^lm$cr5AHD!b7?7KFb8VhmZMbX^cV9iZ6Zp?Jk z^pYvFsK7293^*XP%#?+7D?`DMmFoM+@o_gEQ#yx4GG!$ia}BRejf~saL_%YnaW9L( zkrC_e?X{ksJ{uGGbN&_TH?EghTHj5lGuGbOYoYQg8ycIoczLBwtKU?5O8u7#P)jrd z0i6L4kA57W>4nCh`ut*VuLRC4h6&ynd2LbPyVwWpC*}NLD6kL!kH^0|$Bn?0^8gM8H3#^VI>hpFl4ZvYCv4S>QC4Ojuo2 zg(U?tho67a{`D)Lx4w>J*4j{G2{GA$0RfO+7NOFhyB}5M71q?yXpLgm6#~kbP_b0~ z!t%<+_Cf%kyB7TjA+Sg|di z0fa;SeU6FQjLxNG+CqRA*TFBxc6BZK1E6QRde@?UwE=*KDeRdo0N6tA0$vtg3&>sI z$eyk_D1e<2D8r&lgtTda-Go4-MAsF@Y!?XjcAm9IAH3hX&UMP1?Q`Q5kH_74!#EkE zNF-wQ_4QU)SLfKWZ|N|mn7RD+-5C64e_Rm!Fh=|vHeMv#!DH&i<5hZa{|y{=4frm`o1}+!Mw6BRLruKgHf`M^^R--{ zAzxT|#A;-I)z>#TvlZxwXwoowFqg6dv>7?Z0j5&1EI2_(eX$+p7n2n>#7qp7L>#F2 zEJbkWf*tflupukYTp*xHuKu$*0Gbh~rDO_Ej3*rM!EC0~mt#cJ9+L$4f{{r(a_E4? z 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

~^8@FKr4NryJffr$@%DVa!>QQzFp z+C638v5w#zYgvD+*#_zgP3?TpDZ{sv?YDG!l|5Zq91N-IngfSz5(<-p>T1N~3Vky6 zuIb-btB;J#N>+J@i-AO8Jz^wdEq)|-?uz^|m%_#(n%w^Kl`#sKiVamM6|23e*zC@~ zSL@1Zq}wAZs(N{X3D_?wXZ%gXG}t#H(RSK;_rSmkACG+SuT#WTGHoZE4o*qO7%k>b z&j_P#RgN=33Kt1i1_ptPA?YGn!?;qef*|2>%ImwV3)VuJ5M%S0A$q%JEr}V>dK<&i z=b?9GLG}@yFMroaG`)^h-}Kp;jke+wE_CyY(r}u0$X>DOMPA z%K>=BDRAR>wWfc3Z8gV#J*GUt)kXo+Sx9dw@YhSWL;N!b=JZ<}rK3D*J z2g8Ne3b5w)&wjWLXiP5W*;Ye4`B|hNxducvJY8*P-d_l4Rh=G~fd#FAnd(BJ20XYx zV53UWUs|~)+Rs!q}84)zTwvIFH z)+PNN8oKUoMRU(ehu79(4>cZDCxz~Sd;q;ta++HC-p z^buDr#6{`U5OU|YvH|J1CJS#vCdW$h$Md@qJf92B$x+rHYjJ^vBJjvD;&yPEuwPvx4*1*TKhxw%*xQ!X=|^Qlx(f{AIZNXD8L zV#gU3WEf6URhcy?0t!?OXjQcl>EOz3^zuSE6X)QwUcf&p4PLLZGJg2};&Y7+&G}zl zWoccjS7u&?NL7|9N(p{Nv-#X*%u=pPuMxi@GQ}4}$rP;c01lsDse(XB_2sReHs(xK zXS$Moqt)h88jnd9aaYBesyazxSl>j5hfv)809-NfEfm$AHO+Fs zhg<_7$#1o%mD`!;%eqy*J$T7N(hmCj;s5PrdHlehw2#}T3i`Tx_=EO~Lmw<T)&z zGqq4|5gnCZN&reDqPtYvujupqRxRgvWXuCtG92D{;|GgC&)+8%D2#0KIRFlL^^f){ z8D0e?)#fuk*d8n?zoqB-_=w1Uw)(TPn=lF2|ER&*uPbDG_{V(q7#&aA8TpKu#N9RW z!7h;O`M|F}lN)2RznOROcO2bdAHDBtw^)t}MY+lAn;x)0fa3WotzA2>6`0!cjg3+} zPe8e4^F}+T67YamqEVGt%8RoR9{%$9wJGH}%<5cNQOS)Bd@a8da!Sf|H`JHOlCyR8WWrZV5#Mq#!1k z!d-IJSroOc(CTwSe^|8m8^w{TR~Gj~!sXZ+_Dh~(z8z4i55 zY;*Gtx6eXm?)mP??$o74n=AyN`6iDa09mZ>`Am%M8YGGE+tu4{u*PaFlCwYqn3D7S zc5D_*dBZ!Q6~Ns1l%$G;-1zqZ$o!VipfNe`DZj@$^W;7oj<&i$%ka8F7KePpci?B` z0~^p}s+2SRV12P4?tEi>X}?O=YhnXhgUXEHJm&jq4%GG*b>lN^;AowW-L5Jvn2%Zc zxi+jC5q(I~;^$$|mh!8{}KV z**LEeu=0aazF(6}jHtxQ2)pu-4-h zU{dt7l+;4jJr>i>3IM5I!;6A^=xJ46nHCH0k?Fs4{dT+KuD81dga%YFm8vXW`FPJV zaA&-x-8gg`>sT0HQDFl}-1Dag`^F`N&u2oX7rX(H zaqrx`!kT7B!$N7LueUs-a!9Hs z5|p{tzP>nudcG%SQ-@N^sFNoX7eH{;m}R=r+*-8pkezBMbW!AwdA-axmWZsbu9A~b+X>b0Q|82ks<$A~iD}ieGkYDp746!0$ac zL%wl}@?UPbPW*>@hE%yFE!RQcu+G1}$@boGi?waqCV-;~H6_Bh79aHd-*_I^+AA>v zD+WGJ0&}+z_`HWNrHAD!)x{}ZTOV3Cd+wW>F0qY`UX&BSIT}8^qU(x{MC>zP{;zJI z8+YAquf5_e?(d}n80lukW&~<(JY^<-yX@Nwvb}|b)3d<=(S>@IH$n$S3RV01l`%d{ z=5M#B>^s>mTQ6t)ZOSh2n=6ea|JAXW*7=yXk~MAxuA5fxO2}ti1>$nCqW+# zD&1+F+ls;Jd=EkUWTSPh4_Rd*X1ge$kd4`atGH*&2hos((QQvR*_OxJ3Rw2u(wHkd zg@2PYG9tIi%(WPmZ^jjW?ZHH>otl8D$r}{k=bHKlh8jq<~eab#?VxeRG?lz~0Nz_wffcWFn6h@;y0h6oQ}2I!|?|S&XG)qd?rF9*Ka4o$BO_5V7vOuiwl=a({s@- z0v0q+8&?RRPRo^JY;9tDOI^>nfN!DZo2mq1mTFJL9xM7f|C+usz!7LNbzEV53DqzvqgLD$*&Da7mvH)fR#}WB8iSUAAbA zw-Txz=R%CT8=9<1G7H8JKNjo{9omUq6e}o9IjhXmUZ%Uc1dIxTP~g+0c$L^-U%!Pa z<92pkabV*$pWjnt^3Ps4vY4E&wp^lM*6<*z+_`mS)8Wc5?UjA!+(lc(lD|*iQA2+wJdsm%a79KVv&Cz1&sXp@0JtpQJQ`uJn|Aghl1asD=1% zVDXIAN>)~O2j{k4&GD!{4GdxChDO|-l)A8 zD^3gJg#hR-r){mOv29v_$InUt{4ja<$BWJTe6SBHRc=UZC$5PN6LO+m0mHG%ks_ZP z=e=Apbxg%qVVy9mvm1+FSbM&ulK0YF_HG}vbC(xJ>3en#764y^G;(*>G$}nyOG3Ij zdMdroQjaPkkixG={Xc%t?Q^zB8(%jC_YR zHBM_;fN8NtfN<{(x7s`2|FgE^itF514v(vU_9Oe+g>Ah6%50zpz+0nw%XvgswaqGl zuTaFC)g4R6GWOE?!kpZu!?lJ##cy6DcdjiIknr_;=ewFJ30%u^=Ifz_{Cj!q!)Jzz z^%;}dv`|AXLRwdS<>GbrzFF~&BDoE0srgWy>#|Zhws@r6Mgqe!SqrP9%*ig~9GBme z%n=Xxl(ME=%_nxx)h~qYt$$!nhnQ$9Ow%e5YOA(eNW$OBifTL8R?Mz^{oeV;7+qVF zob;HsIPcwJ+X_+uJg3(eyEOdkiEGAQy?uwh`1I4(CzeMVm|Mt43H)cb5P2`0(aSv_ z&NnzX>_q6J8v~Y#7E&R=zx9AOMsZ1X<^x~no<6(ef%O(m2kp9w8oRTh#p)zb#yaq% zP6|#)vJ#@Km;}#Bu}ccM)+n8gS5y0$v#QQA>K0QXNRyR+FDP(i2|1#|1Tj<@))k1& zQrTK8?wrc;r)AQ9^f!Oic3rVo3k(VHlG7?YN`Y3TBx=b^`U*%u%^Jd-1;06$xj_7y zrtf^u>Po*~74Q{FT6BlbUDl_FRiT)5>%((Q=@K#>=1EExis z-S&eTe7_)b%$a>W(1@UyYSq%b_1)&zjms1@eBzv z1U|BEnH^vfAdX4asV#ZfsVay}gPC%bb>3Oi<1- zOzlf{3~LDcV=@bN`ojZ3{%@^F!IBer6<@9j_~x6sIK#X2HqCD>2ek-Jo;fw2?GYRy zeJj5OTB-sL6&8uVaq8qn4l*l$?#i1%wb&~ z+pE0PW0U#KpY+s$tCKFQMAD+5%Ece5T|jG>W5aD?GaF^fdQSbIGu!xa%Sh&yOf09! zf&OMjrOEHd7t;6~4m;(O_zdl;YDs4A|=|5chR z55@5Sz8M8bIIpaHhD1-#Y$(>Y<(t~qsLoyBscQS)TM7d^XRqq9C%@WbEn;Q_Uz8c^ zR+jj4;vF zBd|)~D>QWz^;+2~uQU-jwdLWq*)!d_{t9W}=KjqW6cq4%TKMzi5?iz>``Y|pWhO~u+Zf6}*>H@XG6 zDb;>F)@@_KLX#Llb~Go-t{3wl`WTfH-(No>B>4+#MfkPo+@d|y0n}~rvpNpd&sE^| zuRot{H7Q^aiD>-^`S)7ad23GK8=ThSJN@emr*ZzvE+Sw6AXxy2>!kcUvXBXZ2#q2M zESMe$NDJu6KnvPzjxfGCEv0$Yc*MScmw+iR`aIT@u=^{{+I!1)XlYD6ClvVfq*gl#Ko2eBp#P`TIJ*`Z2Og0+cnqU zBx#Tsw9Hzsc7X;{FH_3=b87U%+P<^Ru{>9`UyaWfFVj^#3TvM%X9O+?@XfEs)*?TP z?}W!t-FIoBZ;umO$jqInN!tFO@oLqzR}b0ef`w~mlxvNP|F`GQ3$E*^^mp^Mao7jX zz1x$RbX1MngYm*F5czQb9&>_T zG5};io4+qY@`2s^-eS#TKHI8>v8k_UR3c*qBdV*BiZs~C9Rfw=Ar$}sAOJ~3K~%7k zViWnP4|u-eAZvlh6r43T#dbJ#JONw@})eKj=92w>~oG zQ@eEI)wAhZ`8Q@EzZ?M)KO14#inVc9PhL}W{}<|qe9ykEUf!&h1uEJno>f)idF8d% zr#HCuJN}{Si$&X?E<0`fw^Va7oUiPhm1MvaUVFJG-J!_pNT4uRbT*=^3Ds27rlO(q z7M92zY{Mb3JpmgHf6%_2x7c4r{;uFu@?|6~iXzTP_vGn%Calq$x1zP(j}kU636wU^wqXnrnz`BhX1yii1Sp*1>OB=^C&DlofS zcH3ZFi`10P4{Xepms(dY;j%ML19q@x)%z$PnS^S(wWQx6PA}m%5wJXSvyd>^3wH+eMWl zoYFp+*O$LoVd2+`t*P9--?9>nKe_F+9Uj7|P0E{{NZXem`n3B=$baWImZAKk!H?PY`!^QU-&^e5U}8J(-DFoQ{mCz8z_!g| zaIec{l<@EQ{=NLF&~5dbT=!)&#fuZQzF7DYxqkh%b~IMZgnhHQM%6z`1UlrIjwDCz zNOf^8?fJ%evNL9180oct9G4$YlX-R}F@Xqd>&b92^UW_HS|k6W%gxQXl=B~5D*%dg0O05zWj{+vG`=#K@Z<$Zv}4S*bl{JaV+h1sZTLjBV5 z@xy0SU*YB9gXnzF}Yf7pF|@XWJH(q0}j$sU}vj>IX|x4XpcOO3iWjCRw*@TU0Nz<2?smR1J+?eygu&vJafP+aCI>du1+5h^hjilhsOeR~i{XlgL=a zrDah?I={5J-v=_ByKnnQLCN_YQ#P-)35s9!f4p0Ia6v)jp3 z&SX@YN$YPcr$vrc+Bs!L)W{1@sr8hYpId;NYRCF)sxx7?f2+zB$ZRi{2`4s_Ur#Wu zKtw_&Vwv_m*VXM7>W;`4R<26N*FQ1t{E4Tv*6dV$G22+^{OXR^*kmPvAgw;v+2@4B z+fJ%F%<}_Q)!Je`Ro?w;&~~(l3JYkpT6bJnD@^RR`)YL0;?}K7HTmtS=>fYd-RKon z)i_1My1zuYF+~Mf03iSe(JN;szRHaeESt^)hHNh&AA$#Bc-OgY2wPc@F%q$Q1{A+EN#>|yn zXJKtjT$qg}_hwavX?(Fxg{s6<56y6xd+h2-cIOL=yo9iir{(ey7C40L8bH zZWu4KKkYw(t5d|?ZfZLXH;QGD!00! zsm`jJS}mxuQ>4L>VUm$A&=qziq>IU{=7ZlH@U1RQBdVeg9b$0LKJt=XfBRLAo!$S9 z$5c6HQvcTT^o1X|<2AK;&v9)-m4H$O6?8qYyDhtlvy!&%_EJGoK%{D#l!#$9)u_d3TWwX(c^R@xSlsB{jTS!p*#f@=SP zgq6JWrVrWUhwryX4&LVgeyLU?!SMAvZ?kJJxp_%{&AEX>Gch95v`+h?+k!y^sDvUZ z#r$^d`aM=JAek5$5DS?Qqe-h8aj`zHNhOcQM@JlciREM(ILilC2zneppfzjT2S*+2 zK|EY7Ul_$UnXnBF4Hj;tzF=XnqDkjoB|jV>QYz3v$9Ln6H@ZpR-QDeg6cP0FrAsU& zu*g7%C4vTw_~}v}@RluGETJVK=eIHeBw&PJk;bHTMMiZ_l}a}ot}hg;KdXg+uBwPy z1A=Cvx>QdF(8LyDxD(k4$L##d%$V-94O)LUq-$h6;;PQVnj0xqR!*0e%crLRh{W3^ zim;Cp#npo0X_dF401H_s`Has$$EDV#Uh3Jq^F90QGk^9Kd*q&{9q>K(=phGsTXt*a6O+po~sO^J6*KL4ir=QrPpi@CP%(l@U7^w&3RDcA?#y=>F9^ZHpr_L+OR zZf{efzDre_Yx$9 z^)mqcUY!tD6NxoCZr$_7*$jb735|ou8RQ#vA_h&c-RpK+9PlG^LgPxl=GchzFR0ohVSp`w z$N$+LmubtnJ$#S17WAF(-sgUYbLM?YYmfLXnX3d7mzld*UD7T^No+o$T!WzwW2#ct{wwV1WQuup5r zdiu10D>~4pvQYrUsPuo+0vkX$o+~UcswgR-RH?N#Se%>*)~2;4FbM$}f&guG%`W=+ zfR0se79yoKx zF1_Uz3o2TzgkR@x?C9vQOSDf_cb|(z?^vzmT5S6C-369yR7 z*i9a9cdU`?<~QS#(K$^B+(J3wvRmgw;DqTr&bq|vk5?1h1H$C$QkR;S0zb%J((xI6 zFAGS1V<~i;FAqYH)w(J8a|6=(AzDVkjGTq$fEb;d?x7s&2M~h)OAY;kfz17reP-3eai`IOK_%(p*jDMB5EvK zNL65_jdQBI^609PjE*cX^(iS4Fn|PDF@agB3{Y~J2AH7|;Ih9TwFIbGxVp2mQ~Q*= zZIF%dvuoEbD--C@7a$UqK9B7IJ$@<$DE-o}ofR0t&L+j!Dzxn-+qXNDlplbPA_V{; zjPc=zA9hTM&jz$FU`XHe3qxaRN6v-guh-&403iFdw6r)t<2V2}CO6{Mz1^8#EGOg?OSfW>Ie+Dg|K#cPN1RZGR zfRBFMXJXkzf7u5%&UGafSSg8!sY|eorBF(ASb>cxrMzYIlZ64w|L4g8bIV6M;dHLz z=qjH@f*1hZfhRqw(;MXX^V>F?f-b&@6R&7xBcwvoxkt7#}3+W-u*E} zYisO_4}Zd$%>+tl-}>SY?c0C3ZfyB<9H}((=uv6}BNbv|)AB#M zq<-y78WoB2H2Psbx2A0cg$8CE5Y&q$0a&mnCO)75a8$~__U1Rg*#Q{a#x2;TUfQ@4VGJqjEM8F!x=QH3)?q$)Z?^q>`>&2vZ*(Ne*lVak4Gm;$kWlZUN zO#V=OccxS`{CAY6az0gQ2uqcx>|S215+_h(Ny?JADl@ZgN3P&SzHr%lDX#uOL}2`u zpS|6F`1MDvT!mh*x%INy&hYVl{GWcy{_=M}Z-<^et|)WJ-u2-(+6VvRJKZ*Qii%fM zm)WN6>l_ebCb!Is*881B=E;Fpf5xx=o+bC6n0y&SzOoBgiEnLXJukHNh4Jw%_dI+h zxbL)HOr7uhb=-o;0;y819fFxl6ZW}D>X?H59u-(PM(C2|db6;yLo|6u6eP)rc>qBBAAiCFLrwNtF*UYt zY*dt4=K@n>q5~e&xoXUql%%e4E&vpa>ZO-nvMrZvv*X8)+fn)PI2H_x^tKuSD6G!& z+l@K1*O`m@@tGcg1iOUc-FfGo0!7tzRG;i~#~pV#fIfNhqytV&*{xf*&YGQjD$VbSv5@G4}jEx;Ow%JE6XWZ%4A;ZyMpHv+0Hzf&qB}rYR&T{=Y1~Vv)p<1 za<0(O7{Kn0^j0z{z{yOAB$TE_`J#CZNbCi{CfbOtIFY>Cpoch z8zG)sphoOrQ05vn-^#@1SkNX!_JCL!3}cvys_7C}6mo2+-^PZuppQTU_Y@2I&<~6R zY0i`wV^AjYK!1-b1)r54k<5gC6$!<^2D^zW5**ZbJ>5O>>1heXu~9L%L5piKacbiw zFuQa4yInoqxymKpHI=Ckf*1JquD$kJTeohV*q|gZ_!!jy1>YogV61E6H(=PRHFr4& zfbH7r_Bx$jSkX}y^G19J0LD4c4`YOZI8`RPd{@QQ-zCRiBf3J2h@+bcM zzgK<2tJ?9x1E24gS@0UE?vZ=QnIx1PV&Xa<8uZP*)m0*iHEF5irfUL(loje^4yE{) zs5hr=$0iasplBjMgyNrmsi^FUYhwbf({6eMX>L@^_AA7$K-^4p;ZG+Yvng%5;I}xzg*xmu8H5PwP4dsa#F8k+7iSVCE}{{ z9hXVR6;)PMJ1_w>>gyV;tyKjoSj=2zk;g96PDV2>AnoevRAgDcMwwc!f;k z7JKQ?A+gm_G0%<4Xo%@r`rJ9<3#|lPHQo`8H{(%O5w{_|NL+fFW8I!hVML3T2gTMr zxsTG$^gAxrOrU~{2<{>BAf!ip2W#RxN>yR@;*aGV0Y!?trscv2Yp0lm=wY|2nhGJP zm;~$aPVq4G88|KpS)o!{ki|}+rPiiFuok% zTi$JXrMJ59z_-+x_`wT5Xu!Ym-N9}mQp%%|^J})j0`LJ5!f-_`VL+N_lsrnv>`LNr z7o#W_)5G5elZuMj5dcYunPl<#A%98AN2VXlWz=KS3V0+nc^%jZ0Fh4wWIq$JW&2Sv zNEX9}-S`U!!DcW!F-@85(_(|wGCwCU{{{!#1QzLX=dl0DC*64xU?6yae~{}V>PkO+ zj@b-=G&MI`kATZ(xA+hN5KKsXaj-osB2B?(+6AOq%NUKH z#sT)}F-8H9j`!e$54xAeR`RIMM!|; ze5D%aQjHIOuW^->BEQ(vdI4apP>22x;PT4Kd)GoV-rLuI5dxF|A=R2$C5?;?0FUa- zQGtF~086Aj!rHkm$U*HtnaP^$(=L^b(o~+fnHQw1w@epQhIUclSDOF5vU72@Sn^7q z)mm`l6&T%o$2HwgO?f$^JOKV=p$e~yl=9w}=>u3y<%HlB5(B;s;RBo!0Ce|s%4}5mdjZbTV~5@6+qP{Juml`v)GHU4pNQ7G z&5j_F4q2NPDe|j1b6obtPe=cN2~k?c1onwv@R_IfONSV7e*=)1kpwqF>fZ$_#O!Dj z3PNCt9u>nQV%#Xet=0BhLY}V?k@JR4UfPvQCzB}+1D+K4P72IjRi?p zO(k&Sa&C2QQ@P+0`bgH<@E2R+x+C$J%FEY?!KD<$@v?fHL`G~gs*H^gT0Q5&)r(5U zRQW*p{NipL@t0zPGtO>3A(_9t1gS~u3Ofh^-Lg}j>Lus@`?n2zufP<0*VX;F%4`Fjiul*@a`bicf z*F+p28&cBvgo=bFY(^2>5)~epM$`%~ zMgV$#K8Bd3m0(R-0xDAyow-lR+M^D)uLhWlLGC!<_a--`AK!H&hM8t7f1DO%7?$q$Yt7PJXhvu-6lUp%z-!bf$N8k2ngDC6Lwe7S%H?9Mlp ze7_6kj$;=R(Uz6RoZkyyV0K2JA>SQB=4mDK&&Xkqe{4#Azl_XOCdc$}mjH2COk~Xa zELS@&q##97cBx|%n1>2j)KyZdYFOYpY<&t=9655-0UO7L<@qb1p?C95I)Dr|1?%&L z%K!#I0-yje1VjKF`T@YWetxqrKZ9acHPuSj(&qpdG8r#mFzP_;kbH2M#*~@je2KX3 z*s(+A_@Es}Vt$X}{z8YJ6MWoD?P@^vSbnSz`Jv2$sFGW29|C<{c7YETS) zMrRK zn9(H60*0D2C6Y=^i3O5f5tqazEdQ0Hvo@%7wC=7BJEw9BLK{cOqRL{ zj|PuP53rCg3t#~h{0->2oMQnV4Idk<%dsdmWGDn+7z6r*`N0hR{W&H8K|g*K6qgH3 zCPXfUjB{hPvI(lZ5Kz(iMD*D?nZwn!s?DbW3qS<;xl9s)Fj5_5tGULx08+?VI8Ik@ zkL!c$;27uRLmbq~Z{#z#2>5WahUE`zZBs#5jX^@e8;Ypn2X$g*^+}|czz;8pDrL67 z{!X=@Q81^@okLK8C%%WBR#w$1Ff{2vJChyIOHF06h{y{tV*bj_I?lZu^`SvuU znB*Y=S=wc0NY5uJ1kNEMvq71;DS{C4A>n7j+0FL&`T&M74t`Bo|2TLHkxPUm?yVH~ zl&f+{seFN9G057+CMWg4q$?B9`erNu0RRCce%1uP2|oDX0XTYsdpBP@ZcEF zKmWWljqwBWJ>PLmm>U&ZICs{+jR|~$0#IHi{)>g<;VXnq0(>;DlC*MI^&LJ&zuF3* z$UPjAYzDyOd>Gp;TQ<4A{RTTrB6m?Gmd={D8r1**AOJ~3K~$W+l9vbl0N_>C_3oU4 zV)RMfSNfUK?+RtVP^iIGlNO*)r4&42U0p?&Nq2qVc)wCy)1ks-iRP7k}%0l2$ic6%v?ASVc&g9b#L`aM+sFxEAu|#0tsywTF7m-|lp>@U&21tFru*$Z@Wa_S3)m zwt~%8I~RM;_1pjWgD=`s55DN?_LWza+1`D7?3aK0r=4~3oqxRF9p^*8{w_t@U!D&# zf1Wy@CV-n`xyyC8-Sd?P?Q5U?zSDeusf<2MW_S92P+3wxD#q_#o{efJYV{Yo3AO(OAaFYYqeQEi~+}jnI0F&?cU?%#^Nrx=bn39A%tyP zwpwFzlk*R#qmv;2UQ+iQW+WM_6t~Awij~-Mip9)xr0bXp46g+t6;St>;n9T}IVPZ^FfDg9lo60V# ztngNaKtlq?QRRu~?s zcpazDxJojB_gVdoD`0GR#7SfZeK;0L{Yf|TxR&WTRaZ($!ZQLT}N9Ks`jb1dPa!Hhnupo=^16}Or^olInE+&tQ zLKNJIs1>po^V34&WNZ+9r!O5VEfYB-KP~r-@>Yn*&;7f_TGQ_yc;_|(_rJD#vNzVB9N9{O!$Xv~2gFAB=p1Ag?^BNj;I=7oT7 zQp*ic2$kax_KjF|eWl&;&YNx5Ra@);Xb#x{r~pS_s*uqJ^1bYcJP^FTH9lb zz3%NdIBiG0+ggDQvv~Pt;|eUHCY7(C|;`YuLWx)Q&CYrfCgC%=i{56OiaMX zueJj40n#uSjzPK{;0Uwg-)s+v0aE16@|(tI02aW8dFxnCZt)&|!=UH`@T70dZC(g` z4$yiF{Pap!=SgR1^wHflW-q?@q9>!-wZlb-0YZF$t*QbI&;iQS(nH*hOlYG575g81 z!D=q6wCk_E&K`c?K^swAy_L9*<#&4DIZU$7gN{E`q7h;^Aj)jB=)M(@_%~YA3M{OhWYo&fB9XvrUqtUO?{Q! z{ti{c*6{(OFMjIl&TRcTRb0t$aruS;DObY&n~&SuK6twui^U2TurGl3Uw{3tU4aAq zfmCkB)coW>|BkB!eeYKvacg$$d(*YH;|i~c@aI19RqHs}WxxHGzv}(_&;P}NG(blM zX+ZFaA3kTF(lw9~^V>iFaXT!4Yiz4?zQ-T^>*KB<1Hkvf6Nd$^UWxrR;Cp#kb;0*` zLfTyO9M)cT#s6aOJ;40D&NA;ay=yd*M!icea__NY$FZFRl8^*QfQt`8LR*%;l%-~Y zuq+=J%L4ncgtDP7gry__7Lpi}kme*Vakpj5lC0iGqnXk4`u%>-d5<(vB-t8dFShmi z8qero-uJxa+~+Cxb3Y~K7nxM!K)6%Zi^~J+TF@OJHL3(v8%Yodu+QZLX zN0o)Hbx~7s44L1EVCIoy0i6C@IV#8W%$BFZJ#GQq1+1&DzA}L5;C{;P_}nE}o-39h zhsAj~_W6M6%{N{aviD=SvR7YmdGx-}y4&^)HO^d9-ROm#*Qcp#uL@WHzWW|XyD5{C zYp49z0Iw}0zIg48H;2XM_3EvN-HM3{hBg*5qik$YtITAC=V$fI#5e-7Rf|pU?O|%5 zn_Fz|)0rAs0NjQlmQOa3@x{74bi9v=+>zS45KQH|9cSSR-XNrClE2Tn9qZ{&zxwAt zl`h4~`Y%85+v$N%KbAiHhj*u+_;246pk*xZ38bm6dex->Oz!C-5UI;Id(*1>rhotD z^cTPVf70*$(g)H%B1`o>Kl{z;raP}bBUSC1S6`NX`A`01x(4k(ukFu$*Kb68Q1CL& zcGuf(q4&op!YUN_wC)5wRbg&{CEHjS3AS)A;UW|sB!Uw>&U!IX#qXjm_SOv@9{Fy&WM-Jrv$%>-76@9?ZBa82O``hhK08yZI z>%h``^xmfc+rLO3Ku}ey)2$CcZ-wW&dgJnVZod7sKat+|-aiX#^;FP4+x0p1y$is1 zE>~-gpDk30AfqZ~U~sC+^>}V68}@H!z9&kJ65F zf^01oPzSC>t5zR4IMm;phL86`fk8J<$#XKpR)1I>A|QDDVca}r;ROU;#?mNUP|$7k zH33GtiB_8G&Cl-InVx)lOM3M2&BPR`EeknO0`-43{kqS6CX&ChBmz za3in0{E7e($8Vf)BUa`_xYD@jobx$cR8gcz`T^pK6K#}aICE4NT zxFR9M_l;93GEqZy#yE>YX#ClNl50Ig385}>Gw(NWk2{pJ$GsdPGwwA~jYrEeyZt#P zTz*dX7}og!RGUEIrpxJOLn7T!T}eBu8N*vl*EOxb(``PJ_U!nRPybqa*N=RCEH)p) zdelWebyI-hXW#v60gein4jdzMU4dEo?1szNgv)C;xII{V_h9+CeRb6r#JJtNZvXN0 zzF+vW0Kb?9vt+V#&wu@|-jm+)&ew50#>1`suGjrkBwXgY%?bE?V9xY2*MXe>vW#!&db|s)W34*^$%x2OykkxhciI!&)>)&Q* z6&Yw?KjEg4@zr|l0|3XF*;IiBUELKg(sr^5SeT+5%&hk) zfpG8d1SNbO#6Q=rTfsgF?!W1)(@i(s6u|NL<4JB(B<=?vel&gYzWdTJ?bYuuDO?;B!U`oS6d-pOHdvmQ#?vV&NPeC6fFlDuukV2pCx(H$wJV9H91D2 z#0Ew!xWzSquAv6A)y4&l`I!WZ7_oYQ71Kpfu|bfVuA*CadS0w?_duQ^)vETW&o73-o^uL6AekyqC7XRQcep~vp&;5FO?OR?IvCO~wlYc&^ zsZ7+A>zaQqBsb3K(#`F|g}!pC%WF{ET<#E-O4RKVXLE*!FaeW9*U?(eLSzl<3vO*I zRqriz5Pf0&NB^2=U;}vqqobuB%8CjgK~IY(UTI5JO%dybwU?`jbZ z9#<~P?6E{%C>!02u*z0pZBnVw(3iGt-;o}E;_kHhsjVbh?Br%MpLwC^ykucV+6GDZ z-uoX2Ye{fdLD|Gd?=iS_oR_6v^5Fy`<9j9~JYU^O-C!*}0lKAw_!q#{4>&s6#{h@c z=4^e+L3E6|zFN&vTO9j*z;+pOR&~5L!v0_P#;;1h^SiV~1T@+@7o^9YdS~fd5B$B8cqM#8%shQ}K5z zW4G__6xV;6MNaTks!Z!P#8WGP3!;4W7|wp4bT@zFr~fcrLYDNefBz%#i5qA_RJJ#N z%bn@7AH6@az5#r1&+hQ4+x91Sh4rS2^O}oS5F4a_^VWRpkN)6S{xbb@^`6#V@pj5*&CD8`{LDYz7g^l09q${8HkUWvab+}5Dun9Bs)71k zX62lK?}Z=-&t>p0mG2by_RAjBS*1u)1QerGQ6;QG;!sKy33QTxr$UAb;w+PW8qGcR z>m)e~3ii?%SDp$>qOoo|t)RA5>#tNW=q@)@oISPYMzIKw<4zq0n2fmzQd%c&ow%`b zNW0M0Tf780cPJPVto!KLwiSt}EnD}*+;`$B2eo5gZ|Z837SjW1xhowZrgQHDkMiDF z>Ownj2zOlyhgs06DYCEZe}r>Ah!yBO?wqIFPt$KQt@gFuym@nQ*BSwWCSKR&UPu*) zz@&TZf?%S+40mLsbqnPojxHoI(4LyHRIa$>#lN*aMtPl>XVD*HVsI+v$kOCLe4XWVX}ovg0w!vgyKp_M;D^?VEQ+V#Kjav$^}PK6BzT_kHS-^ubY(ft+%4zkEMu795z%@m>hv*0nv;c4|Vy2e-tR#;QQKJtveT zFXdJyExOv^_>CUN)x)iYlQ#pPDMdAT>;Phx*4EN%S2~16@%(!}uC5l4KZTlMV4v9( z$&F(uFdxOOJbbh#Vo}d--wAl+_K6nKYfVK01nVcuS3z{f>p|y$i5lJ&x_F2l@Zk3J z4w4yN96Uay-u1c^8M%DZ+Vn=;%v)}}Aw6)<7t?1y2lt8=kkIkS=C0-aO>n`6!m?UI zHAR%fkz9}t(~0G_0}fuX>Wt}`#kJ3{34+ovNZ{MEZ*O{f8>-J(iL2JEO>g+>H-#WT z2kzlBTeqj*`mNtdy~Om6_Tjoi_vt+@tfaJl&B7>w3rQ`m-6E{kEl)my8@wHhuPG=w zTC&Ci4GAn+xjuE^u6E4t#tMXwrzO||xtrvS^dl2D#xlL9T8i@lXwOBf&av4j%5mRF z?~xG}kht%LJzT`F?uGzP7bo@le56G?K=v2 zop>x3(ALLyhQ{E=E7peWR@ofikG$iTf_m~}zxrJy9Bx13^Xft@2=#N1=aWH|Q&$oR za{5e~xUIc}umaD5*5;FN^5mm#xQt{JK)Lgoy^)kyOetO2`i>5yqlfx}dS&+3t$?_R z5Mz&v0JK%O)^hm{?m3c{uU#Ao8!f@6r2M zvChW#c?*_UwVij1Z?0uBa{;iI+IPyeIW^`e=+&^&t1K*GS)P}smmR=R9mV>wrwv8$ zqXTII7jU@$XsUs4<@>6D=mr@JF7={Lt|HB@gpx|ihbS?#8=K`^T6nfiG{E0OR?`A| zd2q&`UuY<#-^&8eBUw1ZCYjQL97M`YF zB({$6(?}!j>uwL@Eb(O9aEWbnv}yIK^hWA~uekCWEZv#(v48$}`sH8#6(R=pXzFo3Uxa6r6WQcT+k>;)hS9cC1!i?!z=u*LpR{;pTBHYnuen zRNWrq_{M&f_}4A(V&B$m!;cP~z%y-vj6VekFTdfU6Cd?C6U0nt*oa8blL~UjTW>z= z)G@Yq*>xxPGyB6P8U(l5o3X+xZn*f%UYkvdE^ioX^yhS&5lzqB*C9qex}cX)016`3{$MYLYr$+^d=? z_!*a8iY^lqY zRsK$p`dpCKVR0!Wp!ICbaSytGW?o0Jvg-#_vDOu( z^ly?x184pNvGJmd z)6)Q-s>aeZj*;+aZ9_1X32g#o(s6-I%a<$-+KJ=S3N((VI74X>>G>EZ(>gqi-k)yi z#hWfo^TLf zK;vF1QE%y*_ar3sb!!zGXdI1fs#P={obSv8z)U9D&(YgjJI<#EAGSu^Y!!=!$ zkJ&?43&PMqZugTDA)l8Gva34i}X=Hd11(m%KZyKhTh2-1;CbG?ObZrgx2dNh7grF1ky|{K3 z;)@ZlqSWio*WHyiKlW()&|ea$29&#rp?vLIzdG&T_x?m!C1DUo2J?!`Hc~4KAo6ef z`>G>sM%H+tmXmxt?VE!K2h*~}xY_Ux*S4>U2_D_o022R}%eQVr<~n+fwD^kjmb|_R zK+npG((mKC8?H}l5meA+w6N9lyzSOo(yEoq(+B?d-RY12*M|e#+A(mhyzY|dh4Hz= zkEABX={e4$i&>Eb<5^twfqyRXG zg{x5PDAsH<)R!9SZ(GRT_8l7tnvhN2I{;24!KQ0))s-Q@CAUN|79i;@23ZCQl z%cKtAGfN5Zv{(h8>jJq|?q>~F+yt4?kv=+u!P%R}vWOTJF(&JBTTnf5;tY@^aIzN? zvZY{I9zIuR1n3c;YJ!T;Mu2#7B)eJ?q_a%A)(~nb3XVeDU5IYq-o5)H_V>h-kEQSW z?(ax9U3+yp-g6*5^~9s;3JA)A^t-?Ho8j7RNxMlfyz1sxll8@YX3ScFW{&smp;Cj= zu?~_mGgHuU09)eB1iuqjTz`Ov$LI%$jFFi+HKumMjn{ELqfsT|)AlX6*+dfFn^HMujU`lsKjM7-;mBgg3zwW94)Ccc-1Tbz*Z+XjG z;1dp}zy14b0oOsRkUl07%OO?U+ER!ze%R33nHv$=Z^dRqfLXl(JnON&|B~AUHIrX1jw&y6 zpL021UMNeLL>-^T_NEne5OtKJDjVaDby^=v_HBM=2f_x36%fx_ zM9i%XOY~3v@DI`oWSxHEXMQHNVe^t8c+N+nDaU>kc}p9Z3K4Pyd1V z7L(YZKl*aEgWN-{?!4yuw14l;)Z23yR~XNtwD@0| zN-I`VSVRQQt%VJ716&G{u`B=pAOJ~3K~!G>b)f3qu^3+7aw;=gm(sv!=ic<2|NXzG zzx~jM(l-EUK$gFK^Eak%`o=c_)T`6Czw7JLU3cD|ZoBQ(WN$Y^Rp!o}J0fW`hLvffq6x_0lUQJO!0WTlnPia)I?cGn zmIpQgekN>cuokBkFV}~{E zWYu8+-yjiH+2DA-stTZ!F~7Mywde0UFT|WWf1}HLn%%qng2i{fukLB~cSb?QC}HD< zKxvttB$G%CZ?vDr*F*h)9c~_tYfM6fAOSHUV_k}2N<#KQvsyw|(z;h`dbJF6cc-Fi z1FHhpNWjES2)At-%c^uw(|WMgq@ZkKWDJ+n+R!yCmZyV94r5_0Nc#Y--~9Fe7NGjI z-|^k)GoSxLdgAd%)7_u=1zc@hI>7Jn5J`HrV8+p!d_G~t{)7b`WSokVvt|IO!2ax~YHVZ};( zf-<;^h>vdHxi3h`hkK^d&;P z?+$nQeee6-v>(cZmfmY$`x<)sqWr;P;R2$o>$S4(I4fV*Fg{8FvG}~!&4Bu(nReR) zS%txMZo*YfjmS7*R1F^ILJYm3TC z9r3-+7G*4n>yVAD0hnxTV@<5QeX9s;gd^x#^Qu*3)^k4Ys4p|EdAUs zyfV*)RX(&!z_++nH8%Q89rJzMq~X@)EW9MkpyWd{Csd zyjJ9X;(T*|V3Hs61;0jkmb^yn)y2vfU*}T$DD%E8KCPd|LNpxL($X0;75iz-c2P^K zf=Ev3GV2Ogl{@~ao180e?CYDKnUc*df4>Ni@XPKH7l7|Yx}^o^JX|3#3rgs+pg?gX z4IYIAJJ^d(c$FOR8;hPv-FCaW+&si61Uo+?}qu z^3wFsQ_rR!{mEZUAN=b-O0R$8*ARGTU^d&~DwZ@VK1-h)E})EZ~Gq^qyGHg&C9mwxrTaV|__MAOyY`JKAdi06Ssb>IW#tWE-L?bb z9$RXP#}{lJqor$0moE$V-HvCYymk!ngc+<;$J98C+H^At$}Ad>J@I&qc?t2c4i*O+ znup5`xcu-B|8VdZ-L%x)_k7CC-uoYZJpJDL|1w>3#k#PTC&!LQ7CPET5_|0JrOyfD zaUt-xNtMW=R+d(FM-^V}Nu!X%n*gqPos`QVQlK1GAMwml?sp3=e~SVLb#VGnZy5s= zr)HEDLtYHvp5fjYD=hn-0XQ?&DZ9IZr1=j;NP~DIcaitBk|jFVvoMxq$9Z~$Us1cM zFO2U~&-&EWBsfnMjq-WuBL17ch-atH%<=3ftS@XU6MSARCB{Qa$P6wIs}E{X_8lI! z5jcG>dbX#&-J8=<>OT8;_>Y?5!wOkU=C%vVt_I6#9RIDE$!#E9F5XHye~n?u4a0{U zB@3$3$uMBHD2nwLMLbWs!!Xv{0JWkP$eUd>PH4Q$4s6B&k6~Fjxy`KV2FBzj4pmS5 z${JI_!uE_ysBqY@9wiOHriPPyoCZDumzL@scia(~&7MPhar?&7+9mC{gCoef?}3+A z6Fj~dTtC%?wOISfka}$-?A8NNLnKo=5ISgMOrxWWi|gdr1_5TNEN(+I-Vp@%;f@Nz zZi^O^4d#07-Mt#0M`xBfkfd4lLMUFNB-fMlM8N{~657F|Owz=iqD25S8t; zF=L4t;u1B=fnll7kaS+KtTP=a!7+&dpCxE~JooabXhlu7AW*f(j(gmLF|2V3%+@lW zP$uG8QXkMct92+ZGAcmMu+5cXGM56;+Bs6(=5nm(0+&K9`^wLqG)-zgvF+R&ia#G= z#OE5^xxDYn_+o8y&Q94IaMqU9G_?3|>GVtlDySzjl7tD056k_6s&e$%htuAzkI~tS zavtT2l%&E!a`HLJaqY}@HG<^{W_mEwMbAbs(r>979!!|BtX+f3P5N~@OE zrT4t&Jpo*1itZd9uOINE`3r`U9Z#Rz@=V?4vKx3RL*35&5 z+=Wh}8FA@CA?0N~EnqbU`5c9MVu>W57B5eoix#KuHJ7H^PGXK&fUzv<=E_%$3@#!F z{O!5tWRp2ub$fDirx~9uehT?LzFz^&T;1KXE&g)>qdDo@aURi7={#@G2ly_yy65?# z^CZU}^TXq#8zGzMjcZm@s>3*RkA7$;{a8g4{H+SfGHOP<`R&APimqE!BJjmg6;1#? zD;{+JbUi28=@@`k76hvdY)qpSrUh4&nFA=GXhdII;&(Gl%(CB7;h{{D$%UmP0@U6+ zS~VbY!rP$7$MXLBA4=;0m==Qh^7zbjE?LwalW)tGX8`H0bSW;dK;-)e5B7vRD6j|? zW>7=@n4Jkmo|nCZ%{m82t40&tHA&dCZmdeN1R+7rBuoLkh()TNP=S(SqY{M`60JiQ zZVAa_KlA&)|NH4?YK48hBy>u*xB_nTps<+H?Sxk2YhEV-G50%$W>YNy#q#r*(T>xM zI3a`MGxYftG)Z=t=xBpz+(lc=R^oM*uMLp!*tG*e2CmD(rK?g6R3(IBg0dpmxtLA! zw=@fRd1ibC#1qsgD|&+YXACeusI%#1=ljupCGGD5@SW$oJ-aE41XQ0_t*!=E>=vpn zwBGuN=k0rTE59KN?|U|V%~#(Qz*o=YkAAzv=>)LS0y6sQ+O-V#*O1SCga)*}ENgO7 zwsq+KDQu7}hgi+4$`B`TBbK5QZ4#H+$?q}3%>vvc%xbOZLJFz^g`p)CM?y%IW53}I zZ-6SIMEk+Ca>YftXD-uoCsv%Ac(>nvJJwnsJuS9|1=B`3+QFVI<9-lXC`A$k6!+g@ zvu*b47{{@K!~G^RoP`K*=iztQ!3|q0DoF$+^#%pXaVR4Kp@Is&?qXnXj|U!jFn!Hi z-Ub+dBJQTjFUIfe6S4q>7fH@q;972qdit8jQ(M^)YjbeOxh5{l$ z;#hS-j}cqUnNwA%C$f?ZfaTaq_c!m6pLg84#p5J)Byo3RVfr~OP5X8DT{ZqxR`xrB zWpMP^ug4eQ3@IEsbTF=upz3e;StviH zWaklQ>;mLku?7dR?7ar$WrNbA_<-_Ww^2S_Iv7bd3>f16G@sU^Nj6LB3eV zbDmgd_~Klg&$(j4T#ofz=X>VG;N&PTSbWcQ6`$|>m5b}gi&}08N*jBdL`SUu$li2h z-wyunO$%D!&B42KQue?_8^_{Om3XXbVr}*{tPHG@qBwjEurt$oVBh|5hr6)=ZQ8Be zk<}1Z7!>=U0Nf0&WKn(5l`HBf(bWx#g*4je$6NKFSObXV-VieZAcEMoZ94;0;@p^^ zT3G_bQ%`Lt8BiaYW&bU2PI7TS?xPxhZNw~nPr!33OD!?`Jc)Z478mP;uA>=mty+Pv z6G}-rew5r*d^(S-)f>&$0WCq_?-+;Fb$l8b`RlH~A)1K`V4k0@y8xcKcey!`mY|#G z>;5l7m(NB+?z2c?W%cST{7}Hv@A&Tm#LcZ(e^PWLRhxz79vkb^N|g{kNIkLwvK{n2 zHtAs;b8O;x@HBZRbxtl%P4kz8l8bsdDlfZeYBAS%y@JbfB3*XkDV$vuX zL%>t8a@@umOOtW|R;hbk_!1+Hx?j#gq0a5wx1IpQEcKsxb%JG%vF7TQ2pMo(K{dxC z0Xi%TCDchK;%G0>P2LW2h@ zGfcaaPvsdBdvo?o&hzvBQr+Eissmnbv-A#+o4&(;^q~%a)i#)3`cv(zeh$ww)}YytvHmN?0tLV!~^g$h&iL zt%u68am~tzgXL^46U|n9Oq`uaMs-$I8OHlcg~K8e5l-4+tVk!ffGQ{%A8h8w7LGlT zflH^gg#eJ2jo{b>aI9XvKCMIo>b`rwK$+V}dg6&E(qj+rO>emUvb1XLn$(91u;ZLY zh|_YnY%O+IVgt}|$Yk4BcK`lEk>S4iRoBG+n;Ca=^UBz#;No}`4zQ7WRCHhst)cmk zz$#EiY*C=WCG;~p`gR4NIex`FJ?^#FAi=(63nc1e0i^yMgLAm`nJocCwym^Dy6&|= zZ6l-^+dy>NB3!-Mu7;s)tAjg!Mtps88BURWsVA+A@=24>3Mt;JR1^jCI$J0gzj8`4%JT zz8J8J2?`(%@VbD}iD0Idlom}1*b6qgiw5O=UCU0uESXTX`UC(iASbl|q?@^v(yT~? zRIxAPTAt7NK7>dC4$tQRK&JwX=hBNJF?dTAitSSM^!MnQ)x z$Cb0)K@{h>j$g~sV+FC9*P~*@$_uWepHV8_)YUkkILX50_Z5k0=N?N-Q7pAhf-~c` zB1K`*N#e1J4zzGzEo+w7*CrMD;0RB<3`x1-!`cdIVkG7a|gNo*w*{=x#0m z-+8)mF(Gv2=qEb?5q7Bma5{bj#gU%*b7;ad72rF<6 z1nIH`ahunSryV#nhF~epevq9%icGt$Fk4kSHp6T+0{eGtrAd*iX~C9cg`d+Jl+NPjXZ^R=SP=I71-~o_l^ed7 zy5bDX6=6vv<$gLYtysemDm`nhs=^ZeGYDqdNXji153=!%w5+UWF`ULLm;m_d5j=H^ z7~YH>*K(39L!f!~3)}g;0vCYqJY7xu(yB?vde6i`g(OsykEF2yg6IeLLRbdWRO^U% z8}5varg`9{nK+S~v3>-n8e(zXxL(GidMJN7M&`8(LbF>0_W?XzEaOVnzN)h~KAuAl z0-%)|T2c0{aDRLD?h0^NwsJ*k!;N%;7hatcSZgj~bYzPKh(k_uCMsrTmZiF^a#i`tHpI`B1R)WO|_0;irq*0P%EL=Y7;JB%{1>BFJe;Ltt(n;u83PO}kqIy8nQ+ho)7o=i?pF}<90mXs zUsH!pD^i!ZbVgGq-m&k){hUYqZ<<7aZfb1m23Ga3T5367I2~i~rx2ElYn$EThUjos zEHU)~9ZOjtipA=9i;9hwr{}7hE7%9QT7Dwe%j@iWC64F<;lg0nJ;YlpYcvu^m38Vk za!fCSujOQ(r&hD~&vW-2hl%C&;OfsedBOA1LJ6Mt%Ft}x3_M0eDOB55@W5hYYjtOI zdrfj^0bVfp&eJSCxtj~Xcb+b36qVb+hvM}k=-BPq`gA(HYikFBkAI-r_lOQhjFcznZIjwezlU&YsM{G0>0NSg!PX1pbi5&K;Jy$R2o2cS5 zGYy6f$SRxQQ0d`CP9mPCsqm86O}zq(r`yh!!8G-LrfsgZgJ-{5XU-y@DeimrbpPqk zS$Cq&tU*N~$UW<$j6MIhh|-lk2iQ6IZ}6x+ABrjRc70T+>WOts;4P2Faso z5MXC7+i?Tw{6tqS#$*2mExB%p!e)M*+&$1s1cK%0%|+D&=P2Oq-m@=&z)2}d`5Ba* zjosOpXf+m)+m4;Pa`8QG;eGer7gZeU>FwmZLyX@hi01!>Ue9v-i{rwKp{7NAOOtdVo}Iw)!7xiui|q_C-deH%%F zwBQad?4F-%j<@eZ5oMa5tc$~P^O08C%4>Snwbx`o=e0IU)Qp#|#VV7w<8k)w+Y^~| z6=dSxVu{&c$>SS`wUN6&bu3sTkdQ~XR@O{gs^&>~zvi>w2CjF|btGMLZqaZEtF%@9 zL+)<}*q?w{s;`<31|X_lQqOhv7{OSZ!I(}xn9Brj>+RI z;2@YZDA5jhTM^U6b-jdf?V|F;@d{$4EH4%juBQu1t`(jMU@z|R01HSX`|DWKU zIqdHO@Xfl5u32A>d)ge*31Uh_eQFR*d8kw<_Hmq2gpwe3|FPb5lq_z~!2>}W_A{y^ z&p=eRY)s07E>mZFXSh+8oJo1O43*;oR8CkYl2Yu2G&{wTuZ!n|3ngGcrH08~YI)t4 zT)vVVPyt3OPM0xfLlqggKipKMt_0n+8`j6~7OW6eTZ(%rkZG+s;T=ON|fl_~<|rKFA6$JdfuIg7?iu+RvF$V;&i3HS?Mr=-UYzg;}aB^YO#!v1o!a> z3x<|;CP8EU+d$k7c$`NkJ(KFG=FvSi20X4lpUOMLj_ahpL~Jp)q%>I)f(EB?cV0vx zqn9-PT>!o>yM4N~*`&)Pg_9b;oKAYAe4^Ns?AvBrtGm2a#188tfxZofT3V zO=w{4Dbqe?W1!BwzhYsmT)ir27*42CQssj!NCxe}z7JSH0#n)S39%cHbrTe#t~II# za#mF0Zn0AVzpjkC1_FnxUkX#%UQn;y0b1lTxTpfpAq zKxlsr)SW%Hv=BEz!X$DW>H6o$&JT3Sf?w{mw2h??0AjY zc`f~n)DDJ!FkrC3{=W2@09iG4KC zfLyAQ-?u`eBu12x_uAA;EyD5){Xw{(ZCJ)Gd|8ntCw-6sG@PJUUUqr>TL95o zv)nAVfs`amQ=w2KX{d_yDf*|aV>IM3D~vTEy~RnI6D0NFGCJwAg)$W!Kob%}OfDy* zF{ncPl}_VU5+9bZ37`?XGIOgK05jCy7LHF)b$dov z4$6qhk`bDh4>As|RznHh-I+%xP-V53tf%+*F z34ntQy4Y|hEAq7a6-Ygg0D2K9o{KF0pxTXUFN5*}G5gKhd(AqDIqIrwIgc6Flaj<3 zngMXj-=?|O#%{+sm!P$<07Qo0x)7~Q7i-$>dT(Y({`}Se4hlapYn#Qp3juX=xIYQ5j(E(g1dPh)$Yd%6E>kL-P zeCkM}a>5D2Bq~;ulThm)u`(anp%g_t)5; zuN$w_n%cE{kVsWZ-}mp|7~^{Ap@+i#jg0I3g^}4*O~v+-T~twIAq9AE(0QVCi;lZ1 zSFA=Oa7h&X-wH@tZ#x7aE?T72I~FP58KH+r9q)JW@5{TCtUngkq2svXiYww;7iq6@ z{(`d$ko(h3Ukpjvv0LV*3+($YHYJ;_q?ImhEHD-MyHs%aT3B{f;wx%=0q?@qET0qf z9f!SieWV(Q>opJpDc11>_s15Lwe@iN0RGwrtvXa}hznX~S4-kU8jN%)w^*Fx_gAJU z0vk`CyL)LO)C)A~3&1z)!ukM5n}c}ti_m~5ST&T9l7iJV4%kf%^+v|FdAu)m)}m`y zZ>zy^=nZ58AzszkhzCxBlVJeMq&(!hA}erl1q-bg-N$SJ?d5n!U>jsiFR+e6iubF^;J3yvX{<@9pB1=7jb$!{@*QTA&akOe5 ze`-sBmCb={B;MJ9>r7(d)vviN;(WjNd%qXJR|E$H=fd%`9`}uJd}CO6g<9i+B*?o@ zOYQ7vS1evh&V>_M%H`be6jpNpS%44Obp2KnDIpb~B73a$IgEQBWr2WX1Wzzk8!1`z zI}xS;z|Gb-nf28gKr^oQ*uQl2V>bO6^~y>ObM`I zVh@hiq-Rj|7$mMY0Tp9Y1zfk^Af&pMbB*;|D}8AEY>OO2`JYhK$nz(YwSCwZwj$YfvI9U4ob<5yUlx>U+R z0j>f`Aw0Ve$KrRietdrb3P*46u>eJ@9&}ynbkni+l*uY}zk>KGggghyAd?D6H(YmJ zx>B%0tn}lbyg$9}&Km-pEN!z5W;02i8LXQlxUpmHl+Q&CGyFfw*t!q})kU;VtrlN1 z$rZt4!NMisiav$Lp8I|H(I=w2e2~EF9U|_{+{Rz}sgDwam zp~dee^$V+VIKf%6v%ptsmF@c>K)Mx+FG|;Fc;3eMvc;Xt3)TWLU+S7W~MMtR<6|33S?KRemwbTjVX(!BS)XP@iI`F`@eE?9i0xr;t8 zHiS{}WQ?HxIFu0S1(dL-S}eK=lr~teLw0I_zx#>p&9ExVGt*iuh4xyF0*#t_#*fV4 zIx)4vcJ9nQZ4J8XdihgoQnf7nJCYha;f0=&<5S$c3fz3(NmNhiT9HfF(kiZ2mR_RC zH!&wCW^tvRz%edPv?6p>~CP~E$=8!t&;`qG!8)7-%xs!>pAS-fNgwZDg{SlOPw;Tzr_Nf0eOU0JV< zRQHd91OE35S zbu+2q{q;T{K%!$j zDZN>*BzNzd-6(8!QlmW57p%QA8$}m@@04q!oVev#QZ4+ip5U|byr~*o5_;E8vJwv? z`#uQ2YzqEdBbnZIfNGN8Q&XJR{!)4IKoO|gC&v09{%WSOZJEP&nsHi0lyJ&Y@H z%uq)$KyLB`a0+V;SGTydCF465YQ9+{GZF^u)4$Cy3$}i*xYnLt$y)lE_@4EjReb1V ztT1OBwaB8(ssdw$MU5fKxyvfb%14ozrJI}M5?G#n_Sy94qmP7D<+wwo1dC{KcUPR_ zJ)eIF7j$2`n#M!RR;^8U|I^2VkZal9d}5H@-3vq6dD9hFWA&hdtQv~fAAoFTKXuSp zMgXPHeEy4R>(;Gl4_4ICzT>>!mPYAXcJMGnalo@!X@Z)Toz)zISM&*1>;Z7jrF8>g z*$IgIi3w^&3V@DXYg>+``|y0UCZJkrgnTtzVQX5arqH~z$8AIkv*p)b)#P4(-3`(E)d$~IemOlhlt(g=&;;c{ z04r)NRtz{nTUwQp&$lR(Vcl<*fAakx%|?NH2FGhM15U zXt8F=AUgrI@*JC%sledpw1DI;I_ZsHmCj=Vg6AN3NPiLdo_%&lxLZm>F(mZzAOJ(c zz6AI4;)~a$`yStwwmrKOYDQz)e{=}IBil)-SM3P=MiLrB^t1io-+v^nT(XSsL83-f zVI4^jTWfy)-Y=%@J9k9WbOBlMgdkimU6Bqj?m?`%70WVE6_?X-6*1G2K^N$B$vr>A&*VKSg^oGI62N#)J>X@G%DOav$|2)z(mUc)`*3fYg7ww zPTrr^itn3GZQ*jM5Jz+jo0 zG??{drY)Cql1GIFL|o&B#_g718D5K}_bfoQ`@jH}+Cu;%uKxgZ7-nl93e24gC(~t@ zZX!!NkRG8ad7}cQkK05^CFY6LOSnOh>IM||PzLwRGdt2KtlzFKD=3hpU$7V#yD>m2 zTTWR}!E-V@U3inm^vqyK*EzsW3r@Gw&lHuK08c^#f~3dvx)r`&$W zg~i?|wUnd@K*loX3wFfrj-!a9I(76jVc&}vEuo&5zQd5Y$GK-qIHzd?c06lp7Yc?G zBsIJ*@hl=*I-~LrcDm1$vX3WQ8eh*UPx1eOUKf z5*`7HRR-E}Fq?4~>o9}e7!@U1Jiu;meqI+WtU+9IYi2Y3r6w#s`{&xl%&`k(0>%ES z8IP8qE?^Nk5WqKV*bo3-O4OjtekN*j8Umyqe|lT`$j3jK{^sxgF&!Ny z#>g|-L=}hUDG)0l`Xk@>ed#cDvRc_LAa-{1?5?4ghVJN}{^_5l;c+WyaxY%NR`6Q0 zd{O$IZ+Tm~?#fF7Fdlf|ft+E+%JuihKK8LVpVzv28*;2qzcP^BOargY&o8`oEvKn7>(}mTQ3b3lO zhF-#PERo{Q3VOv-)Y>yfIlE3bTskb6W-Nzd4NU>kk?_EpGJEa62eG#7se6RrzU6jx zOfFv^)h(06%w!iVmU3>kt2Fj?5w4((l7ipJxk>$K<(hcj#=X|8Sshu~<;YzvCKhVeJ_~=JI3MdbyMT7@T zgp3Okz~Uy7DBt_t?@HHQeo5-?q6cr!k&v0^%E1xDMQ1Ts1VHy-fnI&pRaHIhyl{E# zQKGf%we=pIXf{+`!ze?ud3eR+I{_l{EKM|M>6zHzS_SvgXQ^7KA~~k@#qr~r;=QQF z=3GlJk|s;<*#98sWp_FkIVmesJQIy93f3g+HcR_4o>5$eeeqlva^~ZmJg>TM`=R%oGw_8??LN-+r&@i= z12p^hTrbX@|6WI53bS@IOafzpPWT=uBFY)9 zTfHW%w=4;-q6Yv4cP$bTxXfr`!58w2)&*d&2cxhy~KZr1F@%ckc?II1U(kZJfLwV<{O~D-zVRTLx#) ziii(?@Z%&g=A}nyKCWnk){Ff$6jrDOxQ}7s2v>GGbljJ-WD-} zdD9KuqX{?BV`*tBu{EDX;l3|?o+QNnbn~@W64#@VIlR6$T-pb9-` z_0EIZGr~e-%(G4+swWpV_<@^C-$Q1qR^t(}5EELdq8)$t%}pJ}NGP$AZd zQizjAuGkXfnf#F^QFPyoFZqx!B);ce~HCup}g~&vTtS~jDrMb`|EFhJl1r~v}g~_ZYpA%ovgd$AP$ZHa?RX`3U7sjgF zyX~2+>A?qg!b9v%^KXHp_Qfv(PPre)zCAnIyy>?al`JefT^ab4E&wS?d>0dtknY3lwfmlm>vDR$Ztqbk=9C!CZ zdgVd@??R4hDOTCyB@5H0jce1zkek2lo!=1F;?YBU(|i_~&5t}Bl^f5H(d__$_jSw< z=_eBv0<|u>1m;)0>Q&)#KJv&Tp?;#b>NPGDow6Voi>^?AidAm2@aeIf$&CfGq6XCg zLN)wE79Ef)9LM5%6-v#CWD}ng=v*W^;VSxhWveU)G^rJB0RY)4WUI8gyKzQ&346P|LX^1~jp+UnSWaCG%e(>S2wCn>bU10<_koXuDU$~meoMTBj zxpXNFn9FYy`~(zTSYLAz_?hzX>n{6{8r0U+(JHh_xYZc`EvRL;Jg;4u;$ln5d|dZ6 z3=sQUE6IBk6Hj**7fy>tX(=tP1r<3tK%32~2q&)Jm1sK%9VY%hY?B~lt4Pe`3I>{D zGYRF$U4Hrcbp7?$2RI1~ciwqtxNx`Kc0&M)pYt3qzie$}HKlXhra%I9wu*9kOIq09 z2L8Q%{YJ94E7Rcv`=NUD@R|}?*}_%iAiMYoB0!AZ0ohymsW-p*jp=P~duzJ&)wl3I z)-30)67>UQexH2o@$?M*JuUG7H#l`S-gslU+l>`Ba@+Q2W86|)F1l!Akd?KvU6@?7 zN@<(tU|g}B&us5R_tWnvvz1E?^Sv+`)`I&S7CPV0v8`-*1-aP_eB~Y&?w@Y13t@Rr z3(n8Gk4RSeeXU8(p`L|kLQOsaP4~C5`ZAxdJk=*%-jnH$7ascN>XyFnBRl;OUe*@h z*)FQ?NjxqUipPyJ9R`3kz{!iwTBOiMrCdlNwxUBoG=N=o{gSIyDkKbEYh_t&p}S|y zs7N1aT}WjRw6s37@cg%%aU{Ee`H-ta=T;tk{}zb+w_sOzc!vR|2%c9Hqy(0=SfsBw z#@PX7xpu)@tD5a(Yc(eZ98*7_UgX^~NjeAW{hR^-N4M{Y@2?|!E8w{as_F;|wpL0A zQtrcZe#=|l5`^9ThxVt-F26F$$0B}4!iT7R8Wd1Cwq<)8=-JzatkloG=RZ{-10S(c zTk#$UMkXL^nEskOZ%OZFaXPI`)ca&viLwz37c z5Q;D+iZz|DWmWnpHKkf`yncm_hycFcD4eXrviy;c2VDBGjs|c^Mv0+08I&)Q z_^Op4Kv9U^B?CY2M=V>d+H zExWowfJW0TxM=*_QaC4*mW&hChTXl!_Lz{WtT?{oSVCi*YXQ07n;q?|dUH@ZWo;=m z6t^BeuhNRZWn*zIAot+3gMg!?%UX?|3v zA#zKRv7z=b@wQf!YeuosR;^ykg0MP73@nG!LN+mR^%a+=@A-G%MYdDw2}vVee64R< zcv?a@f<@jyuuy3yty_uXy8V9N>te<_T4Yv#7k-~jfg%&Gk`C9?>rq-ux_Eib1h#Um zJy-9GF*=htWs%ak^!^D@iY62eq^~uYtdau60_Jrs?-dKF632=6y}yptYa4t^zHid3 zo}XzJVYT_j8@mYl!Q*U&^Vb0Pld{MuUZ0vJQ(qbrP0gIy)ji)9+FUcR&kH!t@k7t& z++Ma8Us0(ziM6u;khqWwu@@+G_5{di{EVy1gsmf!TLYD)J_6WGFk@`Q_JWpxMK}l@ z^y?;B2!K6roX0_<>W9!y8DLcRVU{LuTu6?Z>{ zkX%U0o}=fZ^t+;|B@XIuYiqq;hAINgsw{ilf33E07EY>D0tB`G0r!Z{g?Ou6%KX%M zjca&De8v<`9R}RW;#L-V!x_c>^_Xf7x_Liy6%4A~W^|hYR`#tbjlG*g5~yswphneV$YMjptdhHfLVwGOg{(c7SwgaH6Ji00lXz+)q!D3jII zF{?_ChYVgP$;VofT02fUK}YZi&9|yrL-*DR=tmZIF7`)H*LaFf7J-?(}QF{u{wx^SuT1 z;MJX8xnV7>I_Y(Rl#@aWHPUvtM$Kr}x!++Hh54%5a4H9|^d2U|>^Sbe`<@t+WBueO zKN;txCHUqyzd4A*p|XMnT`cMn#waC%0-y_)3s*@@m73@p?n@zr*VX1`*}>JTkCH@wj`qp(0LA$!F`^}F-=l=pS>4?qgJ4cuJKr7l})k^95?rS6pzGyye`}( z>0}q2+|Heg_e#RTGrGEGO*Wj!ke=J6dNEzyDCTE7@i5E+Jo_Fvd1}nYMV2=TY@tNJ z)F+wIFa@e<~nWx)$(gN6I;X#ao_3Pp+QrJ>{Mi7j6@jAViAet0ao(| z{^GCF`+oa(NoH)1egBXD@3+$Bn=Xb1Lj2S3#C_F550l+=EQ=CN+{w5%ar_?QcUq-h zpQoOBD$dtXgFSV%oO3_k;dp%+^qfvCWGOlVdnr%z*)IU+y)5lBi@>YD1-V>yhEj_) z#g+sHIXtT-mrQQyqw8f*f~(v|fBQMJy3tpe?+bVeGx{baT4xra4&Xq&$* zHO^zvq=a&Wn>0h~jVdyfx76}n1 zpWpTPrRS}#r>?P+&w5|0JpHU)$Rcxz3W&1zj%RQX;w+$9XfuxQ2=%0&`qZbwt?b0| zxZXbKZMylUSEax1{$P}Jz3pvpi;9j&*kFOnZ{%PiY5u-9lZ7Kn2wBXg0J{ZP()J4O z=R8$n_8NGey1a)DWQUf#)tDF z>-P%9#p9N^pYKJIs*R?l;3Uyg=4vb?c; z@m$X2sXRxlJ73uHlG)G882HX)>hquw&y(LrbS?Pr0-qi2XR!E2havq!uGKo=g=lRk zr6gx^v;2F>^5tH(nbQ{)4V7%WE435MNg^PW;C7h^r991v$1FTH5Q!}&{QjQq)-H}Pp+4^v0c{Y6AMJJ$@{Z8<{rH2iau#p?f0BX2JkpzyB zUEY@r{`_AX=*t%PPH>n`$t0V=5b)&aDK}LI7t99Tlf#H=LROw3Zc zy{?2j=+RH$TNh5(upNF{G_kP%$#Xdj56*M{uxqR1oS%0_q{i!JD6cDqVZ+wE=j>1rHs; zT|NY$ptUDJ8J810q?|Zew{y%+z|w_4z=*6VL|wxJ_4Whdv5$%suCMQz)Y!gb?}^&s zBS#K}C_~qRcJ3Ap!m+3$1f!c-&!Pb6nvL%5IhaPCJ_b*5Ui1Jlju+kD06iB0D^UEs zd&f@rg_p*4(b}`@PQZ@VkL*`#D>qx}jPnS<<(|d@SGl7_GqGgEb5bLh6h;>}-1wja ziG?h%oZX4am6A{@8GZrQ>XOh@Fz*IWVmu#YGf_0l0U}La6=Rl6(sl>hq|Jxq^oJ& zs@9heb!0xJdYG8tVbfC39hCIElFX-@6QgwE((3c!F2U>uJh8KfdckG!9oR zA+4aD#K(5++8tme7ftK07dp$~!~N+^Z@TM*nsHH_DN4icOHeGaVfSfvTMJWga3K(E z=8?z|4E=l**XyzqyK>VNb*%R2Q2iwXaT{Y^#r4kMQY=V6ua)Y*+psvvdvG4O+azSd z(j=U*nDcQw3IZ5++;s6Jl;Rxagz6A$siyWUY!rQ6Q)Q#xoZ1R`;a?P+(-0V zsOT}RST%lEKAuS#_Z12s>gQF`NCKYUb5f{=Qj|h+0X^*&0Xge$55qNdf&`yUJv~a# zj!>g&Y*HScns3JVv?$iBUl$To`*CBpvrm&1x`;}{+fQN_O3LobgvC4Gc}oD&Kl}r3 zF#D0dVkL*cc;kegvw-3_1P!yT#-RdS8RLeFFRHEqp;Tgik>S+U4H+pUudKjeA7+Yo z@7f#9iag$8%KervUkm`Yay^NKYC%N1SJ=j;^r1tvw+p?h0F05VAGwgV_$9xDU>Z!lQJRP zd*S9{;x+QMqM{Sle)ZY}g!ux9Wk2zKl5f+v<&A3d!OyeyDC9?dq)+48UZ60a9sXXv zz!yY4V;^7sWVjw`4OmcLV@471tHGr+Xl!F}C&;KoS3Z-$1R^se(TIaFVQXrL%h5F2 zhgO$w8Jx8c5oI6NSZWPwsVUb~dbpYdXoQ9^j8uBe@@W+~l(2%vV1sZTY(FbNYC%bB zal#59_PNj<)8+GhC#e4}Nt9eNl?6r*;o|KlHYNbMKdmQ0$8slWHiF*~LPFg(vi$TA(oGP-!fMjHNXNNd-uPY*o!r2xh&uDm*JM%>`x$A33u zo;=Tf1Q!H{jR0x5!PPcaEdaWvkyVDQY=OVmbCTmVa^BHA+qzr;Ly7s%eD<@ra>OL@ z@sG%rI%c@~d5RsF0W%OT>R~Goh)Wu!)hZNX@y4A^I|oWpthgs#KI9LO+*}xI^3F=3=u10 zB9uns0nmW8?hV4#Ydib$)(WRG@6pb`4Gt+s^?ijuaEpUlp1yuQru>H&v}-{BK2ZzXQQ)}xm`Xp`}f)K;!I;NjhVvo zez8vQ#XgS2UoVvRFlt%p%@FFutdR2nraIz#^;mEd#J#i%gh$nd{X)3G#!+~^6s{{J zWdZp-=omiKg>$F7r|Z|p1AiPVYoSfBndkz@DA`L%!iy!Zl5_~_2a`gpW!mf=pf`zC z>Le&Mmpta|3OZ2?@cY{e4?h!DpjlqCii{&H0)Cewc)`zOYk4}^1w|*6pS$(eTO$FX zP^jZlC?KdWOej@>S7N;$1EANVi&vWUXx-^%UVHtG>AnXZNpE`d*HE!CNmaz(rlZFv z0K1lSIlVG^u_SHuy_G5rEv~E3zVmlvx^ZK*GVPtAdPzUzZ2x_Dlm*B5BNn5oDgXO# zKA5^tcMo<_8mTyV235&RtYI!{!w<51z zJCAV)LicsoT%B&Z?pn?lHD|0^ucsEG7Icx^_j(9Oe$T(PC`%m9bLrsT3WSCG=soQq zO`GwBp=jZ%rVZA;V;ysxR26Of_R=wADiF==5j>O%w2$Nn9{2z-eN9(n<_)J6Xc*#WE-@3QGls52g*MB`7;NJRk^c7YKNi-EApdD3rfvhEw3>FIq&x_zSRFgJ87v>4wSLIL z&urZh*X*A@@kxXXxc)APRes;ko%u-FeGn&3TZC;1_G zI38o7%IB9ZaVB8A&ffDPKVPh~(wNDG%?@aVL)nK{;Pvv~-H6OahP{gEj$qk)9ZL(* z!h7^yl?BZG>(2VD887wUkxec_1=ap+KRJpFRm4Ym&t5Pt&P_=D>lsY&+!00Kyo>?n zXa2%=`YZ6V1-|l7sn=$4erS@~B@7E;2o>d8TwKLQ?M^>s-%=?pYMoW_vI0P%D#GT& z>)!&7`!2Fmg|4E?NNKR`KEl0|T55S4E|ayUWuxo+{usq-972eU-jC^zV_iJdrrEBjz^NPl`T0F-G(JO5HYsR zGy{Kf^V3jHsD>f0q)pP*l`9{%YiPR;!W$Y&<=4fs3_BgO;8-dqVDM(O?Q9v zQ|a-CA4Sz=UHW(5`JHJ#;45gg0kB)1dOFGtn{oN9-q?UJg2y}#kV{}T31TJ6rb{l4 z_^Jz#*Iw(mloEPfJP$46P)wGZQgQkUhtGez2nJV?aeIzhjlLh&c}3{jyv&H1#35rtjF2bcJ)H1^JpvO={e-A4;W>l$B@`~__-h(kpmR5Lv zsK$h(*@f+NSKwvq>Sm4;D%v^4)9&AwIVH~WXuKP7)`~^l$UF7|0BNmN zMD;>-@*ZWLCm_%D^QVDh6h5ahN6x6)T^>7%zTzM7_WFVtb6AV z{LFh#cNNZ>Y?yO$6O>VBo3ZeRZ`X(^`5y{ zU4STkHQZ;GBg)(@dt-P$(JR*q1OQ4PE)$_l62}qe#!nF=^>1H~I$b73A=*00%j7Xm zsMTf<3@5r)j+RZ3t&Lv+e8!XvZA9KpZ& z+q-c=AuE&UXd%{QJ@W|PZzX~7IBx1gxYv(A`D8i>Shhky-gMcfbjO`{MQw5?B=E(E zAgly9jW;gCP5ipA`�IS44B3O_yE_7Z34PJBV>z%R7_0rXm^-p3)b_J$Pv+szoM=kNFg zC&Au-du_cpQGvq!v>!%^p(-*_O3hS$j*1egNTj(& z-}j#QU0>5`%u@H2B^S-bRmq0l1R&OtgqoseH^K>mZI#fQE4rO6;V{>I&gFY&#e_Lv z&}{U1`C5El7~I(0U&9OsB~Acf9c0rs9@^uOUysv4ZET1@J8rJLuQ6+LM^)EtjMY10 zDQrH3{upF8{$(8K_S|F{EheXl-ZJX0f=^W}Q73Yl%xdsR$-G zp`F;)xcXXgbKR#g!L@7GBlCnNUk~C9&F$&)pM5x8d*!;&{rd+R09`@PT;pku&*KZM zCVB4v(nFynt;Mu-$?~{%h6kk7Xdy-7fZrjysptdeF%P$P6&c|yot0g{X7u)6gMzal zt?L#^1gzvPEH<;vUVG2)Vz`F7yjpwx#N6f+4{RmAwr6h-C4sHc>}h`2yr}Ak+G!RQ zLCx!47MfBXCip$}*kkFMYpyww#3sP!_e5rX~W{mAGiBy)AJYA8f4*0ndq0R7pw_!MKu>R1&g8+)Ifi`QAR< z;iV99{f<_pnM9lT2yp)FMx8}~7^_zM!t5`{)q)7xM^%sOK1;)FzwhMpedAN^$8kAv zN}MWJ07wAt2mrUep~r9^PRLMIuHsmNhwu4)$6I2Kg4QQLeGjhVs*s-l#J_wBzF~cU ziGl(G)aaPah5EuRwwbt@-PJT&l5#=s->Yxp2J>Ec)W*xQ1 ze%EYqTvOv=jHfIDL0#c>uc!CY*rjwA!L^7l6moGluDo+6nmYkNg0BmR0IjuHth@PS zj|DnESFWe$TS&_#tl~Y%almXzhTB|~NG@8mG*e}0sGehvW_z_1>r)qcibKZuT8JM4 zvJsnxw;2tz&SDjvZGN8#6JAW3i}U@|F&1no*Y=#jd6w%~^Uqs`&N=lnXA^Xea#m*p zzB8SC9V@c56jhaLadB(O!wGu(XDSTLVPLrWs2w;1qVfb zk8I^d`5gB>t3bEMeUfS2e;m&N`LiYfZwVFzb4^9+?VSriMg~M zI(44R#jtrHfNw72I2|ZH9}t`^-te+@b#q%po)u^p2&Vve*QMM%VF1DCdrI$gs!0#D>s%|bV^KhLYdUbGC-V-?lm=4Rt`E7R_M zdjY{Lny`^ZJ@(6RA6j{8-_56KkeYw0D(f20i#lHFptWQ;k3M6ijM4pDO(N)dY=K-$ z)Xc7nlC{EZ5XG&3Rq%D`ij`3bV($+v`9>@n8;yB=>Q!oiL+OuwgS~F=cQ;|qHLlN&Q6zEPp?Uya^S&dPvr3ctWV#A2Kye1}~xP6MgTCT@TJQ5Ict;GtyNBcUg~GD2q94JzxOEQpRoSCf8*S5_viQoKgUw& zKtWXId&RvrJfH$i+!JEN#pSArWK33CF}^5Zc`d_jrvBLL>~TG>lEf-;D#u@FO1@^? z&Mjhf0gz_fbF9?{=01lm>W=IGZ|~lh?X0rvfFB7VBqR_ZHMBr!j0=9~{7z(B}3q54#1)W-pI8OOiQ%Xe3p|H|gDX>9Je5G5YXBv7JJ zs(0VXUSdb?#6~2&6bi)0Sx}wCRY|JK0=wF!CiXLujNRNd8WGXVxh8Qrfc37HzjZqk zN0|1xMk7VsuY_MujM`d>sHv$eP$1`iK#b}|I`h4A-P6mWwXj>9+oH9o8?904&;kQ+ zq&t9G=LWU7lo&w%Pe1+1EGYeI=N zqooKnVE|zAe49qhxVu)lj3htTl9}2V8s};RfYdWCJGQ zD#CU~(mIk|_i$o7sjw!@yGfua`^MoTON{{}=i@y@QJ~_O>|Pe%T8z)kqBiM(kwxZj z6WlRVNQP%E6ZVx?{&Bv$kmQ%c&QDvLM1bMm0&*-cu?S7l+U+cK3#DFLlEUd^IvzDYsGoRkr*ghpvai4KW)gZHn|AxNu zky;V&2VCCx1LS(I9z=G-GB8blN|JPo+;3HBx6o>)_2#LO)KV&x88VtnG6z(Y&<;^U}p@CF|&fd zIkQS#F+Xe~=AX~y{{v)|VoABKqrducIx)kJ2njlrO#PFif$QOrka4@z-Onj4^ovmI>z;Y;Af-l6DhT6amiJ1LN zA|9RyU4)c03n;zUW;QkiR;3{K#{s||_d%}rP8Q>Hk?M{F_zvcrD~SnKne3Md-2YM1 zQvLE*@8rDwJFmX}X2vs_NcJBLEhJ!f@#@ao@10JV)U};Mx!+Hc@#K-5FK$=3l$7Te zlIH2@<0q2V>4}M2I2ar0iRWLKI8!Ac563~{ji+`tR2LbbEn3g+X|nrA001BWNklUiV8Mou7PtB294jkj77FPs{;7PR8lu;s5^-O3syoWOWSF%y5-X}SMXYL7KWwuEl)z`hM@9cV^KOIPhn@d4-)z%aj zLK|G_a-17meu6#p>5-Rjzt<{S@0%0PdHe0Sez*I#zTN5byCwzPFz0?fxA&4(XYJqR zy=S5p?(p7q!LO{1Z`3M_FB9g#7Uf{Vj8Qoy3Y!t5oB@U}Y;ab|^I@&PdN&zzORRH|_ z?DFp4H=4_MW5@ZSKg&M%t$^bGEQk*iluyF=AT%2uPg0Re|qJPlbOk1ajA>_Md$B{Yhrv z{+uVI#+6hcA&}{danUAYTq1cTcVUf=FGmmnlkTc2kLNyi(&g}29^Oe+m81l&$x-1) zNZvz8$!e4k0@7}YOM{R@gAvKGGBXDVBS~-k`>(a#JB+|X|K-2^&%gSjBXi#p-#rMG z_iBS3zpvSyyta>gRdIgpRBQ8!S0l*@B28uCJ*>fDzF&)-N38xrnC=obp4Tc|H)QGXDXxB30-tUR)8UO)=Bj8qEHCYeGO8*jL_ zd?y>hBIKq}Qb6zVCsMyR$aOxze<=WSIFT9OIQn$J=)EjV$pj9@gRAC6oJlcB2uZs{ zB9v%hoK4XYHi=FWP|s;%dc{8nZRP~>%+4hwO};(=Y+e|1pk%|~ z2?h>mAHw)nM9qP5=+p){30${2xf4YblR-Va08nib|2HW_lMa{zB=+6TFwX<ax<0tz3&~d&a<=8fiEZA@F0@^!rBWELR5Pl7}yw$DUkcWx)}~ zCP9KrbfJ85g?)nR@+l|C-FzbVmfXHvPs+WhI>zs_UiXV(pLN~e`1j&-_h!-vZf8@w z617Kdj0?#HXK_dfP-FB0kNvab``hI2j@;PSA&}-Z@s-)(zyJO3&%`C^0ZAlRwm=aZ z7EO)jBn|f|FRXUG(iuntig}YjUW?6%N`}yDVN(m)&qP0nX=M*1c`c9eszWPlF~j$v z6!F=YiGNYO-+-NZ6p~`Tn?)$ur3GruHaN|Y1-PrcW)XMfs3jYsLQ@U@do zP6R8UW5Y5oqCqZ1MfA6vepF=~N$n%C8e?E1%7esaBLaTfS_h4`8go)P1F(v6`yzQw zX~?uiTO~?;>G%I&veBqB@{ME9+_J+SJr>(!qElpchRHS(j;%T|j3tRx|Ba3N?zUF~ zHx3k(nF&YBvuNmA6H_?X=e3KL6kYnu&|T8j4A+1fBabxxB1bsoY&f&==b6=e{2O10 zzoiZM$^zf)#Mv>?T9h-C2!LmAq=85*i=MHZ9l!{fG~mBM=|1ZO8tE*%Fg2zIXyI1= zM{#gJ=UHG=v^)Qovf#QFK{F;#ylL)xDhsw=*c#n;6Ifk?$Cb3_`>Mc-OH*qnk-@HH zqkz(JKmZ8687ZxOvuF^twWTq>Lyg#+E7}cEoZBoZB%pe50h{kE=qa|eam`%IwTIG^ zmt(vEjNEusZYk$UYU+EumH1@Z=Pn`e^mPDf()w+zv<@nl0A zj+}cZSpl`KG1S^|Hqxu(^y5^X_q(yF0f{Ff&99={%QT?>4LfumFmvw-7t0*+%>3^e z)wu=anKAkd%_kCILEre+$)ru4L$1-!ThOSKDgT>09rrqdx;feiv|qnTayAL*YvX$; zQZgXl6N!*>iPO%QY4_K!yN|$ER@L1v3++@-i`2sI5zzZ6(%$*A??+9*+lnzg6KVKT z48nN>mYQ|zl{0}X?AmdfWU@QAcvN(ZAHMRVndH*0k1?AaDL^zpmPIU^$j}$^ECzho z+ah@$NjXAEKn15uXfU{pjNWlC3-1FTOT0fWXwc$wA zY9bn`dW?9Dv5p}>8d8|oQfi{wWt^k$CuTGhQX_;NV~13&o<}5|N^sh3@&5D|KfjeW9o2%`tk1vq;!9%y zuQ6UfoHO|XBR!IKrUYc#<9K~5<)-P_CmuKLbB=zJP|!{q@&z>I$*c1~f838s*hHVM z+1O`eL*jBi71jANGrO*9kh+>CijC2jh>!kZ(p>qDnZf7IH@@2Bc$?DYct(#*k@I~M zWBXQ$r^_V$#F3|V-ibHaxo^j-E!~snTgs8PkyM#2N@J;1*f2oHFUQfcio5NLJVwVmUDfkzBX4Yp4*F0hbSMiN zs7y$9z$?D7oxNAjg@_l=BM#aJCwDHKesAYW5)*uURXy3T;MMAi6g0fB7>%!omSMuF zIYH%X$XI_jTS=2r)(+_Rkf5_0lX~QmJD#Y9QB&Be)Wk?)f+rFyI^^d_GhQMXgmB@W zy&rW6*sM~KQDZk%xLHy}HH@ma5tjcb66PBL6QDAoV&UAebxoY^c@&+c%KHu(n7s{X zAa7fw`pmX?I;tb5@Pu2X&wvA!0w^wCTtfSDti>zD?O?csfB%pFFy;ah%GjjBrTfoG z@M!^`2^azAr=R}jEOLoP^;x^?{dQgU(Gs`oNe5Q!v6n(Sk}v}B@TL~no#(zlw*|Q; z^;gd0^msHiaEtTDAxGT{^XB$e)7|QnlX7Q_EYWq(?9#; zox_oaE15kS$?rg%EuM$SpV@rUrbP^n1Ru4Xt+F<_X2|heg|_|sMDG}Xz)u3SVHCjE zu}k_Vjh#qtL7U0xL=k!JD<}1$Cvj2$y6bFqq`pXT4;{?&3)sa%n4|;&I*AF;dXmWC zMx8VD(IQ0wwxVDq`->0P5PqhmiWs%d^w;H z-EDYe2cCR%=U4y>h+3c|G5vJCU%d9lt;H{Ap?*k$St-z_VEh0G3Flz$cO%c?;doLN zTe`SalN0Wxf3>LrfhJ~9S?*W4r-I@*kjAs|$G?<;CBWDC=;s6MDx-~;J>#wVn(xLj zljDp((2?1OXY_1<9GE|ucD@(e?4_vLm(%aJ!ejjQ^WTbh`2BHSJofxX7SA<~#p`WO z0lrFTS7rPOuy}sJiu9-N?#n^+w}1P$Q`iaJ$@mQjZB8O%IqC5;Clq)ZTF<7|*Xus! zhp`ba5cL1l(3O;6eAIS(w!WydH?qMxE}*!)!qnRZa`pNT*zHQsd~X1|9~gP?&UxT@ ze6yo%<6bFM=30y4;qb)v1q|8pCWcWR+c64A4i zRH}*i$nnk0;#z>kLgLYLA3*YC$huTCUQ5?_FN+yKvD^Lp)z>2ZoS%L!j#=z;0UGy| z{++6IB-iW$1Uj5ICr+dUW&WP)terFc<$JZ9nsEVfjvMn21i3_H7EJ-QirelnwXq;;b z`k;oSkNWNzoP77~0VTlm;ZJ%dReko%D6fFJ%}rE}xSY~h$bOLND@V6y}F_K zV^^LKwq=8s$$NoYHV}HvP*(zsvM|#%0*Kegt|t&-Wviy=R@xg)%Xbsyvc>g&4$8-a z2WOwZ#Si`EfBheSbw~B`9s_f~hQVJ))x9m2e~QOv56nliOAf%rsSzOCdo3*bWLCZw zyWXWpJ(n*=-HCx(DC^~)&iYFSh>{JU>Vl1tuLkq62~s@St9i8%iuKy+m1_=)Gn;4gE^hv~QPP&VEL zrfKuBq;e-Mz7_D%C*DoPwns9`d5=WOYbYKddpqa77&;A5cu)3|^xbisCVq=7d3>wQ zp+Cm&qc}kve=HP`LnN|{J8%Mgmm__OjyQAngPqr6dy{7B@kpjW{n?LaQ+PLkC#4ji z9FUJhhL8<(j>OqA1(Ray0xDu}MaYQg5S>8k^=!srl`+TB8x=Z#YwMFwJvmMhW6HU~ zDReXAh_ADwl4-g0tuA?;q@Uojrv=YhZb#jwwF?Jwn++AOL%)(%6(6^}X86lCT|9d>vB zTYM(nfn@mwUf#{VOGT}eXY$vePrYRj_npt!ux9aX+*TCiq9r~q(4K>O_Ac6aBJ z7)f~U<2VZh9me9K&ibESh=kB=CnPAK3aHqPyewpkX)VTtrUfKOGVWW$d!099XN=Li zSx(sf#R_fV#hLi0k^JFCN{NLPr zbySe>N}g_p7T0MEZh@)JBG=MJuk!b?yr?VSm=R^{T*Flr;j$7yjBv53wWU+t zlJd4|-md`ir7Y+#=_U_6@O6UJR#JlTG9Ig6yU59%i1pr9Ep9-#@9o_6paO&Dv@^*q z6l!1T3*FsKeCiBfa*bY3dkQ?Gu4EB5Td)OEj+Vs-ARAdR$pvC7M1*d375CchjUA^$ z<~JwC#HdE?q`Kj8QEf$$Sis26ICSp2B1mL(S!y$&el>(?-)Xz#15~)7V6aC8SS^0- z1H#Vn0(4&bIgX?(RM$0Ljsa2yMYJ2C8zLqACP~Rrs8A0{>am0die(k)B4*WkWs97& z{JECm?2^Tv58(OyDv;mebuVOtcwF2uJJ}YS)odjti&N>JjRXuV*!SL##Hx+P(>UN2 z;sEl5dPY8XZXFXce#Ur&3G6;&(L*kO1H}!EKZ;Enxw$* z*_5|5Sak>(U)Sv>TF~-8|AZv12JU8}Uk2bd>1~{Sp5><6Rv@?qpkETe{I=fauBz^C zd%&mv9(zH0^hMwI@~+=?7YcYKNN4SFiU(~LJ+=+ONPhN4GF z;M&E6VSRXd=dlA(AEpZ0;bH0vK0kW&*nrxvLat?7ITDo!@s?`I5rD@w4uN!l*G!eN zQ&O}?0;KwVFQq00&R@$tpH6L!gR*~Yi(cp$m1@O(UfMaDbnIvr*D_H>Fr1EUO~IJu znAtZRv)5Bhdl6}$NSl)TFWk9fN1xfbEVp31c&{Woh^l9H`V%qAi-~2hKbv|J1>Do& z&RvLfq+KW9eP`4~*;)<;Z1kO-lPbg>O8v7yE$sJ`xkivaE~4Mx`Tdv1{`pQMmzQ7u z^R$a}c{>edUEYO z8!1?M#rIBz!lE#H+PB)4`H;RnpFTXBzEPb44_+j`B>D%lf|*PbzL3pg3R7iLzmVD- zY-N|CvU`^PHkM+GACKdSBSH2T*Ek$iUkI9c=Zz;Rg5sHHz8SFl)?5!i5?P)N@=7FL zDf0Kli~e}jYu}+6;WpY-FGuqAzxyZ$AmT;LvU}qjK9WSF`|&+P^j2X_asy6}3{LK^ zTAStxZ`+(4z;9R3{%)S?tu;3eLXT!S3VD+^pW!EdmwVa1?*2W}rv-k8_sFYv`R-ny zuL$KkmKl9{mr+%ri;0IW@9IRCZuI`U@OIyi*;qo)8@Ptqq#iA z>@_^cqq+qQc~9-g)Lbvr6d*(;8>2p3o@^sImSl1w(jBS7eMoFLh3@UMrc)w0)P^+a zouogHq$Mu)#+H4!{b}L>ZO}%2@?AZxBs!8_W0XmV7OBlb1v7sSttgw5^j|k_gw&hy zlxDwSZJZ3+b3FF1#?^8>Jr)plU<E5)wL9@`Dek+k*e8tlFJuggi5#c^(n{++#yW z0g`NL7lk5*3Ue%}>rsc!oJnkTA~ihc<^-g6Rw{E(Subzt(X2y|p@3($A4kz$> z#((&e`CLEtW+QR!dZZh5;h9jyy-3T(%{54k=rls#NNLUC$Aj;0*IIVM6(>GY9%lyn zQ|2dvx;Sj#MVFY!4UVH5B)!SSw=y5?79egDR(@TW`0AbiU;y7|jr$#n7*Us4LDt0G zfG?k2ySnN!REi}fmdGobqaNC!5aiuZGK(GnGzGlxEV!3netCI4ZGTiWe***nNp&fg56~{v?9?oxQPuIevcN1hDp=_z7B(<-z2>uP zNZBN@+zRW|t_0*Zo9nq3dnk~toz?Y?Ih&n(6l|#COa$`ewG9KN=pK1a#*#$qJLlC_ zIk_0OJbXSQVHuAb*?0!vA~~YLj5-?dHTFPpLIoqObsU>S)c?L~l2c=7e2m?hGp7d- z#rr-I8WKUxtBFtBULe0x@T;2v)NXrXCw194Za6t2oeDR)%d5}evzE^9>i2h5b?-GG z_iG%zmqGFFsxIZWbj91@J4<$62H;gR+$Y;az@JOyw5w51d2_Gu=w6BX6o<$4NPoPZ z>?8P4Jf;@00@M2<5nb85Z68vfk_m;f7}X_NNo3&7z}At{!_MoI!wjp}eriwccbIPb>5@)&o6(WdcUX7M(8 zz~L>h-*a5HtVl9xC!kmn6sM-2bu4JRb~GNgYQ1Y8k2#>EXp(x90yH8+yLx`B=TS=! zN8P>1pc%pqzb^C}NVbF;~5pKBNVjB~l1t#r5YCPr%A z3SEg7(cdW^p3iyDsf~&$<-nQO^Bsp$q;q3}a7@+R`$)nJhrle3nOdvRkQ%!y1aQ2! zWAeGR8twzz?rfC0*ScRy>4WonFq|7B#(%P@>}_`n$!R{t;e-X`aQDy}N;mNE#$!jS zqM~JjmG9J$BolYQaH}y3^nj0vJQ;g4k*P7H3~HtXrH-B25|JhkC&Gd4u*ndBW$SH_ z`~>1_PpL3ftsAvbZypB(fT?r`2!vnN5MPgR7kT48fmpr1Es``Tq&5ktyu0St(++L* zcI_rwC_tFb4c>1Z3S$9~T6NCnRcmYff9}Nb*sj8dOUyL;n}SQ|L*Iz{&R$0nrrI77 zB@va@YrMiC5g&gxHG+?%W^59Lsv-NqnPf5!_2p9df3CSdRn}n1k)*zI3@~aMe49Z% z(Z&ckHQ9iBR7&+M0XZ6wqz_CA_5MouKvl%`MAKQ`)uEu%dnKLf# zS1-3QbxrqrKD3}ixtG^~_6DML64ik*nO!1v)`2vfy?D6I)v_3>k<7KM1xC3|5a{(E z92olYTlzq$x}zT4VPm-u< zZjoiH@@NYQi`3J6fPj&@=)SILewG4Ldz9~7)3r*(Z49Wz;yi7g zy0jt{vUt&HMnc@AvwAQq$vUsKJ4$K>qS~x~+Ub}VU;6!7@HUc@Ng&{*9~X0+whzfX zpj*Hn={11nT+f4QPnFjfYCMqj8XMc@QEtW)=V-g}Xptk&OP{FIEyiwhsBKd}FGkwa z?yK1(we9rjm4{JOj5eNl_M0O~8K+WBwC&23w{Lmy3pfC^cHHp*Z)F6UwFiC2{8==U z&Dx)S@Gc(?AP(?to^>h2_U>I-xieXo34}ufhY!dFq#rSVQ*5ZOk;rzb$0PlH{kr1_ z+%;DBUgK3N$QK>cd&T(PQ+fUAn0564>fk5ek$b*GwEb$ws03+VAaE@k$ZB!We2x-{!Ki& zQqRBe!V4oo{K4;iXC^5djdM)=qwye~O`7xBjbp!(^1OgzF)xy8;w=L_>+eNB;P@a> z{@vgG-4K#%ckIG`J0VuoS_{`=Y;hYZr?%7<2Z(_lsd1t(lDHFKRcZq!`dAu|&$Z(R zKls6b&iB6ey`jDE3IYSixxT)+Ch*lJ=Zx__3!Q3Qo85`$zkMsNR^RX-)y?oXb^6>^ zjuSWo6HW{L2cE7`Cx~&-4s9|PzSCEIYCb&QRZ=%5o|9u-Pg{YY>#+}NKY(0qPYy%U zYp!)Bo^Z|-8|fnl5^)ld^36A1o3!+YV_tYTHouxVfT?i@{O&uOU)pAFxL=>C|0k*G z*o;?Q4Iyj1uDrhoQV`rmXX^6S3LE4_cZ~E?{@$1C-^j^(_5|=BO?{V?3HbPC-XsBb z;wM{7g6o)ku!m}Q* zVh{Z8cmHtcHqS+hJ`qZhP_A#M2-L*kW(|i|AiwnB!mvypO2Q>{;dak|Can(2~@u3}w_;61zG1V;CYK2@? zTVq%TQcfi~L1Lgs@wk(paNvu_j&N?v$uqQxwih=SGr@G9o#;WKLF$4z)h2N zC2zd2kMl;Cozzg-$fTT?@z=G`1$bReQm^pxR1p^g0MI}Xy90zpz8uV ziAN z{V}G<%|ad}gs7uJ)Je;8me77Vw!hj>>kL{kwy8R|g{@V+ZyWvg45h2D{m-$8;*(nb zW+c1J)0?6{Y?J}BN0NkKXjeSBLgd(~18$Rco^pf9FzMD7+pW%pg>~yc0>IalmOI1F z0|R`wR9xgrri`sWgZ8_2wyH|1Tybo4p;L#2u~|HOB4zQQHT#8$T4wvGnu5YN)y4n5 zGYP4NqBKZYqzQ|m*^vRn5B|^p-T9CI=|3#5A4xP5^>9i!3e$28*KBkLPTsoDx|egb zjj9MB8ya^$o@oF(LGxSAKy9i-NCMj(a@O`+;1+bHGXM$rYUisuIy9Hu8_14SxOrMz zUjzuDSGbP88!LU+pMrs7{jbe+5Don&_Z+H_j3l+mMe1>na`&{S;c_1hNjQ>u7I}+r zyAC^PL-T++sn)B2v!X>-G4>WOuf4Kf@AkWGHRhh<(MXs3(r(A8g68vP#@QE=wBtQ@ zJW7!<*CUw$R_&^-vG*_@&h-omo{i-3c@q))0q^wnW{A;+0ZUsnT^M@dV0_1uW&t`M)%kf`RrGv}X>#a0`2L zZRQE%^xnx+J1_tFpUzz2O`e$N%^~#gzC*Xn2-Oz)%*q#>IkxGI`XrOIem=Zq^hRXf z_wdT7GS3*bc_K9y|8P9R?7qNpTL3=agA-#7DnNrmeQ7N~3`>@^DTJoHeU8_2^rcDwFU z+t`qk^Q!tsN@!ApN;$qKQK`%D_OB9@o+@rL=^3wczKkC0{(B}9inQ`l)LYQ5Vewit zDzmXaS+EwF%p%VBodr$0)2=CCzNy7ipnE%p4GsvFR^c4)dQgvSQdb=;&KqxGUVyQM zl?tl*;QC@&TQmuPRGM^NDHm1mYzoH5xiZxNSX{Xy=@an0nzblokXp&}em&uO98RS3 zjO+?WIxLfT@7ym88W+LA*glx-*{o2R_vQRhY_^Qzp{0Hl%!oO?xO zkoJ@TG%M0xV$vibXCVxcJ%wO7*Hy*CsZvW0*0#jgBy=H<`E@!xvq z#%;b{gZ);gx|cm{2bHlR3qY!;>j_EQn+beme*(ZWnQth>oZm9(iWSAndMr|v1eD~& z>zU1$vLL0evT&4K#II`f1shSpuG}@z1h2gE%JBGr79QV`!$(Fkk))v503D8(H>&xOPrcQ8xUO7lF zpE(zqd$QxCA0jy_8O`RR$a;=NAQr~yNnL{O0 zm!hJN#>MrGXC*(+UFlEPaqhv?I^n%l&5do+@ek#1=LpL)9@mnQWu#v8l+akz+u#`4 zdB}#%=9hL1ua88ya`hRwv9V6{+FCE4)4%Zvq_~@N$P+z8N7bAuWI615E$Gru-Je^)j0 znMv8$RiFRj=dWcFKeF@e^T#J7Z1`pI94m=LnF8m8x#8rQbe36rwY3fzk#_ zeyZOqM%x(f44j#*JbiI+Z@_~k7O(|BIi$;@NPIPm*!?o~Y8N10pU z@r@HGqTXmeWB*=8aE_3#Uw0URmOl<|KKT0yqKMdvKWC~nsB!D`dOeUlv(HwSs&gaXy z9}d^SghI&xGg&<%#qi(>?mwFFDdAKA0hjF2M1vd(p_T+Bt-knD6390rVgBmPw?>UV z8i_{&Pa#%Q2`mduT6P9>P6p$PHnF6ZmCFxcQK<_Aq+9(4l-|_aQ5|=w$dzvDYyf6d zY~>aGw)jcDj=K;*0*sF*^T_gcuhdEUD)S8KnmtWlC!SxL- zPO^aX$7ezd_vKiiq1qboY78+cyPH%&w>DdSCH3kjaP-`dMwLZmuA01PH8+R;<2X{8 z&6EA@;e;5H1}{#&zRV5DFw_K={wtr!#KVlDAP{W=I0`$He1I*P6~;V3ZDMkHIO;Dl zv*(}~k_^yiLgo2Zw2Es1UI@3BBQE`P-XtK+oo}QcHi7Z-^5TxO##}kp?S``54AC zzkc!BtCK$HeBwRj$s5mG7SMPZlTpONW7Bhw+Vq63J@)v4k@oZh7-_d-`_h(1o-8n4 z(qpMAl}wvQuBOqL=G*k(a_!TU~jHlO8|nw7v?_a{PG2H%CeBX==)n z?z|cnSBbgF1)SR$V#oNt+(Gwkw1Pf0^Yy602)&W)NYF8I&GR&=AC1kBM2!!3Dx_lo z=Gl~T!@*RE+I?NOJjk}A$poBRuXZcgNpRX{a3GTB*o=cJLL6-@9B3kM#;$B918Dhm zA)3SSNP7wqJrY~tP*&1+9KEsUN-l7dIVNDr;WeRfQHulOZk6MMAuoRE=GP!H4?&4l`Hx`uzAdn==*2hz`I2_AX2PmG+E=}q=7rT}T z`&1$~*z-DBOv)w9NL|(zzny#fy`|2xQF@6a zDRJ1j4e6=`T9vaBXg32O<$8^I{zSHy^qHjB@{=#V_~O(#Zb72PI(LI0h1UUOoIE?9 zY0Dxh=N&K_4{iWjjTUV!O@;kWG6S_jw6B}U*!8ZhqVq#_2?x;iOL_76!9&abH&SHg z!th78v8?G}$m&zsEtz5tM+K${ad2$!+1zP0!ay(v#+iYvywqDS*Ius&BNYEPfwT|& z$W(Iaf}CgqlVx}?{>ExwS!yHfMHNwrY}us7HnMT_M)KGup~YEnF;UAkBi%U9!nR-* z^UV8}<7wZdz)G~03`>;OE&y7fZbq2$`Z%22<8S`vZ+8CrumAd1j=QFwr^a^! zXeux5Zb~gc*NihA%SvqeQ)fkiuA7tgdY69Unbbk#nw6Ss^Q<&nNxn8@_DX;zQfKDI zCf)Vg?0a*}Hb>p3bNJJr-2%zq1fo13_q;C~-@W>FFB{*kXZFu7aZlUe2=V1It!=Hc zS`$^NtFdgli^ab0M)-MlNuRLUjXg=m<;2MHmWk(`qA#mdV7Xsc@_(fgyERZV*@wmv z{#PQOO%I?xo%BgI=gEYU1tSNb z#bF^d5kd3N0V}U2JkSmxF{z@`=DsatF{q?1ziZf^s%~im4|KVK3M>^^wY3$|0E9On z-Oc6GrVqB5+De06=MY@y0GX&&n@1w$2IgUo}FS%%B{=_OCX&f<%GD zZzO6UsBdC;Z8U0#*pw>ylE$mH0z=m?@Y;-O+XQ?*FWsczh8>`d?ju`rO%3`vs(<>x zYH(wtN|_r2z)6708$BCVRAA>#yl>$jc zwWXQVK0le-xj<_2>SdA*$1N&gZGU)Phhlh_RhO!wZtGK_OFWwBjSo(r9RoRA5Dy=7 ztVkpa%QdRbH49AD>q-=)j}m+9EiGAEJ1s^)X)#&M+6Vxg4`gI=E_vAk-_7Bx1lY}| zWZ!ylO9`p8W$}AC4~Q6(MpL-Id;5Jn^?5CR(pbsNJ_l0nRZ_S%t}wnuGH(j6N~^}1 z6l9zWV6`V|N6iD;1z@ya+Z^vcrLI(ZD&3@JhPK6VBgMxCx3FVLu3n9}@qA_j&AWi! zg(k5 + +#include "hardware/gpio.h" +#include "pico/stdlib.h" + +#include "st7789.h" + +static struct st7789_config st7789_cfg; +static uint16_t st7789_width; +static uint16_t st7789_height; +static bool st7789_data_mode = false; + +void st7789_cmd(uint8_t cmd, const uint8_t *data, size_t len) { + if (st7789_cfg.gpio_cs > -1) { + spi_set_format(st7789_cfg.spi, 8, SPI_CPOL_0, SPI_CPHA_0, SPI_MSB_FIRST); + } else { + spi_set_format(st7789_cfg.spi, 8, SPI_CPOL_1, SPI_CPHA_1, SPI_MSB_FIRST); + } + st7789_data_mode = false; + + sleep_us(1); + if (st7789_cfg.gpio_cs > -1) { + gpio_put(st7789_cfg.gpio_cs, 0); + } + gpio_put(st7789_cfg.gpio_dc, 0); + sleep_us(1); + + spi_write_blocking(st7789_cfg.spi, &cmd, sizeof(cmd)); + + if (len) { + sleep_us(1); + gpio_put(st7789_cfg.gpio_dc, 1); + sleep_us(1); + + spi_write_blocking(st7789_cfg.spi, data, len); + } + + sleep_us(1); + if (st7789_cfg.gpio_cs > -1) { + gpio_put(st7789_cfg.gpio_cs, 1); + } + gpio_put(st7789_cfg.gpio_dc, 1); + sleep_us(1); +} + +void st7789_caset(uint16_t xs, uint16_t xe) { + uint8_t data[] = { + xs >> 8, + xs & 0xff, + xe >> 8, + xe & 0xff, + }; + + // CASET (2Ah): Column Address Set + st7789_cmd(0x2a, data, sizeof(data)); +} + +void st7789_raset(uint16_t ys, uint16_t ye) { + uint8_t data[] = { + ys >> 8, + ys & 0xff, + ye >> 8, + ye & 0xff, + }; + + // RASET (2Bh): Row Address Set + st7789_cmd(0x2b, data, sizeof(data)); +} + +void st7789_init(const struct st7789_config *config, uint16_t width, uint16_t height) { + memcpy(&st7789_cfg, config, sizeof(st7789_cfg)); + st7789_width = width; + st7789_height = height; + + spi_init(st7789_cfg.spi, 125 * 1000 * 1000); + if (st7789_cfg.gpio_cs > -1) { + spi_set_format(st7789_cfg.spi, 8, SPI_CPOL_0, SPI_CPHA_0, SPI_MSB_FIRST); + } else { + spi_set_format(st7789_cfg.spi, 8, SPI_CPOL_1, SPI_CPHA_1, SPI_MSB_FIRST); + } + + gpio_set_function(st7789_cfg.gpio_din, GPIO_FUNC_SPI); + gpio_set_function(st7789_cfg.gpio_clk, GPIO_FUNC_SPI); + + if (st7789_cfg.gpio_cs > -1) { + gpio_init(st7789_cfg.gpio_cs); + } + gpio_init(st7789_cfg.gpio_dc); + gpio_init(st7789_cfg.gpio_rst); + gpio_init(st7789_cfg.gpio_bl); + + if (st7789_cfg.gpio_cs > -1) { + gpio_set_dir(st7789_cfg.gpio_cs, GPIO_OUT); + } + gpio_set_dir(st7789_cfg.gpio_dc, GPIO_OUT); + gpio_set_dir(st7789_cfg.gpio_rst, GPIO_OUT); + gpio_set_dir(st7789_cfg.gpio_bl, GPIO_OUT); + + if (st7789_cfg.gpio_cs > -1) { + gpio_put(st7789_cfg.gpio_cs, 1); + } + gpio_put(st7789_cfg.gpio_dc, 1); + gpio_put(st7789_cfg.gpio_rst, 1); + sleep_ms(100); + + // SWRESET (01h): Software Reset + st7789_cmd(0x01, NULL, 0); + sleep_ms(150); + + // SLPOUT (11h): Sleep Out + st7789_cmd(0x11, NULL, 0); + sleep_ms(50); + + // COLMOD (3Ah): Interface Pixel Format + // - RGB interface color format = 65K of RGB interface + // - Control interface color format = 16bit/pixel + st7789_cmd(0x3a, (uint8_t[]){0x55}, 1); + sleep_ms(10); + + // MADCTL (36h): Memory Data Access Control + // - Page Address Order = Top to Bottom + // - Column Address Order = Left to Right + // - Page/Column Order = Normal Mode + // - Line Address Order = LCD Refresh Top to Bottom + // - RGB/BGR Order = RGB + // - Display Data Latch Data Order = LCD Refresh Left to Right + st7789_cmd(0x36, (uint8_t[]){0xA0}, 1); + + st7789_caset(0, width); + st7789_raset(0, height); + + // INVON (21h): Display Inversion On + st7789_cmd(0x21, NULL, 0); + sleep_ms(10); + + // NORON (13h): Normal Display Mode On + st7789_cmd(0x13, NULL, 0); + sleep_ms(10); + + // DISPON (29h): Display On + st7789_cmd(0x29, NULL, 0); + sleep_ms(10); + + st7789_fill(0x0000); + sleep_ms(50); + gpio_put(st7789_cfg.gpio_bl, 1); +} + +void st7789_ramwr() { + sleep_us(1); + if (st7789_cfg.gpio_cs > -1) { + gpio_put(st7789_cfg.gpio_cs, 0); + } + gpio_put(st7789_cfg.gpio_dc, 0); + sleep_us(1); + + // RAMWR (2Ch): Memory Write + uint8_t cmd = 0x2c; + spi_write_blocking(st7789_cfg.spi, &cmd, sizeof(cmd)); + + sleep_us(1); + if (st7789_cfg.gpio_cs > -1) { + gpio_put(st7789_cfg.gpio_cs, 0); + } + gpio_put(st7789_cfg.gpio_dc, 1); + sleep_us(1); +} + +void st7789_write(const void *data, size_t len) { + if (!st7789_data_mode) { + st7789_ramwr(); + + if (st7789_cfg.gpio_cs > -1) { + spi_set_format(st7789_cfg.spi, 16, SPI_CPOL_0, SPI_CPHA_0, SPI_MSB_FIRST); + } else { + spi_set_format(st7789_cfg.spi, 16, SPI_CPOL_1, SPI_CPHA_1, SPI_MSB_FIRST); + } + + st7789_data_mode = true; + } + + spi_write16_blocking(st7789_cfg.spi, data, len / 2); +} + +void st7789_put(uint16_t pixel) { st7789_write(&pixel, sizeof(pixel)); } + +void st7789_fill(uint16_t pixel) { + int num_pixels = st7789_width * st7789_height; + + st7789_set_cursor(0, 0); + + for (int i = 0; i < num_pixels; i++) { + st7789_put(pixel); + } +} + +void st7789_set_cursor(uint16_t x, uint16_t y) { + st7789_caset(x, st7789_width); + st7789_raset(y, st7789_height); +} + +void st7789_vertical_scroll(uint16_t row) { + uint8_t data[] = {(row >> 8) & 0xff, row & 0x00ff}; + + // VSCSAD (37h): Vertical Scroll Start Address of RAM + st7789_cmd(0x37, data, sizeof(data)); +} diff --git a/st7789.h b/st7789.h new file mode 100644 index 0000000..06ffad0 --- /dev/null +++ b/st7789.h @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2021 Arm Limited and Contributors. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * + */ + +#ifndef _PICO_ST7789_H_ +#define _PICO_ST7789_H_ + +#include "hardware/spi.h" + +struct st7789_config { + spi_inst_t *spi; + uint gpio_din; + uint gpio_clk; + int gpio_cs; + uint gpio_dc; + uint gpio_rst; + uint gpio_bl; +}; + +void st7789_cmd(uint8_t cmd, const uint8_t *data, size_t len); +void st7789_init(const struct st7789_config *config, uint16_t width, uint16_t height); +void st7789_write(const void *data, size_t len); +void st7789_put(uint16_t pixel); +void st7789_fill(uint16_t pixel); +void st7789_caset(uint16_t xs, uint16_t xe); +void st7789_raset(uint16_t ys, uint16_t ye); +void st7789_set_cursor(uint16_t x, uint16_t y); +void st7789_vertical_scroll(uint16_t row); + +#endif diff --git a/utf8.c b/utf8.c new file mode 100644 index 0000000..255ed6d --- /dev/null +++ b/utf8.c @@ -0,0 +1,59 @@ +#include "utf8.h" + +// This macro tests if a char is a continuation byte in utf8. +#define IS_CONT(x) (((x) & 0xc0) == 0x80) + +// This returns the code point encoded at **s and advances *s to point to the +// next character. Thus it can easily be used in a loop. +int decode_code_point(const char **s) { + int k = **s ? __builtin_clz(~(**s << 24)) : 0; // Count # of leading 1 bits. + int mask = (1 << (8 - k)) - 1; // All 1s with k leading 0s. + int value = **s & mask; + // k = 0 for one-byte code points; otherwise, k = #total bytes. + for (++(*s), --k; k > 0 && IS_CONT(**s); --k, ++(*s)) { + value <<= 6; + value += (**s & 0x3F); + } + return value; +} + +// This assumes that `code` is <= 0x10FFFF and ensures that nothing will be +// written at or beyond `end`. It advances *s so it's easy to use in a loop. +void encode_code_point(char **s, char *end, int code) { + char val[4]; + int lead_byte_max = 0x7F; + int val_index = 0; + while (code > lead_byte_max) { + val[val_index++] = (code & 0x3F) | 0x80; + code >>= 6; + lead_byte_max >>= (val_index == 1 ? 2 : 1); + } + val[val_index++] = (code & lead_byte_max) | (~lead_byte_max << 1); + while (val_index-- && *s < end) { + **s = val[val_index]; + (*s)++; + } +} + +// This returns 0 if no split was needed. +int split_into_surrogates(int code, int *surr1, int *surr2) { + if (code <= 0xFFFF) + return 0; + *surr2 = 0xDC00 | (code & 0x3FF); // Save the low 10 bits. + code >>= 10; // Drop the low 10 bits. + // If `code` now has low bits "uuu uuxx xxxx", then the bits of *surr are + // "1101 10ww wwxx xxxx" where wwww = (uuuuu - 1). + *surr1 = 0xD800 | ((code & 0x7FF) - 0x40); + return 1; +} + +// This expects to be used in a loop and see all code points in *code. Start +// *old at 0; this function updates *old for you - don't change it after +// initialization. This returns 0 when *code is the 1st of a surrogate pair; +// otherwise use *code as the final code point. +int join_from_surrogates(int *old, int *code) { + if (*old) + *code = (((*old & 0x3FF) + 0x40) << 10) + (*code & 0x3FF); + *old = ((*code & 0xD800) == 0xD800 ? *code : 0); + return !(*old); +} diff --git a/utf8.h b/utf8.h new file mode 100644 index 0000000..2958666 --- /dev/null +++ b/utf8.h @@ -0,0 +1,9 @@ +#ifndef VELOMETER_UTF8_H +#define VELOMETER_UTF8_H + +int decode_code_point(const char **s); +void encode_code_point(char **s, char *end, int code); +int split_into_surrogates(int code, int *surr1, int *surr2); +int join_from_surrogates(int *old, int *code); + +#endif