package kvm import ( "bufio" "io" "strconv" "strings" "time" "github.com/pion/webrtc/v4" "go.bug.st/serial" ) const serialPortPath = "/dev/ttyS3" var port serial.Port func mountATXControl() error { _ = port.SetMode(defaultMode) go runATXControl() return nil } func unmountATXControl() error { _ = reopenSerialPort() return nil } var ( ledHDDState bool ledPWRState bool btnRSTState bool btnPWRState bool ) func runATXControl() { scopedLogger := serialLogger.With().Str("service", "atx_control").Logger() reader := bufio.NewReader(port) for { line, err := reader.ReadString('\n') if err != nil { scopedLogger.Warn().Err(err).Msg("Error reading from serial port") return } // Each line should be 4 binary digits + newline if len(line) != 5 { scopedLogger.Warn().Int("length", len(line)).Msg("Invalid line length") continue } // Parse new states newLedHDDState := line[0] == '0' newLedPWRState := line[1] == '0' newBtnRSTState := line[2] == '1' newBtnPWRState := line[3] == '1' if currentSession != nil { writeJSONRPCEvent("atxState", ATXState{ Power: newLedPWRState, HDD: newLedHDDState, }, currentSession) } if newLedHDDState != ledHDDState || newLedPWRState != ledPWRState || newBtnRSTState != btnRSTState || newBtnPWRState != btnPWRState { scopedLogger.Debug(). Bool("hdd", newLedHDDState). Bool("pwr", newLedPWRState). Bool("rst", newBtnRSTState). Bool("pwr", newBtnPWRState). Msg("Status changed") // Update states ledHDDState = newLedHDDState ledPWRState = newLedPWRState btnRSTState = newBtnRSTState btnPWRState = newBtnPWRState } } } func pressATXPowerButton(duration time.Duration) error { _, err := port.Write([]byte("\n")) if err != nil { return err } _, err = port.Write([]byte("BTN_PWR_ON\n")) if err != nil { return err } time.Sleep(duration) _, err = port.Write([]byte("BTN_PWR_OFF\n")) if err != nil { return err } return nil } func pressATXResetButton(duration time.Duration) error { _, err := port.Write([]byte("\n")) if err != nil { return err } _, err = port.Write([]byte("BTN_RST_ON\n")) if err != nil { return err } time.Sleep(duration) _, err = port.Write([]byte("BTN_RST_OFF\n")) if err != nil { return err } return nil } func mountDCControl() error { _ = port.SetMode(defaultMode) go runDCControl() return nil } func unmountDCControl() error { _ = reopenSerialPort() return nil } var dcState DCPowerState func runDCControl() { scopedLogger := serialLogger.With().Str("service", "dc_control").Logger() reader := bufio.NewReader(port) for { line, err := reader.ReadString('\n') if err != nil { scopedLogger.Warn().Err(err).Msg("Error reading from serial port") return } // Split the line by semicolon parts := strings.Split(strings.TrimSpace(line), ";") if len(parts) != 4 { scopedLogger.Warn().Str("line", line).Msg("Invalid line") continue } // Parse new states powerState, err := strconv.Atoi(parts[0]) if err != nil { scopedLogger.Warn().Err(err).Msg("Invalid power state") continue } dcState.IsOn = powerState == 1 milliVolts, err := strconv.ParseFloat(parts[1], 64) if err != nil { scopedLogger.Warn().Err(err).Msg("Invalid voltage") continue } volts := milliVolts / 1000 // Convert mV to V milliAmps, err := strconv.ParseFloat(parts[2], 64) if err != nil { scopedLogger.Warn().Err(err).Msg("Invalid current") continue } amps := milliAmps / 1000 // Convert mA to A milliWatts, err := strconv.ParseFloat(parts[3], 64) if err != nil { scopedLogger.Warn().Err(err).Msg("Invalid power") continue } watts := milliWatts / 1000 // Convert mW to W dcState.Voltage = volts dcState.Current = amps dcState.Power = watts if currentSession != nil { writeJSONRPCEvent("dcState", dcState, currentSession) } } } func setDCPowerState(on bool) error { _, err := port.Write([]byte("\n")) if err != nil { return err } command := "PWR_OFF\n" if on { command = "PWR_ON\n" } _, err = port.Write([]byte(command)) if err != nil { return err } return nil } var defaultMode = &serial.Mode{ BaudRate: 115200, DataBits: 8, Parity: serial.NoParity, StopBits: serial.OneStopBit, } func initSerialPort() { _ = reopenSerialPort() switch config.ActiveExtension { case "atx-power": _ = mountATXControl() case "dc-power": _ = mountDCControl() } } func reopenSerialPort() error { if port != nil { port.Close() } var err error port, err = serial.Open(serialPortPath, defaultMode) if err != nil { serialLogger.Error(). Err(err). Str("path", serialPortPath). Interface("mode", defaultMode). Msg("Error opening serial port") } return nil } func handleSerialChannel(d *webrtc.DataChannel) { scopedLogger := serialLogger.With(). Uint16("data_channel_id", *d.ID()).Logger() d.OnOpen(func() { go func() { buf := make([]byte, 1024) for { n, err := port.Read(buf) if err != nil { if err != io.EOF { scopedLogger.Warn().Err(err).Msg("Failed to read from serial port") } break } err = d.Send(buf[:n]) if err != nil { scopedLogger.Warn().Err(err).Msg("Failed to send serial output") break } } }() }) d.OnMessage(func(msg webrtc.DataChannelMessage) { if port == nil { return } _, err := port.Write(msg.Data) if err != nil { scopedLogger.Warn().Err(err).Msg("Failed to write to serial") } }) d.OnError(func(err error) { scopedLogger.Warn().Err(err).Msg("Serial channel error") }) d.OnClose(func() { scopedLogger.Info().Msg("Serial channel closed") }) }