99``MapDpsTrait`` owns the low-level DPS read model. ``MapContentTrait`` depends
1010on it and combines that state with the latest map/trace packets through the pure
1111functions in :mod:`roborock.map.b01_q10_render`. The high-level trait keeps only
12- one grouped source snapshot and one replace-whole rendered image; calibration,
13- path placement and overlay placement are not independently mutable trait state .
12+ the latest value from each source and one replace-whole rendered image;
13+ calibration, path placement and overlay placement remain inside the renderer .
1414"""
1515
1616import logging
17- from dataclasses import dataclass , field , replace
17+ from dataclasses import dataclass , field
1818
1919from roborock .data import RoborockBase
2020from roborock .data .b01_q10 .b01_q10_code_mappings import B01_Q10_DP
@@ -52,21 +52,22 @@ def __init__(self) -> None:
5252 MapDps .__init__ (self )
5353 UpdatableTrait .__init__ (self , command = None , logger = _LOGGER )
5454
55+ @property
56+ def zones (self ) -> list [Q10Zone ]:
57+ """Restricted zones decoded from the latest DPS value."""
58+ return parse_zone_blob (self .restricted_zone_up )
5559
56- @dataclass (frozen = True )
57- class Q10MapSource :
58- """The latest map-protocol inputs, replaced atomically on every push."""
59-
60- map_packet : Q10MapPacket | None = None
61- trace_packet : Q10TracePacket | None = None
60+ @property
61+ def virtual_walls (self ) -> list [Q10Zone ]:
62+ """Virtual walls decoded from the latest DPS value."""
63+ return parse_virtual_wall_blob (self .virtual_wall_up )
6264
6365
6466class MapContentTrait (TraitUpdateListener ):
6567 """High-level composed Q10 map view.
6668
67- Map and trace packet updates replace :attr:`_source`; DPS updates are owned
68- by the injected :class:`MapDpsTrait`. Rendering always produces one new
69- image, keeping the externally visible fields consistent.
69+ The latest map and trace packets are combined with the injected
70+ :class:`MapDpsTrait` whenever any of those three sources changes.
7071 """
7172
7273 def __init__ (
@@ -78,7 +79,8 @@ def __init__(
7879 TraitUpdateListener .__init__ (self , logger = _LOGGER )
7980 self ._config = map_parser_config or B01Q10MapParserConfig ()
8081 self ._map_dps = map_dps or MapDpsTrait ()
81- self ._source = Q10MapSource ()
82+ self ._map_packet : Q10MapPacket | None = None
83+ self ._trace_packet : Q10TracePacket | None = None
8284 self ._image_content : bytes | None = None
8385 self ._map_dps .add_update_listener (self ._map_dps_updated )
8486
@@ -90,77 +92,53 @@ def image_content(self) -> bytes | None:
9092 @property
9193 def rooms (self ) -> list [Q10Room ]:
9294 """Rooms reported by the device."""
93- packet = self ._source .map_packet
94- return packet .rooms if packet else []
95+ return self ._map_packet .rooms if self ._map_packet else []
9596
9697 @property
9798 def path (self ) -> list [Q10Point ]:
9899 """Full path from the latest trace packet."""
99- trace = self ._source .trace_packet
100- return trace .points if trace else []
100+ return self ._trace_packet .points if self ._trace_packet else []
101101
102102 @property
103103 def robot_position (self ) -> Q10Point | None :
104104 """Current robot position from the latest trace packet."""
105- trace = self ._source .trace_packet
106- return trace .robot_position if trace else None
105+ return self ._trace_packet .robot_position if self ._trace_packet else None
107106
108107 @property
109108 def robot_heading (self ) -> int | None :
110109 """Current robot heading from the latest trace packet."""
111- trace = self ._source .trace_packet
112- return trace .heading if trace else None
113-
114- @property
115- def zones (self ) -> list [Q10Zone ]:
116- """Restricted zones decoded from the low-level DPS trait."""
117- return parse_zone_blob (self ._map_dps .restricted_zone_up )
118-
119- @property
120- def virtual_walls (self ) -> list [Q10Zone ]:
121- """Virtual walls decoded from the low-level DPS trait."""
122- return parse_virtual_wall_blob (self ._map_dps .virtual_wall_up )
110+ return self ._trace_packet .heading if self ._trace_packet else None
123111
124112 def update_from_map_packet (self , packet : Q10MapPacket ) -> None :
125- """Replace the current map packet and compose a consistent result."""
126- source = replace (self ._source , map_packet = packet )
127- render = self ._compose (source )
128- if render is None :
129- return
130- self ._source = source
131- self ._image_content = render
113+ """Store a map-protocol update and render the latest sources."""
114+ self ._map_packet = packet
115+ self ._render ()
132116 self ._notify_update ()
133117
134118 def update_from_trace_packet (self , packet : Q10TracePacket ) -> None :
135- """Replace the complete current-session trace packet."""
136- self ._source = replace (self ._source , trace_packet = packet )
137- if self ._source .map_packet is not None :
138- self ._rebuild ()
119+ """Store a trace-protocol update and render the latest sources."""
120+ self ._trace_packet = packet
121+ self ._render ()
139122 self ._notify_update ()
140123
141124 def _map_dps_updated (self ) -> None :
142- """Recompose placed overlays after the low-level DPS state changes."""
143- if self ._source .map_packet is not None :
144- self ._rebuild ()
125+ """Render after the low-level DPS source changes."""
126+ self ._render ()
145127 self ._notify_update ()
146128
147- def _compose (self , source : Q10MapSource ) -> bytes | None :
148- """Compose a source snapshot, preserving the previous result on error ."""
149- if source . map_packet is None :
150- return None
129+ def _render (self ) -> None :
130+ """Render the latest map, trace and DPS sources, if a map is available ."""
131+ if self . _map_packet is None :
132+ return
151133 try :
152- return render_q10_map (
153- source .map_packet ,
154- source .trace_packet ,
155- Q10MapOverlays (zones = tuple (self .zones ), virtual_walls = tuple (self .virtual_walls )),
134+ self ._image_content = render_q10_map (
135+ self ._map_packet ,
136+ self ._trace_packet ,
137+ Q10MapOverlays (
138+ zones = tuple (self ._map_dps .zones ),
139+ virtual_walls = tuple (self ._map_dps .virtual_walls ),
140+ ),
156141 config = self ._config ,
157142 )
158143 except RoborockException as ex :
159144 _LOGGER .debug ("Failed to render Q10 map packet: %s" , ex )
160- return None
161-
162- def _rebuild (self ) -> None :
163- """Replace the derived render from the current source snapshot."""
164- render = self ._compose (self ._source )
165- if render is not None :
166- self ._image_content = render
0 commit comments