<?sphp $this->text('pagetitle') ?>
 
Home of the Squeezebox™ & Transporter® network music players.

SqueezePlay Applet Developing Guide

From SqueezeboxWiki

Jump to: navigation, search

Contents

Introduction

This document serves as a starting point for those wanting to develop Lua applets for SqueezePlay (and, by extention, the SqueezeboxController)

It is not intended as a Lua programming guide, which can be found online here.

First things first, check yourself out a copy of SqueezePlay. Once you have a working desktop version of SqueezePlay, beginning development will be much easier.

Please also not that the sourcecode provided here was written for the 7.3 firmware. 7.4 changes some variables names when creating menus or popup-windows. Please study the 'Test'-applet provided with SqueezePlay so you can adapt the needed changes.

Applet Files

The two main files in any SqueezePlay applet are the Meta and Applet files. The naming convention is AppletNameMeta.lua and AppletNameApplet.lua, respectively. You can also have any number of helper files, notably a strings.txt file to translate string tokens into other languages, and a settings.lua file that contains saved configuration information that your applet might need to store.

Meta file

The Meta file registers your applet to SqueezePlay for use. It is required for your applet to load correctly. Typically, a registerApplet function lets SqueezePlay add a menu item(s) for your applet, and an applet function to run when drilling into that menu item.

Example: DoomsdayMeta.lua

 local oo            = require("loop.simple")
 local AppletMeta    = require("jive.AppletMeta")
 local appletManager = appletManager
 local jiveMain      = jiveMain
 
 module(...)
 oo.class(_M, AppletMeta)
  
 function jiveVersion(meta)
       return 1, 1
 end
 
 function defaultSettings(meta)
       return {
               currentSetting = 0,
       }
 end
 
 function registerApplet(meta)
       jiveMain:addItem(meta:menuItem('doomsdayApplet', 'home', "DOOMSDAY", function(applet, ...) applet:menu(...) end, 20))
 end

Dissecting DoomsdayMeta.lua

Now let's break the file down into pieces

Variable Declaration
 local oo            = require("loop.simple")
 local AppletMeta    = require("jive.AppletMeta")
 local appletManager = appletManager
 local jiveMain      = jiveMain

In SqueezePlay it is necessary to explicitly define all functions and classes you wish to use in your applet. This is done through require() statements, and in some cases, the pulling in of global variables into local space. Note: this also needs to be done for Lua internal functions like tostring, setmetatable, etc.

So, in this example, we are defining four things: oo, AppletMeta, appletManager, and jiveMain.

Module and Class Declaration

Next we define the type of file we are constructing:

 module(...)
 oo.class(_M, AppletMeta)

In essence what these two lines say are "this is a meta file, and we're going to define all variables to be used locally" or "new class (AppleNameMeta) inheriting from AppetMeta". Note we had to define AppletMeta above by requiring our superclass jive.AppletMeta.

Meta File Functions

Now on to the meta functions:

 function jiveVersion(meta)
       return 1, 1
 end

sets the valid versions (min and max) that this applet will run under. In this example, min and max are set to 1, which means that the jiveVersion has to be set to 1 for this applet to load. Note: this number does not correspond to the external SqueezePlay version number, e.g., 7.0.1

Default settings are important to define if a settings.lua file is going to be used:

 function defaultSettings(meta)
       return {
               currentSetting = 0,
       }
 end

settings.lua files are created dynamically by an applet, so it doesn't exist when first using the applet. This is where you define default settings so SqueezePlay doesn't balk when trying to getSettings()

Finally we register our applet:

 function registerApplet(meta)
       jiveMain:addItem(meta:menuItem('doomsdayApplet', 'home', "DOOMSDAY", function(applet, ...) applet:menu(...) end, 20))
 end

This line is a bit complicated with arguments. What we are doing here is adding a menu item to the "homeMenu" area of Squeezeplay, which are those top-level menus that are managed by jive/ui/HomeMenu.lua. The menuItem() function general form is

 menuItem(id, node, token, callback, [weight])
  • id => unique key that HomeMenu uses to reference this item
  • node => HomeMenu node to add this item to. 'home' places the item in the top-most menu. Other options include 'myMusic', 'extras', 'settings', and 'advancedSettings'.
  • token => string token to be used for displaying the menu item
  • callback => function to callback when this menu item is drilled into. Typically in a meta file you reference the corresponding applet file function to execute.
  • weight => an optional parameter given to place the item higher or lower in the list. Default weight of unweighted items is 5, and by convention items sent from SqueezeCenter (e.g., Music Library) have weights between 10 and 100.
The Fruits of Our Labor

The Meta file has now provided a HomeMenu item for the applet:

Doomsday1.png

Applet file

The Applet file is the main part of your applet. It typically does the majority of the "heavy lifting" for whatever it is you want to do.

Example: DoomsdayApplet.lua

 --[[
 =head1 NAME
 
 applets.Doomsday.DoomsdayApplet - Doomsday Applet
 
 =head1 DESCRIPTION
 
 This applet was created solely for the purpose of a demonstration
 
 =head1 FUNCTIONS
 
 Applet related methods are described in L<jive.Applet>. 
 
 =cut
 --]]
 
 
 -- stuff we use
 local tostring = tostring
 local oo                     = require("loop.simple")
 local string                 = require("string")
 
 local Applet                 = require("jive.Applet")
 local RadioButton            = require("jive.ui.RadioButton")
 local RadioGroup             = require("jive.ui.RadioGroup")
 local Window                 = require("jive.ui.Window")
 local Popup                  = require("jive.ui.Popup")
 local Textarea               = require('jive.ui.Textarea')
 local SimpleMenu             = require("jive.ui.SimpleMenu")


 module(...)
 oo.class(_M, Applet)
 
 function menu(self, menuItem)
 
       log:info("menu")
       local group = RadioGroup()
       local currentSetting = self:getSettings().currentSetting
 
       -- create a SimpleMenu object with selections to be created
       local menu = SimpleMenu("menu", {
               -- first menu item
               {
                       -- text for the menu item
                       text = self:string("DOOMSDAY_OPTION1"),
                       -- add a radiobutton with a callback function to be used when selected
                       icon = RadioButton(
                               -- skin style of radio button (defined in DefaultSkin)
                               "radio",
                               -- group to attach button
                               group,
                               -- callback function
                               function()
                                       log:info("radio button 1 selected")
                                       -- show the warning
                                       self:warnMasses('DOOMSDAY_MESSAGE1')
                                       -- store the setting to settings.lua
                                       self:getSettings()['currentSetting'] = 1
                                       self:storeSettings()
                               end,
                               -- fill the radio button if this is the currentSetting
                               (currentSetting == 1)
                       ),
               },
               {
                       text = self:string("DOOMSDAY_OPTION2"),
                       icon = RadioButton(
                               "radio",
                               group,
                               function()
                                       log:info("radio button 2 selected")
                                       self:warnMasses('DOOMSDAY_MESSAGE2')
                                       self:getSettings()['currentSetting'] = 2
                                       self:storeSettings()
                               end,
                               (currentSetting == 2)
                       ),
               },
               {
                       text = self:string("DOOMSDAY_OPTION3"),
                       icon = RadioButton(
                               "radio",
                               group,
                               function()
                                       log:info("radio button 3 selected")
                                       self:warnMasses('DOOMSDAY_MESSAGE3')
                                       self:getSettings()['currentSetting'] = 3 
                                       self:storeSettings()
                               end,
                               (currentSetting == 3)
                       ),
               },
               {
                       text = self:string("DOOMSDAY_OPTION4"),
                       icon = RadioButton(
                               "radio",
                               group,
                               function()
                                       log:info("radio button 4 selected")
                                       self:warnMasses('DOOMSDAY_MESSAGE4')
                                       self:getSettings()['currentSetting'] = 4
                                       self:storeSettings()
                               end,
                               (currentSetting == 4)
                       ),
               },
       })
 
       -- create a window object
       local window = Window("window", self:string("DOOMSDAY")) 
 
       -- add the SimpleMenu to the window
       window:addWidget(menu)
 
       -- show the window
       window:show()
 end
 
 function warnMasses(self, warning)
       log:info(self:string(warning))
 
       -- create a Popup object, using already established 'toast_popup_text' skin style
       local doomsday = Popup('toast_popup_text')
 
       -- add message to popup
       local doomsdayMessage = Group("group", {
                       text = Textarea('toast_popup_textarea',self:string(warning)),
             })
       doomsday:addWidget(doomsdayMessage)
 
       -- display the message for 3 seconds
       doomsday:showBriefly(3000, nil, Window.transitionPushPopupUp, Window.transitionPushPopupDown)
 end

Dissecting DoomsdayApplet.lua

Embedded documentation
 --[[
 =head1 NAME
 
 applets.Doomsday.DoomsdayApplet - Doomsday Applet
 
 =head1 DESCRIPTION
 
 This applet was created solely for the purpose of a demonstration
 
 =head1 FUNCTIONS
 
 Applet related methods are described in L<jive.Applet>. 
 
 =cut
 --]]

The section at the top of the file here that's within a Lua comment section --[[ comments... ]]--- is in standard POD format for documentation, which Wikipedia has a nice write-up on.

Variable Declaration

As we did in the meta file, it is necessary to declare everything external that's going to be used in the file.

 -- stuff we use
 local tostring = tostring
 local oo                     = require("loop.simple")
 local string                 = require("string")
 
 local Applet                 = require("jive.Applet")
 local RadioButton            = require("jive.ui.RadioButton")
 local RadioGroup             = require("jive.ui.RadioGroup")
 local Window                 = require("jive.ui.Window")
 local Popup                  = require("jive.ui.Popup")
 local Textarea               = require('jive.ui.Textarea')
 local SimpleMenu             = require("jive.ui.SimpleMenu")
 local Group                  = require("jive.ui.Group")

In the case of the applet, we are going to be accessing a bunch of ui widgets (RadioButton, RadioGroup, Window, Popup, Textarea, SimpleMenu, RadioGroup).

Module and Class Declaration
 module(...)
 oo.class(_M, Applet)

This is no different from the Meta file, other than declaring that this is an Applet class, not Meta. By using these required lines, you are stating that all variables, including Lua internal functions, need to be declared explicitly above these lines (e.g., see previous section).

Applet Functions

There are two functions here, menu() and warnMasses(). menu() as you may recall is the function defined in the Meta file as the one to be called when the "Doomsday" menu item is selected. This function creates a menu via the jive.ui.SimpleMenu widget, and creates four items that can be selected for a special Doomsday popup warning, which is driven by the radioButton callback to warnMasses().

(Screenshots below)

Additionally, when a radio button is selected that setting is stored in settings.lua and subsequent returns to this menu will fill in the last selected item in the menu.


Note the first row in the menu function

 log:info("menu")

This uses a "log" variable that hasn't been declared in the declaration section. The reason this works is because SqueezePlay automatically declares a "log" variable for all registered applets. It's declared with the identity "applet.Doomsday" where "Doomsday" is the name of the applet. You can enable different log levels through the SqueezePlay menu Settings/Advanced/Logging.

The functions are commented for clarity within the code:

 function menu(self, menuItem)
 
       log:info("menu")
       local group = RadioGroup()
       local currentSetting = self:getSettings().currentSetting
 
       -- create a SimpleMenu object with selections to be created
       local menu = SimpleMenu("menu", {
               -- first menu item
               {
                       -- text for the menu item
                       text = self:string("DOOMSDAY_OPTION1"),
                       -- add a radiobutton with a callback function to be used when selected
                       icon = RadioButton(
                               -- skin style of radio button (defined in DefaultSkin)
                               "radio",
                               -- group to attach button
                               group,
                               -- callback function
                               function()
                                       log:info("radio button 1 selected")
                                       -- show the warning
                                       self:warnMasses('DOOMSDAY_MESSAGE1')
                                       -- store the setting to settings.lua
                                       self:getSettings()['currentSetting'] = 1
                                       self:storeSettings()
                               end,
                               -- fill the radio button if this is the currentSetting
                               (currentSetting == 1)
                       ),
               },
               {
                       text = self:string("DOOMSDAY_OPTION2"),
                       icon = RadioButton(
                               "radio",
                               group,
                               function()
                                       log:info("radio button 2 selected")
                                       self:warnMasses('DOOMSDAY_MESSAGE2')
                                       self:getSettings()['currentSetting'] = 2
                                       self:storeSettings()
                               end,
                               (currentSetting == 2)
                       ),
               },
               {
                       text = self:string("DOOMSDAY_OPTION3"),
                       icon = RadioButton(
                               "radio",
                               group,
                               function()
                                       log:info("radio button 3 selected")
                                       self:warnMasses('DOOMSDAY_MESSAGE3')
                                       self:getSettings()['currentSetting'] = 3 
                                       self:storeSettings()
                               end,
                               (currentSetting == 3)
                       ),
               },
               {
                       text = self:string("DOOMSDAY_OPTION4"),
                       icon = RadioButton(
                               "radio",
                               group,
                               function()
                                       log:info("radio button 4 selected")
                                       self:warnMasses('DOOMSDAY_MESSAGE4')
                                       self:getSettings()['currentSetting'] = 4
                                       self:storeSettings()
                               end,
                               (currentSetting == 4)
                       ),
               },
       })
 
       -- create a window object
       local window = Window("window", self:string("DOOMSDAY")) 
 
       -- add the SimpleMenu to the window
       window:addWidget(menu)
 
       -- show the window
       window:show()
 end
 
 function warnMasses(self, warning)
       log:info(self:string(warning))
 
       -- create a Popup object, using already established 'toast_popup_text' skin style
       local doomsday = Popup('toast_popup_text')
 
       -- add message to popup
       local doomsdayMessage = Group("group", {
                       text = Textarea('toast_popup_textarea',self:string(warning)),
             })
       doomsday:addWidget(doomsdayMessage)
 
       -- display the message for 3 seconds
       doomsday:showBriefly(3000, nil, Window.transitionPushPopupUp, Window.transitionPushPopupDown)
 end
The Fruits of Our Labor

A Simple Menu with 4 Radio Button selections:

Doomsday2.png

After a button is selected, a popup "toast" slides up and appears for 3 seconds before sliding back down:

Doomsday3.png

settings.lua

Applets can write to a settings.lua through the getSettings() method, and allows your applet to load in stored setting information from a previous run of the applet. The contents of a settings.lua file are a Lua table that can be read in by the Applet file through the same getSettings() method.

an example settings.lua file written after selecting the 4th radio button in the example applet:

 settings = {};
 settings["currentSetting"] = 4;

strings.txt

Applets may have strings that need translation to other languages. The strings.txt file stores these tokens for use in the Meta and Applet files. The standard format for strings.txt files is listed below (note: whitespace is delimited with tabs not spaces). After defining a strings.txt file, you can add language support simply by adding the associated translations to these strings (DE given as an example in first string)

Note, if you copy the contents below into a text file you have to replace the spaces before/after language code with a tab character

 #
 # The two letter codes are defined by ISO 639-1
 # http://en.wikipedia.org/wiki/List_of_ISO_639_codes
 
 DOOMSDAY
       EN      Doomsday Machine
       DE      Doomstag Arbeitsmashine
 
 DOOMSDAY_OPTION1
       EN      Dire Warning 1
 
 DOOMSDAY_OPTION2
       EN      Dire Warning 2
 
 DOOMSDAY_OPTION3
       EN      Dire Warning 3
 
 DOOMSDAY_OPTION4
       EN      Dire Warning 4
 
 DOOMSDAY_MESSAGE0
       EN      Fear is in the air!
 
 DOOMSDAY_MESSAGE1
       EN      The End is Nigh!
 
 DOOMSDAY_MESSAGE2
       EN      Run for the hills!
 
 DOOMSDAY_MESSAGE3
       EN      Get your affairs in order!
 
 DOOMSDAY_MESSAGE4
       EN      DUCK!

Additional Resources

What I've laid out above is an applet that does next-to-nothing. Chances are you want your applet to do a little more than that. Here's some pointers to additonal resources to make your SqueezePlay development easier.

Embedded documentation

Much of SqueezePlay files, including the various ui widgets, are documented in POD format. From a command-line you can use `perldoc` to display the documentation for a particular file. For example, the DoomsdayApplet example above has POD documentation embedded in it.

 squeezeplay/src/squeezeplay/share/applets/Doomsday: perldoc DoomsdayApplet.lua 
 DOOMSDAYAPPLET.LUA(1) User Contributed Perl DocumentationDOOMSDAYAPPLET.LUA(1)
 
 NAME
      applets.Doomsday.DoomsdayApplet - Doomsday Applet
 
 DESCRIPTION
      This applet was created solely for the purpose of a demonstration
 
 FUNCTIONS
      Applet related methods are described in jive.Applet.
 
 
 perl v5.8.8                       2008-04-25             DOOMSDAYAPPLET.LUA(1)

The POD documentation for the ui widgets are particularly helpful in understanding what methods are available. Browse to src/squeezeplay/src/share/jive and explore this area with perldoc.

See this url http://search.cpan.org/dist/Pod-Perldoc/lib/perldoc.pod for a description of PerlDoc

PerlDoc should normally be included in a default instance of perl. If you have downloaded ActivePerl from ActiveState http://www.activestate.com/activeperl/ then it is included in the BIN folder of the perl directory typically c:\perl\bin on Windows.

Milking the TestApplet for Fun and Profit

Thankfully, there is a nice applet in the squeezeplay_test directory that can help you learn-by-example. It covers things like fullscreen popup windows, text input entry (text, IP address, time), checkboxes, radio buttons, textareas, etc.

From your subversion checkout, the TestApplet can be found at src/squeezeplay_test/share/applets/TestApplet

Getting your applet on a SqueezeboxController

In order to make your applet available via the applet installer, put all your files (*Applet.lua, *Meta.lua, strings.txt, ...) in a simple zip-file which is named like your applet. Please be aware, that the zip MUST NOT contain any subfolders ! Then create a repository-file like described here: SqueezeCenter_Repositories_Developers.

Both file must be uploaded to a web-server, so that squeezebox-server can easily download the files.

This page should guide you through the process of getting your applet on to your controller via Squeezbox-Server

Development best practices

  • As long as possible you should try to develop and test using a desktop build of SqueezePlay. This gives you the most rapid development success. Most but not all applets are not dependent on the target hardware.

Ben's Tips and tricks on the desktop

I do a lot of development in OS X in a terminal, and it's very useful to make heavy use of the bash shell's alias function and ability to customize your terminal environment. With a couple path changes in the first few lines, these can be of general use on either an OS X or Linux development environment.


export MYHOME='/Users/bklaas'
export SQUEEZEPLAY='$MYHOME/svk/squeezeplay'
export SPAPPLETS='squeezeplay/src/squeezeplay/share'
export SC74='$MYHOME/svk/slim/7.4/trunk/server'
export SC75='$MYHOME/svk/slim/7.5/trunk/server'

# alias to quickly jump to the OS X directory where squeezeplay settings files are kept
alias settings='cd $MYHOME/Library/Preferences/SqueezePlay/userpath/settings'

# 7.4 trunk branch
export    SP74PATH="$SQUEEZEPLAY/7.4/trunk/squeezeplay"
alias   make74jive="cd $SP74PATH/src && make -f Makefile.osx && go74jive"
alias       74jive="cd $SQUEEZEPLAY/7.4/trunk/$SPAPPLETS"
alias         74up="cd $SP74PATH && svk update -s && 74jive"
alias     go74jive="cd $SP74PATH/build/osx/bin && source ~/7.4rc"

# 7.5 trunk branch
export    SP75PATH="$SQUEEZEPLAY/7.5/trunk/squeezeplay"
alias   make75jive="cd $SP75PATH/src && make -f Makefile.osx && go75jive"
alias       75jive="cd $SQUEEZEPLAY/7.5/trunk/$SPAPPLETS"
alias       75test="cd $SQUEEZEPLAY/7.5/trunk/squeezeplay/src/squeezeplay_test/share/applets"
alias         75up="cd $SP75PATH && svk update -s && 75jive"
alias     go75jive="cd $SP75PATH/build/osx/bin && source ~/7.5rc"
alias       75fab4="cd $SQUEEZEPLAY/7.5/trunk/squeezeplay/src/squeezeplay_fab4/share/applets"
alias  75squeezeos="cd $SQUEEZEPLAY/7.5/trunk/squeezeplay/src/squeezeplay_squeezeos/share/applets"
alias       75baby="cd $SQUEEZEPLAY/7.5/trunk/squeezeplay/src/squeezeplay_baby/share/applets"

# 7.4 SC
alias         74scup="cd $SC74 && svk update -s"
alias         74sc="cd $SC74/Slim"

# 7.5 SC
alias         scup="cd $SC75 && svk update -s"
alias         75sc="cd $SC75/Slim"


You'll note in the above file there are a few alias directives to source rc files, which I manage separately. The purpose of these rc files is to set the LUA_PATH environmental variable so changes that I make in the subversion source area are reflected when running the build. Without these, only changes in the applet files of the build itself will be seen-- Or to put it another way, if you make a change in the source area without redefining LUA_PATH you need to rebuild before you see the change.

Here's an example of an rc file for my 7.5 checkout:

export LUA_PATH='/Users/bklaas/svk/squeezeplay/7.5/trunk/squeezeplay/src/squeezeplay/share/?.lua'
export LUA_PATH=$LUA_PATH\;/Users/bklaas/svk/squeezeplay/7.5/trunk/squeezeplay/src/squeezeplay_desktop/share/?.lua
export LUA_PATH=$LUA_PATH\;/Users/bklaas/svk/squeezeplay/7.5/trunk/squeezeplay/src/squeezeplay_contrib/share/?.lua
export LUA_PATH=$LUA_PATH\;/Users/bklaas/svk/squeezeplay/7.5/trunk/squeezeplay/src/squeezeplay_test/share/?.lua


Testing custom changes on the target hardware

  • For testing on the target device (e.g., Squeezebox Radio) use secure copy (scp) to transfer the appropiate files to the device. You can do this with scp on a console like
scp APPLETNAMEApplet.lua root@192.168.?.?:/usr/share/jive/applets/APPLETNAME/

the path for applets on the target device is /usr/share/jive/applets/

On windows a program like WinSCP or Filezilla can copy the files, or alternatively use a windows shell environment that will give you a command-line, Cygwin.

Remember to restart SqueezePlay after the changes. Rebooting is one way to accomplish this, but even faster the following line, issued directly from the SSH-console of your device:

/etc/init.d/squeezeplay stopwdog && /etc/init.d/squeezeplay restart

If you do this often, its recommended to upload this as a little script to your device:

#!/bin/sh
/etc/init.d/squeezeplay stopwdog && /etc/init.d/squeezeplay restart
  • To watch the messages produced by your applet just follow the messages file
tail -f /var/log/messages