Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion device/device.go
Original file line number Diff line number Diff line change
Expand Up @@ -699,7 +699,27 @@ func (d *VirtualDevice) listenStatus(ctx context.Context) {
if d.mixerStatuses == nil {
d.mixerStatuses = make(map[uint8]*proto.MixerStatus)
}
_, alreadyLogged := d.mixerStatuses[mx.DeviceNumber]
prev, alreadyLogged := d.mixerStatuses[mx.DeviceNumber]
// A stripped 0x30 (newer DJMs) carries only presence —
// no channel or master state. Merge it over what the
// 0x03 channel listener has learned instead of
// clobbering, or the mixer views flip between "channel
// state" and "detected" at packet cadence as the two
// writers fight. A rich 0x29 carries everything and
// may replace the entry wholesale.
if prev != nil && !mx.ChannelStateKnown && prev.ChannelStateKnown {
mx.ChannelOnAir = prev.ChannelOnAir
mx.ChannelStateKnown = true
if mx.MasterBPM == 0 {
mx.MasterBPM = prev.MasterBPM
}
if mx.MasterDevice == 0 {
mx.MasterDevice = prev.MasterDevice
}
if mx.BeatInBar == 0 {
mx.BeatInBar = prev.BeatInBar
}
}
d.mixerStatuses[mx.DeviceNumber] = mx
d.mixerStatusesMu.Unlock()
if !alreadyLogged {
Expand Down
75 changes: 75 additions & 0 deletions device/logring.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// SPDX-License-Identifier: GPL-3.0-or-later

package device

import (
"bytes"
"sync"
)

// LogRing is an io.Writer holding the most recent log lines in a fixed-size
// ring, so the TUI's Logs tab can show live output while the full stream
// still goes to the log file (tee both via io.MultiWriter). Safe for
// concurrent writers — the stdlib log package serializes its own writes, but
// nothing stops other code from writing to the same tee.
type LogRing struct {
mu sync.Mutex
lines []string
head int // next slot to overwrite
full bool
partial bytes.Buffer // trailing bytes of an unterminated line
}

// NewLogRing returns a ring holding up to capacity lines.
func NewLogRing(capacity int) *LogRing {
if capacity < 1 {
capacity = 1
}
return &LogRing{lines: make([]string, capacity)}
}

// Write splits p into lines and appends each to the ring. A write that does
// not end in a newline leaves its tail buffered until the newline arrives,
// so interleaved fmt-style writes still land as whole lines. Always returns
// len(p), nil — a log tee must never fail the other writer.
func (r *LogRing) Write(p []byte) (int, error) {
r.mu.Lock()
defer r.mu.Unlock()
n := len(p)
for len(p) > 0 {
i := bytes.IndexByte(p, '\n')
if i < 0 {
r.partial.Write(p)
break
}
r.partial.Write(p[:i])
r.push(r.partial.String())
r.partial.Reset()
p = p[i+1:]
}
return n, nil
}

func (r *LogRing) push(line string) {
r.lines[r.head] = line
r.head++
if r.head == len(r.lines) {
r.head = 0
r.full = true
}
}

// Lines returns the buffered lines, oldest first.
func (r *LogRing) Lines() []string {
r.mu.Lock()
defer r.mu.Unlock()
if !r.full {
out := make([]string, r.head)
copy(out, r.lines[:r.head])
return out
}
out := make([]string, 0, len(r.lines))
out = append(out, r.lines[r.head:]...)
out = append(out, r.lines[:r.head]...)
return out
}
58 changes: 58 additions & 0 deletions device/logring_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// SPDX-License-Identifier: GPL-3.0-or-later

package device

import (
"fmt"
"log"
"reflect"
"testing"
)

func TestLogRingBasic(t *testing.T) {
r := NewLogRing(3)
if got := r.Lines(); len(got) != 0 {
t.Fatalf("empty ring returned %v", got)
}
r.Write([]byte("a\nb\n"))
if got := r.Lines(); !reflect.DeepEqual(got, []string{"a", "b"}) {
t.Fatalf("got %v", got)
}
// Overflow: oldest lines drop, order stays oldest-first.
r.Write([]byte("c\nd\n"))
if got := r.Lines(); !reflect.DeepEqual(got, []string{"b", "c", "d"}) {
t.Fatalf("after wrap got %v", got)
}
}

func TestLogRingPartialWrites(t *testing.T) {
r := NewLogRing(8)
// A line split across writes must land as one line once terminated.
r.Write([]byte("hel"))
r.Write([]byte("lo wor"))
if got := r.Lines(); len(got) != 0 {
t.Fatalf("unterminated line surfaced early: %v", got)
}
r.Write([]byte("ld\nnext\n"))
if got := r.Lines(); !reflect.DeepEqual(got, []string{"hello world", "next"}) {
t.Fatalf("got %v", got)
}
}

func TestLogRingAsStdlibLogSink(t *testing.T) {
r := NewLogRing(16)
l := log.New(r, "", log.LstdFlags)
for i := 0; i < 3; i++ {
l.Printf("line %d", i)
}
got := r.Lines()
if len(got) != 3 {
t.Fatalf("want 3 lines, got %v", got)
}
for i, line := range got {
want := fmt.Sprintf("line %d", i)
if len(line) < len(want) || line[len(line)-len(want):] != want {
t.Errorf("line %d = %q, want suffix %q", i, line, want)
}
}
}
8 changes: 7 additions & 1 deletion device/peers.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,13 @@ import (
"github.com/vynulldev/vynull/proto"
)

const peerTimeout = 5 * time.Second
// peerTimeout is how long a peer stays "active" after its last keep-alive.
// Devices broadcast keep-alives every 2s (measured on a DJM-A9; CDJs are
// similar), so the old 5s window meant two consecutive lost broadcasts —
// easy on a busy link-local segment — made the device vanish from the
// players/mixer views until the next packet. 10s tolerates four lost
// packets while still clearing genuinely departed devices quickly.
const peerTimeout = 10 * time.Second

// Peer represents a device seen on the Pro DJ Link network.
type Peer struct {
Expand Down
Loading
Loading