rs2322tcp/internal/config/config.go
2026-08-10 16:20:28 +02:00

295 lines
8 KiB
Go

/*
Package config provides configuration types and JSON handling for rs2322tcp.
The server configuration describes the physical serial devices available
on the remote system. The client configuration describes the connection to
the server and the user's assignment of virtual serial ports to remote
devices.
Project: rs2322tcp
Module: git.lang-dieter.de/rs2322tcp
*/
package config
import (
"encoding/json"
"fmt"
"os"
)
///////////////////////////////////////////////////////////////////////////////
// Server configuration
///////////////////////////////////////////////////////////////////////////////
// ServerConfig contains the complete server configuration.
type ServerConfig struct {
Listen ListenConfig `json:"listen"`
Devices []DeviceConfig `json:"devices"`
}
// ListenConfig contains the network listener configuration.
type ListenConfig struct {
Address string `json:"address"`
Port int `json:"port"`
}
// DeviceConfig describes one physical RS232 device connected to the server.
type DeviceConfig struct {
ID string `json:"id"`
Name string `json:"name"`
SerialPort string `json:"serial_port"`
BaudRate int `json:"baud_rate"`
DataBits int `json:"data_bits"`
Parity string `json:"parity"`
StopBits int `json:"stop_bits"`
}
///////////////////////////////////////////////////////////////////////////////
// Client configuration
///////////////////////////////////////////////////////////////////////////////
// ClientConfig contains the complete client configuration.
type ClientConfig struct {
Server ServerConnectionConfig `json:"server"`
VirtualPortRange VirtualPortRangeConfig `json:"virtual_port_range"`
VirtualPorts []VirtualPortConfig `json:"virtual_ports"`
}
// ServerConnectionConfig contains the connection information for the
// rs2322tcp server.
type ServerConnectionConfig struct {
Address string `json:"address"`
Port int `json:"port"`
}
// VirtualPortRangeConfig defines the reserved range of /dev/ttyUSB device
// numbers that may be used for virtual rs2322tcp ports.
type VirtualPortRangeConfig struct {
First int `json:"first"`
Last int `json:"last"`
}
// VirtualPortConfig describes one local virtual serial port and the
// remote device assigned to it.
type VirtualPortConfig struct {
Port string `json:"port"`
RemoteDevice string `json:"remote_device"`
}
///////////////////////////////////////////////////////////////////////////////
// Defaults
///////////////////////////////////////////////////////////////////////////////
const (
// DefaultVirtualPortFirst is the first virtual USB serial port number
// used when no virtual port range is specified.
DefaultVirtualPortFirst = 100
// DefaultVirtualPortLast is the last virtual USB serial port number
// used when no virtual port range is specified.
DefaultVirtualPortLast = 199
)
// DefaultVirtualPortRange returns the default virtual port range.
func DefaultVirtualPortRange() VirtualPortRangeConfig {
return VirtualPortRangeConfig{
First: DefaultVirtualPortFirst,
Last: DefaultVirtualPortLast,
}
}
///////////////////////////////////////////////////////////////////////////////
// JSON loading
///////////////////////////////////////////////////////////////////////////////
// LoadServer loads a server configuration from a JSON file.
func LoadServer(filename string) (*ServerConfig, error) {
var cfg ServerConfig
if err := loadJSON(filename, &cfg); err != nil {
return nil, err
}
if err := cfg.Validate(); err != nil {
return nil, err
}
return &cfg, nil
}
// LoadClient loads a client configuration from a JSON file.
func LoadClient(filename string) (*ClientConfig, error) {
var cfg ClientConfig
if err := loadJSON(filename, &cfg); err != nil {
return nil, err
}
// A missing virtual_port_range is intentionally supported for backwards
// compatibility. Apply the documented default before validation.
if cfg.VirtualPortRange.First == 0 && cfg.VirtualPortRange.Last == 0 {
cfg.VirtualPortRange = DefaultVirtualPortRange()
}
if err := cfg.Validate(); err != nil {
return nil, err
}
return &cfg, nil
}
///////////////////////////////////////////////////////////////////////////////
// JSON saving
///////////////////////////////////////////////////////////////////////////////
// SaveServer saves a server configuration as formatted JSON.
func SaveServer(filename string, cfg *ServerConfig) error {
if cfg == nil {
return fmt.Errorf("server configuration is nil")
}
if err := cfg.Validate(); err != nil {
return err
}
return saveJSON(filename, cfg)
}
// SaveClient saves a client configuration as formatted JSON.
func SaveClient(filename string, cfg *ClientConfig) error {
if cfg == nil {
return fmt.Errorf("client configuration is nil")
}
if cfg.VirtualPortRange.First == 0 && cfg.VirtualPortRange.Last == 0 {
cfg.VirtualPortRange = DefaultVirtualPortRange()
}
if err := cfg.Validate(); err != nil {
return err
}
return saveJSON(filename, cfg)
}
///////////////////////////////////////////////////////////////////////////////
// Validation
///////////////////////////////////////////////////////////////////////////////
// Validate checks the server configuration for basic errors.
func (cfg *ServerConfig) Validate() error {
if cfg == nil {
return fmt.Errorf("server configuration is nil")
}
if cfg.Listen.Port < 1 || cfg.Listen.Port > 65535 {
return fmt.Errorf("invalid listen port: %d", cfg.Listen.Port)
}
ids := make(map[string]bool)
for i, device := range cfg.Devices {
if device.ID == "" {
return fmt.Errorf("device %d: ID is empty", i)
}
if device.SerialPort == "" {
return fmt.Errorf("device %q: serial port is empty", device.ID)
}
if ids[device.ID] {
return fmt.Errorf("duplicate device ID: %q", device.ID)
}
ids[device.ID] = true
if device.BaudRate <= 0 {
return fmt.Errorf("device %q: invalid baud rate", device.ID)
}
if device.DataBits < 5 || device.DataBits > 8 {
return fmt.Errorf("device %q: invalid data bits", device.ID)
}
if device.StopBits < 1 || device.StopBits > 2 {
return fmt.Errorf("device %q: invalid stop bits", device.ID)
}
}
return nil
}
// Validate checks the client configuration for basic errors.
func (cfg *ClientConfig) Validate() error {
if cfg == nil {
return fmt.Errorf("client configuration is nil")
}
if cfg.Server.Port < 1 || cfg.Server.Port > 65535 {
return fmt.Errorf("invalid server port: %d", cfg.Server.Port)
}
if cfg.VirtualPortRange.First < DefaultVirtualPortFirst {
return fmt.Errorf("virtual port range first must be >= %d: %d",
DefaultVirtualPortFirst, cfg.VirtualPortRange.First)
}
if cfg.VirtualPortRange.Last < cfg.VirtualPortRange.First {
return fmt.Errorf("invalid virtual port range: %d-%d",
cfg.VirtualPortRange.First, cfg.VirtualPortRange.Last)
}
ports := make(map[string]bool)
for i, virtualPort := range cfg.VirtualPorts {
if virtualPort.Port == "" {
return fmt.Errorf("virtual port %d: port is empty", i)
}
if virtualPort.RemoteDevice == "" {
return fmt.Errorf("virtual port %q: remote device is empty",
virtualPort.Port)
}
if ports[virtualPort.Port] {
return fmt.Errorf("duplicate virtual port: %q",
virtualPort.Port)
}
ports[virtualPort.Port] = true
}
return nil
}
///////////////////////////////////////////////////////////////////////////////
// Internal JSON helpers
///////////////////////////////////////////////////////////////////////////////
func loadJSON(filename string, target interface{}) error {
data, err := os.ReadFile(filename)
if err != nil {
return fmt.Errorf("read configuration %q: %w", filename, err)
}
if err := json.Unmarshal(data, target); err != nil {
return fmt.Errorf("parse configuration %q: %w", filename, err)
}
return nil
}
func saveJSON(filename string, value interface{}) error {
data, err := json.MarshalIndent(value, "", " ")
if err != nil {
return fmt.Errorf("encode configuration: %w", err)
}
data = append(data, '\n')
if err := os.WriteFile(filename, data, 0644); err != nil {
return fmt.Errorf("write configuration %q: %w", filename, err)
}
return nil
}