Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vacuum: Implement TUI for the manual mode #845

Merged
merged 4 commits into from
Oct 31, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -42,6 +42,7 @@
from miio.pwzn_relay import PwznRelay
from miio.toiletlid import Toiletlid
from miio.vacuum import Vacuum, VacuumException
from miio.vacuum_tui import VacuumTUI
rytilahti marked this conversation as resolved.
Show resolved Hide resolved
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()
rytilahti marked this conversation as resolved.
Show resolved Hide resolved


@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:
rytilahti marked this conversation as resolved.
Show resolved Hide resolved
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)