HD Radio Controller
(c) Hal Vaughan 2008
hal@halblog.com
Licensed under the Free Software Foundations General Public License 2.0

Control protocols provided by Paul Cotter.

HD Radio Controller (HDRC) is a C++ library that makes it possible to communicate with various HD and satellite radios.  Included with the library is a small program to interface with it from the command line.  While it was mainly intended for debugging the library, the command line program can interface with and control the radio quite well.

While the library consists of 6-7 objects, there is only one that is used as part of the API interface, HDControl.  This object provides a way to pass command line arguments on to the library as well as passing other values on to the configuration settings.  It provides several ways to control the radio as well as to get settings and data returned from the radio, such as the current title or artist for an HD subchannel.  It also provides the ability to save, name, and edit favorite stations and keeps settings in a config file that is updated when the tuner is changed or when other changes to the radio settings are made so any time the radio is restarted, it can be set to it's last configuration, even if it was shut off by accident or by a sudden power loss.


Using HD Radio Controller:

Basically instantiate an object of the class HDControl.  That will do most of the setup.  Once it's created, if there are no extra arguments to pass on, call HDControl::activate() and it'll take care of the rest.  (The exception to this is finding the radio, see Autodiscovery below.)  In many cases there's a need to either pass on command line arguments or to pass on other values to the library before activating it.  Here's several examples of activating HDRC.  The class has already been instantiated as the object HDCon, most likely by a reference in the header.

Starting without passing on settings:

	HDCon.activate();

Starting with passing on the command line arguments:

	HDCon.setArguments(argc, argv);
	HDCon.activate();

Starting with passing on specific arguments NOT from the command line:
	
	HDCon.setArgument("device", "/dev/ttyS0");
	HDCon.setArgument("autodiscover", "true");
	HDCon.activate();

Once the library has been activated, the only settings that are changeable are the ones that are passed on to the radio directly.

When HDControl::activate() is called, the serial port to the radio is opened as a file, which brings the DTR line to high and turns on the radio.  When the program quits, either on purpose or accident, the DTR line will drop and the radio will turn off.  Also, two additional threads are started.  One monitors the incoming data from the serial port for data from the radio and stores incoming data in a setting config.  The other monitors when changes are made, such as tuning to a different station.  Approximately 2 seconds after such a change is made, a config file with the current settings is written to the hard drive in the user's home directory.  (The 2 second delay is to avoid rewriting the file at each change if the user is tuning the dial up or down the band or doing the same with the volume.)  The thread reading data from the radio also ensures that the serial port is kept open and in use so the radio stays on.

The radio can be shut off by calling HDControl::close() but it is not expected, since the original design was to use the radio in a carputer, where turning off the ignition would shut off the computer without a proper shutdown procedure.  Under normal circumstances, any time the program exits (even if it's killed), the DTR will drop to low, shutting off the radio.

Configuration and Settings:

There are two configuration files, /etc/HDRadio.cfg, and ~/.hdradio/HDRadioSession.cfg.  The second is kept in a directory in case there is a need, in the future, to add more configuration files.  Note it's a hidden directory to reduce clutter in the home directory.  When HDControl is created, it reads in the system config file (/etc/HDRadio.cfg), then looks to see if there is a user/session config file (~/.hdradio/HDRadioSession.cfg) it loads that too, with the user or session config settings overriding the system configuration.  Then, if any command line arguments are passed on through HDControl::setArguments(), they will override the config settings.  If, instead of command line arguments, individual arguments are passed on through HDControl::setArgument(), then those will take the same priority as command line arguments.

This has been worked out to start with system settings, then to allow the user to override them, then to let command line arguments override that.

The file ~/.hdradio/HDRadioSession.cfg also contains the favorite stations and is where the most recent radio settings are stored so any setting passed on through the command line will be saved to this file if any changes, such as tuning to any station, are made while the library is active.

The main purpose of requiring the use of HDControl::activate() to start radio functions is to provide the opportunity to pass on settings to override the control config that has already been read in from the config files.

Calling saveState() will save the current configuration to the session config file.

All config options on the command line should have "--" in front of them.  This is expected if the command line args are passed to HDRC.  Anything passed on from the command line without a "--" (not those items passed in one at a time through setArgument) are viedwed as commands.  The default separator is a comma by itself, so a number of commands for the radio can be passed through on the command line itself and will be stored by the config object until they are requested.  Separate all commands and arguments by a space and a comma will indicate the end of a command and arguments.

The different config options will be explained later.


Autodiscovery and Firstrun:

The radio hooks up to the computer through the RS232C serial port.  It can also connect through a USB to RS232C adaptor.  The issue is finding the radio to connect to it.  To do that, there is an autodiscovery feature.  The drawback is that in order to find the radio, any possible serial port (or serial port made up of an adaptor connect to a USB port) has to be probed.  This could disrupt other devices connected to the computer.

To solve this, autodiscovery is disabled by default, except the first time HDRC is used.  There are two config settings that control autodiscovery: autodiscover and firstrun.  Normally autodiscover is set to false, but users who need it to run each time HDRC is used can change that setting.  The first time HDRC is used on a computer, it reads the sytem config but will not find a user config.  In the system config, firstrun=true, so HDRC will probe the ports, in the order given in the config file, until it finds the radio.  Once it does, it'll set that port as the device to use and save that info to the user/session config file so it won't need to be changed again.

If the radio is moved to another port or is on a port that moves for some reason, either autodiscover can be set to true to autodiscover on each usage of HDRC, or firstrun in the user config can be set to true so HDRC will look for the radio again.

Search Devices:

In the config file, there are two variables for search devices.  Also if serial device is set (the setting name is device), it'll be checked first, since the radio is most likely there.  searchdevices should equal a list of devices to be searched for the radio, all separated by a comma.  The other variable is searchtypes.  This is a bit different.  There could be several serial ports on a computer or several USB->RS232C adaptors.  A short hand can be used to specify these.  For example, if you have:

searchtypes=/dev/ttyS,/dev/ttyUSB

HDRC will automatically keep appending numbers to those devices, starting at 0, until it reaches a device file that doesn't exist.  In this case, it'll start with "/dev/ttyS", add a "0" on the end and search "/dev/ttyS0", then "/dev/ttyS1", and so on until it reaches a device that doesn't exist.  Then it'll go on to the next device type.  Here, it'll go on to "/dev/ttyUSB" and start with "/dev/ttyUSB0" and continue to check that pattern until it finds one that doesn't exist.

Devices are searched before device types.  If it reaches the end of the list without confirmation of the radio, then it might be necessary to do some research to find out what the radio is connected to.  The only other option to change is autodiscoverwait.  When autodiscover opens a port to look for the radio, it waits as long as specified by autodiscoverwait.  If it doesn't get any data at all in that time, it'll move on to the next port.  If it starts to get data, it'll wait longer and look for a byte it recognizes.  Finding that byte will let autodiscover give the radio even more time to respond.  This time can be lengthened if needed to give the radio more time to respond.  This time can also be shortened if the radio on a system responds faster to speed up autodiscover.

After HDRC is run once and has run through autodiscover, whether it has found a radio or not, it'll change firstrun to false and save it to the session config file.


Controlling the Radio:

There are 3 basic ways to control the radio through HDControl.  The first is designed to work with a command line program.  Just pass it a command line that includes a command and the right number of arguments.  There are three command() functions in HDControl, one for commands with no arguments, one for commands with one argument, one for commands with two arguments.  Here is a list of the commands, starting with ones that have no arguments up throgh ones that take two arguments.  Note all the commands starting with "request."  Those are to ask the radio to send us those values.  They do not return a value, they get the radio to give us a value, then that value can be retrieved from our settings.  There is no guarantee, though, that the desired value will be updated immediately so do not count on it.

on
off
muteon
muteoff
tunedef (a bit of a bonus command, tune to default station originally set by the author to his favorit station in Richmond, VA, but configurable in the config files)
tuneup
tunedown
seekup
seekdown
seekall
seekhd
requestpower
requestvolume
requestmute
requestbass
requesttune
requestfrequency
requestband
requesthdsubchannel
requesthdsubchannelcount
requesthdcallsign
requesthdstationname
requestuniqueid
requesttitle
requestartist
requestsignalstrength
requeststreamlock
requesthdactive
requesthdtunerenabled
requestapiversion
requesthwversion
requestrdsenable
requestrdsservice
requestrdsradiotext
requestrdsgenre
makefavorite
savestate
restore
dtr  (should only be used with debugging!)

volume {0 - 100}
bass {0 - 100}
treble {0 - 100}
hdsubchannel {0 - 3 or highest subchannel}
makefavorite {new favorite name}
removefavorite {favorite name}
settofavorite {favorite name}
dtr {1 or 0}  (should only be used with debugging!)

hangonexit {true or false}
tune {frequency, band) (like 889 fm or 88.9 fm or 88.9:1 fm)
renamefavorite (old name, new name}

Calling HDControl::command_line() with a string consisting of one of these commands or a comand with the spaces separating the command and arguments will leat to it being parsed and processed.  It's also possible to call command() with the argument and proper number of arguments, in string form, for parsing, for example, all of these will do the same thing:

HDCon.commandline("tune 88.9:1 fm");
HDCon.command("tune", "889 fm");
HDCon.command("tune", "88.9", "fm");

Tune is a unique command because it can work with 1 or two arguments.  FM frequencies can have a decimal in it or not (the radio takes the argument without a decimal).  While some calls to tune() can be done with an integer, due to conversion issues, there is no overloaded version of tune() that takes a floating point.  The band must be specified and the subchannel does not have to to be specified.  If no subchannel is given, the radio will lock on to the first one (if there is one) by default.  The subchannel is specified with a colon and the number directly after the frequency.

Another important point about tune: the radio will not tune to an HD subchannel until it gets a streamlock so calling tune can take as long as specified for the streamlockwait in the config file.

So far this covers two ways to send commands.  One is by passing an actual command line, as if it were typed, and the other is to call command() with the right number of string arguments.

The third way to access the radio commands is throug the functions in HDControl.  For instance:

HDCon.radioOn(); //Turn the radio on
HDCon.requestVolume();  //Ask the radio to send us the current volume level

Note that the volume, bass, and treble settings aceept a range of 0-100.  The radio actually uses 0-90, so the values are converted to a range humans are used to.  This means some levels will be the same, specifically any level the number ends with a 9 -- it'll be the same leve as the next number ending in 0.  For example, 49 and 50 are the same level to the radio and so are 69 and 70.

Read the API documentation for more information on the functions to call to control the radio.


Getting Data From the Radio:

As mentioned above, to get data from the radio, first send a request?????() command to the radio.  This will cause it to respond with the current setting for that item.  Also any time a value is changed, the radio automatically sends us an update and that data is entered into our settings.  It is important to remember we can call requestVolume() and the radio can respond, but the volume setting in our config settings may not be updated immediately.

To get a setting, one can call HDControl::getValue() with the name of the value to retrieve, or call any of the get????() functions like getVolume() to get the actual value.  In most cases the value is returned as a string.  This provides 2 ways to get current values: getValue() with the name of the desired value or calling a function directly.


Notes on Commands and Data:

- Commands and setting names that deal with specifically HD signal data have "hd" in them, like hdartist.  The same with RDS data.  The text version of the commands, for the most part, matches the names of the functions called for the same command.

- The first HD channel on a frequency is supposed to be the same as the analog signal.  Unless the HD channel is specified, the radio will tune to that frequency and if it detects the HD signal, will switch over to HD automatically, but it is also possible for it to lose the streamlock and switch back to analog or to do this repeatedly as it acquires and loses the HD signal.  If you check the settings and the subchannel is 0, there is either no HD or the HD signal is weak and there isn't a good lock on it.


Favorite Stations:

A station can be added to favorites with addToFavorites(), which adds the current station to the list of favorites (if it's not already on that list).  There is also an overloaded version that allows specifying a name for the station.  If no name is specified, the station name from the HD signal data will be used.  If that isn't available, the name (or as close as we can get) in RDS data is used and if that isn't available, the tune information is used as a name (like 88.9:1 FM).

Once saved, it is possible to rename a favorite, delete it, or tune to it.  See the API docs for more information on this.

The favorites are saved in the user's session config file whenever any config info is saved.  Calling saveFavorites() will also save the config file.

Config Settings and Options:

autodiscover: {true or false} true to use autodiscover
autodiscoverwait: {number} how long to wait on each port for autodiscover
commandseparator: {character} what character to use to separate commands passed as arguments from the command line
defaultband: {frequency} any am or fm frequency (default station for "tunedef" command)
defaultfrequency: {am or fm} (default band for "tunedef" command)
device: {device file name} the name of the device where the radio is attached
firstrun: {true or false} true if the program has not been run or if autodiscover needs to be used just once
hanguponexit: {true or false} controls the HUPCL on the serial port -- If set to false, on most systems the radio won't turn off when the program quits.  This can cause problems on some systems if it's set to false.
searchdevices: {comma separated list}  see Autodiscovery for info on this
searchtypes: {comma separated list}  see Autodiscovery for info on this
streamlockwait: {number} how long to wait for a streamlock when tuning to a subchannel

More Info:

Most info on specific functions can be found in the docs, but as more information is needed for a complete overview of this program, it'll be added to this document.
