changeset 1:568e7573fa8c

Basic inverse kinematics
author Radomir Dopieralski <sheep-devel@sheep.art.pl>
date Thu, 11 Sep 2014 21:41:08 +0200
parents 327a77c0cd71
children 10ddd812378b
files home.py katka.py leg.py robot.py servo.py swing.py
diffstat 6 files changed, 252 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/home.py	Thu Sep 11 21:41:08 2014 +0200
@@ -0,0 +1,9 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import katka
+
+
+robot = katka.Robot()
+robot.update()
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/katka.py	Thu Sep 11 21:41:08 2014 +0200
@@ -0,0 +1,40 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import serial
+
+import robot
+import servo
+import leg
+
+
+class Leg(leg.Leg2D):
+    # Lengths of leg segments, in mm
+    femur = 38.0  # between the knee and the ankle
+    tibia = 35.0  # between the ankle and the foot
+    base = 22.0, 45.0  # from the center of body
+
+
+class Servo(servo.Servo):
+    span = 180
+    center = 90
+
+
+class Robot(robot.Robot):
+    def __init__(self):
+        s = serial.Serial('/dev/ttyUSB0', 115200)
+        self.legs = (
+            # front left
+            Leg(Servo(s, 1, reverse=False, trim=5),
+                Servo(s, 0, reverse=False, trim=5), -1),
+            # front right
+            Leg(Servo(s, 6, reverse=False),
+                Servo(s, 7, reverse=False, trim=-5), -1),
+            # hind left
+            Leg(Servo(s, 3, reverse=True),
+                Servo(s, 2, reverse=True, trim=5), 1),
+            # hind right
+            Leg(Servo(s, 4, reverse=True),
+                Servo(s, 5, reverse=True, trim=-5), 1),
+        )
+        super(Robot, self).__init__()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/leg.py	Thu Sep 11 21:41:08 2014 +0200
@@ -0,0 +1,84 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import math
+
+
+SQRT2 = math.sqrt(2)
+
+
+def _solve_triangle(a, b, c):
+    """Calculate an angle of a triangle using the law of cosines."""
+    a, b, c = abs(a), abs(b), abs(c)
+    if a + b < c or a + c < b or b + c < a:
+        raise ValueError("Impossible triangle: %r, %r, %r" % (a, b, c))
+    cos = float(a ** 2 + b ** 2 - c ** 2) / (2 * a * b)
+    return math.acos(cos)
+
+
+def _length(a, b):
+    """Calculate the length of a vector."""
+    return math.sqrt(a ** 2 + b ** 2)
+
+
+class Leg2D(object):
+    """A signle leg, containing 2 servos: knee and ankle, and two
+       connections between them: femur and tibia.
+    """
+
+    # Lengths of leg segments, in mm
+    femur = 38.0  # between the knee and the ankle
+    tibia = 30.0  # between the ankle and the foot
+    base = 15.0, 15.0  # from the center of body
+
+    def __init__(self, knee, ankle, x_dir):
+        self.knee = knee
+        self.ankle = ankle
+        self.x_dir = x_dir
+        self.home = [
+            self.tibia / SQRT2,
+            self.femur + self.tibia / SQRT2,
+        ]
+        self.position = self.home
+
+    def __iter__(self):
+        """Iterate over all joints."""
+        return iter([self.knee, self.ankle])
+
+    def _inverse_kinematics(self, (x, y)):
+        """Calculate the 3 angles of the leg, in degrees."""
+        d = _length(x, y)
+        knee = (math.atan2(y, x) +
+                _solve_triangle(self.femur, d, self.tibia))
+        ankle = _solve_triangle(self.femur, self.tibia, d)
+        knee = 90 - math.degrees(knee)
+        ankle = math.degrees(ankle) - 180 + 45
+        return knee, ankle
+
+    def move_to(self, position):
+        """Move the foot to the specified point.
+
+        If any of the coordinates is None, no movement will be done in that
+        axis.
+        """
+        position = list(position)
+        for i, coord in enumerate(position):
+            if coord is None:
+                position[i] = self.position[i]
+        self.position = position
+
+    def move_by(self, (dx, dy)):
+        """Move the foot by the specified amount."""
+        x, y = self.position
+        self.move_to((x + dx, y + dy))
+
+    def update(self, shift=(0, 0), send=True):
+        """Send the positions to the servos."""
+        dx, dy = shift
+        x, y = self.position
+        position = x + dx * self.x_dir, y + dy
+        angles = self._inverse_kinematics(position)
+        for joint, angle in zip(self, angles):
+            joint.move_degrees(angle)
+            if send:
+                joint.update()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/robot.py	Thu Sep 11 21:41:08 2014 +0200
@@ -0,0 +1,21 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import pygame
+
+
+class Robot(object):
+    legs = ()
+
+    def __init__(self):
+        self.shift = [0.0, 0.0]
+        self.clock = pygame.time.Clock()
+
+    def update(self, fps=12, at_once=True):
+        servos = []
+        for leg in self.legs:
+            leg.update(self.shift, send=not at_once)
+            servos.extend(leg)
+        if at_once:
+            self.legs[0].ankle.update_all(servos)
+        self.clock.tick(fps)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/servo.py	Thu Sep 11 21:41:08 2014 +0200
@@ -0,0 +1,60 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+
+def _normalize_degrees(angle):
+    """Convert an angle to range +-180 degree range."""
+    while angle > 180:
+        angle -= 360
+    while angle < -180:
+        angle += 360
+    return angle
+
+def _word(value):
+    value = int(value)
+    return chr(value & 0xff) + (chr((value & 0xff00) >> 8))
+
+
+class Servo(object):
+    span = 180
+    center = 90
+    min_freq = 600
+    max_freq = 2400
+
+    def __init__(self, serial, index, reverse=False, trim=0):
+        self.index = index
+        self.position = self.center
+        self.reverse = reverse
+        self.trim = trim
+        self.serial = serial
+
+    def update(self):
+        freq = self.calculate_freq()
+        command = '\xfd%c%s' % (self.index, _word(freq))
+        self.serial.write(command)
+
+    @classmethod
+    def update_all(cls, servos):
+        serial = servos[0].serial
+        servos = sorted(servos, key=lambda servo: servo.index)
+        command = '\xfc%s' % ''.join(_word(servo.calculate_freq())
+                                     for servo in servos)
+        serial.write(command)
+
+    def calculate_angle(self):
+        position = self.position
+        if self.reverse:
+            position = -position
+        angle = min(max(0, self.center + position + self.trim), self.span)
+        return angle
+
+    def calculate_freq(self):
+        angle = self.calculate_angle()
+        return int(self.min_freq +
+                   angle * (self.max_freq - self.min_freq) / self.span)
+
+    def move_degrees(self, degrees):
+        degrees = _normalize_degrees(degrees)
+        if not self.center - self.span <= degrees <= self.center + self.span:
+            raise ValueError("Servo angle out of range.")
+        self.position = degrees
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/swing.py	Thu Sep 11 21:41:08 2014 +0200
@@ -0,0 +1,38 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import katka
+
+
+FPS = 24
+
+
+def swing(robot):
+    for leg in robot.legs:
+        leg.move_to((None, 53))
+    yield
+    dx, dy = 10, 10
+    while True:
+        for x in range(0, dx, 1):
+            robot.shift[0] = x
+            yield
+        for y in range(0, dy, 1):
+            robot.shift[1] = y
+            yield
+        for x in range(dx, -dx, -1):
+            robot.shift[0] = x
+            yield
+        for y in range(dy, -dy, -1):
+            robot.shift[1] = y
+            yield
+        for x in range(-dx, 0, 1):
+            robot.shift[0] = x
+            yield
+        for y in range(-dy, 0, 1):
+            robot.shift[1] = y
+            yield
+
+robot = katka.Robot()
+for frame in swing(robot):
+    robot.update(FPS)
+