Metis Framework Programming Guide


Introduction

The Metis hardware device communicates through UDP packets on Ethernet.

For details on the Metis UDP protocol, please refer to
Metis - How it works in the OpenHPSDR support page. For details on the HPSDR data frames, please refer to the HPSDR — USB Data Protocol document in the same support page.

Each Metis device has a distinct
MAC address. When it is powered on, the Metis hardware looks for a DHCP server and obtains an IP address (e.g., 192.168.1.22) from the server.

If a DHCP server is not found, the Metis hardware will assign an address in the 169.254.x.x block that by convention is designated for automatic private allocation — in the case of Metis hardware, the last two octets of the APIPA IP address have the the values of the last two octets of the Metis MAC address.

When the Metis hardware cannot find a DHCP server, it can take a few seconds before it starts listening on the 169.254.x.x block.

In addition to the main IP address, the Metis device includes a discovery process by listening to a specific 63 byte broadcast packet (IP address 255.255.255.255). All packets are sent and received on port 1024.

When the discovery packet is received, the Metis device will respond to the requestor IP with a 60 byte packet that contains the Metis device's MAC address; at the same time, the requester obtains the IP address of the Metis hardware by inspecting the
IPC socket information that comes with the reply from the recvfrom()Unix system call.

The code that handles the discovery process is in the
MetisDiscovery module of the Metis Framework. You should not need not interact directly with MetisDicovery. When you initialize the Metis object in the framework, it would have done the discovery for you, and the Metis object has already established the proper sockets to communicate with the Metis hardware.

During initialization, the Metis object goes through the different network interfaces (e.g., "en0," "en1", "fw0," etc) in turn to discover a Metis device.

The code in MetisDiscovery detects multiple Metis devices, returning a list of the ones found. The simple Metis initialization (-init) method uses the first Metis device that is found. If you have more than one Metis device on the network, you will need to directly call methods in MetisDiscovery (see Discover Metis code example in the
Metis Framework's Example folder).

The following code segment shows a simple initialization of a Metis object.


#import "Metis.h"

- (
void)applicationDidFinishLaunching:(NSNotification*)notification
{
Metis *metis ;

metis = [ [
Metis alloc ] init ] ;
if ( metis == nil ) printf( "no Metis found\n" ) ;
}


We can display some of the network properties of the Metis device:


printf( "Metis Board ID 0x%02x\n", metis->metisType ) ;

printf(
"MAC %02x:%02x:%02x:%02x:%02x:%02x\n",
metis->metisMAC[0],
metis->
metisMAC[1],
metis->
metisMAC[2],
metis->
metisMAC[3],
metis->
metisMAC[4],
metis->
metisMAC[5] ) ;

printf(
"IP %d:%d:%d:%d\n",
(int)( metis->metisIP >> 0 )&0xff,
(
int)( metis->metisIP >> 8 )&0xff,
(
int)( metis->metisIP >> 16 )&0xff,
(
int)( metis->metisIP >> 24 ) ) ;


The board ID indicates if the Metis is a stand-alone Metis board, or part of
Hermes or Angelia.


Radios and Receivers

In the context of the Metis framework, a
radio (shaded in magenta below) includes an ADC (analog-to-digital converter), RF preamp and attenuator, and one or more receivers whose input come from the common ADC, while each receiver (shaded in cyan below) is associated with a mixer, a numerically controlled oscillator (NCO) and a decimator.

radioScaled


All receivers within a radio block take their input signals from a shared ADC, and therefore from a common antenna. Each NCO shifts a different slice of the antenna's passband into individual baseband signals.

The output of all receivers are low pass filtered and downconverted to a common sampling rate and multiplexed to a single UDP data stream. The Metis framework demultiplexes the analytic data from the separate receivers into their individual buffers.

Hermes has a single radio, with up to 7 receivers, each with its own NCO. (
Note: with the v2.9 firmware onwards, Hermes is limited to 4 receivers.)

Angelia has two radios (similar to the above figure), also with a total of up to 7 receivers. Receivers 1, 3, 4 and 5 of Angelia are part of Radio 1, while receivers 2, 6 and 7 are part of Radio 2. This "wiring" is fixed by the firmware 1.6 for Angelia.

When Angelia is asked to use a single receiver, it uses an NCO that is connected to the ADC in Radio 1. When Angelia is asked to use 2 receivers, the first receiver is in Radio 1, and the second receiver is in Radio 2, thus receiver 1 and receiver 2 can come from separate antennas. When Angelia is asked to use 5 receivers, four receivers are in the first radio, allowing four contiguous slices from the first antenna to be stitched together to create a single wider passband.

In the Metis framework, radios are numbered starting at 1, so are the receivers.

You need not memorize the Angelia receiver mapping. The correspondence between receivers and radios can be obtained by calling:


[ metis radioForReceiver:receiver ] ;


Wth Hermes, the above call will always return 1. With Angelia, receivers 1, 3, 4 and 5 will return 1, and receivers 2, 6 and 7 will return 2. Undefined receivers (less than 1 or greater than 7) will return 0.

With a good initialized Metis object, we can
open communications to it. The following sets some parameters of a Hermes receiver that is controlled by the Metis hardware.


[ metis open ] ;

[ metis setkHzSamplingRate:96 ] ;
[ metis
radio:1 usePreamp:YES ] ;
[ metis
radio:1 setInputAttenuator:0 ] ;
[ metis
setNumberOfReceivers:1 ] ;
[ metis
useDuplex:YES ] ;
[ metis
receiver:1 setMHzFrequency:10.000 ] ;


The -open call establishes a UDP socket to the Metis device. Once opened, UDP packets can now be sent to the device and received from the device.

In the above code segment, the sampling rate is set to 96 kHz. The RF preamp of radio 1 (radio 1 is the only radio in Hermes) is next turned on, while its stepped attenuator is turned off.

Following that, the Hermes is asked to return sampled data from a single receiver — Hermes will use the output from the first receiver out of 7 that it has at its disposal.

The -useDuplex call tells Hermes to use the first receiver's own NCO. If
duplex is set to false, Hermes will instead use the transmitter's NCO as the first receiver's local oscillator.

The next call sets that first receiver's frequency to 10.0 MHz.

Notice that the parameters are sent after an open message is sent to Metis. If the commands are sent to an unopened device, the Framework will indicate an error with an NSLog message to the system console.

Once the Hermes receiver is set up, it can be commanded to start sending sampled data. The following code example starts the sampling process, waits 10 seconds, and stops it.


[ metis start ] ;
[ NSThread sleepUntilDate:[ NSDate dateWithTimeIntervalSinceNow:10.0 ] ] ;
[ metis
stop ] ;


Bandscope data is started and stopped in a similar fashion:


[ metis startBandscope ] ;
[ NSThread sleepUntilDate:[ NSDate dateWithTimeIntervalSinceNow:20.0 ] ] ;
[ metis
stopBandscope ] ;


Bandscope data are simply contiguous blocks of 16,384 samples which are periodically extracted from the 122.88 MHz ADC.


When the Metis object is no longer need, we release it. Releasing the objects first closes the port it is open. You can also close the port without releasing the object, if you wish to later use the Metis hardware address and port again.


[ metis release ] ;



Hermes Attenuator and Pre-amp

The Metis protocol (and Metis Framework) provides three controls for the preamp and the receiving attenuator. However, they are not independent controls on Hermes. The following figure shows the relationship between the antenna, the Hermes attenuator, the lowpass filter, preamp and the ADC on Hermes.

atten

The 0-31 dB attenuator is a Mini-circuits Labs attenuator. The Lowpass filter (behaving as a Nyquist filter) is a Minicircuits Labs 50 MHz filter. The "preamp" is the Linear Technology ADC driver for the Linear Technology ADC.

The 31 dB stepped attenuator can be controlled in 1 dB steps, and can be disabled by the Hermes Attenuator Enable bit (corresponding to the Metis Framework's radio:enableAttenuator: API).

The LPF has a 3 dB cutoff at 59 MHz. The ADC driver (a.k.a. "preamp") has a fixed gain of 20 dB.

Metis has a Preamp Enable bit (corresponding to the Metis Framework's radio:usePreamp: API). However, the preamp on Hermes is always in line as the ADC driver, and cannot be turned off. Instead, when the preamp is "turned off" the firmware directs the attenuator to use a 20 dB setting instead, to back off the fixed 20 dB gain of the LTC driver.

Because of this, the Hermes Attenuator enable and the preamp enable are
not independent controls. The preamp enable/disable bit can only be used when the attenuator is turned off. When the attenuator is turned on, the "preamp" is always in line.

As such, with a Hermes, there is no reason to use the preamp enable/disable function. You get the same function by enabling the Hermes attenuator and choosing the 20 dB position.


Delegates

The Metis object maintains three delegates, one is a delegate to receive control information, a second is the delegate that receives sampled data and a third delegate that accepts raw 512-byte HPSDR frames.

You can designate any Objective C object as one (or all three) of the delegates. If you do not supply a delegate, or if the delegate is set to nil, the data that is received from the hardware will be discarded.

The example below sets an Objective-C object called "hermes" as the target for the sampled data that comes from the hardware, and the example sets the calling object itself as the target for receiving hardware control information.


[ metis setDataDelegate:hermes ] ;
[ metis setControlDelegate:self ] ;


The delegates themselves must adopt the MetisDataDelegate, MetisControlDelegate or MetisHPSDRDelegate protocol (the three protocols are defined in Metis.h), as shown in the following example:


#import "Metis.h"

@interface HermesExample : NSObject <MetisDataDelegate> {
}
@end


The following are the delegate methods that are sent to the control delegate:


- (void)metis:(Metis*)metis ptt:(Boolean)state ;
- (void)metis:(Metis*)metis dah:(Boolean)state ;
- (
void)metis:(Metis*)metis dit:(Boolean)state ;

- (
void)metis:(Metis*)metis adcOverflow:(Boolean)state ;

- (
void)metis:(Metis*)metis ioBit:(int)bit state:(Boolean)state ;
- (
void)metis:(Metis*)metis analogInput:(unsigned int)n value:(float)voltage ;
- (
void)metis:(Metis*)metis powerSupply:(float)voltage ;

- (
void)metis:(Metis*)metis board:(HPSDRBoard)board versionNumber:(unsigned int)version ;
- (
void)metis:(Metis*)metis board:(HPSDRBoard)board forwardPower:(unsigned int)power ;
- (
void)metis:(Metis*)metis board:(HPSDRBoard)board reversePower:(unsigned int)power ;

- (
void)metis:(Metis*)metis mercury:(int)n adcOverflow:(Boolean)state ;
- (
void)metis:(Metis*)metis mercury:(int)n versionNumber:(unsigned int)version ;
- (
void)metis:(Metis*)metis cyclopsFrequencyChanged:(Boolean)state ;
- (
void)metis:(Metis*)metis cyclopsPLLLock:(Boolean)locked ;



The first argument of the delegate methods is the Metis object that sent the message. This allows multiple Metis objects that control different physical devices to be identified.

All delegate methods are optional. You do not have to supply the methods that you don't use. The messages are sent only when there is a change in the state. Please check the
HPSDR — USB Data Protocol document that can be found in the "Metis Ethernet interface" section here for details.

You can request a repeat of all the callbacks, even when no change has taken place, by sending the Metis object a -resendControls message:


[ metis resendControls ] ;




Receiving Raw HPSDR Frames


The raw HPSDR frame delegate (MetisHPSDRDelegate) receives the following call when a new frame arrives from Metis. Each Metis UDP data packet carries two HPSDR frames.


- (void)metis:(Metis*)metis newFrame:(HPSDRFrame*)frame ;


Except for special circumstances (for example, to save HPSDR frames to a disk file, or you prefer to accept integer streams instead of floating point streams), you will usually not use the HPSDR frames directly. Instead, you use the Control and Data delegates.

Each HPSDR frame is 512 bytes long and contains raw (24 bit integer per component) I/Q data from the DDC, 16 bit integer microphone data, control bytes from Metis and three sync bytes.

HPSDRFrame (defined in MetisTypes.h) is defined as:


typedef union {
struct {
unsigned char sync[8] ; // 3 sync bytes followed by 5 control bytes
unsigned char data[504] ;
} iq ;
unsigned char bandscopeData[512] ;
} HPSDRFrame ;




Receiving Data from the DDC


The data delegate (MetisDataDelegate) receives the following call when new data from the Digital Down Converters (DDC) is available:


- (void)metis:(Metis*)metis newData:(MetisData*)data ;


This delegate method is called when a new UDP packet arrives from the Metis hardware.

The number of data samples that arrives per call is a function of the number of receivers and the rate it arrives is a function of the sampling rate. With a single receiver, each call has 126 new samples. With two receivers, each call has 72 new samples, and with three receivers, each call has 50 new samples.

At a 96 kHz sampling rate, a data delegate call from a single receiver setup arrives on average each 1.3125 milliseconds. With two receivers, the callbacks arrive every 750 microseconds, etc.

The delegate that receives the callback does not need to know of how the number of samples is related to the number of receivers. The delegate can inspect the data count that is passed in by the callback. The MetisData structure that is passed in has the form:


typedef struct {
AnalyticData receiver[MAXRCVR] ; // up to 7 receivers (aligned buffers)
float *lineData ;                // line/microphone samples
UInt64 producerIndex ;           // common producer index for all radio buffers
UInt64 consumerIndex ;           // common consumer index for all radio buffers
UInt32 size ;                    // NOTE: must be power of two, and greater than 512
int receivers ;                  // number of receivers
int samplingRate ;               // sampling rate in kHz
int samplesPerFrame ;            // samples per HPSDR Frame
} MetisData ;


You can also request the MetisData structure by calling:


[ metis metisData ] ;


The AnalyticData type has the form (similar to the
DSPSplitComplex type in vDSP):


typedef struct {
float *i ;                      // pointer to buffer of in-phase samples
float *q ;                      // pointer to buffer of quadrature samples
} AnalyticData ;


The AnalyticData and line data buffers are allocated by Metis and are used in a ring buffer fashion.

To avoid the need for locks, MetisData provides two 64-bit pointers, a producerIndex and a consumerIndex. Metis only increments the value of the producerIndex and the client increments the value of the consumerIndex when it consumes some data from the ring buffer (the client can use its own copies of consumerIndex; the existence of consumerIndex in the MetisData structure is purely for convenience) As 64 bit integers, producerIndex and consumerIndex will never practically overflow even at the highest sampling rate.

The actual memory location is the producerIndex (or consumer index) modulo the
size parameter in MetisData. Metis initializes the size of the ring buffers to 131,072 samples. If you need a different size, you can use the setMetisDataSize call:


[ metis setMetisDataSize:1<<18 ] ;


The size must be a power of two, and greater than 512.  
setMetisDataSize can only be called when Metis is not started.

The producerIndex increases each time the delegate receives a callback. The difference between producerIndex and consumerIndex is the amount of data that is not yet consumed. When the delegate consumes the data, not necessarily by the maximum amount available, it is responsible for increasing the consumerIndex count, by the number of samples it has consumed. The delegate can ignore the -
newData callback until there is sufficient data to process.

The
samplesPerBuffer field is the amount of new samples that is added to the producerIndex per callback. It is a function of the number of active receivers and the sampling rate.

Please note that Metis makes no attempt to check for the case when there is a data overrun. Metis can overwrite unprocessed data in the ring buffer if the consumer is late in processing it.

The data in the lineData (also used as microphone data) buffer is upsampled from its native rate of 48 kHz to the sampling rate of the receivers. You can therefore simply discard samples down to just 48 ksamples/second without incurring any additional aliasing error.


Receiving Bandscope Data from the DDC


The data delegate (MetisDataDelegate) receives the following call when new bandscope data from the Digital Down Converters (DDC) is available:


- (void)metis:(Metis*)metis newBandscopeData:(SInt16*)data samples:(int)samples ;


This delegate method is called when 16,384 samples of Bandscope data have arrived from the Metis hardware. These are 16,384 contiguous samples that are extracted from the 122.88 Msamples/second ADC.

The delegate that receives the callback can assume that the 16,384 samples are contiguous, but will not be contiguous with the next set of bandscope data. The number of samples should (for now) always be 16,384.

The data buffer that is passed by the delegate call is an array of signed 16 bit values, that correspond to the output of the ADC.


Transmitting Data to the DUC

Hermes uses a fixed sampling rate of 48 ksps for the Digital Up Converter (DUC) and the headphones/speakers output.

Both DUC and headphones samples are sent together in a group of 8 octets that consists of the four 16-bit samples for the I and Q DUC channels and the left and right headphones channels. These samples are contained in the 504 byte data buffer of an HPSDR frame (63 samples per frame). A Metis UDP packet includes two HPSDR data frames, resulting in 126 DUC/Headphone samples per UDP packet (note: not 128).

The two HPSDR data frames can also include the MOX bit that controls the receive/transmit direction.

The Metis Framework provides a function to send the outbound samples:


[ metis sendIQ:iData:qData audio:left:right samples:samples ptt:ptt ] ;


iData, qData, left and right are floating point buffers. When the framework detects that the ptt parameter has changed from a previous call, the framework will tell the hardware that the MOX state has changed.

The framework buffers the data and sends a UDP packet every 126 samples are available. For best synchronicity between samples and PTT state (e.q., when using QSK), the best sample size per call is 63. Please note that you cannot get better PTT granularity than 1.3 milliseconds (63 samples at 48 ksps).

Please note that left audio and right audio are reversed in the Hermes hardware relative to the HPSDR frame documentation.


Thread Safety

The Metis Framework uses a separate thread to wait for UDP data, and to decode status bytes and convert sampled streams.

Delegates are called from this UDP read thread.

If the delegates need to interact with GUI (for example, any NSView, which includes buttons and plots), they must do so in the Main thread. The code snippet below shows executing of code (in this example, calling setNeedsDisplay) in the main thread by using
Grand Central Dispatch (GCD) instead of the much more processor intensive -performSelectorOnMainThread method in NSObject.


dispatch_queue_t q = dispatch_get_main_queue() ;
dispatch_async( q, ^{
self setNeedsDisplay: YES ] ;
} ) ;


If you believe any processing might interfere with the arrival of UDP packets (about a packet per 1.3 ms for a single receiver at 96 ksps sampling rate), you should create your own thread to buffer the data and process it in a separate thread (or GCD queue).