Skip to content

Commit

Permalink
Add a bunch of convenience features
Browse files Browse the repository at this point in the history
Microsoft Windows only:
    - Only allow a single instance
    - Always on top option
    - Touch/click doesn’t steal focus
    - Events triggered without having to activate the window fist

- Automatically try to reconnect with exponential backoff
- Implemented command timeouts per spec
- Fixed volume calculations and display at extremes
- Disable shortcuts for disabled buttons
- Disable volume buttons at extremes
- Improved debugging system
- Update readme and screenshots
  • Loading branch information
rdoursenaud committed Mar 11, 2022
1 parent 6500199 commit a8a05ba
Show file tree
Hide file tree
Showing 11 changed files with 172 additions and 62 deletions.
20 changes: 15 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ Denon Remote
Control [Denon Professional DN-500AV surround preamplifier](https://www.denonpro.com/index.php/products/view/dn-500av)
remotely.

![Screenshot](screenshot-v0.5.0-main.png)
![Screenshot](screenshot-v0.7.0-main.png)

![Settings Screenshot](screenshot-v0.5.0-settings.png)
![Settings Screenshot](screenshot-v0.7.0-settings.png)

Author: Raphael Doursenaud <rdoursenaud+denonremote@gmail.com>

Expand All @@ -19,17 +19,22 @@ Fonts used:
- [Unicode Power Symbol](https://unicodepowersymbol.com/) Copyright (c) 2013 Joe Loughry licensed under MIT
- [Free Serif](https://savannah.gnu.org/projects/freefont/) licensed under GPLv3


### Features


#### Target hardware

- [x] Denon Professional DN-500AV (Seems based on the same platform as the Denon AVR-1912 and AVR-2112CI.)
- [ ] More? Contributions welcome!


#### Communication

- [x] Ethernet
- [x] Using [Twisted](https://twistedmatrix.com):
- [x] Using [Twisted](https://twistedmatrix.com)
- [x] connection status detection
- [x] automatically try to reconnect with exponential backoff
- [ ] RS-232? also using Twisted
- [ ] General MIDI input using [Mido](https://mido.readthedocs.io/en/latest/)
- [ ] Define control scheme.
Expand Down Expand Up @@ -87,6 +92,7 @@ Fonts used:
- [x] Update the GUI
- [ ] Import EQ settings
- [ ] From [REW](https://www.roomeqwizard.com/) value file
- [ ] Only use negative values! You can’t compensate a destructive room mode by adding energy to it.
- [ ] Full Profiles/presets?

##### GUI
Expand All @@ -98,7 +104,11 @@ Fonts used:
- [ ] Left/Right VolPreset +/-
- [ ] PgUp/PgDwn SrcPreset +/-
- [x] Systray/Taskbar support using [pystray](https://pypi.org/project/pystray/)
- [ ] Only one instance should be allowed
- [x] Only one instance is allowed (Microsoft Windows only)
- [X] Option to make window stay always on top (Microsoft Windows only)
- [x] Touch doesn’t activate the window and doesn’t steal focus (Microsoft Windows only)
- [x] Trigger events without having to activate the window first (Microsoft Windows only)
- [ ] Draw it on the first touch enabled display if available instead of the main one

##### Windows executable

Expand Down Expand Up @@ -161,7 +171,7 @@ PHP
Python:

- https://github.com/jeroenvds/denonremote (XBMC plugin)
- https://github.com/Tom360V/DenonAvr (Similar objectives?
- https://github.com/Tom360V/DenonAvr (Similar objectives?)
- https://github.com/toebsen/python-denonavr (HTTP RESTful server)
- https://github.com/MrJavaWolf/DenonPhoneController (Landline phone controller)
- https://github.com/troykelly/python-denon-avr-serial-over-ip (Library)
Expand Down
29 changes: 23 additions & 6 deletions denonremote/denon/communication.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from twisted.internet import task, reactor
from twisted.internet.protocol import ClientFactory
from twisted.protocols.basic import LineOnlyReceiver
from twisted.protocols.policies import TimeoutMixin

from denon.dn500av import DN500AVMessage, DN500AVFormat

Expand All @@ -15,12 +16,20 @@
# See: https://twistedmatrix.com/documents/15.4.0/api/twisted.internet.serialport.SerialPort.html


class DenonProtocol(LineOnlyReceiver):
class DenonProtocol(LineOnlyReceiver, TimeoutMixin):
# From DN-500 manual (DN-500AVEM_ENG_CD-ROM_v00.pdf) page 91 (97 in PDF form)
MAX_LENGTH = 135
DELAY = 0.04 # in seconds. The documentation requires 200 ms. 40 ms seems safe.
DELAY = 0.04
"""
Delay between messages in seconds.
The documentation requires 200 ms. 40 ms seems safe.
"""
TIMEOUT = 0.2
"""
Requests shall time out if no reply is received in under 200 ms.
"""
delimiter = b'\r'
ongoing_calls = 0 # Delay handling. FIXME: should timeout after 200 ms.
ongoing_calls = 0 # Delay handling.

def connectionMade(self):
logger.debug("Connection made")
Expand All @@ -38,14 +47,22 @@ def sendLine(self, line):
# A request is made. We need to delay the next calls
self.ongoing_calls += 1
logger.debug("Ongoing calls for delay: %s", self.ongoing_calls)
delay = 0
delay = 0 # Send now
if self.ongoing_calls > 0:
delay = self.DELAY * (self.ongoing_calls - 1)
delay = self.DELAY * (self.ongoing_calls - 1) # Send after other messages
logger.debug("Will send line: %s in %f seconds", line, delay)
return task.deferLater(reactor, delay=delay,
callable=super().sendLine, line=line)
callable=self.sendLineWithTimeout, line=line)

def sendLineWithTimeout(self, line):
timeout = self.TIMEOUT if self.timeOut is None else self.timeOut + self.TIMEOUT
self.setTimeout(timeout)
del timeout
super().sendLine(line)

def lineReceived(self, line):
self.resetTimeout()
self.setTimeout(None)
if self.ongoing_calls:
# We received a reply
self.ongoing_calls -= 1
Expand Down
50 changes: 27 additions & 23 deletions denonremote/denon/dn500av.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,42 +83,46 @@ def compute_master_volume_label(value, zerodb_ref=MASTER_VOLUME_ZERODB_REF):
"""Convert Master Volume ASCII value to dB"""
# TODO: Handle absolute values
label = '---.-dB'
result = None
if int(value[:2]) < MASTER_VOLUME_MIN or int(value[:2]) > MASTER_VOLUME_MAX:
logger.error("Master volume value %s out of bounds (%s-%s)", value, MASTER_VOLUME_MIN, MASTER_VOLUME_MAX)
# Quirks
if value == '99':
result = "-∞dB"
elif value == '995':
result = "-80.5dB"
elif len(value) == 2:
# General case
result = str(float(value) - zerodb_ref)
if value == '99':
result = "" # Minus inf
else:
# General case
result = str(float(value) - zerodb_ref)
elif len(value) == 3:
# Handle undocumented special case for half dB

# Hardcode values around 0 because of computing sign uncertainty
# FIXME: test '985' which seems invalid
if value == str((zerodb_ref - 1)) + '5':
result = "-0.5"
elif value == str(zerodb_ref) + '5':
result = "0.5"
if value == '995':
result = "-80.5"
else:
value = int(value[:2]) # Drop the third digit
offset = 0
if value < zerodb_ref:
offset = 1
logger.debug("Add offset %i to dB calculation with value %i5", offset, value)
result = str(int(value + offset - zerodb_ref)) + ".5"

# Hardcode values around 0 because of computing sign uncertainty
if value == str((zerodb_ref - 1)) + '5':
result = "-0.5"
elif value == str(zerodb_ref) + '5':
result = "0.5"
else:
value = int(value[:2]) # Drop the third digit
offset = 0
if value < zerodb_ref:
offset = 1
logger.debug("Add offset %i to dB calculation with value %i5", offset, value)
result = str(int(value + offset - zerodb_ref)) + ".5"
else:
raise ValueError

# Format label with fixed width like the actual display:
# [ NEG SIGN or EMPTY ] [ DIGIT er EMPTY ] [ DIGIT ] [ DOT ] [ DIGIT ] [ d ] [ B ]
label = "%s%s%s.%sdB" % (
result[0] if result[0] == '-' else " ",
" " if len(result) <= 3 or result[-4] == '-' else result[-4],
result[-3],
result[-1])
if result:
label = "%s%s%s.%sdB" % (
result[0] if result[0] == '-' else " ",
" " if len(result) <= 3 or result[-4] == '-' else result[-4],
result[-3],
result[-1])

logger.debug(label)
return label
Expand Down
4 changes: 2 additions & 2 deletions denonremote/denonremote.kv
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,12 @@ FloatLayout:
id: mode_k20
text: "SMPTE/K-20"
group: 'mode_ref'
state: 'down' # Default
on_press: app.mode_changed(self)

ForcedToggleButton
id: mode_ebu
text: "EBU"
state: 'down' # Default
group: 'mode_ref'
on_press: app.mode_changed(self)

Expand Down Expand Up @@ -131,7 +131,7 @@ FloatLayout:

TextInput:
id: volume_display
text: "---.-dB"
text: "---.-dB" # TODO: decorrelate display from serial commands (SI mandates a space before the unit)
font_name: 'RobotoMono-Regular'
font_size: 36
halign: 'center'
Expand Down
Loading

0 comments on commit a8a05ba

Please sign in to comment.