diff --git a/python_payload/demo_menu.py b/python_payload/demo_menu.py new file mode 100644 index 0000000000000000000000000000000000000000..e3c5e62590b9199133db1efb23cda1e6156f9ac2 --- /dev/null +++ b/python_payload/demo_menu.py @@ -0,0 +1,48 @@ +import menu +import event +import hardware + +import demo_worms,demo_sparabo + +def start_worms(action): + menu.menu_stack.append(menu.active_menu) + menu.active_menu=None + demo_worms.run() + +def start_sparabo(action): + menu.menu_stack.append(menu.active_menu) + menu.active_menu=None + demo_sparabo.run() + +menu_demo = menu.Menu("demo") +item_worms = menu.MenuItem("worms") +item_worms.action = start_worms +menu_demo.add(item_worms) + +item_abo = menu.MenuItem("abo") +item_abo.action = start_sparabo +menu_demo.add(item_abo) + +testmenu = menu.Menu("test") + +item_add = menu.MenuItem("+") +item_add.action = lambda x: testmenu.add(menu.MenuItem("new {}".format(len(testmenu.items)))) + +item_sub = menu.MenuItem("-") +item_sub.action = lambda x: testmenu.pop() if len(testmenu.items) > 4 else None + +item_foo = menu.MenuItem("foo") +testmenu.add(item_foo) +testmenu.add(item_sub) +testmenu.add(item_add) + +menu_main = menu.Menu("main",has_back=False) +menu_main.add(menu.MenuItemSubmenu(testmenu)) +menu_main.add(menu.MenuItemSubmenu(menu_demo)) +menu_main.add(menu.MenuItem("nix")) + +menu.set_active_menu(menu_main) +menu.render() + + +event.the_engine.eventloop() diff --git a/python_payload/demo_sparabo.py b/python_payload/demo_sparabo.py index 691f26559df634cdc9a98d2975a32f66f471453d..50b6d465821a491425b2df0aa793d44951b00107 100644 --- a/python_payload/demo_sparabo.py +++ b/python_payload/demo_sparabo.py @@ -12,13 +12,11 @@ def xy_from_polar(r,deg): ) ) ctx = hardware.get_ctx() -ctx.text_align = ctx.CENTER -ctx.text_baseline = ctx.MIDDLE - +popcorn = [9,7,9,5,0,5,-3,999] synth = tinysynth(440,1) -synth.decay(25) -popcorn = [9,7,9,5,0,5,-3,999] +sequencer = None +handler = None def on_step(data): note = popcorn[data["step"]] @@ -39,8 +37,24 @@ def on_step(data): ctx.move_to(x,y).rgb(0.5,0.5,0).text("{}".format(data["step"])) hardware.display_update() - - - -event.Sequence(bpm=160, steps=8, action=on_step, loop=True) -event.the_engine.eventloop() +def handle_input(data={}): + sequencer.remove() + ev.remove() + + +def init(): + ctx.text_align = ctx.CENTER + ctx.text_baseline = ctx.MIDDLE + + synth = tinysynth(440,1) + synth.decay(25) + global sequencer + sequencer = event.Sequence(bpm=160, steps=8, action=on_step, loop=True) + global ev + ev=event.Event(name="sparabo",action=handle_input, + condition=lambda e: e["type"] =="button" and e["change"] and e["value"] == 2) + +def run(): + init(); + print("run") + event.the_engine.eventloop() diff --git a/python_payload/demo_worms.py b/python_payload/demo_worms.py index 8784a3860b93cc9247c6ece29ea57c0e4fd3b760..ece958f380e652f236719ecef4089ec88b5b93ad 100644 --- a/python_payload/demo_worms.py +++ b/python_payload/demo_worms.py @@ -30,16 +30,21 @@ GREY = (0.5,0.5,0.5) # The global context (representing the whole screen) ctx = None -worms = None - +worms = [] class Worm(): - def __init__(self): + def __init__(self,direction=None): self.color = randrgb() - self.direction = random.random()*math.pi*2 - self.size = 10 + + if direction: + self.direction = direction + else: + self.direction = random.random()*math.pi*2 + + + self.size = 50 self.speed = self.size/5 - (x,y) = xy_from_polar(40, self.direction+90) + (x,y) = xy_from_polar(100, self.direction) self.x = x self.y= y #(self.dx,self.dy) = xy_from_polar(1,self.direction) @@ -59,7 +64,12 @@ class Worm(): def move(self): dist = math.sqrt(self.x**2+self.y**2) - self.size = (120-dist)/3 + target_size = (130-dist)/3 + + if self.size>target_size: self.size-=1 + + if self.size<target_size: self.size+=1 + self.speed = self.size/5 self.direction += (random.random()-0.5)*math.pi/4 @@ -69,21 +79,23 @@ class Worm(): self.y+=dy - if dist>110-self.size/2 and dist>self._lastdist: + if dist>120-self.size/2 and dist>self._lastdist: polar_position=math.atan2(self.y,self.x) dx=dx*-abs(math.cos(polar_position)) dy=dy*-abs(math.sin(polar_position)) self.direction=-math.atan2(dy,dx) self.mutate() self._lastdist = dist + +def handle_input(data): + worms.append(Worm(data.get("index",0)*2*math.pi/10+math.pi )) + if len(worms)>10: + worms.pop(0) -def init(): - global worms - global ctx - worms = [] - for i in range(23): - worms.append(Worm()) + +def init(data={}): + # Get the global context (representing the whole screen) ctx = hardware.get_ctx() # TODO(q3k): factor out frame limiter @@ -129,8 +141,27 @@ def foreground(): ctx.rgb(*BLUE).rectangle(-WIDTH/2,-HEIGHT/2,WIDTH,HEIGHT).fill() #Write some text - ctx.move_to(0,0).rgb(*WHITE).text("Hi :)") + ctx.move_to(0,0).rgb(*WHITE).text("touch me :)") hardware.display_update() + global worms + worms = [] + for i in range(0): + worms.append(Worm()) + + event.Event(name="worms_control",action=handle_input, + condition=lambda data: data.get("type","")=="captouch" and data.get("value")==1 and data["change"]) + +def loop(data={}): + for w in worms: + w.draw() + w.move() + + hardware.display_update() + +def run(data={}): + init() + event.the_engine.userloop = loop + event.the_engine.eventloop() #Known problems: #ctx.rotate(math.pi) turns the display black until powercycled diff --git a/python_payload/event.py b/python_payload/event.py index 3e8e9dac10407acd22fde36c9e350f7b44a80dd9..4c176ae308a84b5ee6a473b1d0904848801c7b67 100644 --- a/python_payload/event.py +++ b/python_payload/event.py @@ -1,6 +1,7 @@ import hardware import time import math +import random EVENTTYPE_TIMED = 1 @@ -10,152 +11,170 @@ EVENTTYPE_INPUT = 2 #EVENTTYPE_CAPCROSS = 4 class Engine(): - def __init__(self): - self.events_timed = [] - self.events_input = [] - self.next_timed = None - self.last_input_state = None - - def add(self,event): - if isinstance(event,EventTimed): - self.add_timed(event) - elif isinstance(event,Event): - self.add_input(event) - - def add_timed(self,event): - self.events_timed.append(event) - self.events_timed = sorted(self.events_timed, key = lambda event: event.deadline) - - - def add_input(self,event): - self.events_input.append(event) - - def _handle_timed(self): - if not self.next_timed and self.events_timed: - self.next_timed = self.events_timed.pop(0) - - now = time.ticks_ms() - - if self.next_timed: - diff = time.ticks_diff(self.next_timed.deadline, now) - if diff <= 0: - self.next_timed.trigger({"ticks_ms":now, "ticks_delay": -diff}) - self.next_timed = None - - def _handle_input(self): - input_state={ - "b0":(hardware.get_button(0),"button",0), - "b1":(hardware.get_button(1),"button",1) - } - - for i in range(0,10): - input_state["c"+str(i)]=(hardware.get_captouch(i),"captouch",i) - - - if not self.last_input_state: - self.last_input_state=input_state - return - - diff=[] - for key in input_state: - if input_state[key][0] != self.last_input_state[key][0]: - diff.append({ - "type" : input_state[key][1], - "index" : input_state[key][2], - "to" : input_state[key][0], - "from" : self.last_input_state[key][0], - "ticks_ms": time.ticks_ms() - }) - - - if diff: - #print(diff) - for d in diff: - triggered_events = list(filter(lambda e: e.condition(d),self.events_input)) - #print (triggered_events) - #map(lambda e: e.trigger(d), triggered_events) - for e in triggered_events: - e.trigger(d) - - - self.last_input_state=input_state - - - - def _eventloop_single(self): - self._handle_timed() - self._handle_input() - - def eventloop(self): - while True: - self._eventloop_single() - time.sleep(0.005) - + def __init__(self): + self.events_timed = [] + self.events_input = [] + self.next_timed = None + self.last_input_state = None + self.userloop = None + self.is_running = False + + def add(self,event): + if isinstance(event,EventTimed): + self.add_timed(event) + elif isinstance(event,Event): + self.add_input(event) + + def add_timed(self,event): + self.events_timed.append(event) + self._sort_timed() + + def remove_timed(self,group_id): + self.events_timed = [event for event in self.events_timed if event.group_id==group_id] + self._sort_timed() + + def _sort_timed(self): + self.events_timed = sorted(self.events_timed, key = lambda event: event.deadline) + + def add_input(self,event): + self.events_input.append(event) + + def _handle_timed(self): + if not self.next_timed and self.events_timed: + self.next_timed = self.events_timed.pop(0) + + now = time.ticks_ms() + + if self.next_timed: + diff = time.ticks_diff(self.next_timed.deadline, now) + if diff <= 0: + self.next_timed.trigger({"ticks_ms":now, "ticks_delay": -diff}) + self.next_timed = None + + def _handle_input(self): + input_state={ + "b0":(hardware.get_button(0),"button",0), + "b1":(hardware.get_button(1),"button",1) + } + + for i in range(0,10): + input_state["c"+str(i)]=(hardware.get_captouch(i),"captouch",i) + + + if not self.last_input_state: + self.last_input_state=input_state + return + + diff=[] + for key in input_state: + if input_state[key][0] != self.last_input_state[key][0]: + diff.append({ + "type" : input_state[key][1], + "index" : input_state[key][2], + "value" : input_state[key][0], + "from" : self.last_input_state[key][0], + "ticks_ms": time.ticks_ms(), + "change": True + }) + else: + diff.append({ + "type" : input_state[key][1], + "index" : input_state[key][2], + "value" : input_state[key][0], + "ticks_ms": time.ticks_ms(), + "change": False + }) + + if diff: + #print(diff) + for d in diff: + triggered_events = list(filter(lambda e: e.condition(d),self.events_input)) + #print (triggered_events) + #map(lambda e: e.trigger(d), triggered_events) + for e in triggered_events: + e.trigger(d) + + + self.last_input_state=input_state + + def _handle_userloop(self): + if self.userloop: + self.userloop() + + def _eventloop_single(self): + self._handle_timed() + self._handle_input() + self._handle_userloop() + + + def eventloop(self): + if self.is_running: + print ("eventloop already running") + return + else: + print("eventloop started") + self.is_running=True + while self.is_running: + self._eventloop_single() + + time.sleep(0.005) + class Event(): - def __init__(self,name="unknown",data={},action=None,condition=None): - #print (action) - self.name = name - self.eventtype = None - self.data = data - self.action = action - self.condition = condition - if not condition: - self.condition = lambda x: True - - the_engine.add(self) - - #print (data) - - def trigger(self,triggerdata={}): - print ("triggered {} (with {})".format(self.name,triggerdata)) - if not self.action is None: - triggerdata.update(self.data) - #print("trigger") - self.action(triggerdata) - + def __init__(self,name="unknown",data={},action=None,condition=None,group_id=None): + #print (action) + self.name = name + self.eventtype = None + self.data = data + self.action = action + self.condition = condition + if not condition: + self.condition = lambda x: True + self.group_id=group_id + the_engine.add(self) + + + def trigger(self,triggerdata={}): + print ("triggered {} (with {})".format(self.name,triggerdata)) + if not self.action is None: + triggerdata.update(self.data) + self.action(triggerdata) + + def remove(self): + if self in the_engine.events_input: + the_engine.events_input.remove(self) + if self in the_engine.events_timed: + the_engine.events_timed.remove(self) class EventTimed(Event): - def __init__(self,ms,name="timer", *args, **kwargs): - #super().__init__(name,data,action) - self.deadline = time.ticks_add(time.ticks_ms(),ms) - - super().__init__(*args, **kwargs) - self.name=name - self.type=EVENTTYPE_TIMED - - def __repr__(self): - return ("event on tick {} ({})".format(self.deadline,self.name)) + def __init__(self,ms,name="timer", *args, **kwargs): + #super().__init__(name,data,action) + self.deadline = time.ticks_add(time.ticks_ms(),ms) + + super().__init__(*args, **kwargs) + self.name=name + self.type=EVENTTYPE_TIMED + + def __repr__(self): + return ("event on tick {} ({})".format(self.deadline,self.name)) class Sequence(): - def __init__(self,bpm=60,loop=True,steps=16,action=None): - self.bpm = bpm - if not action: - self.action = lambda data: print("step {}".format(data.get("step"))) - else: - self.action = action - stepsize_ms = int(60*1000/bpm) - for i in range(steps): - EventTimed(stepsize_ms*i,name="seq{}".format(i),action=self.action, data={'step':i}) - - if loop: - EventTimed(stepsize_ms*steps,name="loop",action=lambda data: Sequence(bpm=bpm,loop=loop,steps=steps,action=action)) + def __init__(self,bpm=60,loop=True,steps=16,action=None): + self.group_id = random.randint(0,100000000) + self.bpm = bpm + if not action: + self.action = lambda data: print("step {}".format(data.get("step"))) + else: + self.action = action + stepsize_ms = int(60*1000/bpm) + for i in range(steps): + EventTimed(stepsize_ms*i,name="seq{}".format(i),action=self.action, data={'step':i}, group_id=self.group_id) + + if loop: + EventTimed(stepsize_ms*steps,name="loop", group_id=self.group_id, action=lambda data: Sequence(bpm=bpm,loop=loop,steps=steps,action=action)) - + def remove(self): + the_engine.remove_timed(self.group_id) + global the_engine -the_engine = Engine() - -EventTimed(200,name="bar",action=lambda data: print("GNANGNAGNA")) -EventTimed(100,name="foo") -Event(name="baz", - action=lambda data: print(data), - condition=lambda data: data.get('type')=="captouch" -) - - - -#Sequence(action=axelf) - -print (the_engine.events_timed) -#the_engine.eventloop() - +the_engine = Engine() \ No newline at end of file diff --git a/python_payload/menu.py b/python_payload/menu.py new file mode 100644 index 0000000000000000000000000000000000000000..4e44a2b7d7088a23808918ef0e97a6d5363fd0cf --- /dev/null +++ b/python_payload/menu.py @@ -0,0 +1,180 @@ +import ui +import time +import hardware +import math +import event + +menu_stack = [] +active_menu = None + +class Menu(): + def __init__(self,name="menu",has_back=True): + self.name=name + self.items=[] + self.__index = 0 + self.ui = ui.GroupRing(r=80) + self.ui.element_center = ui.Text(self.name) + self.icon = ui.Icon(label=name) + self.angle = 0 + self.angle_step= 0.2 + if has_back: + self.add(MenuItemBack()) + + + def __repr__(self): + return "{} ({}): {}".format(self.name, self.__index, self.items) + + def add(self, item): + self.items.append(item) + self.ui.add(item.ui) + + def pop(self): + self.items.pop() + self.ui.children.pop() + + def start(self): + print(self) + active_menu = self + render() + + def scroll(self, n=0): + self.__index= (self.__index+n)%len(self.items) + return self.items[self.__index] + + def rotate_by(self,angle): + self.rotate_to(self.angle+angle) + + def rotate_to(self, angle): + self.angle = angle%(math.pi*2) + self.ui.angle_offset = self.angle + + def rotate_steps(self, steps=1): + self.rotate_by(self.angle_step*steps) + + def _get_hovered_index(self): + index = round(-self.angle/(math.pi*2)*len(self.items)) + i = index%len(self.items) + return i + + def get_hovered_item(self): + return self.items[self._get_hovered_index()] + + def _get_angle_for_index(self,index): + return (math.pi*2/len(self.items)*(index)+self.angle)%(math.pi*2) + + def _get_topness_for_index(self,index): + angle = self._get_angle_for_index(index) + dist = min(angle,math.pi*2-angle) + topness = 1-(dist/math.pi) + return topness + + + + def draw(self): + + hovered_index = self._get_hovered_index() + for i in range(len(self.items)): + item = self.items[i] + my_extra = abs(self._get_topness_for_index(i))*40 + + if i == hovered_index: + item.ui.has_highlight=True + my_extra+=20 + else: + item.ui.has_highlight=False + item.ui.size=30+my_extra + self.ui.draw() + + +class MenuItem(): + def __init__(self,name="item"): + self.name= name + self.action= None + self.ui = ui.Icon(label=name) + + def __repr__(self): + return "item: {} (action: {})".format(self.name,"?") + + def enter(self,data={}): + print("Enter MenuItem {}".format(self.name)) + if self.action: + self.action(data) + +class MenuItemSubmenu(MenuItem): + def __init__(self,submenu): + super().__init__(name=submenu.name) + self.ui = submenu.icon + self.target = submenu + + def enter(self,data={}): + print("Enter Submenu {}".format(self.target.name)) + menu_stack.append(active_menu) + set_active_menu(self.target) + +class MenuItemBack(MenuItem): + def __init__(self): + super().__init__(name="<-") + + def enter(self,data={}): + menu_back() + +def on_scroll(d): + if active_menu is None: + return + + if active_menu.angle_step<0.5: + active_menu.angle_step+=0.025 + if d["value"] == -1: + active_menu.rotate_steps(-1) + elif d["value"] == 1: + active_menu.rotate_steps(1) + render() + +def on_release(d): + if active_menu is None: + return + + active_menu.angle_step = 0.2 + render() + +def on_enter(d): + if active_menu is None: + event.the_engine.userloop=None + menu_back() + return + else: + active_menu.get_hovered_item().enter() + +event.Event(name="menu rotation", + condition=lambda e: e["type"] =="button" and not e["change"] and abs(e["value"])==1 , + action=on_scroll +) + +event.Event(name="menu rotation release", + condition=lambda e: e["type"] =="button" and e["change"] and e["value"] ==0, + action=on_release +) + +event.Event(name="menu enter", + condition=lambda e: e["type"] =="button" and e["change"] and e["value"] == 2, + action=on_enter +) + +def render(): + print (active_menu) + if active_menu is None: + return + hardware.get_ctx().rectangle(-120,-120,240,240).rgb(0,0,0).fill() + active_menu.draw() + hardware.display_update() + +def set_active_menu(menu): + global active_menu + active_menu = menu + +def menu_back(): + if not menu_stack: + return + + previous = menu_stack.pop() + set_active_menu(previous) \ No newline at end of file diff --git a/python_payload/ui.py b/python_payload/ui.py new file mode 100644 index 0000000000000000000000000000000000000000..48fb0eed34f2e8cc036e8be73a9ffef82c0ee226 --- /dev/null +++ b/python_payload/ui.py @@ -0,0 +1,112 @@ +import hardware +import random +import math +import time + +class UIElement(): + def __init__(self,origin=(0,0)): + self.children = [] + self.origin = origin + self.ctx = hardware.get_ctx() + + def draw(self, offset=(0,0)): + pos = (self.origin[0]+offset[0],self.origin[1]+offset[1]) + + self._draw(pos) + for child in self.children: + child.draw(pos) + + def _draw(self,pos): + #print(pos) + pass + + def add(self, child): + self.children.append(child) + +class Text(UIElement): + def __init__(self,s="foo"): + self.s = s + super().__init__() + + def _draw(self, pos): + self.ctx.text_align = self.ctx.CENTER + self.ctx.text_baseline = self.ctx.MIDDLE + self.ctx.rgb(1,1,1).move_to(pos[0],pos[1]).text(self.s) + +class Icon(UIElement): + def __init__(self,label="?", size=60): + self.bg_r = random.random() + self.bg_g = random.random() + self.bg_b = random.random() + self.fg = 0 + self.label=label + self.size = size + self.has_highlight = False + super().__init__() + + def _draw(self,pos): + x = int(pos[0]) + y = int(pos[1]) + width = 55 + height = 40 + self.ctx.text_align = self.ctx.CENTER + self.ctx.text_baseline = self.ctx.MIDDLE + + if self.has_highlight: + hs = self.size+5; + self.ctx.move_to(x,y-hs/2).rgb(1,1,1).round_rectangle( + x-hs/2, + y-hs/2, + hs,hs,hs//2 + ).fill() + self.ctx.move_to(x,y-self.size/2).rgb(self.bg_r,self.bg_g,self.bg_b).round_rectangle( + x-self.size/2, + y-self.size/2, + self.size,self.size,self.size//2 + ).fill() + self.ctx.rgb(1,1,1).move_to(x,y).text(self.label) +class GroupStackedVertical(UIElement): + pass + + +class GroupRing(UIElement): + def __init__(self,r=100,origin=(0,0),element_center=None): + self.r = r + self.angle_offset = 0 + self.element_center=element_center + super().__init__(origin) + + def draw(self, offset=(0,0)): + pos = (self.origin[0]+offset[0],self.origin[1]+offset[1]) + self._draw(pos) + for index in range(len(self.children)): + child = self.children[index] + angle = 2*math.pi/len(self.children)*index+self.angle_offset + x = math.sin(angle)*self.r+pos[0] + y = -math.cos(angle)*self.r+pos[1] + child.draw(offset=(x,y)) + + def _draw(self,pos): + if self.element_center: + self.element_center._draw(pos) + +def test(): + group = UIElement((10,0)) + group.add(UIElement((10,10))) + group.add(UIElement((20,20))) + #group.draw() + + ring = GroupRing(r=80) + ctx = hardware.get_ctx() + for i in range(12): + ring.add(Icon(str(i))) + hardware.display_update() + while True: + ctx.rectangle(0,0,240,240).rgb(1,1,1).fill() + ring.draw() + hardware.display_update() + ring.angle_offset+=2*math.pi/60 + time.sleep(0.01) + hardware.display_update() + +#test()