Add working hardware connectivity and SPID rotor support
This commit is contained in:
parent
1621ed4330
commit
8d98e85453
6 changed files with 332 additions and 16 deletions
186
README.md
186
README.md
|
|
@ -184,6 +184,192 @@ analysieren zu können.
|
|||
Während der Entwicklung kann `socat` als zusätzliches Werkzeug für Tests
|
||||
und Diagnose eingesetzt werden.
|
||||
|
||||
## Hardwaretest mit SPID Rot2Prog
|
||||
|
||||
Für den praktischen Hardwaretest wurde ein SPID Rot2Prog als
|
||||
Antennenrotor über einen FT232R USB-to-RS232-Adapter angeschlossen.
|
||||
|
||||
Unter Linux wurde der Adapter als:
|
||||
|
||||
```text
|
||||
/dev/ttyUSB0
|
||||
```
|
||||
|
||||
erkannt.
|
||||
|
||||
Der Rot2Prog verwendet für die RS232-Kommunikation:
|
||||
|
||||
```text
|
||||
600 Baud
|
||||
8 Datenbits
|
||||
keine Parität
|
||||
1 Stopbit
|
||||
```
|
||||
|
||||
also:
|
||||
|
||||
```text
|
||||
600 8N1
|
||||
```
|
||||
|
||||
### Rotor einschalten
|
||||
|
||||
Vor dem Kommunikationstest muss der SPID-Rot2Prog eingeschaltet sein.
|
||||
|
||||
Der Controller muss für die serielle Steuerung im SPID-/Auto-Betrieb
|
||||
betrieben werden.
|
||||
|
||||
Der Zustand des Controllers kann über die vorhandenen Bedientasten
|
||||
und die LED-Anzeigen kontrolliert werden.
|
||||
|
||||
### Direkter Hardwaretest
|
||||
|
||||
Der serielle Port kann zunächst unabhängig von `rs2322tcp` getestet
|
||||
werden:
|
||||
|
||||
```bash
|
||||
stty -F /dev/ttyUSB0 600 cs8 -cstopb -parenb raw -echo
|
||||
```
|
||||
|
||||
Kontrolle:
|
||||
|
||||
```bash
|
||||
stty -F /dev/ttyUSB0
|
||||
```
|
||||
|
||||
Anschließend kann die serielle Antwort des Controllers beobachtet werden:
|
||||
|
||||
```bash
|
||||
cat /dev/ttyUSB0 | xxd -g 1
|
||||
```
|
||||
|
||||
Eine Statusabfrage kann beispielsweise mit folgendem Paket gesendet
|
||||
werden:
|
||||
|
||||
```bash
|
||||
printf '\x57\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1f\x20' > /dev/ttyUSB0
|
||||
```
|
||||
|
||||
Bei erfolgreicher Kommunikation antwortet der Rot2Prog mit einem
|
||||
12-Byte-Datenpaket.
|
||||
|
||||
### Test über rs2322tcp
|
||||
|
||||
Nach erfolgreichem direkten Hardwaretest wird der reale serielle Port
|
||||
im Server konfiguriert:
|
||||
|
||||
```text
|
||||
Rotor:
|
||||
/dev/ttyUSB0
|
||||
600 Baud
|
||||
8 Datenbits
|
||||
keine Parität
|
||||
1 Stopbit
|
||||
```
|
||||
|
||||
Der Client stellt dem Anwender dafür den virtuellen Port:
|
||||
|
||||
```text
|
||||
/dev/ttyUSB101
|
||||
```
|
||||
|
||||
zur Verfügung.
|
||||
|
||||
Die gleiche Statusabfrage kann anschließend über den vollständigen
|
||||
rs2322tcp-Datenpfad gesendet werden:
|
||||
|
||||
```bash
|
||||
printf '\x57\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1f\x20' > /dev/ttyUSB101
|
||||
```
|
||||
|
||||
Der Datenweg ist dann:
|
||||
|
||||
```text
|
||||
/dev/ttyUSB101
|
||||
│
|
||||
▼
|
||||
rs2322tcp-client
|
||||
│
|
||||
│ TCP
|
||||
▼
|
||||
rs2322tcp-server
|
||||
│
|
||||
▼
|
||||
/dev/ttyUSB0
|
||||
│
|
||||
│ 600 Baud / 8N1
|
||||
▼
|
||||
SPID Rot2Prog
|
||||
│
|
||||
│ Antwort
|
||||
▼
|
||||
rs2322tcp-server
|
||||
│
|
||||
│ TCP
|
||||
▼
|
||||
rs2322tcp-client
|
||||
│
|
||||
▼
|
||||
/dev/ttyUSB101
|
||||
```
|
||||
|
||||
Damit kann die Antwort des Rotors wieder über den virtuellen seriellen
|
||||
Port gelesen werden.
|
||||
|
||||
Ein erfolgreich beobachteter Antwort-Datenstrom war:
|
||||
|
||||
```text
|
||||
57 03 06 05 0C 01 03 06 02 0F 01 20
|
||||
```
|
||||
|
||||
Damit wurde die bidirektionale Übertragung zwischen dem virtuellen
|
||||
seriellen Port des Clients und dem realen SPID-Rot2Prog über TCP
|
||||
erfolgreich nachgewiesen.
|
||||
|
||||
## Manuelle Einrichtung der virtuellen seriellen Ports
|
||||
|
||||
Die virtuellen seriellen Ports werden vor dem Start des Clients manuell
|
||||
eingerichtet. Dadurch muss der `rs2322tcp-client` selbst nicht als
|
||||
`root` bzw. mit `sudo` laufen.
|
||||
|
||||
Zunächst das Verzeichnis für die internen virtuellen Links anlegen:
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.rs2322tcp/virtual
|
||||
```
|
||||
|
||||
Anschließend werden die öffentlichen `/dev/ttyUSBxxx`-Links einmalig
|
||||
mit administrativen Rechten angelegt:
|
||||
|
||||
```bash
|
||||
sudo ln -s "$HOME/.rs2322tcp/virtual/ttyUSB100" /dev/ttyUSB100
|
||||
sudo ln -s "$HOME/.rs2322tcp/virtual/ttyUSB101" /dev/ttyUSB101
|
||||
```
|
||||
|
||||
Die Zuordnung ist:
|
||||
|
||||
```text
|
||||
/dev/ttyUSB100 -> ~/.rs2322tcp/virtual/ttyUSB100 -> PTY
|
||||
/dev/ttyUSB101 -> ~/.rs2322tcp/virtual/ttyUSB101 -> PTY
|
||||
```
|
||||
|
||||
Die äußeren Links unter `/dev` gehören dabei `root`. Das ist beabsichtigt.
|
||||
Der Client selbst läuft anschließend als normaler Benutzer.
|
||||
|
||||
Die internen Links unter `~/.rs2322tcp/virtual/` werden vom Client auf die
|
||||
jeweils verwendeten PTYs gesetzt.
|
||||
|
||||
Die beiden virtuellen Ports werden in der Client-Konfiguration den
|
||||
Geräten zugeordnet:
|
||||
|
||||
```text
|
||||
/dev/ttyUSB100 -> radio
|
||||
/dev/ttyUSB101 -> rotor
|
||||
```
|
||||
|
||||
Die manuelle Einrichtung muss nur erfolgen, wenn die `/dev/ttyUSB100`-
|
||||
und `/dev/ttyUSB101`-Links noch nicht vorhanden sind.
|
||||
|
||||
## Virtuelle serielle Schnittstelle unter Linux
|
||||
|
||||
Für den Linux-Client wird die virtuelle serielle Schnittstelle direkt
|
||||
|
|
|
|||
|
|
@ -6,11 +6,13 @@
|
|||
|
||||
"hardware_error_response": "ERROR - HARDWARE NOT AVAILABLE",
|
||||
|
||||
"serial_monitor": true,
|
||||
|
||||
"devices": [
|
||||
{
|
||||
"id": "radio",
|
||||
"name": "Funkgerät",
|
||||
"serial_port": "/dev/ttyUSB0",
|
||||
"serial_port": "/dev/ttyUSB1",
|
||||
"baud_rate": 9600,
|
||||
"data_bits": 8,
|
||||
"parity": "none",
|
||||
|
|
@ -19,8 +21,8 @@
|
|||
{
|
||||
"id": "rotor",
|
||||
"name": "Antennenrotor",
|
||||
"serial_port": "/dev/ttyUSB1",
|
||||
"baud_rate": 4800,
|
||||
"serial_port": "/dev/ttyUSB0",
|
||||
"baud_rate": 600,
|
||||
"data_bits": 8,
|
||||
"parity": "none",
|
||||
"stop_bits": 1
|
||||
|
|
|
|||
|
|
@ -73,7 +73,6 @@ type Application struct {
|
|||
// The client configuration is loaded immediately. No network connection
|
||||
// is established. Start must be called before the application connects to
|
||||
// the server or starts the Runtime.
|
||||
|
||||
func NewApplication(configFile string) (*Application, error) {
|
||||
if configFile == "" {
|
||||
return nil, fmt.Errorf("configuration file is empty")
|
||||
|
|
@ -94,14 +93,11 @@ func NewApplication(configFile string) (*Application, error) {
|
|||
// Start
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Start loads the client configuration and connects to the server.
|
||||
// Start loads the client configuration, connects to the server and starts
|
||||
// the client Runtime.
|
||||
//
|
||||
// Start is intended for the initial application startup. An already running
|
||||
// application must be stopped first by calling Close or Reconnect.
|
||||
//
|
||||
// The server device list is retrieved during startup. The client Runtime is
|
||||
// not started here; data connections are established separately when the
|
||||
// runtime is explicitly started.
|
||||
// The Runtime is started asynchronously because Runtime.Run blocks while
|
||||
// the configured virtual serial connections are active.
|
||||
func (a *Application) Start() error {
|
||||
if a == nil {
|
||||
return fmt.Errorf("application is nil")
|
||||
|
|
@ -140,17 +136,39 @@ func (a *Application) Start() error {
|
|||
return fmt.Errorf("get remote devices: %w", err)
|
||||
}
|
||||
|
||||
manager := NewVirtualPortManager(
|
||||
cfg.VirtualPortRange,
|
||||
)
|
||||
|
||||
runtime, err := NewRuntime(
|
||||
client,
|
||||
manager,
|
||||
)
|
||||
if err != nil {
|
||||
_ = client.Close()
|
||||
|
||||
return fmt.Errorf("create runtime: %w", err)
|
||||
}
|
||||
|
||||
runtimeDone := make(chan error, 1)
|
||||
|
||||
a.mu.Lock()
|
||||
|
||||
a.config = cfg
|
||||
a.client = client
|
||||
a.runtime = nil
|
||||
a.runtime = runtime
|
||||
a.devices = append(
|
||||
[]transport.RemoteDeviceInfo(nil),
|
||||
devices...,
|
||||
)
|
||||
a.runtimeDone = nil
|
||||
a.runtimeDone = runtimeDone
|
||||
|
||||
a.mu.Unlock()
|
||||
|
||||
go func() {
|
||||
runtimeDone <- runtime.Run(*cfg)
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import (
|
|||
type ServerConfig struct {
|
||||
Listen ListenConfig `json:"listen"`
|
||||
HardwareErrorResponse string `json:"hardware_error_response"`
|
||||
SerialMonitor bool `json:"serial_monitor"`
|
||||
Devices []DeviceConfig `json:"devices"`
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -352,6 +352,11 @@ func (s *ControlServer) runDataListener(
|
|||
continue
|
||||
}
|
||||
|
||||
dataConnection.SetSerialMonitor(
|
||||
s.config.SerialMonitor,
|
||||
device.SerialPort,
|
||||
)
|
||||
|
||||
if err := session.AddDataConnection(
|
||||
device.ID,
|
||||
dataConnection,
|
||||
|
|
@ -363,6 +368,7 @@ func (s *ControlServer) runDataListener(
|
|||
err,
|
||||
)
|
||||
|
||||
_ = dataConnection.Close()
|
||||
continue
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ package server
|
|||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"sync"
|
||||
)
|
||||
|
|
@ -43,6 +44,9 @@ type DataConnection struct {
|
|||
serial io.ReadWriteCloser
|
||||
hardwareErrorResponse string
|
||||
|
||||
serialMonitor bool
|
||||
serialPort string
|
||||
|
||||
closeOnce sync.Once
|
||||
closeErr error
|
||||
}
|
||||
|
|
@ -98,6 +102,60 @@ func (c *DataConnection) SerialConn() io.ReadWriteCloser {
|
|||
return c.serial
|
||||
}
|
||||
|
||||
// SetSerialMonitor enables or disables the data monitor.
|
||||
//
|
||||
// If enabled, transmitted and received data is written to the server log.
|
||||
// serialPort is used only for identifying the physical interface in the
|
||||
// monitor output.
|
||||
func (c *DataConnection) SetSerialMonitor(
|
||||
enabled bool,
|
||||
serialPort string,
|
||||
) {
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.serialMonitor = enabled
|
||||
c.serialPort = serialPort
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Data monitor
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// logTCPData writes TCP data to the server log.
|
||||
func (c *DataConnection) logTCPData(
|
||||
direction string,
|
||||
data []byte,
|
||||
) {
|
||||
if c == nil || !c.serialMonitor || len(data) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf(
|
||||
"%s % X",
|
||||
direction,
|
||||
data,
|
||||
)
|
||||
}
|
||||
|
||||
// logSerialData writes serial data to the server log.
|
||||
func (c *DataConnection) logSerialData(
|
||||
direction string,
|
||||
data []byte,
|
||||
) {
|
||||
if c == nil || !c.serialMonitor || len(data) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf(
|
||||
"%s [SERIAL %s] % X",
|
||||
direction,
|
||||
c.serialPort,
|
||||
data,
|
||||
)
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Data transfer
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
|
@ -118,7 +176,11 @@ func (c *DataConnection) copyTCPToSerial() error {
|
|||
n, err := c.tcp.Read(buffer)
|
||||
|
||||
if n > 0 {
|
||||
if _, writeErr := c.serial.Write(buffer[:n]); writeErr != nil {
|
||||
c.logTCPData("TCP RX", buffer[:n])
|
||||
|
||||
written, writeErr := c.serial.Write(buffer[:n])
|
||||
|
||||
if writeErr != nil {
|
||||
_, _ = io.WriteString(
|
||||
c.tcp,
|
||||
c.hardwareErrorResponse,
|
||||
|
|
@ -126,6 +188,17 @@ func (c *DataConnection) copyTCPToSerial() error {
|
|||
|
||||
return writeErr
|
||||
}
|
||||
|
||||
if written != n {
|
||||
_, _ = io.WriteString(
|
||||
c.tcp,
|
||||
c.hardwareErrorResponse,
|
||||
)
|
||||
|
||||
return io.ErrShortWrite
|
||||
}
|
||||
|
||||
c.logSerialData("SERIAL TX", buffer[:n])
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
|
|
@ -141,9 +214,39 @@ func (c *DataConnection) copySerialToTCP() error {
|
|||
return fmt.Errorf("data connection is nil")
|
||||
}
|
||||
|
||||
_, err := io.Copy(c.tcp, c.serial)
|
||||
buffer := make([]byte, 32*1024)
|
||||
|
||||
return err
|
||||
for {
|
||||
n, err := c.serial.Read(buffer)
|
||||
|
||||
if n > 0 {
|
||||
c.logSerialData("SERIAL RX", buffer[:n])
|
||||
|
||||
written := 0
|
||||
|
||||
for written < n {
|
||||
count, writeErr := c.tcp.Write(
|
||||
buffer[written:n],
|
||||
)
|
||||
|
||||
written += count
|
||||
|
||||
if writeErr != nil {
|
||||
return writeErr
|
||||
}
|
||||
|
||||
if count == 0 {
|
||||
return io.ErrShortWrite
|
||||
}
|
||||
}
|
||||
|
||||
c.logTCPData("TCP TX", buffer[:n])
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
|
|
|||
Loading…
Reference in a new issue