From 8033405d0c8ecf51cdbfcb5ca903d1bc5df35c3b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 18 Aug 2026 19:45:01 +0000 Subject: [PATCH] fix: parse foldable display size from 4607 dump The first activeMode: match on RenderService output is wrong on foldables: the inner panel stays in the dump after fold. Prefer DisplayManagerService (hidumper -s 4607) Bounds/FoldStatus, and fall back to the POWER_STATUS_ON RenderService panel. Fixes #17 Co-authored-by: Tony Jin <407243179@qq.com> --- hmdriver2/hdc.py | 133 ++++++++++++++++++-- tests/test_display_size.py | 243 +++++++++++++++++++++++++++++++++++++ 2 files changed, 369 insertions(+), 7 deletions(-) create mode 100644 tests/test_display_size.py diff --git a/hmdriver2/hdc.py b/hmdriver2/hdc.py index e1ff09c..872143f 100644 --- a/hmdriver2/hdc.py +++ b/hmdriver2/hdc.py @@ -49,6 +49,120 @@ def _build_hdc_prefix() -> str: return "hdc" +# DisplayManagerService SA id. Huawei's recommended dump for fold-aware bounds: +# hidumper -s 4607 -a -a +# See https://github.com/codematrixer/hmdriver2/issues/17 +_DMS_DISPLAY_SIZE_CMD = 'hidumper -s 4607 -a -a' +_RS_DISPLAY_SIZE_CMD = 'hidumper -s RenderService -a screen' + +_DMS_BOUNDS_RE = re.compile( + r'(?:\s*\d+,\s*\d+,\s*(\d+),\s*(\d+)' +) +_DMS_ACTIVE_MODES_RE = re.compile( + r'activeModes:\s*\d+,\s*(\d+),\s*(\d+),\s*\d+' +) +_DMS_FOLD_STATUS_RE = re.compile(r'FoldStatus:\s*(\w+)') +_RS_SCREEN_SPLIT_RE = re.compile(r'(?=screen\[\d+\]:)') +_RS_POWER_RE = re.compile(r'powerstatus=(\w+)') +_RS_ACTIVE_MODE_RE = re.compile(r'activeMode:\s*(\d+)x(\d+),\s*refreshrate=\d+') +_RS_VIRTUAL_RE = re.compile(r'isvirtual\s*=\s*true', re.IGNORECASE) + +_RS_POWER_PRIORITY = { + 'POWER_STATUS_ON': 0, + 'POWER_STATUS_SUSPEND': 1, + 'POWER_STATUS_OFF': 2, +} + + +def _parse_dms_display_size(data: str) -> Tuple[int, int]: + """Parse hidumper -s 4607 / DisplayManagerService output. + + Huawei highlighted Bounds as the current panel. When the dump + lists more than one Screen ID (inner + cover), FoldStatus selects the + live panel: FOLDED -> smaller cover, EXPANDED/HALF_FOLDED -> larger inner. + """ + if not data: + return (0, 0) + if 'FoldStatus' not in data and 'activeModes' not in data and 'Bounds<' not in data: + return (0, 0) + + fold_m = _DMS_FOLD_STATUS_RE.search(data) + fold_status = fold_m.group(1).upper() if fold_m else '' + + sections = re.split(r'(?=Screen ID\s*:)', data) + sizes: List[Tuple[int, int]] = [] + for section in sections: + bounds = _DMS_BOUNDS_RE.search(section) + if not bounds: + bounds = _DMS_ACTIVE_MODES_RE.search(section) + if bounds: + w, h = int(bounds.group(1)), int(bounds.group(2)) + if w > 0 and h > 0: + sizes.append((w, h)) + + if not sizes: + return (0, 0) + if len(sizes) == 1: + return sizes[0] + + # Multiple Screen ID blocks: do not take the first (often the stale inner). + if fold_status == 'FOLDED': + return min(sizes, key=lambda s: s[0] * s[1]) + return max(sizes, key=lambda s: s[0] * s[1]) + + +def _parse_renderservice_display_size(data: str) -> Tuple[int, int]: + """Parse hidumper -s RenderService -a screen. + + Foldables emit one screen[] block per panel, each with its own activeMode. + Prefer POWER_STATUS_ON, then SUSPEND (locked single panel). Skip virtual + screens. When every physical panel is OFF (locked foldable), there is no + live panel in this dump — return (0, 0) rather than a stale inner mode. + """ + if not data or 'activeMode:' not in data: + return (0, 0) + + screens: List[Tuple[int, int, int]] = [] # (priority, w, h) + for block in _RS_SCREEN_SPLIT_RE.split(data): + if _RS_VIRTUAL_RE.search(block): + continue + status_m = _RS_POWER_RE.search(block) + mode_m = _RS_ACTIVE_MODE_RE.search(block) + if not mode_m: + continue + w, h = int(mode_m.group(1)), int(mode_m.group(2)) + if w <= 0 or h <= 0: + continue + status = status_m.group(1) if status_m else '' + priority = _RS_POWER_PRIORITY.get(status, 99) + screens.append((priority, w, h)) + + if not screens: + match = _RS_ACTIVE_MODE_RE.search(data) + if match: + return int(match.group(1)), int(match.group(2)) + return (0, 0) + + best = min(screens, key=lambda s: s[0]) + if best[0] >= _RS_POWER_PRIORITY['POWER_STATUS_OFF'] and len(screens) > 1: + # Locked foldable: both cover and inner are OFF. Do not guess. + return (0, 0) + return (best[1], best[2]) + + +def parse_display_size(data: str) -> Tuple[int, int]: + """Extract (width, height) from a 4607 or RenderService hidumper dump. + + DisplayManagerService (4607) is preferred: its Bounds / FoldStatus already + reflect the current fold configuration. RenderService is the fallback and + must pick the powered-on panel, not the first activeMode: match. + """ + size = _parse_dms_display_size(data) + if size != (0, 0): + return size + return _parse_renderservice_display_size(data) + + def list_devices() -> List[str]: devices = [] hdc_prefix = _build_hdc_prefix() @@ -286,14 +400,19 @@ def cpu_abi(self) -> str: return self.__split_text(data) def display_size(self) -> Tuple[int, int]: - data = self.shell("hidumper -s RenderService -a screen").output - match = re.search(r'activeMode:\s*(\d+)x(\d+),\s*refreshrate=\d+', data) + """Return the current physical display size (width, height). - if match: - w = int(match.group(1)) - h = int(match.group(2)) - return (w, h) - return (0, 0) + Prefer DisplayManagerService (SA 4607): Huawei's recommended dump for + foldables, because Bounds tracks the live / unfolded panel. Fall back + to RenderService and pick POWER_STATUS_ON rather than the first + activeMode: match (which is often a stale inner panel when folded). + """ + data = self.shell(_DMS_DISPLAY_SIZE_CMD, error_raise=False).output + size = parse_display_size(data) + if size != (0, 0): + return size + data = self.shell(_RS_DISPLAY_SIZE_CMD, error_raise=False).output + return parse_display_size(data) def send_key(self, key_code: Union[KeyCode, int]) -> None: if isinstance(key_code, KeyCode): diff --git a/tests/test_display_size.py b/tests/test_display_size.py new file mode 100644 index 0000000..45133c9 --- /dev/null +++ b/tests/test_display_size.py @@ -0,0 +1,243 @@ +# -*- coding: utf-8 -*- +"""Unit tests for fold-aware display resolution parsing (issue #17). + +Dumps are reconstructed from the screenshots in +https://github.com/codematrixer/hmdriver2/issues/17 (lock / unlock / fold) +and Huawei's DisplayManagerService (SA 4607) sample. No device required. +""" + +import re + +from hmdriver2.hdc import parse_display_size + +# Exact pre-#17 display_size regex from hdc.py (first activeMode: match). +_LEGACY_ACTIVE_MODE_RE = re.compile(r'activeMode:\s*(\d+)x(\d+),\s*refreshrate=\d+') + + +def parse_legacy_active_mode(data: str): + match = _LEGACY_ACTIVE_MODE_RE.search(data or '') + if match: + return int(match.group(1)), int(match.group(2)) + return (0, 0) + + +# --- RenderService dumps from issue #17 comments --- + +# 1. Non-foldable, locked: single panel POWER_STATUS_SUSPEND +RS_NONFOLD_LOCK = """ +-------------------------------[ability]------------------------------- +----------------------------------RenderService--------------------------------- +-- ScreenInfo +screen[0]: id=0, powerstatus=POWER_STATUS_SUSPEND, backlight=15677, screenType=EXTERNAL_TYPE, render size: 1216x2688, physical screen resolution: 1216x2688, isvirtual=false, skipFrameInterval_:1 + supportedMode[0]: 1216x2688, refreshrate=120 + supportedMode[1]: 1216x2688, refreshrate=90 + supportedMode[2]: 1216x2688, refreshrate=60 + supportedMode[3]: 1216x2688, refreshrate=30 + activeMode: 1216x2688, refreshrate=60 + capability: name=, phywidth=70, phyheight=154, supportlayers=12, virtualDispCount=0, propCount=0, type=DISP_INTF_HDMI, supportWriteBack=false +""" + +# 2. Non-foldable, unlocked +RS_NONFOLD_UNLOCK = """ +-------------------------------[ability]------------------------------- +----------------------------------RenderService--------------------------------- +-- ScreenInfo +screen[0]: id=0, powerstatus=POWER_STATUS_ON, backlight=15677, screenType=EXTERNAL_TYPE, render size: 1216x2688, physical screen resolution: 1216x2688, isvirtual=false, skipFrameInterval_:1 + supportedMode[0]: 1216x2688, refreshrate=120 + supportedMode[1]: 1216x2688, refreshrate=90 + supportedMode[2]: 1216x2688, refreshrate=60 + supportedMode[3]: 1216x2688, refreshrate=30 + activeMode: 1216x2688, refreshrate=60 + capability: name=, phywidth=70, phyheight=154, supportlayers=12, virtualDispCount=0, propCount=0, type=DISP_INTF_HDMI, supportWriteBack=false +""" + +# 3. Foldable unfolded, unlocked: inner ON, cover OFF +RS_FOLD_UNFOLDED = """ +-------------------------------[ability]------------------------------- +----------------------------------RenderService--------------------------------- +-- ScreenInfo +screen[0]: id=0, powerstatus=POWER_STATUS_ON, backlight=4, screenType=EXTERNAL_TYPE, render size: 2496x2224, physical screen resolution: 2496x2224, isvirtual=false, skipFrameInterval_:1 + supportedMode[0]: 2496x2224, refreshrate=60 + activeMode: 2496x2224, refreshrate=60 + capability: name=express_display, phywidth=158, phyheight=141, supportlayers=10, virtualDispCount=1, propCount=0, type=DISP_INTF_HDMI, supportWriteBack=false +-- ScreenInfo +screen[1]: id=5, powerstatus=POWER_STATUS_OFF, backlight=4, screenType=EXTERNAL_TYPE, render size: 1080x2504, physical screen resolution: 1080x2504, isvirtual=false, skipFrameInterval_:1 + supportedMode[0]: 1080x2504, refreshrate=60 + activeMode: 1080x2504, refreshrate=60 + capability: name=express_display, phywidth=1080, phyheight=2504, supportlayers=10, virtualDispCount=1, propCount=0, type=DISP_INTF_HDMI, supportWriteBack=false +""" + +# 4. Foldable folded, unlocked: inner OFF (stale), cover ON +RS_FOLD_FOLDED = """ +-------------------------------[ability]------------------------------- +----------------------------------RenderService--------------------------------- +-- ScreenInfo +screen[0]: id=0, powerstatus=POWER_STATUS_OFF, backlight=4, screenType=EXTERNAL_TYPE, render size: 2496x2224, physical screen resolution: 2496x2224, isvirtual=false, skipFrameInterval_:1 + supportedMode[0]: 2496x2224, refreshrate=60 + activeMode: 2496x2224, refreshrate=60 + capability: name=express_display, phywidth=158, phyheight=141, supportlayers=10, virtualDispCount=1, propCount=0, type=DISP_INTF_HDMI, supportWriteBack=false +-- ScreenInfo +screen[1]: id=5, powerstatus=POWER_STATUS_ON, backlight=4, screenType=EXTERNAL_TYPE, render size: 1080x2504, physical screen resolution: 1080x2504, isvirtual=false, skipFrameInterval_:1 + supportedMode[0]: 1080x2504, refreshrate=60 + activeMode: 1080x2504, refreshrate=60 + capability: name=express_display, phywidth=1080, phyheight=2504, supportlayers=10, virtualDispCount=1, propCount=0, type=DISP_INTF_HDMI, supportWriteBack=false +""" + +# 5. Foldable locked: both panels POWER_STATUS_OFF — no live panel in this dump +RS_FOLD_LOCK = """ +-------------------------------[ability]------------------------------- +----------------------------------RenderService--------------------------------- +-- ScreenInfo +screen[0]: id=0, powerstatus=POWER_STATUS_OFF, backlight=4, screenType=EXTERNAL_TYPE, render size: 2496x2224, physical screen resolution: 2496x2224, isvirtual=false, skipFrameInterval_:1 + supportedMode[0]: 2496x2224, refreshrate=60 + activeMode: 2496x2224, refreshrate=60 + capability: name=express_display, phywidth=158, phyheight=141, supportlayers=10, virtualDispCount=1, propCount=0, type=DISP_INTF_HDMI, supportWriteBack=false +-- ScreenInfo +screen[1]: id=5, powerstatus=POWER_STATUS_OFF, backlight=4, screenType=EXTERNAL_TYPE, render size: 1080x2504, physical screen resolution: 1080x2504, isvirtual=false, skipFrameInterval_:1 + supportedMode[0]: 1080x2504, refreshrate=60 + activeMode: 1080x2504, refreshrate=60 + capability: name=express_display, phywidth=1080, phyheight=2504, supportlayers=10, virtualDispCount=1, propCount=0, type=DISP_INTF_HDMI, supportWriteBack=false +""" + +# --- DisplayManagerService (hidumper -s 4607 -a "-a") from Huawei's reply --- + +# Folded cover panel. Huawei boxed Bounds / PhyBounds / AvailableArea. +DMS_FOLD_FOLDED = """ +----------------------------------DisplayManagerService---------------------------------- +-------------- DMS Multi User Info -------------- +---------------- Screen ID: 0 ---------------- +FoldStatus: FOLDED +TentMode: FALSE +[SCREEN SESSION] +Name: UNKNOWN +RSScreenId: 0 +activeModes: 0, 1136, 2690, 120 +SourceMode: 0 +ScreenCombination: 0 +[SCREEN INFO] +VirtualWidth: 363 +VirtualHeight: 860 +VirtualPixelRatio: 3.125 +[SCREEN PROPERTY] +Density: 3.125 +PhyWidth: 71 +PhyHeight: 164 +RefreshRate: 60 +DPI: 406.4, 416.621 +Offset: 0, 0 +Bounds: 0, 0, 1136, 2690 +PhyBounds: 0, 0, 1136, 2690 +AvailableArea: 0, 0, 1136, 2690 +DefaultDeviceRotationOffset 0 +""" + +# Same 4607 format, two Screen ID blocks (stale inner first). FoldStatus=FOLDED +# must select the cover, not 2496x2224. +DMS_FOLD_FOLDED_MULTI = """ +----------------------------------DisplayManagerService---------------------------------- +---------------- Screen ID: 0 ---------------- +FoldStatus: FOLDED +activeModes: 0, 2496, 2224, 60 +Bounds: 0, 0, 2496, 2224, +PhyBounds: 0, 0, 2496, 2224, +---------------- Screen ID: 5 ---------------- +FoldStatus: FOLDED +activeModes: 0, 1136, 2690, 120 +Bounds: 0, 0, 1136, 2690, +PhyBounds: 0, 0, 1136, 2690, +""" + +# Unfolded inner panel (issue #17 screen[0] size, 4607 layout). +DMS_FOLD_UNFOLDED = """ +----------------------------------DisplayManagerService---------------------------------- +---------------- Screen ID: 0 ---------------- +FoldStatus: EXPANDED +TentMode: FALSE +activeModes: 0, 2496, 2224, 60 +Bounds: 0, 0, 2496, 2224 +PhyBounds: 0, 0, 2496, 2224 +AvailableArea: 0, 0, 2496, 2224 +""" + +# Non-foldable 4607 dump (public hidumper sample, Bounds without spaces). +DMS_NONFOLD = """ +----------------------------------DisplayManagerService---------------------------------- +-------------- DMS Multi User Info -------------- +---------------- Screen ID: 0 ---------------- +FoldStatus: UNKNOWN +[SCREEN SESSION] +Name: UNKNOWN +RSScreenId: 0 +activeModes: 0, 1260, 2720, 120 +[SCREEN PROPERTY] +Bounds: 0, 0, 1260, 2720, +PhyBounds: 0, 0, 1260, 2720, +AvailableArea 0, 0, 1260, 2720, +""" + + +def test_legacy_regex_picks_stale_inner_when_folded(): + """The old first-activeMode regex must fail on the folded dump.""" + assert parse_legacy_active_mode(RS_FOLD_FOLDED) == (2496, 2224) + assert parse_display_size(RS_FOLD_FOLDED) == (1080, 2504) + + +def test_legacy_regex_misses_4607_dump(): + """4607 uses Bounds / activeModes, so the old regex returns (0, 0).""" + assert parse_legacy_active_mode(DMS_FOLD_FOLDED) == (0, 0) + assert parse_display_size(DMS_FOLD_FOLDED) == (1136, 2690) + + +def test_legacy_regex_picks_stale_inner_on_fold_lock(): + """Locked foldable: both panels OFF; old regex still takes the inner mode.""" + assert parse_legacy_active_mode(RS_FOLD_LOCK) == (2496, 2224) + assert parse_display_size(RS_FOLD_LOCK) == (0, 0) + + +def test_rs_nonfold_lock(): + assert parse_display_size(RS_NONFOLD_LOCK) == (1216, 2688) + assert parse_legacy_active_mode(RS_NONFOLD_LOCK) == (1216, 2688) + + +def test_rs_nonfold_unlock(): + assert parse_display_size(RS_NONFOLD_UNLOCK) == (1216, 2688) + + +def test_rs_fold_unfolded_picks_inner_on(): + assert parse_display_size(RS_FOLD_UNFOLDED) == (2496, 2224) + + +def test_rs_fold_folded_picks_cover_on(): + assert parse_display_size(RS_FOLD_FOLDED) == (1080, 2504) + + +def test_dms_folded_bounds(): + assert parse_display_size(DMS_FOLD_FOLDED) == (1136, 2690) + + +def test_dms_folded_multi_screen_skips_stale_inner(): + assert parse_legacy_active_mode(DMS_FOLD_FOLDED_MULTI) == (0, 0) + assert parse_display_size(DMS_FOLD_FOLDED_MULTI) == (1136, 2690) + + +def test_dms_unfolded_bounds(): + assert parse_display_size(DMS_FOLD_UNFOLDED) == (2496, 2224) + + +def test_dms_nonfold_bounds(): + assert parse_display_size(DMS_NONFOLD) == (1260, 2720) + + +def test_dms_does_not_match_phybounds_only(): + dump = """ +---------------- Screen ID: 0 ---------------- +FoldStatus: UNKNOWN +PhyBounds: 0, 0, 999, 999 +""" + assert parse_display_size(dump) == (0, 0) + + +def test_empty_and_garbage(): + assert parse_display_size('') == (0, 0) + assert parse_display_size('no screen info here') == (0, 0) + assert parse_legacy_active_mode('') == (0, 0)