By Connor Colombo and Michael Mong
December 8th, 2018
Abstract
Laser World is an interactive environment which allows the user to better visualize the path of lights using lasers and fog. This experience was designed to evoke a mysterious atmosphere as the fog and lights moving on their own using rotating mirrors draws you in and invites you to interact with it. In the end, it seems that the level of darkness and mysteriousness required for this project ended up scaring the children a little too much and ended making our project less effective as it did not draw them in enough.
Objectives
- Show users how light travels in a straight line and can be reflected off objects
- The mirrors should be able to move on their own and stop and take inputs in response to being touched by a user
- The fog generator should maintain the fog at optimal laser viewing levels either though sensor feedback or manual manipulation
- The moving lasers and fog should be mysterious and enticing to draw in users
- The water level in the tank for the fog generator should be adjustable and refillable.
Implementation
Mechanical:
- The walls of the Laser World enclosure were made out of clear acrylic to ensure that there was maximum to visibility into the enclosure. To further facilitate this we coated the inside of the enclosure with an anti-fogging mixture to prevent condensations from obscuring the view.
- The enclosure was made to be a 20-inch square prism with a height of 6 inches as through experiments we determined that this was large enough to allow 3 mirrors fit comfortably while also allowing the lasers to reflect off the furthest side of the box before becoming too obscured in fog to be seen.
- In order to make the box as airtight as possible to keep the fog enclosed, we used acrylic cement to weld the sides of the boxes together instead of using typical bolts.
- The enclosure was designed in two parts: a top and a bottom half. These two halves were then clamped together with screws using a flanged area in order to prevent water leakage while still allowing the halves to decouple to allow for future maintenance.
- The outside of the enclosure except for the very top panel was enclosed in a reflective mylar sheet to allow the walls to act as mirrors and reflect the lasers.
- The rotating mirrors required a second interior walled off section to prevent water from leaking into the gearboxes and damaging them and the electronics.
- The handles of the rotating mirrors were designed to be detectable using countersunk screws to allow for easy removal of the top half of the enclosure.
- Series-elastic actuators with integrated force-feedback were used to drive each of the mirrors to allow for autonomous behaviour, which could be cleanly interrupted at any time by a user grabbing a mirror’s handle – an event which, itself, could be detected by the actuator, allowing it to collaborate with the user while they are engaging with a particular mirror. Additionally, to prevent the user from feeling like they are going to break the mirror when they grab and move it, these actuators can be programmed to provide a smooth, consistent, low-torque tactile response to the user.
- Rubber bands were used to couple the output of the gearbox to the motor to allow for some elasticity when grabbing the handles to prevent the motor from hurting the user by attempting to keep moving.
Electrical:
- Stepper motors (28BYJ-48) were used to control each actuator since they provide a cheap, precise, and easy to control means of rotary motion.
- Rotary dial encoders were chosen to measure the angular difference between the input and output to determine the load on the motor using the following formula. These were chosen since they are very low cost, lots of them are needed, and this application does not require particularly precise torque estimations so long as they are consistent enough for a stable threshold to be chosen.
- Since providing a consistent force feedback in the series elastic actuators requires a fast reaction time, requiring the control and processing of a multitude of signals, each actuator was given its own independent microcontroller.
- These microcontrollers were networked to a Raspberry Pi for diagnostics and potential control of music over an I2C bus. This bus was routing using USB cables because they are cheap, shielded, and available.
Outcomes
Successes
Due to a failed fog generator we were unable to get good test data when at the Children’s Museum; however, when testing later, we did observe that the enclosure achieved most of our objectives by making the laser path visible and allowing us to interact with it by changing the angle that the beam bounced off at as shown by our video below.
Through early testing, we discovered that the top viewing surface became obscured with condensation and made the lasers harder to see. We were able to correct this issue by applying an anti-fogging coating to the inside of the acrylic box which prevented this from occurring.
Our actuators worked as intended and allowed the enclosure to have a life of its own while also taking commands from the user without it overpowering the children. In the future, we could improve this by adding a gripping pattern to the handle to help the children grasp it but as we were testing we wanted to make sure it would not dig into the children’s hands so we left it smooth.
Failures
From our testing at the Children’s School at Carnegie Mellon, we discovered a factor which we had not accounted for: children’s fear of the dark. We had hoped that the lights shining and moving in a darkened area would draw children in but some children found it scary and did not want to investigate. As our project’s viability was dependent on a slightly darkened area this was an issue that we were unable to resolve; however, this was only a smaller percentage of the children so it was not too large of an issue.
Another failure which we encountered and hope to fix in the future was the enclosure’s draining issues. As the fog condensed within the box it began to create a pool of water which made the enclosure difficult to disassemble cleanly without spilling water everywhere. This could be solved by adding a hole at the bottom of the box which we could open and reseal in order to let the water out before opening the enclosure fully.
Although our ideal vision of the project involved the lasers rebounding near infinitely in order to form intricate patterns, we began to realize that this was impossible as by traveling in the fog and making itself visible it lost power and began to fade over time. Because of this, we had to limit the size of our box so that the beam would be able to reflect off the walls some before fading. Alternatively, we could have focused on the intricate pattern and kept it smaller but we decided it needed to be larger in order to attract attention and allow for more mirrors for multiple children to play at once and interact with each other.
Contribution
Connor Colombo:
- CAD and construction of actuators
- Control software
- PCB and general electrical design and assembly
- Part sourcing
Michael Mong:
- CAD and construction of enclosure
- Laser integration into enclosure
- Mirror design
Supporting Media
Final Demo: Our final project featured 3 mirrors which could be rotated in change the paths of the lasers. In this video, only one actuator is functional.
Initial Test Setup: This test enclosure showed that we could create a visible laser path within our box and that we could interact with it by rotating the mirror.
Foggy Test Box: After running our test set up for a while we realized that the laser’s path was no longer as visible which prompted us to apply an anti-fogging coating to the enclosure window.
Two Laser Test with Actuation: Prior to our final design we modified our initial test to better integrate the actuation and interact with two lasers instead of one to get a better understanding of how all the components worked together.
Internals of the series-elastic actuator during an early functionality test with drive motor at center bottom, input encoder at bottom right, and output encoder visible through center of square slot. Output is the square slot. Observed behaviour shows the actuator rotating independently with unloaded coupling of input and output disks, then when the applied load passes a certain specified torque threshold, the motor pauses until the user lets go.
Citations
N/A.
Supporting Material
SolidWorks (CAD) Files
Electronic Schematics
EWM Module Schematic
EWM Module ROUTING
EWM Module PCB TOP
EWM Module PCB BOTTOM
Repository available at https://circuitmaker.com/Projects/Details/Connor-Colombo/EW-Series-Elastic-Motor-Module/embeded
Source Code
Available as gist at: https://gist.github.com/zCoCo/3e6f14de4269775628b97b6d82cae161
main control script – driver.ino
/* Driving Script for Series Elastic Actuator controlling a mirror with a handle. This involves a basic autonomous behaviour of bouncing back and forth between -180 and 180 degrees but following the user's motion if the actuator detects that its handle being grabbed. Once the user lets go, the actuator will stay at the desired position for 1 second. */ // NB: CCW is +ve #include "Arduino.h" #include "HAL.h" #include "Sensing.h" #include "Motion.h" #include "Schedule.h" //#include "Comm.h" #define sgn(x) ( (x==0) ? 0 : abs(x) / (x) ) Schedule* sch = new Schedule(); // Maximum Angular Difference between Input and Output before Actuator Enters Follower Mode: #define DIFF_THRESH 12 bool is_following = false; // Whether Currently in Follower Mode bool holding_position = false; // Whether Currently Holding the Position Set by the User unsigned long let_go_time = 0; // Time when the user last let go void setup(){ Serial.begin(9600); initHAL(); // initComm(); // -TODO: Implement I2C Communications for Sound Sync. schedule(); moveTo(180); // Kick off the Autonomous Motion } // #setup void schedule(){ /** Perform Basic Life-Line Tasks: **/ sch->ALWAYS->DO(updateSensors); sch->ALWAYS->DO(updateMotion); /** Coordinate Responses: **/ // Enter Follower Mode: sch->WHEN(Sensors.diff > DIFF_THRESH)->do_([](){ is_following = true; move( sgn(Sensors.diff) * (abs(Sensors.diff) - DIFF_THRESH + 1) ); }); // Move to Rest at Position the User Set and Stay There for a Time: sch->WHEN(Sensors.diff < DIFF_THRESH)->do_([](){ move( Sensors.diff ); let_go_time = millis(); holding_position = true; }); // Exit Follower Mode and Resume Autonomous Operation after User has Let Go // for 1 Second: sch->WHEN(let_go_time - millis() > 1000 && holding_position)->do_([](){ is_following = false; holding_position = false; moveTo(180); }); sch->WHEN(idle() && !is_following)->do_([](){ moveTo(-getCommAng()); // Bounce Back and Forth }); /** Give Status Updates: **/ // Plot Load on Actuator: sch->EVERY(200)->do_([](){ Serial.print(Sensors.diff); Serial.print(","); Serial.println(torque()); }); } // #schedule void loop(){ sch->loop(); } // #loop
hardware abstraction layer – hal.h
#ifndef _HAL_H #define _HAL_H /** Hardware Abstraction Layer **/ // Uses ESP8266 12-F (AI-Thinker Variant) // Program as Adafruit Feather HUZZAH // Flash: 4M // No Debug // lwIP: v2 Lower Memory // CPU: 80MHz // Baud: 115200 // Erase: Sketch Only #include <Encoder.h> #define ENC_STEPS_PER_REV 80.0 Encoder EncO(13,12); // Output Encoder Encoder EncI(10,9); // Input Encoder #include <AccelStepper.h> #define STP 1 #define DIR 3 #define EN 8 #define MS1 6 #define MS2 4 #define MS3 5 AccelStepper stepper(1, STP, DIR); /** Basic Motion Parameters: **/ const float GEAR_RATIO = 43.0 / 11.0; // Output to Input Gear Ratio const float MOT_STEPS_PER_REV = 4075.7728 * GEAR_RATIO; // Account for internal gearbox /** Series Elastic Parameters: **/ // Radial Position of the Mounting Point of the Rubber Bands on the Inner Disk [m]: const float RP_INNER = 7.46e-3; // Unloaded Length of Rubber Bands (when mounted in actuator): const float L0 = 15.5e-3; // Amount of Stretching Required for Rubber Bands to Reach their Unloaded // Position (L0) from their Relaxed Length: #define d0 8e-3 // Number of Rubber Bands: #define N_BANDS 4 // Average Effective Stiffness of Each Rubber Band [N/m]: #define K_BAND 15 void initHAL(){ // Initialize Motor Driver Pins and Setup for Full-Step Mode: pinMode(STP, OUTPUT); pinMode(DIR, OUTPUT); pinMode(EN, OUTPUT); pinMode(MS1, OUTPUT); pinMode(MS2, OUTPUT); pinMode(MS3, OUTPUT); digitalWrite(MS1, 0); digitalWrite(MS2, 0); digitalWrite(MS3, 0); digitalWrite(EN, 0); // Setup Motor Control Parameters: stepper.setMaxSpeed(100); stepper.setAcceleration(1000); } // #initHAL #endif //_HAL_H
Sensing primitives – sensing.h
#ifndef _SENSING_H #define _SENSING_H #include "HAL.h" struct SensorsType{ // Useful Data: float input_ang = 0.0; // - Angle of Input Disk float output_ang = 0.0; //- Angle of Output Disk float diff = 0.0; // - Angular Difference between Input and Output Disks [deg] // Helper Variables: float lag_sum = 0.0; // Sum of all measured values for diff unsigned long lag_count = 0; // Number of measured values for diff } Sensors; // Returns the Output Angle from the Encoder in Degrees float outputAng(){ return 360.0 * EncO.read() / ENC_STEPS_PER_REV; } // #outputAng // Returns the Input Angle from the Encoder in Degrees float inputAng(){ return -360.0 * EncI.read() / ENC_STEPS_PER_REV / GEAR_RATIO; } // #outputAng // Computes the Torque Loading the Actuator in N-m. This is an expensive // calculation, only call on an as-needed basis: float torque(){ // Constant Geometric Helper Parameters to Speed Up Calculations: static const float L0_2 = sq(L0); static const float A = 2 * RP_INNER * (L0 + RP_INNER); static const float L0_d = d0 - L0; // Compute Torque (only valid for diff <= 180deg, bands will snap before this): const float th = Sensors.diff * M_PI / 180.0; const float cm = cos(th) - 1; return N_BANDS * RP_INNER * K_BAND * (sqrt(L0_2 - A*cm) + L0_d) * sin( th + atan(RP_INNER * sin(th) / (L0 - RP_INNER*cm)) ); } // #torque // Update Sensor Metadata: void updateSensors(){ Sensors.input_ang = inputAng(); Sensors.output_ang = outputAng(); Sensors.lag_sum += Sensors.output_ang - Sensors.input_ang; Sensors.lag_count += 1; Sensors.diff = Sensors.output_ang - Sensors.input_ang - Sensors.lag_sum / Sensors.lag_count; } // #updateSensors #endif //_SENSING_H
Motion primitives – motion.h
#ifndef _MOTION_H #define _MOTION_H #include "HAL.h" #define MOT_DIR -1 // Used to Invert Motor Direction (-1 for Invert, 1 for Normal) // Immediately Set the New Position Target of the Motor to the Given Angle [deg] void moveTo(float ang){ stepper.stop(); stepper.moveTo(MOT_DIR * ang * MOT_STEPS_PER_REV / 360.0); } // #moveTo // Immediately Set the New Position Target of the Motor to the Given Angle // Relative to the Motor's Current Position [deg] void move(float ang){ stepper.stop(); stepper.move(MOT_DIR * ang * MOT_STEPS_PER_REV / 360.0); } // #move // Returns Whether the Motor is Currently Idle (awaiting a new command) bool idle(){ return stepper.distanceToGo() == 0; } // #idle // Returns the Most Recently Commanded Angle to the Motor float getCommAng(){ return stepper.targetPosition() * 360.0 / MOT_DIR / MOT_STEPS_PER_REV; } // Perform All Necessary Motion Control Commands: void updateMotion(){ stepper.run(); } // #updateMotion #endif // _MOTION_H
Scheduler – schedule.h
/* Schedule.h * Intuitive Scheduling Utility that Allows for Complex Time and Condition Based * Behaviors to be Constructed out of Simple, Legible Event-Based Primitives. * (admittedly, this has a bit of a ways to go in terms of memory efficiency - * badly needs a ring buffer. (especially bad now that state persistence has * been added)) * KNOWN BUGS / PROBLEMS: * - Semi-Required memory leak on the %done% state of Actions. Need to have * some way of determining whether / how long other functions will need access to * this information after the Action has been deleted. NOTE: Until this is fixed, * the ability to create unbounded series of SingleTimedEvents with #in_ is * gone. Keep total number of events known and bounded. * Author: Connor W. Colombo, 9/21/2018 * Version: 0.1.4 * License: MIT */ #ifndef SCHEDULE_H #define SCHEDULE_H #include "Arduino.h" #include <ArduinoSTL.h> #include <vector> /* Example Usage (only call these once, likely in setup): ** avoid calling variables directly from inside these functions unless they are global variables ** void setup(){ // Basic Call: sch->EVERY(500)->DO(blink()); // Will call #blink every 500ms sch->EVERY_WHILE(750, dist < 10)->DO(togglePeek()); // Will peek / unpeek every 750ms while dist is < 10cm sch->IN(2500)->DO(doThisOnce()); // Will call #doThisOnce one time in 2.5s sch->NOW->DO(sortOfUrgent()); // Will call #sortOfUrgent as soon as possible without blocking other events (useful in comm. interrupts for longer behavior) sch->WHILE(dist < 10)->DO(swing_arms()); // Will call #swing_arms as often as possible as long as dist < 10. sch->WHEN(dist > 10)->DO(someOtherThing()); // Will call #someOtherThing every time dist goes from <=10 to >10. sch->WHEN(touched())->DO(uncoverEyes()); // Will uncover eyes when touched goes from false to true (so, when touched) // Other more efficient notation for simple function calls: sch->EVERY(250)->do_(blink); // if you're just calling a void function with no arguments, it's more effective to just use the lowercase #do_ // Note: sch->EVERY(100)->DO(x++); // x or other variables accessed directly must be a global variables (not local scope) // Or Save Events to be Registered to Later: Event* FREQ_100Hz = schedule->EVERY(10); Event* TOO_CLOSE = schedule->WHEN(dist < 10); // ... somewhere else in code: TOO_CLOSE->DO(tone(BUZZER, 1000, 25)); TOO_CLOSE->SIGNUP(tone(BUZZER, 1000, 25)); // Additionally, events which setup other events (using nested actions) return // a double pointer to a bool which indicates when all sub-events have been // executed at least once. // Note: bool** beepboopd must be global. beepboopd = sch->IN(3100)->DO_LONG( *(sch->IN(1000)->DO( plt("***BEEP***BOOP***"); )); ); sch->WHEN(**beepboopd)->DO( plt("## BOP ##"); ); } */ /* NB: Some functionality must be assigned in macros b/c lambdas with captures can't be converted to function pointers. */ // More Legible Shorthand for "do_" syntax: #define DO(x) do_([](){x;}) /* Shorthand for Calling a Function which Takes a Long Time to Complete after it Returns (has its own event calls) and returns a double pointer of a boolean which indicates when it is done. */ #define DO_LONG(x) \ do_(new NestingAction([](Action* action){ \ delete action->done; \ action->done = x; \ })); // More Legible Shorthand for "do_" syntax: #define SIGNUP(x) signup([](){x;}) // More Legible Shorthand for "while_" syntax: #define WHILE(x) while_([](){return (x);}) // More Legible Shorthand for "when" syntax #define WHEN(x) when([](){return (x);}) // Syntax to Normalize All-Caps Syntax used by Conditionals: #define EVERY(x) every(x) // More Legible Shorthand for "everyWhile" syntax: #define EVERY_WHILE(x,y) everyWhile(x, [](){return (y);}) // Syntax to Normalize All-Caps Syntax used by Conditionals: #define IN(x) in_(x) // Shorthand Syntax for Performing a Task as Soon as Possible: #define NOW in_(0) // Shorthand Syntax for Performing a Task as Frequently as Possible: #define ALWAYS EVERY(1) typedef bool** ActionState; #define new_ActionState(b) new bool*(new bool(b)); /* * Container for Action which are called in events and their respective metadata. */ class Action{ // Abstract Container for Use in Arrays of Pointers public: bool* done = new bool(false); virtual ~Action(){ //delete done; // <- Leave the done state variable behind //done = nullptr; } // dtor virtual void call() = 0; /* Tells Whether this Action and its Required Actions are Complete. Returns the dereferrenced state of member %done% */ bool isDone(){ return *(this->done); } // #isDone }; // class Action /* * Most basic form of an Action which takes a void-void function which has no * dependencies and thus is considered to be done executing once the function * returns (ie. doesn't generate any Events). */ class BasicAction : public Action{ public: // Type of Function to be Called which Consumes the Stored Data: typedef void (*function) (); BasicAction(function f) : oncall{f} {}; void call(){ oncall(); *(this->done) = true; } private: // Function to be Executed when this Action is Called: function oncall; }; // class BasicAction /* * Most basic form of an Action which takes a void-Action* function which has * dependencies / triggers other events and is expected to set this Action's * done value to true once all of its sub-functions are complete. */ class NestingAction : public Action{ public: // Type of Function to be Called which Consumes the Stored Data: typedef void (*function) (Action*); NestingAction(function f) : oncall{f} {}; void call(){ oncall(this); } private: // Function to be Executed when this Action is Called: function oncall; }; // class NestingAction /* * An Action (ie function) to be Performed by being Called when an Event * Triggers and Must Receive some Piece(s) of Stored Data of type T to Execute. * The contained function is considered to have no dependencies and thus be * done executing once the function returns (ie. doesn't generate any Events). */ template <typename T> class DataAction : public Action{ public: // Type of Function to be Called which Consumes the Stored Data: typedef void (*function) (T); // Stored Data to be Given to the Function: T data; DataAction(function f, T d) : data{d}, oncall{f} {}; // Calls this Action by Passing the Stored Data to #oncall and Calling It. void call(){ oncall(data); *(this->done) = true; } private: // Function to be Executed when this Action is Called: function oncall; }; // Class: DataAction /* * An Action (ie function) to be Performed by being Called when an Event * Triggers and Must Receive some Piece(s) of Stored Data of type T to Execute. * The contained function has dependencies / triggers other events and is * expected to set this Action's done value to true once all of its s * sub-functions are complete. */ template <typename T> class NestingDataAction : public Action{ public: // Type of Function to be Called which Consumes the Stored Data: typedef void (*function) (T, Action*); // Stored Data to be Given to the Function: T data; NestingDataAction(function f, T d) : data{d}, oncall{f} {}; // Calls this Action by Passing the Stored Data to #oncall and Calling It. void call(){ oncall(this); } private: // Function to be Executed when this Action is Called: function oncall; }; // Class: NestingDataAction /* * Basic Event Class which Triggers only when Called Directly. */ class Event{ public: // Basic void-void function which can signup for the event: typedef void (*RegisteredFunction) (); const bool runs_once; // Indentifies whether this event only happens once. Event() : runs_once{false} {}; virtual ~Event(){ /*for( std::vector<Action*>::iterator it = this->registry.begin(); it != this->registry.end(); ++it ){ delete (*it); } this->registry.clear(); // TODO: Need to come up with way to make Action::done itself stick around*/ } // dtor /* * Request this Event to Execute ASAP. * NOTE: Calls happen IN ADDITION to any event-specific timings or conditions. */ void call(){ this->calledButNotRun = true; } // #call /* * Executes this Event if it Should Execute either Because it's been Called or * Should Self-Trigger. * Returns Whether the Event was Executed. */ bool tryExecute(){ if(this->shouldTrigger() || this->calledButNotRun){ // Call #shouldTrigger first this->execute(); this->calledButNotRun = false; return 1; } return 0; } // #tryExecute /* Test if this Event Should Self-Trigger*/ virtual bool shouldTrigger(){ return 0; // Basic Events only Trigger when Explicitly Called } // #shouldTrigger /* Add the Given Function to the %registry% as a BasicAction to be Executed Every Time the Event is Triggered. Returns a double pointer of the done variable of the Action created. */ bool** signup(RegisteredFunction fcn){ Action* a = new BasicAction(fcn); this->registry.push_back(a); return &(a->done); } // #signup /* Add the Given Action to the %registry% to be Executed Every Time the Event is Triggered. Returns a double pointer of the done variable of the Action. */ bool** signup(Action* a){ this->registry.push_back(a); return &(a->done); } // #signup // Alias for Signing Up for the Event bool** do_(RegisteredFunction fcn){ return signup(fcn); } bool** do_(Action* a){ return signup(a); } // Calls All Functions Registered to this Event void execute(){ if(!this->ran || !this->runs_once){ // Do this ^ check instead of deleting self b/c pointer might be accessed later if in list. for(std::vector<Action*>::size_type i = 0; i != this->registry.size(); i++) { this->registry[i]->call(); } this->ran = true; } } // #execute protected: Event(bool ro) : runs_once{ro} {}; std::vector<Action*> registry; bool ran = false; // Whether this function has been run before (ever). bool calledButNotRun = false; // Whether this Event has been Called Recently but Not Yet Executed }; // Class: Event /* Event which Triggers Anytime #shouldTrigger is called and its condition is True*/ class ConditionalEvent : public Event{ public: typedef bool (*EventCondition) (); EventCondition condition; // Function that Triggers the Event if it's Ready to be Triggered ConditionalEvent(EventCondition t) : condition{t} {}; // Constructor virtual ~ConditionalEvent(){ delete& condition; } // Destructor /* * Triggers this Event if its %condition% Allows It. * Returns Whether the Event was Triggered. */ virtual bool shouldTrigger(){ if(this->condition()){ return 1; } return 0; } // #shouldTrigger }; /* * Event Class which Triggers when its EventCondition is True When #shouldTrigger * is Called and was False the Last time it was Called. */ class TransitionEvent : public ConditionalEvent{ public: TransitionEvent(EventCondition t) : ConditionalEvent(t) {}; // Constructor bool shouldTrigger(){ bool curr_state = this->condition(); if(curr_state && !this->last_state){ this->last_state = curr_state; return 1; } this->last_state = curr_state; return 0; } // #shouldTrigger protected: bool last_state = false; }; /* * Event which Triggers as Close to its Specified Interval after its Previous * Execution as Possible */ class TimedEvent : public Event{ public: unsigned long interval; // Interval between Executions TimedEvent(unsigned long i) : interval{i} { this->timer = i; this->last_time = millis(); }; // Constructor ~TimedEvent(){ } // Destructor /* * Triggers this Event if its %condition% Allows It. * Returns Whether the Event was Triggered. */ bool shouldTrigger(){ unsigned long now = millis(); this->timer -= now - last_time; this->last_time = now; if(this->timer < 0){ this->timer += this->interval; // Keeps execution freq. as close to interval as possible return 1; } return 0; } // #shouldTrigger protected: unsigned long last_time; long timer; TimedEvent(bool runs_once_, unsigned long i) : Event(runs_once_), interval{i} { this->timer = i; this->last_time = millis(); }; }; /* An Event which Triggers Once After a Set Period of Time */ class SingleTimedEvent : public TimedEvent{ public: SingleTimedEvent(unsigned long i) : TimedEvent(true, i) {}; // Constructor }; /* An Event which Triggers at a Certain Frequency so Long as a Given Condition is True */ class ConditionalTimedEvent : public TimedEvent{ public: typedef bool (*EventCondition) (); EventCondition condition; // Function that Triggers the Event if it's Ready to be Triggered ConditionalTimedEvent(unsigned long i, EventCondition t) : TimedEvent(i), condition(t){}; virtual ~ConditionalTimedEvent(){ delete& condition; } // Destructor /* * Triggers this Event if its %condition% Allows It. * Returns Whether the Event was Triggered. */ bool shouldTrigger(){ unsigned long now = millis(); this->timer -= now - last_time; this->last_time = now; bool curr_state = this->condition(); // Everytime Condition Becomes True, Restart Timer if(curr_state && !this->last_state){ timer = this->interval; } this->last_state = curr_state; if(curr_state && this->timer < 0){ this->timer += this->interval; // Keeps execution freq. as close to interval as possible return 1; } return 0; } // #shouldTrigger protected: bool last_state = false; }; class Schedule{ public: std::vector<Event*> events; /* Create an Event to be Triggered as Long as the Given Condition is True */ ConditionalEvent* while_( bool (*condition)() ){ ConditionalEvent* e = new ConditionalEvent(condition); this->events.push_back(e); return e; } // #while_ /* Create an Event to be Triggered Once for Every Time the Given Condition Changes from false to true: */ TransitionEvent* when( bool (*condition)() ){ TransitionEvent* e = new TransitionEvent(condition); this->events.push_back(e); return e; } // #when /* Create an Event that will be Triggered Every %interval% Milliseconds */ TimedEvent* every(const unsigned long interval){ TimedEvent* e = new TimedEvent(interval); this->events.push_back(e); return e; } // #every /* Create an Event that will be Triggered Once in %t% Milliseconds */ SingleTimedEvent* in_(const unsigned long t){ SingleTimedEvent* e = new SingleTimedEvent(t); this->events.push_back(e); return e; } // #in_ /* * Create an Event that will be Triggered Every %interval% Milliseconds While * a Given Condition is True, starting %interval% Milliseconds AFTER the * Condition Becomes True. */ ConditionalTimedEvent* everyWhile(const unsigned long interval, bool (*condition)()){ ConditionalTimedEvent* e = new ConditionalTimedEvent(interval, condition); this->events.push_back(e); return e; } // #everyWhile // Function to be Executed on Every Main Loop (as fast as possible) void loop(){ // Iteration has to account for the fact that elements are intentionally // deleted from the vector in the loop and potentially added at any call // of #Event::tryExecute std::vector<Event*>::size_type size = this->events.size(); std::vector<Event*>::size_type i = 0; while(i < size){ if( this->events[i]->tryExecute() && this->events[i]->runs_once ){ // Delete Event if it's been Executed and Only Runs Once delete this->events[i]; // Delete the Event this->events.erase(this->events.begin() + i); // Remove the addr from the vector size--; // As far as we know, the vector is now smaller } else{ ++i; // Increment iterator normally } } } // #loop }; // Class: Schedule #endif // SCHEDULE_H
Leave a Reply
You must be logged in to post a comment.