| 
  • If you are citizen of an European Union member nation, you may not use this service unless you are at least 16 years old.

  • You already know Dokkio is an AI-powered assistant to organize & manage your digital files & messages. Very soon, Dokkio will support Outlook as well as One Drive. Check it out today!

View
 

MP3Writeup_JoseOchoa

Page history last edited by Jose Ochoa 12 years, 7 months ago

 

Disclaimer: Just like many Google products … this design is always in Beta! :)

 

Deliverables:

documentation of the design and development process

your project code, and

a video of the final working player in use.

 

Project Description:

Design an MP3 player of your own interpretation.

 

Point of View:

     The problem:

     Imagine a runner warming up. She wants to listen to music that slowly builds up in tempo or energy that will help ease her mind and focus onto a difficult workout during her warmups. During the middle portion of her workout, she would love to keep a steady energy or tempo. And finally, during her cooldown, she would enjoy changing to a more mellow set of songs to keep her in tune with winding down her workout. This type of scenario would work for many other types of exercise including cycling and weight training. The problem, however, is that most athletes don't enjoy carrying a music player in their hand to change music while they workout. The more serious athletes would rather strap their player onto their upper arm, onto their bike, onto their waist belt, or even onto their clothes if the player is small enough. And they don't want to have to strain their neck or bend over completely to view the lcd screen if they want to change to a different set of songs.

 

     The Solution:

     A 2-piece MP3 player with an accelerometer + wireless controller in the remote unit; in addition to an accelerometer controller solution on the main unit.

 

     Once again, imagine a runner warming up. She has pre-recorded a handful of playlists to her mp3 player for different training occasions. As she walks into her warm-up, without looking at any part of the mp3 two-piece unit she is wearing on her upper-arm(main unit) and on her wrist (watch unit), she taps on the remote unit and, in a few seconds, she hears the playlist she wants to hear on her warmup.

     Similarly, picture a cyclist, also using a 2-piece unit for his workout. Placing his remote unit within reach of his right thumb (while still gripping the bike handlebars), he taps the remote unit a few times and finds the particular playlist he wants to listen to.

     And again, picture an athlete at the gym taking a few seconds of rest in between sets of lifting. She doesn't want to spend her short rest period changing the songs that are currently in play on her player. She is using a single-unit mp3 player on her upper left arm. To change songs quickly, she taps on the main player until she finds the playlist that fits the mood she wants to reinforce during the rest of her workout. On another occasion, she decides that although she really loves the current playlist, she has heard it in this same order too many times already. And so she sends a series of taps on her main unit and in a couple of seconds, her player starts playing the same playlist but in a different order.

     As is evident in these scenarios, we take advantage of the accelerometer in either the main unit or the remote unit to command the player to perform certain specific functions which will include, change playlist, shuffle, forward one song, repeat song, and pause.

 

     Results:

  • Did the design work?  What problems arose?

    • Unfortunately, the design was not completely implemented.  The wireless and accelerometer control were left out because of time constraints.  The wireless solution required another microcontroller and transceiver hardware on the remote unit.  The complexity of of getting that part working in under a couple of weeks (and particularly under one week) was ignored by myself.  I overestimated my ability to get work done, and didn't take into account debugging and problems in hardware that take up valuable time.  

    • Lack of mechanical support and structure also helped to prevent a working solution.  In particular, neglecting the design of power connectors, mounting holes on the perfboards, a frame to secure the boards to, and trying to obtain the tiniest footprint possible caused various issues including an unstable electrical system.  The

    • Arrogance.  I am extremely confident in my abilities and that gets me into trouble when I try to take on too much at once or when I decide to leave to little time to important milestones.  

    • Not consulting with the staff.  I didn't consult any of the staff about my design ahead of time since I knew exactly what I wanted to build.  However, I didn't think about the valuable input that I'm sure I would have received to cut back on features and get at least a working prototype. 

  • What would you do differently?

    • Give myself more time.  Start at least one week earlier.

    • Begin prototyping sooner.  I had a lot of notes worked out on paper, but I scrapped most once I got started physically placing parts.

    • Consult with the staff on my proposed design.  I failed to do that since I thought I had it under control.

    • Restrict the design.  The original concept and the one presented in this report is for a 2-piece player with a wearable remote unit.  I would instead concentrate on just the main unit since that has all the functionality except for wireless control.  It would still have motion or shock-based control.

    • Use the lab a lot more.  Once I finally started appearing more often in the lab, it was the little tips and tricks that were shared to remove stumbling blocks or help on getting around them.

    • Concentrate on designing a larger prototype that works instead of trying to cram the entire hardware into the smallest footprint possible.  The hardware stopped working once I started squeezing it into the footprint I had in mind.  I later found several wire-wrapped connections broke during that process.  The player would sporadically work, at times the LCD would just flicker on and off, and for a time it wouldn't even turn on.

    • Never again use balsa wood for making a hardware case for electronics.  I chose it because it was easy to cut with exacto knives and it would be great for quickly sketching out ideas.  However, when it had to become the primary structure keeping the whole thing together, I found it was too brittle, and in many cases trying to bore a hold for screws caused the material to completely fracture along the grain.

    • Make room on the perfboards for power connectors

    • Make room on the perfboards for standoffs and ways of securing the boards to their housing

 

The Design:

     Hardware Design

      Primary Components:

    1. Teensy 2.0

    2. Nokia 5110 GLCD

    3. 3-axis accelerometer board

    4. MP3 decoder board

    5. Rotary encoder

    6. SD card adapter for microSD card

    7. Amber Wireless

    8. Blackberry trackball board

 

     Minor components:

 

 

 

 

Hardware connections:

Teensy 2.0:

 

                    

 

Nokia 5110 GLCD:

Graphical LCD

Teensy

8 - LED

3.3V

7 - SCLK

Pin 1 (SCLK)

6 - DN(MOSI)

Pin 2 (MOSI)

5 - D/C

Pin 4

4 - RST

Pin 8

3 - SCE

Pin 9

2 - GND

GND

1 - Vcc

Pin 10



 

Accelerometer board: 

Accelerometer

Teensy

Vcc

3.3V

GND

GND

X0

Pin 2 (MOSI)

Y0

Pin 4

Z0

N/C



 

MP3 decoder board: 

Decoder

Teensy

CS

Pin 21

SCLK

Pin 1 (SCLK)

SI

Pin 2 (MOSI)

SO

Pin 3 (MISO)

VCC

3.3V

GND

GND

BSYNC (DCS)

Pin 20

DREQ

Pin 19

RST

Pin 18

 

 

Audio Jack to MP3 board:

Decoder

Audio Jack

LEFT

Pin 2 (TIP)

GBUF

Pin 1 (GND on jack NOT Teensy GND)

RIGHT

Pin 3 (RING)

 

 

Blackberry Trackball board:

Decoder

Teensy

CS

Pin 21

SCLK

Pin 1 (SCLK)

SI

Pin 2 (MOSI)

SO

Pin 3 (MISO)

VCC

3.3V

GND

GND

BSYNC (DCS)

Pin 20

DREQ

Pin 19

RST

Pin 18

 

 

Amber Wireless Transceiver board:

Ez430_Pin

Description

Teensy_pin

1

GND

GND

2

VCC

Vcc

3

P2.0/A0

N/C

4

P2.1/A1

N/C

5

P2.2/A2

N/C

6

P2.3/A3

N/C

7

P2.4/A4

N/C

8

P4.3/A12

N/C

9

P4.4/A13

N/C

10

P4.5/A14

N/C

11

P4.6/A15

N/C

12

GND

GND

13

P2.6

N/C

14

P2.7

N/C

15

P3.2/B0_MISO/SCL

Pin3 (MISO)

16

P3.3/B0_SCLK

Pin1 (SCLK)

17

P3.0/B0_STE/A5

Pin 24

18

P3.1/B0_MOSI

Pin2 (MOSI)

 

 

SD Card adapter: 

microSD card

Teensy

MISO

Pin 3 (MISO)

GND

GND

SCK

Pin 1 (SCLK)

3.3V+

3.3V

GND

GND

MOSI

Pin 2 (MOSI)

CS

Pin 12

 

 

Software:

State diagrams

 

          The code:

/*

 * example sketch to play audio file(s) in a directory using the mp3

 * library for playback and the arduino fatfs wrapper to read files.

 * based on frank zhao's player at http://frank.circleofcurrent.com/

 *

 * (c) 2010 david sirkin sirkin@stanford.edu

 *

 * Heavily reworked by Matthew Seal (mattseal@stanford.edu)

 * Modified for use with the Teensy 2.0 by Akil Srinivasan (akils@stanford.edu)

 *

 */

 

// Use 40*13 = 520 bytes of EEPROM

#define MAX_FILE_COUNT 40

#define MAX_NAME_LENGTH 13

 

#include <mp3.h>

#include <mp3conf.h>

 

// include the SD library:

#include <SD.h>

#include <EEPROM.h>

 

// set up variables using the SD utility library functions:

Sd2Card card;

SdVolume volume;

SdFile root;

 

#define DEBUG // Comment this line to remove debugging features

 

 

 

#define mp3_cs      21             // 'command chip select' to cs pin

#define sd_cs       12             // 'chip select' for SD card

 

#define dcs         20            // 'data chip select' to bsync pin

#define rst         18             // 'reset' to decoder's reset pin

#define dreq        19            // 'data request line' to dreq pin

 

#define read_buffer 512           // size of the microsd read buffer

#define mp3_vol     175          // output volume range is 0 to 254 (0 = minimum, 254 = max)

 

// these DEFINES used by the spi functions

#define CS         21           // command CS, connect to CS

#define DCS        20          // (Pin A0) data CS, connect to BSYNC

#define RESET      18           // soft reset, connect to RESET

 

#define SCLK       1          // clock, connect to SCK

#define MOSI       2          // MOSI, connect to SI 

#define MISO       3          // MISO, not used but part of built-in SPI

#define SS   0          //SS pin on Teensy

 

#define READ       0x3

#define WRITE      0x2

 

#define SCI_MODE   0x0

#define SCI_CLOCKF 0x3

#define SCI_VOL    0xB

 

#define DEFAULT_MODE   0x0820 // sets SDINEW mode and SM_TESTS

#define DEFAULT_CLOCKF 0x0000 // set to no internal clock multiplier

#define DEFAULT_VOL    0x8181 // a middle-range initial volume level

 

 

// Creating volume function using Rotary Encoder

#define encoder0PinA  8

#define encoder0PinB  7

 

// declare global variables

volatile unsigned int encoder0Pos = 0;

unsigned char myVolume = 128;

 

 

unsigned char spi_transfer(volatile unsigned char data) {

  SPDR = data;                    // start the transmission

  while (!(SPSR & (1 << SPIF)));  // wait for end of transmission

  return SPDR;                    // return the received byte

}

 

 

void spi_init() {

  pinMode(CS, OUTPUT);

  pinMode(DCS, OUTPUT);

  pinMode(RESET, OUTPUT);

 

  pinMode(SCLK, OUTPUT);

  pinMode(MOSI, OUTPUT);

  pinMode(MISO, INPUT);

  pinMode(SS, OUTPUT);  //SS Pin on Teensy must be set to output

  digitalWrite(SS, HIGH); // disable any SPI device using hardware SS pin

 

  digitalWrite(CS, HIGH);

  digitalWrite(RESET, HIGH);

  delay(100);

  digitalWrite(RESET, LOW);

  delay(100);

  digitalWrite(RESET, HIGH);

 

  // set interrupt disabled, spi enabled, msb 1st, master, clk low when 

  // idle, sample on leading edge of clk and system clock/8 (fastest).

 

  // the VS1053 datasheet says that the max SPI clock frequency is 1.755MHz,

  // the data order is MSB, and the data is valid on the rising edge of

  // the clock, so the default settings of the SPI shortcut (at 500kHz,

  // MSB, rising edge) are applicable. SPR0 set to 1 and SPI2X set to 1 means 8/8 = 1 MHz

 

  SPCR = (1 << SPE) | (1 << MSTR) | (1 << SPR0);

  delay(10);

  SPSR = (1 << SPI2X);

  delay(10);

}

 

 

 

void write_sci(unsigned int address, unsigned int value) {

  digitalWrite(CS, LOW); // select chip for start of transfer

 

  spi_transfer(WRITE);            // send a 'write' cmd

  spi_transfer(address);          // address to write to

 

  if (value > 0xFF) {

    // spi_transfer(value / 0x0100);   // data  to write

    // spi_transfer(value % 0x0100);   // data  to write

 

    spi_transfer((value >> 8) & 0xFF); // data  to write

    spi_transfer(value & 0xFF);        // data  to write

  } else {

    spi_transfer(value);               // data  to write

  }

 

#ifdef DEBUG

  Serial.print("Wrote ");

  Serial.print(value, HEX);

  Serial.print(" to address ");

  Serial.println(address, HEX);

  delay(100);

#endif

 

  digitalWrite(CS, HIGH); //release chip, signal end of transfer

}

 

void write_sci(unsigned int address, unsigned int value1, unsigned int value2) {

  digitalWrite(CS, LOW); // select chip for start of transfer

 

  spi_transfer(WRITE);            // send a 'write' cmd

  spi_transfer(address);          // address to write to

  spi_transfer(value1);           // data to write

  spi_transfer(value2);           // data to write

 

#ifdef DEBUG

  Serial.print("Wrote ");

  Serial.print(value1, HEX);

  Serial.print(value2, HEX);

  Serial.print(" to address ");

  Serial.println(address, HEX);

  delay(100);

#endif

 

  digitalWrite(CS, HIGH); //release chip, signal end of transfer

}

 

unsigned int read_sci(unsigned int address) {

  unsigned int data;

 

  digitalWrite(CS, LOW); // select chip for start of transfer

 

  spi_transfer(READ);            // send a 'read' cmd

  spi_transfer(address);         // address to read from

 

  data = spi_transfer(0xFF);     // get the data bytes

  data = data << 8;

  data |= (spi_transfer(0xFF) & 0xFF);

 

  digitalWrite(CS, HIGH); //release chip, signal end of transfer

  return data;

}

 

 

 

/*

 * Adopted from the ladyada.net tutorial on Ethernet protocol 

 * @http://www.ladyada.net/learn/arduino/ethfiles.html

 *

 * Lists all files from a directory:

 * ListFiles(SdFile *dir, char *doubleBuf, int maxCount)

 *

 * dir is the directory to list

 * doubleBuf is the buffer to store the results in -- this should be fileNames to start

 * maxCount is the size of the buffer.

 *

 * returns the number of file names written into the doubleBuf array

 */

int ListFiles(SdFile *dir) {

  int count = 0;

  dir_t p;

 

  dir->rewind();

  while (dir->readDir(&p) > 0 && count < MAX_FILE_COUNT) {

    // done if past last used entry

    if (p.name[0] == DIR_NAME_FREE) break;

 

    // skip deleted entry and entries for . and  ..

    if (p.name[0] == DIR_NAME_DELETED || p.name[0] == '.') continue;

 

    /* Uncomment to allow subdirectories to be listed (with a '/' after the name) */

    // only list subdirectories and files

    //if (!DIR_IS_FILE_OR_SUBDIR(&p)) continue;

 

    /* Uncomment to only allow files to be listed */

    // only list files

    if (!DIR_IS_FILE(&p)) continue;

 

    // print file name into string

    unsigned char pos = 0;

    for (unsigned char i = 0; i < 11; i++) {

      if (p.name[i] != ' ') {

        EEPROM.write((count*MAX_NAME_LENGTH)+pos, p.name[i]);

        pos++;

      }

    }

 

    // append slash if file is a directory

    if (DIR_IS_SUBDIR(&p)) {

      EEPROM.write((count*MAX_NAME_LENGTH)+pos, '/');

      pos++;

    }

 

    // add the end string character

    EEPROM.write((count*MAX_NAME_LENGTH)+pos, '\0');

    count++;

  }

#ifdef DEBUG

  Serial.print("Stored ");

  Serial.print(count+1);

  Serial.print(" files, using ");

  Serial.print((count+1)*MAX_NAME_LENGTH, DEC);

  Serial.println(" bytes in EEPROM.");

#endif

  return count+1;

}

 

/*

 * read in buffer 'bytes' of 'read_buffer' size from the file opened

 * in the while loop below. This function assumes that file has already

 * been opened and does NOT close the file. This means you need to do this

 * outside of the function.

 */

void mp3_play (SdFile *file) {

  unsigned char bytes[read_buffer]; // buffer to send to the decoder

  unsigned int bytes_to_read;       // number of bytes to read from sd card

  // reset the file to be at the beginning of data

  file->seekSet(0);

  // Try to read 'read_buffer' length of bytes and send it to the decoder.

  // If less than 'read_buffer' bytes are available, stop last send.

  do {

    bytes_to_read = file->read(bytes, read_buffer);

    Mp3.play(bytes, bytes_to_read);

    //Serial.println('.');

  }

  while (bytes_to_read == read_buffer);

#ifdef DEBUG

  Serial.println("Played song...");

#endif

}

 

/*

 * play back an mp3 or wav file (only!) in the root directory. first

 * check that it's a file (and not a directory); next, check that it

 * has a proper extension; finally, play only if it opens cleanly.

 */

void dir_play (SdFile *dir) {

  int numFiles = ListFiles(dir);

  for (int i = 0; i < numFiles; i++) {

    char fn[MAX_NAME_LENGTH+2];

    int fnln = MAX_NAME_LENGTH;

    // get file name and name length

    for (int j = 0; j < MAX_NAME_LENGTH; j++) {

      fn[j] = EEPROM.read((i*MAX_NAME_LENGTH)+j);

      // end of name

      if (fn[j] == '\0') {

        fnln = j;

        j = MAX_NAME_LENGTH;

        break;

      } 

      // directory => nullify entry name

      else if (fn[j] == '/') {

        fn[0] = '\0';

        fnln = 0;

        j = MAX_NAME_LENGTH;

        break;

      }

    }

    if (fnln > 4) {

      fn[fnln+1] = '\0';

      fn[fnln] = fn[fnln-1];

      fn[fnln-1] = fn[fnln-2];

      fn[fnln-2] = fn[fnln-3];

      fn[fnln-3] = '.';

      fnln++;

#ifdef DEBUG

      Serial.print("Opening ");

      Serial.println(fn);

#endif

    }

    SdFile dataFile;

    // ensure we can open the file

    if (dataFile.open(dir, fn, O_RDONLY) > 0) {

      // ensure it's not a directory

      if (!dataFile.isDir() || fn == 0) {

        //get filenames in directory fn

        //get length of the filename fnln

 

#ifdef DEBUG

        Serial.print(fn);

        Serial.println(": File is valid.");

#endif

 

        if ((fn[fnln-3] == 'M' && fn[fnln-2] == 'P' && fn[fnln-1] == '3') ||

  (fn[fnln-3] == 'W' && fn[fnln-2] == 'A' && fn[fnln-1] == 'V')) {

 

#ifdef DEBUG

          Serial.print("Playing ");

          Serial.println(fn);

#endif

   //buffer data for playing

   mp3_play(&dataFile);

        }

#ifdef DEBUG

        else {

          Serial.print("Skipping ");

   Serial.println(fn);

        }

#endif

      }

#ifdef DEBUG

      else {

        Serial.print("File is directory ");

Serial.println(fn);

      }

#endif

    }

    else {

      Serial.print("File is not valid ");

      Serial.println(fn);

    }

  }

}

 

 

 

void doEncoder() {

  /* If pinA and pinB are both high or both low, it is spinning

   * forward. If they're different, it's going backward.

   */

  if (digitalRead(encoder0PinA) == digitalRead(encoder0PinB)) {

 write_sci(SCI_VOL, 0x6060);

    encoder0Pos++;

    myVolume++;

    Mp3.volume(myVolume);

  } else {

    encoder0Pos--;

    myVolume--;

    Mp3.volume(myVolume);

  }

  Serial.println (encoder0Pos, DEC);

  Serial.println (myVolume, DEC);

}

 

 

 

/*

 * initialize the processor speed, setup the fatfs sd card (or, mms)

 * filesystem, setup mp3 playback and register the pins used (device

 * specific configuration)

 */

void setup() {

  Serial.begin(9600);            // initialize the serial terminal

  spi_init();

 

  pinMode(encoder0PinA, INPUT);

  digitalWrite(encoder0PinA, HIGH);       // turn on pullup resistor

  pinMode(encoder0PinB, INPUT);

  digitalWrite(encoder0PinB, HIGH);       // turn on pullup resistor

 

  attachInterrupt(3, doEncoder, CHANGE);  // encoder pin on interrupt 3 - pin 8

 

  pinMode(SS_PIN, OUTPUT);     // change this to 53 on a mega

  digitalWrite(SS_PIN, HIGH);

 

  myVolume = mp3_vol;

 

  // see if the card is present and can be initialized:

  if (!SD.begin(sd_cs)) {

    Serial.println("Card failed, or not present");

    // don't do anything more:

    return;

  }

 

  // we'll use the initialization code from the utility libraries

  // since we're just testing if the card is working!

  if (!card.init(SPI_HALF_SPEED, sd_cs)) {

    Serial.println("Initialization failed. Things to check:");

    Serial.println("* is a card is inserted?");

    Serial.println("* Is your wiring correct?");

    Serial.println("* did you change the SD chipSelect pin to match your setup?");

    return;

  }

 

  // Now we will try to open the 'volume'/'partition' - it should be FAT16 or FAT32

  if (!volume.init(card)) {

    Serial.println("Could not find FAT16/FAT32 partition.\nMake sure you've formatted the card");

    return;

  }

 

  if (!root.openRoot(&volume)) {

    Serial.println("Failed to open root");

    // don't do anything more:

    return;

  }

 

  Mp3.begin(mp3_cs,dcs,rst,dreq);  // decoder cs, dcs, rst, dreq pin from your setup

  Mp3.volume(mp3_vol);             // default volume level is silent (note: 0 = minimum, 254 = max)

 

#ifdef DEBUG

  Serial.println("Card initialized.");

  Serial.println("\nFiles found on the card (name, date and size in bytes): ");

  // list all files in the card with date and size

  root.ls(LS_R | LS_DATE | LS_SIZE);

 

  Serial.println("Checking free memory...");

  Serial.print("There are ");

  Serial.print(get_free_memory(), DEC);

  Serial.println(" bytes of free memory.");

  Serial.print("~");

  Serial.print(read_buffer, DEC);

  Serial.println(" free bytes required for mp3 playing.");

#endif

 

  // List all files to in the root directory and play each

  // one at a time. Maximum of MAX_FILE_COUNT files will be read

  dir_play(&root);

  root.close();

}

 

// Do nothing for now

void loop() {

  while (1);

 

 

 

 

Prototyping Process:

               Some notes and diagrams on paper:

               

 

               

 

 

               

 

               My initial attempts at physically trying to miniaturize the footprint.  I chose a couple of small perfboard pieces and commenced to place the parts on the board.                  

                               

 

 

The next set of three pictures shows a second iteration of working out the optimal part placement for a compact footprint.

               

 

               

 

               

 

 

The next board is the final configuration that actually got implemented.  Notice that the trackball was moved from the bottom third of the player to the middle third.  When physically placing the parts on the board and getting a feel for their placement, it became evident that the trackball and that same boards control buttons would be in the wrong spot and the player would feel awkward to hold and clumsy to control.  

 

               

 

Here is an indication that the wirewrapping would work out just fine.  I was nervous to use it as I was afraid it would take too long and spotting errors might be difficult.  It was neither.  It went fairly quickly and there were no uncaught errors during the whole process.

 

               

 

 

Finally, after the housing I tried to put together for the presentation on August 14th was an absolute failure.  Shortly after the presentation, I had an idea that pieces from an Erector set would be great for putting a frame around my player.  And so I tried it out the next day and I was right.  The boards, as poorly mechanically designed as they are, are now secure in the Erector frame and the whole thing feels strong.  It would be fairly easy to open it up for fixes.  Here are a couple pictures of the new housing:

 

 

Appendix:

    • Datasheets for all parts

    • Appnotes

    • Websites for further info

      • Arduino Website references 

      • Part Vendors used for this project:

        • Sparkfun (most parts on system)

        • PJRC (Teensy 2.0)

        • Mouser (standoffs & other hardware)

        • Newark (Amber Wireless Transceiver)

        • Toys-R-Us (Erector Set for frame) 

      • MSP430 links:

        • Open Source compiler and utilities

          •   

        • Simpliciti Wireless protocol from Texas Instruments

        • Tutorial on getting started with MSP430 

        •  Useful Blogs

          • 43oh

          •   

 

Comments (1)

Akil Srinivasan said

at 8:32 pm on Aug 16, 2011

We like:
- Great motivation/story that describes the 'use scenario' of your player.
- Your 'results' section is a very useful hit list of tips/tricks for new students.
- Detailed parts list, connection pinouts .. again very useful for someone trying to replicate/take inspiration from your project.
- Small and clean layout .. well done! Although sometimes going larger for initial prototype would make your life a lot easier.
- Your design notes and sketches. We can see you spent some time planning!
- Great, creative thinking with the Erector set package at the end!

We wish:
- We also had a verplank diagram from when you were in the mapping/brainstorming stage.

Overall, good project and excellent final report!

Akil, David, and Ben

You don't have permission to comment on this page.