Hi all,
I am writing a little code to run at startup and monitor for keyboard clicks. Here's the skinny:
I have a Pi 4B and an official RPi 7" LCD in a Smartipi Touch Pro case. It is sitting on my desk.
I have two desktops enabled on the Pi and a Dakboard running on the first desktop. When I first turn on the Pi, it loads the Dakboard and all is well. I have a little keyboard on the desk for the Pi and I use it to pop over to the second desktop to do work with the OS as needed. The script in question is meant to run in the background and do a little count up to whatever the variable "maxidle" is set to. Once it reaches that, the script checks the active desktop and IF the active desktop isn't #1 (the Dakboard), it switches the active desktop over to that. Keyboard input should reset the count up to 0. In short, it puts the display back to a dakboard after I stop messing with the keyboard for some time.
The issue I have is that in testing, the keyboard input does reset the loop and whatnot. From time to time though, the script just stops on keyboard input. It doesn't reset the count. Here is my code thus far. Any advice is greatly welcomed:

Code: Select all

from pynput.keyboard import Listener
import subprocess
from subprocess import call
import time
maxidle = 3*60 #cycles
def on_press(key):
    global keyed
        a = key.char
        if a:
            print("key pressed!!")
            keyed = True
    except AttributeError:
        a = key
        if a:
            print("special key pressed!!")
            keyed = True
while True:
    lastkeyed = 0
    keyed = False
    print("keyed is", keyed)
    listener = Listener(on_press=on_press)
    listener.start()
    while not keyed:
        if lastkeyed < maxidle:
            print("lastkeyed is", lastkeyed)
            lastkeyed += 1
        elif lastkeyed == maxidle:
            lastkeyed += 1
            print("lastkeyed is", lastkeyed)
            print("maxidle reached (",maxidle,"). checking active desktop.")
            outputcheck = subprocess.check_output(["wmctrl", "-d"])
            outputcheckstr = outputcheck.decode('utf-8')
            splita,splitb = outputcheckstr.split("Primary",2)
            activedesk = "*"
            if activedesk in splita:
                print("Primary screen already active. do not change.")
            if activedesk in splitb:
                print("Secondary screen active. changing.")
                call (["wmctrl", "-s", "0"])
        elif lastkeyed > maxidle:
            lastkeyed += 1
            print("lastkeyed is", lastkeyed)
        time.sleep(1)
    listener.stop()
    print("resetting primary loop")
This is one of two scripts I have written for the project. The other is for the Adafruit BS412 PIR that I used in place of the Pi camera on the front of the SmartiPi case (drilling required for fit). It does a similar count up for 20 minutes on lack of PIR detection and then blanks the screen. Obviously, I have disabled the build-in screen blanking to do this method. One interesting bit of code that I needed there was a sort of "is this really motion" check because the PIR throws false positives that close to the Pi. The net result of that check is a delay of 8 or so seconds before turning on the monitor after valid motion is detected.
Thanks!
Let me propose an object oriented approach:
ScreenAction, responsible for the screen switching (did not find the action when IDLE and then go back to ACTIVE)
StateHandler is responsible to sort all keypress- and timeout-events. Instead of counting loop count, there is a Timer used.

Code: Select all

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import pynput.keyboard
import subprocess
import threading
# for development environment, set debug=True
# for production environment , set debug=False
debug = True
if debug:
    MAXIDLE = 10 # while developing, do not wait too long
else:
    MAXIDLE = 3*60 
class ScreenAction:
    def idle(self):
        if debug: print("ScreenAction.idle()")
        if not debug:
            pass # could not find this action in your code
    def acive(self):
        if debug: print("ScreenAction.active()")
        if not debug:
            outputcheck = subprocess.check_output(["wmctrl", "-d"])
            outputcheckstr = outputcheck.decode('utf-8')
            splita,splitb = outputcheckstr.split("Primary",2)
            activedesk = "*"
            if activedesk in splita:
                print("Primary screen already active. do not change.")
            if activedesk in splitb:
                print("Secondary screen active. changing.")
                subprocess.call (["wmctrl", "-s", "0"])
class StateHandler():
    START  = 'START'
    IDLE   = 'IDLE'
    ACTIVE = 'ACTIVE'
    def __init__(self):
        self.state = StateHandler.START
        # the callback come from either Timer or pyinput
        # to handle these threaded environments a Lock is needed
        self.lock = threading.Lock()
        self.timer = threading.Timer(MAXIDLE, self.timeout)
        self.timer.start()
    def keypress(self):
        with self.lock:
            if self.state == StateHandler.START:
                screen_action.acive()
                self.state = StateHandler.ACTIVE
            elif self.state == StateHandler.IDLE:
                screen_action.acive()
                self.state = StateHandler.ACTIVE
            elif self.state == StateHandler.ACTIVE:
            else:
                print("unknown state !", self.state)
            if debug: print("state", self.state)
            self.timer.cancel()
            self.timer=threading.Timer(MAXIDLE, self.timeout)
            self.timer.start()
    def timeout(self):
        with self.lock:
            if self.state == StateHandler.START:
                screen_action.idle()
                self.state = StateHandler.IDLE
            elif self.state == StateHandler.IDLE:
            elif self.state == StateHandler.ACTIVE:
                screen_action.idle()
                self.state = StateHandler.IDLE
            else:
                print("unknown state !", self.state)    
            if debug: print("state", self.state)        
print("started")
screen_action = ScreenAction()
state_handler = StateHandler()
def keypress(key):
    # if filtering on key is needed do this here
    state_handler.keypress()
listener = pynput.keyboard.Listener(on_press=keypress)
listener.start()
wait = threading.Event()
    wait.wait()
except KeyboardInterrupt:
except Exception as e:
    print(e)
print("terminated")
ghp wrote: ↑
Sat Jul 10, 2021 10:26 am
Let me propose an object oriented approach:
ScreenAction, responsible for the screen switching (did not find the action when IDLE and then go back to ACTIVE)
StateHandler is responsible to sort all keypress- and timeout-events. Instead of counting loop count, there is a Timer used.

Code: Select all

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import pynput.keyboard
import subprocess
import threading
# for development environment, set debug=True
# for production environment , set debug=False
debug = True
if debug:
    MAXIDLE = 10 # while developing, do not wait too long
else:
    MAXIDLE = 3*60 
class ScreenAction:
    def idle(self):
        if debug: print("ScreenAction.idle()")
        if not debug:
            pass # could not find this action in your code
    def acive(self):
        if debug: print("ScreenAction.active()")
        if not debug:
            outputcheck = subprocess.check_output(["wmctrl", "-d"])
            outputcheckstr = outputcheck.decode('utf-8')
            splita,splitb = outputcheckstr.split("Primary",2)
            activedesk = "*"
            if activedesk in splita:
                print("Primary screen already active. do not change.")
            if activedesk in splitb:
                print("Secondary screen active. changing.")
                subprocess.call (["wmctrl", "-s", "0"])
class StateHandler():
    START  = 'START'
    IDLE   = 'IDLE'
    ACTIVE = 'ACTIVE'
    def __init__(self):
        self.state = StateHandler.START
        # the callback come from either Timer or pyinput
        # to handle these threaded environments a Lock is needed
        self.lock = threading.Lock()
        self.timer = threading.Timer(MAXIDLE, self.timeout)
        self.timer.start()
    def keypress(self):
        with self.lock:
            if self.state == StateHandler.START:
                screen_action.acive()
                self.state = StateHandler.ACTIVE
            elif self.state == StateHandler.IDLE:
                screen_action.acive()
                self.state = StateHandler.ACTIVE
            elif self.state == StateHandler.ACTIVE:
            else:
                print("unknown state !", self.state)
            if debug: print("state", self.state)
            self.timer.cancel()
            self.timer=threading.Timer(MAXIDLE, self.timeout)
            self.timer.start()
    def timeout(self):
        with self.lock:
            if self.state == StateHandler.START:
                screen_action.idle()
                self.state = StateHandler.IDLE
            elif self.state == StateHandler.IDLE:
            elif self.state == StateHandler.ACTIVE:
                screen_action.idle()
                self.state = StateHandler.IDLE
            else:
                print("unknown state !", self.state)    
            if debug: print("state", self.state)        
print("started")
screen_action = ScreenAction()
state_handler = StateHandler()
def keypress(key):
    # if filtering on key is needed do this here
    state_handler.keypress()
listener = pynput.keyboard.Listener(on_press=keypress)
listener.start()
wait = threading.Event()
    wait.wait()
except KeyboardInterrupt:
except Exception as e:
    print(e)
print("terminated")
Thanks! I will give that a look over and see how it works :)
Salty_McBitters wrote: ↑
Sat Jul 10, 2021 4:56 pm
ghp wrote: ↑
Sat Jul 10, 2021 10:26 am
Let me propose an object oriented approach:
ScreenAction, responsible for the screen switching (did not find the action when IDLE and then go back to ACTIVE)
StateHandler is responsible to sort all keypress- and timeout-events. Instead of counting loop count, there is a Timer used.

Code: Select all

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import pynput.keyboard
import subprocess
import threading
# for development environment, set debug=True
# for production environment , set debug=False
debug = True
if debug:
    MAXIDLE = 10 # while developing, do not wait too long
else:
    MAXIDLE = 3*60 
class ScreenAction:
    def idle(self):
        if debug: print("ScreenAction.idle()")
        if not debug:
            pass # could not find this action in your code
    def acive(self):
        if debug: print("ScreenAction.active()")
        if not debug:
            outputcheck = subprocess.check_output(["wmctrl", "-d"])
            outputcheckstr = outputcheck.decode('utf-8')
            splita,splitb = outputcheckstr.split("Primary",2)
            activedesk = "*"
            if activedesk in splita:
                print("Primary screen already active. do not change.")
            if activedesk in splitb:
                print("Secondary screen active. changing.")
                subprocess.call (["wmctrl", "-s", "0"])
class StateHandler():
    START  = 'START'
    IDLE   = 'IDLE'
    ACTIVE = 'ACTIVE'
    def __init__(self):
        self.state = StateHandler.START
        # the callback come from either Timer or pyinput
        # to handle these threaded environments a Lock is needed
        self.lock = threading.Lock()
        self.timer = threading.Timer(MAXIDLE, self.timeout)
        self.timer.start()
    def keypress(self):
        with self.lock:
            if self.state == StateHandler.START:
                screen_action.acive()
                self.state = StateHandler.ACTIVE
            elif self.state == StateHandler.IDLE:
                screen_action.acive()
                self.state = StateHandler.ACTIVE
            elif self.state == StateHandler.ACTIVE:
            else:
                print("unknown state !", self.state)
            if debug: print("state", self.state)
            self.timer.cancel()
            self.timer=threading.Timer(MAXIDLE, self.timeout)
            self.timer.start()
    def timeout(self):
        with self.lock:
            if self.state == StateHandler.START:
                screen_action.idle()
                self.state = StateHandler.IDLE
            elif self.state == StateHandler.IDLE:
            elif self.state == StateHandler.ACTIVE:
                screen_action.idle()
                self.state = StateHandler.IDLE
            else:
                print("unknown state !", self.state)    
            if debug: print("state", self.state)        
print("started")
screen_action = ScreenAction()
state_handler = StateHandler()
def keypress(key):
    # if filtering on key is needed do this here
    state_handler.keypress()
listener = pynput.keyboard.Listener(on_press=keypress)
listener.start()
wait = threading.Event()
    wait.wait()
except KeyboardInterrupt:
except Exception as e:
    print(e)
print("terminated")
Thanks! I will give that a look over and see how it works :) Ok, I tried your code and it works great from the terminal! I had to modify it a little bit so the active/idle if statements worked as I wanted them:

Code: Select all

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import pynput.keyboard
import subprocess
import threading
# for development environment, set debug=True
# for production environment , set debug=False
debug = False
if debug:
    MAXIDLE = 10 # while developing, do not wait too long
else:
    MAXIDLE = 3*60
class ScreenAction:
    def idle(self):
        if debug: print("ScreenAction.idle()")
        if not debug:
            outputcheck = subprocess.check_output(["wmctrl", "-d"])
            outputcheckstr = outputcheck.decode('utf-8')
            splita,splitb = outputcheckstr.split("Primary",2)
            activedesk = "*"
            if activedesk in splita:
                print("Primary screen already active. do not change.")
            if activedesk in splitb:
                print("Secondary screen active. changing.")
                subprocess.call (["wmctrl", "-s", "0"])
    def acive(self):
        if debug: print("ScreenAction.active()")
        if not debug:# print("ScreenAction.active()", "ha!")
            pass # could not find this action in your code
class StateHandler():
    START  = 'START'
    IDLE   = 'IDLE'
    ACTIVE = 'ACTIVE'
    def __init__(self):
        self.state = StateHandler.START
        # the callback come from either Timer or pyinput
        # to handle these threaded environments a Lock is needed
        self.lock = threading.Lock()
        self.timer = threading.Timer(MAXIDLE, self.timeout)
        self.timer.start()
    def keypress(self):
        with self.lock:
            if self.state == StateHandler.START:
                screen_action.acive()
                self.state = StateHandler.ACTIVE
            elif self.state == StateHandler.IDLE:
                screen_action.acive()
                self.state = StateHandler.ACTIVE
            elif self.state == StateHandler.ACTIVE:
            else:
                print("unknown state !", self.state)
            if debug: print("state", self.state)
            self.timer.cancel()
            self.timer=threading.Timer(MAXIDLE, self.timeout)
            self.timer.start()
    def timeout(self):
        with self.lock:
            if self.state == StateHandler.START:
                screen_action.idle()
                self.state = StateHandler.IDLE
            elif self.state == StateHandler.IDLE:
            elif self.state == StateHandler.ACTIVE:
                screen_action.idle()
                self.state = StateHandler.IDLE
            else:
                print("unknown state !", self.state)    
            if debug: print("state", self.state)        
print("started")
screen_action = ScreenAction()
state_handler = StateHandler()
def keypress(key):
    # if filtering on key is needed do this here
    state_handler.keypress()
listener = pynput.keyboard.Listener(on_press=keypress)
listener.start()
wait = threading.Event()
    wait.wait()
except KeyboardInterrupt:
except Exception as e:
    print(e)
print("terminated")
The issue I am now running into is when I try to start the script at boot using crontab. Here's my launcher:

Code: Select all

#!/bin/sh
#!launcher.sh
cd home/pi/scripts/kb_timeout
/bin/sleep 30
sudo python kb_timeout.py
The "/bin/sleep 30" line was added to try to address the error I am still getting.  Unfortunately, it doesn't help. 
And here's the entry in 'sudo crontab -e':

Code: Select all

@reboot sh /home/pi/scripts/kb_timeout/launcher.sh >/home/pi/scripts/kb_timeout/logs/cronlog 2>&1
After reboot, the cronlog shows:

Code: Select all

Traceback (most recent call last):
  File "kb_timeout.py", line 4, in <module>
    import pynput.keyboard
  File "/usr/local/lib/python2.7/dist-packages/pynput/__init__.py", line 40, in <module>
    from . import keyboard
  File "/usr/local/lib/python2.7/dist-packages/pynput/keyboard/__init__.py", line 31, in <module>
    backend = backend(__name__)
  File "/usr/local/lib/python2.7/dist-packages/pynput/_util/__init__.py", line 82, in backend
    if resolutions else '')
ImportError: this platform is not supported: ('failed to acquire X connection: Bad display name ""', DisplayNameError())
Try one of the following resolutions:
 * Please make sure that you have an X server running, and that the DISPLAY environment variable is set correctly
I am sure this is something I am simply overlooking... should I start the script another way than crontab maybe?  This is a Raspberry Pi 8GB running with the official RPi 7" touchscreen attached.
In the launcher
- you should use 'python3' instead of python(2). Python2 is obsolete.
- add "export DISPLAY=:0" before starting the python app.
Or start your application with "desktop auto start". viewtopic.php?t=294014
ghp wrote: ↑
Sun Jul 11, 2021 6:32 am
In the launcher
- you should use 'python3' instead of python(2). Python2 is obsolete.
- add "export DISPLAY=:0" before starting the python app.
Or start your application with "desktop auto start". viewtopic.php?t=294014 You have been tremendously helpful. Thanks! I had already tried using python3 and "export DISPLAY=:0". The former resulted in no change in outcome and the latter generated a more verbose error about no protocol being specified.
I even tried the desktop auto start at one point, but I did so with no guidance and had created a blank "Autostart" file. Not having those two mandatory lines at the beginning did indeed create a blank screen so I left that method be and focused on crontab. Now that I realize my folly (thank you for the link!), I was able to get autostart configured correctly and everything is working as intended.
Thank you again for your assistance!