Difference between revisions of "DCE Whisperer"

From LinuxMCE
Jump to: navigation, search
m (Prerequisits for being able to run it: typo fix)
(Replaced page with lengthy DCE Whisperer manual)
Line 1: Line 1:
= How to write a DCE Whisperer Bash Device Template =
+
[[Category: Programmer's Guide]]
 +
= Description =
  
== Prerequisites for being able to run it ==
+
DCE Whisperer is a framework intended to be middle ground between C++ DCE devices and GSD devices: you can quickly create a DCE device that can be as powerful as a C++ device (especially if you have deep understanding of Bash and its idiosyncrasies), but as versatile as a GSD (but without the database/web browser limitations).
Install the dce-whisperer and shell-io packages
+
  
== Create the Device Template ==
+
It consists of a Bash framework - the DCE-connect.sh script, and a C++ compiled DCE protocol bridge - DCE-Whisperer.
* Create a regular GSD device template in Web admin.
+
* The Command line contains a call to Generic_Serial_Devices for a regular GSD device. For the whisperer based template, you put the filename of the bash script.
+
* The device template bash script looks very similar to a ruby device template. The easiest way to start, get the [http://svn.linuxmce.org/trac.cgi/browser/branches/LinuxMCE-0810/src/DenonAmp/DenonAmp?format=raw DenonAmp] script.
+
  
= How to use a DCE Whisperer Bash Device Template =
+
A PHP version is envisioned, but I haven't done any work on it yet. -[[User:Uplink|Uplink]] 19:29, 6 September 2011 (CEST)
A DCE device based on the whisperer must be added the same way any other device is added to the system. If the device template has a detection script, it should get added automatically.
+
  
= Notes on DCE Whisperer =
+
= Who uses it =
<Uplink_> in SVN, under [http://svn.linuxmce.org/trac.cgi/browser/branches/LinuxMCE-0810/src/DCE-Whisperer /branches/LinuxMCE-0810/src/DCE-Whisperer]
+
<Uplink_> it was committed at revision 22089
+
<Uplink_> if you have a SVN checkout, look in src/DCE-Whisperer
+
<Uplink_> there, you have a self-contained deb package that you can build
+
<Uplink_> dpkg-buildpackage -rfakeroot -b -us -uc -tc
+
<Uplink_> and you get a deb
+
<K0K05> Stop right there. Do not let me waste more of your time. I'm putting together a new VirtualBox development installation
+
<Uplink_> if you install that deb, then you can read the script Test231.sh to get an idea of how to write a DCE device with it
+
<K0K05> That was crystal clear
+
<Uplink_> put this in a text file or something :)
+
<Uplink_> when you get the hang of it, write a Wiki page too
+
<K0K05> I promise to do that
+
<K0K05> And is it obvious how to write a device for TCP/IP ?
+
<Uplink_> it should be
+
<Uplink_> there a function at the top of the file, called Configure, and it has all the communication parameters that you need to set for any application
+
<Uplink_> basically, you say DeviceConnection_Type=inet
+
<Uplink_> and then find the relevant variables to set the IP address and port, below, in the same function
+
<Uplink_> everything that the device sends will be passed to the ProcessDeviceStream function (which is below Configure)
+
<Uplink_> also in configure, you can set some pre-processing parameters if you like
+
<Uplink_> for example, if the protocol is all text, and all the messages end up with the same character (\r for example), you can tell the framework that
+
<Uplink_> and it will split it at the terminator into lines before calling ProcessDeviceStream
+
  
The package shell-io in src/shell-io needs to be build as well.
+
Right now LinuxMCE only has some example scripts and it is not using the Whisperer framework actively.
  
The packages have been build are available from possy:
+
In Dianemo, the following device scripts are based on Whisperer:
 +
 
 +
* Sound amplifiers
 +
** DenonAmp
 +
** ArcamAmp
 +
** TEAC_AG980
 +
** Pioneer_SCLX82
 +
 
 +
* Blu-ray players
 +
** Yamaha_BD-S1900
 +
** Denon_2500BT_3800BD
 +
 
 +
* TVs
 +
** Panasonic_TH50PHD.sh
 +
** Seura_Hydra.sh
 +
** LG_LCD_RS232
 +
** LG_32LD690_RS232
 +
 
 +
* AV matrixes and switches
 +
** Cisco_Managed_Switch
 +
** Kramer_AV_Switch
 +
** Grand_Being_HDMI_Switch
 +
** SierraVideo_VS
 +
** CLUX-42S
 +
** CLUX61
 +
 
 +
* Set-top boxes
 +
** Sky_Digibox.sh
 +
 
 +
* Software DCE devices
 +
** External_Source_Player
 +
** Video_Screen_Saver
 +
** Generic_IR_AV_Device
 +
 
 +
* Others
 +
** Lutron_Homeworks
 +
 
 +
= Dependencies =
 +
 
 +
* dce-whisperer
 +
The dce-whisperer package contains DCE-connect.sh and DCE-Whisperer.
 +
 
 +
* shell-io
 +
The shell-io package contains the fdread, fdwrite and fdselect utilities, which are used by DCE-connect.sh to access the read(2), write(2), and select(2) libc library functions from Bash, with some help to work around Bash idiosyncrasies (like for example no support for binary-safe strings).
 +
 
 +
* php5-cli
 +
This is used for things like urlencode/urldecode, for which Bash and its traditional support tools (e.g. sed, awk) doesn't have equivalent functions.
 +
 
 +
While Dianemo already includes these, the dce-whisperer and shell-io packages aren't currently part of the LinuxMCE build system. Pre-built packages are available from possy:
 
* [http://www.possy.de/dce-whisperer_3.0.6_i386.deb DCE Whisperer] package
 
* [http://www.possy.de/dce-whisperer_3.0.6_i386.deb DCE Whisperer] package
 
* [http://www.possy.de/shell-io_1.0.1_i386.deb shell-io] package
 
* [http://www.possy.de/shell-io_1.0.1_i386.deb shell-io] package
 +
 +
= Example script analysis =
 +
 +
== What it looks like ==
 +
 +
The following is the source code of the DCE device that controls Denon amplifiers in Dianemo.
 +
 +
Syntax highlighted version on Ubuntu Pastebin: http://paste.ubuntu.com/683553/
 +
 +
<nowiki>#!/bin/bash
 +
 +
. /usr/pluto/bin/Config_Ops.sh
 +
. /usr/pluto/bin/Utils.sh
 +
 +
DEVICEDATA_COM_Port_on_PC=37
 +
DEVICEDATA_Port=171
 +
DEVICETEMPLATE_Denon_AVR_Zone=1948
 +
 +
## Mandatory functions
 +
Configure()
 +
{
 +
local Q R
 +
local SerialPort
 +
 +
Q="SELECT IK_DeviceData FROM Device_DeviceData WHERE FK_Device=$DevNo AND FK_DeviceData=$DEVICEDATA_COM_Port_on_PC"
 +
R=$(RunSQL "$Q")
 +
SerialPort=$(Field 1 "$R")
 +
SerialPort=$(TranslateSerialPort "$SerialPort")
 +
 +
## Device connection generics
 +
DeviceConnection_Type=serial # choices: none, serial, inet, custom
 +
## Connection parameters
 +
# type=serial:
 +
DeviceConnection_BaudRate=9600
 +
DeviceConnection_Parity=N81 # choices: N81, E81, O81
 +
DeviceConnection_SerialPort="$SerialPort"
 +
# type=inet:
 +
DeviceConnection_Protocol=TCP4 # choices: TCP4, TCP6, UDP4, UDP6
 +
DeviceConnection_Endpoint=127.0.0.1:2001 # Address:Port
 +
# type=custom:
 +
DeviceConnection_Command=
 +
 +
## What to do when the device is lost
 +
DeviceConnection_OnDisconnect=reconnect # choices: disable, reconnect
 +
 +
## Device protocol generics
 +
DeviceProtocol_Type=line # choices: line, stream
 +
# type=line:
 +
DeviceProtocol_Separator=$'\r' # must match encoding
 +
 +
DeviceProtocol_Encoding=none # choices: none, enc, hex
 +
DeviceProtocol_AutoAppendSeparator=yes # choices: yes, no
 +
DeviceProtocol_Delay=.2 # delay between commands (seconds)
 +
}
 +
 +
ProcessDeviceStream()
 +
{
 +
local Data="$1"
 +
Data=$(builtin echo "$Data" | tr -d '\r')
 +
echo "Denon Amp said: $Data"
 +
 +
local Zone Parm
 +
 +
case "$Data" in
 +
SI*) ZM_CurrentInput="${Data:2}" ;;
 +
MUOFF) ZM_Mute=NoMute ;;
 +
MUON) ZM_Mute=Mute ;;
 +
MV[0-9][0-9])
 +
ZM_VolumeLevel="${Data:2}"
 +
if [[ "$ZM_VolumeLevel" == 0* ]]; then
 +
ZM_VolumeLevel="${ZM_VolumeLevel:1}"
 +
fi
 +
;;
 +
Z*)
 +
Zone="${Data:1:1}"
 +
Parm="${Data:2}"
 +
case "$Parm" in
 +
MUOFF) eval "Z${Zone}_Mute=NoMute" ;; # not muted
 +
MUON) eval "Z${Zone}_Mute=Mute" ;; # muted
 +
OFF|ON) : ;; # on/off
 +
[0-9][0-9]) # volume
 +
if [[ "$Parm" == 0* ]]; then
 +
Parm="${Parm:1}"
 +
fi
 +
eval "Z${Zone}_VolumeLevel=$Parm"
 +
;;
 +
PHONO|CD|TUNER|DVD|TV|TV/CBL|VDR|DVR|V.AUX|XM|IPOD|AUX) eval "Z${Zone}_CurrentInput=$Parm" ;; # input
 +
esac
 +
;;
 +
esac
 +
}
 +
 +
## Unknown command handlers
 +
ReceivedCommandForChild()
 +
{
 +
local From="$1" To="$2" Type="$3" Cmd="$4"
 +
local Zone
 +
local VarZone
 +
 +
VarZone="Child_$To"
 +
if [[ -z "${!VarZone}" || "$Type" != 1 ]]; then
 +
ReplyToDCE "UNHANDLED" ""
 +
return
 +
fi
 +
 +
Zone="${!VarZone}"
 +
case "$Cmd" in
 +
966) RouteMediaPath "$Zone" "$Parm_219" ;;
 +
91) InputSelect "$Zone" "$Parm_71" ;;
 +
97) Mute "$Zone" ;;
 +
90) VolDown "$Zone" ;;
 +
89) VolUp "$Zone" ;;
 +
193) TurnOff "$Zone" ;;
 +
192) TurnOn "$Zone" ;;
 +
esac
 +
 +
ReplyToDCE "OK" ""
 +
}
 +
 +
ReceivedUnknownCommand()
 +
{
 +
local From="$1" To="$2" Type="$3" Cmd="$4"
 +
ReplyToDCE "UNHANDLED" ""
 +
}
 +
 +
## Hooks
 +
OnDeviceConnect()
 +
{
 +
SendToDevice "PW?"
 +
}
 +
 +
OnInit()
 +
{
 +
## User configuration code
 +
##########################
 +
 +
SendToDevice "SI?"
 +
#SendToDevice "MV40"
 +
SendToDevice "MV?"
 +
ZM_CurrentInput=
 +
ZM_Mute=Mute
 +
ZM_VolumeLevel=40
 +
 +
VolumeStep=4
 +
 +
local Q R Var
 +
local ZoneDevice
 +
Q="SELECT PK_Device FROM Device WHERE FK_Device_ControlledVia=$DevNo AND FK_DeviceTemplate=$DEVICETEMPLATE_Denon_AVR_Zone"
 +
R=$(RunSQL "$Q")
 +
 +
for ZoneDevice in $R; do
 +
Q="SELECT IK_DeviceData FROM Device_DeviceData WHERE FK_Device=$ZoneDevice AND FK_DeviceData=$DEVICEDATA_Port"
 +
R=$(RunSQL "$Q")
 +
if [[ -z "$R" ]]; then
 +
continue
 +
fi
 +
eval "Child_$ZoneDevice=$R"
 +
eval "Z${R}_VolumeLevel=40"
 +
#SendToDevice "Z${R}40"
 +
SendToDevice "Z${R}?"
 +
done
 +
}
 +
 +
OnExit()
 +
{
 +
:
 +
}
 +
 +
## Command functions
 +
# Parameters come as environment variables of this form: Parm_<Number>
 +
 +
## Route media path
 +
Cmd_966()
 +
{
 +
local From="$1" To="$2"
 +
local MediaPath
 +
 +
MediaPath="$Parm_219"
 +
RouteMediaPath M "$MediaPath"
 +
ReplyToDCE "OK" ""
 +
}
 +
 +
## Input Select
 +
Cmd_91()
 +
{
 +
local From="$1" To="$2"
 +
local Input="$Parm_71"
 +
InputSelect M "$Input"
 +
ReplyToDCE "OK" ""
 +
}
 +
 +
## Mute
 +
Cmd_97()
 +
{
 +
local From="$1" To="$2"
 +
Mute M
 +
ReplyToDCE "OK"
 +
}
 +
 +
## Vol Down
 +
Cmd_90()
 +
{
 +
local From="$1" To="$2"
 +
VolDown M
 +
ReplyToDCE "OK"
 +
}
 +
 +
## Vol Up
 +
Cmd_89()
 +
{
 +
local From="$1" To="$2"
 +
VolUp M
 +
ReplyToDCE "OK"
 +
}
 +
 +
## Off
 +
Cmd_193()
 +
{
 +
local From="$1" To="$2"
 +
TurnOff M
 +
ReplyToDCE "OK"
 +
}
 +
 +
## On
 +
Cmd_192()
 +
{
 +
local From="$1" To="$2"
 +
TurnOn M
 +
ReplyToDCE "OK"
 +
}
 +
 +
Cmd_1000()
 +
{
 +
Log "One thousand!! $Parm_999"
 +
SendToDevice "$Parm_999"
 +
}
 +
 +
## User functions
 +
 +
VolUp()
 +
{
 +
local Zone="$1"
 +
local VarVolumeLevel VolumeCmd
 +
 +
if [[ -z "$Zone" ]]; then
 +
return
 +
fi
 +
 +
Log "Volume up zone $Zone"
 +
if [[ "$Zone" == M ]]; then
 +
VolumeCmd=MV
 +
else
 +
VolumeCmd=Z2
 +
fi
 +
VarVolumeLevel="Z${Zone}_VolumeLevel"
 +
 +
((${VarVolumeLevel} += $VolumeStep))
 +
if ((${!VarVolumeLevel} > 98)); then
 +
eval "$VarVolumeLevel=98"
 +
fi
 +
SendToDevice "$VolumeCmd$(builtin printf "%02d" "${!VarVolumeLevel}")"
 +
}
 +
 +
VolDown()
 +
{
 +
local Zone="$1"
 +
local VarVolumeLevel VolumeCmd
 +
 +
if [[ -z "$Zone" ]]; then
 +
return
 +
fi
 +
 +
Log "Volume down zone $Zone"
 +
if [[ "$Zone" == M ]]; then
 +
VolumeCmd=MV
 +
else
 +
VolumeCmd=Z2
 +
fi
 +
VarVolumeLevel="Z${Zone}_VolumeLevel"
 +
 +
((${VarVolumeLevel} -= $VolumeStep))
 +
if ((${!VarVolumeLevel} < 0)); then
 +
eval "${VarVolumeLevel}=0"
 +
fi
 +
SendToDevice "$VolumeCmd$(builtin printf "%02d" "${!VarVolumeLevel}")"
 +
}
 +
 +
Mute()
 +
{
 +
local Zone="$1"
 +
local VarMute MuteCmd
 +
 +
if [[ -z "$Zone" ]]; then
 +
return
 +
fi
 +
 +
if [[ "$Zone" == M ]]; then
 +
MuteCmd="MU"
 +
else
 +
MuteCmd="Z${Zone}MU"
 +
fi
 +
 +
VarMute="Z${Zone}_Mute"
 +
if [[ "${!VarMute}" == Mute ]]; then
 +
eval "$VarMute=NoMute"
 +
SendToDevice "${MuteCmd}OFF"
 +
Log "Unmute zone $Zone"
 +
else
 +
eval "$VarMute=Mute"
 +
SendToDevice "${MuteCmd}ON"
 +
Log "Mute zone $Zone"
 +
fi
 +
}
 +
 +
RouteMediaPath()
 +
{
 +
local Zone="$1" MediaPath="$2"
 +
local Path Type Input
 +
 +
if [[ -z "$Zone" ]]; then
 +
return
 +
fi
 +
 +
Log "Route media path zone $Zone: MediaPath=$MediaPath"
 +
MediaPath="${MediaPath//:/ }"
 +
 +
for Path in $MediaPath; do
 +
Type="${Path%_*}"
 +
Input="${Path#*_}"
 +
if [[ "$Type" != IN ]]; then
 +
continue
 +
fi
 +
InputSelect "$Zone" "$Input"
 +
done
 +
}
 +
 +
InputSelect()
 +
{
 +
local Zone="$1" Input="$2"
 +
local VarCurrentInput InputCmd
 +
 +
if [[ -z "$Zone" ]]; then
 +
return
 +
fi
 +
 +
Log "Input Select for zone $Zone: $Input"
 +
if [[ "$Zone" == M ]]; then
 +
InputCmd="SI"
 +
else
 +
InputCmd="Z${Zone}"
 +
fi
 +
VarCurrentInput="Z${Zone}_CurrentInput"
 +
 +
Input=$(TranslateInput "$Input")
 +
if [[ "${!VarCurrentInput}" != "$Input" ]]; then
 +
SendToDevice "$InputCmd$Input"
 +
eval "$VarCurrentInput=$Input"
 +
fi
 +
}
 +
 +
TurnOff()
 +
{
 +
local Zone="$1"
 +
if [[ -z "$Zone" ]]; then
 +
return
 +
fi
 +
Log "Turn off zone $Zone"
 +
SendToDevice "Z${Zone}OFF"
 +
}
 +
 +
TurnOn()
 +
{
 +
local Zone="$1"
 +
if [[ -z "$Zone" ]]; then
 +
return
 +
fi
 +
Log "Turn on zone $Zone"
 +
SendToDevice "Z${Zone}ON"
 +
}
 +
 +
TranslateInput()
 +
{
 +
local Input="$1"
 +
local input_name
 +
 +
if [[ -z "$Input" ]]; then
 +
return
 +
fi
 +
 +
case "$Input" in
 +
162) # CD
 +
input_name="CD"
 +
;;
 +
164) # Aux
 +
input_name="AUX"
 +
;;
 +
165) # DVD
 +
input_name="DVD"
 +
;;
 +
714) # DVR
 +
input_name="DVR"
 +
;;
 +
163) # Phono
 +
input_name="Phono"
 +
;;
 +
166) # Tuner
 +
input_name="Tuner"
 +
;;
 +
161) # TV
 +
input_name="TV"
 +
;;
 +
285) # V.AUX
 +
input_name="V.AUX"
 +
;;
 +
282) # VCR
 +
input_name="VCR"
 +
;;
 +
909) # XM
 +
input_name="XM"
 +
;;
 +
*) input_name="$Input" ;;
 +
esac
 +
builtin echo "$input_name"
 +
}
 +
 +
## Start eveything up
 +
. /usr/pluto/bin/DCE-connect.sh "$@"</nowiki>
 +
 +
== Script breakdown ==
 +
 +
=== Global section ===
 +
<nowiki>#!/bin/bash
 +
 +
. /usr/pluto/bin/Config_Ops.sh
 +
. /usr/pluto/bin/Utils.sh
 +
 +
DEVICEDATA_COM_Port_on_PC=37
 +
DEVICEDATA_Port=171
 +
DEVICETEMPLATE_Denon_AVR_Zone=1948</nowiki>
 +
 +
At the top of the script, /bin/bash is declared as the interpreter, two library scripts are included (Config_Ops.sh and Utils.sh), and three global varialbes are declared. Normally you'd declare DeviceData and DeviceTemplate variables here to be used in your script for better readability and "grep"-ability.
 +
 +
=== Mandatory functions ===
 +
 +
Functions used by DCE-connect.sh at various stages.
 +
 +
==== Configure ====
 +
 +
DCE-connect needs to know what type of communication channel is used to talk to the device, and what setup the channel requires.
 +
 +
DCE-connect can use RS232 or IP communications, can use a custom connection command (as of yet nothing requires this), or not connect to any device at all - only to the DCE side.
 +
 +
In the example, this sets up a serial communication channel at 9600 baud, N81 parity, using the serial port setting from the database.
 +
 +
<nowiki>## Mandatory functions
 +
Configure()
 +
{
 +
local Q R
 +
local SerialPort
 +
 +
Q="SELECT IK_DeviceData FROM Device_DeviceData WHERE FK_Device=$DevNo AND FK_DeviceData=$DEVICEDATA_COM_Port_on_PC"
 +
R=$(RunSQL "$Q")
 +
SerialPort=$(Field 1 "$R")
 +
SerialPort=$(TranslateSerialPort "$SerialPort")
 +
 +
## Device connection generics
 +
DeviceConnection_Type=serial # choices: none, serial, inet, custom
 +
## Connection parameters
 +
# type=serial:
 +
DeviceConnection_BaudRate=9600
 +
DeviceConnection_Parity=N81 # choices: N81, E81, O81
 +
DeviceConnection_SerialPort="$SerialPort"
 +
# type=inet:
 +
DeviceConnection_Protocol=TCP4 # choices: TCP4, TCP6, UDP4, UDP6
 +
DeviceConnection_Endpoint=127.0.0.1:2001 # Address:Port
 +
# type=custom:
 +
DeviceConnection_Command=
 +
 +
## What to do when the device is lost
 +
DeviceConnection_OnDisconnect=reconnect # choices: disable, reconnect
 +
 +
## Device protocol generics
 +
DeviceProtocol_Type=line # choices: line, stream
 +
# type=line:
 +
DeviceProtocol_Separator=$'\r' # must match encoding
 +
 +
DeviceProtocol_Encoding=none # choices: none, enc, hex
 +
DeviceProtocol_AutoAppendSeparator=yes # choices: yes, no
 +
DeviceProtocol_Delay=.2 # delay between commands (seconds)
 +
}</nowiki>
 +
 +
===== Device connection =====
 +
 +
; DeviceConnection_Type
 +
: Specifies the connection type for the device
 +
choices:
 +
* none
 +
: no device connection
 +
* serial
 +
: RS232 connection or similar
 +
* inet
 +
: Network/Internet connection
 +
* custom
 +
: custom connection command
 +
 +
For '''serial''' devices the following settings are required:
 +
; DeviceConnection_BaudRate
 +
: The baud rate for the serial port (e.g. 9600, 19200, 57600, 115200)
 +
; DeviceConnection_Parity
 +
: The parity setting for the serial port
 +
choices:
 +
* N81
 +
: no parity, 8 bit data, 1 stop bit
 +
* E81
 +
: even parity, 8 bit data, 1 stop bit
 +
* O81
 +
: odd parity, 8 bit data, 1 stop bit
 +
; DeviceConnection_SerialPort
 +
: The serial port to connect to (e.g. /dev/ttyS0)
 +
 +
For '''inet''' devices the following settings are required:
 +
; DeviceConnection_Protocol
 +
: The protocol used for communication
 +
choices:
 +
* TCP4
 +
* TCP6
 +
* UDP4
 +
* UDP6
 +
; DeviceConnection_Endpoint
 +
: The IP address of the device and the port to connect to
 +
: Format Address:Port - e.g. 127.0.0.1:2001
 +
 +
For '''custom''' connection devices the following setting is required:
 +
; DeviceConnection_Command
 +
: Command to be executed to establish the connection
 +
: TBD: how the command is to be passed and how it is supposed to behave
 +
: Right now custom connection commands aren't required by any device.
 +
 +
===== Connection and protocol settings =====
 +
 +
; DeviceConnection_OnDisconnect
 +
: What to do if the connection to the device is lost
 +
choices:
 +
* disable
 +
: disable the device so it doesn't restart
 +
* reconnect
 +
: attempt to reconnect
 +
 +
; DeviceProtocol_Type
 +
: What kind of communication protocol to assume
 +
choices:
 +
* line
 +
: The protocol is line-based, i.e. the byte stream can be chuncked into distinct lines delimited by an end-of-line charcter. The framework will help out by splitting the stream into lines.
 +
* stream
 +
: The protocol is stream-based, i.e. the byte stream isn't uniform and it needs custom parsing in the ProcessDeviceStream function. The framework will pass bytes to ProcessDeviceStream as bytes are received.
 +
 +
; DeviceProtocol_Separator
 +
: Contains the character that terminates a line if the protocol is line-based
 +
: Example values:
 +
* For encoding "none"
 +
: $'\n', $'\r'
 +
: about the wacky format: Bash interprets control characters inside $'text_goes_here' constructs, which we need
 +
* For encoding "enc"
 +
: %0A, %0D
 +
* For encoding "hex"
 +
: 0A, 0D
 +
 +
; DeviceProtocol_Encoding
 +
: How should the communication be encoded
 +
choices:
 +
* none
 +
: no special encoding; stream is passed as-is
 +
: useful for text-based protocols and protocols that only use printable characters
 +
* enc
 +
: stream is URL encoded, with non-printable characters encoded as %XY, where XY are hex digits
 +
: useful for protocols that consist of mostyle printable characters, but have a few non-printable control characters
 +
* hex
 +
: stream is hex encoded; all characters are transmitted as hex digit pairs
 +
: useful for binary protocols
 +
 +
; DeviceProtocol_AutoAppendSeparator
 +
: Tells the framework if it should help out and append the line termination character specified above when sending data to the device if the protocol is line-based
 +
choices:
 +
* yes
 +
* no
 +
 +
; DeviceProtocol_Delay
 +
: Delay to be inserted after commands, in seconds. Usually in fractions of a second, i.e: 0.2 for 200 ms
 +
: This is handy if devices get confused by or just can't accept back-to-back commands.
 +
 +
==== ProcessDeviceStream ====
 +
 +
This function is called by the framework each time data is received from the device.
 +
 +
Parameters:
 +
; Data="$1"
 +
: contains the data received from the device, as described below
 +
 +
If the protocol type is '''line''', then this function is called each time a full line is received.
 +
 +
If the protocol type is '''stream''', then this function is called each time there's data available. Applications should do their own buffering, since the data may not be a complete parsable block.
 +
 +
In the example, the protocol type is '''line''' as the protocol is text-based. Data is then interpreted and device state is stored. Since this device also has subzones, their details are handled here as well (the Z* case in the example).
 +
 +
<nowiki>ProcessDeviceStream()
 +
{
 +
local Data="$1"
 +
Data=$(builtin echo "$Data" | tr -d '\r')
 +
echo "Denon Amp said: $Data"
 +
 +
local Zone Parm
 +
 +
case "$Data" in
 +
SI*) ZM_CurrentInput="${Data:2}" ;;
 +
MUOFF) ZM_Mute=NoMute ;;
 +
MUON) ZM_Mute=Mute ;;
 +
MV[0-9][0-9])
 +
ZM_VolumeLevel="${Data:2}"
 +
if [[ "$ZM_VolumeLevel" == 0* ]]; then
 +
ZM_VolumeLevel="${ZM_VolumeLevel:1}"
 +
fi
 +
;;
 +
Z*)
 +
Zone="${Data:1:1}"
 +
Parm="${Data:2}"
 +
case "$Parm" in
 +
MUOFF) eval "Z${Zone}_Mute=NoMute" ;; # not muted
 +
MUON) eval "Z${Zone}_Mute=Mute" ;; # muted
 +
OFF|ON) : ;; # on/off
 +
[0-9][0-9]) # volume
 +
if [[ "$Parm" == 0* ]]; then
 +
Parm="${Parm:1}"
 +
fi
 +
eval "Z${Zone}_VolumeLevel=$Parm"
 +
;;
 +
PHONO|CD|TUNER|DVD|TV|TV/CBL|VDR|DVR|V.AUX|XM|IPOD|AUX) eval "Z${Zone}_CurrentInput=$Parm" ;; # input
 +
esac
 +
;;
 +
esac
 +
}</nowiki>
 +
 +
==== ReceivedCommandForChild ====
 +
 +
Equivalent to the DCE function with the same name. All commands that are received by a child of this device, which are forwarded to the parent, are processed in this function. For commands received by the device itself read below.
 +
 +
Parameters:
 +
; From="$1"
 +
: The number of the DCE device that sent the command
 +
; To="$2"
 +
: The number of the DCE device the command was sent to
 +
; Type="$3"
 +
: The DCE command type; see '''enum eMessageType''' in src/DCE/Message.h for possible values
 +
; Cmd="$4"
 +
: The DCE command value
 +
 +
In the example, this function handles volume, power and input select commands for amplifier subzones.
 +
 +
</nowiki>## Unknown command handlers
 +
ReceivedCommandForChild()
 +
{
 +
local From="$1" To="$2" Type="$3" Cmd="$4"
 +
local Zone
 +
local VarZone
 +
 +
VarZone="Child_$To"
 +
if [[ -z "${!VarZone}" || "$Type" != 1 ]]; then
 +
ReplyToDCE "UNHANDLED" ""
 +
return
 +
fi
 +
 +
Zone="${!VarZone}"
 +
case "$Cmd" in
 +
966) RouteMediaPath "$Zone" "$Parm_219" ;;
 +
91) InputSelect "$Zone" "$Parm_71" ;;
 +
97) Mute "$Zone" ;;
 +
90) VolDown "$Zone" ;;
 +
89) VolUp "$Zone" ;;
 +
193) TurnOff "$Zone" ;;
 +
192) TurnOn "$Zone" ;;
 +
esac
 +
 +
ReplyToDCE "OK" ""
 +
}</nowiki>
 +
 +
==== ReceivedUnknownCommand ====
 +
 +
Equivalent to the DCE function with the same name. This function is called when a command sent to the device itself doesn't have handler function (see below).
 +
 +
In the example, this function doesn't do anything, but it is still required.
 +
 +
You would use this function for command types other than 1 (MESSAGETYPE_COMMAND in src/DCE/Message.h) - e.g. events (MESSAGETYPE_EVENT in src/DCE/Message.h)
 +
 +
<nowiki>ReceivedUnknownCommand()
 +
{
 +
local From="$1" To="$2" Type="$3" Cmd="$4"
 +
ReplyToDCE "UNHANDLED" ""
 +
}</nowiki>
 +
 +
=== Hooks ===
 +
 +
==== OnDeviceConnect ====
 +
 +
This function is called by the framework upon successful (re)connection to the device. Suggested use for this hook is to get updated status from the device (especially on re-connect, which could make the stored status and real status be out of sync).
 +
 +
In the example, the Denon amplifier is queried for its power status. The reply is processed asynchronously in ProcessDeviceStream (see above).
 +
 +
<nowiki>## Hooks
 +
OnDeviceConnect()
 +
{
 +
SendToDevice "PW?"
 +
}</nowiki>
 +
 +
==== OnInit ====
 +
 +
This function is called by the framework on startup.
 +
 +
In this example, the state variables are initialized for the main zone and subzones (if they exist) and the amplifier is queried. Just looking at it, the queries should probably be moved from OnInit to OnDeviceConnect, based on the rationale above (see OnDeviceConnect above).
 +
 +
<nowiki>OnInit()
 +
{
 +
## User configuration code
 +
##########################
 +
 +
SendToDevice "SI?"
 +
#SendToDevice "MV40"
 +
SendToDevice "MV?"
 +
ZM_CurrentInput=
 +
ZM_Mute=Mute
 +
ZM_VolumeLevel=40
 +
 +
VolumeStep=4
 +
 +
local Q R Var
 +
local ZoneDevice
 +
Q="SELECT PK_Device FROM Device WHERE FK_Device_ControlledVia=$DevNo AND FK_DeviceTemplate=$DEVICETEMPLATE_Denon_AVR_Zone"
 +
R=$(RunSQL "$Q")
 +
 +
for ZoneDevice in $R; do
 +
Q="SELECT IK_DeviceData FROM Device_DeviceData WHERE FK_Device=$ZoneDevice AND FK_DeviceData=$DEVICEDATA_Port"
 +
R=$(RunSQL "$Q")
 +
if [[ -z "$R" ]]; then
 +
continue
 +
fi
 +
eval "Child_$ZoneDevice=$R"
 +
eval "Z${R}_VolumeLevel=40"
 +
#SendToDevice "Z${R}40"
 +
SendToDevice "Z${R}?"
 +
done
 +
}</nowiki>
 +
 +
==== OnExit ====
 +
 +
This function is called by the framework when the device is about to exit.
 +
 +
In the example, this function does nothing.
 +
 +
Also notice the double colon character, which in Bash is a built-in "no op" command (traditionally /bin/true would be called, but that's an external command, which means overhead). Bash cannot have empty content between curly brackets, and this function is mandatory to exist. That is why this workaround is used.
 +
 +
<nowiki>OnExit()
 +
{
 +
:
 +
}</nowiki>
 +
 +
=== Command functions ===
 +
 +
These functions handle DCE commands of type 1 (MESSAGETYPE_COMMAND in src/DCE/Message.h).
 +
 +
Their names have the following format:
 +
 +
Cmd_''CommandId''()
 +
 +
where CommandId is the ID of the DCE command the function will handle.
 +
 +
Parameters:
 +
; From="$1"
 +
: The number of the DCE device that sent the command
 +
; To="$2"
 +
: The number of the DCE device the command was sent to
 +
 +
Command parameters:
 +
; Parm_''ParameterId''
 +
: The command parameters come in environment variables named as above. They aren't function parameters.
 +
 +
<nowiki>## Command functions
 +
# Parameters come as environment variables of this form: Parm_<Number></nowiki>
 +
 +
In the example, the following commands are handled:
 +
 +
* Route Media Path (CommandId=966)
 +
:Parameters:
 +
:* MediaPath (ParameterId=219)
 +
 +
This command is specific to Dianemo. It activates a specified MediaPath. Similar to Media Pipes.
 +
 +
<nowiki>## Route media path
 +
Cmd_966()
 +
{
 +
local From="$1" To="$2"
 +
local MediaPath
 +
 +
MediaPath="$Parm_219"
 +
RouteMediaPath M "$MediaPath"
 +
ReplyToDCE "OK" ""
 +
}</nowiki>
 +
 +
* Input Select (CommandId=91)
 +
:Parameters:
 +
:* Input (ParameterId=71)
 +
 +
Selects an input on the amplifier's main zone.
 +
 +
<nowiki>## Input Select
 +
Cmd_91()
 +
{
 +
local From="$1" To="$2"
 +
local Input="$Parm_71"
 +
InputSelect M "$Input"
 +
ReplyToDCE "OK" ""
 +
}</nowiki>
 +
 +
* Mute (CommandId=97)
 +
:Parameters: ''none''
 +
 +
Toggles mute on the main zone of the amplifier.
 +
 +
<nowiki>## Mute
 +
Cmd_97()
 +
{
 +
local From="$1" To="$2"
 +
Mute M
 +
ReplyToDCE "OK"
 +
}</nowiki>
 +
 +
* Vol Down (CommandId=90) and Vol Up (CommandId=89)
 +
:Parameters: ''none''
 +
 +
Turn volume up or down on the main zone of the amplifier.
 +
 +
<nowiki>## Vol Down
 +
Cmd_90()
 +
{
 +
local From="$1" To="$2"
 +
VolDown M
 +
ReplyToDCE "OK"
 +
}
 +
 +
## Vol Up
 +
Cmd_89()
 +
{
 +
local From="$1" To="$2"
 +
VolUp M
 +
ReplyToDCE "OK"
 +
}</nowiki>
 +
 +
* On (CommandId=192) and Off (CommandId=193)
 +
:Parameters: ''none''
 +
 +
Turn the main zone on and off.
 +
 +
<nowiki>## Off
 +
Cmd_193()
 +
{
 +
local From="$1" To="$2"
 +
TurnOff M
 +
ReplyToDCE "OK"
 +
}
 +
 +
## On
 +
Cmd_192()
 +
{
 +
local From="$1" To="$2"
 +
TurnOn M
 +
ReplyToDCE "OK"
 +
}</nowiki>
 +
 +
* One Thousand! (CommandId=1000)
 +
:Parameters:
 +
:* 999 (ParameterId=999)
 +
 +
This command doesn't really exist. It's a debug "feature" that allows sending arbitrary commands to the device using MessageSend without shutting down the DCE device.
 +
 +
Usage example:
 +
/usr/pluto/bin/MessageSend dcerouter 0 99 1 1000 999 'SI?'
 +
In the above example, 99 is a Denon amplifier in the DCE installation, and SI? happens to be the query string for the current input. Very useful when you don't know what an input is called, because it was renamed on the LCD, but you can identify it visually using the knobs on the front and eyeballs on the TV and/or ears on speakers, while not disturbing the DCE device itself.
 +
 +
<nowiki>Cmd_1000()
 +
{
 +
Log "One thousand!! $Parm_999"
 +
SendToDevice "$Parm_999"
 +
}</nowiki>
 +
 +
=== User functions ===
 +
 +
You should write all your support code under here: functions that you'll call from the hooks or command handlers above.
 +
 +
In the example, these functions handle volume, mute, and power commands for both the main zone and subzones, depending on the parameters they receive. They are used both in ReceivedCommandForChild (which handles subzone child devices) and the Cmd_''CommandId'' functions (which handle the main zone).
 +
 +
<nowiki>## User functions
 +
 +
VolUp()
 +
{
 +
local Zone="$1"
 +
local VarVolumeLevel VolumeCmd
 +
 +
if [[ -z "$Zone" ]]; then
 +
return
 +
fi
 +
 +
Log "Volume up zone $Zone"
 +
if [[ "$Zone" == M ]]; then
 +
VolumeCmd=MV
 +
else
 +
VolumeCmd=Z2
 +
fi
 +
VarVolumeLevel="Z${Zone}_VolumeLevel"
 +
 +
((${VarVolumeLevel} += $VolumeStep))
 +
if ((${!VarVolumeLevel} > 98)); then
 +
eval "$VarVolumeLevel=98"
 +
fi
 +
SendToDevice "$VolumeCmd$(builtin printf "%02d" "${!VarVolumeLevel}")"
 +
}
 +
 +
VolDown()
 +
{
 +
local Zone="$1"
 +
local VarVolumeLevel VolumeCmd
 +
 +
if [[ -z "$Zone" ]]; then
 +
return
 +
fi
 +
 +
Log "Volume down zone $Zone"
 +
if [[ "$Zone" == M ]]; then
 +
VolumeCmd=MV
 +
else
 +
VolumeCmd=Z2
 +
fi
 +
VarVolumeLevel="Z${Zone}_VolumeLevel"
 +
 +
((${VarVolumeLevel} -= $VolumeStep))
 +
if ((${!VarVolumeLevel} < 0)); then
 +
eval "${VarVolumeLevel}=0"
 +
fi
 +
SendToDevice "$VolumeCmd$(builtin printf "%02d" "${!VarVolumeLevel}")"
 +
}
 +
 +
Mute()
 +
{
 +
local Zone="$1"
 +
local VarMute MuteCmd
 +
 +
if [[ -z "$Zone" ]]; then
 +
return
 +
fi
 +
 +
if [[ "$Zone" == M ]]; then
 +
MuteCmd="MU"
 +
else
 +
MuteCmd="Z${Zone}MU"
 +
fi
 +
 +
VarMute="Z${Zone}_Mute"
 +
if [[ "${!VarMute}" == Mute ]]; then
 +
eval "$VarMute=NoMute"
 +
SendToDevice "${MuteCmd}OFF"
 +
Log "Unmute zone $Zone"
 +
else
 +
eval "$VarMute=Mute"
 +
SendToDevice "${MuteCmd}ON"
 +
Log "Mute zone $Zone"
 +
fi
 +
}
 +
 +
RouteMediaPath()
 +
{
 +
local Zone="$1" MediaPath="$2"
 +
local Path Type Input
 +
 +
if [[ -z "$Zone" ]]; then
 +
return
 +
fi
 +
 +
Log "Route media path zone $Zone: MediaPath=$MediaPath"
 +
MediaPath="${MediaPath//:/ }"
 +
 +
for Path in $MediaPath; do
 +
Type="${Path%_*}"
 +
Input="${Path#*_}"
 +
if [[ "$Type" != IN ]]; then
 +
continue
 +
fi
 +
InputSelect "$Zone" "$Input"
 +
done
 +
}
 +
 +
InputSelect()
 +
{
 +
local Zone="$1" Input="$2"
 +
local VarCurrentInput InputCmd
 +
 +
if [[ -z "$Zone" ]]; then
 +
return
 +
fi
 +
 +
Log "Input Select for zone $Zone: $Input"
 +
if [[ "$Zone" == M ]]; then
 +
InputCmd="SI"
 +
else
 +
InputCmd="Z${Zone}"
 +
fi
 +
VarCurrentInput="Z${Zone}_CurrentInput"
 +
 +
Input=$(TranslateInput "$Input")
 +
if [[ "${!VarCurrentInput}" != "$Input" ]]; then
 +
SendToDevice "$InputCmd$Input"
 +
eval "$VarCurrentInput=$Input"
 +
fi
 +
}
 +
 +
TurnOff()
 +
{
 +
local Zone="$1"
 +
if [[ -z "$Zone" ]]; then
 +
return
 +
fi
 +
Log "Turn off zone $Zone"
 +
SendToDevice "Z${Zone}OFF"
 +
}
 +
 +
TurnOn()
 +
{
 +
local Zone="$1"
 +
if [[ -z "$Zone" ]]; then
 +
return
 +
fi
 +
Log "Turn on zone $Zone"
 +
SendToDevice "Z${Zone}ON"
 +
}
 +
 +
TranslateInput()
 +
{
 +
local Input="$1"
 +
local input_name
 +
 +
if [[ -z "$Input" ]]; then
 +
return
 +
fi
 +
 +
case "$Input" in
 +
162) # CD
 +
input_name="CD"
 +
;;
 +
164) # Aux
 +
input_name="AUX"
 +
;;
 +
165) # DVD
 +
input_name="DVD"
 +
;;
 +
714) # DVR
 +
input_name="DVR"
 +
;;
 +
163) # Phono
 +
input_name="Phono"
 +
;;
 +
166) # Tuner
 +
input_name="Tuner"
 +
;;
 +
161) # TV
 +
input_name="TV"
 +
;;
 +
285) # V.AUX
 +
input_name="V.AUX"
 +
;;
 +
282) # VCR
 +
input_name="VCR"
 +
;;
 +
909) # XM
 +
input_name="XM"
 +
;;
 +
*) input_name="$Input" ;;
 +
esac
 +
builtin echo "$input_name"
 +
}</nowiki>
 +
 +
=== Start everything up ===
 +
 +
After everything is defined, DCE-connect.sh, which does all the magic of calling your handlers, needs to be called. It also starts a main program loop, which is why you only define handlers.
 +
 +
<nowiki>## Start eveything up
 +
. /usr/pluto/bin/DCE-connect.sh "$@"</nowiki>
 +
 +
== Framework functions ==
 +
 +
In the example script, you saw these already. Time to explain what they do and when they are required.
 +
 +
=== DCE communication functions ===
 +
 +
==== SendToDCE ====
 +
Send a DCE command.
 +
 +
Parameters:
 +
; Cmd="$1"
 +
: DCE command structured like the parameters passed to MessageSend
 +
: All command parameters values must be URLencoded if they contain whitespace or other special characters.
 +
: See '''BuildCommand''' below.
 +
 +
Note: the ''-r'' and ''-o'' options aren't supported for sent messages.
 +
 +
==== ReplyToDCE ====
 +
Send a reply after handling a DCE message.
 +
 +
Parameters:
 +
; ReplyString="$1"
 +
: Result of the message
 +
choices:
 +
* OK
 +
: message was handled sucessfully
 +
* UNHANDLED
 +
: message wasn't handled at all
 +
; OutVars="$2"
 +
: Response parameters to send to a DCE message, which will be received by a command sent with the ''-o'' option.
 +
 +
The following commands must call this function before returning:
 +
 +
* ReceivedCommandForChild
 +
* ReceivedUnknownCommand
 +
* Cmd_''CommandId''
 +
 +
None others should call this function.
 +
 +
=== Device communication functions ===
 +
 +
==== ReadFromDevice ====
 +
 +
Read data from the device. The returned data is encoded as per the DeviceProtocol_Encoding setting set in the Configure function. The data is sent to STDOUT, so you'll need to capture it as in the usage example below.
 +
 +
The Whisperer is most useful when used asynchronously, according to the following pattern send command/query to the device, then update the internal status when the device replies later; postpone action until device replies. This means you shouldn't use this function unless this pattern is useless to you. For example the VDR script in Dianemo has to use this, but the protocol is line-based, so it doesn't mess anything up. If your protocol is stream based, you should make sure you add any extra bytes back to the buffer you're processing in ProcessDeviceStream.
 +
 +
Usage example:
 +
 +
Data=$(ReadFromDevice)
 +
 +
==== SendToDevice ====
 +
 +
Send data to the device
 +
 +
Parameters:
 +
; Data="$1"
 +
: Data to be sent to the device. Data must be encoded as per the DeviceProtocol_Encoding setting set in the Configure function.
 +
 +
=== DCE Message utility functions ===
 +
 +
==== ParmEncode ====
 +
 +
URLencode a string to be used in conjunction with the ''enc'' encoding.
 +
 +
Output is sent to STDOUT, so you much capture the output as in the usage example below.
 +
 +
Parameters:
 +
; Data="$1"
 +
: Data to be encoded
 +
 +
Usage example:
 +
EncodedString=$(ParmEncode "$String")
 +
 +
==== ParmDecode ====
 +
 +
URLdecode a string
 +
 +
Output is sent to STDOUT, so you much capture the output as in the usage example below.
 +
 +
Parameters:
 +
; Data="$1"
 +
: Data to be decoded
 +
 +
Usage example:
 +
String=$(ParmDecode "$EncodedString")
 +
 +
==== BuildCommand ====
 +
 +
Build a string that can be passed to SendToDCE as a parameter.
 +
 +
All the bits of the command to be assembled must be passed as individual parameters.
 +
 +
Usage example:
 +
AssembledCommand=$(BuildCommand $DevNo $DestDevice 1 1000 999 "string with spaces" 998 "another string with spaces")
 +
 +
In the example above, all the command bits are given, including the From, To, CommandType, and CommandId, but you can build a command like this too:
 +
AssembledCommand="$DevNo $DestDevice 1 1000"$(BuildCommand 999 "string with spaces" 998 "another string with spaces")
 +
but it isn't as readable.
 +
 +
=== Utility functions ===
 +
 +
==== Log ====
 +
 +
Write a log message. All the parameters will be written to STDERR and to the log file (if one has been specified with the ''-l'' parameter when the device started).
 +
 +
=== CAVEAT ===
 +
 +
==== echo and printf ====
 +
 +
The ''echo'' and ''printf'' functions are taken over and redirected to STDERR to prevent STDOUT polution. STDOUT is used by some obscure DCE bits, and writing random data to it from a Whisperer scripts will confuse the DCE bridge and crash the device.
 +
 +
If you need to use ''echo'' and ''printf'' in constructs like:
 +
Var=$(echo "$Stuff"|cut -d: -f1)
 +
Var=$(printf "%s:%s" "value1" "value2")
 +
Then you must adapt the calls like this:
 +
Var=$(builtin echo "$Stuff"|cut -d: -f1)
 +
Var=$(builtin printf "%s:%s" "value1" "value2")
 +
 +
If you don't, the captured output will be an empty string because the result will be sent to STDERR instead (and you'll see it on the screen).
 +
 +
= How to write a DCE Whisperer Bash Device Template =
 +
 +
== Create the Device Template ==
 +
* Create a regular GSD device template in Web admin.
 +
* The Command line contains a call to Generic_Serial_Devices for a regular GSD device. For the Whisperer based template, you put the filename of the bash script.
 +
* The device template bash script looks very similar to a ruby device template. The easiest way to start it to edit an existing DCE Whisperer script, like DenonAmp - also shown above. (Get the [http://svn.linuxmce.org/trac.cgi/browser/branches/LinuxMCE-0810/src/DenonAmp/DenonAmp?format=raw DenonAmp] script.)
 +
 +
= How to use a DCE Whisperer Bash Device Template =
 +
A DCE device based on the whisperer works the same way any other C++ DCE device.

Revision as of 18:29, 6 September 2011

Description

DCE Whisperer is a framework intended to be middle ground between C++ DCE devices and GSD devices: you can quickly create a DCE device that can be as powerful as a C++ device (especially if you have deep understanding of Bash and its idiosyncrasies), but as versatile as a GSD (but without the database/web browser limitations).

It consists of a Bash framework - the DCE-connect.sh script, and a C++ compiled DCE protocol bridge - DCE-Whisperer.

A PHP version is envisioned, but I haven't done any work on it yet. -Uplink 19:29, 6 September 2011 (CEST)

Who uses it

Right now LinuxMCE only has some example scripts and it is not using the Whisperer framework actively.

In Dianemo, the following device scripts are based on Whisperer:

  • Sound amplifiers
    • DenonAmp
    • ArcamAmp
    • TEAC_AG980
    • Pioneer_SCLX82
  • Blu-ray players
    • Yamaha_BD-S1900
    • Denon_2500BT_3800BD
  • TVs
    • Panasonic_TH50PHD.sh
    • Seura_Hydra.sh
    • LG_LCD_RS232
    • LG_32LD690_RS232
  • AV matrixes and switches
    • Cisco_Managed_Switch
    • Kramer_AV_Switch
    • Grand_Being_HDMI_Switch
    • SierraVideo_VS
    • CLUX-42S
    • CLUX61
  • Set-top boxes
    • Sky_Digibox.sh
  • Software DCE devices
    • External_Source_Player
    • Video_Screen_Saver
    • Generic_IR_AV_Device
  • Others
    • Lutron_Homeworks

Dependencies

  • dce-whisperer

The dce-whisperer package contains DCE-connect.sh and DCE-Whisperer.

  • shell-io

The shell-io package contains the fdread, fdwrite and fdselect utilities, which are used by DCE-connect.sh to access the read(2), write(2), and select(2) libc library functions from Bash, with some help to work around Bash idiosyncrasies (like for example no support for binary-safe strings).

  • php5-cli

This is used for things like urlencode/urldecode, for which Bash and its traditional support tools (e.g. sed, awk) doesn't have equivalent functions.

While Dianemo already includes these, the dce-whisperer and shell-io packages aren't currently part of the LinuxMCE build system. Pre-built packages are available from possy:

Example script analysis

What it looks like

The following is the source code of the DCE device that controls Denon amplifiers in Dianemo.

Syntax highlighted version on Ubuntu Pastebin: http://paste.ubuntu.com/683553/

#!/bin/bash

. /usr/pluto/bin/Config_Ops.sh
. /usr/pluto/bin/Utils.sh

DEVICEDATA_COM_Port_on_PC=37
DEVICEDATA_Port=171
DEVICETEMPLATE_Denon_AVR_Zone=1948

## Mandatory functions
Configure()
{
	local Q R
	local SerialPort

	Q="SELECT IK_DeviceData FROM Device_DeviceData WHERE FK_Device=$DevNo AND FK_DeviceData=$DEVICEDATA_COM_Port_on_PC"
	R=$(RunSQL "$Q")
	SerialPort=$(Field 1 "$R")
	SerialPort=$(TranslateSerialPort "$SerialPort")

	## Device connection generics
	DeviceConnection_Type=serial # choices: none, serial, inet, custom
	## Connection parameters
	# type=serial:
	DeviceConnection_BaudRate=9600
	DeviceConnection_Parity=N81 # choices: N81, E81, O81
	DeviceConnection_SerialPort="$SerialPort"
	# type=inet:
	DeviceConnection_Protocol=TCP4 # choices: TCP4, TCP6, UDP4, UDP6
	DeviceConnection_Endpoint=127.0.0.1:2001 # Address:Port
	# type=custom:
	DeviceConnection_Command=

	## What to do when the device is lost
	DeviceConnection_OnDisconnect=reconnect # choices: disable, reconnect

	## Device protocol generics
	DeviceProtocol_Type=line # choices: line, stream
	# type=line:
	DeviceProtocol_Separator=$'\r' # must match encoding

	DeviceProtocol_Encoding=none # choices: none, enc, hex
	DeviceProtocol_AutoAppendSeparator=yes # choices: yes, no
	DeviceProtocol_Delay=.2 # delay between commands (seconds)
}

ProcessDeviceStream()
{
	local Data="$1"
	Data=$(builtin echo "$Data" | tr -d '\r')
	echo "Denon Amp said: $Data"

	local Zone Parm

	case "$Data" in
		SI*) ZM_CurrentInput="${Data:2}" ;;
		MUOFF) ZM_Mute=NoMute ;;
		MUON) ZM_Mute=Mute ;;
		MV[0-9][0-9])
			ZM_VolumeLevel="${Data:2}"
			if [[ "$ZM_VolumeLevel" == 0* ]]; then
				ZM_VolumeLevel="${ZM_VolumeLevel:1}"
			fi
		;;
		Z*)
			Zone="${Data:1:1}"
			Parm="${Data:2}"
			case "$Parm" in
				MUOFF) eval "Z${Zone}_Mute=NoMute" ;; # not muted
				MUON) eval "Z${Zone}_Mute=Mute" ;; # muted
				OFF|ON) : ;; # on/off
				[0-9][0-9]) # volume
					if [[ "$Parm" == 0* ]]; then
						Parm="${Parm:1}"
					fi
					eval "Z${Zone}_VolumeLevel=$Parm"
				;;
				PHONO|CD|TUNER|DVD|TV|TV/CBL|VDR|DVR|V.AUX|XM|IPOD|AUX) eval "Z${Zone}_CurrentInput=$Parm" ;; # input
			esac
		;;
	esac
}

## Unknown command handlers
ReceivedCommandForChild()
{
	local From="$1" To="$2" Type="$3" Cmd="$4"
	local Zone
	local VarZone

	VarZone="Child_$To"
	if [[ -z "${!VarZone}" || "$Type" != 1 ]]; then
		ReplyToDCE "UNHANDLED" ""
		return
	fi

	Zone="${!VarZone}"
	case "$Cmd" in
		966) RouteMediaPath "$Zone" "$Parm_219" ;;
		91) InputSelect "$Zone" "$Parm_71" ;;
		97) Mute "$Zone" ;;
		90) VolDown "$Zone" ;;
		89) VolUp "$Zone" ;;
		193) TurnOff "$Zone" ;;
		192) TurnOn "$Zone" ;;
	esac

	ReplyToDCE "OK" ""
}

ReceivedUnknownCommand()
{
	local From="$1" To="$2" Type="$3" Cmd="$4"
	ReplyToDCE "UNHANDLED" ""
}

## Hooks
OnDeviceConnect()
{
	SendToDevice "PW?"
}

OnInit()
{
	## User configuration code
	##########################
	
	SendToDevice "SI?"
	#SendToDevice "MV40"
	SendToDevice "MV?"
	ZM_CurrentInput=
	ZM_Mute=Mute
	ZM_VolumeLevel=40

	VolumeStep=4

	local Q R Var
	local ZoneDevice
	Q="SELECT PK_Device FROM Device WHERE FK_Device_ControlledVia=$DevNo AND FK_DeviceTemplate=$DEVICETEMPLATE_Denon_AVR_Zone"
	R=$(RunSQL "$Q")

	for ZoneDevice in $R; do
		Q="SELECT IK_DeviceData FROM Device_DeviceData WHERE FK_Device=$ZoneDevice AND FK_DeviceData=$DEVICEDATA_Port"
		R=$(RunSQL "$Q")
		if [[ -z "$R" ]]; then
			continue
		fi
		eval "Child_$ZoneDevice=$R"
		eval "Z${R}_VolumeLevel=40"
		#SendToDevice "Z${R}40"
		SendToDevice "Z${R}?"
	done
}

OnExit()
{
	:
}

## Command functions
# Parameters come as environment variables of this form: Parm_<Number>

## Route media path
Cmd_966()
{
	local From="$1" To="$2"
	local MediaPath

	MediaPath="$Parm_219"
	RouteMediaPath M "$MediaPath"
	ReplyToDCE "OK" ""
}

## Input Select
Cmd_91()
{
	local From="$1" To="$2"
	local Input="$Parm_71"
	InputSelect M "$Input"
	ReplyToDCE "OK" ""
}

## Mute
Cmd_97()
{
	local From="$1" To="$2"
	Mute M
	ReplyToDCE "OK"
}

## Vol Down
Cmd_90()
{
	local From="$1" To="$2"
	VolDown M
	ReplyToDCE "OK"
}

## Vol Up
Cmd_89()
{
	local From="$1" To="$2"
	VolUp M
	ReplyToDCE "OK"
}

## Off
Cmd_193()
{
	local From="$1" To="$2"
	TurnOff M
	ReplyToDCE "OK"
}

## On
Cmd_192()
{
	local From="$1" To="$2"
	TurnOn M
	ReplyToDCE "OK"
}

Cmd_1000()
{
	Log "One thousand!! $Parm_999"
	SendToDevice "$Parm_999"
}

## User functions

VolUp()
{
	local Zone="$1"
	local VarVolumeLevel VolumeCmd

	if [[ -z "$Zone" ]]; then
		return
	fi
	
	Log "Volume up zone $Zone"
	if [[ "$Zone" == M ]]; then
		VolumeCmd=MV
	else
		VolumeCmd=Z2
	fi
	VarVolumeLevel="Z${Zone}_VolumeLevel"

	((${VarVolumeLevel} += $VolumeStep))
	if ((${!VarVolumeLevel} > 98)); then
		eval "$VarVolumeLevel=98"
	fi
	SendToDevice "$VolumeCmd$(builtin printf "%02d" "${!VarVolumeLevel}")"
}

VolDown()
{
	local Zone="$1"
	local VarVolumeLevel VolumeCmd

	if [[ -z "$Zone" ]]; then
		return
	fi
	
	Log "Volume down zone $Zone"
	if [[ "$Zone" == M ]]; then
		VolumeCmd=MV
	else
		VolumeCmd=Z2
	fi
	VarVolumeLevel="Z${Zone}_VolumeLevel"

	((${VarVolumeLevel} -= $VolumeStep))
	if ((${!VarVolumeLevel} < 0)); then
		eval "${VarVolumeLevel}=0"
	fi
	SendToDevice "$VolumeCmd$(builtin printf "%02d" "${!VarVolumeLevel}")"
}

Mute()
{
	local Zone="$1"
	local VarMute MuteCmd

	if [[ -z "$Zone" ]]; then
		return
	fi

	if [[ "$Zone" == M ]]; then
		MuteCmd="MU"
	else
		MuteCmd="Z${Zone}MU"
	fi

	VarMute="Z${Zone}_Mute"
	if [[ "${!VarMute}" == Mute ]]; then
		eval "$VarMute=NoMute"
		SendToDevice "${MuteCmd}OFF"
		Log "Unmute zone $Zone"
	else
		eval "$VarMute=Mute"
		SendToDevice "${MuteCmd}ON"
		Log "Mute zone $Zone"
	fi
}

RouteMediaPath()
{
	local Zone="$1" MediaPath="$2"
	local Path Type Input

	if [[ -z "$Zone" ]]; then
		return
	fi

	Log "Route media path zone $Zone: MediaPath=$MediaPath"
	MediaPath="${MediaPath//:/ }"

	for Path in $MediaPath; do
		Type="${Path%_*}"
		Input="${Path#*_}"
		if [[ "$Type" != IN ]]; then
			continue
		fi
		InputSelect "$Zone" "$Input"
	done
}

InputSelect()
{
	local Zone="$1" Input="$2"
	local VarCurrentInput InputCmd

	if [[ -z "$Zone" ]]; then
		return
	fi

	Log "Input Select for zone $Zone: $Input"
	if [[ "$Zone" == M ]]; then
		InputCmd="SI"
	else
		InputCmd="Z${Zone}"
	fi
	VarCurrentInput="Z${Zone}_CurrentInput"

	Input=$(TranslateInput "$Input")
	if [[ "${!VarCurrentInput}" != "$Input" ]]; then
		SendToDevice "$InputCmd$Input"
		eval "$VarCurrentInput=$Input"
	fi
}

TurnOff()
{
	local Zone="$1"
	if [[ -z "$Zone" ]]; then
		return
	fi
	Log "Turn off zone $Zone"
	SendToDevice "Z${Zone}OFF"
}

TurnOn()
{
	local Zone="$1"
	if [[ -z "$Zone" ]]; then
		return
	fi
	Log "Turn on zone $Zone"
	SendToDevice "Z${Zone}ON"
}

TranslateInput()
{
	local Input="$1"
	local input_name

	if [[ -z "$Input" ]]; then
		return
	fi

	case "$Input" in
		162) # CD
			input_name="CD"
		;;
		164) # Aux
			input_name="AUX"
		;;
		165) # DVD
			input_name="DVD"
		;;
		714) # DVR
			input_name="DVR"
		;;
		163) # Phono
			input_name="Phono"
		;;
		166) # Tuner
			input_name="Tuner"
		;;
		161) # TV
			input_name="TV"
		;;
		285) # V.AUX
			input_name="V.AUX"
		;;
		282) # VCR
			input_name="VCR"
		;;
		909) # XM
			input_name="XM"
		;;
		*) input_name="$Input" ;;
	esac
	builtin echo "$input_name"
}

## Start eveything up
. /usr/pluto/bin/DCE-connect.sh "$@"

Script breakdown

Global section

#!/bin/bash

. /usr/pluto/bin/Config_Ops.sh
. /usr/pluto/bin/Utils.sh

DEVICEDATA_COM_Port_on_PC=37
DEVICEDATA_Port=171
DEVICETEMPLATE_Denon_AVR_Zone=1948

At the top of the script, /bin/bash is declared as the interpreter, two library scripts are included (Config_Ops.sh and Utils.sh), and three global varialbes are declared. Normally you'd declare DeviceData and DeviceTemplate variables here to be used in your script for better readability and "grep"-ability.

Mandatory functions

Functions used by DCE-connect.sh at various stages.

Configure

DCE-connect needs to know what type of communication channel is used to talk to the device, and what setup the channel requires.

DCE-connect can use RS232 or IP communications, can use a custom connection command (as of yet nothing requires this), or not connect to any device at all - only to the DCE side.

In the example, this sets up a serial communication channel at 9600 baud, N81 parity, using the serial port setting from the database.

## Mandatory functions
Configure()
{
	local Q R
	local SerialPort

	Q="SELECT IK_DeviceData FROM Device_DeviceData WHERE FK_Device=$DevNo AND FK_DeviceData=$DEVICEDATA_COM_Port_on_PC"
	R=$(RunSQL "$Q")
	SerialPort=$(Field 1 "$R")
	SerialPort=$(TranslateSerialPort "$SerialPort")

	## Device connection generics
	DeviceConnection_Type=serial # choices: none, serial, inet, custom
	## Connection parameters
	# type=serial:
	DeviceConnection_BaudRate=9600
	DeviceConnection_Parity=N81 # choices: N81, E81, O81
	DeviceConnection_SerialPort="$SerialPort"
	# type=inet:
	DeviceConnection_Protocol=TCP4 # choices: TCP4, TCP6, UDP4, UDP6
	DeviceConnection_Endpoint=127.0.0.1:2001 # Address:Port
	# type=custom:
	DeviceConnection_Command=

	## What to do when the device is lost
	DeviceConnection_OnDisconnect=reconnect # choices: disable, reconnect

	## Device protocol generics
	DeviceProtocol_Type=line # choices: line, stream
	# type=line:
	DeviceProtocol_Separator=$'\r' # must match encoding

	DeviceProtocol_Encoding=none # choices: none, enc, hex
	DeviceProtocol_AutoAppendSeparator=yes # choices: yes, no
	DeviceProtocol_Delay=.2 # delay between commands (seconds)
}
Device connection
DeviceConnection_Type
Specifies the connection type for the device

choices:

  • none
no device connection
  • serial
RS232 connection or similar
  • inet
Network/Internet connection
  • custom
custom connection command

For serial devices the following settings are required:

DeviceConnection_BaudRate
The baud rate for the serial port (e.g. 9600, 19200, 57600, 115200)
DeviceConnection_Parity
The parity setting for the serial port

choices:

  • N81
no parity, 8 bit data, 1 stop bit
  • E81
even parity, 8 bit data, 1 stop bit
  • O81
odd parity, 8 bit data, 1 stop bit
DeviceConnection_SerialPort
The serial port to connect to (e.g. /dev/ttyS0)

For inet devices the following settings are required:

DeviceConnection_Protocol
The protocol used for communication

choices:

  • TCP4
  • TCP6
  • UDP4
  • UDP6
DeviceConnection_Endpoint
The IP address of the device and the port to connect to
Format Address:Port - e.g. 127.0.0.1:2001

For custom connection devices the following setting is required:

DeviceConnection_Command
Command to be executed to establish the connection
TBD: how the command is to be passed and how it is supposed to behave
Right now custom connection commands aren't required by any device.
Connection and protocol settings
DeviceConnection_OnDisconnect
What to do if the connection to the device is lost

choices:

  • disable
disable the device so it doesn't restart
  • reconnect
attempt to reconnect
DeviceProtocol_Type
What kind of communication protocol to assume

choices:

  • line
The protocol is line-based, i.e. the byte stream can be chuncked into distinct lines delimited by an end-of-line charcter. The framework will help out by splitting the stream into lines.
  • stream
The protocol is stream-based, i.e. the byte stream isn't uniform and it needs custom parsing in the ProcessDeviceStream function. The framework will pass bytes to ProcessDeviceStream as bytes are received.
DeviceProtocol_Separator
Contains the character that terminates a line if the protocol is line-based
Example values:
  • For encoding "none"
$'\n', $'\r'
about the wacky format: Bash interprets control characters inside $'text_goes_here' constructs, which we need
  • For encoding "enc"
 %0A, %0D
  • For encoding "hex"
0A, 0D
DeviceProtocol_Encoding
How should the communication be encoded

choices:

  • none
no special encoding; stream is passed as-is
useful for text-based protocols and protocols that only use printable characters
  • enc
stream is URL encoded, with non-printable characters encoded as %XY, where XY are hex digits
useful for protocols that consist of mostyle printable characters, but have a few non-printable control characters
  • hex
stream is hex encoded; all characters are transmitted as hex digit pairs
useful for binary protocols
DeviceProtocol_AutoAppendSeparator
Tells the framework if it should help out and append the line termination character specified above when sending data to the device if the protocol is line-based

choices:

  • yes
  • no
DeviceProtocol_Delay
Delay to be inserted after commands, in seconds. Usually in fractions of a second, i.e: 0.2 for 200 ms
This is handy if devices get confused by or just can't accept back-to-back commands.

ProcessDeviceStream

This function is called by the framework each time data is received from the device.

Parameters:

Data="$1"
contains the data received from the device, as described below

If the protocol type is line, then this function is called each time a full line is received.

If the protocol type is stream, then this function is called each time there's data available. Applications should do their own buffering, since the data may not be a complete parsable block.

In the example, the protocol type is line as the protocol is text-based. Data is then interpreted and device state is stored. Since this device also has subzones, their details are handled here as well (the Z* case in the example).

ProcessDeviceStream()
{
	local Data="$1"
	Data=$(builtin echo "$Data" | tr -d '\r')
	echo "Denon Amp said: $Data"

	local Zone Parm

	case "$Data" in
		SI*) ZM_CurrentInput="${Data:2}" ;;
		MUOFF) ZM_Mute=NoMute ;;
		MUON) ZM_Mute=Mute ;;
		MV[0-9][0-9])
			ZM_VolumeLevel="${Data:2}"
			if [[ "$ZM_VolumeLevel" == 0* ]]; then
				ZM_VolumeLevel="${ZM_VolumeLevel:1}"
			fi
		;;
		Z*)
			Zone="${Data:1:1}"
			Parm="${Data:2}"
			case "$Parm" in
				MUOFF) eval "Z${Zone}_Mute=NoMute" ;; # not muted
				MUON) eval "Z${Zone}_Mute=Mute" ;; # muted
				OFF|ON) : ;; # on/off
				[0-9][0-9]) # volume
					if [[ "$Parm" == 0* ]]; then
						Parm="${Parm:1}"
					fi
					eval "Z${Zone}_VolumeLevel=$Parm"
				;;
				PHONO|CD|TUNER|DVD|TV|TV/CBL|VDR|DVR|V.AUX|XM|IPOD|AUX) eval "Z${Zone}_CurrentInput=$Parm" ;; # input
			esac
		;;
	esac
}

ReceivedCommandForChild

Equivalent to the DCE function with the same name. All commands that are received by a child of this device, which are forwarded to the parent, are processed in this function. For commands received by the device itself read below.

Parameters:

From="$1"
The number of the DCE device that sent the command
To="$2"
The number of the DCE device the command was sent to
Type="$3"
The DCE command type; see enum eMessageType in src/DCE/Message.h for possible values
Cmd="$4"
The DCE command value

In the example, this function handles volume, power and input select commands for amplifier subzones.

</nowiki>## Unknown command handlers

ReceivedCommandForChild() { local From="$1" To="$2" Type="$3" Cmd="$4" local Zone local VarZone

VarZone="Child_$To" if [[ -z "${!VarZone}" || "$Type" != 1 ]]; then ReplyToDCE "UNHANDLED" "" return fi

Zone="${!VarZone}" case "$Cmd" in 966) RouteMediaPath "$Zone" "$Parm_219" ;; 91) InputSelect "$Zone" "$Parm_71" ;; 97) Mute "$Zone" ;; 90) VolDown "$Zone" ;; 89) VolUp "$Zone" ;; 193) TurnOff "$Zone" ;; 192) TurnOn "$Zone" ;; esac

ReplyToDCE "OK" "" }</nowiki>

ReceivedUnknownCommand

Equivalent to the DCE function with the same name. This function is called when a command sent to the device itself doesn't have handler function (see below).

In the example, this function doesn't do anything, but it is still required.

You would use this function for command types other than 1 (MESSAGETYPE_COMMAND in src/DCE/Message.h) - e.g. events (MESSAGETYPE_EVENT in src/DCE/Message.h)

ReceivedUnknownCommand()
{
	local From="$1" To="$2" Type="$3" Cmd="$4"
	ReplyToDCE "UNHANDLED" ""
}

Hooks

OnDeviceConnect

This function is called by the framework upon successful (re)connection to the device. Suggested use for this hook is to get updated status from the device (especially on re-connect, which could make the stored status and real status be out of sync).

In the example, the Denon amplifier is queried for its power status. The reply is processed asynchronously in ProcessDeviceStream (see above).

## Hooks
OnDeviceConnect()
{
	SendToDevice "PW?"
}

OnInit

This function is called by the framework on startup.

In this example, the state variables are initialized for the main zone and subzones (if they exist) and the amplifier is queried. Just looking at it, the queries should probably be moved from OnInit to OnDeviceConnect, based on the rationale above (see OnDeviceConnect above).

OnInit()
{
	## User configuration code
	##########################
	
	SendToDevice "SI?"
	#SendToDevice "MV40"
	SendToDevice "MV?"
	ZM_CurrentInput=
	ZM_Mute=Mute
	ZM_VolumeLevel=40

	VolumeStep=4

	local Q R Var
	local ZoneDevice
	Q="SELECT PK_Device FROM Device WHERE FK_Device_ControlledVia=$DevNo AND FK_DeviceTemplate=$DEVICETEMPLATE_Denon_AVR_Zone"
	R=$(RunSQL "$Q")

	for ZoneDevice in $R; do
		Q="SELECT IK_DeviceData FROM Device_DeviceData WHERE FK_Device=$ZoneDevice AND FK_DeviceData=$DEVICEDATA_Port"
		R=$(RunSQL "$Q")
		if [[ -z "$R" ]]; then
			continue
		fi
		eval "Child_$ZoneDevice=$R"
		eval "Z${R}_VolumeLevel=40"
		#SendToDevice "Z${R}40"
		SendToDevice "Z${R}?"
	done
}

OnExit

This function is called by the framework when the device is about to exit.

In the example, this function does nothing.

Also notice the double colon character, which in Bash is a built-in "no op" command (traditionally /bin/true would be called, but that's an external command, which means overhead). Bash cannot have empty content between curly brackets, and this function is mandatory to exist. That is why this workaround is used.

OnExit()
{
	:
}

Command functions

These functions handle DCE commands of type 1 (MESSAGETYPE_COMMAND in src/DCE/Message.h).

Their names have the following format:

Cmd_CommandId()

where CommandId is the ID of the DCE command the function will handle.

Parameters:

From="$1"
The number of the DCE device that sent the command
To="$2"
The number of the DCE device the command was sent to

Command parameters:

Parm_ParameterId
The command parameters come in environment variables named as above. They aren't function parameters.
## Command functions
# Parameters come as environment variables of this form: Parm_<Number>

In the example, the following commands are handled:

  • Route Media Path (CommandId=966)
Parameters:
  • MediaPath (ParameterId=219)

This command is specific to Dianemo. It activates a specified MediaPath. Similar to Media Pipes.

## Route media path
Cmd_966()
{
	local From="$1" To="$2"
	local MediaPath

	MediaPath="$Parm_219"
	RouteMediaPath M "$MediaPath"
	ReplyToDCE "OK" ""
}
  • Input Select (CommandId=91)
Parameters:
  • Input (ParameterId=71)

Selects an input on the amplifier's main zone.

## Input Select
Cmd_91()
{
	local From="$1" To="$2"
	local Input="$Parm_71"
	InputSelect M "$Input"
	ReplyToDCE "OK" ""
}
  • Mute (CommandId=97)
Parameters: none

Toggles mute on the main zone of the amplifier.

## Mute
Cmd_97()
{
	local From="$1" To="$2"
	Mute M
	ReplyToDCE "OK"
}
  • Vol Down (CommandId=90) and Vol Up (CommandId=89)
Parameters: none

Turn volume up or down on the main zone of the amplifier.

## Vol Down
Cmd_90()
{
	local From="$1" To="$2"
	VolDown M
	ReplyToDCE "OK"
}

## Vol Up
Cmd_89()
{
	local From="$1" To="$2"
	VolUp M
	ReplyToDCE "OK"
}
  • On (CommandId=192) and Off (CommandId=193)
Parameters: none

Turn the main zone on and off.

## Off
Cmd_193()
{
	local From="$1" To="$2"
	TurnOff M
	ReplyToDCE "OK"
}

## On
Cmd_192()
{
	local From="$1" To="$2"
	TurnOn M
	ReplyToDCE "OK"
}
  • One Thousand! (CommandId=1000)
Parameters:
  • 999 (ParameterId=999)

This command doesn't really exist. It's a debug "feature" that allows sending arbitrary commands to the device using MessageSend without shutting down the DCE device.

Usage example:

/usr/pluto/bin/MessageSend dcerouter 0 99 1 1000 999 'SI?'

In the above example, 99 is a Denon amplifier in the DCE installation, and SI? happens to be the query string for the current input. Very useful when you don't know what an input is called, because it was renamed on the LCD, but you can identify it visually using the knobs on the front and eyeballs on the TV and/or ears on speakers, while not disturbing the DCE device itself.

Cmd_1000()
{
	Log "One thousand!! $Parm_999"
	SendToDevice "$Parm_999"
}

User functions

You should write all your support code under here: functions that you'll call from the hooks or command handlers above.

In the example, these functions handle volume, mute, and power commands for both the main zone and subzones, depending on the parameters they receive. They are used both in ReceivedCommandForChild (which handles subzone child devices) and the Cmd_CommandId functions (which handle the main zone).

## User functions

VolUp()
{
	local Zone="$1"
	local VarVolumeLevel VolumeCmd

	if [[ -z "$Zone" ]]; then
		return
	fi
	
	Log "Volume up zone $Zone"
	if [[ "$Zone" == M ]]; then
		VolumeCmd=MV
	else
		VolumeCmd=Z2
	fi
	VarVolumeLevel="Z${Zone}_VolumeLevel"

	((${VarVolumeLevel} += $VolumeStep))
	if ((${!VarVolumeLevel} > 98)); then
		eval "$VarVolumeLevel=98"
	fi
	SendToDevice "$VolumeCmd$(builtin printf "%02d" "${!VarVolumeLevel}")"
}

VolDown()
{
	local Zone="$1"
	local VarVolumeLevel VolumeCmd

	if [[ -z "$Zone" ]]; then
		return
	fi
	
	Log "Volume down zone $Zone"
	if [[ "$Zone" == M ]]; then
		VolumeCmd=MV
	else
		VolumeCmd=Z2
	fi
	VarVolumeLevel="Z${Zone}_VolumeLevel"

	((${VarVolumeLevel} -= $VolumeStep))
	if ((${!VarVolumeLevel} < 0)); then
		eval "${VarVolumeLevel}=0"
	fi
	SendToDevice "$VolumeCmd$(builtin printf "%02d" "${!VarVolumeLevel}")"
}

Mute()
{
	local Zone="$1"
	local VarMute MuteCmd

	if [[ -z "$Zone" ]]; then
		return
	fi

	if [[ "$Zone" == M ]]; then
		MuteCmd="MU"
	else
		MuteCmd="Z${Zone}MU"
	fi

	VarMute="Z${Zone}_Mute"
	if [[ "${!VarMute}" == Mute ]]; then
		eval "$VarMute=NoMute"
		SendToDevice "${MuteCmd}OFF"
		Log "Unmute zone $Zone"
	else
		eval "$VarMute=Mute"
		SendToDevice "${MuteCmd}ON"
		Log "Mute zone $Zone"
	fi
}

RouteMediaPath()
{
	local Zone="$1" MediaPath="$2"
	local Path Type Input

	if [[ -z "$Zone" ]]; then
		return
	fi

	Log "Route media path zone $Zone: MediaPath=$MediaPath"
	MediaPath="${MediaPath//:/ }"

	for Path in $MediaPath; do
		Type="${Path%_*}"
		Input="${Path#*_}"
		if [[ "$Type" != IN ]]; then
			continue
		fi
		InputSelect "$Zone" "$Input"
	done
}

InputSelect()
{
	local Zone="$1" Input="$2"
	local VarCurrentInput InputCmd

	if [[ -z "$Zone" ]]; then
		return
	fi

	Log "Input Select for zone $Zone: $Input"
	if [[ "$Zone" == M ]]; then
		InputCmd="SI"
	else
		InputCmd="Z${Zone}"
	fi
	VarCurrentInput="Z${Zone}_CurrentInput"

	Input=$(TranslateInput "$Input")
	if [[ "${!VarCurrentInput}" != "$Input" ]]; then
		SendToDevice "$InputCmd$Input"
		eval "$VarCurrentInput=$Input"
	fi
}

TurnOff()
{
	local Zone="$1"
	if [[ -z "$Zone" ]]; then
		return
	fi
	Log "Turn off zone $Zone"
	SendToDevice "Z${Zone}OFF"
}

TurnOn()
{
	local Zone="$1"
	if [[ -z "$Zone" ]]; then
		return
	fi
	Log "Turn on zone $Zone"
	SendToDevice "Z${Zone}ON"
}

TranslateInput()
{
	local Input="$1"
	local input_name

	if [[ -z "$Input" ]]; then
		return
	fi

	case "$Input" in
		162) # CD
			input_name="CD"
		;;
		164) # Aux
			input_name="AUX"
		;;
		165) # DVD
			input_name="DVD"
		;;
		714) # DVR
			input_name="DVR"
		;;
		163) # Phono
			input_name="Phono"
		;;
		166) # Tuner
			input_name="Tuner"
		;;
		161) # TV
			input_name="TV"
		;;
		285) # V.AUX
			input_name="V.AUX"
		;;
		282) # VCR
			input_name="VCR"
		;;
		909) # XM
			input_name="XM"
		;;
		*) input_name="$Input" ;;
	esac
	builtin echo "$input_name"
}

Start everything up

After everything is defined, DCE-connect.sh, which does all the magic of calling your handlers, needs to be called. It also starts a main program loop, which is why you only define handlers.

## Start eveything up
. /usr/pluto/bin/DCE-connect.sh "$@"

Framework functions

In the example script, you saw these already. Time to explain what they do and when they are required.

DCE communication functions

SendToDCE

Send a DCE command.

Parameters:

Cmd="$1"
DCE command structured like the parameters passed to MessageSend
All command parameters values must be URLencoded if they contain whitespace or other special characters.
See BuildCommand below.

Note: the -r and -o options aren't supported for sent messages.

ReplyToDCE

Send a reply after handling a DCE message.

Parameters:

ReplyString="$1"
Result of the message

choices:

  • OK
message was handled sucessfully
  • UNHANDLED
message wasn't handled at all
OutVars="$2"
Response parameters to send to a DCE message, which will be received by a command sent with the -o option.

The following commands must call this function before returning:

  • ReceivedCommandForChild
  • ReceivedUnknownCommand
  • Cmd_CommandId

None others should call this function.

Device communication functions

ReadFromDevice

Read data from the device. The returned data is encoded as per the DeviceProtocol_Encoding setting set in the Configure function. The data is sent to STDOUT, so you'll need to capture it as in the usage example below.

The Whisperer is most useful when used asynchronously, according to the following pattern send command/query to the device, then update the internal status when the device replies later; postpone action until device replies. This means you shouldn't use this function unless this pattern is useless to you. For example the VDR script in Dianemo has to use this, but the protocol is line-based, so it doesn't mess anything up. If your protocol is stream based, you should make sure you add any extra bytes back to the buffer you're processing in ProcessDeviceStream.

Usage example:

Data=$(ReadFromDevice)

SendToDevice

Send data to the device

Parameters:

Data="$1"
Data to be sent to the device. Data must be encoded as per the DeviceProtocol_Encoding setting set in the Configure function.

DCE Message utility functions

ParmEncode

URLencode a string to be used in conjunction with the enc encoding.

Output is sent to STDOUT, so you much capture the output as in the usage example below.

Parameters:

Data="$1"
Data to be encoded

Usage example:

EncodedString=$(ParmEncode "$String")

ParmDecode

URLdecode a string

Output is sent to STDOUT, so you much capture the output as in the usage example below.

Parameters:

Data="$1"
Data to be decoded

Usage example:

String=$(ParmDecode "$EncodedString")

BuildCommand

Build a string that can be passed to SendToDCE as a parameter.

All the bits of the command to be assembled must be passed as individual parameters.

Usage example:

AssembledCommand=$(BuildCommand $DevNo $DestDevice 1 1000 999 "string with spaces" 998 "another string with spaces")

In the example above, all the command bits are given, including the From, To, CommandType, and CommandId, but you can build a command like this too:

AssembledCommand="$DevNo $DestDevice 1 1000"$(BuildCommand 999 "string with spaces" 998 "another string with spaces")

but it isn't as readable.

Utility functions

Log

Write a log message. All the parameters will be written to STDERR and to the log file (if one has been specified with the -l parameter when the device started).

CAVEAT

echo and printf

The echo and printf functions are taken over and redirected to STDERR to prevent STDOUT polution. STDOUT is used by some obscure DCE bits, and writing random data to it from a Whisperer scripts will confuse the DCE bridge and crash the device.

If you need to use echo and printf in constructs like:

Var=$(echo "$Stuff"|cut -d: -f1)
Var=$(printf "%s:%s" "value1" "value2")

Then you must adapt the calls like this:

Var=$(builtin echo "$Stuff"|cut -d: -f1)
Var=$(builtin printf "%s:%s" "value1" "value2")

If you don't, the captured output will be an empty string because the result will be sent to STDERR instead (and you'll see it on the screen).

How to write a DCE Whisperer Bash Device Template

Create the Device Template

  • Create a regular GSD device template in Web admin.
  • The Command line contains a call to Generic_Serial_Devices for a regular GSD device. For the Whisperer based template, you put the filename of the bash script.
  • The device template bash script looks very similar to a ruby device template. The easiest way to start it to edit an existing DCE Whisperer script, like DenonAmp - also shown above. (Get the DenonAmp script.)

How to use a DCE Whisperer Bash Device Template

A DCE device based on the whisperer works the same way any other C++ DCE device.