Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 126 additions & 7 deletions hmdriver2/hdc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'(?<![\w])Bounds<L,\s*T,\s*W,\s*H>:\s*\d+,\s*\d+,\s*(\d+),\s*(\d+)'
)
_DMS_ACTIVE_MODES_RE = re.compile(
r'activeModes<id,\s*W,\s*H,\s*RS>:\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<L,T,W,H> 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()
Expand Down Expand Up @@ -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):
Expand Down
243 changes: 243 additions & 0 deletions tests/test_display_size.py
Original file line number Diff line number Diff line change
@@ -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<id, W, H, RS>: 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<X, Y>: 406.4, 416.621
Offset<X, Y>: 0, 0
Bounds<L, T, W, H>: 0, 0, 1136, 2690
PhyBounds<L, T, W, H>: 0, 0, 1136, 2690
AvailableArea<X, Y, W, H>: 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<id, W, H, RS>: 0, 2496, 2224, 60
Bounds<L,T,W,H>: 0, 0, 2496, 2224,
PhyBounds<L,T,W,H>: 0, 0, 2496, 2224,
---------------- Screen ID: 5 ----------------
FoldStatus: FOLDED
activeModes<id, W, H, RS>: 0, 1136, 2690, 120
Bounds<L,T,W,H>: 0, 0, 1136, 2690,
PhyBounds<L,T,W,H>: 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<id, W, H, RS>: 0, 2496, 2224, 60
Bounds<L, T, W, H>: 0, 0, 2496, 2224
PhyBounds<L, T, W, H>: 0, 0, 2496, 2224
AvailableArea<X, Y, W, H>: 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<id, W, H, RS>: 0, 1260, 2720, 120
[SCREEN PROPERTY]
Bounds<L,T,W,H>: 0, 0, 1260, 2720,
PhyBounds<L,T,W,H>: 0, 0, 1260, 2720,
AvailableArea<X,Y,W,H> 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<L, T, W, H>: 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)