274 lines
8.5 KiB
ReStructuredText
274 lines
8.5 KiB
ReStructuredText
|
CANopen API Usage
|
||
|
=================
|
||
|
|
||
|
|
||
|
Synchronous API
|
||
|
---------------
|
||
|
|
||
|
The synchronous use is very simple, all you need is a ``CANopenClient`` instance.
|
||
|
|
||
|
On creation the client automatically connects to the active CANopen worker or starts a
|
||
|
new worker instance if necessary.
|
||
|
|
||
|
The ``CANopenClient`` methods will block until the job is done (or has failed).
|
||
|
Job results and/or error details are returned in the caller provided job.
|
||
|
|
||
|
.. caution:: Do not make synchronous calls from code that may run in a restricted context,
|
||
|
e.g. within a metric update handler. Avoid using synchronous calls from time critical
|
||
|
code, e.g. a CAN or event handler.
|
||
|
|
||
|
|
||
|
Example
|
||
|
^^^^^^^
|
||
|
|
||
|
.. code-block:: c++
|
||
|
|
||
|
#include "canopen.h"
|
||
|
|
||
|
// find CAN interface:
|
||
|
canbus* bus = (canbus*) MyPcpApp.FindDeviceByName("can1");
|
||
|
// …or simply use m_can1 if you're a vehicle subclass
|
||
|
|
||
|
// create CANopen client:
|
||
|
CANopenClient client(bus);
|
||
|
|
||
|
// a CANopen job holds request and response data:
|
||
|
CANopenJob job;
|
||
|
|
||
|
// read value from node #1 SDO 0x1008.00:
|
||
|
uint32_t value;
|
||
|
if (client.ReadSDO(&job, 1, 0x1008, 0x00, (uint8_t)&value, sizeof(value)) == COR_OK) {
|
||
|
// read result is now in value
|
||
|
}
|
||
|
|
||
|
// start node #2, wait for presence:
|
||
|
if (client.SendNMT(&job, 2, CONC_Start, true) == COR_OK) {
|
||
|
// node #2 is now started
|
||
|
}
|
||
|
|
||
|
// write value into node #3 SDO 0x2345.18:
|
||
|
if (client.WriteSDO(&job, 3, 0x2345, 0x18, (uint8_t)&value, 0) == COR_OK) {
|
||
|
// value has now been written into register 0x2345.18
|
||
|
}
|
||
|
|
||
|
|
||
|
Main API methods
|
||
|
^^^^^^^^^^^^^^^^
|
||
|
|
||
|
.. code-block:: c++
|
||
|
|
||
|
/**
|
||
|
* SendNMT: send NMT request and optionally wait for NMT state change
|
||
|
* a.k.a. heartbeat message.
|
||
|
*
|
||
|
* Note: NMT responses are not a part of the CANopen NMT protocol, and
|
||
|
* sending "heartbeat" NMT state updates is optional for CANopen nodes.
|
||
|
* If the node sends no state info, waiting for it will result in timeout
|
||
|
* even though the state has in fact changed -- there's no way to know
|
||
|
* if the node doesn't tell.
|
||
|
*/
|
||
|
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);
|
||
|
|
||
|
/**
|
||
|
* ReceiveHB: wait for next heartbeat message of a node,
|
||
|
* return state received.
|
||
|
*
|
||
|
* Use this to read the current state or synchronize to the heartbeat.
|
||
|
* Note: heartbeats are optional in CANopen.
|
||
|
*/
|
||
|
CANopenResult_t ReceiveHB(CANopenJob& job,
|
||
|
uint8_t nodeid, CANopenNMTState_t* statebuf=NULL,
|
||
|
int recv_timeout_ms=1000, int max_tries=1);
|
||
|
|
||
|
/**
|
||
|
* ReadSDO: read bytes from SDO server into buffer
|
||
|
* - reads data into buf (up to bufsize bytes)
|
||
|
* - returns data length read in job.sdo.xfersize
|
||
|
* - … and data length available in job.sdo.contsize (if known)
|
||
|
* - remaining buffer space will be zeroed
|
||
|
* - on result COR_ERR_BufferTooSmall, the buffer has been filled up to bufsize
|
||
|
* - on abort, the CANopen error code can be retrieved from job.sdo.error
|
||
|
*
|
||
|
* Note: result interpretation is up to caller (check device object dictionary
|
||
|
* for data types & sizes). As CANopen is little endian as ESP32, we don't
|
||
|
* need to check lengths on numerical results, i.e. anything from int8_t to
|
||
|
* uint32_t can simply be read into a uint32_t buffer.
|
||
|
*/
|
||
|
CANopenResult_t ReadSDO(CANopenJob& job,
|
||
|
uint8_t nodeid, uint16_t index, uint8_t subindex, uint8_t* buf, size_t bufsize,
|
||
|
int resp_timeout_ms=50, int max_tries=3);
|
||
|
|
||
|
/**
|
||
|
* WriteSDO: write bytes from buffer into SDO server
|
||
|
* - sends bufsize bytes from buf
|
||
|
* - … or 4 bytes from buf if bufsize is 0 (use for integer SDOs of unknown type)
|
||
|
* - returns data length sent in job.sdo.xfersize
|
||
|
* - on abort, the CANopen error code can be retrieved from job.sdo.error
|
||
|
*
|
||
|
* Note: the caller needs to know data type & size of the SDO register (check
|
||
|
* device object dictionary). As CANopen servers normally are intelligent,
|
||
|
* anything from int8_t to uint32_t can simply be sent as a uint32_t with
|
||
|
* bufsize=0, the server will know how to convert it.
|
||
|
*/
|
||
|
CANopenResult_t WriteSDO(CANopenJob& job,
|
||
|
uint8_t nodeid, uint16_t index, uint8_t subindex, uint8_t* buf, size_t bufsize,
|
||
|
int resp_timeout_ms=50, int max_tries=3);
|
||
|
|
||
|
|
||
|
If you want to create custom jobs, use the low level method ``ExecuteJob()`` to execute them.
|
||
|
|
||
|
|
||
|
Asynchronous API
|
||
|
----------------
|
||
|
|
||
|
The ``CANopenAsyncClient`` class provides the asynchronous interface and the response queue.
|
||
|
|
||
|
To use the asynchronous API you need to handle asynchronous responses, which normally
|
||
|
means adding a dedicated task for this. A minimal handling would be to simply discard
|
||
|
the responses (just empty the queue), if you don't need to care about the results.
|
||
|
|
||
|
|
||
|
Example
|
||
|
^^^^^^^
|
||
|
|
||
|
Instantiate the async client for a CAN bus and a queue size like this:
|
||
|
|
||
|
.. code-block:: c++
|
||
|
|
||
|
CANopenAsyncClient m_async(m_can1, 50);
|
||
|
|
||
|
Example response handler task:
|
||
|
|
||
|
.. code-block:: c++
|
||
|
|
||
|
void MyAsyncTask()
|
||
|
{
|
||
|
CANopenJob job;
|
||
|
while (true) {
|
||
|
if (m_async.ReceiveDone(job, portMAX_DELAY) != COR_ERR_QueueEmpty) {
|
||
|
// …process job results…
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
Sending requests is following the same scheme as with the synchronous API. Standard
|
||
|
result code is ``COR_WAIT``, an error may occur if the queue is full.
|
||
|
|
||
|
.. code-block:: c++
|
||
|
|
||
|
if (m_async.WriteSDO(m_nodeid, index, subindex, (uint8_t*)value, 0) != COR_WAIT) {
|
||
|
// …handle error…
|
||
|
}
|
||
|
|
||
|
|
||
|
Main API methods
|
||
|
^^^^^^^^^^^^^^^^
|
||
|
|
||
|
The API methods are similar to the synchronous methods (see above).
|
||
|
|
||
|
.. code-block:: c++
|
||
|
|
||
|
CANopenResult_t SendNMT(uint8_t nodeid, CANopenNMTCommand_t command,
|
||
|
bool wait_for_state=false, int resp_timeout_ms=1000, int max_tries=3);
|
||
|
|
||
|
CANopenResult_t ReceiveHB(uint8_t nodeid, CANopenNMTState_t* statebuf=NULL,
|
||
|
int recv_timeout_ms=1000, int max_tries=1);
|
||
|
|
||
|
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);
|
||
|
|
||
|
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);
|
||
|
|
||
|
|
||
|
``CANopenJob`` objects are created automatically by these methods. Jobs done
|
||
|
need to be fetched by looping ``ReceiveDone()`` until it returns ``COR_ERR_QueueEmpty``.
|
||
|
|
||
|
If you want to create custom jobs, use the low level method ``SubmitJob()`` to add them
|
||
|
to the worker queue.
|
||
|
|
||
|
|
||
|
Error Handling
|
||
|
--------------
|
||
|
|
||
|
If an error occurs, it will be given as a ``CANopenResult_t`` other than ``COR_OK`` or
|
||
|
``COR_WAIT``, either by a method result or by the ``CANopenJob.result`` field.
|
||
|
|
||
|
Result codes are:
|
||
|
|
||
|
.. code-block:: c++
|
||
|
|
||
|
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
|
||
|
|
||
|
Additionally, if an SDO read/write error occurs, an abortion error code may be given
|
||
|
by the slave. These codes follow the CANopen standard and may be extended by device
|
||
|
specific codes.
|
||
|
|
||
|
To translate a ``CANopenResult_t`` and/or a known SDO abort code into a string,
|
||
|
use the ``CANopen`` class utility methods:
|
||
|
|
||
|
.. code-block:: c++
|
||
|
|
||
|
std::string GetAbortCodeName(const uint32_t abortcode);
|
||
|
std::string GetResultString(const CANopenResult_t result);
|
||
|
std::string GetResultString(const CANopenResult_t result, const uint32_t abortcode);
|
||
|
std::string GetResultString(const CANopenJob& job);
|
||
|
|
||
|
|
||
|
Example
|
||
|
^^^^^^^
|
||
|
|
||
|
.. code-block:: c++
|
||
|
|
||
|
if (job.result != COR_OK) {
|
||
|
ESP_LOGE(TAG, "Result for %s: %s",
|
||
|
CANopen::GetJobName(job).c_str(),
|
||
|
CANopen::GetResultString(job).c_str());
|
||
|
}
|
||
|
|
||
|
|
||
|
Custom Address Schemes
|
||
|
----------------------
|
||
|
|
||
|
The standard clients use 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.
|
||
|
|
||
|
|
||
|
More Code Examples
|
||
|
------------------
|
||
|
|
||
|
* See shell commands in ``canopen_shell.cpp``
|
||
|
* See classes ``SevconClient`` and ``SevconJob`` in the Twizy SEVCON module
|
||
|
|