182 lines
4.4 KiB
Go
182 lines
4.4 KiB
Go
package fdim
|
|
|
|
import (
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
"fusion/can"
|
|
)
|
|
|
|
// RefreshInterval is how long CANDisplay waits after its last send before
|
|
// resending the text and clock: the FDIM drops text it hasn't seen for about
|
|
// a second.
|
|
const RefreshInterval = 500 * time.Millisecond
|
|
|
|
// StatusInterval is how often CANDisplay sends the frames that must keep
|
|
// flowing regardless of text changes: the backlight and the clock.
|
|
const StatusInterval = time.Second
|
|
|
|
// frameGap spaces the frames of one text message, as the bench code does.
|
|
const frameGap = 5 * time.Millisecond
|
|
|
|
// CANDisplay is the real FDIM on MS-CAN. Calls only record the newest
|
|
// rows/clock and wake one sender goroutine, so the app loop never waits on
|
|
// CAN; during a fast volume spin the intermediate frames are skipped and the
|
|
// display jumps straight to the latest one.
|
|
type CANDisplay struct {
|
|
tx can.Sender
|
|
refresh time.Duration
|
|
status time.Duration
|
|
trace func(string) // may be nil; one timing line per change-driven text send
|
|
wake chan struct{}
|
|
stop chan struct{}
|
|
once sync.Once
|
|
|
|
mu sync.Mutex
|
|
r1, r2 string
|
|
haveText bool
|
|
textDirty bool
|
|
clock *can.Frame
|
|
clockDirty bool
|
|
changedAt time.Time // first unsent change
|
|
coalesced int // changes folded into the next send
|
|
err error // last send error, reported by the next call
|
|
}
|
|
|
|
// NewCANDisplay starts the sender; Close stops it. trace may be nil.
|
|
func NewCANDisplay(tx can.Sender, trace func(string)) *CANDisplay {
|
|
return newCANDisplay(tx, RefreshInterval, StatusInterval, trace)
|
|
}
|
|
|
|
func newCANDisplay(tx can.Sender, refresh, status time.Duration, trace func(string)) *CANDisplay {
|
|
d := &CANDisplay{tx: tx, refresh: refresh, status: status, trace: trace,
|
|
wake: make(chan struct{}, 1), stop: make(chan struct{})}
|
|
go d.run()
|
|
return d
|
|
}
|
|
|
|
func (d *CANDisplay) run() {
|
|
t := time.NewTimer(d.refresh)
|
|
defer t.Stop()
|
|
st := time.NewTicker(d.status)
|
|
defer st.Stop()
|
|
d.tx.Send(BacklightFrame)
|
|
for {
|
|
refresh, status := false, false
|
|
select {
|
|
case <-d.stop:
|
|
return
|
|
case <-d.wake:
|
|
case <-t.C:
|
|
refresh = true
|
|
case <-st.C:
|
|
status = true
|
|
}
|
|
|
|
d.mu.Lock()
|
|
r1, r2 := d.r1, d.r2
|
|
sendText := d.haveText && (d.textDirty || refresh)
|
|
clock := d.clock
|
|
sendClock := clock != nil && (d.clockDirty || refresh || status)
|
|
changed := d.textDirty
|
|
changedAt, coalesced := d.changedAt, d.coalesced
|
|
d.textDirty, d.clockDirty, d.coalesced = false, false, 0
|
|
d.mu.Unlock()
|
|
|
|
start := time.Now()
|
|
var err error
|
|
if sendText {
|
|
err = d.sendText(r1, r2)
|
|
}
|
|
if sendClock && err == nil {
|
|
err = d.tx.Send(*clock)
|
|
}
|
|
if status && err == nil {
|
|
err = d.tx.Send(BacklightFrame)
|
|
}
|
|
if err != nil {
|
|
d.mu.Lock()
|
|
d.err = err
|
|
d.mu.Unlock()
|
|
}
|
|
if changed && d.trace != nil {
|
|
d.trace(fmt.Sprintf("fdim: text sent in %.1f ms, %.1f ms after change, %d coalesced",
|
|
ms(time.Since(start)), ms(time.Since(changedAt)), coalesced))
|
|
}
|
|
if !status {
|
|
t.Reset(d.refresh)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (d *CANDisplay) sendText(r1, r2 string) error {
|
|
for i, f := range textFrames(r1, r2) {
|
|
if i > 0 {
|
|
time.Sleep(frameGap)
|
|
}
|
|
if err := d.tx.Send(f); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// note records a change and wakes the sender; d.mu must be held. It returns
|
|
// (and clears) the error from an earlier send.
|
|
func (d *CANDisplay) note() error {
|
|
if d.coalesced == 0 && !d.textDirty && !d.clockDirty {
|
|
d.changedAt = time.Now()
|
|
}
|
|
select {
|
|
case d.wake <- struct{}{}:
|
|
default:
|
|
}
|
|
err := d.err
|
|
d.err = nil
|
|
return err
|
|
}
|
|
|
|
func (d *CANDisplay) DisplayText(r1, r2 string) error {
|
|
d.mu.Lock()
|
|
defer d.mu.Unlock()
|
|
if d.textDirty {
|
|
d.coalesced++
|
|
}
|
|
d.r1, d.r2, d.haveText = r1, r2, true
|
|
err := d.note()
|
|
d.textDirty = true
|
|
return err
|
|
}
|
|
|
|
// SetSourceLabel is a no-op until the source box (AM/FM/SYNC/AUX) is decoded.
|
|
func (d *CANDisplay) SetSourceLabel(label string) error { return nil }
|
|
|
|
// SetClock queues the clock and keeps refreshing it. "" stops sending;
|
|
// the FDIM's own handling of a missing clock is untested.
|
|
func (d *CANDisplay) SetClock(text string) error {
|
|
d.mu.Lock()
|
|
defer d.mu.Unlock()
|
|
if text == "" {
|
|
d.clock = nil
|
|
return nil
|
|
}
|
|
f, err := clockFrame(text)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
d.clock = &f
|
|
err = d.note()
|
|
d.clockDirty = true
|
|
return err
|
|
}
|
|
|
|
func (d *CANDisplay) KeepAlive() error { return d.tx.Send(KeepAliveFrame) }
|
|
|
|
func (d *CANDisplay) Close() error {
|
|
d.once.Do(func() { close(d.stop) })
|
|
return nil
|
|
}
|
|
|
|
func ms(d time.Duration) float64 { return float64(d.Microseconds()) / 1000 }
|