// // rhctl // // Copyright (C) 2009-2016 Christian Pointner // // This file is part of rhctl. // // rhctl is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // any later version. // // rhctl is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with rhctl. If not, see . // package main import ( "errors" "fmt" "time" "github.com/btittelbach/pubsub" ) type Mood uint const ( MoodAwakening Mood = iota MoodSad MoodNervous MoodHappy ) func (m Mood) String() string { switch m { case MoodAwakening: return "awakening" case MoodSad: return "sad" case MoodNervous: return "nervous" case MoodHappy: return "happy" } return "unknown" } type State struct { Mood Mood Switch SwitchState ActiveServer string Server map[string]ServerState Settled bool } type CommandType uint8 const ( CmdState CommandType = iota CmdServer CmdSwitch ) func (c CommandType) String() string { switch c { case CmdState: return "state" case CmdServer: return "server" case CmdSwitch: return "switch" } return "unknown" } type Command struct { Type CommandType Args []string Response chan<- interface{} } type SwitchControl struct { sw *AudioSwitch servers []*PlayoutServer state State settling *time.Timer Updates *pubsub.PubSub Commands chan *Command AwakeningSettlingTime time.Duration // 30 * time.Second SelectServerSettlingTime time.Duration // 10 * time.Second UpdateStatesSettlingTime time.Duration // 3 * time.Second StaleStateCheckInterval time.Duration // 1 * time.Minute StatesMaxAge time.Duration // 2 * time.Hour } func (ctrl *SwitchControl) handleCommand(cmd *Command) { switch cmd.Type { case CmdState: if cmd.Response != nil { cmd.Response <- ctrl.state } case CmdServer: if len(cmd.Args) == 0 { if cmd.Response != nil { cmd.Response <- fmt.Errorf("no server specified") } return } result := ctrl.reconcile(cmd.Args[0]) if cmd.Response != nil { cmd.Response <- result } case CmdSwitch: if len(cmd.Args) == 0 { if cmd.Response != nil { cmd.Response <- fmt.Errorf("no command specified") } return } c, err := NewSwitchCommandFromStrings(cmd.Args[0], cmd.Args[1:]...) if err != nil { if cmd.Response != nil { cmd.Response <- fmt.Errorf("switch command syntax error: %s", err.Error()) } return } c.Response = cmd.Response ctrl.sw.Commands <- c } } func handleServer(sin <-chan ServerState, sout chan<- ServerState, cin <-chan Command, cout chan<- *Command) { for { select { case update := <-sin: sout <- update case cmd := <-cin: cout <- &cmd } } } func (ctrl *SwitchControl) checkMissingOrStaleStates(maxAge time.Duration) (isStale bool) { if time.Since(ctrl.state.Switch.AudioInputsUpdated) > maxAge { ctrl.sw.Commands <- &SwitchCommand{SwitchCmdStateAudio, nil, nil} isStale = true } if time.Since(ctrl.state.Switch.AudioSilenceUpdated) > maxAge { ctrl.sw.Commands <- &SwitchCommand{SwitchCmdStateSilence, nil, nil} isStale = true } if time.Since(ctrl.state.Switch.GPIUpdated) > maxAge { ctrl.sw.Commands <- &SwitchCommand{SwitchCmdStateGPIAll, nil, nil} isStale = true } if time.Since(ctrl.state.Switch.RelayUpdated) > maxAge { ctrl.sw.Commands <- &SwitchCommand{SwitchCmdStateRelay, nil, nil} isStale = true } if time.Since(ctrl.state.Switch.OCUpdated) > maxAge { ctrl.sw.Commands <- &SwitchCommand{SwitchCmdStateOC, nil, nil} isStale = true } for _, server := range ctrl.servers { s, exists := ctrl.state.Server[server.name] if !exists || time.Since(s.Updated) > maxAge { server.UpdateRequest <- true isStale = true } } return } func (ctrl *SwitchControl) getSwitchServerAssignment() (swsrv, swch string, err error) { activeInput := SwitchInputNum(0) cnt := 0 for idx, in := range ctrl.state.Switch.Audio[0].Inputs { if in { activeInput = SwitchInputNum(idx + 1) cnt++ } } if cnt != 1 || activeInput == SwitchInputNum(0) { return "", "", errors.New("no or more than one input is active") } return ctrl.sw.Inputs.GetServerAndChannel(activeInput) } func (ctrl *SwitchControl) selectServer(server string) bool { newIn, err := ctrl.sw.Inputs.GetNumber(server, ctrl.state.Server[server].Channel) if err != nil { rhdl.Printf("SwitchCTRL: no audio input configured for server/channel: '%s/%s'", server, ctrl.state.Server[server].Channel) return false } for idx, in := range ctrl.state.Switch.Audio[0].Inputs { if in && SwitchInputNum(idx+1) != newIn { ctrl.sw.Commands <- &SwitchCommand{SwitchCmdAudioFadeDownInput, []interface{}{SwitchInputNum(idx + 1)}, nil} } } ctrl.sw.Commands <- &SwitchCommand{SwitchCmdAudioFadeUpInput, []interface{}{newIn}, nil} ctrl.settling.Reset(ctrl.SelectServerSettlingTime) ctrl.state.Settled = false return true } func (ctrl *SwitchControl) checkAndSelectServer(server string) bool { if state, exists := ctrl.state.Server[server]; exists { if state.Health == ServerAlive && state.Channel != "" { return ctrl.selectServer(server) } } return false } func (ctrl *SwitchControl) selectNextValidServer() { for name, state := range ctrl.state.Server { if state.Health == ServerAlive && state.Channel != "" { if !ctrl.selectServer(name) { continue } ctrl.state.Mood = MoodNervous rhl.Printf("SwitchCTRL: found another valid server '%s' -> now in mood: %s", name, ctrl.state.Mood) return } } ctrl.state.Mood = MoodSad rhl.Printf("SwitchCTRL: found no alive/valid server -> now in mood: %s", ctrl.state.Mood) } func (ctrl *SwitchControl) reconcile(requestedServer string) (result bool) { if !ctrl.state.Settled { if requestedServer != "" { rhl.Printf("SwitchCTRL: ignoreing preferred server requests while settling") } return } if len(ctrl.servers) < 1 { ctrl.state.Mood = MoodHappy rhdl.Printf("SwitchCTRL: there are no servers configured -> now in mood: %s", ctrl.state.Mood) return } if ctrl.state.Mood == MoodAwakening && requestedServer != "" { rhl.Printf("SwitchCTRL: ignoreing preferred server requests while waking-up") return } reqSrvSuffix := "" if requestedServer != "" { reqSrvSuffix = " (requested server: '" + requestedServer + "')" } rhdl.Printf("SwitchCTRL: reconciling state...%s", reqSrvSuffix) swsrv, swch, err := ctrl.getSwitchServerAssignment() if err != nil { ctrl.state.Mood = MoodSad rhl.Printf("SwitchCTRL: switch input assignments are ambigious -> now in mood: %s", ctrl.state.Mood) return } rhl.Printf("SwitchCTRL: switch is set to server/channel: '%s/%s'", swsrv, swch) s, exists := ctrl.state.Server[swsrv] if !exists || s.Health != ServerAlive || s.Channel == "" { rhl.Printf("SwitchCTRL: server '%s' is unknown, dead or has invalid channel!", swsrv) if requestedServer != "" { if ctrl.checkAndSelectServer(requestedServer) { rhl.Printf("SwitchCTRL: switching to requested server '%s' ...", requestedServer) return true } rhl.Printf("SwitchCTRL: requested server '%s' is not selectable, ignoring request", requestedServer) } ctrl.selectNextValidServer() return } ctrl.state.ActiveServer = swsrv if requestedServer != "" { if ctrl.state.ActiveServer != requestedServer { if ctrl.checkAndSelectServer(requestedServer) { rhl.Printf("SwitchCTRL: switching to requested server '%s' ...", requestedServer) result = true } else { rhl.Printf("SwitchCTRL: requested server '%s' is not selectable, ignoring request", requestedServer) } } else { result = true } } else { if s.Channel != swch { // TODO: during normal operation switching server channels is expected and shouldn't lead to mood nervous?!?!? ctrl.state.Mood = MoodNervous rhl.Printf("SwitchCTRL: switch and server channel mismatch: '%s' != '%s' -> now in mood: %s", swch, s.Channel, ctrl.state.Mood) if !ctrl.selectServer(ctrl.state.ActiveServer) { ctrl.selectNextValidServer() } return } } for _, s := range ctrl.state.Server { if s.Health != ServerAlive { ctrl.state.Mood = MoodNervous rhl.Printf("SwitchCTRL: at least one configured server is dead -> now in mood: %s", ctrl.state.Mood) return } } ctrl.state.Mood = MoodHappy rhl.Printf("SwitchCTRL: all servers are alive -> now in mood: %s", ctrl.state.Mood) return } func (ctrl *SwitchControl) Run() { rhl.Printf("SwitchCTRL: starting up -> now in mood: %s", ctrl.state.Mood) ctrl.settling = time.NewTimer(ctrl.AwakeningSettlingTime) ctrl.state.Settled = false serverStateChanges := make(chan ServerState, 8) for _, srv := range ctrl.servers { go handleServer(srv.StateChanges, serverStateChanges, srv.Commands, ctrl.Commands) } // send out commands to switch and/or server to get missing infos ctrl.checkMissingOrStaleStates(0) staleTicker := time.NewTicker(ctrl.StaleStateCheckInterval) ctrl.reconcile("") for { select { case <-staleTicker.C: if ctrl.state.Settled { // better don't interfere with changes in progress if ctrl.checkMissingOrStaleStates(ctrl.StatesMaxAge) { ctrl.settling.Reset(ctrl.UpdateStatesSettlingTime) ctrl.state.Settled = false } } case <-ctrl.settling.C: ctrl.state.Settled = true ctrl.reconcile("") ctrl.Updates.Pub(ctrl.state, "state") case update := <-ctrl.sw.Updates: rhdl.Printf("got update from switch: %+v", update) for _, srv := range ctrl.servers { srv.SwitchUpdates <- update } ctrl.Updates.Pub(update, "switch:"+update.Type.String()) case state := <-ctrl.sw.StateChanges: // rhdl.Printf("got switch state update: %+v", state) ctrl.Updates.Pub(state, "switch:state") ctrl.state.Switch = state ctrl.reconcile("") ctrl.Updates.Pub(ctrl.state, "state") case state := <-serverStateChanges: rhdl.Printf("got server state update: %+v", state) ctrl.Updates.Pub(state, "server:state") ctrl.state.Server[state.Name] = state ctrl.reconcile("") ctrl.Updates.Pub(ctrl.state, "state") case cmd := <-ctrl.Commands: ctrl.handleCommand(cmd) } } } func SwitchControlInit(conf *Config, sw *AudioSwitch, servers []*PlayoutServer) (ctrl *SwitchControl) { ctrl = &SwitchControl{} ctrl.sw = sw ctrl.servers = servers ctrl.state.Mood = MoodAwakening ctrl.state.Server = make(map[string]ServerState) ctrl.Updates = pubsub.NewNonBlocking(32) ctrl.Commands = make(chan *Command, 8) // TODO: get this from conf ctrl.AwakeningSettlingTime = 30 * time.Second ctrl.SelectServerSettlingTime = 10 * time.Second ctrl.UpdateStatesSettlingTime = 3 * time.Second ctrl.StaleStateCheckInterval = 1 * time.Minute ctrl.StatesMaxAge = 2 * time.Hour return }