-
Notifications
You must be signed in to change notification settings - Fork 0
/
generate_firmwares.py
261 lines (216 loc) · 8.24 KB
/
generate_firmwares.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
import os
import sys
import math
from PIL import Image
import random
import json
sys.path.append(os.path.join(sys.path[0], 'third_party'))
import cocas
# This code is badly structured because it was merged from three different files
# Yes, I know about midules
def write_image(filename: str, arr: list):
"""
Write the contents or array into file in logisim-compatible format
:param filename: Path to output file
:param arr: Array to be written
"""
f = open(filename, mode='wb')
f.write(bytes("v2.0 raw\n", 'UTF-8'))
for byte in arr:
f.write(bytes('%08x' % byte + '\n', 'UTF-8'))
f.close()
def asm():
"""
Compile ai.asm and write resulting code to firmware/ai.img
"""
cwd = os.getcwd()
os.chdir(os.path.join(cwd, 'third_party'))
with open('../ai.asm') as asm_file:
# we use cocas from CocoIDE distribution
obj_code, _, err_msg = cocas.compile_asm(asm_file) # type: (str, str, str)
os.chdir(cwd)
if err_msg is not None:
print("COMPILE ERROR: ", err_msg)
sys.exit(-1)
rom = [0] * 128
# this primitive linker ignores code overlapping
for line in obj_code.splitlines():
if 'ABS' in line:
mem_addr, mem_content = line.removeprefix("ABS").strip().split(':')
mem_addr = int(mem_addr, 16)
mem_content = list(map(lambda a: int(a, 16), mem_content.split())) # type: [int]
for byte in mem_content:
if mem_addr > 127:
print(f'WARNING: byte {hex(byte)} at address {hex(mem_addr)} does not fit into ROM')
continue
rom[mem_addr] = byte
mem_addr += 1
write_image('firmware/ai.img', rom)
def sin_cos():
"""
Write sin(a) and cos(a) for 127 different values of a in [0; max_angle]
into corresponding roms
"""
speed = 1
max_angle = 0.906 # max angle that does not cause overflows (~ 52 deg)
sins = []
for i in range(0, 128):
s = math.sin(i / 127 * max_angle) * speed
sins.append(round(s * 128))
write_image('firmware/sin.img', sins)
cos = []
for i in range(0, 128):
s = math.cos(i / 127 * max_angle) * speed
# print(round(s*128))
cos.append(round(s * 128))
write_image('firmware/cos.img', cos)
def animations():
"""
Create startup and game over animations
convert them into binary format that video_player chip uses
and write it to firmware/animation.img
"""
os.chdir(os.path.join(os.getcwd(), 'animation'))
# Needed for fade effect to be the same every run
random.seed(228)
def process(imgs: [Image]) -> list[int]:
"""
Convert list of images into animation for video chip.
Every image is represented as 32 32-bit integers,
where each integer represents one column on display
:param imgs: List of PIL images
:return: List of 32 bit numbers
"""
arr = []
for img in imgs:
for col in range(32):
i = 0
for row in range(32):
pixel = img.getpixel((col, row))
if pixel != 0:
i = i | (1 << (31 - row))
arr.append(i)
return arr
def new_img() -> Image:
"""
:return: Empty white image
"""
return Image.new("1", (32, 32), 255) # type: Image.Image
def file_image(s: str) -> Image:
"""
:param s: Path to image file
:return: Image from file converted to 1-bit format
"""
return Image.open(s).convert('1')
def scroll_down(a: Image, b: Image, frames: int) -> [Image]:
"""
Create sliding transition from first image to second.
Resulting animation will take specified amount of frames (not really accurate)
:param a: First image
:param b: Second Image
:param frames: Number of frames
:return: List of images - resulting animation
"""
res = []
for d in range(0, 33, round(32 / frames)):
ni = new_img()
ni.paste(a, (0, -d))
ni.paste(b, (0, 32 - d))
res.append(ni)
return res
def still_image(a: Image, frames: int) -> [Image]:
"""Repeat still image for specified amount of frames"""
return [a.copy() for _ in range(frames)]
def fade(a: Image, b: Image, frames: int):
"""
Create fading transition from first image to second.
Resulting animation will take specified amount of frames (not really accurate)
:param a: First image
:param b: Second Image
:param frames: Number of frames
:return: List of images - resulting animation
"""
# we only need to change pixels with different colors
different_pixels = []
for x in range(32):
for y in range(32):
if a.getpixel((x, y)) != b.getpixel((x, y)):
different_pixels.append((x, y))
# we will change them in random order
random.shuffle(different_pixels)
im = a.copy()
ret = [a.copy()]
for i in range(frames - 1):
# Each frame we will change equal amount of pixels
to_take = len(different_pixels) // (frames - 1)
# last frame is special case - since to_take was rounded down, there will be more pixels
# so we change all remaining pixels
to_process = different_pixels[i * to_take:(i + 1) * to_take] if i != frames - 1 else different_pixels[
i * to_take:]
for pos in to_process:
im.putpixel(pos, b.getpixel(pos))
ret.append(im.copy())
ret.append(b.copy())
return ret
def generate_intro():
"""Generate animation that will play on system start"""
frames = []
team = file_image('team_logo.png')
thanks = file_image('thanks.png')
# naming is not mine
god = file_image('god.png')
logo = file_image('logo.png')
frames += still_image(team, 3)
frames += scroll_down(team, thanks, 16)
frames += fade(thanks, god, 10)
frames += still_image(god, 10)
frames += fade(god, logo, 10)
frames += still_image(logo, 1)
for i, f in enumerate(frames):
# All frames of animation are saved for debugging purposes
f.save('frames/%03d.png' % i)
return process(frames)
def generate_easter_egg():
json_file = open('ba.json')
img_list_list = json.loads(json_file.read())
frames = []
for img_list in img_list_list:
im = Image.new("1", (32, 32), 0 if img_list[0] == 0 else 255)
c = 0 if img_list[0] == 1 else 255
img_list.pop(0)
for i in img_list:
x, y, w = i
for xx in range(w):
xx = xx + x
cx = xx % 32
cy = (xx // 32) * 8 + y
im.putpixel((cx, cy), c)
frames.append(im)
return process(frames)
bin_arr = []
# Addresses printed here must be manually put into video_chip constants
# so it will know where every animation starts and ends
print("Start intro:", hex(len(bin_arr)))
bin_arr += generate_intro()
print("End intro:", hex(len(bin_arr) - 1))
print("Start win:", hex(len(bin_arr)))
bin_arr += process(fade(file_image('black.png'), file_image('winner.png'), 16))
print("End win:", hex(len(bin_arr) - 1))
print("Start loose:", hex(len(bin_arr)))
bin_arr += process(fade(file_image('black.png'), file_image('looser.png'), 16))
print("End loose:", hex(len(bin_arr) - 1))
print("Start easter egg:", hex(len(bin_arr)))
bin_arr += generate_easter_egg()
print("End easter egg:", hex(len(bin_arr) - 1))
f = open('../firmware/animation.img', mode='wb')
f.write(bytes("v2.0 raw\n", 'UTF-8'))
for byte in bin_arr:
f.write(bytes('%08x' % byte + '\n', 'UTF-8'))
f.close()
if __name__ == "__main__":
os.chdir(sys.path[0])
os.makedirs('firmware', exist_ok=True)
os.makedirs('animation/frames', exist_ok=True)
asm()
sin_cos()
animations()