package udp import ( "bytes" "encoding/binary" "fmt" "strings" "hamlog/internal/applog" ) // WSJT-X Highlight Callsign (13) and Replay (7) — the two halves of making the // Band Activity window log-aware. // // Highlight paints a callsign in the decoding application's own window with the // colours OpsLog chooses — new DXCC, new band, a watchlist member — the way // JTAlert does. Replay asks a freshly-discovered instance to resend the decodes // it already has on screen, so the FT decodes panel starts full instead of // empty until the next period. const ( wsjtMsgReplay = 7 wsjtMsgHighlight = 13 ) // RGB is one highlight colour. A nil *RGB means "invalid QColor", which is the // protocol's way of saying "remove the highlight". type RGB struct{ R, G, B uint8 } // writeQColor serializes a QColor as QDataStream does: a spec byte (1 = RGB, // 0 = invalid) followed by five 16-bit channels (alpha, red, green, blue, pad), // each 8-bit value doubled into 16 bits the way Qt stores them. func writeQColor(b *bytes.Buffer, c *RGB) { if c == nil { b.WriteByte(0) // invalid — clears the highlight for i := 0; i < 5; i++ { _ = binary.Write(b, binary.BigEndian, uint16(0)) } return } b.WriteByte(1) // spec = RGB wide := func(v uint8) uint16 { return uint16(v) * 0x101 } _ = binary.Write(b, binary.BigEndian, uint16(0xFFFF)) // alpha, opaque _ = binary.Write(b, binary.BigEndian, wide(c.R)) _ = binary.Write(b, binary.BigEndian, wide(c.G)) _ = binary.Write(b, binary.BigEndian, wide(c.B)) _ = binary.Write(b, binary.BigEndian, uint16(0)) // pad } // EncodeHighlight builds a Highlight Callsign datagram. bg/fg nil = invalid // colour; both nil clears the callsign's highlight. func EncodeHighlight(programID, callsign string, bg, fg *RGB, lastPeriodOnly bool) []byte { var b bytes.Buffer _ = binary.Write(&b, binary.BigEndian, uint32(wsjtMagic)) _ = binary.Write(&b, binary.BigEndian, uint32(2)) _ = binary.Write(&b, binary.BigEndian, uint32(wsjtMsgHighlight)) writeQString(&b, programID) writeQString(&b, callsign) writeQColor(&b, bg) writeQColor(&b, fg) var last uint8 if lastPeriodOnly { last = 1 } _ = binary.Write(&b, binary.BigEndian, last) return b.Bytes() } // EncodeReplay builds a Replay datagram — "resend what your window holds". func EncodeReplay(programID string) []byte { var b bytes.Buffer _ = binary.Write(&b, binary.BigEndian, uint32(wsjtMagic)) _ = binary.Write(&b, binary.BigEndian, uint32(2)) _ = binary.Write(&b, binary.BigEndian, uint32(wsjtMsgReplay)) writeQString(&b, programID) return b.Bytes() } // sendToInstance routes a raw datagram to the application that owns programID, // the same way SendReply does: to the address its packets actually arrive from. func (m *Manager) sendToInstance(programID string, pkt []byte, what string) error { if strings.TrimSpace(programID) == "" { return fmt.Errorf("no application id") } m.mu.Lock() servers := make([]*Server, 0, len(m.inbound)) for _, s := range m.inbound { servers = append(servers, s) } m.mu.Unlock() for _, s := range servers { conn, addr := s.replyTarget(programID) if conn == nil || addr == nil { continue } if _, err := conn.WriteToUDP(pkt, addr); err != nil { return fmt.Errorf("send %s to %s at %s: %w", what, programID, addr, err) } return nil } return fmt.Errorf("no packet has arrived from %q yet", programID) } // SendHighlight paints (or clears) one callsign in the given instance. func (m *Manager) SendHighlight(programID, callsign string, bg, fg *RGB, lastPeriodOnly bool) error { return m.sendToInstance(programID, EncodeHighlight(programID, callsign, bg, fg, lastPeriodOnly), "highlight") } // SendClearHighlights removes every highlighting instruction OpsLog installed // in the instance. "CLEARALL!" is the protocol's own magic callsign for it. func (m *Manager) SendClearHighlights(programID string) error { return m.sendToInstance(programID, EncodeHighlight(programID, "CLEARALL!", nil, nil, false), "clear-highlights") } // SendReplay asks the instance to resend its on-screen decodes. func (m *Manager) SendReplay(programID string) error { err := m.sendToInstance(programID, EncodeReplay(programID), "replay") if err == nil { applog.Printf("udp: replay requested from %q — its existing decodes will arrive marked not-new", programID) } return err } // Instances lists every program id a packet has arrived from, for "clear the // highlights everywhere" and the startup replay. func (m *Manager) Instances() []string { m.mu.Lock() servers := make([]*Server, 0, len(m.inbound)) for _, s := range m.inbound { servers = append(servers, s) } m.mu.Unlock() seen := map[string]struct{}{} var out []string for _, s := range servers { s.mu.Lock() for id := range s.lastFrom { if _, dup := seen[id]; !dup { seen[id] = struct{}{} out = append(out, id) } } s.mu.Unlock() } return out }