Example #1
0
 def on_desired_speed(self, dt, channel, data):
     forward = data[0]/1000.               # from millimeters per second to meters per second
     angular = math.radians(data[1]/100.)  # from centidegrees per second to radians per second
     dtime = 0.1                           # simulation step time in seconds
     max_acc_forward = 1                   # meters per sec**2
     max_acc_angular = 1                   # radians per sec**2
     acc_forward = np.clip(-max_acc_forward, (forward - self.speed_forward)/dtime, max_acc_forward)
     acc_angular = np.clip(-max_acc_angular, (angular - self.speed_angular)/dtime, max_acc_angular)
     self.speed_forward += acc_forward * dtime
     self.speed_angular += acc_angular * dtime
     log("desired", f"forward {forward:.2f} m/s", f"angular {math.degrees(angular):.2f} deg/s")
     log("current", f"forward {self.speed_forward:.2f} m/s", f"angular {math.degrees(self.speed_angular):.2f} deg/s")
     dist = dtime * self.speed_forward
     dist3d = quaternion.rotate_vector([dist, 0, 0], self.orientation)
     self.xyz = [a + b for a, b in zip(self.xyz, dist3d)]
     turn = quaternion.from_axis_angle((0,0,1), dtime * self.speed_angular)
     self.orientation = quaternion.multiply(self.orientation, turn)
     self.time += datetime.timedelta(seconds=dtime)
     heading_centidegrees = round(math.degrees(quaternion.heading(self.orientation))*100)
     x_mm, y_mm, z_mm = [round(c*1000) for c in self.xyz]
     self.bus.publish('orientation', self.orientation)
     self.bus.publish('rot', [heading_centidegrees, 0, 0])
     self.bus.publish('pose2d', [x_mm, y_mm, heading_centidegrees])
     self.bus.publish('pose3d', [[a - b for a, b, in zip(self.xyz, self.origin)], self.orientation])
     self.bus.publish('sim_time_sec', self.time.total_seconds())
     self.bus.publish('scan', self.scan())
Example #2
0
    def test_min_dist(self):
        bus = MagicMock()
        logimage = ImageExtractor(bus=bus, config={})

        robot_pose = [[0, 0, 0], [0, 0, 0, 1]]
        angle = quaternion.euler_to_quaternion(yaw=math.radians(20),
                                               roll=0,
                                               pitch=0)
        camera_pose = None
        rgb_compressed = b'dummy image'
        depth_compressed = None

        bus.reset_mock()
        for i in range(10):
            data = robot_pose, camera_pose, rgb_compressed, depth_compressed
            logimage.on_rgbd(data)
            robot_pose = [
                robot_pose[0],
                quaternion.multiply(robot_pose[1], angle)
            ]

        self.assertEqual(len(bus.method_calls), 10)
Example #3
0
    def parse_packet(self, data):
        """
        Parse cortexpilot sensors status message
        """
        # expects already validated single sample with 3 bytes length prefix
        #   and checksum at the end
        high, mid, low = data[:3]
        assert high == 0, high  # fixed packet size 2*256+89 bytes
        assert mid == 2, mid
        assert low == 89, low
        addr, cmd = data[3:5]
        assert addr == 1, addr
        assert cmd == 0xD, cmd
        offset = 5  # payload offset

        # 4 byte Flags (unsigned long)  0
        #   bit 0 -> 1 = BigRedSwitch
        #   bit 1 -> 1 = MissionButton
        #   bit 2 -> copy of EnableRun flag (motors enabled)
        #   bit 3 -> 1 = Payload loaded, 0 = Payload unloaded - payload indicator
        #   bit 4 -> 1 = LIDAR ScanValid
        #   bit 5 -> 1 = Manual override
        # 4 byte SystemVoltage (float)  4 - battery level for control electronics [V]
        # 4 byte PowerVoltage (float)   8 - battery level for motors [V]
        self.flags, system_voltage, power_voltage = struct.unpack_from(
            '<Iff', data, offset)
        self.lidar_valid = (self.flags & 0x10) == 0x10
        self.emergency_stop = (self.flags & 0x01) == 0x01

        self.voltage = [system_voltage, power_voltage]
        self.bus.publish('voltage', [int(v * 100) for v in self.voltage])

        # skipped parsing of:
        # 4 byte SpeedM1 (float)       12 - normalized motor M1 (R) speed <-1.0 1.0>
        # 4 byte SpeedM2 (float)       16 - normalized motor M2 (L) speed <-1.0 1.0>
        motors = struct.unpack_from('<ff', data, offset + 12)

        # skipped parsing of:
        # 4 byte ActualDir (float)     20 - normalized direction for PID controller

        # 4 byte EncM1 (signed long)   24 - incremental encoders count for motor M1 (R) since last reset
        # 4 byte EncM2 (signed long)   28 - incremental encoders count for motor M2 (L) since last reset
        encoders = struct.unpack_from('<II', data, offset + 24)

        # skipped parsing of:
        # 1 byte GPS_Valid             32 - 1 = valid data from GPS module
        # 1 byte GPS_Fix               33 - 0 = no fix, 1 = 2D fix, 2 = 3D fix
        # 4 byte GPS_UTCDate (ulong)   34 - GPS date in YYMMDD format
        # 4 byte GPS_UTCTime (ulong)   38 - GPS time in HHMMSS format
        # 4 byte GPS_Lat (ulong)       42 - format Lat * 1E7
        # 4 byte GPS_Lon (ulong)       46 - format Lon * 1E7
        # 4 byte GPS_Brg (float)       50 - GPS Bearing <0 .. 359> deg

        # 4 byte AHRS_q0 (float)       54 - Orientation Quaternion
        # 4 byte AHRS_q1 (float)       58 -
        # 4 byte AHRS_q2 (float)       62 -
        # 4 byte AHRS_q3 (float)       66 -

        qw, qx, qy, qz = struct.unpack_from('<ffff', data, offset + 54)
        orientation = qx, qy, qz, qw
        # identity quat points to north, we need it to point to east
        orientation = quaternion.multiply(orientation,
                                          [0, 0, 0.7071068, 0.7071068])
        # correct roll axis by 1.7 degrees
        self.orientation = quaternion.multiply(orientation,
                                               [0.0148348, 0, 0, 0.99989])
        self.bus.publish('orientation', list(self.orientation))

        q1, q2, q3, q0 = self.orientation  # quaternion
        #print(self.time, "{:.4f} {:.4f} {:.4f} {:.4f}".format(q0, q1, q2, q3))
        ax = math.atan2(2 * (q0 * q1 + q2 * q3), 1 - 2 * (q1 * q1 + q2 * q2))
        ay = math.asin(2 * (q0 * q2 - q3 * q1))
        az = math.atan2(2 * (q0 * q3 + q1 * q2), 1 - 2 * (q2 * q2 + q3 * q3))
        # rotation Euler angles are yaw, pitch and roll
        #print(self.time, "{:.4f} {:.4f} {:.4f}".format(math.degrees(az), math.degrees(ay), math.degrees(ax)))
        self.bus.publish(
            'rotation',
            [round(math.degrees(angle) * 100) for angle in [az, ay, ax]])

        # 4 byte Yaw (float)           70 - Heading (Yaw) - machine orientation to magnetic north <0 .. 359> deg
        self.yaw = struct.unpack_from('<f', data, offset + 70)[0]
        #print(math.degrees(x), math.degrees(y), math.degrees(z), self.yaw)
        # 4 byte AccelX (float)        74
        # 4 byte AccelY (float)        78
        # 4 byte AccelZ (float)        82
        # 4 byte GyroX (float)         86
        # 4 byte GyroY (float)         90
        # 4 byte GyroZ (float)         94
        # 4 byte MagX (float)          98
        # 4 byte MagY (float)          102
        # 4 byte MagZ (float)          106
        # 4 byte SystemTick (ulong) 110  - Uptime in milisecond
        uptime = struct.unpack_from('<I', data, offset + 110)[0]
        if self.uptime is not None:
            uptime_diff = uptime - self.uptime
        self.uptime = uptime

        if self.last_encoders is not None:
            step = [
                sint32_diff(x, prev)
                for x, prev in zip(encoders, self.last_encoders)
            ]
            self.publish('encoders', step)

            dist = ENC_SCALE * sum(step) / len(step)
            angle = ENC_SCALE * (step[0] - step[1]) / WHEEL_DISTANCE
            x, y, heading = self.pose
            # advance robot by given distance and angle
            if abs(angle) < 0.0000001:  # EPS
                # Straight movement - a special case
                x += dist * math.cos(heading)
                y += dist * math.sin(heading)
                #Not needed: heading += angle
            else:
                # Arc
                r = dist / angle
                x += -r * math.sin(heading) + r * math.sin(heading + angle)
                y += +r * math.cos(heading) - r * math.cos(heading + angle)
                heading += angle  # not normalized
            self.pose = (x, y, heading)
            self.send_pose()
        self.last_encoders = encoders

        # 4 byte LidarTimestamp (ulong) 114  - Value of SystemTick when lidar scan was received
        lidar_timestamp = struct.unpack_from('<I', data, offset + 114)[0]
        lidar_diff = lidar_timestamp - self.lidar_timestamp
        self.lidar_timestamp = lidar_timestamp
        if lidar_diff > 150 and self.lidar_valid:
            print(self.time, "lidar invalid:", lidar_diff)
            self.lidar_valid = False
        if lidar_diff != 0 and self.lidar_valid:
            # laser
            # 480 byte Lidar_Scan (ushort) 118 - 239 two-bytes distances from Lidar <0 .. 65535> in [cm]
            # Scan is whole 360 deg with resolution 1.5 deg
            scan = struct.unpack_from('<' + 'H' * 239, data,
                                      offset + 118)  # TODO should be 240

            # restrict scan only to 270 degrees - cut 1/8th on both sides
            #            scan = scan[30:-30]
            #            zero_sides = 20
            scan = [10 * d for d in reversed(scan)]  # scale to millimeters
            #            scan[:zero_sides] = [0]*zero_sides
            #            scan[-zero_sides:] = [0]*zero_sides
            self.publish('scan', scan)
Example #4
0
    def parse_packet(self, data):
        """
        Parse cortexpilot sensors status message
        """
        # expects already validated single sample with 3 bytes length prefix
        #   and checksum at the end
        high, mid, low = data[:3]
        assert high == 0, high  # fixed packet size 2*256+89 bytes
        assert mid == 2, mid
        assert low == 89, low
        addr, cmd = data[3:5]
        assert addr == 1, addr
        assert cmd == 0xD, cmd
        offset = 5  # payload offset

        # 4 byte Flags (unsigned long)  0
        #   bit 0 -> 1 = BigRedSwitch
        #   bit 1 -> 1 = MissionButton
        #   bit 2 -> copy of EnableRun flag (motors enabled)
        #   bit 3 -> 1 = Payload loaded, 0 = Payload unloaded - payload indicator
        #   bit 4 -> 1 = LIDAR ScanValid
        #   bit 5 -> 1 = Manual override
        # 4 byte SystemVoltage (float)  4 - battery level for control electronics [V]
        # 4 byte PowerVoltage (float)   8 - battery level for motors [V]
        self.flags, system_voltage, power_voltage = struct.unpack_from('<Iff', data, offset)
        self.lidar_valid = (self.flags & 0x10) == 0x10
        self.emergency_stop = (self.flags & 0x01) == 0x01

        self.voltage = [system_voltage, power_voltage]
        self.bus.publish('voltage', [int(v*100) for v in self.voltage])

        # skipped parsing of:
        # 4 byte SpeedM1 (float)       12 - normalized motor M1 (R) speed <-1.0 1.0>
        # 4 byte SpeedM2 (float)       16 - normalized motor M2 (L) speed <-1.0 1.0>
        motors = struct.unpack_from('<ff', data, offset + 12)

        # skipped parsing of:
        # 4 byte ActualDir (float)     20 - normalized direction for PID controller
        
        # 4 byte EncM1 (signed long)   24 - incremental encoders count for motor M1 (R) since last reset
        # 4 byte EncM2 (signed long)   28 - incremental encoders count for motor M2 (L) since last reset
        encoders = struct.unpack_from('<II', data, offset + 24)

        # skipped parsing of:
        # 1 byte GPS_Valid             32 - 1 = valid data from GPS module
        # 1 byte GPS_Fix               33 - 0 = no fix, 1 = 2D fix, 2 = 3D fix 
        # 4 byte GPS_UTCDate (ulong)   34 - GPS date in YYMMDD format
        # 4 byte GPS_UTCTime (ulong)   38 - GPS time in HHMMSS format
        # 4 byte GPS_Lat (ulong)       42 - format Lat * 1E7
        # 4 byte GPS_Lon (ulong)       46 - format Lon * 1E7
        # 4 byte GPS_Brg (float)       50 - GPS Bearing <0 .. 359> deg

        # 4 byte AHRS_q0 (float)       54 - Orientation Quaternion
        # 4 byte AHRS_q1 (float)       58 - 
        # 4 byte AHRS_q2 (float)       62 - 
        # 4 byte AHRS_q3 (float)       66 - 

        orientation = struct.unpack_from('<ffff', data, offset+54)
        # identity quat points to north, we need it to point to east
        orientation = quaternion.multiply(orientation, [0.7071068, 0, 0, 0.7071068])
        # correct roll axis by 1.7 degrees
        self.orientation = quaternion.multiply(orientation, [0.99989, 0.0148348, 0, 0,  ])
        self.bus.publish('orientation', list(self.orientation))

        q0, q1, q2, q3 = self.orientation  # quaternion
        #print(self.time, "{:.4f} {:.4f} {:.4f} {:.4f}".format(q0, q1, q2, q3))
        ax =  math.atan2(2*(q0*q1+q2*q3), 1-2*(q1*q1+q2*q2))
        ay =  math.asin(2*(q0*q2-q3*q1))
        az =  math.atan2(2*(q0*q3+q1*q2), 1-2*(q2*q2+q3*q3))
        # rotation Euler angles are yaw, pitch and roll
        #print(self.time, "{:.4f} {:.4f} {:.4f}".format(math.degrees(az), math.degrees(ay), math.degrees(ax)))
        self.bus.publish('rotation', [round(math.degrees(angle)*100) for angle in [az, ay, ax]])

        # 4 byte Yaw (float)           70 - Heading (Yaw) - machine orientation to magnetic north <0 .. 359> deg
        self.yaw = struct.unpack_from('<f', data, offset + 70)[0]
        #print(math.degrees(x), math.degrees(y), math.degrees(z), self.yaw)
        # 4 byte AccelX (float)        74
        # 4 byte AccelY (float)        78
        # 4 byte AccelZ (float)        82
        # 4 byte GyroX (float)         86
        # 4 byte GyroY (float)         90
        # 4 byte GyroZ (float)         94
        # 4 byte MagX (float)          98
        # 4 byte MagY (float)          102
        # 4 byte MagZ (float)          106
        # 4 byte SystemTick (ulong) 110  - Uptime in milisecond
        uptime = struct.unpack_from('<I', data, offset + 110)[0]
        if self.uptime is not None:
            uptime_diff = uptime - self.uptime
        self.uptime = uptime

        if self.last_encoders is not None:
            step = [sint32_diff(x, prev) for x, prev in zip(encoders, self.last_encoders)]
            self.publish('encoders', step)

            dist = ENC_SCALE * sum(step)/len(step)
            angle = ENC_SCALE * (step[0] - step[1])/WHEEL_DISTANCE
            x, y, heading = self.pose
            # advance robot by given distance and angle
            if abs(angle) < 0.0000001:  # EPS
                # Straight movement - a special case
                x += dist * math.cos(heading)
                y += dist * math.sin(heading)
                #Not needed: heading += angle
            else:
                # Arc
                r = dist / angle
                x += -r * math.sin(heading) + r * math.sin(heading + angle)
                y += +r * math.cos(heading) - r * math.cos(heading + angle)
                heading += angle # not normalized
            self.pose = (x, y, heading)
            self.send_pose()
        self.last_encoders = encoders

        # 4 byte LidarTimestamp (ulong) 114  - Value of SystemTick when lidar scan was received
        lidar_timestamp = struct.unpack_from('<I', data, offset + 114)[0]
        lidar_diff = lidar_timestamp - self.lidar_timestamp
        self.lidar_timestamp = lidar_timestamp
        if lidar_diff > 150 and self.lidar_valid:
            print(self.time, "lidar invalid:", lidar_diff)
            self.lidar_valid = False
        if lidar_diff != 0 and self.lidar_valid:
            # laser
            # 480 byte Lidar_Scan (ushort) 118 - 239 two-bytes distances from Lidar <0 .. 65535> in [cm]
            # Scan is whole 360 deg with resolution 1.5 deg
            scan = struct.unpack_from('<' + 'H'*239, data, offset + 118)  # TODO should be 240

            # restrict scan only to 270 degrees - cut 1/8th on both sides
            scan = scan[30:-30]
            zero_sides = 20
            scan = [10 * d for d in reversed(scan)]  # scale to millimeters
            scan[:zero_sides] = [0]*zero_sides
            scan[-zero_sides:] = [0]*zero_sides
            self.publish('scan', scan)