initial commit
This commit is contained in:
1 parent
3f403dbca1
commit
1436174509
116 files changed
+15931
No files matched your search
@@ -0,0 +1,181 @@
|
||||
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 }
|
||||
Reference in new issue
Block a user