Atari 800 MP3 Player

by Bobbs1971 in Circuits > Arduino

518 Views, 2 Favorites, 0 Comments

Atari 800 MP3 Player

DSC_0132.JPG

Overview

This Instructable examines the Atari's SIO protocol with a view to making it accessible for a newbie.


Introduction

This is a bit of a cheat. Everyone's favourite CPU just hasn't got the cajones to decode an MP3 file fast enough to make a tune. At least, not without some external help - step forward the Arduino Mega and Adafruit MP3 shield.

This paper draws heavily on the work done by Whizzosoftware, which links the SIO bus to an SD card

http://www.whizzosoftware.com/sio2arduino/index.ht...

and Adfruit's Simple MP3 player file

https://learn.adafruit.com/adafruit-music-maker-sh...

Readers might also like to look at the Altirra hardware manual, specifically chapter 9 for some background reading on the SIO bus

http://www.virtualdub.org/downloads/Altirra%20Hard...

I'm assuming that readers know that the MP3 shield needs an SD card and know how to transfer MP3 files to it. If not, take a look at the Adafruit link above.

This link explains what some of the Atari's command codes mean.

https://www.atarimax.com/jindroush.atari.org/asio....

Supplies

Arduino Mega

Adafruit MP3 shield and SD card

Atari 800XL (any 8 bit Atari with an SIO port should work)

Wires. I'm using dupont connectors

Some means of loading DOS. I'm using an SIO2SD unit with Atari DOS 2.5

Connect the Atari, Arduino and the MP3 Shield

sio bus.png
DSC_0131.JPG

1) The MP3 shield slots straight on top of the Arduino Mega.

2) I'm using Dupont connectors to link the Atari to the Arduino. The SIO pins have a larger diameter and the connectors need to be modified by removing the plastic case.

BE CARFULL NOT TO SHORT THE PINS.

BE CAREFULL NOT TO PRESS TOO HARD ON THE SIO PINS

The connections are;

Atari --------------------> Arduino

3 - Data in ------------> 14 - Serial 3 TX

5 - Data out ----------> 15 - Serial 3 RX

7 - Command --------> 2

4 - Ground ------------> GND

3) I've also attached an SIO2SD module to enable Atari DOS, but I found the Arduino interfered with the signal. To get around this it was necessary to disconnect the Serial RX and TX pins on the Arduino when booting the Atari. Once this is done, the Arduino can be re-attached.

Progranming the Atari

DSC_0135.JPG

To get a directory listing, run the following Basic program.

10 Dim a$(16)
20 Trap 100
30 Close #1
40 Open #1,6,0,"D2:*.*"
50 Input #1, A$
60 Print A$
70 Goto 50
100 End

To play a file called 'ALL.MP3', enter

LPRINT "ALL.MP3"

Program the Arduino - Part 1

Here's the Arduino code

1) Global variables, included libraries, constants, complier definitions and pre-defined routines

// include SPI, MP3 and SD libraries
#include <SPI.h>
#include <Adafruit_VS1053.h>
#include <SD.h>

// Replies to be sent to the Atari
const byte ACK      = 0x41;
const byte NAK      = 0x4E;
const byte COMPLETE = 0x43;
const byte ERR      = 0x45;

// Sector size is 128 bytes
const unsigned long MAX_SECTOR_SIZE = 128;

// Commands received from the Atari
const byte CMD_FORMAT           = 0x21;
const byte CMD_FORMAT_MD        = 0x22;
const byte CMD_POLL             = 0x3F;
const byte CMD_PUT              = 0x50;
const byte CMD_READ             = 0x52;
const byte CMD_STATUS           = 0x53;
const byte CMD_WRITE            = 0x57;

// Other
const unsigned long READ_CMD_TIMEOUT     = 500;
const unsigned long READ_FRAME_TIMEOUT   = 2000;
const byte DELAY_T2 = 1;
const byte DELAY_T3 = 2;
const byte DELAY_T4 = 1;
const byte DELAY_T5 = 1;

// Whizzosoftware allows for up to 8 drives and an 850 module.
// Modified to include a printer
const byte DEVICE_D1            = 0x31;
const byte DEVICE_D2            = 0x32;
const byte DEVICE_D3            = 0x33;
const byte DEVICE_D4            = 0x34;
const byte DEVICE_D5            = 0x35;
const byte DEVICE_D6            = 0x36;
const byte DEVICE_D7            = 0x37;
const byte DEVICE_D8            = 0x38;
const byte PRINTER              = 0x40;
const byte DEVICE_R1            = 0x50;


#define PIN_ATARI_CMD         2   // the Atari SIO command line 
#define SIO_UART     Serial3
#define SIO_CALLBACK serialEvent3


// These are the pins used for the music maker shield
#define SHIELD_RESET  -1      // VS1053 reset pin (unused!)
#define SHIELD_CS     7      // VS1053 chip select pin (output)
#define SHIELD_DCS    6      // VS1053 Data/command select pin (output)

// These are common pins between breakout and shield
#define CARDCS 4     // Card chip select pin

// DREQ should be an Int pin, see 
#define DREQ 3       // VS1053 Data request, ideally an Interrupt pin

Adafruit_VS1053_FilePlayer musicPlayer =  Adafruit_VS1053_FilePlayer(SHIELD_RESET, SHIELD_CS, SHIELD_DCS, DREQ, CARDCS);
  
// variable used to produce the directory listing
String filelist;
byte dirlist;
byte* pdirlist;
String playfile;

//Command frame structure
const byte COMMAND_FRAME_SIZE   = 5;
struct CommandFrame {
  byte deviceId;
  byte command;
  byte aux1;
  byte aux2;
  byte checksum;
};


// variables used in serial communication
int m_cmdPinState;
int m_cmdPin;
Stream* m_stream;
unsigned long     m_startTimeoutInterval;
CommandFrame      m_cmdFrame;
byte*             m_cmdFramePtr;
byte              m_sectorBuffer[MAX_SECTOR_SIZE + 1];
byte*             m_putSectorBufferPtr;
int               m_putBytesRemaining;

void resetCommandFrameBuffer();
void dumpCommandFrame();
byte processCommand();
byte checksum(byte* chunk, int length);
void cmdPutSector(int deviceId);
void doPutSector();
boolean isChecksumValid();
boolean isCommandForThisDevice();
boolean isValidCommand();
boolean isValidDevice(byte b);
boolean isValidAuxData();
void cmdGetStatus();
void printDirectory(File dir);
void cmdGetSector(int deviceId);

Program the Arduino - Part 2

2) Void setup.

The Atari uses 19200 baud, and this is replicated for Serial0. Serial0 allows us to see what's going on by using the serial monitor in the IDE.

We also set up the MP3 shield here, and read the directory on the SD card.

void setup() {

  // initialize serial port to Atari
  SIO_UART.begin(19200);
  Serial.begin(19200); // does not like different baud rates
  Serial.println("Go!");

  pinMode(PIN_ATARI_CMD, INPUT);
  m_cmdPinState = 1; // initial state
  m_cmdPin = PIN_ATARI_CMD;

  Serial.println("Adafruit VS1053 Simple Test");

  if (! musicPlayer.begin()) { // initialise the music player
     Serial.println(F("Couldn't find VS1053, do you have the right pins defined?"));
     while (1);
  }
  Serial.println(F("VS1053 found"));
  
   if (!SD.begin(CARDCS)) {
    Serial.println(F("SD failed, or not present"));
    while (1);  // don't do anything more
  }

  // list files
  printDirectory(SD.open("/"));
  
  // Set volume for left, right channels. lower numbers == louder volume!
  musicPlayer.setVolume(20,20);


  // If DREQ is on an interrupt pin (on uno, #2 or #3) we can do background
  // audio playing
  musicPlayer.useInterrupt(VS1053_FILEPLAYER_PIN_INT);  // DREQ int
  
}

Program the Arduino - Part 3

time.png

3) Void loop()

The loop follows the process of receiving data from the Atari. The exert from the Altirra hardware manual shows the process as a timeline. In the initial state, the command line is set to high by the Atari.

The first action is for the command line to be set low. This alerts the peripherals by saying "Heads up guys, I've got something to say". The first piece of data to be sent is the command frame which is 5 bytes long.

The loop moves to stage 2 and resets the command frame buffer. Reading the data is done by the SIO_CALLBACK() routine - see next page.

Stage 3 waits for 5 bytes to be read, then parses the command frame. We're looking to see if command frame byte 1 is for this device (a printer or drive 2 in this case). It also checks if command frame byte 2 is a valid command. Even though there's a routine, the aux bytes aren't checked.

Note for anyone unfamiliar with pointers and the & operator.

(m_cmdFramePtr - (byte*)&m_cmdFrame == COMMAND_FRAME_SIZE)

This horrible looking thing isn't too bad once you understand it.

i) m_cmdFramePtr. this is the current memory location that was filled by the SIO_CALLBACK routine. It gets updated in SIO_CALLBACK.

ii) (byte*)&m_cmdFrame. &m_cmdFrame means get the memory address of m_cmdFrame

When m_cmdFramePtr is 5 bytes greater the &m_cmdFrame it means we've read 5 bytes.

Finally we ensure the checksum adds up correctly.

Stage 4. If stage 3 checks out okay, we start to read the data frame, if there is one. Again, SIO_CALLBACK takes care of this.

Stage 5. When stage 4 is complete (see later) we move to stage 5 and wait for the next batch.

void loop() {

  switch (m_cmdPinState) {
    case 1:
      // initial state
      if (digitalRead(m_cmdPin) == HIGH)
      {
        m_cmdPinState = 2;
      }
      break;
    case 2:
      // waiting for start
      if (digitalRead(m_cmdPin) == LOW) {
        m_cmdPinState = 3;
        resetCommandFrameBuffer();
        Serial.println("Cmd pin low");
      }
      break;
    case 3:
      // Read command
      m_startTimeoutInterval = millis();
      // if command frame is fully read...
      if (m_cmdFramePtr - (byte*)&m_cmdFrame == COMMAND_FRAME_SIZE)
      {
        Serial.println("Frame Read");
        dumpCommandFrame();
        // process command frame
        if (isChecksumValid() && isCommandForThisDevice())
        {
          Serial.println("For this device and checksum okay");
          if (isValidCommand() && isValidAuxData())
          {
            m_cmdPinState = processCommand();
            Serial.print("New cmd status ");
            Serial.println(m_cmdPinState);
          }
          else
          {
            m_cmdPinState = 2; //wait for start
            Serial.println("Invalid Command");
          }
        }
        else
        {
          m_cmdPinState = 2; //wait for start
          Serial.println("Not this device or checksum error");
        }
        // otherwise, check for command read timeout
      }
      else if (millis() - m_startTimeoutInterval > READ_CMD_TIMEOUT)
      {
        m_cmdPinState = 2; //wait for start
        Serial.println("Time out");
      }
      break;
    case 4:
      // Read data frame
      // Serial.println("Read data frame");
      // check for timeout
      if (millis() - m_startTimeoutInterval > READ_FRAME_TIMEOUT) {
        m_cmdPinState = 2; //wait for start
      }
      break;
    case 5:
      // wait for end

      if (digitalRead(m_cmdPin) == HIGH)
      {
        m_cmdPinState = 2; // wait for start
        Serial.println("Wait for end done");
      }
      break;
  }
}

Programming the Arduino - Part 4

Part 4 - SIO_CALLBACK. This is a like an interrupt which is triggered when a byte is received at serial port 3. See https://www.arduino.cc/en/Tutorial/BuiltInExamples...

The received byte is store in 'b', and depending on the command pin state, we either read a command frame or a data frame. There are a couple of work arounds to get it running.

In case 3 we can see the command frame pointer being updated as mentioned in part 3, and there are some more nasty pointer things;

*m_cmdFramePtr = b; This line means put the value of b (the byte received) into the memory location pointed to by m_cmdFramePtr. In Atari basic its like POKE m_cmdFramePtr, b.

m_cmdFramePtr++; This moves the pointer on one memory location. Eventually we'll get the 5 bytes which make up the command frame buffer.

Case 4. If we're reading a data frame then we're writing the byte received to the 'sectorBuffer'.

*m_putSectorBufferPtr = b; As above, think of this as POKE m_putSectorBufferPtr, b.

m_putSectorBufferPtr++; This moves the pointer in one memory location. We keep going until we get 128 bytes, at which point we'll do something with them.

void SIO_CALLBACK()
{
  // read the next byte from the bus
  byte b = SIO_UART.read();


  switch (m_cmdPinState) {
    // if we read a valid device byte and are in a "command wait" state, come out of it and
    // process the byte
    case 1: //initial state
    case 2: // wait for command
    case 5: // wait for end
      if (digitalRead(m_cmdPin) == LOW && isValidDevice(b)) {
        m_cmdPinState = 3; // Read
        resetCommandFrameBuffer();
      }
      else
      {
        break;
      }
    // if we're reading a command frame...
    case 3: //read
      {
        // read the data into the command frame
        // sometimes we see extra bytes between command frames on the bus while reading a command and things get lost --
        // the isValidDevice() check prevents a command frame read from getting corrupted by them

        int idx = (int)m_cmdFramePtr - (int)&m_cmdFrame;
        if (idx < COMMAND_FRAME_SIZE && (idx > 0 || (idx == 0 && isValidDevice(b)))) {
          *m_cmdFramePtr = b;
          m_cmdFramePtr++;
          return;
        }
        break;
      }
    // if we're reading a data frame...
    case 4:
      {
           // add byte to read sector buffer
          *m_putSectorBufferPtr = b;
          m_putSectorBufferPtr++;
          m_putBytesRemaining--;

          if (m_putBytesRemaining == 0 || b == 0x9b)
          {
            doPutSector();
          }
        break;
      }
    default:

      break;
  }
}

Programming the Arduino - Part 5

cmm.png

Let's take a closer look at the command frame validations in void loop().

Device ID. We want a yes or no that the first byte of the command frame relates to our device. For our purposes, this is a printer or disk 2.

boolean isCommandForThisDevice() {

boolean result = (m_cmdFrame.deviceId == PRINTER ||
                  m_cmdFrame.deviceId == DEVICE_D2);

  return result;
}

Valid Command. Again, a yes or no that the second byte is a valid command. I've left this largely unchanged from Whizzosoftware, but the ones we're interested in are CMD_STATUS, CMD_WRITE and CMD_READ.

boolean isValidCommand() {
  boolean result = (m_cmdFrame.command == CMD_READ ||
                    m_cmdFrame.command == CMD_WRITE ||
                    m_cmdFrame.command == CMD_STATUS ||
                    m_cmdFrame.command == CMD_PUT ||
                    m_cmdFrame.command == CMD_FORMAT ||
                    m_cmdFrame.command == CMD_FORMAT_MD);

  if (!result) {
    result = 0;
  }

  return result;
}

Aux 1 and 2. We don't bother checking these.

boolean isValidAuxData() {
  return true;
}

Checksum. Add the bytes received by running through the command or data buffers one byte at a time and if the result >256, deduct 256 and add one.

boolean isChecksumValid() {
  byte chkSum = checksum((byte*)&m_cmdFrame, 4);
  if (chkSum != m_cmdFrame.checksum)
  {
    Serial.println("Checksum fail");
    return false;
  }
  else
  {
    Serial.println("Checksum pass");
    return true;
  }
}


byte checksum(byte* chunk, int length) {
  int chkSum = 0;
  for (int i = 0; i < length; i++) 
  {
    chkSum = chkSum + chunk[i];
     if (chkSum >= 256)
      {
        chkSum = (chkSum -256) +1;
      }  

  }
  return (byte)chkSum;
}

Programming the Arduino - Part 6

Okay, so we have our command frame, what happens next? Well, we'd better take some action which is dependent on byte 2.

byte processCommand() {
  int deviceId = 1;
  byte nextCmdPinState = 5; // wait for end

  switch (m_cmdFrame.command) {
    case CMD_READ:
      cmdGetSector(deviceId);
      break;
    case CMD_PUT:
    case CMD_WRITE:
      cmdPutSector(deviceId);
      nextCmdPinState = 4; // read data frame
      m_startTimeoutInterval = millis();
      break;
    case CMD_STATUS:
      cmdGetStatus(deviceId);
      break;
    case CMD_FORMAT:
      break;
    case CMD_FORMAT_MD:
      break;
    default:
      break;
  }

  return nextCmdPinState;
}<br>

If we were to ask the MP3 player to play a track called 'ALL.MP3', we would type the following line into the Atari

LPRINT "ALL.MP3"

The first thing the Atari sends is a get status command which asks the printer to tell the Atari about itself. In the processCommand routine, cmdGetStatus is called.

We need to reply to the Atari or it will keep sending the command frame and eventually timeout. The reply is a fairly simple; an 'A' to acknowledge we've received the command frame, then a 'C'. Finally we send 4 bytes to tell the Atari our status and a checksum.

What we'll send is two zeros, a timeout value for the device (measured in seconds) and another zero, which are held in byte array 'b'.

void cmdGetStatus(int deviceId)
{
  byte b[4] = {0, 0, 2, 0};
  byte chksum;

  chksum = checksum((byte*)b, 4);
  // send ACK
  delay(DELAY_T2);
  SIO_UART.write(ACK);
  Serial.println("ACK");

  // send complete
  delay(DELAY_T5);
  SIO_UART.write(COMPLETE);
  Serial.println("Get status COMPLETE");

  // send status to bus
  for (int i = 0; i < 4; i++) {
    SIO_UART.write(b[i]);
  }
  SIO_UART.write(chksum);
}<br>

If the Atari's happy, it'll start sending the data, and we start using CMD_WRITE option, cmdPutSector.

In this routine we send another 'A', set the number of bytes to receive as 128 and pointer the sector buffer pointer to equal the memory address of the sector buffer

void cmdPutSector(int deviceId) {
  // send ACK
  delay(DELAY_T2);
  SIO_UART.write(ACK);
  Serial.println("Put sector ACK");

  m_putBytesRemaining = MAX_SECTOR_SIZE;
  m_putSectorBufferPtr = m_sectorBuffer;

}<br>

The SIO_CALLBACK routine keeps track of how many bytes have been received. If we get 128 bytes or character $9b is received, then that's the block of data sent and we need to do something. This is handled by the doPutSector routine.

Firstly we send another 'A' and 'C' to keep the Atari happy. I noticed that sometimes the 'C' wasn't processed by the Atari and use a work around to send it a number of times. The extra 'C's are ignored.

Next we find the file name 'ALL.MP3' in the data frame.

Another odd thing the Atari does sometimes is repeat the command frame. I've added another work around to ignore the first five bytes fi this happens.

Finally, we send an instruction to the MP3 shield to play the track,

void doPutSector()
{
  int sectorSize = m_putSectorBufferPtr - m_sectorBuffer - 1;
 int j;

  // clear playfile
      playfile = "";  
 
  // send ACK
  delay(2);

  SIO_UART.write(ACK);
  Serial.println("doPutSector ACK");

 for (j=0; j <2; j++)
 {
 delay(1);

  // send COMPLETE
  SIO_UART.write(COMPLETE);
 }
  Serial.println("doPutSector complete");

  // get filename from data frame
  for (int i = 0; i < sectorSize; i++)
  {
    playfile += (char)m_sectorBuffer[i];
  }

  // change command pin state
  m_cmdPinState = 2; //wait for start

  
// check for repeated frame buffer
    if (playfile[0] == m_cmdFrame.deviceId &&
        playfile[1] == m_cmdFrame.command) 
       {
       playfile = playfile.substring(5, playfile.length());
       }
          
  musicPlayer.startPlayingFile(playfile.c_str());
  


}

Programming the Arduino - Part 7

Supposing you wanted to know what tracks were on the SD card. In void setup() a routine called printDirectory is actioned. This is done in setup to avoid time outs later.

The printDirectory routine does that by firstly reading what's on the SD card then translating it into a format the Atari understands. The Adafruit bit is explained on their website so I'll cover the Atari bit.

Atari DOS 2.5 is looking for 16 bytes for each file name. The first five contain the file size and where it is located on the disk. For our purposes, these can all be zeros. Next is the filename up to the extender ".". This must be 8 bytes long, so if the file is 'ALL.MP3' then this becomes 'ALL' and five space characters. The final bit is the extender. The "." is ignored.

The final name looks like

00000ALL     MP3

So we can send this to the Atari, it's converted into a byte array.

The Atari is looking for a 128 byte long block of data. If our filelist isn't that long it should be padded out with zeros.

Some more guidance on pointers;

pdirlist = &dirlist;. This line puts the memory location of dirlist into pdirlist

*pdirlist = (byte)filelist[i];. Think of this as POKE dirlist, filelist character [i]

*pdirlist = 0;. Think of this as POKE dirlist, 0, which puts a space character into the byte array dirlist.

void printDirectory(File dir) 
{
 String tempName;  
 int i;
 int dot;
 String fileHeader = "00000";

   while(true) {
     
     File entry =  dir.openNextFile();
     if (! entry) 
  {
       // no more files
       //Serial.println("**nomorefiles**");
       break;
     }

     if (!entry.isDirectory()) 
  {


 // each entry needs to be in the format 5 digits 8 characters 3 ext (16 chrs long)
    filelist += fileHeader;
    tempName = entry.name();
    dot = tempName.indexOf(".");

    filelist += tempName.substring(0, dot);

     if (dot<8)
     {
      for (i=0; i<(8-dot); i++)
      {
        filelist += " ";
      }
     }

     filelist += tempName.substring(dot+1, tempName.length());

     } 
     entry.close();
   }

  
 // convert to byte array
  pdirlist = &dirlist; 
  for (i =0; i<129; i++)
  {
   if (filelist.length()>i)
    {
    *pdirlist = (byte)filelist[i];
    }
    else
    {
     *pdirlist = 0;
    }
    pdirlist ++;
   }
}

On the Atari, we type

OPEN #1,6,0,"D2:*.*"

Our Atari will send a command frame to read a sector (169 for the directory in DOS 2.5). When the command frame is processed, cmdGetSector is called.

As usual 'A' and 'C' are sent, followed by a 128 block of bytes containing the file list, and the checksum

void cmdGetSector(int deviceId) {
 
int i;

 // send ACK
  delay(2);
  SIO_UART.write(ACK);
  Serial.println("Get sector ACK");

  // write data frame + checksum
  // send complete
  delay(1);
  SIO_UART.write(COMPLETE);
  Serial.println("Get sector COMPLETE");
  SIO_UART.flush();

  
    for (i=0; i<128; i++)
  {
     SIO_UART.write((byte)filelist[i]);
  }


    // write checksum
   byte chksum = checksum(&dirlist, filelist.length());
   SIO_UART.write(chksum);

  SIO_UART.flush();
}<br>

Further Development

download.jpg

The Atari code could be tidied up to make a proper media player type program, or re-written into something else. The key technology here is the SIO process.

The Arduino Mega will also accept the list command, and there are numerous articles on the internet on linking a serial printer to the Arduino. Potentially it shouldn't be too much work to print from your Atari.

Appendix a - Full Arduino Code

Here's the full Arduino code. For some reason Instructables keeps adding
to the code. I think I've got rid of them all.

// include SPI, MP3 and SD libraries
#include <SPI.h>
#include <Adafruit_VS1053.h>
#include <SD.h>

// Replies to be sent to the Atari
const byte ACK      = 0x41;
const byte NAK      = 0x4E;
const byte COMPLETE = 0x43;
const byte ERR      = 0x45;

// Sector size is 128 bytes
const unsigned long MAX_SECTOR_SIZE = 128;

// Commands received from the Atari
const byte CMD_FORMAT           = 0x21;
const byte CMD_FORMAT_MD        = 0x22;
const byte CMD_POLL             = 0x3F;
const byte CMD_PUT              = 0x50;
const byte CMD_READ             = 0x52;
const byte CMD_STATUS           = 0x53;
const byte CMD_WRITE            = 0x57;

// Other
const unsigned long READ_CMD_TIMEOUT     = 500;
const unsigned long READ_FRAME_TIMEOUT   = 2000;
const byte DELAY_T2 = 1;
const byte DELAY_T3 = 2;
const byte DELAY_T4 = 1;
const byte DELAY_T5 = 1;

// Whizzosoftware allows for up to 8 drives and an 850 module.
// Modified to include a printer
const byte DEVICE_D1            = 0x31;
const byte DEVICE_D2            = 0x32;
const byte DEVICE_D3            = 0x33;
const byte DEVICE_D4            = 0x34;
const byte DEVICE_D5            = 0x35;
const byte DEVICE_D6            = 0x36;
const byte DEVICE_D7            = 0x37;
const byte DEVICE_D8            = 0x38;
const byte PRINTER              = 0x40;
const byte DEVICE_R1            = 0x50;


#define PIN_ATARI_CMD         2   // the Atari SIO command line 
#define SIO_UART     Serial3
#define SIO_CALLBACK serialEvent3


// These are the pins used for the music maker shield
#define SHIELD_RESET  -1      // VS1053 reset pin (unused!)
#define SHIELD_CS     7      // VS1053 chip select pin (output)
#define SHIELD_DCS    6      // VS1053 Data/command select pin (output)

// These are common pins between breakout and shield
#define CARDCS 4     // Card chip select pin

// DREQ should be an Int pin, see 
#define DREQ 3       // VS1053 Data request, ideally an Interrupt pin

Adafruit_VS1053_FilePlayer musicPlayer =  Adafruit_VS1053_FilePlayer(SHIELD_RESET, SHIELD_CS, SHIELD_DCS, DREQ, CARDCS);
  
// variable used to produce the directory listing
String filelist;
byte dirlist;
byte* pdirlist;
String playfile;

//Command frame structure
const byte COMMAND_FRAME_SIZE   = 5;
struct CommandFrame {
  byte deviceId;
  byte command;
  byte aux1;
  byte aux2;
  byte checksum;
};


// variables used in serial communication
int m_cmdPinState;
int m_cmdPin;
Stream* m_stream;
unsigned long     m_startTimeoutInterval;
CommandFrame      m_cmdFrame;
byte*             m_cmdFramePtr;
byte              m_sectorBuffer[MAX_SECTOR_SIZE + 1];
byte*             m_putSectorBufferPtr;
int               m_putBytesRemaining;

void resetCommandFrameBuffer();
void dumpCommandFrame();
byte processCommand();
byte checksum(byte* chunk, int length);
void cmdPutSector(int deviceId);
void doPutSector();
boolean isChecksumValid();
boolean isCommandForThisDevice();
boolean isValidCommand();
boolean isValidDevice(byte b);
boolean isValidAuxData();
void cmdGetStatus();
void printDirectory(File dir);
void cmdGetSector(int deviceId);


void setup() {

  // initialize serial port to Atari
  SIO_UART.begin(19200);
  Serial.begin(19200); // does not like different baud rates
  Serial.println("Go!");

  pinMode(PIN_ATARI_CMD, INPUT);
  m_cmdPinState = 1; // initial state
  m_cmdPin = PIN_ATARI_CMD;

  Serial.println("Adafruit VS1053 Simple Test");

  if (! musicPlayer.begin()) { // initialise the music player
     Serial.println(F("Couldn't find VS1053, do you have the right pins defined?"));
     while (1);
  }
  Serial.println(F("VS1053 found"));
  
   if (!SD.begin(CARDCS)) {
    Serial.println(F("SD failed, or not present"));
    while (1);  // don't do anything more
  }

  // list files
  printDirectory(SD.open("/"));
  
  // Set volume for left, right channels. lower numbers == louder volume!
  musicPlayer.setVolume(20,20);


  // If DREQ is on an interrupt pin (on uno, #2 or #3) we can do background
  // audio playing
  musicPlayer.useInterrupt(VS1053_FILEPLAYER_PIN_INT);  // DREQ int
  
}

void loop() {

  switch (m_cmdPinState) {
    case 1:
      // initial state
      if (digitalRead(m_cmdPin) == HIGH)
      {
        m_cmdPinState = 2;
      }
      break;
    case 2:
      // waiting for start
      if (digitalRead(m_cmdPin) == LOW) {
        m_cmdPinState = 3;
        resetCommandFrameBuffer();
        Serial.println("Cmd pin low");
      }
      break;
    case 3:
      // Read command
      m_startTimeoutInterval = millis();
      // if command frame is fully read...
      if (m_cmdFramePtr - (byte*)&m_cmdFrame == COMMAND_FRAME_SIZE)
      {
        Serial.println("Frame Read");
        dumpCommandFrame();
        // process command frame
        if (isChecksumValid() && isCommandForThisDevice())
        {
          Serial.println("For this device and checksum okay");
          if (isValidCommand() && isValidAuxData())
          {
            m_cmdPinState = processCommand();
            Serial.print("New cmd status ");
            Serial.println(m_cmdPinState);
          }
          else
          {
            m_cmdPinState = 2; //wait for start
            Serial.println("Invalid Command");
          }
        }
        else
        {
          m_cmdPinState = 2; //wait for start
          Serial.println("Not this device or checksum error");
        }
        // otherwise, check for command read timeout
      }
      else if (millis() - m_startTimeoutInterval > READ_CMD_TIMEOUT)
      {
        m_cmdPinState = 2; //wait for start
        Serial.println("Time out");
      }
      break;
    case 4:
      // Read data frame
      // Serial.println("Read data frame");
      // check for timeout
      if (millis() - m_startTimeoutInterval > READ_FRAME_TIMEOUT) {
        m_cmdPinState = 2; //wait for start
      }
      break;
    case 5:
      // wait for end

      if (digitalRead(m_cmdPin) == HIGH)
      {
        m_cmdPinState = 2; // wait for start
        Serial.println("Wait for end done");
      }
      break;
  }
}



void SIO_CALLBACK()
{
  // read the next byte from the bus
  byte b = SIO_UART.read();


  switch (m_cmdPinState) {
    // if we read a valid device byte and are in a "command wait" state, come out of it and
    // process the byte
    case 1: //initial state
    case 2: // wait for command
    case 5: // wait for end
      if (digitalRead(m_cmdPin) == LOW && isValidDevice(b)) {
        m_cmdPinState = 3; // Read
        resetCommandFrameBuffer();
      }
      else
      {
        break;
      }
    // if we're reading a command frame...
    case 3: //read
      {
        // read the data into the command frame
        // sometimes we see extra bytes between command frames on the bus while reading a command and things get lost --
        // the isValidDevice() check prevents a command frame read from getting corrupted by them

        int idx = (int)m_cmdFramePtr - (int)&m_cmdFrame;
        if (idx < COMMAND_FRAME_SIZE && (idx > 0 || (idx == 0 && isValidDevice(b)))) {
          *m_cmdFramePtr = b;
          m_cmdFramePtr++;
          return;
        }
        break;
      }
    // if we're reading a data frame...
    case 4:
      {
           // add byte to read sector buffer
          *m_putSectorBufferPtr = b;
          m_putSectorBufferPtr++;
          m_putBytesRemaining--;

          if (m_putBytesRemaining == 0 || b == 0x9b)
          {
            doPutSector();
          }
        break;
      }
    default:

      break;
  }
}

void cmdPutSector(int deviceId) {
  // send ACK
  delay(DELAY_T2);
  SIO_UART.write(ACK);
  Serial.println("Put sector ACK");

  m_putBytesRemaining = MAX_SECTOR_SIZE;
  m_putSectorBufferPtr = m_sectorBuffer;

}

void doPutSector()
{
  int sectorSize = m_putSectorBufferPtr - m_sectorBuffer - 1;
 int j;

  // clear playfile
      playfile = "";  
 
  // send ACK
  delay(2);

  SIO_UART.write(ACK);
  Serial.println("doPutSector ACK");

 for (j=0; j <2; j++)
 {
 delay(1);

  // send COMPLETE
  SIO_UART.write(COMPLETE);
 }
  Serial.println("doPutSector complete");

  // get filename
  for (int i = 0; i < sectorSize; i++)
  {
    playfile += (char)m_sectorBuffer[i];
  }

  // change state
  m_cmdPinState = 2; //wait for start


  
// check for repeated frame buffer
    if (playfile[0] == m_cmdFrame.deviceId &&
        playfile[1] == m_cmdFrame.command) 
       {
       playfile = playfile.substring(5, playfile.length());
       }
       
   
  musicPlayer.startPlayingFile(playfile.c_str());
  }


boolean isChecksumValid() {
  byte chkSum = checksum((byte*)&m_cmdFrame, 4);
  if (chkSum != m_cmdFrame.checksum)
  {
    Serial.println("Checksum fail");
    return false;
  }
  else
  {
    Serial.println("Checksum pass");
    return true;
  }
}

boolean isCommandForThisDevice() {

boolean result = (m_cmdFrame.deviceId == PRINTER ||
                  m_cmdFrame.deviceId == DEVICE_D2);

  return result;
}

boolean isValidCommand() {
  boolean result = (m_cmdFrame.command == CMD_READ ||
                    m_cmdFrame.command == CMD_WRITE ||
                    m_cmdFrame.command == CMD_STATUS ||
                    m_cmdFrame.command == CMD_PUT ||
                    m_cmdFrame.command == CMD_FORMAT ||
                    m_cmdFrame.command == CMD_FORMAT_MD);

  if (!result) {
    result = 0;
  }

  return result;
}

boolean isValidDevice(byte b) {
  boolean result = (b == DEVICE_D1 ||
                    b == DEVICE_D2 ||
                    b == DEVICE_D3 ||
                    b == DEVICE_D4 ||
                    b == DEVICE_D5 ||
                    b == DEVICE_D6 ||
                    b == DEVICE_D7 ||
                    b == DEVICE_D8 ||
                    b == PRINTER   ||
                    b == DEVICE_R1);

  if (!result) {
    result = 0;
  }

  return result;
}

boolean isValidAuxData() {
  return true;
}


void cmdGetStatus(int deviceId)
{
  byte b[4] = {0, 0, 2, 0};
  byte chksum;

  chksum = checksum((byte*)b, 4);
  // send ACK
  delay(DELAY_T2);
  SIO_UART.write(ACK);
  Serial.println("ACK");

  // send complete
  delay(DELAY_T5);
  SIO_UART.write(COMPLETE);
  Serial.println("Get status COMPLETE");


  // send status to bus
  for (int i = 0; i < 4; i++) {
    SIO_UART.write(b[i]);
  }

  SIO_UART.write(chksum);

}



byte checksum(byte* chunk, int length) {
  int chkSum = 0;
  for (int i = 0; i < length; i++) 
  {
    chkSum = chkSum + chunk[i];
     if (chkSum >= 256)
      {
        chkSum = (chkSum -256) +1;
      }  

  }
  return (byte)chkSum;
}



byte processCommand() {
  int deviceId = 1;
  byte nextCmdPinState = 5; // wait for end

  switch (m_cmdFrame.command) {
    case CMD_READ:
      cmdGetSector(deviceId);
      break;
    case CMD_PUT:
    case CMD_WRITE:
      cmdPutSector(deviceId);
      nextCmdPinState = 4; // read data frame
      m_startTimeoutInterval = millis();
      break;
    case CMD_STATUS:
      cmdGetStatus(deviceId);
      break;
    case CMD_FORMAT:
      //cmdFormat(deviceId, DENSITY_SD);
      break;
    case CMD_FORMAT_MD:
      //cmdFormat(deviceId, DENSITY_ED);
      break;
    default:
      //m_sdriveHandler.processCommand(&m_cmdFrame, m_stream);
      break;
  }

  return nextCmdPinState;
}


void dumpCommandFrame() {

  Serial.println(m_cmdFrame.deviceId, HEX);
  Serial.println(m_cmdFrame.command, HEX);
  Serial.println(m_cmdFrame.aux1, HEX);
  Serial.println(m_cmdFrame.aux2, HEX);
  Serial.println(m_cmdFrame.checksum, HEX);

}


void resetCommandFrameBuffer() {
  // reset last command frame info
  memset(&m_cmdFrame, 0, sizeof(m_cmdFrame));
  m_cmdFramePtr = (byte*)&m_cmdFrame;
  Serial.println("buffer reset");
}



void printDirectory(File dir) 
{
 String tempName;  
 int i;
 int dot;
 String fileHeader = "00000";

   while(true) {
     
     File entry =  dir.openNextFile();
     if (! entry) 
  {
       // no more files
       //Serial.println("**nomorefiles**");
       break;
     }

     if (!entry.isDirectory()) 
  {

 // each entry needs to be in the format 5 digits 8 characters 3 ext (16 chrs long)
    filelist += fileHeader;
    tempName = entry.name();
    dot = tempName.indexOf(".");

    filelist += tempName.substring(0, dot);

     if (dot<8)
     {
      for (i=0; i<(8-dot); i++)
      {
        filelist += " ";
      }
     }

     filelist += tempName.substring(dot+1, tempName.length());

     } 
     entry.close();
   }

  
 // convert to byte array
  pdirlist = &dirlist; 
  for (i =0; i<129; i++)
  {
   if (filelist.length()>i)
    {
    *pdirlist = (byte)filelist[i];
    }
    else
    {
     *pdirlist = 0;
    }
    pdirlist ++;
   }
}


void cmdGetSector(int deviceId) {
 
int i;

 // send ACK
  delay(2);
  SIO_UART.write(ACK);
  Serial.println("Get sector ACK");

  // write data frame + checksum
  // send complete
  delay(1);
  SIO_UART.write(COMPLETE);
  Serial.println("Get sector COMPLETE");
  SIO_UART.flush();

  Serial.println("Directory");
  
    for (i=0; i<128; i++)
  {
     SIO_UART.write((byte)filelist[i]);
  }


    // write checksum
   byte chksum = checksum(&dirlist, filelist.length());
   SIO_UART.write(chksum);
   Serial.print("Get sector checksum ");
   Serial.println(chksum);


  SIO_UART.flush();
}