IR Sensor Not working with motor shield

Adafruit Ethernet, Motor, Proto, Wave, Datalogger, GPS Shields - etc!

Moderators: adafruit_support_bill, adafruit

Please be positive and constructive with your questions and comments.
Locked
User avatar
kevcanni
 
Posts: 7
Joined: Thu Jun 09, 2022 4:28 pm

IR Sensor Not working with motor shield

Post by kevcanni »

I'm using Arduino UNO with an Adafruit Motor Shield
https://shop.pimoroni.com/products/adaf ... =382364528

and IR sensor that looks similar to this one:
https://uk.banggood.com/3Pcs-KY-022-Inf ... reateTmp=1

and two 28BYJ-48 stepper motors

https://thepihut.com/products/small-red ... ngQAvD_BwE

I'm running this code:

Code: Select all

//======Includes=======================
#include "Arduino.h"
#include <AFMotor.h>
//IR controls
//#include "PinDefinitionsAndMore.h"
#include <IRremote.hpp>

//======Define stuff===================

#define RAMotor 1
#define DECMotor 2
#define FORWARD 1
#define BACKWARD 2
#define IR_RECEIVE_PIN A0

//====================================
//=======Classes======================
//====================================

//=======Create Classes================
class timeControl {
  public:
    unsigned long MPC;
    long countNumber;

    unsigned long currentTime;
    unsigned long storedTime;
    unsigned long TimeToWait;
    double storedNumber;
    //double waitTime;

    timeControl() {
      storedTime = savedTime(micros());
      storedNumber = 6; //
      //TimeToWait = 500000;
      currentTime = 1;
      countNumber = 2;
      _startTime = 1;
      MPC = microsPerCount();
    }

    unsigned long getTime(uint8_t currentTime = 1) { //0 = currentTime = 0, 1 = stored time, 2 = wait time, 3 = count
      if (currentTime == 0) {
        return micros(); //return the time now
      }
      else if (currentTime == 1) {
        storedTime = _storedTime;
        return storedTime; //return the time (for last time activated)
      }
      else if (currentTime == 2) {
        return TimeToWait; //return the time to wait before attempting to activate again
      }
      else if (currentTime == 3) {
        return countNumber; //return the time to wait before attempting to activate again
      }
    }
    unsigned long savedTime(unsigned long whatTime) {
      _storedTime = whatTime;  //2) Store some arbitrary time that I pass to it so I can reference it later. normally used to save the last step time
      unsigned long storedTime = _storedTime;
      return _storedTime;
    }
    double savedNumber(unsigned long whatNumber) {
      _storedNumber = whatNumber;  //2) Store some arbitrary time that I pass to it so I can reference it later. normally used to save the last step time
      unsigned long storedNumber = _storedNumber;
      return _storedNumber;
    }


    void startTimer() {
      _startTime = millis();
    }
    unsigned long get_startTime() {
      return _startTime;
    }


    double Counter (uint8_t controlCounter = 1) { //if countrolCounter = 0, then reset to 0, controlCounter = 1 then increment the counter by 1. If controlCounter = 2 then return current count

      if (controlCounter == 0) {
        _countNumber = 0;
      }
      else if (controlCounter == 1) {
        _countNumber ++;
      }
      else if (controlCounter == 2) {
        countNumber = _countNumber;
        return countNumber;

      }

    }



    unsigned long microsPerCount() {
      _endTime = micros();
      //_startTime = startTime;
      //_counts = counts;
      MPC = (_endTime  - _startTime) / _counts; //3) A function that subtracts one time against another, divides by a counter and returns the answer.
      return MPC;
    }

    unsigned long timeDifference(long startTime, long endTime) {
      _endTime = endTime;
      _startTime = startTime;
      return _endTime  - _startTime; //3) A function that subtracts one time against another and returns the answer.
    }


  private:
    unsigned long _storedTime;
    double _storedNumber;
    long _startTime;
    long _endTime;
    long _counts;
    long _countNumber;


};

//-------------------------------------
class motorControl {



  public:
    //constructor for motorControl class
    //I think this is a constructor and should execute when the object is created AND IT DOES!! YAY!
    motorControl (uint8_t motorType) {
      _type = motorType;
    }

    //No idea if this bit will work:
    //double get_incrementRPH();

    //I think these are member variables.
    int incrementIndex;
    int gap;
    double maxRPM;
    double minRPM;
    double incrementRPM;


    double steps;

    double currentRPH;

    double _motorDirection ;
    unsigned long _startTimeGTD;
    unsigned long _endTimeGTD;
    unsigned long _counterGTD;







    //Set methods so that they are available all the time
    motorControl() {

      //double _steps = get_Steps(1);
      //double this->_currentRPH = 0;
      //double _solarRPM = get_solarRPM();
      //double _lunarRPM = get_lunarRPM();
      //double _stepGap = calculate_stepGapRPM(_currentRPH, _steps);
      //double _increment = get_increment(_incrementSpeed);
      //double _increment = 1;
      //double _motorDirection = get_motorDirection();
      //int _gap = 0;
      unsigned long _startTimeGTD = 0;
      unsigned long _endTimeGTD = 0;
      unsigned long _counterGTD = 0;


    }

    void begin() {

      if (_type == RAMotor) {
        Serial.println("I'm an RA Motor!");
        currentRPH = 00;
        //_motorDirection = FORWARD;
        incrementIndex = 1;
        calc_BaseRPM(currentRPH, incrementIndex);

      }
      else if (_type == DECMotor) {
        Serial.println("I'm a DEC Motor!");
        currentRPH = 600;
        //_motorDirection = FORWARD;
        incrementIndex = 2;
        minRPM = 0;
        maxRPM = 6;
        incrementRPM = 0.01;
      }
      else {
        Serial.println("I'm an unknown Motor!");
        _motorDirection = FORWARD;
      }
      steps = 2048;
      Serial.println("Motor RPM's (current/min/max/incrementRPH/IncrementIndex/Steps");
      //calc_BaseRPM(currentRPH, incrementIndex);

      Serial.println(this->currentRPH);
      Serial.println(this->minRPM);
      Serial.println(this->maxRPM);
      Serial.println(this->incrementRPM);
      Serial.println(this->incrementIndex);
      Serial.println(this->steps);
    }


    //Functions for motorControl class
    //calculate the step gap for a given number of steps and desired output RPM
    unsigned long calc_stepGapTimeDiff(unsigned long startTimeGTD, unsigned long endTimeGTD, unsigned long counterGTD) {
      //startTime is millis (to limit overflow rollover issues - millis repeats every 47 days, micros repeats every 17 minutes)
      double _stepGap;
      unsigned long _startTimeGTD = startTimeGTD;
      unsigned long _endTimeGTD = endTimeGTD;
      unsigned long _counterGTD = counterGTD;

      _stepGap = ((_endTimeGTD - _startTimeGTD) / _counterGTD);
      return _stepGap;

    }

    long calc_stepGapRPM( double output_RPM, double _steps) {
      double _stepGap;

      if (output_RPM == 0) {
        _stepGap = -99;

      } else {

        //Serial.println("I'm in calculate_stepGapRPM");
        _stepGap = (60UL * 60UL * 1000UL * 1000UL ) / (_steps * output_RPM); //(steps * revs per hour (6 revs per hour for RA) / 3.6million MS per hour
        currentRPH = output_RPM;

      }
      return _stepGap;
    }




    void calc_BaseRPM(double baseRPM, int resolutionIndex) {
      double resolution;
      if (resolutionIndex == 1) {
        resolution = 100;
      }
      else if (resolutionIndex == 2) {
        resolution = 50;
      }
      else if (resolutionIndex == 3) {
        resolution = 10;
      }

      double _minRPM = baseRPM *  (0.95);;
      double _maxRPM = baseRPM * (1.05);;
      double _incrementRPM = (_maxRPM - _minRPM) / resolution;

      //      if (requiredStat == 1) {
      //        return _minRPM;
      //      } else if (requiredStat == 2) {
      //        return _maxRPM;
      //      } else if (requiredStat == 3)
      //        return _incrementRPM;
      //    }

      minRPM = _minRPM;
      maxRPM = _maxRPM;
      incrementRPM = _incrementRPM;
    }


  private:


    uint8_t _type;

    //double _currentRPM;


    //int gap;
    uint8_t _incrementSpeed;
    //double _increment;
    //int _incrementIndex;
    unsigned long startTimeGTD;
    unsigned long endTimeGTD;
    unsigned long counterGTD;
    int _increment;
};

//=======Objects=======================
motorControl raMotor(RAMotor) ;
motorControl decMotor(DECMotor) ;
//double whatsthis1;
//double whatsthis2;
//whatsthis1=raMotor._currentRPH;
//whatsthis2=decMotor._currentRPH;

//Serial.println(whatsthis1);
//Serial.println(whatsthis2);

AF_Stepper motor_dec_stepper(1024, 1);
AF_Stepper motor_ra_stepper(1024, 2);

timeControl RATimer;
timeControl DecTimer;
timeControl IRTimer;

//--------------------------------------
//=======Functions=====================
//--------translateIR Function----------

bool counterHasStarted;
void translateIR(unsigned long IRraw) // takes action based on IR code received
// describing Remote IR codes
//unsigned long IRraw = IrReceiver.decodedIRData.decodedRawData;
{
  //Serial.print(IrReceiver.decodedIRData.decodedRawData,HEX);
  switch (IRraw)

  {
    //Elgoo Controller
    //    case 0xFFA25D: Serial.println("POWER");       break;
    //    case 0xFFE21D: Serial.println("FUNC/STOP");   break;
    //    case 0xFF629D: Serial.println("VOL+");        break;
    //    case 0xFF22DD: Serial.println("FAST BACK");   break;
    //    case 0xFF02FD: Serial.println("PAUSE");       break;
    //    case 0xFFC23D: Serial.println("FAST FORWARD"); break;
    //    case 0xFFE01F: Serial.println("DOWN");        break;
    //    case 0xFFA857: Serial.println("VOL-");        break;
    //    case 0xFF906F: Serial.println("UP");          break;
    //    case 0xFF9867: Serial.println("EQ");          break;
    //    case 0xFFB04F: Serial.println("ST/REPT");     break;
    //    case 0xFF6897: Serial.println("0");           break;
    //    case 0xFF30CF: Serial.println("1");     break;
    //    case 0xFF18E7: Serial.println("2");     break;
    //    case 0xFF7A85: Serial.println("3");     break;
    //    case 0xFF10EF: Serial.println("4");     break;
    //    case 0xFF38C7: Serial.println("5");     break;
    //    case 0xFF5AA5: Serial.println("6");     break;
    //    case 0xFF42BD: Serial.println("7");     break;
    //    case 0xFF4AB5: Serial.println("8");     break;
    //    case 0xFF52AD: Serial.println("9");     break;

    //Sony controller
    case 404758: Serial.println("Eject");        break;
    case 165: Serial.println("Input Select");
      Serial.println("Current RA RPH");
      Serial.println(raMotor.currentRPH);
      Serial.println("Current DEC RPH");
      Serial.println(decMotor.currentRPH);


      break;
    case 149: Serial.println("TV Power");    break;
    case 929045: Serial.println("Main Power");        break;

    case 404736: Serial.println("#1");
      decMotor.incrementIndex = 1;
      raMotor.incrementIndex = 1;
      Serial.println("Getting increment indexfor both motors");
      Serial.println(decMotor.incrementIndex);
      Serial.println(raMotor.incrementIndex);

      break;
    case 404737: Serial.println("#2");
      decMotor.incrementIndex = 2;
      raMotor.incrementIndex = 2;
      Serial.println("Getting increment indexfor both motors");
      Serial.println(decMotor.incrementIndex);
      Serial.println(raMotor.incrementIndex);
      break;
    case 404738: Serial.println("#3");
      decMotor.incrementIndex = 3;
      raMotor.incrementIndex = 3;
      Serial.println("Getting increment indexfor both motors");
      Serial.println(decMotor.incrementIndex);
      Serial.println(raMotor.incrementIndex);
      break;
    case 404739: Serial.println("4");          break;
    case 929028: Serial.println("5");          break;
    case 929029: Serial.println("6");          break;
    case 929030: Serial.println("7");          break;
    case 929031: Serial.println("8");          break;
    case 929032: Serial.println("9");          break;
    case 929033: Serial.println("0");          break;
    case 929127: Serial.println("Red");          break;
    case 929128: Serial.println("Green");          break;
    case 929129: Serial.println("Yellow");         break;
    case 929126: Serial.println("Blue");           break;
    case 929068: Serial.println("Top Menu");       break;
    case 929065: Serial.println("Pop Up/Menu");    break;
    case 929091: Serial.println("Return");         break;
    case 929087: Serial.println("Options");        break;
    case 929090:
      Serial.println("Home");
      Serial.println("RATimer.countNumber");
      Serial.println(RATimer.countNumber);
      if (counterHasStarted == true) { //if the counter has been run before, then
        Serial.println("I'm going to start the RA timer");
        //raMotor.get_stepGap(RATimer.microsPerCount());//calculate the micros Per Count (step gap), and store it
        RATimer.startTimer();//reset the timer
        RATimer.Counter(0); //reset the counter
      } else {
        counterHasStarted = true;
        RATimer.startTimer();
        RATimer.Counter(0);
        //raMotor.get_stepGap(raMotor.calculate_stepGapRPM(raMotor.get_solarRPM(), raMotor.get_Steps(1)) ) ;
      }
      Serial.println("New Step Gap");
      //Serial.println(raMotor.get_stepGap(0));

      Serial.println("");
      break;
    case 929081: Serial.println("Up");
      // get current speed
      Serial.println("DEC RPM before change");
      Serial.println(decMotor.currentRPH);
      decMotor.currentRPH = decMotor.currentRPH + decMotor.incrementRPM;
      Serial.println("Dec RPM after change");
      Serial.println(decMotor.currentRPH);
      DecTimer.TimeToWait = decMotor.calc_stepGapRPM(decMotor.currentRPH , decMotor.steps);
      Serial.println(DecTimer.TimeToWait);
      // get increment

      break;
    case 929082: Serial.println("Down");           break;
    case 929083: Serial.println("Left");           break;
    case 929084: Serial.println("Right");          break;
    case 929085: Serial.println("Select/OK");      break;
    case 929050: Serial.println(">");              break;
    case 929049: Serial.println("\"");             break;
    case 929051: Serial.println("<< ");            break;
    case 929052: Serial.println(">>");             break;
    case 929111: Serial.println("|<< or Pentax Shutter Release");    break;
    case 929110: Serial.println(">>| or Pentax Shutter Release");    break;
    case 929048: Serial.println("Stop");           break;
    case 929123: Serial.println("Subtitles");      break;
    case 929124: Serial.println("Audio");          break;
    case 146: Serial.println("Vol + ");            break;
    case 147: Serial.println("Vol - ");            break;
    case 148: Serial.println("Mute");              break;
    case 929118: Serial.println("Favourite");      break;
    case 929089: Serial.println("Display");        break;
    default: Serial.println(IRraw); break;

  }// End Case

  //Serial.print(step_delay);
} //END translateIR





//=======Global Variables=======================
int doit = 1;
double last_IR_Time;

//=======SET UP=======================
void setup() {
  Serial.begin(9600);
  counterHasStarted = false;
  raMotor.begin();
  decMotor.begin();
  IrReceiver.begin(IR_RECEIVE_PIN, ENABLE_LED_FEEDBACK);




  //waitTime
  Serial.println("Initialise Timers");
  RATimer.savedTime(RATimer.getTime(1));//save the time NOW
  DecTimer.savedTime(DecTimer.getTime(1));
  IRTimer.savedTime(IRTimer.getTime(1));

  RATimer.TimeToWait = raMotor.calc_stepGapRPM(raMotor.currentRPH , raMotor.steps);
  Serial.println("Debugging");
  Serial.println(raMotor.currentRPH);
  Serial.println(raMotor.steps);
  Serial.println(RATimer.TimeToWait);
  Serial.println("Debugging END");
  
  DecTimer.TimeToWait = decMotor.calc_stepGapRPM(decMotor.currentRPH , decMotor.steps);
  IRTimer.TimeToWait = 500;

  Serial.println("Print wait times: RA/DEC/IR");
  Serial.println(RATimer.TimeToWait);
  Serial.println(DecTimer.TimeToWait);
  Serial.println(IRTimer.TimeToWait);
  Serial.println("END Initialise Timers");



}
//=======MAIN LOOP=======================
void loop() {
  // raMotor.steps(2048);

  //serial
  //Wait for IR Time to pass
  //need to initialise these variables then write the code in
  if   (micros() >= IRTimer.getTime(1) + IRTimer.getTime(2)) {
    
    //Serial.println(micros());
    //Serial.println(IRTimer.getTime(1));
    //Serial.println(IRTimer.getTime(2));
    //Serial.println(IRTimer.getTime(1) + IRTimer.getTime(2));
    //Serial.println("NOW, STORED, WAIT, STORED+WAIT");
    //check the wait time here!! is it working?


    if (IrReceiver.decode()) {
      Serial.println("decoding:");
      IrReceiver.printIRResultShort(&Serial);
      //Serial.println(IrReceiver.decodedIRData.command);
      Serial.println(IrReceiver.decodedIRData.decodedRawData);
      translateIR(IrReceiver.decodedIRData.decodedRawData);
      IRTimer.savedTime(micros());
      IrReceiver.resume(); // Enable receiving of the next value
      
    }
  };


  if (micros() >= (RATimer.getTime(1) + RATimer.getTime(2)))
      
  {
    
    //Serial.println("time difference-desired difference");
    //get time now   NOW-SAVEDTIME-WAIT TIME
    //Serial.println("NOW-SAVEDTIME-WAIT TIME");
    //Serial.println(micros());
    //Serial.println(RATimer.getTime(1));
    //Serial.println(RATimer.getTime(2));
    //Serial.println("--------------------");
    //end get time now
//    Serial.println("RA WAIT TIME");
//    Serial.println(RATimer.getTime(2));
//    Serial.println("Dec WAIT TIME");
//    Serial.println(DecTimer.getTime(2));
//    Serial.println("END NOW-SAVEDTIME-WAIT TIME");
    //Serial.println("time difference-actual error gap");
    //Serial.println(RATimer.timeDifference(RATimer.getTime(1), micros())- RATimer.getTime(2));
    //Serial.println("GAP");
    //Serial.println(RATimer.getTime(2));
    motor_ra_stepper.step(1, BACKWARD, DOUBLE); // need to change direction to variable
    RATimer.savedTime(micros());//save the time of the last step
    RATimer.Counter(1);//increment the counter



  }  if (micros() >= (DecTimer.getTime(1) + DecTimer.getTime(2)))
  {
    //    Serial.println("time difference-desired difference");
    //    Serial.println(DecTimer.getTime(2));
    //Serial.println("time difference-actual error gap");
    //Serial.println(DecTimer.timeDifference(DecTimer.getTime(1), micros())- DecTimer.getTime(2));

    motor_dec_stepper.step(1, FORWARD, DOUBLE); // need to change direction to variable
    DecTimer.savedTime(micros());//save the time of the last step
    DecTimer.Counter(1);//increment the counter



  }

};
And here's a wiring diagram:
Motor Wiring Diagram.png
Motor Wiring Diagram.png (118.72 KiB) Viewed 708 times
The IR sensor isn't working as I'd expect.

For example, button "1" pressed on my remote control:

IR sensor returns 929024 as expected when both motors are set to 0 RPH.
If I rotate DEC motor from M1/ M2 on the shield, the IR sensor/decoder returns unexpected result 404736.
If I rotate RA motor from M3/M4 on the the IR sensor/ decoder returns 929024 as I expect.
If I rotate motor both motors using M1/M2 and M3/M4, the IR sensor/decoder only returns expected result when M1/M2 stepper is really slow (<0.12 RPH).
Lastly I can power both Motors at the same time (together) as expected at a sensible speed - 600 RPH works fine on both.

For the "UP" button (and most other buttons), the expected result is 929081, but the unexpected result is always 0

For the "1" button the expected result is 929024, but the unexpected result is always 404736

Any thoughts on how I can fix?

User avatar
adafruit_support_bill
 
Posts: 88136
Joined: Sat Feb 07, 2009 10:11 am

Re: IR Sensor Not working with motor shield

Post by adafruit_support_bill »

I'm using Arduino UNO with an Adafruit Motor Shield
https://shop.pimoroni.com/products/adaf ... =382364528
I'm running this code:

Code: Select all | TOGGLE FULL SIZE
//======Includes=======================
#include "Arduino.h"
#include <AFMotor.h>
The AFMotor library does not work with that motor shield. The V1 shield was discontinued long ago and the library is no longer supported.
The V2 motor shield requires the V2 motor shield library, since it has a completely different architecture.

User avatar
kevcanni
 
Posts: 7
Joined: Thu Jun 09, 2022 4:28 pm

Re: IR Sensor Not working with motor shield

Post by kevcanni »

The AFMotor library does not work with that motor shield
Thanks for the reply, much appreciated. I hadn't realized the different versions of board. This is the shield I bought - is it different from the one pictured, and does it require V1 or V2 library?

https://www.amazon.co.uk/gp/product/B07 ... =UTF8&th=1

Can I use the shield as I intend? That is to run two steppers at a time and independently control the speed of each using an IR remote?

User avatar
adafruit_support_bill
 
Posts: 88136
Joined: Sat Feb 07, 2009 10:11 am

Re: IR Sensor Not working with motor shield

Post by adafruit_support_bill »

That is not an Adafruit board. It looks like probably a copy of our V1 board. In which case it would use the AFMotor library.

Debugging your IR problem would be simpler if you worked in HEX instead of decimal.
929024 = E2D00
404736 = 62D00
Looking at it that way, you can clearly see that you are dropping the first bit.

Since the error is consistent, my guess would be that you are seeing some sort of interrupt contention or other timing interference rather than an EMI problem.

User avatar
kevcanni
 
Posts: 7
Joined: Thu Jun 09, 2022 4:28 pm

Re: IR Sensor Not working with motor shield

Post by kevcanni »

adafruit_support_bill wrote:That is not an Adafruit board. It looks like probably a copy of our V1 board. In which case it would use the AFMotor library.

Debugging your IR problem would be simpler if you worked in HEX instead of decimal.
929024 = E2D00
404736 = 62D00
Looking at it that way, you can clearly see that you are dropping the first bit.

Since the error is consistent, my guess would be that you are seeing some sort of interrupt contention or other timing interference rather than an EMI problem.
OK thank you for taking the time, much appreciated. I'll focus on the timing for now - I'm a noob, so if you have any thoughts there, then that would be appreciated also. I noticed there are IR libraries that do not require timers - maybe that will help?

User avatar
adafruit_support_bill
 
Posts: 88136
Joined: Sat Feb 07, 2009 10:11 am

Re: IR Sensor Not working with motor shield

Post by adafruit_support_bill »

I'm not familiar with the internals of the IR library you are using. But one thing to try is using the onestep() function instead of the step() function for the stepper.

The step() function has a built-in delay based on the motor speed setting. Since you are doing your own step timing, you don't need that. The onestep() function has no delay, so it will have less of an impact on how frequently your code checks the IR sensor.

User avatar
kevcanni
 
Posts: 7
Joined: Thu Jun 09, 2022 4:28 pm

Re: IR Sensor Not working with motor shield

Post by kevcanni »

OK thank you. I've added that, but still have the same problem. I changed pin on the sensor from A0 to 2, and I'm getting a slightly more useful error message.

Code: Select all

Protocol=PULSE_WIDTH Address=0x0 Command=0x0 Raw-Data=0x62D01 19 bits LSB first

Space between two detected transmission is greater than 5000 but smaller than the minimal gap of 20000 known for a protocol.
If you get unexpected results, try to increase the RECORD_GAP_MICROS in IRremote.h.
I'll explore changing the gap, see what that does!

I appreciate that my board is not Adafruit, and am guessing that this might be something to do with the IR Remote library that I'm using. I'll not take any more of your time, but thanks so much for helping.

User avatar
kevcanni
 
Posts: 7
Joined: Thu Jun 09, 2022 4:28 pm

Re: IR Sensor Not working with motor shield

Post by kevcanni »

Leaving this here in case it's helpful to anyone in the future.
adafruit_support_bill wrote: my guess would be that you are seeing some sort of interrupt contention or other timing interference rather than an EMI problem.
Thanks Bill - this was the bit of info I needed. After trying a few things, I eventually solved by using an IR library that doesn't use timers.

This one worked with my Sony remote:
https://github.com/LuisMiCa/IRsmallDecoder

Code: Select all

#include <IRsmallDecoder.h
Changing from Pin A0 to Pin2 is essential, so I'll need to solder that on the Motor Shield.

Wired as follows
Motor Wiring Diagramv2.png
Motor Wiring Diagramv2.png (118.36 KiB) Viewed 636 times
The attachment Motor Wiring Diagramv2.png is no longer available
Thanks for your help!

Locked
Please be positive and constructive with your questions and comments.

Return to “Arduino Shields from Adafruit”