bookmark_borderRemote Control Lawnmower

Just wrapped this up and delivered to it’s new owner. It has just about everything except for autopilot! I’ll post some followups with the details, for now here are the specs:

  • Craftsman M270 21″ mower
  • 24v wheelchair motors and batteries
  • 24v alternator (never run out of juice, only run out of gas!)
  • Electric start – remote controlled of course
  • Dimension Engineering Sabertooth 2×32 motor controller
  • FPV camera for mowing at a distance

This is also posted on instructables here: https://www.instructables.com/id/Remote-Control-Lawn-Mower/

bookmark_borderVacuum Former Done!

New circuit board arrived, firmware written, all wiring complete, and it’s working! I posted some info in an instructable here: https://www.instructables.com/id/Revive-a-Vacuum-Former/

The rest of this post is for future-me if I need to troubleshoot my work and can’t find it on my hard drive:

//Replacement controller for Clarke Vacuum Former 1820
//Elliotmade 4/27/2020
//Manual for the machine: https://www.abbeon.com/ItemFiles/Manual/1820.pdf

/*
 * Components attached to the arduino:
 * 4 heater relays (12v, using an IRFZ44 n-channel mosfet to switch these
 * 1 buzzer relay
 * 4 heater potentiometers
 * 2 7-segment displays each using a TM1637 driver
 * 1 Rotary encoder with button
 * 2 existing switches from the machine for the heater location
 * 
 * Operation should be similar to this:
 * Power on the machine, switch heaters on and off slowly according to power level set on the knobs
 * Timer set value is stored in eeprom, displayed on the first 7-segment display
 * Changing the timer set value is done with the rotary encoder
 * Extending the heater triggers a switch and the timer should count down on the second display
 * When the timer reaches zero the buzzer should be turned on
 * Retracting the heater triggers a switch and the timer should be reset and buzzer turned off
 * 
 * Pay attention to the heater position switches and which order they get pushed in
 * States for the timer:
 *  Not running, buzzer not triggered (normal idle status, assume we started from here)
 *  Running, buzzer not triggered (heater extended)
 *  Running, buzzer triggered (heater extended, but time has run out)
 *  Not running, buzzer not triggered (heater retracted, timer is reset and buzzer is canceled)
 */

//////////////////////////////////////////////libraries////////////////////////////////////////////////////////////
#include <RotaryEncoder.h>
//http://www.mathertel.de/Arduino/RotaryEncoderLibrary.aspx
//https://github.com/mathertel/RotaryEncoder

#include "OneButton.h"
//https://github.com/mathertel/OneButton

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

#include <TM1637Display.h>
//https://github.com/avishorp/TM1637

#include <EEPROMex.h>
//https://github.com/thijse/Arduino-EEPROMEx

//////////////////////////////////////////Pins////////////////////////////////////////////////////////////////
const int encoderButton = A1;
const int encoderA = A2;
const int encoderB = A3;
const int timerStart = 7; //D7
const int timerReset = 6; //D6
const int buzzer = 8; //D8
const int dispClock1 = 2; //D2
const int dispClock2 = 4; //D4
const int dispData1 = 3; //D3
const int dispData2 = 5; //D5
const int heatRelay[4] = {12,11,10,9}; //D9-D12
const int heatKnob[4] = {A4,A5,A6,A7};



///////////////////////////////////////////Constants///////////////////////////////////////////////////////////////
//This will impact the duration of the on/off cycles for the heaters.  Milliseconds.  Heaters should stay on for the duty cycle percent, then off for 100-duty cycle
const unsigned long heatIntervalMult = 1000; 

//read heater knobs and update heaters every (milliseconds)
const int updateHeatInterval = 500;

//minimum duty cycle for heaters (old controls were approx. 55% according to the manual "power reduction is 45% on setting 1"
const int minDutyPct = 55;

//thresholds for knob to identify zero and max heat (0-1023)
const int minThreshold = 30;
const int maxThreshold = 1000;

const byte heaterCount = 4;

//eeprom memory locations
const int minAddress = 10;
const int secAddress = 20;
const int memBase = 350;

////////////////////////////////////////////Variables//////////////////////////////////////////////////////////////

bool heatStatus[4] = {false,false,false,false};   //current on/off status of a heater
int heatSetting[4] = {0,0,0,0};                   //current heat setting 0-100 percent, updated when the knobs are read
unsigned long heatLastChange[4] = {0,0,0,0};      //last time in milliseconds that the heater was turned on
bool timerRunning = false;                        //state of the timer
bool buzzerOn = false;                            //state of the buzzer
bool minutesMode = false;                         //change behavior of encoder based on this
int minutes;                                      //timer set time
int seconds;                                      //timer set time
int elapsedMinutes = 0;                           //timer current countdown time
int elapsedSeconds = 0;                           //timer current countdown time
bool rollover = false;                            //used to help display the countdown

//Initialize some things
SimpleTimer countdownTimer;
RotaryEncoder encoder(encoderA, encoderB);
OneButton startSwitch(timerStart, true);
OneButton resetSwitch(timerReset, true);
OneButton encoderButt(encoderButton, true);
TM1637Display display1(dispClock1, dispData1);
TM1637Display display2(dispClock2, dispData2);



void setup() { ////////////////////////////////////////Setup///////////////////////////////////////////////////
//Serial for debugging
Serial.begin(9600);

Serial.println("Setup Start");

//interrupts for encoder
PCICR |= (1 << PCIE1);    // This enables Pin Change Interrupt 1 that covers the Analog input pins or Port C.
PCMSK1 |= (1 << PCINT10) | (1 << PCINT11);  // This enables the interrupt for pin 2 and 3 of Port C.

// Set Up EEPROM
EEPROM.setMemPool(memBase, EEPROMSizeNano);

//pins
pinMode(buzzer, OUTPUT);
for (byte i = 0; i <= heaterCount - 1; i++) {
  pinMode(heatRelay[i], OUTPUT);
  pinMode(heatKnob[i], INPUT);
}

//button functions
startSwitch.attachClick(startTime);
resetSwitch.attachClick(resetTime);
encoderButt.attachClick(encoderClick);
encoderButt.attachLongPressStart(saveTime);

//Load the stored timer value
minutes = EEPROM.readInt(minAddress);
seconds = EEPROM.readInt(secAddress);


//displays
display1.setBrightness(2);
display2.setBrightness(2);
display1.showNumberDec(8008);
display2.showNumberDecEx(8008,0x40,true);
delay(50);

updateDisplay1();

countdownTimer.setInterval(1000,incrimentTimer); //make timer count every second
countdownTimer.setInterval(updateHeatInterval,readKnobs);
countdownTimer.setInterval(250,updateHeatStatus);
countdownTimer.setInterval(250,updateHeatRelays);

Serial.println("Setup Complete");
} //////////////////////////////////////////////End Setup/////////////////////////////////////////////////////





void loop() { ////////////////////////////////////Loop/////////////////////////////////////////////////////
  //monitor buttons
  startSwitch.tick();
  resetSwitch.tick();
  encoderButt.tick();
  readEncoder();
  countdownTimer.run();
  checkTimer();
  updateDisplay1();
  updateDisplay2();
  updateBuzzer();
} //////////////////////////////////////////////End Loop/////////////////////////////////////////////////////


void encoderClick() { //When the encoder button is clicked, change from minutes to seconds for timer adjustment
 
  minutesMode = !minutesMode; //toggle time setting modes
  Serial.print("Mode: ");
  Serial.println(minutesMode);
}

void readEncoder() {
      static int pos = 0;
      int newPos = encoder.getPosition();
      

      if (!timerRunning) { //only let the set time be changed when the timer is not counting down
        if (pos != newPos) {
  
        
          if (minutesMode) {
          minutes = minutes + (newPos - pos);
          
          }
          else {
            seconds = seconds + (newPos - pos);
            if (seconds == 60) {
              seconds = 0;
              minutes++;
            }
            if (seconds < 0) {
              seconds = 59;
              minutes--;
            }
          }
          pos = newPos;
  
          minutes = constrain(minutes,0,59);
          seconds = constrain(seconds,0,59);
          
          Serial.print("Minutes: ");
          Serial.print(minutes);
          Serial.print(" Seconds: ");
          Serial.println(seconds);
        }
      }
      else {
        encoder.setPosition(pos); //reset the encoder, as if it didn't move while the timer was counting
      }
}

void startTime() { //When the heater is extended, start counting down
  if (!timerRunning) {
    timerRunning = true;
    elapsedSeconds = 0;
    elapsedMinutes = 0;
    Serial.println("Timer Started");
  }
}

void resetTime() { //When the heater is retracted, stop counting down, rest the timer, and shut off the buzzer
  if (timerRunning) {
    timerRunning = false;
    buzzerOn = false;
    elapsedMinutes = 0;
    elapsedSeconds = 0;
  }
  updateBuzzer();
}

void saveTime() { //When the encoder knob is held down, save the set time to EEPROM
  EEPROM.writeInt(minAddress, minutes);
  EEPROM.writeInt(secAddress, seconds);
  Serial.println("Timer setting saved to EEPROM");
}

void incrimentTimer() { //update the elapsed time variables after each second passes
  if (timerRunning) {
    elapsedSeconds++;  
    if (elapsedSeconds == seconds && rollover == false) {  //used to help display differently if the original number of seconds have passed
        rollover = true;
        elapsedSeconds = 60;
        elapsedMinutes++;
    }
    if (elapsedSeconds == 60) {
      elapsedSeconds = 0;
      elapsedMinutes++;
    }
  }
}

void checkTimer() { //If the timer is running, sound the buzzer once it has reached zero
  if (elapsedMinutes >= minutes && (elapsedSeconds % 60) >= seconds && !buzzerOn) {
    Serial.println("Timer has reached zero");
    buzzerOn = true; //turn on the buzzer
    elapsedSeconds = 0; //reset the elapsed time so it can be displayed counting up easily
    elapsedMinutes = 0;
  }
}

void updateBuzzer() { //Turn buzzer on/off based on the variable
  if (buzzerOn) {
    digitalWrite(buzzer, LOW);
    //Serial.println("Buzzer On");
  }
  else {
    digitalWrite(buzzer, HIGH);
    //Serial.println("Buzzer Off");
  }
}

void updateDisplay1() {
  display1.showNumberDecEx(minutes * 100 + seconds, 0x40, true);  //Set time should always show on display 1
}

void updateDisplay2() {
  if (timerRunning) {
    if (buzzerOn) { //count up if the buzzer is on (timer has reached zero)
      display2.showNumberDecEx((elapsedMinutes) * 100 + (elapsedSeconds), 0x40, true); //count up.  These were reset to 0 when the timer ran out
    }
    else if (rollover == true) {  //count down from 60 seconds
      display2.showNumberDecEx((minutes - elapsedMinutes) * 100 + (60 - elapsedSeconds), 0x40, true); //count down
    }
    
    else { //count down from the original number of seconds
      display2.showNumberDecEx((minutes - elapsedMinutes) * 100 + (seconds - elapsedSeconds), 0x40, true); //count down
    }
  }
  else {
    display2.showNumberDecEx(minutes * 100 + seconds, 0x40, true); //show the same thing on 2 as 1
  }

}

void readKnobs() { //read knob position and update the heat setting array
  for (byte j = 0; j <= heaterCount - 1; j++) {

    if (analogRead(heatKnob[j]) < minThreshold) {
      heatSetting[j] = 0;
    }
    else if (analogRead(heatKnob[j]) > maxThreshold) {
      heatSetting[j] = 100;
    }
    else {
      heatSetting[j] = map(analogRead(heatKnob[j]),0,1023,minDutyPct * 10,maxThreshold)/10;
    }

  
  }
//Serial.println(" ");
}

void updateHeatStatus() { //update the on/off array for each heater based on the knob position
  //calculate the time to the next change
unsigned long curMillis = millis();

  for (byte m = 0; m <= heaterCount - 1; m++) {
    if (heatSetting == 0 && heatStatus[m] == true) {          //fully off if the knob is at the minimum position
      heatStatus[m] = false;
      heatLastChange[m] = curMillis;
    }
    else if (heatSetting == 100 && heatStatus[m] == false) {   //fully on if the knob is at the max position
      heatStatus[m] = true;
      heatLastChange[m] = curMillis;
    }
    
    else if (heatStatus[m] == true) {
      if (heatLastChange[m] + heatSetting[m] * heatIntervalMult < curMillis) {  //if the heater is currently on, wait until last change time + (duty cycle * multiplier) seconds have gone by then turn it off
        heatStatus[m] = false;
        heatLastChange[m] = curMillis;
      }
    }
    else {  //if the heater is currently off, wait until last change time + (100 - duty cycle * multiplier) seconds have gone by then turn it on
      if (heatLastChange[m] + (100 - heatSetting[m]) * heatIntervalMult < curMillis) {  //if the heater is currently off, wait until last change time + (100 - duty cycle) * multiplier seconds have gone by then turn it off
        heatStatus[m] = true;
        heatLastChange[m] = curMillis;
      }
    }


  }

  
}

void updateHeatRelays() { //Turn heaters relays on or off
  for (byte k = 0; k <= heaterCount - 1; k++) {
  
    if(heatStatus[k] == true) {
      digitalWrite(heatRelay[k], HIGH);
    }
    else { 
      digitalWrite(heatRelay[k], LOW);
    }
}


}

ISR(PCINT1_vect) { // The Interrupt Service Routine for Pin Change Interrupt 1
  encoder.tick(); // just call tick() to check the state.
}

bookmark_borderNew (old) Vacuum Forming Machine

I just became the surprise owner of this vacuum forming machine:

This is a C. R. Clarke Vacuum Former 1820. Looks like this model is still made currently (link to site), but I would guess that this one was made in the mid ’90s. The concept is fairly straightforward: a form/mold is placed on a moving platform inside, material is clamped in place over it, the heater slides forward to heat it, then the mold is raised and vacuum applied. This one has a few things wrong with it:

  • Control panel has been partially removed
  • Something electrical caught fire or let out some smoke
  • Power switch broken/disassembled and there are many wires disconnected (and a suspicious wire nut, always a bad sign)
  • One of the relay bases is broken from it’s rail, another relay is melted, and many of the relay contacts look worn
  • One of the heat controllers is cracked open, all are rusted
  • Corrosion on all of the connectors. It probably spent some time outside

The controls are completely electromechanical except for the timer, which has exactly two transistors in it, and everything in the panel including the indicator lights runs at 220 volts. The heating elements and vacuum system look like they’re in good shape, so I would say it’s worth fixing. With that said… the amount of corrosion on everything in the panel and the obvious burn marks from a previous fire give me doubts about just hooking everything back up. Because of the high voltage, I would want to replace all of the heater controls, timer, connectors, and relays… and the original stuff isn’t super easy to come by. I plan to replace it all, but first I have to understand how it was supposed to work.

Ignoring the vacuum part, there are only a couple things that actually have to happen: the heat level needs to be regulated, and a timer needs to tell the operator how long to keep the heat over the material. Heat regulation appears to be open loop with a sort of thermostat – Diamond H 30ER1HT 38. From what I gather these are also referred to as a “simmerstat” and are used on some kinds of hot plates/kitchen/catering type appliances. Instead of sensing the temperature of air like a thermostat, there is an internal heater acting on the bimetallic strip that opens and closes the contacts. When the knob is off the contacts are open, when it is at MAX they are closed, and in between it cycles open and closed. Basically this is PWM with variable duty cycle, but with a very slow frequency.

As for the timer, there are two contacts (pictured above) on the heater. When the heater slides out the lower contact starts the timer when the drawer slides are fully extended, and when it is returned home the upper contact resets the timer (also cancels the buzzer that is sounded when it hits zero). The placement of the bumps on the heater are clever and probably helps prevent people from leaving the heater in some intermediate position where it interferes with other things.

The plan:

  • Replace all of the relay/timer logic with a microcontroller
  • Heater controls will be replaced with potentiometers
  • Extremely slow PWM to relays for heaters
  • Timer display and control
  • Buzzer will be kept but activated with a relay

I will have to add a DC power supply for this to work, but a big benefit of that is that I can test everything out on the bench, far, far away from 220v mains. AC won’t be involved anywhere in this circuit, it will only be switched by relays:

I didn’t bother prototyping this – just went ahead and ordered from JLCPCB so I will find out what mistakes I made in a few days when it gets here. The components I chose are based on what I have on hand, not that there are many of them, it’s mostly connectors. I don’t think four knobs for the heaters are really necessary, but I wanted to keep the panel the same if possible. I used potentiometers for these because I don’t have a handy way to interface five rotary encoders with an arduino nano.

Here are some more photos for future-me to reference, and I’ll post again when I make more progress!

bookmark_borderPhysical Mute Button for Zoom Meetings

This is a big physical button you can put on your desk that will toggle your mute in zoom meetings, and if you hold the button down it leaves the meeting or ends it if you are the host.

It consists of a Digispark clone board (attiny85), a resistor, and a switch. The microcontroller acts as a keyboard with with just one button, and I’m taking advantage of the keyboard shortcuts built into the zoom app. The main thing that makes this work is the fact that CTRL+ALT+SHIFT brings focus to the meeting controls. This brings the zoom window to the front if you are a participant (sometimes I toggle mute with the button just to find the window), and it also works while you are sharing your screen. A short press sends ALT+A which toggles your mute state, and a long press sends ALT+Q then ENTER, which exits the meeting entirely.

Source code will be at the end of the post, it’s a slightly modified example from the digikeyboard library. I used the Arduino IDE – you’ll need to install the digistump boards through the board manager and also get the button library I used here: https://github.com/mathertel/OneButton. The wiring is very simple, it is just a momentary switch between GND and P0, and a 10k pullup resistor between 5V and P0 (this is not required at all in fact, so you can leave the resistor out).

I have created an instructable for this as well, a PDF copy is downloadable below and the link is: https://www.instructables.com/id/Zoom-Meetings-Physical-Mute-Button/

//Elliotmade 4/22/2020
//https://elliotmade.com/2020/04/23/physical-mute-button-for-zoom-meetings/
//https://www.youtube.com/watch?v=apGbelheIzg
//Used a digispark clone

//this will switch to the zoom application and mute it or exit on long press
//momentary button on pin 0 with pullup resistor

//https://github.com/mathertel/OneButton
//button library
#include "OneButton.h"

int button1pin = 0;

#include "DigiKeyboard.h"

//set up buttons
  OneButton button1(button1pin, true);

void setup() {
  // put your setup code here, to run once:


  //set up button functions

  button1.attachClick(click1);
  button1.attachLongPressStart(longPressStart1);

  DigiKeyboard.sendKeyStroke(0);
  DigiKeyboard.delay(500);
  
}

void loop() {
  // put your main code here, to run repeatedly:
  //monitor buttons
  button1.tick();
}


// This function will be called when the button1 was pressed 1 time (and no 2. button press followed).
void click1() {
  // this is generally not necessary but with some older systems it seems to
  // prevent missing the first character after a delay:
  DigiKeyboard.sendKeyStroke(0);
  
  // Type out this string letter by letter on the computer (assumes US-style
  // keyboard)
  DigiKeyboard.sendKeyStroke(0, MOD_SHIFT_LEFT | MOD_CONTROL_LEFT | MOD_ALT_LEFT);
  DigiKeyboard.delay(100);
  DigiKeyboard.sendKeyStroke(KEY_A, MOD_ALT_LEFT);


} // click1


// This function will be called once, when the button1 is pressed for a long time.
void longPressStart1() {
  // this is generally not necessary but with some older systems it seems to
  // prevent missing the first character after a delay:
  DigiKeyboard.sendKeyStroke(0);
  
  // Type out this string letter by letter on the computer (assumes US-style
  // keyboard)
  DigiKeyboard.sendKeyStroke(0, MOD_SHIFT_LEFT | MOD_CONTROL_LEFT | MOD_ALT_LEFT);
  DigiKeyboard.delay(50);
  DigiKeyboard.sendKeyStroke(KEY_Q, MOD_ALT_LEFT);
  DigiKeyboard.delay(50);
  DigiKeyboard.sendKeyStroke(KEY_ENTER);

} // longPressStart1

bookmark_borderConway’s Game of Life in a spreadsheet

I saw this: https://hackaday.com/2020/04/13/john-horton-conway-creator-of-conways-game-of-life-has-died/, decided that I would try to write his game myself. This was just for fun and it isn’t optimized for anything really, but it might be useful to check out if you are getting into macros for Excel and need to read/update cells in a worksheet.

If you’re not comfortable downloading an .xlsm from (which is wise), the code below can just be pasted in an empty module. Make sure to name a sheet in your workbook “Life”, type in a number in cell B9 (or hardcode it), and add buttons to call the subs if you want.

The black cells are 1’s, white cells are 0’s. Might be interesting to tweak this so it generates QR codes and see where it takes you!

Option Explicit

'by Elliot (elliotmade.com) 4/14/2020
'Conway's game of life

'there is no input validation, and literally no optimization for anything
'wrote this purposely without looking at any examples other than a description of the rules, just for fun

'rules (from wikipedia):
'    Any live cell with two or three live neighbors survives.
'    Any dead cell with three live neighbors becomes a live cell.
'    All other live cells die in the next generation. Similarly, all other dead cells stay dead.

Dim xMax As Long
Dim yMax As Long

Dim x As Long
Dim y As Long

Dim xOffset As Long 'so it doesn't have to occupy the top and left cells
Dim yOffset As Long

Dim ws As Worksheet
Dim ticks As Long
Dim maxTicks As Long

Dim currentGrid() As Byte
Dim nextGrid() As Byte


Sub initialize()
'read in the initial state

xMax = 40
yMax = 40
ticks = 0

xOffset = 3
yOffset = 3

Set ws = Worksheets("Life")

ReDim currentGrid(1 To xMax, 1 To yMax)
ReDim nextGrid(1 To xMax, 1 To yMax)

For y = 1 To yMax
    
    For x = 1 To xMax
        currentGrid(x, y) = ws.Cells(y + yOffset, x + xOffset)
    Next x
Next y

maxTicks = ws.Cells(9, 2).Value


Debug.Print "Initialized"

End Sub

Sub clear()
    Call initialize
    ws.Range(ws.Cells(1 + yOffset, 1 + xOffset), ws.Cells(yMax + yOffset, xMax + xOffset)).Value = 0
    Call initialize
    Call output
    Debug.Print "Cleared"

End Sub

Sub tick()
'really should do initialize if the variables aren't populated...

Dim countNeighbors As Integer

For y = 1 To yMax
    For x = 1 To xMax
        countNeighbors = 0
        'top neighbor
        If y > 1 Then
            countNeighbors = countNeighbors + currentGrid(x, y - 1)
        End If
        'bottom neighbor
        If y < yMax Then
            countNeighbors = countNeighbors + currentGrid(x, y + 1)
        End If
        'left neighbor
        If x > 1 Then
            countNeighbors = countNeighbors + currentGrid(x - 1, y)
        End If
        'right neighbor
        If x < xMax Then
            countNeighbors = countNeighbors + currentGrid(x + 1, y)
        End If
        
        'top left neighbor
        If x > 1 And y > 1 Then
            countNeighbors = countNeighbors + currentGrid(x - 1, y - 1)
        End If
        
        'top right neighbor
        If x < xMax And y > 1 Then
            countNeighbors = countNeighbors + currentGrid(x + 1, y - 1)
        End If
        
        'bottom right neighbor
        If x < xMax And y < yMax Then
            countNeighbors = countNeighbors + currentGrid(x + 1, y + 1)
        End If
        
        'bottom left neighbor
        If x > 1 And y < yMax Then
            countNeighbors = countNeighbors + currentGrid(x - 1, y + 1)
        End If
        
        If currentGrid(x, y) = 1 And (countNeighbors = 2 Or countNeighbors = 3) Then
            nextGrid(x, y) = 1
        ElseIf currentGrid(x, y) = 0 And countNeighbors = 3 Then
            nextGrid(x, y) = 1
        Else
            nextGrid(x, y) = 0
        End If
        
    Next x
Next y



currentGrid = nextGrid
Call output
ticks = ticks + 1

End Sub


Sub output()

Application.ScreenUpdating = False

For y = 1 To yMax
    
    For x = 1 To xMax
        ws.Cells(y + yOffset, x + xOffset).Value = nextGrid(x, y)
        If nextGrid(x, y) = 1 Then
            ws.Cells(y + yOffset, x + xOffset).Interior.ColorIndex = 1
            ws.Cells(y + yOffset, x + xOffset).Font.Color = vbBlack
        Else
            ws.Cells(y + yOffset, x + xOffset).Interior.ColorIndex = 2
            ws.Cells(y + yOffset, x + xOffset).Font.Color = vbWhite
        End If
    Next x
Next y

Application.ScreenUpdating = True
DoEvents

End Sub


Sub run()
Dim a As Long
'would be neat to end early if no changes occur between ticks...

Call initialize

If ticks = maxTicks Then
    MsgBox "Tick limit " & maxTicks & " reached"
    Exit Sub
End If

For a = 0 To maxTicks
    If WorksheetFunction.Sum(currentGrid) = 0 Then
        MsgBox "No live cells after " & ticks & " ticks"
        Exit Sub
    End If
    Call tick
Next a

End Sub

Sub randomize()
    Call initialize
    ws.Range(ws.Cells(1 + yOffset, 1 + xOffset), ws.Cells(yMax + yOffset, xMax + xOffset)).FormulaR1C1 = "=RANDBETWEEN(0,1)"
    ws.Calculate
    ws.Range(ws.Cells(1 + yOffset, 1 + xOffset), ws.Cells(yMax + yOffset, xMax + xOffset)).Value = ws.Range(ws.Cells(1 + yOffset, 1 + xOffset), ws.Cells(yMax + yOffset, xMax + xOffset)).Value
    ws.Range(ws.Cells(1 + yOffset, 1 + xOffset), ws.Cells(yMax + yOffset, xMax + xOffset)).Interior.ColorIndex = 0
    ws.Range(ws.Cells(1 + yOffset, 1 + xOffset), ws.Cells(yMax + yOffset, xMax + xOffset)).Font.Color = vbBlack
End Sub