diff --git a/modules/config.py b/modules/config.py index 48d27823..f510fb38 100644 --- a/modules/config.py +++ b/modules/config.py @@ -24,15 +24,6 @@ ) from modules.utils.timer import Timer -_IS_RASPI = False -try: - import RPi.GPIO as GPIO - - GPIO.setmode(GPIO.BCM) - _IS_RASPI = True -except ImportError: - pass - class Config: ####################### @@ -75,7 +66,7 @@ class Config: G_VERSION_MAJOR = 0 # need to be initialized G_VERSION_MINOR = 1 # need to be initialized G_UNIT_ID = "0000000000000000" # initialized in get_serial - G_UNIT_ID_HEX = 0x1A2B3C4D # initialized in get_serial + G_UNIT_ID_HEX = 0x00000000 # initialized in get_serial G_UNIT_MODEL = "" G_UNIT_HARDWARE = "" @@ -224,7 +215,9 @@ class Config: G_FULLSCREEN = False # display type (overwritten with setting.conf) - G_DISPLAY = "None" # PiTFT, MIP, MIP_640, Papirus, MIP_Sharp, MIP_Sharp_320, DFRobot_RPi_Display + # PiTFT, MIP, MIP_640, MIP_Mraa, MIP_Mraa_640, MIP_Sharp, MIP_Sharp_320, + # Papirus, DFRobot_RPi_Display, Pirate_Audio, Pirate_Audio_old(Y button is GPIO 20), Display_HAT_Mini + G_DISPLAY = "None" G_DISPLAY_PARAM = { "SPI_CLOCK": 2000000, @@ -400,8 +393,15 @@ class Config: def __init__(self): # Raspbian OS detection - if _IS_RASPI: - self.G_IS_RASPI = True + proc_model = "/proc/device-tree/model" + if os.path.exists(proc_model) and os.path.exists(proc_model): + with open(proc_model) as f: + p = f.read() + if p.find("Raspberry Pi") == 0: + self.G_IS_RASPI = True + elif p.find("Radxa Zero") == 0: + self.G_IS_RASPI = True + self.G_DISPLAY_PARAM["SPI_CLOCK"] = 10000000 # get options parser = argparse.ArgumentParser() diff --git a/modules/display/display_core.py b/modules/display/display_core.py index e7571c9a..a1ee16d0 100644 --- a/modules/display/display_core.py +++ b/modules/display/display_core.py @@ -10,10 +10,15 @@ "PiTFT": None, "MIP": None, # LPM027M128C, LPM027M128B "MIP_640": (640, 480), # LPM044M141A + "MIP_Mraa": None, # LPM027M128C, LPM027M128B + "MIP_Mraa_640": (640, 480), # LPM044M141A "MIP_Sharp": None, "MIP_Sharp_320": (320, 240), "Papirus": None, "DFRobot_RPi_Display": None, + "Pirate_Audio": None, + "Pirate_Audio_old": None, + "Display_HAT_Mini": (320, 240), } @@ -130,11 +135,16 @@ def init_display(config): if _SENSOR_DISPLAY: display = MipDisplay(config, SUPPORTED_DISPLAYS[config.G_DISPLAY]) - elif config.G_DISPLAY in ("MIP_Sharp", "MIP_Sharp_320"): + elif config.G_DISPLAY.startswith("MIP_Sharp"): from .mip_sharp_display import _SENSOR_DISPLAY, MipSharpDisplay if _SENSOR_DISPLAY: display = MipSharpDisplay(config, SUPPORTED_DISPLAYS[config.G_DISPLAY]) + elif config.G_DISPLAY.startswith("MIP_Mraa"): + from .mip_mraa_display import _SENSOR_DISPLAY, MipMraaDisplay + + if _SENSOR_DISPLAY: + display = MipMraaDisplay(config, SUPPORTED_DISPLAYS[config.G_DISPLAY]) elif config.G_DISPLAY == "Papirus": from .papirus_display import _SENSOR_DISPLAY, PapirusDisplay @@ -145,5 +155,13 @@ def init_display(config): if _SENSOR_DISPLAY: display = DFRobotRPiDisplay(config) + elif config.G_DISPLAY.startswith("Pirate_Audio") or config.G_DISPLAY == "Display_HAT_Mini": + from .st7789_display import _SENSOR_DISPLAY, ST7789Display + + if _SENSOR_DISPLAY: + if config.G_DISPLAY.startswith("Pirate_Audio"): + display = ST7789Display(config) + elif config.G_DISPLAY == "Display_HAT_Mini": + display = ST7789Display(config, SUPPORTED_DISPLAYS[config.G_DISPLAY]) return display diff --git a/modules/display/mip_mraa_display.py b/modules/display/mip_mraa_display.py new file mode 100644 index 00000000..012de1dd --- /dev/null +++ b/modules/display/mip_mraa_display.py @@ -0,0 +1,303 @@ +import time + +import asyncio +import numpy as np + +from logger import app_logger +from .display_core import Display + +_SENSOR_DISPLAY = False +MODE = "Python" +try: + import mraa + + _SENSOR_DISPLAY = True + + #import pyximport + #pyximport.install() + #from .cython.mip_helper import conv_3bit_color + + #MODE = "Cython" +except ImportError: + pass + +app_logger.info(f"MIP_Mraa DISPLAY: {_SENSOR_DISPLAY}") + +# https://qiita.com/hishi/items/669ce474fcd76bdce1f1 +# LPM027M128C, LPM027M128B, + +# GPIO.BCM +GPIO_DISP = 13 #27 # 13 in GPIO.BOARD +GPIO_SCS = 16 #23 # 16 in GPIO.BOARD +GPIO_VCOMSEL = 11 #17 # 11 in GPIO.BOARD +# GPIO_BACKLIGHT = 18 # 12 in GPIO.BOARD with hardware PWM in pigpio + +# update mode +# https://www.j-display.com/product/pdf/Datasheet/3LPM027M128C_specification_ver02.pdf +# 0x90 4bit update mode +# 0x80 3bit update mode (fast) +# 0x88 1bit update mode (most fast, but 2-color) +UPDATE_MODE = 0x80 + +# BACKLIGHT frequency +# GPIO_BACKLIGHT_FREQ = 64 + + +class MipMraaDisplay(Display): + pi = None + spi = None + interval = 0.25 + mip_display_cpp = None + + has_auto_brightness = True + has_touch = False + send = True + + brightness_table = [0, 1, 2, 3, 5, 7, 10, 25, 50, 100] + brightness = 0 + + dithering_cutoff = { + "LOW": [128, 150, 170], + "HIGH": [170, 193, 216], + } + dithering_cutoff_low_index = 0 + dithering_cutoff_high_index = 2 + + size = (400, 240) + + def __init__(self, config, size=None): + super().__init__(config) + + if size: + self.size = size + + #if MODE == "Cython": + # self.conv_color = conv_3bit_color + #else: + self.conv_color = self.conv_3bit_color_py + + self.init_buffer() + + # spi + self.spi = mraa.Spi(1) + self.spi.mode(0) + self.spi.frequency(config.G_DISPLAY_PARAM["SPI_CLOCK"]) + + self.gpio = {} + for key in [GPIO_DISP, GPIO_SCS, GPIO_VCOMSEL]: + self.gpio[key] = mraa.Gpio(key) + self.gpio[key].dir(mraa.DIR_OUT) + + self.gpio[GPIO_SCS].write(0) + self.gpio[GPIO_DISP].write(1) + self.gpio[GPIO_VCOMSEL].write(1) + + time.sleep(0.01) + + # backlight + # self.pi.set_mode(GPIO_BACKLIGHT, pigpio.OUTPUT) + # self.pi.hardware_PWM(GPIO_BACKLIGHT, GPIO_BACKLIGHT_FREQ, 0) + + def init_buffer(self): + self.buff_width = int(self.size[0] * 3 / 8) + 2 # for 3bit update mode + self.img_buff_rgb8 = np.empty((self.size[1], self.buff_width), dtype="uint8") + self.pre_img = np.zeros((self.size[1], self.buff_width), dtype="uint8") + self.img_buff_rgb8[:, 0] = UPDATE_MODE + self.img_buff_rgb8[:, 1] = np.arange(self.size[1]) + # for MIP_640 + self.img_buff_rgb8[:, 0] = self.img_buff_rgb8[:, 0] + ( + np.arange(self.size[1]) >> 8 + ) + + def start_coroutine(self): + self.draw_queue = asyncio.Queue() + asyncio.create_task(self.draw_worker()) + + def clear(self): + self.gpio[GPIO_SCS].write(1) + time.sleep(0.000006) + self.spi.write(bytearray([0b00100000, 0])) # ALL CLEAR MODE + self.gpio[GPIO_SCS].write(0) + time.sleep(0.000006) + self.set_brightness(0) + + def no_update(self): + self.gpio[GPIO_SCS].write(1) + time.sleep(0.000006) + self.spi.write(bytearray([0b00000000, 0])) # NO UPDATE MODE + self.gpio[GPIO_SCS].write(0) + time.sleep(0.000006) + + def blink(self, sec): + s = sec + state = True + while s > 0: + self.gpio[GPIO_SCS].write(1) + time.sleep(0.000006) + if state: + self.spi.write(bytearray([0b00010000, 0])) # BLINK(BLACK) MODE + else: + self.spi.write(bytearray([0b00011000, 0])) # BLINK(WHITE) MODE + self.gpio[GPIO_SCS].write(0) + time.sleep(self.interval) + s -= self.interval + state = not state + self.no_update() + + def inversion(self, sec): + s = sec + state = True + while s > 0: + self.gpio[GPIO_SCS].write(1) + time.sleep(0.000006) + if state: + self.spi.write(bytearray([0b00010100, 0])) # INVERSION MODE + else: + self.no_update() + self.gpio[GPIO_SCS].write(0) + time.sleep(self.interval) + s -= self.interval + state = not state + self.no_update() + + async def draw_worker(self): + while True: + img_bytes = await self.draw_queue.get() + if img_bytes is None: + break + # self.config.check_time("mip_draw_worker start") + # t = datetime.datetime.now() + self.gpio[GPIO_SCS].write(1) + await asyncio.sleep(0.000006) + self.spi.write(bytearray(img_bytes)) + # dummy output for ghost line + self.spi.write(bytearray([0x00000000, 0])) + await asyncio.sleep(0.000006) + self.gpio[GPIO_SCS].write(0) + # self.config.check_time("mip_draw_worker end") + # print("####### draw(Py)", (datetime.datetime.now()-t).total_seconds()) + self.draw_queue.task_done() + + def update(self, im_array, direct_update): + # direct update not yet supported for MPI_640 + if direct_update and self.config.G_DISPLAY in ("MIP_Mraa_640",): + direct_update = False + + # self.config.check_time("mip_update start") + self.img_buff_rgb8[:, 2:] = self.conv_color(im_array) + # self.config.check_time("packbits") + + # differential update + diff_lines = np.where( + np.sum((self.img_buff_rgb8 == self.pre_img), axis=1) != self.buff_width + )[0] + # print("diff ", int(len(diff_lines)/self.size[1]*100), "%") + # print(" ") + + if not len(diff_lines): + return + self.pre_img[diff_lines] = self.img_buff_rgb8[diff_lines] + # self.config.check_time("diff_lines") + + if direct_update: + self.gpio[GPIO_SCS].write(1) + time.sleep(0.000006) + self.spi.write(bytearray(self.img_buff_rgb8[diff_lines].tobytes())) + time.sleep(0.000006) + self.gpio[GPIO_SCS].write(0) + # put queue + elif len(diff_lines) < 270: + # await self.draw_queue.put((self.img_buff_rgb8[diff_lines].tobytes())) + asyncio.create_task( + self.draw_queue.put((self.img_buff_rgb8[diff_lines].tobytes())) + ) + else: + # for MIP 640x480 + l = int(len(diff_lines) / 2) + # await self.draw_queue.put((self.img_buff_rgb8[diff_lines[0:l]].tobytes())) + # await self.draw_queue.put((self.img_buff_rgb8[diff_lines[l:]].tobytes())) + asyncio.create_task( + self.draw_queue.put((self.img_buff_rgb8[diff_lines[0:l]].tobytes())) + ) + asyncio.create_task( + self.draw_queue.put((self.img_buff_rgb8[diff_lines[l:]].tobytes())) + ) + + def conv_2bit_color_py(self, im_array): + return np.packbits( + (im_array >= 128).reshape(self.size[1], self.size[0] * 3), + axis=1, + ) + + def conv_3bit_color_py(self, im_array): + # pseudo 3bit color (128~216: simple dithering) + # set even pixel and odd pixel to 0 + # 1. convert 2bit color + # im_array_bin = (im_array >= 128) + + # 2. set even pixel (2n, 2n) to 0 + # im_array_bin[0::2, 0::2, :][im_array[0::2, 0::2, :] <= 216] = 0 + # 3. set odd pixel (2n+1, 2n+1) to 0 + # im_array_bin[1::2, 1::2, :][im_array[1::2, 1::2, :] <= 216] = 0 + + im_array_bin = np.zeros(im_array.shape).astype("bool") + im_array_bin[0::2, 0::2, :][ + im_array[0::2, 0::2, :] + >= self.dithering_cutoff["LOW"][ + self.dithering_cutoff_low_index + ] + ] = 1 + im_array_bin[1::2, 1::2, :][ + im_array[1::2, 1::2, :] + >= self.dithering_cutoff["LOW"][ + self.dithering_cutoff_low_index + ] + ] = 1 + im_array_bin[0::2, 1::2, :][ + im_array[0::2, 1::2, :] + > self.dithering_cutoff["HIGH"][ + self.dithering_cutoff_high_index + ] + ] = 1 + im_array_bin[1::2, 0::2, :][ + im_array[1::2, 0::2, :] + > self.dithering_cutoff["HIGH"][ + self.dithering_cutoff_high_index + ] + ] = 1 + + return np.packbits(im_array_bin.reshape(self.size[1], self.size[0] * 3), axis=1) + + def set_brightness(self, b): + pass + #if b == self.brightness: + # return + #self.pi.hardware_PWM(GPIO_BACKLIGHT, GPIO_BACKLIGHT_FREQ, b * 10000) + #self.brightness = b + + def backlight_blink(self): + pass + #for x in range(2): + # for pw in range(0, 100, 1): + # self.pi.hardware_PWM(GPIO_BACKLIGHT, GPIO_BACKLIGHT_FREQ, pw * 10000) + # time.sleep(0.05) + # for pw in range(100, 0, -1): + # self.pi.hardware_PWM(GPIO_BACKLIGHT, GPIO_BACKLIGHT_FREQ, pw * 10000) + # time.sleep(0.05) + + def quit(self): + asyncio.create_task(self.draw_queue.put(None)) + self.set_brightness(0) + self.clear() + + self.gpio[GPIO_DISP].write(1) + time.sleep(0.01) + + #self.pi.spi_close(self.spi) + #self.pi.stop() + + def screen_flash_long(self): + return self.inversion(0.8) + + def screen_flash_short(self): + return self.inversion(0.3) diff --git a/modules/pyqt/graph/pyqt_map.py b/modules/pyqt/graph/pyqt_map.py index 05f6092f..8e15703a 100644 --- a/modules/pyqt/graph/pyqt_map.py +++ b/modules/pyqt/graph/pyqt_map.py @@ -774,7 +774,7 @@ async def draw_map_tile_by_overlay( )).convert("RGBA") if ( - self.config.G_DISPLAY in ("MIP", "MIP_640") + self.config.G_DISPLAY in ("MIP", "MIP_640", "MIP_Mraa", "MIP_Mraa_640") and ( map_name.startswith("jpn_scw") or map_name.startswith("jpn_jma_bousai")