/** * Project: Open Vehicle Monitor System * Module: CANopen * * (c) 2017 Michael Balzer * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef __CANOPEN_H__ #define __CANOPEN_H__ #include #include "can.h" #include "ovms_log.h" #include "ovms_config.h" #include "ovms_metrics.h" #include "ovms_command.h" #define CAN_INTERFACE_CNT 4 #define CANopen_GeneralError 0x08000000 // check for device specific error details #define CANopen_BusCollision 0xffffffff // another master is active / non-CANopen frame received typedef enum __attribute__ ((__packed__)) { COR_OK = 0, // API level: COR_WAIT, // job waiting to be processed COR_ERR_UnknownJobType, COR_ERR_QueueFull, COR_ERR_QueueEmpty, COR_ERR_NoCANWrite, COR_ERR_ParamRange, COR_ERR_BufferTooSmall, // Protocol level: COR_ERR_Timeout, COR_ERR_SDO_Access, COR_ERR_SDO_SegMismatch, // General purpose application level: COR_ERR_DeviceOffline = 0x80, COR_ERR_UnknownDevice, COR_ERR_LoginFailed, COR_ERR_StateChangeFailed } CANopenResult_t; typedef enum __attribute__ ((__packed__)) { CONC_Start = 1, CONC_Stop = 2, CONC_PreOp = 128, CONC_Reset = 129, CONC_CommReset = 130 } CANopenNMTCommand_t; typedef enum __attribute__ ((__packed__)) { CONS_Booting = 0, CONS_Stopped = 4, CONS_Operational = 5, CONS_PreOperational = 127 } CANopenNMTState_t; typedef struct CANopenNMTEvent // event "canopen.node.state" { canbus* origin; uint8_t nodeid; CANopenNMTState_t state; } CANopenNMTEvent_t; typedef struct CANopenEMCYEvent // event "canopen.node.emcy" { canbus* origin; uint8_t nodeid; uint16_t code; // see CiA DS301, Table 21: Emergency Error Codes uint8_t type; // see CiA DS301, Table 48: Structure of the Error Register uint8_t data[5]; // device specific } CANopenEMCYEvent_t; typedef struct CANopenNodeMetrics { OvmsMetricString* m_state; // metric "co..nd.state" OvmsMetricInt* m_emcy_code; // metric "co..nd.emcy.code" OvmsMetricInt* m_emcy_type; // metric "co..nd.emcy.type" } CANopenNodeMetrics_t; typedef std::map CANopenNodeMetricsMap; class CANopenAsyncClient; /** * A CANopenJob is a transfer process consisting of a single send or a * series of one or more requests & responses. * * CANopenClients create and submit Jobs to be processed to a CANopenWorker. * After finish/abort, the Worker sends the Job to the clients done queue. */ typedef enum __attribute__ ((__packed__)) { COJT_None = 0, COJT_SendNMT, COJT_ReceiveHB, COJT_ReadSDO, COJT_WriteSDO } CANopenJob_t; struct __attribute__ ((__packed__)) CANopenJob { CANopenAsyncClient* client; CANopenJob_t type; CANopenResult_t result; // current job state or result uint16_t txid; // requests sent to ← uint16_t rxid; // expecting responses from ← uint16_t timeout_ms; // single response timeout uint8_t maxtries; // max try count uint8_t trycnt; // actual try count union __attribute__ ((__packed__)) { struct __attribute__ ((__packed__)) { uint8_t nodeid; // 0=broadcast to all nodes CANopenNMTCommand_t command; } nmt; struct __attribute__ ((__packed__)) { uint8_t nodeid; CANopenNMTState_t state; // state received CANopenNMTState_t* statebuf; // state received } hb; struct __attribute__ ((__packed__)) { uint8_t nodeid; // no broadcast allowed uint16_t index; // SDO register address uint8_t subindex; // SDO subregister address uint8_t* buf; // tx from / rx to size_t bufsize; // rx: buffer capacity, tx: unused size_t xfersize; // byte count sent / received size_t contsize; // content size of SDO (if indicated by slave) uint32_t error; // CANopen general error code } sdo; }; void Init() { memset(this, 0, sizeof(*this)); } CANopenResult_t SetResult(CANopenResult_t p_result, uint32_t p_error=0) { sdo.error = p_error; return result = p_result; } }; typedef union __attribute__ ((__packed__)) { uint8_t byte[8]; // raw access struct __attribute__ ((__packed__)) { uint8_t state; // heartbeat state uint8_t unused[7]; } hb; struct __attribute__ ((__packed__)) { uint8_t control; // protocol request / response uint16_t index; // SDO register address (little endian) uint8_t subindex; // SDO register sub index uint8_t data[4]; // expedited payload } exp; struct __attribute__ ((__packed__)) { uint8_t control; // protocol request / response uint8_t data[7]; // segmented payload } seg; struct __attribute__ ((__packed__)) { uint8_t control; // protocol request / response uint16_t index; // SDO register address (little endian) uint8_t subindex; // SDO register sub index uint32_t data; // abort reason / error code (little endian) } ctl; } CANopenFrame_t; /** * A CANopenWorker processes CANopenJobs on a specific bus. * * CANopenClients create and submit Jobs to be processed to a CANopenWorker. * After finish/abort, the Worker sends the Job to the clients done queue. * * A CANopenWorker also monitors the bus for emergency and heartbeat * messages, and translates these into events and metrics updates. */ typedef std::forward_list CANopenClientList; class CANopenWorker { public: CANopenWorker(canbus* canbus); ~CANopenWorker(); public: void JobTask(); void IncomingFrame(CAN_frame_t* frame); void Open(CANopenAsyncClient* client); void Close(CANopenAsyncClient* client); bool IsClient(CANopenAsyncClient* client); void StatusReport(int verbosity, OvmsWriter* writer); CANopenNodeMetrics *GetNodeMetrics(uint8_t nodeid); public: CANopenResult_t SubmitJob(CANopenJob& job, TickType_t maxqueuewait=0); protected: CANopenResult_t ProcessSendNMTJob(); CANopenResult_t ProcessReceiveHBJob(); CANopenResult_t ProcessReadSDOJob(); CANopenResult_t ProcessWriteSDOJob(); private: void SendSDORequest(); void AbortSDORequest(uint32_t reason); CANopenResult_t ExecuteSDORequest(); public: canbus* m_bus; // max one worker per bus int m_clientcnt; CANopenClientList m_clients; char m_taskname[12]; // "COwrk canX" TaskHandle_t m_jobtask; // worker task QueueHandle_t m_jobqueue; // job rx queue uint32_t m_nmt_rxcnt; uint32_t m_emcy_rxcnt; uint32_t m_jobcnt; uint32_t m_jobcnt_timeout; uint32_t m_jobcnt_error; CANopenJob m_job; // job currently processed CANopenNodeMetricsMap m_nodemetrics; // map: nodeid → node metrics private: CANopenFrame_t m_request; CANopenFrame_t m_response; }; /** * CANopenAsyncClient provides the asynchronous API to the CANopen framework. * * Jobs done will be sent to the m_done_queue and need to be fetched * by looping ReceiveDone() until it returns COR_ERR_QueueEmpty. * * On creation the client automatically connects to the worker or starts a * new worker if necessary. * * CANopenAsyncClient uses the CiA DS301 default IDs for node addressing, i.e. * NMT request → 0x000 * NMT response → 0x700 + nodeid * SDO request → 0x600 + nodeid * SDO response → 0x580 + nodeid * * If you need another address scheme, create a sub class of CANopenAsyncClient * or CANopenClient and override the Init…() methods as necessary. */ class CANopenAsyncClient { public: CANopenAsyncClient(canbus* canbus, int queuesize=20); CANopenAsyncClient(CANopenWorker* worker, int queuesize=20); virtual ~CANopenAsyncClient(); public: // CANopenWorker callback: virtual CANopenResult_t SubmitDoneCallback(CANopenJob& job, TickType_t maxqueuewait=0); public: // Queue API: virtual CANopenResult_t SubmitJob(CANopenJob& job, TickType_t maxqueuewait=0); virtual CANopenResult_t ReceiveDone(CANopenJob& job, TickType_t maxqueuewait=0); public: // Customisation: virtual void InitSendNMT(CANopenJob& job, uint8_t nodeid, CANopenNMTCommand_t command, bool wait_for_state=false, int resp_timeout_ms=1000, int max_tries=3); virtual void InitReceiveHB(CANopenJob& job, uint8_t nodeid, CANopenNMTState_t* statebuf=NULL, int recv_timeout_ms=1000, int max_tries=1); virtual void InitReadSDO(CANopenJob& job, uint8_t nodeid, uint16_t index, uint8_t subindex, uint8_t* buf, size_t bufsize, int resp_timeout_ms=100, int max_tries=3); virtual void InitWriteSDO(CANopenJob& job, uint8_t nodeid, uint16_t index, uint8_t subindex, uint8_t* buf, size_t bufsize, int resp_timeout_ms=100, int max_tries=3); public: // Main API: virtual CANopenResult_t SendNMT(uint8_t nodeid, CANopenNMTCommand_t command, bool wait_for_state=false, int resp_timeout_ms=1000, int max_tries=3); virtual CANopenResult_t ReceiveHB(uint8_t nodeid, CANopenNMTState_t* statebuf=NULL, int recv_timeout_ms=1000, int max_tries=1); virtual CANopenResult_t ReadSDO(uint8_t nodeid, uint16_t index, uint8_t subindex, uint8_t* buf, size_t bufsize, int resp_timeout_ms=100, int max_tries=3); virtual CANopenResult_t WriteSDO(uint8_t nodeid, uint16_t index, uint8_t subindex, uint8_t* buf, size_t bufsize, int resp_timeout_ms=100, int max_tries=3); public: CANopenWorker* m_worker; QueueHandle_t m_done_queue; }; /** * CANopenClient provides the synchronous API to the CANopen framework. * * The API methods will block until the job is done, job detail results * are returned in the caller provided job. * * See CANopen shell commands for usage examples. */ class CANopenClient : public CANopenAsyncClient { public: CANopenClient(canbus* canbus); CANopenClient(CANopenWorker* worker); virtual ~CANopenClient(); public: // Queue API: virtual CANopenResult_t ExecuteJob(CANopenJob& job, TickType_t maxqueuewait=0); public: // Main API: virtual CANopenResult_t SendNMT(CANopenJob& job, uint8_t nodeid, CANopenNMTCommand_t command, bool wait_for_state=false, int resp_timeout_ms=1000, int max_tries=3); virtual CANopenResult_t ReceiveHB(CANopenJob& job, uint8_t nodeid, CANopenNMTState_t* statebuf=NULL, int recv_timeout_ms=1000, int max_tries=1); virtual CANopenResult_t ReadSDO(CANopenJob& job, uint8_t nodeid, uint16_t index, uint8_t subindex, uint8_t* buf, size_t bufsize, int resp_timeout_ms=100, int max_tries=3); virtual CANopenResult_t WriteSDO(CANopenJob& job, uint8_t nodeid, uint16_t index, uint8_t subindex, uint8_t* buf, size_t bufsize, int resp_timeout_ms=100, int max_tries=3); public: SemaphoreHandle_t m_mutex; // thread mutex }; /** * The CANopen master manages all CANopenWorkers, provides shell commands * and the CAN rx task for all workers. */ class CANopen { public: CANopen(); ~CANopen(); public: void CanRxTask(); public: CANopenWorker* Start(canbus* bus); bool Stop(canbus* bus); CANopenWorker* GetWorker(canbus* bus); void StatusReport(int verbosity, OvmsWriter* writer); public: static const std::string GetJobName(const CANopenJob_t jobtype); static const std::string GetJobName(const CANopenJob& job); static const std::string GetCommandName(const CANopenNMTCommand_t command); static const std::string GetStateName(const CANopenNMTState_t state); static const std::string GetAbortCodeName(const uint32_t abortcode); static const std::string GetResultString(const CANopenResult_t result); static const std::string GetResultString(const CANopenResult_t result, const uint32_t abortcode); static const std::string GetResultString(const CANopenJob& job); static int PrintNodeInfo(int capacity, OvmsWriter* writer, canbus* bus, int nodeid, int timeout_ms=100, bool brief=false, bool quiet=false); public: static void shell_start(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc, const char* const* argv); static void shell_stop(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc, const char* const* argv); static void shell_status(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc, const char* const* argv); static void shell_nmt(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc, const char* const* argv); static void shell_readsdo(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc, const char* const* argv); static void shell_writesdo(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc, const char* const* argv); static void shell_info(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc, const char* const* argv); static void shell_scan(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc, const char* const* argv); public: QueueHandle_t m_rxqueue; // CAN rx queue TaskHandle_t m_rxtask; // CAN rx task CANopenWorker* m_worker[CAN_INTERFACE_CNT]; int m_workercnt; }; extern CANopen MyCANopen; #endif // __CANOPEN_H__