diff options
Diffstat (limited to 'audiotrond.go')
-rw-r--r-- | audiotrond.go | 270 |
1 files changed, 270 insertions, 0 deletions
diff --git a/audiotrond.go b/audiotrond.go new file mode 100644 index 0000000..f59a4db --- /dev/null +++ b/audiotrond.go @@ -0,0 +1,270 @@ +// Copyright 2022 Benjamin Barenblat +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +package main + +import ( + "fmt" + "log" + "os" + "os/signal" + "time" + + "benjamin.barenblat.name/audiotrond/cfa635" + "github.com/fhs/gompd/v2/mpd" + "github.com/tarm/serial" +) + +func putWrapped(lcd *cfa635.Module, col, row int, data []byte) error { + for row < 4 && len(data) > 0 { + if err := lcd.Put(col, row, data); err != nil { + return err + } + if len(data) <= 20-col { + data = nil + } else { + data = data[20-col:] + } + col = 0 + row++ + } + return nil +} + +func update[T comparable](dst *T, src T, mtime *time.Time, now time.Time) { + if *dst == src { + return + } + *dst = src + *mtime = now +} + +type playbackState byte + +const ( + _ playbackState = 1 + iota + stopped + playing + paused +) + +type foreground byte + +const ( + _ foreground = iota + mpdForeground + clockForeground +) + +type model struct { + State playbackState + LastStateChange time.Time + + Duration time.Duration + Elapsed time.Duration + + Track string + Artist string + Album string + LastTrackInfoUpdate time.Time + + Foreground foreground +} + +func connectToCFA635() *cfa635.Module { + s, err := serial.OpenPort(&serial.Config{Name: "/dev/lcd", Baud: 115200}) + if err != nil { + panic(err) + } + m := cfa635.Connect(s) + + go func() { + for r := m.ReadReport(); r != nil; r = m.ReadReport() { + log.Println("report:", r) + } + }() + + return m +} + +func reportPanicOrClear(lcd *cfa635.Module) { + if v := recover(); v != nil { + putWrapped(lcd, 0, 0, encode(fmt.Sprint("panic: ", v))) + panic(v) + } + lcd.Clear() +} + +func poll(mpd *mpd.Client, now time.Time, model *model) error { + cmds := mpd.BeginCommandList() + statusP := cmds.Status() + currentP := cmds.CurrentSong() + if err := cmds.End(); err != nil { + return err + } + + status, err := statusP.Value() + if err != nil { + return err + } + + switch status["state"] { + case "stop": + update(&model.State, stopped, &model.LastStateChange, now) + case "play": + update(&model.State, playing, &model.LastStateChange, now) + case "pause": + update(&model.State, paused, &model.LastStateChange, now) + } + + if status["duration"] == "" { + model.Duration = 0 + model.Elapsed = 0 + } else { + if model.Duration, err = time.ParseDuration(status["duration"] + "s"); err != nil { + panic(err) + } + if model.Elapsed, err = time.ParseDuration(status["elapsed"] + "s"); err != nil { + panic(err) + } + } + + current, err := currentP.Value() + if err != nil { + return err + } + + update(&model.Track, current["Title"], &model.LastTrackInfoUpdate, now) + update(&model.Artist, current["Artist"], &model.LastTrackInfoUpdate, now) + update(&model.Album, current["Album"], &model.LastTrackInfoUpdate, now) + return nil +} + +func ellipsize(src []byte, ellipsis byte, dst []byte) { + if len(src) <= len(dst) { + copy(dst, src) + } else { + dots := len(dst) - 1 + copy(dst, src[:dots]) + dst[dots] = ellipsis + } +} + +func fmtTime(t, max time.Duration) string { + f := func(neg string, h, m, s int) string { + _ = h + return fmt.Sprintf("%s%d:%02d", neg, m, s) + } + if max/time.Hour > 0 { + f = func(neg string, h, m, s int) string { + return fmt.Sprintf("%s%d:%02d:%02d", neg, h, m, s) + } + } else if int(max/time.Minute)%60 >= 10 { + f = func(neg string, h, m, s int) string { + _ = h + return fmt.Sprintf("%s%02d:%02d", neg, m, s) + } + } + + var neg string + if t < 0 { + t *= -1 + neg = "-" + } + return f(neg, int(t/time.Hour), int(t/time.Minute)%60, int(t/time.Second)%60) +} + +func main() { + lcd := connectToCFA635() + defer reportPanicOrClear(lcd) + defer lcd.SetBacklight(0, 0) + if err := lcd.SetLED(0, false, 0); err != nil { + panic(err) + } + if err := lcd.SetLED(0, true, 0); err != nil { + panic(err) + } + + if err := lcd.Clear(); err != nil { + panic(err) + } + + mpd, err := mpd.Dial("unix", "/run/mpd/socket") + if err != nil { + panic(err) + } + + var model model + + view1 := new(view) + view1.LCD = cfa635.ClearedLCDState() + view1.Mtime = time.Now() + + // Create an idle timer and put it in a drained state so the event loop + // can set it. + idle := time.NewTimer(0) + <-idle.C + + sigterm := make(chan os.Signal, 1) + signal.Notify(sigterm, os.Interrupt) + +EventLoop: + for { + now := time.Now() + + if err := poll(mpd, now, &model); err != nil { + panic(err) + } + + var foreground2 foreground + if model.State == playing || now.Sub(model.LastStateChange).Seconds() < 17 { + foreground2 = mpdForeground + } else { + foreground2 = clockForeground + } + + if foreground2 != model.Foreground { + switch foreground2 { + case mpdForeground: + if err := initializeMPDDisplay(lcd); err != nil { + panic(err) + } + case clockForeground: + if err := initializeClockDisplay(lcd); err != nil { + panic(err) + } + } + } + model.Foreground = foreground2 + + var view2 *view + switch model.Foreground { + case mpdForeground: + view2 = mpdView(&model, now, view1) + case clockForeground: + view2 = clockView(now) + } + if err := updateView(lcd, view1, view2); err != nil { + panic(err) + } + view1 = view2 + + idle.Reset(10 * time.Millisecond) + select { + case <-sigterm: + break EventLoop + case <-idle.C: + } + } +} |