diff --git a/Arduino.ino b/Arduino.ino new file mode 100644 index 0000000000000000000000000000000000000000..7a219ecd995d5a603eadd81b23b6c65c2449fffc --- /dev/null +++ b/Arduino.ino @@ -0,0 +1,424 @@ +/*! + * @file Arduino.ino + * + * This Arduino sketch is designed for an Arduino Nano, with which two NeoPixel + * rings from Adafruit are connected. It is assumed that both rings have 16 LEDs + * each. + * + * This code uses Adafruit's Neopixel library, which is released under a GNU Lesser + * General Public License. + * + * Objective: + This sketch should make it possible to use the Arduino Nano to play animation on + the two connected NeoPixel rings. Since the memory of the Nano is very limited, + there is the functionality to load animations via the serial interface. + * + * Let the left NeoPixel ring be abbreviated to: lR + * Let the right NeoPixel ring be abbreviated to: rR + * + * Wiring: + Arduino pin 6 -> lR Data Input + Arduino pin 7 -> rR Data Input + Arduino pin 5V -> lR Power + Arduino pin 5V -> rR Power + Arduino pin GND -> lR Ground + Arduino pin GND -> rR Ground + */ + +#include <Adafruit_NeoPixel.h> + +#define MSG_SIZE 50 // Expected size of a received message in bytes. +#define BUF_SIZE 16 // Maximum number of frames that can be stored in either frame + // buffer. +#define L_PIN 6 // Arduino pin that is connected to the left NeoPixel ring. +#define R_PIN 7 // Arduino pin that is connected to the right NeoPixel ring. +#define PIXELS 16 // Number of pixels on either NeoPixel ring. + +/* +* To make the serial communication a little more robust, all serial requests are +* answered by the Nano. Furthermore, the Nano only receives 50-bit messages and +* only sends 8-bit messages. The serial buffer is also emptied before each written +* message in order to be able to react to the following incoming message. +* +* There are two types of messages, data messages and instruction messages. Data +* messages are used to convey the animation frames, while instruction messages are +* used to convey instructions, which the Nano then executes. +*/ + +// Variables used for communication. + +uint8_t msg[MSG_SIZE]; // Holds a received message. +int msg_size; // Determines how many bits of a message are received. +bool rcvd_msg; // Determines if all 50 bits of a message are received. +bool rcvg_animation; // Determines if an animation is currently been received. +int msgs_to_rcv; // If an animation is received: Determines the number of + // (data) messages that still have to be received. + +/* +* Every animation consists of frames. A frame describes the colors of all 16 +* pixels on one NeoPixel ring. Since two NeoPixel rings are connected to the +* Arduino, each animation consists of at least two frames, one frame for the left +* ring and one frame for the right ring. To play an animation, two frames are +* displayed in parallel. The term frame ensemble or ensemble is used to refer to +* these two frames that play in parallel. +*/ + +// Variables used for animation storage. + +int values_per_frame; // Number of bytes that are needed to store an entire + // frame. + +uint8_t ** l_frame_buffer; // Stores all animation frames for the left + // NeoPixel ring. +uint8_t ** r_frame_buffer; // Stores all animation frames for the right + // NeoPixel ring. +uint8_t num_ensembles; // Number of frame ensembles in the animation. +uint8_t num_iterations; // Number of times the animation is repeated. +unsigned long frame_time; // Delay in milliseconds between each displayed + // frame ensemble. + +// Variables used for animation playback. + +bool animation_loaded; // Determine if an animation is loaded. +bool playback; // Determine if animation should be played. +long start_time; // Time at which each individual frame ensemble + // started to be displayed. +uint8_t curr_ensemble; // Frame ensemble that is currently displayed. +uint8_t curr_iteration; // Animation iteration that is currently played. + +// Variables used for NeoPixel ring. + +Adafruit_NeoPixel l_pixels(PIXELS, L_PIN, NEO_GRB + NEO_KHZ800); +Adafruit_NeoPixel r_pixels(PIXELS, R_PIN, NEO_GRB + NEO_KHZ800); + +void setup() +{ + msg_size = 0; + rcvd_msg = false; + rcvg_animation = false; + msgs_to_rcv = 0; + + // Three color values (red, green, blue) must be saved for each pixel. + values_per_frame = PIXELS * 3; + + animation_loaded = false; + playback = false; + start_time = 0; + curr_ensemble = 0; + curr_iteration = 0; + + init_frame_buffers(); + + l_pixels.begin(); + r_pixels.begin(); + + Serial.begin(57600); +} + +/*! + @brief Allocates memory for the two frame buffers. + + @note The two frame buffers each have the structure of a 2D array in which + 16 frames are stored. Each frame consists of 48 bytes of color + information. A total of 1536 bytes are allocated in this function. +*/ +void init_frame_buffers() +{ + l_frame_buffer = (uint8_t **) malloc(sizeof(uint8_t *) * BUF_SIZE); + r_frame_buffer = (uint8_t **) malloc(sizeof(uint8_t *) * BUF_SIZE); + + for (int i = 0; i < BUF_SIZE; i++) + { + l_frame_buffer[i] = (uint8_t *) malloc(sizeof(uint8_t) * values_per_frame); + r_frame_buffer[i] = (uint8_t *) malloc(sizeof(uint8_t) * values_per_frame); + } +} + +/*! + @brief Sends a 8-bit message via the serial connection. The serial buffer + is emptied beforehand in order to react to following incoming messages. + + @param msg 8-bit message from Arduino. + + @note This function is used to respond to incomming messages. +*/ +void respond(uint8_t msg) +{ + while (Serial.available() > 0) Serial.read(); // Flush serial buffer. + Serial.write(msg); +} + +/*! + @brief This function sends the color information in the two frame buffers to the + NeoPixel rings. The frame ensembles are shown delayed depending on the + animation speed. In addition, the animation is repeated depending on the + desired iterations. +*/ +void play_animation() +{ + if (curr_ensemble < num_ensembles) + { + if (millis() > frame_time + start_time) + { + for (int i = 0; i < PIXELS; i++) + { + int pixel = i * 3; + + uint8_t r; + uint8_t g; + uint8_t b; + + r = l_frame_buffer[curr_ensemble][pixel + 0]; + g = l_frame_buffer[curr_ensemble][pixel + 1]; + b = l_frame_buffer[curr_ensemble][pixel + 2]; + l_pixels.setPixelColor(i, l_pixels.Color(r, g, b)); + + r = r_frame_buffer[curr_ensemble][pixel + 0]; + g = r_frame_buffer[curr_ensemble][pixel + 1]; + b = r_frame_buffer[curr_ensemble][pixel + 2]; + r_pixels.setPixelColor(i, r_pixels.Color(r, g, b)); + } + + l_pixels.show(); + r_pixels.show(); + + start_time = millis(); + curr_ensemble++; + } + } + + if (curr_ensemble >= num_ensembles) + { + if (millis() > frame_time + start_time) + { + if (num_iterations != 0xFF) curr_iteration++; // Animation is looped indefenitly. + if (curr_iteration >= num_iterations) + { + reset_playback(); + respond(0x4F); // Animation successfully played! + } + else curr_ensemble = 0; + } + } +} + +/*! + @brief Executes the appropriate function depending on the instruction read + from a received instruction message. +*/ +void handle_instruction() +{ + switch ((char) msg[1]) + { + case 'A': + load_animation(); + break; + case 'B': + start_playback(); + break; + case 'C': + pause_playback(); + break; + case 'D': + resume_playback(); + break; + case 'E': + reset_display(); + break; + default: + respond(0xF2); // Instruction was not recognized. + break; + } +} + +/*! + @brief Breaks down an instruction message into its individual components. +*/ +void load_animation() +{ + if (msg[2] % 2 != 0) respond(0xF7); // Unequal number of frames for the + // left and right NeoPixel ring. + else if (msg[2] * 0.5 > BUF_SIZE) respond(0xF0); // Animation has to many frame ensembles. + else if (msg[2] * 0.5 == 0x00) respond(0xF8); // Animation has no frame ensemble. + else if (msg[7] == 0x00) respond(0xF6); // Animation has no iteration. + else + { + reset_playback(); + + num_ensembles = msg[2] * 0.5; + frame_time = msg[3]; + frame_time = (frame_time << 8) + msg[4]; + frame_time = (frame_time << 8) + msg[5]; + frame_time = (frame_time << 8) + msg[6]; + num_iterations = msg[7]; + + animation_loaded = false; + rcvg_animation = true; + msgs_to_rcv = msg[2]; + + respond(0x0F); // Waiting for frames to be send. + } +} + +/*! + @brief Starts animation playback. +*/ +void start_playback() +{ + if (animation_loaded) + { + playback = true; + start_time = millis() - frame_time; + curr_ensemble = 0; + + respond(0x1F); // Animation playback has been started. + } + else respond(0xF1); // No animation is loaded. +} + +/*! + @brief Pauses animation playback +*/ +void pause_playback() +{ + if (animation_loaded) + { + playback = false; + + respond(0x5F); // Animation playback has been paused. + } + else respond(0xF1); // No animation is loaded. +} + +/*! + @brief Resumes animation playback. +*/ +void resume_playback() +{ + if (animation_loaded && !playback) + { + playback = true; + start_time = millis() - frame_time; + + respond(0x6F); // Animation playback has been resumed. + } + else if (playback) respond(0xF5); // Animation is playing. + else respond(0xF1); // No animation is loaded. +} + +/*! + @brief Resets all animation playback parameters. +*/ +void reset_playback() +{ + playback = false; + start_time = 0; + curr_ensemble = 0; + curr_iteration = 0; +} + +/*! + @brief Clears both NeoPixel rings. +*/ +void reset_display() +{ + l_pixels.clear(); + r_pixels.clear(); + l_pixels.show(); + r_pixels.show(); + + respond(0x7F); // Displays have been cleared. +} + +/*! + @brief Loads receiving data messages into one of the frame buffers. + + @note Each animation consists of an even number of frames. All even frames in an + animation are stored in the left frame buffer and all odd frames in the + right one. +*/ +void handle_data() +{ + if (rcvg_animation && msgs_to_rcv > 0) + { + int frame = 0.5 * (2 * num_ensembles - msgs_to_rcv); // Data type int always rounds + // off a floating point number. + for (int i = 0; i < values_per_frame; i++) + { + if (msgs_to_rcv % 2 == 0) l_frame_buffer[frame][i] = msg[i + 1]; + else r_frame_buffer[frame][i] = msg[i + 1]; + } + + msgs_to_rcv--; + + if (msgs_to_rcv == 0) + { + rcvg_animation = false; + animation_loaded = true; + respond(0x3F); // Last frame successfully received. + } + else respond(0x2F); // Frame successfully received. + } + else respond(0xF3); // No data message expected. +} + +/*! + @brief Responds to wrong formated message. +*/ +void handle_wrong_format() +{ + respond(0xF4); // Message has wrong format. +} + +/*! + @brief Loades a received message form the serial buffer into memmory. + + @note Data messages as well as instruction messages always have a length + of 50 bits. +*/ +void read_serial() +{ + while (Serial.available() > 0) + { + if (msg_size < MSG_SIZE) + { + msg[msg_size] = Serial.read(); + msg_size++; + } + if (msg_size >= MSG_SIZE) + { + rcvd_msg = true; + break; + } + } +} + +/*! + @brief First, checks if a message is received. If thats the case determines if the + message is a data or an instruction message and preecedes accordingly. If the + received message is neither one of the two, informs the communication partner. + Secondly, checks if a message is currently beeing send to the Nano. If thats + the case, writes the message to memory. + Thirdly, if an animation is loaded and if the animaiton should be played, + plays the loaded animation. + + @note Data messages beginn and end with ascii encoded curling brackets. + Instruction messages beginn and end with ascii encoded square brackets. +*/ +void loop() +{ + if (rcvd_msg) + { + char startMarker = msg[0]; + char endMarker = msg[MSG_SIZE - 1]; + + if (startMarker == '[' && endMarker == ']') handle_instruction(); + else if (startMarker == '{' && endMarker == '}') handle_data(); + else handle_wrong_format(); + + rcvd_msg = false; + msg_size = 0; + } + else if (Serial.available() > 0) read_serial(); + else if (playback && animation_loaded) play_animation(); +}