pythonpython-3.xkivygpiogpiozero

kivy: "Exception: Shader didnt link" when called by gpiozero callback, but not by kivy.uix.button callback


I am writing a GUI using a camera in kivy, and am unsure why my code is not working. I have a camera feed, and two methods of capturing a picture from it: one triggered by a gpiozero when_pressed callback, and one triggered by a kivy.uix.button on_press callback.

The kivy.uix.button callback succeeds in capturing an image, but the gpiozero callback says Exception: Shader didnt link, check info log., fails to save an image, and then makes the camera feed go black (although images can later still be captured with the successful option). Why does one callback work but not the other?

Here is the related code, and the corresponding terminal outputs. I've annotated the terminal output with # ALL CAPS COMMENTS. (My code is inspired by the kivy docs camera example, which also captures successfully).

main.py

import kivy
#kivy.require('1.11.1')

# Uncomment these lines to see all the messages
#from kivy.logger import Logger
#import logging
#Logger.setLevel(logging.TRACE)

from kivy.app import App
from kivy.uix.widget import Widget
from kivy.properties import ObjectProperty
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.button import Button
from kivy.uix.camera import Camera
from kivy.core.window import Window
from gpiozero import Button as gpiozeroButton   # renamed to avoid conflict w/ kivy.uix.button
import time

Window.fullscreen = 'auto'  # uses display's current resolution

capture_btn  = gpiozeroButton(pin=13, pull_up=False)    # set up GPIO

class RootWidget(FloatLayout):

    def __init__(self, **kwargs):
        super(RootWidget, self).__init__(**kwargs)
        capture_btn.when_pressed = self.capture    # initialize callback for GPIO button
        
    def capture(self):
        print('Capture step 1')
        camera = self.ids['camera']
        print('Capture step 2')
        timestr = time.strftime("%Y%m%d_%H%M%S")
        print('Capture step 3')
        camera.export_to_png("IMG_{}.png".format(timestr))
        print("Captured")


class LifterApp(App):

    def build(self):
        return RootWidget()


if __name__ == '__main__':
    LifterApp().run()

lifter.kv

# #:kivy 1.11.1

<RootWidget>:

    Camera:
        id: camera
        resolution: (640, 480)
        play: True
    Button:
        text: "capture"
        pos_hint: {'x':0.0, 'y':0.0}
        size_hint: (0.2, 0.2)
        on_press: root.capture()

terminal output (annotated)

[INFO   ] [Logger      ] Record log in /home/pi/.kivy/logs/kivy_20-12-30_27.txt
[INFO   ] [Kivy        ] v1.11.1
[INFO   ] [Kivy        ] Installed at "/usr/local/lib/python3.7/dist-packages/kivy/__init__.py"
[INFO   ] [Python      ] v3.7.3 (default, Dec 20 2019, 18:57:59) 
[GCC 8.3.0]
[INFO   ] [Python      ] Interpreter at "/usr/bin/python3"
[INFO   ] [Factory     ] 184 symbols loaded
[INFO   ] [Image       ] Providers: img_tex, img_dds, img_sdl2, img_pil, img_gif (img_ffpyplayer ignored)
[INFO   ] [Text        ] Provider: sdl2(['text_pango'] ignored)
[INFO   ] [Camera      ] Provider: picamera
[INFO   ] [Window      ] Provider: sdl2(['window_egl_rpi'] ignored)
[INFO   ] [GL          ] Using the "OpenGL" graphics system
[INFO   ] [GL          ] Backend used <sdl2>
[INFO   ] [GL          ] OpenGL version <b'3.1 Mesa 19.3.2'>
[INFO   ] [GL          ] OpenGL vendor <b'VMware, Inc.'>
[INFO   ] [GL          ] OpenGL renderer <b'llvmpipe (LLVM 9.0.1, 128 bits)'>
[INFO   ] [GL          ] OpenGL parsed version: 3, 1
[INFO   ] [GL          ] Shading version <b'1.40'>
[INFO   ] [GL          ] Texture max size <8192>
[INFO   ] [GL          ] Texture max units <32>
[INFO   ] [Window      ] auto add sdl2 input provider
[INFO   ] [Window      ] virtual keyboard not allowed, single mode, not docked
[INFO   ] [ProbeSysfs  ] device match: /dev/input/event0
[INFO   ] [MTD         ] Read event from </dev/input/event0>
[INFO   ] [ProbeSysfs  ] device match: /dev/input/event0
[INFO   ] [HIDInput    ] Read event from </dev/input/event0>
[INFO   ] [Base        ] Start application main loop
[INFO   ] [MTD         ] </dev/input/event0> range position X is 0 - 800
[INFO   ] [MTD         ] </dev/input/event0> range position Y is 0 - 480
[INFO   ] [HIDMotionEvent] using <WaveShare WS170120>
[INFO   ] [MTD         ] </dev/input/event0> range touch major is 0 - 0
[INFO   ] [HIDMotionEvent] <WaveShare WS170120> range ABS X position is 0 - 800
[INFO   ] [MTD         ] </dev/input/event0> range touch minor is 0 - 0
[INFO   ] [HIDMotionEvent] <WaveShare WS170120> range ABS Y position is 0 - 480
[INFO   ] [MTD         ] </dev/input/event0> range pressure is 0 - 255
[INFO   ] [HIDMotionEvent] <WaveShare WS170120> range ABS pressure is 0 - 255
[INFO   ] [MTD         ] </dev/input/event0> axes invertion: X is 0, Y is 0
[INFO   ] [HIDMotionEvent] <WaveShare WS170120> range position X is 0 - 800
[INFO   ] [MTD         ] </dev/input/event0> rotation set to 0
[INFO   ] [HIDMotionEvent] <WaveShare WS170120> range position Y is 0 - 480
[INFO   ] [HIDMotionEvent] <WaveShare WS170120> range pressure is 0 - 255
[INFO   ] [GL          ] NPOT texture support is available
Capture step 1 # TRIGGERED BY KIVY.UIX.BUTTON ON_PRESS CALLBACK
Capture step 2
Capture step 3
Captured
Capture step 1 # TRIGGERED BY GPIOZERO WHEN_PRESSED CALLBACK
Capture step 2
Capture step 3
 Traceback (most recent call last):
   File "/usr/lib/python3/dist-packages/gpiozero/pins/rpigpio.py", line 244, in _call_when_changed
     super(RPiGPIOPin, self)._call_when_changed()
   File "/usr/lib/python3/dist-packages/gpiozero/pins/local.py", line 143, in _call_when_changed
     self.state if state is None else state)
   File "/usr/lib/python3/dist-packages/gpiozero/pins/pi.py", line 293, in _call_when_changed
     method(ticks, state)
   File "/usr/lib/python3/dist-packages/gpiozero/input_devices.py", line 197, in _pin_changed
     self._fire_events(ticks, bool(self._state_to_value(state)))
   File "/usr/lib/python3/dist-packages/gpiozero/mixins.py", line 368, in _fire_events
     self._fire_activated()
   File "/usr/lib/python3/dist-packages/gpiozero/mixins.py", line 397, in _fire_activated
     super(HoldMixin, self)._fire_activated()
   File "/usr/lib/python3/dist-packages/gpiozero/mixins.py", line 344, in _fire_activated
     self.when_activated()
   File "main.py", line 35, in capture
     camera.export_to_png("IMG_{}.png".format(timestr))
   File "/usr/local/lib/python3.7/dist-packages/kivy/uix/widget.py", line 727, in export_to_png
     self.export_as_image().save(filename, flipped=False)
   File "/usr/local/lib/python3.7/dist-packages/kivy/uix/widget.py", line 744, in export_as_image
     with_stencilbuffer=True)
   File "kivy/graphics/fbo.pyx", line 152, in kivy.graphics.fbo.Fbo.__init__
   File "kivy/graphics/instructions.pyx", line 777, in kivy.graphics.instructions.RenderContext.__init__
   File "kivy/graphics/shader.pyx", line 184, in kivy.graphics.shader.Shader.__init__
   File "kivy/graphics/shader.pyx", line 701, in kivy.graphics.shader.Shader.vs.__set__
   File "kivy/graphics/shader.pyx", line 557, in kivy.graphics.shader.Shader.build_vertex
   File "kivy/graphics/shader.pyx", line 587, in kivy.graphics.shader.Shader.link_program
 Exception: Shader didnt link, check info log.
^C[INFO   ] [Base        ] Leaving application in progress...
 Traceback (most recent call last):
   File "main.py", line 46, in <module>
     LifterApp().run()
   File "/usr/local/lib/python3.7/dist-packages/kivy/app.py", line 855, in run
     runTouchApp()
   File "/usr/local/lib/python3.7/dist-packages/kivy/base.py", line 504, in runTouchApp
     EventLoop.window.mainloop()
   File "/usr/local/lib/python3.7/dist-packages/kivy/core/window/window_sdl2.py", line 747, in mainloop
     self._mainloop()
   File "/usr/local/lib/python3.7/dist-packages/kivy/core/window/window_sdl2.py", line 479, in _mainloop
     EventLoop.idle()
   File "/usr/local/lib/python3.7/dist-packages/kivy/base.py", line 339, in idle
     Clock.tick()
   File "/usr/local/lib/python3.7/dist-packages/kivy/clock.py", line 591, in tick
     self._process_events()
   File "kivy/_clock.pyx", line 384, in kivy._clock.CyClockBase._process_events
   File "kivy/_clock.pyx", line 414, in kivy._clock.CyClockBase._process_events
   File "kivy/_clock.pyx", line 412, in kivy._clock.CyClockBase._process_events
   File "kivy/_clock.pyx", line 167, in kivy._clock.ClockEvent.tick
   File "/usr/local/lib/python3.7/dist-packages/kivy/core/camera/camera_picamera.py", line 71, in _update
     self._camera.capture(output, self._format, use_video_port=True)
   File "/usr/lib/python3/dist-packages/picamera/camera.py", line 1421, in capture
     if not encoder.wait(self.CAPTURE_TIMEOUT):
   File "/usr/lib/python3/dist-packages/picamera/encoders.py", line 393, in wait
     result = self.event.wait(timeout)
   File "/usr/lib/python3.7/threading.py", line 552, in wait
     signaled = self._cond.wait(timeout)
   File "/usr/lib/python3.7/threading.py", line 300, in wait
     gotit = waiter.acquire(True, timeout)
 KeyboardInterrupt

Solution

  • you can use the kivy.clock.mainthread decorator to replace your __wrapper function.

    from kivy.clock import mainthread
    
    ...
    
    class RootWidget(FloatLayout):
       ...
    
       @mainthread
       def capture(self):
            ...
    

    also this gets ride of the dt argument that you don't need, as it preserves the signature of the function.

    The original issue is certainly that the code managing when_pressed runs in a thread, and hence plays badly with any opengl operation (you get a "shader didn't link", but you might as well get a crash, or nothing, on another computer), as opengl is not supposed to be used from multiple threads, the mainthread decorator, by using the Clock to delegate to the main thread, avoids the issue.