-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- New way of adding routes, makes it cleaner - Handlers per route type - Adding the concept of a BaseGateway for easy extending - Adding more tests
- Loading branch information
1 parent
d51f4c8
commit 3e5f611
Showing
10 changed files
with
653 additions
and
391 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 |
---|---|---|
@@ -1,4 +1,5 @@ | ||
__pycache__/ | ||
*.py[cod] | ||
*$py.class | ||
*.vscode | ||
*.vscode | ||
node_modules |
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,54 @@ | ||
package canopen | ||
|
||
// BaseGateway implements all the basic gateway features defined by CiA 309 | ||
// CiA 309 currently defines 4 types: | ||
// CiA 309-2 : Modbus TCP | ||
// CiA 309-3 : ASCII | ||
// CiA 309-4 : Profinet | ||
// CiA 309-5 : HTTP / Websocket | ||
// Each gateway maps its own parsing logic to this base gateway | ||
type BaseGateway struct { | ||
network *Network | ||
defaultNetwork uint16 | ||
defaultNodeId uint8 | ||
} | ||
|
||
type GatewayVersion struct { | ||
vendorId string | ||
productCode string | ||
revisionNumber string | ||
serialNumber string | ||
gatewayClass string | ||
protocolVersion string | ||
implementationClass string | ||
} | ||
|
||
// Set default network to use | ||
func (gw *BaseGateway) SetDefaultNetwork(id uint16) error { | ||
gw.defaultNetwork = id | ||
return nil | ||
} | ||
|
||
// Set default node Id to use | ||
func (gw *BaseGateway) SetDefaultNodeId(id uint8) error { | ||
gw.defaultNodeId = id | ||
return nil | ||
} | ||
|
||
// Get gateway version information | ||
func (gw *BaseGateway) GetVersion() (GatewayVersion, error) { | ||
return GatewayVersion{}, nil | ||
} | ||
|
||
// Broadcast nmt command to one or all nodes | ||
func (gw *BaseGateway) NMTCommand(id uint8, command NMTCommand) error { | ||
return gw.network.Command(id, command) | ||
} | ||
|
||
// Set SDO timeout | ||
func (gw *BaseGateway) SetSDOTimeout(timeoutMs uint32) error { | ||
// TODO : maybe add mutex in case ongoing transfer | ||
gw.network.sdoClient.timeoutTimeUs = timeoutMs * 1000 | ||
gw.network.sdoClient.timeoutTimeBlockTransferUs = timeoutMs * 1000 | ||
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
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,152 @@ | ||
package canopen | ||
|
||
import ( | ||
"encoding/hex" | ||
"encoding/json" | ||
"net/http" | ||
"strconv" | ||
|
||
log "github.com/sirupsen/logrus" | ||
) | ||
|
||
// A ResponseWriter but keeps track of any writes already done | ||
// This is useful for custom processing in each handler | ||
// But adding default behaviour for errors / success | ||
|
||
type doneWriter struct { | ||
http.ResponseWriter | ||
done bool | ||
} | ||
|
||
func (w *doneWriter) WriteHeader(status int) { | ||
w.done = true | ||
w.ResponseWriter.WriteHeader(status) | ||
} | ||
|
||
func (w *doneWriter) Write(b []byte) (int, error) { | ||
w.done = true | ||
return w.ResponseWriter.Write(b) | ||
} | ||
|
||
// Gets an HTTP request and handles it according to CiA 309-5 | ||
type HTTPRequestHandler func(w doneWriter, req *HTTPGatewayRequest) error | ||
|
||
func createNmtHandler(bg *BaseGateway, command NMTCommand) HTTPRequestHandler { | ||
return func(w doneWriter, req *HTTPGatewayRequest) error { | ||
switch req.nodeId { | ||
case TOKEN_DEFAULT, TOKEN_NONE: | ||
return bg.NMTCommand(bg.defaultNodeId, command) | ||
case TOKEN_ALL: | ||
return bg.NMTCommand(0, command) | ||
default: | ||
return bg.NMTCommand(uint8(req.nodeId), command) | ||
} | ||
} | ||
} | ||
|
||
// Can be used for specifying some routes that can be implemented in CiA 309 | ||
// But are not in this gateway | ||
func handlerNotSupported(w doneWriter, req *HTTPGatewayRequest) error { | ||
return ErrGwRequestNotSupported | ||
} | ||
|
||
// Handle a read | ||
// This includes different type of handlers : SDO, PDO, ... | ||
func (gw *HTTPGatewayServer) handlerRead(w doneWriter, req *HTTPGatewayRequest) error { | ||
matchSDO := regSDO.FindStringSubmatch(req.command) | ||
if len(matchSDO) >= 2 { | ||
return gw.handlerSDORead(w, req, matchSDO) | ||
} | ||
matchPDO := regPDO.FindStringSubmatch(req.command) | ||
if len(matchPDO) >= 2 { | ||
return handlerNotSupported(w, req) | ||
} | ||
return ErrGwSyntaxError | ||
} | ||
|
||
func (gw *HTTPGatewayServer) handlerSDORead(w doneWriter, req *HTTPGatewayRequest, commands []string) error { | ||
index, subindex, err := parseSdoCommand(commands[1:]) | ||
if err != nil { | ||
log.Errorf("[HTTP][SERVER] unable to parse SDO command : %v", err) | ||
return err | ||
} | ||
net := gw.base.network | ||
buffer := gw.sdoBuffer | ||
|
||
n, err := net.ReadRaw(uint8(req.nodeId), uint16(index), uint8(subindex), buffer) | ||
if err != nil { | ||
w.Write(NewResponseError(int(req.sequence), err)) | ||
return nil | ||
} | ||
sdoResp := httpSDOReadResponse{ | ||
Sequence: int(req.sequence), | ||
Response: "OK", | ||
Data: "0x" + hex.EncodeToString(buffer[:n]), | ||
Length: n, | ||
} | ||
sdoResRaw, err := json.Marshal(sdoResp) | ||
if err != nil { | ||
return ErrGwRequestNotProcessed | ||
} | ||
w.Write(sdoResRaw) | ||
return nil | ||
} | ||
|
||
// Handle a write | ||
// This includes different type of handlers : SDO, PDO, ... | ||
func (gw *HTTPGatewayServer) handleWrite(w doneWriter, req *HTTPGatewayRequest) error { | ||
matchSDO := regSDO.FindStringSubmatch(req.command) | ||
if len(matchSDO) >= 2 { | ||
return gw.handlerSDOWrite(w, req, matchSDO) | ||
} | ||
matchPDO := regPDO.FindStringSubmatch(req.command) | ||
if len(matchPDO) >= 2 { | ||
return handlerNotSupported(w, req) | ||
} | ||
return ErrGwSyntaxError | ||
} | ||
|
||
func (gw *HTTPGatewayServer) handlerSDOWrite(w doneWriter, req *HTTPGatewayRequest, commands []string) error { | ||
index, subindex, err := parseSdoCommand(commands[1:]) | ||
if err != nil { | ||
log.Errorf("[HTTP][SERVER] unable to parse SDO command : %v", err) | ||
return err | ||
} | ||
net := gw.base.network | ||
|
||
var sdoWrite httpSDOWriteRequest | ||
err = json.Unmarshal(req.parameters, &sdoWrite) | ||
if err != nil { | ||
return ErrGwSyntaxError | ||
} | ||
datatype, ok := HTTP_DATATYPE_MAP[sdoWrite.Datatype] | ||
if !ok { | ||
log.Errorf("[HTTP][SERVER] requested datatype is either wrong or unsupported : %v", sdoWrite.Datatype) | ||
return ErrGwRequestNotSupported | ||
} | ||
encodedValue, err := encode(sdoWrite.Value, datatype, 0) | ||
if err != nil { | ||
return ErrGwSyntaxError | ||
} | ||
err = net.WriteRaw(uint8(req.nodeId), uint16(index), uint8(subindex), encodedValue) | ||
if err != nil { | ||
w.Write(NewResponseError(int(req.sequence), err)) | ||
return nil | ||
} | ||
return nil | ||
} | ||
|
||
// Update SDO client timeout | ||
func (gw *HTTPGatewayServer) handleSDOTimeout(w doneWriter, req *HTTPGatewayRequest) error { | ||
|
||
var sdoTimeout httpSDOTimeoutRequest | ||
err := json.Unmarshal(req.parameters, &sdoTimeout) | ||
if err != nil { | ||
return ErrGwSyntaxError | ||
} | ||
sdoTimeoutInt, err := strconv.ParseUint(sdoTimeout.Value, 0, 64) | ||
if err != nil || sdoTimeoutInt > 0xFFFF { | ||
return ErrGwSyntaxError | ||
} | ||
return gw.base.SetSDOTimeout(uint32(sdoTimeoutInt)) | ||
} |
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,123 @@ | ||
package canopen | ||
|
||
import ( | ||
"encoding/json" | ||
"io" | ||
"net/http" | ||
"strconv" | ||
|
||
log "github.com/sirupsen/logrus" | ||
) | ||
|
||
const TOKEN_NONE = -3 | ||
const TOKEN_DEFAULT = -2 | ||
const TOKEN_ALL = -1 | ||
|
||
// Gets SDO command as list of strings and processes it | ||
func parseSdoCommand(command []string) (index uint64, subindex uint64, err error) { | ||
if len(command) != 3 { | ||
return 0, 0, ErrGwSyntaxError | ||
} | ||
indexStr := command[1] | ||
subIndexStr := command[2] | ||
// Unclear if this is "supported" not really specified in 309-5 | ||
if indexStr == "all" { | ||
return 0, 0, ErrGwRequestNotSupported | ||
} | ||
index, e := strconv.ParseUint(indexStr, 0, 64) | ||
if e != nil { | ||
return 0, 0, ErrGwSyntaxError | ||
} | ||
subIndex, e := strconv.ParseUint(subIndexStr, 0, 64) | ||
if e != nil { | ||
return 0, 0, ErrGwSyntaxError | ||
} | ||
if index > 0xFFFF || subindex > 0xFF { | ||
return 0, 0, ErrGwSyntaxError | ||
} | ||
return index, subIndex, nil | ||
} | ||
|
||
// Parse raw network / node string param | ||
func parseNodeOrNetworkParam(param string) (int, error) { | ||
// Check if any of the string values | ||
switch param { | ||
case "default": | ||
return TOKEN_DEFAULT, nil | ||
case "none": | ||
return TOKEN_NONE, nil | ||
case "all": | ||
return TOKEN_ALL, nil | ||
} | ||
// Else try a specific id | ||
// This automatically treats 0x,0X,... correctly | ||
// which is allowed in the spec | ||
paramUint, err := strconv.ParseUint(param, 0, 64) | ||
if err != nil { | ||
return 0, err | ||
} | ||
return int(paramUint), nil | ||
} | ||
|
||
// Create a new sanitized api request object from raw http request | ||
// This function also checks that values are within bounds etc. | ||
func NewGatewayRequestFromRaw(r *http.Request) (*HTTPGatewayRequest, error) { | ||
// Global expression match | ||
match := regURI.FindStringSubmatch(r.URL.Path) | ||
if len(match) != 6 { | ||
log.Error("[HTTP][SERVER] request does not match a known API pattern") | ||
return nil, ErrGwSyntaxError | ||
} | ||
// Check differents components of API route : api, sequence number, network and node | ||
apiVersion := match[1] | ||
if apiVersion != API_VERSION { | ||
log.Errorf("[HTTP][SERVER] api version %v is not supported", apiVersion) | ||
return nil, ErrGwRequestNotSupported | ||
} | ||
sequence, err := strconv.Atoi(match[2]) | ||
if err != nil || sequence > MAX_SEQUENCE_NB { | ||
log.Errorf("[HTTP][SERVER] error processing sequence number %v", match[2]) | ||
return nil, ErrGwSyntaxError | ||
} | ||
netStr := match[3] | ||
netInt, err := parseNodeOrNetworkParam(netStr) | ||
if err != nil || netInt == 0 || netInt > 0xFFFF { | ||
log.Errorf("[HTTP][SERVER] error processing network param %v", netStr) | ||
return nil, ErrGwUnsupportedNet | ||
} | ||
nodeStr := match[4] | ||
nodeInt, err := parseNodeOrNetworkParam(nodeStr) | ||
if err != nil || nodeInt == 0 || nodeInt > 127 { | ||
log.Errorf("[HTTP][SERVER] error processing node param %v", nodeStr) | ||
} | ||
|
||
// Unmarshall request body | ||
var parameters json.RawMessage | ||
err = json.NewDecoder(r.Body).Decode(¶meters) | ||
if err != nil && err != io.EOF { | ||
log.Warnf("[HTTP][SERVER] failed to unmarshal request body : %v", err) | ||
return nil, ErrGwSyntaxError | ||
} | ||
request := &HTTPGatewayRequest{ | ||
nodeId: nodeInt, | ||
networkId: netInt, | ||
command: match[5], // Contains rest of URL after node | ||
sequence: uint32(sequence), | ||
parameters: parameters, | ||
} | ||
return request, nil | ||
} | ||
|
||
func NewResponseError(sequence int, error error) []byte { | ||
gwErr, ok := error.(*HTTPGatewayError) | ||
if !ok { | ||
gwErr = ErrGwRequestNotProcessed // Apparently no "internal error" | ||
} | ||
jData, _ := json.Marshal(map[string]string{"sequence": strconv.Itoa(sequence), "response": gwErr.Error()}) | ||
return jData | ||
} | ||
|
||
func NewResponseSuccess(sequence int) []byte { | ||
jData, _ := json.Marshal(map[string]string{"sequence": strconv.Itoa(sequence), "response": "OK"}) | ||
return jData | ||
} |
Oops, something went wrong.