104 lines
2.7 KiB
Go
104 lines
2.7 KiB
Go
package protocol
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"fmt"
|
||
"github.com/google/uuid"
|
||
"regexp"
|
||
"sync"
|
||
)
|
||
|
||
type DataType string
|
||
|
||
const (
|
||
Bool DataType = "BOOL"
|
||
Int DataType = "INT"
|
||
Real DataType = "REAL"
|
||
)
|
||
|
||
type DeviceConnectionConfig struct {
|
||
DeviceID uuid.UUID
|
||
Host string
|
||
Port int
|
||
ConnectTimeoutSeconds int
|
||
ProtocolConfig map[string]any
|
||
}
|
||
type Connection interface {
|
||
Read(context.Context, string, DataType) (float64, error)
|
||
Write(context.Context, string, DataType, float64) error
|
||
Close() error
|
||
}
|
||
type Factory interface {
|
||
ProtocolType() string
|
||
ValidateConfig(map[string]any) error
|
||
ValidateAddress(string, DataType, bool) error
|
||
ConfigSchema() map[string]any
|
||
NewConnection(context.Context, DeviceConnectionConfig) (Connection, error)
|
||
}
|
||
type Registry struct {
|
||
mu sync.RWMutex
|
||
items map[string]Factory
|
||
}
|
||
|
||
func NewRegistry() *Registry { return &Registry{items: map[string]Factory{}} }
|
||
func (r *Registry) Register(f Factory) error {
|
||
r.mu.Lock()
|
||
defer r.mu.Unlock()
|
||
if _, ok := r.items[f.ProtocolType()]; ok {
|
||
return fmt.Errorf("协议已注册: %s", f.ProtocolType())
|
||
}
|
||
r.items[f.ProtocolType()] = f
|
||
return nil
|
||
}
|
||
func (r *Registry) Get(t string) (Factory, error) {
|
||
r.mu.RLock()
|
||
defer r.mu.RUnlock()
|
||
f, ok := r.items[t]
|
||
if !ok {
|
||
return nil, errors.New("不支持的协议")
|
||
}
|
||
return f, nil
|
||
}
|
||
func (r *Registry) Metadata() []map[string]any {
|
||
r.mu.RLock()
|
||
defer r.mu.RUnlock()
|
||
out := []map[string]any{}
|
||
for _, f := range r.items {
|
||
port := 502
|
||
if f.ProtocolType() == "S7" {
|
||
port = 102
|
||
}
|
||
out = append(out, map[string]any{"protocol_type": f.ProtocolType(), "default_port": port, "config_schema": f.ConfigSchema()})
|
||
}
|
||
return out
|
||
}
|
||
|
||
var s7DB = regexp.MustCompile(`^DB\d+\.\d+(?:\.[0-7])?$`)
|
||
var s7Standard = regexp.MustCompile(`^[MIQ]\d+(?:\.[0-7])?$`)
|
||
var s7Word = regexp.MustCompile(`^[MQ][WD]\d+$`)
|
||
|
||
func ValidateS7Address(a string, t DataType, w bool) error {
|
||
if !s7DB.MatchString(a) && !s7Standard.MatchString(a) && !s7Word.MatchString(a) {
|
||
return errors.New("S7 地址格式无效")
|
||
}
|
||
hasBit := regexp.MustCompile(`\.[0-7]$`).MatchString(a)
|
||
numericDBZeroSuffix := t != Bool && regexp.MustCompile(`^DB\d+\.\d+\.0$`).MatchString(a)
|
||
if t == Bool && !hasBit {
|
||
return errors.New("BOOL 地址必须包含 bit")
|
||
}
|
||
if t != Bool && hasBit && !numericDBZeroSuffix {
|
||
return errors.New("INT/REAL 地址不能包含 bit(仅兼容 DB 数值地址末尾 .0)")
|
||
}
|
||
if t == Int && regexp.MustCompile(`^[MQ]D`).MatchString(a) {
|
||
return errors.New("INT 应使用 MW/QW 地址")
|
||
}
|
||
if t == Real && regexp.MustCompile(`^[MQ]W`).MatchString(a) {
|
||
return errors.New("REAL 应使用 MD/QD 地址")
|
||
}
|
||
if w && len(a) > 0 && a[0] == 'I' {
|
||
return errors.New("I 区只读")
|
||
}
|
||
return nil
|
||
}
|