-- -- CONFIDENTIAL AND PROPRIETARY. -- © 2007 NetStreams, Inc. -- This software code contains proprietary trade secrets of NetStreams and is also -- protected by U.S. and other copyright laws and applicable international -- treaties. Any use, compilation, modification, distribution, reproduction, -- performance, display, or disclosure (“Use”) of this software code is subject to -- the terms and conditions of your written agreement with NetStreams. If you do -- not have such an agreement, then any Use of this material is strictly -- prohibited. Unauthorized Use of this software code, or any portion of it, will -- result in civil liability and/or criminal penalties. By modifying this software -- code, you agree to assign any and all intellectual property rights related in -- any way to your modification to NetStreams pursuant to your written agreement -- with NetStreams. -- -------------------------------------------------------------------------------- -- -- $Archive: /scripts/MultiFunction/Denon/Denon_AVR-3311CI.lua $ -- $Revision: 2 $ -- -------------------------------------------------------------------------------- -- -- Driver for Denon AVR-3311CI AV Surround Receiver -- Seema V. Nikam -- --[[ --]] --------------------------------------------------------------------------------------------- -- various debugging flags --------------------------------------------------------------------------------------------- setDebug('error', 'on') setDebug('warning', 'on') --setDebug('all', 'on') --setDebug('state', 'on') --setDebug('stream', 'on') --setDebug('i/o', 'on') setDebug('verbose', 'on') setDebug('status', 'on') setDebug('commands', 'on') --setDebug('save', 'on') --setDebug('scan', 'on') local NSStatus = require('NSStatus') -- for debugging set this to greatly shorten the channel scan -- time at the expense of not having all channels in the menus --xmMaxScanChannels = 20 -- this table must be kept in sync with the information above -- and maps #INPUT parameters to @SRC parameters. Although the -- AVR considers the AM and FM tuners to be separate sources -- we want them to appear as a single source. SourceMap = { 'TV', 'DVD', 'DVR', 'SAT/CBL', 'BD', 'CD', 'PHONO', -- Instead of {'TUNER','XM'}: in Denon_AVR 3311 we have: {'HDRADIO','SIRIUS'}, -- Following sources can only be controlled and selected through the -- AVR itself, not through the netstreams system. -- The '?' as the first key means that #INPUT 9, will not change the current -- selection -- { '?', 'V.AUX', 'IPOD', 'NET/USB','GAME','DOCK', 'RHAPSODY','NAPSTER','PANDORA','FLICKR'}, Index2Key = function(self, index) entry = self[tonumber(index)] if not entry then debug('error', 'Index2Key: no mapping for '..prettyprint(index)) return nil elseif type(entry) == 'table' then return entry[1] elseif type(entry) ~= 'function' then return entry else debug('error', 'Index2Key: function mapping for '..prettyprint(index)) return nil end end, Key2Index = function(self, key) for index, entry in ipairs(self) do if type(entry) == 'table' then for _, e in ipairs(entry) do if e == key then return index end end elseif type(entry) ~= 'function' then if entry == key then return index end end end debug('error', 'Key2Index: no mapping for '..prettyprint(key)) return nil end, } -- -- GUI: as it is taken from Denon_AVR-3808CI.lua -- added on 12/4 (Dan McGauley) -- -- Seema V. Nikam -- Need to ask question about this: Answer: Refer to Line Number: 2469 -- XM and Tuner is not supported for this devide, so we probably don't need XM artwork -- but we may need SIRIUS artworkNameLookup -- --[[ artworkNameLookup = { [4] = "XM/40s_on_4.swf", [5] = "XM/50s_on_5.swf", [6] = "XM/60s_on_6.swf", [7] = "XM/70s_on_7.swf", [8] = "XM/80s_on_8.swf", [9] = "XM/90s_on_9.swf", [10] = "XM/The_Roadhouse.swf", [11] = "XM/Nashville!_cm.swf", [12] = "XM/Outlaw_Country.swf", [13] = "XM/Willie's_Place.swf", [14] = "XM/Bluegrass_Juncti.swf", [15] = "XM/Folk_Village.swf", [16] = "XM/The_Highway.swf", [17] = "XM/Prime_Country.swf", [18] = "XM/Elvis_Radio.swf", [20] = "XM/XM_20_on_20.swf", [21] = "XM/KISS-XM_cm.swf", [22] = "XM/MIX_22_cm.swf", [23] = "XM/Love.swf", [24] = "XM/Pink_cm.swf", [25] = "XM/The_Blend.swf", [26] = "XM/The_Pulse.swf", [28] = "XM/Escape.swf", [29] = "XM/BBC_Radio_1.swf", [30] = "XM/Pop2K.swf", [32] = "XM/The_Message.swf", [33] = "XM/Praise.swf", [34] = "XM/enLighten.swf", [35] = "XM/Holly.swf", [36] = "XM/HolidayTradition.swf", [37] = "XM/Holiday_Pops.swf", [39] = "XM/Led_Zeppelin.swf", [40] = "XM/Deep_Tracks.swf", [41] = "XM/Hair_Nation.swf", [42] = "XM/Liquid_Metal.swf", [43] = "XM/SIRIXM_U.swf", [44] = "XM/1st_Wave.swf", [45] = "XM/The_Spectrum.swf", [46] = "XM/Classic_Vinyl.swf", [47] = "XM/Alt_Nation.swf", [48] = "XM/Octane.swf", [49] = "XM/Classic_Rewind.swf", [50] = "XM/The_Loft.swf", [51] = "XM/The_Coffee_House.swf", [52] = "XM/Faction.swf", [53] = "XM/AC_DC_Radio.swf", [54] = "XM/Lithium.swf", [55] = "XM/Margaritaville.swf", [56] = "XM/Jam_ON.swf", [57] = "XM/Grateful_Dead.swf", [58] = "XM/E_Street_Radio.swf", [59] = "XM/Undergrnd_Garage.swf", [60] = "XM/Soul_Town.swf", [62] = "XM/Heart_&_Soul.swf", [64] = "XM/The_Groove.swf", [66] = "XM/Shade_45.swf", [67] = "XM/Hip-HopNation.swf", [68] = "XM/The_Heat.swf", [70] = "XM/Real_Jazz.swf", [71] = "XM/Watercolors.swf", [72] = "XM/Spa.swf", [73] = "XM/SIRIUSLY_Sinatra.swf", [74] = "XM/Bluesville.swf", [76] = "XM/Cinemagic.swf", [75] = "XM/On_Broadway.swf", [77] = "XM/Holiday_Pops.swf", [78] = "XM/Symphony_Hall.swf", [79] = "XM/Met_Opera_Radio.swf", [80] = "XM/Area.swf", [81] = "XM/BPM.swf", [84] = "XM/Chill.swf", [85] = "XM/Caliente_sp.swf", [86] = "XM/The_Joint.swf", [87] = "XM/The_Verge.swf", [88] = "XM/Air_Musique_fr.swf", [89] = "XM/Sur_La_Route_fr.swf", [95] = "XM/XM_Scoreboard.swf", [96] = "XM/Canada_360.swf", [97] = "XM/Cal_Sportif_fr.swf", [115] = "XM/Radio_Disney.swf", [116] = "XM/Kids_Place_Live.swf", [117] = "XM/Catholic_Channel.swf", [119] = "XM/Doctor_Radio.swf", [120] = "XM/Specials.swf", [121] = "XM/Fox_News_Channel.swf", [122] = "XM/CNN.swf", [123] = "XM/CNN_HDLN.swf", [125] = "XM/Quoi_De_Neuf_fr.swf", [126] = "XM/CNN_espanol_sp.swf", [127] = "XM/CNBC.swf", [129] = "XM/Bloomberg_Radio.swf", [130] = "XM/POTUS_Politics.swf", [131] = "XM/BBC_World_Svc.swf", [132] = "XM/C-SPAN_Radio.swf", [133] = "XM/XM_Public_Radio.swf", [134] = "XM/NPR_Now.swf", [135] = "XM/World_Radio_Net.swf", [136] = "XM/Fox_Business.swf", [140] = "XM/ESPN_Radio.swf", [141] = "XM/ESPN_Xtra.swf", [142] = "XM/Fox_Sports_Radio.swf", [144] = "XM/Mad_Dog_Radio.swf", [145] = "XM/IndyCar_Racing.swf", [146] = "XM/PGA_TOUR_Network.swf", [147] = "XM/XM_Deportivo_sp.swf", [148] = "XM/BlueCollarRad.swf", [149] = "XM/The_Foxxhole.swf", [150] = "XM/RawDog_Comedy.swf", [151] = "XM/Laugh_USA.swf", [152] = "XM/Extreme_Talk.swf", [153] = "XM/Laugh_Attack.swf", [154] = "XM/National_Lampoon.swf", [155] = "XM/SIRIUSXMStars.swf", [156] = "XM/Oprah_&_Friends.swf", [158] = "XM/America's_Talk.swf", [159] = "XM/ATN_Radio.swf", [160] = "XM/ReachMD.swf", [161] = "XM/Rock@Random_cm.swf", [162] = "XM/Cosmo_Radio.swf", [163] = "XM/Book_Radio.swf", [164] = "XM/RadioClassics.swf", [165] = "XM/Talk_Radio.swf", [166] = "XM/America_Right.swf", [167] = "XM/America_Left.swf", [168] = "XM/Fox_News_Talk.swf", [169] = "XM/The_Power.swf", [170] = "XM/FAMILYTALK.swf", [171] = "XM/Open_Road.swf", [172] = "XM/RadioParallelefr.swf", [173] = "XM/WLW_700.swf", [174] = "XM/MLB_Espanol_sp.swf", [175] = "XM/MLB_Home_Plate.swf", [176] = "XM/MLB_Play-by-Play.swf", [177] = "XM/MLB_Play-by-Play.swf", [178] = "XM/MLB_Play-by-Play.swf", [179] = "XM/MLB_Play-by-Play.swf", [180] = "XM/MLB_Play-by-Play.swf", [181] = "XM/MLB_Play-by-Play.swf", [182] = "XM/MLB_Play-by-Play.swf", [183] = "XM/MLB_Play-by-Play.swf", [184] = "XM/MLB_Play-by-Play.swf", [185] = "XM/MLB_Play-by-Play.swf", [186] = "XM/MLB_Play-by-Play.swf", [187] = "XM/MLB_Play-by-Play.swf", [188] = "XM/MLB_Play-by-Play.swf", [189] = "XM/MLB_Play-by-Play.swf", [190] = "XM/ACC.swf", [191] = "XM/ACC.swf", [192] = "XM/ACC.swf", [193] = "XM/Pac-10.swf", [194] = "XM/Pac-10.swf", [195] = "XM/Pac-10.swf", [196] = "XM/Big_Ten.swf", [197] = "XM/Big_Ten.swf", [198] = "XM/Big_Ten.swf", [199] = "XM/SEC.swf", [200] = "XM/SEC.swf", [201] = "XM/SEC.swf", [202] = "XM/The_Virus.swf", [203] = "XM/Big_East.swf", [204] = "XM/NHL_Home_Ice.swf", [205] = "XM/NHL_Play-by-P.swf", [206] = "XM/NHL_Play-by-P.swf", [207] = "XM/NHL_Play-by-P.swf", [208] = "XM/NHL_Play-by-P.swf", [209] = "XM/NHL_Play-by-P.swf", [210] = "XM/Boston.swf", [211] = "XM/New_York_City.swf", [212] = "XM/Philadelphia.swf", [213] = "XM/Baltimore.swf", [214] = "XM/Washington_DC.swf", [215] = "XM/Pittsburgh.swf", [216] = "XM/Detroit.swf", [217] = "XM/Chicago.swf", [218] = "XM/St_Louis.swf", [219] = "XM/Minneapolis.swf", [220] = "XM/Seattle.swf", [221] = "XM/San_Francisco.swf", [222] = "XM/Los_Angeles.swf", [223] = "XM/San_Diego.swf", [224] = "XM/Phoenix.swf", [225] = "XM/Dallas_Ft_Worth.swf", [226] = "XM/Houston.swf", [227] = "XM/Atlanta.swf", [228] = "XM/Tampa.swf", [229] = "XM/Orlando.swf", [230] = "XM/Miami.swf", [231] = "XM/Big_12.swf", [232] = "XM/NBA_Play-By-Play.swf", [233] = "XM/NBA_Play-By-Play.swf", [234] = "XM/NBA_Play-By-Play.swf", [235] = "XM/NBA_Play-By-Play.swf", [236] = "XM/NBA_Play-By-Play.swf", [237] = "XM/Sports_Play-by-P.swf", [238] = "XM/Sports_Play-by-P.swf", [239] = "XM/Sports_Play-by-P.swf", [241] = "XM/Sports_Play-by-P.swf", [242] = "XM/Sports_Play-by-P.swf", [243] = "XM/Sports_Play-by-P.swf", [244] = "XM/Sports_Play-by-P.swf", [245] = "XM/Sports_Play-by-P.swf", [246] = "XM/Sports_Play-by-P.swf", [247] = "XM/Emergency_Alert.swf" } ]]-- SurroundMode = { { key='DIRECT', label='Direct' }, { key='PURE DIRECT', label='Pure Direct' }, { key='STEREO', label='Stereo' }, { key='STANDARD', label='Standard' }, { key='DOLBY DIGITAL', label='Dolby Digital' }, { key='DTS SURROUND', label='DTS Surround' }, { key='7CH STEREO', label='7 Ch. Stereo' }, { key='WIDE SCREEN', label='Wide Screen' }, { key='NEURAL', label='Neural' }, { key='SUPER STADIUM', label='Super Stadium' }, { key='ROCK ARENA', label='Rock Arena' }, { key='JAZZ CLUB', label='Jazz Club' }, { key='CLASSIC CONCERT', label='Classic Concert' }, { key='MONO MOVIE', label='Mono. Movie' }, { key='MATRIX', label='Matrix' }, { key='VIDEO GAME', label='Video Game' }, { key='VIRTUAL', label='Virtual' }, -- { key='MPEG AAC', label='MPEG AAC' }, -- removed since it's Japan only { key='QUICK1', label='Quick 1' }, { key='QUICK2', label='Quick 2' }, { key='QUICK3', label='Quick 3' }, Index2Key = function(self, index) local entry = self[tonumber(index)] if entry == nil then return nil end return entry.key end, Index2Label = function(self, index) local entry = self[tonumber(index)] if entry == nil then return nil end return entry.label end, Key2Index = function(self, key) for index, entry in ipairs(self) do if entry.key == key then return index end end return nil end, Key2Label = function(self, key) local index = self:Key2Index(key) if index == nil then return nil end local entry = self[tonumber(index)] if entry == nil then return nil end return entry.label end, } -- -- set up default configuration parameters -- if not config then config = {} end if not config.port then config.port = 'comm://default' end if not config.func then debug('error', 'No Module Function Specified') end -- -- basic_set(self, param) -- Seema V. Nikam -- 06/01/2011 -- -- basic "Set" routine for most (all?) of the commands in the command -- table -- function basic_set(self, param) if self.Encode then param = self:Encode(param) end return stream:QueueCommand(self.Command, param, self.SetTimeout) end -- -- basic_query(self, param) -- Seema V. Nikam -- 06/01/2011 -- -- basic "Query" routine for most (all?) of the commands in the command -- table. This sends a request to the device for the current state -- of a command value -- function basic_query(self) return stream:QueueCommand(self.Command, Commands.Query, self.QueryTimeout) end -- -- encode_toggle(self, param) -- Seema V. Nikam -- 06/01/2011 -- -- an Encode function to be used by parameters that can toggle between -- the On and Off states -- function encode_toggle(self, param) if param == nil or param == "" then param = "TOGGLE" else param = param:upper() end if param == "TOGGLE" then if self.Value == self.Param.On then param = self.Param.Off else param = self.Param.On end elseif param == "ON" then param = self.Param.On elseif param == "OFF" then param = self.Param.Off end return param end ------------------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------------------ -- Commands Table, common to all device types -- ------------------------------------------------------------------------------------------------------------------ -- Each entry in the Commands table must contain the following entries: -- -- Command The command prefix to be used when send or requesting the -- -- command value -- -- Set The method used to set the value in the device (usually -- -- basic_set -- -- Query The method used to request the value from the device -- -- usually basic_query) -- ------------------------------------------------------------------------------------------------------------------ -- In addition, each entry in the Commands table may contain the following -- -- entries: -- -- Param A table of useful values to be used in the Set routine -- -- or that will be set in the Value entry -- -- Decode A method used to translate the response from the device -- -- into the entries in Param. If Decode is not specified -- -- the Value saved will be the same as returned from the -- -- device. -- -- Encode A method used to translate the entries in Param into a -- -- value to be sent to the device. If Encode is not specified -- -- the value passed to Query will be sent to the device -- -- unmolested. -- ------------------------------------------------------------------------------------------------------------------ -- -- Commands Table: -- Commands = { Query = '?', -- Select Input Source Source = { Command = 'SI', SetTimeout = 1500, Query = basic_query, Param = { TV = SourceMap:Key2Index('TV'), DVD = SourceMap:Key2Index('DVD'), DVR = SourceMap:Key2Index('DVR'), Sat = SourceMap:Key2Index('SAT/CBL'), BD = SourceMap:Key2Index('BD'), CD = SourceMap:Key2Index('CD'), Tuner = SourceMap:Key2Index('HDRADIO'), ST = 'SIRIUS', AMFM = 'HDRADIO', }, Set = function(self, param) basic_set(self, param) -- If we're changing the source between AM/FM and SIRIUS (XM) then we won't get a channel update -- (the channel hasn't actually changed, so it doesn't get sent) -- so explicitly query for one -- debug("verbose","Line:548: Command SI: Set ",param) if param == self.Param.ST then stream:QueueCommand('TFST', Commands.Query) elseif param == self.Param.AMFM then stream:QueueCommand('TFHD', Commands.Query) end end, -- -- Decode(self, param) -- Seema V. Nikam -- 06/07/2011 -- -- find the source label returned in the command and turn it into -- an index which is more useful for the DC -- Decode = function(self, param) self.RawValue = param -- We also save the raw source value -- so we know which tuner is in use return SourceMap:Key2Index(param) end, -- -- Encode(self, param) -- Seema V. Nikam -- map the source index into a source label to be used by the device -- Encode = function(self, param) if param == self.Param.ST or param == self.Param.AMFM then return param elseif param == self.Param.Tuner then debug("verbose","Line:578: In SI Encode: Band value is",Commands.Band.Value) debug("verbose","Line:579: next statement is checking above value with ST") if Commands.Band.Value == 'ST' then return self.Param.ST else return self.Param.AMFM end else return SourceMap:Index2Key(param) end end, }, Band = { Command = 'TMHD', Set = function(self, param) param = self:Encode(param) -- A little bit different on the set function since setting -- the band may require first switching the source debug("verbose","Line:597: In band=Command is TMHD: param is: ",param) if param == self.Param.ST then -- To select the SIRIUS band, just select the SIRIUS source if -- necessary Commands.Source:Set(Commands.Source.Param.ST) else -- To select a non-xm band, first make sure the AM/FM -- tuner is the current real source Commands.Source:Set(Commands.Source.Param.AMFM) -- And then use the normal set logic stream:QueueCommand(self.Command, param) end end, Update = function(self, new) if self.Value ~= new then local old = self.Value debug('verbose', 'Band = '..prettyprint(new)) self.Value = new State:Event('UpdateBand', new, old) State:Event('Update', 'Band', new, old) else debug('verbose', 'Line:618: Band = '..prettyprint(new)..' (unchanged)') end end, Query = basic_query, Param = { AM = 'AM', FM = 'FM', ST = 'ST', Auto = 'AUTO', Manual = 'MANUAL', Next = 'NEXT', Prev = 'PREV', }, Next = { AM = 'FM', FM = 'ST', ST = 'AM', }, Prev = { AM = 'ST', FM = 'AM', ST = 'FM', }, -- -- Decode -- Seema V. Nikam -- 06/01/2011 -- -- Decode the return value. The AUTO and MANUAL return values -- aren't really frequency settings, so filter them out and -- save the last known mode away for future reference -- Decode = function(self, param) if param == self.Param.Auto or param == self.Param.Manual then self.Mode = param param = self.Value elseif Commands.Source.RawValue == 'ST' then -- If the source is SIRIUS, then the band is always ST param = self.Param.ST else self.RawValue = param end return param end, -- -- Encode -- Seema V. Nikam -- 06/01/2011 -- -- Encode the command parameter, we really only need to worry about Next -- and Prev -- Encode = function(self, param) if param == self.Param.Next then return self.Next[self.Value] elseif param == self.Param.Prev then return self.Prev[self.Value] else return param end end, }, Power = { Command = 'PW', SetTimeout = 5000, Set = basic_set, Query = basic_query, Param = { On = 'ON', Off = 'STANDBY', }, Encode = encode_toggle, }, Mute = { Command = 'MU', Set = basic_set, Query = basic_query, Param = { On = 'ON', Off = 'OFF', }, Encode = encode_toggle, }, } ------------------------------------------------------------------------------ -- -- -- States, common to all device types -- -- -- ------------------------------------------------------------------------------ -- -- event_power(self, new, old) -- Seema V. Nikam -- 06/01/2011 -- -- Common UpdatePower event function to be used by all states -- event_power = function(self, new, old) debug('verbose', 'Power Event: '..new) if new == Commands.Power.Param.On then self.Machine:Set('Power On') else self.Machine:Set('Power Off') end end -- -- event_close(self, new, old) -- Seema V. Nikam -- 06/01/2011 -- -- Common UpdateClose event function to be used by all states. -- Actually, we take advantage of the putting a string into -- the handler as a shortcut for self.Machine:Set(state) -- event_close = 'Closed' -- -- In the Closed state the stream is not yet opened, or has -- been closed -- StateClosed = { Open = 'Open', } -- -- In the Open state, the stream is open, but we have no contact with -- the device or have lost contact with the device -- StateOpen = { -- -- Enter(self, from) -- Seema V. Nikam -- 06/01/2011 -- -- While we're in the open state, every 15 seconds we want to ping -- the device to see if it's been plugged in yet. To avoid power -- on spam when the StreamNet device is first powered up, we only -- issue an immediate power query on the main MUX device. -- Enter = function(self, from) if config.func == 'MUX' then self:StartTimer(5000) else self:StartTimer(15000) end end, -- -- Timer(self) -- Seema V. Nikam -- 06/01/2011 -- -- Event handler for the Timer event, as promised, when it fires -- we ping the device with a power query and restart the timer -- to try again in 15 seconds. -- Timer = function(self) Commands.Power:Query() if config.func == 'MUX' then self:StartTimer(15000) else self:StartTimer(20000) end end, UpdatePower = event_power, Close = event_close, } -- -- In the PowerOn state, the stream is open and the device is powered -- on and ready for use. -- StatePowerOn = { UpdatePower = event_power, Close = event_close, -- -- Enter(self, from) -- Seema V. Nikam -- 06/01/2011 -- Enter = function(self, from) -- When we enter the power on state, query each command we care -- about for it's value if we haven't already seen it if type(self.StartUpQueries) == 'table' then for _, entry in pairs(self.StartUpQueries) do if entry.Value == nil then entry:Query() end end end -- And if there's a status attribute specified, then update -- it with power on if self.Attribute ~= nil then status:setField(self.Attribute, '1') end end, } -- -- -- In the Power Off state, the stream is open and the device is present -- and communicative, but the power is turned off -- StatePowerOff = { -- -- Enter(self, from) -- Seema V. Nikam -- 06/01/2011 -- Enter = function(self, from) -- When we enter the power off state update any specified -- power attribute if StatePowerOn.Attribute ~= nil then status:setField(StatePowerOn.Attribute, '0') end end, -- -- UpdatePower(self, new, old) -- Seema V. Nikam -- 06/01/2011 -- -- When we receive a power on event, we set a timer for 4 seconds -- before we actually enter the PowerOn state to allow the device -- to stabilize. If we receive a Power Off during that time we -- cancel the pending state change. -- UpdatePower = function(self, new, old) if new == Commands.Power.Param.On then self:StartTimer(5000) else self:CancelTimer() end end, -- -- Timer() -- Seema V. Nikam -- 06/01/2011 -- -- If our 4 second timer elapses and the power is still on then -- move us into the Power On state. -- Timer = function(self) if Commands.Power.Value == Commands.Power.Param.On then self.Machine:Set('Power On') end end, Close = event_close, } ------------------------------------------------------------------------------ -- Create the common state machine ------------------------------------------------------------------------------ State = createStateMachine('State') State:Add('Closed', StateClosed) State:Add('Open', StateOpen) State:Add('Power On', StatePowerOn) State:Add('Power Off', StatePowerOff) State:Set('Closed') ------------------------------------------------------------------------------ -- -- -- Common Functions to all drivers -- -- -- ------------------------------------------------------------------------------ -- -- start() -- Seema V. Nikam -- 06/01/2011 -- -- common driver entry point, we just need to open the stream, the rest -- will happen once the stream is opened -- function start() stream = createProtocolStream(config.port) collectgarbage('collect') debug('verbose', 'Memory Usage: '..collectgarbage('count')) debug.enableWatchdog() end function stop() debug.disableWatchdog() end -- -- createProtocolStream(comm) -- Seema V. Nikam -- 06/01/2011 -- -- Open the stream specified as requested by the caller. This -- will also store the stream handle away for future usage and -- insure that the appropriate asynchronous callback is registered -- function createProtocolStream(comm) -- We use the port parameter from the stream, other than that -- We use hard-wired RS-232 settings local port if stream then port = comm:match('^comm://([^;]*)') end if not port then port = 'default' end comm = 'comm://'..port..';baud=9600;parity=none;bits=8;stop=1' -- create the real stream debug('stream', 'opening "'..comm..'"') stream = createStream(comm) -- -- stream:onOpen() -- Seema V. Nikam -- 06/01/2011 -- -- automatically called when the stream is successfully opened -- function stream:onOpen() debug('stream', 'opened') self.isOpened = true self.locked = false -- start the async input running self:startAsyncInput(self.onDataReady, {endString = '\r'}) -- timer that fires when a command times out with no response self.commandTimer = createTimer(nil, self.CommandTimeout) -- timer that fires to attempt to lock the stream and send the -- next command self.startTimer = createTimer(nil, function (timer) self:SendNextCommand() end ) State:Event('Open') end -- -- stream:onClose() -- Seema V. Nikam -- 06/01/2011 -- -- register the onClose function which will be invoked if the -- stream is ever closed (the only time this will happen is -- if the z8 crashes) function stream:onClose() debug('stream', 'closed') State:Event('Close', self) self.isOpened = false self.locked = false if self.powerCheckTimer then self.powerCheckTimer:cancel() self.powerCheckTimer = nil end if self.startTimer then self.startTimer:cancel() self.startTimer = nil end end -- -- stream:onDataReady(data) -- Seema V. Nikam -- 06/01/2011 -- -- register the async input function, this will get invoked -- whenver there's a \r terminated string available with the -- available data function stream:onDataReady(data) debug('i/o', '>'..data) -- strip the trailing space and return local cmd = data:match('^(.*[^ ]) *\r') local arg = nil if cmd == nil then debug('error', 'Unknown response: "'..data..'"') return end debug('i/o', 'Command="'..cmd..'"') -- find a command in the Commands table that prefixes -- this response/event and update it and generate -- an appropriate update event for name, info in pairs(Commands) do if type(info) ~= 'table' then elseif info.Command == nil then debug('error', 'Commands.'..name..'.Command = nil') elseif cmd:sub(1, info.Command:len()) == info.Command then arg = cmd:sub(info.Command:len() + 1) com = cmd:sub(1, info.Command:len()) debug('i/o', 'com="'..com..'" arg="'..arg..'"') local new = arg local old = info.Value if info.Decode then new = info:Decode(new) end if new ~= nil then info.Value = new State:Event('Update'..name, new, old) State:Event('Update', name, new, old) end end end -- attempt to find a matching command in the CommandQueue -- that we've now completed self:CompleteCommand(cmd) end -- -- stream:SendNextCommand() -- Seema V. Nikam -- 06/01/2011 -- -- send the next command in the queue. This will take care of -- locking and unlocking the stream as required. If the stream -- can not be locked, then a timer will be set to try again -- shortly -- function stream:SendNextCommand() local cmd = self.CommandQueue[1] if cmd then if not self.locked then if not self:lock(100) then debug('stream', 'lock failed') self.startTimer:queue(100) return else debug('stream', 'locked') self.locked = true end end local line = cmd.cmd..cmd.param debug('i/o', '<'..line) -- send the command self:write(line..'\r'); -- and restart the timer self.commandTimer:queue(cmd.timeout); else -- there are no more commands to send, so unlock the stream if self.locked then if not self:unlock() then debug('stream', 'unlock failed') self.startTimer:queue(100) else debug('stream', 'unlocked') self.locked = false end end -- and cancel the timer self.commandTimer:cancel() end end -- -- stream:QueueCommand(cmd, param, timeout, onComplete, retries) -- Seema V. Nikam -- 06/01/2011 -- -- queue a command to be sent, we have to put the command in a queue and -- deal with the response asynchronously because we're using async -- input, which can only happen when we're not in the driver already. -- stream.CommandQueue = {} function stream:QueueCommand(cmd, param, timeout, onComplete, retries) if type(cmd) ~= 'table' then cmd = { cmd=cmd, param=(param or ""), Complete=onComplete, retries=(retries or 3), timeout=(timeout or 500) } end -- put the command in the queue table.insert(self.CommandQueue, cmd) debug('stream', 'Queued = '..#self.CommandQueue..' '..cmd.cmd..' '..cmd.param..' to='..cmd.timeout) -- start the transmission process if this is the first command -- in the queue if #self.CommandQueue == 1 then self:SendNextCommand() end return command end -- -- stream:CompleteCommand() -- Seema V. Nikam -- 06/01/2011 -- -- check if this is a response for the last transmitted command and -- remove it from the queue if so. function stream:CompleteCommand(cmd) local last = self.CommandQueue[1] if not last then return end if cmd:sub(1, last.cmd:len()) == last.cmd then -- last command successfully completed debug('stream', 'Completed: '..last.cmd..':'..last.param) -- remove the command from the queue table.remove(self.CommandQueue, 1) -- let the caller know it completed if they care if last.Complete then last:Complete(cmd:sub(last.cmd:len() + 1)) end -- send the next command in the queue self:SendNextCommand() end end -- -- stream:ClearCommandQueue() -- Seema V. Nikam -- 06/01/2011 -- -- clear all the queued commands out of the queue, sending each -- an aborted message if it cares -- function stream:ClearCommandQueue() debug('stream', 'Clear Command Queue') -- notify everybody they've been aborted for _, cmd in pairs(self.CommandQueue) do if cmd.Complete then cmd:Complete(nil, 'abort') end end -- clear the command queue self.CommandQueue = {} -- and process it to kill timers and unlock as necessary self:SendNextCommand() end -- -- stream.CommandTimeout(timer) -- Seema V. Nikam -- 06/01/2011 -- -- deal with a command that has timed out by taking it out of the queue -- and calling the Timeout and DriverTimeout routines if defined -- function stream.CommandTimeout(timer) local last = stream.CommandQueue[1] if not last then return nil end debug('error', 'Timeout: '..last.cmd..':'..last.param) -- let the caller know it timed out if they care if last.Complete then last:Complete(nil, 'timeout') end -- remove the command from the queue table.remove(stream.CommandQueue, 1) -- Since not all commands have a response (they won't respond -- if nothing changes, we can't purge the queue when we don't -- get a response, instead we have to try to send the next -- command since ClearCommandQueue normally does it for us. -- clear the rest of the command queue --self:ClearCommandQueue() stream:SendNextCommand() end return stream end -- -- interpolate(from, frommin, frommax, tomin, tomax) -- Seema V. Nikam -- 06/01/2011 -- -- interpolate a value from in the range frommin..frommax to a -- value in the range tomin..tomax -- function interpolate(from, frommin, frommax, tomin, tomax) if from == nil then return nil end return tomin + ((from - frommin) * (tomax - tomin) / (frommax - frommin)) end -- -- doHandleToggle -- Seema V. Nikam -- 06/01/2011 -- -- Handle the real work of all the toggleable commands (#MUTE and #AMP) for -- now function doHandleToggle(command, how) if command ~= Commands.Power then if State:Get() ~= 'Power On' then debug('warning', 'ignoring command while power not on') return end else if State:Get() == 'Closed' then return end end command:Set(how) end -- -- handle_pwr(cmd) -- Seema V. Nikam -- 06/07/2011 -- Handle the #PWR command -- function handle_pwr(cmd) debug('commands', '#PWR '..(cmd.params[1] or '')) doHandleToggle(Commands.Power, cmd.params[1]) end ------------------------------------------------------------------------------------------------------------------------------------------------------------ -- -- -- The Multiplexer service (MUX) -- -- -- ------------------------------------------------------------------------------------------------------------------------------------------------------------ if config.func == 'MUX' then status = NSStatus:new() -- We care about the Source attribute StatePowerOn.StartUpQueries = { Commands.Source, } -- We want power mirrored into the 'power' attribute StatePowerOn.Attribute = 'power' -- -- StatePowerOn:UpdateSource(new, old) -- Seema V. Nikam -- 06/01/2011 -- -- When the selected source changes, notify the upper layer -- function StatePowerOn:UpdateSource(new, old) -- Let the upper layer (lower?) know about the source selection status local cmd = '#@'..serviceName..'#MUX_SELECT %'..new debug('verbose', 'Line:1245:In StatePowerOn: send command: "'..cmd..'"') _sendAsciiCommand(cmd) end -- -- handle_input(cmd) -- 06/01/2011 -- Seema V. Nikam -- -- Handle the #INPUT command -- function handle_input(cmd) if State:Get() ~= 'Power On' then return end local si = tonumber(cmd.params[1]) if Commands.Source.Value ~= si then Commands.Source:Set(si) end end ------------------------------------------------------------------------------------------------------------------------------------------------------------ -- -- -- AVR (renderer) driver -- -- -- ------------------------------------------------------------------------------------------------------------------------------------------------------------ elseif config.func == 'AVR' then function table.mergen(dst, ...) for _, tab in pairs({...}) do for _, item in pairs(tab) do table.insert(dst, item) end end return dst end function table.merge(dst, ...) for _, tab in pairs({...}) do for key, item in pairs(tab) do if dst[key] == nil then dst[key] = item end end end return dst end -- -- decode_level(cmd) -- 06/01/2011 -- Seema V. Nikam -- -- standard function for decoding level based commands. -- function decode_level(self, param) param = tonumber(param) -- scale to 0..100 return interpolate(param, self.Param.MinEncode, self.Param.MaxEncode, self.Param.Min, self.Param.Max) end -- -- encode_level(cmd) -- 06/01/2011 -- Seema V. Nikam -- -- standard function for encoding level based commands. -- function encode_level(self, param) if param == self.Param.Up then return 'UP' elseif param == self.Param.Down then return 'DOWN' end param = interpolate(param, self.Param.Min, self.Param.Max, self.Param.MinEncode, self.Param.MaxEncode) debug("verbose","Line:1318:In encode_level:",string.format("%02d", param)) return string.format("%02d", param) end -- -- Commands table entries for the Bass and Treble, -- which only we care about -- Commands.Volume = { Command = 'MV', Set = basic_set, Query = basic_query, Param = { Min = 0, -- Default = 30, Max = 100, MaxEncode = 810, MinEncode = 000, Up = "UP", Down = "DOWN", }, -- -- Decode(self, param) -- Seema V. Nikam -- 06/01/2011 -- -- map the MV parameter into a 0-100 range used by the GUI -- Decode = function(self, param) if param:sub(1,4) == 'MAX ' then -- check for MVMAX command, which seems to be a range change param = param:sub(5) if param:len() == 2 then param = tonumber(param) * 10 else param = tonumber(param) end if self.Param.MaxEncode ~= param then debug('warning', 'Max Volume Changed: '..param) self.Param.MaxEncode = param end param = self.RawValue elseif param == '99' then -- check special case self.RawValue = 0; return 0 else -- convert to integer in the range Min..Max if param:len() == 2 then param = tonumber(param) * 10 else param = tonumber(param) end end self.RawValue = param -- scale to 0..100 return interpolate(param, self.Param.MinEncode, self.Param.MaxEncode, self.Param.Min, self.Param.Max) end, -- -- Encode(self, param) -- Seema V. Nikam -- 06/01/2011 -- -- Map 0-100 into the values used by the device -- Encode = function(self, param) -- check special case if param == 0 then return '99' elseif param == self.Param.Up then return 'UP' elseif param == self.Param.Down then return 'DOWN' end -- scale to Min..Max param = interpolate(param, self.Param.Min, self.Param.Max, self.Param.MinEncode, self.Param.MaxEncode) -- round to nearest 0 or 5 param = (param * 2 + 4) / 10 * 5 -- convert to string param = string.format('%03d', param) -- lop trailing 0 off if param:sub(3, 3) == '0' then param = param:sub(1, 2) end return param end, } Commands.Surround = { Command = 'MS', Set = basic_set, SetTimeout = 2000, -- Setting Surround mode can be slow Query = basic_query, Decode = function(self, param) return SurroundMode:Key2Index(param) end, Encode = function(self, param) if param == nil or param == "" then param = "NEXT" elseif type(param) == "string" then param = param:upper() end if param == "NEXT" then param = (self.Value or 0) + 1 if param > #SurroundMode then param = 1 end elseif param == "PREV" then param = (self.Value or (#SurroundMode + 1)) - 1 if param <= 0 then param = #SurroundMode end end return SurroundMode:Index2Key(param) end, } Commands.Menu = { Command = 'MNMEN ', Set = function(self, param) -- Since the MNMEN command has no response/value, we -- attempt to track whether the menu is up or not so -- we can toggle it param = self:Encode(param) basic_set(self, param) self.Value = param end, Query = basic_query, Param = { On = "ON", Off = "OFF", }, Encode = encode_toggle, } Commands.Nav = { Command = 'MNC', Set = basic_set, Param = { UP = "UP", DN = "DN", LT = "LT", RT = "RT", }, } Commands.Enter = { Command = 'MNENT', Set = basic_set, Encode = function(self, param) return "" end, } Commands.NightTime = { Command = "PSNIGHT ", Set = basic_set, Query = basic_query, Param = { On = "HI", Off = "OFF", }, Encode = encode_toggle, } Commands.Bass = { Command = 'PSBAS ', Set = basic_set, Query = basic_query, Param = { Min = 0, Default = 50, Max = 100, MinEncode = 44, MaxEncode = 56, Up = "UP", Down = "DOWN", }, decode = decode_level, encode = encode_level, } Commands.Treble = { Command = 'PSTRE ', Set = basic_set, Query = basic_query, Param = { Min = 0, Default = 50, Max = 100, MinEncode = 44, MaxEncode = 56, Up = "UP", Down = "DOWN", }, decode = decode_level, encode = encode_level, } -- -- A table of things we can change with #LEVEL_SET, #LEVEL_UP and #LEVEL_DN. -- Each entry maps a #LEVEL_xxx parameter to a Commands table entry -- Levels = { vol = Commands.Volume, bass = Commands.Bass, treb = Commands.Treble, } -- -- A list of the toggleable attributes that we care about, really this is -- just a list of values to ask for at power up -- Toggles = { mute = Commands.Mute, } -- -- A list of other attributes that we care about -- Attributes = { Commands.Surround } -- Attributes we care about is all those in Levels and Toggles StatePowerOn.StartUpQueries = table.mergen({}, Levels, Toggles, Attributes) -- We want power mirrored into the 'pwrOn' attribute StatePowerOn.Attribute = 'ampOn' -- use the renderer report as the status report status = rendererReportInstance() -- -- StatePowerOn:UpdateVolume -- 06/01/2011 -- Seema V. Nikam -- -- When we receive a volume update, update the 'vol' attribute -- function StatePowerOn:UpdateVolume(new, old) debug('verbose', 'volume = '..Commands.Volume.Value) status:setField('vol', Commands.Volume.Value) end -- -- StatePowerOn:UpdateSurround -- 06/01/2011 -- Seema V. Nikam -- -- When we receive a surround mode update, update the 'surround' attribute -- function StatePowerOn:UpdateSurround(new, old) local label = SurroundMode:Index2Label(new) debug('verbose', 'surround = '..new..' label="'..(label or 'nil')..'"') status:setField('surround', new) status:setField('surroundLabel', label) end -- -- StatePowerOn:UpdateBass -- 06/01/2011 -- Seema V. Nikam -- -- When we receive a bass update, update the 'bass' attribute -- function StatePowerOn:UpdateBass(new, old) debug('verbose', 'bass = '..Commands.Bass.Value) status:setField('bass', Commands.Bass.Value) end -- -- StatePowerOn:UpdateTreble -- 06/01/2011 -- Seema V. Nikam -- -- When we receive a treble update, update the 'treb' attribute -- function StatePowerOn:UpdateTreble(new, old) debug('verbose', 'treble = '..Commands.Treble.Value) status:setField('treb', Commands.Treble.Value) end local OnOffMap={ ON = '1', OFF = '0' } -- StatePowerOn:UpdateMute -- 06/01/2011 -- Seema V. Nikam -- -- When we receive a mute update, update the 'mute' attribute -- function StatePowerOn:UpdateMute(new, old) local report = OnOffMap[Commands.Mute.Value] or Commands.Mute.Value debug('verbose', 'mute=',report) status:setField('mute', report) end -- -- handle_surround -- Seema V. Nikam -- 06/01/2011 -- -- Handle the #SURROUND command -- function handle_surround(cmd) cmd.params[1] = cmd.params[1] or '' debug('commands', '#SURROUND '..cmd.params[1]) Commands.Surround:Set(cmd.params[1]) end function handle_nav(cmd) debug('commands', '#NAV '..(cmd.params[1] or '')) Commands.Nav:Set(cmd.params[1]) end function handle_menu(cmd) debug('commands', '#MENU') Commands.Menu:Set() end function handle_enter(cmd) debug('commands', '#ENTER') Commands.Enter:Set() end function handle_nighttime(cmd) cmd.params[1] = cmd.params[1] or '' debug('commands', '#NIGHTTIME '..cmd.params[1]) Commands.NightTime:Set(cmd.params[1]) end -- -- handle_level_set -- Seema V. Nikam -- 06/01/2011 -- -- Handle the level_set command by looking the parameter up in -- the Levels table and passing it off for doHandleLevel to -- do the real work -- function handle_level_set(cmd) debug('commands', '#LEVEL_SET '..(cmd.params[1] or '')..' '..cmd.params[2]) local param = cmd.params[1]:lower() local level = cmd.params[2]:lower() local command = Levels[param] if command == nil then debug("error", "unknown argument to level_set: "..param) return end doHandleLevel(command, level) end -- -- handle_level_up -- Seema V. Nikam -- 06/01/2011 -- -- Handle the level_up command by looking the parameter up in -- the Levels table and passing it off for doHandleLevel to -- do the real work -- function handle_level_up(cmd) debug('commands', '#LEVEL_UP '..(cmd.params[1] or '')) local param = cmd.params[1]:lower() local command = Levels[param] if command == nil then debug("error", "unknown argument to level_up: "..param) return end doHandleLevel(command, command.Param.Up) end -- -- handle_level_dn -- Seema V. Nikam -- 06/01/2011 -- -- Handle the level_dn command by looking the parameter up in -- the Levels table and passing it off for doHandleLevel to -- do the real work -- function handle_level_dn(cmd) debug('commands', '#LEVEL_DN '..(cmd.params[1] or '')) local param = cmd.params[1]:lower() local command = Levels[param] if command == nil then debug("error", "unknown argument to level_dn: "..param) return end doHandleLevel(command, command.Param.Down) end -- -- handle_amp -- Seema V. Nikam -- 06/01/2011 -- -- Handle the amp command by passing it off to doHandleToggle -- to do the real work -- function handle_amp(cmd) debug('commands', '#AMP '..(cmd.params[1] or '')) doHandleToggle(Commands.Power, cmd.params[1]) end -- -- handle_mute -- Seema V. Nikam -- 06/01/2011 -- -- Handle the mute command by passing it off to doHandleToggle -- to do the real work -- function handle_mute(cmd) debug('commands', '#MUTE '..(cmd.params[1] or '')) doHandleToggle(Commands.Mute, cmd.params[1]) end -- -- doHandleLevel -- Seema V. Nikam -- 06/01/2011 -- -- handle the real work of all the #LEVEL_* commands -- function doHandleLevel(command, level) if State:Get() ~= "Power On" then return end if level == "default" then level = command.Param.Default elseif level ~= command.Param.Up and level ~= command.Param.Down then level = tonumber(level) end if level ~= nil then command:Set(level) end end ------------------------------------------------------------------------------------------------------------------------------------------------------------ -- -- -- AM/FM/ Tuner Module -- -- -- ------------------------------------------------------------------------------------------------------------------------------------------------------------ elseif config.func == 'Tuner' then -- -- Commands table entries for the Band and Frequency, -- which only we care about -- Commands.Tune = { Command = 'TF', -- -- Set -- 06/01/2011 -- Seema V. Nikam -- -- When setting a frequency, we first have to insure that the -- proper band is selected -- Set = function(self, param) debug("verbose", "param=", param) param = self:Encode(param) local band = nil local timeout --====================================================================== debug("verbose","what is this:", param:sub(1,2)) if param:sub(1,2) == 'ST' then band = Commands.Band.Param.ST debug("verbose","Line:1788: Set to ST",band) timeout = 2500 elseif param:sub(1,2) == 'HD' then band = Commands.Band.Param.AMFM debug("verbose","Line:1792: Set to HDRADIO",band) timeout = 2500 elseif tonumber(param:sub(3)) > 50000 then debug("verbose","Line:1795: this is that compared value:", tonumber(param:sub(3))) -- tuning AM band = Commands.Band.Param.AM else -- tuning FM band = Commands.Band.Param.FM end debug("verbose","Line:1802: this is band value:", band) -- Check for band if Commands.Band.Value ~= band then debug("verbose","Line:1804: what is this band value?", (Commands.Band.Value)) Commands.Band:Set(band) end --====================================================================== stream:QueueCommand(self.Command, param, timeout) end, -- -- Query -- Seema V. Nikam -- 06/01/2011 -- -- Query the right setting depending on the real selected source -- Query = function(self, param) debug("verbose","Line:1819: In Command TF: Query func:") if Commands.Source.RawValue == 'ST' then debug("verbose","Line:1821: Commands.Source.RawValue is",Commands.Source.RawValue) stream:QueueCommand(self.Command..'ST', Commands.Query) else debug("verbose","Line:1824: Commands.Source.RawValue is",Commands.Source.RawValue) stream:QueueCommand(self.Command..'HD', Commands.Query) end end, Param = { Up = 'UP', Down = 'DOWN', Next = 'NEXT', Prev = 'PREV', Band = { AM = { Min = 52000, Delta = 1000, Max = 171000}, FM = { Min = 8750, Delta = 20, Max = 10790}, ST = { Min = 1, Delta = 1, Max = 256}, } }, -- -- Decode -- Seema V. Nikam -- 06/01/2011 -- -- When parsing the return value we have to filter out the Up/Down -- verification commands by turning them into the current value. -- Otherwise we just turn the 6-digit value into a string rep of -- the current frequency. -- Decode = function(self, param) if param == nil then elseif param == self.Param.Up or param == self.Param.Down then param = self.Value else local band = param:sub(1, 2) local tune = param:sub(3) if band == 'ST' then -- SIRIUS (XM) channel needs no conversion param = tonumber(tune) debug("verbose","Line:1861: param value in Command TF: Decode() : ",param) self.LastST = param else self.RawValue = tonumber(tune) debug("verbose","Line:1865: RawValue in Command TF: Decode() : ",self.RawValue) if(self.RawValue == nil) then debug('error', 'nil RawValue') else if(self.RawValue > 50000) then -- AM - strip leading zeroes and two trailing digits -- (which should both be zero) param = tune / 100 else -- FM - strip leading zeroes and insert the decimal local left, right = tune:match('^0*(%d-)(%d%d)$') param = left..'.'..right end self.LastHD = param end end end return param end, -- -- Encode -- Seema V. Nikam -- 06/01/2011 -- -- Encode a string representation of the tune request into the rep -- required by the device -- Encode = function(self, param) -- filter out Up/Down requests since we use the external -- representation already if param == self.Param.Up or param == self.Param.Down then if Commands.Source.RawValue == 'ST' then debug("verbose","Line:1897: RawValue in Command TF: Encode() :",Commands.Source.RawValue) return 'ST'..param else return 'HD'..param end end if param == self.Param.Next then if Commands.Source.RawValue == 'ST' then return 'ST'..self.Param.Up end if self.RawValue then local band = self.Param.Band[Commands.Band.Value] param = self.RawValue - band.Delta if param < band.Min then param = band.Max end return string.format('HD%06d', param) else return 'HD'..Commands.Query end elseif param == self.Param.Prev then if Commands.Source.RawValue == 'ST' then return 'ST'..self.Param.Down end if self.RawValue then local band = self.Param.Band[Commands.Band.Value] param = self.RawValue + band.Delta if param > band.Max then param = band.Min end return string.format('HD%06d', param) else return 'HD'..Commands.Query end end -- if type(param) == 'number' then param = tostring(param) end -- local left, dot, right = param:match('0*(%d+)(%.?)(%d*)') -- if dot == '.' then -- if there's a dot, then it's FM and we just have to put -- the two halves together if right:len() == 1 then return string.format('HD%04d%d0', tonumber(left), tonumber(right)) else return string.format('HD%04d%02d', tonumber(left), tonumber(right)) end else -- There's no dot, so we have to figure out which band -- is being requested by inference local num = tonumber(left) -- if num < 256 then -- SIRIUS return string.format('ST%03d', num) elseif left:sub(left:len()) == '0' then -- AM always ends in 0 return string.format('HD%04d00', num) elseif num >= 875 and num <= 1079 then -- xxx.x FM return string.format('HD%05d0', num) elseif num >= 8750 and num <= 10790 then -- xxx.xx FM return string.format('HD%06d', num) else debug('error', 'Unknown Frequency Request: "'..param..'"') return Commands.Query end end end, } Commands.Preset = { Command = 'TPHD', Set = basic_set, Query = basic_query, Param = { Up = 'UP', Down = 'DOWN', Off = 'OFF', }, Decode = function(self, param) if param == self.Param.Off then param = nil end end, } Commands.STAll = { Command = 'ST?', Query = function (self) return stream:QueueCommand(self.Command, '') end } Commands.ChannelName = { Command = 'STCH NAME ', Query = basic_query, } Commands.Artist = { Command = 'STARTIST ', Query = basic_query, } Commands.Title = { Command = 'STTITLE ', Query = basic_query, } Commands.Antenna = { Command = 'STSIGNAL ', Query = basic_query, Param = { GOOD = 100, WEAK = 66, MARGINAL = 33, NOSIGNAL = 0, }, -- -- Decode -- Seema V. Nikam -- 06/01/2011 -- -- Translate the string param returned into a number appropriate -- for GUI. -- Decode = function(self, param) return self.Param[param] or self.Param.NOSIGNAL end, } -- An engine to reliably save the current state of the tuner -- and restore it when we're done scanning the SIRIUS channels SaveStates = { { Status = 'Power', Set = Commands.Power.Param.On }, { Status = 'Mute', Set = Commands.Mute.Param.On }, { Status = 'Source', Set = 7 }, { Status = 'Tune', Set = 0 }, -- -- Reset -- Seema V. Nikam -- 06/01/2011 -- -- Forget all saved state information -- Reset = function(self) for _, info in ipairs(self) do info.Restore = nil end end, -- -- Save -- Seema V. Nikam -- 06/01/2011 -- -- Save all state information, if the state was successfully -- saved, true is returned. If the state was not successfully -- saved, a command is issued for the needed state info and -- false is returned. If false is returned, Save should be -- called again in a short period (500ms seems to work) -- Save = function(self) if not self:Lock() then return false end for _, info in ipairs(self) do local state = Commands[info.Status] if state.Value == nil then state:Query() return false elseif info.Set ~= nil then if info.Restore == nil then debug('save', 'Save: '..info.Status..' = '..state.Value) info.Restore = state.Value end if info.Set ~= state.Value then debug('save', 'Set: '..info.Status..' = '..info.Set..' ('..state.Value..')') state:Set(info.Set) return false end end end return true end, -- -- Restore -- Seema V. Nikam -- 06/01/2011 -- -- Restore the saved state, no attempt is made to verify that the -- state is successfully restored -- Restore = function(self) for _, info in ripairs(self) do local command = Commands[info.Status] if info.Set ~= nil and info.Restore ~= nil then debug('save', 'Restore: '..info.Status..' = '..info.Restore) command:Set(info.Restore) end end self:Unlock() end, -- -- Saved -- Seema V. Nikam -- 06/01/2011 -- -- returns true iff the state is completely saved -- Saved = function(self) for _, info in ipairs(self) do if info.Set ~= nil and info.Restore == nil then return false end end return true end, -- -- Relevant -- Seema V. Nikam -- 06/01/2011 -- -- returns true iff we care about the state named in the parameter -- Relevant = function(self, param) for _, info in ipairs(self) do if info.Status == param then return true end end return false end, -- -- Lock -- Seema V. Nikam -- 06/01/2011 -- Lock = function(self, param) if not self.Locked then if not stream:lock(200) then return false end self.Locked = true end return true end, -- -- Unlock -- Seema V. Nikam -- 06/01/2011 -- Unlock = function(self, param) if self.Locked then self.Locked = false stream:unlock() end end, } -- -- We need some states to deal with the channel list (no categories are -- available from the Denon, Bad Denon! *smack*) -- StatePreRefresh = { Enter = function(self, from) status:setField('controlState', 'REFRESH') self:Save() end, Timer = function(self) self:Save() end, Save = function(self) if SaveStates:Save() then self.Machine:Set('Refresh') else self:StartTimer(5000); end end, Update = function(self, name, new, old) if SaveStates:Relevant(name) then self:Save() end if name == 'Source' then Commands.Tune:Query() debug("verbose","Line:2185: StatePreRefresh: Source RawValue", Commands.Source.RawValue) if Commands.Source.RawValue == 'ST' then Commands.STAll:Query() end end end, Exit = function(self, to) if to ~= 'Refresh' then status:setField('controlState', 'STOP') end end, } StateRefresh = { -- Add Channel, GetNextChannel, UpdateChannelName, UpdateTune Enter = function(self, from) status:setField('controlState', 'REFRESH') Channels = {} self.Channels = {} self.NextChannel = 0 self.Channel = nil self.ChannelName = nil self:GetNextChannel() end, AddChannel = function(self, channel, name) debug('scan', 'Add Channel #'..channel..' "'..name..'"') self.Channels[channel] = { Channel = channel, ChannelName = name } end, GetNextChannel = function(self) self.NextChannel = self.NextChannel + 1 if self.NextChannel >= 256 then self.Machine:Set('Post-Refresh') end self.ChannelName = nil self.Channel = nil self.Retries = 0 Commands.Tune:Set(self.NextChannel) self:StartTimer(3000) end, UpdateChannelName = function(self, new, old) -- The second time we get a valid channel name we've got the info for -- the tuned channel. This is a valid new channel if it's the one -- we requested if new ~= '' then self.ChannelName = new if self.Channel == self.NextChannel then debug('scan', 'Add Channel: "'..self.ChannelName..'" Retries='..self.Retries) self:AddChannel(self.Channel, self.ChannelName) self:GetNextChannel() elseif self.Channel then debug('scan', 'Incorrect Channel #'..(self.Channel or 'nil')..' ~ '..self.NextChannel) self:GetNextChannel() end else self.ChannelName = nil end self:StartTimer(3000) end, UpdateTune = function(self, new, old) self.Channel = new self:StartTimer(5000) end, Timer = function(self) if self.Channel == nil then debug('scan', 'Timeout on channel number, invalid channel') -- If we haven't gotten a channel number by the time everything -- is quiet, then this is an invalid channel, so just skip it self:GetNextChannel() elseif self.Retries < 10 then -- Sometimes the tuner takes awhile to come up with the channel -- name. We should be on a valid channel since the channel # -- has been updated as expected. In this case we need to ask -- for the channel name until we get it. (Perhaps we should -- just set the channel name to some predetermined string and -- wait for an update?) debug('scan', 'Retry Info Request') self.Retries = self.Retries + 1 Commands.STAll:Query() self:StartTimer(5000) else -- Sometimes the tuner just gets completely lost and doesn't -- seem to know what channel it's on, much less what the -- channel name is. In this case we back up a couple of -- channels and restart the process if Commands.Artist.Value and Commands.Artist.Value ~= '' then debug('scan', 'Channel Name Not Available, Fabricating') self:AddChannel(self.Channel, 'SIRIUS Channel '..self.Channel) else debug('scan', 'Channel Name Not Available, Skipping') end self:GetNextChannel() end end, Exit = function(self, to) Channels = {} for i = 1, 256 do if self.Channels[i] then table.insert(Channels, self.Channels[i]) end end if to ~= 'Post-Refresh' then status:setField('controlState', 'STOP') end end, } StatePostRefresh = { Enter = function(self, from) status:setField('controlState', 'REFRESH') SaveStates:Restore() self:StartTimer(5000) end, Timer = function(self) Commands.Power:Query() self:StartTimer(5000) end, UpdatePower = event_power, Exit = function(self, to) status:setField('controlState', 'PLAY') end, } State:Add('Pre-Refresh', StatePreRefresh) State:Add('Refresh', StateRefresh) State:Add('Post-Refresh', StatePostRefresh) -- At power up we need to know the band and the frequency StatePowerOn.StartUpQueries = { Commands.Band, Commands.Tune, Commands.Source } -- put power state in the 'power' attribute StatePowerOn.Attribute = 'power' -- use the song report as our status status = songReportInstance() -- -- Info -- Seema V. Nikam -- 06/01/2011 -- -- Update the AM/FM song report -- function UpdateAMFMInfo() debug("verbose","Line 2329: In UpdateAMFMInfo", Commands.Band.Value) local caption = (Commands.Band.Value or '')..' '..(Commands.Tune.Value or '') -- these are the AM/FM screen values status:setField('band', Commands.Band.Value or '?') status:setField('caption', caption) -- these are the SIRIUS (XM) screen values we make up as we go status:setField('song', '') status:setField('channelNum', '') status:setField('category', '') status:setField('artist', '') status:setField('channel', caption) status:setField('strength', '') end -- -- UpdateSTInfo -- Seema V. Nikam -- 06/01/2011 -- -- Update the SIRIUS song report -- function UpdateSTInfo() -- update the band status:setField('band', Commands.Band.Value) -- channel information status:setField('channelNum', Commands.Tune.Value) status:setField('channel', Commands.ChannelName.Value) debug("verbose","Line:2355: In UpdateSTInfo",Commands.Band.Value, Commands.Tune.Value, Commands.ChannelName.Value) -- for XM we set the artwork and use the channel name -- local CommandsTuneValue = Commands.Tune.Value or '' -- -- Previously done in Denon_AVR 3808 -- local strArtwork = artworkNameLookup[CommandsTuneValue] or 'def_src_img_1.jpg' -- status:setField('artwork', 'http://'..ipAddress..'/'..strArtwork) -- --------------------------------------------------------------------------------------------------------------------------- -- setting a artwork for SIRIUS -- Dan's suggested: 06/21/2011 -- Give SIRIUS artworkName here: as done in PolkXM local CommandsChannelValue = Commands.ChannelName.Value or '' if CommandsChannelValue~=nil then local strArtwork= "SIRIUS/"..CommandsChannelValue:gsub("[ /]","_"):gsub("^xL_","")..".swf" end -- set the caption to the channel # and name status:setField('caption', CommandsTuneValue..' '..(Commands.ChannelName.Value or '')) -- song information status:setField('song', Commands.Title.Value or '') status:setField('artist', Commands.Artist.Value or '') status:setField('category', '') -- 3808 doesn't support categories -- signal strength status:setField('strength', Commands.Antenna.Value or '0') end -- -- UpdateInfo -- Seema V. Nikam -- 06/01/2011 -- -- Update the song report -- function UpdateInfo() debug("verbose","Line:2389: In UpdateInfo(): If Commands.Source.RawValue is ST then call UpdateSTInfo() else call UpdateAMFMInfo(): ",Commands.Source.RawValue) if Commands.Source.RawValue == 'ST' then UpdateSTInfo() else UpdateAMFMInfo() end end -- -- StatePowerOn:UpdateSource -- Seema V. Nikam -- 06/01/2011 -- -- When the source changes, force an update of the tuning -- function StatePowerOn:UpdateSource(new, old) -- Since the channel/frequency hasn't actually changed, we won't -- get an update, so explicitly query for the tuning information Commands.Tune:Query() if new == 8 then -- Update the band as necessary as well local new = Commands.Band.Value debug("verbose","Line:2412: band value in StatePowerOn:UpdateSource",new) if Commands.Source.RawValue == 'ST' then new = Commands.Band.Param.ST elseif Commands.Source.RawValue == 'HD' then --new = Commands.Band.RawValue new = Commands.Band.Param.AMFM end Commands.Band:Update(new) end -- And query the SIRIUS information if it's appropriate just to be -- sure we have current information if Commands.Source.RawValue == 'ST' then Commands.STAll:Query() end end -- -- StatePowerOn:UpdateBand -- Seema V. Nikam -- 06/01/2011 -- -- When the band changes, update the attribute -- function StatePowerOn:UpdateBand(new, old) UpdateInfo() end -- -- StatePowerOn:UpdateTune -- Seema V. Nikam -- 06/01/2011 -- function StatePowerOn:UpdateTune(new, old) debug('verbose', 'channel = '..(new or 'nil')) UpdateInfo() end -- -- StatePowerOn:UpdatePreset -- Seema V. Nikam -- 06/01/2011 -- -- When the band changes, update the attribute -- function StatePowerOn:UpdatePreset(new, old) status:setField('preset', new) end -- -- StatePowerOn:UpdateChannelName -- Seema V. Nikam -- 06/01/2011 -- function StatePowerOn:UpdateChannelName(new, old) debug('verbose', 'channel name = '..(new or 'nil')) UpdateInfo() end -- -- StatePowerOn:UpdateArtist -- Seema V. Nikam -- 06/01/2011 -- function StatePowerOn:UpdateArtist(new, old) debug('verbose', 'artist = '..(new or 'nil')) UpdateInfo() end -- -- StatePowerOn:UpdateTitle -- Seema V. Nikam -- 06/01/2011 -- function StatePowerOn:UpdateTitle(new, old) debug('verbose', 'title = '..(new or 'nil')) UpdateInfo() end -- -- StatePowerOn:UpdateAntenna -- Seema V. Nikam -- 06/01/2011 -- function StatePowerOn:UpdateAntenna(new, old) debug('antenna', 'antenna = '..(new or 'nil')) UpdateInfo() end -- function handle_band(command) -- Seema V. Nikam -- 06/01/2011 -- Receive and process #BAND commands function handle_band(command) debug('verbose', command) if State:Get() ~= 'Power On' then return end local band = command.params[1]:upper() debug("verbose","Band: ",band) if band == 'NEXT' then band = Commands.Band.Param.Next elseif band == 'PREV' then band = Commands.Band.Param.Prev else band = Commands.Band.Param[band] end Commands.Band:Set(band) end -- -- function handle_tune(command) -- Seema V. Nikam -- 06/01/2011 -- Receive and process #TUNE commands function handle_tune(command) local dir = command.params[1]:upper() debug('verbose', 'Line:2522: In_Handle_tune: Tuning what:?', dir) if( dir == "DN" ) then Commands.Tune:Set(Commands.Tune.Param.Next) elseif( dir == "UP" ) then Commands.Tune:Set(Commands.Tune.Param.Prev) else Commands.Tune:Set(dir) end end -- -- function handle_tune(command) -- Seema V. Nikam -- 06/01/2011 -- Receive and process #CHANNEL commands handle_channel = handle_tune -- function handle_seek(command) -- Seema V. Nikam -- 06/01/2011 -- Receive and process #SEEK commands function handle_seek(command) local dir = command.params[1]:upper() if( dir == "DN" ) then Commands.Tune:Set(Commands.Tune.Param.Down) elseif( dir == "UP" ) then Commands.Tune:Set(Commands.Tune.Param.Up) else Commands.Tune:Set(dir) end end -- -- function handle_scan(command) -- Seema V. Nikam -- 06/01/2011 -- Receive and process #SCAN commands handle_scan = handle_seek -- -- function handle_preset(command) -- Seema V. Nikam -- 06/01/2011 -- Receive and process #PRESET UP|DN|ad commands a is A..G, d is 1..8 function handle_preset(command) local dir = command.params[1]:upper() if( dir == "DN" ) then Commands.Preset:Set(Commands.Preset.Param.Up) elseif( dir == "UP" ) then Commands.Preset:Set(Commands.Preset.Param.Down) else Commands.Preset:Set(command.params[1]) end end -- -- function handle_next(command) -- Seema V. Nikam -- 06/01/2011 -- Receive and process #NEXT commands function handle_next(command) Commands.Preset:Set(Commands.Preset.Param.Up) end -- -- function handle_prev(command) -- Seema V. Nikam -- 06/01/2011 -- Receive and process #PREV commands function handle_prev(command) Commands.Preset:Set(Commands.Preset.Param.Down) end -- -- function handle_key(command) -- Seema V. Nikam -- 06/01/2011 -- Receive and process #KEY commands function handle_key(command) debug('key', '#KEY '..prettyprint(command.params)) if( command == nil or command.params[1] == nil) then return end if not szInput then szInput = "" end if( szInput:len() > 6 ) then szInput = szInput:sub(2,-1) end szInput = szInput..command.params[1]:upper() end -- -- function handle_key(command) -- Seema V. Nikam -- 06/01/2011 -- Receive and process #CLEAR commands function handle_clear( command ) debug('key', '#CLEAR '..prettyprint(command.params)) szInput = nil end -- -- function handle_enter(command) -- Seema V. Nikam -- 06/01/2011 -- Receive and process #ENTER commands function handle_enter(command) debug('key', '#ENTER '..prettyprint(command.params)) if szInput then debug("verbose", "szInput is "..szInput) Commands.Tune:Set(szInput) end -- clear out szInput szInput = nil end -- -- function handle_menu_list(command) -- Seema V. Nikam -- 06/01/2011 -- handler for #MENU_LIST indexStart, indexEnd, path, searchParams -- path format: "PRESET"|"MEDIA"|"MEDIA2" [ ">" [ ">" [...] ] ]; ex: "MEDIA>ALLDISCS>Disc_10>Title_5>Chapter_2" function handle_menu_list(aCommand) if (#aCommand.params > 4) then debug("warning", "Extended #MENU_LIST params received: ", aCommand.params) end if State:Get() ~= 'Power On' then return end local nStart = tonumber(aCommand.params[1]) local nEnd = tonumber(aCommand.params[2]) local root = aCommand.vPath[1]:upper() if (root == "PRESETS") then -- default preset handling is sufficient defaultHandleMenuListPresets(aCommand, nStart, nEnd) elseif (root == "MEDIA" or root == "MEDIA2") then handleMenuListMedia(aCommand, nStart, nEnd) else aCommand:sendFinalMenuResp() end end function handleMenuListMedia(aCommand, nStart, nEnd) local vPath = aCommand.vPath local strDisplayPath = vPath[1] local response = aCommand.menuResponse if #vPath ~= 1 then aCommand:sendFinalMenuResp() return end response.disppath = 'All SIRIUS Channels' -- put a note symbol next to this item aCommand.replyTag = 'song' for nIndex = nStart, nEnd do -- look up the channel local channel = Channels[nIndex] if(channel == nil) then response.error = "Unknown channel "..(nIndex or "nil") debug("error", response.error); return aCommand:sendFinalMenuResp(response) end -- use the channel number and name as the display string response.display = channel.Channel..' '..channel.ChannelName -- never any children response.children = 0 -- use the index as the itemnum response.itemnum = tostring(nIndex) -- use the channel # as the ID response.id = tostring(channel.Channel) -- send a response aCommand:sendMenuResp(response) if(nIndex == #Channels) then --we have sent the entire list return aCommand:sendFinalMenuResp(response) end end end -- function handle_menu_sel(command) -- Seema V. Nikam -- 06/01/2011 -- handler for #MENU_SEL path -- path format: see handle_menu_list handle_menu_sel = function(command) if (#command.params > 1) then debug("warning", "Extended #MENU_SEL params received: ", command.params) end if State:Get() ~= 'Power On' then return end local root = command.vPath[1]:upper() if root == "PRESETS" then -- handle "#MENU_SEL {{presets>...}}" commands, where the -- presets are stored in the table passed by the dealer setup command:handlePresetMenuSel(command.vPath[2]) end end -- function handle_menu_set(command) -- Seema V. Nikam -- 06/01/2011 -- handler for #MENU_SET index handle_menu_set = function(command) if (#command.params > 1) then debug("warning", "Extended #MENU_SET params received: ", command.params) end if State:Get() ~= 'Power On' then return end local strDisplay = (Commands.Band.Value or "")..szFreq command:handlePresetMenuSet(strDisplay, "#TUNE "..szFreq) -- permanently save presets presetSaveOverrides() end -- -- function handle_refresh(command) -- Seema V. Nikam -- 06/01/2011 -- -- Handler for #REFRESH command, just starts the refresh process -- function handle_refresh(command) if State:Get() == 'Power On' or State:Get() == 'Power Off' then State:Set('Pre-Refresh') end end ------------------------------------------------------------------------------------------------------------------------------------------------------------ -- Excluded from Denon AVR 3311CI, was not implemented even in Denon AVR 3808CI driver -- -- XM Tuner Module -- -- -- ------------------------------------------------------------------------------------------------------------------------------------------------------------ --elseif config.func == 'XM' then else debug('error', 'Unknown Service Function: '..config.func) end