bookmark_borderBias Tape Maker – Work In Progress

Here’s a quick video of a machine I’m working on. It folds and irons a strip of fabric, similar to this machine: “Simplicity Bias Tape Maker“.

It’s functioning nicely, I just want to add an insulated handle to the top to prevent burns. The heating element is well insulated, the body doesn’t heat up with the exception of the two bolts that hold the floating top plate on… which will definitely burn you.

The temperature is variable and has closed loop control. Motor speed is also adjustable, in the video I have it running fairly slow. As you can see I’m not great with textiles, so my strip isn’t very consistent and has some big lumps at the seams.

bookmark_borderMail Merge Alternative – Send Outlook Email From Excel

Here is an example application for some of the functions I have posted previously. This sheet will take a list of recipients as an input and generate an Outlook email for each. This came about when a colleague of mine was using the traditional mail merge in office for the first time and ran into a roadblock trying to add a CC to each message. I am not an expert with mail merge, but when I looked around the first search results suggested using a rule in Outlook as a workaround. Here’s my workaround and what it does:

  • Variables in the email body/subject can be entered in a similar way to mail merge, using this format <<variable>>. Just like a mad-lib.
  • The same attachment can be added to each message, or a unique attachment can be added for each entry on the list
  • Multiple recipients are added to the list by separating them with a semicolon
  • CC and BCC are supported
  • The default outlook signature can be used
  • Messages can be sent immediately, or displayed in the editor to be sent manually

Ultimately this is also a workaround, but there are certainly some situations where this can be useful. It’s probably not right if you’re running an email marketing campaign, or as a replacement for billing automation software; I think it is suited to one-off or infrequent tasks like:

  • Emailing unique coupon or gift certificate codes
  • Sending a reminder to vendors while including their specific account numbers
  • “Personalizing” an email that you would otherwise just CC or BCC to group of recipients.
  • Speeding up a task where humans are copy/pasting details in to a pre-written message repeatedly

This also makes it easy to be a spammer, please don’t do that. Here is the file:

Also I used some hints from Ron de Bruin to get the signature working (thanks!): https://www.rondebruin.nl/win/s1/outlook/signature.htm. Here is the subroutine that does the work, the functions it requires can be found separately here.

Sub sendEmail()
'by Elliot 8/8/20 www.elliotmade.com
'check the sheet "Instructions" for more info
'tested with office 2010 and 2013
'method for using outlook default signature taken from here: https://www.rondebruin.nl/win/s1/outlook/signature.htm

Dim rSheetName As String, vSheetName As String, iSheetName As String

'-------------------------------------
rSheetName = "Recipients"
vSheetName = "Variables"
iSheetName = "Instructions"
'-------------------------------------

Dim rSht As Worksheet, vSht As Worksheet
Set rSht = Worksheets(rSheetName)
Set vSht = Worksheets(vSheetName)
Dim rLastRow As Long, vLastRow As Long
rLastRow = getLastRow(rSheetName, 1)
vLastRow = getLastRow(vSheetName, 1)

'fixed inputs
Dim subject As String, greeting As String, body As String, signature As String, att1 As String, att2 As String, att3 As String
subject = vSht.Cells(getRowNum("Subject", vSheetName), 2).Value2
greeting = vSht.Cells(getRowNum("Greeting", vSheetName), 2).Value2
body = vSht.Cells(getRowNum("Body", vSheetName), 2).Value2
signature = vSht.Cells(getRowNum("Signature", vSheetName), 2).Value2
att1 = vSht.Cells(getRowNum("Attachment 1", vSheetName), 2).Value2
att2 = vSht.Cells(getRowNum("Attachment 2", vSheetName), 2).Value2
att3 = vSht.Cells(getRowNum("Attachment 3", vSheetName), 2).Value2

Dim mode As String, sigMode As String
mode = vSht.Cells(getRowNum("Send Mode", vSheetName), 2).Value2
sigMode = vSht.Cells(getRowNum("Signature Mode", vSheetName), 2).Value2

Dim fixedVArray As Variant
Dim fixedVCount As Long

If vSht.Cells(getLastRow(vSheetName, 1), 1).Value2 = "Variables" Then GoTo skipFixedVariables 'skip fixed variables if there are none

fixedVArray = vSht.Range(vSht.Cells(getRowNum("Variables", vSheetName, 1) + 1, 1), vSht.Cells(vLastRow, 2)).Value
fixedVCount = UBound(fixedVArray, 1)

'replace the fixed variables into each part of the email
Dim i As Long
For i = 1 To fixedVCount
    If InStr(1, subject, "<<") Then
        subject = Replace(subject, fixedVArray(i, 1), fixedVArray(i, 2))
    End If
    If InStr(1, greeting, "<<") Then
        greeting = Replace(greeting, fixedVArray(i, 1), fixedVArray(i, 2))
    End If
    If InStr(1, body, "<<") Then
        body = Replace(body, fixedVArray(i, 1), fixedVArray(i, 2))
    End If
    If InStr(1, signature, "<<") Then
        signature = Replace(signature, fixedVArray(i, 1), fixedVArray(i, 2))
    End If
Next i

skipFixedVariables:

Dim curSubject As String, curGreeting As String, curBody As String, curSignature As String

'now go through the list of recipients, update each part of the email, then create and send it
Dim listArray As Variant 'columns 1 thru 4 are fixed: TO, CC, BCC, Attachment.  Any number of variable columns can follow
Dim listRowCount As Long, listColCount As Long
listRowCount = getLastRow(rSheetName)
listColCount = getLastCol(rSheetName)

If listRowCount < 2 Then 'don't proceed if the list is empty
    MsgBox "Nothing found in the recipient list"
    Exit Sub
End If

listArray = rSht.Range(rSht.Cells(1, 1), rSht.Cells(listRowCount, listColCount)).Value
Dim j As Long
Dim k As Long
Dim outlookApp As Object
Dim outlookEmail As Object

Set outlookApp = CreateObject("Outlook.Application")

For j = 2 To UBound(listArray, 1)
    'reset the temporary message parts
    curSubject = subject
    curGreeting = greeting
    curBody = body
    curSignature = signature
    
    If UBound(listArray, 2) > 4 Then 'if there are variable columns, replace each of those into the current message
        For k = 5 To UBound(listArray, 2)
            If InStr(1, curSubject, "<<") Then
                curSubject = Replace(curSubject, listArray(1, k), listArray(j, k))
            End If
            If InStr(1, curGreeting, "<<") Then
                curGreeting = Replace(curGreeting, listArray(1, k), listArray(j, k))
            End If
            If InStr(1, curBody, "<<") Then
                curBody = Replace(curBody, listArray(1, k), listArray(j, k))
            End If
            If InStr(1, curSignature, "<<") Then
                curSignature = Replace(curSignature, listArray(1, k), listArray(j, k))
            End If
        Next k
    End If
    
    'compose the email
    Set outlookEmail = outlookApp.CreateItem(0)
    With outlookEmail
    
        If sigMode = "Outlook Default" Then 'in order to get the predefined signature from outlook into the message it has to be displayed first
            .display
            .HTMLbody = curGreeting & "<br>" & curBody & "<br>" & .HTMLbody
        Else
            .HTMLbody = curGreeting & "<br><br>" & curBody & "<br><br>" & curSignature
        End If
        
        .To = listArray(j, 1)
        .cc = listArray(j, 2)
        .bcc = listArray(j, 3)
        .subject = curSubject
        
        'attachments
        If att1 <> "" Then .Attachments.Add (att1)
        If att2 <> "" Then .Attachments.Add (att2)
        If att3 <> "" Then .Attachments.Add (att3)
        If listArray(j, 4) <> "" Then .Attachments.Add (listArray(j, 4))
        
        If mode = "Send" Then
            .send
        Else
            .display
        End If
            
    End With
    Set outlookEmail = Nothing
    
Next j

    Set outlookApp = Nothing

Debug.Print

End Sub

bookmark_borderExcel VBA: getRowNum() and getColNum()

Here are two functions that I will be using in an upcoming sheet, I want to explain them briefly and add them to my small library (file is downloadable at the end of this post).

The names hint at the purpose: given a value to match, they will return the number of the row or column the value was found in, or zero if it wasn’t found at all. The concept is similar to the “vlookup” formula, but performance for these two functions is terrible by comparison and for that reason I don’t recommend using them repetitively in a loop or with large columns of data. I maintain several files with built-in functionality where most of the user input comes from cells in a sheet but I can’t always guarantee the position of those cells – that’s where these two come in handy. Instead of hard-coding the cell address, these functions can find the address by matching the value in that cell which allows you to modify the user-facing sheet without having to update code every time; it also makes the sheet somewhat tolerant of being rearranged by the user, but you need to be careful in case it’s been mangled in such a way that the inputs are found but are totally bogus.

getRowNum()

Function getRowNum(findValue As Variant, Optional sheetName As String, Optional colNum As Long) As Long
'by Elliot 8/9/20 www.elliotmade.com
'this function will return the row number in the first (or specified) column that matches the "findValue" parameter
'this is similar to vlookup, but not as fast or performant
'an example use is to get an input from a sheet where you can't guarantee the position of the cell
'combine this with getColNum() to find a cell value where a known column heading and row label intersect
'****** this requires the "getlastrow" function *******

Dim i As Long
Dim lastRow As Long

Dim j As Long
If sheetName <> "" Then
    For j = 1 To Worksheets.Count
        If Worksheets(j).Name = sheetName Then GoTo sheetOK
    Next j
    GoTo abort 'specified sheet name was not found
sheetOK:
End If

'default values for optional parameters
If sheetName = "" Then sheetName = ActiveSheet.Name
If colNum = 0 Then colNum = 1

lastRow = getLastRow(sheetName, colNum)

For i = 1 To lastRow
    If Worksheets(sheetName).Cells(i, colNum).Value2 = findValue Then
        getRowNum = i
        GoTo found
    End If
Next i

found:
abort:

End Function

getColNum()

Function getColNum(findValue As Variant, Optional sheetName As String, Optional rowNum As Long) As Long
'by Elliot 8/9/20 www.elliotmade.com
'this function will return the column number in the first (or specified) column that matches the "findValue" parameter
'this is similar to vlookup, but inverted not as fast or performant
'an example use is to get an input from a sheet where you can't guarantee the position of the cell
'combine this with getRowNum() to find a cell value where a known column heading and row label intersect
'****** this requires the "getLastCol" function *******

Dim i As Long
Dim lastCol As Long

Dim j As Long
If sheetName <> "" Then
    For j = 1 To Worksheets.Count
        If Worksheets(j).Name = sheetName Then GoTo sheetOK
    Next j
    GoTo abort 'specified sheet name was not found
sheetOK:
End If

'default values for optional parameters
If sheetName = "" Then sheetName = ActiveSheet.Name
If rowNum = 0 Then rowNum = 1

lastCol = getLastCol(sheetName, rowNum)

For i = 1 To lastCol
    If Worksheets(sheetName).Cells(rowNum, i).Value2 = findValue Then
        getColNum = i
        GoTo found
    End If
Next i

found:
abort:

End Function

The usage for both of these is simple, just give them a value to match, and if you want to search in a different row/column or a different sheet use the optional parameters. Note that these both rely on two functions I posted previously – getLastRow() and getLastCol(), both of which are included in the file below as well.

bookmark_borderMini Lathe Power Feed Demo

Here’s a quick video of this power feed in action. This is a simplified version without a display, it just does direction and speed. Feed rate can be adjusted at any time independent of the spindle RPM.

Here is a circuit diagram for this version:

And here is the code running on the arduino:

//Mini lathe power feed.  https://elliotmade.com/2020/08/03/mini-lathe-power-feed-demo/

//need validation for the right/left positions and current position

//////////////////////////////////////////////libraries////////////////////////////////////////////////////////////

#include <SimpleTimer.h>
//https://github.com/jfturcot/SimpleTimer

#include <AccelStepper.h>
//http://www.airspayce.com/mikem/arduino/AccelStepper/

#include <Ewma.h>;
//https://github.com/jonnieZG/EWMA

//////////////////////////////////////////Pins////////////////////////////////////////////////////////////////


const int leftPin = 2;
const int rightPin = 3;
const int speedPin = A0;
const int stepPin = 9;
const int dirPin = 8;
const int enablePin = 10;

////////////////////////////////////////////Configuration//////////////////////////////////////////////////////////////
const int speedMult = 2; //multiplier used for max steps/sec.  curSpeed (0-100) * speedMult = steps per second
const int maxAccel = 1000; //steps/sec squared

//operation modes
const byte left = 0;
const byte right = 1;
const byte neutral = 2;
byte curDirection = neutral; //the first time this is checked it will always be different than 3

////////////////////////////////////////////Variables//////////////////////////////////////////////////////////////
int curSpeed = 0;

//Initialize some things
AccelStepper motor(1, stepPin, dirPin);
SimpleTimer timer;
Ewma filteredSpeed(0.1);

void setup() { //Setup

  digitalWrite(enablePin,HIGH);

  //configure the stepper library
  motor.setMaxSpeed(4000);
  motor.setAcceleration(maxAccel);

  //pin configuration

  pinMode(leftPin, INPUT_PULLUP);
  pinMode(rightPin, INPUT_PULLUP);
  pinMode(enablePin, OUTPUT);
}

void loop() { //Loop

  readDirection();
  readSpeed();
  motor.runSpeed();
}

void readSpeed() { //update the speed knob reading except for auto mode
 
    curSpeed = filteredSpeed.filter(analogRead(speedPin)* speedMult);
    if (curDirection == neutral) {
      motor.setSpeed(0);
    }
    else {
      if(curDirection == left) {
        motor.setSpeed(curSpeed);
      }
      else {
        motor.setSpeed(-curSpeed);
      }
    }
    
    
}

void readDirection() {
  if(digitalRead(leftPin) == LOW) {
    curDirection = left;
  }
  else if(digitalRead(rightPin) == LOW) {
    curDirection = right;
  }
  else {
    curDirection = neutral;
  }
  
}

If you’d like to build this for yourself, more details can be found on this post. It is powered by a 19v laptop charger, an arduino, and a tb6600 stepper driver.