DCSIMG
December 2008 - Posts - Just code - Tamir Khason

December 2008 - Posts

[This blog was migrated. You will not be able to comment here.
The new URL of this post is http://khason.net/blog/read-and-use-fm-radio-or-any-other-usb-hid-device-from-c/]


Last time we spoke about reading and decoding RDS information from FM receivers. Also we already know how to stream sound from DirectSound compatible devices. However, before we can do it, we should be able to “speak” with such devices. So, today we’ll spoke about detection and reading information from Radio USB adapters (actually from any Human Input Devices). Let’s start.

USB FM HID

First, if you want to do it, go and buy such device. The are not a lot of alternatives, but if you’ll seek, you’ll find it very quickly.

So, let’s start. First of all, we’ll use platform invoke to get and set the information. Also, we have to preserve handle of the device from being collected by GC. After we’ll finish using the device, we’ll have to dispose it. Thus it makes sense to inherit from SafeHandle and IDisposable.

[SecurityPermission(SecurityAction.InheritanceDemand, UnmanagedCode = true)]
[SecurityPermission(SecurityAction.Demand, UnmanagedCode = true)]
public class USBDevice : SafeHandleZeroOrMinusOneIsInvalid, IDisposable {

Next, we’ll set a number of arguments, that will be in use during the device lifetime.

public uint ProductID { get; private set; }
public uint VendorID { get; private set; }
public uint VersionNumber { get; private set; }
public string Name { get; private set; }
public string SerialNumber { get; private set; }
public override bool IsInvalid { get { return !isValid; } }

internal ushort FeatureReportLength { get; private set; }
internal ushort[] Registers { get; set; }

Now, we have to find it. The best way of detection human input devices is by product and vendor IDs. Those values are always unique for certain device type.

[SecurityPermission(SecurityAction.Demand, UnmanagedCode = true)]
internal USBDevice(uint pid, uint vid) : base(true) { findDevice(pid, vid); }

Next step is to find a device. To do this, we have to provide extern interfaces to methods of hid.dll and setupapi.dll. Here all methods we will use in our class

[SuppressUnmanagedCodeSecurity()]
internal static class Native {
   #region methods
   [DllImport("hid.dll", SetLastError = true)]
   internal static extern void HidD_GetHidGuid(
      ref Guid lpHidGuid);

   [DllImport("hid.dll", SetLastError = true)]
   internal static extern bool HidD_GetAttributes(
      IntPtr hDevice,
      out HIDD_ATTRIBUTES Attributes);

   [DllImport("hid.dll", SetLastError = true)]
   internal static extern bool HidD_GetPreparsedData(
      IntPtr hDevice,
      out IntPtr hData);

   [DllImport("hid.dll", SetLastError = true)]
   internal static extern bool HidD_FreePreparsedData(
      IntPtr hData);

   [DllImport("hid.dll", SetLastError = true)]
   internal static extern bool HidP_GetCaps(
      IntPtr hData,
      out HIDP_CAPS capabilities);

   [DllImport("hid.dll", SetLastError = true, CallingConvention = CallingConvention.StdCall)]
   internal static extern bool HidD_GetFeature(
      IntPtr hDevice,
      IntPtr hReportBuffer,
      uint ReportBufferLength);

   [DllImport("hid.dll", SetLastError = true, CallingConvention = CallingConvention.StdCall)]
   internal static extern bool HidD_SetFeature(
      IntPtr hDevice,
      IntPtr ReportBuffer,
      uint ReportBufferLength);

   [DllImport("hid.dll", SetLastError = true, CallingConvention = CallingConvention.StdCall)]
   internal static extern bool HidD_GetProductString(
      IntPtr hDevice,
      IntPtr Buffer,
      uint BufferLength);

   [DllImport("hid.dll", SetLastError = true, CallingConvention = CallingConvention.StdCall)]
   internal static extern bool HidD_GetSerialNumberString(
      IntPtr hDevice,
      IntPtr Buffer,
      uint BufferLength);

   [DllImport("setupapi.dll", SetLastError = true)]
   internal static extern IntPtr SetupDiGetClassDevs(
      ref Guid ClassGuid,
      [MarshalAs(UnmanagedType.LPTStr)] string Enumerator,
      IntPtr hwndParent,
      UInt32 Flags);

   [DllImport("setupapi.dll", SetLastError = true)]
   internal static extern bool SetupDiEnumDeviceInterfaces(
      IntPtr DeviceInfoSet,
      int DeviceInfoData,
      ref  Guid lpHidGuid,
      uint MemberIndex,
      ref  SP_DEVICE_INTERFACE_DATA lpDeviceInterfaceData);

   [DllImport("setupapi.dll", SetLastError = true)]
   internal static extern bool SetupDiGetDeviceInterfaceDetail(
      IntPtr DeviceInfoSet,
      ref SP_DEVICE_INTERFACE_DATA lpDeviceInterfaceData,
      IntPtr hDeviceInterfaceDetailData,
      uint detailSize,
      out uint requiredSize,
      IntPtr hDeviceInfoData);

   [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
   [DllImport("kernel32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall, SetLastError = true)]
   internal static extern IntPtr CreateFile(
         string lpFileName,
         uint dwDesiredAccess,
         uint dwShareMode,
         IntPtr SecurityAttributes,
         uint dwCreationDisposition,
         uint dwFlagsAndAttributes,
         IntPtr hTemplateFile);

   [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
   [DllImport("kernel32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall, SetLastError = true)]
   internal static extern bool CloseHandle(IntPtr hHandle);

Also, we will need a number of structures, such as device attributes and capabilities.

[StructLayout(LayoutKind.Sequential)]
internal struct SP_DEVICE_INTERFACE_DATA {
   public int cbSize;
   public Guid InterfaceClassGuid;
   public int Flags;
   public int Reserved;
}

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
internal class PSP_DEVICE_INTERFACE_DETAIL_DATA {
   public int cbSize;
   [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
   public string DevicePath;
}

[StructLayout(LayoutKind.Sequential)]
internal struct HIDD_ATTRIBUTES {
   public int Size; // = sizeof (struct _HIDD_ATTRIBUTES) = 10
   public UInt16 VendorID;
   public UInt16 ProductID;
   public UInt16 VersionNumber;
}
[StructLayout(LayoutKind.Sequential)]
internal struct HIDP_CAPS {
   public UInt16 Usage;
   public UInt16 UsagePage;
   public UInt16 InputReportByteLength;
   public UInt16 OutputReportByteLength;
   public UInt16 FeatureReportByteLength;
   [MarshalAs(UnmanagedType.ByValArray, SizeConst = 17)]
   public UInt16[] Reserved;
   public UInt16 NumberLinkCollectionNodes;
   public UInt16 NumberInputButtonCaps;
   public UInt16 NumberInputValueCaps;
   public UInt16 NumberInputDataIndices;
   public UInt16 NumberOutputButtonCaps;
   public UInt16 NumberOutputValueCaps;
   public UInt16 NumberOutputDataIndices;
   public UInt16 NumberFeatureButtonCaps;
   public UInt16 NumberFeatureValueCaps;
   public UInt16 NumberFeatureDataIndices;
}

And a number of system constants

internal const uint DIGCF_PRESENT = 0x00000002;
internal const uint DIGCF_DEVICEINTERFACE = 0x00000010;
internal const uint GENERIC_READ = 0x80000000;
internal const uint GENERIC_WRITE = 0x40000000;
internal const uint FILE_SHARE_READ = 0x00000001;
internal const uint FILE_SHARE_WRITE = 0x00000002;
internal const int OPEN_EXISTING = 3;
internal const int FILE_FLAG_OVERLAPPED = 0x40000000;
internal const uint MAX_USB_DEVICES = 16;

Now, we are ready to start. So let’s find all devices and get its information

Native.HidD_GetHidGuid(ref _hidGuid);
hHidDeviceInfo = Native.SetupDiGetClassDevs(ref _hidGuid, null, IntPtr.Zero, Native.DIGCF_PRESENT | Native.DIGCF_DEVICEINTERFACE);

Now, if a handle we get is valid, we should search our specific device. For this purpose, we have to read device interface information and then get details info about this device.

if (hHidDeviceInfo.ToInt32() > -1) {
   uint i = 0;
   while (!isValid && i < Native.MAX_USB_DEVICES) {
      var hidDeviceInterfaceData = new Native.SP_DEVICE_INTERFACE_DATA();
      hidDeviceInterfaceData.cbSize = Marshal.SizeOf(hidDeviceInterfaceData);
      if (Native.SetupDiEnumDeviceInterfaces(hHidDeviceInfo, 0, ref _hidGuid, i, ref hidDeviceInterfaceData)) {

Once we have all this and information is valid, let’s detect its capabilities

bool detailResult;
uint length, required;
Native.SetupDiGetDeviceInterfaceDetail(hHidDeviceInfo, ref hidDeviceInterfaceData, IntPtr.Zero, 0, out length, IntPtr.Zero);
var hidDeviceInterfaceDetailData = new Native.PSP_DEVICE_INTERFACE_DETAIL_DATA();
hidDeviceInterfaceDetailData.cbSize = 5; //DWORD cbSize (size 4) + Char[0] (size 1) for 32bit only!
var hDeviceInterfaceDetailData = Marshal.AllocHGlobal(Marshal.SizeOf(hidDeviceInterfaceDetailData));
Marshal.StructureToPtr(hidDeviceInterfaceDetailData, hDeviceInterfaceDetailData, true);
detailResult = Native.SetupDiGetDeviceInterfaceDetail(hHidDeviceInfo, ref hidDeviceInterfaceData, hDeviceInterfaceDetailData, length, out required, IntPtr.Zero);
Marshal.PtrToStructure(hDeviceInterfaceDetailData, hidDeviceInterfaceDetailData);
if (detailResult) {

To do this, we have to create memory file first and then share device attributes by using this file.

base.handle = Native.CreateFile(hidDeviceInterfaceDetailData.DevicePath,
                        Native.GENERIC_READ |
                        Native.GENERIC_WRITE,
                        Native.FILE_SHARE_READ |
                        Native.FILE_SHARE_WRITE,
                        IntPtr.Zero,
                        Native.OPEN_EXISTING,
                        Native.FILE_FLAG_OVERLAPPED,
                        IntPtr.Zero);
                     if (base.handle.ToInt32() > -1) {
                        Native.HIDD_ATTRIBUTES hidDeviceAttributes;
                        if (Native.HidD_GetAttributes(base.handle, out hidDeviceAttributes)) {

All the rest is straight forward. Just compare info retrieved with one we already have. And, of cause, release all resources were used (remember, we’re in win32 api world!)

if ((hidDeviceAttributes.VendorID == vid) && (hidDeviceAttributes.ProductID == pid)) {
                              isValid = true;
                              ProductID = pid;
                              VendorID = vid;
                              VersionNumber = hidDeviceAttributes.VersionNumber;
                              IntPtr buffer = Marshal.AllocHGlobal(126);//max alloc for string;
                              if (Native.HidD_GetProductString(this.handle, buffer, 126)) Name = Marshal.PtrToStringAuto(buffer);
                              if (Native.HidD_GetSerialNumberString(this.handle, buffer, 126)) SerialNumber = Marshal.PtrToStringAuto(buffer);
                              Marshal.FreeHGlobal(buffer);
                              var capabilities = new Native.HIDP_CAPS();
                              IntPtr hPreparsedData;
                              if (Native.HidD_GetPreparsedData(this.handle, out hPreparsedData)) {
                                 if (Native.HidP_GetCaps(hPreparsedData, out capabilities)) FeatureReportLength = capabilities.FeatureReportByteLength;
                                 Native.HidD_FreePreparsedData(hPreparsedData);
                              }
                              break;
                           }
                        } else {
                           Native.CloseHandle(base.handle);
                        }
                     }
                  }
                  Marshal.FreeHGlobal(hDeviceInterfaceDetailData);
               }
               i++;

            }

Now we have a handle to our device and can manipulate it. Like this:

using (var device = USBRadioDevice.FindDevice(0x0000, 0x1111)) {

}

But we still have to provide methods for such usage. Here there are no very complicated code.

public static USBDevice FindDevice(uint pid, uint vid) {
   var device = new USBDevice(pid,vid);
   var fillRegisters = device.InitRegisters();
   if (!device.IsInvalid && fillRegisters) return device;
   else throw new ArgumentOutOfRangeException(string.Format("Human input device {0} was not found.", pid));
}

public override string ToString() {
   return string.Format("{0} (Product:{1:x}, Vendor:{2:x}, Version:{3:x}, S/N:{4})", Name, ProductID, VendorID, VersionNumber, SerialNumber);
}

[ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
protected override bool ReleaseHandle() {
   return Native.CloseHandle(base.handle);
}

#region IDisposable Members
public void Dispose() {
   Dispose(true);
   GC.SuppressFinalize(this);

}
[SecurityPermission(SecurityAction.Demand, UnmanagedCode = true)]
void IDisposable.Dispose() {
   if (base.handle != null && !base.IsInvalid) {
      // Free the handle
      base.Dispose();
   }
}

#endregion

We done. Have a nice day and be good people.

[This blog was migrated. You will not be able to comment here.
The new URL of this post is http://khason.net/blog/capturing-and-streaming-sound-by-using-directsound-with-c/]


I already wrote a little about managed way to use DirectX DirectSound. Today we’ll speak about how to get sound from your microphone or any other DirectSound capturing device (such as FM receiver) and stream it out to your PC speakers and any other DirectSound Output device. So, let’s start creating our first echo service by using managed DirectX.

image

First of all we should decide what Wave format we want to use for capturing and recording. So, let’s choose anything reasonable :)

var format = new WaveFormat {
            SamplesPerSecond = 96000,
            BitsPerSample = 16,
            Channels = 2,
            FormatTag = WaveFormatTag.Pcm
         };

Now, we should calculate block align and average byte per second value for this format. I’m wondering why it cannot be done automatically…

format.BlockAlign = (short)(format.Channels * (format.BitsPerSample / 8));
format.AverageBytesPerSecond = format.SamplesPerSecond * format.BlockAlign;

Next step is to set the size of two buffers – one for input and other for output. Generally those buffers are circular, and capturing one should be twice bigger, then output. Why? Because we choose two channels to use. Also, we should decide about chunk size of the buffer, we want to signal when filled.

_dwNotifySize = Math.Max(4096, format.AverageBytesPerSecond / 8);
_dwNotifySize -= _dwNotifySize % format.BlockAlign;
_dwCaptureBufferSize = NUM_BUFFERS * _dwNotifySize;
_dwOutputBufferSize = NUM_BUFFERS * _dwNotifySize / 2;

Next step is to create CaptureBufferDescriptor and actual capturing buffer. We’ll enumerate all devices and choose one, satisfies given string (captureDescriptor) – for example “Mic” :)

var cap = default(Capture);
var cdc = new CaptureDevicesCollection();
for (int i = 0; i < cdc.Count; i++) {
   if (cdc[i].Description.ToLower().Contains(captureDescriptor.ToLower())) {
      cap = new Capture(cdc[i].DriverGuid);
      break;
   }
}
var capDesc = new CaptureBufferDescription {
   Format = format,
   BufferBytes = _dwCaptureBufferSize
};
_dwCapBuffer = new CaptureBuffer(capDesc, cap);

Then we’ll create output device and buffer. To simplify program, we will use default speakers to output, however, you can choose output device the same way we did for capturing. Also, because DirectSound uses any window as it’s message pump, we have to use SetCooperativeLevel method. In my case (windowless application), I’ll use desktop window as message broker. This why you will have to add Windows.Forms as reference for your project, even if it console application. Also, do not forget to set GlobalFocus value to True, if you want to play echo, even if desktop window is not focused.

var dev = new Device();
dev.SetCooperativeLevel(Native.GetDesktopWindow(), CooperativeLevel.Priority);

var devDesc = new BufferDescription {
   BufferBytes = _dwOutputBufferSize,
   Format = format,
   DeferLocation = true,
   GlobalFocus = true
};
_dwDevBuffer = new SecondaryBuffer(devDesc, dev);

Now, we will subscribe to buffer notifications and set autoResetEvent to be fired when it filled up.

var _resetEvent = new AutoResetEvent(false);
var _notify = new Notify(_dwCapBuffer);
//half&half
var bpn1 = new BufferPositionNotify();
bpn1.Offset = _dwCapBuffer.Caps.BufferBytes / 2 - 1;
bpn1.EventNotifyHandle = _resetEvent.SafeWaitHandle.DangerousGetHandle();
var bpn2 = new BufferPositionNotify();
bpn2.Offset = _dwCapBuffer.Caps.BufferBytes - 1;
bpn2.EventNotifyHandle = _resetEvent.SafeWaitHandle.DangerousGetHandle();

_notify.SetNotificationPositions(new BufferPositionNotify[] { bpn1, bpn2 });

Almost done, the only thing we should do is to fire worker thread to take care on messages

int offset = 0;
_dwCaptureThread = new Thread((ThreadStart)delegate {
   _dwCapBuffer.Start(true);

   while (IsReady) {
      _resetEvent.WaitOne();
      var read = _dwCapBuffer.Read(offset, typeof(byte), LockFlag.None, _dwOutputBufferSize);
      _dwDevBuffer.Write(0, read, LockFlag.EntireBuffer);
      offset = (offset + _dwOutputBufferSize) % _dwCaptureBufferSize;
      _dwDevBuffer.SetCurrentPosition(0);
      _dwDevBuffer.Play(0, BufferPlayFlags.Default);
   }
   _dwCapBuffer.Stop();
});
_dwCaptureThread.Start();

That’s it. Compile and run. Now if you’ll speak, you can hear your echo from PC speakers.

Merry Christmas for whom concerns and be good people – do not scare your co-workers with strange sounds – be polite and make the volume lower :)

[This blog was migrated. You will not be able to comment here.
The new URL of this post is http://khason.net/blog/quick-silverlight-and-wpf-tip-how-to-write-program-without-xaml/]


From the moment, 10K MIX09 contest was launched, I got more, then 20 people, asking the same question: Is it possible to have Silverlight program up and running without XAML at all?

image

The answer is “YES, IT IS”. Here is how:

All you need for run WPF or Silverlight application is

  1. Class inherited from System.Windows.Application
  2. Class inherited from System.Windows.Controls.UserControl

So, Let’s create new WPF or Silverlight application and delete all files from the project directory. Then add one file, named App.cs (or Foo.cs or Whatever.cs – the length of the file name is not included :) ) and write there :

using System.Windows.Controls;
using System.Windows;

public class App : Application {public App() {this.Startup += (s, e) => { this.RootVisual = Foo.M; };}} 
class Foo: UserControl {static Foo _b = new Foo();public static Board M { get { return _b; } }

We done. F5, be happy. You just wrote first officially smallest Silverlight functional application. Good luck with Mix09 contest.

[This blog was migrated. You will not be able to comment here.
The new URL of this post is http://khason.net/blog/what-boots-faster-%e2%80%93-netbook-powered-windows-xp-or-nokia-e71-mobile-phone/]


Some days ago, somebody from Microsoft was shocked, when I told him, that I’m planning to run Windows XP (and later Windows 7) as operation system for mission critical automotive device. He even checked with Windows XP embedded team boot times for XP. They told him, that the minimum can be achieved is about 40 seconds cold boot and 30 seconds from hibernate state. I was upset and decided to tweak my system for smallest possible boot time. Here the result video. This is not the limit. I believe, that I’ll be able to decrease Windows XP boot time to less, then 10 seconds with a bit more efforts.

Note: This is absolutely authentic and non-touched video, recorded today by me, comparing boot time of Windows XP on unbranded weak netbook (Atom 1.6, 128MB and 8G SSD) and my Nokia E71 mobile phone. 15 seconds boot time of Windows XP achieved by tweaking only well known registry values and OS configuration values without special profundity of system settings.

Now the question: with todays’ devices, why we are not running XP for mobile and automotive mission critical devices?

[This blog was migrated. You will not be able to comment here.
The new URL of this post is http://khason.net/blog/reading-and-decoding-rds-radio-data-system-in-c/]


RDS or Radio Data System is very common in US and many European countries. It is communication protocol used to send small amount of digital information using regular FM radio broadcast. This protocol is used to "tell" your receiver about alternative frequencies, time, program notifications, program types, traffic information and regular text (such as singer name or genre). Unfortunately in Israel RDS is not very common and there is very limited number of radio stations broadcasts RDS information.

image

How RDS works?

As mentioned earlier, it uses FM subcarrier to broadcast digital information. It was designed to support 10 and 18 characters numeric and 80 characters alphanumeric displays. RDS operates at 1187.5 bps and based on 26-bit word consisting of 16 data and 10 error detection bits. Due to the fact, that FM carrier is not very reliable, error code allows correct information to be received even if an error of 3-5 bits exists within 26 bit block. Each four data blocks interpreted as 104-bit signal and named "group". Depending of the type of information, contained within the group, as different group type code is defined and transmitted within the group as upper five bits code. Even if more, then 104 bits required to completely send the information, there is no requirement that the next segment of the transmission be sent in the next group. There are 32 known groups types, defined by RFC:

private enum groupType : byte {
   RDS_TYPE_0A = (0 * 2 + 0),
   RDS_TYPE_0B = (0 * 2 + 1),
   RDS_TYPE_1A = (1 * 2 + 0),
   RDS_TYPE_1B = (1 * 2 + 1),
   RDS_TYPE_2A = (2 * 2 + 0),
   RDS_TYPE_2B = (2 * 2 + 1),
   RDS_TYPE_3A = (3 * 2 + 0),
   RDS_TYPE_3B = (3 * 2 + 1),
   RDS_TYPE_4A = (4 * 2 + 0),
   RDS_TYPE_4B = (4 * 2 + 1),
   RDS_TYPE_5A = (5 * 2 + 0),
   RDS_TYPE_5B = (5 * 2 + 1),
   RDS_TYPE_6A = (6 * 2 + 0),
   RDS_TYPE_6B = (6 * 2 + 1),
   RDS_TYPE_7A = (7 * 2 + 0),
   RDS_TYPE_7B = (7 * 2 + 1),
   RDS_TYPE_8A = (8 * 2 + 0),
   RDS_TYPE_8B = (8 * 2 + 1),
   RDS_TYPE_9A = (9 * 2 + 0),
   RDS_TYPE_9B = (9 * 2 + 1),
   RDS_TYPE_10A = (10 * 2 + 0),
   RDS_TYPE_10B = (10 * 2 + 1),
   RDS_TYPE_11A = (11 * 2 + 0),
   RDS_TYPE_11B = (11 * 2 + 1),
   RDS_TYPE_12A = (12 * 2 + 0),
   RDS_TYPE_12B = (12 * 2 + 1),
   RDS_TYPE_13A = (13 * 2 + 0),
   RDS_TYPE_13B = (13 * 2 + 1),
   RDS_TYPE_14A = (14 * 2 + 0),
   RDS_TYPE_14B = (14 * 2 + 1),
   RDS_TYPE_15A = (15 * 2 + 0),
   RDS_TYPE_15B = (15 * 2 + 1)
}

Not all groups are in use all the time. However, there are some commitments, defined by the protocol. For example, 1A have to be transmitted at least once a second. This group contains special information, required for receivers to be synchronized and locked into the transmitting channel.

Within the error correction information we also receive the direction to treat them.

private enum correctedType : byte {
   NONE = 0,
   ONE_TO_TWO = 1,
   THREE_TO_FIVE = 2,
   UNCORRECTABLE = 3
}

Also, each message type has it own limits. For example RT (Radio Text - 64 character text to display on your receiver) and PS (Programme Service - eight character station identification) message are limited to 2 groups, when PI (Programme Identification - unique code of the station) and PTY (Programme Type - one of 31 predefined program types - e.g. News, Drama, Music) are limited to 4.

In addition to those constraints, block types are also different. But in this case, there are only 4 kinds

private enum blockType : byte {
   A = 6,
   B = 4,
   C = 2,
   D = 0
}

So, what we're waiting for? Let's start working.

Handling errors

First of all we should take care on errors and fix them if possible. For this purpose, we should first count them and detect the way of fixing

var errorCount = (byte)((registers[0xa] & 0x0E00) >> 9);
var errorFlags = (byte)(registers[0x6] & 0xFF);
if (errorCount < 4) {
   _blocksValid += (byte)(4 - errorCount);
} else { /*drop data on more errors*/ return; }

Once it done, we can try to fix them

//Also drop the data if more than two errors were corrected
if (_getErrorsCorrected(errorFlags, blockType.B) > correctedType.ONE_TO_TWO) return;

private correctedType _getErrorsCorrected(byte data, blockType block) { return (correctedType)((data >> (byte)block) & 0x30); }

Now, our registers should be fine and we can start the detection of group type

Group Type Detection

This is very simple task, all we have to do is to get five upper bites to get a type and version.

var group_type = (groupType)(registers[0xD] >> 11);

Then we can handle PI and PTY, which we always have in RDS.

PI and PTY treatment

Now, let's update pi code, due to the fact, that B format always have PI in words A and C

_updatePI(registers[0xC]);

if (((byte)group_type & 0x01) != 0) {
_updatePI(registers[0xE]);
}

To update PI, we should check whether the new value is different from the previous and update it only in case it changed.

private void _updatePI(byte pi) {
   uint rds_pi_validate_count = 0;
   uint rds_pi_nonvalidated = 0;

   // if the pi value is the same for a certain number of times, update a validated pi variable
   if (rds_pi_nonvalidated != pi) {
      rds_pi_nonvalidated = pi;
      rds_pi_validate_count = 1;
   } else {
      rds_pi_validate_count++;
   }

   if (rds_pi_validate_count > PI_VALIDATE_LIMIT) {
      _piDisplay = rds_pi_nonvalidated;
   }
}

Then we will update PTY

_updatePTY((byte)((registers[0xd] >> 5) & 0x1f));

PTY treatment is very similar to PI, however it can be multiplied. 

private void _updatePTY(byte pty) {
   uint rds_pty_validate_count = 0;
   uint rds_pty_nonvalidated = 0;

   // if the pty value is the same for a certain number of times, update a validated pty variable
   if (rds_pty_nonvalidated != pty) {
      rds_pty_nonvalidated = pty;
      rds_pty_validate_count = 1;
   } else {
      rds_pty_validate_count++;
   }

   if (rds_pty_validate_count > PTY_VALIDATE_LIMIT) {
      _ptyDisplay = rds_pty_nonvalidated;
   }
}

When we done with those two groups, we can start handling another. Today, we'll handle only 0B, 2A and 2B types (I have a good reason for it, due to the fact, that only those are supported in Israel by now :) ) So,

Handling PS and different RTs

Simple switch on those groups

switch (group_type) {
   case groupType.RDS_TYPE_0B:
      addr = (byte)((registers[0xd] & 0x3) * 2);
      _updatePS((byte)(addr + 0), (byte)(registers[0xf] >> 8));
      _updatePS((byte)(addr + 1), (byte)(registers[0xf] & 0xff));
      break;
   case groupType.RDS_TYPE_2A:
      addr = (byte)((registers[0xd] & 0xf) * 4);
      abflag = (byte)((registers[0xb] & 0x0010) >> 4);
      _updateRT(abflag, 4, addr, (byte[])registers.Skip(0xe), errorFlags);
      break;
   case groupType.RDS_TYPE_2B:
      addr = (byte)((registers[0xd] & 0xf) * 2);
      abflag = (byte)((registers[0xb] & 0x0010) >> 4);
      // The last 32 bytes are unused in this format
      _rtTmp0[32] = 0x0d;
      _rtTmp1[32] = 0x0d;
      _rtCnt[32] = RT_VALIDATE_LIMIT;
      _updateRT(abflag, 2, addr, (byte[])registers.Skip(0xe), errorFlags);
      break;
}

and let's dig into PS.

In PS, we have high and low probability bits. So, if new bit in sequence matches the high probability bite and we have recieved enough bytes to max out the counter, we'll push it into the low probability array.

if (_psTmp0[idx] == default(byte)) {
           if (_psCnt[idx] < PS_VALIDATE_LIMIT) {
               _psCnt[idx]++;
            } else {
               _psCnt[idx] = PS_VALIDATE_LIMIT;
               _psTmp1[idx] = default(byte);
            }
         }

Else, if new byte matches with the low probability byte, we should swap them and then reset the counter, by flagging the text as in transition.

else if (_psTmp1[idx] == default(byte)) {
            if (_psCnt[idx] >= PS_VALIDATE_LIMIT) {
               isTextChange = true;
            }
            _psCnt[idx] = PS_VALIDATE_LIMIT + 1;
            _psTmp1[idx] = _psTmp0[idx];
            _psTmp0[idx] = default(byte);
         }

When we have an empty byte in high probability array or new bytes does not match anything we know, we should put it into low probability array.

else if (_psCnt[idx] == null) {
            _psTmp0[idx] = default(byte);
            _psCnt[idx] = 1;
         } else {
            _psTmp1[idx] = default(byte);
         }

Now, if we marked our text as changed, we should decrement the count for all characters to prevent displaying of partical message, which in still in transition.

         if (isTextChange) {
            for (byte i = 0; i < _psCnt.Length; i++) {
               if (_psCnt[i] > 1) {
                  _psCnt[i]--;
               }
            }
         }

Then by checking PS text for incompetence, when there are characters in high probability array has been seen fewer times, that was limited by validation.

         for (byte i = 0; i < _psCnt.Length; i++) {
            if (_psCnt[i] < PS_VALIDATE_LIMIT) {
               isComplete = false;
               break;
            }
         }

Only if PS text in the high probability array is complete, we'll copy it into display.


         if (isComplete) {
            for (byte i = 0; i < _psDisplay.Length; i++) {
               _psDisplay[i] = _psTmp0[i];
            }
         }

It is not very hard to treat PS. Isn't it? Let's see what's going on with RT.

If A and B message flag changes, we'll try to force a display by increasing the validation count for each byte. Then, we'll wipe any cached text.

   if (abFlag != _rtFlag && _rtFlagValid) {
      // If the A/B message flag changes, try to force a display
      // by increasing the validation count of each byte
      for (i = 0; i < _rtCnt.Length; i++) _rtCnt[addr + i]++;
      _updateRTValue();

      // Wipe out the cached text
      for (i = 0; i < _rtCnt.Length; i++) {
         _rtCnt[i] = 0;
         _rtTmp0[i] = 0;
         _rtTmp1[i] = 0;
      }
   }

Now A and B flags are safe, sp we can start with message processing. First of all, NULL in RDS means space :)


   _rtFlag = abFlag;   
   _rtFlagValid = true;   

   for (i = 0; i < count; i++) {
      if (p[i] == null) p[i] = (byte)' ';

The new byte matches the high probability byte also in this case. We habe to recieve this bite enough to max out counters. Then we can push it into the low probability as well.

      if (_rtTmp0[addr + i] == p[i]) {
         if (_rtCnt[addr + i] < RT_VALIDATE_LIMIT) _rtCnt[addr + i]++;
         else {
            _rtCnt[addr + i] = RT_VALIDATE_LIMIT;
            _rtTmp1[addr + i] = p[i];
         }
      }

When the new byte matches with low probability byte, we'll swap them as well and reset counters to update text in transition flag. However in this case, our counter will go higher, then the validation limit. So we'll have to remove it down later.

else if (_rtTmp1[addr + i] == p[i]) {

         if (_rtCnt[addr + i] >= PS_VALIDATE_LIMIT) isChange = true;

         _rtCnt[addr + i] = RT_VALIDATE_LIMIT + 1;
         _rtTmp1[addr + i] = _rtTmp0[addr + i];
         _rtTmp0[addr + i] = p[i];
      }

Now, the new byte is replaced an empty byte in the high probability array. Also, if this byte does not match anything, we should move it into low probability.

else if (_rtCnt[addr + i] == null) {
         _rtTmp0[addr + i] = p[i];
         _rtCnt[addr + i] = 1;
      } else _rtTmp1[addr + i] = p[i];

   }

Now when the text is changing, we'll decrement the counter for all characters exactly as we did for PS.


      for (i = 0; i < _rtCnt.Length; i++) {
         if (_rtCnt[i] > 1) _rtCnt[i]--;
      }
   }

However, right after, we'll update display. 

   _updateRTValue();
}

Displaying RT

But how to convert all those byte arrays into readable message? Simple :)

First of all if text is incomplete, we should keep loading it. Also it makes sense to check whether the target array is shorter then maximum allowed to prevent junk from being displayed.

for (i = 0; i < _rtTmp0.Length; i++) {
   if (_rtCnt[i] < RT_VALIDATE_LIMIT) {
      isComplete = false;
      break;
   }
   if (_rtTmp0[i] == 0x0d) {
      break;
   }
}

Now, when our Radio Text is in the high probability and it complete, we should copy buffers.

if (isComplete) {
   _Text = string.Empty;

   for (i = 0; i < _rtDisplay.Length; i += 2) {
      if ((_rtDisplay[i] != 0x0d) && (_rtDisplay[i + 1] != 0x0d)) {
         _rtDisplay[i] = _rtTmp0[i + 1];
         _rtDisplay[i + 1] = _rtTmp0[i];
      } else {
         _rtDisplay[i] = _rtTmp0[i];
         _rtDisplay[i + 1] = _rtTmp0[i + 1];
      }

      if (_rtDisplay[i] != 0x0d)
         _Text += _rtDisplay[i];

      if (_rtDisplay[i + 1] != 0x0d)
         _Text += _rtDisplay[i + 1];

      if ((_rtDisplay[i] == 0x0d) || (_rtDisplay[i + 1] == 0x0d))
         i = (byte)_rtDisplay.Length;
   }

And not forget to wipe out everything after the end of the message :)


   for (i++; i < _rtDisplay.Length; i++) {
      _rtDisplay[i] = 0;
      _rtCnt[i] = 0;
      _rtTmp0[i] = 0;
      _rtTmp1[i] = 0;
   }
}

And finally update the text

Text = _Text;

We done. Now we can handle RDS digital messages, but what to do with analog data we get? Don't you already know? I blogged about it here.

Have a nice day and be good people, because you know how to write client, knows to get and parse radio data in managed code.

image