I have a python script running on my RPi. It uses the Gpiozero library (that's really great by the way).
For testing purposes i was wondering if it was possible to emulate GPIO states somehow (say emulate pressing a button) and have it picked up by the gpiozero library.
Thanks !
TLDNR: Yes, it is possible.
I am not aware of any already prepared solution that can help you achieve what you want to do. Hence I found it very interesting whether it is feasible at all.
I was looking a while for a seam which can be used to stub the GPIO features and I have found that gpiozero uses GPIOZERO_PIN_FACTORY environmental variable to pick a backend. The plan is to write own pin factory, that will provide possibility to test other scripts.
NOTE: Please treat my solution as a proof of concept. It is far from being production ready.
The idea is to get GPIO states out of script under test scope. My solution uses env variable RPI_STUB_URL to get path of unix socket which will be used to communicate with the stub controller.
I have introduced very simple one request/response per connection protocol:
"GF {pin}\n" - ask what is the current function of the pin. Stub does not validate the response, but I would expect "input", "output" to be used.
"SF {pin} {function}\n" - request change of the pin's current function. Stub does not validate the function, bu I would expect "input", "output" to be used. Stub expects "OK" as a response.
"GS {pin}\n" - ask what is the current state of the pin. Stub expects values "0" or "1" as a response.
"SS {pin} {value|]n" - request change of the pin's current state. Stub expects "OK" as a response.
My "stub package" contains following files:
- setup.py # This file is needed in every package, isn't it?
- rpi_stub/
- __init__.py # This file collects entry points
- stubPin.py # This file implements stub backend for gpiozero
- controller.py # This file implements server for my stub
- trigger.py # This file implements client side feature of my stub
Let's start with setup.py content:
from setuptools import setup, find_packages
setup(
name="Raspberry PI GPIO stub",
version="0.1",
description="Package with stub plugin for gpiozero library",
packages=find_packages(),
install_requires = ["gpiozero"],
include_package_data=True,
entry_points="""
[console_scripts]
stub_rpi_controller=rpi_stub:controller_main
stub_rpi_trigger=rpi_stub:trigger_main
[gpiozero_pin_factories]
stub_rpi=rpi_stub:make_stub_pin
"""
)
It defines two console_scripts entry points one for controller and one for trigger. And one pin factory for gpiozero.
Now rpi_stub/__init__.py:
import rpi_stub.stubPin
from rpi_stub.controller import controller_main
from rpi_stub.trigger import trigger_main
def make_stub_pin(number):
return stubPin.StubPin(number)
It is rather simple file.
File rpi_stub/trigger.py:
import socket
import sys
def trigger_main():
socket_addr = sys.argv[1]
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.connect(socket_addr)
request = "{0}\n".format(" ".join(sys.argv[2:]))
sock.sendall(request.encode())
data = sock.recv(1024)
sock.close()
print(data.decode("utf-8"))
trigger allows you to make your own request. You can use it to check what is the state of GPIO pin or change it.
File rpi_stub/controller.py:
import socketserver
import sys
functions = {}
states = {}
class MyHandler(socketserver.StreamRequestHandler):
def _respond(self, response):
print("Sending response: {0}".format(response))
self.wfile.write(response.encode())
def _handle_get_function(self, data):
print("Handling get_function: {0}".format(data))
try:
self._respond("{0}".format(functions[data[0]]))
except KeyError:
self._respond("input")
def _handle_set_function(self, data):
print("Handling set_function: {0}".format(data))
functions[data[0]] = data[1]
self._respond("OK")
def _handle_get_state(self, data):
print("Handling get_state: {0}".format(data))
try:
self._respond("{0}".format(states[data[0]]))
except KeyError:
self._respond("0")
def _handle_set_state(self, data):
print("Handling set_state: {0}".format(data))
states[data[0]] = data[1]
self._respond("OK")
def handle(self):
data = self.rfile.readline()
print("Handle: {0}".format(data))
data = data.decode("utf-8").strip().split(" ")
if data[0] == "GF":
self._handle_get_function(data[1:])
elif data[0] == "SF":
self._handle_set_function(data[1:])
elif data[0] == "GS":
self._handle_get_state(data[1:])
elif data[0] == "SS":
self._handle_set_state(data[1:])
else:
self._respond("Not understood")
def controller_main():
socket_addr = sys.argv[1]
server = socketserver.UnixStreamServer(socket_addr, MyHandler)
server.serve_forever()
This file contains the simplest server I was able to write.
And the most complicated file rpi_stub/stubPin.py:
from gpiozero.pins import Pin
import os
import socket
from threading import Thread
from time import sleep
def dummy_func():
pass
def edge_detector(pin):
print("STUB: Edge detector for pin: {0} spawned".format(pin.number))
while pin._edges != "none":
new_state = pin._get_state()
print("STUB: Edge detector for pin {0}: value {1} received".format(pin.number, new_state))
if new_state != pin._last_known:
print("STUB: Edge detector for pin {0}: calling callback".format(pin.number))
pin._when_changed()
pin._last_known = new_state
sleep(1)
print("STUB: Edge detector for pin: {0} ends".format(pin.number))
class StubPin(Pin):
def __init__(self, number):
super(StubPin, self).__init__()
self.number = number
self._when_changed = dummy_func
self._edges = "none"
self._last_known = 0
def _make_request(self, request):
server_address = os.getenv("RPI_STUB_URL", None)
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.connect(server_address)
sock.sendall(request.encode())
data = sock.recv(1024)
sock.close()
return data.decode("utf-8")
def _get_function(self):
response = self._make_request("GF {pin}\n".format(pin=self.number))
return response;
def _set_function(self, function):
response = self._make_request("SF {pin} {function}\n".format(pin=self.number, function=function))
if response != "OK":
raise Exception("STUB Not understood", response)
def _get_state(self):
response = self._make_request("GS {pin}\n".format(pin=self.number))
if response == "1":
return 1
else:
return 0
def _set_pull(self, value):
pass
def _set_edges(self, value):
print("STUB: set edges called: {0}".format(value))
if self._edges == "none" and value != "none":
self._thread = Thread(target=edge_detector,args=(self,))
self._thread.start()
if self._edges != "none" and value == "none":
self._edges = value;
self._thread.join()
self._edges = value
pass
def _get_when_changed(self, value):
return self._when_changed
def _set_when_changed(self, value):
print("STUB: set when changed: {0}".format(value))
self._when_changed = value
def _set_state(self, value):
response = self._make_request("SS {pin} {value}\n".format(pin=self.number, value=value))
if response != "OK":
raise Exception("Not understood", response)
The file defines StubPin which extends Pin from gpiozero. It defines all functions that was mandatory to be overriden. It also contains very naive edge detection as it was needed for gpio.Button to work.
Let's make a demo :). Let's create virtualenv which gpiozero and my package installed:
$ virtualenv -p python3 rpi_stub_env
[...] // virtualenv successfully created
$ source ./rpi_stub_env/bin/activate
(rpi_stub_env)$ pip install gpiozero
[...] // gpiozero installed
(rpi_stub_env)$ python3 setup.py install
[...] // my package installed
Now let's create stub controller (open in other terminal etc.):
(rpi_stub_env)$ stub_rpi_controller /tmp/socket.sock
I will use the following script example.py:
from gpiozero import Button
from time import sleep
button = Button(2)
while True:
if button.is_pressed:
print("Button is pressed")
else:
print("Button is not pressed")
sleep(1)
Let's execute it:
(rpi_stub_env)$ RPI_STUB_URL=/tmp/socket.sock GPIOZERO_PIN_FACTORY=stub_rpi python example.py
By default the script prints that the button is pressed. Now let's press the button:
(rpi_stub_env)$ stub_rpi_trigger /tmp/socket.sock SS 2 1
Now the script should print that the button is not pressed. If you execute the following command it will be pressed again:
(rpi_stub_env)$ stub_rpi_trigger /tmp/socket.sock SS 2 0
I hope it will help you.
Related
I'm trying to write a terminal application to interact with an Arduino microcontroller via pyserial. The following features are important:
Print incoming messages to the command line.
Allow the user to enter output messages to the serial port. The input should be possible, while new incoming messages are printed.
In principle, this should be possible with cmd. But I'm struggling with printing incoming messages, when the user started typing.
For simplicity, I wrote the following test script emulating incoming messages every second. Outgoing messages are just echoed back to the command line with the prefix ">":
#!/usr/bin/env python3
from cmd import Cmd
from threading import Thread
import time
class Prompt(Cmd):
def default(self, inp):
print('>', inp)
stop = False
def echo():
while not stop:
print(time.time())
time.sleep(1)
thread = Thread(target=echo)
thread.daemon = True
thread.start()
try:
Prompt().cmdloop()
except KeyboardInterrupt:
stop = True
thread.join()
In Spyder IDE, the result is just perfect:
But in iterm2 (Mac OS) the output is pretty messed up:
Since I want to use this application from within Visual Studio Code, it should work outside Spyder. Do you have any idea how to get the same behaviour in iterm2 as in Spyder?
Things I already considered or tried out:
Use the curses library. This solves my problem of printing text to different regions. But I'm loosing endless scrolling, since curses defines its own fullscreen window.
Move the cursor using ansi escape sequences. It might be a possible solution, but I'm just not getting it to work. It always destroys the bottom line where the user is typing. I might need to adjust the scrolling region, which I still didn't manage to do.
Use a different interpreter. I already tried Python vs. iPython, without success. It might be a more subtle setting in Spyder's interpreter.
Yes! I found a solution: The Prompt Toolkit 3.0 in combination with asyncio lets you handle this very problem using patch_stdout, "a context manager that ensures that print statements within it won’t destroy the user interface".
Here is a minimum working example:
#!/usr/bin/env python3
from prompt_toolkit import PromptSession
from prompt_toolkit.patch_stdout import patch_stdout
import asyncio
import time
async def echo():
while True:
print(time.time())
await asyncio.sleep(1)
async def read():
session = PromptSession()
while True:
with patch_stdout():
line = await session.prompt_async("> ")
print(line.upper())
loop = asyncio.get_event_loop()
loop.create_task(echo())
loop.create_task(read())
loop.run_forever()
It's a while since I was interacting with an Arduino with my Mac. I used pyserial and it was 100% reliable. key is user read_until(). I've included my wrapper class for illustration. (Also has an emulation mode for when I didn't have a Arduino)
import serial # pip install PySerial
from serial.tools import list_ports
import pty, os # for creating virtual serial interface
from serial import Serial
from typing import Optional
class SerialInterface:
# define constants which control how class works
FULLEMULATION=0
SERIALEMULATION=1
URLEMULATION=2
FULLSOLUTION=3
# define private class level variables
__emulate:int = FULLEMULATION
__ser:Serial
__port:str = ""
def __init__(self, emulate:int=FULLEMULATION, port:str="") -> None:
self.__buffer:list = []
self.__emulate = emulate
self.__port = port
#self.listports()
# setup connection to COM/serial port
# emulation sets up a virtual port, but this has not been working
if emulate == self.FULLSOLUTION:
self.__ser = serial.Serial(port, 9600)
elif emulate == self.SERIALEMULATION:
master, slave = pty.openpty()
serialport = os.ttyname(slave)
self.__ser = serial.Serial(port=serialport, baudrate=9600, timeout=1)
elif emulate == self.URLEMULATION:
self.__ser = serial.serial_for_url("loop://")
# useful to show COM/serial ports on a computer
#staticmethod
def listports() -> list:
for p in serial.tools.list_ports.comports():
print(p, p.device)
serialport = p.device
return serial.tools.list_ports.comports()
def read_until(self, expected:bytes=b'\n', size:Optional[int]=None) -> bytes:
if self.__emulate == self.FULLEMULATION:
return self.__buffer.pop()
else:
return self.__ser.read_until(expected, size)
# note it is important to have \n on end of every write to allow data to be read item by item
def write(self, bytes:bytes=b'') -> None:
if self.__emulate == self.FULLEMULATION:
self.__buffer.append(bytes)
else:
self.__ser.write(bytes)
def dataAvail(self) -> bool:
if self.__emulate == self.FULLEMULATION:
return len(self.__buffer) > 0
else:
return self.__ser.inWaiting() > 0
def close(self) -> None:
self.__ser.close()
def mode(self) -> int:
return self.__emulate
I want to run some Python code as a service on Windows. I use Python 3.6 by the way.
I have put together (from various sources) this generalized windows daemon class. When I run it, it works well. However, when I subclass this to use in a service that contains some actual useful code, the service is not started but returns immediately with error The service did not respond to the start or control request in a timely fashion.
So - see example below - windows_daemon.py works, but new_service.py does not.
Can anyone see why my python service does not start on windows when I subclass it?
windows_daemon.py
import time
import sys
import logging
import logging.handlers
import traceback
import win32serviceutil
import win32service
import win32event
import servicemanager
import os
def handle(instance):
win32serviceutil.HandleCommandLine(instance)
class windows_daemon(win32serviceutil.ServiceFramework):
# you can NET START/STOP the service by the following name
_svc_name_ = "Daemon"
# this text shows up as the service name in the Service
# Control Manager (SCM)
_svc_display_name_ = "Windows daemon"
# this text shows up as the description in the SCM
_svc_description_ = "This service controls some stuff"
# list dependencies
_svc_deps_ = ["EventLog"]
def __init__(self, args):
win32serviceutil.ServiceFramework.__init__(self,args)
# create an event to listen for stop requests on
self.hWaitStop = win32event.CreateEvent(None,0,0,None)
self.Alive = True
self.host = os.environ['COMPUTERNAME'].lower()
self.pid = os.getpid()
self.name = self._svc_display_name_
self.rootlogger = logging.getLogger()
#self.logger = logging.getLogger("{}".format(self.name))
self.rootlogger.setLevel(logging.DEBUG)
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
handler = logging.handlers.RotatingFileHandler("{0}\log\{1}.log".format(os.path.dirname(os.path.realpath(__file__)), self.name), maxBytes = 1000000, backupCount = 5)
handler.setFormatter(formatter)
self.rootlogger.addHandler(handler)
self.logger = logging.getLogger("service")
# called when we're being shut down
def SvcStop(self):
self.logger.info("stopping service")
# tell the SCM we're shutting down
self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
# fire the stop event
win32event.SetEvent(self.hWaitStop)
self.Alive = False
# core logic of the service
def SvcDoRun(self):
self.ReportServiceStatus(win32service.SERVICE_RUNNING)
servicemanager.LogMsg(servicemanager.EVENTLOG_INFORMATION_TYPE,
servicemanager.PYS_SERVICE_STARTED,
(self._svc_name_, ''))
self.logger.debug("starting service")
self.timeout = 100
try:
self.main()
self.logger.debug("no exception in main")
exitcode = 0
except Exception as e:
#could also debug to Windows event log
#servicemanager.LogErrorMsg(traceback.format_exc())
#servicemanager.LogErrorMsg("{}".format(e))
try:
self.logger.error(e)
self.logger.error(traceback.format_exc())
except Exception as e:
pass
# servicemanager.LogErrorMsg("{}".format(e))
#return some value other than 0 to os so that service knows to restart
self.logger.error("some exception in main")
exitcode = -1
servicemanager.LogMsg(servicemanager.EVENTLOG_INFORMATION_TYPE,
servicemanager.PYS_SERVICE_STOPPED,
(self._svc_name_, ''))
self.ReportServiceStatus(win32service.SERVICE_STOPPED)
self.logger.info("stopped with exit code {}".format(exitcode))
os._exit(exitcode)
def check_for_signals(self, timeout = 100):
rc = win32event.WaitForSingleObject(self.hWaitStop, self.timeout)
def main(self):
# override in subclass
print("this is function main() that should be subclassed")
#time.sleep(1)
pass
if __name__ == '__main__':
handle(windows_daemon)
print("you should subclass windows_daemon from this module")
new_service.py
from windows_daemon import windows_daemon, handle
import time
import sys
import win32serviceutil
class New_service(windows_daemon):
# you can NET START/STOP the service by the following name
_svc_name_ = "NEW"
# this text shows up as the service name in the Service
# Control Manager (SCM)
_svc_display_name_ = "New service"
# this text shows up as the description in the SCM
_svc_description_ = "This service exists as a template for new services"
# override dependencies?
# _svc_deps_ = ["EventLog"]
def __init__(self, args):
print(super())
super().__init__(args)
def main(self):
time.sleep(10)
if __name__ == '__main__':
sys.frozen = 'windows_exe'
handle(New_service)
Apparently, this line caused the problem. I found that by learning about the python debugging module (pdb) and using it to investigate.
sys.frozen = 'windows_exe'
When I removed this line, the new_service.py also ran. Hope this helps someone in the future.
I've been trying to perform communication between a kivy GUI module (my server) and another python module (my client). But so far I have problems running the xml rpc server along with the GUI run() function. I still have that problem even after running my server in a thread.
I hope someone has suggestions on how to fix my code, or just how to xml-rpc along with kivy.
Here is my code:
import kivy
kivy.require('1.7.1')
from kivy.lang import Builder
from kivy.uix.gridlayout import GridLayout
from kivy.app import App
from threading import Thread
from kivy.clock import Clock
Builder.load_file('kivy_gui.kv')
class RoamClientInterface(GridLayout):
"""
Sets up connection with XMLRPC server
"""
move = False
"""
driveForward() -> Moves robot forward
"""
def driveForward(self):
self.move = True
"""
stop() -> stops robot from moving
"""
def stop(self):
self.move = False
def returnBool(self):
return self.move
class ClientInterface(App):
def build(self):
return RoamClientInterface()
def sendCommands(dt):
print "start"
print ""
from SimpleXMLRPCServer import SimpleXMLRPCServer
server = SimpleXMLRPCServer(("localhost", 5000))
print "initialize server"
print ""
server.register_instance(RoamClientInterface())
print "register instance"
print ""
# while True:
try:
print "try handle request"
print ""
server.handle_request()
print "print handle request"
print ""
except KeyboardInterrupt:
import sys
sys.exit()
if __name__ == '__main__':
serverThread = Thread(target=sendCommands(4))
serverThread.start()
# Clock.schedule_once(sendCommands)
ClientInterface().run()
I got to solve the problem.
It is actually necessary to put it into a inside the RoamClientInterface to make it work, instead of putting it into my main function like I have it above.
I can give more detail info (show code) if anybody needs help
Basically I'm building an application that has a couple of numbered options for you to pick from.
It's named main.py, I wrote standalone modules for each possible option so I can run the modules seperately. Now this one module I wrote It contains a threaded class. a problem I'm having when I command : python mod_keepOnline.py is that it does not pass control back to the terminal |AND| When I run the module trough main.py, main.py stops listening for a new choice to pick. I know it's because of the threads. I was wondering how I can "let the threads manage their own after they have been spawned". So get back control from mod_keepOnline.py to the terminal or main script.
I also want to be able to kill the released threads again.
something like mod_keepOnline.py -killAll
Uhm heres my code :
###########################################
################## SynBitz.net ############
import threading
import objects
import time
import mechanize
import os
import gb
##########################################
class Class_putOnline (threading.Thread):
def __init__ (self,person,onlineTime):
threading.Thread.__init__ (self)
self.startTime = time.time()
self.alive = True
self.person = person
self.onlineTime = onlineTime
self.firstMessage=True
def run(self):
while(self.alive):
if(self.firstMessage):
print self.person.getInfo() + " SPAWNED ONLINE"
self.firstMessage=False
self.person.login()
time.sleep(300)
self.person.logout()
if((time.time()-self.startTime) > self.onlineTime):
print self.person.getInfo() + " SPAWNED OFFLINE "
self.alive = False
self._Thread__stop()
#########################################
def main():
for line in open(gb.accFile,"r"):
gb.accountList.append(line.rstrip('\n'))
for account in gb.accountList:
gb.accountInfo = account.split('|',4)
browser = mechanize.Browser()
browser.set_handle_robots(False)
browser.set_handle_redirect(True)
browser.set_handle_referer(True)
browser.addheaders = [('User-agent', 'Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.1) Gecko/2008071615 Fedora/3.0.1-1.fc9 Firefox/3.0.1')]
gb.spiderList.append(objects.spider.Bot(gb.accountInfo[0],gb.accountInfo[2],gb.accountInfo[1],gb.accountInfo[3],browser))
if gb.accountInfo[2] not in gb.distros:
gb.distros.append(gb.accountInfo[2])
onlineAccounts = []
for index, acc in enumerate(gb.spiderList):
onlineAccounts.append(Class_putOnline(acc,115200)) # 600*6*8*4= 28800 = 8 uur 3600 test seconds = 1 h (1200 seconds for test time of 20 minutes... )
time.sleep(0.1)
onlineAccounts[index].start()
if __name__ == "__main__":
main()
When I open a ssh session to my server and run a python script, even when I run it in background, it dies after I close my session. How do i keep my scripts running when I'm not connected?
When I open a ssh session to my server and run a python script, even when I run it in background, it dies after I close my session. How do i keep my scripts running when I'm not connected?
Run it as a cronjob, and manually start the cronjob if you need to run script on demand.
Ok I'm pretty new to python
Me too.
EDIT:
Quick tip, use """ for long comments.
Example:
"""Description:
This does this and that and extends this and that. Use it like this and that.
"""
as I understand:
When you run a process then the input and output of the terminal is redirected to the process input and output.
If you start threads this will not change anything with this. The process has the terminal in and output as long both exist. What you can do is send this program to the background (with control-z).
If you run a program, it has its own namespace. You can import a module and change attributes of it but this will never change the module in another program.
If you want to have two programs, One is in the background running all the tine (eg with jobs as proposed by Tyson) and one from the command line, you need to communicate between those two processes.
Maybe there are other ways to circumwent the borders of processes but i do not know them.
Therefore I wrote this module where I can store values in. Everytime a value is stored directly, the state of the module is saved to disk.
'''
This is a module with persistent attributes
the attributes of this module are spread all over all instances of this module
To set attributes:
import runningConfiguration
runningConfiguration.x = y
to get attributes:
runningConfiguration.x
'''
import os
import types
import traceback
fn = fileName = fileName = os.path.splitext(__file__)[0] + '.conf'
class RunningConfiguration(types.ModuleType):
fileName = fn
def __init__(self, *args, **kw):
types.ModuleType.__init__(self, *args, **kw)
import sys
sys.modules[__name__] = self
self.load()
def save(self):
import pickle
pickle.dump(self.__dict__, file(self.fileName, 'wb'))
def load(self):
import pickle
try:
dict = pickle.load(file(self.fileName, 'rb'))
except EOFError:
pass
except:
import traceback
traceback.print_exc()
else:
self.__dict__.update(dict)
def __setattr__(self, name, value):
## print 'set', name, value,
l = []
v1 = self.__dict__.get(name, l)
self.__dict__[name] = value
try:
self.save()
## print 'ok'
except:
if v1 is not l:
self.__dict__[name] = v1
raise
def __getattribute__(self, name):
import types
if name in ('__dict__', '__class__','save','load','__setattr__', '__delattr__', 'fileName'):
return types.ModuleType.__getattribute__(self, name)
## print 'get', name
self.load()
l = []
ret = self.__dict__.get(name, l)
if ret is l:
if hasattr(self.__class__, name):
return getattr(self.__class__, name)
if name in globals():
return globals()[name]
raise AttributeError('%s object has no attribute %r' % (self.__class__.__name__, name))
return ret
def __delattr__(self, name):
del self.__dict__[name]
self.save()
RunningConfiguration(__name__)
I saved it to runningConfiguration.py.
You can use it like this:
# program1
import runningConfiguration
if not hasattr(runningConfiguration, 'programs'):
runningConfiguration.programs = [] ## variable programs is set
runningConfiguration.programs+= ['program1'] ## list is changed and = is used -> module is saved
This is an insecure module and not everything can be saved to it, but many things.
Also when two modules save at the same time the first written values may get lost.
Try it out: import ist from two different programs and see how it behaves.
I am trying to capture a single frame from the Apple iSight camera built into a Macbook Pro using Python (version 2.7 or 2.6) and the PyObjC (version 2.2).
As a starting point, I used this old StackOverflow question. To verify that it makes sense, I cross-referenced against Apple's MyRecorder example that it seems to be based on. Unfortunately, my script does not work.
My big questions are:
Am I initializing the camera correctly?
Am I starting the event loop correctly?
Was there any other setup I was supposed to do?
In the example script pasted below, the intended operation is that after calling startImageCapture(), I should start printing "Got a frame..." messages from the CaptureDelegate. However, the camera's light never turns on and the delegate's callback is never executed.
Also, there are no failures during startImageCapture(), all functions claim to succeed, and it successfully finds the iSight device. Analyzing the session object in pdb shows that it has valid input and output objects, the output has a delegate assigned, the device is not in use by another processes, and the session is marked as running after startRunning() is called.
Here's the code:
#!/usr/bin/env python2.7
import sys
import os
import time
import objc
import QTKit
import AppKit
from Foundation import NSObject
from Foundation import NSTimer
from PyObjCTools import AppHelper
objc.setVerbose(True)
class CaptureDelegate(NSObject):
def captureOutput_didOutputVideoFrame_withSampleBuffer_fromConnection_(self, captureOutput,
videoFrame, sampleBuffer,
connection):
# This should get called for every captured frame
print "Got a frame: %s" % videoFrame
class QuitClass(NSObject):
def quitMainLoop_(self, aTimer):
# Just stop the main loop.
print "Quitting main loop."
AppHelper.stopEventLoop()
def startImageCapture():
error = None
# Create a QT Capture session
session = QTKit.QTCaptureSession.alloc().init()
# Find iSight device and open it
dev = QTKit.QTCaptureDevice.defaultInputDeviceWithMediaType_(QTKit.QTMediaTypeVideo)
print "Device: %s" % dev
if not dev.open_(error):
print "Couldn't open capture device."
return
# Create an input instance with the device we found and add to session
input = QTKit.QTCaptureDeviceInput.alloc().initWithDevice_(dev)
if not session.addInput_error_(input, error):
print "Couldn't add input device."
return
# Create an output instance with a delegate for callbacks and add to session
output = QTKit.QTCaptureDecompressedVideoOutput.alloc().init()
delegate = CaptureDelegate.alloc().init()
output.setDelegate_(delegate)
if not session.addOutput_error_(output, error):
print "Failed to add output delegate."
return
# Start the capture
print "Initiating capture..."
session.startRunning()
def main():
# Open camera and start capturing frames
startImageCapture()
# Setup a timer to quit in 10 seconds (hack for now)
quitInst = QuitClass.alloc().init()
NSTimer.scheduledTimerWithTimeInterval_target_selector_userInfo_repeats_(10.0,
quitInst,
'quitMainLoop:',
None,
False)
# Start Cocoa's main event loop
AppHelper.runConsoleEventLoop(installInterrupt=True)
print "After event loop"
if __name__ == "__main__":
main()
Thanks for any help you can provide!
OK, I spent a day diving through the depths of PyObjC and got it working.
For future record, the reason the code in the question did not work: variable scope and garbage collection. The session variable was deleted when it fell out of scope, which happened before the event processor ran. Something must be done to retain it so it is not freed before it has time to run.
Moving everything into a class and making session a class variable made the callbacks start working. Additionally, the code below demonstrates getting the frame's pixel data into bitmap format and saving it via Cocoa calls, and also how to copy it back into Python's world-view as a buffer or string.
The script below will capture a single frame
#!/usr/bin/env python2.7
#
# camera.py -- by Trevor Bentley (02/04/2011)
#
# This work is licensed under a Creative Commons Attribution 3.0 Unported License.
#
# Run from the command line on an Apple laptop running OS X 10.6, this script will
# take a single frame capture using the built-in iSight camera and save it to disk
# using three methods.
#
import sys
import os
import time
import objc
import QTKit
from AppKit import *
from Foundation import NSObject
from Foundation import NSTimer
from PyObjCTools import AppHelper
class NSImageTest(NSObject):
def init(self):
self = super(NSImageTest, self).init()
if self is None:
return None
self.session = None
self.running = True
return self
def captureOutput_didOutputVideoFrame_withSampleBuffer_fromConnection_(self, captureOutput,
videoFrame, sampleBuffer,
connection):
self.session.stopRunning() # I just want one frame
# Get a bitmap representation of the frame using CoreImage and Cocoa calls
ciimage = CIImage.imageWithCVImageBuffer_(videoFrame)
rep = NSCIImageRep.imageRepWithCIImage_(ciimage)
bitrep = NSBitmapImageRep.alloc().initWithCIImage_(ciimage)
bitdata = bitrep.representationUsingType_properties_(NSBMPFileType, objc.NULL)
# Save image to disk using Cocoa
t0 = time.time()
bitdata.writeToFile_atomically_("grab.bmp", False)
t1 = time.time()
print "Cocoa saved in %.5f seconds" % (t1-t0)
# Save a read-only buffer of image to disk using Python
t0 = time.time()
bitbuf = bitdata.bytes()
f = open("python.bmp", "w")
f.write(bitbuf)
f.close()
t1 = time.time()
print "Python saved buffer in %.5f seconds" % (t1-t0)
# Save a string-copy of the buffer to disk using Python
t0 = time.time()
bitbufstr = str(bitbuf)
f = open("python2.bmp", "w")
f.write(bitbufstr)
f.close()
t1 = time.time()
print "Python saved string in %.5f seconds" % (t1-t0)
# Will exit on next execution of quitMainLoop_()
self.running = False
def quitMainLoop_(self, aTimer):
# Stop the main loop after one frame is captured. Call rapidly from timer.
if not self.running:
AppHelper.stopEventLoop()
def startImageCapture(self, aTimer):
error = None
print "Finding camera"
# Create a QT Capture session
self.session = QTKit.QTCaptureSession.alloc().init()
# Find iSight device and open it
dev = QTKit.QTCaptureDevice.defaultInputDeviceWithMediaType_(QTKit.QTMediaTypeVideo)
print "Device: %s" % dev
if not dev.open_(error):
print "Couldn't open capture device."
return
# Create an input instance with the device we found and add to session
input = QTKit.QTCaptureDeviceInput.alloc().initWithDevice_(dev)
if not self.session.addInput_error_(input, error):
print "Couldn't add input device."
return
# Create an output instance with a delegate for callbacks and add to session
output = QTKit.QTCaptureDecompressedVideoOutput.alloc().init()
output.setDelegate_(self)
if not self.session.addOutput_error_(output, error):
print "Failed to add output delegate."
return
# Start the capture
print "Initiating capture..."
self.session.startRunning()
def main(self):
# Callback that quits after a frame is captured
NSTimer.scheduledTimerWithTimeInterval_target_selector_userInfo_repeats_(0.1,
self,
'quitMainLoop:',
None,
True)
# Turn on the camera and start the capture
self.startImageCapture(None)
# Start Cocoa's main event loop
AppHelper.runConsoleEventLoop(installInterrupt=True)
print "Frame capture completed."
if __name__ == "__main__":
test = NSImageTest.alloc().init()
test.main()
QTKit is deprecated and PyObjC is a big dependency (and seems to be tricky to build if you want it in HomeBrew). Plus PyObjC did not have most of AVFoundation so I created a simple camera extension for Python that uses AVFoundation to record a video or snap a picture, it requires no dependencies (Cython intermediate files are committed to avoid the need to have Cython for most users).
It should be possible to build it like this:
pip install -e git+https://github.com/dashesy/pyavfcam.git
Then we can use it to take a picture:
import pyavfcam
# Open the default video source
cam = pyavfcam.AVFCam(sinks='image')
frame = cam.snap_picture('test.jpg') # frame is a memory buffer np.asarray(frame) can retrieve
Not related to this question, but if the AVFCam class is sub-classed, the overridden methods will be called with the result.