// // 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 } func (ctrl *SwitchControl) handleCommand(cmd *Command) { switch cmd.Type { case CmdState: cmd.Response <- ctrl.state case CmdServer: if len(cmd.Args) == 0 { cmd.Response <- fmt.Errorf("no server specified") return } cmd.Response <- ctrl.reconcile(cmd.Args[0]) case CmdSwitch: if len(cmd.Args) == 0 { cmd.Response <- fmt.Errorf("no command specified") return } c, err := NewSwitchCommandFromStrings(cmd.Args[0], cmd.Args[1:]...) if err != nil { cmd.Response <- fmt.Errorf("switch command syntax error: %s", err.Error()) return } c.Response = cmd.Response ctrl.sw.Commands <- c } } func handleServer(in <-chan ServerState, out chan<- ServerState) { for { update := <-in out <- update } } func (ctrl *SwitchControl) checkMissingOrStaleStates(maxAge time.Duration) { if time.Since(ctrl.state.Switch.AudioInputsChanged) > maxAge { ctrl.sw.Commands <- &SwitchCommand{SwitchCmdStateAudio, nil, nil} } if time.Since(ctrl.state.Switch.AudioSilenceChanged) > maxAge { ctrl.sw.Commands <- &SwitchCommand{SwitchCmdStateSilence, nil, nil} } if time.Since(ctrl.state.Switch.GPIChanged) > maxAge { ctrl.sw.Commands <- &SwitchCommand{SwitchCmdStateGPIAll, nil, nil} } if time.Since(ctrl.state.Switch.RelayChanged) > maxAge { ctrl.sw.Commands <- &SwitchCommand{SwitchCmdStateRelay, nil, nil} } if time.Since(ctrl.state.Switch.OCChanged) > maxAge { ctrl.sw.Commands <- &SwitchCommand{SwitchCmdStateOC, nil, nil} } for _, server := range ctrl.servers { s, exists := ctrl.state.Server[server.name] if !exists || time.Since(s.Changed) > maxAge { server.UpdateRequest <- true } } } 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(10 * time.Second) // TODO: hardcoded value ctrl.state.Settled = false return true } 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 -> now in mood: %s", 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) { // TODO: requested server is ignored in any case... fix this if !ctrl.state.Settled { 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 } rhdl.Printf("SwitchCTRL: reconciling state... (requested server: '%s')", requestedServer) 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) ctrl.selectNextValidServer() return } ctrl.state.ActiveServer = swsrv 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() { rhdl.Printf("SwitchCTRL: handler running...") ctrl.settling = time.NewTimer(30 * time.Second) // TODO: hardcode value ctrl.state.Settled = false serverStateChanges := make(chan ServerState, 8) for _, srv := range ctrl.servers { go handleServer(srv.StateChanges, serverStateChanges) } // send out commands to switch and/or server to get missing infos // TODO: this should be called from reconcile but with some sort of rate-limiting... ctrl.checkMissingOrStaleStates(2 * time.Hour) // TODO: hardcoded value ctrl.reconcile("") for { select { 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) return }