diff --git a/gateway.go b/gateway.go index 892ad0f..ef24c2f 100644 --- a/gateway.go +++ b/gateway.go @@ -11,6 +11,16 @@ type BaseGateway struct { network *Network defaultNetwork uint16 defaultNodeId uint8 + sdoBuffer []byte +} + +func NewBaseGateway(network *Network, defaultNetwork uint16, defaultNodeId uint8, sdoUploadBufferSize int) *BaseGateway { + return &BaseGateway{ + network: network, + defaultNetwork: defaultNetwork, + defaultNodeId: defaultNodeId, + sdoBuffer: make([]byte, sdoUploadBufferSize), + } } type GatewayVersion struct { @@ -24,17 +34,27 @@ type GatewayVersion struct { } // Set default network to use -func (gw *BaseGateway) SetDefaultNetwork(id uint16) error { +func (gw *BaseGateway) SetDefaultNetworkId(id uint16) error { gw.defaultNetwork = id return nil } +// Get the default network +func (gw *BaseGateway) DefaultNetworkId() uint16 { + return gw.defaultNetwork +} + // Set default node Id to use func (gw *BaseGateway) SetDefaultNodeId(id uint8) error { gw.defaultNodeId = id return nil } +// Get default node Id +func (gw *BaseGateway) DefaultNodeId() uint8 { + return gw.defaultNodeId +} + // Get gateway version information func (gw *BaseGateway) GetVersion() (GatewayVersion, error) { return GatewayVersion{}, nil @@ -52,3 +72,27 @@ func (gw *BaseGateway) SetSDOTimeout(timeoutMs uint32) error { gw.network.sdoClient.timeoutTimeBlockTransferUs = timeoutMs * 1000 return nil } + +// Access SDO read buffer +func (gw *BaseGateway) Buffer() []byte { + return gw.sdoBuffer +} + +// Read SDO +func (gw *BaseGateway) ReadSDO(nodeId uint8, index uint16, subindex uint8) (int, error) { + return gw.network.ReadRaw(nodeId, index, subindex, gw.sdoBuffer) +} + +// Write SDO +func (gw *BaseGateway) WriteSDO(nodeId uint8, index uint16, subindex uint8, value string, datatype uint8) error { + encodedValue, err := encode(value, datatype, 0) + if err != nil { + return SDO_ABORT_TYPE_MISMATCH + } + return gw.network.WriteRaw(nodeId, index, subindex, encodedValue) +} + +// Disconnect from network +func (gw *BaseGateway) Disconnect() { + gw.network.Disconnect() +} diff --git a/gateway_http_client.go b/gateway_http_client.go deleted file mode 100644 index 7473074..0000000 --- a/gateway_http_client.go +++ /dev/null @@ -1,87 +0,0 @@ -package canopen - -import ( - "encoding/json" - "fmt" - "net/http" - "strconv" - "strings" - - log "github.com/sirupsen/logrus" -) - -type HTTPGatewayClient struct { - BaseURL string - ApiVersion string - CurrentSequenceNb int - NetworkId int -} - -func NewHTTPGatewayClient(baseURL string, apiVersion string, networkId int) *HTTPGatewayClient { - return &HTTPGatewayClient{BaseURL: baseURL, NetworkId: networkId, ApiVersion: apiVersion} -} - -type HTTPGatewayResponse struct { - Sequence int `json:"sequence,omitempty"` - Data string `json:"data,omitempty"` - Length int `json:"length,omitempty"` - Response string `json:"response,omitempty"` -} - -// Get the actual json response, return error if bad http or json decode error -func (client *HTTPGatewayClient) get(uri string) (resp *HTTPGatewayResponse, err error) { - client.CurrentSequenceNb += 1 - newUri := client.BaseURL + "/cia309-5" + fmt.Sprintf("/%s/%d/%d", client.ApiVersion, client.CurrentSequenceNb, client.NetworkId) - httpResponse, err := http.Get(newUri + uri) - if err != nil { - log.Errorf("[HTTP][CLIENT] http request errored : %v", err) - return - } - // Get JSON response - jsonRsp := new(HTTPGatewayResponse) - err = json.NewDecoder(httpResponse.Body).Decode(jsonRsp) - if err != nil { - log.Errorf("[HTTP][CLIENT] error decoding json response : %v", err) - return - } - // Check if any gateway errors - if strings.HasPrefix(jsonRsp.Response, "ERROR:") { - responseSplitted := strings.Split(jsonRsp.Response, ":") - if len(responseSplitted) != 2 { - log.Errorf("[HTTP][CLIENT] error decoding error field ('ERROR:xxxx') inside json response : %v", err) - err = fmt.Errorf("error decoding error field ('ERROR:' : %v, %T)", jsonRsp.Response, jsonRsp.Response) - return - } - var errorCode uint64 - errorCode, err = strconv.ParseUint(responseSplitted[1], 0, 64) - if err != nil { - return - } - err = &HTTPGatewayError{Code: int(errorCode)} - log.Warnf("[HTTP][CLIENT][SEQ:%v] command resulted in error from server : %v", jsonRsp.Sequence, err) - return - } - // Check if sequence number is correct - if client.CurrentSequenceNb != jsonRsp.Sequence { - log.Warnf("[HTTP][CLIENT][SEQ:%v] sequence number does not match expected value (%v)", jsonRsp.Sequence, client.CurrentSequenceNb) - } - return jsonRsp, nil -} - -// Read via SDO -func (client *HTTPGatewayClient) Read(nodeId uint8, index uint16, subIndex uint8) (data string, length int, err error) { - resp, err := client.get(fmt.Sprintf("/%d/r/%d/%d", nodeId, index, subIndex)) - if err != nil { - return - } - return resp.Data, 0, nil -} - -// Write via SDO -// func (client *HTTPGatewayClient) Write(nodeId uint8, index uint16, subIndex uint8, data string) error { -// resp, err := client.get(fmt.Sprintf("/%d/w/%d/%d", nodeId, index, subIndex)) -// if err != nil { -// return -// } -// return resp.Data, 0, nil -// } diff --git a/gateway_http_handlers.go b/gateway_http_handlers.go deleted file mode 100644 index f2c4053..0000000 --- a/gateway_http_handlers.go +++ /dev/null @@ -1,152 +0,0 @@ -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)) -} diff --git a/gateway_http_schemas.go b/gateway_http_schemas.go deleted file mode 100644 index a9a7779..0000000 --- a/gateway_http_schemas.go +++ /dev/null @@ -1,17 +0,0 @@ -package canopen - -type httpSDOTimeoutRequest struct { - Value string `json:"value"` -} - -type httpSDOWriteRequest struct { - Value string `json:"value"` - Datatype string `json:"datatype"` -} - -type httpSDOReadResponse struct { - Sequence int `json:"sequence"` - Response string `json:"response"` - Data string `json:"data"` - Length int `json:"length,omitempty"` -} diff --git a/gateway_http_server.go b/gateway_http_server.go deleted file mode 100644 index 0f19a51..0000000 --- a/gateway_http_server.go +++ /dev/null @@ -1,218 +0,0 @@ -package canopen - -import ( - "encoding/json" - "fmt" - "net/http" - "regexp" - "strings" - - log "github.com/sirupsen/logrus" -) - -const API_VERSION = "1.0" -const MAX_SEQUENCE_NB = 2<<31 - 1 -const URI_PATTERN = `/cia309-5/(\d+\.\d+)/(\d{1,10})/(0x[0-9a-f]{1,4}|\d{1,10}|default|none|all)/(0x[0-9a-f]{1,2}|\d{1,3}|default|none|all)/(.*)` -const SDO_COMMAND_URI_PATTERN = `(r|read|w|write)/(all|0x[0-9a-f]{1,4}|\d{1,5})/?(0x[0-9a-f]{1,2}|\d{1,3})?` -const PDO_COMMAND_URI_PATTERN = `(r|read|w|write)/(p|pdo)/(0x[0-9a-f]{1,3}|\d{1,4})` - -var regURI = regexp.MustCompile(URI_PATTERN) -var regSDO = regexp.MustCompile(SDO_COMMAND_URI_PATTERN) -var regPDO = regexp.MustCompile(PDO_COMMAND_URI_PATTERN) - -var ERROR_GATEWAY_DESCRIPTION_MAP = map[int]string{ - 100: "Request not supported", - 101: "Syntax error", - 102: "Request not processed due to internal state", - 103: "Time-out (where applicable)", - 104: "No default net set", - 105: "No default node set", - 106: "Unsupported net", - 107: "Unsupported node", - 108: "Command cancellation failed or ignored", - 109: "Emergency consumer not enabled", - 204: "Wrong NMT state", - 300: "Wrong password (User management)", - 301: "Number of super users exceeded (User management)", - 302: "Node access denied (User management)", - 303: "No session available (User management)", - 400: "PDO already used", - 401: "PDO length exceeded", - 501: "LSS implementation-/manufacturer-specific error", - 502: "LSS node-ID not supported", - 503: "LSS bit-rate not supported", - 504: "LSS parameter storing failed", - 505: "LSS command failed because of media error", - 600: "Running out of memory", - 601: "CAN interface currently not available", - 602: "Size to be set lower than minimum SDO buffer size", - 900: "Manufacturer-specific error", -} - -var ( - ErrGwRequestNotSupported = &HTTPGatewayError{Code: 100} - ErrGwSyntaxError = &HTTPGatewayError{Code: 101} - ErrGwRequestNotProcessed = &HTTPGatewayError{Code: 102} - ErrGwTimeout = &HTTPGatewayError{Code: 103} - ErrGwNoDefaultNetSet = &HTTPGatewayError{Code: 104} - ErrGwNoDefaultNodeSet = &HTTPGatewayError{Code: 105} - ErrGwUnsupportedNet = &HTTPGatewayError{Code: 106} - ErrGwUnsupportedNode = &HTTPGatewayError{Code: 107} - ErrGwCommandCancellationFailed = &HTTPGatewayError{Code: 108} - ErrGwEmergencyConsumerNotEnabled = &HTTPGatewayError{Code: 109} - ErrGwWrongNMTState = &HTTPGatewayError{Code: 204} - ErrGwWrongPassword = &HTTPGatewayError{Code: 300} - ErrGwSuperUsersExceeded = &HTTPGatewayError{Code: 301} - ErrGwNodeAccessDenied = &HTTPGatewayError{Code: 302} - ErrGwNoSessionAvailable = &HTTPGatewayError{Code: 303} - ErrGwPDOAlreadyUsed = &HTTPGatewayError{Code: 400} - ErrGwPDOLengthExceeded = &HTTPGatewayError{Code: 401} - ErrGwLSSImplementationError = &HTTPGatewayError{Code: 501} - ErrGwLSSNodeIDNotSupported = &HTTPGatewayError{Code: 502} - ErrGwLSSBitRateNotSupported = &HTTPGatewayError{Code: 503} - ErrGwLSSParameterStoringFailed = &HTTPGatewayError{Code: 504} - ErrGwLSSCommandFailed = &HTTPGatewayError{Code: 505} - ErrGwRunningOutOfMemory = &HTTPGatewayError{Code: 600} - ErrGwCANInterfaceNotAvailable = &HTTPGatewayError{Code: 601} - ErrGwSizeLowerThanSDOBufferSize = &HTTPGatewayError{Code: 602} - ErrGwManufacturerSpecificError = &HTTPGatewayError{Code: 900} -) - -var HTTP_DATATYPE_MAP = map[string]uint8{ - "b": BOOLEAN, - "u8": UNSIGNED8, - "u16": UNSIGNED16, - "u32": UNSIGNED32, - "u64": UNSIGNED64, - "i8": INTEGER8, - "i16": INTEGER16, - "i32": INTEGER32, - "i64": INTEGER64, - "r32": REAL32, - "r64": REAL64, - "vs": VISIBLE_STRING, -} - -type HTTPGatewayError struct { - Code int // Can be either an sdo abort code or a gateway error code -} - -func (e *HTTPGatewayError) Error() string { - if e.Code <= 999 { - return fmt.Sprintf("ERROR:%d", e.Code) - } - // Return as a hex value (sdo aborts) - return fmt.Sprintf("ERROR:0x%x", e.Code) -} - -type HTTPGatewayServer struct { - base *BaseGateway - network *Network - serveMux *http.ServeMux - defaultNetworkId uint16 - defaultNodeId uint8 - sdoBuffer []byte - routes map[string]HTTPRequestHandler -} - -type HTTPGatewayRequest struct { - nodeId int // node id concerned, some special negative values are used for "all", "default" & "none" - networkId int // networkId to be used, some special negative values are used for "all", "default" & "none" - command string // command can be composed of different parts - sequence uint32 // sequence number - parameters json.RawMessage -} - -// Default handler of any HTTP gateway request -// This parses a typical request and forwards it to the correct handler -func (gateway *HTTPGatewayServer) handleRequest(w http.ResponseWriter, raw *http.Request) { - log.Debugf("[HTTP][SERVER] new request : %v", raw.URL) - req, err := NewGatewayRequestFromRaw(raw) - if err != nil { - w.Write(NewResponseError(0, err)) - return - } - // An api command (URI) is in the form /command/sub-command/... etc... - // and can have variable parameters such as indexes as well as a body. - // We first check inside a map that the full command is present inside of a handler map. - // If full command is not found we then check again - // but with truncated command up to the first "/". - // e.g. '/reset/node' exists and is handled straight away - // '/read/0x2000/0x0' does not exist in map, so we then check 'read' which does exist - var route HTTPRequestHandler - route, ok := gateway.routes[req.command] - if !ok { - indexFirstSep := strings.Index(req.command, "/") - var firstCommand string - if indexFirstSep != -1 { - firstCommand = req.command[:indexFirstSep] - } else { - firstCommand = req.command - } - route, ok = gateway.routes[firstCommand] - if !ok { - log.Debugf("[HTTP][SERVER] no handler found for : '%v' or '%v'", req.command, firstCommand) - w.Write(NewResponseError(int(req.sequence), ErrGwRequestNotSupported)) - return - } - } - // Process the actual command - dw := doneWriter{ResponseWriter: w, done: false} - err = route(dw, req) - if err != nil { - w.Write(NewResponseError(int(req.sequence), err)) - return - } - if !dw.done { - // No response specific command has been given, reply with default success - dw.Write(NewResponseSuccess(int(req.sequence))) - return - } -} - -// Create a new gateway -func NewGateway(defaultNetworkId uint16, defaultNodeId uint8, sdoUploadBufferSize int, network *Network) *HTTPGatewayServer { - gw := &HTTPGatewayServer{} - gw.defaultNetworkId = defaultNetworkId - gw.defaultNodeId = defaultNodeId - gw.sdoBuffer = make([]byte, sdoUploadBufferSize) - gw.network = network - gw.serveMux = http.NewServeMux() - gw.serveMux.HandleFunc("/", gw.handleRequest) - base := &BaseGateway{network: network} - gw.base = base - gw.routes = make(map[string]HTTPRequestHandler) - - // Add all handlers - - // CiA 309-5 | 4.1 - gw.addRoute("r", gw.handlerRead) - gw.addRoute("read", gw.handlerRead) - gw.addRoute("w", gw.handleWrite) - gw.addRoute("write", gw.handleWrite) - gw.addRoute("set/sdo-timeout", gw.handleSDOTimeout) - - // CiA 309-5 | 4.3 - gw.addRoute("start", createNmtHandler(base, NMT_ENTER_OPERATIONAL)) - gw.addRoute("stop", createNmtHandler(base, NMT_ENTER_STOPPED)) - gw.addRoute("preop", createNmtHandler(base, NMT_ENTER_PRE_OPERATIONAL)) - gw.addRoute("preoperational", createNmtHandler(base, NMT_ENTER_PRE_OPERATIONAL)) - gw.addRoute("reset/node", createNmtHandler(base, NMT_RESET_NODE)) - gw.addRoute("reset/comm", createNmtHandler(base, NMT_RESET_COMMUNICATION)) - gw.addRoute("reset/communication", createNmtHandler(base, NMT_RESET_COMMUNICATION)) - gw.addRoute("enable/guarding", handlerNotSupported) - gw.addRoute("disable/guarding", handlerNotSupported) - gw.addRoute("enable/heartbeat", handlerNotSupported) - gw.addRoute("disable/heartbeat", handlerNotSupported) - - return gw -} - -func (gateway *HTTPGatewayServer) ListenAndServe(addr string) error { - return http.ListenAndServe(addr, gateway.serveMux) -} - -// Add a route to the server for handling a specific command -func (g *HTTPGatewayServer) addRoute(command string, handler HTTPRequestHandler) { - g.routes[command] = handler -} diff --git a/gateway_http_server_test.go b/gateway_http_server_test.go deleted file mode 100644 index 6df8737..0000000 --- a/gateway_http_server_test.go +++ /dev/null @@ -1,72 +0,0 @@ -package canopen - -import ( - "fmt" - "net/http/httptest" - "testing" - - "github.com/stretchr/testify/assert" -) - -const NODE_GW_TEST_LOCAL_ID = uint8(0x66) - -func createClient() (*HTTPGatewayClient, func()) { - gw := createGateway() - ts := httptest.NewServer(gw.serveMux) - client := &HTTPGatewayClient{ts.URL, "1.0", 0, 1} - return client, func() { - defer gw.network.Disconnect() - } -} - -func TestInvalidURIs(t *testing.T) { - client, close := createClient() - defer close() - _, err := client.get("/") - assert.EqualValues(t, ErrGwSyntaxError, err) - _, err = client.get("/10/start//") - assert.EqualValues(t, ErrGwSyntaxError, err) -} - -func TestNMTCommand(t *testing.T) { - client, close := createClient() - defer close() - commands := []string{ - "start", - "stop", - "preop", - "preoperational", - "reset/node", - "reset/comm", - "reset/communication", - } - for _, command := range commands { - resp, err := client.get(fmt.Sprintf("/10/%s", command)) - assert.Nil(t, err) - assert.NotNil(t, resp) - } - for _, command := range commands { - resp, err := client.get(fmt.Sprintf("/all/%s", command)) - assert.Nil(t, err) - assert.NotNil(t, resp) - } - for _, command := range commands { - resp, err := client.get(fmt.Sprintf("/none/%s", command)) - assert.Nil(t, err) - assert.NotNil(t, resp) - } - for _, command := range commands { - resp, err := client.get(fmt.Sprintf("/default/%s", command)) - assert.Nil(t, err) - assert.NotNil(t, resp) - } -} - -func TestSDOAccessCommands(t *testing.T) { - client, close := createClient() - defer close() - for i := uint16(0x2001); i <= 0x2009; i++ { - _, _, err := client.Read(NODE_ID_TEST, i, 0) - assert.Nil(t, err) - } -} diff --git a/http_gateway_test.go b/http_gateway_test.go deleted file mode 100644 index ac6a031..0000000 --- a/http_gateway_test.go +++ /dev/null @@ -1,156 +0,0 @@ -// CiA 309-5 implementation -package canopen - -import ( - "net/http/httptest" - "testing" - "time" - - log "github.com/sirupsen/logrus" -) - -var NODE_ID_TEST uint8 = 0x30 - -func init() { - // Set the logger to debug - log.SetLevel(log.WarnLevel) -} - -func createGateway() *HTTPGatewayServer { - network := createNetwork() - gateway := NewGateway(1, 1, 100, network) - return gateway -} - -func TestHTTPRead(t *testing.T) { - gateway := createGateway() - net := createNetwork() - defer net.Disconnect() - defer gateway.network.Disconnect() - time.Sleep(1 * time.Second) - ts := httptest.NewServer(gateway.serveMux) - defer ts.Close() - client := NewHTTPGatewayClient(ts.URL, "1.0", 1) - type args struct { - index uint16 - subindex uint8 - value string - } - tests := []struct { - name string - args args - }{ - { - name: "1", - args: args{0x2001, 0, "0x01"}, - }, - { - name: "2", - args: args{0x2002, 0, "0x33"}, - }, - { - name: "3", - args: args{0x2003, 0, "0x4444"}, - }, - { - name: "4", - args: args{0x2004, 0, "0x55555555"}, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - data, _, err := client.Read(NODE_ID_TEST, tt.args.index, tt.args.subindex) - if err != nil { - t.Fatalf(tt.name) - } - if data != tt.args.value { - t.Fatalf("expected value %v, got %v", tt.args.value, data) - } - }) - } -} - -// func TestHandler(t *testing.T) { -// gateway := createGateway() -// ts := httptest.NewServer(gateway.serveMux) -// defer ts.Close() - -// newreq := func(method, url string, body io.Reader) *http.Request { -// r, err := http.NewRequest(method, url, body) -// if err != nil { -// t.Fatal(err) -// } -// return r -// } - -// type args struct { -// uri string -// } - -// tests := []struct { -// name string -// r *http.Request -// wantErr bool -// }{ -// {name: "1 : invalid request (no command)", r: newreq("GET", ts.URL+"/cia309-5/1.0/", nil), wantErr: true}, -// {name: "2 : invalid request (improper node)", r: newreq("POST", ts.URL+"/cia309-5/1.0/xxx10/all/all/start/", nil), wantErr: true}, -// {name: "3 : valid sdo read request", r: newreq("GET", ts.URL+"/cia309-5/1.0/10/0x1/0x10/read/", nil), wantErr: false}, -// } - -// // tests := []struct { -// // name string -// // args args -// // wantErr bool -// // }{ -// // { -// // name: "1", -// // args: args{uri: "/cia309-5/1.0/"}, -// // wantErr: true, -// // }, -// // { -// // name: "2", -// // args: args{uri: `/cia309-5/1.0/10/all/all/start/`}, -// // wantErr: false, -// // }, -// // { -// // name: "3", -// // args: args{uri: "/cia309-5/1.0/xxx10/all/all/start/"}, -// // wantErr: true, -// // }, -// // { -// // name: "4", -// // args: args{uri: "/cia309-5/1.0/10/0x1/0x10/read/"}, -// // wantErr: false, -// // }, -// // { -// // name: "network_to_high", -// // args: args{uri: "/cia309-5/1.0/10/333333/0x10/read/"}, -// // wantErr: true, -// // }, -// // { -// // name: "node_to_high", -// // args: args{uri: "/cia309-5/1.0/10/1/0x100/read/"}, -// // wantErr: true, -// // }, -// // } -// for _, tt := range tests { -// t.Run(tt.name, func(t *testing.T) { -// resp, err := http.DefaultClient.Do(tt.r) -// if err != nil { -// t.Fatal(err) -// } -// // Get JSON resonse -// jsonRsp := new(HTTPGatewayResponse) -// err = json.NewDecoder(resp.Body).Decode(jsonRsp) -// fmt.Print(jsonRsp) -// if err != nil { -// t.Fatal(err) -// } -// if tt.wantErr && jsonRsp.Error == 0 { -// t.Fatal("expecting error in gateway response") -// } else if !tt.wantErr && jsonRsp.Error != 0 { -// t.Fatal("gateway returned an error (not expected)") -// } -// }) -// } -// } diff --git a/network_test.go b/network_test.go index f0491c0..87c80e5 100644 --- a/network_test.go +++ b/network_test.go @@ -6,6 +6,8 @@ import ( "github.com/stretchr/testify/assert" ) +const NODE_ID_TEST uint8 = 0x30 + func createNetworkEmpty() *Network { bus := NewVirtualCanBus("localhost:18888") bus.receiveOwn = true diff --git a/pkg/http/client.go b/pkg/http/client.go new file mode 100644 index 0000000..7258911 --- /dev/null +++ b/pkg/http/client.go @@ -0,0 +1,98 @@ +package http + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "strconv" + "strings" + + log "github.com/sirupsen/logrus" +) + +type GatewayClient struct { + client *http.Client + baseURL string + apiVersion string + currentSequenceNb int + networkId int +} + +func NewGatewayClient(baseURL string, apiVersion string, networkId int) *GatewayClient { + return &GatewayClient{ + client: &http.Client{}, + baseURL: baseURL, + networkId: networkId, + apiVersion: apiVersion, + } +} + +// Extract error if any inside of reponse +func (resp *GatewayResponse) GetError() error { + // Check if any gateway errors + if !strings.HasPrefix(resp.Response, "ERROR:") { + return nil + } + responseSplitted := strings.Split(resp.Response, ":") + if len(responseSplitted) != 2 { + return fmt.Errorf("error decoding error field ('ERROR:' : %v)", resp.Response) + } + var errorCode uint64 + errorCode, err := strconv.ParseUint(responseSplitted[1], 0, 64) + if err != nil { + return fmt.Errorf("error decoding error field ('ERROR:' : %v)", err) + } + return NewGatewayError(int(errorCode)) +} + +// HTTP request to CiA endpoint +// Does high level error checking : http related errors, json decode errors +// or wrong sequence number +func (client *GatewayClient) do(method string, uri string, body io.Reader) (resp *GatewayResponse, err error) { + client.currentSequenceNb += 1 + baseUri := client.baseURL + "/cia309-5" + fmt.Sprintf("/%s/%d/%d", client.apiVersion, client.currentSequenceNb, client.networkId) + req, err := http.NewRequest(method, baseUri+uri, body) + if err != nil { + log.Errorf("[HTTP][CLIENT] http error : %v", err) + return nil, err + } + // HTTP request + httpResp, err := client.client.Do(req) + if err != nil { + log.Errorf("[HTTP][CLIENT] http error : %v", err) + return nil, err + } + // Decode JSON "generic" response + jsonRsp := new(GatewayResponse) + err = json.NewDecoder(httpResp.Body).Decode(jsonRsp) + if err != nil { + log.Errorf("[HTTP][CLIENT] error decoding json response : %v", err) + return nil, err + } + // Check if sequence number is correct + sequence, err := strconv.Atoi(jsonRsp.Sequence) + if client.currentSequenceNb != sequence || err != nil { + log.Errorf("[HTTP][CLIENT][SEQ:%v] sequence number does not match expected value (%v)", jsonRsp.Sequence, client.currentSequenceNb) + return nil, fmt.Errorf("error in sequence number") + } + return jsonRsp, nil +} + +// Read via SDO +func (client *GatewayClient) Read(nodeId uint8, index uint16, subIndex uint8) (data string, length int, err error) { + resp, err := client.do(http.MethodGet, fmt.Sprintf("/%d/r/%d/%d", nodeId, index, subIndex), nil) + if err != nil { + return + } + return resp.Data, 0, nil +} + +// Write via SDO +// func (client *HTTPGatewayClient) Write(nodeId uint8, index uint16, subIndex uint8, data string) error { +// resp, err := client.get(fmt.Sprintf("/%d/w/%d/%d", nodeId, index, subIndex)) +// if err != nil { +// return +// } +// return resp.Data, 0, nil +// } diff --git a/pkg/http/client_test.go b/pkg/http/client_test.go new file mode 100644 index 0000000..f5f7f2b --- /dev/null +++ b/pkg/http/client_test.go @@ -0,0 +1,16 @@ +package http + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGetError(t *testing.T) { + req := GatewayResponse{Sequence: "1", Data: "", Length: "", Response: "ERROR:100"} + err := req.GetError() + assert.Equal(t, NewGatewayError(100), err) + req = GatewayResponse{Sequence: "1", Data: "", Length: "", Response: "OK"} + err = req.GetError() + assert.Equal(t, nil, err) +} diff --git a/pkg/http/errors.go b/pkg/http/errors.go new file mode 100644 index 0000000..72b21ce --- /dev/null +++ b/pkg/http/errors.go @@ -0,0 +1,77 @@ +package http + +import "fmt" + +var ERROR_GATEWAY_DESCRIPTION_MAP = map[int]string{ + 100: "Request not supported", + 101: "Syntax error", + 102: "Request not processed due to internal state", + 103: "Time-out (where applicable)", + 104: "No default net set", + 105: "No default node set", + 106: "Unsupported net", + 107: "Unsupported node", + 108: "Command cancellation failed or ignored", + 109: "Emergency consumer not enabled", + 204: "Wrong NMT state", + 300: "Wrong password (User management)", + 301: "Number of super users exceeded (User management)", + 302: "Node access denied (User management)", + 303: "No session available (User management)", + 400: "PDO already used", + 401: "PDO length exceeded", + 501: "LSS implementation-/manufacturer-specific error", + 502: "LSS node-ID not supported", + 503: "LSS bit-rate not supported", + 504: "LSS parameter storing failed", + 505: "LSS command failed because of media error", + 600: "Running out of memory", + 601: "CAN interface currently not available", + 602: "Size to be set lower than minimum SDO buffer size", + 900: "Manufacturer-specific error", +} + +var ( + ErrGwRequestNotSupported = &GatewayError{Code: 100} + ErrGwSyntaxError = &GatewayError{Code: 101} + ErrGwRequestNotProcessed = &GatewayError{Code: 102} + ErrGwTimeout = &GatewayError{Code: 103} + ErrGwNoDefaultNetSet = &GatewayError{Code: 104} + ErrGwNoDefaultNodeSet = &GatewayError{Code: 105} + ErrGwUnsupportedNet = &GatewayError{Code: 106} + ErrGwUnsupportedNode = &GatewayError{Code: 107} + ErrGwCommandCancellationFailed = &GatewayError{Code: 108} + ErrGwEmergencyConsumerNotEnabled = &GatewayError{Code: 109} + ErrGwWrongNMTState = &GatewayError{Code: 204} + ErrGwWrongPassword = &GatewayError{Code: 300} + ErrGwSuperUsersExceeded = &GatewayError{Code: 301} + ErrGwNodeAccessDenied = &GatewayError{Code: 302} + ErrGwNoSessionAvailable = &GatewayError{Code: 303} + ErrGwPDOAlreadyUsed = &GatewayError{Code: 400} + ErrGwPDOLengthExceeded = &GatewayError{Code: 401} + ErrGwLSSImplementationError = &GatewayError{Code: 501} + ErrGwLSSNodeIDNotSupported = &GatewayError{Code: 502} + ErrGwLSSBitRateNotSupported = &GatewayError{Code: 503} + ErrGwLSSParameterStoringFailed = &GatewayError{Code: 504} + ErrGwLSSCommandFailed = &GatewayError{Code: 505} + ErrGwRunningOutOfMemory = &GatewayError{Code: 600} + ErrGwCANInterfaceNotAvailable = &GatewayError{Code: 601} + ErrGwSizeLowerThanSDOBufferSize = &GatewayError{Code: 602} + ErrGwManufacturerSpecificError = &GatewayError{Code: 900} +) + +type GatewayError struct { + Code int // Can be either an sdo abort code or a gateway error code +} + +func NewGatewayError(code int) error { + return &GatewayError{Code: code} +} + +func (e *GatewayError) Error() string { + if e.Code <= 999 { + return fmt.Sprintf("ERROR:%d", e.Code) + } + // Return as a hex value (sdo aborts) + return fmt.Sprintf("ERROR:0x%x", e.Code) +} diff --git a/pkg/http/handlers.go b/pkg/http/handlers.go new file mode 100644 index 0000000..247793a --- /dev/null +++ b/pkg/http/handlers.go @@ -0,0 +1,193 @@ +package http + +import ( + "encoding/hex" + "encoding/json" + "net/http" + "strconv" + "strings" + + canopen "github.com/samsamfire/gocanopen" + log "github.com/sirupsen/logrus" +) + +// Wrapper around [http.ResponseWriter] but keeps track of any writes already done +// This allows us to perform default behaviour if handler has not already sent a response +type doneWriter struct { + http.ResponseWriter + done bool +} + +// Handle a [GatewayRequest] according to CiA 309-5 +type GatewayRequestHandler func(w doneWriter, req *GatewayRequest) error + +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) +} + +// Default handler of any HTTP gateway request +// This parses a typical request and forwards it to the correct handler +func (gateway *GatewayServer) handleRequest(w http.ResponseWriter, raw *http.Request) { + log.Debugf("[HTTP][SERVER] new request : %v", raw.URL) + req, err := NewGatewayRequestFromRaw(raw) + if err != nil { + w.Write(NewResponseError(0, err)) + return + } + // An api command (URI) is in the form /command/sub-command/... etc... + // and can have variable parameters such as indexes as well as a body. + // We first check inside a map that the full command is present inside of a handler map. + // If full command is not found we then check again + // but with truncated command up to the first "/". + // e.g. '/reset/node' exists and is handled straight away + // '/read/0x2000/0x0' does not exist in map, so we then check 'read' which does exist + var route GatewayRequestHandler + route, ok := gateway.routes[req.command] + if !ok { + indexFirstSep := strings.Index(req.command, "/") + var firstCommand string + if indexFirstSep != -1 { + firstCommand = req.command[:indexFirstSep] + } else { + firstCommand = req.command + } + route, ok = gateway.routes[firstCommand] + if !ok { + log.Debugf("[HTTP][SERVER] no handler found for : '%v' or '%v'", req.command, firstCommand) + w.Write(NewResponseError(int(req.sequence), ErrGwRequestNotSupported)) + return + } + } + // Process the actual command + dw := doneWriter{ResponseWriter: w, done: false} + err = route(dw, req) + if err != nil { + w.Write(NewResponseError(int(req.sequence), err)) + return + } + if !dw.done { + // No response specific command has been given, reply with default success + dw.Write(NewResponseSuccess(int(req.sequence))) + return + } +} + +// Create a handler for processing NMT request +func createNmtHandler(bg *canopen.BaseGateway, command canopen.NMTCommand) GatewayRequestHandler { + return func(w doneWriter, req *GatewayRequest) 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 *GatewayRequest) error { + return ErrGwRequestNotSupported +} + +// Handle a read +// This includes different type of handlers : SDO, PDO, ... +func (gw *GatewayServer) handlerRead(w doneWriter, req *GatewayRequest) 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 *GatewayServer) handlerSDORead(w doneWriter, req *GatewayRequest, 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 + } + + n, err := gw.ReadSDO(uint8(req.nodeId), uint16(index), uint8(subindex)) + if err != nil { + w.Write(NewResponseError(int(req.sequence), err)) + return nil + } + sdoResp := SDOReadResponse{ + Sequence: strconv.Itoa(int(req.sequence)), + Response: "OK", + Data: "0x" + hex.EncodeToString(gw.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 *GatewayServer) handleWrite(w doneWriter, req *GatewayRequest) 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 *GatewayServer) handlerSDOWrite(w doneWriter, req *GatewayRequest, 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 + } + + var sdoWrite SDOWriteRequest + err = json.Unmarshal(req.parameters, &sdoWrite) + if err != nil { + return ErrGwSyntaxError + } + datatype, ok := DATATYPE_MAP[sdoWrite.Datatype] + if !ok { + log.Errorf("[HTTP][SERVER] requested datatype is either wrong or unsupported : %v", sdoWrite.Datatype) + return ErrGwRequestNotSupported + } + err = gw.WriteSDO(uint8(req.nodeId), uint16(index), uint8(subindex), sdoWrite.Value, datatype) + if err != nil { + w.Write(NewResponseError(int(req.sequence), err)) + return nil + } + return nil +} + +// Update SDO client timeout +func (gw *GatewayServer) handleSDOTimeout(w doneWriter, req *GatewayRequest) error { + + var sdoTimeout SDOTimeoutRequest + 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.SetSDOTimeout(uint32(sdoTimeoutInt)) +} diff --git a/gateway_http_parser.go b/pkg/http/parser.go similarity index 95% rename from gateway_http_parser.go rename to pkg/http/parser.go index d0502df..0c95193 100644 --- a/gateway_http_parser.go +++ b/pkg/http/parser.go @@ -1,4 +1,4 @@ -package canopen +package http import ( "encoding/json" @@ -61,7 +61,7 @@ func parseNodeOrNetworkParam(param string) (int, error) { // 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) { +func NewGatewayRequestFromRaw(r *http.Request) (*GatewayRequest, error) { // Global expression match match := regURI.FindStringSubmatch(r.URL.Path) if len(match) != 6 { @@ -98,7 +98,7 @@ func NewGatewayRequestFromRaw(r *http.Request) (*HTTPGatewayRequest, error) { log.Warnf("[HTTP][SERVER] failed to unmarshal request body : %v", err) return nil, ErrGwSyntaxError } - request := &HTTPGatewayRequest{ + request := &GatewayRequest{ nodeId: nodeInt, networkId: netInt, command: match[5], // Contains rest of URL after node @@ -109,7 +109,7 @@ func NewGatewayRequestFromRaw(r *http.Request) (*HTTPGatewayRequest, error) { } func NewResponseError(sequence int, error error) []byte { - gwErr, ok := error.(*HTTPGatewayError) + gwErr, ok := error.(*GatewayError) if !ok { gwErr = ErrGwRequestNotProcessed // Apparently no "internal error" } diff --git a/pkg/http/schemas.go b/pkg/http/schemas.go new file mode 100644 index 0000000..d8d68be --- /dev/null +++ b/pkg/http/schemas.go @@ -0,0 +1,36 @@ +package http + +import "encoding/json" + +// HTTP response from the server +type GatewayResponse struct { + Sequence string `json:"sequence,omitempty"` + Data string `json:"data,omitempty"` + Length string `json:"length,omitempty"` + Response string `json:"response,omitempty"` +} + +// HTTP request to the server +type GatewayRequest struct { + nodeId int // node id concerned, some special negative values are used for "all", "default" & "none" + networkId int // networkId to be used, some special negative values are used for "all", "default" & "none" + command string // command can be composed of different parts + sequence uint32 // sequence number + parameters json.RawMessage +} + +type SDOTimeoutRequest struct { + Value string `json:"value"` +} + +type SDOWriteRequest struct { + Value string `json:"value"` + Datatype string `json:"datatype"` +} + +type SDOReadResponse struct { + Sequence string `json:"sequence"` + Response string `json:"response"` + Data string `json:"data"` + Length int `json:"length,omitempty"` +} diff --git a/pkg/http/server.go b/pkg/http/server.go new file mode 100644 index 0000000..667857b --- /dev/null +++ b/pkg/http/server.go @@ -0,0 +1,83 @@ +package http + +import ( + "net/http" + "regexp" + + canopen "github.com/samsamfire/gocanopen" +) + +const API_VERSION = "1.0" +const MAX_SEQUENCE_NB = 2<<31 - 1 +const URI_PATTERN = `/cia309-5/(\d+\.\d+)/(\d{1,10})/(0x[0-9a-f]{1,4}|\d{1,10}|default|none|all)/(0x[0-9a-f]{1,2}|\d{1,3}|default|none|all)/(.*)` +const SDO_COMMAND_URI_PATTERN = `(r|read|w|write)/(all|0x[0-9a-f]{1,4}|\d{1,5})/?(0x[0-9a-f]{1,2}|\d{1,3})?` +const PDO_COMMAND_URI_PATTERN = `(r|read|w|write)/(p|pdo)/(0x[0-9a-f]{1,3}|\d{1,4})` + +var regURI = regexp.MustCompile(URI_PATTERN) +var regSDO = regexp.MustCompile(SDO_COMMAND_URI_PATTERN) +var regPDO = regexp.MustCompile(PDO_COMMAND_URI_PATTERN) + +var DATATYPE_MAP = map[string]uint8{ + "b": canopen.BOOLEAN, + "u8": canopen.UNSIGNED8, + "u16": canopen.UNSIGNED16, + "u32": canopen.UNSIGNED32, + "u64": canopen.UNSIGNED64, + "i8": canopen.INTEGER8, + "i16": canopen.INTEGER16, + "i32": canopen.INTEGER32, + "i64": canopen.INTEGER64, + "r32": canopen.REAL32, + "r64": canopen.REAL64, + "vs": canopen.VISIBLE_STRING, +} + +type GatewayServer struct { + *canopen.BaseGateway + serveMux *http.ServeMux + routes map[string]GatewayRequestHandler +} + +// Create a new gateway +func NewGatewayServer(network *canopen.Network, defaultNetworkId uint16, defaultNodeId uint8, sdoUploadBufferSize int) *GatewayServer { + base := canopen.NewBaseGateway(network, defaultNetworkId, defaultNodeId, sdoUploadBufferSize) + gw := &GatewayServer{BaseGateway: base} + gw.serveMux = http.NewServeMux() + gw.serveMux.HandleFunc("/", gw.handleRequest) // This base route handles all the requests + gw.routes = make(map[string]GatewayRequestHandler) + + // CiA 309-5 | 4.1 + gw.addRoute("r", gw.handlerRead) + gw.addRoute("read", gw.handlerRead) + gw.addRoute("w", gw.handleWrite) + gw.addRoute("write", gw.handleWrite) + gw.addRoute("set/sdo-timeout", gw.handleSDOTimeout) + + // CiA 309-5 | 4.3 + gw.addRoute("start", createNmtHandler(base, canopen.NMT_ENTER_OPERATIONAL)) + gw.addRoute("stop", createNmtHandler(base, canopen.NMT_ENTER_STOPPED)) + gw.addRoute("preop", createNmtHandler(base, canopen.NMT_ENTER_PRE_OPERATIONAL)) + gw.addRoute("preoperational", createNmtHandler(base, canopen.NMT_ENTER_PRE_OPERATIONAL)) + gw.addRoute("reset/node", createNmtHandler(base, canopen.NMT_RESET_NODE)) + gw.addRoute("reset/comm", createNmtHandler(base, canopen.NMT_RESET_COMMUNICATION)) + gw.addRoute("reset/communication", createNmtHandler(base, canopen.NMT_RESET_COMMUNICATION)) + gw.addRoute("enable/guarding", handlerNotSupported) + gw.addRoute("disable/guarding", handlerNotSupported) + gw.addRoute("enable/heartbeat", handlerNotSupported) + gw.addRoute("disable/heartbeat", handlerNotSupported) + + // CiA 309-5 | 4.6 + //gw.addRoute("info/version") + + return gw +} + +// Process server, blocking +func (gateway *GatewayServer) ListenAndServe(addr string) error { + return http.ListenAndServe(addr, gateway.serveMux) +} + +// Add a route to the server for handling a specific command +func (g *GatewayServer) addRoute(command string, handler GatewayRequestHandler) { + g.routes[command] = handler +} diff --git a/pkg/http/server_test.go b/pkg/http/server_test.go new file mode 100644 index 0000000..f0c2b47 --- /dev/null +++ b/pkg/http/server_test.go @@ -0,0 +1,105 @@ +package http + +import ( + "fmt" + "net/http" + "net/http/httptest" + "testing" + + canopen "github.com/samsamfire/gocanopen" + log "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" +) + +const NODE_ID_TEST = uint8(0x66) + +func init() { + log.SetLevel(log.DebugLevel) +} + +func createNetworkEmpty() *canopen.Network { + bus := canopen.NewVirtualCanBus("localhost:18888") + bus.SetReceiveOwn(true) + network := canopen.NewNetwork(bus) + e := network.Connect() + if e != nil { + panic(e) + } + return &network +} + +func createNetwork() *canopen.Network { + network := createNetworkEmpty() + _, err := network.CreateLocalNode(NODE_ID_TEST, "../../testdata/base.eds") + if err != nil { + panic(err) + } + return network +} + +func createGateway() *GatewayServer { + network := createNetwork() + gateway := NewGatewayServer(network, 1, 1, 100) + return gateway +} + +func createClient() (*GatewayClient, func()) { + gw := createGateway() + ts := httptest.NewServer(gw.serveMux) + client := NewGatewayClient(ts.URL, API_VERSION, 1) + return client, func() { + defer gw.Disconnect() + } +} + +func TestInvalidURIs(t *testing.T) { + client, close := createClient() + defer close() + _, err := client.do(http.MethodGet, "/", nil) + assert.EqualValues(t, ErrGwSyntaxError, err) + _, err = client.do(http.MethodGet, "/10/start//", nil) + assert.EqualValues(t, ErrGwSyntaxError, err) +} + +func TestNMTCommand(t *testing.T) { + client, close := createClient() + defer close() + commands := []string{ + "start", + "stop", + "preop", + "preoperational", + "reset/node", + "reset/comm", + "reset/communication", + } + for _, command := range commands { + resp, err := client.do(http.MethodPut, fmt.Sprintf("/10/%s", command), nil) + assert.Nil(t, err) + assert.NotNil(t, resp) + } + for _, command := range commands { + resp, err := client.do(http.MethodPut, fmt.Sprintf("/all/%s", command), nil) + assert.Nil(t, err) + assert.NotNil(t, resp) + } + for _, command := range commands { + resp, err := client.do(http.MethodPut, fmt.Sprintf("/none/%s", command), nil) + assert.Nil(t, err) + assert.NotNil(t, resp) + } + for _, command := range commands { + resp, err := client.do(http.MethodPut, fmt.Sprintf("/default/%s", command), nil) + assert.Nil(t, err) + assert.NotNil(t, resp) + } +} + +func TestSDOAccessCommands(t *testing.T) { + client, close := createClient() + defer close() + for i := uint16(0x2001); i <= 0x2009; i++ { + _, _, err := client.Read(NODE_ID_TEST, i, 0) + assert.Nil(t, err) + } +} diff --git a/virtual.go b/virtual.go index 146b4dd..09b1db5 100644 --- a/virtual.go +++ b/virtual.go @@ -176,6 +176,10 @@ func (client *VirtualCanBus) handleReception() { } } +func (client *VirtualCanBus) SetReceiveOwn(receiveOwn bool) { + client.receiveOwn = receiveOwn +} + func NewVirtualCanBus(channel string) *VirtualCanBus { return &VirtualCanBus{channel: channel, stopChan: make(chan bool), isRunning: false} }