1# Driver for official MicroPython LCD160CR display
2# MIT license; Copyright (c) 2017 Damien P. George
3
4from micropython import const
5from utime import sleep_ms
6from ustruct import calcsize, pack_into
7import uerrno, machine
8
9# for set_orient
10PORTRAIT = const(0)
11LANDSCAPE = const(1)
12PORTRAIT_UPSIDEDOWN = const(2)
13LANDSCAPE_UPSIDEDOWN = const(3)
14
15# for set_startup_deco; can be or'd
16STARTUP_DECO_NONE = const(0)
17STARTUP_DECO_MLOGO = const(1)
18STARTUP_DECO_INFO = const(2)
19
20_uart_baud_table = {
21    2400: 0,
22    4800: 1,
23    9600: 2,
24    19200: 3,
25    38400: 4,
26    57600: 5,
27    115200: 6,
28    230400: 7,
29    460800: 8,
30}
31
32
33class LCD160CR:
34    def __init__(self, connect=None, *, pwr=None, i2c=None, spi=None, i2c_addr=98):
35        if connect in ("X", "Y", "XY", "YX"):
36            i = connect[-1]
37            j = connect[0]
38            y = j + "4"
39        elif connect == "C":
40            i = 2
41            j = 2
42            y = "A7"
43        else:
44            if pwr is None or i2c is None or spi is None:
45                raise ValueError('must specify valid "connect" or all of "pwr", "i2c" and "spi"')
46
47        if pwr is None:
48            pwr = machine.Pin(y, machine.Pin.OUT)
49        if i2c is None:
50            i2c = machine.I2C(i, freq=1000000)
51        if spi is None:
52            spi = machine.SPI(j, baudrate=13500000, polarity=0, phase=0)
53
54        if not pwr.value():
55            pwr(1)
56            sleep_ms(10)
57        # else:
58        # alread have power
59        # lets be optimistic...
60
61        # set connections
62        self.pwr = pwr
63        self.i2c = i2c
64        self.spi = spi
65        self.i2c_addr = i2c_addr
66
67        # create temp buffers and memoryviews
68        self.buf16 = bytearray(16)
69        self.buf19 = bytearray(19)
70        self.buf = [None] * 10
71        for i in range(1, 10):
72            self.buf[i] = memoryview(self.buf16)[0:i]
73        self.buf1 = self.buf[1]
74        self.array4 = [0, 0, 0, 0]
75
76        # set default orientation and window
77        self.set_orient(PORTRAIT)
78        self._fcmd2b("<BBBBBB", 0x76, 0, 0, self.w, self.h)  # viewport 'v'
79        self._fcmd2b("<BBBBBB", 0x79, 0, 0, self.w, self.h)  # window 'y'
80
81    def _send(self, cmd):
82        i = self.i2c.writeto(self.i2c_addr, cmd)
83        if i == len(cmd):
84            return
85        cmd = memoryview(cmd)
86        n = len(cmd)
87        while True:
88            i += self.i2c.writeto(self.i2c_addr, cmd[i:])
89            if i == n:
90                return
91            sleep_ms(10)
92
93    def _fcmd2(self, fmt, a0, a1=0, a2=0):
94        buf = self.buf[calcsize(fmt)]
95        pack_into(fmt, buf, 0, 2, a0, a1, a2)
96        self._send(buf)
97
98    def _fcmd2b(self, fmt, a0, a1, a2, a3, a4=0):
99        buf = self.buf[calcsize(fmt)]
100        pack_into(fmt, buf, 0, 2, a0, a1, a2, a3, a4)
101        self._send(buf)
102
103    def _waitfor(self, n, buf):
104        t = 5000
105        while t:
106            self.i2c.readfrom_into(self.i2c_addr, self.buf1)
107            if self.buf1[0] >= n:
108                self.i2c.readfrom_into(self.i2c_addr, buf)
109                return
110            t -= 1
111            sleep_ms(1)
112        raise OSError(uerrno.ETIMEDOUT)
113
114    def oflush(self, n=255):
115        t = 5000
116        while t:
117            self.i2c.readfrom_into(self.i2c_addr + 1, self.buf1)
118            r = self.buf1[0]
119            if r >= n:
120                return
121            t -= 1
122            machine.idle()
123        raise OSError(uerrno.ETIMEDOUT)
124
125    def iflush(self):
126        t = 5000
127        while t:
128            self.i2c.readfrom_into(self.i2c_addr, self.buf16)
129            if self.buf16[0] == 0:
130                return
131            t -= 1
132            sleep_ms(1)
133        raise OSError(uerrno.ETIMEDOUT)
134
135    #### MISC METHODS ####
136
137    @staticmethod
138    def rgb(r, g, b):
139        return ((b & 0xF8) << 8) | ((g & 0xFC) << 3) | (r >> 3)
140
141    @staticmethod
142    def clip_line(c, w, h):
143        while True:
144            ca = ce = 0
145            if c[1] < 0:
146                ca |= 8
147            elif c[1] > h:
148                ca |= 4
149            if c[0] < 0:
150                ca |= 1
151            elif c[0] > w:
152                ca |= 2
153            if c[3] < 0:
154                ce |= 8
155            elif c[3] > h:
156                ce |= 4
157            if c[2] < 0:
158                ce |= 1
159            elif c[2] > w:
160                ce |= 2
161            if ca & ce:
162                return False
163            elif ca | ce:
164                ca |= ce
165                if ca & 1:
166                    if c[2] < c[0]:
167                        c[0], c[2] = c[2], c[0]
168                        c[1], c[3] = c[3], c[1]
169                    c[1] += ((-c[0]) * (c[3] - c[1])) // (c[2] - c[0])
170                    c[0] = 0
171                elif ca & 2:
172                    if c[2] < c[0]:
173                        c[0], c[2] = c[2], c[0]
174                        c[1], c[3] = c[3], c[1]
175                    c[3] += ((w - 1 - c[2]) * (c[3] - c[1])) // (c[2] - c[0])
176                    c[2] = w - 1
177                elif ca & 4:
178                    if c[0] == c[2]:
179                        if c[1] >= h:
180                            c[1] = h - 1
181                        if c[3] >= h:
182                            c[3] = h - 1
183                    else:
184                        if c[3] < c[1]:
185                            c[0], c[2] = c[2], c[0]
186                            c[1], c[3] = c[3], c[1]
187                        c[2] += ((h - 1 - c[3]) * (c[2] - c[0])) // (c[3] - c[1])
188                        c[3] = h - 1
189                else:
190                    if c[0] == c[2]:
191                        if c[1] < 0:
192                            c[1] = 0
193                        if c[3] < 0:
194                            c[3] = 0
195                    else:
196                        if c[3] < c[1]:
197                            c[0], c[2] = c[2], c[0]
198                            c[1], c[3] = c[3], c[1]
199                        c[0] += ((-c[1]) * (c[2] - c[0])) // (c[3] - c[1])
200                        c[1] = 0
201            else:
202                return True
203
204    #### SETUP COMMANDS ####
205
206    def set_power(self, on):
207        self.pwr(on)
208        sleep_ms(15)
209
210    def set_orient(self, orient):
211        self._fcmd2("<BBB", 0x14, (orient & 3) + 4)
212        # update width and height variables
213        self.iflush()
214        self._send(b"\x02g0")
215        self._waitfor(4, self.buf[5])
216        self.w = self.buf[5][1]
217        self.h = self.buf[5][2]
218
219    def set_brightness(self, value):
220        self._fcmd2("<BBB", 0x16, value)
221
222    def set_i2c_addr(self, addr):
223        # 0x0e set i2c addr
224        if addr & 3:
225            raise ValueError("must specify mod 4 aligned address")
226        self._fcmd2("<BBW", 0x0E, 0x433249 | (addr << 24))
227
228    def set_uart_baudrate(self, baudrate):
229        try:
230            baudrate = _uart_baud_table[baudrate]
231        except KeyError:
232            raise ValueError("invalid baudrate")
233        self._fcmd2("<BBB", 0x18, baudrate)
234
235    def set_startup_deco(self, value):
236        self._fcmd2("<BBB", 0x19, value)
237
238    def save_to_flash(self):
239        self._send(b"\x02fn")
240
241    #### PIXEL ACCESS ####
242
243    def set_pixel(self, x, y, c):
244        self._fcmd2b("<BBBBH", 0x41, x, y, c)
245
246    def get_pixel(self, x, y):
247        self._fcmd2("<BBBB", 0x61, x, y)
248        t = 1000
249        while t:
250            self.i2c.readfrom_into(self.i2c_addr, self.buf1)
251            if self.buf1[0] >= 2:
252                self.i2c.readfrom_into(self.i2c_addr, self.buf[3])
253                return self.buf[3][1] | self.buf[3][2] << 8
254            t -= 1
255            sleep_ms(1)
256        raise OSError(uerrno.ETIMEDOUT)
257
258    def get_line(self, x, y, buf):
259        l = len(buf) // 2
260        self._fcmd2b("<BBBBB", 0x10, l, x, y)
261        l *= 2
262        t = 1000
263        while t:
264            self.i2c.readfrom_into(self.i2c_addr, self.buf1)
265            if self.buf1[0] >= l:
266                self.i2c.readfrom_into(self.i2c_addr, buf)
267                return
268            t -= 1
269            sleep_ms(1)
270        raise OSError(uerrno.ETIMEDOUT)
271
272    def screen_dump(self, buf, x=0, y=0, w=None, h=None):
273        if w is None:
274            w = self.w - x
275        if h is None:
276            h = self.h - y
277        if w <= 127:
278            line = bytearray(2 * w + 1)
279            line2 = None
280        else:
281            # split line if more than 254 bytes needed
282            buflen = (w + 1) // 2
283            line = bytearray(2 * buflen + 1)
284            line2 = memoryview(line)[: 2 * (w - buflen) + 1]
285        for i in range(min(len(buf) // (2 * w), h)):
286            ix = i * w * 2
287            self.get_line(x, y + i, line)
288            buf[ix : ix + len(line) - 1] = memoryview(line)[1:]
289            ix += len(line) - 1
290            if line2:
291                self.get_line(x + buflen, y + i, line2)
292                buf[ix : ix + len(line2) - 1] = memoryview(line2)[1:]
293                ix += len(line2) - 1
294
295    def screen_load(self, buf):
296        l = self.w * self.h * 2 + 2
297        self._fcmd2b("<BBHBBB", 0x70, l, 16, self.w, self.h)
298        n = 0
299        ar = memoryview(buf)
300        while n < len(buf):
301            if len(buf) - n >= 0x200:
302                self._send(ar[n : n + 0x200])
303                n += 0x200
304            else:
305                self._send(ar[n:])
306                while n < self.w * self.h * 2:
307                    self._send(b"\x00")
308                    n += 1
309
310    #### TEXT COMMANDS ####
311
312    def set_pos(self, x, y):
313        self._fcmd2("<BBBB", 0x58, x, y)
314
315    def set_text_color(self, fg, bg):
316        self._fcmd2("<BBHH", 0x63, fg, bg)
317
318    def set_font(self, font, scale=0, bold=0, trans=0, scroll=0):
319        self._fcmd2(
320            "<BBBB",
321            0x46,
322            (scroll << 7) | (trans << 6) | ((font & 3) << 4) | (bold & 0xF),
323            scale & 0xFF,
324        )
325
326    def write(self, s):
327        # TODO: eventually check for room in LCD input queue
328        self._send(s)
329
330    #### PRIMITIVE DRAWING COMMANDS ####
331
332    def set_pen(self, line, fill):
333        self._fcmd2("<BBHH", 0x50, line, fill)
334
335    def erase(self):
336        self._send(b"\x02\x45")
337
338    def dot(self, x, y):
339        if 0 <= x < self.w and 0 <= y < self.h:
340            self._fcmd2("<BBBB", 0x4B, x, y)
341
342    def rect(self, x, y, w, h, cmd=0x72):
343        if x + w <= 0 or y + h <= 0 or x >= self.w or y >= self.h:
344            return
345        elif x < 0 or y < 0:
346            left = top = True
347            if x < 0:
348                left = False
349                w += x
350                x = 0
351            if y < 0:
352                top = False
353                h += y
354                y = 0
355            if cmd == 0x51 or cmd == 0x72:
356                # draw interior
357                self._fcmd2b("<BBBBBB", 0x51, x, y, min(w, 255), min(h, 255))
358            if cmd == 0x57 or cmd == 0x72:
359                # draw outline
360                if left:
361                    self._fcmd2b("<BBBBBB", 0x57, x, y, 1, min(h, 255))
362                if top:
363                    self._fcmd2b("<BBBBBB", 0x57, x, y, min(w, 255), 1)
364                if x + w < self.w:
365                    self._fcmd2b("<BBBBBB", 0x57, x + w, y, 1, min(h, 255))
366                if y + h < self.h:
367                    self._fcmd2b("<BBBBBB", 0x57, x, y + h, min(w, 255), 1)
368        else:
369            self._fcmd2b("<BBBBBB", cmd, x, y, min(w, 255), min(h, 255))
370
371    def rect_outline(self, x, y, w, h):
372        self.rect(x, y, w, h, 0x57)
373
374    def rect_interior(self, x, y, w, h):
375        self.rect(x, y, w, h, 0x51)
376
377    def line(self, x1, y1, x2, y2):
378        ar4 = self.array4
379        ar4[0] = x1
380        ar4[1] = y1
381        ar4[2] = x2
382        ar4[3] = y2
383        if self.clip_line(ar4, self.w, self.h):
384            self._fcmd2b("<BBBBBB", 0x4C, ar4[0], ar4[1], ar4[2], ar4[3])
385
386    def dot_no_clip(self, x, y):
387        self._fcmd2("<BBBB", 0x4B, x, y)
388
389    def rect_no_clip(self, x, y, w, h):
390        self._fcmd2b("<BBBBBB", 0x72, x, y, w, h)
391
392    def rect_outline_no_clip(self, x, y, w, h):
393        self._fcmd2b("<BBBBBB", 0x57, x, y, w, h)
394
395    def rect_interior_no_clip(self, x, y, w, h):
396        self._fcmd2b("<BBBBBB", 0x51, x, y, w, h)
397
398    def line_no_clip(self, x1, y1, x2, y2):
399        self._fcmd2b("<BBBBBB", 0x4C, x1, y1, x2, y2)
400
401    def poly_dot(self, data):
402        if len(data) & 1:
403            raise ValueError("must specify even number of bytes")
404        self._fcmd2("<BBB", 0x71, len(data) // 2)
405        self._send(data)
406
407    def poly_line(self, data):
408        if len(data) & 1:
409            raise ValueError("must specify even number of bytes")
410        self._fcmd2("<BBB", 0x78, len(data) // 2)
411        self._send(data)
412
413    #### TOUCH COMMANDS ####
414
415    def touch_config(self, calib=False, save=False, irq=None):
416        self._fcmd2("<BBBB", 0x7A, (irq is not None) << 2 | save << 1 | calib, bool(irq) << 7)
417
418    def is_touched(self):
419        self._send(b"\x02T")
420        b = self.buf[4]
421        self._waitfor(3, b)
422        return b[1] >> 7 != 0
423
424    def get_touch(self):
425        self._send(b"\x02T")  # implicit LCD output flush
426        b = self.buf[4]
427        self._waitfor(3, b)
428        return b[1] >> 7, b[2], b[3]
429
430    #### ADVANCED COMMANDS ####
431
432    def set_spi_win(self, x, y, w, h):
433        pack_into(
434            "<BBBHHHHHHHH", self.buf19, 0, 2, 0x55, 10, x, y, x + w - 1, y + h - 1, 0, 0, 0, 0xFFFF
435        )
436        self._send(self.buf19)
437
438    def fast_spi(self, flush=True):
439        self._send(b"\x02\x12")
440        if flush:
441            self.oflush()
442        return self.spi
443
444    def show_framebuf(self, buf):
445        self.fast_spi().write(buf)
446
447    def set_scroll(self, on):
448        self._fcmd2("<BBB", 0x15, on)
449
450    def set_scroll_win(self, win, x=-1, y=0, w=0, h=0, vec=0, pat=0, fill=0x07E0, color=0):
451        pack_into("<BBBHHHHHHHH", self.buf19, 0, 2, 0x55, win, x, y, w, h, vec, pat, fill, color)
452        self._send(self.buf19)
453
454    def set_scroll_win_param(self, win, param, value):
455        self._fcmd2b("<BBBBH", 0x75, win, param, value)
456
457    def set_scroll_buf(self, s):
458        l = len(s)
459        if l > 32:
460            raise ValueError("length must be 32 or less")
461        self._fcmd2("<BBB", 0x11, l)
462        self._send(s)
463
464    def jpeg_start(self, l):
465        if l > 0xFFFF:
466            raise ValueError("length must be 65535 or less")
467        self.oflush()
468        self._fcmd2("<BBH", 0x6A, l)
469
470    def jpeg_data(self, buf):
471        self._send(buf)
472
473    def jpeg(self, buf):
474        self.jpeg_start(len(buf))
475        self.jpeg_data(buf)
476
477    def feed_wdt(self):
478        self._send(b"\x02\x17")
479
480    def reset(self):
481        self._send(b"\x02Y\xef\xbe\xad\xde")
482        sleep_ms(15)
483