diff --git a/tft_gui/TFT_io.py b/tft_gui/TFT_io.py new file mode 100644 index 0000000..822eb7c --- /dev/null +++ b/tft_gui/TFT_io.py @@ -0,0 +1,602 @@ +# +# The MIT License (MIT) +# +# Copyright (c) 2016 Robert Hammelrath +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# Class supporting TFT LC-displays with a parallel Interface +# First example: Controller SSD1963 +# It uses X1..X8 for data and Y3, Y9, Y10, Y11 and Y12 for control signals. +# The minimal connection just for writes is X1..X8 for data, Y9 for /Reset. Y11 for /WR and Y12 for /RS +# Then LED and /CS must be hard tied to Vcc and GND, and /RD is not used. +# +# Some parts of the software are a port of code provided by Rinky-Dink Electronics, Henning Karlsen, +# with the following copyright notice: +## Copyright (C)2015 Rinky-Dink Electronics, Henning Karlsen. All right reserved +## This library is free software; you can redistribute it and/or +## modify it under the terms of the CC BY-NC-SA 3.0 license. +## Please see the included documents for further information. +# + +import pyb, stm +from uctypes import addressof + +# define constants +# +RESET = const(1 << 10) ## Y9 +RD = const(1 << 11) ## Y10 +WR = const(0x01) ## Y11 +D_C = const(0x02) ## Y12 + +LED = const(1 << 8) ## Y3 +POWER = const(1 << 9) ## Y4 + +## CS is not used and must be hard tied to GND + +PORTRAIT = const(1) +LANDSCAPE = const(0) + +class TFT_io: +# +# display bitmap +# + @staticmethod + @micropython.viper + def displaySCR_bitmap(bits: ptr8, size: int, control: ptr8, bg_buf: ptr8): + gpioa = ptr8(stm.GPIOA) + gpiob = ptr16(stm.GPIOB + stm.GPIO_BSRRL) + gpioam = ptr16(stm.GPIOA + stm.GPIO_MODER) +# + transparency = control[6] + bm_ptr = 0 + bg_ptr = 0 + mask = 0x80 +# rd_command = 0x2e ## start read + while size: +# if False: # transparency: # read back data +# gpioa[stm.GPIO_ODR] = rd_command # start/continue read command +# gpiob[1] = D_C | WR # set C/D and WR low +# gpiob[0] = D_C | WR # set C/D and WR high + +# gpioam[0] = 0 # configure X1..X8 as Input + +# gpiob[1] = RD # set RD low. C/D still high +# rd_command = 0x3e # continue read +# bg_red = gpioa[stm.GPIO_IDR] # get data from port A +# gpiob[0] = RD # set RD high again + +# gpiob[1] = RD # set RD low. C/D still high +# delay = 1 +# bg_green = gpioa[stm.GPIO_IDR] # get data from port A +# gpiob[0] = RD # set RD high again + +# gpiob[1] = RD # set RD low. C/D still high +# delay = 1 +# bg_blue = gpioa[stm.GPIO_IDR] # get data from port A +# gpiob[0] = RD # set RD high again + +# gpioam[0] = 0x5555 # configure X1..X8 as Output + +# gpioa[stm.GPIO_ODR] = 0x3c # continue write command +# gpiob[1] = D_C | WR # set C/D and WR low +# gpiob[0] = D_C | WR # set C/D and WR high + + if bits[bm_ptr] & mask: + if transparency & 8: # Invert bg color as foreground + gpioa[stm.GPIO_ODR] = 255 - bg_buf[bg_ptr] # set data on port A + gpiob[1] = WR # set WR low. C/D still high + gpiob[0] = WR # set WR high again + + gpioa[stm.GPIO_ODR] = 255 - bg_buf[bg_ptr + 1] # set data on port A + gpiob[1] = WR # set WR low. C/D still high + gpiob[0] = WR # set WR high again + + gpioa[stm.GPIO_ODR] = 255 - bg_buf[bg_ptr + 2] # set data on port A + gpiob[1] = WR # set WR low. C/D still high + gpiob[0] = WR # set WR high again + else: # not invert + gpioa[stm.GPIO_ODR] = control[3] # set data on port A + gpiob[1] = WR # set WR low. C/D still high + gpiob[0] = WR # set WR high again + + gpioa[stm.GPIO_ODR] = control[4] # set data on port A + gpiob[1] = WR # set WR low. C/D still high + gpiob[0] = WR # set WR high again + + gpioa[stm.GPIO_ODR] = control[5] # set data on port A + gpiob[1] = WR # set WR low. C/D still high + gpiob[0] = WR # set WR high again + else: + if transparency & 1: # Dim background + gpioa[stm.GPIO_ODR] = bg_buf[bg_ptr] >> 1 # set data on port A + gpiob[1] = WR # set WR low. C/D still high + gpiob[0] = WR # set WR high again + + gpioa[stm.GPIO_ODR] = bg_buf[bg_ptr + 1] >> 1 # set data on port A + gpiob[1] = WR # set WR low. C/D still high + gpiob[0] = WR # set WR high again + + gpioa[stm.GPIO_ODR] = bg_buf[bg_ptr + 2] >> 1 # set data on port A + gpiob[1] = WR # set WR low. C/D still high + gpiob[0] = WR # set WR high again + elif transparency & 2: # keep Background + gpioa[stm.GPIO_ODR] = bg_buf[bg_ptr] # set data on port A + gpiob[1] = WR # set WR low. C/D still high + gpiob[0] = WR # set WR high again + + gpioa[stm.GPIO_ODR] = bg_buf[bg_ptr + 1] # set data on port A + gpiob[1] = WR # set WR low. C/D still high + gpiob[0] = WR # set WR high again + + gpioa[stm.GPIO_ODR] = bg_buf[bg_ptr + 2] # set data on port A + gpiob[1] = WR # set WR low. C/D still high + gpiob[0] = WR # set WR high again + elif transparency & 4: # invert Background + gpioa[stm.GPIO_ODR] = 255 - bg_buf[bg_ptr] # set data on port A + gpiob[1] = WR # set WR low. C/D still high + gpiob[0] = WR # set WR high again + + gpioa[stm.GPIO_ODR] = 255 - bg_buf[bg_ptr + 1] # set data on port A + gpiob[1] = WR # set WR low. C/D still high + gpiob[0] = WR # set WR high again + + gpioa[stm.GPIO_ODR] = 255 - bg_buf[bg_ptr + 2] # set data on port A + gpiob[1] = WR # set WR low. C/D still high + gpiob[0] = WR # set WR high again + else: # not transparent + gpioa[stm.GPIO_ODR] = control[0] # set data on port A + gpiob[1] = WR # set WR low. C/D still high + gpiob[0] = WR # set WR high again + + gpioa[stm.GPIO_ODR] = control[1] # set data on port A + gpiob[1] = WR # set WR low. C/D still high + gpiob[0] = WR # set WR high again + + gpioa[stm.GPIO_ODR] = control[2] # set data on port A + gpiob[1] = WR # set WR low. C/D still high + gpiob[0] = WR # set WR high again + mask >>= 1 + if mask == 0: # map ptr advance on byte exhaust + mask = 0x80 + bm_ptr += 1 + size -= 1 + bg_ptr += 3 +# +# Set the address range for various draw commands and set the TFT for expecting data +# + @staticmethod + @micropython.viper + def setXY_P(x1: int, y1: int, x2: int, y2: int): ## set the adress range, Portrait +# set column address + gpioa = ptr8(stm.GPIOA + stm.GPIO_ODR) + gpiob = ptr16(stm.GPIOB + stm.GPIO_BSRRL) + gpioa[0] = 0x2b # command + gpiob[1] = D_C | WR # set C/D and WR low + gpiob[0] = D_C | WR # set C/D and WR high + + gpioa[0] = x1 >> 8 # high byte of x1 + gpiob[1] = WR # set WR low. C/D still high + gpiob[0] = WR # set WR high again + + gpioa[0] = x1 & 0xff# low byte of x1 + gpiob[1] = WR # set WR low. C/D still high + gpiob[0] = WR # set WR high again + + gpioa[0] = x2 >> 8 # high byte of x2 + gpiob[1] = WR # set WR low. C/D still high + gpiob[0] = WR # set WR high again + + gpioa[0] = x2 & 0xff# low byte of x2 + gpiob[1] = WR # set WR low. C/D still high + gpiob[0] = WR # set WR high again +# set row address + gpioa[0] = 0x2a # command + gpiob[1] = D_C | WR # set C/D and WR low + gpiob[0] = D_C | WR # set C/D and WR high + + gpioa[0] = y1 >> 8 # high byte of x1 + gpiob[1] = WR # set WR low. C/D still high + gpiob[0] = WR # set WR high again + + gpioa[0] = y1 & 0xff# low byte of x1 + gpiob[1] = WR # set WR low. C/D still high + gpiob[0] = WR # set WR high again + + gpioa[0] = y2 >> 8 # high byte of x2 + gpiob[1] = WR # set WR low. C/D still high + gpiob[0] = WR # set WR high again + + gpioa[0] = y2 & 0xff# low byte of x2 + gpiob[1] = WR # set WR low. C/D still high + gpiob[0] = WR # set WR high again + + gpioa[0] = 0x2c # Start data entry + gpiob[1] = D_C | WR # set C/D and WR low + gpiob[0] = D_C | WR # set C/D and WR high + + @staticmethod + @micropython.viper + def setXY_L(x1: int, y1: int, x2: int, y2: int): ## set the adress range, Landscape +# set column address + gpioa = ptr8(stm.GPIOA + stm.GPIO_ODR) + gpiob = ptr16(stm.GPIOB + stm.GPIO_BSRRL) + gpioa[0] = 0x2a # command + gpiob[1] = D_C | WR # set C/D and WR low + gpiob[0] = D_C | WR # set C/D and WR high + + gpioa[0] = x1 >> 8 # high byte of x1 + gpiob[1] = WR # set WR low. C/D still high + gpiob[0] = WR # set WR high again + + gpioa[0] = x1 & 0xff# low byte of x1 + gpiob[1] = WR # set WR low. C/D still high + gpiob[0] = WR # set WR high again + + gpioa[0] = x2 >> 8 # high byte of x2 + gpiob[1] = WR # set WR low. C/D still high + gpiob[0] = WR # set WR high again + + gpioa[0] = x2 & 0xff# low byte of x2 + gpiob[1] = WR # set WR low. C/D still high + gpiob[0] = WR # set WR high again +# set row address + gpioa[0] = 0x2b # command + gpiob[1] = D_C | WR # set C/D and WR low + gpiob[0] = D_C | WR # set C/D and WR high + + gpioa[0] = y1 >> 8 # high byte of x1 + gpiob[1] = WR # set WR low. C/D still high + gpiob[0] = WR # set WR high again + + gpioa[0] = y1 & 0xff# low byte of x1 + gpiob[1] = WR # set WR low. C/D still high + gpiob[0] = WR # set WR high again + + gpioa[0] = y2 >> 8 # high byte of x2 + gpiob[1] = WR # set WR low. C/D still high + gpiob[0] = WR # set WR high again + + gpioa[0] = y2 & 0xff# low byte of x2 + gpiob[1] = WR # set WR low. C/D still high + gpiob[0] = WR # set WR high again + + gpioa[0] = 0x2c # Start data entry + gpiob[1] = D_C | WR # set C/D and WR low + gpiob[0] = D_C | WR # set C/D and WR high +# +# Assembler version of +# Fill screen by writing size pixels with the color given in data +# data must be 3 bytes of red, green, blue +# The area to be filled has to be set in advance by setXY +# The speed is about 214 ns/pixel +# + @staticmethod + @micropython.asm_thumb + def fillSCR_AS(r0, r1): # r0: ptr to data, r1: number of pixels (3 bytes/pixel) +# set up pointers to GPIO +# r5: bit mask for control lines +# r6: GPIOA OODR register ptr +# r7: GPIOB BSSRL register ptr + mov(r5, WR) + movwt(r6, stm.GPIOA) # target + add (r6, stm.GPIO_ODR) + movwt(r7, stm.GPIOB) + add (r7, stm.GPIO_BSRRL) + ldrb(r2, [r0, 0]) # red + ldrb(r3, [r0, 1]) # green + ldrb(r4, [r0, 2]) # blue + b(loopend) + + label(loopstart) + strb(r2, [r6, 0]) # Store red + strb(r5, [r7, 2]) # WR low +# nop() + strb(r5, [r7, 0]) # WR high + + strb(r3, [r6, 0]) # store blue + strb(r5, [r7, 2]) # WR low + nop() + strb(r5, [r7, 0]) # WR high + + strb(r4, [r6, 0]) # store blue + strb(r5, [r7, 2]) # WR low +# nop() + strb(r5, [r7, 0]) # WR high + + label(loopend) + sub (r1, 1) # End of loop? + bpl(loopstart) +# +# Assembler version of: +# Fill screen by writing size pixels with the data +# data must contains size triplets of red, green and blue data values +# The area to be filled has to be set in advance by setXY +# the speed is 266 ns for a byte triple +# + @staticmethod + @micropython.asm_thumb + def displaySCR_AS(r0, r1): # r0: ptr to data, r1: is number of pixels (3 bytes/pixel) +# set up pointers to GPIO +# r5: bit mask for control lines +# r6: GPIOA OODR register ptr +# r7: GPIOB BSSRL register ptr + mov(r5, WR) + movwt(r6, stm.GPIOA) # target + add (r6, stm.GPIO_ODR) + movwt(r7, stm.GPIOB) + add (r7, stm.GPIO_BSRRL) + b(loopend) + + label(loopstart) + ldrb(r2, [r0, 0]) # red + strb(r2, [r6, 0]) # Store red + strb(r5, [r7, 2]) # WR low + strb(r5, [r7, 0]) # WR high + + ldrb(r2, [r0, 1]) # pre green + strb(r2, [r6, 0]) # store greem + strb(r5, [r7, 2]) # WR low + strb(r5, [r7, 0]) # WR high + + ldrb(r2, [r0, 2]) # blue + strb(r2, [r6, 0]) # store blue + strb(r5, [r7, 2]) # WR low + strb(r5, [r7, 0]) # WR high + + add (r0, 3) # advance data ptr + + label(loopend) + sub (r1, 1) # End of loop? + bpl(loopstart) +# Assembler version of: +# Fill screen by writing size pixels with the data +# data must contains size packed duplets of red, green and blue data values +# The area to be filled has to be set in advance by setXY +# the speed is 266 ns for a byte pixel +# + @staticmethod + @micropython.asm_thumb + def displaySCR565_AS(r0, r1): # r0: ptr to data, r1: is number of pixels (3 bytes/pixel) +# set up pointers to GPIO +# r5: bit mask for control lines +# r6: GPIOA OODR register ptr +# r7: GPIOB BSSRL register ptr + mov(r5, WR) + movwt(r6, stm.GPIOA) # target + add (r6, stm.GPIO_ODR) + movwt(r7, stm.GPIOB) + add (r7, stm.GPIO_BSRRL) + b(loopend) + + label(loopstart) + + ldrb(r2, [r0, 0]) # red + mov (r3, 0xf8) # mask out lower 3 bits + and_(r2, r3) + strb(r2, [r6, 0]) # Store red + strb(r5, [r7, 2]) # WR low + strb(r5, [r7, 0]) # WR high + + ldrb(r2, [r0, 0]) # pre green + mov (r3, 5) # shift 5 bits up to + lsl(r2, r3) + ldrb(r4, [r0, 1]) # get the next 3 bits + mov (r3, 3) # shift 3 to the right + lsr(r4, r3) + orr(r2, r4) # add them to the first bits + mov(r3, 0xfc) # mask off the lower two bits + and_(r2, r3) + strb(r2, [r6, 0]) # store green + strb(r5, [r7, 2]) # WR low + strb(r5, [r7, 0]) # WR high + + ldrb(r2, [r0, 1]) # blue + mov (r3, 3) + lsl(r2, r3) + strb(r2, [r6, 0]) # store blue + strb(r5, [r7, 2]) # WR low + strb(r5, [r7, 0]) # WR high + + add (r0, 2) # advance data ptr + + label(loopend) + + sub (r1, 1) # End of loop? + bpl(loopstart) +# +# Send a command and data to the TFT controller +# cmd is the command byte, data must be a bytearray object with the command payload, +# int is the size of the data +# For the startup-phase use this function. +# + @staticmethod + @micropython.viper + def tft_cmd_data(cmd: int, data: ptr8, size: int): + gpioa = ptr8(stm.GPIOA + stm.GPIO_ODR) + gpiob = ptr16(stm.GPIOB + stm.GPIO_BSRRL) + gpioa[0] = cmd # set data on port A + gpiob[1] = D_C | WR # set C/D and WR low + gpiob[0] = D_C | WR # set C/D and WR high + for i in range(size): + gpioa[0] = data[i] # set data on port A + gpiob[1] = WR # set WR low. C/D still high + gpiob[0] = WR # set WR high again +# +# Assembler version of send command & data to the TFT controller +# data must be a bytearray object, int is the size of the data. +# The speed is about 120 ns/byte +# + @staticmethod + @micropython.asm_thumb + def tft_cmd_data_AS(r0, r1, r2): # r0: command, r1: ptr to data, r2 is size in bytes +# set up pointers to GPIO +# r5: bit mask for control lines +# r6: GPIOA OODR register ptr +# r7: GPIOB BSSRL register ptr + movwt(r6, stm.GPIOA) # target + add (r6, stm.GPIO_ODR) + movwt(r7, stm.GPIOB) + add (r7, stm.GPIO_BSRRL) +# Emit command byte + mov(r5, WR | D_C) + strb(r0, [r6, 0]) # set command byte + strh(r5, [r7, 2]) # WR and D_C low + strh(r5, [r7, 0]) # WR and D_C high +# now loop though data + mov(r5, WR) + b(loopend) + + label(loopstart) + ldrb(r4, [r1, 0]) # load data + strb(r4, [r6, 0]) # Store data + strh(r5, [r7, 2]) # WR low + strh(r5, [r7, 0]) # WR high + add (r1, 1) # advance data ptr + + label(loopend) + sub (r2, 1) # End of loop? + bpl(loopstart) +# +# Send a command to the TFT controller +# + @staticmethod + @micropython.viper + def tft_cmd(cmd: int): + gpioa = ptr8(stm.GPIOA + stm.GPIO_ODR) + gpiob = ptr16(stm.GPIOB + stm.GPIO_BSRRL) + gpioa[0] = cmd # set data on port A + gpiob[1] = D_C | WR # set C/D and WR low + gpiob[0] = D_C | WR # set C/D and WR high +# +# Assembler version of send data to the TFT controller +# data must be a bytearray object, int is the size of the data. +# The speed is about 120 ns/byte +# + @staticmethod + @micropython.asm_thumb + def tft_write_data_AS(r0, r1): # r0: ptr to data, r1: is size in Bytes +# set up pointers to GPIO +# r5: bit mask for control lines +# r6: GPIOA OODR register ptr +# r7: GPIOB BSSRL register ptr + movwt(r6, stm.GPIOA) # target + add (r6, stm.GPIO_ODR) + movwt(r7, stm.GPIOB) + add (r7, stm.GPIO_BSRRL) + mov(r5, WR) +# and go, first test size for 0 + b(loopend) + + label(loopstart) + ldrb(r3, [r0, 0]) # load data + strb(r3, [r6, 0]) # Store data + strh(r5, [r7, 2]) # WR low + strh(r5, [r7, 0]) # WR high + + add (r0, 1) # advance data ptr + label(loopend) + sub (r1, 1) # End of loop? + bpl(loopstart) +# +# Assembler version of send a command byte and read data from to the TFT controller +# data must be a bytearray object, int is the size of the data. +# The speed is about 130 ns/byte +# + @staticmethod + @micropython.asm_thumb + def tft_read_cmd_data_AS(r0, r1, r2): +# r0: command, r1: ptr to data buffer, r2 is expected size in bytes +# set up pointers to GPIO +# r5: bit mask for control lines +# r6: GPIOA base register ptr +# r7: GPIOB BSSRL register ptr + movwt(r6, stm.GPIOA) # target + movwt(r7, stm.GPIOB) + add (r7, stm.GPIO_BSRRL) +# Emit command byte + movw(r5, WR | D_C) + strb(r0, [r6, stm.GPIO_ODR]) # set command byte + strh(r5, [r7, 2]) # WR and D_C low + strh(r5, [r7, 0]) # WR and D_C high +# now switch gpioaa to input + movw(r0, 0) + strh(r0, [r6, stm.GPIO_MODER]) +# now loop though data + movw(r5, RD) + b(loopend) + + label(loopstart) + strh(r5, [r7, 2]) # RD low + nop() # short delay + nop() + ldrb(r4, [r6, stm.GPIO_IDR]) # load data + strh(r5, [r7, 0]) # RD high + strb(r4, [r1, 0]) # Store data + add (r1, 1) # advance data ptr + + label(loopend) + sub (r2, 1) # End of loop? + bpl(loopstart) +# now switch gpioaa back to input + movw(r0, 0x5555) + strh(r0, [r6, stm.GPIO_MODER]) +# +# swap byte pairs in a buffer +# sometimes needed for picture data +# + @staticmethod + @micropython.asm_thumb + def swapbytes(r0, r1): # bytearray, len(bytearray) + mov(r2, 1) # divide loop count by 2 + lsr(r1, r2) # to avoid odd valued counter + b(loopend) + + label(loopstart) + ldrb(r2, [r0, 0]) + ldrb(r3, [r0, 1]) + strb(r3, [r0, 0]) + strb(r2, [r0, 1]) + add(r0, 2) + + label(loopend) + sub (r1, 1) # End of loop? + bpl(loopstart) + +# +# swap colors red/blue in the buffer +# + @staticmethod + @micropython.asm_thumb + def swapcolors(r0, r1): # bytearray, len(bytearray) + mov(r2, 3) + udiv(r1, r1, r2) # 3 bytes per triple + b(loopend) + + label(loopstart) + ldrb(r2, [r0, 0]) + ldrb(r3, [r0, 2]) + strb(r3, [r0, 0]) + strb(r2, [r0, 2]) + add(r0, 3) + + label(loopend) + sub (r1, 1) # End of loop? + bpl(loopstart) + diff --git a/tft_gui/asynctouch.py b/tft_gui/asynctouch.py new file mode 100644 index 0000000..ef590cf --- /dev/null +++ b/tft_gui/asynctouch.py @@ -0,0 +1,245 @@ +# asynctouch.py Asynchronous version of XPT2046 touchscreen controller. + +# The MIT License (MIT) +# +# Copyright (c) 2016 Robert Hammelrath, Peter Hinch +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# Class supporting the resisitve touchpad of TFT LC-displays +# First example: Controller XPT2046 +# It uses Y5..Y8 of PyBoard +# +import pyb, stm +# define constants +# +PCB_VERSION = 2 + +#if PCB_VERSION == 1: +# CONTROL_PORT = stm.GPIOB +# T_CLOCK = const(1 << 15) ## Y8 = B15 +# T_DOUT = const(1 << 14) ## Y7 = B14 +# T_DIN = const(1 << 13) ## Y6 = B13 +# T_IRQ = const(1 << 12) ## Y5 = B12 + +if PCB_VERSION == 2: + CONTROL_PORT = stm.GPIOC + T_CLOCK = const(1 << 5) ## X12 = C5 + T_DOUT = const(1 << 4) ## X11 = C4 + T_DIN = const(1 << 7) ## Y2 = C7 + T_IRQ = const(1 << 6) ## Y1 = C6 + +# T_CS is not used and must be hard tied to GND + +T_GETX = const(0xd0) ## 12 bit resolution +T_GETY = const(0x90) ## 12 bit resolution +T_GETZ1 = const(0xb8) ## 8 bit resolution +T_GETZ2 = const(0xc8) ## 8 bit resolution +# +X_LOW = const(10) ## lowest reasonable X value from the touchpad +Y_HIGH = const(4090) ## highest reasonable Y value + +class TOUCH: +# +# Init just sets the PIN's to In / out as required +# + def __init__(self, objsched, controller = "XPT2046", calibration = None, raw = False): + self.raw = raw + if PCB_VERSION == 1: + self.pin_clock = pyb.Pin("Y8", pyb.Pin.OUT_PP) + self.pin_clock.value(0) + self.pin_d_out = pyb.Pin("Y7", pyb.Pin.OUT_PP) + self.pin_d_in = pyb.Pin("Y6", pyb.Pin.IN) + self.pin_irq = pyb.Pin("Y5", pyb.Pin.IN) + else: + self.pin_clock = pyb.Pin("X11", pyb.Pin.OUT_PP) + self.pin_clock.value(0) + self.pin_d_out = pyb.Pin("X12", pyb.Pin.OUT_PP) + self.pin_d_in = pyb.Pin("Y1", pyb.Pin.IN) + self.pin_irq = pyb.Pin("Y2", pyb.Pin.IN) +# set default values + self.margin = 50 * 50 # tolerance margin: standard deviation of distance of touch from mean; store square + self.buff = [[-self.margin, -self.margin] for x in range(5)] # confidence == 5. Impossible coords + if calibration: + self.calibration = calibration + else: # default values for my tft + self.calibration = (-3917,-0.127,-3923,-0.1267,-3799,-0.07572,-3738,-0.07814) + self.position = [0, 0] + self.ready = False + self.touched = False + objsched.add_thread(self.main_thread()) + +# +# Now I'm up to get my touches +# +# +# set parameters for get_touch() +# confidence: confidence level - number of consecutive touches with a margin smaller than the given level +# which the function will sample until it accepts it as a valid touch +# margin: Difference from mean centre at which touches are considered at the same position +# + def touch_parameter(self, confidence = 5, margin = 50, calibration = None): + confidence = max(min(confidence, 25), 5) + margin = max(min(margin, 100), 1) + m = margin * margin + if confidence != len(self.buff): + self.buff = [[-m, -m] for x in range(confidence)] # impossible values + self.margin = margin * margin # store the square value + if calibration: + self.calibration = calibration +# +# get_touch(): get a touch value; Parameters: +# +# initital: Wait for a non-touch state before getting a sample. +# True = Initial wait for a non-touch state +# False = Do not wait for a release +# wait: Wait for a touch or not? +# False: Do not wait for a touch and return immediately +# True: Wait until a touch is pressed. +# raw: Setting whether raw touch coordinates (True) or normalized ones (False) are returned +# setting the calibration vector to (0, 1, 0, 1, 0, 1, 0, 1) result in a identity mapping + +# updates self.position + def main_thread(self): + buff = self.buff + buf_length = len(buff) + buffptr = 0 + nsamples = 0 + yield # Initialisation complete, wait for scheduler to start + while True: + if nsamples == buf_length: + meanx = sum([c[0] for c in buff]) // buf_length + meany = sum([c[1] for c in buff]) // buf_length + dev = sum([(c[0] - meanx)**2 + (c[1] - meany)**2 for c in buff]) / buf_length + if dev <= self.margin: # got one; compare against the square value + self.ready = True + if self.raw: + self.position[0] = meanx + self.position[1] = meany + else: + self.do_normalize((meanx, meany)) + sample = self.raw_touch() # get a touch + if sample == None: + self.touched = False + self.ready = False + nsamples = 0 # Invalidate buff + else: + self.touched = True + buff[buffptr] = sample # put in buff + buffptr = (buffptr + 1) % buf_length + nsamples = min(nsamples +1, buf_length) + yield + + def get_touch(self): + if self.ready: + self.ready = False + return self.position + return None +# +# do_normalize(touch) +# calculate the screen coordinates from the touch values, using the calibration values +# touch must be the tuple return by get_touch +# + def do_normalize(self, touch): + xmul = self.calibration[3] + (self.calibration[1] - self.calibration[3]) * (touch[1] / 4096) + xadd = self.calibration[2] + (self.calibration[0] - self.calibration[2]) * (touch[1] / 4096) + ymul = self.calibration[7] + (self.calibration[5] - self.calibration[7]) * (touch[0] / 4096) + yadd = self.calibration[6] + (self.calibration[4] - self.calibration[6]) * (touch[0] / 4096) + self.position[0] = int((touch[0] + xadd) * xmul) + self.position[1] = int((touch[1] + yadd) * ymul) + +# +# raw_touch(tuple) +# raw read touch. Returns (x,y) or None +# + def raw_touch(self): + global CONTROL_PORT + x = self.touch_talk(T_GETX, 12, CONTROL_PORT) + y = self.touch_talk(T_GETY, 12, CONTROL_PORT) + if x > X_LOW and y < Y_HIGH: # touch pressed? + return (x, y) + else: + return None +# +# Send a command to the touch controller and wait for the response +# cmd is the command byte +# int is the expected size of return data bits +# port is the gpio base port +# +# Straight down coding of the data sheet's timing diagram +# Clock low & high cycles must last at least 200ns, therefore +# additional delays are required. At the moment it is set to +# about 500ns each, 1µs total at 168 MHz clock rate. +# Total net time for a 12 bit sample: ~ 25 µs, 8 bit sample ~20 µs +# + @staticmethod + @micropython.viper + def touch_talk(cmd: int, bits: int, port: int) -> int: + gpio_bsr = ptr16(port + stm.GPIO_BSRRL) + gpio_idr = ptr16(port + stm.GPIO_IDR) +# +# now shift the command out, which is 8 bits +# data is sampled at the low-> high transient +# + gpio_bsr[1] = T_CLOCK # Empty clock cycle before start, maybe obsolete + for i in range(2): pass #delay +# gpio_bsr[0] = T_CLOCK # clock High +# for i in range(2): pass #delay +# gpio_bsr[1] = T_CLOCK # set clock low in the beginning + mask = 0x80 # high bit first + for i in range(8): + gpio_bsr[1] = T_CLOCK # set clock low in the beginning + if cmd & mask: + gpio_bsr[0] = T_DOUT # set data bit high + else: + gpio_bsr[1] = T_DOUT # set data bit low + for i in range(1): pass #delay + gpio_bsr[0] = T_CLOCK # set clock high + mask >>= 1 + for i in range(0): pass #delay + gpio_bsr[1] = T_CLOCK | T_DOUT# Another clock & data, low + for i in range(2): pass #delay + gpio_bsr[0] = T_CLOCK # clock High + for i in range(0): pass #delay +# +# now shift the data in, which is 8 or 12 bits +# data is sampled after the high->low transient +# + result = 0 + for i in range(bits): + gpio_bsr[1] = T_CLOCK # Clock low + for i in range(1): pass # short delay + if gpio_idr[0] & T_DIN: # get data + bit = 1 + else: + bit = 0 + result = (result << 1) | bit # shift data in + gpio_bsr[0] = T_CLOCK # Clock high + for i in range(1): pass # delay +# +# another clock cycle, maybe obsolete +# + gpio_bsr[1] = T_CLOCK # Another clock toggle, low + for i in range(2): pass # delay + gpio_bsr[0] = T_CLOCK # clock High + for i in range(2): pass #delay + gpio_bsr[1] = T_CLOCK # Clock low +# now we're ready to leave + return result + diff --git a/tft_gui/button.py b/tft_gui/button.py new file mode 100644 index 0000000..aee68bb --- /dev/null +++ b/tft_gui/button.py @@ -0,0 +1,171 @@ +from delay import Delay +from ui import get_stringsize, print_centered, touchable, CIRCLE, RECTANGLE, CLIPPED_RECT +# Button coordinates relate to bounding box (BB). x, y are of BB top left corner. +# likewise width and height refer to BB, regardless of button shape + +class Button(touchable): + def __init__(self, objsched, tft, objtouch, location, *, shape=CIRCLE, height=50, width=50, fill=True, + fgcolor=None, bgcolor=None, fontcolor=None, litcolor=None, text='', font=None, show=True, callback=lambda x : None, + args=[]): + super().__init__(objsched, objtouch) + self.objsched = objsched + self.tft = tft + self.location = location + self.shape = shape + self.height = height + self.width = width + self.radius = height // 2 + self.fill = fill + self.fgcolor = fgcolor + self.bgcolor = bgcolor + self.font = font if font is not None else tft.text_font + self.fontcolor = fontcolor if fontcolor is not None else tft.getColor() + self.litcolor = litcolor + self.text = text + self.callback = callback + self.callback_args = args + self.orig_fgcolor = fgcolor + if self.litcolor is not None: + self.delay = Delay(objsched, self.shownormal) + self.visible = True # ditto + self.litcolor = litcolor if self.fgcolor is not None else None + self.busy = False + if show: + self.show() + + def show(self): + tft = self.tft + fgcolor = tft.getColor() # save old colors + bgcolor = tft.getBGColor() + x = self.location[0] + y = self.location[1] + if not self.visible: # erase the button + tft.setColor(bgcolor) + tft.fillRectangle(x, y, x + self.width, y + self.height) + tft.setColor(fgcolor) + return + if self.fgcolor is not None: + tft.setColor(self.fgcolor) + if self.bgcolor is not None: + tft.setBGColor(self.bgcolor) + if self.shape == CIRCLE: # Button coords are of top left corner of bounding box + x += self.radius + y += self.radius + if self.fill: + tft.fillCircle(x, y, self.radius) + else: + tft.drawCircle(x, y, self.radius) + if self.font is not None and len(self.text): + print_centered(tft, x, y, self.text, self.fontcolor, self.font) + else: + x1 = x + self.width + y1 = y + self.height + if self.shape == RECTANGLE: # rectangle + if self.fill: + tft.fillRectangle(x, y, x1, y1) + else: + tft.drawRectangle(x, y, x1, y1) + if self.font is not None and len(self.text): + print_centered(tft, (x + x1) // 2, (y + y1) // 2, self.text, self.fontcolor, self.font) + elif self.shape == CLIPPED_RECT: # clipped rectangle + if self.fill: + tft.fillClippedRectangle(x, y, x1, y1) + else: + tft.drawClippedRectangle(x, y, x1, y1) + if self.font is not None and len(self.text): + print_centered(tft, (x + x1) // 2, (y + y1) // 2, self.text, self.fontcolor, self.font) + tft.setColor(fgcolor) # restore them + tft.setBGColor(bgcolor) + + def shownormal(self): + self.fgcolor = self.orig_fgcolor + self.show() + + def touched(self, x, y): # If touched, process it otherwise do nothing + is_touched = False + if self.shape == CIRCLE: + r = self.radius + dx = r - (x - self.location[0]) + dy = r - (y - self.location[1]) + if (dx * dx + dy * dy) < (r * r): # Pythagoras is alive! + is_touched = True + elif self.shape in (RECTANGLE, CLIPPED_RECT): # rectangle + x0 = self.location[0] + x1 = self.location[0] + self.width + y0 = self.location[1] + y1 = self.location[1] + self.height + if x0 <= x <= x1 and y0 <= y <= y1: + is_touched = True + if is_touched and self.litcolor is not None: + self.fgcolor = self.litcolor + self.show() + self.delay.trigger(1) + if is_touched and not self.busy: # Respond once to a press + self.callback(self, self.callback_args) # Callback not a bound method so pass self + self.busy = True # Ensure no response to continued press + + def untouched(self): # User has released touchpad or touched elsewhere + self.busy = False + +class Buttons(object): + def __init__(self, user_callback): + self.user_callback = user_callback + self.lstbuttons = [] + + def add_button(self, *args, **kwargs): + kwargs['show'] = False + self.lstbuttons.append(Button(*args, **kwargs)) + +# Group of buttons, typically at same location, where pressing one shows +# the next e.g. start/stop toggle or sequential select from short list +class Buttonset(Buttons): + def __init__(self, user_callback): + super().__init__(user_callback) + + def run(self): + for idx, button in enumerate(self.lstbuttons): + if idx: + button.visible = False # Only button zero visible and sensitive + button.enabled = False + button.callback_args.append(idx) + button.callback = self.callback + self.lstbuttons[0].show() + + def callback(self, button, args): + button_no = args[-1] + old = self.lstbuttons[button_no] + new = self.lstbuttons[(button_no + 1) % len(self.lstbuttons)] + old.enabled = False + old.visible = False + old.show() + new.enabled = True + new.visible = True + new.busy = True # Don't respond to continued press + new.show() + self.user_callback(new, args[:-1]) # user gets button with args they specified + +# Group of buttons at different locations, where pressing one shows +# only current button highlighted and oes callback from current one +class RadioButtons(Buttons): + def __init__(self, user_callback, highlight, selected=0): + super().__init__(user_callback) + self.highlight = highlight + self.selected = selected + + def run(self): + for idx, button in enumerate(self.lstbuttons): + if idx == self.selected: # Initial selection + button.fgcolor = self.highlight + else: + button.fgcolor = button.orig_fgcolor + button.show() + button.callback = self.callback + + def callback(self, button, args): + for but in self.lstbuttons: + if but is button: + but.fgcolor = self.highlight + else: + but.fgcolor = but.orig_fgcolor + but.show() + self.user_callback(button, args) # user gets button with args they specified diff --git a/tft_gui/buttontest.py b/tft_gui/buttontest.py new file mode 100644 index 0000000..c26713e --- /dev/null +++ b/tft_gui/buttontest.py @@ -0,0 +1,106 @@ + +import gc +from font14 import font14 +from tft import TFT, LANDSCAPE +from usched import Sched +from asynctouch import TOUCH +from button import Button, Buttonset, RadioButtons +from ui import CIRCLE, RECTANGLE, CLIPPED_RECT +#gc.collect() + +def callback(button, args): + arg = args[0] + print('Returned: ', arg) + tft = button.tft + tft.setTextPos(0, 240) + tft.setTextStyle(None, None, 0, font14) + tft.printString('Button argument zero: {} '.format(arg)) + if arg == 'Q': + button.objsched.stop() + +#gc.collect() +# These tables contain args that differ between members of a set of related buttons +table = [ + {'fgcolor' : (0, 128, 0), 'litcolor' : (0, 255, 0), 'text' : 'Yes', 'args' : ('A'), 'fontcolor' : (0, 0, 0)}, + {'fgcolor' : (255, 0, 0), 'text' : 'No', 'args' : ('B')}, + {'fgcolor' : (0, 0, 255), 'text' : '???', 'args' : ('C'), 'fill': False}, + {'fgcolor' : (128, 128, 128), 'text' : 'Quit', 'args' : ('Q'), 'shape' : CLIPPED_RECT}, +] + +# similar buttons: only tabulate data that varies +table2 = [ + {'text' : 'P', 'args' : ('p')}, + {'text' : 'Q', 'args' : ('q')}, + {'text' : 'R', 'args' : ('r')}, + {'text' : 'S', 'args' : ('s')}, +] + +# A Buttonset with two entries +# If buttons to be used in a buttonset, Use list rather than tuple for args because buttonset appends. + +table3 = [ + {'fgcolor' : (0, 255, 0), 'shape' : CLIPPED_RECT, 'text' : 'Start', 'args' : ['Live']}, + {'fgcolor' : (255, 0, 0), 'shape' : CLIPPED_RECT, 'text' : 'Stop', 'args' : ['Die']}, +] + +table4 = [ + {'text' : '1', 'args' : ('1')}, + {'text' : '2', 'args' : ('2')}, + {'text' : '3', 'args' : ('3')}, + {'text' : '4', 'args' : ('4')}, +] + +#gc.collect() +# THREADS + +def stop(fTim, objsched): # Stop the scheduler after fTim seconds + yield fTim + objsched.stop() + +# USER TEST FUNCTION + +def test(duration = 0): + if duration: + print("Test TFT panel for {:3d} seconds".format(duration)) + else: + print('Testing TFT...') + objsched = Sched() # Instantiate the scheduler + mytft = TFT("SSD1963", "LB04301", LANDSCAPE) + mytouch = TOUCH(objsched, "XPT2046") + mytft.backlight(100) # light on + +# Button assortment + x = 50 + for t in table: + Button(objsched, mytft, mytouch, (x, 0), font = font14, callback = callback, **t) + x += 70 + +# Highlighting buttons + x = 50 + for t in table2: + Button(objsched, mytft, mytouch, (x, 60), font = font14, fgcolor = (128, 128, 128), + fontcolor = (0, 0, 0), litcolor = (255, 255, 255), callback = callback, **t) + x += 70 + +# On/Off toggle + x = 50 + bs = Buttonset(callback) + for t in table3: # Buttons overlay each other at same location + bs.add_button(objsched, mytft, mytouch, (x, 120), font = font14, fontcolor = (0, 0, 0), **t) + bs.run() + +# Radio buttons + x = 50 + rb = RadioButtons(callback, (0, 0, 255)) # color of selected button + for t in table4: + rb.add_button(objsched, mytft, mytouch, (x, 180), font = font14, fontcolor = (255, 255, 255), + fgcolor = (0, 0, 90), height = 30, **t) + x += 40 + rb.run() + +# Start scheduler + if duration: + objsched.add_thread(stop(duration, objsched)) # Commit suicide after specified no. of seconds + objsched.run() # Run it! + +test() # Forever: we have a Quit button! diff --git a/tft_gui/delay.py b/tft_gui/delay.py new file mode 100644 index 0000000..f7cc3c3 --- /dev/null +++ b/tft_gui/delay.py @@ -0,0 +1,42 @@ +# A time delay class for the micropython board. Based on the scheduler class. 24th Aug 2014 +# Author: Peter Hinch +# V1.0 25th Aug 2014 +# Used by Pushbutton library. +# This class implements the software equivalent of a retriggerable monostable. When first instantiated +# a delay object does nothing until it trigger method is called. It then enters a running state until +# the specified time elapses when it calls the optional callback function and stops running. +# A running delay may be retriggered by calling its trigger function: its time to run is now specified +# by the passed value. + +# The usual caveats re microsheduler time periods applies: if you need millisecond accuracy +# (or better) use a hardware timer. Times can easily be -0 +20mS or more, depending on other threads + +from usched import Sched, microsWhen, seconds, after, microsUntil, Timeout + +class Delay(object): + def __init__(self, objSched, callback = None, callback_args = ()): + self.objSched = objSched + self.callback = callback + self.callback_args = callback_args + self._running = False + + def stop(self): + self._running = False + + def trigger(self, duration): + self.tstop = microsWhen(seconds(duration)) # Update end time + if not self._running: # Start a thread which stops the + self.objSched.add_thread(self.killer()) # delay after its period has elapsed + self._running = True + + def running(self): + return self._running + + def killer(self): + to = Timeout(1) # Initial value is arbitrary + while not after(self.tstop): # Might have been retriggered + yield to._ussetdelay(microsUntil(self.tstop)) + if self._running and self.callback: + self.callback(*self.callback_args) + self._running = False + diff --git a/tft_gui/font10.py b/tft_gui/font10.py new file mode 100644 index 0000000..6eb27ee --- /dev/null +++ b/tft_gui/font10.py @@ -0,0 +1,118 @@ +# Code generated by cfonts_to_trans_py.py +import TFTfont +_font10 = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x04\x92\x49\x24\x82\x00\x00'\ +b'\x02\x52\x94\x80\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x04\x82\x42\x41\x27\xfc\x48\x48\x24\x7f\xc9\x09\x04\x80\x00\x00\x00\x00\x00'\ +b'\x08\x1e\x2b\x49\x48\x48\x38\x0e\x09\x49\x49\x2a\x1c\x08\x08\x00\x00'\ +b'\x00\x00\xe0\x84\x44\x11\x10\x44\x81\x12\x04\x53\x8e\x51\x02\x44\x11\x10\x44\x42\x11\x08\x38\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x03\x81\x10\x44\x11\x02\x81\xc0\x90\x42\x50\xa4\x10\x86\x1e\x40\x00\x00\x00\x00\x00'\ +b'\x15\x40\x00\x00\x00'\ +b'\x01\x22\x24\x44\x44\x44\x22\x21\x00'\ +b'\x01\x04\x21\x04\x21\x08\x42\x11\x08\x44\x00'\ +b'\x00\x47\xc4\x28\xa0\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x08\x08\x08\x7f\x08\x08\x08\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x02\x48\x00'\ +b'\x00\x00\x00\x00\x00\xf8\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x02\x00\x00'\ +b'\x00\x44\x21\x10\x84\x42\x11\x08\x00\x00\x00'\ +b'\x00\x1c\x22\x41\x41\x41\x41\x41\x41\x41\x41\x22\x1c\x00\x00\x00\x00'\ +b'\x00\x10\xc5\x24\x10\x41\x04\x10\x41\x04\x00\x00\x00'\ +b'\x00\x1e\x22\x41\x01\x01\x02\x02\x04\x08\x10\x20\x7f\x00\x00\x00\x00'\ +b'\x00\x1c\x22\x42\x02\x04\x1c\x02\x01\x01\x41\x62\x1c\x00\x00\x00\x00'\ +b'\x00\x04\x0c\x14\x14\x24\x24\x44\x84\xff\x04\x04\x04\x00\x00\x00\x00'\ +b'\x00\x3f\x20\x20\x40\x7c\x42\x01\x01\x01\x41\x22\x1c\x00\x00\x00\x00'\ +b'\x00\x1c\x23\x41\x40\x5c\x62\x41\x41\x41\x41\x22\x1c\x00\x00\x00\x00'\ +b'\x00\x7f\x02\x02\x04\x04\x08\x08\x08\x08\x10\x10\x10\x00\x00\x00\x00'\ +b'\x00\x1c\x22\x41\x41\x22\x1c\x22\x41\x41\x41\x22\x1c\x00\x00\x00\x00'\ +b'\x00\x1c\x22\x41\x41\x41\x41\x23\x1d\x01\x41\x22\x3c\x00\x00\x00\x00'\ +b'\x00\x02\x00\x00\x02\x00\x00'\ +b'\x00\x02\x00\x00\x02\x48\x00'\ +b'\x00\x00\x00\x20\x60\xc1\x81\x00\x60\x0c\x01\x80\x20\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x03\xfc\x00\x00\x00\x3f\xc0\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x10\x06\x00\xc0\x18\x02\x06\x0c\x18\x10\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x1c\x22\x41\x41\x01\x02\x04\x08\x08\x08\x00\x08\x00\x00\x00\x00'\ +b'\x00\x00\x01\xfc\x03\x01\x82\x00\x22\x1c\x91\x11\xc4\x90\x42\x90\x21\x48\x10\xa4\x10\x52\x08\x48\x8c\x42\x3b\xc1\x00\x04\x40\x04\x18\x0c\x03\xf8\x00'\ +b'\x00\x00\x80\x28\x05\x01\x10\x22\x04\x41\x04\x3f\x88\x09\x01\x40\x18\x02\x00\x00\x00\x00\x00\x00'\ +b'\x00\x1f\xc4\x09\x01\x40\x50\x27\xf9\x02\x40\x50\x14\x05\x02\x7f\x00\x00\x00\x00\x00\x00'\ +b'\x00\x01\xf0\x41\x10\x14\x00\x80\x10\x02\x00\x40\x08\x00\x80\x88\x20\xf8\x00\x00\x00\x00\x00\x00'\ +b'\x00\x0f\xe1\x02\x20\x24\x02\x80\x50\x0a\x01\x40\x28\x05\x01\x20\x47\xf0\x00\x00\x00\x00\x00\x00'\ +b'\x00\x1f\xf4\x01\x00\x40\x10\x07\xf9\x00\x40\x10\x04\x01\x00\x7f\xc0\x00\x00\x00\x00\x00'\ +b'\x00\x3f\xd0\x08\x04\x02\x01\xfc\x80\x40\x20\x10\x08\x04\x00\x00\x00\x00\x00\x00'\ +b'\x00\x01\xe0\x42\x10\x24\x00\x80\x10\x02\x1f\x40\x28\x04\x81\x08\x40\xf0\x00\x00\x00\x00\x00\x00'\ +b'\x00\x10\x14\x05\x01\x40\x50\x17\xfd\x01\x40\x50\x14\x05\x01\x40\x40\x00\x00\x00\x00\x00'\ +b'\x04\x92\x49\x24\x92\x00\x00'\ +b'\x00\x01\x01\x01\x01\x01\x01\x01\x01\x41\x41\x22\x3c\x00\x00\x00\x00'\ +b'\x00\x10\x14\x09\x04\x42\x11\x04\xc1\x48\x61\x10\x44\x09\x02\x40\x40\x00\x00\x00\x00\x00'\ +b'\x00\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x7f\x00\x00\x00\x00'\ +b'\x00\x04\x01\x60\x36\x03\x50\x55\x05\x48\x94\x89\x49\x14\x51\x45\x14\x21\x42\x10\x00\x00\x00\x00\x00\x00'\ +b'\x00\x10\x16\x05\x41\x50\x52\x14\x45\x11\x42\x50\x54\x15\x03\x40\x40\x00\x00\x00\x00\x00'\ +b'\x00\x01\xe0\x42\x10\x24\x02\x80\x50\x0a\x01\x40\x28\x04\x81\x08\x40\xf0\x00\x00\x00\x00\x00\x00'\ +b'\x00\x1f\xc4\x09\x01\x40\x50\x14\x09\xfc\x40\x10\x04\x01\x00\x40\x00\x00\x00\x00\x00\x00'\ +b'\x00\x01\xe0\x42\x10\x24\x02\x80\x50\x0a\x01\x40\x28\x04\x8d\x88\x60\xf6\x00\x00\x00\x00\x00\x00'\ +b'\x00\x1f\xc4\x09\x01\x40\x50\x14\x09\xfc\x42\x10\x44\x11\x02\x40\x40\x00\x00\x00\x00\x00'\ +b'\x00\x07\xc2\x09\x01\x40\x08\x01\xc0\x0e\x00\x50\x14\x04\x82\x1f\x00\x00\x00\x00\x00\x00'\ +b'\x00\x7f\xc2\x01\x00\x80\x40\x20\x10\x08\x04\x02\x01\x00\x80\x00\x00\x00\x00\x00'\ +b'\x00\x10\x14\x05\x01\x40\x50\x14\x05\x01\x40\x50\x14\x04\x82\x1f\x00\x00\x00\x00\x00\x00'\ +b'\x00\x10\x14\x04\x82\x20\x88\x21\x10\x44\x11\x02\x80\xa0\x10\x04\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x40\x40\x50\x50\x48\x28\x24\x14\x11\x11\x10\x88\x88\x44\x44\x14\x14\x0a\x0a\x05\x05\x01\x01\x00\x80\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x08\x08\x82\x08\x81\x10\x14\x01\x00\x50\x11\x02\x20\x82\x20\x28\x02\x00\x00\x00\x00\x00\x00'\ +b'\x00\x10\x12\x08\x82\x11\x02\x80\xa0\x10\x04\x01\x00\x40\x10\x04\x00\x00\x00\x00\x00\x00'\ +b'\x00\x3f\xc0\x40\x40\x20\x20\x20\x10\x10\x10\x08\x08\x0f\xf8\x00\x00\x00\x00\x00'\ +b'\x07\x44\x44\x44\x44\x44\x44\x47\x00'\ +b'\x04\x10\x84\x10\x84\x10\x84\x10\x80\x00\x00'\ +b'\x07\x11\x11\x11\x11\x11\x11\x17\x00'\ +b'\x00\x20\xa1\x44\x48\x91\x41\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xff\x00\x00'\ +b'\x08\x80\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x1e\x21\x41\x07\x39\x41\x41\x43\x3d\x00\x00\x00\x00'\ +b'\x00\x40\x40\x40\x5c\x62\x41\x41\x41\x41\x41\x62\x5c\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x1c\x22\x41\x40\x40\x40\x41\x22\x1c\x00\x00\x00\x00'\ +b'\x00\x01\x01\x01\x1d\x23\x41\x41\x41\x41\x41\x23\x1d\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x1c\x22\x41\x41\x7f\x40\x41\x22\x1c\x00\x00\x00\x00'\ +b'\x00\xc8\x47\x90\x84\x21\x08\x42\x00\x00\x00'\ +b'\x00\x00\x00\x00\x1d\x23\x41\x41\x41\x41\x41\x23\x1d\x01\x42\x3c\x00'\ +b'\x00\x40\x40\x40\x5e\x61\x41\x41\x41\x41\x41\x41\x41\x00\x00\x00\x00'\ +b'\x10\x55\x55\x40\x00'\ +b'\x10\x55\x55\x56\x00'\ +b'\x00\x40\x40\x40\x41\x42\x44\x48\x58\x64\x42\x42\x41\x00\x00\x00\x00'\ +b'\x15\x55\x55\x40\x00'\ +b'\x00\x00\x00\x00\x00\x00\x5c\xe6\x31\x42\x14\x21\x42\x14\x21\x42\x14\x21\x42\x10\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x5e\x61\x41\x41\x41\x41\x41\x41\x41\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x1c\x22\x41\x41\x41\x41\x41\x22\x1c\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x5c\x62\x41\x41\x41\x41\x41\x62\x5c\x40\x40\x40\x00'\ +b'\x00\x00\x00\x00\x1d\x23\x41\x41\x41\x41\x41\x23\x1d\x01\x01\x01\x00'\ +b'\x00\x00\x00\x5d\x84\x10\x41\x04\x10\x40\x00\x00\x00'\ +b'\x00\x00\x00\x03\xc8\x50\x20\x3c\x04\x0a\x13\xc0\x00\x00\x00'\ +b'\x04\x44\xf4\x44\x44\x44\x70\x00\x00'\ +b'\x00\x00\x00\x00\x41\x41\x41\x41\x41\x41\x41\x43\x3d\x00\x00\x00\x00'\ +b'\x00\x00\x00\x08\x30\x51\x22\x44\x50\xa0\x81\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x08\x43\x08\x52\x92\x52\x51\x4a\x29\x45\x10\x42\x08\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x08\x28\x8a\x14\x10\x50\xa2\x28\x20\x00\x00\x00'\ +b'\x00\x00\x00\x00\x41\x41\x41\x22\x22\x26\x14\x14\x08\x08\x10\x60\x00'\ +b'\x00\x00\x00\x0f\xe0\x82\x04\x10\x40\x82\x0f\xe0\x00\x00\x00'\ +b'\x00\x31\x04\x10\x41\x04\x60\x41\x04\x10\x41\x03\x00'\ +b'\x04\x92\x49\x24\x92\x49\x20'\ +b'\x06\x08\x42\x10\x84\x19\x08\x42\x10\x98\x00'\ +b'\x00\x00\x00\x00\x00\x00\x01\xe3\x1e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x7b\x6d\xb6\xf0\x00\x00'\ + +_font10_index = b'\x00\x00\x14\x00\x1b\x00\x26\x00\x3a\x00\x4b\x00\x69\x00\x7f\x00'\ +b'\x84\x00\x8d\x00\x98\x00\xa5\x00\xb6\x00\xbd\x00\xc8\x00\xcf\x00'\ +b'\xda\x00\xeb\x00\xf8\x00\x09\x01\x1a\x01\x2b\x01\x3c\x01\x4d\x01'\ +b'\x5e\x01\x6f\x01\x80\x01\x87\x01\x8e\x01\xa2\x01\xb6\x01\xca\x01'\ +b'\xdb\x01\x00\x02\x18\x02\x2e\x02\x46\x02\x5e\x02\x74\x02\x88\x02'\ +b'\xa0\x02\xb6\x02\xbd\x02\xce\x02\xe4\x02\xf5\x02\x0f\x03\x25\x03'\ +b'\x3d\x03\x53\x03\x6b\x03\x81\x03\x97\x03\xab\x03\xc1\x03\xd7\x03'\ +b'\xfc\x03\x14\x04\x2a\x04\x3e\x04\x47\x04\x52\x04\x5b\x04\x6a\x04'\ +b'\x7e\x04\x85\x04\x96\x04\xa7\x04\xb8\x04\xc9\x04\xda\x04\xe5\x04'\ +b'\xf6\x04\x07\x05\x0c\x05\x11\x05\x22\x05\x27\x05\x41\x05\x52\x05'\ +b'\x63\x05\x74\x05\x85\x05\x92\x05\xa1\x05\xaa\x05\xbb\x05\xca\x05'\ +b'\xe2\x05\xf1\x05\x02\x06\x11\x06\x1e\x06\x25\x06\x30\x06\x44\x06'\ +b'\x4b\x06' + +font10 = TFTfont.TFTFont(_font10, _font10_index, 17, 17, 96) + +fonts = {"font10":font10, +} + diff --git a/tft_gui/font14.py b/tft_gui/font14.py new file mode 100644 index 0000000..82c932e --- /dev/null +++ b/tft_gui/font14.py @@ -0,0 +1,118 @@ +# Code generated by cfonts_to_trans_py.py +import TFTfont +_font14 = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\xc6\x31\x8c\x63\x18\xc6\x31\x8c\x00\x18\xc0\x00\x00\x00'\ +b'\x00\xcd\x9b\x36\x6c\xd9\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x61\x83\x0c\x18\x61\x86\x7f\xff\xff\xe3\x0c\x18\x61\xc6\x0c\x31\xff\xff\xff\x98\x60\xc3\x0c\x30\x61\x83\x0c\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x02\x00\xf8\x3f\xe7\x27\x62\x36\x20\x62\x03\xa0\x3f\x00\xfc\x02\xe0\x27\x02\x36\x23\x72\x33\x26\x3f\xe0\xf8\x02\x00\x20\x00\x00\x00\x00\x00'\ +b'\x00\x00\x03\x80\x60\xd8\x18\x31\x83\x06\x30\xc0\xc6\x18\x18\xc6\x03\x19\x80\x36\x30\x03\x8c\x70\x01\x9b\x00\x66\x30\x0c\xc6\x03\x18\xc0\xe3\x18\x18\x63\x06\x06\xc0\xc0\x70\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x1c\x00\xf8\x06\x30\x18\xc0\x63\x01\xcc\x03\x60\x07\x00\x7c\x03\xb8\x1c\x76\x60\xd1\x81\xc6\x03\x0c\x3e\x3f\xdc\x3e\x20\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x0d\xb6\xd8\x00\x00\x00\x00\x00\x00'\ +b'\x00\x10\x86\x10\xc3\x08\x61\x86\x18\x61\x86\x18\x30\xc3\x04\x18\x20\x40'\ +b'\x00\x40\x40\xc0\x81\x83\x02\x06\x0c\x18\x30\x60\xc1\x83\x0c\x18\x30\x41\x82\x08\x00'\ +b'\x00\x06\x03\x0f\xf1\xe0\xf0\xcc\x24\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x30\x01\x80\x0c\x00\x60\x03\x03\xff\xdf\xfe\x06\x00\x30\x01\x80\x0c\x00\x60\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x33\x11\x20\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\xff\xe0\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x33\x00\x00\x00'\ +b'\x00\x0c\x18\x60\xc1\x86\x0c\x18\x70\xc1\x83\x0c\x18\x30\xc1\x80\x00\x00\x00\x00\x00'\ +b'\x00\x00\xf8\x1f\xc3\x8e\x30\x66\x03\x60\x36\x03\x60\x36\x03\x60\x36\x03\x60\x36\x03\x30\x63\x8e\x3f\xc0\xf8\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\xc0\x60\x70\xf8\xec\x66\x03\x01\x80\xc0\x60\x30\x18\x0c\x06\x03\x01\x80\xc0\x00\x00\x00\x00\x00'\ +b'\x00\x00\xf8\x3f\xe3\x06\x60\x36\x03\x00\x30\x03\x00\x60\x0c\x01\x80\x30\x06\x00\xc0\x18\x03\x00\x7f\xf7\xff\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\xf0\x3f\xc3\x0e\x60\x60\x06\x00\x60\x0c\x07\x80\x7c\x00\x60\x03\x00\x36\x03\x70\x33\x06\x1f\xc0\xf8\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x0c\x01\xc0\x3c\x03\xc0\x6c\x0e\xc0\xcc\x18\xc1\x8c\x30\xc7\x0c\x7f\xf7\xff\x00\xc0\x0c\x00\xc0\x0c\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x01\xfe\x1f\xe3\x00\x30\x03\x00\x37\x83\xfc\x70\x66\x03\x00\x30\x03\x00\x36\x03\x60\x23\x06\x1f\xc0\xf8\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x7c\x1f\xe3\x87\x30\x33\x00\x60\x06\x78\x6f\xe7\x86\x70\x36\x03\x60\x36\x03\x30\x33\x86\x1f\xc0\xf8\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x07\xff\x7f\xf0\x02\x00\x60\x0c\x00\x80\x18\x03\x00\x30\x03\x00\x60\x06\x00\x60\x0c\x00\xc0\x0c\x00\xc0\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\xf8\x1f\xc3\x8e\x30\x63\x06\x30\x61\x8c\x0f\x81\xfc\x30\x66\x03\x60\x36\x03\x60\x33\x06\x3f\xe0\xf8\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\xf8\x1f\xc3\x06\x70\x76\x03\x60\x36\x03\x60\x33\x0f\x3f\xb0\xf3\x00\x30\x06\x60\x67\x0e\x3f\xc1\xf0\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x03\x30\x00\x00\x00\x00\x33\x00\x00\x00'\ +b'\x00\x00\x03\x30\x00\x00\x00\x00\x33\x11\x20\x00'\ +b'\x00\x00\x00\x00\x00\x00\x02\x01\xc0\xf0\x78\x38\x0c\x00\xe0\x07\x80\x3c\x01\xc0\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x1f\xfb\xff\x00\x00\x00\x00\x3f\xf7\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x04\x00\xe0\x0f\x00\x78\x01\xc0\x0c\x07\x07\x83\xc0\xe0\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\xf8\x1f\xe3\x86\x70\x36\x03\x00\x30\x06\x00\xe0\x1c\x03\x00\x60\x06\x00\x60\x00\x00\x00\x06\x00\x60\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x03\xf8\x00\x3f\xf8\x01\xe0\xf0\x0e\x00\xe0\x73\xcd\xc3\x9f\xb3\x0c\xe3\xc6\x36\x06\x19\x98\x18\x66\xc0\x61\x9b\x01\x86\x6c\x06\x39\xb0\x30\xc6\xc1\xc7\x19\x8f\x38\x37\xff\xc0\xcf\x1c\x01\x80\x00\xc7\x00\x0e\x0f\x80\xf0\x0f\xff\x00\x0f\xf0\x00'\ +b'\x00\x00\x07\x00\x0e\x00\x36\x00\x6c\x01\xdc\x03\x18\x06\x30\x18\x30\x30\x60\x60\xc1\xff\xc3\xff\x8e\x03\x98\x03\x30\x06\xc0\x07\x80\x0c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\xff\x83\xff\x0c\x0e\x30\x18\xc0\x63\x01\x8c\x0c\x3f\xf0\xff\xc3\x03\x8c\x03\x30\x0c\xc0\x33\x00\xcc\x06\x3f\xf8\xff\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x03\xf0\x0f\xfc\x1c\x0e\x38\x06\x30\x03\x60\x00\x60\x00\x60\x00\x60\x00\x60\x00\x60\x00\x60\x03\x30\x03\x38\x06\x1c\x0e\x0f\xfc\x03\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x3f\xf0\x3f\xf8\x30\x1c\x30\x0e\x30\x06\x30\x03\x30\x03\x30\x03\x30\x03\x30\x03\x30\x03\x30\x03\x30\x06\x30\x06\x30\x1c\x3f\xf8\x3f\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\xff\xf3\xff\xcc\x00\x30\x00\xc0\x03\x00\x0c\x00\x3f\xf8\xff\xe3\x00\x0c\x00\x30\x00\xc0\x03\x00\x0c\x00\x3f\xfc\xff\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x01\xff\xcf\xfe\x60\x03\x00\x18\x00\xc0\x06\x00\x3f\xf1\xff\x8c\x00\x60\x03\x00\x18\x00\xc0\x06\x00\x30\x01\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x01\xfc\x03\xff\x83\xc1\xe3\x80\x31\x80\x0d\xc0\x00\xc0\x00\x60\x00\x30\x1f\xd8\x0f\xec\x00\x37\x00\x19\x80\x0c\xe0\x06\x3c\x0f\x0f\xfe\x00\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x60\x0c\xc0\x19\x80\x33\x00\x66\x00\xcc\x01\x98\x03\x3f\xfe\x7f\xfc\xc0\x19\x80\x33\x00\x66\x00\xcc\x01\x98\x03\x30\x06\x60\x0c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x03\x33\x33\x33\x33\x33\x33\x33\x33\x00\x00\x00'\ +b'\x00\x00\x30\x0c\x03\x00\xc0\x30\x0c\x03\x00\xc0\x30\x0c\x03\x00\xd8\x36\x0d\xc7\x3f\x87\xc0\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x30\x0e\x30\x1c\x30\x38\x30\x70\x30\xe0\x31\xc0\x33\x80\x37\x00\x3f\x80\x3d\xc0\x38\xe0\x30\x70\x30\x30\x30\x38\x30\x1c\x30\x0e\x30\x07\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x03\x00\x30\x03\x00\x30\x03\x00\x30\x03\x00\x30\x03\x00\x30\x03\x00\x30\x03\x00\x30\x03\x00\x3f\xf3\xff\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x1c\x01\xcf\x01\xe7\x80\xf3\xc0\x79\xb0\x6c\xd8\x36\x6c\x1b\x33\x19\x99\x8c\xcc\xc6\x66\x63\x33\x1b\x19\x8d\x8c\xc6\xc6\x61\xc3\x30\xe1\x98\x70\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x60\x0c\xe0\x19\xe0\x33\xc0\x66\xc0\xcc\xc1\x99\x83\x31\x86\x63\x8c\xc3\x19\x83\x33\x06\x66\x06\xcc\x07\x98\x0f\x30\x0e\x60\x0c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x01\xf8\x03\xff\x03\x81\xc3\x80\x71\x80\x19\x80\x0e\xc0\x03\x60\x01\xb0\x00\xd8\x00\x6c\x00\x36\x00\x39\x80\x18\xe0\x1c\x38\x1c\x0f\xfc\x01\xf8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\xff\x83\xff\x8c\x06\x30\x0c\xc0\x33\x00\xcc\x03\x30\x18\xff\xe3\xfe\x0c\x00\x30\x00\xc0\x03\x00\x0c\x00\x30\x00\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x01\xf8\x03\xff\x03\x81\xc3\x80\x71\x80\x19\x80\x0e\xc0\x03\x60\x01\xb0\x00\xd8\x00\x6c\x00\x37\x00\x19\x83\x18\xe1\xdc\x38\x3c\x0f\xfe\x01\xfd\xc0\x00\x60\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x3f\xf8\x3f\xfc\x30\x0e\x30\x06\x30\x06\x30\x06\x30\x0e\x3f\xfc\x3f\xf0\x30\xe0\x30\x70\x30\x38\x30\x38\x30\x1c\x30\x0c\x30\x0e\x30\x07\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x1f\x81\xff\x06\x0e\x30\x0c\xc0\x33\x00\x07\x00\x1f\xc0\x1f\xc0\x07\x80\x03\x60\x0d\x80\x33\x00\xce\x0e\x1f\xf0\x1f\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x03\xff\xdf\xfe\x06\x00\x30\x01\x80\x0c\x00\x60\x03\x00\x18\x00\xc0\x06\x00\x30\x01\x80\x0c\x00\x60\x03\x00\x18\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x60\x0c\xc0\x19\x80\x33\x00\x66\x00\xcc\x01\x98\x03\x30\x06\x60\x0c\xc0\x19\x80\x33\x00\x66\x00\xc6\x03\x0e\x0e\x0f\xf8\x0f\xe0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x01\x80\x0f\x80\x3b\x00\x66\x00\xce\x03\x8c\x06\x18\x0c\x38\x38\x30\x60\x60\xc0\x63\x00\xc6\x01\xdc\x01\xb0\x03\x60\x07\xc0\x07\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x01\x80\x70\x0f\x00\xe0\x1b\x03\x60\x66\x06\xc0\xcc\x0d\x81\x98\x31\x83\x18\x63\x0c\x30\xc6\x18\x63\x06\x30\xc6\x0c\x61\xcc\x19\xc1\xb8\x3b\x03\x60\x36\x06\xc0\x6c\x0d\x80\xd8\x0e\x00\xe0\x1c\x01\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\xe0\x38\xe0\xe0\xc1\x81\xc7\x01\xdc\x01\xb0\x03\xe0\x03\x80\x07\x00\x1b\x00\x77\x00\xc6\x03\x8e\x0e\x0e\x18\x0c\x70\x1d\xc0\x1c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x03\x80\x76\x01\x8c\x0c\x38\x70\x61\x81\xce\x03\x30\x07\x80\x1e\x00\x30\x00\xc0\x03\x00\x0c\x00\x30\x00\xc0\x03\x00\x0c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x03\xff\xdf\xfe\x00\x60\x06\x00\x70\x03\x00\x30\x03\x00\x38\x01\x80\x18\x01\x80\x1c\x00\xc0\x0c\x00\xff\xff\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x03\xde\xc6\x31\x8c\x63\x18\xc6\x31\x8c\x63\x18\xc6\x3d\xe0'\ +b'\x01\x83\x03\x06\x0c\x0c\x18\x30\x70\x60\xc1\x81\x83\x06\x06\x0c\x00\x00\x00\x00\x00'\ +b'\x03\xde\x31\x8c\x63\x18\xc6\x31\x8c\x63\x18\xc6\x31\xbd\xe0'\ +b'\x00\x00\xc0\x3c\x07\x81\x98\x33\x06\x61\x86\x30\xcc\x0c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xff\xff\xe0'\ +b'\x03\x8c\x30\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\xf8\x3f\xc7\x0e\x60\x60\x1e\x1f\xe3\xe6\x70\x66\x06\x60\xe7\x1e\x3f\xe1\xe3\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x0c\x01\x80\x30\x06\x00\xde\x1f\xf3\x86\x60\x6c\x0d\x81\xb0\x36\x06\xc0\xd8\x1b\x86\x7f\x8d\xe0\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x1f\x07\xf1\xc7\x70\x6c\x01\x80\x30\x06\x00\xc0\x1c\x19\x87\x1f\xc1\xf0\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x0c\x01\x80\x30\x06\x1e\xc7\xf9\x87\x60\x6c\x0d\x81\xb0\x36\x06\xc0\xd8\x19\x87\x1f\xe1\xec\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\xf8\x1f\xc3\x86\x60\x36\x03\x7f\xf7\xff\x60\x06\x00\x70\x33\x86\x1f\xe0\xf8\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x3c\xf9\x83\x1f\xbf\x18\x30\x60\xc1\x83\x06\x0c\x18\x30\x60\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x1e\xc7\xf9\x87\x70\x6c\x0d\x81\xb0\x36\x06\xc0\xdc\x19\xc7\x1f\xe1\xec\x01\xb0\x37\x0c\x7f\x87\xc0'\ +b'\x00\x0c\x01\x80\x30\x06\x00\xcf\x1b\xf3\xc7\x70\x6c\x0d\x81\xb0\x36\x06\xc0\xd8\x1b\x03\x60\x6c\x0c\x00\x00\x00\x00\x00\x00\x00'\ +b'\x0d\x80\xdb\x6d\xb6\xdb\x6c\x00\x00'\ +b'\x0d\x80\xdb\x6d\xb6\xdb\x6d\xb7\xf0'\ +b'\x00\x0c\x01\x80\x30\x06\x00\xc1\xd8\x73\x1c\x67\x0d\xc1\xf0\x3f\x07\x70\xc6\x18\x63\x0e\x60\xcc\x1c\x00\x00\x00\x00\x00\x00\x00'\ +b'\x0d\xb6\xdb\x6d\xb6\xdb\x6c\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x38\x71\xbe\x7c\xf3\xe7\x70\xe1\xb0\x60\xd8\x30\x6c\x18\x36\x0c\x1b\x06\x0d\x83\x06\xc1\x83\x60\xc1\xb0\x60\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\xcf\x1b\xf3\xc7\x70\x6c\x0d\x81\xb0\x36\x06\xc0\xd8\x1b\x03\x60\x6c\x0c\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\xf8\x1f\xc3\x8e\x70\x76\x03\x60\x36\x03\x60\x36\x03\x70\x73\x8e\x1f\xc0\xf8\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\xde\x1f\xf3\x86\x60\xec\x0d\x81\xb0\x36\x06\xc0\xd8\x3b\x8e\x7f\x8d\xe1\x80\x30\x06\x00\xc0\x18\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x1e\xcf\xf9\xc7\x70\x6c\x0d\x81\xb0\x36\x06\xc0\xdc\x19\xc7\x1f\xe1\xec\x01\x80\x30\x06\x00\xc0\x18'\ +b'\x00\x00\x00\x00\x00\x6f\x7f\x70\x60\x60\x60\x60\x60\x60\x60\x60\x60\x60\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x3f\x0f\xf3\x83\x60\x0e\x00\xfc\x0f\xe0\x3e\x00\xd8\x1b\x87\x3f\xc3\xf0\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x43\x0c\x33\xff\xcc\x30\xc3\x0c\x30\xc3\x0c\x3c\x70\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\xc0\xd8\x1b\x03\x60\x6c\x0d\x81\xb0\x36\x06\xc0\xd8\x3b\x8f\x3f\x63\xcc\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x01\x80\xf0\x1b\x06\x60\xcc\x18\xc6\x18\xc1\xb0\x36\x06\xc0\x70\x0e\x00\x80\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x18\x20\xf0\xe1\xe1\xc3\x63\x8c\xc5\x19\x9b\x31\x36\xc3\x6d\x86\x8b\x05\x14\x0e\x38\x1c\x70\x10\x40\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x01\xc0\xd8\x31\x8c\x39\x83\x60\x38\x07\x00\xe0\x36\x0e\xe1\x8c\x60\xdc\x1c\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x01\x80\xf0\x1b\x06\x60\xc6\x18\xc6\x0c\xc1\x98\x36\x03\xc0\x78\x06\x00\xc0\x18\x03\x00\xc0\x78\x0e\x00'\ +b'\x00\x00\x00\x00\x00\x00\x1f\xf7\xfc\x07\x03\x81\xc0\xe0\x70\x18\x0e\x07\x03\x80\xff\xff\xf0\x00\x00\x00\x00\x00\x00'\ +b'\x00\x1c\x78\xc1\x83\x06\x0c\x18\x30\xc3\x07\x06\x06\x0c\x18\x30\x60\xc1\x83\xc3\x80'\ +b'\x03\x33\x33\x33\x33\x33\x33\x33\x33\x33\x33\x30'\ +b'\x00\xe1\xe0\xc1\x83\x06\x0c\x18\x30\x30\x30\xe1\x86\x0c\x18\x30\x60\xc1\x8f\x1c\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x3c\x07\xf1\x47\xf0\x1e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x01\xf8\xc6\x31\x8c\x63\x18\xc6\x31\xf8\x00\x00\x00\x00'\ + +_font14_index = b'\x00\x00\x23\x00\x32\x00\x47\x00\x6d\x00\x90\x00\xc7\x00\xf0\x00'\ +b'\xf9\x00\x0b\x01\x20\x01\x3a\x01\x60\x01\x6c\x01\x81\x01\x8d\x01'\ +b'\xa2\x01\xc5\x01\xdf\x01\x02\x02\x25\x02\x48\x02\x6b\x02\x8e\x02'\ +b'\xb1\x02\xd4\x02\xf7\x02\x03\x03\x0f\x03\x2f\x03\x4f\x03\x6f\x03'\ +b'\x92\x03\xd2\x03\xfe\x03\x27\x04\x55\x04\x83\x04\xac\x04\xd2\x04'\ +b'\x03\x05\x2f\x05\x3b\x05\x58\x05\x86\x05\xa9\x05\xda\x05\x06\x06'\ +b'\x37\x06\x60\x06\x91\x06\xbf\x06\xe8\x06\x0e\x07\x3a\x07\x66\x07'\ +b'\xa9\x07\xd5\x07\xfe\x07\x24\x08\x33\x08\x48\x08\x57\x08\x77\x08'\ +b'\x9d\x08\xac\x08\xcf\x08\xef\x08\x0f\x09\x2f\x09\x52\x09\x67\x09'\ +b'\x87\x09\xa7\x09\xb0\x09\xb9\x09\xd9\x09\xe2\x09\x13\x0a\x33\x0a'\ +b'\x56\x0a\x76\x0a\x96\x0a\xad\x0a\xcd\x0a\xdf\x0a\xff\x0a\x1f\x0b'\ +b'\x4b\x0b\x6b\x0b\x8b\x0b\xa8\x0b\xbd\x0b\xc9\x0b\xde\x0b\x01\x0c'\ +b'\x10\x0c' + +font14 = TFTfont.TFTFont(_font14, _font14_index, 23, 23, 96) + +fonts = {"font14":font14, +} + diff --git a/tft_gui/slider.py b/tft_gui/slider.py new file mode 100644 index 0000000..4070e6d --- /dev/null +++ b/tft_gui/slider.py @@ -0,0 +1,146 @@ +# slider.py +# A slider's text items lie outside its bounding box (area sensitive to touch) + +from ui import touchable +from TFT_io import TFT_io +class Slider(touchable): + def __init__(self, objsched, tft, objtouch, location, font, *, height=200, width=30, divisions=10, legends=None, + fgcolor=None, bgcolor=None, fontcolor=None, slidecolor=None, cb_end=lambda x, y : None, + cbe_args=[], cb_move=lambda x, y : None, cbm_args=[], to_string=lambda x : str(x), value=0.0): + super().__init__(objsched, objtouch) + self.objsched = objsched + self.tft = tft + self.location = location + self.height = height + self.width = width + self.divisions = divisions + self.legends = legends + self.fgcolor = fgcolor + self.bgcolor = bgcolor + self.fontcolor = fontcolor if fontcolor is not None else tft.getColor() + self.font = font + self.slidecolor = slidecolor + self.cb_end = cb_end + self.cbe_args = cbe_args + self.cb_move = cb_move + self.cbm_args = cbm_args + self.to_string = to_string # Applied to display at bottom: user converts 0-1.0 to string with any scaling applied + self.was_touched = False + self.old_text_end = False + self.slidewidth = int(width / 1.3) + self.slidewidth += self.slidewidth % 2 # Ensure divisible by 2 + self.slideheight = 6 # must be divisible by 2 + # We draw an odd number of pixels: + self.slidebytes = (self.slideheight + 1) * (self.slidewidth + 1) * 3 + self.slidebuf = bytearray(self.slidebytes) + self.slide_x = -1 + self.slide_y = -1 # Invalidate old position + self.border_y = min(self.slideheight // 2, 10) # Allow space above and below slot + + self._value = min(max(value, 0.0), 1.0) # User supplies 0-1.0 + self.show() + objsched.add_thread(self.mainthread()) + + def show(self): + tft = self.tft + fgcolor = tft.getColor() # save old colors + bgcolor = tft.getBGColor() + mybgcolor = bgcolor + x = self.location[0] + y = self.location[1] + self.border_y + if self.bgcolor is not None: + tft.setColor(self.bgcolor) + else: + tft.setColor(bgcolor) + tft.setTextStyle(self.fontcolor, None, 2, self.font) + if self.fgcolor is not None: + tft.setColor(self.fgcolor) + else: + tft.setColor(fgcolor) + if self.bgcolor is not None: + mybgcolor = self.bgcolor + tft.setBGColor(mybgcolor) + height = self.height + width = self.width + dx = width // 3 + xcentre = x + width // 2 + tft.drawRectangle(x + dx, y, x + 2 * dx, y + height) + + if self.divisions > 0: + dy = height // self.divisions # Tick marks + ytick = y + fhdelta = self.font.bits_vert // 2 + for tick in range(self.divisions + 1): + tft.drawHLine(x, ytick, dx) + tft.drawHLine(x + 2 * dx, ytick, dx) + ytick += dy + + if self.legends is not None: # Legends + if len(self.legends) <= 1: + dy = 0 + else: + dy = height // (len(self.legends) -1) + yl = y + height # Start at bottom + for legend in self.legends: + tft.setTextPos(x + width, yl - fhdelta) + tft.printString(legend) + yl -= dy + + sw = self.slidewidth # Handle slider + sh = self.slideheight + sliderpos = int(y + height - self._value * height) + if self.slidecolor is not None: + tft.setColor(self.slidecolor) + if self.slide_x >= 0: # Restore background + tft.setXY(self.slide_x, self.slide_y, self.slide_x + sw, self.slide_y + sh) + TFT_io.tft_write_data_AS(self.slidebuf, self.slidebytes) + x0 = xcentre - sw // 2 + y0 = sliderpos - sh // 2 + x1 = xcentre + sw // 2 + y1 = sliderpos + sh // 2 + tft.setXY(x0, y0, x1, y1) # Read background + TFT_io.tft_read_cmd_data_AS(0x2e, self.slidebuf, self.slidebytes) + self.slide_x = x0 + self.slide_y = y0 + tft.fillRectangle(x0, y0, x1, y1) # Draw slider + + textx = x + texty = y + height + 2 * self.border_y + tft.setTextPos(textx, texty) + if self.old_text_end: + tft.setColor(mybgcolor) + tft.fillRectangle(textx, texty, self.old_text_end, texty + self.font.bits_vert) + tft.printString(self.to_string(self._value)) + self.old_text_end = tft.getTextPos()[0] + tft.setColor(fgcolor) # restore them + tft.setBGColor(bgcolor) + + def value(self, val=None): + if val is None: + return self._value + self._value = min(max(val, 0.0), 1.0) + self.show() + + def touched(self, x, y): # If touched in bounding box, process it otherwise do nothing + x0 = self.location[0] + x1 = self.location[0] + self.width + y0 = self.location[1] + y1 = self.location[1] + self.height + if x0 <= x <= x1 and y0 <= y <= y1: + self.was_touched = True + self._value = (y1 - y) / self.height + + def untouched(self): # User has released touchpad or touched elsewhere + if self.was_touched: + self.cb_end(self, self.cbe_args) # Callback not a bound method so pass self + self.was_touched = False + + def mainthread(self): + old_value = self._value + while True: + yield + val = self._value + if val != old_value: + old_value = val + self.cb_move(self, self.cbm_args) # Callback not a bound method so pass self + self.show() diff --git a/tft_gui/slidetest.py b/tft_gui/slidetest.py new file mode 100644 index 0000000..4b81405 --- /dev/null +++ b/tft_gui/slidetest.py @@ -0,0 +1,102 @@ + +import gc +from font10 import font10 +from tft import TFT, LANDSCAPE +from usched import Sched +from asynctouch import TOUCH +from slider import Slider +from button import Button +from ui import CLIPPED_RECT +import math +#gc.collect() + +class Dial(object): + def __init__(self, objsched, tft, x, y, r, l): + self.objsched = objsched + self.tft = tft + self.xorigin = x + self.yorigin = y + self.radius = r + self.pointerlen = l + self.angle = None + self.delta = 1 + tft.drawCircle(x, y, r) + objsched.add_thread(self.mainthread()) + + def update(self, angle): + tft = self.tft + fgcolor = tft.getColor() # save old colors + bgcolor = tft.getBGColor() + if self.angle is not None: + tft.setColor(bgcolor) + self.drawpointer(self.angle) # erase old + tft.setColor(fgcolor) + self.drawpointer(angle) # draw new + self.angle = angle # update old + tft.setColor(fgcolor) # restore them + tft.setBGColor(bgcolor) + + def drawpointer(self, radians): + x_end = int(self.xorigin + self.pointerlen * math.sin(radians)) + y_end = int(self.yorigin - self.pointerlen * math.cos(radians)) + self.tft.drawLine(self.xorigin, self.yorigin, x_end, y_end) + + def mainthread(self): + while True: + yield 0.1 + angle = self.angle if self.angle is not None else 0 + angle += math.pi * 2 * self.delta / 10 + self.update(angle) + +# CALLBACKS +# cb_end occurs when user stops touching the control +def callback(slider, args): + print('{} returned {}'.format(args[0], slider.value())) + +def to_string(val): + return '{:4.1f}ohms'.format(val * 10) + +def master_moved(slider, args): + val = slider.value() + slave1 = args[0] + slave1.value(val) + slave2 = args[1] + slave2.value(val) + +# Either slave has had its slider moved (by user or by having value altered) +def slave_moved(slider, args): + dial = args[0] + dial.delta = slider.value() + +def doquit(button, args): + button.objsched.stop() + +# USER TEST FUNCTION +# '0', '1','2','3','4','5','6','7','8','9','10' +# Common arguments for all three sliders +table = {'fontcolor' : (255, 255, 255), + 'legends' : ('0', '5', '10'), + 'to_string' : to_string, + 'cb_end' : callback, + 'value' : 0.5} + +def test(duration = 0): + print('Test TFT panel...') + objsched = Sched() # Instantiate the scheduler + mytft = TFT("SSD1963", "LB04301", LANDSCAPE) + mytouch = TOUCH(objsched, "XPT2046") + mytft.backlight(100) # light on + Button(objsched, mytft, mytouch, (400, 240), font = font10, callback = doquit, fgcolor = (255, 0, 0), + height = 30, text = 'Quit', shape = CLIPPED_RECT) + dial1 = Dial(objsched, mytft, 350, 60, 50, 48) + dial2 = Dial(objsched, mytft, 350, 170, 50, 48) + y = 5 + slave1 = Slider(objsched, mytft, mytouch, (80, y), font10, + fgcolor = (0, 255, 0), cbe_args = ('Slave1',), cb_move = slave_moved, cbm_args = (dial1,), **table) + slave2 = Slider(objsched, mytft, mytouch, (160, y), font10, + fgcolor = (0, 255, 0), cbe_args = ('Slave2',), cb_move = slave_moved, cbm_args = (dial2,), **table) + Slider(objsched, mytft, mytouch, (0, y), font10, + fgcolor = (255, 255, 0), cbe_args = ('Master',), cb_move = master_moved, cbm_args = (slave1, slave2), **table) + objsched.run() # Run it! + +test() diff --git a/tft_gui/tft.py b/tft_gui/tft.py new file mode 100644 index 0000000..d476535 --- /dev/null +++ b/tft_gui/tft.py @@ -0,0 +1,668 @@ +# +# The MIT License (MIT) +# +# Copyright (c) 2016 Robert Hammelrath +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# Class supporting TFT LC-displays with a parallel Interface +# First example: Controller SSD1963 +# It uses X1..X8 for data and Y3, Y9, Y10, Y11 and Y12 for control signals. +# The minimal connection just for writes is X1..X8 for data, Y9 for /Reset. Y11 for /WR and Y12 for /RS +# Then LED and /CS must be hard tied to Vcc and GND, and /RD is not used. +# +# Some parts of the software are a port of code provided by Rinky-Dink Electronics, Henning Karlsen, +# with the following copyright notice: +## Copyright (C)2015 Rinky-Dink Electronics, Henning Karlsen. All right reserved +## This library is free software; you can redistribute it and/or +## modify it under the terms of the CC BY-NC-SA 3.0 license. +## Please see the included documents for further information. +# + +import pyb, stm +from uctypes import addressof +from TFT_io import TFT_io + +# define constants +# +RESET = const(1 << 10) ## Y9 +RD = const(1 << 11) ## Y10 +WR = const(0x01) ## Y11 +D_C = const(0x02) ## Y12 + +LED = const(1 << 8) ## Y3 +POWER = const(1 << 9) ## Y4 + +## CS is not used and must be hard tied to GND + +PORTRAIT = const(1) +LANDSCAPE = const(0) + +class TFT: + + def __init__(self, controller = "SSD1963", lcd_type = "LB04301", orientation = LANDSCAPE, v_flip = False, h_flip = False): + self.tft_init(controller, lcd_type, orientation, v_flip, h_flip) + + def tft_init(self, controller = "SSD1963", lcd_type = "LB04301", orientation = LANDSCAPE, v_flip = False, h_flip = False): +# +# For convenience, define X1..X1 and Y9..Y12 as output port using thy python functions. +# X1..X8 will be redefind on the fly as Input by accessing the MODER control registers +# when needed. Y9 is treate seperately, since it is used for Reset, which is done at python level +# since it need long delays anyhow, 5 and 15 ms vs. 10 µs. +# +# Set TFT general defaults + self.controller = controller + self.lcd_type = lcd_type + self.orientation = orientation + self.v_flip = v_flip # flip vertical + self.h_flip = h_flip # flip horizontal + self.c_flip = 0 # flip blue/red + self.rc_flip = 0 # flip row/column (does not seem to work) + + self.setColor((255, 255, 255)) # set FG color to white as can be. + self.setBGColor((0, 0, 0)) # set BG to black +# special treat for BG LED + self.pin_led = pyb.Pin("Y3", pyb.Pin.OUT_PP) + self.led_tim = pyb.Timer(4, freq=500) + self.led_ch = self.led_tim.channel(3, pyb.Timer.PWM, pin=self.pin_led) + self.led_ch.pulse_width_percent(0) # led off + self.pin_led.value(0) ## switch BG LED off +# special treat for Power Pin + self.pin_power = pyb.Pin("Y4", pyb.Pin.OUT_PP) + self.pin_power.value(1) ## switch Power on + pyb.delay(10) +# this may have to be moved to the controller specific section + if orientation == PORTRAIT: + self.setXY = TFT_io.setXY_P + else: + self.setXY = TFT_io.setXY_L +# ---------- + for pin_name in ["X1", "X2", "X3", "X4", "X5", "X6", "X7", "X8", + "Y10", "Y11", "Y12"]: + pin = pyb.Pin(pin_name, pyb.Pin.OUT_PP) # set as output + pin.value(1) ## set high as default +# special treat for Reset + self.pin_reset = pyb.Pin("Y9", pyb.Pin.OUT_PP) +# Reset the device + self.pin_reset.value(1) ## do a hard reset + pyb.delay(10) + self.pin_reset.value(0) ## Low + pyb.delay(20) + self.pin_reset.value(1) ## set high again + pyb.delay(20) +# +# Now initialiize the LCD +# This is for the SSD1963 controller and two specific LCDs. More may follow. +# Data taken from the SSD1963 data sheet, SSD1963 Application Note and the LCD Data sheets +# + if controller == "SSD1963": # 1st approach for 480 x 272 + TFT_io.tft_cmd_data(0xe2, bytearray(b'\x1d\x02\x54'), 3) # PLL multiplier, set PLL clock to 100M + # N=0x2D for 6.5MHz, 0x1D for 10MHz crystal + # PLLClock = Crystal * (Mult + 1) / (Div + 1) + # The intermediate value Crystal * (Mult + 1) must be between 250MHz and 750 MHz + TFT_io.tft_cmd_data(0xe0, bytearray(b'\x01'), 1) # PLL Enable + pyb.delay(10) + TFT_io.tft_cmd_data(0xe0, bytearray(b'\x03'), 1) + pyb.delay(10) + TFT_io.tft_cmd(0x01) # software reset + pyb.delay(10) +# +# Settings for the LCD +# +# The LCDC_FPR depends on PLL clock and the reccomended LCD Dot clock DCLK +# +# LCDC_FPR = (DCLK * 1048576 / PLLClock) - 1 +# +# The other settings are less obvious, since the definitions of the SSD1963 data sheet and the +# LCD data sheets differ. So what' common, even if the names may differ: +# HDP Horizontal Panel width (also called HDISP, Thd). The value store in the register is HDP - 1 +# VDP Vertical Panel Width (also called VDISP, Tvd). The value stored in the register is VDP - 1 +# HT Total Horizontal Period, also called HP, th... The exact value does not matter +# VT Total Vertical Period, alco called VT, tv, .. The exact value does not matter +# HPW Width of the Horizontal sync pulse, also called HS, thpw. +# VPW Width of the Vertical sync pulse, also called VS, tvpw +# Front Porch (HFP and VFP) Time between the end of display data and the sync pulse +# Back Porch (HBP and VBP Time between the start of the sync pulse and the start of display data. +# HT = FP + HDP + BP and VT = VFP + VDP + VBP (sometimes plus sync pulse width) +# Unfortunately, the controller does not use these front/back porch times, instead it uses an starting time +# in the front porch area and defines (see also figures in chapter 13.3 of the SSD1963 data sheet) +# HPS Time from that horiz. starting point to the start of the horzontal display area +# LPS Time from that horiz. starting point to the horizontal sync pulse +# VPS Time from the vert. starting point to the first line +# FPS Time from the vert. starting point to the vertical sync pulse +# +# So the following relations must be held: +# +# HT > HDP + HPS +# HPS >= HPW + LPS +# HPS = Back Porch - LPS, or HPS = Horizontal back Porch +# VT > VDP + VPS +# VPS >= VPW + FPS +# VPS = Back Porch - FPS, or VPS = Vertical back Porch +# +# LPS or FPS may have a value of zero, since the length of the front porch is detemined by the +# other figures +# +# The best is to start with the recomendations of the lCD data sheet for Back porch, grab a +# sync pulse with and the determine the other, such that they meet the relations. Typically, these +# values allow for some ambuigity. +# + if lcd_type == "LB04301": # Size 480x272, 4.3", 24 Bit, 4.3" + # + # Value Min Typical Max + # DotClock 5 MHZ 9 MHz 12 MHz + # HT (Hor. Total 490 531 612 + # HDP (Hor. Disp) 480 + # HBP (back porch) 8 43 + # HFP (Fr. porch) 2 8 + # HPW (Hor. sync) 1 + # VT (Vert. Total) 275 288 335 + # VDP (Vert. Disp) 272 + # VBP (back porch) 2 12 + # VFP (fr. porch) 1 4 + # VPW (vert. sync) 1 10 + # + # This table in combination with the relation above leads to the settings: + # HPS = 43, HPW = 8, LPS = 0, HT = 531 + # VPS = 14, VPW = 10, FPS = 0, VT = 288 + # + self.disp_x_size = 479 + self.disp_y_size = 271 + TFT_io.tft_cmd_data_AS(0xe6, bytearray(b'\x01\x70\xa3'), 3) # PLL setting for PCLK + # (9MHz * 1048576 / 100MHz) - 1 = 94371 = 0x170a3 + TFT_io.tft_cmd_data_AS(0xb0, bytearray( # # LCD SPECIFICATION + [0x20, # 24 Color bits, HSync/VSync low, No Dithering + 0x00, # TFT mode + self.disp_x_size >> 8, self.disp_x_size & 0xff, # physical Width of TFT + self.disp_y_size >> 8, self.disp_y_size & 0xff, # physical Height of TFT + 0x00]), 7) # Last byte only required for a serial TFT + TFT_io.tft_cmd_data_AS(0xb4, bytearray(b'\x02\x13\x00\x2b\x08\x00\x00\x00'), 8) + # HSYNC, Set HT 531 HPS 43 HPW=Sync pulse 8 LPS 0 + TFT_io.tft_cmd_data_AS(0xb6, bytearray(b'\x01\x20\x00\x0e\x0a\x00\x00'), 7) + # VSYNC, Set VT 288 VPS 14 VPW 10 FPS 0 + TFT_io.tft_cmd_data_AS(0x36, bytearray([(orientation & 1) << 5 | (h_flip & 1) << 1 | (v_flip) & 1]), 1) + # rotation/ flip, etc., t.b.d. + elif lcd_type == "AT070TN92": # Size 800x480, 7", 18 Bit, lower color bits ignored + # + # Value Min Typical Max + # DotClock 26.4 MHz 33.3 MHz 46.8 MHz + # HT (Hor. Total 862 1056 1200 + # HDP (Hor. Disp) 800 + # HBP (back porch) 46 46 46 + # HFP (Fr. porch) 16 210 254 + # HPW (Hor. sync) 1 40 + # VT (Vert. Total) 510 525 650 + # VDP (Vert. Disp) 480 + # VBP (back porch) 23 23 23 + # VFP (fr. porch) 7 22 147 + # VPW (vert. sync) 1 20 + # + # This table in combination with the relation above leads to the settings: + # HPS = 46, HPW = 8, LPS = 0, HT = 1056 + # VPS = 23, VPW = 10, VPS = 0, VT = 525 + # + self.disp_x_size = 799 + self.disp_y_size = 479 + TFT_io.tft_cmd_data_AS(0xe6, bytearray(b'\x05\x53\xf6'), 3) # PLL setting for PCLK + # (33.3MHz * 1048576 / 100MHz) - 1 = 349174 = 0x553f6 + TFT_io.tft_cmd_data_AS(0xb0, bytearray( # # LCD SPECIFICATION + [0x00, # 18 Color bits, HSync/VSync low, No Dithering/FRC + 0x00, # TFT mode + self.disp_x_size >> 8, self.disp_x_size & 0xff, # physical Width of TFT + self.disp_y_size >> 8, self.disp_y_size & 0xff, # physical Height of TFT + 0x00]), 7) # Last byte only required for a serial TFT + TFT_io.tft_cmd_data_AS(0xb4, bytearray(b'\x04\x1f\x00\x2e\x08\x00\x00\x00'), 8) + # HSYNC, Set HT 1056 HPS 46 HPW 8 LPS 0 + TFT_io.tft_cmd_data_AS(0xb6, bytearray(b'\x02\x0c\x00\x17\x08\x00\x00'), 7) + # VSYNC, Set VT 525 VPS 23 VPW 08 FPS 0 + TFT_io.tft_cmd_data_AS(0x36, bytearray([(orientation & 1) << 5 | (h_flip & 1) << 1 | (v_flip) & 1]), 1) + # rotation/ flip, etc., t.b.d. + else: + print("Wrong Parameter lcd_type: ", lcd_type) + return + TFT_io.tft_cmd_data_AS(0xBA, bytearray(b'\x0f'), 1) # GPIO[3:0] out 1 + TFT_io.tft_cmd_data_AS(0xB8, bytearray(b'\x07\x01'), 1) # GPIO3=input, GPIO[2:0]=output + + TFT_io.tft_cmd_data_AS(0xf0, bytearray(b'\x00'), 1) # Pixel data Interface 8 Bit + + TFT_io.tft_cmd(0x29) # Display on + TFT_io.tft_cmd_data_AS(0xbe, bytearray(b'\x06\xf0\x01\xf0\x00\x00'), 6) + # Set PWM for B/L + TFT_io.tft_cmd_data_AS(0xd0, bytearray(b'\x0d'), 1) # Set DBC: enable, agressive + else: + print("Wrong Parameter controller: ", controller) + return +# +# Set character printing defaults +# + self.setTextPos(0,0) + self.setScrollArea(0, self.disp_y_size + 1, 0) + self.text_font = None + self.setTextStyle(None, None, 0, None, 0) +# +# Init done. clear Screen and switch BG LED on +# + self.clrSCR() # clear the display +# self.backlight(100) ## switch BG LED on +# +# Return screen dimensions +# + def getScreensize(self): + if self.orientation == LANDSCAPE: + return (self.disp_x_size + 1, self.disp_y_size + 1) + else: + return (self.disp_y_size + 1, self.disp_x_size + 1) +# +# set backlight brightness +# + def backlight(self, percent): + percent = max(0, min(percent, 100)) + self.led_ch.pulse_width_percent(percent) # set LED +# +# switch power on/off +# + def power(self, onoff): + if onoff: + self.pin_power.value(True) ## switch power on or off + else: + self.pin_power.value(False) + +# +# set the tft flip modes +# + def set_tft_mode(self, v_flip = False, h_flip = False, c_flip = False, orientation = LANDSCAPE): + self.v_flip = v_flip # flip vertical + self.h_flip = h_flip # flip horizontal + self.c_flip = c_flip # flip blue/red + self.orientation = orientation # LANDSCAPE/PORTRAIT + TFT_io.tft_cmd_data_AS(0x36, + bytearray([(self.orientation << 5) |(self.c_flip << 3) | (self.h_flip & 1) << 1 | (self.v_flip) & 1]), 1) + # rotation/ flip, etc., t.b.d. +# +# get the tft flip modes +# + def get_tft_mode(self): + return (self.v_flip, self.h_flip, self.c_flip, self.orientation) # +# +# set the color used for the draw commands +# + def setColor(self, fgcolor): + self.color = fgcolor + self.colorvect = bytearray(self.color) # prepare byte array +# +# Set BG color used for the draw commands +# + def setBGColor(self, bgcolor): + self.BGcolor = bgcolor + self.BGcolorvect = bytearray(self.BGcolor) # prepare byte array +# +# get the color used for the draw commands +# + def getColor(self): + return self.color +# +# get BG color used for +# + def getBGColor(self): + return self.BGcolor +# +# Draw a single pixel at location x, y +# Rather slow at 40µs/Pixel +# + def drawPixel(self, x, y): + self.setXY(x, y, x, y) + TFT_io.displaySCR_AS(self.colorvect, 1) # +# +# clear screen, set it to BG color. +# + def clrSCR(self): + self.clrXY() + TFT_io.fillSCR_AS(self.BGcolorvect, (self.disp_x_size + 1) * (self.disp_y_size + 1)) + self.text_x = self.text_y = self.scroll_start = 0 + self.setScrollStart(0) +# +# reset the address range to fullscreen +# + def clrXY(self): + if self.orientation == LANDSCAPE: + self.setXY(0, 0, self.disp_x_size, self.disp_y_size) + else: + self.setXY(0, 0, self.disp_y_size, self.disp_x_size) +# +# Draw a line from x1, y1 to x2, y2 with the color set by setColor() +# Straight port from the UTFT Library at Rinky-Dink Electronics +# + def drawLine(self, x1, y1, x2, y2): + if y1 == y2: + self.drawHLine(x1, y1, x2 - x1 + 1) + elif x1 == x2: + self.drawVLine(x1, y1, y2 - y1 + 1) + else: + dx, xstep = (x2 - x1, 1) if x2 > x1 else (x1 - x2, -1) + dy, ystep = (y2 - y1, 1) if y2 > y1 else (y1 - y2, -1) + col, row = x1, y1 + if dx < dy: + t = - (dy >> 1) + while True: + self.drawPixel(col, row) + if row == y2: + return + row += ystep + t += dx + if t >= 0: + col += xstep + t -= dy + else: + t = - (dx >> 1) + while True: + self.drawPixel(col, row) + if col == x2: + return + col += xstep + t += dy + if t >= 0: + row += ystep + t -= dx +# +# Draw a horizontal line with 1 Pixel width, from x,y to x + l - 1, y +# Straight port from the UTFT Library at Rinky-Dink Electronics +# + def drawHLine(self, x, y, l): # draw horiontal Line + if l < 0: # negative length, swap parameters + l = -l + x -= l + self.setXY(x, y, x + l - 1, y) # set display window + TFT_io.fillSCR_AS(self.colorvect, l) +# +# Draw a vertical line with 1 Pixel width, from x,y to x, y + l - 1 +# Straight port from the UTFT Library at Rinky-Dink Electronics +# + def drawVLine(self, x, y, l): # draw horiontal Line + if l < 0: # negative length, swap parameters + l = -l + y -= l + self.setXY(x, y, x, y + l - 1) # set display window + TFT_io.fillSCR_AS(self.colorvect, l) +# +# Draw rectangle from x1, y1, to x2, y2 +# Straight port from the UTFT Library at Rinky-Dink Electronics +# + def drawRectangle(self, x1, y1, x2, y2): + if x1 > x2: + t = x1; x1 = x2; x2 = t + if y1 > y2: + t = y1; y1 = y2; y2 = t + self.drawHLine(x1, y1, x2 - x1 + 1) + self.drawHLine(x1, y2, x2 - x1 + 1) + self.drawVLine(x1, y1, y2 - y1 + 1) + self.drawVLine(x2, y1, y2 - y1 + 1) +# +# Fill rectangle +# Almost straight port from the UTFT Library at Rinky-Dink Electronics +# + def fillRectangle(self, x1, y1, x2, y2, color = None): + if x1 > x2: + t = x1; x1 = x2; x2 = t + if y1 > y2: + t = y1; y1 = y2; y2 = t + self.setXY(x1, y1, x2, y2) # set display window + if color: + TFT_io.fillSCR_AS(bytearray(color), (x2 - x1 + 1) * (y2 - y1 + 1)) + else: + TFT_io.fillSCR_AS(self.colorvect, (x2 - x1 + 1) * (y2 - y1 + 1)) + +# +# Draw smooth rectangle from x1, y1, to x2, y2 +# Straight port from the UTFT Library at Rinky-Dink Electronics +# + def drawClippedRectangle(self, x1, y1, x2, y2): + if x1 > x2: + t = x1; x1 = x2; x2 = t + if y1 > y2: + t = y1; y1 = y2; y2 = t + if (x2-x1) > 4 and (y2-y1) > 4: + self.drawPixel(x1 + 2,y1 + 1) + self.drawPixel(x1 + 1,y1 + 2) + self.drawPixel(x2 - 2,y1 + 1) + self.drawPixel(x2 - 1,y1 + 2) + self.drawPixel(x1 + 2,y2 - 1) + self.drawPixel(x1 + 1,y2 - 2) + self.drawPixel(x2 - 2,y2 - 1) + self.drawPixel(x2 - 1,y2 - 2) + self.drawHLine(x1 + 3, y1, x2 - x1 - 5) + self.drawHLine(x1 + 3, y2, x2 - x1 - 5) + self.drawVLine(x1, y1 + 3, y2 - y1 - 5) + self.drawVLine(x2, y1 + 3, y2 - y1 - 5) +# +# Fill smooth rectangle from x1, y1, to x2, y2 +# Straight port from the UTFT Library at Rinky-Dink Electronics +# + def fillClippedRectangle(self, x1, y1, x2, y2): + if x1 > x2: + t = x1; x1 = x2; x2 = t + if y1 > y2: + t = y1; y1 = y2; y2 = t + if (x2-x1) > 4 and (y2-y1) > 4: + for i in range(((y2 - y1) // 2) + 1): + if i == 0: + self.drawHLine(x1 + 3, y1 + i, x2 - x1 - 5) + self.drawHLine(x1 + 3, y2 - i, x2 - x1 - 5) + elif i == 1: + self.drawHLine(x1 + 2, y1 + i, x2 - x1 - 3) + self.drawHLine(x1 + 2, y2 - i, x2 - x1 - 3) + elif i == 2: + self.drawHLine(x1 + 1, y1 + i, x2 - x1 - 1) + self.drawHLine(x1 + 1, y2 - i, x2 - x1 - 1) + else: + self.drawHLine(x1, y1 + i, x2 - x1 + 1) + self.drawHLine(x1, y2 - i, x2 - x1 + 1) +# +# draw a circle at x, y with radius +# Straight port from the UTFT Library at Rinky-Dink Electronics +# + def drawCircle(self, x, y, radius): + + f = 1 - radius + ddF_x = 1 + ddF_y = -2 * radius + x1 = 0 + y1 = radius + + self.drawPixel(x, y + radius) + self.drawPixel(x, y - radius) + self.drawPixel(x + radius, y) + self.drawPixel(x - radius, y) + + while x1 < y1: + if f >= 0: + y1 -= 1 + ddF_y += 2 + f += ddF_y + x1 += 1 + ddF_x += 2 + f += ddF_x + self.drawPixel(x + x1, y + y1) + self.drawPixel(x - x1, y + y1) + self.drawPixel(x + x1, y - y1) + self.drawPixel(x - x1, y - y1) + self.drawPixel(x + y1, y + x1) + self.drawPixel(x - y1, y + x1) + self.drawPixel(x + y1, y - x1) + self.drawPixel(x - y1, y - x1) +# +# fill a circle at x, y with radius +# Straight port from the UTFT Library at Rinky-Dink Electronics +# Instead of caluclating x = sqrt(r*r - y*y), it searches the x +# for r*r = x*x + x*x +# + def fillCircle(self, x, y, radius): + r_square = radius * radius * 4 + for y1 in range (-(radius * 2), 1): + y_square = y1 * y1 + for x1 in range (-(radius * 2), 1): + if x1*x1+y_square <= r_square: + x1i = x1//2 + y1i = y1//2 + self.drawHLine(x + x1i, y + y1i, 2 * (-x1i)) + self.drawHLine(x + x1i, y - y1i, 2 * (-x1i)) + break; +# +# Draw a bitmap at x,y with size sx, sy +# mode determines the type of expected data +# mode = 0: The data must contain 3 bytes/pixel red/green/blue +# mode = 1: The data must contain 2 packed bytes/pixel blue/green/red in 565 format +# mode = 2: The data contains 1 bit per pixel, mapped to fg/bg color +# + def drawBitmap(self, x, y, sx, sy, data, mode = 0): + self.setXY(x, y, x + sx - 1, y + sy - 1) + if mode == 0: + TFT_io.displaySCR_AS(data, sx * sy) + elif mode == 1: + TFT_io.displaySCR565_AS(data, sx * sy) + elif mode == 2: + control = bytearray(self.BGcolorvect + self.colorvect + chr(self.transparency)) + TFT_io.displaySCR_bitmap(data, sx*sy, control, 0) +# +# set scroll area to the region between the first and last line +# + def setScrollArea(self, tfa, vsa, bfa): + TFT_io.tft_cmd_data_AS(0x33, bytearray( #set scrolling range + [(tfa >> 8) & 0xff, tfa & 0xff, + (vsa >> 8) & 0xff, vsa & 0xff, + (bfa >> 8) & 0xff, bfa & 0xff]), 6) + self.scroll_fta = tfa + self.scroll_vsa = vsa + self.scroll_bfa = bfa +# +# set the line which is displayed first +# + def setScrollStart(self, lline): + TFT_io.tft_cmd_data_AS(0x37, bytearray([(lline >> 8) & 0xff, lline & 0xff]), 2) + self.scroll_start = lline # store the logical first line +# +# Set text position +# + def setTextPos(self, x, y, clip = False, scroll = True): + self.text_width, self.text_height = self.getScreensize() + self.text_x = x + self.text_y = y + if clip and (self.text_x + clip) < self.text_width: + self.text_width = self.text_x + clip + self.text_scroll = scroll +# +# Get text position +# + def getTextPos(self): + return (self.text_x, self.text_y) +# +# Set Text Style +# + def setTextStyle(self, fgcolor = None, bgcolor = None, transparency = None, font = None, gap = None): + if font != None: + self.text_font = font + if font: + self.text_rows, self.text_cols, nchar, first = font.get_properties() # + if transparency != None: + self.transparency = transparency + if gap != None: + self.text_gap = gap + self.text_color = bytearray(0) + if bgcolor != None: + self.text_color += bytearray(bgcolor) + else: + self.text_color += self.BGcolorvect + if fgcolor != None: + self.text_color += bytearray(fgcolor) + else: + self.text_color += self.colorvect + if transparency != None: + self.transparency = transparency + self.text_color += bytearray([self.transparency]) + if gap != None: + self.text_gap = gap +# +# Check, if a new line is to be opened +# if yes, advance, including scrolling, and clear line, if flags is set +# + def printNewline(self): + self.text_y += self.text_rows + if (self.text_y + self.text_rows) > self.scroll_vsa: # does the line fit? + self.text_y = 0 + newline = self.text_rows + self.setScrollStart(newline) + elif self.scroll_start > 0: # Scrolling has started + newline = (self.scroll_start + self.text_rows) % self.scroll_vsa + self.setScrollStart(newline) +# +# Carriage Return +# + def printCR(self): # clear to end of line + self.text_x = 0 +# +# clear to end-of-line +# + def printClrEOL(self): # clear to end of line + self.setXY(self.text_x, self.text_y, + self.text_width - self.text_x - 1, self.text_y + self.text_rows - 1) # set display window + TFT_io.fillSCR_AS(self.text_color, self.text_width * self.text_rows) +# +# Print string s, returning the length of the printed string in pixels +# + def printString(self, s, bg_buf = None): + len = 0 + for c in s: + cols = self.printChar(c, bg_buf) + if cols == 0: # could not print (any more) + break + len += cols + return len +# +# Print string c using the given char bitmap at location x, y, returning the width of the printed char in pixels +# + def printChar(self, c, bg_buf = None): +# get the charactes pixel bitmap and dimensions + if self.text_font: + fontptr, rows, cols = self.text_font.get_ch(ord(c)) + else: + raise AttributeError('No font selected') + pix_count = cols * rows # number of bits in the char +# test char fit + if self.text_x + cols > self.text_width: # does the char fit on the screen? + if self.text_scroll: + self.printCR() # No, then CR + self.printNewline() # NL: advance to the next line + self.printClrEOL() # clear to end of line + else: + return 0 +# set data arrays & XY-Range + if self.transparency: # in case of transpareny, the frame buffer content is needed + if not bg_buf: # buffer allocation needed? + bg_buf = bytearray(pix_count * 3) # sigh... + self.setXY(self.text_x, self.text_y, self.text_x + cols - 1, self.text_y + rows - 1) # set area + TFT_io.tft_read_cmd_data_AS(0x2e, bg_buf, pix_count * 3) # read background data + else: + bg_buf = 0 # dummy assignment, since None is not accepted +# print char + self.setXY(self.text_x, self.text_y, self.text_x + cols - 1, self.text_y + rows - 1) # set area + TFT_io.displaySCR_bitmap(fontptr, pix_count, self.text_color, bg_buf) # display char! +#advance pointer + self.text_x += (cols + self.text_gap) + return cols + self.text_gap + + diff --git a/tft_gui/ui.py b/tft_gui/ui.py new file mode 100644 index 0000000..7cde3e8 --- /dev/null +++ b/tft_gui/ui.py @@ -0,0 +1,44 @@ +# ui.py Base classes and utilities for TFT GUI + +CIRCLE = 1 +RECTANGLE = 2 +CLIPPED_RECT = 3 + +def get_stringsize(s, font): + hor = 0 + for c in s: + _, vert, cols = font.get_ch(ord(c)) + hor += cols + return hor, vert + +def print_centered(tft, x, y, s, color, font): + length, height = get_stringsize(s, font) + tft.setTextStyle(color, None, 2, font) + tft.setTextPos(x - length // 2, y - height // 2) + tft.printString(s) + +# Base class for touch-enabled classes. +class touchable(object): + touchlist = [] + objtouch = None + + @classmethod + def touchtest(cls): # Singleton thread tests all touchable instances + mytouch = cls.objtouch + while True: + yield + if mytouch.ready: + x, y = mytouch.get_touch() + for obj in cls.touchlist: + if obj.enabled: + obj.touched(x, y) + elif not mytouch.touched: + for obj in cls.touchlist: + obj.untouched() + + def __init__(self, objsched, objtouch): + touchable.touchlist.append(self) + self.enabled = True # Available to user/subclass + if touchable.objtouch is None: # Initialising class and thread + touchable.objtouch = objtouch + objsched.add_thread(self.touchtest()) # One thread only diff --git a/tft_gui/usched.py b/tft_gui/usched.py new file mode 100644 index 0000000..2e891c5 --- /dev/null +++ b/tft_gui/usched.py @@ -0,0 +1,255 @@ +# ledflash, pause, instrument, roundrobin work + +# Lightweight threading library for the micropython board. +# Author: Peter Hinch +# V1.03 Implements gc +# Copyright Peter Hinch 2016 Released under the MIT license + +import pyb, micropython, gc +micropython.alloc_emergency_exception_buf(100) + +# TIMER ACCESS + +TIMERPERIOD = 0x7fffffff # 35.79 minutes 2148 secs +MAXTIME = TIMERPERIOD//2 # 1073 seconds maximum timeout +MAXSECS = MAXTIME//1000000 + +class TimerException(Exception) : pass + +def microsWhen(timediff): # Expected value of counter in a given no. of uS + if timediff >= MAXTIME: + raise TimerException() + return (pyb.micros() + timediff) & TIMERPERIOD + +def microsSince(oldtime): # No of uS since timer held this value + return (pyb.micros() - oldtime) & TIMERPERIOD + +def after(trigtime): # If current time is after the specified value return + res = ((pyb.micros() - trigtime) & TIMERPERIOD) # the no. of uS after. Otherwise return zero + if res >= MAXTIME: + res = 0 + return res + +def microsUntil(tim): # uS from now until a specified time (used in Delay class) + return ((tim - pyb.micros()) & TIMERPERIOD) + +def seconds(S): # Utility functions to convert to integer microseconds + return int(1000000*S) + +def millisecs(mS): + return int(1000*mS) + +# WAITFOR CLASS +# This is a base class. User threads should use classes derived from this. + +class Waitfor(object): + def __init__(self): + self.uS = 0 # Current value of timeout in uS + self.timeout = microsWhen(0) # End value of microsecond counter when TO has elapsed + self.forever = False # "infinite" time delay flag + self.irq = None # Interrupt vector no + self.pollfunc = None # Function to be called if we're polling + self.pollfunc_args = () # Arguments for the above + self.customcallback = None # Optional custom interrupt handler + self.interruptcount = 0 # Set by handler, tested by triggered() + self.roundrobin = False # If true reschedule ASAP + + def triggered(self): # Polled by scheduler. Returns a priority tuple or None if not ready + if self.irq: # Waiting on an interrupt + self.irq.disable() # Potential concurrency issue here (????) + numints = self.interruptcount # Number of missed interrupts + if numints: # Waiting on an interrupt and it's occurred + self.interruptcount = 0 # Clear down the counter + self.irq.enable() + if numints: + return (numints, 0, 0) + if self.pollfunc: # Optional function for the scheduler to poll + res = self.pollfunc(*self.pollfunc_args) # something other than an interrupt + if res is not None: + return (0, res, 0) + if not self.forever: # Check for timeout + if self.roundrobin: + return (0,0,0) # Priority value of round robin thread + res = after(self.timeout) # uS after, or zero if not yet timed out in which case we return None + if res: # Note: can never return (0,0,0) here! + return (0, 0, res) # Nonzero means it's timed out + return None # Not ready for execution + + def _ussetdelay(self, uS=None): # Reset the timer by default to its last value + if uS: # If a value was passed, update it + self.uS = uS + self.timeout = microsWhen(self.uS) # Target timer value + return self + + def setdelay(self, secs=None): # Method used by derived classes to alter timer values + if secs is None: # Set to infinity + self.forever = True + return self + else: # Update saved delay and calculate a new end time + if secs <= 0 or secs > MAXSECS: + raise ValueError('Invalid time delay') + self.forever = False + return self._ussetdelay(seconds(secs)) + + def __call__(self): # Convenience function allows user to yield an updated + if self.uS: # waitfor object + return self._ussetdelay() + return self + + def intcallback(self, irqno): # Runs in interrupt's context. + if self.customcallback: + self.customcallback(irqno) + self.interruptcount += 1 # Increments count to enable trigger to operate + +class Roundrobin(Waitfor): # Compatibility only. A thread yielding a Roundrobin + def __init__(self): # will be rescheduled as soon as priority threads have been serviced + super().__init__() + self.roundrobin = True + +# Intended for device drivers +class Timeout(Waitfor): + def __init__(self, tim): + super().__init__() + self.setdelay(tim) + +# yield from wait +def wait(secs): + if secs <=0 : + raise TimerException() + count, tstart = divmod(secs, MAXSECS) + overshoot = 0 + if tstart > 0: + res = yield Timeout(tstart) + overshoot = res[2] + while count: + res = yield Timeout(MAXSECS) + overshoot += res[2] + count -= 1 + return (0, 0, overshoot) + +# Block on an interrupt from a pin subject to optional timeout +class Pinblock(Waitfor): + def __init__(self, pin, mode, pull, customcallback = None, timeout = None): + super().__init__() + self.customcallback = customcallback + if timeout is None: + self.forever = True + else: + self.setdelay(timeout) + self.irq = pyb.ExtInt(pin, mode, pull, self.intcallback) + +class Poller(Waitfor): + def __init__(self, pollfunc, pollfunc_args = (), timeout = None): + super().__init__() + self.pollfunc = pollfunc + self.pollfunc_args = pollfunc_args + if timeout is None: + self.forever = True + else: + self.setdelay(timeout) + +# SCHEDULER CLASS + +class Sched(object): + GCTIME = const(50000) + DEAD = const(0) + RUNNING = const(1) + PAUSED = const(2) + YIELDED = const(0) + FUNC = const(1) + PID = const(2) + STATE = const(3) + DUE = const(4) + def __init__(self, gc_enable = True): + self.lstThread = [] # Entries contain [Waitfor object, function, pid, state] + self.bStop = False + self.last_gc = 0 + self.pid = 0 + self.gc_enable = gc_enable + + def __getitem__(self, pid): # Index by pid + threads = [thread for thread in self.lstThread if thread[PID] == pid] + if len(threads) == 1: + return threads[0] + elif len(threads) == 0: + raise ValueError('Unknown thread ID {}'.format(pid)) + else: + raise OSError('Scheduler fault: duplicate thread {}'.format(pid)) + + def stop(self, pid=0): + if pid == 0: + self.bStop = True # Kill _runthreads method + return + self[pid][STATE] = DEAD + + def pause(self, pid): + self[pid][STATE] = PAUSED + + def resume(self, pid): + self[pid][STATE] = RUNNING + + def add_thread(self, func): # Thread list contains [Waitfor object, generator, pid, state] + self.pid += 1 # Run thread to first yield to acquire a Waitfor instance + self.lstThread.append([func.send(None), func, self.pid, RUNNING, True]) # and put the resultant thread onto the threadlist + return self.pid + + def _idle_thread(self): # Runs once then in roundrobin or when there's nothing else to do + if self.gc_enable and (self.last_gc == 0 or microsSince(self.last_gc) > GCTIME): + gc.collect() + self.last_gc = pyb.micros() + + def triggered(self, thread): + wf = thread[YIELDED] + if wf is None: + return (0, 0, 0) # Roundrobin + if isinstance(wf, Waitfor): + return wf.triggered() + try: + tim = float(wf) + except ValueError: + raise ValueError('Thread yielded an invalid object') + waitfor = Timeout(tim) + thread[YIELDED] = waitfor + return waitfor.triggered() + + def _runthread(self, thread, priority): + try: # Run thread, send (interrupt count, poll func value, uS overdue) + thread[YIELDED] = thread[FUNC].send(priority) # Store object yielded by thread + except StopIteration: # The thread has terminated: + thread[STATE] = DEAD # Flag thread for removal + + def _get_thread(self): + p_run = None # priority tuple of thread to run + thr_run = None # thread to run + candidates = [t for t in self.lstThread if t[STATE] == RUNNING] + for thread in candidates: + priority = self.triggered(thread) + if priority is not None: # Ignore threads waiting on time or event + if priority == (0,0,0): # Roundrobin (RR) + if thr_run is None and thread[DUE]: + p_run = priority # Assign one, don't care which + thr_run = thread + else: + if p_run is None or priority > p_run: + p_run = priority + thr_run = thread + return thr_run, p_run + + + def _runthreads(self): + while not self.bStop: + thr_run, p_run = self._get_thread() + if thr_run is None: # All RR's have run, anything else is waiting + return + self._runthread(thr_run, p_run) + thr_run[DUE] = False # Only care if RR + + def run(self): # Returns if the stop method is used or all threads terminate + while not self.bStop: + self.lstThread = [thread for thread in self.lstThread if thread[STATE] != DEAD] # Remove dead threads + self._idle_thread() # Garbage collect + if len(self.lstThread) == 0: + return + for thread in self.lstThread: + thread[DUE] = True # Applies only to roundrobin + self._runthreads() # Returns when all RR threads have run once