@@ -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 ("\n Executing 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 ("\n Verifying 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 ("\n Use --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 ("\n Use --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 ("\n Use --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 ("\n Use --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 ("\n Use --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 ("\n Use --apply flag to execute the edit" )
16431822
16441823
16451824def main ():
0 commit comments