DFPlayerMini cheat sheet

I thought I would tell about my solution to the power up/down clicking. I wanted to run from batteries so leaving it on all the time was not an option. So I route one speaker wire through a MOSFET Relay. I used TLP170AM(TPL,E. I sequenced the software so that the relay was always off on power up/down Worked great, no more pods and clicks

1 Like

I very much doubt the file timestamps make a difference; the FAT file system has a simple linear format, and most likely the unit just assigns numbers to the files according to the order they appear in the directory (which usually is the order in which they were added).

This can be checked: add F1.MP3 F2.MP3 F3.MP3, in order, to an empty flash drive. Then erase F2.MP3, and add F4.MP4. So, F4.MP4 will be the most recently added, but the order in the directory will be F1.MP3 F4.MP3 F3.MP3, and F4.MP4 should play as #2 if I’m right.

Under linux, you can list a directory in its actual order with the -U option: ls -U dirname

(I’m reading this page because I’ve got one that I plan to start using soon, and the ‘instruction manual’ is well, baffling. I will try this experiment I’ve described, at some point).

The diagram is incorrect - the speaker output is mono, the ampifier output is in an ‘H bridge’ arrangement operating from a single power rail (one goes high when the other goes low, but both are > 0 V) and the two speaker pins need to go to two terminals of a single speaker.

Hi! Looking for some help. I have 5 files on my micro SD card that should technically play on my DF Miniplayer. The files are correctly labeled 0001.mp3 to 0005.mp3. HOWEVER, only the first file plays, NONE of the others play. Does anyone know why? Thanks

@SGP There could be a few things going on here. Check the sampling rate of your MP3s to make sure that they conform to 8/11.025/12/16/22.05/24/32/44/48KHz. Also, what mode are you trying to play them in (root folder, subfolders)?

@greg The order issue is confusing and brittle. I found the most reliable way to ensure the order of playback was to format the card, and add all of the files in sorted order.

1 Like

I have tried this - add 3 files, check it’s working, then delete the middle one (#3 then becomes sound #2) and then copy another file (replaces the deleted file in the directory, so it becomes the new #2, previous #2 restored to #3 again). In short, the files are indexed according to the order they appear with “ls -U”, regardess of time stamps.
Also, I avoided using file names that were not compatible with old 8.3 DOS filenames. Non-8.3 filenames are done in VFAT by adding hidden directory entries, and this may affect things.

I gave up on putting them in the root folder. The only reliable way to playback is to create a subfolder named “0001” and put the numbered files in there, and do DFPlayer::playFolder(1, fileIndex);

I had another interesting issue. Since I’m powering the DFPlayer with an ESP32, I found that I had to lower the CPU frequency down to 80MHz (which is essentially the lowest frequency and still have WiFi/BT functionality) in order to reserve enough power to drive the DFPlayer. If your files aren’t playing, make sure that you have sufficient power/amperage at the DFPlayer.

There are several different versions of this device and conflicting information published in different places, but I have found the ones that use the MH2024-24SS. or GD3200B work very well with the following folder structure:
Folders named ‘01’ to ‘10’. It probably goes higher, but that was as many as I tested.
Files named ‘001’ … .mp3 to ‘255’ … .mp3.

The … must be plain ASCII characters. Then use the 0x0F command to select the file and folder at the same time. EG:
7E FF 06 0F 01 04 0A FE DD EF
to play file 010…MP3 in folder /04 (with acknowledgement).

This will give an accurate selection provided that the folders and files are created on the SD card in the exact order of their names - you will probably need to write a small utility to do the copying to ensure that the order is correct. Other arrangements do work (root folder, MP3 folder) but are less reliable.

It’s possible that the actual filenames do not matter!. But the above arrangement complies with several different documents. I tested it like this and got good results.

Other points of importance are:
-The speaker connection is a balanced pair - do not use a ground connection.
-With a large SD card and/or a large number of files, initialization can take a very long time. Wait for the initialization acknowledgement before continuing (7E FF 06 3F 00 00 02 FE BA EF).
-Using the acknowledgement setting and waiting for acknowledgement for all commands is strongly recommended.

This is potentially the most informative source I’ve found in several months of searching, so do hope it’s still active!
I’m an experienced hobbyist but not a programmer, learning Arduino for a year or so.

In setup(), although the initialisation code is obscure to me, I can set volume and start playing all tracks, either successively or randomly; and with the loop() empty, that continues OK.
And in setup I can start with a specific track, either with play() or the RepRage alternative function.
But working in the loop() has so far defeated me, for anything but the trivial example given by DFRobot (playing each track for a fixed duration).
I’ve studied countless examples but still cannot fathom how to add simple commands in the loop as I’ve done successfully with other projects (including the DF RFobot USB version module DF1201S). For example:

If D3 goes LOW, toggle Pause/Play
If D6 goes LOW, jump to track 123
If D12 goes LOW, reduce volume
If D10 is LOW play randomly, if HIGH play successively.
Print the track number just ONCE when any track starts
etc

If I could see just a single reliable example I reckon the penny might finally drop!

My objective in a nutshell is, with minimal rewiring, to replace the code I used for the USB version shown in the photo.

I’ll post my current, unsuccessful code if I’m lucky enough to get a reply to this.

Terry

How are you polling/reading from the pins in your main loop?

P.S. I like the face plate - looks great.

Thanks for the compliment Clinton. You see why I want to replace the circuit with an equivalent that needs no panel changes :wink:

I’ve made good progress in the week or so since my post and show my latest heavily commented code below. It was a struggle and I’d much appreciate any improvements. Getting Pause/Play to toggle from a low on pausePin took an especially long time.

As a general point, many of the sketches I’ve studied seem to add an extra condition to the button operations, testing for states like ‘busy’ or ‘available’ or ‘music finished’, etc. Mine seem to work without, just testing for a low from the button. But I suspect that’s why I’ve had to do so much trial/error to get the ‘right’ delay. So can anyone suggest an example please? Say to replace my Next code. Ideally I’d prefer to do it with a library function, as I get the impression that the module’s BUSY pin is unreliable? But as I said, I’m no programmer so …

// Monday 21 March 2022, 1022, latest notes.
// In both normal & random modes,  Pause finally seems to work,
//  after careful choice of delay.
// Normal mode now starts with random track then automatically
// plays successive tracks as required.
// Jumping to a group (e.g Mozart) OK, then use Next, Previous, etc.
// Random mode OK; but Next buttont gets random track. Not yet
//  decided which option I will choose:
// - always random (desirable for casual listening)
// - always sequential (allowing user to locate a specific track
//    from a full list).
// - either, if I can work out how to do it without new button

// At present, 200 or so test MP3s on SD, mp3\0001.mp3, 0002.mp3, etc,
// but intend two or three thousand (the reason for changing from
// the USB module to SD).

// Uses DFR original library
#include "Arduino.h"
#include "SoftwareSerial.h"
#include "DFRobotDFPlayerMini.h"

// Button LOW is ACTIVE
const byte unusedPin2 = 2; // CURRENTLY UNUSED
const byte pausePin = 3; // Toggles Pause/Play
const byte egcsPin = 4; // EGCS first track 0086
const byte operaPin = 5 ; // OPERA first track 013
const byte previousPin = 6; // Previous
const byte nextPin = 7; // Next
const byte volupPin = 8; //Vol up
const byte voldownPin = 9; // Vol down
// 10 = Rx
// 11 = Tx
const byte mozartPin = 12 ; //C = MOZART first track 0159

const int btnGrpDelay = 500;

// A0 = random seed)
const byte otherPin =  A1; // OTHER first track 1
const byte jumpTenFwdPin = A2; // Jump 10 forward
const byte jumpTenBackPin = A3; // Jump 10 back
const byte modePin = A4; // SWITCH, not button, HIGH = Normal, LOW (sw to right) = Random
const byte unusedPinA5 = A5; // CURRENTLY UNUSED

int paused = false;

SoftwareSerial mySoftwareSerial(10, 11); // RX, TX
DFRobotDFPlayerMini myDFPlayer;

//printDetail(uint8_t type, int value); // Function at end of sketch
//void printDetail(uint8_t type, int value); // Function at end of sketch

bool isPlaying = false;
/////////////////////////////////////////////////

void setup()
{
  pinMode(unusedPin2, INPUT_PULLUP); //    (UNUSED)
  pinMode(pausePin, INPUT_PULLUP); // D3
  pinMode(egcsPin, INPUT_PULLUP); // D4
  pinMode(operaPin, INPUT_PULLUP); // D5
  pinMode(previousPin, INPUT_PULLUP); // D6
  pinMode(nextPin, INPUT_PULLUP); // D7
  pinMode(volupPin, INPUT_PULLUP); // D8
  pinMode(voldownPin, INPUT_PULLUP); // D9
  pinMode(mozartPin, INPUT_PULLUP); // D12

  pinMode(otherPin, INPUT_PULLUP); // A1
  pinMode(jumpTenFwdPin, INPUT_PULLUP); // A2
  pinMode(jumpTenBackPin, INPUT_PULLUP); // A3
  pinMode(modePin, INPUT_PULLUP); // A4
  pinMode(unusedPinA5, INPUT_PULLUP); // A5 (UNUSED)

  mySoftwareSerial.begin(9600);
  Serial.begin(9600);
  // Print sketch name here
  Serial.println(); // Blank line
  Serial.println("MB-OrigLibrary-5");

  Serial.println(F("Initializing DFPlayer ... (May take 3~5 seconds)"));

  //Use SoftwareSerial to communicate with mp3
  //  if (!myDFPlayer.begin(mySoftwareSerial))
  if (!myDFPlayer.begin(mySoftwareSerial, true, false)) // Fixed!
    // (But not sure of any side effects?)
  {
    Serial.println(F("Unable to begin:"));
    Serial.println(F("1.Please recheck the connection!"));
    Serial.println(F("2.Please insert the SD card!"));
    while (true);
  }

  // Random number, using random seed
  // Chooses a random track number from 0 to 210
  // Seed for random number, using noise via unconnected A0
  // This seed option ensures a different set of random tracks after every
  // power up. Delete or comment out if a CONSISTENT (but boring!) set wanted.
  randomSeed(analogRead(0));
  int randNumber = random(1, 211);
  Serial.print("IN SETUP, randNumber = ");
  Serial.println(randNumber);
  delay(2000); // Was 2000

  Serial.println(F("DFPlayer Mini online."));

  myDFPlayer.setTimeOut(1500); //Set serial communication time out; was 500

  // Set volume
  myDFPlayer.volume(5);  // 5 for headphones, 15/20 for speaker
  //////////////////////////////////////////////////////////////////

  // TEST SWITCH ON A4 TO DETERMINE MODE (H = Normal, L = Random)
  if (digitalRead(modePin) == HIGH) // Set normal (successive) mode
  {
    myDFPlayer.enableLoopAll(); //loop all mp3 files
  }
  else
  {
    myDFPlayer.randomAll(); //Random play all the mp3.
  }
  delay(3000);

  Serial.println("In SETUP");
  Serial.println(myDFPlayer.readCurrentFileNumber()); //read current play file number

}

/////////////////////////////////////////////////////////////

void loop()
{
  // Print currently playing track
  int track = myDFPlayer.readCurrentFileNumber();
  Serial.print("Current track = ");
  Serial.println(track);
  delay(5);

  //  Serial.print("paused =  ");
  //  Serial.println(paused);
  //  delay(5);

  // VOLUME
  if (digitalRead(volupPin) == LOW) myDFPlayer.volumeUp();
  delay(50);
  if (digitalRead(voldownPin) == LOW) myDFPlayer.volumeDown();
  delay(50);
  //---------------------------------------------------
  // NEXT
  if (digitalRead(nextPin) == LOW) // D7
  {
    myDFPlayer.next();
    delay(500); // was 10 but sometimes jumped TWO
  }
  //---------------------------------------------------
  // PREVIOUS
  if (digitalRead(previousPin) == LOW)// D6
  {
    myDFPlayer.previous();
    delay(500);  // was 10 but sometimes jumped TWO
  }
  //---------------------------------------------------
  // PAUSE
  // ********** Several days to get this working *****************

  if ((digitalRead(pausePin) == LOW) &&  (paused == false))
  {
    myDFPlayer.pause();
    delay(500); // Seems fairly critical
    paused = true;
  }

  if ((digitalRead(pausePin) == LOW) &&  (paused == true))
  {
    myDFPlayer.start();
    delay(500); // Seems fairly critical
    paused = false;
  }

  //---------------------------------------------------
  // EGCS
  if (digitalRead(egcsPin) == LOW) // D4
  {
    myDFPlayer.play(86);
    delay(btnGrpDelay);
  }
  //---------------------------------------------------
  // OPERA
  if (digitalRead(operaPin) == LOW) // D5
  {
    myDFPlayer.play(113);
    delay(btnGrpDelay);
  }
  //---------------------------------------------------
  // MOZART
  if (digitalRead(mozartPin) == LOW) // D12
  {
    myDFPlayer.play(159);
    delay(btnGrpDelay);
  }
  //---------------------------------------------------
  // OTHER
  if (digitalRead(otherPin) == LOW) // A1
  {
    myDFPlayer.play(1);
    delay(btnGrpDelay);
  }
  //---------------------------------------------------
  // JUMP 10 TRACKS FORWARD
  if (digitalRead(jumpTenFwdPin) == LOW) // A2
    // Get current track, add 10, play that track
  {
    int currentTrack = myDFPlayer.readCurrentFileNumber();
    currentTrack = currentTrack + 10;
    myDFPlayer.play(currentTrack);
    delay(10);
  }
  //---------------------------------------------------
  // JUMP 10 TRACKS BACKWARD
  if (digitalRead(jumpTenBackPin) == LOW) // A3
    // Get current track, subtract 10, play that track
  {
    int currentTrack = myDFPlayer.readCurrentFileNumber();
    currentTrack = currentTrack - 10;
    myDFPlayer.play(currentTrack);
    delay(10);
  }
  //---------------------------------------------------

} // End of loop


///////////////////////////////////////////////////////////////

// DFRobot function, testing for errors
void printDetail(uint8_t type, int value)
{
  switch (type)
  {
    case TimeOut:
      Serial.println(F("Time Out!"));
      break;
    case WrongStack:
      Serial.println(F("Stack Wrong!"));
      break;
    case DFPlayerCardInserted:
      Serial.println(F("Card Inserted!"));
      break;
    case DFPlayerCardRemoved:
      Serial.println(F("Card Removed!"));
      break;
    case DFPlayerCardOnline:
      Serial.println(F("Card Online!"));
      break;
    case DFPlayerPlayFinished:
      Serial.print(F("Number:"));
      Serial.print(value);
      Serial.println(F(" Play Finished!"));
      break;
    case DFPlayerError:
      Serial.print(F("DFPlayerError:"));
      switch (value)
      {
        case Busy:
          Serial.println(F("Card not found"));
          break;
        case Sleeping:
          Serial.println(F("Sleeping"));
          break;
        case SerialWrongStack:
          Serial.println(F("Get Wrong Stack"));
          break;
        case CheckSumNotMatch:
          Serial.println(F("Check Sum Not Match"));
          break;
        case FileIndexOut:
          Serial.println(F("File Index Out of Bound"));
          break;
        case FileMismatch:
          Serial.println(F("Cannot Find File"));
          break;
        case Advertise:
          Serial.println(F("In Advertise"));
          break;
        default:
          break;
      }
      break;
    default:
      break;
  }
}

Terry

Nearly a month since I posted my cry for help, so disappointed I’ve not had any!
But hope springs eternal, so…
Answers to the two following very specific questions would get me over my immediate mental blocks please:

  1. Re Anacapala’s post #29 , he suggested sending a command. How exactly would I code that? IOW, which command from the DFRobotDFPlayerMini library would I use? The only one I can find that uses the string ‘command’ is:
    uint8_t readCommand();
    I was hoping to find something like player.SendCommand() in which the parameter would be that string of hex characters in his example.

  2. I’m using folders SD\01\001.mp3, SD\01\002.mp3, etc and can play a specific folder with
    myDFPlayer.playFolder(5,2);

But is it not possible while debugging to print that in the monitor? I see no command like myDFPlayer.readFolder(5,2);

It looks like you are trying to maintain a lot of state within your main loop, you might find this article helpful. – RepRage – Anatomy of an Arduino Sketch – I find the strategy simplifies things a lot.

Anyone with practical, detailed experience of the module able to help with my two very specific questions?

I’ll study that interesting general article in due course. But meanwhile I certainly don’t intend to abandon my many weeks of effort to start from cold!

hi there, hopefully someone can help me.
i’m working on a project where i read a color with the sensor and thenwrite it on an rgb ledstrip. now i also need to ad a sound reel commenting on the collor, the difficult part is that it has to be more then 1 comment each time the same collor is presented.
so i was thinking of making folders for each comment en then program it so it play’s a random track in that folder… but i have no idea how to start with it? i really hope someone can help me. below you wil find the scipt i use for reading and writing the collors… that is working fine!

#define S0 2
#define S1 9
#define S2 4
#define S3 10
#define sensorOut 11
#define redLED 3
#define greenLED 5
#define blueLED 6
int redFrequency = 0;
int greenFrequency = 0;
int blueFrequency = 0;
int redColor = 0;
int greenColor = 0;
int blueColor = 0;
int redMin;
int redMax;
int greenMin;
int greenMax;
int blueMin;
int blueMax;
int color = 0;

void setup()
{
pinMode(S0, OUTPUT);
pinMode(S1, OUTPUT);
pinMode(S2, OUTPUT);
pinMode(S3, OUTPUT);
pinMode(redLED, OUTPUT);
pinMode(greenLED, OUTPUT);
pinMode(blueLED, OUTPUT);
pinMode(13, OUTPUT);
pinMode(sensorOut, INPUT);
digitalWrite(S0, HIGH);
digitalWrite(S1, LOW);
Serial.begin(9600);
calibrate();
}

void loop()
{
readColor();
decideColor();
printColor();
}

void decideColor()
{

redColor = constrain(redColor, 0, 255);
greenColor = constrain(greenColor, 0, 255);
blueColor = constrain(blueColor, 0, 255);
int maxVal = max(redColor, blueColor);
maxVal = max(maxVal, greenColor);
redColor = map(redColor, 0, maxVal, 0, 255);
greenColor = map(greenColor, 0, maxVal, 0, 255);

blueColor = map(blueColor, 0, maxVal, 0, 255);
redColor = constrain(redColor, 0, 255);
greenColor = constrain(greenColor, 0, 255);
blueColor = constrain(blueColor, 0, 255);

if (gWrite(greenLED, 250);
analogWrite(blueLED, 250);
}
else if (redColor < 25 && greenColor < 25 && blueColor < 25)
{
color = 2;
analoredColor > 250 && greenColor > 250 && blueColor > 250)
{
color = 1;

analogWrite(redLED, 250);
analogWrite(greenLED, 250);
analogWrite(blueLED, 250);
}
else if (redColor > 200 && greenColor > 200 && blueColor < 100)
{
color = 4;
analogWrite(redLED, 240);
analogWrite(greenLED, 150);
analogWrite(blueLED, 130);
}
else if (redColor > 200 && greenColor > 60 /&& blueColor < 100/)
{
color = 3;
analogWrite(redLED, 255);
analogWrite(greenLED, 100);
analogWrite(blueLED, 0);
}
else if (redColor > 200 && greenColor < 100 && blueColor > 200)
{
color = 5;
analogWrite(redLED, 250);
analogWrite(greenLED, 200);
analogWrite(blueLED, 200);
}
else if (redColor > 250 && greenColor < 200 && blueColor < 200)
{
color = 6;
analogWrite(redLED, 255);
analogWrite(greenLED, 0);
analogWrite(blueLED, 0);
}
else if (redColor < 200 && greenColor > 250 && blueColor < 200)
{
color = 7;
analogWrite(redLED, 0);
analogWrite(greenLED, 255);
analogWrite(blueLED, 0);
}
else if (redColor < 200 /&& greenColor < 200/ && blueColor > 250)
{
color = 8;
analogWrite(redLED, 0);
analogWrite(greenLED, 0);
analogWrite(blueLED, 255);
}
else
{
color = 0;
}
}

void calibrate()
{
Serial.println(“Calibrating…”);
Serial.println(“White”);
digitalWrite(13, HIGH);
delay(2000);
digitalWrite(S2, LOW);
digitalWrite(S3, LOW);
redMin = pulseIn(sensorOut, LOW);
delay(100);
digitalWrite(S2, HIGH);
digitalWrite(S3, HIGH);
greenMin = pulseIn(sensorOut, LOW);
delay(100);
digitalWrite(S2, LOW);
digitalWrite(S3, HIGH);
blueMin = pulseIn(sensorOut, LOW);
delay(100);
Serial.println(“next…”);
digitalWrite(13, LOW);
delay(2000);
Serial.println(“Black”);
digitalWrite(13, HIGH);
delay(2000);
digitalWrite(S2, LOW);
digitalWrite(S3, LOW);
redMax = pulseIn(sensorOut, LOW);
delay(100);
digitalWrite(S2, HIGH);
digitalWrite(S3, HIGH);
greenMax = pulseIn(sensorOut, LOW);
delay(100);
digitalWrite(S2, LOW);
digitalWrite(S3, HIGH);
blueMax = pulseIn(sensorOut, LOW);
delay(100);
Serial.println(“Done calibrating.”);
digitalWrite(13, LOW);
}

void printColor()
{
Serial.print("R = “);
Serial.print(redColor);
Serial.print(” G = “);
Serial.print(greenColor);
Serial.print(” B = “);
Serial.print(blueColor);
Serial.print(” Color: ");
switch (color) {
case 1: Serial.println(“WHITE”); break;

case 2: Serial.println(“BLACK”); break;

case 3: Serial.println(“ORANGE”); break;

case 4: Serial.println(“YELLOW”); break;

case 5: Serial.println(“PURPLE”); break;

case 6: Serial.println(“RED”); break;

case 7: Serial.println(“GREEN”); break;

case 8: Serial.println(“BLUE”); break;

default: Serial.println(“unknown”); break;
}
}

void readColor()
{
digitalWrite(S2, LOW);
digitalWrite(S3, LOW);
redFrequency = pulseIn(sensorOut, LOW);
redColor = map(redFrequency, redMin, redMax, 255, 0);
delay(100);
digitalWrite(S2, HIGH);
digitalWrite(S3, HIGH);
greenFrequency = pulseIn(sensorOut, LOW);
greenColor = map(greenFrequency, greenMin, greenMax, 255, 0);
delay(100);
digitalWrite(S2, LOW);
digitalWrite(S3, HIGH);
blueFrequency = pulseIn(sensorOut, LOW);
blueColor = map(blueFrequency, blueMin, blueMax, 255, 0);
delay(100);

I gave up trying to use the library. It is too restrictive, and doesn’t actually do anything that is not easy to do in the main loop.

For Q2, there is no such facility. You have to keep track of the most recent folder and file selection within the code.

I have extensively rewritten the original documentation to remove all the errors I found and to make things much clearer. I have created sample code for the two modes of operation (Named File and Single Folder) that should answer Q1 Please see:
http://www.acamar.com.au/MCUProjects/project_mini_mp3_player.html

Thanks, your document looks as if it will prove helpful. I will study it asap. Meanwhile I did make big progress yesterday by resolving another major obstacle. Namely, how to get automatic (normal or random) play resumed after any button action that plays a new track. Pressing ahead with preparing the 4,000 or so files in 12 folders, even though I’m still unable to get a print identifying a playing track. More later.

Anacapala,

Much of your long sketch is over my head I’m afraid. I’m still trying to iron out the final issues using my own code. As this seems a very quiet ‘forum’, before I again provide details can I check if you or anyone who has worked with the DFPlayerMini library are around please?

As I mentioned, I have worked with the library and found it unusable. The library code assumes an instant response to commands, doesn’t properly wait for a response, treats most responses as an error, doesn’t queue commands and doesn’t properly handle those commands that expect a response. It is developed from the original documentation which contains too many errors to be a reliable source for creating the library. I suspect that the device that it was originally developed for worked very differently (much faster, perhaps) than devices available today.

Also note that the device was not originally designed as an MP3 Player - the likely use is for a message annunciator such as the console operator at a railway station or radio station might have - select a number, push a button and play a very short recorded message. The library assumes that type of usage.

The first part of my code should be easily incorporated into a simple demonstration sketch. It is very important, because if the device is not properly initialized then nothing will work. It should be possible to strip out everything other than the code required to initialize the player and start the first track. (Remove the part from setup that finds the number of TF files - that is complex, and not needed for a demonstration). Then you can add commands such as changing the volume or the EQ using cmdExec(). This requires getResponse() and sendCommand(). If you are not handling user input then almost all of the rest of the code is not required - at that point your demonstration program will follow a hard-coded command sequence (initialize, start play, volume up, for example).

The main loop can be stripped back to checking the user input and checking for a response. For a demonstration, there is no need to handle the response other than displaying it on the console.

Then you can add code to process the user input, validate it, and issue a command. You can see from that code that I started with a simple command line input with a two-letter command/parameter format.

The only part of the code that might be a bit hard to follow is the overloading of cmdExec() and qryExec(). This is a very opaque C construction that allows these functions to be called for both types of commands and queries - those that require a parameter and those that don’t - using the same function name. The same construction is used for sendCommand() - no parameters, byte, or integer.