Sunday, October 24, 2021

An easy way of improving the randomness of the random function in Arduino code

 Here is how to create two random numbers from 1 to 5 on an Arduino:

x = random (1,6) ;

y = random (1,6) ;

Don't be tricked by the 6. The actual output ranges from 1 to 5 in these examples. Anyway.

If you don't use the function randomSeed(iSeed)you will get the same sequence of random numbers every time you run your program. This is because random actually generates a pseudo random number, which means it loops through all available answers and then loops back to the beginning again. 

So randomSeed(iSeed)is used to start in a different place in the loop every time you run the program. iSeed  must vary to change the starting point of random

Often randomSeed is called in the setup part of the Arduino program.

But how can you choose a different iSeed every time you run the program? An often suggested method is to read a floating analog input. "Floating" means it is not connected to anything, so electrical fields wafting in from your hand or cell phone or brain move the electrons on the circuit board track which connects to the input. The moving electrons produce a tiny current and the tiny current produces a tiny change in voltage, which you read using the function  ???

 You can improve the random variation of voltage at A0 (for example) simply by adding a long wire to it!

Here's the board with no connection to A0:


And here are the variations in voltage...

A0, rand seed: 265, min 265, max 265, delta 0
A0, rand seed: 263, min 263, max 265, delta 2
A0, rand seed: 261, min 261, max 265, delta 4
A0, rand seed: 259, min 259, max 265, delta 6
A0, rand seed: 258, min 258, max 265, delta 7
A0, rand seed: 257, min 257, max 265, delta 8
A0, rand seed: 256, min 256, max 265, delta 9
A0, rand seed: 255, min 255, max 265, delta 10
A0, rand seed: 254, min 254, max 265, delta 11
A0, rand seed: 254, min 254, max 265, delta 11
A0, rand seed: 253, min 253, max 265, delta 12
A0, rand seed: 252, min 252, max 265, delta 13
A0, rand seed: 251, min 251, max 265, delta 14
A0, rand seed: 251, min 251, max 265, delta 14
A0, rand seed: 251, min 251, max 265, delta 14
A0, rand seed: 250, min 250, max 265, delta 15
A0, rand seed: 250, min 250, max 265, delta 15
A0, rand seed: 249, min 249, max 265, delta 16
A0, rand seed: 249, min 249, max 265, delta 16
A0, rand seed: 248, min 248, max 265, delta 17
A0, rand seed: 247, min 247, max 265, delta 18
A0, rand seed: 248, min 247, max 265, delta 18
A0, rand seed: 247, min 247, max 265, delta 18
A0, rand seed: 246, min 246, max 265, delta 19
A0, rand seed: 247, min 246, max 265, delta 19

... going from 246 to 265.

Here is the Arduino with a long wire connected to A0:


And here are the variations in voltage (with a long wire)...

A0, rand seed: 48, min 48, max 48, delta 0
A0, rand seed: 59, min 48, max 59, delta 11
A0, rand seed: 67, min 48, max 67, delta 19
A0, rand seed: 81, min 48, max 81, delta 33
A0, rand seed: 93, min 48, max 93, delta 45
A0, rand seed: 133, min 48, max 133, delta 85
A0, rand seed: 193, min 48, max 193, delta 145
A0, rand seed: 249, min 48, max 249, delta 201
A0, rand seed: 308, min 48, max 308, delta 260
A0, rand seed: 369, min 48, max 369, delta 321
A0, rand seed: 431, min 48, max 431, delta 383
A0, rand seed: 488, min 48, max 488, delta 440
A0, rand seed: 537, min 48, max 537, delta 489
A0, rand seed: 577, min 48, max 577, delta 529
A0, rand seed: 612, min 48, max 612, delta 564
A0, rand seed: 629, min 48, max 629, delta 581
A0, rand seed: 799, min 48, max 799, delta 751
A0, rand seed: 747, min 48, max 799, delta 751
A0, rand seed: 325, min 48, max 799, delta 751
A0, rand seed: 310, min 48, max 799, delta 751
A0, rand seed: 297, min 48, max 799, delta 751
A0, rand seed: 288, min 48, max 799, delta 751
A0, rand seed: 278, min 48, max 799, delta 751
A0, rand seed: 273, min 48, max 799, delta 751
A0, rand seed: 268, min 48, max 799, delta 751


...going from 48 to 799, clearly an improvement.

Remember the analog signal is not the random number, but how to make the seed for the random number generator function, random().

Here  is the code I used to do the test above:

/*
 * Adding a wire to a floating analog input can give a better range
 * of random seeds.
 */

// The setup() method runs once, when the sketch starts
void setup()   {                
    Serial.begin (9600) ;
}

// the loop() method runs over and over again,
// as long as the Arduino has power

int iMin = 1024 ;
int iMax = 0 ;
int iLoopResetCount = 0 ;

void loop()                     
{
    int iVal = analogRead (A0) ;
    if (iVal < iMin) {
        iMin = iVal ;
    }
    if (iVal > iMax) {
        iMax = iVal ;
    }
    
    Serial.print ("A0, rand seed: ") ;
    Serial.print (iVal) ;
    Serial.print (", min ") ;
    Serial.print (iMin) ;
    Serial.print (", max ") ;
    Serial.print (iMax) ;
    Serial.print (", delta ") ;
    Serial.print (iMax-iMin) ;
    Serial.println() ;
    
    delay(500);     

    iLoopResetCount+=1 ;
    
    if (iLoopResetCount == 25) {
       iLoopResetCount=0 ;
       Serial.println("loop reset") ;
       iMin = 1024 ;
       iMax = 0 ;
    }
}

And here is how in practice you use randomSeed:

/*
 * Move a servo to random positions every 10 seconds.
 */

// The setup() method runs once, when the sketch starts
void setup()   {                
    Serial.begin (9600) ;

    int iSeed = analogRead (A0) ;
    randomSeed(iSeed);
    Serial.print ("Seed = ") ;
    Serial.println (iSeed) ;
}

// the loop() method runs over and over again,
// as long as the Arduino has power
void loop()                     
{
    // Move a servo randomly between 0 and 180 degrees
    int iServoDegrees = random(0,181) ;    
    Serial.print ("Would move the servo to ") ;
    Serial.print (iServoDegrees) ;
    Serial.println (" degrees ") ;
    delay(10000);     // 10000ms = 10 sec
}





 

 

.

 


 

 


Friday, October 8, 2021

Automatically creating a CRecordset derived class from an Access table

 There used to be Wizard in the Visual Studio IDE which wrote the CPP and H files of a CRecordset derived class just by pointing at a table in an Access database. No more, or I can't find it in the zillion options of the IDE, or I'm not good at searching on line. Anyway. In the end I decided that, since I was going to have to do the work on many tables I may as well write the wizard myself, in C# to give me more practice in that language. 

Here's the overview of the application, RecSecMaker:


I'm now really glad I wrote it. When a database table has more than 3 or 4 columns doing this stuff by hand becomes long winded and error prone. Now (cliche warning) in just a few clicks the whole class is written for me immediately and automatically.

Here is an example of the output of the program, first the H file:

#pragma once

#include <afxdb.h>
class CCustomersRecSet: public CRecordset
{
public:
    CCustomersRecSet(CDatabase* pdb = NULL);
    int m_iCustomerID;
    //(System.Int32)
    CString m_csCompanyName;
    //(System.String)
    CString m_csContactFirstName;
    //(System.String)
    CString m_csContactLastName;
    //(System.String)
    CString m_csAddress1;
    //(System.String)
    CString m_csAddress2;
    //(System.String)
    CString m_csAddress3;
    //(System.String)
    CString m_csPostalCode;
    //(System.String)
    CString m_csTitle;
    //(System.String)
    CString m_csPhoneNumber;
    //(System.String)
    CString m_csFaxNumber;
    //(System.String)
    CString m_csEmailAddress;
    //(System.String)
    CString m_csNotes;
    //(System.String)
    BOOL m_bAcceptsSpam;
    //(System.Boolean)
    CString m_csWEBAddress;
    //(System.String)
    BYTE m_iSexId;
    //(System.Byte)
    int m_iCountryId;
    //(System.Int32)
    CString m_csContactMiddleName;
    //(System.String)
    CString m_csAddress4;
    //(System.String)
    int m_iCompanyID;
    //(System.Int32)

    virtual CString GetDefaultSQL();
    virtual void DoFieldExchange(CFieldExchange* pFX);
    virtual CString GetDefaultConnect();
};

And here is the CPP file:

#include "stdafx.h"
#include "CustomersRecSet.H"

CCustomersRecSet::CCustomersRecSet(CDatabase* pdb /*= NULL*/) : CRecordset(pdb)
{
    m_nFields = 20;
    m_nDefaultType = dynaset;
}

CString CCustomersRecSet::GetDefaultSQL()
{
    CString SqlString = L"SELECT * FROM Customers";
    return SqlString ;
}

void CCustomersRecSet::DoFieldExchange(CFieldExchange* pFX)
{
    pFX->SetFieldType(CFieldExchange::outputColumn);
    RFX_Int(pFX, _T("[CustomerID]"), m_iCustomerID);
    //(System.Int32)

    RFX_Text(pFX, _T("[CompanyName]"), m_csCompanyName);
    //(System.String)

    RFX_Text(pFX, _T("[ContactFirstName]"), m_csContactFirstName);
    //(System.String)

    RFX_Text(pFX, _T("[ContactLastName]"), m_csContactLastName);
    //(System.String)

    RFX_Text(pFX, _T("[Address1]"), m_csAddress1);
    //(System.String)

    RFX_Text(pFX, _T("[Address2]"), m_csAddress2);
    //(System.String)

    RFX_Text(pFX, _T("[Address3]"), m_csAddress3);
    //(System.String)

    RFX_Text(pFX, _T("[PostalCode]"), m_csPostalCode);
    //(System.String)

    RFX_Text(pFX, _T("[Title]"), m_csTitle);
    //(System.String)

    RFX_Text(pFX, _T("[PhoneNumber]"), m_csPhoneNumber);
    //(System.String)

    RFX_Text(pFX, _T("[FaxNumber]"), m_csFaxNumber);
    //(System.String)

    RFX_Text(pFX, _T("[EmailAddress]"), m_csEmailAddress);
    //(System.String)

    RFX_Text(pFX, _T("[Notes]"), m_csNotes);
    //(System.String)

    RFX_Bool(pFX, _T("[AcceptsSpam]"), m_bAcceptsSpam);
    //(System.Boolean)

    RFX_Text(pFX, _T("[WEBAddress]"), m_csWEBAddress);
    //(System.String)

    RFX_Byte(pFX, _T("[SexId]"), m_iSexId);
    //(System.Byte)

    RFX_Int(pFX, _T("[CountryId]"), m_iCountryId);
    //(System.Int32)

    RFX_Text(pFX, _T("[ContactMiddleName]"), m_csContactMiddleName);
    //(System.String)

    RFX_Text(pFX, _T("[Address4]"), m_csAddress4);
    //(System.String)

    RFX_Int(pFX, _T("[CompanyID]"), m_iCompanyID);
    //(System.Int32)

}

CString CCustomersRecSet::GetDefaultConnect()
{
    // You'll have to change this for your own situation, this is a guide...
    CString csDriver = L"MICROSOFT ACCESS DRIVER (*.mdb)";
    CString csConnect;
    csConnect.Format(L"ODBC;DRIVER={%s};DSN='';DBQ=%s", csDriver.GetString(), theApp.GetProgDbFullFileName().GetString());
    return csConnect;
}

The program automatically declares fields of the correct type based on the type of the Access column, and calls the appropriate RFX_ macro, also based on the type of the Access column.

One of the main parts of the program is how the Access database is interrogated, I do that in a class called CTableDetails.

// CTableDetails: A class which reads the details of a particular table, the schema.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Data;
using System.Data.OleDb;
using System.Data.Common;
using System.Diagnostics;
using System.Windows.Forms;

namespace RecSecMaker
{
    // This class contains all I want to know about the format of a column (field) in a database table...
    public struct AccessFieldData_t
    {
        public AccessFieldData_t (string sColName, Type TheType, int iColSize)
        {
            m_sColName = sColName ; // "SAPCODE"
            m_ColType = TheType;  // is this a string or a double or...? System.Int16 for example
            m_iColSize = iColSize ;  // How big is the field in bytes, length of the string if it is a string
        }
        public string m_sColName;
        public Type m_ColType;
        public int m_iColSize;
    };

    public class CTableDetails
    {
        public readonly string m_sTableName; // "Birthdays"

        private List<AccessFieldData_t> m_ListOfFields = null;

        // How many columns are in the table?
        public int GetNumFields ()
        {
            if (m_ListOfFields == null)
            {
                return 0;
            }

            return m_ListOfFields.Count;
        }

        // Get details about a particular column or field
        public AccessFieldData_t GetFieldDataByIndex (int i)
        {
            return m_ListOfFields[i];
        }

        // Construct the object from the database filename and the table inside that filename
        public CTableDetails (string sMdbFullFileName, string sTableName)
        {
            m_sTableName = sTableName;
            try
            {
                // Use using so we don't have to dispose of it...
                using (OleDbConnection ADbConnection = DbHelpers.OpenDbConnection(sMdbFullFileName))
                {
                    // These three lines are just to get hold of the table...
                    DbCommand DbCmd = ADbConnection.CreateCommand();
                    DbCmd.CommandText = "select * from " + sTableName + " where 1 = 0";
                    DbCmd.CommandType = CommandType.Text;

                    // The Reader will give us the table...
                    DbDataReader Reader = DbCmd.ExecuteReader();

                    // Now we can get how the columns are specified...
                    DataTable Schema = Reader.GetSchemaTable();

                    // Get ready to store information on each column...
                    m_ListOfFields = new List<AccessFieldData_t>();

                    foreach (DataRow row in Schema.Rows)
                    {
                        // Get hold of the three things I am interested in...
                        AccessFieldData_t TempData = new AccessFieldData_t(row.Field<string>("ColumnName"), row.Field<Type>("DataType"), row.Field<int>("ColumnSize"));

                        // Save the column details
                        m_ListOfFields.Add(TempData);
                    }

                    foreach (AccessFieldData_t Dat in m_ListOfFields)
                    {
                        Debug.WriteLine("{0}, {1}, {2}", Dat.m_sColName, Dat.m_ColType, Dat.m_iColSize);
                    }
                }
            }
            catch (Exception e)
            {
                MessageBox.Show("Exception in database: " + e.Message);
            }

        }
    }
}

I did not know how to do this before writing the program, so I learned something and built something useful.

I thought about writing about an article RecSecMaker for stackoverflow, but hell, the loops you have to jump through just to impart information! And I thought about GITHub, but life is too short.

So if anyone wants the EXE or the sources just contact me. All offered as is with no guarantees and no support.





Saturday, May 29, 2021

Simultaneous control of multiple servos with the Arduino

 It is easy enough to control multiple servo motors with the Arduino, but I wanted a way to have them all going to different positions and at different speeds simultaneously. For example I did not want to move Servo A to 77° and afterwards Servo B to 25°. I wanted both servos to move (or appear to move) at the same time.

(For the basics of controlling a servo with the Arduino there's a ton of stuff on the web, this link seems like a good article. Read that first before continuing with this post.)

I'd seen some articles on multitasking on the Arduino and I thought: "Aha, I can use a similar technique for driving multiple servos..."

The main action of an Arduino program takes place in a function called loop, which is called automatically again and again. In our case the loop looks like this:



At the head of the loop you capture the command. The command can come over the serial link from a program running on the PC, or maybe from functions which read sensors. 

If a command comes in which says "move Servo A to 33°" the last block in the above loop will start to do that. But the next time round there could be a command which says to "move Servo B to 90°". And again that last block will move both servos towards their final position, so they appear to be moving simultaneously.

Of course once the servos have reached their positions they do not move until another command arrives for them.

What I also wanted to do was to have each servo have acceleration and deceleration. I do this by having a curve which specifies the speed with respect to time. Time being measured with 0 at the start of the movement. So here is what I mean graphically:


 Notice that the curve above does not reach a speed of 0, else the servo might stop before it gets to its target. That last horizontal line guarantees that movement will continue even if it takes a long time to get to the target.

In the program I don't actually do a curve, I have some discreet steps, look at the comments inside the kSpeedCurves part. Each speed curve has an id, I use ids 'A', 'B' and 'C', which means you can move one servo following one speed curve, and another servo following a different curve. Some faster, some slower, some with more acceleration, some with less.

As far as the program is concerned the speed of the servo is actually simply the angular size of the step to take in each loop.

The command from the PC are not gathered together in one iteration of the loop. A character is read in each iteration until a 0 terminator arrives, then the command is interpreted. Remember that commands do not move the servo, but specify a target for a given servo. The servo moves towards that target. It may be interrupted by another command even before it gets to the target.

The format of the commands contain which servo and which curve to use and the degrees target to move to, see the comments about "VA123" in the sources.

Here is the code, it was written for a specific need of mine, but maybe you'll find something useful in there and can adapt it to you own requirements. Have fun!

// Moving multiple servos at different rates simultanesouly...

#include <Servo.h>

/***************************************************************************************************************/

typedef struct {
    Servo* pServoObj ; // Which servo will be driven by this structure
    int iServoPin ; // On which pin is the servo connected
    double CurrPos ; // Where we are now
    double TargetPos ; // Where we want to go
    unsigned long imsRealStartTime ; // The time when we started going
    char chCurveId ; // For example 'A' which curve in kSpeedCurves to use
} ServoCurver_t ;

/***************************************************************************************************************/

// Each one of these is an entry in the speed curve
typedef struct {
    unsigned long iTimeOrPcent ; // x-axis in ms or in %
    double Speed ; // y-axis, actually a delta angle to apply
} SpeedAt_t ;


// Here is a list of speed curves....
#define NUM_X_COORDS 4
typedef struct {
    char chId ;  
    SpeedAt_t Curve [NUM_X_COORDS] ;
} CurverData_t ;

const CurverData_t kSpeedCurves[] = {
    {
        'A', // the id has to e unique inside this array
        {
            {     0,  1.00},  // at 0 ms, the beginning of the move the delta to apply is 1 degrees
            {   500,  2.00},   // at 0.5s the delta to apply is 2 degrees
            {  2000,  1.50},   // at 2s the delta to apply is 1.5 degrees
            { 15000,  1.25},   // at 15s the delta to apply is 1.25 degrees
        }
    } ,
    {  
        'B',
        {
            {    0, 1.0},
            { 500,  3.75},
            { 7000, 1.5},
            {10000, 0.5},
        }
    },
    {  
        'C',
        {
            {    0, 6.0},
            { 500,  5.0},
            { 900,  4.0},
            {10000, 0.75},
        }
    },
};

const size_t ikTotalNumCurves = sizeof(kSpeedCurves)/sizeof(kSpeedCurves[0]) ;

/***************************************************************************************************************/

int LedPin = 13 ;

/***************************************************************************************************************/

// This stores communications from the PC...
int iSerialCharsRead = 0 ;
#define CMD_BUF_SIZE 100
char szCommand[CMD_BUF_SIZE] = "" ;

void ClearCmdBuffer ()
{
    iSerialCharsRead = 0 ;
    memset (szCommand,CMD_BUF_SIZE,0) ;  
}

/***************************************************************************************************************/

// Create the servo objects
const int ikHeadVerticalPin = 3 ;
ServoCurver_t scHeadVertical  ;
Servo srvHeadVertical;  

const int ikHeadHorizontalPin = 10 ;
ServoCurver_t scHeadHorizontal  ;
Servo srvHeadHorizontal;  

Servo srvRightArm ;
const int ikRightArmPin = 5 ;
ServoCurver_t scRightArm ;

Servo srvLeftArm ;
const int ikLeftArmPin = 6 ;
ServoCurver_t scLeftArm ;

/***************************************************************************************************************/

// Look up which CurverData_t corresponds to the given id
const CurverData_t& GetCurveFromId (char chId)
{
    for (size_t c = 0 ; c < ikTotalNumCurves ; ++c) {
        if (kSpeedCurves[c].chId == chId) {
            return kSpeedCurves[c] ;
        }
    }

    Serial.print ("GetCurveFromId unknown id: ") ;
    Serial.print (chId) ;
    Serial.println() ;  

    // Return something
    return kSpeedCurves[0] ;
}

/***************************************************************************************************************/

// First attempt no interpolation
// Calculates and returns the speed we must apply to the servo given its
// position and the speed curve it is using
// imsDeltaTime is how far into the speed curve the servo is
double GetSpeedFromTimeCurve (char chCurve, unsigned long imsDeltaTime)
{
    // Get hold of that speed curve in an an easy to use way...
    const CurverData_t& kThisTimeSpeedCurve = GetCurveFromId(chCurve) ;

    if (imsDeltaTime == 0) {
        // We are at the very beginning of the curve, return the first value
        //Serial.print (" at time start") ;
        //Serial.println (kThisTimeSpeedCurve.Curve[0].Speed) ;
        return kThisTimeSpeedCurve.Curve[0].Speed ;  
    }

    // Look at every section in the time speed curve...
    for (int x = 0 ; x < NUM_X_COORDS-1 ; ++x) {
        // Are we in the range x to x+1 in the graph?
        const bool kbInRange = (imsDeltaTime >= kThisTimeSpeedCurve.Curve[x].iTimeOrPcent) && (imsDeltaTime <  kThisTimeSpeedCurve.Curve[x+1].iTimeOrPcent) ;
        if (kbInRange) {
            const double kSpeed = kThisTimeSpeedCurve.Curve[x].Speed ;
           
            return kSpeed ;
        }
    }


    // if we get here we are beyond the end of the x-axis of the curve,
    // Assume he curve carries on to infinity from the last value

    const double kSpeed = kThisTimeSpeedCurve.Curve[NUM_X_COORDS-1].Speed ;

    return kSpeed ;
}

/*****************************************************************************************************/

// Get direction required to move towards the target of this servo...
int GetSignForServoMovement (const ServoCurver_t& ServoCurver)
{
    const double kOnTargetMargin = 1.0 ;

    const double kError = abs (ServoCurver.CurrPos - ServoCurver.TargetPos) ;
    if (kError < kOnTargetMargin ) {
        // In this case the servo is already at the target and we don't need to
        // do anything
        return 0 ;
    }

    int iSign ; // In which direction to move the servo

    if (ServoCurver.CurrPos < ServoCurver.TargetPos) {
        // need to move in a positive direction to get to the target
        iSign = +1 ;

    } else if (ServoCurver.CurrPos > ServoCurver.TargetPos) {
        // need to move in a negative direction to get to the target      
        iSign = -1 ;
       
    } else {
        // In this case the servo is already at the target and we don't need to
        // do anything
        iSign = 0 ;
    }

    return iSign ;
}

void MoveServoOnCurveByTime (ServoCurver_t& ServoCurver)
{
    const int ikSign = GetSignForServoMovement (ServoCurver) ;
    if (ikSign == 0) {
        // Servo is at target, no movement to do
        return ;
    }  

    // Get the time now. unsigned long is important
    const unsigned long imsNow = millis() ;
 
    // Get the delta time, how far into the curve you are
    unsigned long imsDelta = imsNow - ServoCurver.imsRealStartTime ;

    // See what speed the curve gives us at this time.  
    const double kSpeed = GetSpeedFromTimeCurve (ServoCurver.chCurveId,imsDelta) ;
   
    if (kSpeed > 0) {
        // Update the current pos...
        ServoCurver.CurrPos = ServoCurver.CurrPos + (ikSign*kSpeed) ;
     
    } else if (kSpeed == 0) {
        // Speed 0 means that time is beyond the end of the curve
        // so we must be at the target position. Update vars and move servo
        // to the final target position
        ServoCurver.CurrPos = ServoCurver.TargetPos ;
    }

    // ...and the actual server
    const int ikIntPos = int(round (ServoCurver.CurrPos)) ;
    ServoCurver.pServoObj->write (ikIntPos) ;
}

/********************************************************************************************/

// This should be called everytime we need a new target, i.e every time a new command comes along
void SetServoCurveAndTarget (ServoCurver_t& ServoCurve, const char chCurveToUse, const double kNewTarget)
{
    // ServoCurve.CurrPos should not be changed, the servo is where it is, wherever that
    // may be, but we want to give it a new *target*.
    ServoCurve.TargetPos = kNewTarget ;

    ServoCurve.chCurveId = chCurveToUse ;  // This is which graph to use

    // Only one of these will be used, depends on what sort of curve it is
    ServoCurve.imsRealStartTime = millis() ; // This is the start time of the command, now
}

// This should be called only once per servo in the setup
void InitServoCurverAndHardware (ServoCurver_t& ServoCurve, Servo* pServoObject, int const ikServoPin, char chCurveToUse)
{
    // Set everything to 90° initially, servos in a halfway position
    const double kDefaultPos = 90.0 ;
    ServoCurve.CurrPos = kDefaultPos ; // this is where it is
 
    SetServoCurveAndTarget (ServoCurve,chCurveToUse,kDefaultPos) ;  

    // Remember what hardware this ServoCurver is attached to...
    ServoCurve.pServoObj = pServoObject ;
    ServoCurve.iServoPin = ikServoPin ;  

    // Attach and set the position
    ServoCurve.pServoObj->attach(ServoCurve.iServoPin) ;  
    ServoCurve.pServoObj->write (int(round(ServoCurve.CurrPos))) ;  
}

/*********************************************************************************************************************************/

void setup() {
    Serial.begin (9600) ;

    InitServoCurverAndHardware (scHeadVertical,
                                &srvHeadVertical,
                                ikHeadVerticalPin,
                                'A') ;

    InitServoCurverAndHardware (scHeadHorizontal,
                                &srvHeadHorizontal,
                                ikHeadHorizontalPin,
                                'A') ;

    InitServoCurverAndHardware (scLeftArm,
                                &srvLeftArm,
                                ikLeftArmPin,
                                'A') ;
 
    InitServoCurverAndHardware (scRightArm,
                                &srvRightArm,
                                ikRightArmPin,
                                'A') ;
   
    pinMode(LedPin, OUTPUT);    

    ClearCmdBuffer () ;
}

unsigned long iTimeToMoveServos = 0 ;
const unsigned long imskServoMoveDelta = 200 ;

void loop() {

    // Read a command character from the PC...
    char c ;
    bool bReadingSerial = false ;
    if (Serial.available() > 0) {
        // read next character
        c = Serial.read();

        bReadingSerial = true ;

        // newline is end of command
        if (c != '\n') {
            // not a newline so still collecting chars from the serial
            szCommand[iSerialCharsRead] = c;
            iSerialCharsRead++;
            digitalWrite(LedPin, LOW);
        }
    }

    // If we have some characters and the last char recieved was a newline...
    // ...we got a command, do it
    // Commands set targets and let the servos get on with it
    // Commands are in the form "VA123", "HB90"
    // V is which servo
    // A is which curve to use
    // 123 is the target angle
    if ((iSerialCharsRead > 0) && (c == '\n')) {
        // We have a command...
        szCommand[iSerialCharsRead] = 0 ; // zero terminate the command
        Serial.println(szCommand);

        // Gather data from the command string...
        char chServo = szCommand[0] ;  // 'H' or 'V' etc
        char chCurve = szCommand[1] ;  // 'A' for example
        double kNewTarget = atof(szCommand+2) ;// Degrees position of new target
        if ((kNewTarget >= 0.0) && (kNewTarget <= 180.0)) {
        // Act on the command...
        switch (chServo) {            
            case 'H':
                // Horizontally turn the head
                SetServoCurveAndTarget (scHeadHorizontal,chCurve,kNewTarget) ;
                break ;
           
            case 'V':
                // Vertically nod the head
                SetServoCurveAndTarget (scHeadVertical,chCurve,kNewTarget) ;
                break ;
           
            case 'R':
                // Move the right arm...
                SetServoCurveAndTarget (scRightArm,chCurve,kNewTarget) ;
                break ;
           
            case 'L':
                // Move the leftt arm...
                SetServoCurveAndTarget (scLeftArm,chCurve,kNewTarget) ;
                break ;
           
            default:
               // Don't understand the command, ignore it
               break ;
            }
        }
     
        // get ready to read another command
        ClearCmdBuffer () ;
    }  

    // Whethere there has been a command or not the servos carry on moving till
    // they get to their targets
    if (!bReadingSerial) {
        if (millis() > iTimeToMoveServos) {
            MoveServoOnCurveByTime (scHeadVertical) ;      
            MoveServoOnCurveByTime (scHeadHorizontal) ;      
            MoveServoOnCurveByTime (scLeftArm) ;
            MoveServoOnCurveByTime (scRightArm) ;
            iTimeToMoveServos = millis() + imskServoMoveDelta ;
        }
    }
}

    
 Note, the code above can oscillate for a while near the target position. This versione of MoveServoOnCurveByTime solves that problem, see kbSameSign...

static void MoveServoOnCurveByTime (CServoCurver& ServoCurver)
{
    const int ikSign = GetSignForServoMovement (ServoCurver) ;
    if (ikSign == 0) {
        // Servo is at target, no movement to do
        return ;
    }  

    // Get the time now. unsigned long is important
    const unsigned long imsNow = millis() ;
 
    // Get the delta time, how far into the curve you are
    unsigned long imsDelta = imsNow - ServoCurver.m_imsRealStartTime ;

    // See what speed the curve gives us at this time.  
    const double kSpeed = GetSpeedFromTimeCurve (ServoCurver.m_chCurveId,imsDelta) ;

    // Where are we now?
    const double kCurrPos = ServoCurver.m_CurrPos ;
    const double kCurrDelta = kCurrPos - ServoCurver.m_TargetPos ;

    // Where would we move to?
    const double kNewPos = ServoCurver.m_CurrPos + (ikSign*kSpeed) ;
    const double kNewDelta = kNewPos - ServoCurver.m_TargetPos ;

    // 2021-07-11 This change stops oscillations around the target
    const bool kbBothNegative = (kNewDelta < 0.0) && (kCurrDelta < 0.0) ;
    const bool kbBothPositive = (kNewDelta > 0.0) && (kCurrDelta > 0.0) ;
    const bool kbSameSign = kbBothNegative || kbBothPositive ;
 
    if (kbSameSign && (kSpeed > 0)) {
        // Update the current pos...
        ServoCurver.m_CurrPos = kNewPos ;
        
    } else {
        // Moving to the new position we would overshoot the target, so just move
        // to the target
        ServoCurver.m_CurrPos = ServoCurver.m_TargetPos ;
    }

    // Now move the actual servo
    const int ikIntPos = int(round (ServoCurver.m_CurrPos)) ;
    ServoCurver.m_ServoObj.write (ikIntPos) ;
}

    
 

 

 

 

 

 

 

 














Friday, March 19, 2021

A diabolical for-loop

Older people sometimes think that experience is knowledge. Sometimes it is, and sometimes it isn’t. In my experience.

Look at this:


Ooops now, sorry, that's it for those of you who are not technically minded. You can stop reading now, just after you consider this post sent to me by my daughter. Who must have thought, for some reason that I would appreciate the sentiment:


 Bye. Now for those remaining this...


...caused me some grief. It is the last part of a for loop:

    for (int e = 0 ; e < ikNumDegSymbols ; e = e++) {
        if (eIn == kDegsDescs[e].e) {
            return kDegsDescs[e].pszDesc;
        }
    }

What is strange is that I normally use e++, or i++ or whatever....

    for (int e = 0 ; e < ikNumDegSymbols ; e++) {

I must have been tired or dreaming when I used e = e++. But it has worked for years, this unnoticed glitch, in this one for-loop.

It worked until it didn't, one day the behaviour changed, suddenly the for loop never exited, and debugging revealed that e was stuck at was some random value, 23567 for example. At most it should have done 3 loops.

I asked some programmer friends, and they enlightened me. In the C++ language the behaviour of e=e++ is undefined! I'd never even considered that to be a possibility. Uninitialized variables are one thing, an increment and assignment being undefined is another.

Undefined behaviour means that the compiler can do WTF it wants. And an upgrade to the compiler had changed how it interpreted e = e++. I have no idea what it thinks of that operation now, seems almost like an unitialised variable.

One programmer in particular, Alessio Nava, much younger than me, said he always used ++e, the behaviour of which is defined. So now I always use ++e.

Oh well, older people sometimes think that experience is knowledge. Sometimes it is, and sometimes it isn’t. In my experience.