Hangboard, 2025
Due to having a bust achilles, I've gravitated towards bouldering. I've grown quite fond of it as an activity. And I'd like to be able to practice more at home. Hangboarding is a common way to train finger strength, and I figure that it's not too hard to design + mill a piece of material to make the board.
However, when designing things, I'd like to try my best to use recycled goods + reduce waste as much as possible. I find that enjoyable, both ethically and in the same way that it feels great making a meal with what's left in the koolkast + pantry.
In doing a bit of research, I found that there are a few different types of holds to consider adding. Slopers, Jugs, and just the regular kind.
For simplicity's sake, I'll just make the regular kind, but I like the idea of having one hole with a depth of 30mm and a few variable depth cutouts that I can use to make the space for my fingers smaller. In later spirals, for system integration, I could consider adding small magnets to give the whole thing a professional feel.
That means I'll just have one strip with two handgrips.
To add to the fun, I've decided to add a screen with a timer and a speaker, as well as pressure sensors to begin the timers + measure hang balance.
LCD screen system
I made a Kristallen bol from the Arduino Starter Kit Project book. It incorporated a tilt sensor and a switch()
function.
Kristallen bol code
#include <LiquidCrystal.h>
LiquidCrystal lcd(12,11,5,4,3,2); // generates an instance in the lcd
const int switchPin = 6;
int switchState = 0;
int prevSwitchState = 0;
int reply;
void setup() {
lcd.begin(16,2);
pinMode(switchPin, INPUT);
lcd.print("Vraag het aan de");
lcd.setCursor(0,1); // changes the Cursor to continue writing in the second row
lcd.print("Kristallen Bol");
}
void loop() {
switchState=digitalRead(switchPin);
if (switchState != prevSwitchState) {
if (switchState == LOW) {
reply = random(8);
lcd.clear(); // clears the writing
lcd.setCursor(0,0);
lcd.print("De bol zeg:");
lcd.setCursor(0,1);
switch(reply){ // the program will enter the case assigned to the switch
case 0:
lcd.print("Ja");
break;
case 1:
lcd.print("Waarschijnlijk");
break;
case 2:
lcd.print("Zeker!");
break;
case 3:
lcd.print("Goede vooruitzichten");
break;
case 4:
lcd.print("Ik weet niet");
break;
case 5:
lcd.print("Nog een vraag");
break;
case 6:
lcd.print("Geen idee");
break;
case 7:
lcd.print("Helaas");
break;
}
}
}
prevSwitchState = switchState;
}
I shifted focus to a tutorial for a button controlled, adjustable timer.
The original example code used Hours and Minutes, but that's not a relevant time frame for a normal hang. I asked AI to adjust the code so that it used Minutes and Seconds.
Timer w/ buttons code
#include <LiquidCrystal.h>
#include <EEPROM.h>
bool buttonPressed(int pin, uint16_t hold_ms = 0) {
if (digitalRead(pin) == LOW) {
if (hold_ms == 0) {
while(digitalRead(pin) == LOW); // wait for release
delay(20); // debounce
return true;
} else {
unsigned long t0 = millis();
while (digitalRead(pin) == LOW) {
if (millis() - t0 >= hold_ms) {
while(digitalRead(pin) == LOW); // wait for release
delay(20); return true;
}
}
}
}
return false;
}
// LCD pins (matches your setup)
const int rs=7, en=6, d4=5, d5=4, d6=3, d7=2;
LiquidCrystal lcd(rs, en, d4, d5, d6, d7);
// Button/output pins
const int BTN_STARTSTOP = A0;
const int BTN_INC = A1;
const int BTN_DEC = A2;
const int BTN_SET = A3;
const int BUZZER = 9;
const int RELAY = 8;
// EEPROM storage
#define EEPROM_CHECK_ADDR 0
#define EEPROM_MIN_ADDR 1
#define EEPROM_SEC_ADDR 2
#define EEPROM_OK_VALUE 42
// Timer variables
uint8_t minutes = 0;
uint8_t seconds = 0;
unsigned long lastTick = 0;
enum State { IDLE, SET_MIN, SET_SEC, COUNTDOWN, BUZZ };
State state = IDLE;
void setup() {
lcd.begin(16,2);
pinMode(BTN_STARTSTOP, INPUT_PULLUP);
pinMode(BTN_INC, INPUT_PULLUP);
pinMode(BTN_DEC, INPUT_PULLUP);
pinMode(BTN_SET, INPUT_PULLUP);
pinMode(BUZZER, OUTPUT);
pinMode(RELAY, OUTPUT);
digitalWrite(RELAY, LOW);
digitalWrite(BUZZER, LOW);
// Initialize EEPROM if needed
if (EEPROM.read(EEPROM_CHECK_ADDR) != EEPROM_OK_VALUE) {
EEPROM.write(EEPROM_CHECK_ADDR, EEPROM_OK_VALUE);
EEPROM.write(EEPROM_MIN_ADDR, 1);
EEPROM.write(EEPROM_SEC_ADDR, 0);
}
loadTimer();
showWelcome();
delay(1000);
showIdle();
}
void loadTimer() {
minutes = EEPROM.read(EEPROM_MIN_ADDR);
seconds = EEPROM.read(EEPROM_SEC_ADDR);
}
void saveTimer() {
EEPROM.write(EEPROM_MIN_ADDR, minutes);
EEPROM.write(EEPROM_SEC_ADDR, seconds);
}
void showWelcome() {
lcd.clear();
lcd.setCursor(0,0); lcd.print("Simple Timer");
lcd.setCursor(0,1); lcd.print("MIN:SEC ver.");
}
void showIdle() {
lcd.clear();
lcd.setCursor(0,0); lcd.print("Set/Start Timer");
showTime(minutes, seconds);
}
void showTime(uint8_t min, uint8_t sec) {
lcd.setCursor(4, 1);
if (min < 10) lcd.print('0');
lcd.print(min); lcd.print(":");
if (sec < 10) lcd.print('0');
lcd.print(sec); lcd.print(" "); // Clear leftovers
}
void loop() {
switch(state) {
case IDLE:
showTime(minutes, seconds);
if (buttonPressed(BTN_SET)) { state = SET_MIN; delay(200);}
else if (buttonPressed(BTN_STARTSTOP)) {
if (minutes==0 && seconds==0) {
lcd.setCursor(0,0); lcd.print(" INVALID TIME ");
delay(1000); showIdle();
} else {
lastTick = millis();
digitalWrite(RELAY, HIGH);
state = COUNTDOWN;
lcd.clear(); lcd.setCursor(0,0); lcd.print(" TIMER RUNNING ");
showTime(minutes, seconds);
}
}
break;
case SET_MIN:
lcd.clear(); lcd.setCursor(0,0); lcd.print("Set MINUTES:");
lcd.setCursor(0,1); lcd.print(minutes);
while (!buttonPressed(BTN_SET)) {
if (buttonPressed(BTN_INC)) { minutes = (minutes+1)%100; lcd.setCursor(0,1); lcd.print((minutes<10)?"0":""); lcd.print(minutes); lcd.print(" "); delay(150);}
if (buttonPressed(BTN_DEC)) { minutes = (minutes==0)?99:minutes-1; lcd.setCursor(0,1); lcd.print((minutes<10)?"0":""); lcd.print(minutes); lcd.print(" "); delay(150);}
}
delay(200);
state = SET_SEC;
break;
case SET_SEC:
lcd.clear(); lcd.setCursor(0,0); lcd.print("Set SECONDS:");
lcd.setCursor(0,1); lcd.print(seconds);
while (!buttonPressed(BTN_SET)) {
if (buttonPressed(BTN_INC)) { seconds = (seconds+1)%60; lcd.setCursor(0,1); lcd.print((seconds<10)?"0":""); lcd.print(seconds); lcd.print(" "); delay(150);}
if (buttonPressed(BTN_DEC)) { seconds = (seconds==0)?59:seconds-1; lcd.setCursor(0,1); lcd.print((seconds<10)?"0":""); lcd.print(seconds); lcd.print(" "); delay(150);}
}
delay(200);
// Don't allow 0:0 as a timer value
if (minutes==0 && seconds==0) {
lcd.setCursor(0,0); lcd.print(" INVALID TIME ");
delay(1000);
} else {
saveTimer();
}
state = IDLE;
showIdle();
break;
case COUNTDOWN:
if (buttonPressed(BTN_STARTSTOP, 300)) {
digitalWrite(RELAY, LOW);
lcd.clear(); lcd.setCursor(0,0); lcd.print(" TIMER STOPPED ");
delay(1000); showIdle();
state = IDLE;
break;
}
if (millis() - lastTick >= 1000) {
lastTick += 1000;
if (seconds == 0) {
if (minutes == 0) {
digitalWrite(RELAY, LOW);
state = BUZZ;
} else {
minutes--;
seconds = 59;
}
} else {
seconds--;
}
showTime(minutes, seconds);
}
break;
case BUZZ:
lcd.clear(); lcd.setCursor(3,0); lcd.print("TIME IS UP!");
for (uint8_t i=0; i<8; i++) {
digitalWrite(BUZZER, HIGH); delay(120);
digitalWrite(BUZZER, LOW); delay(120);
}
state = IDLE;
showIdle();
break;
}
}