-
Notifications
You must be signed in to change notification settings - Fork 217
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Docs: added & extended CANopen documentation
- Loading branch information
Showing
11 changed files
with
597 additions
and
0 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
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,273 @@ | ||
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 | ||
|
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,60 @@ | ||
CANopen Shell Commands | ||
====================== | ||
|
||
.. highlight:: none | ||
|
||
Show status report | ||
:: | ||
|
||
copen status | ||
|
||
Start / stop workers | ||
:: | ||
|
||
copen <bus> start | ||
copen <bus> stop | ||
|
||
- stop will fail if clients are still running. | ||
|
||
Send NMT commands | ||
:: | ||
|
||
copen <bus> nmt <start|stop|preop|reset|commreset> [id=0] [timeout_ms=0] | ||
|
||
- id 0 = broadcast | ||
- timeout > 0: wait for state change (heartbeat), 3 tries | ||
Note: state change response is not a mandatory CANopen feature. | ||
|
||
Read SDO | ||
:: | ||
|
||
copen <bus> readsdo <id> <index_hex> <subindex_hex> [timeout_ms=50] | ||
|
||
- index & subindex: hexadecimal without "0x" or "h" | ||
- defaults to 3 tries on timeout | ||
|
||
Write SDO | ||
:: | ||
|
||
copen <bus> writesdo <id> <index_hex> <subindex_hex> <value> [timeout_ms=50] | ||
|
||
- index & subindex: hexadecimal without "0x" or "h" | ||
- value: prefix "0x" = hex, else decimal, string if no decimal | ||
- defaults to 3 tries on timeout | ||
|
||
Show node core attributes | ||
:: | ||
|
||
copen <bus> info <id> [timeout_ms=50] | ||
|
||
- prints device type, error register, device name etc. | ||
- Note: some attributes read are optional and may be empty/zero. | ||
|
||
Scan bus for nodes | ||
:: | ||
|
||
copen <bus> scan [[startid=1][-][endid=127]] [timeout_ms=50] | ||
|
||
- loops "info" over multiple node ids (default: all) | ||
- Note: a full scan with default timeout takes ~20 seconds | ||
|
File renamed without changes.
Oops, something went wrong.