Papywizard Developer Guide
Papywizard is entirely written in python. Branch 2.x now uses PyQt (Python binding for Qt) for the GUI, pybluez and/or pyserial for hardware control.
Build from source
It is best to work on the subversion branch:
$ svn co http://svn.gbiloba.org/papywizard/trunk papywizard
In order to run Papywizard from source, you need:
- Python >= 2.5: http://www.python.org/ftp/python (python-2.6.x)
- PyQt >= 4.5: http://www.riverbankcomputing.co.uk/static/Downloads/PyQt4 (PyQt-Py2.6-gpl-4.7.x)
- pybluez >=0.15: http://code.google.com/p/pybluez/downloads/list (PyBluez-0.18)
- pyserial: http://sourceforge.net/projects/pyserial/files (pyserial-2.4)
Be sure to use the same python version for all of them (except for pyserial, which is a pure python module).
All the needed libs come with most distributions.
Papywizard can be launch by the following commands:
On Windows, PyQt package don't need the Qt package. But the following additional libs are needed:
- ElementTree: http://effbot.org/downloads/#elementtree
- pywin32: http://sourceforge.net/projects/pywin32
- MSVC2008 C++ runtime DLLs: http://www.microsoft.com/downloads/details.aspx?familyid=9B2DA534-3E03-4391-8A4D-074B9F2BC1BF&displaylang=en (first try without it)
To get the sources from the subversion repository, you can use:
- svn tool: http://tortoisesvn.tigris.org
To launch Papywizard, run the windows/papywizard.bat script.
You can generate the complete API documentation by running the mkdoc.sh script; the documentation will then be available as web pages in the api/ directory.
Or you can browse the API (subversion) online. Be carefull that it may not be up-to-date.
Papywizard uses a MVC-like pattern, which keeps the model separated from the views. This allows to use any other toolkit if needed; I recently switched from Tkinter to PyGTK, to run Papywizard on Nokia plateform. I hope, one day, to be able to run Papywizard on QTopia-based devices, like Openmoko. But I first need to get such device ;o)
A plugin architecture allows to control different hardwares.
The head uses a simple ascii serial protocol; the serial line uses standard settings: 9600 8N1 (9600 bauds, 8 bits data, no parity, 1 stop bit). The head waits for incoming commands, and send response to them. The 2 axis are identical, and uses the same commands. They both wait for incoming commands; depending of the first value (see below), the command will be sent to one or the other axis.
A commands always starts with ':', and ends with '\r'. The head response always starts with '=', and also ends with '\r'. As TX/RX lines are wired together, the controller has to first readback its own command, before reading the head response. In the following documentation, the command readback is omitted, as the ':', '=' and '\r' chars.
Parameters used in the documentation:
|<axis>||axis to control||1 or 2||1 ascii digit||1=yaw, 2=pitch axis|
|<pos>||position||0 to 2²⁴-1||6 ascii digits, hex.||Low byte is sent first (A35483 is read 8354A3). When switched on, read value is 800000. Right sens increments axis 1; left sens decrements it. Up sens increments axis 2; down decrements it. A complete turn (360°) increments/decrements by 0E6600 (so, the axis can make almost 9 turns before the counter overflow)|
|<dir>||axis direction||0 or 1||1 ascii digit||0=increments (up/right), 1=decrements (down/left)|
|<status>||Axis status||3 ascii digits||Second digit is 0 when axis is stopped|
|<state>||State of the shutter contact||1 ascii digit||0 or 1||0 opens the contact, 1 closes the contact|
Merlin/Orion commands set
|Read axis position||j<axis>||<pos>|
|Read axis status||f<axis>||<status>|
|Init (at switch on)||F<axis>|
0E62D3 is the number of encoder counts per turn (in hexa)
0006F9 is the sidereal rate (in hexa)
|220000 seems to be the divider for the speed. The lower the value, the higher the speed|
|Drive to position||L<axis>|
|Set shutter contact state||O<axis><state>||Both axis understand the command, but the contact is only set by yaw (0) axis|
Merlin/Orion communication examples
When switched on:
command response :F1\r =\r :F2\r =\r :a1\r =D3620E\r :D1\r =F90600\r :a2\r =D3620E\r :D2\r =F90600\r :D2\r =F90600\r
Note: the last command is sent twice by the original remote control, but all works fine if sent only once (like Papywizard does).
Push up button in fast mode:
command response :L2\r =\r :G230\r =\r :I2220000\r =\r :J2\r =\r
Release up button if fast mode:
command response :L2\r =\r :f2\r =503\r
Clauss Rodeon protocol
Plugins implements one or more 'capacity'. Available capacities are:
The first two are responsible of yaw/pitch axis; the last is responsible of triggering the camera shutter.
A plugin needs to implement 2 classes (for each implemented capacity): one for the model (real work), and one for the controller (configuration dialog).
All model classes inherit from the AbstractPlugin class (defined in the papywizard.common.abstractPlugin module).
Then, plugins implementing 'yawAxis' and 'pitchAxis' capacities must inherit from AbstractAxisPlugin, and plugins implementing the 'shutter' capacity must inherit from AbstractShutterPlugin (defined in the papywizard.hardware.abstractShutterPlugin module). Depending of the implemented capacity, the plugin must overload some methods for Papywizard to work. Have a look at the above classes to see which ones.
The plugin model may need some user parameters in order to work. These parameters must be defined in the _defineConfig() method. For example:
def _defineConfig(self): self._addConfigKey('_myParam', 'MY_PARAM', default='a value')
will add a config param named MY_PARAM. All plugins user params are saved in the config. file. If there is no previous value defined there, the default argument tells Papywizard which value to use. This config param will be saved using MY_PARAM as key.
In the plugin, this config param can be used through self._config['MY_PARAM'] (in a future release, it will be automatically binded to self._myParam). It should not be modified from the model (see below).
Controller classes inherits from AbstractPluginController class (defined in the papywizard/controller/ package). This abstract class inherits from other classes, which are responsible to load the .ui GUI defining file (see previous chapters). The only method which needs to be oversloaded is _defineGui(). This is where the dialog interface is defined. For example:
def _defineGui(self): self._addWidget('Main', "My param", LineEditField, (), 'MY_PARAM')
will create an entry to customize our previous config param. The first argument is the notebook widget tab under which the param will be displayed (Main is the first default tab). The second argument is the label for the config param. The third argument is the field type to use. Availabe fields are defined in the papywizard.view.pluginFields module. The 4th argument is a tuple containing the init arguments of the field. The last argument is the name of the config param, as defined in the plugin model.
Here is another example:
def _defineGui(self): self._addTab('Driver') self._addWidget('Driver', "Type", ComboBoxField, (['bluetooth', 'serial', 'ethernet'],), 'DRIVER_TYPE')
The main addition is that we first create a new tab, and then add a ComboBoxField, with 3 possible entries.
Both previous classes must be defined in the same module. To register the plugin, the module also has to implement a register() function, which will be called at startup. For example:
def register(): PluginManager().register(MyModelClass, MyModelController)
If the plugin module implements several model/controller capacities, just put additional PluginManager().register() calls.
This plugin register() function must be called in the main application. This can be done by modifying the main script; but Papywizard will also automatically register all plugins which are stored in the config. dirrectory, under the plugins/ directory. Just create it if it does not already exist, and put your plugins there.
Note: need to be updated with recent API.
Here is the Tethered plugin, defined in the papywizard/plugins/tetheredPlugins module:
import time import subprocess from papywizard.common.loggingServices import Logger from papywizard.common.pluginManager import PluginManager from papywizard.hardware.abstractShutterPlugin import AbstractShutterPlugin from papywizard.controller.shutterPluginController import ShutterPluginController from papywizard.view.pluginFields import ComboBoxField, LineEditField, CheckBoxField class TetheredShutter(AbstractShutterPlugin): _name = "Tethered" def _init(self): pass def _getTimeValue(self): return -1 def _getMirrorLockup(self): return self._config['MIRROR_LOCKUP'] def _getBracketingNbPicts(self): return self._config['BRACKETING_NB_PICTS'] def _getBracketingIntent(self): return self._config['BRACKETING_INTENT'] def _defineConfig(self): AbstractShutterPlugin._defineConfig(self) self._addConfigKey('_mirrorLockup', 'MIRROR_LOCKUP', default=False) self._addConfigKey('_mirrorLockupCommand', 'MIRROR_LOCKUP_COMMAND', default="") self._addConfigKey('_shootCommand', 'SHOOT_COMMAND', default="") self._addConfigKey('_bracketingNbPicts', 'BRACKETING_NB_PICTS', default=1) self._addConfigKey('_bracketingIntent', 'BRACKETING_INTENT', default='exposure') def activate(self): Logger().trace("TetheredShutter.activate()") def shutdown(self): Logger().trace("TetheredShutter.shutdown()") def establishConnection(self): pass def shutdownConnection(self): pass def lockupMirror(self): Logger().debug("TetheredShutter.lockupMirror(): execute command '%s'..." % self._config['MIRROR_LOCKUP_COMMAND']) time.sleep(1) Logger().debug("TetheredShutter.lockupMirror(): command over") return 0 def shoot(self, bracketNumber): Logger().debug("TetheredShutter.shoot(): execute command '%s'..." % self._config['SHOOT_COMMAND']) # Launch external command args = self._config['SHOOT_COMMAND'].split() p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) # Wait end of execution stdout, stderr = p.communicate() if stderr: Logger().debug("TetheredShutter.shoot(): stderr:\n%s" % stderr) Logger().debug("TetheredShutter.shoot(): stdout:\n%s" % stdout) return p.returncode class TetheredShutterController(ShutterPluginController): def _defineGui(self): ShutterPluginController._defineGui(self) self._addWidget('Main', "Mirror lockup", CheckBoxField, (), 'MIRROR_LOCKUP') self._addWidget('Main', "Mirror lockup command", LineEditField, (), 'MIRROR_LOCKUP_COMMAND') self._addWidget('Main', "Shoot command", LineEditField, (), 'SHOOT_COMMAND') self._addWidget('Main', "Bracketing nb picts", SpinBoxField, (1, 99), 'BRACKETING_NB_PICTS') self._addWidget('Main', "Bracketing intent", ComboBoxField, (['exposure', 'focus', 'white balance', 'movement'],), 'BRACKETING_INTENT') def register(): """ Register plugins. """ PluginManager().register(TetheredShutter, TetheredShutterController)
Output XML file
Papywizard can generate a xml data file. This file mainly contains all positions reached during shooting, and can be used by Autopano Giga to help positionning orphan pictures. This is very helpfull for high-resolution panos with deep blue sky, without details. Sift algorithm often fail to find control points in such areas.
The XML file is made of two parts: a header, containing global informations, and a shoot part, containing a list of all pictures positions. The exact header format depends of the mode.
This format is open, and can be used by anyone who wants to implement motorized panohead output data file. Feel free to contact me if you want to add some entries to support special features. The goal is to make some sort of standard format. More informations here.
Before creating and distributing a package, make sure that the following files are up-to-date:
- papywizard/common/config.py (version number)
- setup.py (new/obsolete files)
- papywizard.pro (new/obsolete files)
Papywizard includes a distutils extension script to build debian package in a easy way:
$ python debian/setup.py bdist_debian
Then, just move the packages to the web site.
As maemo is based on debian packages, as similar script can be used:
$ python maemo/setup.py bdist_debian
- install NSIS software
- build the executable by launching the script windows/buildexe.bat
- edit (update the version number) and run the script windows/papywizard.nsi
There are several ways to contribute:
Test and report bugs
If you want to contribute but don't know how to code, you can make translation.
To do that, you just need to ask me for the corresponding papywizard_xx.ts file which contains all sentences and words used in the soft, translate them using Qt Linguist (important; don't translate using a simple text editor), and send it back to me.
- keep strings length as close as the original ones
- if you are not sure about the usage of a word/sentence, feel free to ask about the context.