Skip to content

Commit 06dde86

Browse files
committedOct 9, 2018
Refactor the whole model and introduce sentinel.py
1 parent 5d1b008 commit 06dde86

File tree

5 files changed

+568
-7
lines changed

5 files changed

+568
-7
lines changed
 

‎README.md

+58-7
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,51 @@ Because the new version does not require `fbx2`, it does not have to be run unde
2222
## How to use
2323
It is still work in progress really...
2424

25-
So what there is currently:
26-
* `SSD1351.py` - the "driver" for the SSD1351 display that initialises it (when connected the right way because all pins are hard-coded)
25+
## Inner bits
26+
27+
### OLED adapter
28+
* `SSD1351.py` - the "driver" for the SSD1351 display that initialises it
2729
and allows flushing the internal framebuffer to the OLED. It also has couple of methods - one to fill the entire framebuffer with a single colour
2830
and another - to copy an image into the framebuffer.
2931
* `test.py` is just a very simple test to check the display and its driver work - it contiuously fills the display with red, then gren, then blue in a loop and prints
3032
how many times per second it can flush the framebuffer (the fps).
31-
* `eye.py` - the `Eye` class with eye-rendering logic extracted from `cyclop.py` and `eyes.py`
32-
* Then there are files from the [Original Adafruit Pi_Eyes sources](https://github.com/adafruit/Pi_Eyes/):
33+
* `blank.py` - a small script to just fill OLED with black - it is handy when you terminated an app you are working on and want last image to disappear from OLED screen.
34+
35+
### Eye graphics
36+
`eye.py`, the `Eye` class with eye-drawing logic (eye 3D model) extracted from the original `cyclop.py` and `eyes.py`
37+
38+
It relies on files from the [Original Adafruit Pi_Eyes sources](https://github.com/adafruit/Pi_Eyes/):
39+
* `gfxutil.py` - no changes, original code
40+
* `graphics/` directory - the original graphics, no changes
41+
42+
### Eye model
43+
`model.py`, the `EyesModel`, `TwoEyesModel` and friends - model state of one or more eyes.
44+
They implements autonomous random blinking and expose methods to modify state of a single eye or all of them.
45+
Sush as `open_eye`, `close_eye`, `start_blink`, `complete_blink`, `set_position`, `set_pupil_size`.
46+
47+
The model is the key in separating eye graphics rendering and behaviour. Once the rendering is initialised and started,
48+
the application can just make decisions on eye behaviour (where eyes should be looking, what pupil size should be etc) and then implementing
49+
these decisions by manipulating the model properties.
50+
51+
### Rendering
52+
`renderer.py` provides `Renderer` class that just continuously reads state of the eyes (from an `EyesModel` passed to it) and renders frames based on that changing state.
53+
54+
`copier.py` provides `FrameCopier` class whose job is to continuously copy a fragment of the frame (obtained from `Renderer`) into OLED screen.
55+
You will need one copier per eye each dealing with its own OLED.
56+
57+
### Sample
58+
59+
`sentinel.py` - a simple application that demonstrates how to use all the machinery. It initialises two-eyes model, renderer and two OLED screens,
60+
and then sleeps polling a proximity sensor. When sensor reports an object in range, the eyes open and begin autonomous movements
61+
that continue as long as sensor keep confirming the object is still in range. When object leaves, eyes close and the whole thing goes to sleep.
62+
Then the cycle repeats.
63+
64+
### Other
65+
66+
Other files from the [Original Adafruit Pi_Eyes sources](https://github.com/adafruit/Pi_Eyes/):
3367
* `cyclop.py` - that has been refactored. The animation logic still remains the this file (although moved to `Animator` class) while eye-rendering logic was moved to
3468
a new `eye.py` file (`Eye` class). The refactoring is still work in progress. When run, it displays the animated eye on the screen and duplicates image to the OLED screen via `SSD1351` library above.
3569
* `eyes.py` - I only started working on it and converted to use `Eye` class. As it is still work in progress, when run, it just displays the animated eyes on the screen but does not try to duplicate image to OLED screens. Need to connect two first...
36-
* `gfxutil.py` - no changes, original code
37-
* `graphics/` directory - the original graphics, no changes
3870

3971
If you want to run it - just run `cyclop.py` - it should render an eye into a small 128x128 window and at the same time copy the content to the OLED screen connected.
4072

@@ -55,10 +87,29 @@ The image below is from [OLED Display Library Setup for the Raspberry Pi featuri
5587
It shows Pi Zero but I used exactly the same pins for my regular Pi 3.
5688
![OLED Display Library Setup for the Raspberry Pi featuring SSD1331](docs/images/ssd1331-oled-display-raspberry-pi-connection.jpg)
5789

90+
## Performance
91+
92+
First of all, `python3` is recommended to run the application over the 2.x version.
93+
It seems that with Python 2 `py-spidev` gets into a GIL (global interpreter lock) or something so it cannot flush frames to two OLED screens concurrently..
94+
Because of that there is no benefit from running independend threads for each screen, operations gets performed sequentially and the frame rate suffers.
95+
96+
Also, the standard `py-spidev` library used to communicate with SPI devices only support 4K buffer. Which is not optimal as each 128x128 frame for OLED takes about 48K,
97+
it has to be sliced into small chunks. For optimal performance you need to:
98+
1. install `py-spidev` version that supports `xfer3` method - see https://github.com/doceme/py-spidev/pull/75
99+
2. then make sure your kernel is configured with large blocks
100+
101+
You can check the current block size with:
102+
```
103+
cat /sys/module/spidev/parameters/bufsiz
104+
```
105+
If it is set to 4K, increase it by editing `/boot/cmdline.txt` and adding `spidev.bufsiz=65535` there and rebooting.
106+
(This is for Raspberry. Not sure how it is done on other Linux'es)
107+
108+
58109
## Notes
59110

60111
The choice of the format for the framebuffer was driven by two things:
61-
* when you are using `pi3d` to take the screenshot of what you rendered with OpenGL, you are getting back a 3-dimensional `numpy.ndarray` - width x height x 3 (for RGB).
112+
* when you are using `pi3d` to take the screenshot of what you rendered with OpenGL, you are getting back a 3-dimensional `numpy.ndarray` - height x width x 3 (for RGB).
62113
* `spidev.xfer` method can only understand normal python lists (`[0, 1, 2...]`) and does not understand neither `array.array` nor `numpy.ndarray`
63114

64115
Obviously conversion was unavoidable and as I discovered that in Python operations like `dst[b:b+n] = src[a:a+n]` are INSANELY slow, I choosen to keep

‎copier.py

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import time
2+
3+
try:
4+
import thread
5+
except ImportError:
6+
import _thread as thread #Py3K changed it.
7+
8+
# Copies frames from renderer into OLED screen
9+
class FrameCopier:
10+
def __init__(self, renderer, oled, srcX, srcY):
11+
self.renderer = renderer
12+
self.oled = oled
13+
self.srcX = srcX
14+
self.srcY = srcY
15+
16+
def start(self):
17+
thread.start_new_thread(self.run, ())
18+
19+
def run(self):
20+
frame = None
21+
while True:
22+
frame = self.renderer.wait_frame(frame)
23+
self.oled.copy_image(frame, 0, 0, self.srcX, self.srcY)
24+
self.oled.flush()
25+

‎model.py

+271
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
import random
2+
3+
# Represents the instant state of an eye - pupil size, position
4+
# as well as weight for both upper and lower eyelids
5+
class EyeState:
6+
pupilSize = 0
7+
posX = 0
8+
posY = 0
9+
upperLidWeight = 0
10+
lowerLidWeight = 0
11+
12+
# Model representing a blink state of an eye.
13+
# It performs smooth transition between open an closed state being driven by methods
14+
# start_blink / complete_blink
15+
# start_open / start_close
16+
# In the end it just calculates current eyelid "weight" where 0.0 means fully open
17+
# and 1.0 means fully closed.
18+
class BlinkStateModel:
19+
# 0 = open, 1 = blinking/closing/closed, 2 = opening
20+
state = 0
21+
# When the closing move started and how much time should it take (0 = instantly)
22+
closeStart = 0
23+
closeDuration = 0
24+
# When the opening move started and how much time should it take (0 = instantly)
25+
openStart = 0
26+
openDuration = 0
27+
# Should the eye stay closed when closing move finishes. If not, it will transition to opening move
28+
keepClosed = False
29+
30+
# Begins blinking movement (close and open the eye once).
31+
# Note, that after closing the the eye it will stay closed until complete_blink() is called.
32+
# There is no need to wait until the eye closes fully before calling complete_blink() - it can be called
33+
# immediately after start_blink() and the eye will still perform the full close/open motion.
34+
# Also note that start_blink() only does anything if the eye is currently fully open and is ignored otherwise.
35+
def start_blink(self, now, closeDuration, openDuration):
36+
if self.state != 0:
37+
return
38+
self.state = 1 # ENBLINK
39+
self.closeStart = now
40+
self.closeDuration = closeDuration
41+
# Followed by open no delay
42+
self.openStart = now + closeDuration
43+
self.openDuration = openDuration
44+
# ... but only if complete_blink() is called in time
45+
self.keepClosed = True
46+
self.advance(now)
47+
48+
# Finish blinking movement and open the eye.
49+
# This method does not actually start the movement but just "allows" it instead.
50+
# If the eye is in the middle of the closing movement because of start_blink(),
51+
# then calling this method does not immediately open the eye but will let it finish
52+
# the closing movement first.
53+
def complete_blink(self, now):
54+
if self.openStart < now:
55+
self.openStart = now
56+
self.keepClosed = False
57+
self.advance(now)
58+
59+
# Begins opening movement.
60+
# Note that the movement overrides any other movement (opening or closing) if it is in progress.
61+
# The duration passed treated as desired duration of the full swing. So if the eye
62+
# is partially closed already (state != 0), duration and start time will be adjusted accordingly
63+
# to maintain the speed that full swing would require for target duration.
64+
def open(self, now, duration):
65+
n = self.get_eyelid_weight(now)
66+
self.state = 2
67+
self.openStart = now - duration * (1.0 - n)
68+
self.openDuration = duration
69+
self.keepClosed = False
70+
self.advance(now)
71+
72+
# print("self.state=%d, self.startTime=%f, self.openDuration=%f, SUM=%f" % (self.state, self.startTime, self.openDuration, self.startTime+self.openDuration))
73+
74+
# Begins closing movement.
75+
# Note that the movement overrides any other movement (opening or closing) if it is in progress.
76+
# The duration passed treated as desired duration of the full swing. So if the eye
77+
# is partially closed already (state != 0), duration and start time will be adjusted accordingly
78+
# to maintain the speed that full swing would require for target duration.
79+
# The eye will remain closed until a call to start_open()
80+
def close(self, now, duration):
81+
n = self.get_eyelid_weight(now)
82+
self.state = 1
83+
self.closeStart = now - duration * (1.0 - n)
84+
self.closeDuration = duration
85+
self.keepClosed = True
86+
self.advance(now)
87+
88+
# Do state transitions if necessary
89+
def advance(self, now):
90+
if self.state == 1: # Eye currently winking/blinking?
91+
if self.closeStart + self.closeDuration <= now and not self.keepClosed:
92+
self.state = 2
93+
94+
if self.state == 2: # Eye currently opening
95+
if self.openStart + self.openDuration <= now:
96+
self.state = 0
97+
98+
# Return eyelid weight for the current blink state ranging from 0.0 (fully open) to 1.0 (fully closed).
99+
def get_eyelid_weight(self, now):
100+
101+
# Do state transitions if necessary
102+
103+
self.advance(now)
104+
105+
# Now figure out progress in the current state
106+
107+
if self.state == 1:
108+
passed = now - self.closeStart
109+
if self.closeDuration == 0 or passed >= self.closeDuration:
110+
return 1.0
111+
else:
112+
return passed / self.closeDuration
113+
114+
elif self.state == 2:
115+
passed = now - self.openStart
116+
if self.openDuration == 0 or passed >= self.openDuration:
117+
return 0.0
118+
else:
119+
return 1.0 - passed / self.openDuration
120+
121+
else:
122+
return 0.0
123+
124+
# All-eye selector for EyesModel's close_eye / open_eye operations.
125+
# To act on all eyes we need to pass None there but it is not very readable,
126+
# so just create an alias for that
127+
ALL = None
128+
129+
# Eyes model - provides instant state of the eyes to the rendering engine
130+
# as well as methods that allow manipulating that state to the animation/control code.
131+
# For example control code can request a certain eye to open, close or blink and the model will
132+
# perform a smooth transition from open to closed state etc.
133+
# Model also implements automatic blinking (when enabled)
134+
class EyesModel:
135+
136+
def __init__(self, num):
137+
self.autoblink = False
138+
self.timeOfNextBlink = 0
139+
self.blinkState = [BlinkStateModel()] * num
140+
self.trackingPos = 0.3
141+
self.posX = 0
142+
self.posY = 0
143+
self.pupilSize = 0
144+
145+
def random_blink_duration(self):
146+
return random.uniform(0.035, 0.06)
147+
148+
def enable_autoblink(self, now):
149+
self.autoblink = True
150+
self.timeOfNextBlink = now + random.uniform(0.0, 4.0)
151+
152+
def disable_autoblink(self):
153+
self.autoblink = False
154+
155+
# Begin blinking movement (close and open an eye once).
156+
# 'index' selects an eye to perform action on, ALL can be passed to perform it on all the eyes at the same time
157+
# For details see matching method in BlinkStateModel
158+
def start_blink(self, index, now, closeDuration = None, openDuration = None):
159+
if closeDuration is None:
160+
closeDuration = self.random_blink_duration()
161+
if openDuration is None:
162+
openDuration = 2 * closeDuration
163+
164+
if index is ALL:
165+
for i in range(len(self.blinkState)):
166+
self.start_blink(i, now, closeDuration, openDuration)
167+
else:
168+
self.blinkState[index].start_blink(now, closeDuration, openDuration)
169+
170+
171+
# Finish blinking movement and open an eye.
172+
# 'index' selects an eye to perform action on, ALL can be passed to perform it on all the eyes at the same time.
173+
# For details see matching method in BlinkStateModel.
174+
def complete_blink(self, index, now):
175+
if index is ALL:
176+
for i in range(len(self.blinkState)):
177+
self.complete_blink(i, now)
178+
else:
179+
self.blinkState[index].complete_blink(now)
180+
181+
182+
# Close an eye.
183+
# 'index' selects an eye to perform action on, ALL can be passed to perform it on all the eyes at the same time.
184+
# For details see matching method in BlinkStateModel.
185+
def close_eye(self, index, now, duration = None):
186+
if duration is None:
187+
duration = self.random_blink_duration()
188+
189+
if index is ALL:
190+
for i in range(len(self.blinkState)):
191+
self.close_eye(i, now, duration)
192+
else:
193+
self.blinkState[index].close(now, duration)
194+
195+
# Open an eye.
196+
# For details see matching method in BlinkStateModel.
197+
# 'index' selects an eye to perform action on, ALL can be passed to perform it on all the eyes at the same time.
198+
def open_eye(self, index, now, duration = None):
199+
if duration is None:
200+
duration = self.random_blink_duration()
201+
202+
if index is ALL:
203+
for i in range(len(self.blinkState)):
204+
self.open_eye(i, now, duration)
205+
else:
206+
self.blinkState[index].open(now, duration)
207+
208+
def auto_blink(self, now):
209+
if self.autoblink and now >= self.timeOfNextBlink:
210+
closeDuration = self.random_blink_duration()
211+
openDuration = closeDuration * 2.0
212+
for blinkState in self.blinkState:
213+
blinkState.start_blink(now, closeDuration, openDuration)
214+
blinkState.complete_blink(now)
215+
self.timeOfNextBlink = now + closeDuration + openDuration + random.uniform(0.0, 4.0)
216+
217+
def set_pupil_size(self, size):
218+
self.pupilSize = size
219+
220+
def set_position(self, x, y):
221+
self.posX = x
222+
self.posY = y
223+
224+
# Return state for each of the eyes in a list
225+
def get_state(self, now):
226+
227+
self.auto_blink(now)
228+
229+
num = len(self.blinkState)
230+
231+
result = []
232+
233+
# self.trackingPos = 1 if TRACKING:
234+
# n = 0.4 - self.posY / 60.0
235+
# if n < 0.0: n = 0.0
236+
# elif n > 1.0: n = 1.0
237+
# self.trackingPos = (self.trackingPos * 3.0 + n) * 0.25
238+
239+
for i in range(num):
240+
n = self.blinkState[i].get_eyelid_weight(now)
241+
state = EyeState()
242+
state.posX = self.posX
243+
state.posY = self.posY
244+
state.pupilSize = self.pupilSize
245+
state.upperLidWeight = self.trackingPos + (n * (1.0 - self.trackingPos))
246+
state.lowerLidWeight = (1.0 - self.trackingPos) + (n * self.trackingPos)
247+
result.append(state)
248+
249+
return result
250+
251+
252+
# Specific implementation of EyesModel for two eyes - it just introduces convergence
253+
# assuming eyes will be drawn horisontally, one next to another.
254+
# Left eye has index 0 while right eye has index 1.
255+
class TwoEyesModel(EyesModel):
256+
257+
def __init__(self):
258+
EyesModel.__init__(self, 2)
259+
260+
def get_state(self, now):
261+
states = EyesModel.get_state(self, now)
262+
263+
convergence = 2.0
264+
265+
# Left eye
266+
states[0].posX += convergence
267+
# Right eye
268+
states[1].posX -= convergence
269+
270+
return states
271+

‎renderer.py

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import pi3d
2+
import threading
3+
import time
4+
5+
# Renderer continuously draws eyes represented by the passed EyesModel.
6+
# The assumption is model state is changing because something uses state-manipulation methods of the EyesModel
7+
# (and also because of autoblink enabled) so sequence of frames we are rendering shows the animation.
8+
# Code interested in new frames should repeatedly call wait_frame method which will return
9+
# a new image as soon as it becomes available.
10+
#
11+
# Note that originally I plannned to implement rendering in a background thread so Renderer would have start/stop
12+
# methods for Renderer that allows controlling but alas:
13+
# load_opengl must be called on main thread for <pi3d.Buffer.Buffer object at 0x74d72250>
14+
# ...
15+
# AttributeError: 'Buffer' object has no attribute 'vbuf'
16+
# so I am going to just call run() from the main thread. Still start/stop methods are provided
17+
# to allow control thread to pause and resume rendering when needed.
18+
class Renderer:
19+
20+
def __init__(self, display, eyes, model):
21+
self.eyes = eyes
22+
self.model = model
23+
self.frame = None
24+
self.condition = threading.Condition()
25+
self.started = False
26+
self.display = display
27+
28+
# Has to be called from the main thread
29+
def run(self):
30+
while True:
31+
# Wait until something calls start()
32+
with self.condition:
33+
while not self.started:
34+
self.condition.wait()
35+
36+
self.render_frame()
37+
38+
39+
def render_frame(self):
40+
self.display.loop_running()
41+
42+
now = time.time()
43+
44+
states = self.model.get_state(now)
45+
46+
for i in range(2):
47+
eye = self.eyes[i]
48+
eye.set_state(states[i])
49+
eye.draw()
50+
51+
img = pi3d.util.Screenshot.screenshot()
52+
53+
# Make new image available to waiting threads
54+
with self.condition:
55+
self.frame = img
56+
self.condition.notifyAll()
57+
58+
# Wait until next frame is rendered and return it
59+
def wait_frame(self, last_frame):
60+
with self.condition:
61+
while self.frame is last_frame:
62+
self.condition.wait()
63+
return self.frame
64+
65+
def start(self):
66+
with self.condition:
67+
self.started = True
68+
self.condition.notifyAll()
69+
print("Renderer started")
70+
71+
def stop(self):
72+
with self.condition:
73+
self.started = False
74+
print("Renderer stopped")

‎sentinel.py

+140
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
#!/usr/bin/python
2+
3+
import pi3d
4+
import time
5+
6+
import SSD1351
7+
import eye
8+
import model
9+
import renderer
10+
import copier
11+
import autonomous
12+
13+
try:
14+
import thread
15+
except ImportError:
16+
import _thread as thread #Py3K changed it.
17+
18+
# Fake proximity sensor, changes its output every 3 seconds
19+
class ProximitySensor:
20+
status = True
21+
end = 0
22+
23+
def object_in_range(self):
24+
if self.end <= time.time():
25+
self.end = time.time() + 3
26+
self.status = not self.status
27+
return self.status
28+
29+
# This is the main logic / behaviour of the application.
30+
# Basically we keep the eyas shut until proximity sensor tells us there is an object in range,
31+
# then we open the eyes and keep them open (blinking an moving around) until
32+
# proximity sensor says there is not object in range.
33+
# At this point we close the eyes and start from the beginning.
34+
def controlThread(renderer, eyesModel):
35+
36+
proximitySensor = ProximitySensor()
37+
38+
eyePosInput = autonomous.AutonomousEyePositionInput()
39+
pupilSizeInput = autonomous.AutonomousPupilSizeInput()
40+
41+
while True:
42+
43+
while not proximitySensor.object_in_range():
44+
time.sleep(0.05)
45+
46+
print("Object in range, waking up")
47+
48+
# Start with the eyes closed
49+
eyesModel.close_eye(model.ALL, time.time(), 0)
50+
eyesModel.set_position(0, 0)
51+
eyesModel.set_pupil_size(0)
52+
53+
renderer.start()
54+
55+
# Wake up, open eyes slowly
56+
eyesModel.open_eye(model.ALL, time.time(), 0.7)
57+
eyesModel.enable_autoblink(time.time())
58+
59+
# Put simulators to the same state as the model so there won't be a sudden jump
60+
# in eye position or pupil size
61+
eyePosInput.set_position(0, 0)
62+
pupilSizeInput.set_size(0)
63+
64+
last_frame = None
65+
66+
# While there is an object in range of the sensor, do autonomous eye movement
67+
while proximitySensor.object_in_range():
68+
now = time.time()
69+
70+
# Get data from simulators
71+
x, y = eyePosInput.get_position(now)
72+
pupilSize = pupilSizeInput.get_size(now)
73+
74+
eyesModel.set_position(x, y)
75+
eyesModel.set_pupil_size(pupilSize)
76+
77+
# There is no point in moving the eye faster than renderer can draw it
78+
# otherwise it is going to be a very tight, CPU hogging loop.
79+
# So wait until Renderer produces another frame even though we do not need it here
80+
last_frame = renderer.wait_frame(last_frame)
81+
82+
print("No object in range, going to sleep")
83+
84+
# Going to sleep, close eyes slowly
85+
eyesModel.disable_autoblink()
86+
eyesModel.close_eye(model.ALL, time.time(), 0.7)
87+
# Give the model time to go through the transition
88+
time.sleep(1)
89+
# Stop rendering to not waste resources while we are sleeping
90+
renderer.stop()
91+
92+
93+
# MAIN
94+
95+
OLED_WIDTH = 128
96+
OLED_HEIGHT = 128
97+
GAP = OLED_WIDTH // 2
98+
99+
leftOLED = SSD1351.SSD1351(spi_bus = 0, spi_device = 0, dc = 24, rst = 25)
100+
rightOLED = SSD1351.SSD1351(spi_bus = 1, spi_device = 0, dc = 23, rst = 26)
101+
102+
displayWidth = 2 * OLED_WIDTH + GAP
103+
displayHeight = OLED_HEIGHT
104+
105+
# Display must be created before the eyes or their draw() method throws...
106+
display = pi3d.Display.create(samples = 4, w = displayWidth, h = displayHeight)
107+
# make background green while debugging and refactoring so it is easier to see individual eye pieces
108+
display.set_background(0, 0.5, 0, 1) # r,g,b,alpha
109+
# A 2D camera is used, mostly to allow for pixel-accurate eye placement,
110+
# but also because perspective isn't really helpful or needed here, and
111+
# also this allows eyelids to be handled somewhat easily as 2D planes.
112+
# Line of sight is down Z axis, allowing conventional X/Y cartesion
113+
# coords for 2D positions.
114+
cam = pi3d.Camera(is_3d=False, at=(0,0,0), eye=(0,0,-1000))
115+
light = pi3d.Light(lightpos=(0, -500, -500), lightamb=(0.2, 0.2, 0.2))
116+
117+
# eyeRadius is the size, in pixels, at which the whole eye will be rendered
118+
# onscreen. eyePosition, also pixels, is the offset (left or right) from
119+
# the center point of the screen to the center of each eye.
120+
eyePosition = OLED_WIDTH // 2 + GAP // 2
121+
eyeRadius = OLED_WIDTH / 2.1
122+
123+
rightEye = eye.Eye(eyeRadius, -eyePosition, 0, True);
124+
leftEye = eye.Eye(eyeRadius, eyePosition, 0, False);
125+
126+
eyesModel = model.TwoEyesModel()
127+
128+
renderer = renderer.Renderer(display, [leftEye, rightEye], eyesModel)
129+
130+
leftOLEDCopier = copier.FrameCopier(renderer, leftOLED, displayWidth // 2 - eyePosition - OLED_WIDTH // 2, 0)
131+
rightOLEDCopier = copier.FrameCopier(renderer, rightOLED, displayWidth // 2 + eyePosition - OLED_WIDTH // 2, 0)
132+
133+
leftOLEDCopier.start()
134+
rightOLEDCopier.start()
135+
136+
thread.start_new_thread(controlThread, (renderer, eyesModel))
137+
138+
# Renderer's run() method never returns. And of course it needs to be invoked from the main thread.
139+
# Because pi3d...
140+
renderer.run()

0 commit comments

Comments
 (0)
Please sign in to comment.