So if you read my last blog post, you may have noticed I was preparing some code for public consumption that I had been working on. This code is for the control of my Onkyo TX-SR705 receiver via its serial port. (Sidenote: this is an excellent receiver and I'd recommend it to anyone. If you want to find out about my entire home theater setup, I'd be willing to do a post on it if you ask kindly.)
Why did I write this? Because I was intrigued that you could do this (without spending a ton of money on a home-automation system), and I managed to get my hands on documentation for the commands, and it introduced me to a ton of programming things I hadn't worked on before.
I've placed the code and what history I have of it in my onkyocontrol GIT repository. The code is currently under a GPLv2 or later license, mostly because thats the only license I've really used on code I've written.
This really should work for controlling any Onkyo brand receiver- they all use basically the same command set, although higher-end and lower-end receivers than mine might have more or fewer controls, respectively.
I'll start with a quick rundown of what I made here, and how it works, and all the things I've learned.
The central component to this code is the
onkyocontrol daemon program. I chose C for this for a few reasons:
- It runs on my server box, so I wanted to keep it lightweight and not drag in a ton of dependencies
- I had never done serial port programming in Linux (or any platform), so I wanted to work with that at a low level that allowed me to completely understand what I was doing
- I had never done a lot of work with file descriptors, sockets, and all of that fun stuff in C
- I really just wanted another project besides pacman to hone my C skills on, and starting from scratch means I couldn't complain about crappy code
I drew inspiration (and needed help because I had never done this stuff before) from two main sources. The first was the Serial Programming HOWTO tutorial, which hasn't been updated in years but is still 100% relevant. Serial ports haven't exactly made leaps and bounds since 2001 as far as I know. The second main source was mpd. This was a good example of a C daemon program that took requests and sent responses on a socket, which is exactly what I wanted to do with my daemon.
Rather than step through the entire code (I'll leave that to you, I think I actually commented it very well), I'll touch on a few design considerations I made when coding this up. The overall architecture can be descriped as follows:
- Daemon starts up, opening the serial device for reads/writes and opening a socket for listening.
- Daemon listens for status messages from the receiver, writing a parsed status message to stdout and any current connected listeners.
- Daemon listens for new connections on socket. When they come, accept the connection and listen for commands from any frontend programs which are translated into the low-level receiver commands.
At first, I was quite inexperienced with the whole file descriptor/socket thing, and thought this application would need to be multithreaded in some way to handle all of the possible inputs and outputs. I quickly saw the light and realized this is exactly what
select() comes in handy for. Once we do all our initialization, our entire program revolves around that system call, which monitors our receiver serial port along with all of the incoming connection attempts and existing connections for activity and behaves accordingly.
The biggest decision (and split from mpd) was to go asynchronous. This means any commands a frontend program issues may not get an immediate response; more importantly, frontends need to be ready to accept status updates at any time. The decision was a fairly simple one to make. Because we are relying on an external piece of hardware for the real logic (the A/V receiver), we don't want to get stuck waiting on it. In addition, we are not the only controller of the receiver- when I turn up the volume using the remote, my daemon should be able to notify any currently connected frontends of the volume change.
If you want to explore or better understand the code, clone the GIT repository and open the source, or if you are more of a visual person, just type
make doc to get some pretty doxygen docs with caller/callee graphs and everything.
Frontend GUI controller
The frontend (required screenshot) was also a learning ground for me. I chose Python + PyGTK for this portion, as I hadn't done a lot of standalone Python and wanted to get a refresher on that. It also didn't make sense to write the frontend in C because I already had the knowledge from writing the backend (so it wouldn't really be more learning, just more work), and there were no real memory or performance constraints to meet. PyGTK seemed like a good choice for the GUI elements because I had done some GTK work before and Sonata, the PyGTK mpd client, could then be used as a reference if necessary.
Besides the use of PyGTK, Sonata shared another thing in common with my frontend- it communicated via a socket to the backend program. Sonata uses a lightweight library to facilitate this, which I was able to use as a reference to perform similar functionality. However, in the case of mpd, all commands are synchronous. Because I designed my backend to be asynchronous, the interaction had to be slightly different. However, this was a relatively easy hurdle to jump- the
gobject module that is utilized by PyGTK comes with a
gobject.io_add_watch() function, which basically acts as a
select() call, except integrated into the main PyGTK event loop. This allowed me to listen for new information on the incoming socket without needing to poll on a regular interval.
I don't know what is next. I've tweaked and played with this code quite a bit, and its currently meeting all of my needs. I would be happy if someone else out there said "Wow, this is cool and it works with my receiver too!" and offered tips or thoughts (or even patches!) for improvement. We'll see if that happens.