package serial import ( "context" "errors" "fmt" "sync" "time" ) type LinkMonitorOptions struct { Interval time.Duration } // DisabledLinkStateController keeps modem control inputs available for // diagnostics without using CTS/DSR/DCD as a cable-presence signal. type DisabledLinkStateController struct { Reason string } func (c DisabledLinkStateController) LinkStates(ctx context.Context) ([]LinkState, error) { if err := ctx.Err(); err != nil { return nil, err } reason := c.Reason if reason == "" { reason = "modem control signals are excluded from connection status" } states := make([]LinkState, 0, ChannelCount) for index := 1; index <= ChannelCount; index++ { states = append(states, LinkState{ Channel: fmt.Sprintf("CH%02d", index), State: "unknown", Signal: "unknown", Available: false, Message: reason, }) } return states, nil } // LinkMonitor owns periodic modem-signal polling. Socket/API requests only // copy its cache, so frequent UI refreshes cannot multiply TTY ioctl traffic. type LinkMonitor struct { source LinkStateController interval time.Duration mu sync.RWMutex states []LinkState } func NewLinkMonitor(source LinkStateController, options LinkMonitorOptions) (*LinkMonitor, error) { if source == nil { return nil, errors.New("serial link-state source is required") } interval := options.Interval if interval <= 0 { interval = time.Second } if interval < 100*time.Millisecond { interval = 100 * time.Millisecond } if interval > 10*time.Second { interval = 10 * time.Second } states := make([]LinkState, 0, ChannelCount) for index := 1; index <= ChannelCount; index++ { states = append(states, LinkState{ Channel: fmt.Sprintf("CH%02d", index), State: "unknown", Signal: "unknown", Message: "serial modem status monitor is starting", }) } return &LinkMonitor{source: source, interval: interval, states: states}, nil } func (m *LinkMonitor) Run(ctx context.Context) { if m == nil { return } for { started := time.Now() m.refresh(ctx) remaining := m.interval - time.Since(started) if remaining < 0 { remaining = 0 } if !waitForProbe(ctx, remaining) { return } } } func (m *LinkMonitor) LinkStates(ctx context.Context) ([]LinkState, error) { if err := ctx.Err(); err != nil { return nil, err } m.mu.RLock() defer m.mu.RUnlock() return append([]LinkState(nil), m.states...), nil } func (m *LinkMonitor) refresh(parent context.Context) { ctx, cancel := context.WithTimeout(parent, m.interval) defer cancel() states, err := m.source.LinkStates(ctx) m.mu.Lock() defer m.mu.Unlock() if err == nil && len(states) > 0 { m.states = append(m.states[:0], states...) return } message := "serial modem status refresh returned no state" if err != nil { message = err.Error() } for index := range m.states { m.states[index].State = "unknown" m.states[index].Available = false m.states[index].Message = message } }