Skip to content

Commit

Permalink
initial client and server implementations
Browse files Browse the repository at this point in the history
  • Loading branch information
activeshadow committed Jun 28, 2024
1 parent 5c5359b commit 0883822
Show file tree
Hide file tree
Showing 12 changed files with 1,179 additions and 6 deletions.
4 changes: 2 additions & 2 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@
[submodule "src/c++/deps/opendnp3"]
path = src/c++/deps/opendnp3
url = https://github.com/dnp3/opendnp3.git
[submodule "src/go/sunspec/models"]
path = src/go/sunspec/models
[submodule "src/go/sunspec/common/models"]
path = src/go/sunspec/common/models
url = https://github.com/sunspec/models.git
6 changes: 3 additions & 3 deletions src/go/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ clean:
$(RM) bin/ot-sim-tailscale-module
$(RM) bin/ot-sim-telnet-module

sunspec/types.go: sunspec/models/json/schema.json bin/go-jsonschema
$(GOBIN)/go-jsonschema -p sunspec -o sunspec/types.go sunspec/models/json/schema.json
sunspec/common/types.go: sunspec/common/models/json/schema.json bin/go-jsonschema
$(GOBIN)/go-jsonschema -p common -o sunspec/common/types.go sunspec/common/models/json/schema.json

MSGBUS_SOURCES := $(shell find msgbus \( -name '*.go' \))

Expand Down Expand Up @@ -81,7 +81,7 @@ bin/ot-sim-node-red-module: $(NODERED_SOURCES)

SUNSPEC_SOURCES := $(shell find sunspec \( -name '*.go' \))

bin/ot-sim-sunspec-module: $(SUNSPEC_SOURCES) sunspec/types.go
bin/ot-sim-sunspec-module: $(SUNSPEC_SOURCES) $(MSGBUS_SOURCES)
mkdir -p bin
GOOS=linux go build -a -ldflags="-s -w" -trimpath -o bin/ot-sim-sunspec-module cmd/ot-sim-sunspec-module/main.go

Expand Down
53 changes: 53 additions & 0 deletions src/go/cmd/ot-sim-sunspec-module/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package main

import (
"context"
"errors"
"fmt"
"os"

otsim "github.com/patsec/ot-sim"
"github.com/patsec/ot-sim/util"
"github.com/patsec/ot-sim/util/sigterm"

// This will cause the SunSpec module to register itself with the otsim package
// so it gets run by the otsim.Start function below.
_ "github.com/patsec/ot-sim/sunspec"
)

func main() {
if len(os.Args) != 2 {
panic("path to config file not provided")
}

if err := otsim.ParseConfigFile(os.Args[1]); err != nil {
fmt.Printf("Error parsing config file: %v\n", err)
os.Exit(util.ExitNoRestart)
}

ctx := sigterm.CancelContext(context.Background())

if err := otsim.Start(ctx); err != nil {
fmt.Printf("Error starting SunSpec module: %v\n", err)

var exitErr util.ExitError
if errors.As(err, &exitErr) {
os.Exit(exitErr.ExitCode)
}

os.Exit(1)
}

<-ctx.Done()

if err := ctx.Err(); err != nil && !errors.Is(err, context.Canceled) {
fmt.Printf("Error running SunSpec module: %v\n", err)

var exitErr util.ExitError
if errors.As(err, &exitErr) {
os.Exit(exitErr.ExitCode)
}

os.Exit(1)
}
}
6 changes: 5 additions & 1 deletion src/go/sunspec/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,8 @@ OT-sim tags?
How do we want to specify point scalings?

* map scaling values to SF values?
* have deault scaling values for SF's that can be overwritten?
* have deault scaling values for SF's that can be overwritten?

## TODO

* [ ] Build out initial Model 1 (static data)
191 changes: 191 additions & 0 deletions src/go/sunspec/client/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
package client

import (
"context"
"fmt"
"strconv"

"github.com/beevik/etree"
otsim "github.com/patsec/ot-sim"
"github.com/patsec/ot-sim/sunspec/common"

"actshad.dev/modbus"
)

type Factory struct{}

func (Factory) NewModule(e *etree.Element) (otsim.Module, error) {
name := e.SelectAttrValue("name", "sunspec")
return New(name), nil
}

type SunSpecClient struct {
pullEndpoint string
pubEndpoint string

name string
id int
endpoint string

models []int
registers map[int]common.Register
}

func New(name string) *SunSpecClient {
return &SunSpecClient{
name: name,
registers: make(map[int]common.Register),
}
}

func (this SunSpecClient) Name() string {
return this.name
}

func (this *SunSpecClient) Configure(e *etree.Element) error {
for _, child := range e.ChildElements() {
switch child.Tag {
case "pull-endpoint":
this.pullEndpoint = child.Text()
case "pub-endpoint":
this.pubEndpoint = child.Text()
case "endpoint":
this.endpoint = child.Text()
case "model":
attr := child.SelectAttr("id")
if attr == nil {
return fmt.Errorf("missing 'id' attribute for SunSpec model")
}

id, err := strconv.Atoi(attr.Value)
if err != nil {
return fmt.Errorf("parsing model ID %s: %w", attr.Value, err)
}

this.models = append(this.models, id)
}
}

return nil
}

func (this *SunSpecClient) Run(ctx context.Context, pubEndpoint, pullEndpoint string) error {
var handler modbus.ClientHandler

handler = modbus.NewTCPClientHandler(this.endpoint)
handler.(*modbus.TCPClientHandler).SlaveId = byte(this.id)

client := modbus.NewClient(handler)

r := common.IdentifierRegister

data, err := client.ReadHoldingRegisters(40000, uint16(r.Count))
if err != nil {
return fmt.Errorf("reading identifier from SunSpec device %s: %w", this.endpoint, err)
}

value, err := r.Value(data)
if err != nil {
return fmt.Errorf("parsing identifier from SunSpec device %s: %w", this.endpoint, err)
}

if value != common.SunSpecIdentifier {
return fmt.Errorf("invalid identifier provided by remote device")
}

// start addr at 40002 after well known identifier
addr := 40002

r = common.Register{DataType: "uint16"}

if err := r.Init(); err != nil {
return fmt.Errorf("initializing generic model register %d: %w", addr, err)
}

data, err = client.ReadHoldingRegisters(uint16(addr), uint16(r.Count))
if err != nil {
return fmt.Errorf("reading model ID from SunSpec device %s: %w", this.endpoint, err)
}

value, err = r.Value(data)
if err != nil {
return fmt.Errorf("parsing model ID from SunSpec device %s: %w", this.endpoint, err)
}

if value != 1 {
return fmt.Errorf("remote SunSpec device missing required Model 1")
}

addr += r.Count

data, err = client.ReadHoldingRegisters(uint16(addr), uint16(r.Count))
if err != nil {
return fmt.Errorf("reading model 1 length from SunSpec device %s: %w", this.endpoint, err)
}

value, err = r.Value(data)
if err != nil {
return fmt.Errorf("parsing model 1 length from SunSpec device %s: %w", this.endpoint, err)
}

addr += r.Count

data, err = client.ReadHoldingRegisters(uint16(addr), uint16(value))
if err != nil {
return fmt.Errorf("reading rest of model 1 data from SunSpec device %s: %w", this.endpoint, err)
}

model, err := common.GetModelSchema(1)
if err != nil {
return fmt.Errorf("getting model schema: %w", err)
}

// track position of current model data array
var pos int

for idx, point := range model.Group.Points {
if idx < 2 {
continue
}

dt := string(point.Type)
if dt == string(common.PointTypeString) {
dt = fmt.Sprintf("string%d", point.Size)
}

r := common.Register{
DataType: dt,
Tag: point.Name,
}

if err := r.Init(); err != nil {
return fmt.Errorf("initializing register %d: %w", addr, err)
}

bytes := data[pos : pos+(point.Size*2)]

if point.Type == common.PointTypeString {
value, err := r.String(bytes)
if err != nil {
return fmt.Errorf("parsing string value for SunSpec point: %w", err)
}

fmt.Printf("%s - %s\n", point.Name, value)
} else {
value, err := r.Value(bytes)
if err != nil {
return fmt.Errorf("parsing value for SunSpec point: %w", err)
}

fmt.Printf("%s - %f\n", point.Name, value)
}

pos += point.Size * 2
}

return nil
}

func (this SunSpecClient) log(format string, a ...any) {
fmt.Printf("[%s] %s\n", this.name, fmt.Sprintf(format, a...))
}
32 changes: 32 additions & 0 deletions src/go/sunspec/common/common.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package common

var SunSpecIdentifier = float64(1400204883)

var IdentifierRegister = Register{
DataType: "uint32",
InternalValue: SunSpecIdentifier,
}

var EndRegister = Register{
DataType: "uint16",
InternalValue: 65535,
}

var EndRegisterLength = Register{
DataType: "uint16",
InternalValue: 0,
}

func init() {
if err := IdentifierRegister.Init(); err != nil {
panic(err)
}

if err := EndRegister.Init(); err != nil {
panic(err)
}

if err := EndRegisterLength.Init(); err != nil {
panic(err)
}
}
Loading

0 comments on commit 0883822

Please sign in to comment.