Upload files to "controller/packagesrc"

This commit is contained in:
2025-04-03 11:01:36 +02:00
parent 5b53bdb499
commit 1c33489821
5 changed files with 1497 additions and 0 deletions

View File

@@ -0,0 +1,42 @@
# -*- coding: utf-8 -*-
# ██████╗ ██████╗ ██████╗ ██████╗ ███╗ ███╗ █████╗ ███████╗████████╗███████╗██████╗ ██████╗ ██╗ ██╗
# ██╔══██╗██╔═══██╗██╔══██╗██╔═══██╗████╗ ████║██╔══██╗██╔════╝╚══██╔══╝██╔════╝██╔══██╗██╔══██╗╚██╗ ██╔╝
# ██████╔╝██║ ██║██████╔╝██║ ██║██╔████╔██║███████║███████╗ ██║ █████╗ ██████╔╝██████╔╝ ╚████╔╝
# ██╔══██╗██║ ██║██╔══██╗██║ ██║██║╚██╔╝██║██╔══██║╚════██║ ██║ ██╔══╝ ██╔══██╗██╔═══╝ ╚██╔╝
# ██║ ██║╚██████╔╝██████╔╝╚██████╔╝██║ ╚═╝ ██║██║ ██║███████║ ██║ ███████╗██║ ██║██║ ██║
# ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝ ╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝
"""
RoboMasterPy: Python library and framework for RoboMaster EP
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
:copyright: (c) 2020 by LI Zhennan.
:license: MIT, see LICENSE for more details.
"""
from . import framework
from . import measure
from .__version__ import (
__title__, __description__, __url__, __version__,
__author__, __author_email__, __license__, __copyright__,
)
from .client import CTX, LOG_LEVEL
from .client import (
ChassisSpeed, ChassisPosition, ChassisAttitude, ChassisStatus,
GimbalAttitude,
ArmorHitEvent, SoundApplauseEvent,
)
from .client import (
VIDEO_PORT, AUDIO_PORT, CTRL_PORT, PUSH_PORT, EVENT_PORT, IP_PORT,
DEFAULT_BUF_SIZE,
SWITCH_ON, SWITCH_OFF,
MODE_CHASSIS_LEAD, MODE_GIMBAL_LEAD, MODE_FREE,
ARMOR_HIT,
SOUND_APPLAUSE,
LED_ALL, LED_TOP_ALL, LED_TOP_RIGHT, LED_TOP_LEFT, LED_BOTTOM_ALL, LED_BOTTOM_FRONT, LED_BOTTOM_BACK, LED_BOTTOM_LEFT, LED_BOTTOM_RIGHT,
LED_EFFECT_SOLID, LED_EFFECT_OFF, LED_EFFECT_PULSE, LED_EFFECT_BLINK, LED_EFFECT_SCROLLING,
)
from .client import get_broadcast_ip, Commander

View File

@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
# ██████╗ ██████╗ ██████╗ ██████╗ ███╗ ███╗ █████╗ ███████╗████████╗███████╗██████╗ ██████╗ ██╗ ██╗
# ██╔══██╗██╔═══██╗██╔══██╗██╔═══██╗████╗ ████║██╔══██╗██╔════╝╚══██╔══╝██╔════╝██╔══██╗██╔══██╗╚██╗ ██╔╝
# ██████╔╝██║ ██║██████╔╝██║ ██║██╔████╔██║███████║███████╗ ██║ █████╗ ██████╔╝██████╔╝ ╚████╔╝
# ██╔══██╗██║ ██║██╔══██╗██║ ██║██║╚██╔╝██║██╔══██║╚════██║ ██║ ██╔══╝ ██╔══██╗██╔═══╝ ╚██╔╝
# ██║ ██║╚██████╔╝██████╔╝╚██████╔╝██║ ╚═╝ ██║██║ ██║███████║ ██║ ███████╗██║ ██║██║ ██║
# ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝ ╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝
__title__ = 'robomasterpy'
__description__ = 'RoboMaster Python SDK and framework.'
__url__ = 'https://github.com/nanmu42/robomasterpy'
__version__ = '0.1.1'
__author__ = 'LI Zhennan'
__author_email__ = 'i@nanmu.me'
__license__ = 'MIT'
__copyright__ = '2020 LI Zhennan'

View File

@@ -0,0 +1,775 @@
# -*- coding: utf-8 -*-
# ██████╗ ██████╗ ██████╗ ██████╗ ███╗ ███╗ █████╗ ███████╗████████╗███████╗██████╗ ██████╗ ██╗ ██╗
# ██╔══██╗██╔═══██╗██╔══██╗██╔═══██╗████╗ ████║██╔══██╗██╔════╝╚══██╔══╝██╔════╝██╔══██╗██╔══██╗╚██╗ ██╔╝
# ██████╔╝██║ ██║██████╔╝██║ ██║██╔████╔██║███████║███████╗ ██║ █████╗ ██████╔╝██████╔╝ ╚████╔╝
# ██╔══██╗██║ ██║██╔══██╗██║ ██║██║╚██╔╝██║██╔══██║╚════██║ ██║ ██╔══╝ ██╔══██╗██╔═══╝ ╚██╔╝
# ██║ ██║╚██████╔╝██████╔╝╚██████╔╝██║ ╚═╝ ██║██║ ██║███████║ ██║ ███████╗██║ ██║██║ ██║
# ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝ ╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝
import logging
import multiprocessing as mp
import socket
from typing import Optional
from dataclasses import dataclass
CTX = mp.get_context('spawn')
LOG_LEVEL = logging.DEBUG
VIDEO_PORT: int = 40921
AUDIO_PORT: int = 40922
CTRL_PORT: int = 40923
PUSH_PORT: int = 40924
EVENT_PORT: int = 40925
IP_PORT: int = 40926
DEFAULT_BUF_SIZE: int = 512
# switch_enum
SWITCH_ON: str = 'on'
SWITCH_OFF: str = 'off'
# mode_enum
MODE_CHASSIS_LEAD: str = 'chassis_lead'
MODE_GIMBAL_LEAD: str = 'gimbal_lead'
MODE_FREE: str = 'free'
MODE_ENUMS = (MODE_CHASSIS_LEAD, MODE_GIMBAL_LEAD, MODE_FREE)
# armor_event_attr_enum
ARMOR_HIT: str = 'hit'
ARMOR_ENUMS = (ARMOR_HIT,)
# sound_event_attr_enum
SOUND_APPLAUSE: str = 'applause'
SOUND_ENUMS = (SOUND_APPLAUSE,)
# led_comp_enum
LED_ALL = 'all'
LED_TOP_ALL = 'top_all'
LED_TOP_RIGHT = 'top_right'
LED_TOP_LEFT = 'top_left'
LED_BOTTOM_ALL = 'bottom_all'
LED_BOTTOM_FRONT = 'bottom_front'
LED_BOTTOM_BACK = 'bottom_back'
LED_BOTTOM_LEFT = 'bottom_left'
LED_BOTTOM_RIGHT = 'bottom_right'
LED_ENUMS = (LED_ALL, LED_TOP_ALL, LED_TOP_RIGHT, LED_TOP_LEFT,
LED_BOTTOM_ALL, LED_BOTTOM_FRONT, LED_BOTTOM_BACK,
LED_BOTTOM_LEFT, LED_BOTTOM_RIGHT)
# led_effect_enum
LED_EFFECT_SOLID = 'solid'
LED_EFFECT_OFF = 'off'
LED_EFFECT_PULSE = 'pulse'
LED_EFFECT_BLINK = 'blink'
LED_EFFECT_SCROLLING = 'scrolling'
LED_EFFECT_ENUMS = (LED_EFFECT_SOLID, LED_EFFECT_OFF,
LED_EFFECT_PULSE, LED_EFFECT_BLINK,
LED_EFFECT_SCROLLING)
@dataclass
class ChassisSpeed:
x: float
y: float
z: float
w1: int
w2: int
w3: int
w4: int
@dataclass
class ChassisPosition:
x: float
y: float
z: Optional[float]
@dataclass
class ChassisAttitude:
pitch: float
roll: float
yaw: float
@dataclass
class ChassisStatus:
# 是否静止
static: bool
# 是否上坡
uphill: bool
# 是否下坡
downhill: bool
# 是否溜坡
on_slope: bool
# 是否被拿起
pick_up: bool
# 是否滑行
slip: bool
# x轴是否感应到撞击
impact_x: bool
# y轴是否感应到撞击
impact_y: bool
# z轴是否感应到撞击
impact_z: bool
# 是否翻车
roll_over: bool
# 是否在坡上静止
hill_static: bool
@dataclass
class GimbalAttitude:
pitch: float
yaw: float
@dataclass
class ArmorHitEvent:
index: int
type: int
@dataclass
class SoundApplauseEvent:
count: int
def get_broadcast_ip(timeout: float = None) -> str:
"""
接收广播以获取机甲IP
Receive broadcasting IP of Robomaster.
:param timeout: 等待超时(秒)。 timeout in second
:return: 机甲IP地址。IP of Robomaster.
"""
BROADCAST_INITIAL: str = 'robot ip '
conn = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
conn.bind(('', IP_PORT))
conn.settimeout(timeout)
msg, ip, port = None, None, None
try:
msg, (ip, port) = conn.recvfrom(DEFAULT_BUF_SIZE)
finally:
conn.close()
msg = msg.decode()
assert len(msg) > len(BROADCAST_INITIAL), f'broken msg from {ip}:{port}: {msg}'
msg = msg[len(BROADCAST_INITIAL):]
assert msg == ip, f'unmatched source({ip}) and reported IP({msg})'
return msg
class Commander:
def __init__(self, ip: str = '', timeout: float = 30):
"""
创建SDK实例并连接机甲实例在创建后立即可用。
Create a new SDK instance and connect it to Robomaster.
Instance is available immediately after creation.
:param ip: 可选机甲IP可在路由器模式下自动获取。 (Optional) IP of Robomaster, which can be detected automatically under router mode.
:param timeout: 可选TCP通讯超时。 (Optional) TCP timeout in second.
"""
self._mu: mp.Lock = CTX.Lock()
with self._mu:
if ip == '':
ip = get_broadcast_ip(timeout)
self._ip: str = ip
self._closed: bool = False
self._conn: socket.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self._timeout: float = timeout
self._conn.settimeout(self._timeout)
self._conn.connect((self._ip, CTRL_PORT))
resp = self._do('command')
assert self._is_ok(resp) or resp == 'Already in SDK mode', f'entering SDK mode: {resp}'
def close(self):
"""
关闭实例回收socket资源。注意这个命令并不会发送quit到机甲避免打扰其他在线的Commander.
Close instance, deallocate system socket resource.
Note this method will NOT send quit command to Robomaster,
because there may be other Commander still active.
"""
with self._mu:
self._conn.close()
self._closed = True
def __enter__(self):
return self
def __exit__(self):
self.close()
@staticmethod
def _is_ok(resp: str) -> bool:
return resp == 'ok'
def _do(self, *args) -> str:
assert len(args) > 0, 'empty arg not accepted'
assert not self._closed, 'connection is already closed'
cmd = ' '.join(map(str, args)) + ';'
self._conn.send(cmd.encode())
buf = self._conn.recv(DEFAULT_BUF_SIZE)
# 返回值后面有时候会多一个迷之空格,
# 为了可能的向后兼容,额外剔除终止符。
return buf.decode().strip(' ;')
def arm_move(self, x: float, y: float) -> str:
return self._do(f'robotic_arm move x {x} y {y}')
def arm_moveto(self, x: float, y: float) -> str:
return self._do(f'robotic_arm moveto x {x} y {y}')
def arm_recenter(self) -> str:
return self._do('robotic_arm recenter')
def arm_pos(self) -> int:
"""
x_pos: The x-axis coordinate in centimeters
y_pos: The y-axis coordinate in centimeters
"""
return self._do('robotic_arm position ?')
def arm_stop(self) -> str:
return self._do('robotic_arm stop')
def gripper_open(self, force: int = None) -> str:
assert force in [None, 1, 2, 3,4 ], "Force should be one of None, 1, 2, 3, 4"
return self._do(f'robotic_gripper open{f' leve {force}' if force else ''}')
def gripper_close(self, force: int = None) -> str:
assert force in [None, 1, 2, 3,4 ], "Force should be one of None, 1, 2, 3, 4"
return self._do(f'robotic_gripper close{f' leve {force}' if force else ''}')
def gripper_status(self) -> int:
"""
0: The mechanical gripper is fully closed.
1: The mechanical gripper is neither fully closed nor fully opened.
2: The mechanical gripper is fully opened.
"""
return self._do('robotic_gripper status ?')
def get_ip(self) -> str:
"""
返回机甲IP。
get IP that the commander currently connects to.
:return: 机甲IP。 IP that the commander currently connects to
"""
assert not self._closed, 'connection is already closed'
return self._ip
def do(self, *args) -> str:
"""
执行任意命令。
Execute any command.
:param args: 命令内容。 command content.
:return: 命令返回。 the response of the command.
"""
with self._mu:
return self._do(*args)
def version(self) -> str:
"""
查询当前机甲的SDK版本。
query robomaster SDK version
:return: SDK版本号。 SDK version string.
"""
return self.do('version')
def robot_mode(self, mode: str) -> str:
"""
更改机甲的运动模式。
Update Robomaster's movement mode.
:param mode: 三种模式之一见enum MODE_*。 Movement mode, refer to enum MODE_*
:return: ok否则raise。 ok, or raise certain exception.
"""
assert mode in MODE_ENUMS, f'unknown mode {mode}'
resp = self.do('robot', 'mode', mode)
assert self._is_ok(resp), f'robot_mode: {resp}'
return resp
def get_robot_mode(self) -> str:
"""
查询当前机甲的运动模式。
Query for Robomaster's current movement mode.
:return: 三种模式之一见enum MODE_*。 Movement mode, refer to enum MODE_*
"""
resp = self.do('robot', 'mode', '?')
assert resp in MODE_ENUMS, f'unexpected robot mode result: {resp}'
return resp
def chassis_speed(self, x: float = 0, y: float = 0, z: float = 0) -> str:
"""
更改底盘运动速度。
Update chassis speed.
:param x: x 轴向运动速度,单位 m/s speed in x axis, in m/s
:param y: y 轴向运动速度,单位 m/s speed in y axis, in m/s
:param z: z 轴向旋转速度,单位 °/s rotation speed in z axis, in °/s
:return: ok否则raise。 ok, or raise certain exception.
"""
assert -3.5 <= x <= 3.5, f'x {x} is out of range'
assert -3.5 <= y <= 3.5, f'y {y} is out of range'
assert -600 <= z <= 600, f'z {z} is out of range'
resp = self.do('chassis', 'speed', 'x', x, 'y', y, 'z', z)
assert self._is_ok(resp), f'chassis_speed: {resp}'
return resp
def get_chassis_speed(self) -> ChassisSpeed:
"""
获取底盘速度。
Query for chassis speed.
:return: x 轴向运动速度(m/s)y 轴向运动速度(m/s)z 轴向旋转速度(°/s)w1 右前麦轮速度(rpm)w2 左前麦轮速速(rpm)w3 右后麦轮速度(rpm)w4 左后麦轮速度(rpm)。
speed in x axis(m/s), speed in y axis(m/s), rotation speed in z axis(°/s), w1(right front) wheel speed(rpm), w2(left front) wheel speed(rpm), w3(right back) wheel speed(rpm), w4(left back) wheel speed(rpm)
"""
resp = self.do('chassis', 'speed', '?')
ans = resp.split(' ')
assert len(ans) == 7, f'get_chassis_speed: {resp}'
return ChassisSpeed(x=float(ans[0]), y=float(ans[1]), z=float(ans[2]), w1=int(ans[3]), w2=int(ans[4]), w3=int(ans[5]), w4=int(ans[6]))
def chassis_wheel(self, w1: int = 0, w2: int = 0, w3: int = 0, w4: int = 0) -> str:
"""
更改底盘轮子速度。
Update chassis wheel rotation speed.
:param w1: 右前麦轮速度,单位 rpm w1(right front) wheel speed(rpm)
:param w2: 左前麦轮速度,单位 rpm w2(left front) wheel speed(rpm)
:param w3: 右后麦轮速度,单位 rpm w3(right back) wheel speed(rpm)
:param w4: 左后麦轮速度,单位 rpm w4(left back) wheel speed(rpm)
:return ok: ok否则raise。 ok, or raise certain exception.
"""
for i, v in enumerate((w1, w2, w3, w4)):
assert -1000 <= v <= 1000, f'w{i + 1} {v} is out of range'
resp = self.do('chassis', 'wheel', 'w1', w1, 'w2', w2, 'w3', w3, 'w4', w4)
assert self._is_ok(resp), f'chassis_wheel: {resp}'
return resp
def chassis_move(self, x: float = 0, y: float = 0, z: float = 0, speed_xy: float = None, speed_z: float = None) -> str:
"""
控制底盘运动当指定位置,坐标轴原点为当前位置。
Make chassis move to specified location. The origin is current location.
:param x: x 轴向运动距离,单位 m movement in x axis, in meter
:param y: y 轴向运动距离,单位 m movement in y axis, in meter
:param z: z 轴向旋转角度,单位 ° movement in z axis, in degree
:param speed_xy: xy 轴向运动速度,单位 m/s speed in both x and y axis, in meter/second
:param speed_z: z 轴向旋转速度, 单位 °/s speed in z axis, in degree/second
:return ok: ok否则raise。 ok, or raise certain exception.
"""
assert -5 <= x <= 5, f'x {x} is out of range'
assert -5 <= y <= 5, f'y {y} is out of range'
assert -1800 <= z <= 1800, f'z {z} is out of range'
assert speed_xy is None or 0 < speed_xy <= 3.5, f'speed_xy {speed_xy} is out of range'
assert speed_z is None or 0 < speed_z <= 600, f'speed_z {speed_z} is out of range'
cmd = ['chassis', 'move', 'x', x, 'y', y, 'z', z]
if speed_xy is not None:
cmd += ['vxy', speed_xy]
if speed_z is not None:
cmd += ['vz', speed_z]
resp = self.do(*cmd)
assert self._is_ok(resp), f'chassis_move: {resp}'
return resp
def get_chassis_position(self) -> ChassisPosition:
"""
获取底盘当前的位置(以上电时刻位置为原点)。
Query for chassis location. The origin is where Robomaster powers on.
:return: x 轴位置(m)y 轴位置(m),偏航角度(°)。 location consisting of x, y, z, in meter, meter, degree.
"""
resp = self.do('chassis', 'position', '?')
ans = resp.split(' ')
assert len(ans) == 3, f'get_chassis_position: {resp}'
return ChassisPosition(float(ans[0]), float(ans[1]), float(ans[2]))
def get_chassis_attitude(self) -> ChassisAttitude:
"""
获取底盘姿态信息。
Query for chassis attitude.
:return: pitch 轴角度(°)roll 轴角度(°)yaw 轴角度(°)。 pitch, roll, yaw in degree.
"""
resp = self.do('chassis', 'attitude', '?')
ans = resp.split(' ')
assert len(ans) == 3, f'get_chassis_attitude: {resp}'
return ChassisAttitude(float(ans[0]), float(ans[1]), float(ans[2]))
def get_chassis_status(self) -> ChassisStatus:
"""
获取底盘状态信息。
Query for chassis status.
:return: 底盘状态,详见 ChassisStatus chassis status, see class ChassisStatus.
"""
resp = self.do('chassis', 'status', '?')
ans = resp.split(' ')
assert len(ans) == 11, f'get_chassis_status: {resp}'
return ChassisStatus(*map(lambda x: bool(int(x)), ans))
def chassis_push_on(self, position_freq: int = None, attitude_freq: int = None, status_freq: int = None, all_freq: int = None) -> str:
"""
打开底盘中相应属性的信息推送,支持的频率 1, 5, 10, 20, 30, 50.
Enable chassis push of specified attribution. Supported frequencies are 1, 5, 10, 20, 30, 50.
:param position_freq: 位置推送频率不设定则设为None. position push frequency, None for no-op.
:param attitude_freq: 姿态推送频率不设定则设为None. attitude push frequency, None for no-op.
:param status_freq: 状态推送频率不设定则设为None. status push frequency, None for no-op.
:param all_freq: 统一设置所有推送频率,设置则开启所有推送。 update all push frequency, this affects all attribution.
:return: ok否则raise。 ok, or raise certain exception.
"""
valid_frequencies = (1, 5, 10, 20, 30, 50)
cmd = ['chassis', 'push']
if all_freq is not None:
assert all_freq in valid_frequencies, f'all_freq {all_freq} is not valid'
cmd += ['freq', all_freq]
else:
if position_freq is not None:
assert position_freq in valid_frequencies, f'position_freq {position_freq} is not valid'
cmd += ['position', SWITCH_ON, 'pfreq', position_freq]
if attitude_freq is not None:
assert attitude_freq in valid_frequencies, f'attitude_freq {attitude_freq} is not valid'
cmd += ['attitude', SWITCH_ON, 'afreq', attitude_freq]
if status_freq is not None:
assert status_freq in valid_frequencies, f'status_freq {status_freq} is not valid'
cmd += ['status', SWITCH_ON, 'sfreq', status_freq]
assert len(cmd) > 2, 'at least one argument should not be None'
resp = self.do(*cmd)
assert self._is_ok(resp), f'chassis_push_on: {resp}'
return resp
def chassis_push_off(self, position: bool = False, attitude: bool = False, status: bool = False, all: bool = False) -> str:
"""
关闭底盘中相应属性的信息推送。
Disable chassis push of specified attribution.
:param position: 是否关闭位置推送。 whether disable position push.
:param attitude: 是否关闭姿态推送。 whether disable attitude push.
:param status: 是否关闭状态推送。 whether disable status push.
:param all: 关闭所有推送。 whether disable all pushes.
:return: ok否则raise。 ok, or raise certain exception.
"""
cmd = ['chassis', 'push']
if all or position:
cmd += ['position', SWITCH_OFF]
if all or attitude:
cmd += ['attitude', SWITCH_OFF]
if all or status:
cmd += ['status', SWITCH_OFF]
assert len(cmd) > 2, 'at least one argument should be True'
resp = self.do(*cmd)
assert self._is_ok(resp), f'chassis_push_off: {resp}'
return resp
def gimbal_speed(self, pitch: float, yaw: float) -> str:
"""
控制云台运动速度。
Update gimbal speed.
:param pitch: pitch 轴速度,单位 °/s Pitch speed in °/s
:param yaw: yaw 轴速度,单位 °/s yaw speed in °/s
:return: ok否则raise。 ok, or raise certain exception.
"""
assert -450 <= pitch <= 450, f'pitch {pitch} is out of range'
assert -450 <= yaw <= 450, f'yaw {yaw} is out of range'
resp = self.do('gimbal', 'speed', 'p', pitch, 'y', yaw)
assert self._is_ok(resp), f'gimbal_speed: {resp}'
return resp
def gimbal_move(self, pitch: float = 0, yaw: float = 0, pitch_speed: float = None, yaw_speed: float = None) -> str:
"""
控制云台运动到指定位置,坐标轴原点为当前位置。
Make gimbal move to specified location. The origin is current location.
:param pitch: pitch 轴角度, 单位 ° pitch delta in degree
:param yaw: yaw 轴角度, 单位 ° yaw delta in degree
:param pitch_speed: pitch 轴运动速速,单位 °/s pitch speed in °/s
:param yaw_speed: yaw 轴运动速速,单位 °/s yaw speed in °/s
:return: ok否则raise。 ok, or raise certain exception.
"""
assert -55 <= pitch <= 55, f'pitch {pitch} is out of range'
assert -55 <= yaw <= 55, f'yaw {yaw} is out of range'
assert pitch_speed is None or 0 < pitch_speed <= 540, f'pitch_speed {pitch_speed} is out of range'
assert yaw_speed is None or 0 < yaw_speed <= 540, f'yaw_speed {yaw_speed} is out of range'
cmd = ['gimbal', 'move', 'p', pitch, 'y', yaw]
if pitch_speed is not None:
cmd += ['vp', pitch_speed]
if yaw_speed is not None:
cmd += ['vy', yaw_speed]
resp = self.do(*cmd)
assert self._is_ok(resp), f'gimbal_move: {resp}'
return resp
def gimbal_moveto(self, pitch: float = 0, yaw: float = 0, pitch_speed: float = None, yaw_speed: float = None) -> str:
"""
控制云台运动到指定位置,坐标轴原点为上电位置。
Make gimbal move to specified location. The origin is gimbal center.
:param pitch: pitch 轴角度, 单位 ° pitch delta in degree
:param yaw: yaw 轴角度, 单位 ° yaw delta in degree
:param pitch_speed: pitch 轴运动速速,单位 °/s pitch speed in °/s
:param yaw_speed: yaw 轴运动速速,单位 °/s yaw speed in °/s
:return: ok否则raise。 ok, or raise certain exception.
"""
assert -25 <= pitch <= 30, f'pitch {pitch} is out of range'
assert -250 <= yaw <= 250, f'yaw {yaw} is out of range'
assert pitch_speed is None or 0 < pitch_speed <= 540, f'pitch_speed {pitch_speed} is out of range'
assert yaw_speed is None or 0 < yaw_speed <= 540, f'yaw_speed {yaw_speed} is out of range'
cmd = ['gimbal', 'moveto', 'p', pitch, 'y', yaw]
if pitch_speed is not None:
cmd += ['vp', pitch_speed]
if yaw_speed is not None:
cmd += ['vy', yaw_speed]
resp = self.do(*cmd)
assert self._is_ok(resp), f'gimbal_moveto: {resp}'
return resp
def gimbal_suspend(self):
"""
使云台进入休眠状态。
Suspend(sleep) the gimbal.
:return: ok否则raise。 ok, or raise certain exception.
"""
resp = self.do('gimbal', 'suspend')
assert self._is_ok(resp), f'gimbal_suspend: {resp}'
return resp
def gimbal_resume(self):
"""
控制云台从休眠状态中恢复
awake the gimbal if it is suspended(sleeping).
:return: ok否则raise。 ok, or raise certain exception.
"""
resp = self.do('gimbal', 'resume')
assert self._is_ok(resp), f'gimbal_resume: {resp}'
return resp
def gimbal_recenter(self):
"""
控制云台回中。
Recenter the gimbal.
:return: ok否则raise。 ok, or raise certain exception.
"""
resp = self.do('gimbal', 'recenter')
assert self._is_ok(resp), f'gimbal_recenter: {resp}'
return resp
def get_gimbal_attitude(self) -> GimbalAttitude:
"""
获取云台姿态信息。
Query for gimbal attitude.
:return: pitch 轴角度(°)yaw 轴角度(°) pitch, yaw in degree
"""
resp = self.do('gimbal', 'attitude', '?')
ans = resp.split(' ')
assert len(ans) == 2, f'get_gimbal_attitude: {resp}'
return GimbalAttitude(pitch=float(ans[0]), yaw=float(ans[1]))
def gimbal_push_on(self, attitude_freq: int = 5) -> str:
"""
打开云台中相应属性的信息推送,支持的频率 1, 5, 10, 20, 30, 50.
Enable gimbal attribution push. Supported frequencies are 1, 5, 10, 20, 30, 50.
:param attitude_freq: 姿态推送频率. attitude push frequency.
:return: ok否则raise。 ok, or raise certain exception.
"""
valid_frequencies = (1, 5, 10, 20, 30, 50)
assert attitude_freq in valid_frequencies, f'invalid attitude_freq {attitude_freq}'
resp = self.do('gimbal', 'push', 'attitude', SWITCH_ON, 'afreq', attitude_freq)
assert self._is_ok(resp), f'gimbal_push_on: {resp}'
return resp
def gimbal_push_off(self, attitude: bool = True) -> str:
"""
关闭云台中相应属性的信息推送。
Disable gimbal push of specified attribution.
:param attitude: 关闭姿态推送。 whether disable attitude push.
:return: ok否则raise。 ok, or raise certain exception.
"""
assert attitude, 'at least one augment should be True'
resp = self.do('gimbal', 'push', 'attitude', SWITCH_OFF)
assert self._is_ok(resp), f'gimbal_push_off: {resp}'
return resp
def armor_sensitivity(self, value: int) -> str:
"""
设置装甲板打击检测灵敏度。
Update armor sensitivity.
:param value: 装甲板灵敏度,数值越大,越容易检测到打击。默认灵敏度值为 5.
armor sensitivity, the bigger, the more sensitive. Default to 5.
:return: ok否则raise。 ok, or raise certain exception.
"""
assert 1 <= value <= 10, f'value {value} is out of range'
resp = self.do('armor', 'sensitivity', value)
assert self._is_ok(resp), f'armor_sensitivity: {resp}'
return resp
def get_armor_sensitivity(self) -> int:
"""
获取装甲板打击检测灵敏度。
Query for armor sensitivity.
:return: 装甲板灵敏度 armor sensitivity.
"""
resp = self.do('armor', 'sensitivity', '?')
return int(resp)
def armor_event(self, attr: str, switch: bool) -> str:
"""
控制装甲板检测事件上报。
Enable or disable specified armor event.
:param attr: 事件属性名称,范围见 ARMOR_ENUMS. armor event name, see ARMOR_ENUMS.
:param switch: 是否开启上报 on/off
:return: ok否则raise。 ok, or raise certain exception.
"""
assert attr in ARMOR_ENUMS, f'unexpected armor event attr {attr}'
resp = self.do('armor', 'event', attr, SWITCH_ON if switch else SWITCH_OFF)
assert self._is_ok(resp), f'armor_event: {resp}'
return resp
def sound_event(self, attr: str, switch: bool) -> str:
"""
控制声音识别事件上报。
Enable or disable specified sound event.
:param attr: 事件属性名称,范围见 SOUND_ENUMS. sound event name, see ARMOR_ENUMS.
:param switch: 是否开启上报 on/off
:return: ok否则raise。 ok, or raise certain exception.
"""
assert attr in SOUND_ENUMS, f'unexpected armor event attr {attr}'
resp = self.do('sound', 'event', attr, SWITCH_ON if switch else SWITCH_OFF)
assert self._is_ok(resp), f'armor_event: {resp}'
return resp
def led_control(self, comp: str, effect: str, r: int, g: int, b: int) -> str:
"""
控制 LED 灯效。跑马灯效果仅可作用于云台两侧 LED。
Update LED effects. Note scrolling effect works only on gimbal LEDs.
:param comp: LED 编号,见 LED_ENUMS LED composition, see LED_ENUMS
:param effect: 灯效类型,见 LED_EFFECT_ENUMS effect type, see LED_EFFECT_ENUMS
:param r: RGB 红色分量值 RGB red value
:param g: RGB 绿色分量值 RGB green value
:param b: RGB 蓝色分量值 RGB blue value
:return: ok否则raise。 ok, or raise certain exception.
"""
assert comp in LED_ENUMS, f'unknown comp {comp}'
assert effect in LED_EFFECT_ENUMS, f'unknown effect {effect}'
assert 0 <= r <= 255, f'r {r} is out of scope'
assert 0 <= g <= 255, f'g {g} is out of scope'
assert 0 <= b <= 255, f'b {b} is out of scope'
if effect == LED_EFFECT_SCROLLING:
assert comp in (LED_TOP_ALL, LED_TOP_LEFT, LED_TOP_RIGHT), 'scrolling effect works only on gimbal LEDs'
resp = self.do('led', 'control', 'comp', comp, 'r', r, 'g', g, 'b', b, 'effect', effect)
assert self._is_ok(resp), f'led_control: {resp}'
return resp
def ir_sensor_measure(self, switch: bool = True):
"""
打开/关闭所有红外传感器开关。
Enable or disable all IR sensor.
:param switch: 打开/关闭 on/off
:return: ok否则raise。 ok, or raise certain exception.
"""
resp = self.do('ir_distance_sensor', 'measure', SWITCH_ON if switch else SWITCH_OFF)
assert self._is_ok(resp), f'ir_sensor_measure: {resp}'
return resp
def get_ir_sensor_distance(self, id: int) -> float:
"""
获取指定 ID 的红外深度传感器距离。
Query for distance reported by specified IR sensor.
:param id: 红外传感器的 ID ID of IR sensor
:return: 指定 ID 的红外传感器测得的距离值,单位 mm distance in mm
"""
assert 1 <= id <= 4, f'invalid IR sensor id {id}'
resp = self.do('ir_distance_sensor', 'distance', id, '?')
return float(resp)
def stream(self, switch: bool) -> str:
"""
视频流开关控制。
Enable or disable video stream.
:param switch: 打开/关闭 on/off
:return: ok否则raise。 ok, or raise certain exception.
"""
resp = self.do('stream', SWITCH_ON if switch else SWITCH_OFF)
assert self._is_ok(resp), f'stream: {resp}'
return resp
def audio(self, switch: bool) -> str:
"""
音频流开关控制。
Enable or disable audio stream.
:param switch: 打开/关闭 on/off
:return: ok否则raise。 ok, or raise certain exception.
"""
resp = self.do('audio', SWITCH_ON if switch else SWITCH_OFF)
assert self._is_ok(resp), f'audio: {resp}'
return resp
def blaster_fire(self) -> str:
"""
控制水弹枪发射一次。
Fire once.
:return: ok否则raise。 ok, or raise certain exception.
"""
resp = self.do('blaster', 'fire')
assert self._is_ok(resp), f'blaster_fire: {resp}'
return resp

View File

@@ -0,0 +1,600 @@
# -*- coding: utf-8 -*-
# ██████╗ ██████╗ ██████╗ ██████╗ ███╗ ███╗ █████╗ ███████╗████████╗███████╗██████╗ ██████╗ ██╗ ██╗
# ██╔══██╗██╔═══██╗██╔══██╗██╔═══██╗████╗ ████║██╔══██╗██╔════╝╚══██╔══╝██╔════╝██╔══██╗██╔══██╗╚██╗ ██╔╝
# ██████╔╝██║ ██║██████╔╝██║ ██║██╔████╔██║███████║███████╗ ██║ █████╗ ██████╔╝██████╔╝ ╚████╔╝
# ██╔══██╗██║ ██║██╔══██╗██║ ██║██║╚██╔╝██║██╔══██║╚════██║ ██║ ██╔══╝ ██╔══██╗██╔═══╝ ╚██╔╝
# ██║ ██║╚██████╔╝██████╔╝╚██████╔╝██║ ╚═╝ ██║██║ ██║███████║ ██║ ███████╗██║ ██║██║ ██║
# ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝ ╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝
import logging
import multiprocessing as mp
import queue
import signal
import socket
import sys
import time
from typing import List, Callable, Tuple, Optional, Iterator
import cv2 as cv
from .client import CTX, LOG_LEVEL, PUSH_PORT, GimbalAttitude, ChassisPosition, ChassisAttitude, ChassisStatus, DEFAULT_BUF_SIZE, EVENT_PORT, ARMOR_HIT, ArmorHitEvent, SOUND_APPLAUSE, SoundApplauseEvent, VIDEO_PORT, Commander
class Worker:
"""
用户逻辑的载体,继承这个类然后将你的逻辑写到 ``work()`` 方法中即可。
你的逻辑可以是有状态的或无状态的,如果需要,你可以在继承的新类中使用自定义的任意属性保存你的状态。
如果你需要打印日志,使用 ``Worker.logger`` 属性。
创建你的新类的实例后使用 ``Hub.worker()`` 注册到Hub实例供其调用。
一个Worker子类一般只做一件事情多个Worker子类各司其职相互协作通过 ``multiprocessing.Queue`` 进行单向通讯,
最终负责传感器的Worker的数据会汇聚到负责控制的Worker中负责控制的Worker再使用 ``Commander`` 向机甲下令。
RoboMasterPy.framework中提供了多个定制化开箱即用的Worker以满足
接收视频流Vision、接收事件EventListener和推送PushListener、汇聚信息控制机甲Mind等常见需求
请参阅API文档中的“预置Worker”部分。
Worker takes user's business logic. Inherit this class and write logic code in ``work()`` method.
A worker can be stateful or stateless, at your choice. You may use some user-defined attributes to store your state if the need arises.
Use ``Worker.logger`` attribute if some logs need to be printed.
Register your well-defined new worker to hub using ``Hub.worker()`` so that hub schedule and calls your logic.
One Worker subclass nearly always does only one business. Multiple Worker subclasses do their own jobs and cooperate,
communicate through ``multiprocessing.Queue`` in one-way fashion.
Data from those subclasses in charge of sensor flows into subclass in charge of controlling, who command your Robomaster by ``Commander``.
RoboMasterPy.framework provides many customized and out-of-box Worker to cover common usage like
receiving video stream(vision), receiving events(EventListener) & pushes(PushListener), gathering info and controlling the Robomaster(Mind), etc.
Please refer to "Sugared Workers" section in API doc.
"""
QUEUE_TIMEOUT: float = 0.05
def __init__(self, name: str, out: Optional[mp.Queue], protocol: Optional[str], address: Tuple[str, int], timeout: Optional[float], loop: bool = True):
"""
初始化Worker这个方法一般在子类的__init__()方法中调用,不会直接使用。
Initialize Worker, called in its subclasses's __init__(), seldom used directly.
:param name: worker实例的名称这个名称也会作为进程的名称你应该使用一个有利于调试的描述性好的名字。
name of Worker instance, which is also the name of the process. Choose wisely to benefit when debugging.
:param out: 可选用于输出产物的multiprocessing.Queue定义后Worker._outlet()方法可用。
(Optional) a multiprocessing.Queue to put worker's product. Worker._outlet() is available after this parameter is defined.
:param protocol: 连接机甲的协议名称从tcp, udp和None中选择在tcp和udp选项下Worker._intake()方法可用。
Protocol to use to connect your Robomaster, choose one in tcp, udp, None. Worker._intake() is available under tcp or udp.
:param address: 机甲的IP地址和端口号可从Commander.get_ip()中获得机甲IP端口号和业务有关见framework.*_PORT.
IP and port to Robomaster. IP is available using Commander.get_ip(), and refer to framework.*_PORT for port number.
:param timeout: tcp或udp的连接超时。 timeout of tcp or udp.
:param loop: 是否在Worker的生命周期中循环调用work()方法常见True在方法提供自己的生命周期管理的时候可选False.
Whether call work() method in a loop for Worker's lifetime, usually True. Set to False when work() has its own lifecycle management.
"""
assert name is not None and name != '', 'choose a good name to make life easier'
self._mu = CTX.Lock()
with self._mu:
signal.signal(signal.SIGINT, self._handle_close_signal)
signal.signal(signal.SIGTERM, self._handle_close_signal)
self._name: str = name
self._closed: bool = False
self._address: Tuple[str, int] = address
self._out: Optional[mp.Queue] = out
self._logger: logging.Logger = logging.getLogger(name)
self._logger.setLevel(LOG_LEVEL)
handler = logging.StreamHandler(sys.stdout)
formatter = logging.Formatter('%(asctime)s %(name)-12s : %(levelname)-8s %(message)s')
handler.setFormatter(formatter)
self._logger.addHandler(handler)
self._loop: bool = loop
if protocol == 'tcp':
self._conn: socket.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self._conn.settimeout(timeout)
self._conn.connect(self._address)
elif protocol == 'udp':
self._conn: socket.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self._conn.settimeout(timeout)
self._conn.bind(self._address)
elif protocol is None:
self._conn: Optional[socket.socket] = None
pass
else:
raise ValueError(f'unknown protocol {protocol}')
def _handle_close_signal(self, sig, stacks):
self.close()
@property
def closed(self) -> bool:
"""
当前Worker是否已经永久停止。
Whether the worker has stopped working eventually.
:return: True for stopped.
"""
return self._closed
def close(self):
"""
让Worker停止工作本方法一般由Hub调用。
Let worker stop. Nearly always called by Hub.
"""
with self._mu:
if self.closed:
return
self._closed = True
self.logger.info('signal received, closing...')
if self._conn is not None:
self._conn.close()
if self._out is not None and type(self._out) == mp.Queue:
self._out.close()
@property
def name(self) -> str:
"""
当前Worker的名字。
The worker's name.
"""
return self._name
def work(self) -> None:
"""
在本方法中实现你的业务逻辑,你可能需要在这里使用下列方法和属性:
* 使用 ``self._intake()`` 方法从tcp或udp中获取数据
* 使用 ``self._outlet()`` 方法将产物放到out中注意如果out被没有即时消费的产物填满self._outlet()会丢弃最新的产物;
* 使用 ``self.logger`` 属性打印日志。
Implement your business logic in this method. These methods and attributes may be useful:
* use ``self._intake()`` method to intake data from tcp or udp connection;
* use ``self._outlet()`` to put product to ``out``. Keep in mind if ``out`` is filled up with unconsumed product, self._outlet() discards the latest products.
* use ``self.logger`` for log printing.
"""
raise NotImplementedError('implement me')
def __call__(self) -> None:
try:
if self._loop:
while not self.closed:
self.work()
else:
self.work()
except EOFError:
if not self.closed:
raise
finally:
self.close()
@property
def logger(self) -> logging.Logger:
"""
使用本属性打印日志。
Use this attribute for logging.
"""
return self._logger
def _assert_ready(self):
assert not self.closed, 'Worker is already closed'
def _intake(self, buf_size: int):
self._assert_ready()
return self._conn.recv(buf_size)
def _outlet(self, payload):
self._assert_ready()
while not self.closed:
try:
self._out.put(payload, block=True, timeout=self.QUEUE_TIMEOUT)
except queue.Full:
continue
break
def get_address(self) -> Tuple[str, int]:
"""
获取Worker连接的IP和port.
get IP and port which the worker currently connects to.
:return: (IP, port)
"""
return self._address
def __enter__(self):
return self
def __exit__(self):
self.close()
class Hub:
"""
程序中枢。
* 使用 ``self.worker()`` 注册你的Worker
* 使用 ``self.run()`` 开始运行;
* 使用 ``Ctrl + C`` 停止程序。
Hub is the orchestrator.
* Use ``self.worker()`` to register your worker;
* Use ``self.run()`` to run;
* Use ``Ctrl + C`` for exiting.
"""
TERMINATION_TIMEOUT = 10
def __init__(self):
"""
创建Hub不需要提供参数。
Hub does not need parameters to initialize.
"""
self._mu = CTX.Lock()
with self._mu:
self._closed: bool = False
self._workers: List = []
def close(self):
"""
让Hub以及Hub名下的所有Worker停止工作。这个方法不需要用户来调用。
Stop hub and workers registered under hub. This method does not need to be called by user.
"""
with self._mu:
if self._closed:
return
self._closed = True
end_time = time.time() + self.TERMINATION_TIMEOUT
for worker in self._workers:
remain_time = max(0.0, end_time - time.time())
worker.join(remain_time)
for worker in self._workers:
if worker.is_alive():
worker.terminate()
def _assert_ready(self):
assert not self._closed, 'Hub is closed'
def __enter__(self):
return self
def __exit__(self):
self.close()
def worker(self, worker_class, name: str, args: Tuple = (), kwargs=None):
"""
将worker注册到hub.
所有的worker都在独立的进程中运行。
Register worker to hub.
All workers run in their own operating system process.
:param worker_class: worker的类注意不是worker实例。
class of worker to be registered, note provide the class, instead of an instance.
:param name: worker的名字选个好名字可以让调试更容易。
worker's name. A good name makes debugging less painful.
:param args: 创建worker需要使用的参数。
args to initialize the worker.
:param kwargs: 创建worker需要使用的kwargs参数。
kwargs to initialize the worker.
"""
if kwargs is None:
kwargs = {}
process = CTX.Process(name=name, target=self._build_worker_and_run, args=(worker_class, name, *args), kwargs=kwargs)
self._workers.append(process)
@staticmethod
def _build_worker_and_run(*args, **kwargs):
worker_class = args[0]
worker = worker_class(*args[1:], **kwargs)
worker()
def run(self):
"""
按注册顺序启动所有worker阻塞主进程。
Hub在接收到 ``SIGTERM`` 或者 ``SIGINT`` 时会尝试安全退出。
Start workers and block the main process.
Hub tries to shutdown itself gracefully when receiving
``SIGTERM`` or ``SIGINT``.
"""
with self._mu:
self._assert_ready()
assert len(self._workers) > 0, 'no worker registered'
for worker in self._workers:
worker.start()
signal.sigwait((signal.SIGINT, signal.SIGTERM))
self.close()
for worker in self._workers:
try:
# close method does not exist until Python 3.7 and better
if getattr(worker, 'close', False):
worker.close()
except Exception as e:
logging.error('[resource leak warning] failed to close process "%s": %s', worker._name, e)
class PushListener(Worker):
"""
监听并解析机甲大师的推送,输出强类型的推送内容。
Listen and parse pushes from Robomaster, product parsed pushes in strong typed manner.
"""
PUSH_TYPE_CHASSIS: str = 'chassis'
PUSH_TYPE_GIMBAL: str = 'gimbal'
PUSH_TYPES: Tuple[str] = (PUSH_TYPE_CHASSIS, PUSH_TYPE_GIMBAL)
def __init__(self, name: str, out: mp.Queue):
"""
初始化自身。
Initialize self.
:param name: worker名称 name of worker
:param out: PushListener会将产物放入其中以供下游消费。
PushListener puts product into ``out`` for downstream consuming.
"""
super().__init__(name, out, 'udp', ('', PUSH_PORT), None)
def _parse(self, msg: str) -> List:
payloads: Iterator[str] = map(lambda x: x.strip(), msg.strip(' ;').split(';'))
current_push_type: Optional[str] = None
has_type_prefix: bool = False
parsed: List = []
for index, payload in enumerate(payloads):
words = payload.split(' ')
assert len(words) > 1, f'unexpected payload at index {index}, context: {msg}'
if words[0] in self.PUSH_TYPES:
current_push_type = words[0]
has_type_prefix = True
else:
has_type_prefix = False
assert current_push_type is not None, f'can not decide push type of payload at index {index}, context: {msg}'
if current_push_type == self.PUSH_TYPE_CHASSIS:
parsed.append(self._parse_chassis_push(words, has_type_prefix))
elif current_push_type == self.PUSH_TYPE_GIMBAL:
parsed.append(self._parse_gimbal_push(words, has_type_prefix))
else:
raise ValueError(f'unknown push type {current_push_type} at index {index}, context: {msg}')
return parsed
@staticmethod
def _parse_gimbal_push(words: List[str], has_type_prefix: bool):
subtype: str = ''
if has_type_prefix:
assert len(words) > 3, f'invalid gimbal push payload, words: {words}'
subtype = words[2]
else:
assert len(words) > 1, f'invalid gimbal push payload, words: {words}'
subtype = words[0]
if subtype == 'attitude':
return GimbalAttitude(float(words[-2]), float(words[-1]))
else:
raise ValueError(f'unknown gimbal push subtype {subtype}, context: {words}')
@staticmethod
def _parse_chassis_push(words: List[str], has_type_prefix: bool):
subtype: str = ''
if has_type_prefix:
assert len(words) > 3, f'invalid chassis push payload, words: {words}'
subtype = words[2]
else:
assert len(words) > 1, f'invalid chassis push payload, words: {words}'
subtype = words[0]
if subtype == 'position':
return ChassisPosition(float(words[-2]), float(words[-1]), None)
elif subtype == 'attitude':
return ChassisAttitude(float(words[-3]), float(words[-2]), float(words[-1]))
elif subtype == 'status':
ans = words[-11:]
assert len(ans) == 11, f'invalid chassis status payload, words: {words}'
return ChassisStatus(*map(lambda x: bool(int(x)), ans))
else:
raise ValueError(f'unknown chassis push subtype {subtype}, context: {words}')
def work(self) -> None:
try:
msg = self._intake(DEFAULT_BUF_SIZE).decode()
except OSError:
if self.closed:
return
else:
raise
payloads = self._parse(msg)
for payload in payloads:
self._outlet(payload)
class EventListener(Worker):
"""
监听并解析机甲大师的事件,输出强类型的推送内容。
Listen and parse events from Robomaster, product parsed events in strong typed manner.
"""
EVENT_TYPE_ARMOR: str = 'armor'
EVENT_TYPE_SOUND: str = 'sound'
EVENT_TYPES: Tuple[str] = (EVENT_TYPE_ARMOR, EVENT_TYPE_SOUND)
def __init__(self, name: str, out: mp.Queue, ip: str):
"""
初始化自身。
Initialize self.
:param name: worker名称 name of worker
:param out: PushListener会将产物放入其中以供下游消费。
PushListener puts product into ``out`` for downstream consuming.
:param ip: 机甲的IP可从Commander.get_ip()取得。
IP of your Robomaster, can be obtained from Commander.get_ip()
"""
super().__init__(name, out, 'tcp', (ip, EVENT_PORT), None)
def _parse(self, msg: str) -> List:
payloads: Iterator[str] = map(lambda x: x.strip(), msg.strip(' ;').split(';'))
current_event_type: Optional[str] = None
has_type_prefix: bool = False
parsed: List = []
for index, payload in enumerate(payloads):
words = payload.split(' ')
assert len(words) > 1, f'unexpected payload at index {index}, context: {msg}'
if words[0] in self.EVENT_TYPES:
current_event_type = words[0]
has_type_prefix = True
else:
has_type_prefix = False
assert current_event_type is not None, f'can not decide event type of payload at index {index}, context: {msg}'
if current_event_type == self.EVENT_TYPE_ARMOR:
parsed.append(self._parse_armor_event(words, has_type_prefix))
elif current_event_type == self.EVENT_TYPE_SOUND:
parsed.append(self._parse_sound_event(words, has_type_prefix))
else:
raise ValueError(f'unknown event type {current_event_type} at index {index}, context: {msg}')
return parsed
@staticmethod
def _parse_armor_event(words: List[str], has_type_prefix: bool):
subtype: str = ''
if has_type_prefix:
assert len(words) > 3, f'invalid armor event payload, words: {words}'
subtype = words[2]
else:
assert len(words) > 1, f'invalid armor event payload, words: {words}'
subtype = words[0]
if subtype == ARMOR_HIT:
return ArmorHitEvent(int(words[-2]), int(words[-1]))
else:
raise ValueError(f'unknown armor event subtype {subtype}, context: {words}')
@staticmethod
def _parse_sound_event(words: List[str], has_type_prefix: bool):
subtype: str = ''
if has_type_prefix:
assert len(words) > 3, f'invalid sound event payload, words: {words}'
subtype = words[2]
else:
assert len(words) > 1, f'invalid sound event payload, words: {words}'
subtype = words[0]
if subtype == SOUND_APPLAUSE:
return SoundApplauseEvent(int(words[-1]))
else:
raise ValueError(f'unknown sound event subtype {subtype}, context: {words}')
def work(self) -> None:
try:
msg = self._intake(DEFAULT_BUF_SIZE).decode()
except OSError:
if self.closed:
return
else:
raise
payloads = self._parse(msg)
for payload in payloads:
self._outlet(payload)
class Vision(Worker):
"""
拉取并解析机甲的视频流回调函数会收到解析好的OpenCV视频帧回调函数的返回值会被放置到 ``out`` 中。
Pull and parse Robomaster's video stream, call the callback with parsed OpenCV frame,
and put return value from callback into ``out``.
"""
TIMEOUT: float = 5.0
def __init__(self, name: str, out: Optional[mp.Queue], ip: str, processing: Callable[..., None], none_is_valid=False):
"""
初始化自身。
Initialize self.
:param name: worker名称 name of worker
:param out: PushListener会将产物放入其中以供下游消费。
PushListener puts product into ``out`` for downstream consuming.
:param ip: 机甲的IP可从Commander.get_ip()取得。
IP of your Robomaster, can be obtained from Commander.get_ip()
:param processing: 回调函数每当有新的视频帧到来时函数都会被Vision调用形如 ``processing(frame=frame, logger=self.logger)``
其中frame为cv2(OpenCV) framelogger可用于日志打印。
callback function, is called every time when a new frame comes, in form ``processing(frame=frame, logger=self.logger)``,
where frame is cv2(OpenCV) frame, and logger is for logging.
:param none_is_valid: 是否在回调函数返回None时将None放入 ``out`` 默认为False.
Whether to put None returned from callback function into ``out``, default to False.
"""
super().__init__(name, out, None, (ip, VIDEO_PORT), self.TIMEOUT)
self._none_is_valid = none_is_valid
self._processing = processing
self._cap = cv.VideoCapture(f'tcp://{ip}:{VIDEO_PORT}')
assert self._cap.isOpened(), 'failed to connect to video stream'
self._cap.set(cv.CAP_PROP_BUFFERSIZE, 4)
def close(self):
self._cap.release()
cv.destroyAllWindows()
super().close()
def work(self) -> None:
ok, frame = self._cap.read()
if not ok:
if self.closed:
return
else:
raise ValueError('can not receive frame (stream end?)')
processed = self._processing(frame=frame, logger=self.logger)
if processed is not None or self._none_is_valid:
self._outlet(processed)
class Mind(Worker):
"""
无状态的控制者,适用于简单的控制。
对于复杂的有状态的控制需求你需要自己继承Worker来实现。
Stateless controller, suits simple and naive controlling scenario.
For complicated, stateful controlling, inherit Worker to implement.
"""
def __init__(self, name: str, queues: Tuple[mp.Queue, ...], ip: str, processing: Callable[..., None], timeout: float = 30, loop: bool = True):
"""
初始化自身。
Initialize self.
:param name: worker名称 name of worker
:param queues: 队列元组,其中队列的内容由上游提供。
Tuple of mp.Queue, where their contents are provided by upstream.
:param ip: 机甲的IP可从Commander.get_ip()取得。
IP of your Robomaster, can be obtained from Commander.get_ip()
:param processing: 回调函数,调用时参数形如 ``processing(cmd=self._cmd, queues=self._queues, logger=self.logger)``
其中cmd为连接到机甲的Commanderqueues为输入的队列元组logger用于日志打印。
callback function, is called in form ``processing(cmd=self._cmd, queues=self._queues, logger=self.logger)``,
where cmd is a connected Commander, queue is the input tuple of mp.Queue, logger is for logging.
:param timeout: Commander的连接超时。
timeout for Commander.
:param loop: 是否循环调用回调函数processing
whether calls processing(callback) function in loop, default to True.
"""
super().__init__(name, None, None, (ip, 0), timeout, loop=loop)
self._queues = queues
self._processing = processing
self._cmd = Commander(ip, timeout)
def close(self):
self._cmd.close()
super().close()
def work(self) -> None:
self._processing(cmd=self._cmd, queues=self._queues, logger=self.logger)

View File

@@ -0,0 +1,61 @@
# -*- coding: utf-8 -*-
# ██████╗ ██████╗ ██████╗ ██████╗ ███╗ ███╗ █████╗ ███████╗████████╗███████╗██████╗ ██████╗ ██╗ ██╗
# ██╔══██╗██╔═══██╗██╔══██╗██╔═══██╗████╗ ████║██╔══██╗██╔════╝╚══██╔══╝██╔════╝██╔══██╗██╔══██╗╚██╗ ██╔╝
# ██████╔╝██║ ██║██████╔╝██║ ██║██╔████╔██║███████║███████╗ ██║ █████╗ ██████╔╝██████╔╝ ╚████╔╝
# ██╔══██╗██║ ██║██╔══██╗██║ ██║██║╚██╔╝██║██╔══██║╚════██║ ██║ ██╔══╝ ██╔══██╗██╔═══╝ ╚██╔╝
# ██║ ██║╚██████╔╝██████╔╝╚██████╔╝██║ ╚═╝ ██║██║ ██║███████║ ██║ ███████╗██║ ██║██║ ██║
# ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝ ╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝
import math
from typing import Tuple
FOCAL_LENGTH_HD: float = 710
HORIZONTAL_DEGREES: float = 96
HORIZONTAL_PIXELS: float = 1280
VERTICAL_DEGREES: float = 54
VERTICAL_PIXELS: float = 720
INFANTRY_LENGTH: float = 0.32
INFANTRY_WIDTH: float = 0.24
INFANTRY_HEIGHT: float = 0.27
ENGINEERING_LENGTH: float = 0.41
ENGINEERING_WIDTH: float = 0.24
ENGINEERING_HEIGHT: float = 0.33
def pinhole_distance(actual_size: float, pixel_size: float, focal_length: float = FOCAL_LENGTH_HD) -> float:
"""
使用针孔相机模型估测物体到相机的距离。
Estimate distance between camera and object using pinhole camera model.
:param actual_size: 实际大小,单位米。 Actual object size in meters.
:param pixel_size: 像素大小,单位像素。 Object size in pixels.
:param focal_length: 可选当前分辨率下的等效焦距和分辨率相关。默认使用1280*720分辨率下的数值。Perceived focal length at specified image resolution, default to value under 1280*720.
:return: 物体到相机的距离,单位米。 Distance between camera and object, in meters.
"""
return focal_length * actual_size / pixel_size
def distance_decomposition(pixel_x: float, distance: float, horizontal_pixels: float = HORIZONTAL_PIXELS, horizontal_degrees: float = HORIZONTAL_DEGREES) -> Tuple[float, float, float]:
"""
将距离分解为前进分量(前为正)和侧向分量(右为正)。本函数要求线段的两端在相同海拔高度。
Decomposition distance into forward vector(forward as positive) and lateral vector(right as positive).
This function requires that both ends of the line segment are at the same altitude.
:param pixel_x: 物体在图像上的x坐标单位像素。 the x coordinate of the object on the image, in pixels.
:param distance: 距离,单位米。 Distance in meter.
:param horizontal_pixels: 图像横向的像素数目默认1280. The number of pixels in the horizontal direction of the image, the default is 1280.
:param horizontal_degrees: 图像横向的视角大小默认96. The horizontal viewing angle of the image, the default is 96.
:return: 前进分量和侧向分量,单位米;水平偏转角度,单位度。 forward vector and lateral vector in meters; horizontal angle in degrees.
"""
horizontal_degree = horizontal_degrees * (pixel_x / horizontal_pixels - 0.5)
rad = horizontal_degree / 180 * math.pi
lateral = distance * math.sin(rad)
forward = distance * math.cos(rad)
return forward, lateral, horizontal_degree