Skip to content

Simulation of Componentables will render components added after starting #602

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Feb 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 15 additions & 8 deletions examples/simulation/pitop_simulate.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from os import path
from time import sleep

from PIL import Image

Expand All @@ -15,27 +16,33 @@
pitop.add_component(Button("D1", name="button1"))
pitop.add_component(LED("D2", name="led2", color="green"))
pitop.add_component(Button("D3", name="button2"))
pitop.add_component(LED("D4", name="led3"))
pitop.add_component(Button("D5", name="button3"))
pitop.add_component(LED("D6", name="led4", color="yellow"))
pitop.add_component(Button("D7", name="button4"))

pitop.button1.when_pressed = pitop.led1.on
pitop.button1.when_released = pitop.led1.off

pitop.button2.when_pressed = pitop.led2.on
pitop.button2.when_released = pitop.led2.off

pitop_sim = simulate(pitop, 0.5)
led_sim = simulate(pitop.led1)
button_sim = simulate(pitop.button1)

sleep(5)

pitop.add_component(LED("D4", name="led3"))
pitop.add_component(Button("D5", name="button3"))

pitop.button3.when_pressed = pitop.led3.on
pitop.button3.when_released = pitop.led3.off

sleep(5)

pitop.add_component(LED("D6", name="led4", color="yellow"))
pitop.add_component(Button("D7", name="button4"))

pitop.button4.when_pressed = pitop.led4.on
pitop.button4.when_released = pitop.led4.off

pitop_sim = simulate(pitop, 0.5)
led_sim = simulate(pitop.led4)
button_sim = simulate(pitop.button4)

rocket = Image.open(
f"{path.dirname(path.realpath(__file__))}/../../packages/miniscreen/pitop/miniscreen/images/rocket.gif"
)
Expand Down
1 change: 1 addition & 0 deletions packages/core/pitop/core/mixins/componentable.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ def add_component(self, component, name=""):

if name and is_recreatable:
component.name = name
component._config["name"] = name

component_name = component.name if is_recreatable else name
if component_name in self.children:
Expand Down
43 changes: 25 additions & 18 deletions packages/simulation/pitop/simulation/simsprite.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def create_sprite_group(cls, sim_size, config, scale):
)

sprite_group.add(sprite)
return sprite_group
return sprite_group, sprite

@staticmethod
def handle_sim_event(e: SimEvent, component):
Expand Down Expand Up @@ -68,33 +68,40 @@ class ComponentableSimSprite(SimSprite):

@classmethod
def create_sprite_group(cls, sim_size, config, scale):
sprite_group = super().create_sprite_group(sim_size, config, scale)
sprite_group, main_sprite = super().create_sprite_group(sim_size, config, scale)

main_sprite_rect = sprite_group.sprites()[0].rect
sprite_centres = cls._generate_sprite_centres(sim_size, main_sprite_rect)
sprite_centres = cls._generate_sprite_centres(sim_size, main_sprite.rect)
main_sprite.sprite_centres = sprite_centres

components = config.get("components", {})
for component in components.values():
sprite_class = getattr(Sprites, component.get("classname"), None)
component_sprite = main_sprite.create_child_sprite(component, scale)
if component_sprite is not None:
sprite_group.add(component_sprite)

if not sprite_class:
continue
return sprite_group, main_sprite

sprite = sprite_class(component, scale, draw_port=True)
if not sprite:
continue
def create_child_sprite(self, component, scale):
sprite_class = getattr(Sprites, component.get("classname"), None)

sprite_centre = sprite_centres.get(component.get("port_name", None), (0, 0))
if not sprite_class:
return None

sprite.set_pos(
sprite_centre[0] - int(sprite.rect.width / 2),
sprite_centre[1] - int(sprite.rect.height / 2),
)
sprite = sprite_class(component, scale, draw_port=True)
if not sprite:
return None

sprite.name = component.get("name")
sprite_group.add(sprite)
sprite_centre = self.sprite_centres.get(
component.get("port_name", None), (0, 0)
)

sprite.set_pos(
sprite_centre[0] - int(sprite.rect.width / 2),
sprite_centre[1] - int(sprite.rect.height / 2),
)

return sprite_group
sprite.name = component.get("name")
return sprite

@staticmethod
def handle_sim_event(e: SimEvent, component):
Expand Down
35 changes: 33 additions & 2 deletions packages/simulation/pitop/simulation/simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ def __init__(self, component, scale=None, size=None):

# communication channels between main process and sim
self._stop_ev = Event() # stopping this simulation
self._config_q = Queue() # sending component config updates into sim
self._state_q = Queue() # sending component state updates into sim
self._out_event_q = Queue() # sending sim events to components
self._in_event_q = Queue() # artificially firing pygame events (tests)
Expand All @@ -59,6 +60,7 @@ def __init__(self, component, scale=None, size=None):
self.scale,
self.size,
self._stop_ev,
self._config_q,
self._state_q,
self._out_event_q,
self._in_event_q,
Expand Down Expand Up @@ -90,16 +92,24 @@ def event(self, type, target):
self._in_event_q.put((type, target))

def __communicate(self):
# store config as a string (not reference) to easily check if it's changed
str_config = str(self.component.config)

while True:
if self._stop_ev.is_set():
break

if self._process is None or not self._process.is_alive():
break

# update the sim with the current state of the simulated component
# update sim with current state of simulated component
self._state_q.put(self.component.state)

# update sim with current config only if it's changed
if str(self.component.config) != str_config:
str_config = str(self.component.config)
self._config_q.put(self.component.config)

# handle events produced by the sim which affect our simulated
# component (eg PMA Button presses, sensor slider updates)
while not self._out_event_q.empty():
Expand All @@ -109,6 +119,7 @@ def __communicate(self):
self.component,
)

# sleep - timed to match pygame loop
sleep(0.05)

self.stop()
Expand All @@ -127,6 +138,7 @@ def _run(
scale,
size,
stop_ev,
config_q,
state_q,
out_event_q,
in_event_q,
Expand All @@ -145,9 +157,10 @@ def _run(
screen.fill((255, 255, 255))

# create our sprites based on the simulated component's config
sprite_group = sprite_class.create_sprite_group(size, config, scale)
sprite_group, main_sprite = sprite_class.create_sprite_group(size, config, scale)

while not stop_ev.is_set():
# handle pygame events eg UI
for event in pygame.event.get():
if event.type == pygame.QUIT:
stop_ev.set()
Expand Down Expand Up @@ -178,6 +191,7 @@ def _run(
sprite.handle_pygame_event(event)
out_event_q.put(SimEvent(SimEventTypes.MOUSE_DOWN, sprite.name))

# send out postion update events for selected sliders
for sprite in sprite_group.sprites():
if hasattr(sprite, "slider") and sprite.slider.selected:
out_event_q.put(
Expand All @@ -188,6 +202,7 @@ def _run(
)
)

# handle incoming simulated ui events
while not in_event_q.empty():
# find which sprite matches the target_name and translate to target
# the event at that sprite's position in the simulation
Expand All @@ -201,10 +216,24 @@ def _run(
)
pygame.event.post(event)

# respond to screenshot requests
if snapshot_ev.is_set():
snapshot_q.put(to_bytes(screen))
snapshot_ev.clear()

# check changes to component config eg newly added sprites
while not config_q.empty():
new_config = config_q.get_nowait()
components = new_config.get("components", {})

sprite_names = [sprite.name for sprite in sprite_group.sprites()]

for component in components.values():
if component.get("name") not in sprite_names:
component_sprite = main_sprite.create_child_sprite(component, scale)
sprite_group.add(component_sprite)

# forward inbound component state updates
while not state_q.empty():
# provide sprites with relevant part of the component state tree
state = state_q.get_nowait()
Expand All @@ -214,10 +243,12 @@ def _run(
else:
sprite.state = state.get(sprite.name)

# draw
sprite_group.update()
sprite_group.draw(screen)
pygame.display.flip()

# sleep - timed to match controlling thread
clock.tick(20)

# Don't pygame.quit() - isn't needed and can cause X server to crash
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions tests/test_led.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ def test_led_color(make_led, create_sim, snapshot):
green_led.on()
yellow_led.on()

sleep(0.1)
sleep(0.2)
snapshot.assert_match(red_sim.snapshot(), "red_led_on.png")
snapshot.assert_match(green_sim.snapshot(), "green_led_on.png")
snapshot.assert_match(yellow_sim.snapshot(), "yellow_led_on.png")
Expand All @@ -93,7 +93,7 @@ def test_led_color(make_led, create_sim, snapshot):
green_led.off()
yellow_led.off()

sleep(0.1)
sleep(0.2)
snapshot.assert_match(red_sim.snapshot(), "red_led_off.png")
snapshot.assert_match(green_sim.snapshot(), "green_led_off.png")
snapshot.assert_match(yellow_sim.snapshot(), "yellow_led_off.png")
Expand Down
28 changes: 28 additions & 0 deletions tests/test_pitop.py
Original file line number Diff line number Diff line change
Expand Up @@ -322,3 +322,31 @@ def on_button_release():

sleep(0.5)
snapshot.assert_match(sim.snapshot(), "default.png")


def test_pitop_simulate_add_component(
pitop, mocker, create_sim, snapshot, analog_sensor_mocks
):
mocker.patch(
"pitop.simulation.sprites.is_virtual_hardware",
return_value=True,
)

from pitop.pma import LED, Button

pitop.add_component(LED("D0"))

sim = create_sim(pitop)

# give time for the screen and sprites to be set up
sleep(2)
snapshot.assert_match(sim.snapshot(), "default.png")

pitop.add_component(Button("D1"))
sleep(0.5) # some time for button to be added
snapshot.assert_match(sim.snapshot(), "with_button.png")

pitop.add_component(Button("D2"), name="button2")
print(pitop.config)
sleep(0.5) # some time for button to be added
snapshot.assert_match(sim.snapshot(), "with_2_button.png")