File Coverage

blib/lib/Device/Dynamixel.pm
Criterion Covered Total %
statement 15 103 14.5
branch 0 22 0.0
condition 0 2 0.0
subroutine 5 18 27.7
pod 9 9 100.0
total 29 154 18.8


line stmt bran cond sub pod time code
1             package Device::Dynamixel;
2              
3 1     1   43129 use strict;
  1         4  
  1         40  
4 1     1   6 use warnings;
  1         3  
  1         39  
5 1     1   6 use List::Util qw(sum);
  1         6  
  1         122  
6 1     1   6 use feature qw(say);
  1         2  
  1         105  
7 1     1   2333 use Const::Fast;
  1         4483  
  1         12  
8              
9             =head1 NAME
10              
11             Device::Dynamixel - Simple control of Robotis Dynamixel servo motors
12              
13             =cut
14              
15             our $VERSION = '0.027';
16              
17              
18             =head1 SYNOPSIS
19              
20             use Device::Dynamixel;
21              
22             open my $pipe, '+<', '/dev/ttyUSB0' or die "Couldn't open pipe for reading and writing";
23             my $motorbus = Device::Dynamixel->new($pipe);
24              
25             # set motion speed of ALL motors to 200
26             $motorbus->writeMotor($Device::Dynamixel::BROADCAST_ID,
27             $Device::Dynamixel::addresses{Moving_Speed_L}, [200, 0]);
28              
29             # move motor 5 to 10 degrees off-center
30             $motorbus->moveMotorTo_deg(5, 10);
31              
32             # read the position of motor 5
33             my $status = $motorbus->readMotor(5,
34             $Device::Dynamixel::addresses{Present_Position_L}, 2);
35             my @params = @{$status->{params}};
36             my $position = $params[1]*255 + $params[0];
37              
38             =head1 DESCRIPTION
39              
40             This is a simple module to communicate with Robotis Dynamixel servo motors. The
41             Dynamixel AX-12 motors have been tested to work with this module, but the others
42             should work also.
43              
44             A daisy-chained series string of motors is connected to the host via a simple
45             serial connection. Each motor in the series has an 8-bit ID. This ID is present
46             in every command to address specific motors. One Device::Dynamixel object should
47             be created for a single string of motors connected to one motor port.
48              
49             These motors communicate using a particular protocol, which is implemented by
50             this module. Commands are sent to the motor. A status reply is sent back after
51             each command. This module handles construction and parsing of Dynamixel packets,
52             as well as the sending and receiving data when needed.
53              
54             =head2 EXPORTED VARIABLES
55              
56             To communicate with all motor at once, send commands to the broadcast ID:
57              
58             $Device::Dynamixel::BROADCAST_ID
59              
60             All the motor control addresses described in the Dynamixel docs are defined in this module,
61             available as
62              
63             $Device::Dynamixel::addresses{$value}
64              
65             Defined values are:
66              
67             ModelNumber_L
68             ModelNumber_H
69             Version_of_Firmware
70             ID
71             Baud_Rate
72             Return_Delay_Time
73             CW_Angle_Limit_L
74             CW_Angle_Limit_H
75             CCW_Angle_Limit_L
76             CCW_Angle_Limit_H
77             Highest_Limit_Temperature
78             Lowest_Limit_Voltage
79             Highest_Limit_Voltage
80             Max_Torque_L
81             Max_Torque_H
82             Status_Return_Level
83             Alarm_LED
84             Alarm_Shutdown
85             Down_Calibration_L
86             Down_Calibration_H
87             Up_Calibration_L
88             Up_Calibration_H
89             Torque_Enable
90             LED
91             CW_Compliance_Margin
92             CCW_Compliance_Margin
93             CW_Compliance_Slope
94             CCW_Compliance_Slope
95             Goal_Position_L
96             Goal_Position_H
97             Moving_Speed_L
98             Moving_Speed_H
99             Torque_Limit_L
100             Torque_Limit_H
101             Present_Position_L
102             Present_Position_H
103             Present_Speed_L
104             Present_Speed_H
105             Present_Load_L
106             Present_Load_H
107             Present_Voltage
108             Present_Temperature
109             Registered_Instruction
110             Reserved
111             Moving
112             Lock
113             Punch_L
114             Punch_H
115              
116             To change the baud rate of the motor, the B address must be written
117             with a value
118              
119             $Device::Dynamixel::baudrateValues{$baud}
120              
121             The available baud rates are
122              
123             1000000
124             500000
125             400000
126             250000
127             200000
128             115200
129             57600
130             19200
131             9600
132              
133             Note that the baud rate generally is cached from the last time the motor was
134             used, defaulting to 1Mbaud at the start
135              
136             =head2 STATUS RETURN
137              
138             Most of the functions return a status hash that describes the status of the motors and/or returns
139             queried data. This hash is defined as
140              
141             { from => $motorID,
142             error => $error,
143             params => \@parameters }
144              
145             If no valid reply was received, undef is returned. Look at the Dynamixel hardware documentation for
146             the exact meaning of each hash element.
147              
148             =cut
149              
150              
151             # Constants defined in the dynamixel docs
152             const our $BROADCAST_ID => 0xFE;
153             const my %instructions =>
154             (PING => 0x01,
155             READ_DATA => 0x02,
156             WRITE_DATA => 0x03,
157             REG_WRITE => 0x04,
158             ACTION => 0x05,
159             RESET => 0x06,
160             SYNC_WRITE => 0x83);
161              
162             const our %addresses =>
163             (ModelNumber_L => 0,
164             ModelNumber_H => 1,
165             Version_of_Firmware => 2,
166             ID => 3,
167             Baud_Rate => 4,
168             Return_Delay_Time => 5,
169             CW_Angle_Limit_L => 6,
170             CW_Angle_Limit_H => 7,
171             CCW_Angle_Limit_L => 8,
172             CCW_Angle_Limit_H => 9,
173             Highest_Limit_Temperature => 11,
174             Lowest_Limit_Voltage => 12,
175             Highest_Limit_Voltage => 13,
176             Max_Torque_L => 14,
177             Max_Torque_H => 15,
178             Status_Return_Level => 16,
179             Alarm_LED => 17,
180             Alarm_Shutdown => 18,
181             Down_Calibration_L => 20,
182             Down_Calibration_H => 21,
183             Up_Calibration_L => 22,
184             Up_Calibration_H => 23,
185             Torque_Enable => 24,
186             LED => 25,
187             CW_Compliance_Margin => 26,
188             CCW_Compliance_Margin => 27,
189             CW_Compliance_Slope => 28,
190             CCW_Compliance_Slope => 29,
191             Goal_Position_L => 30,
192             Goal_Position_H => 31,
193             Moving_Speed_L => 32,
194             Moving_Speed_H => 33,
195             Torque_Limit_L => 34,
196             Torque_Limit_H => 35,
197             Present_Position_L => 36,
198             Present_Position_H => 37,
199             Present_Speed_L => 38,
200             Present_Speed_H => 39,
201             Present_Load_L => 40,
202             Present_Load_H => 41,
203             Present_Voltage => 42,
204             Present_Temperature => 43,
205             Registered_Instruction => 44,
206             Reserved => 45,
207             Moving => 46,
208             Lock => 47,
209             Punch_L => 48,
210             Punch_H => 49);
211              
212             const our %baudrateValues =>
213             (1000000 => 0x01,
214             500000 => 0x03,
215             400000 => 0x04,
216             250000 => 0x07,
217             200000 => 0x09,
218             115200 => 0x10,
219             57600 => 0x22,
220             19200 => 0x67,
221             9600 => 0xCF);
222              
223             # a received packet is deemed complete if no data was received in this much time
224             const my $timeDelimiter_s => 0.1;
225              
226             # motor range in command coordinates and in degrees
227             const my $motorRange_coords => 0x400;
228             const my $motorRange_deg => 300;
229              
230             =head1 CONSTRUCTOR
231              
232             =head2 new( PIPE )
233              
234             Creates a new object to talk to a Dynamixel motor. The file handle has to be opened and set-up
235             prior to constructing the object.
236              
237             =cut
238             sub new
239             {
240 0     0 1   my ($classname, $pipe) = @_;
241              
242 0           my $this = {};
243 0           bless($this, $classname);
244              
245 0           return $this->_init($pipe);
246             }
247              
248             sub _init
249             {
250 0     0     my $this = shift;
251 0           my $pipe = shift;
252              
253 0           $this->{pipe} = $pipe;
254 0           return $this;
255             }
256              
257             # Constructs a binary dynamixel packet with a given command
258             sub _makeInstructionPacket
259             {
260 0     0     my ($motorID, $instruction, $parameters) = @_;
261 0           my $body = pack( 'C3C' . scalar @$parameters,
262             $motorID, 2 + @$parameters, $instruction,
263             @$parameters );
264              
265 0           my $checksum = ( ~sum(unpack('C*', $body)) & 0xFF );
266 0           return pack('CC', 0xFF, 0xFF) . $body . chr $checksum;
267             }
268              
269             =head1 METHODS
270              
271             =head2 pingMotor( motorID )
272              
273             Sends a ping. Status reply is returned
274              
275             =cut
276              
277             sub pingMotor
278             {
279 0     0 1   my $this = shift;
280 0           my ($motorID) = @_;
281              
282 0           my $pipe = $this->{pipe};
283 0           print $pipe _makeInstructionPacket($motorID, $instructions{PING}, []);
284 0           return _pullMotorReply($this->{pipe});
285             }
286              
287             =head2 writeMotor( motorID, startingAddress, data )
288              
289             Sends a command to the motor. Status reply is returned.
290              
291             =cut
292              
293             sub writeMotor
294             {
295 0     0 1   my $this = shift;
296 0           my ($motorID, $where, $what) = @_;
297              
298 0           my $pipe = $this->{pipe};
299 0           print $pipe _makeInstructionPacket($motorID, $instructions{WRITE_DATA}, [$where, @$what]);
300 0           return _pullMotorReply($this->{pipe});
301             }
302              
303             =head2 readMotor( motorID, startingAddress, howManyBytes )
304              
305             Reads data from the motor. Status reply is returned.
306              
307             =cut
308              
309             sub readMotor
310             {
311 0     0 1   my $this = shift;
312 0           my ($motorID, $where, $howmany) = @_;
313              
314 0           my $pipe = $this->{pipe};
315 0           print $pipe _makeInstructionPacket($motorID, $instructions{READ_DATA}, [$where, $howmany]);
316 0           return _pullMotorReply($this->{pipe});
317             }
318              
319             =head2 writeMotor_queue( motorID, startingAddress, data )
320              
321             Queues a particular command to the motor and returns the received reply. Does
322             not actually execute the command until triggered with triggerMotorQueue( )
323              
324             =cut
325              
326             sub writeMotor_queue
327             {
328 0     0 1   my $this = shift;
329 0           my ($motorID, $where, $what) = @_;
330              
331 0           my $pipe = $this->{pipe};
332 0           print $pipe _makeInstructionPacket($motorID, $instructions{REG_WRITE}, [$where, @$what]);
333 0           return _pullMotorReply($this->{pipe});
334             }
335              
336             =head2 triggerMotorQueue( motorID )
337              
338             Sends a trigger for the queued commands. Status reply is returned.
339              
340             =cut
341              
342             sub triggerMotorQueue
343             {
344 0     0 1   my $this = shift;
345 0           my ($motorID) = @_;
346              
347 0           my $pipe = $this->{pipe};
348 0           print $pipe _makeInstructionPacket($motorID, $instructions{ACTION}, []);
349 0           return _pullMotorReply($this->{pipe});
350             }
351              
352             =head2 resetMotor( motorID )
353              
354             Sends a motor reset. Status reply is returned.
355              
356             =cut
357              
358             sub resetMotor
359             {
360 0     0 1   my $this = shift;
361 0           my ($motorID) = @_;
362              
363 0           my $pipe = $this->{pipe};
364 0           print $pipe _makeInstructionPacket($motorID, $instructions{RESET}, []);
365 0           return _pullMotorReply($this->{pipe});
366             }
367              
368             =head2 syncWriteMotor( motorID, startingAddress, data )
369              
370             Sends a synced-write command to the motor. Status reply is returned.
371              
372             =cut
373              
374             sub syncWriteMotor
375             {
376 0     0 1   my $this = shift;
377 0           my ($motorID, $writes, $where) = @_;
378              
379 0           my @parms = map { ($_->{motorID}, @{$_->{what}}) } @$writes;
  0            
  0            
380 0           my $lenchunk = scalar @{$writes->[0]{what}};
  0            
381 0           @parms = ($where, $lenchunk, @parms);
382              
383 0 0         if( ($lenchunk + 1) * @$writes + 2 != @parms )
384             {
385 0           die "syncWriteMotor: size mismatch!";
386             }
387              
388 0           my $pipe = $this->{pipe};
389 0           print $pipe _makeInstructionPacket($BROADCAST_ID, $instructions{SYNC_WRITE}, \@parms);
390 0           return _pullMotorReply($this->{pipe});
391             }
392              
393             sub _pullMotorReply
394             {
395 0     0     my $pipe = shift;
396              
397             # read data until there's a lull of $timeDelimiter_s seconds
398 0           my $packet = '';
399 0           select(undef, undef, undef, $timeDelimiter_s); # sleep for a bit to wait for data
400 0           while(1)
401             {
402 0           my $rin = '';
403 0           vec($rin,fileno($pipe),1) = 1;
404 0           my ($nfound, $timeleft) = select($rin, undef, undef, $timeDelimiter_s);
405 0 0         last if($nfound == 0);
406              
407 0           my $bytes;
408 0           sysread($pipe, $bytes, $nfound);
409 0           $packet .= $bytes;
410             }
411              
412 0           return _parseStatusPacket($packet);
413              
414              
415             # parses a given binary string as a dynamixel status packet
416             sub _parseStatusPacket
417             {
418 0     0     my $str = shift;
419              
420 0 0         my ($key) = unpack('n', substr($str, 0, 2, '')) or return;
421              
422 0 0         return if($key != 0xFFFF);
423              
424 0 0         my ($motorID, $length, $error) = unpack('C3', substr($str, 0, 3, '')) or return;
425 0           my $numParameters = $length - 2;
426 0 0         return if($numParameters < 0);
427              
428 0           my @parameters = ();
429 0           my $sumParameters = 0;
430 0 0         if ($numParameters)
431             {
432 0 0         @parameters = unpack("C$numParameters", substr($str, 0, $numParameters, '')) or return;
433 0           $sumParameters = sum(@parameters);
434             }
435 0   0       my $checksum = unpack('C1', substr($str, 0, 1, '')) // return;
436 0           my $checksumShouldbe = ~($motorID + $length + $error + $sumParameters) & 0xFF;
437 0 0         return if($checksum != $checksumShouldbe);
438              
439 0           return {from => $motorID,
440             error => $error,
441             params => \@parameters};
442             }
443             }
444              
445              
446             =head2 moveMotorTo_deg( motorID, position_degrees )
447              
448             Convenience function that uses the lower-level routines to move a motor to a
449             particular position
450              
451             =cut
452              
453             sub moveMotorTo_deg
454             {
455 0     0 1   my $this = shift;
456 0           my ($motorID, $position_deg) = @_;
457              
458 0           my $position = int( 0.5 + ($position_deg * $motorRange_coords/$motorRange_deg + 0x1ff) );
459 0 0         $position = 0 if $position < 0;
460 0 0         $position = $motorRange_coords-1 if $position >= $motorRange_coords;
461 0           return $this->writeMotor($motorID, $addresses{Goal_Position_L}, [unpack('C2', pack('v', $position))] );
462             }
463              
464             1;
465              
466              
467             __END__