NetMF MP3 Player Part 3

Building on the file listing code from the previous post in the next class we built a basic multi threaded music player using an enhanced version of the Vs1053B driver based on the softelectrotech.and bluecone code.

The code uses five interrupt ports (I used 5 buttons on the 4 analog ports on the Seeedstudio V2 Base shield and D5 to make it easier to fit the Mp3 shield)

As the Vs1053 driver is now running asynchronously it fires an event when the current track has finished playing

   public class Program
   {
      static string[] MusicFiles;
      static Vs1053B player = new Vs1053B(Pins.GPIO_PIN_D2, Pins.GPIO_PIN_D6, Pins.GPIO_PIN_D7, Pins.GPIO_PIN_D8);
      static byte volume = 200;
      static int trackNumber = 1;
      static public string TrackFilename { get { return MusicFiles[trackNumber - 1]; } }

      public static void Main()
      {
         MusicFiles = Directory.GetFiles(@"\SD");

         foreach( string file in MusicFiles)
         {
            Debug.Print(file);
         }

         InterruptPort playPause = new InterruptPort(Pins.GPIO_PIN_D5, false, Port.ResistorMode.Disabled, Port.InterruptMode.InterruptEdgeLow);
         playPause.OnInterrupt += playPause_OnInterrupt;

         InterruptPort volumeUp = new InterruptPort(Pins.GPIO_PIN_A0, false, Port.ResistorMode.Disabled, Port.InterruptMode.InterruptEdgeLow);
         volumeUp.OnInterrupt += volumeUp_OnInterrupt;

         InterruptPort volumeDown = new InterruptPort(Pins.GPIO_PIN_A1, false, Port.ResistorMode.Disabled, Port.InterruptMode.InterruptEdgeLow);
         volumeDown.OnInterrupt += volumeDown_OnInterrupt;

         InterruptPort previousTrack = new InterruptPort(Pins.GPIO_PIN_A2, false, Port.ResistorMode.Disabled, Port.InterruptMode.InterruptEdgeLow);
         previousTrack.OnInterrupt += previousTrack_OnInterrupt;

         InterruptPort nextTrack = new InterruptPort(Pins.GPIO_PIN_A3, false, Port.ResistorMode.Disabled, Port.InterruptMode.InterruptEdgeLow);
         nextTrack.OnInterrupt += nextTrack_OnInterrupt;

         player.SetVolume(volume);
         player.Filename = TrackFilename;
         player.onMusicFinished += player_onMusicFinished;

         player.Play();

         Thread.Sleep(Timeout.Infinite);
      }

      static void player_onMusicFinished()
      {
         trackNumber += 1;

         if (trackNumber > MusicFiles.Length)
         {
            trackNumber = 1;
         }
         Debug.Print("player_onMusicFinished " + trackNumber + " of " + MusicFiles.Length);

         player.Filename = TrackFilename;
      }

      static void previousTrack_OnInterrupt(uint data1, uint data2, DateTime time)
      {
         trackNumber -= 1;

         if (trackNumber < 1)          {             trackNumber = MusicFiles.Length;          }          Debug.Print("previousTrack_OnInterrupt " + trackNumber + " of " + MusicFiles.Length);          player.CancelPlayback();          player.Filename = TrackFilename;       }       static void nextTrack_OnInterrupt(uint data1, uint data2, DateTime time)       {          trackNumber += 1;          if (trackNumber > MusicFiles.Length)
         {
            trackNumber = 1;
         }
         Debug.Print("nextTrack_OnInterrupt " + trackNumber + " of " + MusicFiles.Length);

         player.CancelPlayback();
         player.Filename = TrackFilename;
      }

      static void playPause_OnInterrupt(uint data1, uint data2, DateTime time)
      {
         Debug.Print("playPause_OnInterrupt ");
         if (player.IsPaused())
         {
            player.Resume();
         }
         else
         {
            player.Pause();
         }
      }

      static void volumeDown_OnInterrupt(uint data1, uint data2, DateTime time)
      {
         Debug.Print("volumeDown_OnInterrupt " + volume);
         if (volume > 0)
         {
            volume -= 1;
         }
         player.SetVolume(volume);
      }

      static void volumeUp_OnInterrupt(uint data1, uint data2, DateTime time)
      {
         Debug.Print("volumeUp_OnInterrupt " + volume);
         if (volume < 255)
         {
            volume += 1;
         }
         player.SetVolume(volume);
      }
   }

Next steps are to ignore contact bounce on the buttons and play MP3 files in a more consistent order
musicplayerv2

Netduino with Mp3 Shield and 5 button UI

Netduino Mp3 player with 5 button UI

NetMF MP3 Player Part 1

For one of my class projects the students build an NetMF MP3 player using a Netduino, MP3 shield, MicroSD card and some code. The first step is to learn about files, and directories by enumerating the contents of the MicroSD card.

public static void Main()
 {
    string[] musicFiles = Directory.GetFiles(@"\SD");

    foreach (string musicFile in musicFiles)
    {
       Debug.Print(musicFile);
    }
}

The NetMF implementation of GetFiles doesn’t support wildcards (unlike the full .Net Framework) so the list of files has to be manually filtered.

public static void Main()
{
   string[] musicFiles = Directory.GetFiles(@"\SD");

   foreach (string musicFile in musicFiles)
   {
      if (filePath.IndexOf(".mp3") != -1)
      {
         Debug.Print(musicFile);
      }
   }
}

The code above displayed
\SD\01 Pride (In the Name of Love).mp3
\SD\02 New Year’s Day.mp3
\SD\03 With or Without You.mp3
\SD\04 I Still Haven’t Found What I’m Looking For.mp3
\SD\05 Sunday Bloody Sunday.mp3
\SD\06 Bad.mp3
\SD\07 Where the Streets Have No Name.mp3
\SD\08 I Will Follow.mp3
\SD\09 The Unforgettable Fire.mp3
\SD\10 Sweetest Thing [The Single Mix].mp3
\SD\11 Desire.mp3
\SD\12 When Love Comes to Town.mp3
\SD\13 Angel of Harlem.mp3
\SD\14 All I Want Is You.mp3

For this project Directory.GetFiles was used (rather than Directory.EnumerateFiles) because the list files on the MicroSD will be used in other parts of the application.

The parsing and processing of file paths is important and can if done wrong can introduce hard to find issues(e.g. directory traversal attacks)

Debug.Print(musicFile .Substring( 0, musicFile.LastIndexOf(".mp3")));

Updating the code to use the built in System.IO.Path functionality

public static void Main()
{
   string[] MusicFiles = Directory.GetFiles(@"\SD");

   foreach (string musicFile in MusicFiles)
   {
      if (Path.GetExtension(musicFile) == ".mp3" )
      {
         Debug.Print(Path.GetFileNameWithoutExtension(musicFile));
      }
   }
}

The next step is to load and play the MP3 files using a provided library.

Netduino + SeeedStudio Grove LCD RGB Backlight

For the last year I have been teaching introductory programing classes using Netduino devices and Seeedstudio sensors which were sponsored by Microsoft New Zealand. The kits are based on a Grove Starter Kit for Arduino/Genuino 101 which contain a Grove LCD RGB Backlight display.

Seeedstudio have published an Arduino driver for the display and it looks like Sirsnork used this as the basis for his Netduino NetMF Port. In class a few people have commented that they have had difficulty getting the driver to work on a Netduino 2 or Netduino Plus 2 device.

I think the two main issues are the lack of termination resistors on the Grove RGB Backlight. Possibly the missing R9 & R10 in the picture below?

SeeedStudioRGBLCDBack

I have found the easiest way to work around this issue is to have another I2C device (In this case it’s a Grove 3 Axis Accelerometer ±16G)

SeeedStudioNetduinoAndRGBLCD

The other is the need to strobe the Serial Data Line (SDA) of the I2C port on later Netduino devices to get it to work

using System;
using System.Threading;
using Microsoft.SPOT.Hardware;
using SecretLabs.NETMF.Hardware.Netduino;

namespace SeeedStudio.Grove.RGBLCD
{
   public class Program
   {
      public static void Main()
      {
         using (OutputPort i2cPort = new OutputPort(Pins.GPIO_PIN_SDA, true))
         {
            i2cPort.Write(false);
         }

.....

With the I2C bus terminated and the SDA port strobe I have found the Netduino and display work as expected.

Netduino 3 Wifi pollution Sensor Part 2

In a previous post I had started building a driver for the Seeedstudio Grove Dust Sensor. It was a proof of concept and it didn’t handle some edge cases well.

While building the pollution monitor with a student we started by simulating the negative occupancy of the Shinyei PPD42NJ Particle sensor with the Netduino’s on-board button. This worked and reduced initial complexity. But it also made it harder to simulate the button being pressed as the program launches (the on-board button is also the reset button), or simulate if the button was pressed at the start or end of the period.

Dust sensor simulation with button

Netduino 3 Wifi Test Harness

The first sample code processes button press interrupts and displays the values of the data1 & data2 parameters

public class Program
{
   public static void Main()
   {
      InterruptPort button = new InterruptPort(Pins.GPIO_PIN_D5, false, Port.ResistorMode.Disabled, Port.InterruptMode.InterruptEdgeBoth);
      button.OnInterrupt += button_OnInterrupt;

      Thread.Sleep(Timeout.Infinite);
   }

   static void button_OnInterrupt(uint data1, uint data2, DateTime time)
   {
      Debug.Print(time.ToString("hh:mm:ss.fff") + " data1 =" + data1.ToString() + " data2 = " + data2.ToString());
   }
}

Using the debugging output from this application we worked out that data1 was the Microcontroller Pin number and data2 was the button state.

12:00:14.389 data1 =24 data2 = 0
12:00:14.389 data1 =24 data2 = 1
12:00:14.389 data1 =24 data2 = 0
12:00:15.851 data1 =24 data2 = 1
12:00:16.078 data1 =24 data2 = 0

We then extended the code to record the duration of each button press.

public class Program
{
   static DateTime buttonLastPressedAtUtc = DateTime.UtcNow;

   public static void Main()
   {
      InterruptPort button = new InterruptPort(Pins.ONBOARD_BTN, false, Port.ResistorMode.Disabled, Port.InterruptMode.InterruptEdgeBoth);
      button.OnInterrupt += button_OnInterrupt;

      Thread.Sleep(Timeout.Infinite);
   }

   static void button_OnInterrupt(uint data1, uint data2, DateTime time)
   {
      if (data2 == 0)
      {
         TimeSpan duration = time - buttonLastPressedAtUtc;

         Debug.Print(duration.ToString());
      }
      else
      {
         buttonLastPressedAtUtc = time;
      }
   }
}

The thread ” (0x4) has exited with code 0 (0x0).
00:00:00.2031790
00:00:00.1954150
00:00:00.1962350

The next step was to keep track of the total duration of the button presses since the program started executing.

public class Program
{
   static DateTime buttonLastPressedAtUtc = DateTime.UtcNow;
   static TimeSpan buttonPressedDurationTotal;

   public static void Main()
   {
      InterruptPort button = new InterruptPort(Pins.ONBOARD_BTN, false, Port.ResistorMode.Disabled, Port.InterruptMode.InterruptEdgeBoth);
      button.OnInterrupt += button_OnInterrupt;

      Thread.Sleep(Timeout.Infinite);
   }

   static void button_OnInterrupt(uint data1, uint data2, DateTime time)
   {
      if (data2 == 0)
      {
         TimeSpan duration = time - buttonLastPressedAtUtc;

         buttonPressedDurationTotal += duration;
          Debug.Print(duration.ToString() + " " + buttonPressedDurationTotal.ToString());
      }
      else
      {
         buttonLastPressedAtUtc = time;
      }
   }
}

The thread ” (0x4) has exited with code 0 (0x0).
00:00:00.2476460 00:00:00.2476460
00:00:00.2193600 00:00:00.4670060
00:00:00.2631400 00:00:00.7301460
00:00:00.0001870 00:00:00.7303330

We then added a timer to display the amount of time the button was pressed in the configured period.

public class Program
{
   static TimeSpan measurementDueTime = new TimeSpan(0, 0, 30);
   static TimeSpan measurementperiodTime = new TimeSpan(0, 0, 30);
   static DateTime buttonLastPressedAtUtc = DateTime.UtcNow;
   static TimeSpan buttonPressedDurationTotal;


   public static void Main()
   {
      InterruptPort button = new InterruptPort(Pins.GPIO_PIN_D5, false, Port.ResistorMode.Disabled, Port.InterruptMode.InterruptEdgeBoth);
      button.OnInterrupt += button_OnInterrupt;

      Timer periodTimer = new Timer(periodTimerProc, button, measurementDueTime, measurementperiodTime);

      Thread.Sleep(Timeout.Infinite);
   }

   static void periodTimerProc(object status)
   {
      InterruptPort button = (InterruptPort)status;

      if (button.Read())
      {
         TimeSpan duration = DateTime.UtcNow - buttonLastPressedAtUtc;

         buttonPressedDurationTotal += duration; 
      }

      Debug.Print(buttonPressedDurationTotal.ToString());

      buttonPressedDurationTotal = new TimeSpan(0, 0, 0);
      buttonLastPressedAtUtc = DateTime.UtcNow;
   }

   static void button_OnInterrupt(uint data1, uint data2, DateTime time)
   {
      if (data2 == 0)
      {
         TimeSpan duration = time - buttonLastPressedAtUtc;

         buttonPressedDurationTotal += duration;

         Debug.Print(duration.ToString() + " " + buttonPressedDurationTotal.ToString());
      }
      else
      {
         buttonLastPressedAtUtc = time;
      }
   }
}

The thread ” (0x4) has exited with code 0 (0x0).
00:00:00
00:00:00
00:00:00.2299050 00:00:00.2299050
00:00:00.1956980 00:00:00.4256030
00:00:00.1693190 00:00:00.5949220
00:00:00.5949220

After some testing we identified that the handling of button presses at the period boundaries was problematic and revised the code some more. We added a timer for the startup period to simplify the interrupt handling code.

public class Program
{
   static TimeSpan measurementDueTime = new TimeSpan(0, 0, 60);
   static TimeSpan measurementperiodTime = new TimeSpan(0, 0, 30);
   static DateTime buttonLastPressedAtUtc = DateTime.UtcNow;
   static TimeSpan buttonPressedDurationTotal;

   public static void Main()
   {
      InterruptPort button = new InterruptPort(Pins.GPIO_PIN_D5, false, Port.ResistorMode.Disabled, Port.InterruptMode.InterruptEdgeBoth);
      button.OnInterrupt += button_OnInterrupt;

      Timer periodTimer = new Timer(periodTimerProc, button, Timeout.Infinite, Timeout.Infinite);

      Timer startUpTImer = new Timer(startUpTimerProc, periodTimer, measurementDueTime.Milliseconds, Timeout.Infinite);

      Thread.Sleep(Timeout.Infinite);
   }

   static void startUpTimerProc(object status)
   {
      Timer periodTimer = (Timer)status;

      Debug.Print( DateTime.UtcNow.ToString("hh:mm:ss") + " -Startup complete");

      buttonLastPressedAtUtc = DateTime.UtcNow;
      periodTimer.Change(measurementDueTime, measurementperiodTime);
   }

   static void periodTimerProc(object status)
   {
      InterruptPort button = (InterruptPort)status;
      Debug.Print(DateTime.UtcNow.ToString("hh:mm:ss") + " -Period timer");

      if (button.Read())
      {
         TimeSpan duration = DateTime.UtcNow - buttonLastPressedAtUtc;

         buttonPressedDurationTotal += duration;
      }

      Debug.Print(buttonPressedDurationTotal.ToString());

      buttonPressedDurationTotal = new TimeSpan(0, 0, 0);
      buttonLastPressedAtUtc = DateTime.UtcNow;
   }

   static void button_OnInterrupt(uint data1, uint data2, DateTime time)
   {
      Debug.Print(DateTime.UtcNow.ToString("hh:mm:ss") + " -OnInterrupt");

      if (data2 == 0)
      {
         TimeSpan duration = time - buttonLastPressedAtUtc;

         buttonPressedDurationTotal += duration;

         Debug.Print(duration.ToString() + " " + buttonPressedDurationTotal.ToString());
      }
      else
      {
         buttonLastPressedAtUtc = time;
      }
   }
}

The debugging output looked positive, but more testing is required.

The thread ” (0x2) has exited with code 0 (0x0).
12:00:13 -Startup complete
12:01:13 -Period timer
00:00:00
12:01:43 -Period timer
00:00:00
12:01:46 -OnInterrupt
12:01:48 -OnInterrupt
00:00:01.2132510 00:00:01.2132510
12:01:49 -OnInterrupt
12:01:50 -OnInterrupt
00:00:01.3001240 00:00:02.5133750
12:01:53 -OnInterrupt
12:01:54 -OnInterrupt
00:00:01.1216510 00:00:03.6350260
12:02:13 -Period timer
00:00:03.6350260

Next steps – multi threading, extract code into a device driver and extend to support sensors like the SeeedStudio Smart dust Sensor which has two digital outputs, one for small particles (e.g. smoke) the other for larger particles (e.g. dust).

St Margaret’s CodeClub information

If you want to follow along at home all the software is free (Visual Studio Express 2013) or open source (Netduino software & Hardware + NetMF) and can be downloaded from the following locations. The packages need to be sequentially installed in the order below.

If you want to purchase your own hardware, we use Netduino 2 Plus devices (we use them mainly for their integrated networking and MicroSD card) and Seeedstudio Grove sensors. (Prices as at 05/2015)

We are looking into Apple friendly options for later this term.

CodeClub Internet of Things Boxes sponsored by Microsoft NZ

A few months ago Microsoft NZ donated NZ6K to CodeClub NZ for the purchase of kits for our basic electronics and programming classes.

Over the last couple of months I have been assembling these so we now have 15 kits ready to go. Each one has enough gear for 2-6 students, fits into a 7L Sistema plastic box and contains the following items

2 x Netduino 2 Plus devices
2 x Seeedstudio Grove Starter kits for Arduino which contain

  • 1xBase Shield
  • 1xGrove – LCD RGB Backlight
  • 1xGrove – Smart Relay
  • 1xGrove – Buzzer
  • 1xGrove – Sound Sensor
  • 1xGrove – Touch Sensor
  • 1xGrove – Rotary Angle Sensor
  • 1xGrove – Temperature Sensor
  • 1xGrove – Light Sensor
  • 1xGrove – Button
  • 1xGrove LED Blue-Blue
  • 1xGrove LED Green-Green
  • 1xGrove  LED Red-Red
  • 1xMini Servo
  • 10xGrove Cables
  • 1x9V to Barrel Jack Adapter
  • 1xGrove starter kit Manual
  • 1xGreen Plastic Box
  • 1 x ultrasonic ranger

In addition to the Netduino devices and the Grove starter kits, we also include

Thanks to Embedded coolness, Secret Labs, and Seeedstudio which discounted their products so our funding went further.

CodeClub Programming and electronics kits

CodeClub Programming and electronics kits

Electric Longboard devDuino V2 Controller

I really wanted to get the longboard working so I had a look at buying Wiiceiver from AustinDavid.com.

The source code is available on Github and I had a spare devicter devDuino Sensor Node V2 sitting on my desk. With some modification (changing pins numbers and removing all references to the second LED) I got the wiiceiver code running on my devDuino.

The Electronic Speed Controller(ESC) and the plastic lunch box (containing the batteries and devDuino) are attached to the deck with 3M Command adhesive strips. The first set of command adhesive strips I tried were for hanging pictures and had a Velcro quick release system. This approach was a failure and the ESC & electronics box fell off after 10-15 minutes use. The Velcro backing tape was getting pulled in the wrong direction so was unable to hold the weight of the electronics when vibration levels increased. I tried them because a “quick release” capability would be handy but I have gone back to using conventional 3M Command adhesive strips and these are working well.

devDuino V2 and ESC on longboard

devDuino based controller interfaced with ESC and wireless WiiChuk

Initial rides went well, though I need to recalibrate the acceleration and braking ramp up/down settings to suit my hardware and riding style.

Bill of Materials for this project (Prices as at Feb 2015)

  • Single Motor Mechanical Electric Longboard Kit USD223
  • Turnigy Aerodrive SK3-6364-245kv Brushless Outrunner Motor USD70.68
  • HobbyKing 150A High Performance Brushless Car ESC USD68.99
  • ZIPPY Flightmax 5000mAh battery X 2 USD31.99 each
  • HXT4mm Battery Harness 14AWG for 2 Packs in Series USD2.43
  • HXT 4mm Gold Connector with Protector (10pcs/set)
  • devDuino Sensor Node V2 USD15.99
  • Grove Nunchuck adaptor USD2.90
  • Grove Branch Cable for Servo USD4.90
  • Wireless Nunchuck NZD25.00
  • Moose 9.5×42 Longboard Flush Mount Deck Green Stain NZD57

WARNING – Disconnect the power supply pin on the Grove Branch Cable for Servos as the ESC will supply sufficient current to make the batteries on the devDuino go pop. Wrap some tape around the other servo connector so it can’t cause a short circuit.

Thanks to Austin David for making the code for the Wiiciever open source, if anyone is interested in my code I can tidy it up and share.

/*
 * Pin IDs -- NOT LOCATIONS !!!
 * don't change these ever; see &quot;pinLocation&quot; below for
 * actual locations
 */
#define RED_LED_ID   0
//#define GREEN_LED_ID 1

#define ESC_PPM_ID   2
//#define ESC2_PPM_ID   6
#define ESC_GROUND 00     // hard-wired

//#define WII_POWER_ID 3
//#define WII_GROUND 00     // hard-wired

#define WII_SCL_ID   4
#define WII_SDA_ID   5

I made my devDuino look like a V3 wiiceiver

int pinLocation(int pinID) {
  int pinMap[7][3] = {
  // v1, v2, v3
    {8,   8,  9},  // RED_LED     any digital pin
    {7,   6,  8},  // GREEN_LED   any digital pin
    {10,  9,  3},  // ESC_PPM     PWM required
    {9,  11,  5},  // WII_POWER   any digital pin
    {19, 19, 19}, // WII_SCL     A5, don't change
    {18, 18, 18}, // WII_SDA     A4, don't change
    {0,  10, 0}, // ESC2_PPM    PWM required
  };

The rest of my changes were commenting out all references to the Green LED as the devDuino only has one onboard LED.