295 lines
8 KiB
Go
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
|
|
}
|