Skip to content

feat(display): double-sided mode — mirror one screen across the panel chain#375

Merged
ChuckBuilds merged 3 commits into
ChuckBuilds:mainfrom
rpierce99:feat/double-sided-mode
Jun 25, 2026
Merged

feat(display): double-sided mode — mirror one screen across the panel chain#375
ChuckBuilds merged 3 commits into
ChuckBuilds:mainfrom
rpierce99:feat/double-sided-mode

Conversation

@rpierce99

@rpierce99 rpierce99 commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

What

Adds an optional double-sided mode: render a plugin once at a logical (per-screen) size, then duplicate the rendered frame across the full physical panel chain so multiple panels show identical content.

  • 2 panels → one mirrored 64×32 screen on both
  • 4 panels → two identical 128×32 screens
  • Works on the chain axis (horizontal) or parallel outputs (vertical)

Why it's cheap

Plugins render once at the logical size — no extra render CPU. The only per-frame cost is a single PIL paste per copy in update_display() (an ~8 KB memcpy for 64×32), so it's effectively free.

How it works

Plugins size themselves from matrix.width/matrix.height (the documented convention), so a thin _LogicalMatrix proxy reports the logical size while delegating every real operation (CreateFrameCanvas, SwapOnVSync, brightness, Clear, …) to the underlying physical matrix. No plugin changes required. The full-chain buffer is reused each frame.

The web preview snapshot reads the logical image, so the dashboard shows one clean screen rather than the duplicated output.

Config

"display": {
  "double_sided": { "enabled": true, "copies": 2, "axis": "horizontal" }
}
  • axis: "horizontal" splits the chain (panels side by side); "vertical" splits parallel (stacked).
  • Invalid config (dimension not divisible by copies, bad axis/copies) logs a warning and falls back to single-screen rather than failing to light up.

Web UI

The Settings → Display page gets a Double-Sided Display section (enabled checkbox, copies, axis select). The save handler validates and persists it under display.double_sided. Like the other hardware settings on that page, it takes effect after a display restart.

Tests

  • TestDisplayManagerDoubleSided (6 cases): logical-dimension reporting, pixel-level horizontal and vertical tiling through the real update_display path, divisibility fallback, disabled pass-through, and brightness write forwarding through the proxy.
  • TestConfigAPI double-sided cases (4): form persistence, unchecked-disables, and 400s for invalid copies/axis.
  • test_display_manager.py + test_web_api.py pass (48/48).

🤖 Generated with Claude Code

…panel chain

Renders a plugin once at a logical (per-screen) size, then tiles the
rendered frame across the full physical chain so two (or more) panels show
identical content. A 128x32 chain configured with 2 copies drives two 64x32
screens; vertical axis splits parallel outputs instead of the chain.

Plugins size themselves from matrix.width/height, so a thin _LogicalMatrix
proxy reports the logical size while delegating every real operation
(CreateFrameCanvas, SwapOnVSync, brightness, Clear) to the physical matrix —
no plugin changes required. Duplication is a single PIL paste per copy in
update_display(), so render cost is unchanged.

Config: display.double_sided { enabled, copies, axis }. Invalid config
(non-divisible dimension, bad axis/copies) logs a warning and falls back to
single-screen rather than failing to light up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@rpierce99, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 42 minutes and 45 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 316ccf4f-1eee-4dd9-bb8d-b42669141a63

📥 Commits

Reviewing files that changed from the base of the PR and between eff4bcc and 688a07e.

📒 Files selected for processing (4)
  • src/display_manager.py
  • test/test_web_api.py
  • web_interface/blueprints/api_v3.py
  • web_interface/templates/v3/partials/display.html
📝 Walkthrough

Walkthrough

Adds a double_sided block to the config template and implements a configurable double-sided display mode in DisplayManager. A _LogicalMatrix proxy exposes per-screen logical dimensions to plugins while a new _composite_double_sided method tiles the logical image across physical matrix copies on a horizontal or vertical axis. Tests cover sizing, tiling, fallback, and brightness forwarding.

Changes

Double-sided display mode

Layer / File(s) Summary
Config schema, proxy, and validation
config/config.template.json, src/display_manager.py
Adds double_sided object (enabled, copies, axis) to the config template. Introduces _LogicalMatrix proxy to expose logical per-screen dimensions while delegating hardware ops, _resolve_double_sided to validate config and compute tiling parameters with warning-level disablement on bad input, and new _double_sided/_physical_image state fields on DisplayManager.
Matrix setup, fallback path, and frame compositing
src/display_manager.py
_setup_matrix() allocates a full-chain _physical_image buffer and wraps self.matrix with _LogicalMatrix when double-sided resolves. The simulation/fallback path mirrors this to show one logical screen. _composite_double_sided() pastes self.image into _physical_image across copies on the configured axis. update_display() routes through the composite or the plain logical image before SwapOnVSync.
Double-sided test suite
test/test_display_manager.py
TestDisplayManagerDoubleSided covers logical sizing for horizontal and vertical modes, pixel tiling correctness in both axes, fallback when physical dimensions are not evenly divisible, pass-through when enabled=False, and brightness forwarding through the proxy.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 70.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely summarizes the main change—adding a double-sided mode that mirrors one logical screen across a panel chain. It aligns perfectly with the primary feature introduced in the PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codacy-production

codacy-production Bot commented Jun 23, 2026

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 21 complexity · 0 duplication

Metric Results
Complexity 21
Duplication 0

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
src/display_manager.py (2)

58-58: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Optional: sort __slots__ to clear the Ruff RUF023 warning.

Static analysis flags the tuple ordering; reorder to silence it and keep lint clean.

♻️ Proposed change
-    __slots__ = ("_matrix", "_logical_width", "_logical_height")
+    __slots__ = ("_logical_height", "_logical_width", "_matrix")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/display_manager.py` at line 58, The __slots__ tuple is not sorted in
alphabetical order, which triggers the Ruff RUF023 warning. Reorder the items in
the __slots__ tuple that contains "_matrix", "_logical_width", and
"_logical_height" to be in alphabetical order to resolve the linting warning.

Source: Linters/SAST tools


60-71: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add return type hints to the new public-ish helpers.

The coding guidelines require type hints for parameters and return values. _resolve_double_sided returns Optional[Dict[str, Any]], and the _LogicalMatrix constructor/properties are unannotated. Annotating clarifies the logical/physical contract for future maintainers.

♻️ Suggested annotations
-    def __init__(self, matrix, logical_width, logical_height):
+    def __init__(self, matrix, logical_width: int, logical_height: int) -> None:
         object.__setattr__(self, "_matrix", matrix)
         object.__setattr__(self, "_logical_width", logical_width)
         object.__setattr__(self, "_logical_height", logical_height)

     `@property`
-    def width(self):
+    def width(self) -> int:
         return self._logical_width

     `@property`
-    def height(self):
+    def height(self) -> int:
         return self._logical_height
 def _resolve_double_sided(physical_width: int, physical_height: int,
-                          ds_config: Dict[str, Any]):
+                          ds_config: Dict[str, Any]) -> Optional[Dict[str, Any]]:
As per coding guidelines: "Use type hints for function parameters and return values".

Also applies to: 83-84

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/display_manager.py` around lines 60 - 71, The `__init__` method of the
_LogicalMatrix class is missing type hints for its parameters (matrix,
logical_width, logical_height) and return type annotation, and the `width` and
`height` properties are missing return type hints. Add appropriate type
annotations to the `__init__` method parameters based on their expected types,
add a return type annotation (None) to the `__init__` method, and add return
type hints to the `width` and `height` properties to clarify the data types they
return, following the project's coding guidelines that require type hints for
function parameters and return values.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/display_manager.py`:
- Line 58: The __slots__ tuple is not sorted in alphabetical order, which
triggers the Ruff RUF023 warning. Reorder the items in the __slots__ tuple that
contains "_matrix", "_logical_width", and "_logical_height" to be in
alphabetical order to resolve the linting warning.
- Around line 60-71: The `__init__` method of the _LogicalMatrix class is
missing type hints for its parameters (matrix, logical_width, logical_height)
and return type annotation, and the `width` and `height` properties are missing
return type hints. Add appropriate type annotations to the `__init__` method
parameters based on their expected types, add a return type annotation (None) to
the `__init__` method, and add return type hints to the `width` and `height`
properties to clarify the data types they return, following the project's coding
guidelines that require type hints for function parameters and return values.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: acec5af0-b227-4d82-8f7f-df6f9541a46a

📥 Commits

Reviewing files that changed from the base of the PR and between d297dd6 and eff4bcc.

📒 Files selected for processing (3)
  • config/config.template.json
  • src/display_manager.py
  • test/test_display_manager.py

rpierce99 and others added 2 commits June 22, 2026 20:11
Adds a Double-Sided Display section to the Display settings page (enabled
checkbox, copies, horizontal/vertical axis) and wires the save handler to
persist it under display.double_sided. Validates copies (2-8) and axis,
returning 400 on bad input; an omitted checkbox is saved as disabled.
Like the other hardware fields, changes take effect after a display restart.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Addresses CodeRabbit nits: sort _LogicalMatrix.__slots__ (Ruff RUF023),
annotate the proxy's __init__/properties/dunders and _resolve_double_sided's
return type, and add docstrings to the property/dunder methods.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@ChuckBuilds ChuckBuilds merged commit fefc2d4 into ChuckBuilds:main Jun 25, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants