Control Software

Overview of the control software for Drosophibot II

Code is reproduced here, but can also be found at this public GitHub repo:

Downloading from GitHub is advised for actually running the robot, to ensure you have the most recent version.


Presently, control signals are provided by an external laptop through a U2D2 serial converter (Robotis, Seoul, South Korea) connected to the power board. The laptop runs a MATLAB script that writes servo angle commands formulated by the kinematic solver to each servo. The script then reads the present servo angles and the current draw of each servo. The MATLAB script additionally communicates over Serial communication with the OpenCM microcontroller on the robot running an Arduino (Arduino, Monza, Italy) script that collects and broadcasts strain data from the 36 strain gauge locations. The read-write loop runs between 35-40 Hz.

The following sections will describe each component of this controller architecture in greater detail.

Arduino Script

Full Arduino script
#include <Wire.h>
float targetStrainBaseline = 300;
float strainBaselineErrorMargin = 20;
int digiPotIDs[3] = {44,46,45}; //The three unique digipot IDs that each amplifier board uses
int numDigiPotIDs = 3;
int analogPorts[6] = {0, 1, 2, 3, 4, 5}; //The ports that will recieve the strain data from the amplifier boards. Each leg get a dedicated port.
int numAnalogPorts = 6;
int MUXPins[6] = {10, 11, 12, 13, 14, 15}; //The digital pins that control how the strain data is routed on the MUX
int MUXPinNum = 6;
int ampMUXPins[2] = {22,23}; //The digital pins that control the switches on the amplifier board. Used to calibrate the strain signals.
int ampMUXPinNum = 2;
int I2CMUXPins[6] = {16,17,18,19,20,21}; //The digital pins that control which leg the I2C connection is connected to. Used for calibrating the strain signals
int I2CMUXPinNum = 6;
byte wiperVal = 50; //The wiper value for the digipot from which to start the calibration
float allSGValues[6][6] = {}; //Empty array for the strain data
float value;

void setup() {
  Serial.begin(115200);
  while(!Serial); //Initialize Serial and don't continue until it's ready
  Wire.begin(); //Initialize Wire to be able to communicate with the digipots over I2C

  // Set all digital pins on the microcontroller as OUTPUTS and set them LOW
  for(int i=0;i<MUXPinNum;i++)
  {
    pinMode(MUXPins[i], OUTPUT);
    digitalWrite(MUXPins[i], LOW);
  }
  for(int i=0;i<ampMUXPinNum;i++)
  {
    pinMode(ampMUXPins[i], OUTPUT);
    digitalWrite(ampMUXPins[i], LOW);
  }
  for(int i=0;i<I2CMUXPinNum;i++)
  {
    pinMode(I2CMUXPins[i], OUTPUT);
    digitalWrite(I2CMUXPins[i], LOW);
  }
  
  //Set the chosen analog ports as inputs
  for(int i=0;i<numAnalogPorts;i++)
  {
    pinMode(analogPorts[i], INPUT);
  }

  // Wait until MATLAB code startup is complete and servos are on, then calibrate each of the strain gauges
  Serial.println("Waiting on MATLAB...");
  int waiting = 1;
  while(waiting == 1)
  {
    if(Serial.available()>0) //Check if anything has been sent by MATLAB over Serial
    {
      value = Serial.read(); //If there has been, read it
    }
    if(value>0) //If that value is something other than zero, then we have the go-ahead to start calibration from the MATLAB script
    {
      waiting = 0; //Stop waiting
      Serial.println("Starting calibration.");
    }
  }
  
  // CALIBRATION CODE
  for(int L=0;L<numAnalogPorts;L++) //FOR each each leg...
  {
    digitalWrite(I2CMUXPins[L], HIGH); // Set the appropriate pin for the I2C analog switches HIGH to direct the I2C channels to your desired leg
    for(int i=0;i<ampMUXPinNum;i++) //FOR each possible state of the switches on the amplifier board (corresponding to either "side" of the board)...
    {
      wiperVal = 50; //Begin the wiper at its midpoint
      digitalWrite(ampMUXPins[i], HIGH); //Set the corresponding pin of the switch HIGH to flip the switch and direct the I2C channels to the desired "side" of the board
      for(int j=0;j<numDigiPotIDs;j++) //FOR each of the unique digipot IDs...
      {
        int currMUXID = j+(3*i); //Calculate which strain signal ID (from 0 to 6 as notated on the amplifier boards) we're on currently based on the "side" of the board and the digipot ID
        digitalWrite(MUXPins[currMUXID], HIGH); //Set the MUX pin HIGH to connect the strain signal to that leg's dedicated analog port
        allSGValues[L][currMUXID] = analogRead(analogPorts[L]); //Collect the strain signal for that leg in the strain gage data array
        while(allSGValues[L][currMUXID] < targetStrainBaseline - strainBaselineErrorMargin || allSGValues[L][currMUXID] > targetStrainBaseline + strainBaselineErrorMargin) //WHILE the strain value isn't within our desired calibration range...
        {
          Wire.beginTransmission(digiPotIDs[j]); //Prepare to send a new wiper value to the digipot
          Wire.write(byte(0x00));
          if(allSGValues[L][currMUXID] < targetStrainBaseline - strainBaselineErrorMargin) //IF the strain value is less than the desired level, increment the wiper value
          {
            wiperVal++;
          }
          else if(allSGValues[L][currMUXID] > targetStrainBaseline + strainBaselineErrorMargin) //ELSE decrement the wiper value to lower the strain 
          {
            wiperVal--;
          }
          Wire.write(wiperVal); //Finish sending the new wiper value to the digipot
          Wire.endTransmission();
          allSGValues[L][currMUXID] = analogRead(analogPorts[L]); //Retake the strain data to see the effect of the wiper change. Repeat until you hit the desired calibration range
        }
        // Set all of the pins you set HIGH back to LOW to start the loops again
        digitalWrite(MUXPins[currMUXID], LOW);
      }
      digitalWrite(ampMUXPins[i], LOW);
    }
    digitalWrite(I2CMUXPins[L], LOW);
    Serial.print("Leg ");
    Serial.print(L);
    Serial.println(" complete.");
  }
  Serial.println("Done."); //Let MATLAB know you're done calibrating
}

void loop() {
  //Only read values and broadcast them when MATLAB asks for it by sending a value ~=0 over Serial
  if(Serial.available()>0)
  {
    value = Serial.read();
  }
  if(value>0) 
  {
   //If the MATLAB script wants Serial data, cycle through which digital pin is HIGH and collect the strain data from each leg for each state of the switches. By doing so, you can collect strain from each location on the leg for all legs simultaneously, then switch
   //which location you're collecting from.
   for(int i=0;i<MUXPinNum;i++)
  {
    digitalWrite(MUXPins[i], HIGH);
    for(int L=0;L<numAnalogPorts;L++)
    {
      allSGValues[L][i] = analogRead(analogPorts[L]);
    }
    digitalWrite(MUXPins[i], LOW);
  }
  //Print all of the collected data as one big line in Serial with a space between each point of data. The MATLAB script can then parse this data accordingly for each leg and location.
  for(int L=0;L<numAnalogPorts;L++)
  {
    for(int i=0;i<MUXPinNum;i++)
    {
      Serial.print(allSGValues[L][i]);
      Serial.print(" ");
    }
  }
  //Print the carriage return to tell the MATLAB script that this is one complete timestep of data
  Serial.write(13);
  Serial.write(10);
  //Delay for 2ms then start the read/print cycle again
  delay(2); 
  }
}

The Arduino script runs on the robot's OpenCM microcontroller and facilitates the collection of the strain data by calibrating the gauges at the start of the trial, organizing the switching of the deMUX board during data collection, and sending the strain data over Serial communication to the MATLAB program.

If strain data is not being collected for a trial, using this script is unnecessary and can be omitted.

Upon startup, the program sets all the digital pins on the microcontroller LOW and specifies them as outputs. It similarly specifies all of the analog pins as inputs, then waits for the MATLAB program to send a command over Serial to continue the program. Once this command is received, the program calibrates the rest point of all 36 strain gauges (lines 62-102) following the following process:

  1. The digital pins for a particular leg and side of the amplifier board are set HIGH to direct the I2C connections to the desired strain circuit. The digital pin for a particular strain location is also set HIGH to connect that strain circuit to that leg's analog pin.

  2. A strain reading is collected from that location and compared to a user-defined target rest value.

  3. IF the strain is not within a user-defined threshold of the target, THEN the wiper value of the circuit's digipot is changed.

    1. IF the value is less than the lower calibration limit, THEN the wiper value is incremented.

    2. IF the value is greater than the upper calibration limit, THEN the wiper value is decremented

  4. The strain is rerecorded to check the wiper value's effect on the reading.

This process is repeated until the strain reading is within the calibration range, then repeated for all strain gauge locations across all legs.

Once calibration is complete, the program enters the main strain reading loop (lines 104-138). The loop requires the MATLAB program to send a command variable requesting strain data to initiate strain reading. Otherwise, the loop does nothing. When active, the loop continuously reads strain data from each location on each leg. It accomplishes this by cycling through setting one of the six digital command pins HIGH, thereby connecting a particular location to its leg's analog pin, then reading from all six legs' pins. The program then sends the combined data from all 36 locations (allSGValues on line 128) as one line over Serial to the MATLAB program before waiting for 2 ms and continuing with another set of strain readings. In this way, the Arduino program facilitates sending calibrated strain data to the MATLAB program when requested.

MATLAB Script

Full MATLAB script
%Clarus Goldsmith
%West Virginia University
% TO RECORD STRAIN, RUN 'collectDrosophibotStrain36MUX' IN ARDUINO BEFORE
% RUNNING THIS PROGRAM

clear all
close all

legNumbers = {[1 2 3], [4 5 6], [7 8 9], [10 11 12], [13 14 15 16 17], [18 19 20 21 22]};
motorColLabels = {'LH CTr', 'LH TrF', 'LH FTi',...
    'RH CTr', 'LH TrF', 'LH FTi',...
    'LM CTr', 'LM TrF', 'LM FTi',...
    'RH CTr', 'LH TrF', 'LH FTi',...
    'LF ThC1', 'LF ThC3', 'LF CTr', 'LF TrF', 'LF FTi',...
    'RF ThC1', 'RF ThC3', 'RF CTr', 'RF TrF', 'RF FTi'};

U2D2Port = 'COM3';       % The port that the U2D2 is using
arduinoPort = "COM4";

numSteps = input('Desired number of steps?: ');
numStepsSuccess = 0;
while numStepsSuccess == 0
    if rem(numSteps,1) == 0
        if numSteps >= 1
            numStepsSuccess = 1;
        else
            warning('Invalid entry. Must be an integer greater than or equal to 1.')
            numSteps = input('Desired number of steps?: ');
        end
    else
        warning('Invalid entry. Must be an integer.')
        numSteps = input('Desired number of steps?: ');
    end
end

backward = 0;
pertYN = input('Trial with perturbation (Y/N)?: ',"s");
pertStateSuccess = 0;
while pertStateSuccess == 0
    if strcmp(pertYN, 'Y') || strcmp(pertYN, 'y') || strcmp(pertYN, 'N') || strcmp(pertYN, 'n')
        if strcmp(pertYN, 'Y') || strcmp(pertYN, 'y')
            pert = 1;
            pertLeg = input('What leg should be perturbed (1-6)?: ');
            pertSuccess = 0;
            while pertSuccess == 0
                if rem(pertLeg,1) == 0
                    if pertLeg <= 6 && pertLeg >= 1
                        pertSuccess = 1;
                    else
                        warning('Invalid entry. Must be an integer between 1-6.')
                        pertLeg = input('What leg should be perturbed (1-6)?: ');
                    end
                else
                    warning('Invalid entry. Must be an integer between 1-6.')
                    pertLeg = input('What leg should be perturbed (1-6)?: ');
                end
            end
            pertStep = input('During what step should the perturbation take place?: ');
            pertSuccess = 0;
            while pertSuccess == 0
                if rem(pertStep,1) == 0
                    if pertStep <= numSteps && pertStep >= 1
                        pertSuccess = 1;
                    else
                        warning('Invalid entry. Perturbation must be within step number defined by numSteps.')
                        pertStep = input('During what step should the perturbation take place?: ');
                    end
                else
                    warning('Invalid entry. Must be an integer.')
                    pertStep = input('During what step should the perturbation take place?: ');
                end
            end
        else
            pert = 0;
        end
        pertStateSuccess = 1;
    else
        warning('Invalid entry. Must be Y/N.')
        pertYN = input('Trial with perturbation (Y/N)?: ',"s");
    end
end
% substrate = 'wire mesh';
% substrate = 'anti-slip mat';
substrate = 'sand paper';

% simulationPath = "G:\Other computers\NeuroMINT Lab Computer\MATLAB\Design Drosophibot\Drosophibot 2\Flat\k_spring = 0\T_swing = 0.8\phi_I = 0.5\phi_C = 0.5\allVars.mat";
simulationPath = "G:\Other computers\NeuroMINT Lab Computer\MATLAB\Design Drosophibot\Drosophibot 2 Alt Hind Legs\Flat\k_spring = 0\T_swing = 0.8\phi_I = 0.5\phi_C = 0.5\allVars.mat";
% simulationPath = "C:\Users\Clarissa G\Documents\MATLAB\Design Drosophibot\Drosophibot 2 Alt Hind Legs\Flat\k_spring = 0\T_swing = 0.8\phi_I = 0.5\phi_C = 0.5\allVars.mat";
savePath = 'G:\Other Computers\NeuroMINT Lab Computer\MATLAB\Running Drosophibot\Trials\';

%% Extract the goal positions based on a trial of solver data
%Load the necessary variables from a save of the Design Drosophibot code
vars = {'numJoints','numLegs','numSegs','n','actuator','A','thetaPlot','Tactuator','stepPeriod','stanceDuty','stepHeight','floorLevel','springK','contralateralPhase','ipsalateralPhase','bodyTranslation','configName','terrainShape'};
load(simulationPath,vars{1:9})
solverParams = load(simulationPath,vars{7:end});
solverParams.solverPath = simulationPath;

%Put the thetas into a big matrix where each column corresponds to the
%appropriate servo ID + 1
goalPositions = zeros(n,numJoints);
goalTorques = zeros(n,numJoints);

i = 1;
for l=1:numLegs
    for j=1:numSegs(l)
        if norm(thetaPlot{l}(:,j)) ~= 0
            goalPositions(:,i) = thetaPlot{l}(:,j);
            goalTorques(:,i) = Tactuator{l}(:,j);
            i = i+1;
        end
    end
end
stanceIDs = [];
%Find at which timestep all legs are in stance in the kinematics data
for t=1:length(A)/2
    [Arows(t),~] = size(A{t});
    if Arows(t) < 200
        stanceIDs = [stanceIDs t];
    end
end
%Use this time to identify how much the kinematics should be shifted by
shiftAmnt = mean(stanceIDs);
shiftAmnt = shiftAmnt-1;

%Bias the goalPositions based on the calculated torques and the
%stiffnesses
goalPositionsBiased = goalPositions + goalTorques/actuator.k;

%Convert the thetas from radians into servo bits
%From radians to deg:
goalPositions = goalPositions*180/pi;
goalPositionsBiased = goalPositionsBiased*180/pi;
%From deg to bits:
goalPositions = goalPositions/.088;
goalPositionsBiased = goalPositionsBiased/.088;
goalPositions = round(goalPositions);
goalPositionsBiased = round(goalPositionsBiased);
goalPositions = goalPositions + 2048;
goalPositionsBiased = goalPositionsBiased + 2048;

%Find how we have to downsample the data to make sure the bandwidth will not be too fast for
%the U2D2 to handle
minTimeStep = 0.03;
idealTimeStep = stepPeriod/n;
x = ceil(minTimeStep/idealTimeStep);
actTimeStep = idealTimeStep*x;

%Actually shift the data so all legs start in the middle of stance
goalPositions = circshift(goalPositions,shiftAmnt,1);
goalPositionsBiased = circshift(goalPositionsBiased,shiftAmnt,1);

numIters = ceil((stepPeriod*numSteps)/actTimeStep)+1; %Calculate the number of iterations based on the read/write timestep
%Initialize indexing variables
index = 2;
indexCont = 2;
servoIndex = 2;
strainIndex = 1;
strainStepInds = [];
simInds = 1;
s = 1;
pertInds = [];

%Chain the goal positions together for the number of steps desired
dxl_goal_position(1,:) = goalPositionsBiased(1,:);
% dxl_goal_position(1,:) = goalPositions(1,:);
applyPert = 0;
pertCount = 1;
while indexCont <= numIters
    dxl_goal_position(indexCont,:) = goalPositionsBiased(index,:);
    % dxl_goal_position(indexCont,:) = goalPositions(index,:);
    if applyPert && pertCount <= 5
        dxl_goal_position(indexCont,legNumbers{pertLeg}(1)) = dxl_goal_position(indexCont,legNumbers{pertLeg}(1))-1000;
        dxl_goal_position(indexCont,legNumbers{pertLeg}(end)) = dxl_goal_position(indexCont,legNumbers{pertLeg}(end))+1000;
        pertInds = [pertInds indexCont];
        pertCount = pertCount+1;
    end
    simInds = [simInds index];
    index = index+x;
    if index > n
        index = index - n;
        s = s+1;
        if pert
            if s == pertStep
                applyPert = 1;
            end
        end
    end
    indexCont = indexCont+1;
end

if backward %If we want to walk backward, flip the goalPositions
    dxl_goal_position = flip(dxl_goal_position);
end

strainYN = input('Record Strain (Y/N)?: ',"s");
strainInSuccess = 0;
while strainInSuccess == 0
    if strcmp(strainYN, 'Y') || strcmp(strainYN, 'y') || strcmp(strainYN, 'N') || strcmp(strainYN, 'n')
        if strcmp(strainYN, 'Y') || strcmp(strainYN, 'y')
            strainBool = 1;
        else
            strainBool = 0;
        end
        strainInSuccess = 1;
    else
        warning('Invalid input. Must be Y/N.')
        strainYN = input('Record Strain (Y/N)?: ',"s");
    end
end

%% Run the robot trial
stepNum = 1;
pertGo = 0;

if strainBool
    %Labels for each column of the strain data
    strainColLabels = {'LH Troch. Axial','LH Troch. Transverse', 'LH Troch. 45^{\circ}', 'LH Femur +45^{\circ}', 'LH Femur -45^{\circ}','LH Femur Axial',...
        'RH Troch. Axial','RH Troch. Transverse', 'RH Troch. 45^{\circ}', 'RH Femur +45^{\circ}', 'RH Femur -45^{\circ}','RH Femur Axial',...
        'LM Troch. Axial','LM Troch. Transverse', 'LM Troch. 45^{\circ}', 'LM Femur +45^{\circ}', 'LM Femur -45^{\circ}','LM Femur Axial',...
        'RM Troch. Axial','RM Troch. Transverse', 'RM Troch. 45^{\circ}', 'RM Femur +45^{\circ}', 'RM Femur -45^{\circ}','RM Femur Axial',...
        'LF Troch. Axial','LF Troch. Transverse', 'LF Troch. 45^{\circ}', 'LF Femur +45^{\circ}', 'LF Femur -45^{\circ}','LF Femur Axial',...
        'RF Troch. Axial','RF Troch. Transverse', 'RF Troch. 45^{\circ}', 'RF Femur +45^{\circ}', 'RF Femur -45^{\circ}','RF Femur Axial'};

    %Define the arduino serial connection information
    arduinoObj = serialport(arduinoPort,115200);
    configureTerminator(arduinoObj,"CR/LF");
    flush(arduinoObj);
end

pause('on')
lib_name = '';

%Determine which specific library we need based on operating system
if strcmp(computer, 'PCWIN')
    lib_name = 'dxl_x86_c';
elseif strcmp(computer, 'PCWIN64')
    lib_name = 'dxl_x64_c';
elseif strcmp(computer, 'GLNX86')
    lib_name = 'libdxl_x86_c';
elseif strcmp(computer, 'GLNXA64')
    lib_name = 'libdxl_x64_c';
elseif strcmp(computer, 'MACI64')
    lib_name = 'libdxl_mac_c';
end

% Load libraries for Dynamixels
if ~libisloaded(lib_name)
    [notfound, warnings] = loadlibrary(lib_name, 'dynamixel_sdk.h', 'addheader', 'port_handler.h', 'addheader', 'packet_handler.h', 'addheader', 'group_sync_write.h', 'addheader', 'group_sync_read.h');
end

% Control table addresses
ADDR_TORQUE_ENABLE          = 64;                 % Control table address is different in Dynamixel model
ADDRGOAL_POSITION           = 116;
ADDR_PRESENT_POSITION       = 132;
ADDR_PRESENT_LOAD           = 126;
ADDR_PROFILE_VELOCITY       = 112;

% Data Byte Lengths
LEN_GOAL_POSITION       = 4;
LEN_PRESENT_POSITION    = 4;
LEN_PRESENT_LOAD        = 2;
LEN_PROFILE_VELOCITY    = 4;

% Protocol version
PROTOCOL_VERSION            = 2.0;          % See which protocol version is used in the Dynamixel

ids = linspace(0,numJoints-1,numJoints);
DXL_ID                     = ids;
BAUDRATE                    = 1000000;

TORQUE_ENABLE               = 1;            % Value for enabling the torque
TORQUE_DISABLE              = 0;            % Value for disabling the torque
DXL_MOVING_STATUS_THRESHOLD = 5;           % Dynamixel moving status threshold
DXL_PROFILE_VELOCITY        = 62*2;

ESC_CHARACTER               = 'e';          % Key for escaping loop

COMM_SUCCESS                = 0;            % Communication Success result value
COMM_TX_FAIL                = -1001;        % Communication Tx Failed

% Initialize PortHandler Structs
% Set the port path
% Get methods and members of PortHandlerLinux or PortHandlerWindows
port_num = portHandler(U2D2Port);

% Initialize PacketHandler Structs
packetHandler();

% Initialize Groupsyncwrite Structs
groupwrite_num = groupSyncWrite(port_num, PROTOCOL_VERSION, ADDRGOAL_POSITION, LEN_GOAL_POSITION);

% Initialize Groupsyncread Structs for Present Position
groupread_num = groupSyncRead(port_num, PROTOCOL_VERSION, ADDR_PRESENT_POSITION, LEN_PRESENT_POSITION);
groupread_num2 = groupSyncRead(port_num, PROTOCOL_VERSION, ADDR_PRESENT_LOAD, LEN_PRESENT_LOAD);


dxl_comm_result = COMM_TX_FAIL;           % Communication result
dxl_addparam_result = false;              % AddParam result
dxl_getdata_result = false;               % GetParam result

dxl_error = 0;                              % Dynamixel error
%Create empty vectors for the loads and positions
dxl_present_position = zeros(numIters,numJoints);
dxl_present_load = zeros(numIters,numJoints);
timeVec = zeros(1,numIters);
timeVecInds = 1;
timeVecStrain = zeros(1,numIters*5);
strainDataRaw = strings(numIters*5,1);
earlyTerm = 0;

% Open the U2D2 port
if (openPort(port_num))
    fprintf('Succeeded to open the port!\n');
else
    unloadlibrary(lib_name);
    fprintf('Failed to open the port!\n');
    input('Press any key to terminate...\n');
    return;
end


% Set U2D2 port baudrate
if (setBaudRate(port_num, BAUDRATE))
    fprintf('Succeeded to change the baudrate!\n');
else
    unloadlibrary(lib_name);
    fprintf('Failed to change the baudrate!\n');
    input('Press any key to terminate...\n');
    return;
end



for i=1:numJoints
    % Enable Dynamixel Torques
    write1ByteTxRx(port_num, PROTOCOL_VERSION, DXL_ID(i), ADDR_TORQUE_ENABLE, TORQUE_ENABLE);
    dxl_comm_result = getLastTxRxResult(port_num, PROTOCOL_VERSION);
    dxl_error = getLastRxPacketError(port_num, PROTOCOL_VERSION);
    if dxl_comm_result ~= COMM_SUCCESS
        fprintf('%s\n', getTxRxResult(PROTOCOL_VERSION, dxl_comm_result));
    elseif dxl_error ~= 0
        fprintf('%s\n', getRxPacketError(PROTOCOL_VERSION, dxl_error));
    else
        fprintf('Dynamixel #%d has been successfully connected \n', DXL_ID(i));
    end

    %Set Dynamixel velocities
    write4ByteTxRx(port_num, PROTOCOL_VERSION, DXL_ID(i), ADDR_PROFILE_VELOCITY, DXL_PROFILE_VELOCITY);
    dxl_comm_result = getLastTxRxResult(port_num, PROTOCOL_VERSION);
    dxl_error = getLastRxPacketError(port_num, PROTOCOL_VERSION);
    if dxl_comm_result ~= COMM_SUCCESS
        fprintf('%s\n', getTxRxResult(PROTOCOL_VERSION, dxl_comm_result));
    elseif dxl_error ~= 0
        fprintf('%s\n', getRxPacketError(PROTOCOL_VERSION, dxl_error));
    else
        fprintf('Set velocity in Dynamixel #%d successfully \n', DXL_ID(i));
    end

    dxl_addparam_result = groupSyncReadAddParam(groupread_num, DXL_ID(i));
    if dxl_addparam_result ~= true
        fprintf('[ID:%03d] groupSyncRead addparam failed', DXL_ID(i));
        return;
    end

    dxl_addparam_result = groupSyncReadAddParam(groupread_num2, DXL_ID(i));
    if dxl_addparam_result ~= true
        fprintf('[ID:%03d] groupSyncRead addparam failed', DXL_ID(i));
        return;
    end
end

%Initialize the joints at the first position in goalPositions
if input('Press any key to continue! (or input e to quit!)\n', 's') == ESC_CHARACTER
    earlyTerm = 1;
else
    for i=1:numJoints
        % Add Dynamixel goal position value to the Syncwrite storage
        dxl_addparam_result = groupSyncWriteAddParam(groupwrite_num, DXL_ID(i), typecast(int32(dxl_goal_position(1,i)), 'uint32'), LEN_GOAL_POSITION);
        if dxl_addparam_result ~= true
            fprintf('[ID:%03d] groupSyncWrite addparam failed', DXL_ID(i));
            return;
        end
    end
    groupSyncWriteTxPacket(groupwrite_num);
    dxl_comm_result = getLastTxRxResult(port_num, PROTOCOL_VERSION);
    if dxl_comm_result ~= COMM_SUCCESS
        fprintf('%s\n', getTxRxResult(PROTOCOL_VERSION, dxl_comm_result));
    end

    % Clear syncwrite parameter storage
    groupSyncWriteClearParam(groupwrite_num);

    % Syncread present position
    groupSyncReadTxRxPacket(groupread_num);
    dxl_comm_result = getLastTxRxResult(port_num, PROTOCOL_VERSION);
    if dxl_comm_result ~= COMM_SUCCESS
        fprintf('%s\n', getTxRxResult(PROTOCOL_VERSION, dxl_comm_result));
    end

    %Syncread present load
    groupSyncReadTxRxPacket(groupread_num2);
    dxl_comm_result = getLastTxRxResult(port_num, PROTOCOL_VERSION);
    if dxl_comm_result ~= COMM_SUCCESS
        fprintf('%s\n', getTxRxResult(PROTOCOL_VERSION, dxl_comm_result));
    end

    % Get each Dynamixel present position and load value
    for i=1:numJoints
        dxl_present_position(1,i) = groupSyncReadGetData(groupread_num, DXL_ID(i), ADDR_PRESENT_POSITION, LEN_PRESENT_POSITION);
        dxl_present_load(1,i) = groupSyncReadGetData(groupread_num2, DXL_ID(i), ADDR_PRESENT_LOAD, LEN_PRESENT_LOAD);
    end

end

%Warm up tic/toc
for ii=1:100
    t_wus = tic;
    t_wuf = toc(t_wus);
end

if strainBool
    if ~earlyTerm
        if input('Press any key to calibrate the strain gauges. (or input e to quit!)\n', 's') == ESC_CHARACTER
            earlyTerm = 1;
        else
            write(arduinoObj,1,"uint8"); %Tell the arduino to start the calibration
            
            data = readline(arduinoObj) %Intentionally left without semicolon to report out the 'Done' provided by the Arduino script
            loopDone = 0;
            while loopDone == 0
                data = readline(arduinoObj)
                if strcmp(data,"Done.")
                    loopDone = 1;
                elseif isempty(data)
                    loopDone = 1;
                    earlyTerm = 1;
                    warning('Calibration not completed. Check strain gauge cables on leg for disconnections/breakages.')
                    input('Press any key to terminate...\n');
                end
            end
        end
        write(arduinoObj,0,"uint8"); %Tell the arduino to stop streaming data over Serial
        fprintf('Strain gauge calibration complete. \n')
    end
    if ~earlyTerm
        if input('Press any key to collect zero data for the strain gauges. (or input e to quit!)\n', 's') == ESC_CHARACTER
            earlyTerm = 1;
        else
            write(arduinoObj,1,"uint8"); %Tell the arduino to start streaming data over Serial
            for sd = 1:20
                t_strs = tic;
                %Collect 20 points of strain data to average to find where
                %the unloaded value is
                strainDataRawZero(sd,:) = readline(arduinoObj);
                t_strf(sd) = toc(t_strs);
            end
            t_str = mean(t_strf);
        end
        write(arduinoObj,0,"uint8"); %Tell the arduino to stop streaming data over Serial
        fprintf('Strain gauge zero data collected. \n')
    end
end

if ~earlyTerm
    if input('Press any key to run the robot! (or input e to quit!)\n', 's') == ESC_CHARACTER
        earlyTerm = 1;
    end
end
loopTime = [];

%% Begin loop for running the whole stepping
if ~earlyTerm
    if strainBool
        write(arduinoObj,1,"uint8"); %Tell arduino to start streaming data over Serial
    end
    if strainBool %If recording strain, record it to get a baseline number before moving.
        strainDataRaw(strainIndex,:) = readline(arduinoObj);
        timeVecStrain(strainIndex) = 0;
        strainIndex = strainIndex + 1;
        strainStepInds(strainIndex) = simInds(1);
    end
    fr = 2;
    t_s = tic;
    while fr <= numIters
        t_ls = tic;
        for i=1:numJoints
            % Add Dynamixel goal position value to the Syncwrite storage
            dxl_addparam_result = groupSyncWriteAddParam(groupwrite_num, DXL_ID(i), typecast(int32(dxl_goal_position(fr,i)), 'uint32'), LEN_GOAL_POSITION);
            if dxl_addparam_result ~= true
                fprintf('[ID:%03d] groupSyncWrite addparam failed', DXL_ID(i));
                return;
            end
        end

        % Syncwrite goal position
        groupSyncWriteTxPacket(groupwrite_num);
        dxl_comm_result = getLastTxRxResult(port_num, PROTOCOL_VERSION);
        if dxl_comm_result ~= COMM_SUCCESS
            fprintf('%s\n', getTxRxResult(PROTOCOL_VERSION, dxl_comm_result));
        end

        % Clear syncwrite parameter storage
        groupSyncWriteClearParam(groupwrite_num); 

        % Syncread present position
        groupSyncReadTxRxPacket(groupread_num);
        dxl_comm_result = getLastTxRxResult(port_num, PROTOCOL_VERSION);
        if dxl_comm_result ~= COMM_SUCCESS
            fprintf('%s\n', getTxRxResult(PROTOCOL_VERSION, dxl_comm_result));
        end

        %Syncread present load
        groupSyncReadTxRxPacket(groupread_num2);
        dxl_comm_result = getLastTxRxResult(port_num, PROTOCOL_VERSION);
        if dxl_comm_result ~= COMM_SUCCESS
            fprintf('%s\n', getTxRxResult(PROTOCOL_VERSION, dxl_comm_result));
        end

        % Get each Dynamixel present position and load value
        for i=1:numJoints
            dxl_present_position(servoIndex,i) = groupSyncReadGetData(groupread_num, DXL_ID(i), ADDR_PRESENT_POSITION, LEN_PRESENT_POSITION);
            dxl_present_load(servoIndex,i) = groupSyncReadGetData(groupread_num2, DXL_ID(i), ADDR_PRESENT_LOAD, LEN_PRESENT_LOAD);
        end
        timeVec(servoIndex) = toc(t_s);
        timeVecInds(servoIndex) = simInds(fr);
        servoIndex = servoIndex + 1;

        t_lf = toc(t_ls);
        while t_lf < actTimeStep
            if strainBool %If recording strain, record it.
                if t_lf < actTimeStep - t_str*3
                    strainDataRaw(strainIndex,:) = readline(arduinoObj);
                    timeVecStrain(strainIndex) = toc(t_s);
                    strainIndex = strainIndex + 1;
                    strainStepInds(strainIndex) = simInds(fr);
                end
            end
            t_lf = toc(t_ls);
        end
        loopTime = [loopTime, t_lf];
        fr = fr+1;
    end
end
%% Once the trial is done, turn off the robot, post-process the data and save it
if ~earlyTerm
    input('Press any key to disable torques. \n')
end

% Disable each Dynamixel Torque
for i=1:numJoints
    write1ByteTxRx(port_num, PROTOCOL_VERSION, DXL_ID(i), ADDR_TORQUE_ENABLE, TORQUE_DISABLE);
    dxl_comm_result = getLastTxRxResult(port_num, PROTOCOL_VERSION);
    dxl_error = getLastRxPacketError(port_num, PROTOCOL_VERSION);
    if dxl_comm_result ~= COMM_SUCCESS
        fprintf('%s\n', getTxRxResult(PROTOCOL_VERSION, dxl_comm_result));
    elseif dxl_error ~= 0
        fprintf('%s\n', getRxPacketError(PROTOCOL_VERSION, dxl_error));
    end
end

if strainBool
    %Stop the arduino board transmitting over serial and close the port
    write(arduinoObj,0,"uint8");
    clear arduinoObj
end

% Close U2D2 port
closePort(port_num);

% Unload Library
unloadlibrary(lib_name);

if ~earlyTerm
    saveBool = input('Save the current trial (1 for yes, 0 for no): ');
    if saveBool

        dxl_present_load_pp = dxl_present_load;
        %Convert the torque values into meaningful numbers
        for i=1:length(dxl_present_load)
            for j=1:22
                if dxl_present_load(i,j) > 60000
                    dxl_present_load_pp(i,j) = dxl_present_load(i,j) - 2^16;
                end
            end
        end

        for k=1:22
            if any(k == [1 4 7 10 15 20])
                dxl_present_load_pp(:,k) = dxl_present_load_pp(:,k)/1941;
                dxl_present_load_pp(:,k) = dxl_present_load_pp(:,k)*6;
            else
                dxl_present_load_pp(:,k) = dxl_present_load_pp(:,k)*.001;
                dxl_present_load_pp(:,k) = dxl_present_load_pp(:,k)*2.5;
            end
        end

        if strainBool
            for k=1:length(strainDataRawZero)
                strainDataSepZero = split(strainDataRawZero(k,:),' ')';
                for kk=1:36
                    strainDataRawNumsZero(k,kk) = str2double(strainDataSepZero(1,kk));
                end
            end
            for k=1:length(strainDataRaw)
                strainDataSep = split(strainDataRaw(k,:),' ')';
                for kk=1:length(strainDataSep)
                    strainDataRawNums(k,kk) = str2double(strainDataSep(1,kk));
                end
            end
        end
 
        strainDataRawNums = strainDataRawNums(:,1:36);
        strainDataRawNumsZero = strainDataRawNumsZero(:,1:36);

        trialInfo = cell(9,1);
        if ~backward
            trialInfo{1} = 'Forward walking';
        else
            trialInfo{1} = 'Backward walking';
        end

        trialInfo{2} = ['Substrate: ' substrate];
        trialInfo{3} =  [num2str(stepPeriod) ' s step'];

        if strainBool
            trialInfo{4} = 'Strain Recorded?: Yes';
        else
            trialInfo{4} = 'Strain Recorded?: No';
        end

        if pert
            trialInfo{5} = 'Perturbation?: Yes';
            trialInfo{6} = ['Pert. Leg: ' num2str(pertLeg)];
            trialInfo{7} = ['Pert. Step: ' num2str(pertStep)];
        else
            trialInfo{5} = 'Perturbation?: No';
        end

        trialInfo{8} = ['Actuator Profile Velocity: ' num2str(DXL_PROFILE_VELOCITY)];

        note = input('Additional notes about this trial: ','s');
        trialInfo{end} = ['Additional Notes: ' note];

        tab = table(trialInfo);
        date = datetime;
        date.Format = 'yyyy-MM-dd HH_mm_ss';
        currDate = convertStringsToChars(string(date));

        mkdir(savePath,currDate)

        if pert
            if strainBool
                save([savePath currDate '\robotData.mat'],'motorColLabels','dxl_goal_position','dxl_present_position','actTimeStep','dxl_present_load_pp','simInds','numSteps','timeVec','timeVecInds','strainDataRaw','strainDataRawNums','strainDataRawZero','strainDataRawNumsZero','timeVecStrain','strainStepInds','pertInds','pertLeg','pertStep','solverParams','strainColLabels')
            else
                save([savePath currDate '\robotData.mat'],'motorColLabels','dxl_goal_position','dxl_present_position','actTimeStep','dxl_present_load_pp','simInds','numSteps','timeVec','timeVecInds','pertInds','pertLeg','pertStep','solverParams')
            end
        else
            if strainBool
                save([savePath currDate '\robotData.mat'],'motorColLabels','dxl_goal_position','dxl_present_position','actTimeStep','dxl_present_load_pp','simInds','numSteps','timeVec','timeVecInds','strainDataRaw','strainDataRawNums','strainDataRawZero','strainDataRawNumsZero','timeVecStrain','strainStepInds','solverParams','strainColLabels')
            else
                save([savePath currDate '\robotData.mat'],'motorColLabels','dxl_goal_position','dxl_present_position','actTimeStep','dxl_present_load_pp','simInds','numSteps','timeVec','timeVecInds','solverParams')
            end
        end
        writetable(tab,[savePath currDate '\README.txt'])

        %Write trial description to the master text file
        writelines(['---- ' currDate ' ----'],[savePath '\TRIAL INDEX.txt'],WriteMode = 'append')
        trialDesc = [trialInfo{3} '. ' trialInfo{1} '. ' note];
        writelines(trialDesc,[savePath '\TRIAL INDEX.txt'],WriteMode = 'append')
        writelines('   ',[savePath '\TRIAL INDEX.txt'],WriteMode = 'append')
    end
end

The current function of the MATLAB script is to control the robot using feedforward kinematic replay from the solver, then collect and record sensory data from the servos and the strain gauges. It does so by first importing DoF angle and torque data from the output of an instance of the solver defined by the user (lines 87-96). This data is then preprocessed before the trial begins (lines 100-189). To account for differences in component rigidity between the solver and the physical platform (e.g., gear backlash, 3-D printed material elasticity, foot slippage) causing unwanted deflections and sagging, a bias term is added to the solver's angle commands equal to the solver's expected actuator torque divided by the stiffness coefficient of the actuator (i.e., the proportional feedback gain scaled to Nm/rad, kP; line 127). These changes bring the posture of the robot closer to the planned posture in the solver. The biased DoF angle data is then converted from radians to bits to be suitable for sending as actuator commands (lines 131-139). Once the angle data is biased and converted, the program determines at what timestep all legs are in stance (lines 114-123) and shifts the data by that amount (149-150). This shift ensures that all legs begin on the ground for each walking trial. Finally, the data is down-sampled to ensure the correct amount of points for the robot's read-write frequency (lines 144-146). Based on the user-defined number of steps for the trial and the length of the downsampled step data, a numIters variable is defined as the total number of timesteps for the trial (line 152).

Once this pre-processing is completed, the robot control portion of the program begins. Connection to each of the robot's actuators is established and the desired internal parameters (maximum velocity, KP value, etc.) are set in each servomotor. Each actuator is then commanded to the first position in the walking command data. If data is to be recorded from the strain gauges, the program sends a signal over Serial to the OpenCM to start sensor calibration following a button-press. Once it receives a completion indicator from the OpenCM, another button-press has the program command the OpenCM to stream 20 iteration's worth of strain data. This data is later averaged to find the baseline value of each gauge and normalize the strain data post hoc. One last button-press halts the Arduino streaming over Serial and begins the stepping control loop.

The stepping control loop cycles numIters times, each time first commanding the servos to the appropriate angle in the step cycle. The code then continuously reads the actual position and load until the loop time reaches the designated duration actTimeStep specified during the downsampling in order to give the servos time to move. If strain is being recorded, the strain data is additionally commanded from the OpenCM and recorded during this waiting period. Once the loop has executed numIters times, the robot stops walking and the trial ends. A button-press disables the torques for each servo, and the code post-processes the data before saving it to a user-defined location. This post-processing primarily entails converting the angles and loads from bit values into radian and Newton-meter values, respectively, as well as parsing the strings of strain data into a matrix form. The output from each trial is a .mat file with the raw and post-processed data, as well as a text file documenting the details of the trial (number of steps, terrain, step period, etc.).

Last updated

Was this helpful?