Upload files to "controller/packagesrc"
This commit is contained in:
parent
5b53bdb499
commit
1c33489821
42
controller/packagesrc/__init__.py
Normal file
42
controller/packagesrc/__init__.py
Normal 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
|
19
controller/packagesrc/__version__.py
Normal file
19
controller/packagesrc/__version__.py
Normal 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'
|
775
controller/packagesrc/client.py
Normal file
775
controller/packagesrc/client.py
Normal 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
|
600
controller/packagesrc/framework.py
Normal file
600
controller/packagesrc/framework.py
Normal 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) frame,logger可用于日志打印。
|
||||||
|
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为连接到机甲的Commander,queues为输入的队列元组,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)
|
61
controller/packagesrc/measure.py
Normal file
61
controller/packagesrc/measure.py
Normal 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
|
Loading…
Reference in New Issue
Block a user