/*
* punching_bag: control rotatable motorized punching man using wii nunchuk.
*
* Connections:
* ┌─────────┬─────────────┬─────────────┬──────────┐
* │ Arduino │ Motor │ Max7219 │ Joystick │
* ├─────────┼─────────────┼─────────────┼──────────┤
* │ D2 │ ENC_A │ │ │
* │ D3 │ ENC_B │ │ │
* │ D5 │ MOT_A / PWM │ │ │
* │ D6 │ MOT_B / DIR │ │ │
* │ D10 │ │ CS / LOAD │ │
* │ D11 │ │ DIN / DATA │ │
* │ D13 │ │ CLK / CLOCK │ │
* │ A0 │ │ │ POT_X │
* │ A1 │ │ │ POT_Y │
* │ A2 │ │ │ BTN_A │
* │ A3 │ │ │ BTN_B │
* └─────────┴─────────────┴─────────────┴──────────┘
*
* [email protected] 29/01/2022
*/
#include <Arduino.h>
#include <MD_Parola.h> // https://github.com/MajicDesigns/MD_Parola.git
#include <MD_MAX72xx.h> // https://github.com/MajicDesigns/MD_MAX72XX.git
#include <MD_UISwitch.h> // https://github.com/MajicDesigns/MD_UISwitch.git
#define HW_VERSION (1) // 0=Yuval-setup 1=Arad-setup
#define ENCODER_A_PIN (2) // must be interrupt pin
#define ENCODER_B_PIN (3)
#define MAX7219_CS_PIN (10)
#define MAX7219_DIN_PIN (11)
#define MAX7219_CLK_PIN (13)
#define JOYSTICK_POT_X_PIN (A0)
#define JOYSTICK_POT_Y_PIN (A1)
#define JOYSTICK_BTN_A_PIN (A2)
#define JOYSTICK_BTN_B_PIN (A3)
#if HW_VERSION == 0
#define MOTOR_DIR_PIN (5)
#define MOTOR_SPEED_PIN (6) // must be PWM pin
#define MAX7219_BRIGHTNESS (4) // 0-15
#elif HW_VERSION == 1
#define MOTOR_A_PIN (5) // must be PWM pin
#define MOTOR_B_PIN (6) // must be PWM pin
#define MAX7219_BRIGHTNESS (0) // 0-15
#endif
#define MOTOR_POS_PUNCH (450)
#define MOTOR_POS_THRESHOLD (5)
#define JOYSTICK_POT_THRESHOLD (32) // 0-512
#define PROGRAM_STOP_PUNCH (0xff)
#define MAX7219_DEVICES (4)
#define SHOW_RANDOM_EFFECT ((textEffect_t)0xff)
enum {
JOYSTICK_KEY_NONE = 0,
JOYSTICK_KEY_ENTER,
JOYSTICK_KEY_LEFT,
JOYSTICK_KEY_RIGHT,
};
enum {
MOTOR_STATE_AT_HOME = 0,
MOTOR_STATE_FROM_HOME,
MOTOR_STATE_TO_HOME,
};
enum {
SHOW_INTRO = 0,
SHOW_IDLE,
SHOW_PARAMS,
SHOW_PUNCH,
SHOW_ENCOURAGEMENTS,
};
const uint8_t MOTOR_SPEED[] = { // PWM values
150, 200, 255,
};
const char *MOTOR_PROGRAMS[] = { // 'L'=left 'R'=Right (goes home after each move)
"LRLRLRLRLRLRLRLRLRLRLRLRLRLRLRLRLRLRLRLR",
"LLRRLLRRLLRRLLRRLLRRLLRRLLRRLLRRLLRR",
"LRLLRLLLRLLLLRLLLLLRLRRLRRRLRRRRLRRRRR",
};
const char *TEXT_ENCOURAGEMENTS[] = {
"Keep going!",
"You Are Doing Great",
"No Pain. No Gain.",
"Hit Me Harder!",
"Shut Up And Train",
"Work In Work Out",
};
const uint8_t CHAR_1[] = { // customize char '1' to be 5-pixel width instead of 3
5, 68, 66, 127, 64, 64,
};
struct {
uint8_t joystick_key;
uint8_t joystick_last_key;
uint32_t joystick_key_ms;
uint8_t motor_state;
int16_t motor_speed;
uint8_t motor_speed_index;
uint8_t program_index;
uint8_t program_punch_index;
uint8_t show_index;
uint8_t show_animation_index;
uint8_t punch_count[2];
char display_buff[32];
} self;
typedef struct {
uint16_t speed;
uint16_t pause;
textPosition_t align;
textEffect_t effect_in;
textEffect_t effect_out;
const char *text;
} show_t;
textEffect_t RANDOM_EFFECT[] = {
PA_SCROLL_UP,
PA_SCROLL_DOWN,
PA_SCROLL_LEFT,
PA_MESH,
PA_DISSOLVE,
PA_BLINDS,
PA_RANDOM,
PA_WIPE_CURSOR,
PA_OPENING_CURSOR,
PA_CLOSING_CURSOR,
PA_SCROLL_UP_LEFT,
PA_SCROLL_UP_RIGHT,
PA_SCROLL_DOWN_LEFT,
PA_SCROLL_DOWN_RIGHT,
PA_GROW_UP,
PA_GROW_DOWN,
};
const show_t show_intro[] = {
{40, 800, PA_LEFT, PA_SCROLL_UP_LEFT, PA_SCROLL_UP_LEFT, "Punch"},
{30, 700, PA_RIGHT, PA_SCROLL_UP_RIGHT, PA_SCROLL_UP_RIGHT, "Bag"},
{20, 600, PA_CENTER, PA_SCROLL_DOWN, PA_SCROLL_UP, "V3.1"},
{30, 800, PA_CENTER, PA_OPENING_CURSOR, PA_CLOSING_CURSOR, "BY"},
{5, 800, PA_CENTER, PA_SLICE, PA_RANDOM, "KD"},
{5, 1100, PA_CENTER, PA_RANDOM, PA_SLICE, "Tech"},
{40, 1000, PA_CENTER, PA_WIPE_CURSOR, PA_SCROLL_DOWN, "2022"},
};
const show_t show_idle[] = {
{0, 400, PA_LEFT, PA_NO_EFFECT, PA_NO_EFFECT, ""},
{30, 1200, PA_CENTER, SHOW_RANDOM_EFFECT, SHOW_RANDOM_EFFECT, "Press"},
{30, 1200, PA_CENTER, SHOW_RANDOM_EFFECT, SHOW_RANDOM_EFFECT, "ENTER!"},
{0, 10000, PA_LEFT, PA_NO_EFFECT, PA_NO_EFFECT, ""},
};
const show_t show_fix[] = {
{0, 2000, PA_LEFT, PA_PRINT, PA_GROW_DOWN, self.display_buff}, // SHOW_PARAM
{20, 0, PA_LEFT, PA_OPENING_CURSOR, PA_NO_EFFECT, self.display_buff}, // SHOW_PUNCH
{40, 0, PA_LEFT, PA_SCROLL_LEFT, PA_SCROLL_LEFT, self.display_buff}, // SHOW_ENCOURAGEMENTS
};
volatile int32_t encoder_pos; // used in ISR so have to be volatile
MD_Parola display = MD_Parola(MD_MAX72XX::PAROLA_HW, MAX7219_CS_PIN, MAX7219_DEVICES);
MD_UISwitch_Analog::uiAnalogKeys_t JOY_X_KEYS[] = {
{JOYSTICK_POT_THRESHOLD, JOYSTICK_POT_THRESHOLD, JOYSTICK_KEY_RIGHT},
{1023 - JOYSTICK_POT_THRESHOLD, JOYSTICK_POT_THRESHOLD, JOYSTICK_KEY_LEFT},
};
MD_UISwitch_Analog joy_x(JOYSTICK_POT_X_PIN, JOY_X_KEYS, ARRAY_SIZE(JOY_X_KEYS));
MD_UISwitch_Digital joy_a(JOYSTICK_BTN_A_PIN);
void next_index(uint8_t &index, uint8_t limit)
{
if (++index >= limit)
index = 0;
}
void encoder_isr()
{
// if (digitalRead(ENCODER_A_PIN) != digitalRead(ENCODER_B_PIN))
if ((PIND & 0x0C) % 0x0C) // faster than above!
encoder_pos--;
else
encoder_pos++;
}
void set_motor_speed(int16_t speed)
{
self.motor_speed = speed;
#if HW_VERSION == 0
analogWrite(MOTOR_SPEED_PIN, abs(speed));
digitalWrite(MOTOR_DIR_PIN, speed > 0);
#elif HW_VERSION == 1
analogWrite(MOTOR_A_PIN, speed > 0 ? speed : 0);
analogWrite(MOTOR_B_PIN, speed < 0 ? -speed : 0);
#endif
}
void set_show_animation()
{
show_t s;
switch (self.show_index) {
case SHOW_INTRO:
s = show_intro[self.show_animation_index];
break;
case SHOW_IDLE:
s = show_idle[self.show_animation_index];
break;
case SHOW_PARAMS:
s = show_fix[0];
sprintf(self.display_buff, "P%u S%u", self.program_index, self.motor_speed_index);
break;
case SHOW_PUNCH:
s = show_fix[1];
sprintf(self.display_buff, "%02u %02u", self.punch_count[0], self.punch_count[1]);
break;
case SHOW_ENCOURAGEMENTS:
s = show_fix[2];
strcpy(self.display_buff, TEXT_ENCOURAGEMENTS[random(ARRAY_SIZE(TEXT_ENCOURAGEMENTS))]);
break;
}
if (s.effect_in == SHOW_RANDOM_EFFECT)
s.effect_in = RANDOM_EFFECT[random(ARRAY_SIZE(RANDOM_EFFECT))];
if (s.effect_out == SHOW_RANDOM_EFFECT)
s.effect_out = RANDOM_EFFECT[random(ARRAY_SIZE(RANDOM_EFFECT))];
display.displayText(s.text, s.align, s.speed, s.pause, s.effect_in, s.effect_out);
}
void set_show(uint8_t index)
{
self.show_index = index;
self.show_animation_index = 0;
display.displayClear();
set_show_animation();
}
void set_punch(bool is_right)
{
if (self.motor_state)
return;
self.motor_state = MOTOR_STATE_FROM_HOME;
self.motor_speed = MOTOR_SPEED[self.motor_speed_index];
set_motor_speed(is_right ? self.motor_speed : -self.motor_speed);
}
void update_joystick()
{
if (joy_a.read() == MD_UISwitch::KEY_PRESS) // KEY_LONGPRESS KEY_RPTPRESS
self.joystick_key = JOYSTICK_KEY_ENTER;
else if (joy_x.read() == MD_UISwitch::KEY_PRESS)
self.joystick_key = joy_x.getKey();
else
self.joystick_key = JOYSTICK_KEY_NONE;
}
void set_program(bool is_on)
{
if (is_on) {
self.program_punch_index = 0;
if (self.program_index)
set_show(SHOW_ENCOURAGEMENTS);
else {
self.punch_count[0] = self.punch_count[1] = 0;
set_show(SHOW_PUNCH);
}
}
else {
self.program_punch_index = PROGRAM_STOP_PUNCH;
set_show(SHOW_IDLE);
}
}
void update_program()
{
if (self.program_punch_index == PROGRAM_STOP_PUNCH)
return;
if (self.motor_state)
return;
if (self.program_index) {
char punch = MOTOR_PROGRAMS[self.program_index - 1][self.program_punch_index++];
if (punch)
set_punch(punch == 'R');
else
set_program(false);
}
else if ((self.joystick_key == JOYSTICK_KEY_LEFT) || (self.joystick_key == JOYSTICK_KEY_RIGHT)) {
uint8_t is_right = self.joystick_key - JOYSTICK_KEY_LEFT;
set_punch(is_right);
self.punch_count[is_right]++;
set_show_animation();
}
}
void update_show()
{
if (display.displayAnimate()) {
switch (self.show_index) {
case SHOW_INTRO:
next_index(self.show_animation_index, ARRAY_SIZE(show_intro));
if (self.show_animation_index)
set_show_animation();
else
set_show(SHOW_IDLE);
break;
case SHOW_IDLE:
if (!random(30))
set_show(SHOW_INTRO); // show intro about every 5 minutes
else {
next_index(self.show_animation_index, ARRAY_SIZE(show_idle));
set_show_animation();
}
break;
case SHOW_PARAMS:
set_show(SHOW_IDLE);
break;
case SHOW_ENCOURAGEMENTS:
self.show_animation_index = !self.show_animation_index;
if (self.show_animation_index)
set_show_animation();
else // insert a random pause between encouragements
display.displayText("", PA_LEFT, 0, random(6) * 500, PA_NO_EFFECT);
break;
}
}
}
void update_menu()
{
switch (self.joystick_key) {
case JOYSTICK_KEY_ENTER:
set_program(self.program_punch_index == PROGRAM_STOP_PUNCH);
break;
case JOYSTICK_KEY_LEFT:
case JOYSTICK_KEY_RIGHT:
if (self.program_punch_index == PROGRAM_STOP_PUNCH) {
if (self.joystick_key == JOYSTICK_KEY_LEFT)
next_index(self.program_index, ARRAY_SIZE(MOTOR_PROGRAMS) + 1); // program 0 is manual
else
next_index(self.motor_speed_index, ARRAY_SIZE(MOTOR_SPEED));
set_show(SHOW_PARAMS);
}
break;
}
}
void update_motor()
{
if (!self.motor_state)
return;
int32_t motor_pos = encoder_pos; // fast read volatile
motor_pos = abs(motor_pos);
if (self.motor_state == MOTOR_STATE_FROM_HOME) {
if (motor_pos >= MOTOR_POS_PUNCH) {
self.motor_state = MOTOR_STATE_TO_HOME;
set_motor_speed(-self.motor_speed);
}
}
else if (motor_pos <= MOTOR_POS_THRESHOLD) {
self.motor_state = MOTOR_STATE_AT_HOME;
set_motor_speed(0);
}
}
void setup()
{
attachInterrupt(digitalPinToInterrupt(ENCODER_A_PIN), encoder_isr, CHANGE);
pinMode(ENCODER_A_PIN, INPUT_PULLUP);
pinMode(ENCODER_B_PIN, INPUT_PULLUP);
#if HW_VERSION == 0
pinMode(MOTOR_DIR_PIN, OUTPUT);
pinMode(MOTOR_SPEED_PIN, OUTPUT);
#elif HW_VERSION == 1
pinMode(MOTOR_A_PIN, OUTPUT);
pinMode(MOTOR_B_PIN, OUTPUT);
#endif
set_motor_speed(0);
self.program_punch_index = PROGRAM_STOP_PUNCH;
joy_x.begin();
joy_x.enableDoublePress(false);
joy_x.enableLongPress(false);
joy_x.enableRepeat(false);
joy_x.enableRepeatResult(false);
joy_a.begin();
joy_a.enableDoublePress(false);
joy_a.enableLongPress(false);
joy_a.enableRepeat(false);
joy_a.enableRepeatResult(false);
display.begin();
display.addChar('1', CHAR_1);
display.setIntensity(MAX7219_BRIGHTNESS);
randomSeed(analogRead(A7));
set_show(SHOW_INTRO);
}
void loop()
{
update_joystick();
update_menu();
update_program();
update_motor();
update_show();
}