Kjetil's Information Center: A Blog About My Projects

AE-GraphicLCD-M Control Program

At the second floor of the Akizuki Denshi store in Akihabara I found a "AE-GraphicLCD-M" board in one of the bargain bins. It has a SG12232C LCD module, a PIC16F877A microcontroller, a DE-9 serial port connector and absolutely no documentation. It seems there existed a PC software to control it at some point, but this is no longer anywhere to be found. By extracting the PIC program and reverse engineering the disassembled code I was eventually able to figure out how the serial protocol works. At least the important commands that are used to store and display images on the LCD.

The "JP1 DEMO" jumper will control whether or not the board runs in "demo mode" to display images as a slide show, or to be able to accept commands. I found out that there is a tiny 32K I2C flash chip that actually holds the images, slotted 0 through 55. The EEPROM inside the PIC has information on what slots shall be displayed and for how long in the "demo mode".

AE-GraphicLCD-M


Here is a Python program that implements some of the commands I found:

#!/usr/bin/python3

import serial
import argparse
from PIL import Image

class GraphicLCD(object):
    def __init__(self, port):
        self.s = serial.Serial()
        self.s.port = port
        self.s.baudrate = 38400
        self.s.bytesize = serial.EIGHTBITS
        self.s.parity = serial.PARITY_NONE
        self.s.stopbits = serial.STOPBITS_ONE
        self.s.xonxoff = False
        self.s.rtscts = False
        self.s.dsrdtr = False
        self.s.timeout = 1

        self.s.open()
        self.s.flushInput()
        self.s.flushOutput()

    def __del__(self):
        self.s.close()

    def _image_load(self, filename):
        return Image.open(filename).resize((122,32)).convert('1')

    def _command(self, cmd):
        reply = str()
        self.s.write(b'\x1B' + cmd.encode() + b'\x0D')
        while True:
            c = self.s.read(1)
            if c == b'@':
                break
            else:
                reply += c.decode()
        return reply

    def version(self):
        return self._command("V")

    def erase(self, pattern=0x00):
        self._command("E%02X" % (pattern & 0xFF))

    def draw(self, pattern, length=1):
        self._command("I%02X%03X" % (pattern, length))
    
    def backlight(self, state):
        if state == True:
            self._command("B1")
        else:
            self._command("B0")

    def led1(self, state):
        if state == True:
            self._command("L1")
        else:
            self._command("L0")

    def display_filename(self, filename):
        img = self._image_load(filename)
        self.erase()
        for i in range(0, 488):
            b = 0
            for y in range(0, 8):
                if img.getpixel((i % 122, y + (8 * (i // 122)))) == 0:
                    b += 2 ** y
            self.draw(b)

    def read_eeprom(self):
        cmd = "YR"
        self.s.write(b'\x1B' + cmd.encode() + b'\x0D')
        data = self.s.read(56)
        c = self.s.read(1) # Consume the resulting @.
        return data

    def write_eeprom(self, data):
        if (len(data) != 56):
            raise ValueError("Expected 56 bytes of data")
        cmd = "YW"
        self.s.write(b'\x1B' + cmd.encode() + b'\x0D')
        c = self.s.read(1) # Consume the first @.
        for d in data:
            self.s.write(d.to_bytes(1, 'big'))
        c = self.s.read(1) # Consume the second @.

    def display_slot(self, slot=0):
        if (slot > 55 or slot < 0):
            raise ValueError("Invalid slot")
        self._command("D%02d" % (slot))

    def read_i2c_flash(self, address, length=64):
        cmd = "MR%04X%04X" % (address, length)
        self.s.write(b'\x1B' + cmd.encode() + b'\x0D')
        data = self.s.read(length)
        c = self.s.read(1) # Consume the resulting @.
        return data

    def read_slot(self, slot=0):
        if (slot > 55 or slot < 0):
            raise ValueError("Invalid slot")
        address = 0x1000 + (slot * 0x200)
        return self.read_i2c_flash(address, 488)

    def write_slot(self, filename, slot=0):
        if (slot > 55 or slot < 0):
            raise ValueError("Invalid slot")
        img = self._image_load(filename)
        image_data = []
        for i in range(0, 488):
            b = 0
            for y in range(0, 8):
                if img.getpixel((i % 122, y + (8 * (i // 122)))) == 0:
                    b += 2 ** y
            image_data.append(b)

        for i in range(488, 512):
            image_data.append(0xFF) # Padding.

        address = 0x1000 + (slot * 0x200)

        for i, d in enumerate(image_data):
            if (i % 64 == 0):
                cmd = "MW%04X0040" % (address + i)
                self.s.write(b'\x1B' + cmd.encode() + b'\x0D')
            self.s.write(d.to_bytes(1, 'big'))
            c = self.s.read(1) # Consume Echo.
            if (i % 64 == 63):
                c = self.s.read(1) # Consume the resulting @.

if __name__ == "__main__":
    import sys

    parser = argparse.ArgumentParser()
    parser.add_argument("-p", "--port", default="/dev/ttyS2", help="port/tty to use when connecting")
    parser.add_argument("-v", "--version", action='store_true', help="display firmware version")
    parser.add_argument("-e", "--eeprom", action='store_true', help="dump EEPROM data")
    parser.add_argument("-l", "--led", metavar='1|0', help="turn on or off LED1")
    parser.add_argument("-b", "--backlight", metavar='1|0', help="turn on or off LCD backlight")
    parser.add_argument("-i", "--image", metavar="FILENAME", help="draw and display image file")
    parser.add_argument("-d", "--display", action='store_true', help="display image in slot")
    parser.add_argument("-r", "--read", action='store_true', help="read image data from slot")
    parser.add_argument("-w", "--write", metavar="FILENAME", help="image file to write to slot")
    parser.add_argument("-s", "--slot", default=0, type=int, help="slot number to use instead of 0")
    parser.add_argument("-t", "--time", type=int, help="time each image should be shown in demo mode")
    args = parser.parse_args()

    g = GraphicLCD(args.port)

    if (args.version):
        print(g.version())
    elif (args.eeprom):
        print(g.read_eeprom())
    elif (args.led):
        g.led1(not args.led == "0")
    elif (args.backlight):
        g.backlight(not args.backlight == "0")
    elif (args.image):
        g.display_filename(args.image)
    elif (args.display):
        g.display_slot(args.slot)
    elif (args.read):
        print(g.read_slot(args.slot))
    elif (args.write):
        g.write_slot(args.write, args.slot)
    elif (args.time != None):
        g.write_eeprom(((args.time + 0x10) & 0xFF).to_bytes(1, 'big') * 56)
    else:
        parser.print_help()
          


Topic: Scripts and Code, by Kjetil @ 25/10-2024, Article Link