initial commit

This commit is contained in:
matthewkotras committed 2026-10-27 21:13:36 -05:00
1 parent 3f403dbca1
commit 1436174509
116 files changed
+15931

No files matched your search

+181
View File
@@ -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 }