diff --git a/include/BOARD-DEFINITIONS.h b/include/BOARD-DEFINITIONS.h index 0c80af9..4ded666 100644 --- a/include/BOARD-DEFINITIONS.h +++ b/include/BOARD-DEFINITIONS.h @@ -4,32 +4,67 @@ */ #pragma once +#include +#include #include "PINOUT.h" -#include "CubeStack.h" +#include "BoardTypes.h" +#include "Vector3D.h" + +// define some important buffer sizes +static constexpr uint32_t SERIAL_ARG_LENGTH{15}; +static constexpr uint32_t SERIAL_CHAR_LENGTH{SERIAL_ARG_LENGTH*10}; // define the physical dimensions of the board -#define BOARD_WIDTH 3 -#define BOARD_LENGTH 3 -#define BOARD_HEIGHT 3 +static constexpr uint32_t BOARD_WIDTH{3}; +static constexpr uint32_t BOARD_LENGTH{3}; +static constexpr uint32_t BOARD_HEIGHT{3}; + +static constexpr V3D BOARD_DIMENSIONS{BOARD_WIDTH, BOARD_LENGTH, BOARD_HEIGHT}; // define the number of stacks -#define NUMBER_STACKS BOARD_WIDTH * BOARD_LENGTH +static constexpr uint32_t NUMBER_STACKS{BOARD_WIDTH * BOARD_LENGTH}; // define the CubeStacks -CubeStack stack1(STACK1_ADC_PIN, STACK1_LED_PIN, BOARD_HEIGHT); -CubeStack stack2(STACK2_ADC_PIN, STACK2_LED_PIN, BOARD_HEIGHT); -CubeStack stack3(STACK3_ADC_PIN, STACK3_LED_PIN, BOARD_HEIGHT); -CubeStack stack4(STACK4_ADC_PIN, STACK4_LED_PIN, BOARD_HEIGHT); -CubeStack stack5(STACK5_ADC_PIN, STACK5_LED_PIN, BOARD_HEIGHT); -CubeStack stack6(STACK6_ADC_PIN, STACK6_LED_PIN, BOARD_HEIGHT); -CubeStack stack7(STACK7_ADC_PIN, STACK7_LED_PIN, BOARD_HEIGHT); -CubeStack stack8(STACK8_ADC_PIN, STACK8_LED_PIN, BOARD_HEIGHT); -CubeStack stack9(STACK9_ADC_PIN, STACK9_LED_PIN, BOARD_HEIGHT); +static BOARD_TYPES::CubeStack stack1{ + .adcPin=STACK1_ADC_PIN, + .ledPin=STACK1_LED_PIN +}; +static BOARD_TYPES::CubeStack stack2{ + .adcPin=STACK2_ADC_PIN, + .ledPin=STACK2_LED_PIN +}; +static BOARD_TYPES::CubeStack stack3{ + .adcPin=STACK3_ADC_PIN, + .ledPin=STACK3_LED_PIN +}; +static BOARD_TYPES::CubeStack stack4{ + .adcPin=STACK4_ADC_PIN, + .ledPin=STACK4_LED_PIN +}; +static BOARD_TYPES::CubeStack stack5{ + .adcPin=STACK5_ADC_PIN, + .ledPin=STACK5_LED_PIN +}; +static BOARD_TYPES::CubeStack stack6{ + .adcPin=STACK6_ADC_PIN, + .ledPin=STACK6_LED_PIN +}; +static BOARD_TYPES::CubeStack stack7{ + .adcPin=STACK7_ADC_PIN, + .ledPin=STACK7_LED_PIN +}; +static BOARD_TYPES::CubeStack stack8{ + .adcPin=STACK8_ADC_PIN, + .ledPin=STACK8_LED_PIN +}; +static BOARD_TYPES::CubeStack stack9{ + .adcPin=STACK9_ADC_PIN, + .ledPin=STACK9_LED_PIN +}; -// define the array of stacks -CubeStack * stacks[] = { - &stack1, &stack2, &stack3, - &stack4, &stack5, &stack6, - &stack7, &stack8, &stack9 +static std::array stacks{ + stack1, stack2, stack3, + stack4, stack5, stack6, + stack7, stack8, stack9 }; \ No newline at end of file diff --git a/include/PINOUT.h b/include/PINOUT.h index e6764b4..1a7a974 100644 --- a/include/PINOUT.h +++ b/include/PINOUT.h @@ -5,45 +5,47 @@ #pragma once +#include + // Stack pins // Stack 1 pins -#define STACK1_ADC_PIN 6 -#define STACK1_LED_PIN 11 +static constexpr uint8_t STACK1_ADC_PIN{6}; +static constexpr uint8_t STACK1_LED_PIN{11}; // Stack 2 pins -#define STACK2_ADC_PIN 16 -#define STACK2_LED_PIN 14 +static constexpr uint8_t STACK2_ADC_PIN{16}; +static constexpr uint8_t STACK2_LED_PIN{14}; // Stack 3 pins -#define STACK3_ADC_PIN 8 -#define STACK3_LED_PIN 45 +static constexpr uint8_t STACK3_ADC_PIN{8}; +static constexpr uint8_t STACK3_LED_PIN{45}; // Stack 4 pins -#define STACK4_ADC_PIN 5 -#define STACK4_LED_PIN 10 +static constexpr uint8_t STACK4_ADC_PIN{5}; +static constexpr uint8_t STACK4_LED_PIN{10}; // Stack 5 pins -#define STACK5_ADC_PIN 15 -#define STACK5_LED_PIN 13 +static constexpr uint8_t STACK5_ADC_PIN{15}; +static constexpr uint8_t STACK5_LED_PIN{13}; // Stack 6 pins -#define STACK6_ADC_PIN 18 -#define STACK6_LED_PIN 38 +static constexpr uint8_t STACK6_ADC_PIN{18}; +static constexpr uint8_t STACK6_LED_PIN{38}; // Stack 7 pins -#define STACK7_ADC_PIN 4 -#define STACK7_LED_PIN 9 +static constexpr uint8_t STACK7_ADC_PIN{4}; +static constexpr uint8_t STACK7_LED_PIN{9}; // Stack 8 pins -#define STACK8_ADC_PIN 7 -#define STACK8_LED_PIN 12 +static constexpr uint8_t STACK8_ADC_PIN{7}; +static constexpr uint8_t STACK8_LED_PIN{12}; // Stack 9 pins -#define STACK9_ADC_PIN 17 -#define STACK9_LED_PIN 37 +static constexpr uint8_t STACK9_ADC_PIN{17}; +static constexpr uint8_t STACK9_LED_PIN{37}; // Bluetooth module configuration pins -#define BT_STATE_PIN 2 -#define BT_EN_PIN 3 +static constexpr uint8_t BT_STATE_PIN{2}; +static constexpr uint8_t BT_EN_PIN{3}; diff --git a/include/Vector3D.h b/include/Vector3D.h new file mode 100644 index 0000000..cb60300 --- /dev/null +++ b/include/Vector3D.h @@ -0,0 +1,80 @@ +#pragma once + +#include +#include + +template +class V3D{ + public: + constexpr V3D(const V3D& other): + x(other.x), + y(other.y), + z(other.z){ + static_assert(std::is_arithmetic::value, "Type must be a number"); + } + + constexpr V3D(Type x=0, Type y=0, Type z=0): + x(x), + y(y), + z(z){ + static_assert(std::is_arithmetic::value, "Type must be a number"); + } + + template + constexpr V3D(const V3D other): + x(static_cast(other.x)), + y(static_cast(other.y)), + z(static_cast(other.z)){ + static_assert(std::is_arithmetic::value, "Type must be a number"); + static_assert(std::is_arithmetic::value, "OtherType must be a number"); + } + + V3D& operator=(const V3D &other){ + this->x = other.x; + this->y = other.y; + this->z = other.z; + return *this; + } + + V3D& operator+=(const V3D &other){ + this->x += other.x; + this->y += other.y; + this->z += other.z; + return *this; + } + + V3D& operator-=(const V3D &other){ + this->x -= other.x; + this->y -= other.y; + this->z -= other.z; + return *this; + } + + V3D& operator/=(const Type scalar){ + if(scalar == 0){ + return *this; + } + this->x /= scalar; + this->y /= scalar; + this->z /= scalar; + return *this; + } + + V3D& operator*=(const Type scalar){ + this->x *= scalar; + this->y *= scalar; + this->z *= scalar; + return *this; + } + + bool operator==(const V3D &other){ + return this->x == other.x && this->y == other.y && this->z == other.z; + } + + float magnitude(){ + return std::sqrt(static_cast(this->x * this->x + this->y * this->y + this->z * this->z)); + } + Type x; + Type y; + Type z; +}; \ No newline at end of file diff --git a/lib/Animator/Animation.h b/lib/Animator/Animation.h new file mode 100644 index 0000000..4adfa02 --- /dev/null +++ b/lib/Animator/Animation.h @@ -0,0 +1,221 @@ +#pragma once + +#include "AnimationTypes.h" +#include "Vector3D.h" +#include "Animator.h" + +using namespace ANIMATION_TYPES; + +namespace AnimationHelpers{ + V3D red{255,0,0}; + V3D green{0,255,0}; + V3D blue{0,0,255}; + V3D cyan{0,255,255}; + V3D magenta{255,0,255}; + + Cell CreateCell(float x_percent, float y_percent, float z_percent, V3D &color){ + float continuousMaxValue{static_cast(std::numeric_limits::max())}; + Cell cell{ + .position = V3D{ + static_cast(continuousMaxValue*x_percent), + static_cast(continuousMaxValue*y_percent), + static_cast(continuousMaxValue*z_percent) + }, + .color = color + }; + + return cell; + } +} + +namespace RotatingCubes{ + using namespace AnimationHelpers; + + AnimationFrame frame1{ + .frame = { + CreateCell(0,0,0,red), + CreateCell(1,0.5,0,green), + CreateCell(0,1,0,blue) + }, + .fillInterpolation = FillInterpolation::CLOSEST_COLOR, + .frameInterpolation = FrameInterpolation::FADE, + .delay = std::chrono::milliseconds(1000) + }; + + AnimationFrame frame2{ + .frame = { + CreateCell(0,0.5,0,red), + CreateCell(1,0,0,green), + CreateCell(0.5,1,0,blue) + }, + .fillInterpolation = FillInterpolation::CLOSEST_COLOR, + .frameInterpolation = FrameInterpolation::FADE, + .delay = std::chrono::milliseconds(500) + }; + + AnimationFrame frame3{ + .frame = { + CreateCell(0,1,0,red), + CreateCell(0.5,0,0,green), + CreateCell(1,1,0,blue) + }, + .fillInterpolation = FillInterpolation::CLOSEST_COLOR, + .frameInterpolation = FrameInterpolation::FADE, + .delay = std::chrono::milliseconds(1000) + }; + + AnimationFrame frame4{ + .frame = { + CreateCell(0.5,1,0,red), + CreateCell(0,0,0,green), + CreateCell(1,0.5,0,blue) + }, + .fillInterpolation = FillInterpolation::CLOSEST_COLOR, + .frameInterpolation = FrameInterpolation::FADE, + .delay = std::chrono::milliseconds(500) + }; + + AnimationFrame frame5{ + .frame = { + CreateCell(1,1,0,red), + CreateCell(0,0.5,0,green), + CreateCell(1,0,0,blue) + }, + .fillInterpolation = FillInterpolation::CLOSEST_COLOR, + .frameInterpolation = FrameInterpolation::FADE, + .delay = std::chrono::milliseconds(1000) + }; + + AnimationFrame frame6{ + .frame = { + CreateCell(1,0.5,0,red), + CreateCell(0,1,0,green), + CreateCell(0.5,0,0,blue) + }, + .fillInterpolation = FillInterpolation::CLOSEST_COLOR, + .frameInterpolation = FrameInterpolation::FADE, + .delay = std::chrono::milliseconds(500) + }; + + AnimationFrame frame7{ + .frame = { + CreateCell(1,0,0,red), + CreateCell(0.5,1,0,green), + CreateCell(0,0,0,blue) + }, + .fillInterpolation = FillInterpolation::CLOSEST_COLOR, + .frameInterpolation = FrameInterpolation::FADE, + .delay = std::chrono::milliseconds(1000) + }; + + AnimationFrame frame8{ + .frame = { + CreateCell(0.5,0,0,red), + CreateCell(1,1,0,green), + CreateCell(0,0.5,0,blue) + }, + .fillInterpolation = FillInterpolation::CLOSEST_COLOR, + .frameInterpolation = FrameInterpolation::FADE, + .delay = std::chrono::milliseconds(500) + }; + + AnimationFrame frame9{ + .frame = { + CreateCell(0,0,0,red), + CreateCell(1,0.5,0,green), + CreateCell(0,1,0,blue) + }, + .fillInterpolation = FillInterpolation::CLOSEST_COLOR, + .frameInterpolation = FrameInterpolation::FADE, + .delay = std::chrono::milliseconds(1) + }; + + std::vector rotating{ + frame1, // 0 + frame2, // 1 + frame3, // 2 + frame4, // 3 + frame5, // 4 + frame6, // 5 + frame7, // 6 + frame8, // 7 + frame9, // 8 + }; +} + +namespace RisingCubes{ + using namespace AnimationHelpers; + + AnimationFrame frame1{ + .frame = { + CreateCell(0,0,0,cyan), + CreateCell(0,1,0.5,green), + CreateCell(1,0,1,blue), + CreateCell(0.5,0.5,0.5,red), + CreateCell(1,1,0,magenta) + }, + .fillInterpolation = FillInterpolation::LINEAR_WEIGHTED_DISTANCE, + .frameInterpolation = FrameInterpolation::FADE, + .delay = std::chrono::milliseconds(800) + }; + + AnimationFrame frame2{ + .frame = { + CreateCell(0,0,0.5,cyan), + CreateCell(0,1,1,green), + CreateCell(1,0,0.5,blue), + CreateCell(0.5,0.5,0,red), + CreateCell(1,1,0.5,magenta) + }, + .fillInterpolation = FillInterpolation::LINEAR_WEIGHTED_DISTANCE, + .frameInterpolation = FrameInterpolation::FADE, + .delay = std::chrono::milliseconds(800) + }; + + AnimationFrame frame3{ + .frame = { + CreateCell(0,0,1,cyan), + CreateCell(0,1,0.5,green), + CreateCell(1,0,0,blue), + CreateCell(0.5,0.5,0.5,red), + CreateCell(1,1,1,magenta) + }, + .fillInterpolation = FillInterpolation::LINEAR_WEIGHTED_DISTANCE, + .frameInterpolation = FrameInterpolation::FADE, + .delay = std::chrono::milliseconds(800) + }; + + AnimationFrame frame4{ + .frame = { + CreateCell(0,0,0.5,cyan), + CreateCell(0,1,0,green), + CreateCell(1,0,0.5,blue), + CreateCell(0.5,0.5,1,red), + CreateCell(1,1,0.5,magenta) + }, + .fillInterpolation = FillInterpolation::LINEAR_WEIGHTED_DISTANCE, + .frameInterpolation = FrameInterpolation::FADE, + .delay = std::chrono::milliseconds(800) + }; + + AnimationFrame frame5{ + .frame = { + CreateCell(0,0,0,cyan), + CreateCell(0,1,0.5,green), + CreateCell(1,0,1,blue), + CreateCell(0.5,0.5,0.5,red), + CreateCell(1,1,0,magenta) + }, + .fillInterpolation = FillInterpolation::LINEAR_WEIGHTED_DISTANCE, + .frameInterpolation = FrameInterpolation::FADE, + .delay = std::chrono::milliseconds(1) + }; + + std::vector rising{ + frame1, // 0 + frame2, // 1 + frame3, // 2 + frame4, // 3 + frame5 + }; +} \ No newline at end of file diff --git a/lib/Animator/AnimationTypes.h b/lib/Animator/AnimationTypes.h new file mode 100644 index 0000000..d3b29c5 --- /dev/null +++ b/lib/Animator/AnimationTypes.h @@ -0,0 +1,33 @@ +#pragma once +#include "Vector3D.h" +#include +#include + +namespace ANIMATION_TYPES{ + // for cube spots which aren't defined in a key frame, + // you can have the controller automatically interpolate a color + enum FillInterpolation{ + NO_FILL, // if not specified, the cube color will be black + CLOSEST_COLOR, // The cube color will be the same color as the cube closest to it + LINEAR_WEIGHTED_DISTANCE, // the cube color will be an average of all specified cube colors weighted by the linear distance to this cube + SQUARE_WEIGHTED_DISTANCE // same as linear, but further colors have exponentially less impact on the color + }; + + enum FrameInterpolation{ + SNAP, // After the delay, snap to the next key frame + FADE // over the course of the delay, fade to the next frame + }; + + struct Cell{ + V3D position; + V3D color; + }; + + // this contains all of the information to specify exactly how a single frame should look and fade to the next frame + struct AnimationFrame{ + std::vector frame; + FillInterpolation fillInterpolation; + FrameInterpolation frameInterpolation; + std::chrono::milliseconds delay; + }; +}; \ No newline at end of file diff --git a/lib/Animator/Animator.h b/lib/Animator/Animator.h new file mode 100644 index 0000000..2499150 --- /dev/null +++ b/lib/Animator/Animator.h @@ -0,0 +1,293 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "Vector3D.h" +#include "AnimationTypes.h" + +using namespace ANIMATION_TYPES; + +template &BOARD_DIMS> +class Animator{ + +public: +typedef std::array, BOARD_DIMS.z>, BOARD_DIMS.y>, BOARD_DIMS.x> Frame; + + +void StartAnimation(const std::vector *animationSequence); + +void RunAnimation(const std::chrono::milliseconds& timePassed); + +void SetLoop(bool isLooping); + +Frame &GetInterpolatedFrame(){return this->interpolatedFrame;} + +bool isEnabled{true}; +bool interpolatedFrameHasChanged{false}; + +private: +bool isLooping{true}; +// these are the uncompressed frames you get by following the key colors and interpolation instructions of an animation frame +Frame startFrame; +Frame interpolatedFrame; +Frame endFrame; +std::chrono::milliseconds timeElapsed; + +const std::vector *animationSequence; +uint32_t animationIndex{0}; + +void incrimentAnimationIndex(); + +void uncompressFrame(const AnimationFrame &keyFrame, Frame &frameBuffer); + +void copyFrame(Frame ©From, Frame ©To){ + std::memcpy(©To, ©From, sizeof(Frame)); +} + +V3D getInterpolatedColor(const AnimationFrame &keyFrame, V3D position); + +V3D keyFrame2BoardCoords(const V3D &keyFramePosition); + +V3D noFillInterpolate(const AnimationFrame &keyFrame, V3D position); + +V3D closestColorInterpolate(const AnimationFrame &keyFrame, V3D position); + +V3D linearInterpolate(const AnimationFrame &keyFrame, V3D position); + +V3D squareInterpolate(const AnimationFrame &keyFrame, V3D position); + +void PrintUncompressedFrame(){ + for(uint32_t x = 0; x < BOARD_DIMS.x; x++){ + for(uint32_t y = 0; y < BOARD_DIMS.y; y++){ + for(uint32_t z = 0; z < BOARD_DIMS.z; z++){ + auto color = this->startFrame[x][y][z]; + Serial.print("Cube X:" + String(x) + ",Y:" + String(y) + ",Z:" + String(z)); + Serial.println("\tColor R:" + String(color.x) + ",G:" + String(color.y) + ",B:" + String(color.z)); + } + } + } +} + +}; + +template &BOARD_DIMS> +void Animator::StartAnimation(const std::vector *animationSequence){ + + this->animationSequence = animationSequence; + this->animationIndex = 0; + this->timeElapsed = std::chrono::milliseconds(0); + + if(animationSequence->size() == 0){ + return; + } + else if(animationSequence->size() == 1){ + AnimationFrame frame{((*this->animationSequence)[0])}; + this->uncompressFrame(frame, this->startFrame); + this->copyFrame(this->startFrame, this->interpolatedFrame); + this->copyFrame(this->startFrame, this->endFrame); + } + else{ + this->uncompressFrame((*this->animationSequence)[0], this->startFrame); + this->copyFrame(this->startFrame, this->interpolatedFrame); + this->uncompressFrame((*this->animationSequence)[1], this->endFrame); + } + this->interpolatedFrameHasChanged = true; +} + +template &BOARD_DIMS> +void Animator::RunAnimation(const std::chrono::milliseconds& timePassed){ + if(!(this->isEnabled)){ + return; + } + + auto delayTime = (*this->animationSequence)[this->animationIndex].delay; + this->timeElapsed += timePassed; + + // load in the next frame if we're done with this transition + if(this->timeElapsed >= delayTime){ + this->incrimentAnimationIndex(); + } + + // don't do frame interpolations if we're doing snap fades + if((*this->animationSequence)[this->animationIndex].frameInterpolation == FrameInterpolation::SNAP){ + return; + } + + // linearly interpolate between the two uncompressed frames + for(uint32_t x = 0; x < BOARD_DIMS.x; x++){ + for(uint32_t y = 0; y < BOARD_DIMS.y; y++){ + for(uint32_t z = 0; z < BOARD_DIMS.z; z++){ + V3D startColor{this->startFrame[x][y][z]}; + V3D endColor{this->endFrame[x][y][z]}; + V3D difference{endColor}; + difference -= startColor; + + V3D interpolatedColor{difference}; + interpolatedColor *= this->timeElapsed.count(); + interpolatedColor /= delayTime.count(); + interpolatedColor += startColor; + + this->interpolatedFrame[x][y][z] = interpolatedColor; + } + } + } + + this->interpolatedFrameHasChanged = true; +} + +template &BOARD_DIMS> +void Animator::SetLoop(bool isLooping){ + this->isLooping = isLooping; +} + +template &BOARD_DIMS> +void Animator::incrimentAnimationIndex(){ + if(this->animationIndex < this->animationSequence->size() - 2){ + this->animationIndex++; + this->timeElapsed = std::chrono::milliseconds(0); + this->uncompressFrame((*this->animationSequence)[this->animationIndex], this->startFrame); + this->copyFrame(this->startFrame, this->interpolatedFrame); + this->uncompressFrame((*this->animationSequence)[this->animationIndex + 1], this->endFrame); + } + else{ + this->StartAnimation(this->animationSequence); + } + + this->interpolatedFrameHasChanged = true; +} + +template &BOARD_DIMS> +void Animator::uncompressFrame(const AnimationFrame &keyFrame, Frame &frameBuffer){ + for(uint32_t x = 0; x < BOARD_DIMS.x; x++){ + for(uint32_t y = 0; y < BOARD_DIMS.y; y++){ + for(uint32_t z = 0; z < BOARD_DIMS.z; z++){ + frameBuffer[x][y][z] = getInterpolatedColor(keyFrame, V3D(x, y, z)); + } + } + } +} + +template &BOARD_DIMS> +V3D Animator::getInterpolatedColor(const AnimationFrame &keyFrame, V3D position){ + switch(keyFrame.fillInterpolation){ + case FillInterpolation::NO_FILL: + return noFillInterpolate(keyFrame, position); + case FillInterpolation::CLOSEST_COLOR: + return closestColorInterpolate(keyFrame, position); + case FillInterpolation::LINEAR_WEIGHTED_DISTANCE: + return linearInterpolate(keyFrame, position); + case FillInterpolation::SQUARE_WEIGHTED_DISTANCE: + return squareInterpolate(keyFrame, position); + default: + V3D black{}; + return black; + } +} + +template &BOARD_DIMS> +V3D Animator::keyFrame2BoardCoords(const V3D &keyFramePosition){ + V3D returnValue{}; + float maxValue{static_cast(std::numeric_limits::max())}; + // scale the key frame values down to be within board coordinates + float keyFrame_X = static_cast(BOARD_DIMS.x - 1) * static_cast(keyFramePosition.x) / maxValue; + float keyFrame_Y = static_cast(BOARD_DIMS.y - 1) * static_cast(keyFramePosition.y) / maxValue; + float keyFrame_Z = static_cast(BOARD_DIMS.z - 1) * static_cast(keyFramePosition.z) / maxValue; + + // carefully quantize the float values back into ints with a precise rounding operation + if(keyFrame_X - std::floor(keyFrame_X) < 0.5f){ + returnValue.x = static_cast(keyFrame_X); + } + else{ + returnValue.x = static_cast(keyFrame_X) + 1; + } + + if(keyFrame_Y - std::floor(keyFrame_Y) < 0.5f){ + returnValue.y = static_cast(keyFrame_Y); + } + else{ + returnValue.y = static_cast(keyFrame_Y) + 1; + } + + if(keyFrame_Z - std::floor(keyFrame_Z) < 0.5f){ + returnValue.z = static_cast(keyFrame_Z); + } + else{ + returnValue.z = static_cast(keyFrame_Z) + 1; + } + + return returnValue; +} + +template &BOARD_DIMS> +V3D Animator::noFillInterpolate(const AnimationFrame &keyFrame, V3D position){ + V3D returnColor{}; + for(Cell cell : keyFrame.frame){ + if(keyFrame2BoardCoords(cell.position) == position){ + returnColor = cell.color; + } + } + + return returnColor; +} + +template &BOARD_DIMS> +V3D Animator::closestColorInterpolate(const AnimationFrame &keyFrame, V3D cubePosition){ + V3D returnColor{keyFrame.frame[0].color}; + V3D distance{keyFrame.frame[0].position}; + distance -= cubePosition; + float closestDistance = distance.magnitude(); + + for(Cell cell : keyFrame.frame){ + distance = keyFrame2BoardCoords(cell.position); + distance -= cubePosition; + float euclidDistance = distance.magnitude(); + if(euclidDistance < closestDistance){ + returnColor = cell.color; + closestDistance = euclidDistance; + } + } + return returnColor; +} + +template &BOARD_DIMS> +V3D Animator::linearInterpolate(const AnimationFrame &keyFrame, V3D position){ + V3D returnColor{}; + + for(Cell cell : keyFrame.frame){ + V3D vectorDistance{keyFrame2BoardCoords(cell.position)}; + vectorDistance -= position; + float distance = vectorDistance.magnitude(); + if(distance == 0) return cell.color; + returnColor += cell.color; + returnColor /= distance; + } + + returnColor /= keyFrame.frame.size(); + + return returnColor; +} + +template &BOARD_DIMS> +V3D Animator::squareInterpolate(const AnimationFrame &keyFrame, V3D position){ + V3D returnColor{}; + + for(Cell cell : keyFrame.frame){ + V3D vectorDistance{keyFrame2BoardCoords(cell.position)}; + vectorDistance -= position; + uint32_t distance = static_cast(vectorDistance.magnitude()); + distance *= distance; + if(distance == 0) return cell.color; + returnColor += cell.color; + returnColor /= distance; + } + + returnColor /= keyFrame.frame.size(); + + return returnColor; +} \ No newline at end of file diff --git a/lib/Animator/TestFrames.h b/lib/Animator/TestFrames.h new file mode 100644 index 0000000..4a3e2b4 --- /dev/null +++ b/lib/Animator/TestFrames.h @@ -0,0 +1,169 @@ +#pragma once + +#include "AnimationTypes.h" +#include "Vector3D.h" +#include "Animator.h" + +using namespace ANIMATION_TYPES; + +namespace TestFrames{ + + V3D red{255,0,0}; + V3D green{0,255,0}; + V3D blue{0,0,255}; + uint32_t maxValue{std::numeric_limits::max()}; + + Cell CreateCell(float x_percent, float y_percent, float z_percent, V3D &color){ + float continuousMaxValue{static_cast(std::numeric_limits::max())}; + Cell cell{ + .position = V3D{ + static_cast(continuousMaxValue*x_percent), + static_cast(continuousMaxValue*y_percent), + static_cast(continuousMaxValue*z_percent) + }, + .color = color + }; + + return cell; + } + + AnimationFrame noFillFrame{ + .frame = { + CreateCell(0,0,0,red), + CreateCell(0.5,0.5,0.5,green), + CreateCell(1,1,1,blue) + }, + .fillInterpolation = FillInterpolation::NO_FILL, + .frameInterpolation = FrameInterpolation::SNAP, + .delay = std::chrono::milliseconds(10000) + }; + + AnimationFrame closestColorFrame{ + .frame = { + CreateCell(0,0,0,red), + CreateCell(0.5,0.5,0.5,green), + CreateCell(1,1,1,blue) + }, + .fillInterpolation = FillInterpolation::CLOSEST_COLOR, + .frameInterpolation = FrameInterpolation::SNAP, + .delay = std::chrono::milliseconds(10000) + }; + + AnimationFrame linearFillFrame{ + .frame = { + CreateCell(0,0,0,red), + CreateCell(0.5,0.5,0.5,green), + CreateCell(1,1,1,blue) + }, + .fillInterpolation = FillInterpolation::LINEAR_WEIGHTED_DISTANCE, + .frameInterpolation = FrameInterpolation::SNAP, + .delay = std::chrono::milliseconds(10000) + }; + + AnimationFrame squareFillFrame{ + .frame = { + CreateCell(0,0,0,red), + CreateCell(0.5,0.5,0.5,green), + CreateCell(1,1,1,blue) + }, + .fillInterpolation = FillInterpolation::SQUARE_WEIGHTED_DISTANCE, + .frameInterpolation = FrameInterpolation::SNAP, + .delay = std::chrono::milliseconds(10000) + }; + + AnimationFrame noFillFadeFrame{ + .frame = { + CreateCell(0,0,0,red), + CreateCell(0.5,0.5,0.5,green), + CreateCell(1,1,1,blue) + }, + .fillInterpolation = FillInterpolation::NO_FILL, + .frameInterpolation = FrameInterpolation::FADE, + .delay = std::chrono::milliseconds(10000) + }; + + AnimationFrame closestColorFadeFrame{ + .frame = { + CreateCell(0,0,0,red), + CreateCell(0.5,0.5,0.5,green), + CreateCell(1,1,1,blue) + }, + .fillInterpolation = FillInterpolation::CLOSEST_COLOR, + .frameInterpolation = FrameInterpolation::FADE, + .delay = std::chrono::milliseconds(10000) + }; + + AnimationFrame linearFillFadeFrame{ + .frame = { + CreateCell(0,0,0,red), + CreateCell(0.5,0.5,0.5,green), + CreateCell(1,1,1,blue) + }, + .fillInterpolation = FillInterpolation::LINEAR_WEIGHTED_DISTANCE, + .frameInterpolation = FrameInterpolation::FADE, + .delay = std::chrono::milliseconds(10000) + }; + + AnimationFrame squareFillFadeFrame{ + .frame = { + CreateCell(0,0,0,red), + CreateCell(0.5,0.5,0.5,green), + CreateCell(1,1,1,blue) + }, + .fillInterpolation = FillInterpolation::SQUARE_WEIGHTED_DISTANCE, + .frameInterpolation = FrameInterpolation::FADE, + .delay = std::chrono::milliseconds(10000) + }; + + std::vector testAnimationSequence2{ + noFillFrame, // 0 + closestColorFrame, // 1 + linearFillFrame, // 2 + squareFillFrame, // 3 + noFillFadeFrame, // 4 + closestColorFadeFrame, // 5 + linearFillFadeFrame, // 6 + squareFillFadeFrame, // 7 + noFillFrame // 8 + }; + + AnimationFrame testFrame1{ + .frame = { + CreateCell(0,0,0,red), + // CreateCell(0.5,0.5,0,green), + CreateCell(1,1,0,blue) + }, + .fillInterpolation = FillInterpolation::NO_FILL, + .frameInterpolation = FrameInterpolation::FADE, + .delay = std::chrono::milliseconds(10000) + }; + + AnimationFrame testFrame2{ + .frame = { + CreateCell(0,1,0,red), + // CreateCell(0.5,0.5,0,green), + CreateCell(1,0,0,green) + }, + .fillInterpolation = FillInterpolation::NO_FILL, + .frameInterpolation = FrameInterpolation::FADE, + .delay = std::chrono::milliseconds(10000) + }; + + AnimationFrame testFrame3{ + .frame = { + CreateCell(0.5,0.5,0,red), + // CreateCell(0.5,0.5,0,green), + CreateCell(0,1,0,blue) + }, + .fillInterpolation = FillInterpolation::NO_FILL, + .frameInterpolation = FrameInterpolation::FADE, + .delay = std::chrono::milliseconds(10000) + }; + + std::vector testAnimationSequence1{ + testFrame1, + testFrame2, + testFrame3, + testFrame1 + }; +} \ No newline at end of file diff --git a/lib/Board/Board.h b/lib/Board/Board.h new file mode 100644 index 0000000..b64774d --- /dev/null +++ b/lib/Board/Board.h @@ -0,0 +1,205 @@ +/** + * @file Board.h + * @brief The Board class is meant to store data about each cube on the board and + * provide an easy way to organize and access that data in a 3-dimensional context. + */ +#pragma once + +#include +#include +#include + +#include "BoardTypes.h" +#include "Vector3D.h" + +template &BOARD_DIMS> +class Board{ + public: + Board() = default; + ~Board() = default; + + constexpr const V3D &GetSize() const{return BOARD_DIMS;} + constexpr uint32_t GetNumberCubes() const{return BOARD_DIMS.x * BOARD_DIMS.y * BOARD_DIMS.z;} + + constexpr uint32_t GetMaxDimension(){return std::max(std::max(BOARD_DIMS.x, BOARD_DIMS.y), BOARD_DIMS.z);} + /** + * @brief Returns a string in the format: + * !a,b,c,d,e,f,g,h,i; + * Where each letter is the number of cubes in a given position's stack + * @param stringBuffer the buffer to write the string into + */ + void ToStackString(String& stringBuffer) const; + + /** + * @brief fill the entire board with the given color + * @param color the color to fill the board with + */ + void FillColor(const V3D &color); + + /** + * @brief Set the color of the cube at the given position. + * Even if the cube is not occupied, it will remember this color + * and turn that color if it becomes occupied. + * @param position the position of the cube. + * @param color the color you want the cube to be + */ + void SetCubeColor(const V3D &position, const V3D &color); + + /** + * @brief Set the occupation status of the cube at a given position + * @param position the position of the cube + * @param the occupation status of the cube + * @post if the new occupation status of the cube is different than + * the old occupation status, this will enable boardStateHasChanged. + */ + void SetCubeOccupation(const V3D &position, bool occupation); + + /** + * @returns true if the board state has changed since this flag was last set to false + */ + bool BoardStateChanged(){return this->boardStateHasChanged;} + + /** + * @brief set the board state status. + * @note you should probably only use this to clear the baordState flag + */ + void SetStateChanged(bool boardState){this->boardStateHasChanged = boardState;} + + /** + * @brief Get a column along any axis read into the sliceBuffer + * @param column .z specifies the normal direction of the plane (see PLANE_NORMAL), and + * the x,y values specify the location of the column in that plane + * to fill. IE To fill one stack at 0,2 I would say give V3D(0,2,PLANE_NORMAL::Z) + * @param sliceBuffer an array of pointers to the cubes along that column + * @returns the number of elements written into the slice buffer + * @note That array is stored locally and will be overwritten everytime this function is called. + * Also, any unused spots at the end of the array will be nullptrs + * @warning allocate the size of the slice buffer using GetMaxDimension if you don't know what you're doing! + */ + uint32_t SliceBoard(const V3D &column, BOARD_TYPES::Cube ** sliceBuffer); + + void PrintEntireBoard() const; + + void UpdateAllColors(const std::array, BOARD_DIMS.z>, BOARD_DIMS.y>, BOARD_DIMS.x>& colorFrame){ + for(uint32_t x = 0; x < BOARD_DIMS.x; x++){ + for(uint32_t y = 0; y < BOARD_DIMS.y; y++){ + for(uint32_t z = 0; z < BOARD_DIMS.z; z++){ + this->cubes[x][y][z].color = colorFrame[x][y][z]; + } + } + } + } + private: + // this is a 3d array of cubes to represent the board. Good luck visualizing it + /* _____________ + / /| + / / | + / / | + /____________/ | + | | | + | | / + | | / + |____________|/ + */ + std::array, BOARD_DIMS.y>, BOARD_DIMS.x> cubes; + + bool boardStateHasChanged; +}; + +template &BOARD_DIMS> +void Board::ToStackString(String &stringBuffer) const{ + std::array linearizedBoard; + for(uint32_t x{0}; x < BOARD_DIMS.x; x++){ + for(uint32_t y{0}; y < BOARD_DIMS.y; y++){ + uint32_t boardIndex{x + y*3}; + linearizedBoard[boardIndex] = 0; + for(uint32_t z{0}; z < BOARD_DIMS.z; z++){ + linearizedBoard[boardIndex] += this->cubes[x][y][z].isOccupied; + } + } + } + + stringBuffer += String(linearizedBoard[0]); + + for(uint32_t i = 1; i < BOARD_DIMS.x * BOARD_DIMS.y; i++){ + stringBuffer += "," + String(linearizedBoard[i]); + } +} + +template &BOARD_DIMS> +void Board::FillColor(const V3D &color){ + for(uint32_t x{0}; x < BOARD_DIMS.x; x++){ + for(uint32_t y{0}; y < BOARD_DIMS.y; y++){ + for(uint32_t z{0}; z < BOARD_DIMS.z; z++){ + this->cubes[x][y][z].color = color; + } + } + } +} + +template &BOARD_DIMS> +void Board::SetCubeColor(const V3D &position, const V3D &color){ + this->cubes[position.x][position.y][position.z].color = color; +} + +template &BOARD_DIMS> +void Board::SetCubeOccupation(const V3D &position, bool occupation){ + bool oldOccupation{this->cubes[position.x][position.y][position.z].isOccupied}; + this->cubes[position.x][position.y][position.z].isOccupied = occupation; + if(occupation != oldOccupation) this->boardStateHasChanged = true; +} + +template &BOARD_DIMS> +uint32_t Board::SliceBoard(const V3D &column, BOARD_TYPES::Cube ** sliceBuffer){ + uint32_t columnLength{0}; + V3D indexIncrimentVector{}; + V3D indexVector{}; + + switch(column.z){ + case BOARD_TYPES::PLANE_NORMAL::X: + columnLength = BOARD_DIMS.x; + indexIncrimentVector.x = 1; + indexVector.z = column.x; + indexVector.y = column.y; + break; + case BOARD_TYPES::PLANE_NORMAL::Y: + columnLength = BOARD_DIMS.y; + indexIncrimentVector.y = 1; + indexVector.x = column.x; + indexVector.z = column.y; + break; + + default: + case BOARD_TYPES::PLANE_NORMAL::Z: + columnLength = BOARD_DIMS.z; + indexIncrimentVector.z = 1; + indexVector.x = column.x; + indexVector.y = column.y; + break; + } + + for(uint32_t i = 0; i < columnLength; i++){ + if(indexVector.x >= BOARD_DIMS.x || indexVector.y >= BOARD_DIMS.y || indexVector.z >= BOARD_DIMS.z){ + Serial.println("Board::SliceBoard: Index Out of Bounds:" + String(indexVector.x) + "," + String(indexVector.y) + "," + String(indexVector.z)); + return 0; + } + sliceBuffer[i] = &(this->cubes[indexVector.x][indexVector.y][indexVector.z]); + indexVector += indexIncrimentVector; + } + return columnLength; +} + +template &BOARD_DIMS> +void Board::PrintEntireBoard() const{ + for(uint32_t x = 0; x < BOARD_DIMS.x; x++){ + for(uint32_t y = 0; y < BOARD_DIMS.y; y++){ + for(uint32_t z = 0; z < BOARD_DIMS.z; z++){ + const BOARD_TYPES::Cube &cube = this->cubes[x][y][z]; + Serial.print("Cube X:" + String(x) + ",Y:" + String(y) + ",Z:" + String(z)); + Serial.print("\tColor R:" + String(cube.color.x) + ",G:" + String(cube.color.y) + ",B:" + String(cube.color.z)); + Serial.println("\tOccupied? " + String(cube.isOccupied)); + } + } + } +} + diff --git a/lib/Board/BoardDriver.h b/lib/Board/BoardDriver.h new file mode 100644 index 0000000..fa49550 --- /dev/null +++ b/lib/Board/BoardDriver.h @@ -0,0 +1,187 @@ +/** + * @file BoardDriver.h + * @brief the board driver provides helpful methods to read the physical board's state, + * filter those results, and send color data to the board + */ +#pragma once + +#include +#include + +#include +#include + +#include "BoardTypes.h" +#include "BoardTypes.h" + +template +class BoardDriver{ +public: + + /** + * @param stacks a reference to an array which contains information about the pins which control each cube stack. + * @param pixelController a reference to a pre-constructed neopixel controller which will allow this driver to control neopixels + */ + BoardDriver( + std::array &stacks, + Adafruit_NeoPixel &pixelController + ); + ~BoardDriver() = default; + + /** + * @brief Initializes all of the pins necessary to read/write to the board + */ + void Init(); + + /** + * @param numXStacks the width of the board along the x dimension. + * @param X_COORD the x coordinate of the stack you want information about + * @param Y_COORD the y coordinate of the stack you want information about + * @returns Get the number of cubes present in a stack + */ + uint32_t GetNumberCubes(uint32_t numXStacks, uint32_t X_COORD, uint32_t Y_COORD); + + /** + * @param stackIndex the index of the stack that you want to know the height of + * @returns Get the number of cubes present in a stack + */ + uint32_t GetNumberCubes(uint32_t stackIndex); + + /** + * @brief stream data to a single cube stack on the board + * @param numXStacks the width of the board along the x dimension. + * @param X_COORD the x coordinate of the stack you want information about + * @param Y_COORD the y coordinate of the stack you want information about + * @param cubes an array of cube pointers which contain the color data you want to stream + * @param numCubes the number of cubes in the cubes array + */ + void UpdateStackLEDs( + uint32_t numXStacks, + uint32_t X_COORD, + uint32_t Y_COORD, + BOARD_TYPES::Cube* cubes[], + uint32_t numCubes + ); + + /** + * @brief stream data to a single cube stack on the board + * @param stackIndex the index of the stack you want to update the color of + * @param cubes an array of cube pointers which contain the color data you want to stream + * @param numCubes the number of cubes in the cubes array + */ + void UpdateStackLEDs( + uint32_t stackIndex, + BOARD_TYPES::Cube* cubes[], + uint32_t numCubes + ); + + +private: + std::array &stacks; + Adafruit_NeoPixel &pixelController; + std::array filteredReadings; + + uint32_t xy2StackIndex(uint32_t x_coord, uint32_t y_coord, uint32_t numXStacks){ + return x_coord + y_coord*numXStacks; + } +}; + +template +void BoardDriver::Init(){ + for(uint32_t i = 0; i < NUM_STACKS; i++){ + pinMode(this->stacks[i].ledPin, OUTPUT); + filteredReadings[i] = 0; + } + + // begin doesn't really do anything besides setting the pinmode + this->pixelController.begin(); +} + +template +BoardDriver::BoardDriver( + std::array &stacks, + Adafruit_NeoPixel &pixelController + ): +stacks(stacks), +pixelController(pixelController) +{ + for(uint32_t i = 0; i < NUM_STACKS; i++){ + this->filteredReadings[i] = 0; + } +} + +template +void BoardDriver::UpdateStackLEDs( + uint32_t numXStacks, + uint32_t X_COORD, + uint32_t Y_COORD, + BOARD_TYPES::Cube* cubes[], + uint32_t numCubes + ){ + this->UpdateStackLEDs(this->xy2StackIndex(X_COORD, Y_COORD, numXStacks), cubes, numCubes); +} + +template +void BoardDriver::UpdateStackLEDs( + uint32_t stackIndex, + BOARD_TYPES::Cube* cubes[], + uint32_t numCubes + ){ + this->pixelController.setPin(this->stacks[stackIndex].ledPin); + for(int i = 0; i < numCubes; i++){ + V3D color{cubes[i]->color}; + this->pixelController.setPixelColor(i*2, this->pixelController.Color(color.x, color.y, color.z)); + this->pixelController.setPixelColor((i*2 + 1), this->pixelController.Color(color.x, color.y, color.z)); + } + this->pixelController.show(); +} + +template +uint32_t BoardDriver::GetNumberCubes( + uint32_t numXStacks, + uint32_t X_COORD, + uint32_t Y_COORD + ){ + return this->GetNumberCubes(this->xy2StackIndex(X_COORD, Y_COORD, numXStacks)); +} + +template +uint32_t BoardDriver::GetNumberCubes(uint32_t stackIndex){ + // read the ADC and return the number of cubes + /* + 0 cubes: 1 : 4095-3400 + 1 cube: 1/2 3400-2500 + 2 cubes: 1/3 2500-1850 + 3 cubes: 1/4 1850-0 + */ + uint16_t value = analogRead(this->stacks[stackIndex].adcPin); + uint16_t lowPassADCRead = + static_cast( + (static_cast(this->filteredReadings[stackIndex]) * 0.9) + + (static_cast(value) * 0.1) + ); + // temporary definitions to define value ranges: + uint16_t zeroCubesHigh = 4095; + uint16_t zeroCubesLow = 3400; + uint16_t oneCubeLow = 2500; + uint16_t twoCubesLow = 1850; + uint16_t threeCubesLow = 0; + + uint8_t stackHeight = 0; + + if(lowPassADCRead >= zeroCubesLow && lowPassADCRead <= zeroCubesHigh){ + stackHeight = 0; + } + else if(lowPassADCRead >= oneCubeLow){ + stackHeight = 1; + } + else if(lowPassADCRead >= twoCubesLow){ + stackHeight = 2; + } + else if(lowPassADCRead >= threeCubesLow){ + stackHeight = 3; + } + + this->filteredReadings[stackIndex] = lowPassADCRead; + return stackHeight; +} \ No newline at end of file diff --git a/lib/Board/BoardLayout.cpp b/lib/Board/BoardLayout.cpp deleted file mode 100644 index b4fee9e..0000000 --- a/lib/Board/BoardLayout.cpp +++ /dev/null @@ -1,38 +0,0 @@ -#include "BoardLayout.h" - -uint8_t BoardLayout::GetNumberStacks(){ - return this->boardWidth * this->boardHeight; -} - -void BoardLayout::SetStackColors(uint8_t stackNum, Color * colors){ - CubeStack * stack = this->stacks[stackNum]; - stack->SetLEDColors(colors, this->boardHeight); -} - -bool BoardLayout::BoardStateHasChanged(){ - uint16_t boardState[this->boardWidth * this->boardLength]; - this->GetBoardState(boardState); - - // compare the board state to the last board state - for(int i = 0; i < (this->boardWidth * this->boardLength); i++){ - uint16_t stackState = boardState[i]; - uint16_t lastStackState = (this->lastBoardState)[i]; - if(stackState != lastStackState){ - // copy the board state into the last board state - for(int k = 0; k < (this->boardWidth * this->boardLength); k++){ - this->lastBoardState[k] = boardState[k]; - } - return true; - } - } - return false; -} - -void BoardLayout::GetBoardState(uint16_t * boardStateBuffer){ - for(int i = 0; i < (this->boardLength * this->boardWidth); i++){ - CubeStack * stack = this->stacks[i]; - stack->SendLEDData(); // Enable this if you want to constantly stream LED data - boardStateBuffer[i] = stack->GetNumberCubes(); - } -} - diff --git a/lib/Board/BoardLayout.h b/lib/Board/BoardLayout.h deleted file mode 100644 index d8684de..0000000 --- a/lib/Board/BoardLayout.h +++ /dev/null @@ -1,69 +0,0 @@ -/** - * @brief This is the full board manager which handles the state of every stack on the board -*/ - -#pragma once - -#include "CubeStack.h" -#include "Color.h" - -class BoardLayout{ - public: - /** - * @brief BoardLayout COnstructor - */ - BoardLayout(uint8_t boardWidth, uint8_t boardLength, uint8_t boardHeight, CubeStack ** stacks) : - boardWidth(boardWidth), - boardLength(boardLength), - boardHeight(boardHeight), - stacks(stacks) - { - this->lastBoardState = new uint16_t[boardWidth * boardLength]; - } - - /** - * @brief Check if our board state has changed - * @return true if the board state has changed, false otherwise - */ - bool BoardStateHasChanged(); - - /** - * @brief Get the Number of Stacks - * @return the number of stacks - */ - uint8_t GetNumberStacks(); - - /** - * @brief Set the LED Colors - * @param stackNum the stack index you would like to address. - * From top left to bottom right, the stack numbers are as follows: - * | 0 1 2 | - * | 3 4 5 | - * | 6 7 8 | - * @param Colors the array of colors to set the LEDs in a stack to - */ - void SetStackColors(uint8_t stackNum, Color * colors); - - /** - * @brief Get the board population state - * @param boardStateBuffer the buffer to write the board state to. It must be at least boardWidth * boardLength in length - */ - void GetBoardState(uint16_t * boardStateBuffer); - - private: - uint8_t boardWidth; - uint8_t boardLength; - uint8_t boardHeight; - /* - An array of arrays of stacks - [ [stack1, stack2, stack3], - [stack4, stack5, stack6], - [stack7, stack8, stack9] ] - etc - */ - CubeStack ** stacks; - - // records the last known board state - uint16_t * lastBoardState; - -}; \ No newline at end of file diff --git a/lib/Board/BoardManager.h b/lib/Board/BoardManager.h new file mode 100644 index 0000000..54624d3 --- /dev/null +++ b/lib/Board/BoardManager.h @@ -0,0 +1,197 @@ +/** + * @file BoardManager.h + * @brief Provides an easy way to manage a board + */ +#pragma once + +#include "Board.h" +#include "BoardDriver.h" +#include "Vector3D.h" +#include "Animator.h" + +template &BOARD_DIMS> +class BoardManager{ + public: + BoardManager(BoardDriver &boardDriver, Animator &animator); + + ~BoardManager() = default; + + /** + * @brief Initialize the board control. This hsould be called during setup + */ + void Init(); + + /** + * @brief Read the board state and update the LED colors + */ + void Update(); + + /** + * @brief Set the color of one cube + * @param position the position of the cube + * @param color the oclor you want the cube to be + */ + void SetCubeColor(const V3D &position, const V3D &color); + + /** + * @brief Set the color of one column of cubes. + * A column is defined as x,y,z where z is the normal vector direction to a plane. + * x,y is the column coordinate on the plane. + * @param column the column vector + * @param color the color you want the column to be + */ + void SetColumnColors(const V3D &column, const V3D *color, uint32_t numColors); + + + /** + * @brief Fill a column along any axis with a color + * @param column .z specifies the normal direction of the plane (see PLANE_NORMAL), and + * the x,y values specify the location of the column in that plane + * to fill. IE To fill one stack at 0,2 I would say give V3D(0,2,PLANE_NORMAL::Z) + * @param color the color you want to fill the column with + */ + void FillColumnColor(const V3D &column, const V3D &color); + + /** + * @returns true if the board has changed state + */ + bool HasBoardChanged(); + + /** + * @brief Clear the board state changed flag + */ + void ClearBoardChanged(); + + /** + * @brief Get the board occupation state returned in the format a,b,c,d.... + */ + void Board2StackString(String& messageBuffer); + + void FillColor(const V3D &color){this->board.FillColor(color);} + void PrintColorState(){this->board.PrintEntireBoard();} + + private: + BoardDriver &driver; + Board board{}; + Animator &animator; + + void updateStackColors(const V3D &column); + + uint32_t getColumnHeight(BOARD_TYPES::PLANE_NORMAL normal){ + switch(normal){ + case BOARD_TYPES::PLANE_NORMAL::X: + return BOARD_DIMS.x; + break; + case BOARD_TYPES::PLANE_NORMAL::Y: + return BOARD_DIMS.y; + break; + case BOARD_TYPES::PLANE_NORMAL::Z: + return BOARD_DIMS.z; + break; + default: + return 0; + } + } + + void updateColorsFromAnimator(); + +}; +template &BOARD_DIMS> +void BoardManager::updateColorsFromAnimator(){ + if(this->animator.interpolatedFrameHasChanged){ + this->board.UpdateAllColors(this->animator.GetInterpolatedFrame()); + this->animator.interpolatedFrameHasChanged = false; + } +} + +template &BOARD_DIMS> +BoardManager::BoardManager(BoardDriver &boardDriver, Animator &animator): + driver(boardDriver), + animator(animator){} + +template &BOARD_DIMS> +void BoardManager::Init(){ + this->driver.Init(); +} + +template &BOARD_DIMS> +void BoardManager::Update(){ + this->updateColorsFromAnimator(); + // update the occupied cubes on the board and the cube colors + for(uint32_t x = 0; x < BOARD_DIMS.x; x++){ + for(uint32_t y = 0; y < BOARD_DIMS.y; y++){ + uint32_t stackIndex{y * BOARD_DIMS.x + x}; + uint32_t numCubes{this->driver.GetNumberCubes(stackIndex)}; + for(uint32_t z = 0; z < BOARD_DIMS.z; z++){ + V3D cubePosition{x, y, z}; + // update the cube's occupation + this->board.SetCubeOccupation(cubePosition, z < numCubes); + } + + // create the column vector for the slice direction + V3D sliceVector{x,y,BOARD_TYPES::PLANE_NORMAL::Z}; + // create a cube slice array buffer + BOARD_TYPES::Cube* sliceBuffer[BOARD_DIMS.z]; + // have the board slice get read into our buffer + this->board.SliceBoard(sliceVector, sliceBuffer); + // send the board slice to the driver to update its LED colors + this->driver.UpdateStackLEDs(stackIndex, sliceBuffer, BOARD_DIMS.z); + } + } +} + +template &BOARD_DIMS> +void BoardManager::updateStackColors(const V3D &column){ + // the only column type allowed here is z. + V3D sliceVector{column.x, column.y, BOARD_TYPES::Z}; + // create a buffer for slice board to write the cube slice into + BOARD_TYPES::Cube * cubeSlice[BOARD_DIMS.z]; + this->board.SliceBoard(column, cubeSlice); + + uint32_t numCubes{this->getColumnHeight(static_cast(column.z))}; + this->driver.UpdateStackLEDs(BOARD_DIMS.x, cubeSlice, numCubes); +} + +template &BOARD_DIMS> +void BoardManager::SetCubeColor(const V3D &position, const V3D &color){ + this->board.SetCubeColor(position, color); + V3D slice{position.x, position.y, BOARD_TYPES::PLANE_NORMAL::Z}; + this->updateStackColors(slice); +} + +template &BOARD_DIMS> +void BoardManager::SetColumnColors(const V3D &column, const V3D *color, uint32_t numColors){ + uint32_t columnHeight{this->getColumnHeight(static_cast(column.z))}; + + // create a cube pointer buffer and store a board slice into it + BOARD_TYPES::Cube * slicedBoard[columnHeight]; + uint32_t sliceLength{this->board.SliceBoard(column, slicedBoard)}; + + uint32_t maxIndex{std::min(numColors, columnHeight)}; + for(uint32_t i = 0; i < maxIndex; i++){ + slicedBoard[i]->color = color[i]; + } +} + +template &BOARD_DIMS> +void BoardManager::FillColumnColor(const V3D &column, const V3D &color){ + uint32_t columnHeight{this->getColumnHeight(column.z)}; + + V3D colors[columnHeight]; + for(uint32_t i = 0; i < columnHeight; i++){ + colors[i] = color; + } + + this->SetColumnColors(column, colors); +} + +template &BOARD_DIMS> +bool BoardManager::HasBoardChanged(){return this->board.BoardStateChanged();} + +template &BOARD_DIMS> +void BoardManager::ClearBoardChanged(){this->board.SetStateChanged(false);} + +template &BOARD_DIMS> +void BoardManager::Board2StackString(String& messageBuffer){ + this->board.ToStackString(messageBuffer); +} diff --git a/lib/Board/BoardTypes.h b/lib/Board/BoardTypes.h new file mode 100644 index 0000000..4eba9a5 --- /dev/null +++ b/lib/Board/BoardTypes.h @@ -0,0 +1,26 @@ +/** + * @file BoardTypes.h + * @brief Some types to that you'll need to define and control the board + */ +#pragma once + +#include +#include "Vector3D.h" + +namespace BOARD_TYPES{ + struct CubeStack{ + uint8_t adcPin; + uint8_t ledPin; + }; + + enum PLANE_NORMAL : uint32_t{ + X = 0, + Y, + Z + }; + + struct Cube{ + V3D color; + bool isOccupied{false}; + }; +}; \ No newline at end of file diff --git a/lib/Board/Color.h b/lib/Board/Color.h deleted file mode 100644 index 015bbfb..0000000 --- a/lib/Board/Color.h +++ /dev/null @@ -1,21 +0,0 @@ -/** - * @file Color.h - * @brief This file contains the color struct -*/ - -#pragma once -#include - -// store a color -struct Color{ - // create a constructor for this struct - Color(uint8_t red, uint8_t green, uint8_t blue) : - red(red), - green(green), - blue(blue) - {} - Color() = default; - uint8_t red{0}; - uint8_t green{0}; - uint8_t blue{0}; -}; \ No newline at end of file diff --git a/lib/Board/CubeStack.cpp b/lib/Board/CubeStack.cpp deleted file mode 100644 index e8954c1..0000000 --- a/lib/Board/CubeStack.cpp +++ /dev/null @@ -1,79 +0,0 @@ -#include "CubeStack.h" - -CubeStack::CubeStack(uint16_t ADCPin, uint16_t ledPin, uint8_t numLEDs){ - this->ADCPin = ADCPin; - this->blockLights = *(new Adafruit_NeoPixel(numLEDs*2, ledPin, NEO_GRB + NEO_KHZ800)); - this->ledColors = new Color[numLEDs]; - this->numLEDs = numLEDs; - - // initialize the LED colors to off - for(int i = 0; i < numLEDs; i++){ - this->ledColors[i] = *(new Color(0, 0, 0)); - } -}; - -uint8_t CubeStack::GetNumberCubes(){ - // read the ADC and return the number of cubes - /* - 0 cubes: 1 : 4095-3400 - 1 cube: 1/2 3400-2500 - 2 cubes: 1/3 2500-1850 - 3 cubes: 1/4 1850-0 - */ - uint16_t value = analogRead(this->ADCPin); - this->lowPassADCRead = static_cast((static_cast(this->lowPassADCRead) * 0.9) + (static_cast(value) * 0.1)); - if(this->lowPassADCRead < 2500 && false){ - Serial.println("ADC Pin:" + String(this->ADCPin) + " Value: " + String(value) + " Low Pass: " + String(this->lowPassADCRead)); - } - - // temporary definitions to define value ranges: - uint16_t zeroCubesHigh = 4095; - uint16_t zeroCubesLow = 3400; - uint16_t oneCubeLow = 2500; - uint16_t twoCubesLow = 1850; - uint16_t threeCubesLow = 0; - - uint8_t stackHeight = 0; - - if(this->lowPassADCRead >= zeroCubesLow && this->lowPassADCRead <= zeroCubesHigh){ - stackHeight = 0; - } - else if(this->lowPassADCRead >= oneCubeLow){ - stackHeight = 1; - } - else if(this->lowPassADCRead >= twoCubesLow){ - stackHeight = 2; - } - else if(this->lowPassADCRead >= threeCubesLow){ - stackHeight = 3; - } - if(this->lastStackHeight != stackHeight){ - this->lastStackHeight = stackHeight; - this->SendLEDData(); - } - - return stackHeight; -} - -void CubeStack::SetLEDColors(Color * colors, uint8_t numColors){ - // copy the colors into the ledColors array - for(int i = 0; i < numColors; i++){ - this->ledColors[i].red = colors[i].red; - this->ledColors[i].green = colors[i].green; - this->ledColors[i].blue = colors[i].blue; - } - - this->SendLEDData(); -} - -void CubeStack::SendLEDData(){ - // we always initialize before we do anything because other CubeStacks could be hogging the hardware - // between our writes - this->blockLights.begin(); - // set the LED colors - for(int i = 0; i < this->numLEDs; i++){ - this->blockLights.setPixelColor(i*2, this->blockLights.Color(this->ledColors[i].red, this->ledColors[i].green, this->ledColors[i].blue)); - this->blockLights.setPixelColor((i*2 + 1), this->blockLights.Color(this->ledColors[i].red, this->ledColors[i].green, this->ledColors[i].blue)); - } - this->blockLights.show(); -} \ No newline at end of file diff --git a/lib/Board/CubeStack.h b/lib/Board/CubeStack.h deleted file mode 100644 index 634ef50..0000000 --- a/lib/Board/CubeStack.h +++ /dev/null @@ -1,52 +0,0 @@ -/** - * @brief this manages a single cube stack and the lighting / detecting of how many cubes -*/ - -#pragma once - -#include -#include -#include "Color.h" - - -class CubeStack{ - public: - /** - * @brief Construct a new Cube Stack object - * @param ADCPin the pin that the ADC is connected to - * @param ledPin the pin that the LED is connected to - */ - CubeStack(uint16_t ADCPin, uint16_t ledPin, uint8_t numLEDs); - - /** - * @brief Returns the number of cubes in the stack - * @return the number of cubes in the stack - */ - uint8_t GetNumberCubes(); - - /** - * @brief Set the led color array and then send the data to the LED strip - * @param colors the array of colors to set the LEDs to - * @param numColors the number of colors in the array - */ - void SetLEDColors(Color * colors, uint8_t numColors); - - /** - * @brief sends the LED data to the LED strip - */ - void SendLEDData(); - - private: - - - uint8_t ADCPin; - // we will probably need a pointer to a fastled object here - Adafruit_NeoPixel blockLights; - - uint16_t lowPassADCRead{0}; - - // store the Color of each LED - Color * ledColors; - uint8_t numLEDs; - uint8_t lastStackHeight{0}; -}; \ No newline at end of file diff --git a/lib/ColorManager/ColorManager.cpp b/lib/ColorManager/ColorManager.cpp deleted file mode 100644 index 95f4294..0000000 --- a/lib/ColorManager/ColorManager.cpp +++ /dev/null @@ -1,48 +0,0 @@ -#include "ColorManager.h" - -void ColorManager::Update(){ - if(!(this->enabled)){ - return; - } - // go through our colors and have them fade from r->g->b->r - for(uint8_t i = 0; i < 9; i++){ - for(uint8_t j = 0; j < 3; j++){ - Color * color = this->colors[i][j]; - // fade from red to green - if(color->red > 0 && color->green >= 0 && color->blue == 0){ - color->red--; - color->green++; - } - // fade from green to blue - else if(color->green > 0 && color->blue >= 0 && color->red == 0){ - color->green--; - color->blue++; - } - // fade from blue to red - else if(color->blue > 0 && color->red >= 0 && color->green == 0){ - color->blue--; - color->red++; - } - } - } - - // set the colors - for(uint8_t i = 0; i < 9; i++){ - Color temp_colors[3] = {*(this->colors[i][0]), *(this->colors[i][1]), *(this->colors[i][2])}; - this->board->SetStackColors(i, temp_colors); - } -} - -void ColorManager::Enable(bool enable){ - this->enabled = enable; - - if(this->enabled == false){ - // set all the colors to black - Color black(0, 0, 0); - Color temp_colors[3] = {black, black, black}; - // set the colors - for(uint8_t i = 0; i < 9; i++){ - this->board->SetStackColors(i, temp_colors); - } - } -} \ No newline at end of file diff --git a/lib/ColorManager/ColorManager.h b/lib/ColorManager/ColorManager.h deleted file mode 100644 index 375c4bf..0000000 --- a/lib/ColorManager/ColorManager.h +++ /dev/null @@ -1,44 +0,0 @@ -/** - * @file ColorManager.h - * @brief Generate pretty colors for the board and make it do something when unity isn't controlling it -*/ - -#pragma once - -#include "BoardLayout.h" -#include "Color.h" - -class ColorManager{ - public: - ColorManager(BoardLayout * board) : - board(board) - {} - - /** - * @brief Allows the color manager to update the board colors - */ - void Update(); - - /** - * @brief Enables or disables the color manager - * @param enable true to enable, false to disable - */ - void Enable(bool enable); - - - private: - BoardLayout * board; - bool enabled{true}; - - Color * colors[9][3] = { - {new Color(255, 0, 0), new Color(0, 255, 0), new Color(0, 0, 255)}, - {new Color(255, 0, 0), new Color(0, 255, 0), new Color(0, 0, 255)}, - {new Color(255, 0, 0), new Color(0, 255, 0), new Color(0, 0, 255)}, - {new Color(255, 0, 0), new Color(0, 255, 0), new Color(0, 0, 255)}, - {new Color(255, 0, 0), new Color(0, 255, 0), new Color(0, 0, 255)}, - {new Color(255, 0, 0), new Color(0, 255, 0), new Color(0, 0, 255)}, - {new Color(255, 0, 0), new Color(0, 255, 0), new Color(0, 0, 255)}, - {new Color(255, 0, 0), new Color(0, 255, 0), new Color(0, 0, 255)}, - {new Color(255, 0, 0), new Color(0, 255, 0), new Color(0, 0, 255)} - }; -}; \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index dd49238..737824f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -2,6 +2,9 @@ #include #include #include +#include +#include +#include // Static Defines #include "PINOUT.h" @@ -11,29 +14,36 @@ // project specific libraries #include "BluetoothSerial.h" #include "SerialMessage.h" -#include "BoardLayout.h" -#include "Color.h" -#include "ColorManager.h" #include "GlobalPrint.h" +#include "BoardManager.h" +#include "BoardDriver.h" +#include "BoardTypes.h" + +#include "Animator.h" +#include "TestFrames.h" +#include "Animation.h" + // -------------------------------------------------- // ----------------- VARIABLES ---------------------- // -------------------------------------------------- TaskHandle_t updateCommunicaitonTask; TaskHandle_t updateBoardTask; -uint32_t boardStateTimer{0}; -bool boardStateHasChanged{false}; -uint32_t boardStateMaxUpdatePeriod{34}; // this is a little slower than 30fps +std::array*, 2> animations = { + &RisingCubes::rising, + &RotatingCubes::rotating, +}; // BluetoothSerial SerialBT; // BluetoothSerialMessage serialMessageBT(&SerialBT); -SerialMessage<500, 10> serialMessage(&Serial); -BoardLayout board(BOARD_WIDTH, BOARD_LENGTH, BOARD_HEIGHT, stacks); +SerialMessage serialMessage(&Serial); -// Temporary thing until we can get bluetooth color management working on the quest -ColorManager colorManager(&board); +Adafruit_NeoPixel pixelController{BOARD_HEIGHT*2, STACK1_LED_PIN, NEO_GRB + NEO_KHZ800}; +Animator animator{}; +BoardDriver boardDriver{stacks, pixelController}; +BoardManager boardManager{boardDriver, animator}; // -------------------------------------------------- // ----------------- FUNCTIONS ---------------------- // -------------------------------------------------- @@ -64,62 +74,63 @@ void SetupBluetoothModule(){ void printBoardState(){ - // create a buffer to hold the board state - uint16_t boardState[BOARD_WIDTH * BOARD_LENGTH]; - // read in the board state - board.GetBoardState(boardState); - GlobalPrint::Print("!0,"); - - for(int i = 0; i < (BOARD_WIDTH * BOARD_LENGTH); i++){ - GlobalPrint::Print(String(boardState[i])); - if(i == (BOARD_WIDTH * BOARD_LENGTH) - 1){ - break; - } - GlobalPrint::Print(","); - } - + String boardString; + boardManager.Board2StackString(boardString); + GlobalPrint::Print(boardString); GlobalPrint::Println(";"); } void SetStackColor(uint32_t * args, int argsLength){ uint32_t stackNum = args[1]; + uint32_t X_COORD{stackNum}; + while(X_COORD > BOARD_DIMENSIONS.x - 1){ + X_COORD -= BOARD_DIMENSIONS.x; + } + uint32_t Y_COORD{(stackNum - X_COORD) / BOARD_DIMENSIONS.y}; + uint32_t numColors = (argsLength - 2) / 3; - Color colors[numColors]; + V3D colors[numColors]; for(int i = 0; i < numColors; i++){ - int red = args[2 + (i * 3)]; - int green = args[3 + (i * 3)]; - int blue = args[4 + (i * 3)]; - colors[i] = Color(red, green, blue); + uint32_t red = args[2 + (i * 3)]; + uint32_t green = args[3 + (i * 3)]; + uint32_t blue = args[4 + (i * 3)]; + colors[i] = V3D{red, green, blue}; } - - board.SetStackColors(stackNum, colors); + boardManager.SetColumnColors(V3D{X_COORD, Y_COORD, BOARD_TYPES::PLANE_NORMAL::Z}, colors, numColors); } -void parseData(Message<500, 10> &message){ +void parseData(Message &message){ int32_t * args{message.GetArgs()}; - uint32_t argsLength{message.GetArgsLength()}; + uint32_t argsLength{message.GetPopulatedArgs()}; uint32_t command = args[0]; switch(command){ - case Commands::BoardState: + case Commands::BoardState:{ printBoardState(); break; - case Commands::PING: + } + case Commands::PING:{ GlobalPrint::Println("!" + String(Commands::PING) + ";"); break; - case Commands::SetStackColors: + } + case Commands::SetStackColors:{ GlobalPrint::Println("!2;"); - colorManager.Enable(false); + animator.isEnabled = false; + V3D black{}; + boardManager.FillColor(black); SetStackColor(reinterpret_cast(args), argsLength); break; - case Commands::GoToIdle: + } + case Commands::GoToIdle:{ GlobalPrint::Println("!3;"); - colorManager.Enable(true); + animator.isEnabled = true; break; - default: + } + default:{ GlobalPrint::Println("INVALID COMMAND"); break; + } } // now that we have run the command we can clear the data for the next command. @@ -150,20 +161,38 @@ void UpdateCommunication(void * params){ void UpdateBoard(void * params){ Serial.println("Spawning UpdateBoard task"); + auto updateTickRate{std::chrono::milliseconds(8)}; + auto boardStateTimer{std::chrono::milliseconds(0)}; + auto boardStateMaxUpdatePeriod{std::chrono::milliseconds(34)}; // this is a little slower than 30fps + unsigned long accurateTimer{millis()}; + auto changeAnimationTimer{std::chrono::milliseconds(0)}; + uint8_t currentAnimation{0}; + for(;;){ - if(board.BoardStateHasChanged()){ - boardStateHasChanged = true; - } + auto actualTimePassed{std::chrono::milliseconds(millis() - accurateTimer)}; + accurateTimer = millis(); - if(millis() - boardStateTimer > boardStateMaxUpdatePeriod && boardStateHasChanged){ - boardStateTimer = millis(); + if(boardStateTimer >= boardStateMaxUpdatePeriod && boardManager.HasBoardChanged()){ printBoardState(); - boardStateHasChanged = false; + boardManager.ClearBoardChanged(); + boardStateTimer = std::chrono::milliseconds(0); } - colorManager.Update(); + if(changeAnimationTimer >= std::chrono::duration_cast(std::chrono::minutes(1))){ + changeAnimationTimer = std::chrono::milliseconds(0); + currentAnimation++; + if(currentAnimation >= animations.size()){ + currentAnimation = 0; + } + animator.StartAnimation(animations[currentAnimation]); + } - vTaskDelay(5); + animator.RunAnimation(actualTimePassed); + boardManager.Update(); + + boardStateTimer += actualTimePassed; + changeAnimationTimer += actualTimePassed; + vTaskDelay(updateTickRate.count()); } Serial.println("UpdateBoard task has ended unexpectedly!"); } @@ -189,10 +218,10 @@ void setup() { xTaskCreate(UpdateCommunication, "UpdateCommunication", 10000, NULL, 0, &updateCommunicaitonTask); Serial.println("Beginning Board Initializaiton"); + boardManager.Init(); + animator.SetLoop(true); + animator.StartAnimation(animations[0]); xTaskCreate(UpdateBoard, "UpdateBoard", 10000, NULL, 0, &updateBoardTask); - Color colors[] = {Color(255, 0, 0), Color(0, 0, 0), Color(0, 0, 0)}; - board.SetStackColors(2, colors); - boardStateTimer = millis(); Serial.println("Setup Complete"); }