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".
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()