Difference between revisions of "DCE Whisperer"
m (→Create the Device Template) |
(→Who uses it) |
||
(2 intermediate revisions by the same user not shown) | |||
Line 1: | Line 1: | ||
− | = | + | [[Category: Programmer's Guide]] |
+ | = 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. -[[User:Uplink|Uplink]] 19:29, 6 September 2011 (CEST) | |
− | A | + | |
− | = | + | = Who uses it = |
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | Right now LinuxMCE only has some example scripts and it is not using the Whisperer framework actively. | |
− | The | + | 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: | ||
* [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. |
Latest revision as of 12:23, 27 September 2011
Contents
- 1 Description
- 2 Who uses it
- 3 Dependencies
- 4 Example script analysis
- 4.1 What it looks like
- 4.2 Script breakdown
- 4.3 Framework functions
- 5 How to write a DCE Whisperer Bash Device Template
- 6 How to use a DCE Whisperer Bash Device Template
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:
- DCE Whisperer package
- 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/
#!/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.