Skip to content

Commit 1cff614

Browse files
committed
feat(cli): wire map editor to TranslationLayer and MapVerifier
Add --apply and --preview flags to all map editor commands: - split-room, merge-rooms, rename-room - add-virtual-wall, add-no-go-zone Add _generate_preview() helper to create PNG previews with red overlays. Add _execute_edit() helper to wire TranslationLayer → device → MapVerifier. Fix interface mismatches: - TranslationLayer constructor requires command_trait - Method is execute_edits() not sync() - MapVerifier takes map_content_trait not device - pending_edits is a property not method - Add missing Point import for preview generation Entire-Checkpoint: 03e83e006558
1 parent df23d69 commit 1cff614

1 file changed

Lines changed: 186 additions & 7 deletions

File tree

roborock/cli.py

Lines changed: 186 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1337,6 +1337,114 @@ async def q10_set_fan_level(ctx: click.Context, device_id: str, level: str) -> N
13371337
click.echo(f"Error: {e}")
13381338

13391339

1340+
# =============================================================================
1341+
# Map Editor Helpers
1342+
# =============================================================================
1343+
1344+
def _generate_preview(map_data, virtual_state, transformer, output_path: str = "temp_preview.png") -> str | None:
1345+
"""Generate a preview image with edits overlaid in red."""
1346+
try:
1347+
from PIL import Image, ImageDraw
1348+
from roborock.map.geometry import Point
1349+
1350+
# Get base map image if available
1351+
if hasattr(map_data, 'image') and map_data.image:
1352+
img = map_data.image.copy()
1353+
else:
1354+
# Create blank image from dimensions
1355+
width = getattr(map_data, 'width', 800)
1356+
height = getattr(map_data, 'height', 600)
1357+
img = Image.new('RGB', (width, height), color=(240, 240, 240))
1358+
1359+
draw = ImageDraw.Draw(img)
1360+
1361+
# Draw each pending edit in red
1362+
for edit in virtual_state.pending_edits:
1363+
if edit.edit_type.value == "split_room":
1364+
# Draw split line in red
1365+
p1 = transformer.robot_to_image(Point(edit.x1, edit.y1))
1366+
p2 = transformer.robot_to_image(Point(edit.x2, edit.y2))
1367+
draw.line([(int(p1.x), int(p1.y)), (int(p2.x), int(p2.y))], fill=(255, 0, 0), width=3)
1368+
elif edit.edit_type.value in ["virtual_wall", "no_go_zone"]:
1369+
# Draw virtual walls/no-go zones in red
1370+
p1 = transformer.robot_to_image(Point(edit.x1, edit.y1))
1371+
p2 = transformer.robot_to_image(Point(edit.x2, edit.y2))
1372+
if edit.edit_type.value == "virtual_wall":
1373+
draw.line([(int(p1.x), int(p1.y)), (int(p2.x), int(p2.y))], fill=(255, 0, 0), width=3)
1374+
else:
1375+
# No-go zone - draw rectangle
1376+
draw.rectangle([(int(p1.x), int(p1.y)), (int(p2.x), int(p2.y))], outline=(255, 0, 0), width=3)
1377+
1378+
img.save(output_path)
1379+
return output_path
1380+
except Exception as e:
1381+
click.echo(f"Warning: Could not generate preview: {e}")
1382+
return None
1383+
1384+
1385+
async def _execute_edit(device, virtual_state, map_flag: int) -> bool:
1386+
"""Execute virtual state edits on the device with verification."""
1387+
from roborock.map import MapVerifier, TranslationLayer
1388+
from roborock.map.editor import EditStatus
1389+
from roborock.devices.traits.v1.command import CommandTrait
1390+
1391+
click.echo("\nExecuting edit...")
1392+
1393+
# Get command trait from device
1394+
if not device.v1_properties:
1395+
click.echo("ERROR: Device does not support V1 protocol")
1396+
return False
1397+
1398+
# Try to get command trait
1399+
command_trait = None
1400+
if hasattr(device.v1_properties, 'command'):
1401+
command_trait = device.v1_properties.command
1402+
1403+
if not command_trait:
1404+
click.echo("ERROR: Device does not have command trait")
1405+
return False
1406+
1407+
# Create translation layer with proper arguments
1408+
translation = TranslationLayer(command_trait=command_trait, protocol="v1")
1409+
1410+
# Execute edits
1411+
results = await translation.execute_edits(virtual_state, map_flag)
1412+
1413+
if not results:
1414+
click.echo("ERROR: No edits were executed")
1415+
return False
1416+
1417+
# Check if all edits succeeded
1418+
failed_results = [r for r in results if not r.success]
1419+
if failed_results:
1420+
click.echo(f"ERROR: {len(failed_results)} edit(s) failed:")
1421+
for r in failed_results:
1422+
click.echo(f" - {r.edit.edit_type.name}: {r.error}")
1423+
return False
1424+
1425+
click.echo(f" Translation layer completed: {len(results)} edit(s)")
1426+
1427+
# Verify the edit was applied
1428+
click.echo("\nVerifying edit was applied...")
1429+
verifier = MapVerifier(map_content_trait=device.v1_properties.map_content)
1430+
1431+
verification_results = await verifier.verify_edits(virtual_state)
1432+
1433+
all_verified = all(r.verified for r in verification_results)
1434+
if all_verified:
1435+
click.echo(" SUCCESS: All edits verified on device")
1436+
# Mark edits as synced in virtual state
1437+
for edit in virtual_state.pending_edits:
1438+
edit.status = EditStatus.SYNCED
1439+
return True
1440+
else:
1441+
failed_verifications = [r for r in verification_results if not r.verified]
1442+
click.echo(f" WARNING: {len(failed_verifications)} edit(s) could not be verified")
1443+
for r in failed_verifications:
1444+
click.echo(f" - {r.edit_type}: {r.mismatch_reason}")
1445+
return False
1446+
1447+
13401448
# =============================================================================
13411449
# Map Editor Commands
13421450
# =============================================================================
@@ -1347,12 +1455,15 @@ async def q10_set_fan_level(ctx: click.Context, device_id: str, level: str) -> N
13471455
@click.option("--room", required=True, help="Room name to split")
13481456
@click.option("--direction", type=click.Choice(["vertical", "horizontal"]), default="vertical")
13491457
@click.option("--ratio", type=float, default=0.5, help="Split position (0.0-1.0)")
1458+
@click.option("--apply", is_flag=True, help="Apply the edit to the device")
1459+
@click.option("--preview", is_flag=True, default=True, help="Generate preview image")
13501460
@click.pass_context
13511461
@async_command
1352-
async def split_room(ctx, device_id: str, room: str, direction: str, ratio: float):
1462+
async def split_room(ctx, device_id: str, room: str, direction: str, ratio: float, apply: bool, preview: bool):
13531463
"""Split a room into two segments."""
13541464
from roborock.map import (
13551465
CoordinateTransformer,
1466+
MapVerifier,
13561467
SplitRoomEdit,
13571468
TranslationLayer,
13581469
VirtualState,
@@ -1428,16 +1539,27 @@ async def split_room(ctx, device_id: str, room: str, direction: str, ratio: floa
14281539
click.echo(f" Line: ({edit.x1:.0f}, {edit.y1:.0f}) -> ({edit.x2:.0f}, {edit.y2:.0f})")
14291540
click.echo(f" Edit ID: {edit.edit_id}")
14301541

1431-
# Preview mode - don't execute yet
1432-
click.echo("\nUse --apply flag to execute (not yet implemented)")
1542+
# Generate preview image
1543+
if preview:
1544+
preview_path = _generate_preview(map_data, virtual_state, transformer, "temp_preview.png")
1545+
if preview_path:
1546+
click.echo(f" Preview: {preview_path}")
1547+
1548+
# Execute if --apply flag is set
1549+
if apply:
1550+
await _execute_edit(device, virtual_state, map_data.map_flag or 0)
1551+
else:
1552+
click.echo("\nUse --apply flag to execute the edit")
14331553

14341554

14351555
@session.command()
14361556
@click.option("--device_id", required=True, help="Device ID")
14371557
@click.option("--rooms", required=True, help="Comma-separated room names to merge")
1558+
@click.option("--apply", is_flag=True, help="Apply the edit to the device")
1559+
@click.option("--preview", is_flag=True, default=True, help="Generate preview image")
14381560
@click.pass_context
14391561
@async_command
1440-
async def merge_rooms(ctx, device_id: str, rooms: str):
1562+
async def merge_rooms(ctx, device_id: str, rooms: str, apply: bool, preview: bool):
14411563
"""Merge multiple rooms into one."""
14421564
from roborock.map import (
14431565
CoordinateTransformer,
@@ -1490,14 +1612,28 @@ async def merge_rooms(ctx, device_id: str, rooms: str):
14901612
click.echo(f" Segment IDs: {segment_ids}")
14911613
click.echo(f" Edit ID: {edit.edit_id}")
14921614

1615+
# Generate preview image
1616+
if preview:
1617+
preview_path = _generate_preview(map_data, virtual_state, transformer, "temp_preview.png")
1618+
if preview_path:
1619+
click.echo(f" Preview: {preview_path}")
1620+
1621+
# Execute if --apply flag is set
1622+
if apply:
1623+
await _execute_edit(device, virtual_state, map_data.map_flag or 0)
1624+
else:
1625+
click.echo("\nUse --apply flag to execute the edit")
1626+
14931627

14941628
@session.command()
14951629
@click.option("--device_id", required=True, help="Device ID")
14961630
@click.option("--room", required=True, help="Room name")
14971631
@click.option("--new-name", required=True, help="New room name")
1632+
@click.option("--apply", is_flag=True, help="Apply the edit to the device")
1633+
@click.option("--preview", is_flag=True, default=True, help="Generate preview image")
14981634
@click.pass_context
14991635
@async_command
1500-
async def rename_room(ctx, device_id: str, room: str, new_name: str):
1636+
async def rename_room(ctx, device_id: str, room: str, new_name: str, apply: bool, preview: bool):
15011637
"""Rename a room."""
15021638
from roborock.map import (
15031639
CoordinateTransformer,
@@ -1550,6 +1686,19 @@ async def rename_room(ctx, device_id: str, room: str, new_name: str):
15501686
return
15511687

15521688
click.echo(f"Created rename edit: '{old_name}' -> '{new_name}'")
1689+
click.echo(f" Edit ID: {edit.edit_id}")
1690+
1691+
# Generate preview image
1692+
if preview:
1693+
preview_path = _generate_preview(map_data, virtual_state, transformer, "temp_preview.png")
1694+
if preview_path:
1695+
click.echo(f" Preview: {preview_path}")
1696+
1697+
# Execute if --apply flag is set
1698+
if apply:
1699+
await _execute_edit(device, virtual_state, map_data.map_flag or 0)
1700+
else:
1701+
click.echo("\nUse --apply flag to execute the edit")
15531702

15541703

15551704
@session.command()
@@ -1558,9 +1707,11 @@ async def rename_room(ctx, device_id: str, room: str, new_name: str):
15581707
@click.option("--y1", type=int, required=True, help="Wall start Y (mm)")
15591708
@click.option("--x2", type=int, required=True, help="Wall end X (mm)")
15601709
@click.option("--y2", type=int, required=True, help="Wall end Y (mm)")
1710+
@click.option("--apply", is_flag=True, help="Apply the edit to the device")
1711+
@click.option("--preview", is_flag=True, default=True, help="Generate preview image")
15611712
@click.pass_context
15621713
@async_command
1563-
async def add_virtual_wall(ctx, device_id: str, x1: int, y1: int, x2: int, y2: int):
1714+
async def add_virtual_wall(ctx, device_id: str, x1: int, y1: int, x2: int, y2: int, apply: bool, preview: bool):
15641715
"""Add a virtual wall."""
15651716
from roborock.map import (
15661717
CoordinateTransformer,
@@ -1595,6 +1746,19 @@ async def add_virtual_wall(ctx, device_id: str, x1: int, y1: int, x2: int, y2: i
15951746
return
15961747

15971748
click.echo(f"Created virtual wall edit: ({x1}, {y1}) -> ({x2}, {y2})")
1749+
click.echo(f" Edit ID: {edit.edit_id}")
1750+
1751+
# Generate preview image
1752+
if preview:
1753+
preview_path = _generate_preview(map_data, virtual_state, transformer, "temp_preview.png")
1754+
if preview_path:
1755+
click.echo(f" Preview: {preview_path}")
1756+
1757+
# Execute if --apply flag is set
1758+
if apply:
1759+
await _execute_edit(device, virtual_state, map_data.map_flag or 0)
1760+
else:
1761+
click.echo("\nUse --apply flag to execute the edit")
15981762

15991763

16001764
@session.command()
@@ -1603,9 +1767,11 @@ async def add_virtual_wall(ctx, device_id: str, x1: int, y1: int, x2: int, y2: i
16031767
@click.option("--y1", type=int, required=True, help="Zone min Y (mm)")
16041768
@click.option("--x2", type=int, required=True, help="Zone max X (mm)")
16051769
@click.option("--y2", type=int, required=True, help="Zone max Y (mm)")
1770+
@click.option("--apply", is_flag=True, help="Apply the edit to the device")
1771+
@click.option("--preview", is_flag=True, default=True, help="Generate preview image")
16061772
@click.pass_context
16071773
@async_command
1608-
async def add_no_go_zone(ctx, device_id: str, x1: int, y1: int, x2: int, y2: int):
1774+
async def add_no_go_zone(ctx, device_id: str, x1: int, y1: int, x2: int, y2: int, apply: bool, preview: bool):
16091775
"""Add a no-go zone."""
16101776
from roborock.map import (
16111777
CoordinateTransformer,
@@ -1640,6 +1806,19 @@ async def add_no_go_zone(ctx, device_id: str, x1: int, y1: int, x2: int, y2: int
16401806
return
16411807

16421808
click.echo(f"Created no-go zone edit: ({x1}, {y1}) -> ({x2}, {y2})")
1809+
click.echo(f" Edit ID: {edit.edit_id}")
1810+
1811+
# Generate preview image
1812+
if preview:
1813+
preview_path = _generate_preview(map_data, virtual_state, transformer, "temp_preview.png")
1814+
if preview_path:
1815+
click.echo(f" Preview: {preview_path}")
1816+
1817+
# Execute if --apply flag is set
1818+
if apply:
1819+
await _execute_edit(device, virtual_state, map_data.map_flag or 0)
1820+
else:
1821+
click.echo("\nUse --apply flag to execute the edit")
16431822

16441823

16451824
def main():

0 commit comments

Comments
 (0)