diff --git a/roborock/devices/device_manager.py b/roborock/devices/device_manager.py index f7d3a150..663df920 100644 --- a/roborock/devices/device_manager.py +++ b/roborock/devices/device_manager.py @@ -19,7 +19,6 @@ from roborock.devices.device import DeviceReadyCallback, RoborockDevice from roborock.diagnostics import Diagnostics, redact_device_data from roborock.exceptions import RoborockException -from roborock.map.b01_q10_map_parser import B01Q10MapParserConfig from roborock.map.map_parser import MapParserConfig from roborock.mqtt.roborock_session import create_lazy_mqtt_session from roborock.mqtt.session import MqttSession, SessionUnauthorizedHook @@ -263,16 +262,7 @@ def device_creator(home_data: HomeData, device: HomeDataDevice, product: HomeDat if "ss" in model_part: b01_q10_channel = create_b01_q10_channel(mqtt_channel) channel = b01_q10_channel - trait = b01.q10.create( - channel, - map_parser_config=( - B01Q10MapParserConfig( - map_scale=map_parser_config.map_scale, drawables=map_parser_config.drawables - ) - if map_parser_config - else None - ), - ) + trait = b01.q10.create(channel, map_parser_config=map_parser_config) elif "sc" in model_part: # Q7 devices start with 'sc' in their model naming. b01_q7_channel = create_b01_q7_channel(device, product, mqtt_channel) diff --git a/roborock/devices/traits/b01/q10/__init__.py b/roborock/devices/traits/b01/q10/__init__.py index c36b25af..96f13ca8 100644 --- a/roborock/devices/traits/b01/q10/__init__.py +++ b/roborock/devices/traits/b01/q10/__init__.py @@ -8,7 +8,8 @@ from roborock.data.containers import RoborockBase from roborock.devices.rpc.b01_q10_channel import B01Q10Channel from roborock.devices.traits import Trait -from roborock.map.b01_q10_map_parser import B01Q10MapParserConfig, Q10MapPacket, Q10TracePacket +from roborock.map.b01_q10_map_parser import Q10MapPacket, Q10TracePacket +from roborock.map.map_parser import MapParserConfig from roborock.protocols.b01_q10_protocol import Q10DpsUpdate, Q10Message from .button_light import ButtonLightTrait @@ -92,7 +93,12 @@ class Q10PropertiesApi(Trait): clean_history: CleanHistoryTrait """Trait for fetching the device clean-record history (``dpCleanRecord``).""" - def __init__(self, channel: B01Q10Channel, *, map_parser_config: B01Q10MapParserConfig | None = None) -> None: + def __init__( + self, + channel: B01Q10Channel, + *, + map_parser_config: MapParserConfig | None = None, + ) -> None: """Initialize the B01Props API.""" self._channel = channel self.command = CommandTrait(channel) @@ -179,6 +185,10 @@ def as_dict(self) -> dict[str, Any]: return result -def create(channel: B01Q10Channel, *, map_parser_config: B01Q10MapParserConfig | None = None) -> Q10PropertiesApi: +def create( + channel: B01Q10Channel, + *, + map_parser_config: MapParserConfig | None = None, +) -> Q10PropertiesApi: """Create traits for B01 devices.""" return Q10PropertiesApi(channel, map_parser_config=map_parser_config) diff --git a/roborock/devices/traits/b01/q10/map.py b/roborock/devices/traits/b01/q10/map.py index cf4dbde5..15d3e77c 100644 --- a/roborock/devices/traits/b01/q10/map.py +++ b/roborock/devices/traits/b01/q10/map.py @@ -24,7 +24,6 @@ from roborock.devices.traits.common import DpsDataConverter, TraitUpdateListener from roborock.exceptions import RoborockException from roborock.map.b01_q10_map_parser import ( - B01Q10MapParserConfig, Q10MapPacket, Q10Point, Q10Room, @@ -32,6 +31,7 @@ ) from roborock.map.b01_q10_overlays import Q10RestrictedZone, Q10VirtualWall, parse_virtual_wall_blob, parse_zone_blob from roborock.map.b01_q10_render import Q10MapOverlays, render_q10_map +from roborock.map.map_parser import MapParserConfig from roborock.protocols.b01_q10_protocol import encode_restricted_zones, encode_room_name, encode_virtual_walls from .command import CommandTrait @@ -101,10 +101,10 @@ def __init__( maps: MapsTrait, command: CommandTrait, *, - map_parser_config: B01Q10MapParserConfig | None = None, + map_parser_config: MapParserConfig | None = None, ) -> None: TraitUpdateListener.__init__(self, logger=_LOGGER) - self._config = map_parser_config or B01Q10MapParserConfig() + self._config = map_parser_config or MapParserConfig() self._map_dps = map_dps self._maps = maps self._command = command diff --git a/roborock/map/b01_q10_map_parser.py b/roborock/map/b01_q10_map_parser.py index c715007e..9690fa9c 100644 --- a/roborock/map/b01_q10_map_parser.py +++ b/roborock/map/b01_q10_map_parser.py @@ -25,8 +25,6 @@ import struct from dataclasses import dataclass, field, replace -from vacuum_map_parser_base.config.drawable import Drawable - from roborock.data.b01_q10.b01_q10_containers import Q10RoborockPoint from roborock.data.code_mappings import RoborockEnum from roborock.data.containers import RoborockBase @@ -45,19 +43,21 @@ # eyeballed): 243 is the background outside the home (~half the grid), 240 is # scanned floor not yet assigned to a room, other values >= 240 are walls, and # 0 < value < 240 are per-room floor cells (value == room_id * 4). -_BACKGROUND_VALUE = 243 -_UNSEGMENTED_FLOOR_VALUE = 240 +Q10_BACKGROUND_VALUE = 243 +Q10_UNSEGMENTED_FLOOR_VALUE = 240 +# Grid cell values >= this are walls / borders rather than room segments. +Q10_WALL_THRESHOLD = 240 def classify_q10_cell(value: int) -> str: """Map a Q10 grid cell value to a canonical layer class.""" if value == 0: return LAYER_UNKNOWN - if value == _BACKGROUND_VALUE: + if value == Q10_BACKGROUND_VALUE: return LAYER_BACKGROUND - if value == _UNSEGMENTED_FLOOR_VALUE: + if value == Q10_UNSEGMENTED_FLOOR_VALUE: return LAYER_FLOOR - if value >= _WALL_THRESHOLD: + if value >= Q10_WALL_THRESHOLD: return LAYER_WALL return LAYER_FLOOR @@ -103,9 +103,6 @@ def classify_q10_cell(value: int) -> str: # although they use different coordinate frames as documented above. _HEADER_UNITS_PER_PIXEL = 10 -# Grid cell values >= this are walls / borders rather than room segments. -_WALL_THRESHOLD = 240 - @dataclass class Q10Room(RoborockBase): @@ -783,7 +780,7 @@ def _parse_clean_record_trace( ) -def erased_packet(packet: "Q10MapPacket", cells: set[int]) -> "Q10MapPacket": +def erased_packet(packet: Q10MapPacket, cells: set[int]) -> Q10MapPacket: """Return a copy of ``packet`` with ``cells`` (grid indices) set to background. Used to apply the app's erase zones: cells inside an erase rectangle are blanked @@ -794,15 +791,5 @@ def erased_packet(packet: "Q10MapPacket", cells: set[int]) -> "Q10MapPacket": grid = bytearray(packet.grid) for cell in cells: if 0 <= cell < len(grid): - grid[cell] = _BACKGROUND_VALUE + grid[cell] = Q10_BACKGROUND_VALUE return replace(packet, grid=bytes(grid)) - - -@dataclass -class B01Q10MapParserConfig: - """Configuration for the Q10 map parser.""" - - map_scale: int = 4 - """Scale factor for the rendered map image.""" - drawables: list[Drawable] | None = None - """Enabled overlays, or ``None`` for the Q10 defaults.""" diff --git a/roborock/map/b01_q10_render.py b/roborock/map/b01_q10_render.py index 20f94d1c..5f848f6a 100644 --- a/roborock/map/b01_q10_render.py +++ b/roborock/map/b01_q10_render.py @@ -34,10 +34,9 @@ solve_calibration_with_origin, ) from .b01_q10_map_parser import ( - _BACKGROUND_VALUE, - _UNSEGMENTED_FLOOR_VALUE, - _WALL_THRESHOLD, - B01Q10MapParserConfig, + Q10_BACKGROUND_VALUE, + Q10_UNSEGMENTED_FLOOR_VALUE, + Q10_WALL_THRESHOLD, Q10EraseZone, Q10HistoricalTracePacket, Q10MapPacket, @@ -45,7 +44,7 @@ erased_packet, ) from .b01_q10_overlays import ZONE_TYPE_NO_GO, ZONE_TYPE_NO_MOP, Q10Zone -from .map_parser import DEFAULT_DRAWABLES, MapParserConfig, _create_image_generator +from .map_parser import MapParserConfig, _create_image_generator from .room_colors import adjacency_aware_room_colors # Path-units-per-pixel candidates for calibration. A dense ss07 path lands a @@ -78,7 +77,6 @@ Drawable.VACUUM_POSITION, Drawable.VIRTUAL_WALLS, } -_Q10_DRAWABLES = [drawable for drawable in DEFAULT_DRAWABLES if drawable in _Q10_DRAWABLE_TYPES] @dataclass(frozen=True) @@ -94,7 +92,7 @@ def render_q10_map( trace: Q10TracePacket | Q10HistoricalTracePacket | None, overlays: Q10MapOverlays, *, - config: B01Q10MapParserConfig, + config: MapParserConfig, robot_at_dock: bool = False, ) -> bytes: """Compose the latest map, trace and DPS inputs into one PNG image. @@ -119,19 +117,21 @@ def render_q10_map( if map_data.image is None: raise RoborockException("Failed to render Q10 map image") - has_drawables = _place_obstacles(map_data, packet) - if trace_calibration is not None and trace is not None: - charger_heading = packet.header_calibration.charger_phi if packet.header_calibration is not None else None - _place_trace(map_data, trace_calibration, trace, charger_heading=charger_heading) - has_drawables = True - has_drawables = _place_charger_from_header(map_data, packet) or has_drawables - if robot_at_dock: - has_drawables = _place_docked_robot(map_data) or has_drawables - if vector_calibration is not None: - _place_overlays(map_data, vector_calibration, overlays) - has_drawables = has_drawables or bool(map_data.no_go_areas or map_data.no_mopping_areas or map_data.walls) - if has_drawables: - return _draw_map_content(map_data, config=config) + drawables = [drawable for drawable in config.drawables if drawable in _Q10_DRAWABLE_TYPES] + if drawables: + has_drawables = _place_obstacles(map_data, packet) + if trace_calibration is not None and trace is not None: + charger_heading = packet.header_calibration.charger_phi if packet.header_calibration is not None else None + _place_trace(map_data, trace_calibration, trace, charger_heading=charger_heading) + has_drawables = True + has_drawables = _place_charger_from_header(map_data, packet) or has_drawables + if robot_at_dock: + has_drawables = _place_docked_robot(map_data) or has_drawables + if vector_calibration is not None: + _place_overlays(map_data, vector_calibration, overlays) + has_drawables = has_drawables or bool(map_data.no_go_areas or map_data.no_mopping_areas or map_data.walls) + if has_drawables: + return _draw_map_content(map_data, config=config, drawables=drawables) buffer = io.BytesIO() map_data.image.data.save(buffer, format="PNG") @@ -341,17 +341,13 @@ def to_area(zone: Q10Zone) -> Area | None: def _draw_map_content( map_data: MapData, *, - config: B01Q10MapParserConfig, + config: MapParserConfig, + drawables: list[Drawable], ) -> bytes: """Draw Q10 content with the shared V1 image generator.""" if map_data.image is None: raise RoborockException("Failed to render Q10 map image") - drawables = ( - _Q10_DRAWABLES - if config.drawables is None - else [drawable for drawable in config.drawables if drawable in _Q10_DRAWABLE_TYPES] - ) - generator = _create_image_generator(MapParserConfig(map_scale=config.map_scale), drawables=drawables) + generator = _create_image_generator(config, drawables=drawables) generator.draw_map(map_data) buffer = io.BytesIO() map_data.image.data.save(buffer, format="PNG") @@ -361,10 +357,10 @@ def _draw_map_content( def _map_data_from_packet( packet: Q10MapPacket, *, - config: B01Q10MapParserConfig, + config: MapParserConfig, ) -> MapData: """Build MapData with a rendered base image, without PNG serialization.""" - image = _render_base_grid(packet, scale=config.map_scale) + image = _render_base_grid(packet, config=config) map_data = MapData() map_data.image = ImageData( @@ -382,29 +378,28 @@ def _map_data_from_packet( # overlays vertically. img_transformation=lambda p: Point(p.x, packet.height - p.y - 1, p.a), ) - room_names = {room.id: room.name for room in packet.rooms} - if room_names: - map_data.additional_parameters["room_names"] = room_names - - # Carpet cells are flat grid indices (y*width + x) in the same top-down - # pixel space as the rendered raster, so they line up with the image. - if packet.carpet_mask is not None: - map_data.carpet_map = {i for i, value in enumerate(packet.carpet_mask) if value} - return map_data -def _render_base_grid(packet: Q10MapPacket, *, scale: int) -> Image.Image: +def _render_base_grid(packet: Q10MapPacket, *, config: MapParserConfig) -> Image.Image: """Render the Q10 grid with the V1 map palette.""" - palette = _build_palette(packet.grid, packet.width) + palette = _build_palette( + packet.grid, + packet.width, + show_walls=config.show_walls, + show_rooms=config.show_rooms, + ) flat_palette = [channel for color in palette for channel in color] p_img = Image.frombytes("P", (packet.width, packet.height), packet.grid) p_img.putpalette(flat_palette, rawmode="RGBA") img = p_img.convert("RGBA") # The ss07 grid is stored top-down (row 0 = top of the home), so it is # rendered as-is -- unlike the V1/Q7 convention, no vertical flip. - if scale > 1: - img = img.resize((packet.width * scale, packet.height * scale), resample=Image.Resampling.NEAREST) + if config.map_scale > 1: + img = img.resize( + (packet.width * config.map_scale, packet.height * config.map_scale), + resample=Image.Resampling.NEAREST, + ) return img @@ -413,27 +408,34 @@ def _opaque(color: tuple[int, ...]) -> tuple[int, int, int, int]: return (color[0], color[1], color[2], color[3] if len(color) == 4 else 255) -def _build_palette(grid: bytes, width: int) -> list[tuple[int, int, int, int]]: +def _build_palette( + grid: bytes, + width: int, + *, + show_walls: bool = True, + show_rooms: bool = True, +) -> list[tuple[int, int, int, int]]: """Map Q10 cells onto the same colors used by the V1 map renderer.""" def room_id(value: int) -> int | None: - return max(1, value // 4) if 0 < value < _WALL_THRESHOLD else None + return max(1, value // 4) if 0 < value < Q10_WALL_THRESHOLD else None colors = ColorsPalette() - room_colors = adjacency_aware_room_colors( - grid, - width, - colors, - room_id, - ) outside = (0, 0, 0, 0) palette = [outside] * 256 - for value in {value for value in grid if 0 < value < _WALL_THRESHOLD}: - palette[value] = _opaque(room_colors[max(1, value // 4)]) - wall = _opaque(colors.get_color(SupportedColor.GREY_WALL)) - for value in range(_WALL_THRESHOLD, 256): - palette[value] = wall - palette[_UNSEGMENTED_FLOOR_VALUE] = _opaque(colors.get_color(SupportedColor.MAP_INSIDE)) - palette[_BACKGROUND_VALUE] = outside - palette[0] = outside + if show_rooms: + room_colors = adjacency_aware_room_colors( + grid, + width, + colors, + room_id, + ) + for value in {value for value in grid if 0 < value < Q10_WALL_THRESHOLD}: + palette[value] = _opaque(room_colors[max(1, value // 4)]) + palette[Q10_UNSEGMENTED_FLOOR_VALUE] = _opaque(colors.get_color(SupportedColor.MAP_INSIDE)) + if show_walls: + wall = _opaque(colors.get_color(SupportedColor.GREY_WALL)) + for value in range(Q10_WALL_THRESHOLD, 256): + if value not in (Q10_UNSEGMENTED_FLOOR_VALUE, Q10_BACKGROUND_VALUE): + palette[value] = wall return palette diff --git a/tests/devices/traits/b01/q10/test_map.py b/tests/devices/traits/b01/q10/test_map.py index 13e5952e..8ee28206 100644 --- a/tests/devices/traits/b01/q10/test_map.py +++ b/tests/devices/traits/b01/q10/test_map.py @@ -8,6 +8,7 @@ import asyncio import base64 +import io from collections.abc import AsyncGenerator, Generator from dataclasses import replace from pathlib import Path @@ -15,6 +16,7 @@ from unittest.mock import Mock, patch import pytest +from PIL import Image from vacuum_map_parser_base.config.drawable import Drawable from roborock.cli import _await_q10_map_push, cli @@ -26,7 +28,6 @@ from roborock.devices.traits.b01.q10.maps import MapsTrait from roborock.exceptions import RoborockException from roborock.map.b01_q10_map_parser import ( - B01Q10MapParserConfig, Q10HeaderCalibration, Q10Obstacle, Q10Point, @@ -36,6 +37,7 @@ ) from roborock.map.b01_q10_overlays import Q10RestrictedZone, Q10VirtualWall from roborock.map.b01_q10_render import Q10MapOverlays +from roborock.map.map_parser import MapParserConfig from roborock.protocols.b01_q10_protocol import Q10DpsUpdate, Q10Message from .conftest import FakeB01Q10Channel @@ -595,10 +597,10 @@ def test_map_content_trait_as_dict_camelizes_child_keys() -> None: assert data["robotPosition"] == {"x": 25875, "y": 26125} -async def test_create_forwards_obstacle_drawables_to_rendering(fake_channel: FakeB01Q10Channel) -> None: - """The public Q10 factory config changes the rendered image.""" - enabled = create(fake_channel, map_parser_config=B01Q10MapParserConfig(drawables=[Drawable.OBSTACLES])) - disabled = create(fake_channel, map_parser_config=B01Q10MapParserConfig(drawables=[])) +def test_create_forwards_map_parser_config(fake_channel: FakeB01Q10Channel) -> None: + """Q10PropertiesApi forwards MapParserConfig scale and drawables to MapContentTrait rendering.""" + enabled = create(fake_channel, map_parser_config=MapParserConfig(map_scale=2, drawables=[Drawable.OBSTACLES])) + disabled = create(fake_channel, map_parser_config=MapParserConfig(map_scale=2, drawables=[])) packet = replace( parse_map_packet(FIXTURE.read_bytes()), header_calibration=Q10HeaderCalibration(0, 50, 5, -1, -1, 0), @@ -606,7 +608,12 @@ async def test_create_forwards_obstacle_drawables_to_rendering(fake_channel: Fak ) enabled.map.update_from_map_packet(packet) disabled.map.update_from_map_packet(packet) + + assert enabled.map.image_content is not None + assert disabled.map.image_content is not None assert enabled.map.image_content != disabled.map.image_content + image = Image.open(io.BytesIO(enabled.map.image_content)) + assert image.size == (packet.width * 2, packet.height * 2) async def test_set_current_map_uses_apply_for_listed_string_id( diff --git a/tests/map/test_b01_q10_render.py b/tests/map/test_b01_q10_render.py index 522bfaea..773fdf15 100644 --- a/tests/map/test_b01_q10_render.py +++ b/tests/map/test_b01_q10_render.py @@ -17,7 +17,6 @@ from roborock.map.b01_grid_layers import GridCalibration from roborock.map.b01_q10_map_parser import ( - B01Q10MapParserConfig, Q10CleanRecordDetail, Q10EraseZone, Q10HeaderCalibration, @@ -40,15 +39,15 @@ Q10MapOverlays, _calibration_from_header_metadata, _erased_cells, - _map_data_from_packet, _place_docked_robot, _vector_calibration, render_q10_map, solve_q10_calibration, ) +from roborock.map.map_parser import MapParserConfig FIXTURE = Path("tests/map/testdata/b01_q10_map.bin") -CONFIG = B01Q10MapParserConfig() +CONFIG = MapParserConfig() # identity-ish calibration used across the geometry tests: world (x, y) -> grid # pixel (x, 5 - y) over the fixture's 8x6 grid (top-down, no flip). @@ -67,12 +66,13 @@ def _render( *, trace: Q10TracePacket | Q10HistoricalTracePacket | None = None, overlays: Q10MapOverlays | None = None, + config: MapParserConfig = CONFIG, ) -> bytes: return render_q10_map( packet if packet is not None else _packet(), trace, overlays or Q10MapOverlays(), - config=CONFIG, + config=config, ) @@ -142,15 +142,31 @@ def test_render_gives_adjacent_rooms_distinct_palette_colors() -> None: assert image.getpixel((0, 0)) != image.getpixel((scale, 0)) -def test_map_data_from_packet_populates_room_names_and_carpet_map() -> None: - """Base MapData populates room_names and flat carpet_map indices.""" - carpet = bytearray(8 * 6) - carpet[10] = 4 - carpet[20] = 3 - packet = replace(_packet(), carpet_mask=bytes(carpet)) - map_data = _map_data_from_packet(packet, config=CONFIG) - assert map_data.additional_parameters["room_names"] == {2: "Living Room", 3: "Bedroom"} - assert map_data.carpet_map == {10, 20} +@pytest.mark.parametrize( + ("show_walls", "show_rooms", "expected_pixels"), + [ + (False, True, [(0, 0, 0, 0), (32, 115, 185, 255), (0, 0, 0, 0), (133, 193, 233, 255)]), + (True, False, [(0, 0, 0, 0), (0, 0, 0, 0), (93, 109, 126, 255), (0, 0, 0, 0)]), + (False, False, [(0, 0, 0, 0), (0, 0, 0, 0), (0, 0, 0, 0), (0, 0, 0, 0)]), + ], +) +def test_render_respects_wall_and_room_visibility( + show_walls: bool, + show_rooms: bool, + expected_pixels: list[tuple[int, int, int, int]], +) -> None: + """Base-grid rendering hides walls and room fills when configured.""" + packet = Q10MapPacket( + kind=Q10MapPacketKind.CURRENT, + map_id=1, + width=4, + height=1, + grid=bytes([243, 240, 249, 8]), + ) + config = MapParserConfig(show_walls=show_walls, show_rooms=show_rooms) + image = Image.open(io.BytesIO(_render(packet, config=config))) + scale = config.map_scale + assert [image.getpixel((i * scale, 0)) for i in range(4)] == expected_pixels def test_render_draws_path_and_position() -> None: @@ -179,15 +195,16 @@ def test_render_accepts_historical_trace() -> None: def test_render_draws_zones_and_virtual_walls() -> None: - """Decoded DPS overlays are included in the composed image.""" + """Decoded DPS overlays are included in the composed image when enabled.""" packet, trace = _calibrated_inputs() zones = [ Q10Zone(type=ZONE_TYPE_NO_GO, vertices=_world_vertices(VECTOR_CALIBRATION, [(1, 1), (4, 1), (4, 4), (1, 4)])), Q10Zone(type=ZONE_TYPE_NO_MOP, vertices=_world_vertices(VECTOR_CALIBRATION, [(4, 1), (6, 1), (6, 3), (4, 3)])), ] walls = [Q10Zone(type=ZONE_TYPE_VIRTUAL_WALL, vertices=_world_vertices(VECTOR_CALIBRATION, [(1, 1), (6, 1)]))] - base = _render(packet, trace=trace) - rendered = _render(packet, trace=trace, overlays=Q10MapOverlays(zones=zones, virtual_walls=walls)) + config = MapParserConfig(drawables=list(Drawable)) + base = _render(packet, trace=trace, config=config) + rendered = _render(packet, trace=trace, overlays=Q10MapOverlays(zones=zones, virtual_walls=walls), config=config) assert rendered != base @@ -206,13 +223,37 @@ def test_render_draws_zones_and_virtual_walls_without_trace() -> None: vertices=_world_vertices(VECTOR_CALIBRATION, [(1, 1), (6, 1)]), ) ] + config = MapParserConfig(drawables=list(Drawable)) - base = _render(packet) - rendered = _render(packet, overlays=Q10MapOverlays(zones=zones, virtual_walls=walls)) + base = _render(packet, config=config) + rendered = _render(packet, overlays=Q10MapOverlays(zones=zones, virtual_walls=walls), config=config) assert rendered != base +def test_render_disables_all_drawables_when_empty() -> None: + """An empty drawables list omits trace, charger, and restriction overlays.""" + packet, trace = _calibrated_inputs() + overlays = Q10MapOverlays( + zones=[ + Q10Zone( + type=ZONE_TYPE_NO_GO, + vertices=_world_vertices(VECTOR_CALIBRATION, [(1, 1), (4, 1), (4, 4), (1, 4)]), + ) + ], + virtual_walls=[ + Q10Zone( + type=ZONE_TYPE_VIRTUAL_WALL, + vertices=_world_vertices(VECTOR_CALIBRATION, [(1, 1), (6, 1)]), + ) + ], + ) + no_drawables = MapParserConfig(drawables=[]) + assert _render(packet, trace=trace, overlays=overlays, config=no_drawables) == _render( + _packet(), config=no_drawables + ) + + def test_render_draws_dock_from_header_without_trace() -> None: """The saved dock remains visible while the robot has no cleaning trace.""" packet = _packet() @@ -353,7 +394,7 @@ def test_render_obstacle_at_header_anchored_position(origin: tuple[int, int]) -> header_calibration=header, obstacles=[Q10Obstacle((50 - origin[0] // 10) * 50, (origin[1] // 10 - 40) * 50)], ) - config = B01Q10MapParserConfig(map_scale=4, drawables=[Drawable.OBSTACLES]) + config = MapParserConfig(map_scale=4, drawables=[Drawable.OBSTACLES]) rendered = Image.open(io.BytesIO(render_q10_map(packet, None, Q10MapOverlays(), config=config))) base = Image.open(io.BytesIO(render_q10_map(replace(packet, obstacles=[]), None, Q10MapOverlays(), config=config))) assert rendered.getpixel((200, 160)) != base.getpixel((200, 160)) @@ -363,7 +404,8 @@ def test_render_obstacle_at_header_anchored_position(origin: tuple[int, int]) -> @pytest.mark.parametrize("header", [None, Q10HeaderCalibration(0, 0, 5, -1, -1, 0)]) def test_render_unanchored_obstacle_matches_base(header: Q10HeaderCalibration | None) -> None: packet = replace(_packet(), header_calibration=header, obstacles=[Q10Obstacle(100, 100)]) - assert _render(packet) == _render(replace(packet, obstacles=[])) + config = MapParserConfig(drawables=[Drawable.OBSTACLES]) + assert _render(packet, config=config) == _render(replace(packet, obstacles=[]), config=config) def test_render_obstacles_respects_drawables_and_ignores_skip_markers() -> None: @@ -379,7 +421,7 @@ def test_render_obstacles_respects_drawables_and_ignores_skip_markers() -> None: ) def render(drawables: list[Drawable], source: Q10MapPacket = packet) -> bytes: - return render_q10_map(source, None, Q10MapOverlays(), config=B01Q10MapParserConfig(drawables=drawables)) + return render_q10_map(source, None, Q10MapOverlays(), config=MapParserConfig(drawables=drawables)) assert render([Drawable.OBSTACLES]) != render([]) assert render([]) == render([], replace(packet, obstacles=[])) diff --git a/tests/map/test_benchmarks.py b/tests/map/test_benchmarks.py index 392decd4..1cf6c057 100644 --- a/tests/map/test_benchmarks.py +++ b/tests/map/test_benchmarks.py @@ -31,7 +31,6 @@ from vacuum_map_parser_base.config.drawable import Drawable from roborock.map.b01_q10_map_parser import ( - B01Q10MapParserConfig, Q10MapPacket, Q10MapPacketKind, Q10Room, @@ -185,7 +184,7 @@ def test_benchmark_q10_map_packet_unpack(benchmark: BenchmarkFixture) -> None: def test_benchmark_q10_map_parse_and_render(benchmark: BenchmarkFixture) -> None: """Benchmark end-to-end Q10 small map packet decoding and PNG rendering.""" raw_data = _Q10_MAP.read_bytes() - config = B01Q10MapParserConfig() + config = MapParserConfig() overlays = Q10MapOverlays() result = benchmark(lambda: render_q10_map(parse_map_packet(raw_data), None, overlays, config=config)) _validate_image(result, (32, 24)) @@ -194,7 +193,7 @@ def test_benchmark_q10_map_parse_and_render(benchmark: BenchmarkFixture) -> None def test_benchmark_q10_map_full_scale_render_scale_1(benchmark: BenchmarkFixture) -> None: """Benchmark Q10 composite rendering on a realistic 200x200 4-room floorplan at scale 1.""" packet = _build_synthetic_q10_grid(200, 200) - config = B01Q10MapParserConfig(map_scale=1) + config = MapParserConfig(map_scale=1) result = benchmark(render_q10_map, packet, None, Q10MapOverlays(), config=config) _validate_image(result, (200, 200)) @@ -202,7 +201,7 @@ def test_benchmark_q10_map_full_scale_render_scale_1(benchmark: BenchmarkFixture def test_benchmark_q10_map_full_scale_render_scale_4(benchmark: BenchmarkFixture) -> None: """Benchmark Q10 composite rendering on a realistic 200x200 4-room floorplan at scale 4.""" packet = _build_synthetic_q10_grid(200, 200) - config = B01Q10MapParserConfig(map_scale=4) + config = MapParserConfig(map_scale=4) result = benchmark(render_q10_map, packet, None, Q10MapOverlays(), config=config) _validate_image(result, (800, 800)) @@ -213,7 +212,7 @@ def test_benchmark_q10_map_render_with_trace(benchmark: BenchmarkFixture) -> Non trace_bytes = _Q10_TRACE.read_bytes() packet = parse_map_packet(map_bytes) trace = parse_trace_packet(trace_bytes) - config = B01Q10MapParserConfig(map_scale=4) + config = MapParserConfig(map_scale=4) result = benchmark(render_q10_map, packet, trace=trace, overlays=Q10MapOverlays(), config=config) _validate_image(result, (32, 24)) @@ -236,8 +235,8 @@ def _run_benchmarks(iterations: int, warmup: int, profile: bool) -> None: p_v1_s6_no_draw = MapParser(MapParserConfig(map_scale=4, drawables=[])) p_v1_s6_all_draw = MapParser(MapParserConfig(map_scale=4, drawables=list(Drawable))) - q10_config_scale1 = B01Q10MapParserConfig(map_scale=1) - q10_render_config = B01Q10MapParserConfig(map_scale=4) + q10_config_scale1 = MapParserConfig(map_scale=1) + q10_render_config = MapParserConfig(map_scale=4) q10_overlays = Q10MapOverlays() q10_trace_data = _Q10_TRACE.read_bytes()