Esempio n. 1
0
class AioRobot:
    """Async/await mode of Cozmars robot

    :param serial_or_ip: IP address or serial number of Cozmars to connect. Default to None, in which case the program will automatically detect the robot on connection if there's only one found.
    :type serial_or_ip: str
    """
    def __init__(self, serial_or_ip=None):
        if serial_or_ip:
            self._host = 'rcute-cozmars-' + serial_or_ip + '.local' if len(
                serial_or_ip) == 4 else serial_or_ip
        self._mode = 'aio'
        self._connected = False
        self._env = env.Env(self)
        self._screen = screen.Screen(self)
        self._camera = camera.Camera(self)
        self._microphone = microphone.Microphone(self)
        self._button = button.Button(self)
        self._sonar = sonar.Sonar(self)
        self._infrared = infrared.Infrared(self)
        self._lift = lift.Lift(self)
        self._head = head.Head(self)
        self._buzzer = buzzer.Buzzer(self)
        self._speaker = speaker.Speaker(self)
        self._motor = motor.Motor(self)
        self._eye_anim = eye_animation.EyeAnimation(self)
        self.on_camera_image = None
        """Callback funciton. After :meth:`show_camera_view` is called, :data:`on_camera_image` will be called every time camera feed receives an new image. The callback must take the captured image as input and return processd image."""

    def _in_event_loop(self):
        return self._event_thread == threading.current_thread()

    @util.mode()
    async def animate(self, name, *args, **kwargs):
        """perform animation

        :param name: the name of the animation
        :type name: str
        """
        anim = animations[name]
        anim = getattr(anim, 'animate', anim)
        await anim(self, *args, **kwargs)

    @classmethod
    def get_animation_list(cl):
        """ """
        return list(animations.keys())

    @property
    def eyes(self):
        """eye animation on screen"""
        return self._eye_anim

    @property
    def env(self):
        """Environmental Variables"""
        return self._env

    @property
    def button(self):
        """ """
        return self._button

    @property
    def infrared(self):
        """Infrared sensor at the bottom of the robot"""
        return self._infrared

    @property
    def sonar(self):
        """Sonar, namely ultrasonic distance sensor"""
        return self._sonar

    @property
    def motor(self):
        """ """
        return self._motor

    @property
    def head(self):
        """ """
        return self._head

    @property
    def lift(self):
        """ """
        return self._lift

    @property
    def buzzer(self):
        """only for Cozmars V1"""
        if self.firmware_version.startswith('2'):
            raise AttributeError('Cozmars V2 has no buzzer')
        return self._buzzer

    @property
    def speaker(self):
        """only for Cozmars V2"""
        if self.firmware_version.startswith('1'):
            raise AttributeError('Cozmars V1 has no speaker')
        return self._speaker

    @property
    def screen(self):
        """ """
        return self._screen

    @property
    def camera(self):
        """ """
        return self._camera

    @property
    def microphone(self):
        """ """
        return self._microphone

    async def connect(self):
        """ """
        if not self._connected:
            self._lo = asyncio.get_running_loop()
            if not hasattr(self, '_event_thread'):
                self._event_thread = threading.current_thread()
            if not hasattr(self, '_host'):
                found = await util.find_service('rcute-cozmars-????',
                                                '_ws._tcp.local.')
                assert len(
                    found
                ) == 1, "More than one cozmars found." if found else "No cozmars found."
                self._host = found[0].server
            self._ws = await websockets.connect(f'ws://{self._host}/rpc')
            if '-1' == await self._ws.recv():
                raise RuntimeError(
                    'Could not connect to Cozmars, please close other programs that are already connected to Cozmars'
                )
            self._rpc = RPCClient(self._ws)
            self._about = json.loads(await self._get('/about'))
            await self._env.load()
            self._eye_anim_task = asyncio.create_task(
                self._eye_anim.animate(self))
            self._event_task = asyncio.create_task(self._get_event())
            self._connected = True

    @property
    def connected(self):
        """ """
        return self._connected

    async def _call_callback(self, cb, *args):
        if cb:
            if self._mode == 'aio':
                (await cb(*args)) if asyncio.iscoroutinefunction(cb) else cb(
                    *args)
            else:
                self._lo.run_in_executor(None, cb, *args)

    async def _get_event(self):
        self._event_rpc = self._rpc.sensor_data()
        async for event, data in self._event_rpc:
            try:
                if event == 'pressed':
                    if not data:
                        self.button._long_pressed = self.button._double_pressed = False
                    self.button._pressed = data
                    await self._call_callback(self.button.when_pressed if data
                                              else self.button.when_released)
                elif event == 'long_pressed':
                    self.button._long_pressed = data
                    await self._call_callback(self.button.when_long_pressed)
                elif event == 'double_pressed':
                    self.button._pressed = data
                    self.button._double_pressed = data
                    await self._call_callback(self.button.when_pressed)
                    await self._call_callback(self.button.when_double_pressed)
                elif event == 'out_of_range':
                    await self._call_callback(self.sonar.when_out_of_range,
                                              data)
                elif event == 'in_range':
                    await self._call_callback(self.sonar.when_in_range, data)
                elif event == 'lir':
                    self.infrared._state = data, self.infrared._state[1]
                    await self._call_callback(self.infrared.when_state_changed,
                                              self.infrared._state)
                elif event == 'rir':
                    self.infrared._state = self.infrared._state[0], data
                    await self._call_callback(self.infrared.when_state_changed,
                                              self.infrared._state)
            except Exception as e:
                logger.exception(e)

    async def disconnect(self):
        """ """
        if self._connected:
            self._event_task.cancel()
            self._eye_anim_task.cancel()
            self._event_rpc.cancel()
            await asyncio.gather(self.when_called(None),
                                 self.close_camera_view(),
                                 return_exceptions=True)
            await asyncio.gather(self.camera.close(),
                                 self.microphone.close(),
                                 self.speaker.close(),
                                 return_exceptions=True)
            await self._ws.close()
            self._connected = False

    async def __aenter__(self):
        await self.connect()
        return self

    async def __aexit__(self, exc_type, exc, tb):
        await self.disconnect()

    @util.mode(force_sync=False)
    async def forward(self, duration=None):
        """

        :param duration: duration (seconds), default is None for non-stop
        :type duration: float
        """
        await self._rpc.speed((1, 1), duration)

    @util.mode(force_sync=False)
    async def backward(self, duration=None):
        """

        :param duration: duration (seconds), default is None for non-stop
        :type duration: float
        """
        await self._rpc.speed((-1, -1), duration)

    @util.mode(force_sync=False)
    async def turn_left(self, duration=None):
        """

        :param duration: duration (seconds), default is None for non-stop
        :type duration: float
        """
        await self._rpc.speed((-1, 1), duration)

    @util.mode(force_sync=False)
    async def turn_right(self, duration=None):
        """

        :param duration: duration (seconds), default is None for non-stop
        :type duration: float
        """
        await self._rpc.speed((1, -1), duration)

    @util.mode()
    async def stop(self):
        """ """
        await self._rpc.speed((0, 0))

    @util.mode()
    async def say(self, txt, repeat=1, **options):
        """Text to speach for Cozmars V2.

        `rcute-ai <https://rcute-ai.readthedocs.io/>`_ must be installed to support this function.

        :param txt: text to be said
        :type txt: str
        :param repeat: playback times, default is 1
        :type repeat: int, optional
        :param options:
            * voice/lang
            * volume
            * pitch
            * speed
            * word_gap

            See `rcute_ai.tts <https://rcute-ai.readthedocs.io/zh_CN/latest/api/tts.html#rcute_ai.tts.TTS.tts_wav/>`_
        :type options: optional
        """
        if not hasattr(self, '_tts'):
            import rcute_ai as ai
            self._tts = ai.TTS()
        op = self.env.vars.get('say', {}).get(sys.platform, {}).copy()
        op.update(options)
        wav_data = await self._lo.run_in_executor(
            None, functools.partial(self._tts.tts_wav, txt, **op))
        with wave.open(io.BytesIO(wav_data)) as f:
            await self.speaker.play(f.readframes(f.getnframes()),
                                    repeat=repeat,
                                    volume=self.env.vars.get('say_vol'),
                                    sample_rate=f.getframerate(),
                                    dtype='int16')

    @util.mode(property_type='setter')
    async def when_called(self, *args):
        """Callback function, called when the wake word 'R-Cute' or Chinese '阿Q' is detected.

        When set, a thread will be running in background using the robot's microphone. If set to `None`, the background thread will stop.

        `rcute-ai <https://rcute-ai.readthedocs.io/>`_ must be installed to support this function.

        See `rcute_ai.WakeWordDetector <https://rcute-ai.readthedocs.io/zh_CN/latest/api/WakeWordDetector.html>`_"""
        if not args:
            return getattr(self, '_when_called', None)
        cb = self._when_called = args[0]
        if cb:

            def wwd_run():
                if not hasattr(self, '_wwd'):
                    import rcute_ai as ai
                    self._wwd = ai.WakeWordDetection()
                logger.info('Start wake word detection.')
                with self.microphone.get_buffer() as buf:
                    while self._when_called:
                        self._wwd.detect(buf) and cb()
                logger.info('Stop wake word detection.')

            self._wwd_thread = threading.Thread(target=wwd_run, daemon=True)
            self._wwd_thread.start()
        else:
            hasattr(self, '_wwd') and self._wwd.cancel()
            hasattr(self, '_wwd_thread') and self._wwd_thread.is_alive(
            ) and await self._lo.run_in_executor(None, self._wwd_thread.join)

    @util.mode()
    async def listen(self, **kw):
        """Speech recognition.

        :param lang: language, default to `'en'`
        :type lang: str
        :return: recognized string
        :rtype: str

        other paramters are the same as `rcute_ai.STT.stt <https://rcute-ai.readthedocs.io/zh_CN/latest/api/STT.html#rcute_ai.STT.stt>`_ 's keyword arguments.

        `rcute-ai <https://rcute-ai.readthedocs.io/>`_ must be installed to support this function.
        """
        if not hasattr(self, '_stts'):
            self._stts = {}
        lang = kw.pop('lang', 'en')

        def stt_run():
            if not self._stts.get(lang):
                import rcute_ai as ai
                self._stts[lang] = ai.STT(lang=lang)
            with self.microphone.get_buffer() as buf:
                return self._stts[lang].stt(buf, **kw)

        return await self._lo.run_in_executor(None, stt_run)

    @util.mode()
    async def show_camera_view(self):
        """Open camera and run a background thread showing the camera view or processed camera images if :data:`on_camera_image` is set"""
        def cam_run():
            import cv2
            self._stop_cam_view = False
            with self.camera.get_buffer() as buf:
                for im in buf:
                    if self._stop_cam_view:
                        cv2.destroyWindow(self.serial)
                        break
                    self._latest_camera_view = im
                    self.on_camera_image and self.on_camera_image(im)
                    cv2.imshow(self.serial, im)
                    cv2.waitKey(10)

        self._cam_view_thread = threading.Thread(target=cam_run, daemon=True)
        self._cam_view_thread.start()

    @property
    def latest_camera_view(self):
        """latest image from :meth:`show_camera_view` """
        return getattr(self, '_latest_camera_view', None)

    @util.mode()
    async def close_camera_view(self):
        """Stop the background thread created in :meth:`show_camera_view`"""
        self._stop_cam_view = True
        hasattr(self, '_cam_view_thread') and self._cam_view_thread.is_alive(
        ) and await self._lo.run_in_executor(None, self._cam_view_thread.join)

    @property
    def hostname(self):
        """Cozmars URL"""
        return self._about['hostname']

    @property
    def ip(self):
        """Cozmars' IP address"""
        return self._about['ip']

    @property
    def firmware_version(self):
        """Cozmars firmware version"""
        return self._about['version']

    @property
    def mac(self):
        """Cozmars MAC address"""
        return self._about['mac']

    @property
    def serial(self):
        """Cozmars serial number"""
        return self._about['serial']

    @util.mode()
    async def poweroff(self):
        """ """
        await AioRobot.disconnect(self)
        try:
            await self._get('/poweroff')
        except Exception as e:
            pass
        finally:
            self._connected = False

    @util.mode()
    async def reboot(self):
        """ """
        await AioRobot.disconnect(self)
        try:
            await self._get('/reboot')
        except Exception as e:
            pass
        finally:
            self._connected = False

    async def _get(self, sub_url):
        async with aiohttp.ClientSession() as session:
            async with session.get('http://' + self._host + sub_url) as resp:
                return await resp.text()
Esempio n. 2
0
class AioRobot:
    """Async/await mode of Cozmars robot

    :param serial_or_ip: IP address or serial number of Cozmars to connect
    :type serial_or_ip: str
    """
    def __init__(self, serial_or_ip):
        self._host ='rcute-cozmars-' + serial_or_ip +'.local' if len(serial_or_ip) == 4 else serial_or_ip
        self._mode ='aio'
        self._connected = False
        self._env = env.Env(self)
        self._screen = screen.Screen(self)
        self._camera = camera.Camera(self)
        self._microphone = microphone.Microphone(self)
        self._button = button.Button(self)
        self._sonar = sonar.Sonar(self)
        self._infrared = infrared.Infrared(self)
        self._lift = lift.Lift(self)
        self._head = head.Head(self)
        self._buzzer = buzzer.Buzzer(self)
        self._motor = motor.Motor(self)
        self._eye_anim = eye_animation.EyeAnimation(self)

    def _in_event_loop(self):
        return True

    @util.mode()
    async def animate(self, name, *args, **kwargs):
        """Execute action

        :param name: the name of the action
        :type name: str
        """
        anim = animations[name]
        anim = getattr(anim,'animate', anim)
        await anim(self, *args, **kwargs)

    @property
    def animation_list(self):
        """Action List"""
        return list(animations.keys())

    @property
    def eyes(self):
        """eye"""
        return self._eye_anim

    @property
    def env(self):
        """Environmental Variables"""
        return self._env

    @property
    def button(self):
        """Button"""
        return self._button

    @property
    def infrared(self):
        """Infrared sensor, on the bottom of the robot"""
        return self._infrared

    @property
    def sonar(self):
        """Sonar, namely ultrasonic distance sensor"""
        return self._sonar

    @property
    def motor(self):
        """motor"""
        return self._motor

    @property
    def head(self):
        """head"""
        return self._head

    @property
    def lift(self):
        """Arm"""
        return self._lift

    @property
    def buzzer(self):
        """buzzer"""
        return self._buzzer

    @property
    def screen(self):
        """screen"""
        return self._screen

    @property
    def camera(self):
        """camera"""
        return self._camera

    @property
    def microphone(self):
        """microphone"""
        return self._microphone

    async def connect(self):
        """Connect Cozmars"""
        if not self._connected:
            self._ws = await websockets.connect(f'ws://{self._host}/rpc')
            if'-1' == await self._ws.recv():
                raise RuntimeError('Could not connect to Cozmars, please close other programs that are already connected to Cozmars')
            self._rpc = RPCClient(self._ws)
            about = json.loads(await self._get('/about'))
            self._ip = about['ip']
            self._serial = about['serial']
            self._firmware_version = about['version']
            self._hostname = about['hostname']
            self._mac = about['mac']
            self._sensor_task = asyncio.create_task(self._get_sensor_data())
            self._eye_anim_task = asyncio.create_task(self._eye_anim.animate(self))
            self._connected = True

    @property
    def connected(self):
        """Are robots connected?"""
        return self._connected

    async def _call_callback(self, cb, *args):
        if cb:
            if self._mode =='aio':
                (await cb(*args)) if asyncio.iscoroutinefunction(cb) else cb(*args)
            else:
                self._lo.run_in_executor(None, cb, *args)

    async def _get_sensor_data(self):
        self._sensor_data_rpc = self._rpc.sensor_data()
        async for event, data in self._sensor_data_rpc:
            try:
                if event =='pressed':
                    if not data:
                        self.button._held = self.button._double_pressed = False
                    self.button._pressed = data
                    await self._call_callback(self.button.when_pressed if data else self.button.when_released)
                elif event =='held':
                    self.button._held = data
                    await self._call_callback(self.button.when_held)
                elif event =='double_pressed':
                    self.button._pressed = data
                    self.button._double_pressed = data
                    await self._call_callback(self.button.when_pressed)
                    await self._call_callback(self.button.when_double_pressed)
                elif event =='out_of_range':
                    await self._call_callback(self.sonar.when_out_of_range, data)
                elif event =='in_range':
                    await self._call_callback(self.sonar.when_in_range, data)
                elif event =='lir':
                    self.infrared._state = data, self.infrared._state[1]
                    await self._call_callback(self.infrared.when_state_changed, self.infrared._state)
                elif event =='rir':
                    self.infrared._state = self.infrared._state[0], data
                    await self._call_callback(self.infrared.when_state_changed, self.infrared._state)
            except Exception as e:
                logger.exception(e)

    async def disconnect(self):
        """Disconnect Cozmars"""
        if self._connected:
            self._sensor_task.cancel()
            self._eye_anim_task.cancel()
            self._sensor_data_rpc.cancel()
            await asyncio.gather(self.camera._close(), self.microphone._close(), self.buzzer._close(), return_exceptions=True)
            await self._ws.close()
            self._connected = False

    async def __aenter__(self):
        await self.connect()
        return self

    async def __aexit__(self, exc_type, exc, tb):
        await self.disconnect()

    @util.mode(force_sync=False)
    async def forward(self, duration=None):
        """go ahead

        :param duration: duration (seconds)
        :type duration: float
        """
        await self._rpc.speed((1,1), duration)
    
    @util.mode(force_sync=False)
    async def drive(self, speed, duration=1):
        """go ahead

        :param duration: duration (seconds)
        :type duration: float
        """
        await self._rpc.speed(speed, duration)

    @util.mode(force_sync=False)
    async def backward(self, duration=None):
        """Back

        :param duration: duration (seconds)
        :type duration: float
        """
        await self._rpc.speed((-1,-1), duration)

    @util.mode(force_sync=False)
    async def turn_left(self, duration=None):
        """Turn left

        :param duration: duration (seconds)
        :type duration: float
        """
        await self._rpc.speed((-1,1), duration)

    @util.mode(force_sync=False)
    async def turn_right(self, duration=None):
        """Turn right

        :param duration: duration (seconds)
        :type duration: float
        """
        await self._rpc.speed((1,-1), duration)

    @util.mode()
    async def stop(self):
        """stop"""
        await self._rpc.speed((0, 0))

    @property
    def hostname(self):
        """Cozmars URL"""
        return self._hostname

    @property
    def ip(self):
        """Cozmars' IP address"""
        return self._ip

    @property
    def firmware_version(self):
        """Cozmars firmware version

        If it is lower than the latest version on |pypi|, you can log in to the robot's page to update

        """
        return self._firmware_version

    @property
    def mac(self):
        """Cozmars MAC address"""
        return self._mac


    @property
    def serial(self):
        """Cozmars serial number"""
        return self._serial

    @util.mode()
    async def poweroff(self):
        """Close Cozmars"""
        await AioRobot.disconnect(self)
        try:
            await self._get('/poweroff')
        except Exception as e:
            pass

    @util.mode()
    async def reboot(self):
        """Restart Cozmars"""
        await AioRobot.disconnect(self)
        try:
            await self._get('/reboot')
        except Exception as e:
            pass

    async def _get(self, sub_url):
        async with aiohttp.ClientSession() as session:
            async with session.get('http://' + self._host + sub_url) as resp:
                return await resp.text()
Esempio n. 3
0
class AioRobot:
    """Cozmars 机器人的异步 (async/await) 模式

    :param serial_or_ip: 要连接的 Cozmars 的 IP 地址或序列号
    :type serial_or_ip: str
    """
    def __init__(self, serial_or_ip):
        self._host = 'rcute-cozmars-' + serial_or_ip + '.local' if len(serial_or_ip) == 4 else serial_or_ip
        self._mode = 'aio'
        self._connected = False
        self._env = env.Env(self)
        self._screen = screen.Screen(self)
        self._camera = camera.Camera(self)
        self._microphone = microphone.Microphone(self)
        self._button = button.Button(self)
        self._sonar = sonar.Sonar(self)
        self._infrared = infrared.Infrared(self)
        self._lift = lift.Lift(self)
        self._head = head.Head(self)
        self._buzzer = buzzer.Buzzer(self)
        self._motor = motor.Motor(self)
        self._eye_anim = eye_animation.EyeAnimation(self)

    def _in_event_loop(self):
        return True

    @util.mode()
    async def animate(self, name, *args, **kwargs):
        """执行动作

        :param name: 动作的名称
        :type name: str
        """
        anim = animations[name]
        anim = getattr(anim, 'animate', anim)
        await anim(self, *args, **kwargs)

    @property
    def animation_list(self):
        """动作列表"""
        return list(animations.keys())

    @property
    def eyes(self):
        """眼睛"""
        return self._eye_anim

    @property
    def env(self):
        """环境变量"""
        return self._env

    @property
    def button(self):
        """按钮"""
        return self._button

    @property
    def infrared(self):
        """红外传感器,在机器人底部"""
        return self._infrared

    @property
    def sonar(self):
        """声纳,即超声波距离传感器"""
        return self._sonar

    @property
    def motor(self):
        """马达"""
        return self._motor

    @property
    def head(self):
        """头"""
        return self._head

    @property
    def lift(self):
        """手臂"""
        return self._lift

    @property
    def buzzer(self):
        """蜂鸣器"""
        return self._buzzer

    @property
    def screen(self):
        """屏幕"""
        return self._screen

    @property
    def camera(self):
        """摄像头"""
        return self._camera

    @property
    def microphone(self):
        """麦克风"""
        return self._microphone

    async def connect(self):
        """连接 Cozmars"""
        if not self._connected:
            self._ws = await websockets.connect(f'ws://{self._host}/rpc')
            if '-1' == await self._ws.recv():
                raise RuntimeError('无法连接 Cozmars, 请先关闭其他已经连接 Cozmars 的程序')
            self._rpc = RPCClient(self._ws)
            about = json.loads(await self._get('/about'))
            self._ip = about['ip']
            self._serial = about['serial']
            self._firmware_version = about['version']
            self._hostname = about['hostname']
            self._mac = about['mac']
            self._sensor_task = asyncio.create_task(self._get_sensor_data())
            self._eye_anim_task = asyncio.create_task(self._eye_anim.animate(self))
            self._connected = True

    @property
    def connected(self):
        """是否连接上了机器人"""
        return self._connected

    async def _call_callback(self, cb, *args):
        if cb:
            if self._mode == 'aio':
                (await cb(*args)) if asyncio.iscoroutinefunction(cb) else cb(*args)
            else:
                self._lo.run_in_executor(None, cb, *args)

    async def _get_sensor_data(self):
        self._sensor_data_rpc = self._rpc.sensor_data()
        async for event, data in self._sensor_data_rpc:
            try:
                if event == 'pressed':
                    if not data:
                        self.button._held = self.button._double_pressed = False
                    self.button._pressed = data
                    await self._call_callback(self.button.when_pressed if data else self.button.when_released)
                elif event == 'held':
                    self.button._held = data
                    await self._call_callback(self.button.when_held)
                elif event == 'double_pressed':
                    self.button._pressed = data
                    self.button._double_pressed = data
                    await self._call_callback(self.button.when_pressed)
                    await self._call_callback(self.button.when_double_pressed)
                elif event == 'out_of_range':
                    await self._call_callback(self.sonar.when_out_of_range, data)
                elif event == 'in_range':
                    await self._call_callback(self.sonar.when_in_range, data)
                elif event == 'lir':
                    self.infrared._state = data, self.infrared._state[1]
                    await self._call_callback(self.infrared.when_state_changed, self.infrared._state)
                elif event == 'rir':
                    self.infrared._state = self.infrared._state[0], data
                    await self._call_callback(self.infrared.when_state_changed, self.infrared._state)
            except Exception as e:
                logger.exception(e)

    async def disconnect(self):
        """断开 Cozmars 的连接"""
        if self._connected:
            self._sensor_task.cancel()
            self._eye_anim_task.cancel()
            self._sensor_data_rpc.cancel()
            await asyncio.gather(self.camera._close(), self.microphone._close(), self.buzzer._close(), return_exceptions=True)
            await self._ws.close()
            self._connected = False

    async def __aenter__(self):
        await self.connect()
        return self

    async def __aexit__(self, exc_type, exc, tb):
        await self.disconnect()

    @util.mode(force_sync=False)
    async def forward(self, duration=None):
        """前进

        :param duration: 持续时间(秒)
        :type duration: float
        """
        await self._rpc.speed((1,1), duration)

    @util.mode(force_sync=False)
    async def backward(self, duration=None):
        """后退

        :param duration: 持续时间(秒)
        :type duration: float
        """
        await self._rpc.speed((-1,-1), duration)

    @util.mode(force_sync=False)
    async def turn_left(self, duration=None):
        """左传

        :param duration: 持续时间(秒)
        :type duration: float
        """
        await self._rpc.speed((-1,1), duration)

    @util.mode(force_sync=False)
    async def turn_right(self, duration=None):
        """右转

        :param duration: 持续时间(秒)
        :type duration: float
        """
        await self._rpc.speed((1,-1), duration)

    @util.mode()
    async def stop(self):
        """停止"""
        await self._rpc.speed((0, 0))

    @property
    def hostname(self):
        """Cozmars 的网址"""
        return self._hostname

    @property
    def ip(self):
        """Cozmars 的 IP 地址"""
        return self._ip

    @property
    def firmware_version(self):
        """Cozmars 的固件版本

        如果低于 |pypi上的最新版本| ,可以登陆机器人的页面进行更新

        """
        return self._firmware_version

    @property
    def mac(self):
        """Cozmars 的 MAC 地址"""
        return self._mac


    @property
    def serial(self):
        """Cozmars 的序列号"""
        return self._serial

    @util.mode()
    async def poweroff(self):
        """关闭 Cozmars"""
        await AioRobot.disconnect(self)
        try:
            await self._get('/poweroff')
        except Exception as e:
            pass

    @util.mode()
    async def reboot(self):
        """重启 Cozmars"""
        await AioRobot.disconnect(self)
        try:
            await self._get('/reboot')
        except Exception as e:
            pass

    async def _get(self, sub_url):
        async with aiohttp.ClientSession() as session:
            async with session.get('http://' + self._host + sub_url) as resp:
                return await resp.text()