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
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.
}
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!
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).
//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
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