Update serial console part

This commit is contained in:
Severin Müller 2025-10-16 21:15:34 +02:00
parent 7c09ac3c08
commit 0630a7bcb1
2 changed files with 84 additions and 96 deletions

View File

@ -255,20 +255,9 @@ func setDCRestoreState(state int) error {
return nil return nil
} }
func mountSerialButtons() error {
_ = port.SetMode(defaultMode)
return nil
}
func unmountSerialButtons() error {
_ = reopenSerialPort()
return nil
}
func sendCustomCommand(command string) error { func sendCustomCommand(command string) error {
scopedLogger := serialLogger.With().Str("service", "custom_buttons_tx").Logger() scopedLogger := serialLogger.With().Str("service", "custom_buttons_tx").Logger()
scopedLogger.Info().Str("Command", command).Msg("Sending custom command.") scopedLogger.Debug().Msgf("Sending custom command: %q", command)
scopedLogger.Info().Msgf("Sending custom command: %q", command)
if serialMux == nil { if serialMux == nil {
return fmt.Errorf("serial mux not initialized") return fmt.Errorf("serial mux not initialized")
} }
@ -325,7 +314,7 @@ type SerialSettings struct {
HideSerialSettings bool `json:"hideSerialSettings"` // Whether to hide the serial settings in the UI HideSerialSettings bool `json:"hideSerialSettings"` // Whether to hide the serial settings in the UI
EnableEcho bool `json:"enableEcho"` // Whether to echo received characters back to the sender EnableEcho bool `json:"enableEcho"` // Whether to echo received characters back to the sender
NormalizeMode string `json:"normalizeMode"` // Normalization mode: "carret", "names", "hex" NormalizeMode string `json:"normalizeMode"` // Normalization mode: "carret", "names", "hex"
NormalizeLineEnd string `json:"normalizeLineEnd"` // Line ending normalization: "keep", "lf", "cr", "crlf" NormalizeLineEnd string `json:"normalizeLineEnd"` // Line ending normalization: "keep", "lf", "cr", "crlf", "lfcr"
TabRender string `json:"tabRender"` // How to render tabs: "spaces", "arrow", "pipe" TabRender string `json:"tabRender"` // How to render tabs: "spaces", "arrow", "pipe"
PreserveANSI bool `json:"preserveANSI"` // Whether to preserve ANSI escape codes PreserveANSI bool `json:"preserveANSI"` // Whether to preserve ANSI escape codes
ShowNLTag bool `json:"showNLTag"` // Whether to show a special tag for new lines ShowNLTag bool `json:"showNLTag"` // Whether to show a special tag for new lines
@ -358,7 +347,7 @@ func getSerialSettings() (SerialSettings, error) {
file, err := os.Open(serialSettingsPath) file, err := os.Open(serialSettingsPath)
if err != nil { if err != nil {
logger.Debug().Msg("SerialButtons config file doesn't exist, using default") logger.Info().Msg("SerialButtons config file doesn't exist, using default")
return serialConfig, err return serialConfig, err
} }
defer file.Close() defer file.Close()
@ -412,7 +401,7 @@ func getSerialSettings() (SerialSettings, error) {
var normalizeMode NormalizeMode var normalizeMode NormalizeMode
switch serialConfig.NormalizeMode { switch serialConfig.NormalizeMode {
case "carret": case "caret":
normalizeMode = ModeCaret normalizeMode = ModeCaret
case "names": case "names":
normalizeMode = ModeNames normalizeMode = ModeNames
@ -422,25 +411,25 @@ func getSerialSettings() (SerialSettings, error) {
normalizeMode = ModeNames normalizeMode = ModeNames
} }
var crlfMode CRLFMode var crlfMode LineEndingMode
switch serialConfig.NormalizeLineEnd { switch serialConfig.NormalizeLineEnd {
case "keep": case "keep":
crlfMode = CRLFAsIs crlfMode = LineEnding_AsIs
case "lf": case "lf":
crlfMode = CRLF_LF crlfMode = LineEnding_LF
case "cr": case "cr":
crlfMode = CRLF_CR crlfMode = LineEnding_CR
case "crlf": case "crlf":
crlfMode = CRLF_CRLF crlfMode = LineEnding_CRLF
case "lfcr": case "lfcr":
crlfMode = CRLF_LFCR crlfMode = LineEnding_LFCR
default: default:
crlfMode = CRLFAsIs crlfMode = LineEnding_AsIs
} }
if consoleBroker != nil { if consoleBroker != nil {
norm := NormOptions{ norm := NormalizationOptions{
Mode: normalizeMode, CRLF: crlfMode, TabRender: serialConfig.TabRender, PreserveANSI: serialConfig.PreserveANSI, ShowNLTag: serialConfig.ShowNLTag, Mode: normalizeMode, LineEnding: crlfMode, TabRender: serialConfig.TabRender, PreserveANSI: serialConfig.PreserveANSI, ShowNLTag: serialConfig.ShowNLTag,
} }
consoleBroker.SetNormOptions(norm) consoleBroker.SetNormOptions(norm)
} }
@ -507,7 +496,7 @@ func setSerialSettings(newSettings SerialSettings) error {
var normalizeMode NormalizeMode var normalizeMode NormalizeMode
switch serialConfig.NormalizeMode { switch serialConfig.NormalizeMode {
case "carret": case "caret":
normalizeMode = ModeCaret normalizeMode = ModeCaret
case "names": case "names":
normalizeMode = ModeNames normalizeMode = ModeNames
@ -517,25 +506,25 @@ func setSerialSettings(newSettings SerialSettings) error {
normalizeMode = ModeNames normalizeMode = ModeNames
} }
var crlfMode CRLFMode var crlfMode LineEndingMode
switch serialConfig.NormalizeLineEnd { switch serialConfig.NormalizeLineEnd {
case "keep": case "keep":
crlfMode = CRLFAsIs crlfMode = LineEnding_AsIs
case "lf": case "lf":
crlfMode = CRLF_LF crlfMode = LineEnding_LF
case "cr": case "cr":
crlfMode = CRLF_CR crlfMode = LineEnding_CR
case "crlf": case "crlf":
crlfMode = CRLF_CRLF crlfMode = LineEnding_CRLF
case "lfcr": case "lfcr":
crlfMode = CRLF_LFCR crlfMode = LineEnding_LFCR
default: default:
crlfMode = CRLFAsIs crlfMode = LineEnding_AsIs
} }
if consoleBroker != nil { if consoleBroker != nil {
norm := NormOptions{ norm := NormalizationOptions{
Mode: normalizeMode, CRLF: crlfMode, TabRender: serialConfig.TabRender, PreserveANSI: serialConfig.PreserveANSI, ShowNLTag: serialConfig.ShowNLTag, Mode: normalizeMode, LineEnding: crlfMode, TabRender: serialConfig.TabRender, PreserveANSI: serialConfig.PreserveANSI, ShowNLTag: serialConfig.ShowNLTag,
} }
consoleBroker.SetNormOptions(norm) consoleBroker.SetNormOptions(norm)
} }
@ -575,8 +564,8 @@ func reopenSerialPort() error {
} }
// new broker (no sink yet—set it in handleSerialChannel.OnOpen) // new broker (no sink yet—set it in handleSerialChannel.OnOpen)
norm := NormOptions{ norm := NormalizationOptions{
Mode: ModeNames, CRLF: CRLF_LF, TabRender: "", PreserveANSI: true, Mode: ModeNames, LineEnding: LineEnding_LF, TabRender: "", PreserveANSI: true,
} }
if consoleBroker != nil { if consoleBroker != nil {
consoleBroker.Close() consoleBroker.Close()
@ -612,8 +601,7 @@ func handleSerialChannel(dataChannel *webrtc.DataChannel) {
dataChannel.OnMessage(func(msg webrtc.DataChannelMessage) { dataChannel.OnMessage(func(msg webrtc.DataChannelMessage) {
scopedLogger.Info().Bytes("Data:", msg.Data).Msg("Sending data to serial mux") scopedLogger.Trace().Bytes("Data:", msg.Data).Msg("Sending data to serial mux")
scopedLogger.Info().Msgf("Sending data to serial mux: %q", msg.Data)
if serialMux == nil { if serialMux == nil {
return return
} }

View File

@ -31,25 +31,25 @@ const (
ModeHex // \x1B ModeHex // \x1B
) )
type CRLFMode int type LineEndingMode int
const ( const (
CRLFAsIs CRLFMode = iota LineEnding_AsIs LineEndingMode = iota
CRLF_LF LineEnding_LF
CRLF_CR LineEnding_CR
CRLF_CRLF LineEnding_CRLF
CRLF_LFCR LineEnding_LFCR
) )
type NormOptions struct { type NormalizationOptions struct {
Mode NormalizeMode Mode NormalizeMode
CRLF CRLFMode LineEnding LineEndingMode
TabRender string // e.g. " " or "" to keep '\t' TabRender string // e.g. " " or "" to keep '\t'
PreserveANSI bool PreserveANSI bool
ShowNLTag bool // <- NEW: also print a visible tag for CR/LF ShowNLTag bool // print a visible tag for CR/LF like <CR>, <LF>, <CRLF>
} }
func normalize(in []byte, opt NormOptions) string { func normalize(in []byte, opt NormalizationOptions) string {
var out strings.Builder var out strings.Builder
esc := byte(0x1B) esc := byte(0x1B)
for i := 0; i < len(in); { for i := 0; i < len(in); {
@ -113,8 +113,8 @@ func normalize(in []byte, opt NormOptions) string {
} }
// now emit the actual newline(s) per the normalization mode // now emit the actual newline(s) per the normalization mode
switch opt.CRLF { switch opt.LineEnding {
case CRLFAsIs: case LineEnding_AsIs:
if isPair { if isPair {
out.WriteByte(b) out.WriteByte(b)
out.WriteByte(in[i+1]) out.WriteByte(in[i+1])
@ -123,28 +123,28 @@ func normalize(in []byte, opt NormOptions) string {
out.WriteByte(b) out.WriteByte(b)
i++ i++
} }
case CRLF_LF: case LineEnding_LF:
if isPair { if isPair {
i += 2 i += 2
} else { } else {
i++ i++
} }
out.WriteByte('\n') out.WriteByte('\n')
case CRLF_CR: case LineEnding_CR:
if isPair { if isPair {
i += 2 i += 2
} else { } else {
i++ i++
} }
out.WriteByte('\r') out.WriteByte('\r')
case CRLF_CRLF: case LineEnding_CRLF:
if isPair { if isPair {
i += 2 i += 2
} else { } else {
i++ i++
} }
out.WriteString("\r\n") // (fixed to actually write CRLF) out.WriteString("\r\n")
case CRLF_LFCR: case LineEnding_LFCR:
if isPair { if isPair {
i += 2 i += 2
} else { } else {
@ -238,14 +238,14 @@ type ConsoleBroker struct {
quietAfter time.Duration quietAfter time.Duration
// normalization // normalization
norm NormOptions norm NormalizationOptions
// labels // labels
labelRX string labelRX string
labelTX string labelTX string
} }
func NewConsoleBroker(s Sink, norm NormOptions) *ConsoleBroker { func NewConsoleBroker(s Sink, norm NormalizationOptions) *ConsoleBroker {
return &ConsoleBroker{ return &ConsoleBroker{
sink: s, sink: s,
in: make(chan consoleEvent, 256), in: make(chan consoleEvent, 256),
@ -264,10 +264,10 @@ func NewConsoleBroker(s Sink, norm NormOptions) *ConsoleBroker {
} }
} }
func (b *ConsoleBroker) Start() { go b.loop() } func (b *ConsoleBroker) Start() { go b.loop() }
func (b *ConsoleBroker) Close() { close(b.done) } func (b *ConsoleBroker) Close() { close(b.done) }
func (b *ConsoleBroker) SetSink(s Sink) { b.sink = s } func (b *ConsoleBroker) SetSink(s Sink) { b.sink = s }
func (b *ConsoleBroker) SetNormOptions(norm NormOptions) { b.norm = norm } func (b *ConsoleBroker) SetNormOptions(norm NormalizationOptions) { b.norm = norm }
func (b *ConsoleBroker) SetTerminalPaused(v bool) { func (b *ConsoleBroker) SetTerminalPaused(v bool) {
if b == nil { if b == nil {
return return
@ -293,23 +293,23 @@ func (b *ConsoleBroker) loop() {
case v := <-b.pauseCh: case v := <-b.pauseCh:
// apply pause state // apply pause state
was := b.terminalPaused wasPaused := b.terminalPaused
b.terminalPaused = v b.terminalPaused = v
if was && !v { if wasPaused && !v {
// we just unpaused: flush buffered output in order // we just unpaused: flush buffered output in order
scopedLogger.Info().Msg("Terminal unpaused; flushing buffered output") scopedLogger.Trace().Msg("Terminal unpaused; flushing buffered output")
b.flushBuffer() b.flushBuffer()
} else if !was && v { } else if !wasPaused && v {
scopedLogger.Info().Msg("Terminal paused; buffering output") scopedLogger.Trace().Msg("Terminal paused; buffering output")
} }
case ev := <-b.in: case ev := <-b.in:
switch ev.kind { switch ev.kind {
case evRX: case evRX:
scopedLogger.Info().Msg("Processing RX data from serial port") scopedLogger.Trace().Msg("Processing RX data from serial port")
b.handleRX(ev.data) b.handleRX(ev.data)
case evTX: case evTX:
scopedLogger.Info().Msg("Processing TX echo request") scopedLogger.Trace().Msg("Processing TX echo request")
b.handleTX(ev.data) b.handleTX(ev.data)
} }
@ -367,10 +367,10 @@ func (b *ConsoleBroker) handleRX(data []byte) {
return return
} }
scopedLogger.Info().Msg("Emitting RX data to sink (with per-line prefixes)") scopedLogger.Trace().Msg("Emitting RX data to sink (with per-line prefixes)")
// Prefix every line, regardless of how the EOLs look // Prefix every line, regardless of how the EOLs look
lines := splitAfterAnyEOL(text, b.norm.CRLF) lines := splitAfterAnyEOL(text, b.norm.LineEnding)
// Start from the broker's current RX line state // Start from the broker's current RX line state
atLineEnd := b.rxAtLineEnd atLineEnd := b.rxAtLineEnd
@ -389,7 +389,7 @@ func (b *ConsoleBroker) handleRX(data []byte) {
} }
// Update line-end state based on this piece // Update line-end state based on this piece
atLineEnd = endsWithEOL(line, b.norm.CRLF) atLineEnd = endsWithEOL(line, b.norm.LineEnding)
} }
// Persist state for next RX chunk // Persist state for next RX chunk
@ -407,11 +407,11 @@ func (b *ConsoleBroker) handleTX(data []byte) {
return return
} }
if b.rxAtLineEnd && b.pendingTX == nil { if b.rxAtLineEnd && b.pendingTX == nil {
scopedLogger.Info().Msg("Emitting TX data to sink immediately") scopedLogger.Trace().Msg("Emitting TX data to sink immediately")
b.emitTX(data) b.emitTX(data)
return return
} }
scopedLogger.Info().Msg("Queuing TX data to emit after RX line completion or quiet period") scopedLogger.Trace().Msg("Queuing TX data to emit after RX line completion or quiet period")
b.pendingTX = &consoleEvent{kind: evTX, data: append([]byte(nil), data...)} b.pendingTX = &consoleEvent{kind: evTX, data: append([]byte(nil), data...)}
b.startQuietTimer() b.startQuietTimer()
} }
@ -430,12 +430,12 @@ func (b *ConsoleBroker) emitTX(data []byte) {
// Check if were in the middle of a TX line // Check if were in the middle of a TX line
if !b.txLineActive { if !b.txLineActive {
// Start new TX line with prefix // Start new TX line with prefix
scopedLogger.Info().Msg("Emitting TX data to sink with prefix") scopedLogger.Trace().Msg("Emitting TX data to sink with prefix")
b.emitToTerminal(fmt.Sprintf("%s: %s", b.labelTX, text)) b.emitToTerminal(fmt.Sprintf("%s: %s", b.labelTX, text))
b.txLineActive = true b.txLineActive = true
} else { } else {
// Continue current line (no prefix) // Continue current line (no prefix)
scopedLogger.Info().Msg("Emitting TX data to sink without prefix") scopedLogger.Trace().Msg("Emitting TX data to sink without prefix")
b.emitToTerminal(text) b.emitToTerminal(text)
} }
@ -455,14 +455,14 @@ func (b *ConsoleBroker) flushPendingTX() {
} }
func (b *ConsoleBroker) lineSep() string { func (b *ConsoleBroker) lineSep() string {
switch b.norm.CRLF { switch b.norm.LineEnding {
case CRLF_CRLF: case LineEnding_CRLF:
return "\r\n" return "\r\n"
case CRLF_LFCR: case LineEnding_LFCR:
return "\n\r" return "\n\r"
case CRLF_CR: case LineEnding_CR:
return "\r" return "\r"
case CRLF_LF: case LineEnding_LF:
return "\n" return "\n"
default: default:
return "\n" return "\n"
@ -470,26 +470,26 @@ func (b *ConsoleBroker) lineSep() string {
} }
// splitAfterAnyEOL splits text into lines keeping the EOL with each piece. // splitAfterAnyEOL splits text into lines keeping the EOL with each piece.
// For CRLFAsIs it treats \r, \n, \r\n, and \n\r as EOLs. // For LineEnding_AsIs it treats \r, \n, \r\n, and \n\r as EOLs.
// For other modes it uses the normalized separator. // For other modes it uses the normalized separator.
func splitAfterAnyEOL(text string, mode CRLFMode) []string { func splitAfterAnyEOL(text string, mode LineEndingMode) []string {
if text == "" { if text == "" {
return nil return nil
} }
// Fast path for normalized modes // Fast path for normalized modes
switch mode { switch mode {
case CRLF_LF: case LineEnding_LF:
return strings.SplitAfter(text, "\n") return strings.SplitAfter(text, "\n")
case CRLF_CR: case LineEnding_CR:
return strings.SplitAfter(text, "\r") return strings.SplitAfter(text, "\r")
case CRLF_CRLF: case LineEnding_CRLF:
return strings.SplitAfter(text, "\r\n") return strings.SplitAfter(text, "\r\n")
case CRLF_LFCR: case LineEnding_LFCR:
return strings.SplitAfter(text, "\n\r") return strings.SplitAfter(text, "\n\r")
} }
// CRLFAsIs: scan bytes and treat \r, \n, \r\n, \n\r as one boundary // LineEnding_AsIs: scan bytes and treat \r, \n, \r\n, \n\r as one boundary
b := []byte(text) b := []byte(text)
var parts []string var parts []string
start := 0 start := 0
@ -511,18 +511,18 @@ func splitAfterAnyEOL(text string, mode CRLFMode) []string {
return parts return parts
} }
func endsWithEOL(s string, mode CRLFMode) bool { func endsWithEOL(s string, mode LineEndingMode) bool {
if s == "" { if s == "" {
return false return false
} }
switch mode { switch mode {
case CRLF_CRLF: case LineEnding_CRLF:
return strings.HasSuffix(s, "\r\n") return strings.HasSuffix(s, "\r\n")
case CRLF_LFCR: case LineEnding_LFCR:
return strings.HasSuffix(s, "\n\r") return strings.HasSuffix(s, "\n\r")
case CRLF_LF: case LineEnding_LF:
return strings.HasSuffix(s, "\n") return strings.HasSuffix(s, "\n")
case CRLF_CR: case LineEnding_CR:
return strings.HasSuffix(s, "\r") return strings.HasSuffix(s, "\r")
default: // AsIs: any of \r, \n, \r\n, \n\r default: // AsIs: any of \r, \n, \r\n, \n\r
return strings.HasSuffix(s, "\r\n") || return strings.HasSuffix(s, "\r\n") ||
@ -606,7 +606,7 @@ func (m *SerialMux) Close() { close(m.done) }
func (m *SerialMux) SetEchoEnabled(v bool) { m.echoEnabled.Store(v) } func (m *SerialMux) SetEchoEnabled(v bool) { m.echoEnabled.Store(v) }
func (m *SerialMux) Enqueue(payload []byte, source string, requestEcho bool) { func (m *SerialMux) Enqueue(payload []byte, source string, requestEcho bool) {
serialLogger.Info().Str("src", source).Bool("echo", requestEcho).Msg("Enqueuing TX data to serial port") serialLogger.Trace().Str("src", source).Bool("echo", requestEcho).Msg("Enqueuing TX data to serial port")
m.txQ <- txFrame{payload: append([]byte(nil), payload...), source: source, echo: requestEcho} m.txQ <- txFrame{payload: append([]byte(nil), payload...), source: source, echo: requestEcho}
} }
@ -627,7 +627,7 @@ func (m *SerialMux) reader() {
continue continue
} }
if n > 0 && m.broker != nil { if n > 0 && m.broker != nil {
scopedLogger.Info().Msg("Sending RX data to console broker") scopedLogger.Trace().Msg("Sending RX data to console broker")
m.broker.Enqueue(consoleEvent{kind: evRX, data: append([]byte(nil), buf[:n]...)}) m.broker.Enqueue(consoleEvent{kind: evRX, data: append([]byte(nil), buf[:n]...)})
} }
} }
@ -641,14 +641,14 @@ func (m *SerialMux) writer() {
case <-m.done: case <-m.done:
return return
case f := <-m.txQ: case f := <-m.txQ:
scopedLogger.Info().Msg("Writing TX data to serial port") scopedLogger.Trace().Msg("Writing TX data to serial port")
if _, err := m.port.Write(f.payload); err != nil { if _, err := m.port.Write(f.payload); err != nil {
scopedLogger.Warn().Err(err).Str("src", f.source).Msg("serial write failed") scopedLogger.Warn().Err(err).Str("src", f.source).Msg("serial write failed")
continue continue
} }
// echo (if requested AND globally enabled) // echo (if requested AND globally enabled)
if f.echo && m.echoEnabled.Load() && m.broker != nil { if f.echo && m.echoEnabled.Load() && m.broker != nil {
scopedLogger.Info().Msg("Sending TX echo to console broker") scopedLogger.Trace().Msg("Sending TX echo to console broker")
m.broker.Enqueue(consoleEvent{kind: evTX, data: append([]byte(nil), f.payload...)}) m.broker.Enqueue(consoleEvent{kind: evTX, data: append([]byte(nil), f.payload...)})
} }
} }