Two-axis solar trackersRaspberry PLC 21CANopenAcquisition
Reading CANopen inclinometers and encoders with Python on a PLC
A motor without position feedback is a guess, not a control loop. This example reads two TY7953 CANopen encoders/inclinometers — one per axis — from the CAN bus of a Raspberry PLC 21 using Python. It comes from a real two-axis solar tracker deployment, where the same bus carries the drive and both sensors. You will see how to bring each node to OPERATIONAL state, read position_value over SDO, and convert raw units into degrees with a simple plant calibration.
Two nodes, one bus
The elevation sensor sits at node 4 and the azimuth sensor at node 5, sharing the twisted pair with the motor drive. Each is added to the network with its EDS file, so the object dictionary can be addressed by name. A unique node ID per device and termination resistors at both bus ends are all the wiring discipline CANopen asks for.
NMT state before any reading
A freshly powered CANopen node boots into PRE-OPERATIONAL, where SDO works but the device is not committed to its task. The script explicitly sets nmt.state to OPERATIONAL and waits briefly before polling. Doing this per node, rather than broadcasting, makes startup deterministic: if one sensor is dead, you learn exactly which one at connection time instead of discovering it mid-tracking with the structure already moving.
From raw counts to degrees
Reading encoder.sdo["position_value"].raw returns integer units, not angles. The plant calibration in this deployment is 10 units per degree, plus a per-axis zero offset captured during commissioning with the table horizontal and pointing south. Keeping calibration constants at the top of the file means a mechanical re-zeroing after maintenance is a one-line change, and each tracker in the fleet carries its own offsets without touching the control logic.
A snippet from the implementation
Straight from the example as deployed on the Raspberry PLC 21 — copy it freely:
def connect_encoder(network, node_id):
"""Registers an encoder on the bus and switches it to OPERATIONAL."""
encoder = network.add_node(node_id, EDS_ENCODER)
encoder.nmt.state = 'OPERATIONAL'
# Short wait so the node processes the NMT state change.
time.sleep(0.1)
print(f"Encoder node {node_id}: state {encoder.nmt.state}")
return encoder
The full example is a complete program — wiring header, setup and main loop — ready to adapt to your application.
Frequently asked questions
What is the difference between an encoder and an inclinometer here?
The TY7953 units report absolute tilt as a CANopen position object, so the software treats them like absolute encoders. Because they measure gravity-referenced angle, the reading survives power cycles without homing.
How fast can I poll position over SDO?
An SDO read is a request-response transaction, comfortably done at a few tens of hertz at 125 kbit/s. For a solar tracker, the 1-second polling in the example is already generous; PDOs are only needed for fast motion control.
What happens if an encoder stops answering?
The SDO read raises a timeout exception in the canopen library. In production, that should stop the affected axis via the drive and flag the fault, since moving without feedback risks driving the structure into its end stops.