Introduction: IOT123 - SOLAR TRACKER - CONTROLLER

About: The tension between novelty and familiarity...

This is an extension of the Instructable

IOT123 - SOLAR TRACKER - TILT/PAN, PANEL FRAME, LDR MOUNTS RIG. Here we concentrate on the controller of the servos, and sensors of the sun's position. It is important to point out that this design assumes 2 MCUs will be employed: one (3.3V 8mHz Arduino Pro Mini) for the solar tracker, and one independent MCU for your sensors/actors.

This is Version 0.3.

Rather than publish all the projects after complete satisfaction, I will practice continuous integration and deliver something more often, modifying what I have delivered as I need. I will write another instructable for the battery charger, _when_ the optimization of the controller software/hardware is complete. I will point out where the optimizations are needed as we step through this.

Part of the reason for this approach is client feedback. If you guys see a need or have a better approach, please comment, but keep in mind I cannot deliver everything and possibly not to a time-frame that suits you. As these explanations seem less relevant they will be deleted from this article.

What this includes:

  1. Use the LDRs from the original Instructable to sense the suns approximate location.
  2. Move the servos to face the sun.
  3. Options for the sensitivity of the movements.
  4. Options for the step size when moving to the sun.
  5. Options for the angular constraints used on the servos.
  6. Options for the delays of the movements.
  7. I2C interface for setting/getting values between MCUs.

  8. Deep sleep between movements.

What this does not include (and will be addressed as time permits):

  1. Only using power during daylight hours.
  2. Remembering the dawn position and going there at dusk shutdown.
  3. Removing the regulator from the MCU.
  4. Disabling the LED(s) on the MCU.
  5. Rerouting the power through VCC rather than RAW.
  6. Providing workarounds for flashing without regulated power from the USB to Serial TTL converter.
  7. Battery voltage monitor.


HISTORY

Dec 20, 2017 V0.1 CODE

  • Initial version tracks light source, always on, no charging

Jan 7, 2018 V0.2 CODE

  • HARDWARE CHANGES
    • Add I2C pins
    • Add switch to servo GNDs
    • Printed Label on controller box fascia
  • SOFTWARE CHANGES
    • Read configuration from EEPROM

    • I2C bus support as a slave to another MCU (3.3V)

    • Set configuration via I2C

    • Set Enabled via I2C

    • Get configuration via I2C

    • Get runtime properties via I2C (currently Enabled, and Current Light Intensity)

    • Remove serial logging (it affected I2C values)

Jan 19, 2018 V0.3 CODE

  • HARDWARE
    • Label updated. Switch is now used to choose either CONFIG or TRACK mode
  • SOFTWARE
    • I2C only used for configuration
    • Controller waits 5 seconds before initializing tracking, allows for moving hands
    • To use I2C configuration, SPDT must be on CONFIG as unit boots
    • In between tracking movement, unit is in deep sleep mode for configuration value SLEEP MINUTES (default 20 minutes).

Step 1: Materials and Tools

    There is now a full Bill of Materials and Sources list.

    1. 3D printed parts.
    2. Arduino Pro Mini 3.3V 8mHz
    3. 1 of 4x6cm Double Side Prototype PCB Universal Printed Circuit Board (to be cut in half)
    4. 1 off 40P male header (to be cut to size).
    5. 1 off 40P female header (to be cut to size).
    6. 4 off 10K 1/4W restistors.
    7. Hookup wire.
    8. Solder and Iron.
    9. 20 off 4G x 6mm stainless pan head self tapping screws.
    10. 4 off 4G x 6mm stainless countersunk self tapping screws.
    11. 1 off 3.7V LiPo battery and holder (terminating in 2P dupont connectors).

    12. 1 off 2P male right angle header

    13. 1 off SPDT switch 3 pin 2.54mm pitch

    14. Strong Cyanoacrylate glue

    15. Dupont connectors female 1P header (1 off blue, 1 off green).

    Step 2: Assembling the Circuit

    The circuit currently does not have the Voltage Divider Circuit (volt meter).

    1. Cut the 4x6cm Double Side Prototype PCB Universal Printed Circuit Board in half across the long axis.
    2. Cut the 40P male header into pieces:
      1. 2 off 12P
      2. 3 off 3P
      3. 6 off 2P.
    3. Cut the 40P female header into pieces:
      1. 2 off 12P
      2. 1 off 6P
    4. Solder 2 off 12Pfemale header as shown.
    5. Glue the spacer removed from a 3P male (additional) header onto the underside of the SPDT Switch withCyanoacrylate glue
    6. On other side place then solder 6 off 2P, 2 off 3Pmale header and the SPDT switch as shown.
    7. Solder 4 off 10K resistors (A, B, C, D black) via lead to GND pin header (#2 black) and to A0 - A3 header pins (#5, #6, #7, #8) then through hole (yellow) as shown (3 photos + 1 diagram).
    8. Trace 3.3V from LDR PINS soldering PINS #4, #6, #8, #10 and thread though hole to feamale header VCC pin (green).
    9. Trace 3.3V on female header side as shown (red) soldering to PINS #1, #12, #15.
    10. 3.3V through hole the soldered over side (red) RAW header PIN #1.
    11. Trace orange hookup from PIN #11 through hole to solder Female pin on other side as shown.
    12. Trace and solder blue hookup wire from #20 to #30 and from #31 to #13 and #16.
    13. Solder Female Header PIN #11 to Male Header PIN #11 through hole.
    14. Prepare 2 dupont connector 30mm long with female 1P header (1 off blue, 1 off green). Strip and tin other end.
    15. Solder blue Dupont wire to #28; solder green Dupont wire to #29.
    16. On the topside of the Arduino fix the 6P female header then solder.
    17. On the topside of the Arduino fix the 2P right angle female header int #29 and #30 then solder.

    18. On the underside of the Arduino fix the 2 of 12P and 1 off 3P male pins then solder.
    19. Insert Arduino male 12P pins into PCB 12P female headers.

    Step 3: Flashing the MCU

    The Arduino Pro Mini is conveniently flashed using a FTDI232 USB to TTL converter using the 6P female header. See the photo above for the alignment of the 2 boards.

    Ensure the 3.3V setting is chosen on your FTDI232. Follow the instructions here using the code below (use link to GIST).

    The lowpower library (attached and https://github.com/rocketscream/Low-Power) needs to be installed.

    Once the Arduino Pro Mini + PCB is installed in the casing it can still be flashed as the header pins are exposed. Just disconnect the Controller Unit from the Panel Frame exposing the header.

    Tilt pan solar tracker with I2C/EEPROM configuration and sleep cycle between movements. Sleep cycle duration precision decreases as duration increases, but sufficient for this purpose.

    /*
    * modified from code
    * by Mathias Leroy
    *
    * V0.2 MODIFICATIONS
    ** I2C SET GET
    ** EEPROM SET GET
    ** REMOVE SERIAL OUTPUT - AFFECTED I2C
    ** ENABLE/DISABLE TRACKING
    ** MOVE SERVOS TO LIMITS VIA I2C
    ** READ CURRENT AVG INTENSITY VIA I2C
    * V0.3 MODIFICATIONS
    ** SWITCH FOR 2 MODES - TRACK (NO I2C) and CONFIGURE (USES I2C)
    ** SLEEP IN TRACK MODE (VERY LOW PRECISION DUE TO 8 SECOND CHUNKS)
    ** DETACH/ATTACH SERVOS ON SLEEP/WAKE (TRANSISTOR USED EVENTUALLY)
    ** REMOVE CONFIGURABLE INITIAL POSITION (REDUNDANT)
    ** REMOVE CONFIGURABLE WAKE SECONDS (REDUNDANT)
    ** REMOVE CONFIGURABLE ENABLE/DISABLE (REDUNDANT)
    ** REMOVE CONFIGURABLE TRACKER ENABLED (USE HARDWARE SWITCH)
    ** REMOVE VOLTAGE GETTER - WILL USE SEPARATE I2C COMPONENT
    ** ADD SERIAL LOGGING WHEN NOT USING I2C
    */
    #include<Wire.h>
    #include<stdio.h>
    #include<Servo.h>
    #include<EEPROM.h>
    #include<LowPower.h>
    #defineEEPROM_VERSION1
    #defineI2C_MSG_IN_SIZE3
    #definePIN_LDR_TL A0
    #definePIN_LDR_TR A1
    #definePIN_LDR_BR A3
    #definePIN_LDR_BL A2
    #definePIN_SERVO_V11
    #definePIN_SERVO_H5
    #defineIDX_I2C_ADDR0
    #defineIDX_V_ANGLE_MIN1
    #defineIDX_V_ANGLE_MAX2
    #defineIDX_V_SENSITIVITY3
    #defineIDX_V_STEP4
    #defineIDX_H_ANGLE_MIN5
    #defineIDX_H_ANGLE_MAX6
    #defineIDX_H_SENSITIVITY7
    #defineIDX_H_STEP8
    #defineIDX_SLEEP_MINUTES9
    #defineIDX_V_DAWN_ANGLE10
    #defineIDX_H_DAWN_ANGLE11
    #defineIDX_DAWN_INTENSITY12// average of all LDRS
    #defineIDX_DUSK_INTENSITY13// average of all LDRS
    #defineIDX_END_EEPROM_SET14
    #defineIDX_CURRENT_INTENSITY15// average of all LDRS - used for calculating IDX_DAWN_INTENSITY ambiant non-direct light
    #defineIDX_END_VALUES_GET16
    #defineIDX_SIGN_117
    #defineIDX_SIGN_218
    #defineIDX_SIGN_319
    Servo _servoH;
    Servo _servoV;
    byte _i2cVals[20] = {10, 10, 170, 20, 5, 10, 170, 20, 5, 20, 40, 10, 30, 40, 0, 0, 0, 0, 0, 0};
    int _servoLoopDelay = 10;
    int _slowingDelay=0;
    int _angleH = 90;
    int _angleV = 90;
    int _averageTop = 0;
    int _averageRight = 0;
    int _averageBottom = 0;
    int _averageLeft = 0;
    byte _i2cResponse = 0;
    bool _inConfigMode = false;
    voidsetup()
    {
    Serial.begin(115200);
    getFromEeprom();
    if (inConfigMode()){
    Serial.println("Config Mode");
    Serial.print("I2C Address: ");
    Serial.println(_i2cVals[IDX_I2C_ADDR]);
    Wire.begin(_i2cVals[IDX_I2C_ADDR]);
    Wire.onReceive(receiveEvent);
    Wire.onRequest(requestEvent);
    }else{
    Serial.println("Tracking Mode");
    delay(5000);// time to get hands out of way if connecting the battery etc.
    }
    }
    voidloop()
    {
    getLightValues();
    if (!_inConfigMode){
    // ToDo: TURN ON TRANSISTOR SWITCH
    _servoH.attach(PIN_SERVO_H);
    _servoV.attach(PIN_SERVO_V);
    for (int i = 0; i < 20; i++){
    if (i != 0){
    getLightValues();
    }
    moveServos();
    }
    delay(500);
    _servoH.detach();
    _servoV.detach();
    // ToDo: TURN OFF TRANSISTOR SWITCH
    delay(500);
    asleepFor((_i2cVals[IDX_SLEEP_MINUTES] * 60) / 8);
    }
    }
    //---------------------------------CURRENT MODE
    boolinConfigMode(){
    pinMode(PIN_SERVO_H, INPUT);
    _inConfigMode = digitalRead(PIN_SERVO_H) == 1;
    return _inConfigMode;
    }
    //---------------------------------EEPROM
    voidgetFromEeprom(){
    if(
    EEPROM.read(IDX_SIGN_1) != 'S' ||
    EEPROM.read(IDX_SIGN_2) != 'T' ||
    EEPROM.read(IDX_SIGN_3) != EEPROM_VERSION
    ) EEPROM_write_default_configuration();
    EEPROM_read_configuration();
    }
    voidEEPROM_write_default_configuration(){
    Serial.println("EEPROM_write_default_configuration");
    for (int i = 0; i < IDX_END_EEPROM_SET; i++){
    EEPROM.update(i, _i2cVals[i]);
    }
    EEPROM.update(IDX_SIGN_1,'S');
    EEPROM.update(IDX_SIGN_2, 'T');
    EEPROM.update(IDX_SIGN_3, EEPROM_VERSION);
    }
    voidEEPROM_read_configuration(){
    Serial.println("EEPROM_read_configuration");
    for (int i = 0; i < IDX_END_EEPROM_SET; i++){
    _i2cVals[i] = EEPROM.read(i);
    //Serial.println(String(i) + " = " + _i2cVals[i]);
    }
    }
    //---------------------------------I2C
    voidreceiveEvent(int count) {
    if (count == I2C_MSG_IN_SIZE)
    {
    char cmd = Wire.read();
    byte index = Wire.read();
    byte value = Wire.read();
    switch (cmd) {
    case'G':
    if (index< IDX_END_VALUES_GET){
    _i2cResponse = _i2cVals[index];
    }
    break;
    case'S':
    if (index< IDX_END_EEPROM_SET){
    _i2cVals[index] = value;
    EEPROM.update(index, _i2cVals[index]);
    }
    break;
    default:
    return;
    }
    }
    }
    voidrequestEvent()
    {
    Wire.write(_i2cResponse);
    }
    //---------------------------------LDRs
    voidgetLightValues(){
    int valueTopLeft = analogRead(PIN_LDR_TL);
    int valueTopRight = analogRead(PIN_LDR_TR);
    int valueBottomRight = analogRead(PIN_LDR_BR);
    int valueBottomLeft = analogRead(PIN_LDR_BL);
    _averageTop = ( valueTopLeft + valueTopRight ) / 2;
    _averageRight = ( valueTopRight + valueBottomRight ) / 2;
    _averageBottom = ( valueBottomRight + valueBottomLeft ) / 2;
    _averageLeft = ( valueBottomLeft + valueTopLeft ) / 2;
    int avgIntensity = (valueTopLeft + valueTopRight + valueBottomRight + valueBottomLeft) / 4;
    _i2cVals[IDX_CURRENT_INTENSITY] = map(avgIntensity, 0, 1024, 0, 255);
    }
    //---------------------------------SERVOS
    voidmoveServos(){
    Serial.println("moveServos");
    if ( (_averageLeft-_averageRight)>_i2cVals[IDX_H_SENSITIVITY] && (_angleH-_i2cVals[IDX_H_STEP])>_i2cVals[IDX_H_ANGLE_MIN] ) {
    // going left
    Serial.println("moveServos going left");
    delay(_slowingDelay);
    for (int i=0; i < _i2cVals[IDX_H_STEP]; i++){
    _servoH.write(_angleH--);
    delay(_servoLoopDelay);
    }
    }
    elseif ( (_averageRight-_averageLeft)>_i2cVals[IDX_H_SENSITIVITY] && (_angleH+_i2cVals[IDX_H_STEP])<_i2cVals[IDX_H_ANGLE_MAX] ) {
    // going right
    Serial.println("moveServos going left");
    delay(_slowingDelay);
    for (int i=0; i < _i2cVals[IDX_H_STEP]; i++){
    _servoH.write(_angleH++);
    delay(_servoLoopDelay);
    }
    }
    else {
    // doing nothing
    Serial.println("moveServos doing nothing");
    delay(_slowingDelay);
    }
    if ( (_averageTop-_averageBottom)>_i2cVals[IDX_V_SENSITIVITY] && (_angleV+_i2cVals[IDX_V_STEP])<_i2cVals[IDX_V_ANGLE_MAX] ) {
    // going up
    Serial.println("moveServos going up");
    delay(_slowingDelay);
    for (int i=0; i < _i2cVals[IDX_V_STEP]; i++){
    _servoV.write(_angleV++);
    delay(_servoLoopDelay);
    }
    }
    elseif ( (_averageBottom-_averageTop)>_i2cVals[IDX_V_SENSITIVITY] && (_angleV-_i2cVals[IDX_V_STEP])>_i2cVals[IDX_V_ANGLE_MIN]) {
    // going down
    Serial.println("moveServos going down");
    delay(_slowingDelay);
    for (int i=0; i < _i2cVals[IDX_V_STEP]; i++){
    _servoV.write(_angleV--);
    delay(_servoLoopDelay);
    }
    }
    else {
    Serial.println("moveServos doing nothing");
    delay(_slowingDelay);
    }
    }
    //---------------------------------SLEEP
    voidasleepFor(unsignedint eightSecondSegments){
    Serial.println("asleepFor");
    for (unsignedint sleepCounter = eightSecondSegments; sleepCounter >0; sleepCounter--)
    {
    LowPower.powerDown(SLEEP_8S, ADC_OFF, BOD_OFF);
    }
    }

    Step 4: Assembling the Circuit Casing

    1. Ensure the Ardiuno Pro Mini is inserted into the headers on the PCB.
    2. Insert SOLAR TRACKER controller box base into SOLAR TRACKER controller box walls and affix with 2 off 4G x 6mm stainless countersunk self tapping screws.
    3. Insert Ardiuno Pro Mini + PCB with 6P Header slotting into the void in SOLAR TRACKER controller box base.
    4. Insert SOLAR TRACKER controller box lid into SOLAR TRACKER controller box walls and affix with 2 off 4G x 6mm stainless countersunk self tapping screws.
    5. Affix assembly above to the base of the Panel Frame with 4 off 4G x 6mm stainless countersunk self tapping screws.

    Step 5: Connecting the Rig Leads to the Controller

    The pertinent connections ready from the previous Instructable, are 4 off 2P LDR connections and 2 off 3P connections from the servos. What is temporary until the recharging is ready is the battery. Use a 3.7V LiPo that terminates in a 2P DuPont connection for now.

    1. Insert the LDR connections (no polarity) from top:
      1. Top Right
      2. Top Left
      3. Bottom Right
      4. Bottom Left
    2. Insert the Servo connections (with the signal wire to the left) from top:
      1. Horizontal
      2. Vertical
    3. WAIT TILL READY FOR TEST THEN: Insert the 3.7V DC Power lead +ve to the top, -ve to the bottom.

    Step 6: Testing the Controller

    As stated before, the software has not been optimized for Solar Charging workflow. It can be tested and tweaked using natural (sun) and unnatural light sources though.

    To test the tracking in a controlled environment it may be convenient to set the SLEEP MINUTES to a lower value (see next step).

    Step 7: Configuring Via I2C Using Console Input

    This explains configuring the controller via a second MCU, entering settings into a console window.

    1. Upload the following script onto a D1M WIFI BLOCK (or Wemos D1 Mini).
    2. Disconnect USB from PC
    3. PIN CONNECTIONS:
      -ve (Controller) => GND (D1M)
      +ve (Controller) => 3V3 (D1M)
      SCL (Controller) => D1 (D1M)

      SDA (Controller) => D2 (D1M)

    4. Turn the SPDT switch to CONFIG

    5. Connect USB to PC

    6. From the Arduino IDE start a console window with the correct COM Port

    7. Make sure "Newline" and "9600 baud" are selected

    8. The commands get entered into the Send Textbox followed by the Enter key

    9. The commands are in the format Character byte byte

    10. If the second byte (third segment) is not included 0 (zero) is sent by the script

    11. Be careful using serial input; review what you have entered prior to hitting the "Enter" key. If you are locked out (for example changing the I2C address to a value you have forgotten) you will need to flash the controller firmware again.

    The supported variations in the command first character are:

    • E (Enable servo tracking) useful for stopping movement during configuration. This is input using: E 0
    • D (Disable servo tracking) useful to start automatic tracking if not rebooting the device. This is input using: D 0
    • G (Get configuration value) reads values from EEPROM and IN-MEMORY: This is input using: G (index is valid byte values 0 - 13 and 15)
    • S (Set EEPROM value) sets values to EEPROM which are available after reboots. This is input using: S (index is valid byte values 0 - 13, value is valid byte values and vary per property)

    The code is the point of truth for the indexes but the following is used for a guide for valid values/comments:

    • I2C ADDRESS 0 - controller slave address, the master needs this to communicate with the controller (default 10)
    • MINIMUM VERTICAL ANGLE 1 - angle vertical servo lower limit (default 10, range 0 - 180)
    • MAXIMUM VERTICAL ANGLE 2 - angle vertical servo upper limit (default 170, range 0 - 180)
    • SENSITIVITY VERTICAL LDR 3 - Vertical LDR reading margin (default 20, range 0 - 1024)
    • VERTICAL ANGLE STEP 4 - angle vertical servo steps on each adjustment (default 5, range 1 - 20)
    • MINIMUM HORIZONTAL ANGLE 5 - angle horizontal servo lower limit (default 10, range 0 - 180)
    • MAXIMUM HORIZONTAL ANGLE 6 - angle horizontal servo upper limit (default 170, range 0 - 180)
    • SENSITIVITY HORIZONTAL LDR 7 - Horizontal LDR reading margin (default 20, range 0 - 1024)
    • HORIZONTAL ANGLE STEP 8 - angle horizontal servo steps on each adjustment (default 5, range 1 - 20)
    • SLEEP MINUTES 9 - the approximate sleep period between tracking (default 20, range 1 - 255)
    • VERTICAL DAWN ANGLE 10 - FUTURE USE - the vertical angle to return to when the sun goes down
    • HORIZONTAL DAWN ANGLE 11 - FUTURE USE - the horizontal angle to return to when the sun goes down
    • DAWN INTENSITY 12 - FUTURE USE - the minimum average of all the LDRs that triggers a start of daily sun tracking
    • DUSK INTENSITY 13 - FUTURE USE - the minimum average of all the LDRs that triggers a end of daily sun tracking
    • END OF EEPROM VALUES MARKER 14 - VALUE NOT USED
    • CURRENT INTENSITY 15 - the current average percentage of the light intensity
    • END OF IN-MEMORY VALUES MARKER 16 - VALUE NOT USED.

    Captures serial input (keyboard input in the console window) and forwards it to an I2C slave in the format char, byte, byte.

    #include<Wire.h>
    #defineI2C_MSG_IN_SIZE2
    #defineI2C_MSG_OUT_SIZE3
    #defineI2C_SLAVE_ADDRESS10
    boolean _newData = false;
    const byte _numChars = 32;
    char _receivedChars[_numChars]; // an array to store the received data
    voidsetup() {
    Serial.begin(9600);
    Wire.begin(D2, D1);
    delay(5000);
    }
    voidloop() {
    recvWithEndMarker();
    parseSendCommands();
    }
    voidrecvWithEndMarker() {
    static byte ndx = 0;
    char endMarker = '\n';
    char rc;
    while (Serial.available() >0 && _newData == false) {
    rc = Serial.read();
    if (rc != endMarker) {
    _receivedChars[ndx] = rc;
    ndx++;
    if (ndx >= _numChars) {
    ndx = _numChars - 1;
    }
    } else {
    _receivedChars[ndx] = '\0'; // terminate the string
    ndx = 0;
    _newData = true;
    }
    }
    }
    voidparseSendCommands() {
    if (_newData == true) {
    constchar delim[2] = "";
    char *token;
    token = strtok(_receivedChars, delim);
    char cmd = _receivedChars[0];
    byte index = 0;
    byte value = 0;
    int i = 0;
    while( token != NULL ) {
    //Serial.println(token);
    i++;
    switch (i){
    case1:
    token = strtok(NULL, delim);
    index = atoi(token);
    break;
    case2:
    token = strtok(NULL, delim);
    if (token != NULL){
    value = atoi(token);
    }
    break;
    default:
    token = NULL;
    }
    }
    sendCmd(cmd, index, value);
    _newData = false;
    }
    }
    voidsendCmd(char cmd, byte index, byte value) {
    Serial.println("-----");
    Serial.println("Sending command:");
    Serial.println("\t" + String(cmd) + "" + String(index) + "" + String(value) );
    Serial.println("-----");
    Wire.beginTransmission(I2C_SLAVE_ADDRESS); // transmit to device
    Wire.write(cmd); // sends a char
    Wire.write(index); // sends one byte
    Wire.write(value); // sends one byte
    Wire.endTransmission();
    byte response = 0;
    bool hadResponse = false;
    if (cmd == 'G'){
    Wire.requestFrom(I2C_SLAVE_ADDRESS,1);
    while(Wire.available()) // slave may send less than requested
    {
    hadResponse = true;
    response = Wire.read();
    }
    if (hadResponse == true){
    Serial.println("Getting response:");
    Serial.println(response);
    }else{
    Serial.println("No response, check the address/connection");
    }
    }
    }

    Step 8: Next Steps

    Check back periodically to check for changes in software/hardware.

    Modify the software/hardware to your requirements.

    Comment on any requests/optimizations.