Compare commits

..

1 Commits

Author SHA1 Message Date
Sevi 1b845fe141
Merge 7c09ac3c08 into 74e64f69a7 2025-10-16 16:25:38 +02:00
2 changed files with 96 additions and 84 deletions

View File

@ -255,9 +255,20 @@ 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.Debug().Msgf("Sending custom command: %q", command) scopedLogger.Info().Str("Command", command).Msg("Sending custom 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")
} }
@ -314,7 +325,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", "lfcr" NormalizeLineEnd string `json:"normalizeLineEnd"` // Line ending normalization: "keep", "lf", "cr", "crlf"
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
@ -347,7 +358,7 @@ func getSerialSettings() (SerialSettings, error) {
file, err := os.Open(serialSettingsPath) file, err := os.Open(serialSettingsPath)
if err != nil { if err != nil {
logger.Info().Msg("SerialButtons config file doesn't exist, using default") logger.Debug().Msg("SerialButtons config file doesn't exist, using default")
return serialConfig, err return serialConfig, err
} }
defer file.Close() defer file.Close()
@ -401,7 +412,7 @@ func getSerialSettings() (SerialSettings, error) {
var normalizeMode NormalizeMode var normalizeMode NormalizeMode
switch serialConfig.NormalizeMode { switch serialConfig.NormalizeMode {
case "caret": case "carret":
normalizeMode = ModeCaret normalizeMode = ModeCaret
case "names": case "names":
normalizeMode = ModeNames normalizeMode = ModeNames
@ -411,25 +422,25 @@ func getSerialSettings() (SerialSettings, error) {
normalizeMode = ModeNames normalizeMode = ModeNames
} }
var crlfMode LineEndingMode var crlfMode CRLFMode
switch serialConfig.NormalizeLineEnd { switch serialConfig.NormalizeLineEnd {
case "keep": case "keep":
crlfMode = LineEnding_AsIs crlfMode = CRLFAsIs
case "lf": case "lf":
crlfMode = LineEnding_LF crlfMode = CRLF_LF
case "cr": case "cr":
crlfMode = LineEnding_CR crlfMode = CRLF_CR
case "crlf": case "crlf":
crlfMode = LineEnding_CRLF crlfMode = CRLF_CRLF
case "lfcr": case "lfcr":
crlfMode = LineEnding_LFCR crlfMode = CRLF_LFCR
default: default:
crlfMode = LineEnding_AsIs crlfMode = CRLFAsIs
} }
if consoleBroker != nil { if consoleBroker != nil {
norm := NormalizationOptions{ norm := NormOptions{
Mode: normalizeMode, LineEnding: crlfMode, TabRender: serialConfig.TabRender, PreserveANSI: serialConfig.PreserveANSI, ShowNLTag: serialConfig.ShowNLTag, Mode: normalizeMode, CRLF: crlfMode, TabRender: serialConfig.TabRender, PreserveANSI: serialConfig.PreserveANSI, ShowNLTag: serialConfig.ShowNLTag,
} }
consoleBroker.SetNormOptions(norm) consoleBroker.SetNormOptions(norm)
} }
@ -496,7 +507,7 @@ func setSerialSettings(newSettings SerialSettings) error {
var normalizeMode NormalizeMode var normalizeMode NormalizeMode
switch serialConfig.NormalizeMode { switch serialConfig.NormalizeMode {
case "caret": case "carret":
normalizeMode = ModeCaret normalizeMode = ModeCaret
case "names": case "names":
normalizeMode = ModeNames normalizeMode = ModeNames
@ -506,25 +517,25 @@ func setSerialSettings(newSettings SerialSettings) error {
normalizeMode = ModeNames normalizeMode = ModeNames
} }
var crlfMode LineEndingMode var crlfMode CRLFMode
switch serialConfig.NormalizeLineEnd { switch serialConfig.NormalizeLineEnd {
case "keep": case "keep":
crlfMode = LineEnding_AsIs crlfMode = CRLFAsIs
case "lf": case "lf":
crlfMode = LineEnding_LF crlfMode = CRLF_LF
case "cr": case "cr":
crlfMode = LineEnding_CR crlfMode = CRLF_CR
case "crlf": case "crlf":
crlfMode = LineEnding_CRLF crlfMode = CRLF_CRLF
case "lfcr": case "lfcr":
crlfMode = LineEnding_LFCR crlfMode = CRLF_LFCR
default: default:
crlfMode = LineEnding_AsIs crlfMode = CRLFAsIs
} }
if consoleBroker != nil { if consoleBroker != nil {
norm := NormalizationOptions{ norm := NormOptions{
Mode: normalizeMode, LineEnding: crlfMode, TabRender: serialConfig.TabRender, PreserveANSI: serialConfig.PreserveANSI, ShowNLTag: serialConfig.ShowNLTag, Mode: normalizeMode, CRLF: crlfMode, TabRender: serialConfig.TabRender, PreserveANSI: serialConfig.PreserveANSI, ShowNLTag: serialConfig.ShowNLTag,
} }
consoleBroker.SetNormOptions(norm) consoleBroker.SetNormOptions(norm)
} }
@ -564,8 +575,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 := NormalizationOptions{ norm := NormOptions{
Mode: ModeNames, LineEnding: LineEnding_LF, TabRender: "", PreserveANSI: true, Mode: ModeNames, CRLF: CRLF_LF, TabRender: "", PreserveANSI: true,
} }
if consoleBroker != nil { if consoleBroker != nil {
consoleBroker.Close() consoleBroker.Close()
@ -601,7 +612,8 @@ func handleSerialChannel(dataChannel *webrtc.DataChannel) {
dataChannel.OnMessage(func(msg webrtc.DataChannelMessage) { dataChannel.OnMessage(func(msg webrtc.DataChannelMessage) {
scopedLogger.Trace().Bytes("Data:", msg.Data).Msg("Sending data to serial mux") scopedLogger.Info().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 LineEndingMode int type CRLFMode int
const ( const (
LineEnding_AsIs LineEndingMode = iota CRLFAsIs CRLFMode = iota
LineEnding_LF CRLF_LF
LineEnding_CR CRLF_CR
LineEnding_CRLF CRLF_CRLF
LineEnding_LFCR CRLF_LFCR
) )
type NormalizationOptions struct { type NormOptions struct {
Mode NormalizeMode Mode NormalizeMode
LineEnding LineEndingMode CRLF CRLFMode
TabRender string // e.g. " " or "" to keep '\t' TabRender string // e.g. " " or "" to keep '\t'
PreserveANSI bool PreserveANSI bool
ShowNLTag bool // print a visible tag for CR/LF like <CR>, <LF>, <CRLF> ShowNLTag bool // <- NEW: also print a visible tag for CR/LF
} }
func normalize(in []byte, opt NormalizationOptions) string { func normalize(in []byte, opt NormOptions) 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 NormalizationOptions) string {
} }
// now emit the actual newline(s) per the normalization mode // now emit the actual newline(s) per the normalization mode
switch opt.LineEnding { switch opt.CRLF {
case LineEnding_AsIs: case CRLFAsIs:
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 NormalizationOptions) string {
out.WriteByte(b) out.WriteByte(b)
i++ i++
} }
case LineEnding_LF: case CRLF_LF:
if isPair { if isPair {
i += 2 i += 2
} else { } else {
i++ i++
} }
out.WriteByte('\n') out.WriteByte('\n')
case LineEnding_CR: case CRLF_CR:
if isPair { if isPair {
i += 2 i += 2
} else { } else {
i++ i++
} }
out.WriteByte('\r') out.WriteByte('\r')
case LineEnding_CRLF: case CRLF_CRLF:
if isPair { if isPair {
i += 2 i += 2
} else { } else {
i++ i++
} }
out.WriteString("\r\n") out.WriteString("\r\n") // (fixed to actually write CRLF)
case LineEnding_LFCR: case CRLF_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 NormalizationOptions norm NormOptions
// labels // labels
labelRX string labelRX string
labelTX string labelTX string
} }
func NewConsoleBroker(s Sink, norm NormalizationOptions) *ConsoleBroker { func NewConsoleBroker(s Sink, norm NormOptions) *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 NormalizationOptions) *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 NormalizationOptions) { b.norm = norm } func (b *ConsoleBroker) SetNormOptions(norm NormOptions) { 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
wasPaused := b.terminalPaused was := b.terminalPaused
b.terminalPaused = v b.terminalPaused = v
if wasPaused && !v { if was && !v {
// we just unpaused: flush buffered output in order // we just unpaused: flush buffered output in order
scopedLogger.Trace().Msg("Terminal unpaused; flushing buffered output") scopedLogger.Info().Msg("Terminal unpaused; flushing buffered output")
b.flushBuffer() b.flushBuffer()
} else if !wasPaused && v { } else if !was && v {
scopedLogger.Trace().Msg("Terminal paused; buffering output") scopedLogger.Info().Msg("Terminal paused; buffering output")
} }
case ev := <-b.in: case ev := <-b.in:
switch ev.kind { switch ev.kind {
case evRX: case evRX:
scopedLogger.Trace().Msg("Processing RX data from serial port") scopedLogger.Info().Msg("Processing RX data from serial port")
b.handleRX(ev.data) b.handleRX(ev.data)
case evTX: case evTX:
scopedLogger.Trace().Msg("Processing TX echo request") scopedLogger.Info().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.Trace().Msg("Emitting RX data to sink (with per-line prefixes)") scopedLogger.Info().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.LineEnding) lines := splitAfterAnyEOL(text, b.norm.CRLF)
// 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.LineEnding) atLineEnd = endsWithEOL(line, b.norm.CRLF)
} }
// 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.Trace().Msg("Emitting TX data to sink immediately") scopedLogger.Info().Msg("Emitting TX data to sink immediately")
b.emitTX(data) b.emitTX(data)
return return
} }
scopedLogger.Trace().Msg("Queuing TX data to emit after RX line completion or quiet period") scopedLogger.Info().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.Trace().Msg("Emitting TX data to sink with prefix") scopedLogger.Info().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.Trace().Msg("Emitting TX data to sink without prefix") scopedLogger.Info().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.LineEnding { switch b.norm.CRLF {
case LineEnding_CRLF: case CRLF_CRLF:
return "\r\n" return "\r\n"
case LineEnding_LFCR: case CRLF_LFCR:
return "\n\r" return "\n\r"
case LineEnding_CR: case CRLF_CR:
return "\r" return "\r"
case LineEnding_LF: case CRLF_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 LineEnding_AsIs it treats \r, \n, \r\n, and \n\r as EOLs. // For CRLFAsIs 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 LineEndingMode) []string { func splitAfterAnyEOL(text string, mode CRLFMode) []string {
if text == "" { if text == "" {
return nil return nil
} }
// Fast path for normalized modes // Fast path for normalized modes
switch mode { switch mode {
case LineEnding_LF: case CRLF_LF:
return strings.SplitAfter(text, "\n") return strings.SplitAfter(text, "\n")
case LineEnding_CR: case CRLF_CR:
return strings.SplitAfter(text, "\r") return strings.SplitAfter(text, "\r")
case LineEnding_CRLF: case CRLF_CRLF:
return strings.SplitAfter(text, "\r\n") return strings.SplitAfter(text, "\r\n")
case LineEnding_LFCR: case CRLF_LFCR:
return strings.SplitAfter(text, "\n\r") return strings.SplitAfter(text, "\n\r")
} }
// LineEnding_AsIs: scan bytes and treat \r, \n, \r\n, \n\r as one boundary // CRLFAsIs: 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 LineEndingMode) []string {
return parts return parts
} }
func endsWithEOL(s string, mode LineEndingMode) bool { func endsWithEOL(s string, mode CRLFMode) bool {
if s == "" { if s == "" {
return false return false
} }
switch mode { switch mode {
case LineEnding_CRLF: case CRLF_CRLF:
return strings.HasSuffix(s, "\r\n") return strings.HasSuffix(s, "\r\n")
case LineEnding_LFCR: case CRLF_LFCR:
return strings.HasSuffix(s, "\n\r") return strings.HasSuffix(s, "\n\r")
case LineEnding_LF: case CRLF_LF:
return strings.HasSuffix(s, "\n") return strings.HasSuffix(s, "\n")
case LineEnding_CR: case CRLF_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.Trace().Str("src", source).Bool("echo", requestEcho).Msg("Enqueuing TX data to serial port") serialLogger.Info().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.Trace().Msg("Sending RX data to console broker") scopedLogger.Info().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.Trace().Msg("Writing TX data to serial port") scopedLogger.Info().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.Trace().Msg("Sending TX echo to console broker") scopedLogger.Info().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...)})
} }
} }