-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
initial client and server implementations
- Loading branch information
1 parent
5c5359b
commit 0883822
Showing
12 changed files
with
1,179 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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...)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
Submodule models
updated
from 000000 to a11843
Oops, something went wrong.