@@ -0,0 +1,11 @@ | |||
all: images download | |||
images: | |||
cd gallery && for F in *.jpg; do ./convert_image.sh $F $F.bmp | |||
download: jokes.json | |||
jokes.json: | |||
curl -o jokes.json https://github.com/wiz64/superfun/raw/main/database/ichd/jokes-file.json | |||
@@ -0,0 +1,124 @@ | |||
import epaper | |||
import microbmp | |||
import time | |||
import random | |||
import os | |||
import storage | |||
import gc | |||
epd_colormap = [ | |||
[0x00, 0x00, 0x00], # black | |||
[0xff, 0xff, 0xff], # white | |||
[0x00, 0x90, 0x10], # green | |||
[0x00, 0x00, 0xee], # blue | |||
[0xff, 0x00, 0x00], # red | |||
[0xff, 0xdd, 0x00], # yellow | |||
[0xff, 0x77, 0x00], # orange | |||
] | |||
max_free_height = 200 | |||
def init_display(): | |||
#print("ePaper init ", str(time.localtime())) | |||
epd = epaper.EPD_5in65() | |||
epd.fill(epd.White) | |||
return epd | |||
def draw_image(epd, filename): | |||
global epd_colormap | |||
global free_x, free_y, free_width, free_height | |||
offset_x = 0 | |||
offset_y = 0 | |||
free_x = 0 | |||
free_y = 0 | |||
free_width = 0 | |||
free_height = 0 | |||
color_map = {} | |||
def header_callback(header): | |||
#print("header callback: ", str(header)) | |||
#global epd | |||
global offset_x, offset_y | |||
global free_x, free_y, free_width, free_height | |||
global max_free_height | |||
w = header[0] | |||
h = header[1] | |||
rest_x = (epd.width - w) | |||
rest_y = (epd.height - h) | |||
free_x = 0 | |||
free_y = max(h, epd.height - max_free_height) | |||
free_width = epd.width | |||
free_height = min(rest_y, max_free_height) | |||
offset_x = rest_x//2 | |||
offset_y = max(0, rest_y - max_free_height)//2 | |||
def pixel_callback(x, y, color): | |||
#global epd | |||
global epd_colormap | |||
global offset_x, offset_y | |||
# translate color to color index | |||
color_index = 0 | |||
color_key = color[0] + color[1] << 8 + color[2] << 16 | |||
if color_key in color_map: | |||
color_index = color_map[color_key] | |||
else: | |||
# search for the best color | |||
best_index = 0 | |||
best_score = 1000 | |||
for index in range(len(epd_colormap)): | |||
c1 = epd_colormap[index] | |||
c2 = color | |||
score = abs(c1[0] - c2[0]) + abs(c1[1] - c2[1]) + abs(c1[2] - c2[2]) | |||
if score < best_score: | |||
best_score = score | |||
best_index = index | |||
if score < 10: | |||
break | |||
color_index = best_index | |||
color_map[color_key] = color_index | |||
epd.pixel(offset_x + x, offset_y + y, color_index) | |||
#print(str(time.localtime()), " BMP ", filename, " loading") | |||
time_start = time.ticks_ms() | |||
epd.fill(epd.White) | |||
microbmp.MicroBMP(header_callback=header_callback, data_callback=pixel_callback).load(filename) | |||
time_loaded = time.ticks_ms() | |||
print(" time to load: ", (time_loaded - time_start) / 1000, " s") | |||
#print(str(time.localtime()), " BMP loaded") | |||
#epd.EPD_5IN65F_Display(epd.buffer) | |||
#print(str(time.localtime()), " ePaper printed") | |||
return [free_x, free_y, free_width, free_height] | |||
def draw_pattern(epd): | |||
free_x = 0 | |||
free_y = 400 | |||
free_width = epd.width | |||
free_height = epd.height - free_y | |||
epd.fill_rect(0, 0, epd.width, free_y, random.randrange(2,7)) | |||
return [free_x, free_y, free_width, free_height] | |||
def print_text(epd, text, region, center=False): | |||
fnt = 8 | |||
x = region[0] | |||
y = region[1] | |||
w = region[2] | |||
h = region[3] | |||
if center: | |||
x = (w - len(text)*fnt) // 2 | |||
epd.text(text, x, y, epd.Black) | |||
def draw_extra(epd, region, caption): | |||
fnt = 8 | |||
region[0] += 5 | |||
region[1] += 8 | |||
if region[2] < fnt or region[3] < fnt: | |||
print("Not enough space for extra: ", str(region)) | |||
return | |||
#epd.rect(region[0], region[1], region[2], region[3], epd.Black) | |||
#region[0] += random.randrange(50) | |||
#print_text(str(time.localtime()), region, center=True) | |||
if caption is not None: | |||
#region[1] += fnt * 2 | |||
print_text(epd, caption, region) | |||
print("Caption: ", caption) | |||
else: | |||
print("No caption") | |||
@@ -0,0 +1,52 @@ | |||
import epaper | |||
import microbmp | |||
import time | |||
import random | |||
import os | |||
import storage | |||
import display | |||
import gc | |||
gc.enable() | |||
epd = display.init_display() | |||
disk = storage.Storage() | |||
while True: | |||
disk.mount() | |||
images = list(filter(lambda x : x.endswith(".bmp"), os.listdir(disk.get_root_path()))) | |||
epd.EPD_5IN65F_Init() | |||
epd.fill(epd.White) | |||
filename = random.choice(images) | |||
print("TV drawing image ", filename) | |||
free_space = [0,0,0,0] | |||
try: | |||
free_space = display.draw_image(epd, disk.get_root_path() + "/" + filename) | |||
except Exception as e: | |||
print("Failed drawing image from disk: ", e) | |||
try: | |||
free_space = display.draw_image(epd, "tiny.bmp") | |||
except Exception as e: | |||
print("Failed drawing fallback image: ", e) | |||
free_space = display.draw_pattern(epd) | |||
caption = None | |||
try: | |||
caption = storage.load_joke(disk.get_root_path()) | |||
except Exception as e: | |||
print("Failed loading a joke: ", e) | |||
try: | |||
display.draw_extra(epd, free_space, caption) | |||
except Exception as e: | |||
print("Failed drawing extra: ", e) | |||
time_render_start = time.ticks_ms() | |||
epd.EPD_5IN65F_Display(epd.buffer) | |||
time_render_stop = time.ticks_ms() | |||
print(" time to render: ", (time_render_stop - time_render_start) / 1000, " s") | |||
print("TV showing ", filename) | |||
epd.Sleep() | |||
disk.umount() | |||
print("") | |||
gc.collect() | |||
epd.delay_ms(10000 * 1) | |||
@@ -0,0 +1,307 @@ | |||
""" | |||
MicroPython driver for SD cards using SPI bus. | |||
Requires an SPI bus and a CS pin. Provides readblocks and writeblocks | |||
methods so the device can be mounted as a filesystem. | |||
Example usage on pyboard: | |||
import pyb, sdcard, os | |||
sd = sdcard.SDCard(pyb.SPI(1), pyb.Pin.board.X5) | |||
pyb.mount(sd, '/sd2') | |||
os.listdir('/') | |||
Example usage on ESP8266: | |||
import machine, sdcard, os | |||
sd = sdcard.SDCard(machine.SPI(1), machine.Pin(15)) | |||
os.mount(sd, '/sd') | |||
os.listdir('/') | |||
S.ource: https://raw.githubusercontent.com/micropython/micropython-lib/master/micropython/drivers/storage/sdcard/sdcard.py | |||
""" | |||
from micropython import const | |||
import time | |||
_CMD_TIMEOUT = const(100) | |||
_R1_IDLE_STATE = const(1 << 0) | |||
# R1_ERASE_RESET = const(1 << 1) | |||
_R1_ILLEGAL_COMMAND = const(1 << 2) | |||
# R1_COM_CRC_ERROR = const(1 << 3) | |||
# R1_ERASE_SEQUENCE_ERROR = const(1 << 4) | |||
# R1_ADDRESS_ERROR = const(1 << 5) | |||
# R1_PARAMETER_ERROR = const(1 << 6) | |||
_TOKEN_CMD25 = const(0xFC) | |||
_TOKEN_STOP_TRAN = const(0xFD) | |||
_TOKEN_DATA = const(0xFE) | |||
class SDCard: | |||
def __init__(self, spi, cs, baudrate=1320000): | |||
self.spi = spi | |||
self.cs = cs | |||
self.cmdbuf = bytearray(6) | |||
self.dummybuf = bytearray(512) | |||
self.tokenbuf = bytearray(1) | |||
for i in range(512): | |||
self.dummybuf[i] = 0xFF | |||
self.dummybuf_memoryview = memoryview(self.dummybuf) | |||
# initialise the card | |||
self.init_card(baudrate) | |||
def init_spi(self, baudrate): | |||
try: | |||
master = self.spi.MASTER | |||
except AttributeError: | |||
# on ESP8266 | |||
self.spi.init(baudrate=baudrate, phase=0, polarity=0) | |||
else: | |||
# on pyboard | |||
self.spi.init(master, baudrate=baudrate, phase=0, polarity=0) | |||
def init_card(self, baudrate): | |||
# init CS pin | |||
self.cs.init(self.cs.OUT, value=1) | |||
# init SPI bus; use low data rate for initialisation | |||
self.init_spi(100000) | |||
# clock card at least 100 cycles with cs high | |||
for i in range(16): | |||
self.spi.write(b"\xff") | |||
# CMD0: init card; should return _R1_IDLE_STATE (allow 5 attempts) | |||
for _ in range(5): | |||
if self.cmd(0, 0, 0x95) == _R1_IDLE_STATE: | |||
break | |||
else: | |||
raise OSError("no SD card") | |||
# CMD8: determine card version | |||
r = self.cmd(8, 0x01AA, 0x87, 4) | |||
if r == _R1_IDLE_STATE: | |||
self.init_card_v2() | |||
elif r == (_R1_IDLE_STATE | _R1_ILLEGAL_COMMAND): | |||
self.init_card_v1() | |||
else: | |||
raise OSError("couldn't determine SD card version") | |||
# get the number of sectors | |||
# CMD9: response R2 (R1 byte + 16-byte block read) | |||
if self.cmd(9, 0, 0, 0, False) != 0: | |||
raise OSError("no response from SD card") | |||
csd = bytearray(16) | |||
self.readinto(csd) | |||
if csd[0] & 0xC0 == 0x40: # CSD version 2.0 | |||
self.sectors = ((csd[8] << 8 | csd[9]) + 1) * 1024 | |||
elif csd[0] & 0xC0 == 0x00: # CSD version 1.0 (old, <=2GB) | |||
c_size = (csd[6] & 0b11) << 10 | csd[7] << 2 | csd[8] >> 6 | |||
c_size_mult = (csd[9] & 0b11) << 1 | csd[10] >> 7 | |||
read_bl_len = csd[5] & 0b1111 | |||
capacity = (c_size + 1) * (2 ** (c_size_mult + 2)) * (2**read_bl_len) | |||
self.sectors = capacity // 512 | |||
else: | |||
raise OSError("SD card CSD format not supported") | |||
# print('sectors', self.sectors) | |||
# CMD16: set block length to 512 bytes | |||
if self.cmd(16, 512, 0) != 0: | |||
raise OSError("can't set 512 block size") | |||
# set to high data rate now that it's initialised | |||
self.init_spi(baudrate) | |||
def init_card_v1(self): | |||
for i in range(_CMD_TIMEOUT): | |||
time.sleep_ms(50) | |||
self.cmd(55, 0, 0) | |||
if self.cmd(41, 0, 0) == 0: | |||
# SDSC card, uses byte addressing in read/write/erase commands | |||
self.cdv = 512 | |||
# print("[SDCard] v1 card") | |||
return | |||
raise OSError("timeout waiting for v1 card") | |||
def init_card_v2(self): | |||
for i in range(_CMD_TIMEOUT): | |||
time.sleep_ms(50) | |||
self.cmd(58, 0, 0, 4) | |||
self.cmd(55, 0, 0) | |||
if self.cmd(41, 0x40000000, 0) == 0: | |||
self.cmd(58, 0, 0, -4) # 4-byte response, negative means keep the first byte | |||
ocr = self.tokenbuf[0] # get first byte of response, which is OCR | |||
if not ocr & 0x40: | |||
# SDSC card, uses byte addressing in read/write/erase commands | |||
self.cdv = 512 | |||
else: | |||
# SDHC/SDXC card, uses block addressing in read/write/erase commands | |||
self.cdv = 1 | |||
# print("[SDCard] v2 card") | |||
return | |||
raise OSError("timeout waiting for v2 card") | |||
def cmd(self, cmd, arg, crc, final=0, release=True, skip1=False): | |||
self.cs(0) | |||
# create and send the command | |||
buf = self.cmdbuf | |||
buf[0] = 0x40 | cmd | |||
buf[1] = arg >> 24 | |||
buf[2] = arg >> 16 | |||
buf[3] = arg >> 8 | |||
buf[4] = arg | |||
buf[5] = crc | |||
self.spi.write(buf) | |||
if skip1: | |||
self.spi.readinto(self.tokenbuf, 0xFF) | |||
# wait for the response (response[7] == 0) | |||
for i in range(_CMD_TIMEOUT): | |||
self.spi.readinto(self.tokenbuf, 0xFF) | |||
response = self.tokenbuf[0] | |||
if not (response & 0x80): | |||
# this could be a big-endian integer that we are getting here | |||
# if final<0 then store the first byte to tokenbuf and discard the rest | |||
if final < 0: | |||
self.spi.readinto(self.tokenbuf, 0xFF) | |||
final = -1 - final | |||
for j in range(final): | |||
self.spi.write(b"\xff") | |||
if release: | |||
self.cs(1) | |||
self.spi.write(b"\xff") | |||
return response | |||
# timeout | |||
self.cs(1) | |||
self.spi.write(b"\xff") | |||
return -1 | |||
def readinto(self, buf): | |||
self.cs(0) | |||
# read until start byte (0xff) | |||
for i in range(_CMD_TIMEOUT): | |||
self.spi.readinto(self.tokenbuf, 0xFF) | |||
if self.tokenbuf[0] == _TOKEN_DATA: | |||
break | |||
time.sleep_ms(1) | |||
else: | |||
self.cs(1) | |||
raise OSError("timeout waiting for response") | |||
# read data | |||
mv = self.dummybuf_memoryview | |||
if len(buf) != len(mv): | |||
mv = mv[: len(buf)] | |||
self.spi.write_readinto(mv, buf) | |||
# read checksum | |||
self.spi.write(b"\xff") | |||
self.spi.write(b"\xff") | |||
self.cs(1) | |||
self.spi.write(b"\xff") | |||
def write(self, token, buf): | |||
self.cs(0) | |||
# send: start of block, data, checksum | |||
self.spi.read(1, token) | |||
self.spi.write(buf) | |||
self.spi.write(b"\xff") | |||
self.spi.write(b"\xff") | |||
# check the response | |||
if (self.spi.read(1, 0xFF)[0] & 0x1F) != 0x05: | |||
self.cs(1) | |||
self.spi.write(b"\xff") | |||
return | |||
# wait for write to finish | |||
while self.spi.read(1, 0xFF)[0] == 0: | |||
pass | |||
self.cs(1) | |||
self.spi.write(b"\xff") | |||
def write_token(self, token): | |||
self.cs(0) | |||
self.spi.read(1, token) | |||
self.spi.write(b"\xff") | |||
# wait for write to finish | |||
while self.spi.read(1, 0xFF)[0] == 0x00: | |||
pass | |||
self.cs(1) | |||
self.spi.write(b"\xff") | |||
def readblocks(self, block_num, buf): | |||
# workaround for shared bus, required for (at least) some Kingston | |||
# devices, ensure MOSI is high before starting transaction | |||
self.spi.write(b"\xff") | |||
nblocks = len(buf) // 512 | |||
assert nblocks and not len(buf) % 512, "Buffer length is invalid" | |||
if nblocks == 1: | |||
# CMD17: set read address for single block | |||
if self.cmd(17, block_num * self.cdv, 0, release=False) != 0: | |||
# release the card | |||
self.cs(1) | |||
raise OSError(5) # EIO | |||
# receive the data and release card | |||
self.readinto(buf) | |||
else: | |||
# CMD18: set read address for multiple blocks | |||
if self.cmd(18, block_num * self.cdv, 0, release=False) != 0: | |||
# release the card | |||
self.cs(1) | |||
raise OSError(5) # EIO | |||
offset = 0 | |||
mv = memoryview(buf) | |||
while nblocks: | |||
# receive the data and release card | |||
self.readinto(mv[offset : offset + 512]) | |||
offset += 512 | |||
nblocks -= 1 | |||
if self.cmd(12, 0, 0xFF, skip1=True): | |||
raise OSError(5) # EIO | |||
def writeblocks(self, block_num, buf): | |||
# workaround for shared bus, required for (at least) some Kingston | |||
# devices, ensure MOSI is high before starting transaction | |||
self.spi.write(b"\xff") | |||
nblocks, err = divmod(len(buf), 512) | |||
assert nblocks and not err, "Buffer length is invalid" | |||
if nblocks == 1: | |||
# CMD24: set write address for single block | |||
if self.cmd(24, block_num * self.cdv, 0) != 0: | |||
raise OSError(5) # EIO | |||
# send the data | |||
self.write(_TOKEN_DATA, buf) | |||
else: | |||
# CMD25: set write address for first block | |||
if self.cmd(25, block_num * self.cdv, 0) != 0: | |||
raise OSError(5) # EIO | |||
# send the data | |||
offset = 0 | |||
mv = memoryview(buf) | |||
while nblocks: | |||
self.write(_TOKEN_CMD25, mv[offset : offset + 512]) | |||
offset += 512 | |||
nblocks -= 1 | |||
self.write_token(_TOKEN_STOP_TRAN) | |||
def ioctl(self, op, arg): | |||
if op == 4: # get number of blocks | |||
return self.sectors | |||
if op == 5: # get block size in bytes | |||
return 512 |
@@ -0,0 +1,47 @@ | |||
import machine | |||
import sdcard | |||
import uos | |||
import random | |||
class Storage: | |||
def __init__(self): | |||
self.cs = machine.Pin(17, machine.Pin.OUT) | |||
self.spi = machine.SPI(0, | |||
baudrate=1000000, | |||
polarity=0, | |||
phase=0, | |||
bits=8, | |||
firstbit=machine.SPI.MSB, | |||
sck=machine.Pin(18), | |||
mosi=machine.Pin(19), | |||
miso=machine.Pin(16)) | |||
self.sd = None | |||
self.vfs = None | |||
self.sd_path = "/sd" | |||
def mount(self): | |||
self.sd = sdcard.SDCard(self.spi, self.cs) | |||
self.vfs = uos.VfsFat(self.sd) | |||
uos.mount(self.vfs, self.sd_path) | |||
def umount(self): | |||
uos.umount(self.sd_path) | |||
def get_root_path(self): | |||
return self.sd_path | |||
def load_joke(root): | |||
filename = root + "/jokes.json" | |||
num_lines = sum(1 for line in open(filename)) | |||
with open(filename, mode="r") as f: | |||
joke = None | |||
start_pattern = "\"joke\": \"" | |||
end_pattern = "\"" | |||
for i in range(random.randrange(num_lines)): | |||
line = f.readline() | |||
for i in range(10): | |||
line = f.readline() | |||
joke_start = line.find(start_pattern) | |||
joke_end = line.rfind(end_pattern) | |||
if joke_start >= 0 and joke_end >= 0: | |||
joke = line[joke_start+len(start_pattern):joke_end] | |||
return joke | |||
return None |
@@ -3,6 +3,7 @@ import microbmp | |||
import time | |||
import random | |||
import os | |||
import storage | |||
import gc | |||
gc.enable() | |||
@@ -18,7 +19,6 @@ epd_colormap = [ | |||
[0xff, 0x77, 0x00], # orange | |||
] | |||
images = list(filter(lambda x : x.endswith(".bmp"), os.listdir())) | |||
max_free_height = 200 | |||
def init_display(): | |||
@@ -107,6 +107,25 @@ def draw_pattern(): | |||
epd.fill_rect(0, 0, epd_resolution[0], free_y, random.randrange(2,7)) | |||
return [free_x, free_y, free_width, free_height] | |||
def load_joke(root): | |||
filename = root + "/jokes.json" | |||
num_lines = sum(1 for line in open(filename)) | |||
with open(filename, mode="r") as f: | |||
joke = None | |||
start_pattern = "\"joke\": \"" | |||
end_pattern = "\"" | |||
for i in range(random.randrange(num_lines)): | |||
line = f.readline() | |||
for i in range(10): | |||
line = f.readline() | |||
joke_start = line.find(start_pattern) | |||
joke_end = line.rfind(end_pattern) | |||
if joke_start >= 0 and joke_end >= 0: | |||
joke = line[joke_start+len(start_pattern):joke_end] | |||
return joke | |||
return None | |||
def print_text(text, region, center=False): | |||
global epd | |||
fnt = 8 | |||
@@ -118,40 +137,61 @@ def print_text(text, region, center=False): | |||
x = (w - len(text)*fnt) // 2 | |||
epd.text(text, x, y, epd.Black) | |||
def draw_extra(region): | |||
def draw_extra(region, caption): | |||
fnt = 8 | |||
region[0] += 5 | |||
region[1] += 5 | |||
region[1] += 8 | |||
if region[2] < fnt or region[3] < fnt: | |||
print("Not enough space for extra: ", str(region)) | |||
return | |||
#epd.rect(region[0], region[1], region[2], region[3], epd.Black) | |||
#region[0] += random.randrange(50) | |||
print_text(str(time.localtime()), region, center=True) | |||
region[1] += fnt * 3 | |||
print_text(" Today: Rainy, 24 C", region) | |||
region[1] += fnt * 2 | |||
print_text("Tomorrow: Sunshine, 26 C", region) | |||
#print_text(str(time.localtime()), region, center=True) | |||
if caption is not None: | |||
#region[1] += fnt * 2 | |||
print_text(caption, region) | |||
print("Caption: ", caption) | |||
else: | |||
print("No caption") | |||
# MAIN | |||
epd = init_display() | |||
while True: | |||
disk = storage.Storage() | |||
while True: | |||
disk.mount() | |||
images = list(filter(lambda x : x.endswith(".bmp"), os.listdir(disk.get_root_path()))) | |||
epd.EPD_5IN65F_Init() | |||
epd.fill(epd.White) | |||
filename = random.choice(images) | |||
print("TV drawing image ", filename) | |||
free_space = draw_image(filename) | |||
#free_space = draw_pattern() | |||
draw_extra(free_space) | |||
free_space = [0,0,0,0] | |||
try: | |||
free_space = draw_image(disk.get_root_path() + "/" + filename) | |||
except Exception as e: | |||
print("Failed drawing image from disk: ", e) | |||
try: | |||
free_space = draw_image("tiny.bmp") | |||
except Exception as e: | |||
print("Failed drawing fallback image: ", e) | |||
free_space = draw_pattern() | |||
caption = None | |||
try: | |||
caption = load_joke(disk.get_root_path()) | |||
except Exception as e: | |||
print("Failed loading a joke: ", e) | |||
try: | |||
draw_extra(free_space, caption) | |||
except Exception as e: | |||
print("Failed drawing extra: ", e) | |||
time_render_start = time.ticks_ms() | |||
epd.EPD_5IN65F_Display(epd.buffer) | |||
time_render_stop = time.ticks_ms() | |||
print(" time to render: ", (time_render_stop - time_render_start) / 1000, " s") | |||
print("TV showing ", filename) | |||
epd.Sleep() | |||
disk.umount() | |||
print("") | |||
gc.collect() | |||