DCE Whisperer

From LinuxMCE
Jump to: navigation, search

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
    • Bticino

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.