diff --git a/.gitignore b/.gitignore index 5984917..7092d5b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,16 +1,13 @@ # Binaries for programs and plugins *.exe +*.exe~ *.dll *.so *.dylib +example/debug # Test binary, build with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out - -# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 -.glide/ - -*.idea \ No newline at end of file diff --git a/README.md b/README.md index 90e7475..60e7bcb 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,7 @@ -# gofins +# GoFINS [![Build Status](https://travis-ci.org/l1va/gofins.svg?branch=master)](https://travis-ci.org/l1va/gofins) - This is fins command client written by Go. The library support communication to omron PLC from Go application. @@ -10,10 +9,10 @@ The library support communication to omron PLC from Go application. Ideas were taken from https://github.com/hiroeorz/omron-fins-go and https://github.com/patrick--/node-omron-fins Library was tested with Omron PLC NJ501-1300. Mean time of the cycle request-response is 4ms. +Additional work in the siyka-au repository was tested against a CP1L-EM. Feel free to ask questions, raise issues and make pull requests! - PS. Build is ok but Travis is randomly failing to run tests with go 1.10 (with earlier go versions tests are passed too): [travis](https://travis-ci.org/l1va/gofins) ``` diff --git a/example/main.go b/example/main.go index b837386..1f5a101 100644 --- a/example/main.go +++ b/example/main.go @@ -2,47 +2,69 @@ package main import ( "fmt" - "log" - "github.com/l1va/gofins/fins" + "net" + + "github.com/siyka-au/gofins/fins" ) func main() { - plcAddr := "192.168.250.1:9600" - - c := fins.NewClient(plcAddr) - defer c.CloseConnection() - err := c.WriteDAsync(100, []uint16{5, 4, 3, 2, 1}, func(fins.Response) { - log.Println("writing done!") - }) - if err != nil { - log.Println("writing request failed:", err) + localClientAddr := &net.UDPAddr{ + IP: net.ParseIP("192.168.250.2"), + Port: 9600, } - - err = c.ReadDAsync(100, 5, func(r fins.Response) { - log.Println("readed values: ", r.Data) - }) - if err != nil { - log.Println("reading request failed:", err) + // localServerAddr := &net.UDPAddr{ + // IP: net.ParseIP("192.168.250.3"), + // Port: 9600, + // } + plcAddr := &net.UDPAddr{ + IP: net.ParseIP("192.168.250.10"), + Port: 9600, } - for i := 0; i < 10; i += 1 { - t := uint16(i * 5) - err := c.WriteD(200+t, []uint16{t, t + 1, t + 2, t + 3, t + 4}) - if err != nil { - log.Fatal(err) - } + c, e := fins.NewClient(localClientAddr, plcAddr, fins.NewAddress(0, 10, 0), fins.NewAddress(0, 2, 0)) + defer c.Close() + if e != nil { + panic(e) } - err = c.WriteDNoResponse(200, []uint16{5, 4, 3, 2, 1}) - if err != nil { - log.Println("writing request without response failed:", err) - } + // s, e := fins.NewServer(localServerAddr, fins.NewAddress(0, 3, 0)) + // if e != nil { + // panic(e) + // } - vals, err := c.ReadD(200, 50) - if err != nil { - log.Fatal(err) - } + defer c.Close() + // defer s.Close() + + z, _ := c.ReadWords(fins.MemoryAreaDMWord, 10000, 500) + fmt.Println(z) - fmt.Println(vals) + // s, _ := c.ReadString(fins.MemoryAreaDMWord, 10000, 10) + // fmt.Println(s) + // fmt.Println(len(s)) + + // b, _ := c.ReadBits(fins.MemoryAreaDMWord, 10473, 2, 1) + // fmt.Println(b) + // fmt.Println(len(b)) + + // c.WriteWords(fins.MemoryAreaDMWord, 24000, []uint16{z[0] + 1, z[1] - 1}) + // c.WriteBits(fins.MemoryAreaDMBit, 24002, 0, []bool{false, false, false, true, + // true, false, false, true, + // false, false, false, false, + // true, true, true, true}) + // c.SetBit(fins.MemoryAreaDMBit, 24003, 1) + // c.ResetBit(fins.MemoryAreaDMBit, 24003, 0) + // c.ToggleBit(fins.MemoryAreaDMBit, 24003, 2) + + // cron := cron.New() + // s := rasc.NewShelter() + // cron.AddFunc("*/5 * * * * *", func() { + // t, _ := c.ReadClock() + // fmt.Printf("Setting PLC time to: %s\n", t.Format(time.RFC3339)) + // c.WriteString(fins.MemoryAreaDMWord, 10000, 10, t.Format(time.RFC3339)) + // }) + // cron.Start() + + for { + } } diff --git a/fins/address.go b/fins/address.go new file mode 100644 index 0000000..5de2662 --- /dev/null +++ b/fins/address.go @@ -0,0 +1,28 @@ +package fins + +// Address A FINS device address +type Address struct { + network byte + node byte + unit byte +} + +func NewAddress(network byte, node byte, unit byte) *Address { + a := new(Address) + a.network = network + a.node = node + a.unit = unit + return a +} + +func (a *Address) Network() byte { + return a.network +} + +func (a *Address) Node() byte { + return a.node +} + +func (a *Address) Unit() byte { + return a.unit +} diff --git a/fins/client.go b/fins/client.go index 6804138..adc70f9 100644 --- a/fins/client.go +++ b/fins/client.go @@ -1,131 +1,292 @@ package fins import ( + "bufio" + "bytes" + "encoding/binary" + "errors" + "fmt" "log" "net" - "fmt" - "bufio" "sync" + "time" ) +// Client Omron FINS client type Client struct { - conn net.Conn - resp []chan Response + conn *net.UDPConn + resp []chan Frame sync.Mutex - sid byte -} - -type Response struct { - sid byte - Data []uint16 + dst *Address + src *Address + sid byte } -func NewClient(plcAddr string) *Client { +// NewClient creates a new Omron FINS client +func NewClient(localAddr, plcAddr *net.UDPAddr, dst *Address, src *Address) (*Client, error) { c := new(Client) - conn, err := net.Dial("udp", plcAddr) + c.dst = dst + c.src = src + + conn, err := net.DialUDP("udp", localAddr, plcAddr) if err != nil { - log.Fatal(err) - panic(fmt.Sprintf("error resolving UDP port: %s\n", plcAddr)) + return nil, err } c.conn = conn - c.resp = make([]chan Response, 256) //storage for all responses, sid is byte - only 256 values - go c.listenLoop() - - return c + c.resp = make([]chan Frame, 256) //storage for all responses, sid is byte - only 256 values + go c.listenLoop() + return c, nil } -func (c *Client) CloseConnection() { +// Close Closes an Omron FINS connection +func (c *Client) Close() { c.conn.Close() } -func (c *Client) incrementSid() byte { - c.Lock() //thread-safe sid incrementation - c.sid += 1 - sid := c.sid - c.Unlock() - c.resp[sid] = make(chan Response) //clearing cell of storage for new response - return sid +// ReadWords Reads words from the PLC data area +func (c *Client) ReadWords(memoryArea byte, address uint16, readCount uint16) ([]uint16, error) { + if checkIsWordMemoryArea(memoryArea) == false { + return nil, ErrIncompatibleMemoryArea + } + header := c.nextHeader() + command := readCommand(NewIOAddress(memoryArea, address), readCount) + r, e := c.sendCommand(header, command) + if e != nil { + return nil, e + } + + data := make([]uint16, readCount, readCount) + for i := 0; i < int(readCount); i++ { + data[i] = binary.BigEndian.Uint16(r.Data()[i*2 : i*2+2]) + } + + return data, nil } -func (c *Client) ReadD(startAddr uint16, readCount uint16) ([]uint16, error) { - sid := c.incrementSid() - cmd := readDCommand(defaultHeader(sid), startAddr, readCount) - return c.read(sid, cmd) +// ReadBytes Reads a string from the PLC data area +func (c *Client) ReadBytes(memoryArea byte, address uint16, readCount uint16) ([]byte, error) { + if checkIsWordMemoryArea(memoryArea) == false { + return nil, ErrIncompatibleMemoryArea + } + header := c.nextHeader() + command := readCommand(NewIOAddress(memoryArea, address), readCount) + r, e := c.sendCommand(header, command) + if e != nil { + return nil, e + } + + return r.Data(), nil } -func (c *Client) ReadW(startAddr uint16, readCount uint16) ([]uint16, error) { - sid := c.incrementSid() - cmd := readWCommand(defaultHeader(sid), startAddr, readCount) - return c.read(sid, cmd) +// ReadString Reads a string from the PLC data area +func (c *Client) ReadString(memoryArea byte, address uint16, readCount uint16) (*string, error) { + data, e := c.ReadBytes(memoryArea, address, readCount) + if e != nil { + return nil, e + } + n := bytes.Index(data, []byte{0}) + s := string(data[:n]) + return &s, nil } -func (c *Client) read(sid byte, cmd []byte) ([]uint16, error) { - _, err := c.conn.Write(cmd) - if err != nil { - return nil, err +// ReadBits Reads bits from the PLC data area +func (c *Client) ReadBits(memoryArea byte, address uint16, bitOffset byte, readCount uint16) ([]bool, error) { + if checkIsBitMemoryArea(memoryArea) == false { + return nil, ErrIncompatibleMemoryArea + } + header := c.nextHeader() + command := readCommand(NewIOAddressWithBitOffset(memoryArea, address, bitOffset), readCount) + r, e := c.sendCommand(header, command) + if e != nil { + return nil, e + } + + data := make([]bool, readCount, readCount) + for i := 0; i < int(readCount); i++ { + data[i] = r.Data()[i]&0x01 > 0 } - ans := <-c.resp[sid] - return ans.Data, nil + return data, nil } -func (c *Client) WriteD(startAddr uint16, data []uint16) error { - sid := c.incrementSid() - cmd := writeDCommand(defaultHeader(sid), startAddr, data) - return c.write(sid, cmd) +// ReadClock Reads the PLC clock +func (c *Client) ReadClock() (*time.Time, error) { + header := c.nextHeader() + command := NewCommand(CommandCodeClockRead, []byte{}) + responseFrame, e := c.sendCommand(header, command) + if e != nil { + return nil, e + } + year, _ := decodeBCD(responseFrame.Data()[0:1]) + if year < 50 { + year += 2000 + } else { + year += 1900 + } + month, _ := decodeBCD(responseFrame.Data()[1:2]) + day, _ := decodeBCD(responseFrame.Data()[2:3]) + hour, _ := decodeBCD(responseFrame.Data()[3:4]) + minute, _ := decodeBCD(responseFrame.Data()[4:5]) + second, _ := decodeBCD(responseFrame.Data()[5:6]) + + t := time.Date( + int(year), time.Month(month), int(day), int(hour), int(minute), int(second), + 0, // nanosecond + time.Local, + ) + + return &t, nil } -func (c *Client) WriteW(startAddr uint16, data []uint16) error { - sid := c.incrementSid() - cmd := writeWCommand(defaultHeader(sid), startAddr, data) - return c.write(sid, cmd) +// WriteWords Writes words to the PLC data area +func (c *Client) WriteWords(memoryArea byte, address uint16, data []uint16) error { + if checkIsWordMemoryArea(memoryArea) == false { + return ErrIncompatibleMemoryArea + } + header := c.nextHeader() + l := uint16(len(data)) + bytes := make([]byte, 2*l, 2*l) + for i := 0; i < int(l); i++ { + binary.BigEndian.PutUint16(bytes[i*2:i*2+2], data[i]) + } + command := writeCommand(NewIOAddress(memoryArea, address), l, bytes) + r, e := c.sendCommand(header, command) + if e != nil { + return e + } + if r.EndCode() != EndCodeNormalCompletion { + return fmt.Errorf("Error reported by destination, end code 0x%x", r.EndCode) + } + + return nil } -func (c *Client) write(sid byte, cmd []byte) error { - _, err := c.conn.Write(cmd) - if err != nil { - return err +// WriteString Writes a string to the PLC data area +func (c *Client) WriteString(memoryArea byte, address uint16, itemCount uint16, s string) error { + if checkIsWordMemoryArea(memoryArea) == false { + return ErrIncompatibleMemoryArea + } + header := c.nextHeader() + bytes := make([]byte, 2*itemCount, 2*itemCount) + copy(bytes, s) + command := writeCommand(NewIOAddress(memoryArea, address), itemCount, bytes) + r, e := c.sendCommand(header, command) + if e != nil { + return e + } + if r.EndCode() != EndCodeNormalCompletion { + return fmt.Errorf("Error reported by destination, end code 0x%x", r.EndCode) } - <-c.resp[sid] return nil } -func (c *Client) ReadDAsync(startAddr uint16, readCount uint16, callback func(resp Response)) error { - sid := c.incrementSid() - cmd := readDCommand(defaultHeader(sid), startAddr, readCount) - return c.asyncCommand(sid, cmd, callback) +// WriteBits Writes bits to the PLC data area +func (c *Client) WriteBits(memoryArea byte, address uint16, bitOffset byte, data []bool) error { + if checkIsBitMemoryArea(memoryArea) == false { + return ErrIncompatibleMemoryArea + } + header := c.nextHeader() + l := uint16(len(data)) + bytes := make([]byte, 0, l) + var d byte + for i := 0; i < int(l); i++ { + if data[i] { + d = 0x01 + } else { + d = 0x00 + } + bytes = append(bytes, d) + } + command := writeCommand(NewIOAddressWithBitOffset(memoryArea, address, bitOffset), l, bytes) + + r, e := c.sendCommand(header, command) + if e != nil { + return e + } + if r.EndCode() != EndCodeNormalCompletion { + return fmt.Errorf("Error reported by destination, end code 0x%x", r.EndCode) + } + + return nil } -func (c *Client) WriteDAsync(startAddr uint16, data []uint16, callback func(resp Response)) error { - sid := c.incrementSid() - cmd := writeDCommand(defaultHeader(sid), startAddr, data) - return c.asyncCommand(sid, cmd, callback) +// SetBit Sets a bit in the PLC data area +func (c *Client) SetBit(memoryArea byte, address uint16, bitOffset byte) error { + return c.bitTwiddle(memoryArea, address, bitOffset, 0x01) } -func (c *Client) asyncCommand(sid byte, cmd []byte, callback func(resp Response)) error { - _, err := c.conn.Write(cmd) - if err != nil { - return err +// ResetBit Resets a bit in the PLC data area +func (c *Client) ResetBit(memoryArea byte, address uint16, bitOffset byte) error { + return c.bitTwiddle(memoryArea, address, bitOffset, 0x00) +} + +// ToggleBit Toggles a bit in the PLC data area +func (c *Client) ToggleBit(memoryArea byte, address uint16, bitOffset byte) error { + b, e := c.ReadBits(memoryArea, address, bitOffset, 1) + if e != nil { + return e } - asyncResponse(c.resp[sid], callback) - return nil + var t byte + if b[0] { + t = 0x00 + } else { + t = 0x01 + } + return c.bitTwiddle(memoryArea, address, bitOffset, t) } -func asyncResponse(ch chan Response, callback func(r Response)) { - if callback != nil { - go func(ch chan Response, callback func(r Response)) { - ans := <-ch - callback(ans) - }(ch, callback) +func (c *Client) bitTwiddle(memoryArea byte, address uint16, bitOffset byte, value byte) error { + if checkIsBitMemoryArea(memoryArea) == false { + return ErrIncompatibleMemoryArea } + header := c.nextHeader() + command := writeCommand(NewIOAddressWithBitOffset(memoryArea, address, bitOffset), 1, []byte{value}) + + r, e := c.sendCommand(header, command) + if e != nil { + return e + } + if r.EndCode() != EndCodeNormalCompletion { + return fmt.Errorf("Error reported by destination, end code 0x%x", r.EndCode) + } + + return nil } -func (c *Client) WriteDNoResponse(startAddr uint16, data []uint16) error { +// ErrIncompatibleMemoryArea Error when the memory area is incompatible with the data type to be read +var ErrIncompatibleMemoryArea = errors.New("The memory area is incompatible with the data type to be read") + +func (c *Client) nextHeader() *Header { sid := c.incrementSid() - cmd := writeDCommand(newHeaderNoResponse(sid), startAddr, data) - return c.asyncCommand(sid, cmd, nil) + header := defaultCommandHeader(c.dst, c.src, sid) + return header +} + +func (c *Client) incrementSid() byte { + c.Lock() //thread-safe sid incrementation + c.sid++ + sid := c.sid + c.Unlock() + c.resp[sid] = make(chan Frame) //clearing cell of storage for new response + return sid +} + +func (c *Client) sendCommand(header *Header, payload Payload) (*Response, error) { + bytes := encodeFrame(NewFrame(header, payload)) + _, err := (*c.conn).Write(bytes) + if err != nil { + return nil, err + } + + responseFrame := <-c.resp[header.ServiceID()] + p := responseFrame.Payload() + response := NewResponse( + p.CommandCode(), + binary.BigEndian.Uint16(p.Data()[0:2]), + p.Data()[2:]) + return response, nil } func (c *Client) listenLoop() { @@ -137,14 +298,64 @@ func (c *Client) listenLoop() { } if n > 0 { - ans, err := parseResponse(buf[0:n]) + ans := decodeFrame(buf[0:n]) if err != nil { log.Println("failed to parse response: ", err, " \nresponse: ", buf[0:n]) } else { - c.resp[ans.sid] <- *ans + c.resp[ans.Header().ServiceID()] <- *ans } } else { - log.Println("cannot read response: ", buf) + log.Println("Cannot read response: ", buf) } } } + +func checkIsWordMemoryArea(memoryArea byte) bool { + if memoryArea == MemoryAreaDMWord || + memoryArea == MemoryAreaARWord || + memoryArea == MemoryAreaHRWord { + return true + } + return false +} + +func checkIsBitMemoryArea(memoryArea byte) bool { + if memoryArea == MemoryAreaDMBit || + memoryArea == MemoryAreaARBit || + memoryArea == MemoryAreaHRBit { + return true + } + return false +} + +// @ToDo Asynchronous functions +// ReadDataAsync reads from the PLC data area asynchronously +// func (c *Client) ReadDataAsync(startAddr uint16, readCount uint16, callback func(resp response)) error { +// sid := c.incrementSid() +// cmd := readDCommand(defaultHeader(c.dst, c.src, sid), startAddr, readCount) +// return c.asyncCommand(sid, cmd, callback) +// } + +// WriteDataAsync writes to the PLC data area asynchronously +// func (c *Client) WriteDataAsync(startAddr uint16, data []uint16, callback func(resp response)) error { +// sid := c.incrementSid() +// cmd := writeDCommand(defaultHeader(c.dst, c.src, sid), startAddr, data) +// return c.asyncCommand(sid, cmd, callback) +// } +// func (c *Client) asyncCommand(sid byte, cmd []byte, callback func(resp response)) error { +// _, err := c.conn.Write(cmd) +// if err != nil { +// return err +// } +// asyncResponse(c.resp[sid], callback) +// return nil +// } + +// func asyncResponse(ch chan response, callback func(r response)) { +// if callback != nil { +// go func(ch chan response, callback func(r response)) { +// ans := <-ch +// callback(ans) +// }(ch, callback) +// } +// } diff --git a/fins/client_test.go b/fins/client_test.go index 982a175..cf530ef 100644 --- a/fins/client_test.go +++ b/fins/client_test.go @@ -1,12 +1,13 @@ package fins import ( - "testing" + "fmt" "log" "net" - "github.com/stretchr/testify/assert" "strconv" - "fmt" + "testing" + + "github.com/stretchr/testify/assert" ) func TestFinsClient(t *testing.T) { @@ -51,6 +52,7 @@ func makeWriteAnswer(sid byte, respNeeded bool) response { ans[9] = sid return response{data: ans, needed: respNeeded} } + func makeReadAnswer(sid byte, data []uint16) response { ans := make([]byte, 14) ans[9] = sid diff --git a/fins/command.go b/fins/command.go new file mode 100644 index 0000000..b636a5e --- /dev/null +++ b/fins/command.go @@ -0,0 +1,13 @@ +package fins + +// Command A FINS command +type Command struct { + payloadImpl +} + +func NewCommand(commandCode uint16, data []byte) *Command { + c := new(Command) + c.commandCode = commandCode + c.data = data + return c +} diff --git a/fins/command_code.go b/fins/command_code.go new file mode 100644 index 0000000..4b906f2 --- /dev/null +++ b/fins/command_code.go @@ -0,0 +1,138 @@ +package fins + +const ( + // CommandCodeMemoryAreaRead Command code: IO memory area read + CommandCodeMemoryAreaRead uint16 = 0x0101 + + // CommandCodeMemoryAreaWrite Command code: IO memory area write + CommandCodeMemoryAreaWrite uint16 = 0x0102 + + // CommandCodeMemoryAreaFill Command code: IO memory area fill + CommandCodeMemoryAreaFill uint16 = 0x0103 + + // CommandCodeMultipleMemoryAreaRead Command code: IO memory area multiple read + CommandCodeMultipleMemoryAreaRead uint16 = 0x0104 + + // CommandCodeMemoryAreaTransfer Command code: IO memory area transfer + CommandCodeMemoryAreaTransfer uint16 = 0x0105 + + // CommandCodeParameterAreaRead Command code: Parameter area read + CommandCodeParameterAreaRead uint16 = 0x0201 + + // CommandCodeParameterAreaWrite Command code: Parameter area write + CommandCodeParameterAreaWrite uint16 = 0x0202 + + // CommandCodeParameterAreaClear Command code: Parameter area clear + CommandCodeParameterAreaClear uint16 = 0x0203 + + // CommandCodeProgramAreaRead Command code: Program area read + CommandCodeProgramAreaRead uint16 = 0x0301 + + // CommandCodeProgramAreaWrite Command code: Program area write + CommandCodeProgramAreaWrite uint16 = 0x0302 + + // CommandCodeProgramAreaClear Command code: Program area clear + CommandCodeProgramAreaClear uint16 = 0x0303 + + // CommandCodeRun Command code: Set operating mode to run + CommandCodeRun uint16 = 0x0401 + + // CommandCodeStop Command code: Set operating mode to stop + CommandCodeStop uint16 = 0x0402 + + // CommandCodeCPUUnitDataRead Command code: CPU unit data read + CommandCodeCPUUnitDataRead uint16 = 0x0501 + + // CommandCodeConnectionDataRead Command code: connection data read + CommandCodeConnectionDataRead uint16 = 0x0502 + + // CommandCodeCPUUnitStatusRead Command code: CPU unit status read + CommandCodeCPUUnitStatusRead uint16 = 0x0601 + + // CommandCodeCycleTimeRead Command code: cycle time read + CommandCodeCycleTimeRead uint16 = 0x0620 + + // CommandCodeClockRead Command code: clock read + CommandCodeClockRead uint16 = 0x701 + + // CommandCodeClockWrite Command code: clock write + CommandCodeClockWrite uint16 = 0x702 + + // CommandCodeMessageReadClear Command code: message read/clear + CommandCodeMessageReadClear uint16 = 0x0920 + + // CommandCodeAccessRightAcquire Command code: access right acquire + CommandCodeAccessRightAcquire uint16 = 0x0c01 + + // CommandCodeAccessRightForcedAcquire Command code: accress right forced acquire + CommandCodeAccessRightForcedAcquire uint16 = 0x0c02 + + // CommandCodeAccessRightRelease Command code: access right release + CommandCodeAccessRightRelease uint16 = 0x0c03 + + // CommandCodeErrorClear Command code: error clear + CommandCodeErrorClear uint16 = 0x2101 + + // CommandCodeErrorLogRead Command code: error log read + CommandCodeErrorLogRead uint16 = 0x2102 + + // CommandCodeErrorLogClear Command code: error log clear + CommandCodeErrorLogClear uint16 = 0x2103 + + // CommandCodeFINSWriteAccessLogRead Command code: FINS write access log read + CommandCodeFINSWriteAccessLogRead uint16 = 0x2140 + + // CommandCodeFINSWriteAccessLogWrite Command code: FINS write access log write + CommandCodeFINSWriteAccessLogWrite uint16 = 0x2141 + + // CommandCodeFileNameRead Command code: file name read + CommandCodeFileNameRead uint16 = 0x2101 + + // CommandCodeSingleFileRead Command code: file read + CommandCodeSingleFileRead uint16 = 0x2102 + + // CommandCodeSingleFileWrite Command code: file write + CommandCodeSingleFileWrite uint16 = 0x2103 + + // CommandCodeFileMemoryFormat Command code: file memory format + CommandCodeFileMemoryFormat uint16 = 0x2104 + + // CommandCodeFileDelete Command code: file delete + CommandCodeFileDelete uint16 = 0x2105 + + // CommandCodeFileCopy Command code: file copy + CommandCodeFileCopy uint16 = 0x2107 + + // CommandCodeFileNameChange Command code: file name change + CommandCodeFileNameChange uint16 = 0x2108 + + // CommandCodeMemoryAreaFileTransfer Command code: memory area file transfer + CommandCodeMemoryAreaFileTransfer uint16 = 0x210a + + // CommandCodeParameterAreaFileTransfer Command code: parameter area file transfer + CommandCodeParameterAreaFileTransfer uint16 = 0x210b + + // CommandCodeProgramAreaFileTransfer Command code: program area file transfer + CommandCodeProgramAreaFileTransfer uint16 = 0x210b + + // CommandCodeDirectoryCreateDelete Command code: directory create/delete + CommandCodeDirectoryCreateDelete uint16 = 0x2115 + + // CommandCodeMemoryCassetteTransfer Command code: memory cassette transfer (CP1H and CP1L CPU units only) + CommandCodeMemoryCassetteTransfer uint16 = 0x2120 + + // CommandCodeForcedSetReset Command code: forced set/reset + CommandCodeForcedSetReset uint16 = 0x2301 + + // CommandCodeForcedSetResetCancel Command code: forced set/reset cancel + CommandCodeForcedSetResetCancel uint16 = 0x2302 + + // CommandCodeConvertToCompoWayFCommand Command code: convert to CompoWay/F command + CommandCodeConvertToCompoWayFCommand uint16 = 0x2803 + + // CommandCodeConvertToModbusRTUCommand Command code: convert to Modbus-RTU command + CommandCodeConvertToModbusRTUCommand uint16 = 0x2804 + + // CommandCodeConvertToModbusASCIICommand Command code: convert to Modbus-ASCII command + CommandCodeConvertToModbusASCIICommand uint16 = 0x2805 +) diff --git a/fins/driver.go b/fins/driver.go index 1d14210..d92f929 100644 --- a/fins/driver.go +++ b/fins/driver.go @@ -1,119 +1,183 @@ package fins import ( - "fmt" + "encoding/binary" "errors" ) -/* -module.exports.Commands = { - CONTROLLER_STATUS_READ : [0x06,0x01], - MEMORY_AREA_READ : [0x01,0x01], - MEMORY_AREA_WRITE : [0x01,0x02], - MEMORY_AREA_FILL : [0x01,0x03], - RUN : [0x04,0x01], - STOP : [0x04,0x02] -}; -module.exports.MemoryAreas = { - 'E' : 0xA0,//Extended Memories - 'C' : 0xB0,//CIO - 'W' : 0xB1,//Work Area - 'H' : 0xB2,//Holding Bit - 'A' : 0xB3,//Auxiliary Bit - 'D' : 0x82//Data Memories -}; -*/ -//TODO: implement all areas and commands -var CMD_MEMORY_AREA_READ = []byte{0x01, 0x01} -var CMD_MEMORY_AREA_WRITE = []byte{0x01, 0x02} - -var MEMORY_AREA_DATA = byte(0x82) -var MEMORY_AREA_WORK = byte(0xB1) - -func readDCommand(header *Header, startAddress uint16, readCount uint16) []byte { - return readCommand(MEMORY_AREA_DATA, header, startAddress, readCount) +func readCommand(ioAddr *IOAddress, itemCount uint16) *Command { + commandData := make([]byte, 0, 6) + commandData = append(commandData, encodeIOAddress(ioAddr)...) + commandData = append(commandData, []byte{0, 0}...) + binary.BigEndian.PutUint16(commandData[4:6], itemCount) + return NewCommand(CommandCodeMemoryAreaRead, commandData) } -func readWCommand(header *Header, startAddress uint16, readCount uint16) []byte { - return readCommand(MEMORY_AREA_WORK, header, startAddress, readCount) +func writeCommand(ioAddr *IOAddress, itemCount uint16, bytes []byte) *Command { + commandData := make([]byte, 0, 6+len(bytes)) + commandData = append(commandData, encodeIOAddress(ioAddr)...) + commandData = append(commandData, []byte{0, 0}...) + binary.BigEndian.PutUint16(commandData[4:6], itemCount) + commandData = append(commandData, bytes...) + return NewCommand(CommandCodeMemoryAreaWrite, commandData) } -func readCommand(memoryArea byte, header *Header, startAddress uint16, readCount uint16) []byte { - var addressBit byte = 0 - addressLower := byte(startAddress) - addressUpper := byte(startAddress >> 8) - countLower := byte(readCount) - countUpper := byte(readCount >> 8) - - paramsBytes := []byte{ - memoryArea, - addressUpper, addressLower, - addressBit, - countUpper, countLower} - - bytes1 := append(header.Format(), CMD_MEMORY_AREA_READ...) - bytes2 := append(bytes1, paramsBytes...) - return bytes2 +func encodeIOAddress(ioAddr *IOAddress) []byte { + bytes := make([]byte, 4, 4) + bytes[0] = ioAddr.MemoryArea() + binary.BigEndian.PutUint16(bytes[1:3], ioAddr.Address()) + bytes[3] = ioAddr.BitOffset() + return bytes } -func writeDCommand(header *Header, startAddress uint16, data []uint16) []byte { - return writeCommand(MEMORY_AREA_DATA, header, startAddress, data) +func decodeFrame(bytes []byte) *Frame { + header := decodeHeader(bytes[0:10]) + var payload Payload + if header.FrameIsCommand() { + payload = decodeCommand(bytes[10:]) + } else if header.FrameIsResponse() { + payload = decodeResponse(bytes[10:]) + } + frame := NewFrame(header, payload) + return frame } -func writeWCommand(header *Header, startAddress uint16, data []uint16) []byte { - return writeCommand(MEMORY_AREA_WORK, header, startAddress, data) +func encodeFrame(f *Frame) []byte { + bytes := encodeHeader(f.Header()) + var payloadData []byte + if f.Header().FrameIsCommand() { + payloadData = encodeCommand(f.Payload().(*Command)) + } else if f.Header().FrameIsResponse() { + payloadData = encodeResponse(f.Payload().(*Response)) + } + bytes = append(bytes, payloadData...) + return bytes } -func writeCommand(memoryArea byte, header *Header, startAddress uint16, data []uint16) []byte { - var addressBit byte = 0 - addressLower := byte(startAddress) - addressUpper := byte(startAddress >> 8) - dataLen := len(data) - lenLower := byte(dataLen) - lenUpper := byte(dataLen >> 8) - - paramsBytes := []byte{ - memoryArea, - addressUpper, addressLower, - addressBit, - lenUpper, lenLower} - - bytes1 := append(header.Format(), CMD_MEMORY_AREA_WRITE...) - bytes2 := append(bytes1, paramsBytes...) - bytes3 := append(bytes2, toBytes(data)...) - return bytes3 +const ( + icfBridgesBit byte = 7 + icfMessageTypeBit byte = 6 + icfResponseRequiredBit byte = 0 +) + +func decodeHeader(bytes []byte) *Header { + header := new(Header) + icf := bytes[0] + if icf&1<> 8) - res[2*i+1] = byte(data[i]) +func encodeHeader(h *Header) []byte { + var icf byte + icf = 0x80 + if h.responseRequired == false { + icf |= 1 << icfResponseRequiredBit + } + if h.messgeType == MessageTypeResponse { + icf |= 1 << icfMessageTypeBit } - return res + bytes := []byte{ + icf, 0x00, h.gatewayCount, + h.dst.Network(), h.dst.Node(), h.dst.Unit(), + h.src.Network(), h.src.Node(), h.src.Unit(), + h.serviceID} + return bytes } -func parseResponse(bytes []byte) (*Response, error) { - finishCode1 := bytes[12] - finishCode2 := bytes[13] +func decodeCommand(bytes []byte) *Command { + return NewCommand( + binary.BigEndian.Uint16(bytes[0:2]), + bytes[2:]) +} + +func encodeCommand(command *Command) []byte { + bytes := make([]byte, 2, 2+len(command.Data())) + binary.BigEndian.PutUint16(bytes[0:2], command.CommandCode()) + bytes = append(bytes, command.Data()...) + return bytes +} - if finishCode1 != 0 || finishCode2 != 0 { - msg := fmt.Sprintln("failure code: ", finishCode1, ": ", finishCode2) - return nil, errors.New(msg) +func decodeResponse(bytes []byte) *Response { + return NewResponse( + binary.BigEndian.Uint16(bytes[0:2]), + binary.BigEndian.Uint16(bytes[2:4]), + bytes[4:]) +} + +func encodeResponse(response *Response) []byte { + bytes := make([]byte, 4, 4+len(response.Data())) + binary.BigEndian.PutUint16(bytes[0:2], response.CommandCode()) + binary.BigEndian.PutUint16(bytes[2:4], response.EndCode()) + bytes = append(bytes, response.Data()...) + return bytes +} + +var errBCDBadDigit = errors.New("Bad digit in BCD decoding") +var errBCDOverflow = errors.New("Overflow occurred in BCD decoding") + +func encodeBCD(x uint64) []byte { + if x == 0 { + return []byte{0x0f} } + var n int + for xx := x; xx > 0; n++ { + xx = xx / 10 + } + bcd := make([]byte, (n+1)/2) + if n%2 == 1 { + hi, lo := byte(x%10), byte(0x0f) + bcd[(n-1)/2] = hi<<4 | lo + x = x / 10 + n-- + } + for i := n/2 - 1; i >= 0; i-- { + hi, lo := byte((x/10)%10), byte(x%10) + bcd[i] = hi<<4 | lo + x = x / 100 + } + return bcd +} - return &Response{ - sid: bytes[9], - Data: toUint16(bytes[14:]), - }, nil +func timesTenPlusCatchingOverflow(x uint64, digit uint64) (uint64, error) { + x5 := x<<2 + x + if int64(x5) < 0 || x5<<1 > ^digit { + return 0, errBCDOverflow + } + return x5<<1 + digit, nil } -func toUint16(data []byte) []uint16 { - res := make([]uint16, len(data)/2) - for i := 0; i < len(data); i += 2 { - upper := uint16(data[i]) << 8 - lower := uint16(data[i+1]) - res[i/2] = (upper | lower) +func decodeBCD(bcd []byte) (x uint64, err error) { + for i, b := range bcd { + hi, lo := uint64(b>>4), uint64(b&0x0f) + if hi > 9 { + return 0, errBCDBadDigit + } + x, err = timesTenPlusCatchingOverflow(x, hi) + if err != nil { + return 0, err + } + if lo == 0x0f && i == len(bcd)-1 { + return x, nil + } + if lo > 9 { + return 0, errBCDBadDigit + } + x, err = timesTenPlusCatchingOverflow(x, lo) + if err != nil { + return 0, err + } } - return res + return x, nil } diff --git a/fins/end_code.go b/fins/end_code.go new file mode 100644 index 0000000..6ce9ea4 --- /dev/null +++ b/fins/end_code.go @@ -0,0 +1,259 @@ +package fins + +// Data taken from Omron document Cat. No. W342-E1-15, pages 155-161 +const ( + // EndCodeNormalCompletion End code: normal completion + EndCodeNormalCompletion uint16 = 0x0000 + + // EndCodeServiceInterrupted End code: normal completion; service was interrupted + EndCodeServiceInterrupted uint16 = 0x0001 + + // EndCodeLocalNodeNotInNetwork End code: local node error; local node not in network + EndCodeLocalNodeNotInNetwork uint16 = 0x0101 + + // EndCodeTokenTimeout End code: local node error; token timeout + EndCodeTokenTimeout uint16 = 0x0102 + + // EndCodeRetriesFailed End code: local node error; retries failed + EndCodeRetriesFailed uint16 = 0x0103 + + // EndCodeTooManySendFrames End code: local node error; too many send frames + EndCodeTooManySendFrames uint16 = 0x0104 + + // EndCodeNodeAddressRangeError End code: local node error; node address range error + EndCodeNodeAddressRangeError uint16 = 0x0105 + + // EndCodeNodeAddressRangeDuplication End code: local node error; node address range duplication + EndCodeNodeAddressRangeDuplication uint16 = 0x0106 + + // EndCodeDestinationNodeNotInNetwork End code: destination node error; destination node not in network + EndCodeDestinationNodeNotInNetwork uint16 = 0x0201 + + // EndCodeUnitMissing End code: destination node error; unit missing + EndCodeUnitMissing uint16 = 0x0202 + + // EndCodeThirdNodeMissing End code: destination node error; third node missing + EndCodeThirdNodeMissing uint16 = 0x0203 + + // EndCodeDestinationNodeBusy End code: destination node error; destination node busy + EndCodeDestinationNodeBusy uint16 = 0x0204 + + // EndCodeResponseTimeout End code: destination node error; response timeout + EndCodeResponseTimeout uint16 = 0x0205 + + // EndCodeCommunicationsControllerError End code: controller error; communication controller error + EndCodeCommunicationsControllerError uint16 = 0x0301 + + // EndCodeCPUUnitError End code: controller error; CPU unit error + EndCodeCPUUnitError uint16 = 0x0302 + + // EndCodeControllerError End code: controller error; controller error + EndCodeControllerError uint16 = 0x0303 + + // EndCodeUnitNumberError End code: controller error; unit number error + EndCodeUnitNumberError uint16 = 0x0304 + + // EndCodeUndefinedCommand End code: service unsupported; undefined command + EndCodeUndefinedCommand uint16 = 0x0401 + + // EndCodeNotSupportedByModelVersion End code: service unsupported; not supported by model version + EndCodeNotSupportedByModelVersion uint16 = 0x0402 + + // EndCodeDestinationAddressSettingError End code: routing table error; destination address setting error + EndCodeDestinationAddressSettingError uint16 = 0x0501 + + // EndCodeNoRoutingTables End code: routing table error; no routing tables + EndCodeNoRoutingTables uint16 = 0x0502 + + // EndCodeRoutingTableError End code: routing table error; routing table error + EndCodeRoutingTableError uint16 = 0x0503 + + // EndCodeTooManyRelays End code: routing table error; too many relays + EndCodeTooManyRelays uint16 = 0x0504 + + // EndCodeCommandTooLong End code: command format error; command too long + EndCodeCommandTooLong uint16 = 0x1001 + + // EndCodeCommandTooShort End code: command format error; command too short + EndCodeCommandTooShort uint16 = 0x1002 + + // EndCodeElementsDataDontMatch End code: command format error; elements/data don't match + EndCodeElementsDataDontMatch uint16 = 0x1003 + + // EndCodeCommandFormatError End code: command format error; command format error + EndCodeCommandFormatError uint16 = 0x1004 + + // EndCodeHeaderError End code: command format error; header error + EndCodeHeaderError uint16 = 0x1005 + + // EndCodeAreaClassificationMissing End code: parameter error; classification missing + EndCodeAreaClassificationMissing uint16 = 0x1101 + + // EndCodeAccessSizeError End code: parameter error; access size error + EndCodeAccessSizeError uint16 = 0x1102 + + // EndCodeAddressRangeError End code: parameter error; address range error + EndCodeAddressRangeError uint16 = 0x1103 + + // EndCodeAddressRangeExceeded End code: parameter error; address range exceeded + EndCodeAddressRangeExceeded uint16 = 0x1104 + + // EndCodeProgramMissing End code: parameter error; program missing + EndCodeProgramMissing uint16 = 0x1106 + + // EndCodeRelationalError End code: parameter error; relational error + EndCodeRelationalError uint16 = 0x1109 + + // EndCodeDuplicateDataAccess End code: parameter error; duplicate data access + EndCodeDuplicateDataAccess uint16 = 0x110a + + // EndCodeResponseTooBig End code: parameter error; response too big + EndCodeResponseTooBig uint16 = 0x110b + + // EndCodeParameterError End code: parameter error + EndCodeParameterError uint16 = 0x110c + + // EndCodeReadNotPossibleProtected End code: read not possible; protected + EndCodeReadNotPossibleProtected uint16 = 0x2002 + + // EndCodeReadNotPossibleTableMissing End code: read not possible; table missing + EndCodeReadNotPossibleTableMissing uint16 = 0x2003 + + // EndCodeReadNotPossibleDataMissing End code: read not possible; data missing + EndCodeReadNotPossibleDataMissing uint16 = 0x2004 + + // EndCodeReadNotPossibleProgramMissing End code: read not possible; program missing + EndCodeReadNotPossibleProgramMissing uint16 = 0x2005 + + // EndCodeReadNotPossibleFileMissing End code: read not possible; file missing + EndCodeReadNotPossibleFileMissing uint16 = 0x2006 + + // EndCodeReadNotPossibleDataMismatch End code: read not possible; data mismatch + EndCodeReadNotPossibleDataMismatch uint16 = 0x2007 + + // EndCodeWriteNotPossibleReadOnly End code: write not possible; read only + EndCodeWriteNotPossibleReadOnly uint16 = 0x2101 + + // EndCodeWriteNotPossibleProtected End code: write not possible; write protected + EndCodeWriteNotPossibleProtected uint16 = 0x2102 + + // EndCodeWriteNotPossibleCannotRegister End code: write not possible; cannot register + EndCodeWriteNotPossibleCannotRegister uint16 = 0x2103 + + // EndCodeWriteNotPossibleProgramMissing End code: write not possible; program missing + EndCodeWriteNotPossibleProgramMissing uint16 = 0x2105 + + // EndCodeWriteNotPossibleFileMissing End code: write not possible; file missing + EndCodeWriteNotPossibleFileMissing uint16 = 0x2106 + + // EndCodeWriteNotPossibleFileNameAlreadyExists End code: write not possible; file name already exists + EndCodeWriteNotPossibleFileNameAlreadyExists uint16 = 0x2107 + + // EndCodeWriteNotPossibleCannotChange End code: write not possible; cannot change + EndCodeWriteNotPossibleCannotChange uint16 = 0x2108 + + // EndCodeNotExecutableInCurrentModeNotPossibleDuringExecution End code: not executeable in current mode during execution + EndCodeNotExecutableInCurrentModeNotPossibleDuringExecution uint16 = 0x2201 + + // EndCodeNotExecutableInCurrentModeNotPossibleWhileRunning End code: not executeable in current mode while running + EndCodeNotExecutableInCurrentModeNotPossibleWhileRunning uint16 = 0x2202 + + // EndCodeNotExecutableInCurrentModeWrongPLCModeInProgram End code: not executeable in current mode; PLC is in PROGRAM mode + EndCodeNotExecutableInCurrentModeWrongPLCModeInProgram uint16 = 0x2203 + + // EndCodeNotExecutableInCurrentModeWrongPLCModeInDebug End code: not executeable in current mode; PLC is in DEBUG mode + EndCodeNotExecutableInCurrentModeWrongPLCModeInDebug uint16 = 0x2204 + + // EndCodeNotExecutableInCurrentModeWrongPLCModeInMonitor End code: not executeable in current mode; PLC is in MONITOR mode + EndCodeNotExecutableInCurrentModeWrongPLCModeInMonitor uint16 = 0x2205 + + // EndCodeNotExecutableInCurrentModeWrongPLCModeInRun End code: not executeable in current mode; PLC is in RUN mode + EndCodeNotExecutableInCurrentModeWrongPLCModeInRun uint16 = 0x2206 + + // EndCodeNotExecutableInCurrentModeSpecifiedNodeNotPollingNode End code: not executeable in current mode; specified node is not polling node + EndCodeNotExecutableInCurrentModeSpecifiedNodeNotPollingNode uint16 = 0x2207 + + // EndCodeNotExecutableInCurrentModeStepCannotBeExecuted End code: not executeable in current mode; step cannot be executed + EndCodeNotExecutableInCurrentModeStepCannotBeExecuted uint16 = 0x2208 + + // EndCodeNoSuchDeviceFileDeviceMissing End code: no such device; file device missing + EndCodeNoSuchDeviceFileDeviceMissing uint16 = 0x2301 + + // EndCodeNoSuchDeviceMemoryMissing End code: no such device; memory missing + EndCodeNoSuchDeviceMemoryMissing uint16 = 0x2302 + + // EndCodeNoSuchDeviceClockMissing End code: no such device; clock missing + EndCodeNoSuchDeviceClockMissing uint16 = 0x2303 + + // EndCodeCannotStartStopTableMissing End code: cannot start/stop; table missing + EndCodeCannotStartStopTableMissing uint16 = 0x2401 + + // EndCodeUnitErrorMemoryError End code: unit error; memory error + EndCodeUnitErrorMemoryError uint16 = 0x2502 + + // EndCodeUnitErrorIOError End code: unit error; IO error + EndCodeUnitErrorIOError uint16 = 0x2503 + + // EndCodeUnitErrorTooManyIOPoints End code: unit error; too many IO points + EndCodeUnitErrorTooManyIOPoints uint16 = 0x2504 + + // EndCodeUnitErrorCPUBusError End code: unit error; CPU bus error + EndCodeUnitErrorCPUBusError uint16 = 0x2505 + + // EndCodeUnitErrorIODuplication End code: unit error; IO duplication + EndCodeUnitErrorIODuplication uint16 = 0x2506 + + // EndCodeUnitErrorIOBusError End code: unit error; IO bus error + EndCodeUnitErrorIOBusError uint16 = 0x2507 + + // EndCodeUnitErrorSYSMACBUS2Error End code: unit error; SYSMAC BUS/2 error + EndCodeUnitErrorSYSMACBUS2Error uint16 = 0x2509 + + // EndCodeUnitErrorCPUBusUnitError End code: unit error; CPU bus unit error + EndCodeUnitErrorCPUBusUnitError uint16 = 0x250a + + // EndCodeUnitErrorSYSMACBusNumberDuplication End code: unit error; SYSMAC bus number duplication + EndCodeUnitErrorSYSMACBusNumberDuplication uint16 = 0x250d + + // EndCodeUnitErrorMemoryStatusError End code: unit error; memory status error + EndCodeUnitErrorMemoryStatusError uint16 = 0x250f + + // EndCodeUnitErrorSYSMACBusTerminatorMissing End code: unit error; SYSMAC bus terminator missing + EndCodeUnitErrorSYSMACBusTerminatorMissing uint16 = 0x2510 + + // EndCodeCommandErrorNoProtection End code: command error; no protection + EndCodeCommandErrorNoProtection uint16 = 0x2601 + + // EndCodeCommandErrorIncorrectPassword End code: command error; incorrect password + EndCodeCommandErrorIncorrectPassword uint16 = 0x2602 + + // EndCodeCommandErrorProtected End code: command error; protected + EndCodeCommandErrorProtected uint16 = 0x2604 + + // EndCodeCommandErrorServiceAlreadyExecuting End code: command error; service already executing + EndCodeCommandErrorServiceAlreadyExecuting uint16 = 0x2605 + + // EndCodeCommandErrorServiceStopped End code: command error; service stopped + EndCodeCommandErrorServiceStopped uint16 = 0x2606 + + // EndCodeCommandErrorNoExecutionRight End code: command error; no execution right + EndCodeCommandErrorNoExecutionRight uint16 = 0x2607 + + // EndCodeCommandErrorSettingsNotComplete End code: command error; settings not complete + EndCodeCommandErrorSettingsNotComplete uint16 = 0x2608 + + // EndCodeCommandErrorNecessaryItemsNotSet End code: command error; necessary items not set + EndCodeCommandErrorNecessaryItemsNotSet uint16 = 0x2609 + + // EndCodeCommandErrorNumberAlreadyDefined End code: command error; number already defined + EndCodeCommandErrorNumberAlreadyDefined uint16 = 0x260a + + // EndCodeCommandErrorErrorWillNotClear End code: command error; error will not clear + EndCodeCommandErrorErrorWillNotClear uint16 = 0x260b + + // EndCodeAccessWriteErrorNoAccessRight End code: access write error; no access right + EndCodeAccessWriteErrorNoAccessRight uint16 = 0x3001 + + // EndCodeAbortServiceAborted End code: abort; service aborted + EndCodeAbortServiceAborted uint16 = 0x4001 +) diff --git a/fins/frame.go b/fins/frame.go new file mode 100644 index 0000000..902fd92 --- /dev/null +++ b/fins/frame.go @@ -0,0 +1,23 @@ +package fins + +// Frame A FINS frame +type Frame struct { + header *Header + payload Payload +} + +// NewFrame Creates a new FINS frame +func NewFrame(header *Header, payload Payload) *Frame { + f := new(Frame) + f.header = header + f.payload = payload + return f +} + +func (f *Frame) Header() *Header { + return f.header +} + +func (f *Frame) Payload() Payload { + return f.payload +} diff --git a/fins/header.go b/fins/header.go index 16b3759..eed21d8 100644 --- a/fins/header.go +++ b/fins/header.go @@ -1,86 +1,95 @@ package fins -// For now we have only one PLC - it means we do not need in any networks settings - -// almost everywhere is 0. +// Header A FINS frame header type Header struct { - icf byte - rsv byte - gct byte - dna byte - da1 byte - da2 byte - sna byte - sa1 byte - sa2 byte - sid byte + messgeType uint8 + responseRequired bool + dst *Address + src *Address + serviceID byte + gatewayCount uint8 } -func defaultHeader(sid byte) *Header { - h := new(Header) - h.icf = icf() - h.rsv = rsv() - h.gct = gct() - h.dna = dstNetwork() - h.da1 = dstNode() - h.da2 = dstUnit() - h.sna = srcNetwork() - h.sa1 = srcNode() - h.sa2 = srcUnit() - h.sid = sid - return h -} +const ( + // MessageTypeCommand Command message type + MessageTypeCommand uint8 = iota -func newHeaderNoResponse(sid byte) *Header { - h := defaultHeader(sid) - h.icf = icfNoResponse() - return h -} - -func (f *Header) Format() []byte { + // MessageTypeResponse Response message type + MessageTypeResponse uint8 = iota +) - return []byte{ - f.icf, f.rsv, f.gct, - f.dna, f.da1, f.da2, - f.sna, f.sa1, f.sa2, - f.sid} +// IsResponseRequired Returns true if this header indicates that a response should be required +func (h *Header) IsResponseRequired() bool { + return h.responseRequired } -func icf() byte { - return 0x80 //128 +// FrameIsCommand Returns true if the frame this header was contained within was a command +func (h *Header) FrameIsCommand() bool { + return h.messgeType == MessageTypeCommand } -func icfNoResponse() byte { - return 0x81 //129 +// FrameIsResponse Returns true if the frame this header was contained within was a response +func (h *Header) FrameIsResponse() bool { + return h.messgeType == MessageTypeResponse } -func rsv() byte { - return 0 -} +// SetToRequireResponse Will set this header to indicate that a response is required +// func (h *Header) SetToRequireResponse() { +// h.responseRequired = true +// } -func gct() byte { - return 0x02 +// SetToRequireNoResponse Will set this header to indicate that a response is not required +// func (h *Header) SetToRequireNoResponse() { +// h.responseRequired = false +// } + +// SetToCommandMessageType Will set this header to indicate that the message is a command +// func (h *Header) SetToCommandMessageType() { +// h.messgeType = MessageTypeCommand +// } + +// SetToResponseMessageType Will set this header to indicate that the message is a response +// func (h *Header) SetToResponseMessageType() { +// h.messgeType = MessageTypeResponse +// } + +// GatewayCount Gets the gateway count +func (h *Header) GatewayCount() byte { + return h.gatewayCount } -func dstNetwork() byte { - return 0 +// SourceAddress Gets the source address +func (h *Header) SourceAddress() Address { + return *h.src } -func dstNode() byte { - return 0 +// DestinationAddress Gets the destination address +func (h *Header) DestinationAddress() Address { + return *h.dst } -func dstUnit() byte { - return 0 +// ServiceID Gets the service id +func (h *Header) ServiceID() byte { + return h.serviceID } -func srcNetwork() byte { - return 0 +func defaultHeader(messageType uint8, responseRequired bool, dst *Address, src *Address, serviceID byte) *Header { + h := new(Header) + h.messgeType = messageType + h.responseRequired = responseRequired + h.gatewayCount = 2 + h.dst = dst + h.src = src + h.serviceID = serviceID + return h } -func srcNode() byte { - return 0x22 +func defaultCommandHeader(dst *Address, src *Address, serviceID byte) *Header { + h := defaultHeader(MessageTypeCommand, true, src, dst, serviceID) + return h } -func srcUnit() byte { - return 0 +func defaultResponseHeader(commandHeader *Header) *Header { + h := defaultHeader(MessageTypeResponse, false, commandHeader.src, commandHeader.dst, commandHeader.serviceID) + return h } diff --git a/fins/io_address.go b/fins/io_address.go new file mode 100644 index 0000000..275ad1d --- /dev/null +++ b/fins/io_address.go @@ -0,0 +1,32 @@ +package fins + +// IOAddress A FINS IO address representing some type of data or work area within the PLC +type IOAddress struct { + memoryArea byte + address uint16 + bitOffset byte +} + +func NewIOAddress(memoryArea byte, address uint16) *IOAddress { + return NewIOAddressWithBitOffset(memoryArea, address, 0) +} + +func NewIOAddressWithBitOffset(memoryArea byte, address uint16, bitOffset byte) *IOAddress { + ioAddr := new(IOAddress) + ioAddr.memoryArea = memoryArea + ioAddr.address = address + ioAddr.bitOffset = bitOffset + return ioAddr +} + +func (ioAddr *IOAddress) MemoryArea() byte { + return ioAddr.memoryArea +} + +func (ioAddr *IOAddress) Address() uint16 { + return ioAddr.address +} + +func (ioAddr *IOAddress) BitOffset() byte { + return ioAddr.bitOffset +} diff --git a/fins/memory_area.go b/fins/memory_area.go new file mode 100644 index 0000000..4e73c83 --- /dev/null +++ b/fins/memory_area.go @@ -0,0 +1,54 @@ +package fins + +const ( + // MemoryAreaCIOBit Memory area: CIO area; bit + MemoryAreaCIOBit byte = 0x30 + + // MemoryAreaWRBit Memory area: work area; bit + MemoryAreaWRBit byte = 0x31 + + // MemoryAreaHRBit Memory area: holding area; bit + MemoryAreaHRBit byte = 0x32 + + // MemoryAreaARBit Memory area: axuillary area; bit + MemoryAreaARBit byte = 0x33 + + // MemoryAreaCIOWord Memory area: CIO area; word + MemoryAreaCIOWord byte = 0xb0 + + // MemoryAreaWRWord Memory area: work area; word + MemoryAreaWRWord byte = 0xb1 + + // MemoryAreaHRWord Memory area: holding area; word + MemoryAreaHRWord byte = 0xb2 + + // MemoryAreaARWord Memory area: auxillary area; word + MemoryAreaARWord byte = 0xb3 + + // MemoryAreaTimerCounterCompletionFlag Memory area: counter completin flag + MemoryAreaTimerCounterCompletionFlag byte = 0x09 + + // MemoryAreaTimerCounterPV Memory area: counter PV + MemoryAreaTimerCounterPV byte = 0x89 + + // MemoryAreaDMBit Memory area: data area; bit + MemoryAreaDMBit byte = 0x02 + + // MemoryAreaDMWord Memory area: data area; word + MemoryAreaDMWord byte = 0x82 + + // MemoryAreaTaskBit Memory area: task flags; bit + MemoryAreaTaskBit byte = 0x06 + + // MemoryAreaTaskStatus Memory area: task flags; status + MemoryAreaTaskStatus byte = 0x46 + + // MemoryAreaIndexRegisterPV Memory area: CIO bit + MemoryAreaIndexRegisterPV byte = 0xdc + + // MemoryAreaDataRegisterPV Memory area: CIO bit + MemoryAreaDataRegisterPV byte = 0xbc + + // MemoryAreaClockPulsesConditionFlagsBit Memory area: CIO bit + MemoryAreaClockPulsesConditionFlagsBit byte = 0x07 +) diff --git a/fins/payload.go b/fins/payload.go new file mode 100644 index 0000000..0f28bd5 --- /dev/null +++ b/fins/payload.go @@ -0,0 +1,20 @@ +package fins + +// Payload A FINS frame payload +type Payload interface { + CommandCode() uint16 + Data() []byte +} + +type payloadImpl struct { + commandCode uint16 + data []byte +} + +func (p *payloadImpl) CommandCode() uint16 { + return p.commandCode +} + +func (p *payloadImpl) Data() []byte { + return p.data +} diff --git a/fins/response.go b/fins/response.go new file mode 100644 index 0000000..8ab61f0 --- /dev/null +++ b/fins/response.go @@ -0,0 +1,19 @@ +package fins + +// Response A FINS command response +type Response struct { + payloadImpl + endCode uint16 +} + +func NewResponse(commandCode uint16, endCode uint16, data []byte) *Response { + r := new(Response) + r.commandCode = commandCode + r.endCode = endCode + r.data = data + return r +} + +func (r *Response) EndCode() uint16 { + return r.endCode +} diff --git a/fins/server.go b/fins/server.go new file mode 100644 index 0000000..86ebede --- /dev/null +++ b/fins/server.go @@ -0,0 +1,80 @@ +package fins + +import ( + "fmt" + "net" +) + +// Server Omron FINS server (PLC emulator) +type Server struct { + conn *net.UDPConn + addr *Address + handler CommandHandler +} + +type CommandHandler func(*Command) *Response + +func NewServer(udpAddr *net.UDPAddr, addr *Address, handler CommandHandler) (*Server, error) { + s := new(Server) + + conn, err := net.ListenUDP("udp", udpAddr) + if err != nil { + return nil, err + } + s.conn = conn + s.addr = addr + if handler == nil { + s.handler = func(command *Command) *Response { + fmt.Printf("Null command handler: 0x%04x\n", command.CommandCode()) + + response := NewResponse(command.CommandCode(), EndCodeNotSupportedByModelVersion, []byte{}) + return response + } + } else { + s.handler = handler + } + + go func() { + var buf [1024]byte + for { + //rlen + rlen, remote, err := conn.ReadFromUDP(buf[:]) + cmdFrame := decodeFrame(buf[:rlen]) + cmd := cmdFrame.Payload().(*Command) + rsp := s.handler(cmd) + if err != nil { + panic(err) + } + + rspFrame := NewFrame(defaultResponseHeader(cmdFrame.Header()), rsp) + _, err = conn.WriteToUDP(encodeFrame(rspFrame), &net.UDPAddr{IP: remote.IP, Port: remote.Port}) + if err != nil { + panic(err) + } + } + }() + + return s, nil +} + +// Close Closes the FINS server +func (s *Server) Close() { + s.conn.Close() +} + +// Handles incoming requests. +func handleRequest(conn net.Conn) { + // Make a buffer to hold incoming data. + buf := make([]byte, 1024) + // Read the incoming connection into the buffer. + reqLen, err := conn.Read(buf) + if err != nil { + fmt.Println("Error reading:", err.Error()) + } + + fmt.Printf("Received %d bytes\n", reqLen) + // Send a response back to person contacting us. + conn.Write([]byte("Message received.")) + // Close the connection when you're done with it. + conn.Close() +}