<?xml version="1.0" encoding="UTF-8"?>        <rss version="2.0"
             xmlns:atom="http://www.w3.org/2005/Atom"
             xmlns:dc="http://purl.org/dc/elements/1.1/"
             xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
             xmlns:admin="http://webns.net/mvcb/"
             xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
             xmlns:content="http://purl.org/rss/1.0/modules/content/">
        <channel>
            <title>
									Art + Robotics Research LAB Forum - Recent Posts				            </title>
            <link>https://robartx.tech/community/</link>
            <description>Art + Robotics Research LAB Discussion Board</description>
            <language>en-US</language>
            <lastBuildDate>Sat, 13 Jun 2026 06:16:40 +0000</lastBuildDate>
            <generator>wpForo</generator>
            <ttl>60</ttl>
							                    <item>
                        <title>RE: code</title>
                        <link>https://robartx.tech/community/robocall/code/#post-36</link>
                        <pubDate>Fri, 27 Feb 2026 19:30:57 +0000</pubDate>
                        <description><![CDATA[roboclaw_node.py
 
#!/usr/bin/env python
import diagnostic_updater
import rclpy
from geometry_msgs.msg import Twist
from rclpy.node import Node

from tcr_roboclaw import Roboclaw
fr...]]></description>
                        <content:encoded><![CDATA[<div>
<div>roboclaw_node.py</div>
<div> </div>
<div><span>#!/usr/bin/env python</span></div>
<div><span>import</span><span> diagnostic_updater</span></div>
<div><span>import</span><span> rclpy</span></div>
<div><span>from</span><span> geometry_msgs.msg </span><span>import</span><span> Twist</span></div>
<div><span>from</span><span> rclpy.node </span><span>import</span><span> Node</span></div>
<br />
<div><span>from</span><span> tcr_roboclaw </span><span>import</span><span> Roboclaw</span></div>
<div><span>from</span><span> .electrical_wrapper </span><span>import</span><span> ElectricalWrapper</span></div>
<div><span>from</span><span> .encoder_wrapper </span><span>import</span><span> EncoderWrapper</span></div>
<div><span>from</span><span> . </span><span>import</span><span> utils </span><span>as</span><span> u</span></div>
<br />
<div><span>__author__ </span><span>=</span><span> </span><span>"bwbazemore@uga.edu (Brad Bazemore)"</span></div>
<br />
<div><span>import</span><span> serial </span><span># raw serial helper (used only in apply_voltage_limits)</span></div>
<br />
<div><span># -----------------------------</span></div>
<div><span># Raw RoboClaw packet helpers</span></div>
<div><span># -----------------------------</span></div>
<div><span>def</span><span> </span><span>_crc16_ccitt</span><span>(</span><span>packet</span><span>: </span><span>bytes</span><span>) -&gt; </span><span>int</span><span>:</span></div>
<div><span> </span><span>"""RoboClaw CRC16-CCITT (poly 0x1021), init 0."""</span></div>
<div><span> crc </span><span>=</span><span> </span><span>0</span></div>
<div><span> </span><span>for</span><span> b </span><span>in</span><span> packet:</span></div>
<div><span> crc </span><span>^=</span><span> (b </span><span>&lt;&lt;</span><span> </span><span>8</span><span>)</span></div>
<div><span> </span><span>for</span><span> _ </span><span>in</span><span> </span><span>range</span><span>(</span><span>8</span><span>):</span></div>
<div><span> </span><span>if</span><span> crc </span><span>&amp;</span><span> </span><span>0x</span><span>8000</span><span>:</span></div>
<div><span> crc </span><span>=</span><span> ((crc </span><span>&lt;&lt;</span><span> </span><span>1</span><span>) </span><span>^</span><span> </span><span>0x</span><span>1021</span><span>) </span><span>&amp;</span><span> </span><span>0x</span><span>FFFF</span></div>
<div><span> </span><span>else</span><span>:</span></div>
<div><span> crc </span><span>=</span><span> (crc </span><span>&lt;&lt;</span><span> </span><span>1</span><span>) </span><span>&amp;</span><span> </span><span>0x</span><span>FFFF</span></div>
<div><span> </span><span>return</span><span> crc</span></div>
<br /><br />
<div><span>def</span><span> </span><span>_send_packet_expect_ff</span><span>(</span></div>
<div><span> </span><span>ser</span><span>: serial.Serial, </span><span>payload_wo_crc</span><span>: </span><span>bytes</span><span>, </span><span>timeout_s</span><span>: </span><span>float</span><span> </span><span>=</span><span> </span><span>0.2</span></div>
<div><span>) -&gt; </span><span>bool</span><span>:</span></div>
<div><span> </span><span>"""Send payload (addr+cmd+data), append CRC16. Expect 0xFF ack."""</span></div>
<div><span> crc </span><span>=</span><span> _crc16_ccitt(payload_wo_crc)</span></div>
<div><span> pkt </span><span>=</span><span> payload_wo_crc </span><span>+</span><span> crc.to_bytes(</span><span>2</span><span>, </span><span>"big"</span><span>)</span></div>
<div><span> ser.reset_input_buffer()</span></div>
<div><span> ser.write(pkt)</span></div>
<div><span> ser.flush()</span></div>
<div><span> ser.timeout </span><span>=</span><span> timeout_s</span></div>
<div><span> ack </span><span>=</span><span> ser.read(</span><span>1</span><span>)</span></div>
<div><span> </span><span>return</span><span> ack </span><span>==</span><span> </span><span>b</span><span>"</span><span>\xFF</span><span>"</span></div>
<br /><br />
<div><span>def</span><span> </span><span>_read_exact</span><span>(</span><span>ser</span><span>: serial.Serial, </span><span>n</span><span>: </span><span>int</span><span>, </span><span>timeout_s</span><span>: </span><span>float</span><span> </span><span>=</span><span> </span><span>0.3</span><span>) -&gt; </span><span>bytes</span><span>:</span></div>
<div><span> ser.timeout </span><span>=</span><span> timeout_s</span></div>
<div><span> data </span><span>=</span><span> ser.read(n)</span></div>
<div><span> </span><span>if</span><span> </span><span>len</span><span>(data) </span><span>!=</span><span> n:</span></div>
<div><span> </span><span>raise</span><span> </span><span>RuntimeError</span><span>(</span></div>
<div><span> </span><span>f</span><span>"Timeout: expected </span><span>{</span><span>n</span><span>}</span><span> bytes, got </span><span>{</span><span>len</span><span>(data)</span><span>}</span><span> bytes: </span><span>{</span><span>data</span><span>!r}</span><span>"</span></div>
<div><span> )</span></div>
<div><span> </span><span>return</span><span> data</span></div>
<br /><br />
<div><span>def</span><span> </span><span>_set_main_voltages_raw</span><span>(</span></div>
<div><span> </span><span>ser</span><span>: serial.Serial, </span><span>addr</span><span>: </span><span>int</span><span>, </span><span>vmin_tenths</span><span>: </span><span>int</span><span>, </span><span>vmax_tenths</span><span>: </span><span>int</span><span>, </span><span>offset_tenths</span><span>: </span><span>int</span></div>
<div><span>) -&gt; </span><span>bool</span><span>:</span></div>
<div><span> </span><span>"""</span></div>
<div><span> 57 - Set Main Battery Voltages</span></div>
<div><span> Send: </span></div>
<div><span> Receive: </span></div>
<div><span> """</span></div>
<div><span> payload </span><span>=</span><span> </span><span>bytearray</span><span>()</span></div>
<div><span> payload </span><span>+=</span><span> </span><span>int</span><span>(vmin_tenths).to_bytes(</span><span>2</span><span>, </span><span>"big"</span><span>)</span></div>
<div><span> payload </span><span>+=</span><span> </span><span>int</span><span>(vmax_tenths).to_bytes(</span><span>2</span><span>, </span><span>"big"</span><span>)</span></div>
<div><span> payload </span><span>+=</span><span> </span><span>bytes</span><span>()</span></div>
<div><span> </span><span>return</span><span> _send_packet_expect_ff(ser, </span><span>bytes</span><span>(payload))</span></div>
<br /><br />
<div><span>def</span><span> </span><span>_set_logic_voltages_raw</span><span>(</span></div>
<div><span> </span><span>ser</span><span>: serial.Serial, </span><span>addr</span><span>: </span><span>int</span><span>, </span><span>vmin_tenths</span><span>: </span><span>int</span><span>, </span><span>vmax_tenths</span><span>: </span><span>int</span></div>
<div><span>) -&gt; </span><span>bool</span><span>:</span></div>
<div><span> </span><span>"""</span></div>
<div><span> 58 - Set Logic Battery Voltages</span></div>
<div><span> Send: </span></div>
<div><span> Receive: </span></div>
<div><span> """</span></div>
<div><span> payload </span><span>=</span><span> </span><span>bytearray</span><span>()</span></div>
<div><span> payload </span><span>+=</span><span> </span><span>int</span><span>(vmin_tenths).to_bytes(</span><span>2</span><span>, </span><span>"big"</span><span>)</span></div>
<div><span> payload </span><span>+=</span><span> </span><span>int</span><span>(vmax_tenths).to_bytes(</span><span>2</span><span>, </span><span>"big"</span><span>)</span></div>
<div><span> </span><span>return</span><span> _send_packet_expect_ff(ser, </span><span>bytes</span><span>(payload))</span></div>
<br /><br />
<div><span>def</span><span> </span><span>_read_main_voltage_settings_raw</span><span>(</span><span>ser</span><span>: serial.Serial, </span><span>addr</span><span>: </span><span>int</span><span>):</span></div>
<div><span> </span><span>"""</span></div>
<div><span> 59 - Read Main Battery Voltage Settings</span></div>
<div><span> Receive: </span></div>
<div><span> CRC computed over </span></div>
<div><span> """</span></div>
<div><span> cmd </span><span>=</span><span> </span><span>bytes</span><span>()</span></div>
<div><span> ser.reset_input_buffer()</span></div>
<div><span> ser.write(cmd </span><span>+</span><span> _crc16_ccitt(cmd).to_bytes(</span><span>2</span><span>, </span><span>"big"</span><span>))</span></div>
<div><span> ser.flush()</span></div>
<br />
<div><span> resp </span><span>=</span><span> _read_exact(ser, </span><span>2</span><span> </span><span>+</span><span> </span><span>2</span><span> </span><span>+</span><span> </span><span>1</span><span> </span><span>+</span><span> </span><span>2</span><span>)</span></div>
<div><span> data </span><span>=</span><span> resp</span></div>
<div><span> recv_crc </span><span>=</span><span> </span><span>int</span><span>.from_bytes(resp, </span><span>"big"</span><span>)</span></div>
<div><span> check </span><span>=</span><span> _crc16_ccitt(</span><span>bytes</span><span>() </span><span>+</span><span> data)</span></div>
<div><span> </span><span>if</span><span> recv_crc </span><span>!=</span><span> check:</span></div>
<div><span> </span><span>raise</span><span> </span><span>RuntimeError</span><span>(</span></div>
<div><span> </span><span>f</span><span>"Bad CRC on ReadMainVoltageSettings: recv </span><span>{</span><span>recv_crc</span><span>:04X}</span><span> != calc </span><span>{</span><span>check</span><span>:04X}</span><span>"</span></div>
<div><span> )</span></div>
<br />
<div><span> minv </span><span>=</span><span> </span><span>int</span><span>.from_bytes(data, </span><span>"big"</span><span>)</span></div>
<div><span> maxv </span><span>=</span><span> </span><span>int</span><span>.from_bytes(data, </span><span>"big"</span><span>)</span></div>
<div><span> off </span><span>=</span><span> data</span></div>
<div><span> </span><span>return</span><span> (minv, maxv, off)</span></div>
<br /><br />
<div><span>def</span><span> </span><span>_read_logic_voltage_settings_raw</span><span>(</span><span>ser</span><span>: serial.Serial, </span><span>addr</span><span>: </span><span>int</span><span>):</span></div>
<div><span> </span><span>"""</span></div>
<div><span> 60 - Read Logic Battery Voltage Settings</span></div>
<div><span> Receive: </span></div>
<div><span> CRC over </span></div>
<div><span> """</span></div>
<div><span> cmd </span><span>=</span><span> </span><span>bytes</span><span>()</span></div>
<div><span> ser.reset_input_buffer()</span></div>
<div><span> ser.write(cmd </span><span>+</span><span> _crc16_ccitt(cmd).to_bytes(</span><span>2</span><span>, </span><span>"big"</span><span>))</span></div>
<div><span> ser.flush()</span></div>
<br />
<div><span> resp </span><span>=</span><span> _read_exact(ser, </span><span>2</span><span> </span><span>+</span><span> </span><span>2</span><span> </span><span>+</span><span> </span><span>2</span><span>)</span></div>
<div><span> data </span><span>=</span><span> resp</span></div>
<div><span> recv_crc </span><span>=</span><span> </span><span>int</span><span>.from_bytes(resp, </span><span>"big"</span><span>)</span></div>
<div><span> check </span><span>=</span><span> _crc16_ccitt(</span><span>bytes</span><span>() </span><span>+</span><span> data)</span></div>
<div><span> </span><span>if</span><span> recv_crc </span><span>!=</span><span> check:</span></div>
<div><span> </span><span>raise</span><span> </span><span>RuntimeError</span><span>(</span></div>
<div><span> </span><span>f</span><span>"Bad CRC on ReadLogicVoltageSettings: recv </span><span>{</span><span>recv_crc</span><span>:04X}</span><span> != calc </span><span>{</span><span>check</span><span>:04X}</span><span>"</span></div>
<div><span> )</span></div>
<br />
<div><span> minv </span><span>=</span><span> </span><span>int</span><span>.from_bytes(data, </span><span>"big"</span><span>)</span></div>
<div><span> maxv </span><span>=</span><span> </span><span>int</span><span>.from_bytes(data, </span><span>"big"</span><span>)</span></div>
<div><span> </span><span>return</span><span> (minv, maxv)</span></div>
<br /><br />
<div><span>class</span><span> </span><span>RoboclawNode</span><span>(</span><span>Node</span><span>):</span></div>
<div><span> </span><span>"""RoboClaw Node"""</span></div>
<br />
<div><span> </span><span>def</span><span> </span><span>apply_voltage_limits</span><span>(</span><span>self</span><span>):</span></div>
<div><span> min_main_t </span><span>=</span><span> </span><span>int</span><span>(</span><span>round</span><span>(</span><span>float</span><span>(</span><span>self</span><span>.min_main_voltage) </span><span>*</span><span> </span><span>10</span><span>))</span></div>
<div><span> max_main_t </span><span>=</span><span> </span><span>int</span><span>(</span><span>round</span><span>(</span><span>float</span><span>(</span><span>self</span><span>.max_main_voltage) </span><span>*</span><span> </span><span>10</span><span>))</span></div>
<div><span> min_logic_t </span><span>=</span><span> </span><span>int</span><span>(</span><span>round</span><span>(</span><span>float</span><span>(</span><span>self</span><span>.min_logic_voltage) </span><span>*</span><span> </span><span>10</span><span>))</span></div>
<div><span> max_logic_t </span><span>=</span><span> </span><span>int</span><span>(</span><span>round</span><span>(</span><span>float</span><span>(</span><span>self</span><span>.max_logic_voltage) </span><span>*</span><span> </span><span>10</span><span>))</span></div>
<div><span> offset_t </span><span>=</span><span> </span><span>int</span><span>(</span><span>round</span><span>(</span><span>float</span><span>(</span><span>self</span><span>.automax_main_offset_voltage) </span><span>*</span><span> </span><span>10</span><span>))</span></div>
<div><span> offset_t </span><span>=</span><span> </span><span>max</span><span>(</span><span>0</span><span>, </span><span>min</span><span>(</span><span>255</span><span>, offset_t))</span></div>
<br />
<div><span> baud </span><span>=</span><span> </span><span>int</span><span>(</span><span>self</span><span>.BAUD)</span></div>
<br />
<div><span> targets </span><span>=</span><span> </span></div>
<div><span> </span><span>if</span><span> </span><span>self</span><span>.MIRROR_SECOND:</span></div>
<div><span> targets.append((</span><span>str</span><span>(</span><span>self</span><span>.DEV2), </span><span>int</span><span>(</span><span>self</span><span>.ADDRESS2)))</span></div>
<br />
<div><span> </span><span>for</span><span> port, addr </span><span>in</span><span> targets:</span></div>
<div><span> </span><span>self</span><span>.get_logger().info(</span></div>
<div><span> </span><span>f</span><span>"Applying RoboClaw voltage limits on </span><span>{</span><span>port</span><span>}</span><span> @ </span><span>{</span><span>baud</span><span>}</span><span>, addr=</span><span>{</span><span>addr</span><span>}</span><span>: "</span></div>
<div><span> </span><span>f</span><span>"MAIN </span><span>{</span><span>min_main_t</span><span>/</span><span>10</span><span>:.1f}</span><span>-</span><span>{</span><span>max_main_t</span><span>/</span><span>10</span><span>:.1f}</span><span>V (offset </span><span>{</span><span>offset_t</span><span>/</span><span>10</span><span>:.1f}</span><span>V), "</span></div>
<div><span> </span><span>f</span><span>"LOGIC </span><span>{</span><span>min_logic_t</span><span>/</span><span>10</span><span>:.1f}</span><span>-</span><span>{</span><span>max_logic_t</span><span>/</span><span>10</span><span>:.1f}</span><span>V"</span></div>
<div><span> )</span></div>
<br />
<div><span> </span><span>try</span><span>:</span></div>
<div><span> </span><span>with</span><span> serial.Serial(port, baud, </span><span>timeout</span><span>=</span><span>0.3</span><span>) </span><span>as</span><span> ser:</span></div>
<div><span> ok_main </span><span>=</span><span> _set_main_voltages_raw(ser, addr, min_main_t, max_main_t, offset_t)</span></div>
<div><span> ok_logic </span><span>=</span><span> _set_logic_voltages_raw(ser, addr, min_logic_t, max_logic_t)</span></div>
<br />
<div><span> </span><span>if</span><span> </span><span>not</span><span> ok_main:</span></div>
<div><span> </span><span>self</span><span>.get_logger().warn(</span><span>f</span><span>" Set Main Voltage (57) did not ACK (0xFF)."</span><span>)</span></div>
<div><span> </span><span>if</span><span> </span><span>not</span><span> ok_logic:</span></div>
<div><span> </span><span>self</span><span>.get_logger().warn(</span><span>f</span><span>" Set Logic Voltage (58) did not ACK (0xFF)."</span><span>)</span></div>
<br />
<div><span> </span><span>try</span><span>:</span></div>
<div><span> main_min, main_max, main_off </span><span>=</span><span> _read_main_voltage_settings_raw(ser, addr)</span></div>
<div><span> logic_min, logic_max </span><span>=</span><span> _read_logic_voltage_settings_raw(ser, addr)</span></div>
<div><span> </span><span>self</span><span>.get_logger().info(</span></div>
<div><span> </span><span>f</span><span>" Voltage settings now: "</span></div>
<div><span> </span><span>f</span><span>"MAIN </span><span>{</span><span>main_min</span><span>/</span><span>10</span><span>:.1f}</span><span>-</span><span>{</span><span>main_max</span><span>/</span><span>10</span><span>:.1f}</span><span>V (offset </span><span>{</span><span>main_off</span><span>/</span><span>10</span><span>:.1f}</span><span>V), "</span></div>
<div><span> </span><span>f</span><span>"LOGIC </span><span>{</span><span>logic_min</span><span>/</span><span>10</span><span>:.1f}</span><span>-</span><span>{</span><span>logic_max</span><span>/</span><span>10</span><span>:.1f}</span><span>V"</span></div>
<div><span> )</span></div>
<div><span> </span><span>except</span><span> </span><span>Exception</span><span> </span><span>as</span><span> e:</span></div>
<div><span> </span><span>self</span><span>.get_logger().warn(</span><span>f</span><span>" Could not read back voltage settings (59/60): </span><span>{</span><span>e</span><span>}</span><span>"</span><span>)</span></div>
<br />
<div><span> </span><span>except</span><span> </span><span>Exception</span><span> </span><span>as</span><span> e:</span></div>
<div><span> </span><span>self</span><span>.get_logger().warn(</span><span>f</span><span>" Failed to apply voltage limits: </span><span>{</span><span>e</span><span>}</span><span>"</span><span>)</span></div>
<br />
<div><span> </span><span>def</span><span> </span><span>__init__</span><span>(</span><span>self</span><span>):</span></div>
<div><span> </span><span>super</span><span>().</span><span>__init__</span><span>(</span><span>"roboclaw_node"</span><span>)</span></div>
<br />
<div><span> </span><span>self</span><span>.read_parameters()</span></div>
<br />
<div><span> </span><span># Board 1</span></div>
<div><span> </span><span>self</span><span>.driver </span><span>=</span><span> </span><span>self</span><span>.init_device(</span><span>self</span><span>.DEV, </span><span>self</span><span>.ADDRESS, </span><span>self</span><span>.BAUD)</span></div>
<br />
<div><span> </span><span># Board 2 (optional mirror)</span></div>
<div><span> </span><span>self</span><span>.driver2 </span><span>=</span><span> </span><span>None</span></div>
<div><span> </span><span>if</span><span> </span><span>self</span><span>.MIRROR_SECOND:</span></div>
<div><span> </span><span>self</span><span>.driver2 </span><span>=</span><span> </span><span>self</span><span>.init_device(</span><span>self</span><span>.DEV2, </span><span>self</span><span>.ADDRESS2, </span><span>self</span><span>.BAUD)</span></div>
<br />
<div><span> </span><span># Apply voltage limits (one-time at startup)</span></div>
<div><span> </span><span>self</span><span>.apply_voltage_limits()</span></div>
<br />
<div><span> </span><span># Reset + configure both boards</span></div>
<div><span> </span><span>self</span><span>.reset_device()</span></div>
<div><span> </span><span>self</span><span>.configure_device()</span></div>
<br />
<div><span> </span><span>self</span><span>.create_subscribers()</span></div>
<div><span> </span><span>self</span><span>.create_wrappers()</span></div>
<div><span> </span><span>self</span><span>.create_timers()</span></div>
<br />
<div><span> </span><span>self</span><span>.updater </span><span>=</span><span> diagnostic_updater.Updater(</span><span>self</span><span>)</span></div>
<div><span> </span><span>self</span><span>.updater.setHardwareID(</span><span>"RoboClaw"</span><span>)</span></div>
<div><span> </span><span>self</span><span>.updater.add(diagnostic_updater.FunctionDiagnosticTask(</span><span>"Vitals"</span><span>, </span><span>self</span><span>.diagnostics))</span></div>
<br />
<div><span> </span><span>self</span><span>.last_cmd_timestamp </span><span>=</span><span> </span><span>self</span><span>.get_clock().now().nanoseconds</span></div>
<br />
<div><span> </span><span># NEW: for encoder debug logging throttling</span></div>
<div><span> </span><span>self</span><span>._last_enc_log_ns </span><span>=</span><span> </span><span>0</span></div>
<br />
<div><span> </span><span>def</span><span> </span><span>read_parameters</span><span>(</span><span>self</span><span>):</span></div>
<div><span> </span><span>"""Read parameters from the parameter server"""</span></div>
<div><span> </span><span>self</span><span>.get_logger().info(</span><span>"Reading parameters..."</span><span>)</span></div>
<br />
<div><span> </span><span># Device params</span></div>
<div><span> </span><span>self</span><span>.DEV </span><span>=</span><span> </span><span>self</span><span>.init_parameter(</span><span>"dev"</span><span>, </span><span>"/dev/ttyACM0"</span><span>)</span></div>
<div><span> </span><span>self</span><span>.BAUD </span><span>=</span><span> </span><span>self</span><span>.init_parameter(</span><span>"baud"</span><span>, </span><span>38400</span><span>)</span></div>
<div><span> </span><span>self</span><span>.ADDRESS </span><span>=</span><span> </span><span>self</span><span>.init_parameter(</span><span>"address"</span><span>, </span><span>128</span><span>)</span></div>
<div><span> </span><span>self</span><span>.serial_timeout </span><span>=</span><span> </span><span>self</span><span>.init_parameter(</span><span>"serial_timeout"</span><span>, </span><span>0.0</span><span>)</span></div>
<div><span> </span><span>if</span><span> </span><span>self</span><span>.ADDRESS </span><span>&gt;</span><span> </span><span>0x</span><span>87</span><span> </span><span>or</span><span> </span><span>self</span><span>.ADDRESS </span><span>&lt;</span><span> </span><span>0x</span><span>80</span><span>:</span></div>
<div><span> </span><span>self</span><span>.get_logger().fatal(</span><span>"Address out of range"</span><span>)</span></div>
<div><span> </span><span>self</span><span>.shutdown(</span><span>"Address out of range"</span><span>)</span></div>
<br />
<div><span> </span><span># Odometry params</span></div>
<div><span> </span><span>self</span><span>.TICKS_PER_METER </span><span>=</span><span> </span><span>self</span><span>.init_parameter(</span><span>"ticks_per_meter"</span><span>, </span><span>4342.2</span><span>)</span></div>
<div><span> </span><span>self</span><span>.TICKS_PER_ROTATION </span><span>=</span><span> </span><span>self</span><span>.init_parameter(</span><span>"ticks_per_rotation"</span><span>, </span><span>2780</span><span>)</span></div>
<div><span> </span><span>self</span><span>.BASE_WIDTH </span><span>=</span><span> </span><span>self</span><span>.init_parameter(</span><span>"base_width"</span><span>, </span><span>0.315</span><span>)</span></div>
<br />
<div><span> </span><span># Movement params</span></div>
<div><span> </span><span>self</span><span>.max_speed_linear </span><span>=</span><span> </span><span>self</span><span>.init_parameter(</span><span>"max_speed_linear"</span><span>, </span><span>2.0</span><span>)</span></div>
<div><span> </span><span>self</span><span>.max_speed_angular </span><span>=</span><span> </span><span>self</span><span>.init_parameter(</span><span>"max_speed_angular"</span><span>, </span><span>1.0</span><span>)</span></div>
<div><span> </span><span>self</span><span>.accel </span><span>=</span><span> </span><span>int</span><span>(</span><span>self</span><span>.init_parameter(</span><span>"acceleration"</span><span>, </span><span>1.0</span><span>) </span><span>*</span><span> </span><span>self</span><span>.TICKS_PER_METER)</span></div>
<div><span> </span><span>self</span><span>.swap_motors </span><span>=</span><span> </span><span>self</span><span>.init_parameter(</span><span>"swap_motors"</span><span>, </span><span>False</span><span>)</span></div>
<div><span> </span><span>self</span><span>.stop_if_idle </span><span>=</span><span> </span><span>self</span><span>.init_parameter(</span><span>"stop_if_idle"</span><span>, </span><span>True</span><span>)</span></div>
<div><span> </span><span>self</span><span>.idle_timeout </span><span>=</span><span> </span><span>self</span><span>.init_parameter(</span><span>"idle_timeout"</span><span>, </span><span>1.0</span><span>)</span></div>
<br />
<div><span> </span><span># Roboclaw params</span></div>
<div><span> </span><span>self</span><span>.custom_pid </span><span>=</span><span> </span><span>self</span><span>.init_parameter(</span><span>"custom_pid"</span><span>, </span><span>True</span><span>)</span></div>
<div><span> </span><span>self</span><span>.P </span><span>=</span><span> </span><span>self</span><span>.init_parameter(</span><span>"p_constant"</span><span>, </span><span>3.0</span><span>)</span></div>
<div><span> </span><span>self</span><span>.I </span><span>=</span><span> </span><span>self</span><span>.init_parameter(</span><span>"i_constant"</span><span>, </span><span>0.42</span><span>)</span></div>
<div><span> </span><span>self</span><span>.D </span><span>=</span><span> </span><span>self</span><span>.init_parameter(</span><span>"d_constant"</span><span>, </span><span>0.0</span><span>)</span></div>
<div><span> </span><span>self</span><span>.qpps </span><span>=</span><span> </span><span>self</span><span>.init_parameter(</span><span>"qpps"</span><span>, </span><span>6000</span><span>)</span></div>
<br />
<div><span> </span><span># NEW: encoder debug prints (helps detect reversed/dead encoder)</span></div>
<div><span> </span><span>self</span><span>.debug_encoders </span><span>=</span><span> </span><span>self</span><span>.init_parameter(</span><span>"debug_encoders"</span><span>, </span><span>True</span><span>)</span></div>
<div><span> </span><span>self</span><span>.debug_encoder_rate_hz </span><span>=</span><span> </span><span>self</span><span>.init_parameter(</span><span>"debug_encoder_rate_hz"</span><span>, </span><span>2.0</span><span>) </span><span># print ~2 times/sec</span></div>
<br />
<div><span> </span><span># NEW: optionally disable dynamic PID writes (helps rule out instability)</span></div>
<div><span> </span><span>self</span><span>.enable_dynamic_params </span><span>=</span><span> </span><span>self</span><span>.init_parameter(</span><span>"enable_dynamic_params"</span><span>, </span><span>False</span><span>)</span></div>
<div><span> </span><span>self</span><span>.dynamic_params_period </span><span>=</span><span> </span><span>self</span><span>.init_parameter(</span><span>"dynamic_params_period"</span><span>, </span><span>1.0</span><span>)</span></div>
<br />
<div><span> </span><span># Publishing params</span></div>
<div><span> </span><span>self</span><span>.ODOM_RATE </span><span>=</span><span> </span><span>self</span><span>.init_parameter(</span><span>"odom_rate"</span><span>, </span><span>10</span><span>)</span></div>
<div><span> </span><span>self</span><span>.ELEC_RATE </span><span>=</span><span> </span><span>self</span><span>.init_parameter(</span><span>"elec_rate"</span><span>, </span><span>1</span><span>)</span></div>
<div><span> </span><span>self</span><span>.PUBLISH_ODOM </span><span>=</span><span> </span><span>self</span><span>.init_parameter(</span><span>"publish_odom"</span><span>, </span><span>True</span><span>)</span></div>
<div><span> </span><span>self</span><span>.PUBLISH_ENCODERS </span><span>=</span><span> </span><span>self</span><span>.init_parameter(</span><span>"publish_encoders"</span><span>, </span><span>True</span><span>)</span></div>
<div><span> </span><span>self</span><span>.PUBLISH_ELEC </span><span>=</span><span> </span><span>self</span><span>.init_parameter(</span><span>"publish_elec"</span><span>, </span><span>True</span><span>)</span></div>
<div><span> </span><span>self</span><span>.PUBLISH_TF </span><span>=</span><span> </span><span>self</span><span>.init_parameter(</span><span>"publish_tf"</span><span>, </span><span>False</span><span>)</span></div>
<br />
<div><span> </span><span># IO params</span></div>
<div><span> </span><span>self</span><span>.S3Mode </span><span>=</span><span> </span><span>self</span><span>.init_parameter(</span><span>"S3_mode"</span><span>, </span><span>0x</span><span>00</span><span>)</span></div>
<div><span> </span><span>self</span><span>.S4Mode </span><span>=</span><span> </span><span>self</span><span>.init_parameter(</span><span>"S4_mode"</span><span>, </span><span>0x</span><span>00</span><span>)</span></div>
<div><span> </span><span>self</span><span>.S5Mode </span><span>=</span><span> </span><span>self</span><span>.init_parameter(</span><span>"S5_mode"</span><span>, </span><span>0x</span><span>00</span><span>)</span></div>
<br />
<div><span> </span><span># Voltage limit params (RoboClaw EEPROM settings)</span></div>
<div><span> </span><span>self</span><span>.min_main_voltage </span><span>=</span><span> </span><span>self</span><span>.init_parameter(</span><span>"min_main_voltage"</span><span>, </span><span>5.0</span><span>)</span></div>
<div><span> </span><span>self</span><span>.max_main_voltage </span><span>=</span><span> </span><span>self</span><span>.init_parameter(</span><span>"max_main_voltage"</span><span>, </span><span>24.0</span><span>)</span></div>
<div><span> </span><span>self</span><span>.min_logic_voltage </span><span>=</span><span> </span><span>self</span><span>.init_parameter(</span><span>"min_logic_voltage"</span><span>, </span><span>5.0</span><span>)</span></div>
<div><span> </span><span>self</span><span>.max_logic_voltage </span><span>=</span><span> </span><span>self</span><span>.init_parameter(</span><span>"max_logic_voltage"</span><span>, </span><span>24.0</span><span>)</span></div>
<div><span> </span><span>self</span><span>.automax_main_offset_voltage </span><span>=</span><span> </span><span>self</span><span>.init_parameter(</span><span>"automax_main_offset_voltage"</span><span>, </span><span>2.0</span><span>)</span></div>
<br />
<div><span> </span><span># Optional 2nd RoboClaw</span></div>
<div><span> </span><span>self</span><span>.MIRROR_SECOND </span><span>=</span><span> </span><span>self</span><span>.init_parameter(</span><span>"mirror_second_board"</span><span>, </span><span>False</span><span>)</span></div>
<div><span> </span><span>self</span><span>.DEV2 </span><span>=</span><span> </span><span>self</span><span>.init_parameter(</span><span>"dev2"</span><span>, </span><span>""</span><span>)</span></div>
<div><span> </span><span>self</span><span>.ADDRESS2 </span><span>=</span><span> </span><span>self</span><span>.init_parameter(</span><span>"address2"</span><span>, </span><span>self</span><span>.ADDRESS)</span></div>
<br />
<div><span> </span><span># NEW: cmd_vel topic (so we can subscribe to twist_mux output)</span></div>
<div><span> </span><span>self</span><span>.cmd_vel_topic </span><span>=</span><span> </span><span>self</span><span>.init_parameter(</span><span>"cmd_vel_topic"</span><span>, </span><span>"/twist_mux/cmd_vel"</span><span>)</span></div>
<div> </div>
<br />
<div><span> </span><span>def</span><span> </span><span>init_parameter</span><span>(</span><span>self</span><span>, </span><span>name</span><span>, </span><span>default</span><span>):</span></div>
<div><span> </span><span>"""Initialize a parameter and log it"""</span></div>
<div><span> value </span><span>=</span><span> </span><span>self</span><span>.declare_parameter(name, default).value</span></div>
<div><span> </span><span>self</span><span>.get_logger().info(</span><span>f</span><span>" </span><span>{</span><span>name</span><span>}</span><span>: </span><span>{</span><span>value</span><span>}</span><span>"</span><span>)</span></div>
<div><span> </span><span>return</span><span> value</span></div>
<br />
<div><span> </span><span>def</span><span> </span><span>update_parameters</span><span>(</span><span>self</span><span>):</span></div>
<div><span> </span><span>"""</span></div>
<div><span> OPTIONAL dynamic update.</span></div>
<div><span> </span><span>NOTE</span><span>: Writing PID settings repeatedly while driving can cause weirdness on some setups.</span></div>
<div><span> That's why enable_dynamic_params defaults to False.</span></div>
<div><span> """</span></div>
<div><span> </span><span>self</span><span>.max_speed_linear </span><span>=</span><span> </span><span>self</span><span>.get_parameter(</span><span>"max_speed_linear"</span><span>).value</span></div>
<div><span> </span><span>self</span><span>.max_speed_angular </span><span>=</span><span> </span><span>self</span><span>.get_parameter(</span><span>"max_speed_angular"</span><span>).value</span></div>
<div><span> </span><span>self</span><span>.stop_if_idle </span><span>=</span><span> </span><span>self</span><span>.get_parameter(</span><span>"stop_if_idle"</span><span>).value</span></div>
<div><span> </span><span>self</span><span>.idle_timeout </span><span>=</span><span> </span><span>self</span><span>.get_parameter(</span><span>"idle_timeout"</span><span>).value</span></div>
<br />
<div><span> </span><span>self</span><span>.custom_pid </span><span>=</span><span> </span><span>self</span><span>.get_parameter(</span><span>"custom_pid"</span><span>).value</span></div>
<div><span> </span><span>self</span><span>.P </span><span>=</span><span> </span><span>self</span><span>.get_parameter(</span><span>"p_constant"</span><span>).value</span></div>
<div><span> </span><span>self</span><span>.I </span><span>=</span><span> </span><span>self</span><span>.get_parameter(</span><span>"i_constant"</span><span>).value</span></div>
<div><span> </span><span>self</span><span>.D </span><span>=</span><span> </span><span>self</span><span>.get_parameter(</span><span>"d_constant"</span><span>).value</span></div>
<div><span> </span><span>self</span><span>.qpps </span><span>=</span><span> </span><span>self</span><span>.get_parameter(</span><span>"qpps"</span><span>).value</span></div>
<div><span> </span><span>self</span><span>.configure_device()</span></div>
<br />
<div><span> </span><span>def</span><span> </span><span>init_device</span><span>(</span><span>self</span><span>, </span><span>dev_name</span><span>: </span><span>str</span><span>, </span><span>address</span><span>: </span><span>int</span><span>, </span><span>baud_rate</span><span>: </span><span>int</span><span>):</span></div>
<div><span> </span><span>"""Initialize the Roboclaw device and connect to it"""</span></div>
<div><span> </span><span>try</span><span>:</span></div>
<div><span> </span><span>self</span><span>.get_logger().info(</span><span>f</span><span>"Connecting to device </span><span>{</span><span>dev_name</span><span>}</span><span> with address </span><span>{</span><span>str</span><span>(address)</span><span>}</span><span>"</span><span>)</span></div>
<div><span> driver </span><span>=</span><span> Roboclaw(dev_name, baud_rate, address)</span></div>
<div><span> driver.open()</span></div>
<div><span> </span><span>self</span><span>.get_logger().info(</span><span>"Connected!"</span><span>)</span></div>
<div><span> </span><span>except</span><span> </span><span>Exception</span><span> </span><span>as</span><span> e:</span></div>
<div><span> </span><span>self</span><span>.get_logger().fatal(</span><span>"Could not connect to device."</span><span>)</span></div>
<div><span> </span><span>self</span><span>.get_logger().debug(e)</span></div>
<div><span> </span><span>self</span><span>.shutdown(</span><span>"Could not connect to Roboclaw"</span><span>)</span></div>
<br />
<div><span> </span><span>try</span><span>:</span></div>
<div><span> _, version </span><span>=</span><span> driver.ReadVersion()</span></div>
<div><span> </span><span>self</span><span>.get_logger().info(</span><span>f</span><span>"Roboclaw version: </span><span>{</span><span>str</span><span>(version)</span><span>}</span><span>"</span><span>)</span></div>
<div><span> </span><span>except</span><span> </span><span>Exception</span><span> </span><span>as</span><span> e:</span></div>
<div><span> </span><span>self</span><span>.get_logger().warn(</span><span>"Problem getting Roboclaw version"</span><span>)</span></div>
<div><span> </span><span>self</span><span>.get_logger().debug(e)</span></div>
<br />
<div><span> </span><span>return</span><span> driver</span></div>
<br />
<div><span> </span><span>def</span><span> </span><span>reset_device</span><span>(</span><span>self</span><span>):</span></div>
<div><span> </span><span>"""Reset the roboclaw speed and encoder counters"""</span></div>
<div><span> </span><span>self</span><span>.stop_motors()</span></div>
<div><span> </span><span>try</span><span>:</span></div>
<div><span> </span><span>self</span><span>.driver.ResetEncoders()</span></div>
<div><span> </span><span>except</span><span> </span><span>OSError</span><span> </span><span>as</span><span> e:</span></div>
<div><span> </span><span>self</span><span>.get_logger().warn(</span><span>f</span><span>"ResetEncoders OSError: </span><span>{</span><span>str</span><span>(e)</span><span>}</span><span>"</span><span>)</span></div>
<br />
<div><span> </span><span>if</span><span> </span><span>self</span><span>.driver2 </span><span>is</span><span> </span><span>not</span><span> </span><span>None</span><span>:</span></div>
<div><span> </span><span>try</span><span>:</span></div>
<div><span> </span><span>self</span><span>.driver2.ResetEncoders()</span></div>
<div><span> </span><span>except</span><span> </span><span>OSError</span><span> </span><span>as</span><span> e:</span></div>
<div><span> </span><span>self</span><span>.get_logger().warn(</span><span>f</span><span>"ResetEncoders OSError (driver2): </span><span>{</span><span>str</span><span>(e)</span><span>}</span><span>"</span><span>)</span></div>
<br />
<div><span> </span><span>def</span><span> </span><span>configure_device</span><span>(</span><span>self</span><span>):</span></div>
<div><span> </span><span>"""Configure both boards: timeout, PID, pin modes."""</span></div>
<div><span> </span><span>def</span><span> </span><span>_cfg</span><span>(</span><span>drv</span><span>):</span></div>
<div><span> drv.SetSerialTimeout(</span><span>int</span><span>(</span><span>self</span><span>.serial_timeout </span><span>*</span><span> </span><span>10</span><span>))</span></div>
<div><span> </span><span>if</span><span> </span><span>self</span><span>.custom_pid:</span></div>
<div><span> drv.SetM1VelocityPID(</span><span>self</span><span>.P, </span><span>self</span><span>.I, </span><span>self</span><span>.D, </span><span>self</span><span>.qpps)</span></div>
<div><span> drv.SetM2VelocityPID(</span><span>self</span><span>.P, </span><span>self</span><span>.I, </span><span>self</span><span>.D, </span><span>self</span><span>.qpps)</span></div>
<div><span> drv.SetPinFunctions(</span><span>self</span><span>.S3Mode, </span><span>self</span><span>.S4Mode, </span><span>self</span><span>.S5Mode)</span></div>
<br />
<div><span> _cfg(</span><span>self</span><span>.driver)</span></div>
<div><span> </span><span>if</span><span> </span><span>self</span><span>.driver2 </span><span>is</span><span> </span><span>not</span><span> </span><span>None</span><span>:</span></div>
<div><span> _cfg(</span><span>self</span><span>.driver2)</span></div>
<br />
<div><span> </span><span>def</span><span> </span><span>create_subscribers</span><span>(</span><span>self</span><span>):</span></div>
<div><span> </span><span>"""Create subscribers for the node"""</span></div>
<div><span> </span><span>self</span><span>.cmd_vel_sub </span><span>=</span><span> </span><span>self</span><span>.create_subscription(</span></div>
<div><span> Twist,</span></div>
<div><span> </span><span>str</span><span>(</span><span>self</span><span>.cmd_vel_topic),</span></div>
<div><span> </span><span>self</span><span>.cmd_vel_callback,</span></div>
<div><span> </span><span>10</span><span>,</span></div>
<div><span> )</span></div>
<div><span> </span><span>self</span><span>.get_logger().info(</span><span>f</span><span>"Subscribed to cmd_vel_topic: </span><span>{self</span><span>.cmd_vel_topic</span><span>}</span><span>"</span><span>)</span></div>
<br />
<div><span> </span><span>def</span><span> </span><span>create_wrappers</span><span>(</span><span>self</span><span>):</span></div>
<div><span> </span><span>"""Create the encoder and electrical wrappers (</span><span>NOTE</span><span>: wrappers use driver1 only)"""</span></div>
<div><span> </span><span>self</span><span>.encoder_wrapper </span><span>=</span><span> EncoderWrapper(</span></div>
<div><span> </span><span>self</span><span>,</span></div>
<div><span> </span><span>self</span><span>.driver,</span></div>
<div><span> </span><span>self</span><span>.TICKS_PER_METER,</span></div>
<div><span> </span><span>self</span><span>.TICKS_PER_ROTATION,</span></div>
<div><span> </span><span>self</span><span>.BASE_WIDTH,</span></div>
<div><span> </span><span>self</span><span>.PUBLISH_ODOM,</span></div>
<div><span> </span><span>self</span><span>.PUBLISH_ENCODERS,</span></div>
<div><span> </span><span>self</span><span>.PUBLISH_TF,</span></div>
<div><span> </span><span>self</span><span>.swap_motors,</span></div>
<div><span> )</span></div>
<div><span> </span><span>self</span><span>.electrical_wrapper </span><span>=</span><span> ElectricalWrapper(</span><span>self</span><span>, </span><span>self</span><span>.driver, </span><span>self</span><span>.PUBLISH_ELEC, </span><span>self</span><span>.swap_motors)</span></div>
<br />
<div><span> </span><span>def</span><span> </span><span>create_timers</span><span>(</span><span>self</span><span>):</span></div>
<div><span> </span><span>"""Create timers for the node"""</span></div>
<div><span> </span><span>if</span><span> </span><span>self</span><span>.ODOM_RATE:</span></div>
<div><span> </span><span>self</span><span>.odom_timer </span><span>=</span><span> </span><span>self</span><span>.create_timer(</span><span>1.0</span><span> </span><span>/</span><span> </span><span>self</span><span>.ODOM_RATE, </span><span>self</span><span>.encoder_wrapper.update_and_publish)</span></div>
<div><span> </span><span>if</span><span> </span><span>self</span><span>.ELEC_RATE:</span></div>
<div><span> </span><span>self</span><span>.elec_timer </span><span>=</span><span> </span><span>self</span><span>.create_timer(</span><span>1.0</span><span> </span><span>/</span><span> </span><span>self</span><span>.ELEC_RATE, </span><span>self</span><span>.electrical_wrapper.update_and_publish)</span></div>
<br />
<div><span> </span><span>self</span><span>.idle_timer </span><span>=</span><span> </span><span>self</span><span>.create_timer(</span><span>1</span><span> </span><span>/</span><span> </span><span>30</span><span>, </span><span>self</span><span>.idle_callback)</span></div>
<br />
<div><span> </span><span># NEW: make dynamic param updates optional (default False)</span></div>
<div><span> </span><span>if</span><span> </span><span>bool</span><span>(</span><span>self</span><span>.enable_dynamic_params):</span></div>
<div><span> period </span><span>=</span><span> </span><span>float</span><span>(</span><span>self</span><span>.dynamic_params_period)</span></div>
<div><span> </span><span>if</span><span> period </span><span>&lt;=</span><span> </span><span>0</span><span>:</span></div>
<div><span> period </span><span>=</span><span> </span><span>1.0</span></div>
<div><span> </span><span>self</span><span>.dynamic_params_timer </span><span>=</span><span> </span><span>self</span><span>.create_timer(period, </span><span>self</span><span>.update_parameters)</span></div>
<br />
<div><span> </span><span>def</span><span> </span><span>idle_callback</span><span>(</span><span>self</span><span>):</span></div>
<div><span> </span><span>"""Stop the robot if no commands are received for 'idle_timeout' seconds"""</span></div>
<div><span> now </span><span>=</span><span> </span><span>self</span><span>.get_clock().now().nanoseconds</span></div>
<div><span> </span><span>if</span><span> </span><span>self</span><span>.stop_if_idle </span><span>and</span><span> now </span><span>-</span><span> </span><span>self</span><span>.last_cmd_timestamp </span><span>&gt;</span><span> </span><span>self</span><span>.idle_timeout </span><span>*</span><span> </span><span>1e9</span><span>:</span></div>
<div><span> </span><span>self</span><span>.get_logger().debug(</span></div>
<div><span> </span><span>f</span><span>"Did not get command for </span><span>{self</span><span>.idle_timeout</span><span>}</span><span> second, stopping"</span></div>
<div><span> )</span></div>
<div><span> </span><span>self</span><span>.stop_motors()</span></div>
<br />
<div><span> </span><span># -----------------------------</span></div>
<div><span> </span><span># NEW: debug encoder helper</span></div>
<div><span> </span><span># -----------------------------</span></div>
<div><span> </span><span>def</span><span> </span><span>_maybe_log_encoders</span><span>(</span><span>self</span><span>):</span></div>
<div><span> </span><span>if</span><span> </span><span>not</span><span> </span><span>self</span><span>.debug_encoders:</span></div>
<div><span> </span><span>return</span></div>
<div><span> rate_hz </span><span>=</span><span> </span><span>float</span><span>(</span><span>self</span><span>.debug_encoder_rate_hz) </span><span>if</span><span> </span><span>self</span><span>.debug_encoder_rate_hz </span><span>else</span><span> </span><span>0.0</span></div>
<div><span> </span><span>if</span><span> rate_hz </span><span>&lt;=</span><span> </span><span>0</span><span>:</span></div>
<div><span> </span><span>return</span></div>
<br />
<div><span> now_ns </span><span>=</span><span> </span><span>self</span><span>.get_clock().now().nanoseconds</span></div>
<div><span> min_period_ns </span><span>=</span><span> </span><span>int</span><span>(</span><span>1e9</span><span> </span><span>/</span><span> rate_hz)</span></div>
<br />
<div><span> </span><span>if</span><span> now_ns </span><span>-</span><span> </span><span>self</span><span>._last_enc_log_ns </span><span>&lt;</span><span> min_period_ns:</span></div>
<div><span> </span><span>return</span></div>
<br />
<div><span> </span><span>self</span><span>._last_enc_log_ns </span><span>=</span><span> now_ns</span></div>
<br />
<div><span> </span><span>try</span><span>:</span></div>
<div><span> e1 </span><span>=</span><span> </span><span>self</span><span>.driver.ReadEncM1()</span></div>
<div><span> e2 </span><span>=</span><span> </span><span>self</span><span>.driver.ReadEncM2()</span></div>
<div><span> </span><span>self</span><span>.get_logger().info(</span><span>f</span><span>" board1 M1=</span><span>{</span><span>e1</span><span>}</span><span> M2=</span><span>{</span><span>e2</span><span>}</span><span>"</span><span>)</span></div>
<div><span> </span><span>except</span><span> </span><span>Exception</span><span> </span><span>as</span><span> e:</span></div>
<div><span> </span><span>self</span><span>.get_logger().warn(</span><span>f</span><span>" board1 read failed: </span><span>{</span><span>e</span><span>}</span><span>"</span><span>)</span></div>
<br />
<div><span> </span><span>if</span><span> </span><span>self</span><span>.driver2 </span><span>is</span><span> </span><span>not</span><span> </span><span>None</span><span>:</span></div>
<div><span> </span><span>try</span><span>:</span></div>
<div><span> e3 </span><span>=</span><span> </span><span>self</span><span>.driver2.ReadEncM1()</span></div>
<div><span> e4 </span><span>=</span><span> </span><span>self</span><span>.driver2.ReadEncM2()</span></div>
<div><span> </span><span>self</span><span>.get_logger().info(</span><span>f</span><span>" board2 M1=</span><span>{</span><span>e3</span><span>}</span><span> M2=</span><span>{</span><span>e4</span><span>}</span><span>"</span><span>)</span></div>
<div><span> </span><span>except</span><span> </span><span>Exception</span><span> </span><span>as</span><span> e:</span></div>
<div><span> </span><span>self</span><span>.get_logger().warn(</span><span>f</span><span>" board2 read failed: </span><span>{</span><span>e</span><span>}</span><span>"</span><span>)</span></div>
<br />
<div><span> </span><span>def</span><span> </span><span>cmd_vel_callback</span><span>(</span><span>self</span><span>, </span><span>twist</span><span>):</span></div>
<div><span> </span><span>"""Callback for /cmd_vel topic, move the robot according to the Twist message"""</span></div>
<br />
<div><span> </span><span># Limit the speed</span></div>
<div><span> linear_x </span><span>=</span><span> </span><span>min</span><span>(</span><span>max</span><span>(twist.linear.x, </span><span>-</span><span>self</span><span>.max_speed_linear), </span><span>self</span><span>.max_speed_linear)</span></div>
<div><span> angular_z </span><span>=</span><span> </span><span>min</span><span>(</span><span>max</span><span>(twist.angular.z, </span><span>-</span><span>self</span><span>.max_speed_angular), </span><span>self</span><span>.max_speed_angular)</span></div>
<br />
<div><span> </span><span>if</span><span> </span><span>abs</span><span>(linear_x) </span><span>&lt;</span><span> </span><span>0.01</span><span> </span><span>and</span><span> </span><span>abs</span><span>(angular_z) </span><span>&lt;</span><span> </span><span>0.01</span><span>:</span></div>
<div><span> </span><span>self</span><span>.stop_motors()</span></div>
<div><span> </span><span>return</span></div>
<div> </div>
<div><span> </span><span># Convert to motor speeds (ticks/s)</span></div>
<div><span> right_speed </span><span>=</span><span> </span><span>int</span><span>((linear_x </span><span>+</span><span> angular_z </span><span>*</span><span> </span><span>self</span><span>.BASE_WIDTH </span><span>/</span><span> </span><span>2</span><span>) </span><span>*</span><span> </span><span>self</span><span>.TICKS_PER_METER)</span></div>
<div><span> left_speed </span><span>=</span><span> </span><span>int</span><span>((linear_x </span><span>-</span><span> angular_z </span><span>*</span><span> </span><span>self</span><span>.BASE_WIDTH </span><span>/</span><span> </span><span>2</span><span>) </span><span>*</span><span> </span><span>self</span><span>.TICKS_PER_METER)</span></div>
<br />
<div><span> </span><span>self</span><span>.get_logger().debug(</span></div>
<div><span> </span><span>f</span><span>"Sending command -&gt; left: </span><span>{</span><span>left_speed</span><span>}</span><span>, right: </span><span>{</span><span>right_speed</span><span>}</span><span> (ticks/sec)"</span></div>
<div><span> )</span></div>
<br />
<div><span> </span><span># Send the command</span></div>
<div><span> </span><span>try</span><span>:</span></div>
<div><span> </span><span># Your preferred mapping:</span></div>
<div><span> </span><span># each board: M1=left, M2=right</span></div>
<div><span> </span><span>if</span><span> </span><span>self</span><span>.swap_motors:</span></div>
<div><span> </span><span># swap only swaps left/right assignment</span></div>
<div><span> </span><span>self</span><span>.driver.SpeedAccelM1M2(</span><span>self</span><span>.accel, right_speed, left_speed)</span></div>
<div><span> </span><span>if</span><span> </span><span>self</span><span>.driver2 </span><span>is</span><span> </span><span>not</span><span> </span><span>None</span><span>:</span></div>
<div><span> </span><span>self</span><span>.driver2.SpeedAccelM1M2(</span><span>self</span><span>.accel, right_speed, left_speed)</span></div>
<div><span> </span><span>else</span><span>:</span></div>
<div><span> </span><span>self</span><span>.driver.SpeedAccelM1M2(</span><span>self</span><span>.accel, left_speed, right_speed)</span></div>
<div><span> </span><span>if</span><span> </span><span>self</span><span>.driver2 </span><span>is</span><span> </span><span>not</span><span> </span><span>None</span><span>:</span></div>
<div><span> </span><span>self</span><span>.driver2.SpeedAccelM1M2(</span><span>self</span><span>.accel, left_speed, right_speed)</span></div>
<br />
<div><span> </span><span>self</span><span>.last_cmd_timestamp </span><span>=</span><span> </span><span>self</span><span>.get_clock().now().nanoseconds</span></div>
<br />
<div><span> </span><span># NEW: encoder debug prints (throttled)</span></div>
<div><span> </span><span>self</span><span>._maybe_log_encoders()</span></div>
<br />
<div><span> </span><span>except</span><span> </span><span>OSError</span><span> </span><span>as</span><span> e:</span></div>
<div><span> </span><span># NEW: if command fails, STOP BOTH immediately (prevents "one motor keeps running")</span></div>
<div><span> </span><span>self</span><span>.get_logger().warn(</span><span>f</span><span>"SpeedAccelM1M2 OSError: </span><span>{</span><span>str</span><span>(e)</span><span>}</span><span> -&gt; stopping motors"</span><span>)</span></div>
<div><span> </span><span>self</span><span>.stop_motors()</span></div>
<br />
<div><span> </span><span>except</span><span> </span><span>Exception</span><span> </span><span>as</span><span> e:</span></div>
<div><span> </span><span># Catch-all safety stop</span></div>
<div><span> </span><span>self</span><span>.get_logger().warn(</span><span>f</span><span>"SpeedAccelM1M2 unexpected error: </span><span>{</span><span>e</span><span>}</span><span> -&gt; stopping motors"</span><span>)</span></div>
<div><span> </span><span>self</span><span>.stop_motors()</span></div>
<br />
<div><span> </span><span>def</span><span> </span><span>diagnostics</span><span>(</span><span>self</span><span>, </span><span>stat</span><span>):</span></div>
<div><span> </span><span>"""Read the error status of the Roboclaw and report it as diagnostics"""</span></div>
<div><span> stat.summary(diagnostic_updater.DiagnosticStatus.STALE, </span><span>"No data"</span><span>)</span></div>
<br />
<div><span> </span><span>try</span><span>:</span></div>
<div><span> status </span><span>=</span><span> </span><span>self</span><span>.driver.ReadError()</span></div>
<div><span> statuses </span><span>=</span><span> u.decipher_rclaw_status(status)</span></div>
<div><span> </span><span>for</span><span> state, message </span><span>in</span><span> statuses:</span></div>
<div><span> stat.summary(state, message)</span></div>
<div><span> </span><span>self</span><span>.get_logger().debug(</span><span>"Got unit status: "</span><span> </span><span>+</span><span> </span><span>str</span><span>(</span><span>hex</span><span>(status)))</span></div>
<div><span> </span><span>except</span><span> </span><span>Exception</span><span> </span><span>as</span><span> e:</span></div>
<div><span> </span><span>self</span><span>.get_logger().warn(</span><span>"Diagnostics error: "</span><span> </span><span>+</span><span> </span><span>str</span><span>(e))</span></div>
<div><span> </span><span>self</span><span>.get_logger().debug(e)</span></div>
<div><span> </span><span>return</span></div>
<div><span> </span><span>return</span><span> stat</span></div>
<br />
<div><span> </span><span>def</span><span> </span><span>stop_motors</span><span>(</span><span>self</span><span>):</span></div>
<div><span> </span><span>"""Stop motors aggressively; handle each board independently."""</span></div>
<div><span> </span><span># Board 1</span></div>
<div><span> </span><span>try</span><span>:</span></div>
<div><span> </span><span># Immediate stop if supported by your library</span></div>
<div><span> </span><span>self</span><span>.driver.SpeedM1M2(</span><span>0</span><span>, </span><span>0</span><span>)</span></div>
<div><span> </span><span>except</span><span> </span><span>Exception</span><span>:</span></div>
<div><span> </span><span>pass</span></div>
<br />
<div><span> </span><span>try</span><span>:</span></div>
<div><span> </span><span># Ramp stop fallback</span></div>
<div><span> </span><span>self</span><span>.driver.SpeedAccelM1M2(</span><span>self</span><span>.accel, </span><span>0</span><span>, </span><span>0</span><span>)</span></div>
<div><span> </span><span>except</span><span> </span><span>Exception</span><span> </span><span>as</span><span> e:</span></div>
<div><span> </span><span>self</span><span>.get_logger().warn(</span><span>f</span><span>"Stop failed on driver1: </span><span>{</span><span>e</span><span>}</span><span>"</span><span>)</span></div>
<br />
<div><span> </span><span># Board 2</span></div>
<div><span> </span><span>if</span><span> </span><span>self</span><span>.driver2 </span><span>is</span><span> </span><span>not</span><span> </span><span>None</span><span>:</span></div>
<div><span> </span><span>try</span><span>:</span></div>
<div><span> </span><span>self</span><span>.driver2.SpeedM1M2(</span><span>0</span><span>, </span><span>0</span><span>)</span></div>
<div><span> </span><span>except</span><span> </span><span>Exception</span><span>:</span></div>
<div><span> </span><span>pass</span></div>
<br />
<div><span> </span><span>try</span><span>:</span></div>
<div><span> </span><span>self</span><span>.driver2.SpeedAccelM1M2(</span><span>self</span><span>.accel, </span><span>0</span><span>, </span><span>0</span><span>)</span></div>
<div><span> </span><span>except</span><span> </span><span>Exception</span><span> </span><span>as</span><span> e:</span></div>
<div><span> </span><span>self</span><span>.get_logger().warn(</span><span>f</span><span>"Stop failed on driver2: </span><span>{</span><span>e</span><span>}</span><span>"</span><span>)</span></div>
<br />
<div><span> </span><span>self</span><span>.last_cmd_timestamp </span><span>=</span><span> </span><span>self</span><span>.get_clock().now().nanoseconds</span></div>
<br />
<div><span> </span><span>def</span><span> </span><span>shutdown</span><span>(</span><span>self</span><span>, </span><span>str_msg</span><span>):</span></div>
<div><span> </span><span>"""Shutdown the node"""</span></div>
<div><span> </span><span>self</span><span>.get_logger().info(</span><span>"Shutting down: "</span><span> </span><span>+</span><span> str_msg)</span></div>
<div><span> </span><span>self</span><span>.stop_motors()</span></div>
<div><span> </span><span>self</span><span>.stop_motors() </span><span># Call twice to make sure</span></div>
<br /><br />
<div><span>def</span><span> </span><span>main</span><span>(</span><span>args</span><span>=</span><span>None</span><span>):</span></div>
<div><span> rclpy.init(</span><span>args</span><span>=</span><span>args)</span></div>
<br />
<div><span> roboclaw_node </span><span>=</span><span> RoboclawNode()</span></div>
<div><span> rclpy.spin(roboclaw_node)</span></div>
<div><span> roboclaw_node.destroy_node()</span></div>
<div><span> rclpy.shutdown()</span></div>
<br /><br />
<div><span>if</span><span> </span><span>__name__</span><span> </span><span>==</span><span> </span><span>"__main__"</span><span>:</span></div>
<div><span> main()</span></div>
</div>]]></content:encoded>
						                            <category domain="https://robartx.tech/community/"></category>                        <dc:creator>eunsun</dc:creator>
                        <guid isPermaLink="true">https://robartx.tech/community/robocall/code/#post-36</guid>
                    </item>
				                    <item>
                        <title>RE: code</title>
                        <link>https://robartx.tech/community/robocall/code/#post-35</link>
                        <pubDate>Fri, 27 Feb 2026 19:29:11 +0000</pubDate>
                        <description><![CDATA[encoder_wrapper.py
&nbsp;

f

from math import cos, pi, sin, fmod

from tcr_roboclaw import Roboclaw
from rclpy.node import Node
from geometry_msgs.msg import Quaternion, TransformS...]]></description>
                        <content:encoded><![CDATA[<p>encoder_wrapper.py</p>
<p>&nbsp;</p>
<div>
<div><span><span>f</span></span>
<div>
<div><span>from</span><span> math </span><span>import</span><span> cos, pi, sin, fmod</span></div>
<br />
<div><span>from</span><span> tcr_roboclaw </span><span>import</span><span> Roboclaw</span></div>
<div><span>from</span><span> rclpy.node </span><span>import</span><span> Node</span></div>
<div><span>from</span><span> geometry_msgs.msg </span><span>import</span><span> Quaternion, TransformStamped</span></div>
<div><span>from</span><span> nav_msgs.msg </span><span>import</span><span> Odometry</span></div>
<div><span>from</span><span> roboclaw_msgs.msg </span><span>import</span><span> EncoderState</span></div>
<div><span>from</span><span> tf_transformations </span><span>import</span><span> quaternion_from_euler</span></div>
<div><span>from</span><span> tf2_ros </span><span>import</span><span> TransformBroadcaster</span></div>
<br />
<div><span>COUNTER_MAX </span><span>=</span><span> </span><span>2</span><span>**</span><span>32</span></div>
<br /><br />
<div><span>class</span><span> </span><span>EncoderWrapper</span><span>:</span></div>
<br />
<div><span> </span><span>def</span><span> </span><span>__init__</span><span>(</span></div>
<div><span> </span><span>self</span><span>,</span></div>
<div><span> </span><span>node</span><span>: Node,</span></div>
<div><span> </span><span>driver</span><span>: Roboclaw,</span></div>
<div><span> </span><span>ticks_per_meter</span><span>: </span><span>float</span><span>,</span></div>
<div><span> </span><span>ticks_per_rotation</span><span>: </span><span>float</span><span>,</span></div>
<div><span> </span><span>base_width</span><span>: </span><span>float</span><span>,</span></div>
<div><span> </span><span>pub_odom</span><span>: </span><span>bool</span><span>,</span></div>
<div><span> </span><span>pub_encoders</span><span>: </span><span>bool</span><span>,</span></div>
<div><span> </span><span>pub_tf</span><span>: </span><span>bool</span><span>,</span></div>
<div><span> </span><span>swap_motors</span><span>: </span><span>bool</span><span>,</span></div>
<div><span> ):</span></div>
<div><span> </span><span>"""Encoder Wrapper</span></div>
<br />
<div><span> Args:</span></div>
<div><span> node (rclpy.node.Node): ROS 2 node</span></div>
<div><span> driver (tcr_roboclaw.Roboclaw): Roboclaw driver</span></div>
<div><span> ticks_per_meter (float): Encoder ticks per meter</span></div>
<div><span> ticks_per_rotation (float): Encoder ticks per rotation</span></div>
<div><span> base_width (float): Distance between the two wheels</span></div>
<div><span> pub_odom (bool): Publish odometry data</span></div>
<div><span> pub_encoders (bool): Publish encoder data</span></div>
<div><span> pub_tf (bool): Publish the transform from odom to base_link</span></div>
<div><span> swap_motors (bool): Swap the left and right motors</span></div>
<div><span> """</span></div>
<div><span> </span><span>self</span><span>.TICKS_PER_METER </span><span>=</span><span> ticks_per_meter</span></div>
<div><span> </span><span>self</span><span>.TICKS_PER_ROTATION </span><span>=</span><span> ticks_per_rotation</span></div>
<div><span> </span><span>self</span><span>.BASE_WIDTH </span><span>=</span><span> base_width</span></div>
<div><span> </span><span>self</span><span>.driver </span><span>=</span><span> driver</span></div>
<br />
<div><span> </span><span># Node parameters</span></div>
<div><span> </span><span>self</span><span>.node </span><span>=</span><span> node</span></div>
<div><span> </span><span>self</span><span>.clock </span><span>=</span><span> </span><span>self</span><span>.node.get_clock()</span></div>
<div><span> </span><span>self</span><span>.logger </span><span>=</span><span> </span><span>self</span><span>.node.get_logger()</span></div>
<div><span> </span><span>self</span><span>.PUB_ODOM </span><span>=</span><span> pub_odom</span></div>
<div><span> </span><span>self</span><span>.PUB_ENCODERS </span><span>=</span><span> pub_encoders</span></div>
<div><span> </span><span>self</span><span>.PUB_TF </span><span>=</span><span> pub_tf</span></div>
<div><span> </span><span>self</span><span>.swap_motors </span><span>=</span><span> swap_motors</span></div>
<br />
<div><span> </span><span># Encoder data</span></div>
<div><span> </span><span>self</span><span>.timestamp </span><span>=</span><span> </span><span>self</span><span>.clock.now()</span></div>
<div><span> </span><span>self</span><span>.left_ticks </span><span>=</span><span> </span></div>
<div><span> </span><span>self</span><span>.right_ticks </span><span>=</span><span> </span></div>
<div><span> </span><span>self</span><span>.left_velocity </span><span>=</span><span> </span><span>0</span></div>
<div><span> </span><span>self</span><span>.right_velocity </span><span>=</span><span> </span><span>0</span></div>
<div><span> </span><span>self</span><span>.poll_encoders()</span></div>
<br />
<div><span> </span><span># Odometry data</span></div>
<div><span> </span><span>self</span><span>.pose_x </span><span>=</span><span> </span><span>0</span></div>
<div><span> </span><span>self</span><span>.pose_y </span><span>=</span><span> </span><span>0</span></div>
<div><span> </span><span>self</span><span>.pose_theta </span><span>=</span><span> </span><span>0.0</span></div>
<div><span> </span><span>self</span><span>.vel_x </span><span>=</span><span> </span><span>0.0</span></div>
<div><span> </span><span>self</span><span>.vel_theta </span><span>=</span><span> </span><span>0.0</span></div>
<br />
<div><span> </span><span># Publishers</span></div>
<div><span> </span><span># </span><span>TODO</span><span>: Topics should be parameters</span></div>
<div><span> </span><span>self</span><span>.left_encoder_pub </span><span>=</span><span> </span><span>self</span><span>.node.create_publisher(</span></div>
<div><span> EncoderState,</span></div>
<div><span> </span><span>"/motors/left/encoder"</span><span>,</span></div>
<div><span> </span><span>10</span><span>,</span></div>
<div><span> )</span></div>
<div><span> </span><span>self</span><span>.right_encoder_pub </span><span>=</span><span> </span><span>self</span><span>.node.create_publisher(</span></div>
<div><span> EncoderState,</span></div>
<div><span> </span><span>"/motors/right/encoder"</span><span>,</span></div>
<div><span> </span><span>10</span><span>,</span></div>
<div><span> )</span></div>
<div><span> </span><span>self</span><span>.odom_pub </span><span>=</span><span> </span><span>self</span><span>.node.create_publisher(Odometry, </span><span>"/motors/odometry"</span><span>, </span><span>10</span><span>)</span></div>
<div><span> </span><span>self</span><span>.tf_broadcaster </span><span>=</span><span> TransformBroadcaster(</span><span>self</span><span>.node)</span></div>
<br />
<div><span> </span><span>@</span><span>staticmethod</span></div>
<div><span> </span><span>def</span><span> </span><span>normalize_angle</span><span>(</span><span>angle</span><span>):</span></div>
<div><span> </span><span>return</span><span> fmod(angle </span><span>+</span><span> pi, </span><span>2.0</span><span> </span><span>*</span><span> pi) </span><span>-</span><span> pi</span></div>
<br />
<div><span> </span><span>def</span><span> </span><span>update_and_publish</span><span>(</span><span>self</span><span>):</span></div>
<div><span> </span><span>"""Update the odometry and publish the data"""</span></div>
<br />
<div><span> </span><span>if</span><span> </span><span>self</span><span>.poll_encoders():</span></div>
<div><span> </span><span>self</span><span>.update_odometry()</span></div>
<div><span> </span><span>if</span><span> </span><span>self</span><span>.PUB_ODOM:</span></div>
<div><span> </span><span>self</span><span>.publish_odometry()</span></div>
<div><span> </span><span>if</span><span> </span><span>self</span><span>.PUB_ENCODERS:</span></div>
<div><span> </span><span>self</span><span>.publish_encoder_data()</span></div>
<div><span> </span><span>if</span><span> </span><span>self</span><span>.PUB_TF:</span></div>
<div><span> </span><span>self</span><span>.broadcast_tf()</span></div>
<div><span> </span><span>else</span><span>:</span></div>
<div><span> </span><span>self</span><span>.logger.warn(</span><span>"Failed to poll encoders."</span><span>)</span></div>
<br />
<div><span> </span><span>def</span><span> </span><span>poll_encoders</span><span>(</span><span>self</span><span>):</span></div>
<div><span> </span><span>"""Poll the encoder data from the hardware"""</span></div>
<br />
<div><span> status_enc, status_speed </span><span>=</span><span> </span><span>0</span><span>, </span><span>0</span></div>
<br />
<div><span> </span><span>try</span><span>:</span></div>
<div><span> </span><span>self</span><span>.timestamp </span><span>=</span><span> </span><span>self</span><span>.clock.now()</span></div>
<div><span> status_enc, ticks1, ticks2 </span><span>=</span><span> </span><span>self</span><span>.driver.GetEncoderCounters()</span></div>
<div><span> status_speed, speed1, speed2 </span><span>=</span><span> </span><span>self</span><span>.driver.GetMotorAverageSpeeds()</span></div>
<div><span> </span><span>except</span><span> </span><span>Exception</span><span> </span><span>as</span><span> e:</span></div>
<div><span> </span><span>self</span><span>.logger.warn(</span><span>f</span><span>"Read encoders error: </span><span>{</span><span>str</span><span>(e)</span><span>}</span><span>"</span><span>)</span></div>
<br />
<div><span> </span><span># Update the encoder ticks</span></div>
<div><span> </span><span>if</span><span> status_enc </span><span>==</span><span> </span><span>1</span><span>:</span></div>
<div><span> </span><span>if</span><span> </span><span>self</span><span>.swap_motors:</span></div>
<div><span> </span><span>self</span><span>.right_ticks </span><span>=</span><span> [</span><span>self</span><span>.right_ticks, ticks1]</span></div>
<div><span> </span><span>self</span><span>.left_ticks </span><span>=</span><span> [</span><span>self</span><span>.left_ticks, ticks2]</span></div>
<div><span> </span><span>else</span><span>:</span></div>
<div><span> </span><span>self</span><span>.right_ticks </span><span>=</span><span> [</span><span>self</span><span>.right_ticks, ticks2]</span></div>
<div><span> </span><span>self</span><span>.left_ticks </span><span>=</span><span> [</span><span>self</span><span>.left_ticks, ticks1]</span></div>
<br />
<div><span> </span><span># Update the motor velocities</span></div>
<div><span> </span><span>if</span><span> status_speed </span><span>==</span><span> </span><span>1</span><span>:</span></div>
<div><span> </span><span>if</span><span> </span><span>self</span><span>.swap_motors:</span></div>
<div><span> </span><span>self</span><span>.right_velocity </span><span>=</span><span> speed1</span></div>
<div><span> </span><span>self</span><span>.left_velocity </span><span>=</span><span> speed2</span></div>
<div><span> </span><span>else</span><span>:</span></div>
<div><span> </span><span>self</span><span>.right_velocity </span><span>=</span><span> speed2</span></div>
<div><span> </span><span>self</span><span>.left_velocity </span><span>=</span><span> speed1</span></div>
<br />
<div><span> </span><span>return</span><span> status_enc </span><span>and</span><span> status_speed</span></div>
<br />
<div><span> </span><span>def</span><span> </span><span>update_odometry</span><span>(</span><span>self</span><span>):</span></div>
<div><span> </span><span>"""Update the odometry estimation"""</span></div>
<br />
<div><span> </span><span># Compute the ticks difference (handle overflow)</span></div>
<div><span> delta_ticks_left </span><span>=</span><span> </span><span>self</span><span>.left_ticks </span><span>-</span><span> </span><span>self</span><span>.left_ticks</span></div>
<div><span> delta_ticks_right </span><span>=</span><span> </span><span>self</span><span>.right_ticks </span><span>-</span><span> </span><span>self</span><span>.right_ticks</span></div>
<br />
<div><span> </span><span># Handle overflow and underflow</span></div>
<div><span> </span><span>if</span><span> delta_ticks_left </span><span>&lt;</span><span> </span><span>-</span><span>COUNTER_MAX </span><span>/</span><span> </span><span>2</span><span>:</span></div>
<div><span> delta_ticks_left </span><span>+=</span><span> COUNTER_MAX</span></div>
<div><span> </span><span>elif</span><span> delta_ticks_left </span><span>&gt;</span><span> COUNTER_MAX </span><span>/</span><span> </span><span>2</span><span>:</span></div>
<div><span> delta_ticks_left </span><span>-=</span><span> COUNTER_MAX</span></div>
<div><span> </span><span>if</span><span> delta_ticks_right </span><span>&lt;</span><span> </span><span>-</span><span>COUNTER_MAX </span><span>/</span><span> </span><span>2</span><span>:</span></div>
<div><span> delta_ticks_right </span><span>+=</span><span> COUNTER_MAX</span></div>
<div><span> </span><span>elif</span><span> delta_ticks_right </span><span>&gt;</span><span> COUNTER_MAX </span><span>/</span><span> </span><span>2</span><span>:</span></div>
<div><span> delta_ticks_right </span><span>-=</span><span> COUNTER_MAX</span></div>
<br />
<div><span> </span><span># Compute the traveled distance</span></div>
<div><span> dist_left </span><span>=</span><span> delta_ticks_left </span><span>/</span><span> </span><span>self</span><span>.TICKS_PER_METER</span></div>
<div><span> dist_right </span><span>=</span><span> delta_ticks_right </span><span>/</span><span> </span><span>self</span><span>.TICKS_PER_METER</span></div>
<div><span> dist_center </span><span>=</span><span> (dist_right </span><span>+</span><span> dist_left) </span><span>/</span><span> </span><span>2.0</span></div>
<br />
<div><span> </span><span># Compute the displacement</span></div>
<div><span> d_theta </span><span>=</span><span> (dist_right </span><span>-</span><span> dist_left) </span><span>/</span><span> </span><span>self</span><span>.BASE_WIDTH</span></div>
<div><span> d_x </span><span>=</span><span> dist_center </span><span>*</span><span> cos(</span><span>self</span><span>.pose_theta </span><span>+</span><span> d_theta </span><span>/</span><span> </span><span>2.0</span><span>)</span></div>
<div><span> d_y </span><span>=</span><span> dist_center </span><span>*</span><span> sin(</span><span>self</span><span>.pose_theta </span><span>+</span><span> d_theta </span><span>/</span><span> </span><span>2.0</span><span>)</span></div>
<br />
<div><span> </span><span># Update the pose</span></div>
<div><span> </span><span>self</span><span>.pose_x </span><span>+=</span><span> d_x</span></div>
<div><span> </span><span>self</span><span>.pose_y </span><span>+=</span><span> d_y</span></div>
<div><span> </span><span>self</span><span>.pose_theta </span><span>=</span><span> </span><span>self</span><span>.normalize_angle(</span><span>self</span><span>.pose_theta </span><span>+</span><span> d_theta)</span></div>
<br />
<div><span> </span><span># Compute the linear and angular velocities</span></div>
<div><span> vel_left </span><span>=</span><span> </span><span>self</span><span>.left_velocity </span><span>/</span><span> </span><span>self</span><span>.TICKS_PER_METER</span></div>
<div><span> vel_right </span><span>=</span><span> </span><span>self</span><span>.right_velocity </span><span>/</span><span> </span><span>self</span><span>.TICKS_PER_METER</span></div>
<div><span> </span><span>self</span><span>.vel_x </span><span>=</span><span> (vel_left </span><span>+</span><span> vel_right) </span><span>/</span><span> </span><span>2.0</span></div>
<div><span> </span><span>self</span><span>.vel_theta </span><span>=</span><span> (vel_right </span><span>-</span><span> vel_left) </span><span>/</span><span> </span><span>self</span><span>.BASE_WIDTH</span></div>
<br />
<div><span> </span><span>def</span><span> </span><span>publish_odometry</span><span>(</span><span>self</span><span>):</span></div>
<div><span> </span><span>"""Publish odometry data to the ROS network"""</span></div>
<br />
<div><span> odom </span><span>=</span><span> Odometry()</span></div>
<div><span> odom.header.stamp </span><span>=</span><span> </span><span>self</span><span>.timestamp.to_msg()</span></div>
<div><span> odom.header.frame_id </span><span>=</span><span> </span><span>"odom"</span></div>
<div><span> odom.child_frame_id </span><span>=</span><span> </span><span>"base_link"</span></div>
<br />
<div><span> odom.pose.pose.position.x </span><span>=</span><span> </span><span>self</span><span>.pose_x</span></div>
<div><span> odom.pose.pose.position.y </span><span>=</span><span> </span><span>self</span><span>.pose_y</span></div>
<div><span> odom.pose.pose.position.z </span><span>=</span><span> </span><span>0.0</span></div>
<br />
<div><span> quat </span><span>=</span><span> quaternion_from_euler(</span><span>0</span><span>, </span><span>0</span><span>, </span><span>self</span><span>.pose_theta)</span></div>
<div><span> odom.pose.pose.orientation </span><span>=</span><span> Quaternion(</span></div>
<div><span> </span><span>x</span><span>=</span><span>quat,</span></div>
<div><span> </span><span>y</span><span>=</span><span>quat,</span></div>
<div><span> </span><span>z</span><span>=</span><span>quat,</span></div>
<div><span> </span><span>w</span><span>=</span><span>quat,</span></div>
<div><span> )</span></div>
<br />
<div><span> odom.twist.twist.linear.x </span><span>=</span><span> </span><span>self</span><span>.vel_x</span></div>
<div><span> odom.twist.twist.linear.y </span><span>=</span><span> </span><span>0.0</span></div>
<div><span> odom.twist.twist.angular.z </span><span>=</span><span> </span><span>self</span><span>.vel_theta</span></div>
<br />
<div><span> </span><span>self</span><span>.odom_pub.publish(odom)</span></div>
<br />
<div><span> </span><span>def</span><span> </span><span>publish_encoder_data</span><span>(</span><span>self</span><span>):</span></div>
<div><span> </span><span>"""Publish the encoder data to the ROS network"""</span></div>
<br />
<div><span> encoder_state </span><span>=</span><span> EncoderState()</span></div>
<div><span> encoder_state.header.stamp </span><span>=</span><span> </span><span>self</span><span>.timestamp.to_msg()</span></div>
<div><span> encoder_state.header.frame_id </span><span>=</span><span> </span><span>"base_link"</span></div>
<div><span> encoder_state.ticks_per_rotation </span><span>=</span><span> </span><span>float</span><span>(</span><span>self</span><span>.TICKS_PER_ROTATION)</span></div>
<div><span> encoder_state.ticks_per_meter </span><span>=</span><span> </span><span>float</span><span>(</span><span>self</span><span>.TICKS_PER_METER)</span></div>
<br />
<div><span> encoder_state.position </span><span>=</span><span> </span><span>self</span><span>.left_ticks</span></div>
<div><span> encoder_state.velocity </span><span>=</span><span> </span><span>float</span><span>(</span><span>self</span><span>.left_velocity)</span></div>
<div><span> </span><span>self</span><span>.left_encoder_pub.publish(encoder_state)</span></div>
<br />
<div><span> encoder_state.position </span><span>=</span><span> </span><span>self</span><span>.right_ticks</span></div>
<div><span> encoder_state.velocity </span><span>=</span><span> </span><span>float</span><span>(</span><span>self</span><span>.right_velocity)</span></div>
<div><span> </span><span>self</span><span>.right_encoder_pub.publish(encoder_state)</span></div>
<br />
<div><span> </span><span>def</span><span> </span><span>broadcast_tf</span><span>(</span><span>self</span><span>):</span></div>
<div><span> </span><span>"""Broadcast the transform from odom to base_link"""</span></div>
<br />
<div><span> t </span><span>=</span><span> TransformStamped()</span></div>
<div><span> t.header.stamp </span><span>=</span><span> </span><span>self</span><span>.timestamp.to_msg()</span></div>
<div><span> t.header.frame_id </span><span>=</span><span> </span><span>"odom"</span></div>
<div><span> t.child_frame_id </span><span>=</span><span> </span><span>"base_link"</span></div>
<br />
<div><span> t.transform.translation.x </span><span>=</span><span> </span><span>self</span><span>.pose_x</span></div>
<div><span> t.transform.translation.y </span><span>=</span><span> </span><span>self</span><span>.pose_y</span></div>
<div><span> t.transform.translation.z </span><span>=</span><span> </span><span>0.0</span></div>
<br />
<div><span> quat </span><span>=</span><span> quaternion_from_euler(</span><span>0</span><span>, </span><span>0</span><span>, </span><span>self</span><span>.pose_theta)</span></div>
<div><span> t.transform.rotation </span><span>=</span><span> Quaternion(</span><span>x</span><span>=</span><span>quat, </span><span>y</span><span>=</span><span>quat, </span><span>z</span><span>=</span><span>quat, </span><span>w</span><span>=</span><span>quat)</span></div>
<br />
<div><span> </span><span>self</span><span>.tf_broadcaster.sendTransform(t)</span></div>
</div>
</div>
</div>]]></content:encoded>
						                            <category domain="https://robartx.tech/community/"></category>                        <dc:creator>eunsun</dc:creator>
                        <guid isPermaLink="true">https://robartx.tech/community/robocall/code/#post-35</guid>
                    </item>
				                    <item>
                        <title>code</title>
                        <link>https://robartx.tech/community/robocall/code/#post-34</link>
                        <pubDate>Fri, 27 Feb 2026 19:26:46 +0000</pubDate>
                        <description><![CDATA[from roboclaw_msgs.msg import MotorState
from tcr_roboclaw import Roboclaw
from rclpy.node import Node
from copy import deepcopy

class ElectricalWrapper:

 def __init__(self, node: N...]]></description>
                        <content:encoded><![CDATA[<div>
<div><span>from</span><span> roboclaw_msgs.msg </span><span>import</span><span> MotorState</span></div>
<div><span>from</span><span> tcr_roboclaw </span><span>import</span><span> Roboclaw</span></div>
<div><span>from</span><span> rclpy.node </span><span>import</span><span> Node</span></div>
<div><span>from</span><span> copy </span><span>import</span><span> deepcopy</span></div>
<br /><br />
<div><span>class</span><span> </span><span>ElectricalWrapper</span><span>:</span></div>
<br />
<div><span> </span><span>def</span><span> </span><span>__init__</span><span>(</span><span>self</span><span>, </span><span>node</span><span>: Node, </span><span>driver</span><span>: Roboclaw, </span><span>pub_elec</span><span>: </span><span>bool</span><span>, </span><span>swap_motors</span><span>: </span><span>bool</span><span>):</span></div>
<div><span> </span><span>"""Electrical Wrapper</span></div>
<br />
<div><span> Args:</span></div>
<div><span> node (rclpy.node.Node): ROS 2 node</span></div>
<div><span> driver (tcr_roboclaw.Roboclaw): RoboClaw driver</span></div>
<div><span> pub_elec (bool): Publish electrical data</span></div>
<div><span> swap_motors (bool): Swap the left and right motors</span></div>
<div><span> """</span></div>
<br />
<div><span> </span><span># Node parameters</span></div>
<div><span> </span><span>self</span><span>.driver </span><span>=</span><span> driver</span></div>
<div><span> </span><span>self</span><span>.node </span><span>=</span><span> node</span></div>
<div><span> </span><span>self</span><span>.clock </span><span>=</span><span> </span><span>self</span><span>.node.get_clock()</span></div>
<div><span> </span><span>self</span><span>.logger </span><span>=</span><span> </span><span>self</span><span>.node.get_logger()</span></div>
<div><span> </span><span>self</span><span>.PUB_ELEC </span><span>=</span><span> pub_elec</span></div>
<div><span> </span><span>self</span><span>.swap_motors </span><span>=</span><span> swap_motors</span></div>
<br />
<div><span> </span><span># Electrical data</span></div>
<div><span> </span><span>self</span><span>.timestamp </span><span>=</span><span> </span><span>self</span><span>.clock.now()</span></div>
<div><span> </span><span>self</span><span>.voltage </span><span>=</span><span> </span><span>0.0</span></div>
<div><span> </span><span>self</span><span>.currents </span><span>=</span><span> </span></div>
<div><span> </span><span>self</span><span>.temperature </span><span>=</span><span> </span><span>0.0</span></div>
<div><span> </span><span>self</span><span>.duty_cycles </span><span>=</span><span> </span></div>
<div><span> </span><span>self</span><span>.poll_electrical_data()</span></div>
<br />
<div><span> </span><span># Publishers</span></div>
<div><span> </span><span>self</span><span>.left_elec_pub </span><span>=</span><span> </span><span>self</span><span>.node.create_publisher(</span></div>
<div><span> MotorState,</span></div>
<div><span> </span><span>"/motors/left/electrical"</span><span>,</span></div>
<div><span> </span><span>1</span><span>,</span></div>
<div><span> )</span></div>
<div><span> </span><span>self</span><span>.right_elec_pub </span><span>=</span><span> </span><span>self</span><span>.node.create_publisher(</span></div>
<div><span> MotorState,</span></div>
<div><span> </span><span>"/motors/right/electrical"</span><span>,</span></div>
<div><span> </span><span>1</span><span>,</span></div>
<div><span> )</span></div>
<br />
<div><span> </span><span>def</span><span> </span><span>update_and_publish</span><span>(</span><span>self</span><span>):</span></div>
<div><span> </span><span>"""Update and publish electrical data"""</span></div>
<br />
<div><span> </span><span>if</span><span> </span><span>self</span><span>.poll_electrical_data():</span></div>
<div><span> </span><span>if</span><span> </span><span>self</span><span>.PUB_ELEC:</span></div>
<div><span> </span><span>self</span><span>.publish_elec()</span></div>
<div><span> </span><span>else</span><span>:</span></div>
<div><span> </span><span>self</span><span>.logger.warn(</span><span>"Failed to poll electrical data."</span><span>)</span></div>
<br />
<div><span> </span><span>def</span><span> </span><span>poll_electrical_data</span><span>(</span><span>self</span><span>):</span></div>
<div><span> </span><span>"""Poll electrical data from the Roboclaw"""</span></div>
<br />
<div><span> status1, status2, status3, status4 </span><span>=</span><span> </span><span>0</span><span>, </span><span>0</span><span>, </span><span>0</span><span>, </span><span>0</span></div>
<br />
<div><span> </span><span>try</span><span>:</span></div>
<div><span> </span><span>self</span><span>.timestamp </span><span>=</span><span> </span><span>self</span><span>.clock.now()</span></div>
<div><span> status1, </span><span>*</span><span>currents </span><span>=</span><span> </span><span>self</span><span>.driver.ReadCurrents()</span></div>
<div><span> status2, voltage </span><span>=</span><span> </span><span>self</span><span>.driver.ReadMainBatteryVoltage()</span></div>
<div><span> status3, </span><span>*</span><span>pwms </span><span>=</span><span> </span><span>self</span><span>.driver.ReadPWMs()</span></div>
<div><span> status4, temp </span><span>=</span><span> </span><span>self</span><span>.driver.ReadTemp()</span></div>
<div><span> </span><span>except</span><span> </span><span>Exception</span><span> </span><span>as</span><span> e:</span></div>
<div><span> </span><span>self</span><span>.logger.warn(</span><span>f</span><span>"Read electrical data error: </span><span>{</span><span>str</span><span>(e)</span><span>}</span><span>"</span><span>)</span></div>
<br />
<div><span> </span><span>if</span><span> status1 </span><span>==</span><span> </span><span>1</span><span>:</span></div>
<div><span> </span><span>self</span><span>.currents </span><span>=</span><span> </span></div>
<div><span> </span><span>if</span><span> status2 </span><span>==</span><span> </span><span>1</span><span>:</span></div>
<div><span> </span><span>self</span><span>.voltage </span><span>=</span><span> voltage </span><span>/</span><span> </span><span>10</span></div>
<div><span> </span><span>if</span><span> status3 </span><span>==</span><span> </span><span>1</span><span>:</span></div>
<div><span> </span><span>self</span><span>.duty_cycles </span><span>=</span><span> </span></div>
<div><span> </span><span>if</span><span> status4 </span><span>==</span><span> </span><span>1</span><span>:</span></div>
<div><span> </span><span>self</span><span>.temperature </span><span>=</span><span> temp </span><span>/</span><span> </span><span>10</span></div>
<br />
<div><span> </span><span>return</span><span> status1 </span><span>and</span><span> status2 </span><span>and</span><span> status3 </span><span>and</span><span> status4</span></div>
<br />
<div><span> </span><span>def</span><span> </span><span>publish_elec</span><span>(</span><span>self</span><span>):</span></div>
<div><span> </span><span>"""Publish electrical data in two MotorState messages"""</span></div>
<br />
<div><span> motor_state </span><span>=</span><span> MotorState()</span></div>
<div><span> motor_state.header.stamp </span><span>=</span><span> </span><span>self</span><span>.timestamp.to_msg()</span></div>
<div><span> motor_state.header.frame_id </span><span>=</span><span> </span><span>"base_link"</span></div>
<div><span> motor_state.voltage </span><span>=</span><span> </span><span>self</span><span>.voltage</span></div>
<div><span> motor_state.temperature </span><span>=</span><span> </span><span>self</span><span>.temperature</span></div>
<br />
<div><span> motor_state1, motor_state2 </span><span>=</span><span> deepcopy(motor_state), deepcopy(motor_state)</span></div>
<br />
<div><span> motor_state1.current </span><span>=</span><span> </span><span>self</span><span>.currents</span></div>
<div><span> motor_state1.duty </span><span>=</span><span> </span><span>self</span><span>.duty_cycles</span></div>
<div><span> motor_state2.current </span><span>=</span><span> </span><span>self</span><span>.currents</span></div>
<div><span> motor_state2.duty </span><span>=</span><span> </span><span>self</span><span>.duty_cycles</span></div>
<br />
<div><span> </span><span>if</span><span> </span><span>self</span><span>.swap_motors:</span></div>
<div><span> </span><span>self</span><span>.right_elec_pub.publish(motor_state2)</span></div>
<div><span> </span><span>self</span><span>.left_elec_pub.publish(motor_state1)</span></div>
<div><span> </span><span>else</span><span>:</span></div>
<div><span> </span><span>self</span><span>.right_elec_pub.publish(motor_state1)</span></div>
<div><span> </span><span>self</span><span>.left_elec_pub.publish(motor_state2)</span></div>
</div>]]></content:encoded>
						                            <category domain="https://robartx.tech/community/"></category>                        <dc:creator>eunsun</dc:creator>
                        <guid isPermaLink="true">https://robartx.tech/community/robocall/code/#post-34</guid>
                    </item>
				                    <item>
                        <title>RE: Get ROS2 Jazzy docker image on Jetson</title>
                        <link>https://robartx.tech/community/docker-image-ros2-jazzy-on-jetson/get-ros2-jazzy/#post-31</link>
                        <pubDate>Fri, 27 Feb 2026 18:51:02 +0000</pubDate>
                        <description><![CDATA[In Local terminal 
&nbsp;
xhost +local:root
&nbsp;
Docker image start
&nbsp;
sudo docker start -ai robot_10_jazzy
&nbsp;
sudo docker exec -it robot_10_jazzy bash
&nbsp;
source /opt...]]></description>
                        <content:encoded><![CDATA[<p><b>In Local terminal </b></p>
<p>&nbsp;</p>
<p><span style="font-weight: 400">xhost +local:root</span></p>
<p>&nbsp;</p>
<p><b>Docker image start</b></p>
<p>&nbsp;</p>
<p><span style="font-weight: 400">sudo docker start -ai robot_10_jazzy</span></p>
<p>&nbsp;</p>
<p><span style="font-weight: 400">sudo docker exec -it robot_10_jazzy bash</span></p>
<p>&nbsp;</p>
<p><span style="font-weight: 400">source /opt/ros/jazzy/install/setup.bash</span></p>
<p><span style="font-weight: 400">source /ws_big_robot/ws_big_robot/install/setup.bash</span></p>
<p>&nbsp;</p>]]></content:encoded>
						                            <category domain="https://robartx.tech/community/"></category>                        <dc:creator>eunsun</dc:creator>
                        <guid isPermaLink="true">https://robartx.tech/community/docker-image-ros2-jazzy-on-jetson/get-ros2-jazzy/#post-31</guid>
                    </item>
				                    <item>
                        <title>Connecting LIDAR while not disconnecting the Roboclaw</title>
                        <link>https://robartx.tech/community/robocall/connecting-lidar-while-not-disconnecting-the-roboclaw/#post-30</link>
                        <pubDate>Fri, 20 Feb 2026 19:05:35 +0000</pubDate>
                        <description><![CDATA[When we first connected the LIDAR to Jetson through USB-A (serial), the motors stopped moving and teleop stopped working because the connection of the LIDAR suspended the connection of the o...]]></description>
                        <content:encoded><![CDATA[<p>When we first connected the LIDAR to Jetson through USB-A (serial), the motors stopped moving and teleop stopped working because the connection of the LIDAR suspended the connection of the other USBs. The connection activity can be seen by doing the following commands:</p>
<pre contenteditable="false">ls -l /dev/ttyUSB* /dev/ttyACM* /dev/serial/by-id/ 2&gt;/dev/null
dmesg | tail -n 80</pre>
<p>&nbsp;</p>
<p>The problem is fixed by simply disabling the USB autosuspend on Jetson, by running the following command:</p>
<pre contenteditable="false">sudo bash -c 'echo -1 &gt; /sys/module/usbcore/parameters/autosuspend'</pre>]]></content:encoded>
						                            <category domain="https://robartx.tech/community/"></category>                        <dc:creator>qiancai</dc:creator>
                        <guid isPermaLink="true">https://robartx.tech/community/robocall/connecting-lidar-while-not-disconnecting-the-roboclaw/#post-30</guid>
                    </item>
				                    <item>
                        <title>Battery</title>
                        <link>https://robartx.tech/community/main-forum/battery/#post-29</link>
                        <pubDate>Sat, 14 Feb 2026 02:55:19 +0000</pubDate>
                        <description><![CDATA[Battery Size Estimation Calculations:Battery specs depend on a few factors including power consumption of the robot, runtime of the robot, and continuous current draw of the robot. Based on ...]]></description>
                        <content:encoded><![CDATA[<p><span style="font-size: 14pt"><strong>Battery Size Estimation Calculations:</strong></span><br />Battery specs depend on a few factors including power consumption of the robot, runtime of the robot, and continuous current draw of the robot. Based on preliminary estimates, power consumption seems to lie around 150W and current draw seems to be around 8A-12A (mostly dependent on the current draw of the Jetson Orin Nano). We aim to run the robot for around 5 hours, however, this increases the amp-hour capacity we need for the battery. Below are some calculations showing how runtime affects battery capacity as well as links to an excel spreadsheet and google doc containing everything I've done so far:</p>
<table dir="ltr" border="1" cellspacing="0" cellpadding="0" data-sheets-root="1" data-sheets-baot="1"><colgroup><col width="100" /><col width="100" /><col width="100" /></colgroup>
<tbody>
<tr>
<td>Runtime (hours)</td>
<td>Watt-Hours (Wh)</td>
<td>Amp-Hours (Ah)</td>
</tr>
<tr>
<td>2</td>
<td>268.8</td>
<td>22.4</td>
</tr>
<tr>
<td>3</td>
<td>403.2</td>
<td>33.6</td>
</tr>
<tr>
<td>4</td>
<td>537.6</td>
<td>44.8</td>
</tr>
<tr>
<td>5</td>
<td>672</td>
<td>56</td>
</tr>
</tbody>
</table>
<p>As you can see, we would need around 50Ah to run the robot for 5 hours continuously, and only 33Ah to run it for 3 hours. It is important to note that these numbers assume the robot is consuming 150W continuously which may not be the case.</p>
<p><strong><span style="font-size: 14pt">Battery Constraints:</span></strong></p>
<p>The height of the battery should be less than 5, 1/2 to be placed in the bottom compartment of the robot. If the battery is any larger, the frame design will have to be changed to accommodate the larger battery size. Width and length of the battery do not seem to matter (as far as I know!). This space constraint incentivizes us to use multiple smaller batteries and hook them up in parallel rather than using one larger battery, however, the capacity of these smaller batteries may not be enough to effectively support the projects needs. Below are some options I currently have in mind:</p>
<p><strong>Option 1) Hook up Four 12V 8Ah batteries from lab (Model # SLA1079)<br /></strong></p>
<p>This would allow the batteries to fit under the frame while maintaining higher Ah capacity. This matches the project needs pretty well and uses batteries that are already owned, however, there are a few problems with it which I've listed below:</p>
<p>      a) With four batteries in parallel we can achieve 36Ah of capacity which can power the robot for only 3 hours continuously<br />          instead of 5.</p>
<p>      b) The current draw of the robot may be too high for the battery to supply continuously at a healthy level. The SLA1079<br />          datasheet provides us with a table that relates battery runtime and continuous current usage over time. It shows that<br />          continuous current should be lower if the battery is used for longer periods of time. This is to ensure battery health. Basically,<br />          if you run the battery for longer, you shouldn't draw as much current from it continuously, otherwise you hurt the battery over<br />          time. This table can be found in the google doc provided (I'd attach an image but I don't think the forum allows me to do that)</p>
<p><strong>Option 2) Hook up Six 12V 8Ah batteries (SLA1079)</strong></p>
<p>Same as option 1, but we increase capacity and current draw which solve problems a) and b). This could work, however, I'm not sure if we can safely connect 6 batteries, it may cause current draw issues. I will look into this.</p>
<p><strong>Option 3) Purchase a larger battery</strong></p>
<p>We could purchase a larger battery (40-60Ah battery to support 5+ hours of continuous use). This would increase the runtime of the robot as well as ensure the current draw of the robot can be easily managed. There are a few issues with this method though:</p>
<p>a) Increases the weight of the robot, potentially reducing mobility</p>
<p>b) The battery would be too large to fit inside the hull, so the hull would have to be modified to accommodate the larger battery.</p>
<p><strong>Other Notes:</strong></p>
<p>This is all the info I have now, but I'll keep doing research and figure out if hooking up 6 12V 8Ah batteries is safe, as well as looking into the current draw of both small and large batteries to see if they can handle the continuous current draw of our system.</p>
<p>Here are links to the excel spreadsheet and google doc with all my work. This information can also be found in the google drive.</p>
<p>Excel:<br />https://docs.google.com/spreadsheets/d/1c61AJ6dTo-TDHi3su-V8-ZS31RmROvyz_vDzj9C5zlU/edit?usp=sharing <br /><br />Google Doc:<br />https://docs.google.com/spreadsheets/d/1c61AJ6dTo-TDHi3su-V8-ZS31RmROvyz_vDzj9C5zlU/edit?usp=sharing </p>]]></content:encoded>
						                            <category domain="https://robartx.tech/community/"></category>                        <dc:creator>cjheller</dc:creator>
                        <guid isPermaLink="true">https://robartx.tech/community/main-forum/battery/#post-29</guid>
                    </item>
				                    <item>
                        <title>Using SLAM Toolbox + Nav2 with RPLIDAR A1</title>
                        <link>https://robartx.tech/community/lidar/using-slam-toolbox-nav2-with-rplidar-a1/#post-28</link>
                        <pubDate>Fri, 13 Feb 2026 07:15:25 +0000</pubDate>
                        <description><![CDATA[The following must be running correctly:

RPLIDAR publishes /scan (LaserScan)
TF tree is complete: map --&gt; odom --&gt;  base_link --&gt; laser
SLAM toolbox publishes /map and map --&amp;g...]]></description>
                        <content:encoded><![CDATA[<p>The following must be running correctly:</p>
<ul>
<li>RPLIDAR publishes /scan (LaserScan)</li>
<li>TF tree is complete: map --&gt; odom --&gt;  base_link --&gt; laser</li>
<li>SLAM toolbox publishes /map and map --&gt; odom</li>
<li>Nav2 runs without AMCL + without map_server while you are mapping
<ul>
<li>Nav2 expects SLAM to provide those</li>
</ul>
</li>
</ul>
<p><span style="font-size: 12pt"><strong>Bringup: </strong></span></p>
<p><strong>1) Install packages (Jazzy)</strong></p>
<pre contenteditable="false">sudo apt update
sudo apt install -y \
  ros-jazzy-navigation2 ros-jazzy-nav2-bringup \
  ros-jazzy-slam-toolbox \
  ros-jazzy-tf2-ros
</pre>
<p><strong>2) Run RPLIDAR A1 driver (rplidar_ros)</strong></p>
<pre contenteditable="false">ros2 launch rplidar_ros view_rplidar_a1_launch.py</pre>
<p>Confirm the scan topic (in another terminal)</p>
<pre contenteditable="false">ros2 topic list | grep scan
ros2 topic echo /scan --once
</pre>
<p>header.frame_id has to be laser or laser_frame. This frame must match the TF.</p>
<p><strong>3) Provide the TF from base_link --&gt; laser</strong></p>
<p>If not already published with URDF + robot_state_publisher, publish a static transform:</p>
<p>Ex; laser is 0.20m forward, 0.10m up from base_link , no rotation </p>
<p><span style="font-weight: 400">ros2 run tf2_ros static_transform_publisher \</span></p>
<p><span style="font-weight: 400">  0.20 0.0 0.10 0 0 0 base_link laser</span></p>
<p>Check:</p>
<p><span style="font-weight: 400">ros2 run tf2_ros tf2_echo base_link laser</span></p>
<p><strong>4) Check odometry (odom --&gt; base_link)</strong></p>
<p>Odom comes from wheel encoders.</p>
<p>ros2 topic echo /odom --once</p>
<p><span style="font-weight: 400">ros2 run tf2_ros tf2_echo odom base_link</span></p>
<p><strong>5) Start SLAM toolbox (online async)</strong></p>
<p><strong>6) Start Nav2 (navigation only, no AMCL/map_server)</strong></p>
<p><strong>7) Start RViz</strong></p>
<p><strong>8) Save map</strong></p>]]></content:encoded>
						                            <category domain="https://robartx.tech/community/"></category>                        <dc:creator>tanya</dc:creator>
                        <guid isPermaLink="true">https://robartx.tech/community/lidar/using-slam-toolbox-nav2-with-rplidar-a1/#post-28</guid>
                    </item>
				                    <item>
                        <title>RE: Autonomous Exploration</title>
                        <link>https://robartx.tech/community/slam-nav2/autonomous-exploration/#post-27</link>
                        <pubDate>Fri, 13 Feb 2026 06:55:12 +0000</pubDate>
                        <description><![CDATA[If m-explore doesn&#039;t work, try installing nav2_wfd explore.

clone it
build it
run: ros2 run nav2_wfd explore

It computes frontier centroids from the OccupancyGrid and invokes Nav2 wa...]]></description>
                        <content:encoded><![CDATA[<p>If m-explore doesn't work, try installing nav2_wfd explore.</p>
<ul>
<li style="font-weight: 400"><span style="font-weight: 400">clone it</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">build it</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">run: </span><span style="font-weight: 400">ros2 run nav2_wfd explore</span></li>
</ul>
<p><span style="font-weight: 400">It computes frontier centroids from the OccupancyGrid and invokes Nav2 waypoint follower. </span></p>
<h3><b>Install</b></h3>
<p><span style="font-weight: 400">cd ~/ws_big_robot/src</span></p>
<p><span style="font-weight: 400">git clone https://github.com/SeanReg/nav2_wavefront_frontier_exploration.git</span></p>
<p><span style="font-weight: 400">cd ~/ws_big_robot</span></p>
<p><span style="font-weight: 400">rosdep update</span></p>
<p><span style="font-weight: 400">rosdep install -i --from-path src --rosdistro jazzy -y</span></p>
<p><span style="font-weight: 400">colcon build --symlink-install</span></p>
<p><span style="font-weight: 400">source install/setup.bash</span></p>
<h3><b>Run</b></h3>
<p><span style="font-weight: 400">ros2 run nav2_wfd explore</span></p>]]></content:encoded>
						                            <category domain="https://robartx.tech/community/"></category>                        <dc:creator>tanya</dc:creator>
                        <guid isPermaLink="true">https://robartx.tech/community/slam-nav2/autonomous-exploration/#post-27</guid>
                    </item>
				                    <item>
                        <title>Autonomous Exploration</title>
                        <link>https://robartx.tech/community/slam-nav2/autonomous-exploration/#post-26</link>
                        <pubDate>Fri, 13 Feb 2026 06:53:11 +0000</pubDate>
                        <description><![CDATA[What the exploration node does:
Exploration is not part of Nav2. It is a goal generator:
- subscribes to /map (OccupancyGrid)
- finds frontiers (boundary between known free space and unkn...]]></description>
                        <content:encoded><![CDATA[<p><strong><span style="font-size: 12pt">What the exploration node does:</span></strong></p>
<p>Exploration is not part of Nav2. It is a goal generator:</p>
<p>- subscribes to /map (OccupancyGrid)</p>
<p>- finds frontiers (boundary between known free space and unknown space)</p>
<p>- picks a frontier goal pose</p>
<p>- calls Nav2's NavigateToPose (or waypoint follower) action so Nav2 drives the repeats until no frontiers remain</p>
<p>explorer never publishes /cmd_vel. Nav2 still owns motion.</p>
<p>&nbsp;</p>
<p><strong><span style="font-size: 12pt">Prereqs that must already work:</span></strong></p>
<p>Before installing exploration, make sure:</p>
<ol>
<li>SLAM toolbox is publishing /map</li>
<li>TF chain exists: map --&gt; odom --&gt; base_link --&gt; laser</li>
<li>Nav2 action server exists: navigate_to_pose</li>
</ol>
<p>Check using:</p>
<ul>
<li>
<p><span style="font-weight: 400">ros2 topic echo /map --once</span></p>
</li>
<li>
<p><span style="font-weight: 400"></span><span style="font-weight: 400">ros2 run tf2_ros tf2_echo map odom</span></p>
</li>
<li>
<p><span style="font-weight: 400"></span><span style="font-weight: 400">ros2 action list | grep navigate_to_pose</span></p>
</li>
</ul>
<p>&nbsp;</p>
<p><strong><span style="font-size: 12pt">Install:</span></strong></p>
<strong>1) Install dependencies (Jazzy):</strong>
<p><span style="font-weight: 400">sudo apt update</span></p>
<p><span style="font-weight: 400">sudo apt install -y ros-jazzy-nav2-bringup ros-jazzy-slam-toolbox</span></p>
<p><strong>2) Clone + build in your workspace</strong></p>
<p><span style="font-weight: 400">cd ~/ws_big_robot/src   # or whatever your colcon ws is</span></p>
<p><span style="font-weight: 400">git clone https://github.com/robo-friends/m-explore-ros2.git</span></p>
<p><span style="font-weight: 400">cd ~/ws_big_robot</span></p>
<p><span style="font-weight: 400">rosdep update</span></p>
<p><span style="font-weight: 400">rosdep install -i --from-path src --rosdistro jazzy -y</span></p>
<p><span style="font-weight: 400">colcon build --symlink-install</span></p>
<p><span style="font-weight: 400">source install/setup.bash</span></p>
<p><strong>3) Confirm it installed</strong></p>
<p><span style="font-weight: 400">ros2 pkg list | grep explore</span></p>
<p><span style="font-weight: 400">ros2 run explore_lite explore --help</span></p>
<p><strong>4) How to run it manually (first test)</strong></p>
<p><span style="font-weight: 400">ros2 run explore_lite explore --ros-args --params-file \</span></p>
<p><span style="font-weight: 400">  ~/ws_big_robot/src/m-explore-ros2/explore/config/params.yaml</span></p>
<p><br /><span style="font-weight: 400">It also supports </span><b>stop/resume</b><span style="font-weight: 400"> by publishing Bool to </span><span style="font-weight: 400">explore/resume</span><span style="font-weight: 400">.</span></p>
<p>&nbsp;</p>
<p><strong><span style="font-size: 12pt">Where to embed it in your system (your bringup XML):</span></strong></p>
<p><span style="font-weight: 400">You embed it </span><b>in the same bringup launch</b> <i><span style="font-weight: 400">after</span></i><span style="font-weight: 400"> you start SLAM Toolbox + Nav2.</span></p>
<p><span style="font-weight: 400">In your current bringup file, add this node near the bottom (after navigation include is fine):</span></p>
<p><span style="font-weight: 400">&lt;!-- Exploration node (frontier exploration) --&gt;</span></p>
<p><span style="font-weight: 400">&lt;node pkg="explore_lite"</span></p>
<p><span style="font-weight: 400">      exec="explore"</span></p>
<p><span style="font-weight: 400">      name="explore"</span></p>
<p><span style="font-weight: 400">      output="screen"</span></p>
<p><span style="font-weight: 400">      args="--ros-args --params-file $(find-pkg-share big_robot_navigation)/params/explore.yaml"&gt;</span></p>
<p><span style="font-weight: 400">  &lt;param name="use_sim_time" value="$(var use_sim_time)"/&gt;</span></p>
<p>&nbsp;</p>
<p><span style="font-weight: 400">  &lt;!-- If you use namespaces, avoid absolute topics unless you really mean global --&gt;</span></p>
<p><span style="font-weight: 400">  &lt;remap from="/tf" to="tf"/&gt;</span></p>
<p><span style="font-weight: 400">  &lt;remap from="/tf_static" to="tf_static"/&gt;</span></p>
<p><span style="font-weight: 400">&lt;/node&gt;</span></p>
<p>&nbsp;</p>
<h3><b>Important practical note (startup ordering):</b></h3>
<p><span style="font-weight: 400">explore_lite</span><span style="font-weight: 400"> “starts right away” by default. </span><span style="font-weight: 400"><br /></span><span style="font-weight: 400">If it starts before Nav2 is fully active, it may fail goals.</span></p>
<p><b>Easy workaround:</b><span style="font-weight: 400"> start it embedded, but immediately “pause” exploration and unpause after Nav2 is active:</span></p>
<p><span style="font-weight: 400"># pause</span></p>
<p><span style="font-weight: 400">ros2 topic pub /explore/resume std_msgs/msg/Bool "{data: false}" -1</span></p>
<p>&nbsp;</p>
<p><span style="font-weight: 400"># later, resume</span></p>
<p><span style="font-weight: 400">ros2 topic pub /explore/resume std_msgs/msg/Bool "{data: true}" -1</span></p>
<p>&nbsp;</p>
<h2><span style="font-size: 12pt"><b>Create </b><b>params/explore.yaml</b><b> (starter template):</b></span></h2>
<p><span style="font-weight: 400">Start simple; don’t over-tune until it moves.</span></p>
<p><span style="font-weight: 400"># big_robot_navigation/params/explore.yaml</span></p>
<p><span style="font-weight: 400">explore:</span></p>
<p><span style="font-weight: 400">  ros__parameters:</span></p>
<p><span style="font-weight: 400">    # Common knobs you’ll tune:</span></p>
<p><span style="font-weight: 400">    # - how close is "goal reached"</span></p>
<p><span style="font-weight: 400">    # - how long to wait before declaring stuck</span></p>
<p><span style="font-weight: 400">    # - frontier size thresholds</span></p>
<p><span style="font-weight: 400">    # These names vary slightly across explore implementations, so:</span></p>
<p><span style="font-weight: 400">    # 1) start by copying the repo's explore/config/params.yaml</span></p>
<p><span style="font-weight: 400">    # 2) move it here and edit</span></p>
<p>&nbsp;</p>
<p><b>Best workflow:</b></p>
<ol>
<li style="font-weight: 400"><span style="font-weight: 400">Copy the repo’s default params file into your package</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">Edit locally</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">Point the node at your copy</span></li>
</ol>
<p><span style="font-weight: 400">The repo explicitly shows where the default params live and how to run with them. </span></p>
<p>&nbsp;</p>
<h2><span style="font-size: 12pt"><b>Debugging when it “doesn’t move”</b></span></h2>
<p><span style="font-weight: 400">Most common causes:</span></p>
<ol>
<li style="font-weight: 400"><b>No </b><b>/map</b><span style="font-weight: 400"> (SLAM toolbox not running / wrong scan topic)</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">TF broken (especially </span><span style="font-weight: 400">map→odom</span><span style="font-weight: 400"> or </span><span style="font-weight: 400">odom→base_link</span><span style="font-weight: 400">)</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">Nav2 not actually active (bt_navigator lifecycle not “active”)</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">Topic mismatch (namespaces + you used </span><span style="font-weight: 400">/scan</span><span style="font-weight: 400"> absolute in one place, relative in another)</span></li>
</ol>
<p><span style="font-weight: 400">Also: in RViz, add the frontiers marker if available:</span></p>
<ul>
<li style="font-weight: 400"><span style="font-weight: 400">m-explore-ros2</span><span style="font-weight: 400"> mentions a frontiers marker topic </span><span style="font-weight: 400">explore/frontiers</span><span style="font-weight: 400">.</span></li>
</ul>]]></content:encoded>
						                            <category domain="https://robartx.tech/community/"></category>                        <dc:creator>tanya</dc:creator>
                        <guid isPermaLink="true">https://robartx.tech/community/slam-nav2/autonomous-exploration/#post-26</guid>
                    </item>
				                    <item>
                        <title>RE: Roboclaw + ROS2 Setup</title>
                        <link>https://robartx.tech/community/robocall/roboclaw-ros2-setup/#post-25</link>
                        <pubDate>Thu, 12 Feb 2026 22:24:33 +0000</pubDate>
                        <description><![CDATA[To use teleop keyboard (only key I and O works right now):
In terminal:
ros2 run teleop_twist_keyboard teleop_twist_keyboard --ros-args --remap cmd_vel:=/twist_mux/cmd_vel
Reference: see ...]]></description>
                        <content:encoded><![CDATA[<p>To use teleop keyboard (only key I and O works right now):</p>
<p>In terminal:</p>
<p>ros2 run teleop_twist_keyboard teleop_twist_keyboard --ros-args --remap cmd_vel:=/twist_mux/cmd_vel</p>
<p>Reference: see google doc for robotic mouse project: Teleop_keyboard_operation</p>]]></content:encoded>
						                            <category domain="https://robartx.tech/community/"></category>                        <dc:creator>qiancai</dc:creator>
                        <guid isPermaLink="true">https://robartx.tech/community/robocall/roboclaw-ros2-setup/#post-25</guid>
                    </item>
							        </channel>
        </rss>
		