-
Notifications
You must be signed in to change notification settings - Fork 1
Sample application
Lets go through a sample application from stem to stern, everthing from the appliaction ROS package, to the launch file, down to the code and how it connects with Wallframe. Essentially, this should give you everything you need to be able to create an application and run it on a display wall running Wallframe.
For this example, we will use the example python Qt example app provided in the wallframe_apps package. All this app should do is create a Qt widget with a dark gray background, and stretch it across your wall.
Here is the directory structure for the wallframe python app:
wallframe_app_python_example kel$ tree
.
├── CMakeLists.txt
├── Makefile
├── launch
│ └── modulair_app_python_example.launch
├── mainpage.dox
├── manifest.xml
├── menu_icon.png
└── scripts
├── python_example_app.py
└── python_example_app_opengl.pyWe don't care about the Makefile and CMake files (which are auto generated, and we are using python), but we do care about the manifest.txt file:
<package>
<description brief="wallframe_app_python_example">
A sample python example application for the Wallframe platform.
</description>
<author>Kelleher Guerin</author>
<license>BSD</license>
<review status="unreviewed" notes=""/>
<url>http://ros.org/wiki/wallframe_app_python_example</url>
<depend package="rospy"/>
<depend package="roscpp"/>
<depend package="wallframe_core"/>
<depend package="wallframe_msgs"/>
<depend package="wallframe_osg_tools"/>
</package>What you should take away from this file is the depend calls for wallframe_core and wallframe_msg. These are both completely necessary should you want to use Wallframe messages, or inherit Wallframe base classes (in this case the python base class wallframe_app_base.py.
The launch file for this sample app is here:
<launch>
<node pkg="wallframe_app_python_example" name="python_example_app" type="python_example_app.py"/>
<!-- <node pkg="wallframe_app_python_example" name="python_example_app_opengl" type="python_example_app_opengl.py"/> -->
<param name="/wallframe/apps/python_example/paths/assets" value="$(find wallframe_app_python_example)/assets"/>
</launch>There are two executable python files that can be launched by this launch file. We will be ignoring the OpenGL example, and focusing on the vanilla example. As you probably remember from the ROS tutorials,
<node pkg="wallframe_app_python_example" name="python_example_app" type="python_example_app.py"/>will launch the python_example_app.py file when this launch file is launched.
This line:
<param name="/wallframe/apps/python_example/paths/assets" value="$(find wallframe_app_python_example)/assets"/>is important because it sets a parameter on the ROS parameter server for where the python_example_app.py can find assets like images, videos, icons, etc. This is purely for convenience, but makes it very flexible since you don't have any hard coded paths inside the source .py file.
Here is the source file in its entirety. We will go through it step by step:
#!/usr/bin/env python
import roslib; roslib.load_manifest('wallframe_app_python_example')
import rospy, sys
# Wallframe Core Functionality
import wallframe_core
from wallframe_core import WallframeAppWidget
# PySide Imports
import PySide
from PySide.QtGui import QApplication
class PythonAppWidget(WallframeAppWidget):
def __init__(self,name,app):
super(PythonAppWidget,self).__init__(name,app)
# Put stuff in here and make sure to make any widgets children of 'self'
pass
class PythonExampleApp():
def __init__(self):
rospy.init_node('python_example_app',anonymous=True)
app = QApplication(sys.argv)
app_widget = PythonAppWidget("PythonExampleApp",app)
# Running
rospy.logwarn("PythonExampleApp: Started")
app.exec_()
# Done
rospy.logwarn('PythonExampleApp: Finished')
# MAIN
if __name__ == '__main__':
app = PythonExampleApp()Much of the code for this app is wrapped up in the base class, WallframeAppWidget. There are a few important things to notice. First
app = QApplication(sys.argv)
app_widget = PythonAppWidget("PythonExampleApp",app)the application is created outside the app widget, and then passed in as a parameter. This lets the app widget catch the ctrl-c, or any other terminate command and actually close the Qt application context.
The init method is where you would put any other widgets or objects into this application. Any Qt widgets that are made with the parent self will be shown in the context of the main widget.
Lets go into the python base class for a bit to see what is going on under the hood.
Here is the base class code:
#!/usr/bin/env python
# Author: Kelleher Guerin, futureneer@gmail.com, Johns Hopkins University
import roslib; roslib.load_manifest('wallframe_core')
import rospy
### PySide ###
import PySide
from PySide.QtGui import QWidget, QApplication
from PySide.QtCore import QTimer
from PySide import QtCore
# ROS Msgs
from geometry_msgs.msg import Transform
from geometry_msgs.msg import Vector3
from std_msgs.msg import Bool
from std_msgs.msg import String
# Wallframe Msgs
from wallframe_msgs.msg import WallframeUser
from wallframe_msgs.msg import WallframeUserArray
from wallframe_msgs.msg import WallframeUserEvent
from wallframe_msgs.msg import TrackerUser
from wallframe_msgs.msg import TrackerUserArray as tracker_msg
# Wallframe Core
import wallframe_core
from wallframe_core.srv import *
################################################################################
class WallframeAppWidget(QWidget):
def __init__(self,name, app):
super(WallframeAppWidget,self).__init__()
# Member variables
self.name_ = name
self.app_ = app
self.ok_timer_ = QTimer(self)
self.current_users_ = []
self.current_user_event_ = WallframeUserEvent()
self.users_ = {}
self.num_users_ = 0
self.height_perc_ = 1
self.focused_user_id_ = -1
rospy.logwarn(self.name_ + ": App Widget Starting")
# ROS Subscribers
self.user_state_sub_ = rospy.Subscriber("/wallframe/users/state", WallframeUserArray, self.user_state_cb)
self.user_event_sub_ = rospy.Subscriber("/wallframe/users/events", WallframeUserEvent, self.user_event_cb)
# App parameters
if rospy.has_param("/wallframe/core/params/x"):
self.x_ = rospy.get_param("/wallframe/core/params/x")
else:
rospy.logerr(self.name_ + ": parameter [x] not found on server")
if rospy.has_param("/wallframe/core/params/y"):
self.y_ = rospy.get_param("/wallframe/core/params/y")
else:
rospy.logerr(self.name_ + ": parameter [y] not found on server")
if rospy.has_param("/wallframe/core/params/width"):
self.width_ = rospy.get_param("/wallframe/core/params/width")
else:
rospy.logerr(self.name_ + ": parameter [width] not found on server")
if rospy.has_param("/wallframe/core/params/height"):
self.height_ = rospy.get_param("/wallframe/core/params/height")
else:
rospy.logerr(self.name_ + ": parameter [height] not found on server")
if rospy.has_param("/wallframe/app/params/height_percentage"):
self.height_perc_ = rospy.get_param("/wallframe/app/params/height_percentage")
else:
rospy.logerr(self.name_ + ": parameter [height_percentage] not found on server")
rospy.logwarn(self.name_ + ": height percentage set to " + str(self.height_perc_))
# Set base app widget size and hints based on parameters
self.setWindowFlags(QtCore.Qt.FramelessWindowHint)
self.resize(self.width_, int(self.height_*self.height_perc_) )
self.move(self.x_,self.y_)
self.setWindowTitle(self.name_)
self.show()
# Set up ros_ok watchdog timer to handle termination and ctrl-c
self.connect(self.ok_timer_, QtCore.SIGNAL("timeout()"), self.check_ok)
self.ok_timer_.start(15)
rospy.logwarn(self.name_ + ": App Widget Set Up Successfully")
# @QtCore.Slot(bool)
def check_ok(self):
if rospy.is_shutdown():
self.clean_up()
self.app_.exit()
def clean_up(self):
rospy.logwarn(self.name_ + ": App Widget Cleaning up")
# Clean up or delete things, such as ros parameters
pass
def user_state_cb(self,msg):
self.current_users_ = msg.users
# Signal functions here that you want to run on a user state callback
pass
def user_event_cb(self,msg):
self.current_user_event_ = msg
# Signal functions here that you want to run on an event callback
passHere are a few good things to know:
self.user_state_sub_ = rospy.Subscriber("/wallframe/users/state", WallframeUserArray, self.user_state_cb)
self.user_event_sub_ = rospy.Subscriber("/wallframe/users/events", WallframeUserEvent, self.user_event_cb)These subscribers listen to the state and events topics published by the wallframe_user_manager. The events here are handled by these callbacks:
def user_state_cb(self,msg):
self.current_users_ = msg.users
...
def user_event_cb(self,msg):
self.current_user_event_ = msg
...which simply store the messages to a variable for use in your application.
Parameters for the application window size (which should be the size of your display wall) are fetched from the ROS parameter server here:
# App parameters
if rospy.has_param("/wallframe/core/params/x"):
self.x_ = rospy.get_param("/wallframe/core/params/x")
else:
rospy.logerr(self.name_ + ": parameter [x] not found on server")
...The parameters are x_,y_,width_, height_ and height_perc_. The first four should be self explanitory, the last height_perc_ is the ammount of room the application provides the wallframe_infobar at the bottom of the screen.
Then, the code:
self.setWindowFlags(QtCore.Qt.FramelessWindowHint)
self.resize(self.width_, int(self.height_*self.height_perc_) )
self.move(self.x_,self.y_)
self.setWindowTitle(self.name_)
self.show()creates a base Qt widget with the specified size and location. This window becomes the "self" in the example python app, where you put the rest of your widgets.
This code
self.connect(self.ok_timer_, QtCore.SIGNAL("timeout()"), self.check_ok)
self.ok_timer_.start(15)and this
# @QtCore.Slot(bool)
def check_ok(self):
if rospy.is_shutdown():
self.clean_up()
self.app_.exit()makes sure that when the program is terminated (a call which ROS intercepts with is_shutdown()), the rest of the application (i.e. the Qt context) gets shut down properly.
Wallframe hinges on a single launch file, wallframe_full.launch to get everything running, and your applications talk with this core functionality via messages and parameters. Let's just look at that real quick:
<launch>
<!-- Launch the Center Tracker -->
<include file="$(find wallframe_tracker)/launch/center_tracker.launch"/>
<!-- Launch the User Manager Tracker -->
<include file="$(find wallframe_user)/launch/wallframe_user_manager.launch"/>
<!-- Run the Infobar at the bottom of the screen that shows user avatar and messages -->
<node pkg="wallframe_core" name="wallframe_infobar" type="wallframe_infobar.py" output="screen"/>
<group ns="/wallframe/core">
<!-- Parameters for the application manager -->
<arg name="app_manifest_path" default="$(find wallframe_core)/wallframe_application_manifest.yaml" />
<param name="paths/app_manifest" textfile="$(arg app_manifest_path)" />
<param name="paths/application_path" value="/home/kel/wallframe_workspace/wallframe_apps" />
<!-- Run the application manager -->
<node pkg="wallframe_core" name="wallframe_app_manager" type="wallframe_app_manager.py" output="screen"/>
</group>
<!-- Parameters for the application menu -->
<param name="/wallframe/menu/params/cursor_path" value="$(find wallframe_core)/assets/cursor_open.png" />
<param name="/wallframe/menu/params/cursor_path_alt" value="$(find wallframe_core)/assets/cursor_closed.png" />
<param name="/wallframe/menu/params/background_path" value="$(find wallframe_core)/assets/background.jpg" />
<!-- Run the application menu -->
<node pkg="wallframe_core" name="wallframe_app_menu" type="wallframe_app_menu.py" output="screen"/>
<!-- General ROS parameters specific to your wall -->
<rosparam>
wallframe:
core:
params:
x: 1680
y: 24
width: 5760
height: 3197
default_app: image_storm
app:
params:
height_percentage: .97
infobar:
params:
height_percentage: .03
menu:
params:
height_percentage: .97
workspace_size: [-1200,1200,-600,600,1500,3000]
border_scale: .02
y_offset: -600.0
screensaver: no
</rosparam>
</launch>The first section of this launch file is fairly self explanitory. Essentially, it launches the app manager and menu, the user manager, the infobar and the tracker. The second section deserves some explanation. Here in the <rosparam> tag, are the actuall parameters for your display wall. The width and height in pixels of the screen, any x or y offsets, the height percentage of the infobar, and other parameters specific to the menu (which you can leave alone if you want). The last parameter under /menu/params/screensaver sets whether the app manager will load a default application when all users have left.
FYI, there is another launch file wallframe_core.launch which only launches the app manager, the app menu and the infobar, and does NOT launch the tracker or user manager.
Next: [How to Install Wallframe and Run the Python Example App](Installing Wallframe)