Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #11 from crabmusket/modbus
modbus: Introduce support for modbus physical device implementations.
- Loading branch information
Showing
4 changed files
with
458 additions
and
0 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,207 @@ | ||
package modbus | ||
|
||
import ( | ||
"encoding/binary" | ||
"errors" | ||
"github.com/crabmusket/gosunspec" | ||
"github.com/crabmusket/gosunspec/impl" | ||
"github.com/crabmusket/gosunspec/models/model1" | ||
"github.com/crabmusket/gosunspec/smdx" | ||
"github.com/crabmusket/gosunspec/spi" | ||
"github.com/goburrow/modbus" | ||
"log" | ||
) | ||
|
||
const ( | ||
SunSpec = 0x53756e53 // "SunS" - marker bytes used to confirm that a region of Modbus address space is laid out according to SunSpec standards | ||
) | ||
|
||
var ( | ||
ErrNotSunspecDevice = errors.New("not a SunSpec device") // if the Modbus address space doesn't contain the expected marker bytes | ||
ErrShortRead = errors.New("short read") // if an attempt to read from the Modbus addess space returns fewer bytes than expected | ||
) | ||
|
||
// Open uses the Modbus connection provided by client to connect | ||
// to a Modbus address space. The address space is scanned for | ||
// one or more SunSpec devices and a reference to | ||
// a sunspec.Array that provides access to these devices is returned. | ||
func Open(client modbus.Client) (sunspec.Array, error) { | ||
|
||
// Attempt to locate SunSpec register within modbus address space. | ||
|
||
baseRange := []uint16{40000, 50000, 0} | ||
base := uint16(0xffff) | ||
for _, b := range baseRange { | ||
if id, err := client.ReadHoldingRegisters(b, 2); err != nil { | ||
continue | ||
} else if binary.BigEndian.Uint32(id) != SunSpec { | ||
continue | ||
} else { | ||
base = b | ||
break | ||
} | ||
} | ||
if base == 0xffff { | ||
return nil, ErrNotSunspecDevice | ||
} | ||
|
||
phys := &modbusPhysical{client: client} | ||
array := impl.NewArray() | ||
dev := impl.NewDevice() | ||
|
||
// Build up model | ||
|
||
offset := uint16(2) // number of 16 bit registers | ||
for { | ||
if bytes, err := client.ReadHoldingRegisters(base+offset, 2); err != nil { | ||
return nil, err | ||
} else if len(bytes) < 4 { | ||
return nil, ErrShortRead | ||
} else { | ||
modelId := binary.BigEndian.Uint16(bytes) | ||
modelLength := binary.BigEndian.Uint16(bytes[2:]) | ||
|
||
if modelId == 0xffff { | ||
break | ||
} | ||
|
||
me := smdx.GetModel(modelId) | ||
if me != nil { | ||
|
||
if modelId == uint16(model1.ModelID) { | ||
dev = impl.NewDevice() | ||
array.(spi.ArraySPI).AddDevice(dev) | ||
} | ||
|
||
m := impl.NewContiguousModel(me, modelLength, phys) | ||
|
||
// set anchors on the blocks | ||
|
||
blockOffset := offset + 2 | ||
m.DoWithSPI(func(b spi.BlockSPI) { | ||
b.SetAnchor(uint16(base + blockOffset)) | ||
blockOffset += b.Length() | ||
}) | ||
dev.AddModel(m) | ||
} else { | ||
log.Printf("unrecognised model identifier skipped @ offset: %d, %d\n", modelId, offset) | ||
} | ||
offset += 2 + modelLength | ||
} | ||
} | ||
return array, nil | ||
} | ||
|
||
type modbusPhysical struct { | ||
client modbus.Client | ||
} | ||
|
||
// Write out the points in exactly the order specified, coalescing | ||
// adjacent points if they are adjacent in the specified order. | ||
func (p *modbusPhysical) Write(block spi.BlockSPI, pointIds ...string) error { | ||
|
||
if len(pointIds) == 0 { | ||
block.Do(func(p sunspec.Point) { | ||
pointIds = append(pointIds, p.Id()) | ||
}) | ||
} | ||
|
||
// identify runs of adajacent points | ||
|
||
runs := newRunBuilder() | ||
|
||
// note: we preserve the programmer specified order | ||
// (not the specification order) because the write order | ||
// maybe significant in some cases especially if one | ||
// register is used to activate values previously | ||
// written into other registers. | ||
for _, pid := range pointIds { | ||
if p, err := block.Point(pid); err != nil { | ||
return err | ||
} else { | ||
runs.add(p.(spi.PointSPI)) | ||
} | ||
} | ||
|
||
// marshal each group of adjacent points into byte slices and then | ||
// immediately write each byte slice into the modbus client. | ||
for _, run := range runs.runs { | ||
l := uint16(0) | ||
for _, pt := range run { | ||
l += pt.Length() | ||
} | ||
buffer := make([]byte, l*2, l*2) | ||
woff := uint16(0) | ||
for _, pt := range run { | ||
if err := pt.Marshal(buffer[woff : woff+pt.Length()*2]); err != nil { | ||
return err | ||
} | ||
woff += pt.Length() * 2 | ||
} | ||
if _, err := p.client.WriteMultipleRegisters(block.Anchor().(uint16)+run[0].Offset(), l, buffer); err != nil { | ||
return err | ||
} | ||
} | ||
return nil | ||
} | ||
|
||
// Read extends the specified set of points with Block.Plan() then determines | ||
// runs of points that can be read together. The points are read and then | ||
// unmarshaled into the model in the order determined by slice returned by Block.Plan() | ||
func (p *modbusPhysical) Read(block spi.BlockSPI, pointIds ...string) error { | ||
if applicationOrder, err := block.Plan(pointIds...); err != nil { | ||
return err | ||
} else { | ||
runs := newRunBuilder() | ||
offsets := map[string]uint16{} // offsets into read buffer, by point | ||
off := uint16(0) // the current offset | ||
toRead := map[string]bool{} // the set of ponts to read | ||
|
||
// initialise the toRead set | ||
for _, p := range applicationOrder { | ||
toRead[p.Id()] = true | ||
} | ||
|
||
// break the list of points to be retrieved | ||
// into runs of strictly adjacent points and | ||
// record for each point the offset into a buffer | ||
// in which the marshaled point value will be read | ||
block.DoWithSPI(func(pt spi.PointSPI) { | ||
if !toRead[pt.Id()] { | ||
return | ||
} | ||
|
||
runs.add(pt) | ||
offsets[pt.Id()] = off | ||
off += pt.Length() * 2 | ||
}) | ||
|
||
// allocate a buffer that can contain all the read points | ||
buffer := make([]byte, off, off) | ||
|
||
// read runs of points into the buffer | ||
off = 0 | ||
for _, run := range runs.runs { | ||
l := uint16(0) | ||
for _, pt := range run { | ||
l += pt.Length() | ||
} | ||
if bytes, err := p.client.ReadHoldingRegisters(block.Anchor().(uint16)+run[0].Offset(), l); err != nil { | ||
return err | ||
} else { | ||
copy(buffer[off:off+l*2], bytes) | ||
off += (l * 2) | ||
} | ||
} | ||
|
||
// finally, unmarshal the buffer into points in the order determined by the plan | ||
for _, a := range applicationOrder { | ||
lbound := offsets[a.Id()] | ||
rbound := lbound + a.Length()*2 | ||
if err := a.Unmarshal(buffer[lbound:rbound]); err != nil { | ||
return err | ||
} | ||
} | ||
} | ||
return nil | ||
} |
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,119 @@ | ||
package modbus | ||
|
||
import ( | ||
"github.com/crabmusket/gosunspec" | ||
"github.com/crabmusket/gosunspec/memory" | ||
//"github.com/crabmusket/gosunspec/typelabel" | ||
"testing" | ||
) | ||
|
||
func TestModBusSimulator(t *testing.T) { | ||
if sim, err := OpenSimulator(memory.ComplexNonZeroSlab, 40000); err != nil { | ||
t.Fatal(err) | ||
} else { | ||
if arr, err := Open(sim); err != nil { | ||
t.Fatal(err) | ||
} else { | ||
arr.Do(func(d sunspec.Device) {}) | ||
} | ||
|
||
} | ||
} | ||
|
||
// TestComplexSlab iterate over all points and check that they have the expected values. | ||
func TestComplexSlab(t *testing.T) { | ||
if sim, err := OpenSimulator(memory.ComplexNonZeroSlab, 40000); err != nil { | ||
t.Fatal(err) | ||
} else { | ||
if arr, err := Open(sim); err != nil { | ||
t.Fatal(err) | ||
} else { | ||
arr.Do(func(d sunspec.Device) { | ||
count := 0 | ||
d.Do(func(m sunspec.Model) { | ||
m.Do(func(b sunspec.Block) { | ||
if err := b.Read(); err != nil { | ||
t.Fatal(err) | ||
} | ||
b.Do(func(p sunspec.Point) { | ||
if err := p.Error(); err != nil { | ||
t.Fatalf("p has error. model=%d, point=%s\n", m.Id(), p.Id()) | ||
} | ||
|
||
if v := p.Value(); v != memory.ExpectedValues[p.Type()] { | ||
t.Fatalf("unexpected value. model=%d, point=%s. actual=%#v, expected=%#v. type=%s", m.Id(), p.Id(), v, memory.ExpectedValues[p.Type()], p.Type()) | ||
} | ||
count++ | ||
}) | ||
}) | ||
}) | ||
|
||
expected := 213 | ||
if count != expected { | ||
t.Fatalf("unexpected number of points. actual: %d, expected: %d", count, expected) | ||
} | ||
}) | ||
} | ||
|
||
} | ||
} | ||
|
||
// TestComplexSlab iterate over all points and check that they have the expected values. | ||
func TestComplexSlabStaggered(t *testing.T) { | ||
if sim, err := OpenSimulator(memory.ComplexNonZeroSlab, 40000); err != nil { | ||
t.Fatal(err) | ||
} else { | ||
if arr, err := Open(sim); err != nil { | ||
t.Fatal(err) | ||
} else { | ||
count := 0 | ||
arr.Do(func(d sunspec.Device) { | ||
d.Do(func(m sunspec.Model) { | ||
m.Do(func(b sunspec.Block) { | ||
|
||
// read pairs of adjacaent points | ||
|
||
p := []string{} | ||
q := []string{} | ||
c := 0 | ||
b.Do(func(pt sunspec.Point) { | ||
if c%4 < 2 { | ||
p = append(p, pt.Id()) | ||
} else { | ||
q = append(q, pt.Id()) | ||
} | ||
c++ | ||
}) | ||
|
||
if err := b.Read(p...); err != nil { | ||
t.Fatal(err) | ||
} | ||
|
||
if err := b.Read(q...); err != nil { | ||
t.Fatal(err) | ||
} | ||
|
||
// check all the values | ||
|
||
b.Do(func(p sunspec.Point) { | ||
if err := p.Error(); err != nil { | ||
t.Fatalf("p has error. model=%d, point=%s\n", m.Id(), p.Id()) | ||
} | ||
|
||
if v := p.Value(); v != memory.ExpectedValues[p.Type()] { | ||
t.Fatalf("unexpected value. model=%d, point=%s. actual=%#v, expected=%#v. type=%s", m.Id(), p.Id(), v, memory.ExpectedValues[p.Type()], p.Type()) | ||
} | ||
count++ | ||
}) | ||
}) | ||
}) | ||
|
||
}) | ||
expected := 213 | ||
if count != expected { | ||
t.Fatalf("unexpected number of points. actual: %d, expected: %d", count, expected) | ||
} | ||
} | ||
|
||
} | ||
} |
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,48 @@ | ||
package modbus | ||
|
||
import ( | ||
"github.com/crabmusket/gosunspec/spi" | ||
) | ||
|
||
// runBuilder breaks a sequence of points into a sequence of runs, where each run | ||
// of points is strictly adjacent | ||
type runBuilder struct { | ||
runs [][]spi.PointSPI | ||
run []spi.PointSPI | ||
} | ||
|
||
func newRunBuilder() *runBuilder { | ||
run := []spi.PointSPI{} | ||
return &runBuilder{ | ||
run: run, | ||
runs: [][]spi.PointSPI{run}, | ||
} | ||
} | ||
|
||
func (r *runBuilder) spawn(p spi.PointSPI) { | ||
r.run = []spi.PointSPI{p} | ||
r.runs = append(r.runs, r.run) | ||
|
||
} | ||
|
||
func (r *runBuilder) adjacent(p spi.PointSPI) bool { | ||
if len(r.run) == 0 { | ||
return true | ||
} else { | ||
last := r.run[len(r.run)-1] | ||
return last.Offset()+last.Length() == p.Offset() | ||
} | ||
} | ||
|
||
func (r *runBuilder) extend(p spi.PointSPI) { | ||
r.run = append(r.run, p) | ||
r.runs[len(r.runs)-1] = r.run | ||
} | ||
|
||
func (r *runBuilder) add(p spi.PointSPI) { | ||
if r.adjacent(p) { | ||
r.extend(p) | ||
} else { | ||
r.spawn(p) | ||
} | ||
} |
Oops, something went wrong.