Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
b8d4464
Create new extension "Serial Buttons"
sevimuelli Sep 22, 2025
b2b3ee4
Merge branch 'jetkvm:dev' into dev
sevimuelli Sep 22, 2025
d8f670f
Add backend to send custom commands
sevimuelli Sep 23, 2025
67e9136
Add order buttons and response field
sevimuelli Sep 23, 2025
cfd5e7c
Merge branch 'jetkvm:dev' into feat/custom-serial-buttons
sevimuelli Sep 24, 2025
c07ae51
Merge extensions "Serial Console" and "Serial Buttons"
sevimuelli Oct 1, 2025
c2219d1
Merge branch 'dev' into feat/custom-serial-buttons
sevimuelli Oct 1, 2025
2b6571d
Update backend to combine serial console and custom buttons
sevimuelli Oct 2, 2025
897927e
Merge branch 'jetkvm:dev' into feat/custom-serial-buttons
sevimuelli Oct 2, 2025
4ddce3f
Merge branch 'jetkvm:dev' into feat/custom-serial-buttons
sevimuelli Oct 3, 2025
3b14267
Update backend, implement pause function in terminal
sevimuelli Oct 9, 2025
7b9410c
Merge branch 'jetkvm:dev' into feat/custom-serial-buttons
sevimuelli Oct 9, 2025
2ce5623
Improve normalization
sevimuelli Oct 9, 2025
e556530
Minor serial helper improvements
sevimuelli Oct 9, 2025
7c09ac3
Merge branch 'dev' into feat/custom-serial-buttons
sevimuelli Oct 9, 2025
0630a7b
Update serial console part
sevimuelli Oct 16, 2025
39e67f3
Merge branch 'jetkvm:dev' into feat/custom-serial-buttons
sevimuelli Oct 16, 2025
668ca59
Small bug fixes
sevimuelli Oct 17, 2025
86415bc
Merge branch 'dev' into feat/custom-serial-buttons
sevimuelli Nov 1, 2025
cee8d64
Add localization
sevimuelli Nov 1, 2025
8135a38
Merge branch 'jetkvm:dev' into feat/custom-serial-buttons
sevimuelli Nov 9, 2025
d67d465
Merge branch 'dev' into feat/custom-serial-buttons
sevimuelli Dec 1, 2025
7e241b1
Fix linting error
sevimuelli Dec 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions jsonrpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -758,6 +758,8 @@ func rpcSetActiveExtension(extensionId string) error {
_ = unmountATXControl()
case "dc-power":
_ = unmountDCControl()
case "serial-buttons":
_ = unmountSerialButtons()
}
config.ActiveExtension = extensionId
if err := SaveConfig(); err != nil {
Expand All @@ -768,6 +770,8 @@ func rpcSetActiveExtension(extensionId string) error {
_ = mountATXControl()
case "dc-power":
_ = mountDCControl()
case "serial-buttons":
_ = mountSerialButtons()
}
return nil
}
Expand Down Expand Up @@ -802,6 +806,15 @@ func rpcGetATXState() (ATXState, error) {
return state, nil
}

func rpcSendCustomCommand(command string) error {
logger.Debug().Str("Command", command).Msg("JSONRPC: Sending custom serial command")
err := sendCustomCommand(command)
if err != nil {
return fmt.Errorf("failed to send custom command in jsonrpc: %w", err)
}
return nil
}

type SerialSettings struct {
BaudRate string `json:"baudRate"`
DataBits string `json:"dataBits"`
Expand Down Expand Up @@ -893,6 +906,64 @@ func rpcSetSerialSettings(settings SerialSettings) error {
return nil
}

type QuickButton struct {
Id string `json:"id"` // uuid-ish
Label string `json:"label"` // shown on the button
Command string `json:"command"` // raw command to send (without auto-terminator)
Sort int `json:"sort"` // for stable ordering
}

type SerialButtonConfig struct {
Buttons []QuickButton `json:"buttons"` // slice of QuickButton
Terminator string `json:"terminator"` // CR/CRLF/None
HideSerialSettings bool `json:"hideSerialSettings"` // lowercase `bool`
HideSerialResponse bool `json:"hideSerialResponse"` // lowercase `bool`
}

func rpcGetSerialButtonConfig() (SerialButtonConfig, error) {
config := SerialButtonConfig{
Buttons: []QuickButton{},
Terminator: "\r",
HideSerialSettings: false,
HideSerialResponse: true,
}

file, err := os.Open("/userdata/serialButtons_config.json")
if err != nil {
logger.Debug().Msg("SerialButtons config file doesn't exist, using default")
return config, nil
}
defer file.Close()

// load and merge the default config with the user config
var loadedConfig SerialButtonConfig
if err := json.NewDecoder(file).Decode(&loadedConfig); err != nil {
logger.Warn().Err(err).Msg("SerialButtons config file JSON parsing failed")
return config, nil
}

return loadedConfig, nil
}

func rpcSetSerialButtonConfig(config SerialButtonConfig) error {

logger.Trace().Str("path", "/userdata/serialButtons_config.json").Msg("Saving config")

file, err := os.Create("/userdata/serialButtons_config.json")
if err != nil {
return fmt.Errorf("failed to create SerialButtons config file: %w", err)
}
defer file.Close()

encoder := json.NewEncoder(file)
encoder.SetIndent("", " ")
if err := encoder.Encode(config); err != nil {
return fmt.Errorf("failed to encode SerialButtons config: %w", err)
}

return nil
}

func rpcGetUsbDevices() (usbgadget.Devices, error) {
return *config.UsbDevices, nil
}
Expand Down Expand Up @@ -1240,8 +1311,11 @@ var rpcHandlers = map[string]RPCHandler{
"setActiveExtension": {Func: rpcSetActiveExtension, Params: []string{"extensionId"}},
"getATXState": {Func: rpcGetATXState},
"setATXPowerAction": {Func: rpcSetATXPowerAction, Params: []string{"action"}},
"sendCustomCommand": {Func: rpcSendCustomCommand, Params: []string{"command"}},
"getSerialSettings": {Func: rpcGetSerialSettings},
"setSerialSettings": {Func: rpcSetSerialSettings, Params: []string{"settings"}},
"getSerialButtonConfig": {Func: rpcGetSerialButtonConfig},
"setSerialButtonConfig": {Func: rpcSetSerialButtonConfig, Params: []string{"config"}},
"getUsbDevices": {Func: rpcGetUsbDevices},
"setUsbDevices": {Func: rpcSetUsbDevices, Params: []string{"devices"}},
"setUsbDeviceState": {Func: rpcSetUsbDeviceState, Params: []string{"device", "enabled"}},
Expand Down
78 changes: 78 additions & 0 deletions serial.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package kvm

import (
"bufio"
"encoding/base64"
"io"
"strconv"
"strings"
Expand Down Expand Up @@ -251,6 +252,83 @@ func setDCRestoreState(state int) error {
return nil
}

func mountSerialButtons() error {
_ = port.SetMode(defaultMode)
startSerialButtonsRxLoop(currentSession)
return nil
}

func unmountSerialButtons() error {
stopSerialButtonsRxLoop()
_ = reopenSerialPort()
return nil
}

// ---- Serial Buttons RX fan-out (JSON-RPC events) ----
var serialButtonsRXStopCh chan struct{}

func startSerialButtonsRxLoop(session *Session) {
scopedLogger := serialLogger.With().Str("service", "custom_buttons_rx").Logger()
scopedLogger.Debug().Msg("Attempting to start RX reader.")
// Stop previous loop if running
if serialButtonsRXStopCh != nil {
close(serialButtonsRXStopCh)
}
serialButtonsRXStopCh = make(chan struct{})

go func() {
buf := make([]byte, 4096)
scopedLogger.Debug().Msg("Starting loop")

for {
select {
case <-serialButtonsRXStopCh:
return
default:
n, err := port.Read(buf)
if err != nil {
if err != io.EOF {
scopedLogger.Debug().Err(err).Msg("serial RX read error")
}
time.Sleep(50 * time.Millisecond)
continue
}
if n == 0 || currentSession == nil {
continue
}
// Safe for any bytes: wrap in Base64
b64 := base64.StdEncoding.EncodeToString(buf[:n])
writeJSONRPCEvent("serial.rx", map[string]any{
"base64": b64,
}, currentSession)
}
}
}()
}

func stopSerialButtonsRxLoop() {
if serialButtonsRXStopCh != nil {
close(serialButtonsRXStopCh)
serialButtonsRXStopCh = nil
}
}

func sendCustomCommand(command string) error {
scopedLogger := serialLogger.With().Str("service", "custom_buttons_tx").Logger()
scopedLogger.Info().Str("Command", command).Msg("Sending custom command.")
_, err := port.Write([]byte("\n"))
if err != nil {
scopedLogger.Warn().Err(err).Msg("Failed to send serial output \\n")
return err
}
_, err = port.Write([]byte(command))
if err != nil {
scopedLogger.Warn().Err(err).Str("line", command).Msg("Failed to send serial output")
return err
}
return nil
}

var defaultMode = &serial.Mode{
BaudRate: 115200,
DataBits: 8,
Expand Down
Loading