Skip to content

Commit 063a971

Browse files
committed
First prototype
1 parent 2c96a28 commit 063a971

File tree

11 files changed

+1373
-0
lines changed

11 files changed

+1373
-0
lines changed

README.md

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
Denon Remote
2+
============
3+
4+
Author: Raphael Doursenaud <[email protected]>
5+
6+
License: [GPLv3+](LICENSE)
7+
8+
Language: [Python](https://python.org) 3
9+
10+
### Features
11+
12+
#### Target hardware
13+
14+
- [x] DN-500AV
15+
- [ ] More? Contributions welcome!
16+
17+
#### Communication
18+
19+
- [x] Ethernet
20+
- [x] Using [Twisted](https://twistedmatrix.com):
21+
- [ ] RS-232? also using Twisted
22+
- [ ] General MIDI input using [Mido](https://mido.readthedocs.io/en/latest/)
23+
- [ ] Define control scheme.
24+
See: [Summary of MIDI 1.0 Messages](https://www.midi.org/specifications-old/item/table-1-summary-of-midi-message)
25+
, [MIDI 1.0 Control Change Messages](https://www.midi.org/specifications-old/item/table-3-control-change-messages-data-bytes-2)
26+
- [ ] CC7 = Master Volume
27+
- [ ] CC120 = Mute
28+
- [ ] CC? = On/Standby
29+
- [ ] Program Changes -> Inputs select
30+
- [ ] Mapping?
31+
- [ ] Virtual ports
32+
- [ ] using [loopMidi](http://www.tobias-erichsen.de/software/loopmidi.html) for Windows
33+
- [ ] rt-midi native for *NIX OSes
34+
- [ ] rtpMIDI?
35+
36+
#### Controls
37+
38+
- [ ] Setup
39+
- [x] IP address
40+
- [ ] Serial port?
41+
- [ ] COM (Windows)
42+
- [ ] tty (*NIX OSes)
43+
- [x] On/Standby
44+
- [x] Volume control
45+
- [x] Get
46+
- [x] Relative
47+
- [ ] Absolute
48+
- [x] Set
49+
- [x] Relative
50+
- [x] Absolute
51+
- [x] Mute
52+
- [x] Presets! (-18dBFS, -24dBFS…)
53+
- [ ] SPL calibrated display (-18dBFS = 85dBSPL)
54+
- [ ] Input select
55+
- [ ] Security
56+
- [ ] Panel Lock
57+
- [ ] IR Remote Lock
58+
- [ ] Settings backup/restore
59+
- [ ] All
60+
- [ ] Subsystems?
61+
62+
- [x] Retrieve status
63+
- [x] Logger
64+
- [x] Update the GUI
65+
66+
- [ ] Profiles/presets?
67+
68+
- [ ] Import EQ settings
69+
- [ ] From [REW](https://www.roomeqwizard.com/) value file
70+
71+
##### GUI
72+
73+
- [x] Using [Kivy](https://kivy.org)
74+
75+
##### Windows executable
76+
77+
- [ ] Find a way to make it resident in the task bar with a nice icon, like soundcard control panel
78+
- [x] [RBTray](https://sourceforge.net/projects/rbtray/files/latest/download)
79+
- [x] [MinimizeToTray](https://github.com/sandwichdoge/MinimizeToTray/releases/latest)
80+
- [ ] The Pythonic Way
81+
- [ ] Handle shutdown to power off the device
82+
- [ ] PyInstaller
83+
- [ ] VST plugin? (Not required if MIDI input is implemented but would be neat to have in the monitoring section of a
84+
DAW)
85+
- [ ] See [PyVST](https://pypi.org/project/pyvst/)
86+
87+
#### Mobile
88+
89+
- [ ] Autonomous mobile app? Kivy enables that!
90+
- [ ] Android
91+
- [ ] iOS/iPadOS

cli.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from twisted.internet import reactor
2+
3+
from config import RECEIVER_IP, RECEIVER_PORT
4+
from denon.communication import DenonClientFactory
5+
6+
7+
class DenonRemoteApp:
8+
def run(self):
9+
reactor.connectTCP(RECEIVER_IP, RECEIVER_PORT, DenonClientFactory())
10+
reactor.run()

config.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from telnetlib import TELNET_PORT
2+
3+
RECEIVER_IP = '192.168.1.24'
4+
RECEIVER_PORT = TELNET_PORT
5+
GUI = True
6+
DEBUG = True
7+
8+
VOL_PRESET_1 = '-30.0dB'
9+
VOL_PRESET_2 = '-24.0dB'
10+
VOL_PRESET_3 = '-18.0dB'
11+
12+
FAV_SRC_1_CODE = 'GAME'
13+
FAV_SRC_1_LABEL = 'Computer HDMI'
14+
FAV_SRC_2_CODE = 'CD'
15+
FAV_SRC_2_LABEL = 'Pro Analog'
16+
FAV_SRC_3_CODE = 'TV'
17+
FAV_SRC_3_LABEL = 'Pro Digital'

constraints.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Twisted doesn't support python 3.9 yet
2+
python <= 3.8

denon/__init__.py

Whitespace-only changes.

denon/communication.py

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
# -*- coding: utf-8 -*-
2+
3+
import logging
4+
5+
from twisted.internet import task, reactor
6+
from twisted.internet.protocol import ClientFactory
7+
from twisted.protocols.basic import LineOnlyReceiver
8+
9+
from config import GUI
10+
from denon.dn500av import DN500AVMessages, DN500AVFormat
11+
12+
if GUI:
13+
from kivy import Logger
14+
15+
logger = Logger
16+
17+
18+
# TODO: Implement Serial ?
19+
# See: https://twistedmatrix.com/documents/15.4.0/api/twisted.internet.serialport.SerialPort.html
20+
21+
22+
class DenonProtocol(LineOnlyReceiver):
23+
# From DN-500 manual (DN-500AVEM_ENG_CD-ROM_v00.pdf) page 91 (97 in PDF form)
24+
MAX_LENGTH = 135
25+
DELAY = 0.04 # in seconds. The documentation requires 200 ms. 40 ms seems safe.
26+
delimiter = b'\r'
27+
ongoing_calls = -1 # Delay handling. FIXME: should timeout after 200 ms.
28+
29+
logger = None
30+
31+
def sendLine(self, line):
32+
if b'?' in line:
33+
# A request is made. We need to delay the next calls
34+
self.ongoing_calls += 1
35+
self.logger.debug("Ongoing calls for delay: %s", self.ongoing_calls)
36+
self.logger.debug("Will send line: %s", line)
37+
return task.deferLater(reactor, delay=self.DELAY * self.ongoing_calls, callable=super().sendLine, line=line)
38+
39+
def lineReceived(self, line):
40+
if self.ongoing_calls:
41+
# We received a reply
42+
self.ongoing_calls -= 1
43+
self.logger.debug("Ongoing calls for delay: %s", self.ongoing_calls)
44+
receiver = DN500AVMessages(logger=self.logger)
45+
receiver.parse_response(line)
46+
self.logger.info("Received line: %s", receiver.response)
47+
if self.factory.gui:
48+
self.factory.app.print_message(receiver.response)
49+
# FIXME: abstract
50+
# MUTE
51+
if receiver.command_code == 'MU':
52+
state = False
53+
if receiver.parameter_code == 'ON':
54+
state = True
55+
self.factory.app.set_mute_button(state)
56+
57+
# VOLUME
58+
if receiver.command_code == 'MV':
59+
if receiver.subcommand_code is None:
60+
self.factory.app.set_volume(receiver.parameter_label)
61+
62+
# POWER
63+
if receiver.command_code == 'PW':
64+
state = True
65+
if receiver.parameter_code == 'STANDBY':
66+
state = False
67+
self.factory.app.set_power_button(state)
68+
69+
# SOURCE
70+
if receiver.command_code == 'SI':
71+
source = receiver.parameter_code
72+
self.factory.app.set_source(source)
73+
74+
def connectionMade(self):
75+
if self.factory.gui:
76+
self.factory.app.on_connection(self)
77+
78+
def get_power(self):
79+
self.sendLine('PW?'.encode('ASCII'))
80+
81+
def set_power(self, state):
82+
self.logger.debug("Entering power callback")
83+
if state:
84+
self.sendLine('PWON'.encode('ASCII'))
85+
else:
86+
self.sendLine('PWSTANDBY'.encode('ASCII'))
87+
88+
def get_volume(self):
89+
self.sendLine('MV?'.encode('ASCII'))
90+
91+
def set_volume(self, value):
92+
rawvalue = DN500AVFormat().mv_reverse_params.get(value)
93+
if rawvalue is None:
94+
self.logger.warning("Set volume value %s is invalid.", value)
95+
else:
96+
message = 'MV' + rawvalue
97+
self.sendLine(message.encode('ASCII'))
98+
99+
def get_mute(self):
100+
self.sendLine('MU?'.encode('ASCII'))
101+
102+
def set_mute(self, state):
103+
self.logger.debug("Entering mute callback")
104+
if state:
105+
self.sendLine('MUON'.encode('ASCII'))
106+
else:
107+
self.sendLine('MUOFF'.encode('ASCII'))
108+
109+
def get_source(self):
110+
self.sendLine('SI?'.encode('ASCII'))
111+
112+
def set_source(self, source):
113+
message = 'SI' + source
114+
self.sendLine(message.encode('ASCII'))
115+
116+
117+
class DenonClientFactory(ClientFactory):
118+
protocol = DenonProtocol
119+
120+
def __init__(self):
121+
self.gui = False
122+
self.protocol.logger = logging.getLogger(__name__)
123+
124+
125+
class DenonClientGUIFactory(ClientFactory):
126+
protocol = DenonProtocol
127+
128+
def __init__(self, app):
129+
self.gui = True
130+
self.app = app
131+
self.protocol.logger = Logger

0 commit comments

Comments
 (0)