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.

diagram

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;
  }
}