Skip to content

Commit 9016d1d

Browse files
authored
Merge pull request #160 from dastels/master
Add code for Astels' "Digital Circuits 6: EPROM Emulator" project guide
2 parents 484332e + 60e38a7 commit 9016d1d

File tree

6 files changed

+685
-0
lines changed

6 files changed

+685
-0
lines changed

EPROM_Emulator/LICENSE.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
2+
The MIT License (MIT)
3+
4+
Copyright (c) 2018
5+
6+
Permission is hereby granted, free of charge, to any person obtaining a copy
7+
of this software and associated documentation files (the "Software"), to deal
8+
in the Software without restriction, including without limitation the rights
9+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
copies of the Software, and to permit persons to whom the Software is
11+
furnished to do so, subject to the following conditions:
12+
13+
The above copyright notice and this permission notice shall be included in all
14+
copies or substantial portions of the Software.
15+
16+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
SOFTWARE.

EPROM_Emulator/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# 2716 Eprom Emulator
2+
3+
This code goes along with the Digital Circuits 6: An EPROM Emulator learnign guide.

EPROM_Emulator/debouncer.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
"""
2+
The MIT License (MIT)
3+
4+
Copyright (c) 2018 Dave Astels
5+
6+
Permission is hereby granted, free of charge, to any person obtaining a copy
7+
of this software and associated documentation files (the "Software"), to deal
8+
in the Software without restriction, including without limitation the rights
9+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
copies of the Software, and to permit persons to whom the Software is
11+
furnished to do so, subject to the following conditions:
12+
13+
The above copyright notice and this permission notice shall be included in
14+
all copies or substantial portions of the Software.
15+
16+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22+
THE SOFTWARE.
23+
24+
--------------------------------------------------------------------------------
25+
Debounce an input pin.
26+
"""
27+
28+
import time
29+
import digitalio
30+
31+
class Debouncer(object):
32+
"""Debounce an input pin"""
33+
34+
DEBOUNCED_STATE = 0x01
35+
UNSTABLE_STATE = 0x02
36+
CHANGED_STATE = 0x04
37+
38+
39+
def __init__(self, pin, mode=None, interval=0.010):
40+
"""Make am instance.
41+
:param int pin: the pin (from board) to debounce
42+
:param int mode: digitalio.Pull.UP or .DOWN (default is no pull up/down)
43+
:param int interval: bounce threshold in seconds (default is 0.010, i.e. 10 milliseconds)
44+
"""
45+
self.state = 0x00
46+
self.pin = digitalio.DigitalInOut(pin)
47+
self.pin.direction = digitalio.Direction.INPUT
48+
if mode != None:
49+
self.pin.pull = mode
50+
if self.pin.value:
51+
self.__set_state(Debouncer.DEBOUNCED_STATE | Debouncer.UNSTABLE_STATE)
52+
self.previous_time = 0
53+
if interval is None:
54+
self.interval = 0.010
55+
else:
56+
self.interval = interval
57+
58+
59+
def __set_state(self, bits):
60+
self.state |= bits
61+
62+
63+
def __unset_state(self, bits):
64+
self.state &= ~bits
65+
66+
67+
def __toggle_state(self, bits):
68+
self.state ^= bits
69+
70+
71+
def __get_state(self, bits):
72+
return (self.state & bits) != 0
73+
74+
75+
def update(self):
76+
"""Update the debouncer state. Must be called before using any of the properties below"""
77+
self.__unset_state(Debouncer.CHANGED_STATE)
78+
current_state = self.pin.value
79+
if current_state != self.__get_state(Debouncer.UNSTABLE_STATE):
80+
self.previous_time = time.monotonic()
81+
self.__toggle_state(Debouncer.UNSTABLE_STATE)
82+
else:
83+
if time.monotonic() - self.previous_time >= self.interval:
84+
if current_state != self.__get_state(Debouncer.DEBOUNCED_STATE):
85+
self.previous_time = time.monotonic()
86+
self.__toggle_state(Debouncer.DEBOUNCED_STATE)
87+
self.__set_state(Debouncer.CHANGED_STATE)
88+
89+
90+
@property
91+
def value(self):
92+
"""Return the current debounced value of the input."""
93+
return self.__get_state(Debouncer.DEBOUNCED_STATE)
94+
95+
96+
@property
97+
def rose(self):
98+
"""Return whether the debounced input went from low to high at the most recent update."""
99+
return self.__get_state(self.DEBOUNCED_STATE) and self.__get_state(self.CHANGED_STATE)
100+
101+
102+
@property
103+
def fell(self):
104+
"""Return whether the debounced input went from high to low at the most recent update."""
105+
return (not self.__get_state(self.DEBOUNCED_STATE)) and self.__get_state(self.CHANGED_STATE)

EPROM_Emulator/directory_node.py

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
"""
2+
The MIT License (MIT)
3+
4+
Copyright (c) 2018 Dave Astels
5+
6+
Permission is hereby granted, free of charge, to any person obtaining a copy
7+
of this software and associated documentation files (the "Software"), to deal
8+
in the Software without restriction, including without limitation the rights
9+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
copies of the Software, and to permit persons to whom the Software is
11+
furnished to do so, subject to the following conditions:
12+
13+
The above copyright notice and this permission notice shall be included in
14+
all copies or substantial portions of the Software.
15+
16+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22+
THE SOFTWARE.
23+
24+
--------------------------------------------------------------------------------
25+
26+
Manage a directory in the file system.
27+
"""
28+
29+
import os
30+
31+
class DirectoryNode(object):
32+
"""Display and navigate the SD card contents"""
33+
34+
def __init__(self, display, parent=None, name="/"):
35+
"""Initialize a new instance.
36+
:param adafruit_ssd1306.SSD1306 on: the OLED instance to display on
37+
:param DirectoryNode below: optional parent directory node
38+
:param string named: the optional name of the new node
39+
"""
40+
self.display = display
41+
self.parent = parent
42+
self.name = name
43+
self.files = []
44+
self.top_offset = 0
45+
self.old_top_offset = -1
46+
self.selected_offset = 0
47+
self.old_selected_offset = -1
48+
49+
50+
def __cleanup(self):
51+
"""Dereference things for speedy gc."""
52+
self.display = None
53+
self.parent = None
54+
self.name = None
55+
self.files = None
56+
return self
57+
58+
59+
def __is_dir(self, path):
60+
"""Determine whether a path identifies a machine code bin file.
61+
:param string path: path of the file to check
62+
"""
63+
if path[-2:] == "..":
64+
return False
65+
try:
66+
os.listdir(path)
67+
return True
68+
except OSError:
69+
return False
70+
71+
72+
def __sanitize(self, name):
73+
"""Nondestructively strip off a trailing slash, if any, and return the result.
74+
:param string name: the filename
75+
"""
76+
if name[-1] == "/":
77+
return name[:-1]
78+
return name
79+
80+
81+
def __path(self):
82+
"""Return the result of recursively follow the parent links, building a full
83+
path to this directory."""
84+
if self.parent:
85+
return self.parent.__path() + os.sep + self.__sanitize(self.name)
86+
return self.__sanitize(self.name)
87+
88+
89+
def __make_path(self, filename):
90+
"""Return a full path to the specified file in this directory.
91+
:param string filename: the name of the file in this directory
92+
"""
93+
return self.__path() + os.sep + filename
94+
95+
96+
def __number_of_files(self):
97+
"""The number of files in this directory, including the ".." for the parent
98+
directory if this isn't the top directory on the SD card."""
99+
self.__get_files()
100+
return len(self.files)
101+
102+
103+
def __get_files(self):
104+
"""Return a list of the files in this directory.
105+
If this is not the top directory on the SD card, a ".." entry is the first element.
106+
Any directories have a slash appended to their name."""
107+
if len(self.files) == 0:
108+
self.files = os.listdir(self.__path())
109+
self.files.sort()
110+
if self.parent:
111+
self.files.insert(0, "..")
112+
for index, name in enumerate(self.files, start=1):
113+
if self.__is_dir(self.__make_path(name)):
114+
self.files[index] = name + "/"
115+
116+
117+
def __update_display(self):
118+
"""Update the displayed list of files if required."""
119+
if self.top_offset != self.old_top_offset:
120+
self.__get_files()
121+
self.display.fill(0)
122+
for i in range(self.top_offset, min(self.top_offset + 4, self.__number_of_files())):
123+
self.display.text(self.files[i], 10, (i - self.top_offset) * 8)
124+
self.display.show()
125+
self.old_top_offset = self.top_offset
126+
127+
128+
def __update_selection(self):
129+
"""Update the selected file lighlight if required."""
130+
if self.selected_offset != self.old_selected_offset:
131+
if self.old_selected_offset > -1:
132+
self.display.text(">", 0, (self.old_selected_offset - self.top_offset) * 8, 0)
133+
self.display.text(">", 0, (self.selected_offset - self.top_offset) * 8, 1)
134+
self.display.show()
135+
self.old_selected_offset = self.selected_offset
136+
137+
138+
def __is_directory_name(self, filename):
139+
"""Is a filename the name of a directory.
140+
:param string filename: the name of the file
141+
"""
142+
return filename[-1] == '/'
143+
144+
145+
@property
146+
def selected_filename(self):
147+
"""The name of the currently selected file in this directory."""
148+
self.__get_files()
149+
return self.files[self.selected_offset]
150+
151+
152+
@property
153+
def selected_filepath(self):
154+
"""The full path of the currently selected file in this directory."""
155+
return self.__make_path(self.selected_filename)
156+
157+
158+
def force_update(self):
159+
"""Force an update of the file list and selected file highlight."""
160+
self.old_selected_offset = -1
161+
self.old_top_offset = -1
162+
self.__update_display()
163+
self.__update_selection()
164+
165+
166+
def down(self):
167+
"""Move down in the file list if possible, adjusting the selected file indicator
168+
and scrolling the display as required."""
169+
if self.selected_offset < self.__number_of_files() - 1:
170+
self.selected_offset += 1
171+
if self.selected_offset == self.top_offset + 4:
172+
self.top_offset += 1
173+
self.__update_display()
174+
self.__update_selection()
175+
176+
177+
def up(self):
178+
"""Move up in the file list if possible, adjusting the selected file indicator
179+
and scrolling the display as required."""
180+
if self.selected_offset > 0:
181+
self.selected_offset -= 1
182+
if self.selected_offset < self.top_offset:
183+
self.top_offset -= 1
184+
self.__update_display()
185+
self.__update_selection()
186+
187+
188+
def click(self):
189+
"""Handle a selection and return the new current directory.
190+
If the selected file is the parent, i.e. "..", return to the parent directory.
191+
If the selected file is a directory, go into it."""
192+
if self.selected_filename == "..":
193+
if self.parent:
194+
p = self.parent
195+
p.force_update()
196+
self.__cleanup()
197+
return p
198+
elif self.__is_directory_name(self.selected_filename):
199+
new_node = DirectoryNode(self.display, self, self.selected_filename)
200+
new_node.force_update()
201+
return new_node
202+
return self

0 commit comments

Comments
 (0)