Skip to content

Commit 7567fe6

Browse files
committed
Add basic keyboard shortcuts and native systray
1 parent c9e75cc commit 7567fe6

6 files changed

Lines changed: 203 additions & 33 deletions

File tree

README.md

Lines changed: 62 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ Dependencies:
1717

1818
#### Target hardware
1919

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

2323
#### Communication
@@ -55,8 +55,8 @@ Dependencies:
5555
- [x] Relative
5656
- [x] Absolute
5757
- [x] Mute
58-
- [x] Presets! (-18dBFS, -24dBFS…)
59-
- [ ] SPL calibrated display (-18dBFS = 85dBSPL)
58+
- [x] Presets! (-18dB, -24dB…)
59+
- [ ] SPL calibrated display (SMPTE RP200: -18dBFS = 85dB C SPL)
6060
- [ ] Input select
6161
- [ ] Security
6262
- [ ] Panel Lock
@@ -77,19 +77,23 @@ Dependencies:
7777
##### GUI
7878

7979
- [x] Using [Kivy](https://kivy.org)
80+
- [ ] Keyboard shortcuts:
81+
- [x] M for Mute
82+
- [x] Up/Down Vol +/-
83+
- [ ] Left/Right VolPreset +/-
84+
- [ ] PgUp/PgDwn SrcPreset +/-
85+
- [x] Systray/Taskbar support using [pystray](https://pypi.org/project/pystray/)
8086

8187
##### Windows executable
8288

83-
- [x] Find a way to make it resident in the task bar with a nice icon, like soundcard control panel
84-
- [x] [RBTray](https://sourceforge.net/projects/rbtray/files/latest/download)
85-
- [ ] The Pythonic Way
8689
- [ ] Handle shutdown to power off the device
87-
- [x] PyInstaller
90+
- [x] [PyInstaller](https://www.pyinstaller.org)
8891
- [x] Generate icon with [IconMaker](https://github.com/Inedo/iconmaker)
8992
- [x] [UPX](https://upx.github.io/) support
9093
- How to build:
9194
- Review [denonremote.spec](denonremote.spec)
9295
- Use `python -m PyInstaller denonremote.spec --upx-dir=c:\upx-3.96-win64`
96+
- [ ] [cx-Freeze](https://pypi.org/project/cx-Freeze/) for multiplatform support?
9397
- [ ] VST plugin? (Not required if MIDI input is implemented but would be neat to have in the monitoring section of a
9498
DAW)
9599
- [ ] See [PyVST](https://pypi.org/project/pyvst/)
@@ -98,4 +102,54 @@ Dependencies:
98102

99103
- [ ] Autonomous mobile app? Kivy enables that!
100104
- [ ] Android
101-
- [ ] iOS/iPadOS
105+
- [ ] iOS/iPadOS
106+
107+
#### Proxy?
108+
109+
The receiver only allows 1 active connection. A dispatcher proxy could allow multiple simultaneous remotes (Desktop and
110+
mobile).
111+
112+
### Other opportunities
113+
114+
Open ports:
115+
116+
- 23/tcp (TELNET): BridgeCo AG Telnet server
117+
AVR serial protocol used here
118+
- 80/tcp (HTTP): GoAhead WebServer
119+
Web control (index.asp) Shows nothing.
120+
Most of the useful code is commented!
121+
CSS loading at "css/mainMenu.css" times out.
122+
Main control is available at "MainZone/index.html"!
123+
- 443/tcp (HTTPS): ERR_SSL_PROTOCOL_ERROR in Google Chrome
124+
SSL_ERROR_EXTRACT_PUBLIC_KEY_FAILURE in Mozilla Firefox
125+
- 1026/tcp (RTSP): Apple AirTunes rtspd 103.2
126+
- 6666/tcp: ?
127+
- 8080/tcp (HTTP): AV receiver http config
128+
129+
### Similar projects
130+
131+
Android
132+
133+
- [AVR-Remote](https://github.com/pskiwi/avr-remote)
134+
135+
JavaScript:
136+
137+
- https://github.com/phillipsnick/denon-avr
138+
- https://github.com/murderbeard/com.moz.denon
139+
- https://github.com/jtangelder/denon-remote
140+
141+
PHP
142+
143+
- https://github.com/Wolbolar/IPSymconDenon (IP Symcon automation)
144+
145+
Python:
146+
147+
- https://github.com/jeroenvds/denonremote (XBMC plugin)
148+
- https://github.com/Tom360V/DenonAvr (Similar objectives?
149+
- https://github.com/toebsen/python-denonavr (HTTP RESTful server)
150+
- https://github.com/MrJavaWolf/DenonPhoneController (Landline phone controller)
151+
- https://github.com/troykelly/python-denon-avr-serial-over-ip (Library)
152+
- https://github.com/auchter/denonavr_serial (Library)
153+
- https://github.com/jphutchins/pyavreceiver (Nice library)
154+
- https://github.com/frawau/aiomadeavr (Library)
155+
- https://github.com/scarface-4711/denonavr (Uses the HTTP/XML interface. Library)

denonremote.spec

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,29 @@
11
# -*- mode: python ; coding: utf-8 -*-
22

3-
from kivy_deps import sdl2, glew
4-
53
# Minimize dependencies bundling
6-
from kivy.tools.packaging.pyinstaller_hooks import get_deps_minimal, get_deps_all, hookspath, runtime_hooks
4+
from kivy.tools.packaging.pyinstaller_hooks import get_deps_all, hookspath, runtime_hooks
5+
from kivy_deps import sdl2, glew
76

87
block_cipher = None
98

109
added_files = [
1110
('denonremote\\fonts', 'fonts'),
12-
('denonremote\\images', 'images')
11+
('denonremote\\images', 'images'),
1312
]
1413

14+
dependencies = get_deps_all() # FIXME: minimize dependencies
15+
dependencies['hiddenimports'].append('pystray._win32')
16+
1517
a = Analysis(['denonremote\\main.py'],
16-
pathex=['denonremote'],
18+
pathex=['denonremote', '.\\venv\\Lib\\site-packages\\pystray'],
1719
datas=added_files,
1820
hookspath=hookspath(),
19-
runtime_hooks=[],
21+
runtime_hooks=runtime_hooks(),
2022
win_no_prefer_redirects=False,
2123
win_private_assemblies=False,
2224
cipher=block_cipher,
2325
noarchive=False,
24-
**get_deps_all())
26+
**dependencies)
2527
pyz = PYZ(a.pure, a.zipped_data,
2628
cipher=block_cipher)
2729
exe = EXE(pyz, Tree('denonremote'),

denonremote/denonremote.kv

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ FloatLayout:
152152

153153
TextInput:
154154
id: debug_messages
155-
text: u"Initializing GUI...\n"
155+
text: "Initializing GUI...\n"
156156
readonly: True
157157
background_color: [0, 0, 0, 1]
158158
foreground_color: [0, 1, 0, 1]

denonremote/gui.py

Lines changed: 66 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,15 @@
33
import sys
44

55
import kivy.app
6+
import kivy.core
7+
import kivy.core.window
8+
import kivy.logger
69
import kivy.resources
710
import kivy.support
8-
911
# FIXME: should be in Config object?
12+
import pystray
13+
from kivy.clock import mainthread
14+
1015
from config import RECEIVER_IP, RECEIVER_PORT, VOL_PRESET_1, VOL_PRESET_2, VOL_PRESET_3, FAV_SRC_1_CODE, \
1116
FAV_SRC_2_CODE, FAV_SRC_3_CODE, DEBUG
1217

@@ -23,8 +28,7 @@
2328

2429
kivy.require('2.0.0')
2530

26-
# Fixed size window
27-
kivy.Config.set('graphics', 'resizable', False)
31+
logger = kivy.logger.Logger
2832

2933
APP_PATHS = ['fonts', 'images']
3034

@@ -41,20 +45,36 @@ class DenonRemoteApp(kivy.app.App):
4145
A remote for the Denon DN-500AV Receiver
4246
"""
4347

44-
client = None
45-
"""Twisted IP client to the receiver"""
46-
4748
title = "Denon Remote"
4849
"""Application title"""
4950

5051
icon = 'icon.png'
5152
"""Application icon"""
5253

54+
client = None
55+
"""Twisted IP client to the receiver"""
56+
57+
systray: pystray.Icon = None
58+
59+
hidden = True if kivy.config.Config.get('graphics', 'window_state') == 'hidden' else False
60+
61+
def run_with_systray(self, systray):
62+
self.systray = systray
63+
super().run()
64+
5365
def on_start(self):
5466
"""
5567
Fired by Kivy on application startup
5668
:return:
5769
"""
70+
self.systray.visible = True
71+
72+
# Hide window into systray
73+
kivy.core.window.Window.bind(on_request_close=self.hide_on_close)
74+
kivy.core.window.Window.bind(on_minimize=self.hide)
75+
# Enable keyboard shortcuts
76+
kivy.core.window.Window.bind(on_keyboard=self.on_keyboard)
77+
5878
if not DEBUG:
5979
# Hide debug_messages
6080
self.root.ids.debug_messages.size = (0, 0)
@@ -101,6 +121,46 @@ def on_connection(self, connection):
101121
self.client.get_mute()
102122
self.client.get_source()
103123

124+
@mainthread
125+
def show(self, window=None):
126+
if window is None:
127+
window = self.root_window
128+
window.restore()
129+
window.raise_window()
130+
window.show()
131+
self.hidden = False
132+
133+
@mainthread
134+
def hide(self, window=None):
135+
if window is None:
136+
window = self.root_window
137+
window.hide()
138+
self.hidden = True
139+
140+
def hide_on_close(self, window, source=None):
141+
logger.debug("Hide from %s", source)
142+
self.hide(window)
143+
return True # Keeps the application alive instead of stopping
144+
145+
def on_keyboard(self, window, key, scancode, codepoint, modifier):
146+
"""
147+
Handle keyboard shortcuts
148+
149+
:param window:
150+
:param key:
151+
:param scancode:
152+
:param codepoint:
153+
:param modifier:
154+
:return:
155+
"""
156+
logger.debug("key: %s, scancode: %s, codepoint: %s, modifier: %s", key, scancode, codepoint, modifier)
157+
if codepoint == 'm':
158+
self.root.ids.volume_mute.trigger_action()
159+
if scancode == 82: # Up
160+
self.root.ids.volume_plus.trigger_action()
161+
if scancode == 81: # Down
162+
self.root.ids.volume_minus.trigger_action()
163+
104164
def update_power(self, status=True):
105165
if status:
106166
self.root.ids.power.state = 'down'

denonremote/main.py

Lines changed: 64 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,30 @@
99

1010
__author__ = 'Raphaël Doursenaud <rdoursenaud@gmail.com>'
1111

12-
__version__ = '0.3.0' # FIXME: use setuptools
12+
__version__ = '0.4.0' # FIXME: use setuptools
1313

1414
import logging
1515

16+
import PIL.Image
17+
import pystray
18+
1619
from config import DEBUG, GUI
1720

1821
logger = logging.getLogger()
1922

2023

24+
def resource_path(relative_path):
25+
""" Get absolute path to resource, works for dev and for PyInstaller """
26+
import os, sys
27+
if hasattr(sys, '_MEIPASS'):
28+
# PyInstaller creates a temp folder and stores path in _MEIPASS
29+
base_path = sys._MEIPASS
30+
else:
31+
base_path = os.getcwd()
32+
33+
return os.path.join(base_path, relative_path)
34+
35+
2136
def init_logging():
2237
global logger
2338

@@ -35,22 +50,60 @@ def init_logging():
3550
logger.setLevel(kivy.logger.LOG_LEVELS['debug'])
3651
else:
3752
logger.setLevel(kivy.logger.LOG_LEVELS['info'])
53+
logging.getLogger('denon.dn500av').setLevel(logging.WARNING) # Silence module’s logging
54+
55+
56+
def run_from_systray():
57+
default_menu_item = pystray.MenuItem('Denon Remote', systray_clicked, default=True, visible=False)
58+
quit_menu_item = pystray.MenuItem('Quit', quit_systray)
59+
systray_menu = pystray.Menu(default_menu_item, quit_menu_item)
60+
systray = pystray.Icon('Denon Remote', menu=systray_menu)
61+
systray.icon = PIL.Image.open(resource_path(r'images/icon.png'))
62+
systray.run(setup=run_gui)
63+
64+
65+
def run_gui(systray):
66+
import kivy.config
67+
kivy.config.Config.set('kivy', 'window_icon', 'images/icon.png')
68+
# Fixed size window
69+
kivy.config.Config.set('graphics', 'resizable', False)
70+
# Start hidden
71+
kivy.config.Config.set('graphics', 'window_state', 'hidden')
72+
# wm_pen and wm_touch conflicts with hidden window state. See https://github.com/kivy/kivy/issues/6428
73+
kivy.config.Config.remove_option('input', 'wm_pen')
74+
kivy.config.Config.remove_option('input', 'wm_touch')
75+
kivy.config.Config.write()
76+
77+
from gui import DenonRemoteApp
78+
DenonRemoteApp().run_with_systray(systray)
79+
80+
81+
def systray_clicked(icon: pystray.Icon, menu: pystray.MenuItem):
82+
import kivy.app
83+
app = kivy.app.App.get_running_app()
84+
if app.hidden:
85+
app.show()
86+
else:
87+
app.hide()
88+
89+
90+
def quit_systray(icon: pystray.Icon, menu: pystray.MenuItem):
91+
import kivy.app
92+
kivy.app.App.get_running_app().stop()
93+
icon.stop()
94+
95+
96+
def run_cli():
97+
from cli import DenonRemoteApp
98+
DenonRemoteApp().run()
3899

39100

40101
def run():
41102
# FIXME: autodetect when running from CLI
42103
if GUI:
43-
from gui import DenonRemoteApp
44-
45-
# PyInstaller data support
46-
import os, sys
47-
import kivy.resources
48-
if hasattr(sys, '_MEIPASS'):
49-
kivy.resources.resource_add_path(os.path.join(sys._MEIPASS))
104+
run_from_systray()
50105
else:
51-
from cli import DenonRemoteApp
52-
53-
DenonRemoteApp().run()
106+
run_cli()
54107

55108

56109
if __name__ == '__main__':

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
twisted
22
kivy
3+
pystray
34
PyInstaller

0 commit comments

Comments
 (0)