package main // The receiving half of the voice reach (V-671). // // mavwaked used to send and never listen. It wired no PushHandler, and // SendRequest discards a push frame when there is none. The consequence was // not a missing feature but a silent one: mavend routes a nudge to the voice // session that spoke most recently, and once mavwaked had spoken once it WAS // that session. PushToMostRecent succeeded, the dispatcher counted the nudge // delivered and stopped rerouting to telegram and ntfy, and mavwaked threw the // audio away. He heard nothing, anywhere. // // So the connection is opened at startup rather than at the first utterance, // and it is held open. A client that has never connected has no session, and // the dispatcher must be able to tell "he is not at the machine" from "he is, // and she has nothing to say". import ( "context" "encoding/json" "log" "time" "github.com/kami/maven/internal/voice" ) // nudgeRetry is how long to wait before dialling again after the conn ends. // mavend restarts on every deploy, and a listener that gives up then is a // listener that is deaf until the next reboot. const nudgeRetry = 5 * time.Second // nudgeHandler decodes a push and hands the audio to the session, which // speaks it through the same player the reply path uses. It does not play // anything itself: the half-duplex gate and barge-in live on the capture // loop, and a nudge has to sit under both. type nudgeHandler struct{ sess *session } func (h *nudgeHandler) OnPush(p voice.Push) { if p.Kind != voice.PushKindAudioNudge { log.Printf("mavwaked: ignoring push of unknown kind %q", p.Kind) return } var ap voice.AudioNudgePush if err := json.Unmarshal(p.Params, &ap); err != nil { log.Printf("mavwaked: nudge: decode: %v", err) return } log.Printf("mavwaked: nudge from rule %q (severity %d): %q (%.2fs audio)", ap.RuleName, ap.Severity, ap.Text, ap.Audio.Duration()) if len(ap.Audio.Bytes) == 0 { // mavttsd was down or the text was empty. Say so rather than going // quiet: the dispatcher already counted this one as delivered. log.Printf("mavwaked: nudge %q carried no audio, nothing to speak", ap.RuleName) return } h.sess.Nudge(ap.Audio) } // runNudgeReceiver keeps a push handler wired for as long as ctx lives, // redialling whenever the conn ends. Returns when ctx is cancelled. func runNudgeReceiver(ctx context.Context, vc *voice.Client, sess *session) { h := &nudgeHandler{sess: sess} for { err := vc.RunPushReceiver(ctx, h) if ctx.Err() != nil { return } if err != nil { log.Printf("mavwaked: nudge receiver: %v", err) } else { log.Printf("mavwaked: voice connection ended, reconnecting in %s", nudgeRetry) } select { case <-ctx.Done(): return case <-time.After(nudgeRetry): } } }