Skip to content

Sonance fixes#7

Open
haklai wants to merge 5 commits into
rsnodgrass:masterfrom
haklai:sonance-fixes
Open

Sonance fixes#7
haklai wants to merge 5 commits into
rsnodgrass:masterfrom
haklai:sonance-fixes

Conversation

@haklai
Copy link
Copy Markdown

@haklai haklai commented Apr 10, 2026

Sonance C4630 SE: fix EQ commands, response regex, and multi-query zone status

Tested on Sonance C4630 SE (6-zone). Three bugs prevented EQ and volume
status from working correctly with this amplifier.

Changes

pyxantech/series/sonance6.yaml

Add min_bass, min_treble, min_balance to the device config. The
max values were already present but the min values were missing, causing
get_device_config() to return None for EQ minimums and the clamping
logic to silently floor at 0.

pyxantech/__init__.py

Fix EQ command clamping: _set_bass_cmd, _set_treble_cmd,
_set_balance_cmd all hardcoded 0 as the minimum. They now read
min_bass/min_treble/min_balance from device config (falling back
to 0, so all other amp types are unaffected).

pyxantech/protocols/sonance.yaml — three fixes:

  1. Signed EQ commands: set_bass/set_treble/set_balance used
    {value:02} which cannot represent negative numbers. Changed to
    {value:+03d} so negative values produce :L1-05 instead of
    garbage.

  2. Multi-query zone status: :Z{zone}? only returns power state on
    Sonance — not volume, source, mute, or EQ. Added zone_status_queries
    list; __init__.py now sends separate queries for each attribute and
    merges the results into one ZoneStatus. Non-Sonance amp types are
    unaffected (no zone_status_queries defined in their protocol).

  3. Response regex fixes:

    • Volume: \+V(?P<zone>\d+)(?P<volume>\d+) was greedy — +V460
      matched as zone=46, volume=0. Fixed by restricting zone group to
      [1-6].
    • Bass/treble/balance: patterns had spurious BS/TR/BA literals
      that don't appear in actual responses (device sends +L1+5, not
      +L1BS+5). Removed. Capture groups now match signed values with
      [+-]?\d+.

Affected hardware

  • Sonance C4630 SE (6-zone)
  • Sonance 875D MKII (4-zone, same protocol — unverified)

Other amp types (xantech8, monoprice6, dax88, zpr68) are unaffected:
the zone_status_queries path is only taken when the protocol config
defines that key, and their EQ min-value fallback is 0 as before.

@mesa-dot-dev
Copy link
Copy Markdown

mesa-dot-dev Bot commented Apr 10, 2026

Mesa Description

TL;DR

Fixes multiple bugs in the Sonance amplifier protocol, specifically related to EQ command formatting, response parsing, and multi-query zone status retrieval, ensuring correct functionality for Sonance C4630 SE and similar devices.

What changed?

  • pyxantech/series/sonance6.yaml: Added min_balance, min_bass, and min_treble limits to the device configuration.
  • pyxantech/__init__.py:
    • Modified EQ command clamping functions (_set_bass_cmd, _set_treble_cmd, _set_balance_cmd) to read minimum values from the device config.
    • Implemented multi-query zone status retrieval via new _zone_status_query_commands and _merge_zone_status_responses functions, updating synchronous and asynchronous zone_status methods.
  • pyxantech/protocols/sonance.yaml:
    • Updated set_bass, set_treble, and set_balance commands to use {value:+03d} for signed, three-digit values.
    • Added zone_status_queries to define commands for individual attribute queries to construct full zone status.
    • Corrected volume_status regex to restrict the zone capture group to [1-6].
    • Removed spurious literals (BS, TR, BA) from bass/treble/balance response regexes and updated them to correctly match signed values ([+-]?\d+).

Description generated by Mesa. Update settings

Copy link
Copy Markdown

@mesa-dot-dev mesa-dot-dev Bot left a comment

Choose a reason for hiding this comment

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

Performed full review of d994e0c...31e98f7

Analysis

• Silent failure mode in _merge_zone_status_responses() returns partial data without logging or indication to caller, creating observability gaps that make debugging communication issues harder and mask incomplete status as complete

• Configuration validation gap: new min_bass/min_treble/min_balance fields use fallback defaults when missing, allowing misconfigured devices to silently use incorrect EQ bounds without detection

• Inconsistent error handling between single-query path (with warnings via ZoneStatus.from_string()) and multi-query path (silent skipping of failed queries) reduces debuggability across protocol implementations

• Significant test coverage gap: no tests for negative EQ formatting, multi-query zone status flow, partial response handling, or new Sonance regex patterns, leaving edge cases around partial failures unvalidated

Tip

Help

Slash Commands:

  • /review - Request a full code review
  • /review latest - Review only changes since the last review
  • /describe - Generate PR description. This will update the PR body or issue comment depending on your configuration
  • /help - Get help with Mesa commands and configuration options

0 files reviewed | 5 comments | Edit Agent SettingsRead Docs

Comment thread pyxantech/__init__.py Outdated
Comment thread pyxantech/__init__.py
Comment thread pyxantech/protocols/sonance.yaml
bass_status: '\+L(?P<zone>\d+)(?P<bass>[+-]?\d+)'
balance_status: '\+B(?P<zone>\d+)(?P<balance>[+-]?\d+)'

zone_status_queries:
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Medium

The zone_status_queries list defines 7 separate queries that will be executed sequentially for each zone status check. This is a significant behavioral change:

Performance Impact: Single query → 7 sequential queries

  • Each query involves serial I/O with response parsing
  • For slow serial connections (9600 baud), this could add noticeable latency
  • Consider if this could be optimized or cached

Network/Serial Load: 7x increase in command traffic

  • Matters for polled status updates
  • Consider rate limiting or batch status queries

Reliability: More opportunities for failure

  • If 1 of 7 queries times out, you get partial data (see comment on _merge_zone_status_responses)

Document this trade-off and consider adding a config option for query timeout to fail fast if serial communication is unreliable.

Fix in Cursor • Fix in Claude

Prompt for Agent
Task: Address review feedback left on GitHub.
Repository: rsnodgrass/pyxantech#7
File: pyxantech/protocols/sonance.yaml#L89
Action: Open this file location in your editor, inspect the highlighted code, and resolve the issue described below.

Feedback:
The `zone_status_queries` list defines 7 separate queries that will be executed sequentially for each zone status check. This is a significant behavioral change:

**Performance Impact**: Single query → 7 sequential queries
- Each query involves serial I/O with response parsing
- For slow serial connections (9600 baud), this could add noticeable latency
- Consider if this could be optimized or cached

**Network/Serial Load**: 7x increase in command traffic
- Matters for polled status updates
- Consider rate limiting or batch status queries

**Reliability**: More opportunities for failure
- If 1 of 7 queries times out, you get partial data (see comment on `_merge_zone_status_responses`)

Document this trade-off and consider adding a config option for query timeout to fail fast if serial communication is unreliable.

Comment thread pyxantech/protocols/sonance.yaml
haklai added 5 commits April 25, 2026 14:36
asyncio.Queue.put() is a coroutine; calling ensure_future on it from a
synchronous Protocol callback is unnecessary and fragile. put_nowait is
the correct synchronous API for an unbounded queue and avoids scheduling
a redundant task on every received byte.

Signed-off-by: Nadav Haklai <nadav.haklai@gmail.com>
The Sonance C4630 SE supports signed EQ ranges: bass/treble -8..+8,
balance -10..+10. The max values were already defined but min values
were missing, causing get_device_config() to return None for min limits
and the clamping logic to fall back to 0.

Signed-off-by: Nadav Haklai <nadav.haklai@gmail.com>
_set_bass_cmd, _set_treble_cmd, _set_balance_cmd all hardcoded 0 as
the minimum value, silently clamping negative inputs. Now reads
min_bass, min_treble, min_balance from device config (falling back to
0 if not defined, preserving existing behaviour for other amp types).

This allows Sonance C4630 SE to correctly send negative EQ values
(-8 to +8 for bass/treble, -10 to +10 for balance).

Signed-off-by: Nadav Haklai <nadav.haklai@gmail.com>
…_status

Three fixes for Sonance C4630 SE:

- set_bass/treble/balance: use signed format (:L{zone}{bass:+03d}) so negative
  values produce :L1-05 not garbage
- zone_status: :Z{zone}? only returns power; add zone_status_queries list and
  _zone_status_query_commands() / _merge_zone_status_responses() to merge
  separate V/S/M/L/H/B query results into one ZoneStatus
- response regex: remove spurious BS/TR/BA literals; restrict volume zone group
  to [1-6] (+V460 was parsing as zone=46 vol=0 instead of zone=4 vol=60)

Signed-off-by: Nadav Haklai <nadav.haklai@gmail.com>
Sonance C4630 SE sends unsolicited status broadcasts as side effects of
commands (e.g. source change triggers zone power notifications). These
extra lines arrive in the same TCP burst as the actual query response or
as a back-to-back packet, causing two classes of bug:

1. Wrong-zone values: _read_response returned only the first line, which
   could be an unsolicited broadcast. _merge_zone_status_responses then
   matched it without checking zone, silently writing another zone's
   values into the queried zone's status.

2. Dropped responses: the broadcast consumed the read slot; the actual
   response arrived later and was flushed by the next send().

Fix by returning all complete lines from _read_response (list[str])
instead of only the first, draining the queue non-blocking after the
first EOL to capture back-to-back packets, and scanning every returned
line in _merge_zone_status_responses for the first match whose zone
field equals the queried zone.

Signed-off-by: Nadav Haklai <nadav.haklai@gmail.com>
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.

1 participant