Skip to content

Commit

Permalink
Vacuum: Implement TUI for the manual mode (rytilahti#845)
Browse files Browse the repository at this point in the history
* Vacuum: Export the manual mode constants

* Vacuum: Implement TUI for the manual mode

* Vacuum: Document manual mode

* VacuumTUI: Handle unavailable curses
  • Loading branch information
rnovatorov authored and xvlady committed May 9, 2021
1 parent 1b269f3 commit fa7a80f
Show file tree
Hide file tree
Showing 5 changed files with 173 additions and 9 deletions.
37 changes: 37 additions & 0 deletions docs/vacuum.rst
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,43 @@ and updating from an URL requires you to pass the md5 hash of the file.

mirobo update-firmware v11_003094.pkg

Manual control
~~~~~~~~~~~~~~

To start the manual mode:

::

mirobo manual start

To move forward with velocity 0.3 for default amount of time:

::

mirobo manual forward 0.3

To turn 90 degrees to the right for default amount of time:

::

mirobo manual right 90

To stop the manual mode:

::

mirobo manual stop

To run the manual control TUI:

.. NOTE::

Make sure you have got `curses` library installed on your system.

::

mirobo manual tui


DND functionality
~~~~~~~~~~~~~~~~~
Expand Down
1 change: 1 addition & 0 deletions miio/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
from miio.pwzn_relay import PwznRelay
from miio.toiletlid import Toiletlid
from miio.vacuum import Vacuum, VacuumException
from miio.vacuum_tui import VacuumTUI
from miio.vacuumcontainers import (
CleaningDetails,
CleaningSummary,
Expand Down
33 changes: 24 additions & 9 deletions miio/vacuum.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,12 +168,22 @@ def manual_stop(self):
self.manual_seqnum = 0
return self.send("app_rc_end")

MANUAL_ROTATION_MAX = 180
MANUAL_ROTATION_MIN = -MANUAL_ROTATION_MAX
MANUAL_VELOCITY_MAX = 0.3
MANUAL_VELOCITY_MIN = -MANUAL_VELOCITY_MAX
MANUAL_DURATION_DEFAULT = 1500

@command(
click.argument("rotation", type=int),
click.argument("velocity", type=float),
click.argument("duration", type=int, required=False, default=1500),
click.argument(
"duration", type=int, required=False, default=MANUAL_DURATION_DEFAULT
),
)
def manual_control_once(self, rotation: int, velocity: float, duration: int = 1500):
def manual_control_once(
self, rotation: int, velocity: float, duration: int = MANUAL_DURATION_DEFAULT
):
"""Starts the remote control mode and executes
the action once before deactivating the mode."""
number_of_tries = 3
Expand All @@ -191,18 +201,23 @@ def manual_control_once(self, rotation: int, velocity: float, duration: int = 15
@command(
click.argument("rotation", type=int),
click.argument("velocity", type=float),
click.argument("duration", type=int, required=False, default=1500),
click.argument(
"duration", type=int, required=False, default=MANUAL_DURATION_DEFAULT
),
)
def manual_control(self, rotation: int, velocity: float, duration: int = 1500):
def manual_control(
self, rotation: int, velocity: float, duration: int = MANUAL_DURATION_DEFAULT
):
"""Give a command over manual control interface."""
if rotation < -180 or rotation > 180:
if rotation < self.MANUAL_ROTATION_MIN or rotation > self.MANUAL_ROTATION_MAX:
raise DeviceException(
"Given rotation is invalid, should " "be ]-180, 180[, was %s" % rotation
"Given rotation is invalid, should be ]%s, %s[, was %s"
% (self.MANUAL_ROTATION_MIN, self.MANUAL_ROTATION_MAX, rotation)
)
if velocity < -0.3 or velocity > 0.3:
if velocity < self.MANUAL_VELOCITY_MIN or velocity > self.MANUAL_VELOCITY_MAX:
raise DeviceException(
"Given velocity is invalid, should "
"be ]-0.3, 0.3[, was: %s" % velocity
"Given velocity is invalid, should be ]%s, %s[, was: %s"
% (self.MANUAL_VELOCITY_MIN, self.MANUAL_VELOCITY_MAX, velocity)
)

self.manual_seqnum += 1
Expand Down
7 changes: 7 additions & 0 deletions miio/vacuum_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,13 @@ def manual(vac: miio.Vacuum):
# if not vac.manual_mode and command :


@manual.command()
@pass_dev
def tui(vac: miio.Vacuum):
"""TUI for the manual mode."""
miio.VacuumTUI(vac).run()


@manual.command()
@pass_dev
def start(vac: miio.Vacuum): # noqa: F811 # redef of start
Expand Down
104 changes: 104 additions & 0 deletions miio/vacuum_tui.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
try:
import curses
except ImportError:
curses = None

import enum
from typing import Tuple

from .vacuum import Vacuum


class Control(enum.Enum):

Quit = "q"
Forward = "w"
ForwardFast = "W"
Backward = "s"
BackwardFast = "S"
Left = "a"
LeftFast = "A"
Right = "d"
RightFast = "D"


class VacuumTUI:
def __init__(self, vac: Vacuum):
if curses is None:
raise ImportError("curses library is not available")

self.vac = vac
self.rot = 0
self.rot_delta = 30
self.rot_min = Vacuum.MANUAL_ROTATION_MIN
self.rot_max = Vacuum.MANUAL_ROTATION_MAX
self.vel = 0.0
self.vel_delta = 0.1
self.vel_min = Vacuum.MANUAL_VELOCITY_MIN
self.vel_max = Vacuum.MANUAL_VELOCITY_MAX
self.dur = 10 * 1000

def run(self) -> None:
self.vac.manual_start()
try:
curses.wrapper(self.main)
finally:
self.vac.manual_stop()

def main(self, screen) -> None:
screen.addstr("Use wasd to control the device.\n")
screen.addstr("Hold shift to enable fast mode.\n")
screen.addstr("Press q to quit.\n")
screen.refresh()
self.loop(screen)

def loop(self, win) -> None:
done = False
while not done:
key = win.getkey()
text, done = self.handle_key(key)
win.clear()
win.addstr(text)
win.refresh()

def handle_key(self, key: str) -> Tuple[str, bool]:
try:
ctl = Control(key)
except ValueError as e:
return "Ignoring %s: %s.\n" % (key, e), False

done = self.dispatch_control(ctl)
return self.info(), done

def dispatch_control(self, ctl: Control) -> bool:
if ctl == Control.Quit:
return True

if ctl == Control.Forward:
self.vel = min(self.vel + self.vel_delta, self.vel_max)
elif ctl == Control.ForwardFast:
self.vel = 0 if self.vel < 0 else self.vel_max

elif ctl == Control.Backward:
self.vel = max(self.vel - self.vel_delta, self.vel_min)
elif ctl == Control.BackwardFast:
self.vel = 0 if self.vel > 0 else self.vel_min

elif ctl == Control.Left:
self.rot = min(self.rot + self.rot_delta, self.rot_max)
elif ctl == Control.LeftFast:
self.rot = 0 if self.rot < 0 else self.rot_max

elif ctl == Control.Right:
self.rot = max(self.rot - self.rot_delta, self.rot_min)
elif ctl == Control.RightFast:
self.rot = 0 if self.rot > 0 else self.rot_min

else:
raise RuntimeError("unreachable")

self.vac.manual_control(rotation=self.rot, velocity=self.vel, duration=self.dur)
return False

def info(self) -> str:
return "Rotation=%s\nVelocity=%s\n" % (self.rot, self.vel)

0 comments on commit fa7a80f

Please sign in to comment.