├── .gitattributes ├── .gitignore ├── EtchABot.cpp ├── EtchABot.h ├── LICENSE ├── README.md ├── design ├── PocketEtchABot.svg ├── PocketEtchABotNoLogo.dxf ├── PocketEtchABotNoLogo.svg └── TravelEtchABot.svg ├── examples ├── EtchABotAnalogClock │ └── EtchABotAnalogClock.ino ├── EtchABotCalibrateBacklash │ └── EtchABotCalibrateBacklash.ino ├── EtchABotDriver │ └── EtchABotDriver.ino ├── EtchABotJoystickControl │ └── EtchABotJoystickControl.ino └── EtchABotPatterns │ └── EtchABotPatterns.ino ├── keywords.txt └── nodefiles ├── client ├── client.html ├── js │ └── app.js ├── pencil.cur └── pencil.png ├── package.json └── server ├── favicon.ico └── server.js /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #Node modules 2 | nodefiles/node_modules 3 | #Word documents in design 4 | design/*.docx -------------------------------------------------------------------------------- /EtchABot.cpp: -------------------------------------------------------------------------------- 1 | /* EtchABot.cpp - library for EtchABot motor controls. 2 | * Created by Debra Ansell, October 22, 2015 3 | * www.geekmomprojects.com 4 | */ 5 | 6 | #include "Arduino.h" 7 | #include "EtchABot.h" 8 | 9 | // Constructor definition for arbitrary wiring of motors to Arduino pins. The step order 10 | // is different for the horizontal motor because the positive rotation direction is 11 | // clockwise for the horizontal motor, but counter-clockwise for the vertical and erase motors. 12 | EtchABot::EtchABot(int type, int h1=5, int h2=4, int h3=3, int h4=2, //Motor pin connections 13 | int v1=9, int v2=8, int v3=7, int v4=6, int e1=13, 14 | int e2=12, int e3=11, int e4=10 ): _stepperHorz(STEPS_PER_ROT, h4, h2, h3, h1), 15 | _stepperVert(STEPS_PER_ROT, v3, v1, v4, v2), _stepperErase(STEPS_PER_ROT, e3, e1, e4, e2) 16 | { 17 | if (type == TRAVEL_SIZE) { 18 | _xMax = MAX_X_TRAVEL; 19 | _yMax = MAX_Y_TRAVEL; 20 | } else { 21 | _xMax = MAX_X_POCKET; 22 | _yMax = MAX_Y_POCKET; 23 | } 24 | 25 | _etchType = type; 26 | _motorPins[0] = h1; 27 | _motorPins[1] = h2; 28 | _motorPins[2] = h3; 29 | _motorPins[3] = h4; 30 | _motorPins[4] = v1; 31 | _motorPins[5] = v2; 32 | _motorPins[6] = v3; 33 | _motorPins[7] = v4; 34 | _motorPins[8] = e1; 35 | _motorPins[9] = e2; 36 | _motorPins[10] = e3; 37 | _motorPins[11] = e4; 38 | 39 | pinMode(h1, OUTPUT); 40 | pinMode(h2, OUTPUT); 41 | pinMode(h3, OUTPUT); 42 | pinMode(h4, OUTPUT); 43 | pinMode(v1, OUTPUT); 44 | pinMode(v2, OUTPUT); 45 | pinMode(v3, OUTPUT); 46 | pinMode(v4, OUTPUT); 47 | pinMode(e1, OUTPUT); 48 | pinMode(e2, OUTPUT); 49 | pinMode(e3, OUTPUT); 50 | pinMode(e4, OUTPUT); 51 | } 52 | 53 | // Constructor definition using default wiring values. The step order is different for the 54 | // horizontal motor because the positive rotation direction is clockwise for the horizontal motor, 55 | // but counter-clockwise for the vertical and erase motors. 56 | EtchABot::EtchABot(int type) : _stepperHorz(STEPS_PER_ROT, 2, 4, 3, 5), 57 | _stepperVert(STEPS_PER_ROT, 7, 9, 6, 8), _stepperErase(STEPS_PER_ROT, 11, 13, 10, 12) 58 | { 59 | if (type == TRAVEL_SIZE) { 60 | _xMax = MAX_X_TRAVEL; 61 | _yMax = MAX_Y_TRAVEL; 62 | } else { 63 | _xMax = MAX_X_POCKET; 64 | _yMax = MAX_Y_POCKET; 65 | } 66 | _etchType = type; 67 | _motorPins[0] = 3; 68 | _motorPins[1] = 5; 69 | _motorPins[2] = 2; 70 | _motorPins[3] = 4; 71 | _motorPins[4] = 6; 72 | _motorPins[5] = 8; 73 | _motorPins[6] = 7; 74 | _motorPins[7] = 9; 75 | _motorPins[8] = 11; 76 | _motorPins[9] = 13; 77 | _motorPins[10] = 10; 78 | _motorPins[11] = 12; 79 | 80 | pinMode(2, OUTPUT); 81 | pinMode(3, OUTPUT); 82 | pinMode(4, OUTPUT); 83 | pinMode(5, OUTPUT); 84 | pinMode(6, OUTPUT); 85 | pinMode(7, OUTPUT); 86 | pinMode(8, OUTPUT); 87 | pinMode(9, OUTPUT); 88 | pinMode(10, OUTPUT); 89 | pinMode(11, OUTPUT); 90 | pinMode(12, OUTPUT); 91 | pinMode(13, OUTPUT); 92 | } 93 | 94 | // Rotates the Etch-a-Sketch forward then backwards to erase it 95 | void EtchABot::doErase() { 96 | // We won't be using these motors while erasing - turn them off 97 | turnOffHorzMotor(); 98 | turnOffVertMotor(); 99 | 100 | int erase_delay = STEP_DELAY; 101 | int rot_steps = (int) (5*STEPS_PER_ROT/12); 102 | //Serial.println("forwards"); 103 | for (int i = 0; i < rot_steps; i++) { 104 | _stepperErase.step(1); 105 | delay(erase_delay); 106 | } 107 | //Serial.println("backwards"); 108 | for (int i = 0; i < rot_steps; i++) { 109 | _stepperErase.step(-1); 110 | delay(erase_delay); 111 | } 112 | turnOffEraseMotor(); // turn the motor off when not in use 113 | } 114 | 115 | // True if we've changed directions horizontally 116 | boolean EtchABot::horzDirChange(uint8_t dir) { 117 | if (_prevHorzDir && dir != _prevHorzDir) { 118 | _prevHorzDir = dir; 119 | return true; 120 | } else if (!_prevHorzDir) { 121 | _prevHorzDir = dir; 122 | } 123 | return false; 124 | } 125 | 126 | // True if we've changed directions vertically 127 | boolean EtchABot::vertDirChange(uint8_t dir) { 128 | if (_prevVertDir && dir != _prevVertDir) { 129 | _prevVertDir = dir; 130 | return true; 131 | } else if (!_prevVertDir) { 132 | _prevVertDir = dir; 133 | } 134 | return false; 135 | } 136 | 137 | // Draws a line from the current position to the end position. If we are at the end position, returns 0. 138 | // uses Bresenham's line algorithm to compute the next step. Implementation of Bresenham taken from: 139 | // http://rosettacode.org/wiki/Bitmap/Bresenham's_line_algorithm#C.2B.2B 140 | void EtchABot::drawLine(int targetX, int targetY, boolean motorShutOff) { 141 | 142 | /* 143 | Serial.print("Target(x,y) = "); 144 | Serial.print(targetX); 145 | Serial.print(", "); 146 | Serial.println(targetY); 147 | */ 148 | 149 | // Boundary check 150 | if (targetX < _xMin) targetX = _xMin; 151 | if (targetX > _xMax) targetX = _xMax; 152 | if (targetY < _yMin) targetY = _yMin; 153 | if (targetY > _yMax) targetY = _yMax; 154 | 155 | int dx = abs(targetX - _currentX); 156 | int sx = _currentX < targetX ? 1 : -1; 157 | int dy = abs(targetY - _currentY); 158 | int sy = _currentY < targetY ? 1 : -1; 159 | int err = (dx > dy ? dx : -dy)/2; 160 | int e2; 161 | 162 | // Deal with backlash (if any) before stepping. Step the motor by the backlash 163 | // amount without incrementing position. 164 | if (dx && horzDirChange(sx)) { 165 | //Serial.println("horizontal direction change"); 166 | for (int i = 0; i < _hBacklash; i++) { 167 | _stepperHorz.step(sx); 168 | delay(STEP_DELAY); 169 | } 170 | } 171 | if (dy && vertDirChange(sy)) { 172 | //Serial.println("vertical direction change"); 173 | for (int i = 0; i < _vBacklash; i++) { 174 | _stepperVert.step(sy); 175 | delay(STEP_DELAY); 176 | } 177 | } 178 | 179 | // Bresenham's algorithm implemented below 180 | boolean thereYet = false; 181 | while(!thereYet) { 182 | /* 183 | Serial.print("Current x,y = "); 184 | Serial.print(currentX); 185 | Serial.print(", "); 186 | Serial.println(currentY); 187 | */ 188 | e2 = err; 189 | if (e2 > -dx) { 190 | err -= dy; 191 | _stepperHorz.step(sx); 192 | delay(STEP_DELAY); 193 | _currentX += sx; 194 | } 195 | if (e2 < dy) { 196 | err += dx; 197 | _stepperVert.step(sy); 198 | delay(STEP_DELAY); 199 | _currentY += sy; 200 | } 201 | if(_currentX == targetX && _currentY == targetY) thereYet = true; 202 | } 203 | if (motorShutOff) { 204 | turnOffHorzMotor(); 205 | turnOffVertMotor(); 206 | } 207 | } 208 | 209 | // Uses drawLine function to draw a segmented arc originating at the current 210 | // position, with center at (xCenter, yCenter) that consists of nSegs line 211 | // segments and subtends angle degrees of arc. For a complete circle, use 212 | // degrees = 360. You can make the arc run clockwise with degrees > 0 213 | // and counterclockwise with degrees < 0. 214 | /* Not working yet 215 | void EtchABot::drawArc(int xCenter, int yCenter, int nSegs, float degrees) { 216 | const float degToRad = 0.0174533; 217 | 218 | int startX = getX(); 219 | int startY = getY(); 220 | 221 | float radius = sqrt((startX - xCenter, 2) + pow(startY - yCenter, 2)); 222 | float radiansPerSeg = degToRad*degrees/nSegs; 223 | 224 | // Store the existing backlash values, so we can reset them at the end 225 | int hBacklash = getHBacklash(); 226 | int vBacklash = getVBacklash(); 227 | // Backlash values of 0 work better with arcs and circles 228 | setHBacklash(0); 229 | setVBacklash(0); 230 | float xNext, yNext; 231 | for (int i = 1; i < nSegs; i++) { 232 | xNext = startX + radius*cos(i*radiansPerSeg); 233 | yNext = startY - radius*sin(i*radiansPerSeg); // subtract b/c positive y is down 234 | drawLine(xNext, yNext); 235 | } 236 | // Restore previous backlash settings 237 | setHBacklash(hBacklash); 238 | setVBacklash(vBacklash); 239 | } 240 | */ 241 | void EtchABot::turnOffEraseMotor() { 242 | for (int i = 8; i < 12; i++) { 243 | digitalWrite(_motorPins[i], LOW); 244 | } 245 | } 246 | 247 | void EtchABot::turnOffHorzMotor() { 248 | for (int i = 0; i < 4; i++) { 249 | digitalWrite(_motorPins[i], LOW); 250 | } 251 | } 252 | 253 | void EtchABot::turnOffVertMotor() { 254 | for (int i = 4; i < 8; i++) { 255 | digitalWrite(_motorPins[i], LOW); 256 | } 257 | } 258 | 259 | void EtchABot::turnOffMotors() { 260 | turnOffHorzMotor(); 261 | turnOffVertMotor(); 262 | turnOffEraseMotor(); 263 | } 264 | -------------------------------------------------------------------------------- /EtchABot.h: -------------------------------------------------------------------------------- 1 | /* 2 | * EtchABot.h - Library for controlling the EtchABot. 3 | * Created by Debra Ansell (geekmomprojects), October 22, 2015. 4 | * www.geekmomprojects.com 5 | */ 6 | #ifndef EtchABot_h 7 | #define EtchABot_h 8 | 9 | #include "Arduino.h" 10 | #include // You MUST include the stepper library in your sketch 11 | // as well because of Arduino IDE quirkiness in library includes 12 | //#include 13 | 14 | #define POCKET_SIZE 1 15 | #define TRAVEL_SIZE 2 16 | 17 | // Actual backlash varies with each Etch-a-Sketch. Need to run 18 | // calibration to find the optimal values 19 | #define DEFAULT_HORZ_BACKLASH 120 20 | #define DEFAULT_VERT_BACKLASH 120 21 | 22 | #define MOTOR_SPEED 40 // units are rev/min 23 | #define STEPS_PER_ROT 2048 // when running steppers in 4-step mode 24 | // which is what uses 25 | 26 | // Size of drawing area in units of stepper motor steps 27 | #define MAX_X_TRAVEL 6500 28 | #define MAX_Y_TRAVEL 4600 29 | #define MAX_X_POCKET 6000 30 | #define MAX_Y_POCKET 4000 31 | 32 | // Give the pen time to get to the right position 33 | #define STEP_DELAY 3 34 | 35 | class EtchABot 36 | { 37 | private: 38 | int _hBacklash = DEFAULT_HORZ_BACKLASH; 39 | int _vBacklash = DEFAULT_VERT_BACKLASH; 40 | int _etchType; 41 | int _xMin=0, _yMin = 0, _xMax, _yMax; 42 | int _currentX = 0, _currentY = 0; //Assume we're starting at the upper left corner always 43 | uint8_t _prevHorzDir = 0, _prevVertDir = 0; 44 | uint8_t _motorPins[12]; //List of motor pins used (in order horz, vert, erase); 45 | Stepper _stepperHorz, _stepperVert, _stepperErase; 46 | 47 | boolean horzDirChange(uint8_t dir); 48 | boolean vertDirChange(uint8_t dir); 49 | 50 | public: 51 | EtchABot(int type, int h1, int h2, int h3, int h4, int v1, int v2, int v3, int v4, int e1, int e2, int e3, int e4); 52 | EtchABot(int type); 53 | 54 | void doErase(); 55 | void drawLine(int targetX, int targetY, boolean motorShutOff=false); 56 | //void drawArc(int xCenter, int yCenter, int nSegs, float degrees); 57 | void turnOffMotors(); 58 | void turnOffEraseMotor(); 59 | void turnOffHorzMotor(); 60 | void turnOffVertMotor(); 61 | 62 | // Get/set functions 63 | int getType() {return _etchType;} 64 | int getX() {return _currentX;} 65 | int getY() {return _currentY;} 66 | int getMaxX() {return _xMax;} 67 | int getMaxY() {return _yMax;} 68 | int getHBacklash() {return _hBacklash;} 69 | int getVBacklash() {return _vBacklash;} 70 | void setHBacklash(int b) {_hBacklash = b;} 71 | void setVBacklash(int b) {_vBacklash = b;} 72 | 73 | }; 74 | 75 | #endif -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | {description} 294 | Copyright (C) {year} {fullname} 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | 341 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EtchABot 2 | Description of and source software for the EtchABot drawing bot. Instructions for assembly are at http://www.geekmomprojects.com/etchabot-a-cnc-etch-a-sketch/ 3 | 4 | The EtchABot Arduino library files are: 5 | -EtchABot.h 6 | -EtchABot.cpp 7 | -keywords.txt 8 | They should be installed with the other Arduino libraries 9 | 10 | Example Arduino sketches in "examples" folder. They are: 11 | (1) EtchABotAnalogClock - runs EtchABot as analog clock. Requires calibrated DS3231 clock module to work. 12 | (2) EtchABotCalibrateBacklash - runs calibration code to determine horizontal/vertical backlash parameters. 13 | (3) EtchABotDriver - runs EtchABot in a mode to take commands from the Serial port. 14 | (4) EtchABotJoystickControl - runs EtchABot in mode that draws the input from attached thumb joystick. 15 | (5) EtchABotPatterns - draws parametric patterns (Lissajous or Spirograph), erases and restarts with different parameters every few minutes. 16 | 17 | The SVG files for lasercut wood parts are in the "design" folder. Parts are intended to be cut from 1/8" MDF or similar wood. 18 | 19 | The nodefiles folder contains Node.js and javascript/HTML files to run an app to send images to the EtchABot through the serial port while it is running EtchABot driver. This software is still a work in progress, however, to start the program, go to the "nodefiles" directory in a shell, and type "npm start -- portname" where portname is the name of the serial port the Arduino is attached to. Then open a browswer window and type "localhost:8000" as the URL to run the program. 20 | -------------------------------------------------------------------------------- /design/PocketEtchABot.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 30 | 56 | Output from Flights of Ideas SVG Sketchup Plugin 58 | 63 | 68 | 73 | 78 | 83 | 88 | 93 | 98 | 103 | 108 | 113 | 118 | 123 | 128 | 133 | 138 | 143 | 151 | 156 | 160 | 164 | 168 | 172 | 176 | 180 | 184 | 188 | 192 | 196 | 200 | 204 | 208 | 212 | 216 | 220 | 224 | 228 | 232 | 236 | 240 | 244 | 248 | 252 | 253 | EtchABot 265 | horz 277 | vert 289 | erase 301 | 302 | -------------------------------------------------------------------------------- /design/PocketEtchABotNoLogo.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 31 | 57 | Output from Flights of Ideas SVG Sketchup Plugin 59 | 64 | 69 | 74 | 79 | 84 | 89 | 94 | 99 | 104 | 109 | 114 | 119 | 124 | 129 | 134 | 139 | 144 | 152 | 157 | EtchABot 169 | horz 181 | vert 193 | erase 205 | 206 | -------------------------------------------------------------------------------- /examples/EtchABotAnalogClock/EtchABotAnalogClock.ino: -------------------------------------------------------------------------------- 1 | /* 2 | * EtchABotAnalogClock 3 | * by Debra Ansell (GeekMomProjets) 2015 4 | * www.geekmomprojects.com 5 | * 6 | * Arduino firmware for Etch-a-Sketch analog clock. 7 | * 8 | * RTC Clock Library usage copied from Plotclock by Johannes Heberlein 9 | * Obtain RTC and time libraries at http://playground.arduino.cc/Code/time 10 | * 11 | */ 12 | 13 | #include 14 | #include // Have to include stepper library to be able to use EtchABot library 15 | #include "EtchABot.h" 16 | 17 | #define REALTIMECLOCK //Uncomment this to enable the real time clock 18 | 19 | #ifdef REALTIMECLOCK 20 | // for instructions on how to hook up a real time clock, 21 | // see here -> http://www.pjrc.com/teensy/td_libs_DS1307RTC.html 22 | // DS1307RTC works with the DS1307, DS1337 and DS3231 real time clock chips. 23 | // Please run the SetTime example to initialize the time on new RTC chips and begin running. 24 | 25 | #include 26 | #include // see http://playground.arduino.cc/Code/time 27 | // I'm using analog output pins 2 and 3 to supply power to the 28 | // RTC Clock pins, whic connect directly to Arduino as follows: 29 | // VCC -> A3 (digital 17), Set HIGH 30 | // GND -> A2 (digital 16), Set LOW 31 | // SDA -> A4 (SDA) 32 | // SCL -> A5 (SCL) 33 | 34 | #define VCC_PIN 17 35 | #define GND_PIN 16 36 | #endif 37 | 38 | 39 | #define MINUTE_HAND_LENGTH 1800 40 | #define HOUR_HAND_LENGTH 1100 41 | #define UPDATE_INTERVAL 1 //# of minutes between display updates 42 | 43 | // Tracks time of last rendering 44 | int lastMinute = -1; 45 | 46 | // Initializes time - if no RTC or computer attached, just make up a time 47 | void initializeTime() { 48 | Serial.begin(57600); 49 | 50 | #ifdef REALTIMECLOCK 51 | pinMode(GND_PIN, OUTPUT); 52 | pinMode(VCC_PIN, OUTPUT); 53 | digitalWrite(GND_PIN, LOW); // We're using A4, A5 (digital 16, 17) as Power, GND for RTC 54 | digitalWrite(VCC_PIN, HIGH); 55 | 56 | // Set current time only the first to values, hh,mm are needed 57 | tmElements_t tm; 58 | if (RTC.read(tm)) { 59 | setTime(tm.Hour,tm.Minute,tm.Second,tm.Day,tm.Month,tm.Year); 60 | Serial.println("DS1307 time is set OK."); 61 | } else { 62 | if (RTC.chipPresent()){ 63 | Serial.println("DS1307 is stopped. Please run the SetTime example to initialize the time and begin running."); 64 | } else { 65 | Serial.println("DS1307 read error! Please check the circuitry."); 66 | } 67 | } 68 | #else 69 | // Start the clock - just picking a random time for now (actucally, time is not totally random, 70 | // it is an homage to PlotClock) 71 | setTime(19,38,0,0,0,0); 72 | #endif 73 | } 74 | 75 | 76 | // Be sure to initialize this the correct size (TRAVEL_SIZE or POCKET_SIZE) 77 | // for your Etch-a-Sketch. Otherwise you run the risk of ruining your Etch-a-Sketch 78 | EtchABot etch(POCKET_SIZE); 79 | 80 | void setup() { 81 | 82 | Serial.begin(57600); 83 | Serial.println("#start up"); 84 | 85 | // Seem to need delay here before starting to draw with motors 86 | delay(2000); 87 | 88 | // Set up time variables and initialize clock 89 | initializeTime(); 90 | 91 | // Move to the middle of the screen and erase 92 | etch.drawLine(etch.getMaxX()/2, etch.getMaxY()/2); 93 | etch.doErase(); 94 | } 95 | 96 | // Draws the hands of the analog clock, given current hour/min 97 | void drawTime(int h, int m) { 98 | 99 | int centerX = etch.getMaxX()/2; 100 | int centerY = etch.getMaxY()/2; 101 | 102 | // Make sure we are at the center (shouldn't be necessary) 103 | etch.drawLine(centerX, centerY); 104 | 105 | // Compute the angle of the hour and minute hand (in radians) relative to the center 106 | // in a frame where 0 degrees is at 12:00 and angle increases in the clockwise direction 107 | const float twoPi = 6.283; 108 | const float halfPi = 1.5707; 109 | float angleMin = twoPi*((float) m)/60.0; // 110 | float angleHour = twoPi*((float) (60.0*h + m))/(12.0*60.0); 111 | 112 | // convert angle to unit circle reference frame 113 | angleMin = halfPi - angleMin; 114 | angleHour = halfPi - angleHour; 115 | 116 | /* 117 | Serial.print("AngleHour, AngleMin = "); 118 | Serial.print(((int) (angleHour*360/twoPi)) % 360); 119 | Serial.print(", "); 120 | Serial.println(((int) (angleMin*360/twoPi)) % 360); 121 | */ 122 | 123 | // find x,y position relative to the center of the canvas 124 | // subtract y position because our coordinate system has y axis down, not up 125 | int xMinute = centerX + (int) (MINUTE_HAND_LENGTH*cos(angleMin)); 126 | int yMinute = centerY - (int) (MINUTE_HAND_LENGTH*sin(angleMin)); 127 | int xHour = centerX + (int) (HOUR_HAND_LENGTH*cos(angleHour)); 128 | int yHour = centerY - (int) (HOUR_HAND_LENGTH*sin(angleHour)); 129 | 130 | /* 131 | Serial.print("xHour, yHour = "); 132 | Serial.print(xHour); 133 | Serial.print(", "); 134 | Serial.println(yHour); 135 | Serial.print("xMin, yMin = "); 136 | Serial.print(xMinute); 137 | Serial.print(", "); 138 | Serial.println(yMinute); 139 | */ 140 | 141 | // For now, just draw the hands as a straight line. 142 | etch.drawLine(xHour, yHour); 143 | etch.drawLine(centerX, centerY); 144 | etch.drawLine(xMinute, yMinute); 145 | etch.drawLine(centerX, centerY); 146 | etch.turnOffMotors(); 147 | } 148 | 149 | 150 | void loop() { 151 | // When minutes have changed by UPDATE_INTERVAL, draw new clock face 152 | int h = hour(); 153 | int m = minute(); 154 | 155 | if (m >= (lastMinute + UPDATE_INTERVAL) % 60) { 156 | /* 157 | Serial.print("lastMinute = "); 158 | Serial.print(lastMinute); 159 | Serial.print(" Current time = "); 160 | Serial.print(h); 161 | Serial.print(":"); 162 | Serial.println(m); 163 | */ 164 | lastMinute = m; 165 | etch.doErase(); 166 | drawTime(h, m); 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /examples/EtchABotCalibrateBacklash/EtchABotCalibrateBacklash.ino: -------------------------------------------------------------------------------- 1 | /** EtchABotCalibrateBacklash 2 | * Created by Debra Ansell (geekmomprojects), November 1, 2015. 3 | * www.geekmomprojects.com 4 | * This routine tests several different values for calibrating the vertical and horizontal 5 | * backlash on the Etch-a-Sketch. Note that the optimal values will differ for drawing curved 6 | * lines and straight lines, as well as for angled lines and vertical/horizontal lines. Pick 7 | * the backlash values that look best for what kind of drawing you plan to do next. 8 | */ 9 | 10 | #include // Must include Stepper.h anytime we use "EtchABot.h" 11 | #include "EtchABot.h" 12 | 13 | #define MAX_BACKLASH 300 14 | #define MIN_BACKLASH 0 15 | #define N_BACKLASH_VALUES 8 16 | 17 | // Create EtchABot object with default pin assignments. Please specify TRAVEL_SIZE 18 | // or POCKET_SIZE as is appropriate in the constructor below 19 | EtchABot etch(POCKET_SIZE); 20 | 21 | void doBacklashTest(char direction, int startingBacklash, int increment) { 22 | Serial.println("Starting backlash test"); 23 | etch.doErase(); 24 | 25 | int startX = 500; 26 | int startY = 500; 27 | int lastX = etch.getMaxX() - 500; 28 | int lastY = etch.getMaxY() - 500; 29 | int ticLength = 500; 30 | if (direction == 'v') { 31 | etch.drawLine(startX, startY); 32 | etch.drawLine(lastX, startY); //Long horizontal line 33 | etch.drawLine(lastX, startY + ticLength); 34 | int incrementWidth = (int) (1.0*(lastX - startX)/N_BACKLASH_VALUES); 35 | for (int i = 0; i < N_BACKLASH_VALUES; i++) { 36 | int backlash = startingBacklash + i*increment; 37 | Serial.print("Setting backlash to: "); 38 | Serial.println(backlash); 39 | etch.setVBacklash(backlash); 40 | etch.drawLine(lastX, startY); 41 | lastX -= incrementWidth; 42 | etch.drawLine(lastX, startY); 43 | etch.drawLine(lastX, startY + ticLength); 44 | } 45 | } else if (direction == 'h') { 46 | etch.drawLine(startX, startY); 47 | etch.drawLine(startX, lastY); // Long vertical line 48 | etch.drawLine(startX + ticLength, lastY); 49 | int incrementHeight = (int) (1.0*(lastY - startY)/N_BACKLASH_VALUES); 50 | for (int i = 0; i < N_BACKLASH_VALUES; i++) { 51 | int backlash = startingBacklash + i*increment; 52 | Serial.print("Setting backlash to: "); 53 | Serial.println(backlash); 54 | etch.setHBacklash(backlash); 55 | etch.drawLine(startX, lastY); 56 | lastY -= incrementHeight; 57 | etch.drawLine(startX, lastY); 58 | etch.drawLine(startX + ticLength, lastY); 59 | } 60 | } 61 | etch.drawLine(0,0); // Return stylus to origin 62 | 63 | } 64 | 65 | void setup() { 66 | // put your setup code here, to run once: 67 | Serial.begin(57600); 68 | 69 | // Get user input for testing values 70 | Serial.println("Ready for backlash calibration test. BE SURE THE STYLUS"); 71 | Serial.println("IS AT THE TOP LEFT OF THE SCREEN BEFORE CONTINUING."); 72 | Serial.println(""); 73 | 74 | Serial.setTimeout(100000); // Length of time (in millis) to wait for answers 75 | 76 | // Find out if we're testing horizontal or vertical calibration 77 | char calType = ' '; 78 | while (calType != 'h' && calType != 'v') { 79 | 80 | Serial.println("Test horizontal (h) or vertical (v) calibration?"); 81 | Serial.println("Enter 'h' or 'v'"); 82 | while (Serial.available() < 1); // wait for input 83 | calType = Serial.read(); 84 | if (calType == 'h') Serial.println("Testing horizontal calibration"); 85 | else if (calType == 'v') Serial.println("Testing vertical calibration"); 86 | else { 87 | Serial.print("Received character: "); 88 | Serial.print(calType); 89 | Serial.println(" for calibration type."); 90 | } 91 | } 92 | // Clear any characters from buffer 93 | while(Serial.available()) Serial.read(); 94 | 95 | 96 | // Get starting backlash value 97 | int startingBacklash = -1; 98 | while (startingBacklash < MIN_BACKLASH || startingBacklash > MAX_BACKLASH) { 99 | Serial.print("Please enter a backlash value between "); 100 | Serial.print(MIN_BACKLASH); 101 | Serial.print(" and "); 102 | Serial.println(MAX_BACKLASH); 103 | 104 | startingBacklash = Serial.parseInt(); 105 | Serial.print("Starting backlash value: "); 106 | Serial.println(startingBacklash); 107 | 108 | // Clear any remaining characters from serialbuffer; 109 | while (Serial.available() > 0) Serial.read(); 110 | } 111 | 112 | // Get backlash increment for test 113 | int backlashIncrement = -1; 114 | while (backlashIncrement < 1 || backlashIncrement > 50) { 115 | Serial.println("Please enter a backlash increment value between 1 and 50"); 116 | backlashIncrement = Serial.parseInt(); 117 | Serial.print("Backlash increment: "); 118 | Serial.println(backlashIncrement); 119 | Serial.println(" "); 120 | 121 | // Clear any remaining serial buffer characters 122 | while (Serial.available() > 0) Serial.read(); 123 | } 124 | 125 | doBacklashTest(calType, startingBacklash, backlashIncrement); 126 | etch.turnOffMotors(); 127 | } 128 | 129 | 130 | void loop() { 131 | // put your main code here, to run repeatedly: 132 | } 133 | -------------------------------------------------------------------------------- /examples/EtchABotDriver/EtchABotDriver.ino: -------------------------------------------------------------------------------- 1 | /* 2 | * EtchABotDriver 3 | * by Debra Ansell (GeekMomProjects) 2015 4 | * www.geekmomprojects.com 5 | * 6 | * Arduino firmware for Etch-a-Bot to receive commands via the serial port. To be received 7 | * correctly, all command must terminate in a semicolon ';'. You can type all of these commands 8 | * into the Arduino IDE serial monitor to test. Examples command: "L 100 2000;" moves the stylus 9 | * in a straight line from the current position to position (100, 2000).; 10 | * 11 | * Command summary: 12 | * 'B': set backlash values (command of form 'B b_x b_y' where b_x, b_y are backlash values) 13 | * 'b': return currently set backlash values (returned in form 'b b_x b_y') 14 | * 's': return size ("Pocket size") or ("Travel size") 15 | * 'd' or 'D': return dimensions (in x y format) 16 | * 'e' or 'E': erase screen by tilting 17 | * 'l': draw line to relative position (e.g. "l 200 -400;" draws a line to a point 200 steps to 18 | * the right and 400 steps above the current position) 19 | * 'L': draw line to absolute position (e.g. "L 300 500;" draws a line from the current position 20 | * to the coordinate [300, 5000]) 21 | * 'm': move to relative position (same effect as 'l' since we can't lift the stylus) 22 | * 'M': move to absolute position (same effect as 'L' since we can't lift the stylus) 23 | * 'O': turn off motor/motors (specified as 'E', 'H', or 'V'. (e.g. "O HV;" or "O E;" or "O EHV;") 24 | */ 25 | 26 | #include // Must include Stepper.h anytime we use "EtchABot.h" 27 | #include "EtchABot.h" 28 | 29 | 30 | // Command buffer definitiions 31 | #define BUFFER_SIZE 16 32 | typedef struct { 33 | char cmd; 34 | int x; 35 | int y; 36 | } command; 37 | 38 | command cmdBuffer[BUFFER_SIZE]; 39 | byte readPtr = 0; 40 | byte writePtr = 0; 41 | 42 | // Create EtchABot object with default pin assignments. Please specify TRAVEL_SIZE 43 | // or POCKET_SIZE as is appropriate in the constructor below 44 | EtchABot etch(POCKET_SIZE); 45 | 46 | 47 | // Returns true if there is space in the buffer to write a new command 48 | boolean spaceInBuffer() { 49 | if (((readPtr + 1) % BUFFER_SIZE) == writePtr) return false; 50 | else return true; 51 | } 52 | 53 | void setup() { 54 | 55 | Serial.begin(57600); 56 | Serial.println("#start up"); 57 | Serial.println("OK"); 58 | 59 | // Seem to need delay here before starting to draw with motors 60 | delay(4000); 61 | 62 | } 63 | 64 | 65 | // Get the string out of the input. Assumes each string has format: 'c x y', where 66 | // 'c' is a character command and x,y are intergers 67 | boolean extractCmd(String &inputString) { 68 | // First character had better be the command 69 | char cmd = inputString[0]; 70 | int x = 0, y = 0; 71 | int space1, space2, len; 72 | boolean returnVal = true; 73 | 74 | switch (cmd) { 75 | case 'B': // Set backlash to supplied values 76 | space1 = inputString.indexOf(' '); 77 | space2 = inputString.lastIndexOf(' '); 78 | etch.setHBacklash(inputString.substring(space1 + 1, space2).toInt()); 79 | etch.setVBacklash(inputString.substring(space2 + 1).toInt()); 80 | Serial.print("#b "); 81 | Serial.print(etch.getHBacklash()); 82 | Serial.print(", "); 83 | Serial.println(etch.getVBacklash()); 84 | break; 85 | case 'b': // Return backlash values 86 | Serial.print("#b "); 87 | Serial.print(etch.getHBacklash()); 88 | Serial.print(" "); 89 | Serial.println(etch.getVBacklash()); 90 | break; 91 | case 'D': 92 | case 'd': // Return screen dimensions 93 | Serial.print("#D "); 94 | Serial.print(etch.getMaxX()); 95 | Serial.print(" "); 96 | Serial.println(etch.getMaxY()); 97 | break; 98 | case 'E': 99 | case 'e': 100 | // Convert to upper case 101 | cmd = 'E'; 102 | x = 0; 103 | y = 0; 104 | addCmdToBuffer(cmd, x, y); 105 | break; 106 | case 'L': 107 | case 'M': 108 | cmd = 'L'; 109 | space1 = inputString.indexOf(' '); 110 | space2 = inputString.lastIndexOf(' '); 111 | x = inputString.substring(space1 + 1, space2).toInt(); 112 | y = inputString.substring(space2 + 1).toInt(); 113 | addCmdToBuffer(cmd, x, y); 114 | break; 115 | case 'l': 116 | case 'm': 117 | // Coordinates will be converted before moving to next point 118 | cmd = 'l'; 119 | space1 = inputString.indexOf(' '); 120 | space2 = inputString.lastIndexOf(' '); 121 | x = inputString.substring(space1 + 1, space2).toInt(); 122 | y = inputString.substring(space2 + 1).toInt(); 123 | addCmdToBuffer(cmd, x, y); 124 | break; 125 | case 'O': // Shutoff specified motor 126 | space1 = inputString.indexOf(' '); 127 | inputString.toUpperCase(); 128 | len = inputString.length(); 129 | for (int i = space1 + 1; i < len; i++) { 130 | if (inputString.charAt(i) == 'E') etch.turnOffEraseMotor(); 131 | else if (inputString.charAt(i) == 'H') etch.turnOffHorzMotor(); 132 | else if (inputString.charAt(i) == 'V') etch.turnOffVertMotor(); 133 | } 134 | break; 135 | case 's': // Return string containing size 136 | if (etch.getType() == POCKET_SIZE) { 137 | Serial.println("#s Pocket size"); 138 | } else { 139 | Serial.println("#s Travel size"); 140 | } 141 | break; 142 | default: 143 | Serial.print("#unknown command: "); 144 | Serial.println(cmd); 145 | if (spaceInBuffer()) Serial.println("OK"); 146 | returnVal = false; 147 | break; 148 | } 149 | 150 | // Add the command to the buffer and increment the write pointer 151 | if (spaceInBuffer()) Serial.println("OK"); 152 | return returnVal; 153 | } 154 | 155 | // Adds a commad to the buffer and increments the write pointer 156 | void addCmdToBuffer(char cmd, int x, int y) { 157 | cmdBuffer[writePtr].cmd = cmd; 158 | cmdBuffer[writePtr].x = x; 159 | cmdBuffer[writePtr].y = y; 160 | writePtr = (writePtr + 1) % BUFFER_SIZE; 161 | if (spaceInBuffer()) Serial.println("OK"); 162 | } 163 | 164 | void doNextCmd() { 165 | if (readPtr == writePtr) return; // no new commands 166 | 167 | //Serial.println("In doNextCmd"); 168 | char cmd = cmdBuffer[readPtr].cmd; 169 | int x = cmdBuffer[readPtr].x; 170 | int y = cmdBuffer[readPtr].y; 171 | if (cmd == 'l') { 172 | x += etch.getX(); 173 | y += etch.getY(); 174 | cmd = 'L'; 175 | } 176 | 177 | if (cmdBuffer[readPtr].cmd == 'E') { 178 | etch.doErase(); 179 | } else if (cmdBuffer[readPtr].cmd == 'L') { 180 | etch.drawLine(x, y); 181 | } 182 | readPtr = (readPtr + 1) % BUFFER_SIZE; 183 | if (spaceInBuffer()) Serial.println("OK"); 184 | 185 | Serial.print("#"); 186 | Serial.print(cmd); 187 | if (cmd == 'E') { 188 | Serial.println(""); 189 | } else { 190 | Serial.print(" "); 191 | Serial.print(x); 192 | Serial.print(" "); 193 | Serial.println(y); 194 | } 195 | } 196 | 197 | 198 | String inputString = ""; 199 | boolean stringComplete = false; 200 | void loop() { 201 | 202 | #ifndef MODE_CALIBRATION 203 | // Fill the command buffer with commands 204 | // TBD - make sure we don't hang waiting for a new command 205 | while (Serial.available() && spaceInBuffer()) { 206 | // Get the new byte 207 | char inChar = (char) Serial.read(); 208 | // add it to inputString 209 | if (inChar != '\n' && inChar != '\r') { 210 | inputString += inChar; 211 | if (inChar == ';') { 212 | stringComplete = true; 213 | } 214 | } 215 | // Parse the string for instructions 216 | if (stringComplete) { 217 | //delay(50); 218 | //Serial.print("Got String: "); 219 | //Serial.println(inputString.c_str()); 220 | extractCmd(inputString); 221 | inputString = ""; 222 | stringComplete = false; 223 | } 224 | } 225 | 226 | doNextCmd(); 227 | #endif 228 | } 229 | -------------------------------------------------------------------------------- /examples/EtchABotJoystickControl/EtchABotJoystickControl.ino: -------------------------------------------------------------------------------- 1 | /** 2 | * EtchABotJoystickControl 3 | * Debra Ansell (www.geekmomprojects.com) 4 | * Using Sparkfun Analog Joystick to control direction of EtchABot motors. 5 | * Pressing joystick button triggers erase. 6 | */ 7 | 8 | #include 9 | #include "EtchABot.h" 10 | 11 | int vert = 0; 12 | int horz = 0; 13 | int SEL, prevSEL = HIGH; 14 | long lastDebounceTime = 0; 15 | long debounceDelay = 50; //millis 16 | 17 | int VERT_MAX = 1023; 18 | int HORZ_MAX = 1023; 19 | 20 | #define MAX_DISTANCE 10 21 | #define GND_PIN A5 // Set to HIGH 22 | #define VCC_PIN A4 // Set to LOW 23 | #define HORZ_PIN A3 24 | #define VERT_PIN A2 25 | #define BUTTON_PIN A1 26 | 27 | EtchABot etch(POCKET_SIZE); 28 | 29 | void setup() { 30 | pinMode(GND_PIN, OUTPUT); 31 | pinMode(VCC_PIN, OUTPUT); 32 | pinMode(BUTTON_PIN, INPUT); 33 | pinMode(HORZ_PIN, INPUT); 34 | pinMode(VERT_PIN, INPUT); 35 | 36 | digitalWrite(GND_PIN, LOW); 37 | digitalWrite(VCC_PIN, HIGH); 38 | digitalWrite(BUTTON_PIN, HIGH); // turn on pullup resistor 39 | Serial.begin(57600); 40 | } 41 | 42 | void loop() { 43 | // Check for button push - and debounce 44 | int reading = digitalRead(BUTTON_PIN); 45 | //Serial.print("reading = "); 46 | //Serial.println(reading); 47 | if (reading != prevSEL) { 48 | // reset timer 49 | lastDebounceTime = millis(); 50 | } 51 | if (millis() - lastDebounceTime > debounceDelay) { 52 | if (reading != SEL) { 53 | SEL = reading; 54 | 55 | if (SEL == LOW) { // Goes to ground if pressed (see sparkfun tutorial 272) 56 | etch.doErase(); 57 | } 58 | } 59 | } 60 | prevSEL = reading; 61 | 62 | 63 | // Get direction relative to current position 64 | vert = analogRead(VERT_PIN); 65 | horz = analogRead(HORZ_PIN); 66 | 67 | // Get direction and magnitude of joystick vector (normalized to [-1.0,1.0] range 68 | float xNorm = 2.0*(vert - VERT_MAX/2.0)/VERT_MAX; 69 | float yNorm = -2.0*(horz - HORZ_MAX/2.0)/HORZ_MAX; 70 | 71 | // Ignore small fluctuations around the center 72 | if (abs(xNorm) < 0.1) xNorm = 0.0; 73 | if (abs(yNorm) < 0.1) yNorm = 0.0; 74 | 75 | int targetX = (int) round(etch.getX() + MAX_DISTANCE*xNorm); 76 | int targetY = (int) round(etch.getY() + MAX_DISTANCE*yNorm); 77 | targetX = constrain(targetX, 0, etch.getMaxX()); 78 | targetY = constrain(targetY, 0, etch.getMaxY()); 79 | 80 | /* 81 | Serial.print("("); 82 | Serial.print(targetX); 83 | Serial.print(", "); 84 | Serial.print(targetY); 85 | Serial.println(")"); 86 | */ 87 | etch.drawLine(targetX, targetY); 88 | 89 | } 90 | -------------------------------------------------------------------------------- /examples/EtchABotPatterns/EtchABotPatterns.ino: -------------------------------------------------------------------------------- 1 | /* 2 | * EtchABotPatterns 3 | * by Debra Ansell (GeekMomProjects) 2015 4 | * www.geekmomprojects.com 5 | * 6 | * Software to make changing mathematical patterns on the Etch-a-Bot. 7 | * You could add any mathematical function, as long as it is 8 | * parameteric. Just provide a doReset() and calculatePoint() 9 | * function for it inside an #ifdef statement as done below with 10 | * SPIROGRAPH and LISAJOUS. 11 | */ 12 | 13 | #include // Must include this if we will use "EtchABot.h" 14 | #include "EtchABot.h" 15 | 16 | #define DEGREES_T0_RADIANS 0.0174533 17 | #define RESET_MINUTES 4 // # of minutes after which we reset the pattern 18 | 19 | // Pick one of the options below to define the type of curve 20 | #define SPIROGRAPH 21 | //#define LISAJOUS 22 | 23 | // *** !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ** 24 | // Create the correct size EtchABot object (using default pin assignments) 25 | // *** BE SURE TO SPECIFY THE SIZE OF YOUR ETCH-A-SKETCH HERE, OR IT COULD GET RUINED! ** 26 | EtchABot etch(POCKET_SIZE); 27 | 28 | // Global parameteric variables 29 | int angle = 0; 30 | float t = 0; // Parametric angle in radians 31 | 32 | #ifdef LISAJOUS 33 | int hParam = 7; 34 | int vParam = 11; 35 | void doReset() { 36 | hParam = random(4, 12); 37 | vParam = random(4, 12); 38 | } 39 | 40 | int calculatePoint(int*x, int*y) { 41 | t = angle*DEGREES_T0_RADIANS; 42 | *x = (int) round((etch.getMaxX()/2 - 20)*sin(hParam*t) + etch.getMaxX()/2); 43 | *y = (int) round((etch.getMaxY()/2 - 20)*cos(vParam*t) + etch.getMaxY()/2); 44 | // Increment angle 45 | angle = angle + 2; 46 | } 47 | #endif 48 | 49 | #ifdef SPIROGRAPH 50 | // Initial values of spirograph parameters 51 | float lParam = .3; 52 | float kParam = .8; 53 | 54 | // Generates random new Spirograph parameters 55 | void doReset() { 56 | int r = random(15, 85); 57 | lParam = r/100.0; 58 | r = random(15, 85); 59 | kParam = r/100.0; 60 | } 61 | 62 | // Calculates the value of x, y from t (in radians), and increments the angle 63 | int calculatePoint(int* x, int* y) { 64 | Serial.println("Lisajous calculate point"); 65 | t = angle*DEGREES_T0_RADIANS; 66 | float fac = (1.0 - kParam)/kParam; 67 | *x = (int) round((etch.getMaxY()/2-20)*((1 - kParam)*cos(t) + lParam*kParam*cos(fac*t)) + etch.getMaxX()/2); 68 | *y = (int) round((etch.getMaxY()/2-20)*((1 - kParam)*sin(t) - lParam*kParam*sin(fac*t)) + etch.getMaxY()/2); 69 | // Increment angle 70 | angle = angle + 2; 71 | } 72 | #endif 73 | 74 | 75 | // Keeps track of the last time we restarted the spirograph 76 | int lastStart; 77 | 78 | void setup() { 79 | delay(2000); 80 | randomSeed(analogRead(0)); // Initialize random 81 | etch.doErase(); 82 | doReset(); 83 | // put your setup code here, to run once: 84 | lastStart = millis()/1000; 85 | 86 | // Smooth, repeating curves draw better with backlash turned off 87 | etch.setHBacklash(0); 88 | etch.setVBacklash(0); 89 | } 90 | 91 | void loop() { 92 | // Erase and restart every few minutes 93 | if (millis()/1000 - lastStart > 60*RESET_MINUTES) { 94 | etch.doErase(); 95 | lastStart = millis()/1000; 96 | angle = 0; 97 | 98 | doReset(); 99 | 100 | } 101 | // Draw the next line segment 102 | int x, y; 103 | calculatePoint(&x, &y); 104 | etch.drawLine(x,y); 105 | } 106 | -------------------------------------------------------------------------------- /keywords.txt: -------------------------------------------------------------------------------- 1 | EtchABot KEYWORD1 2 | doErase KEYWORD2 3 | drawLine KEYWORD2 4 | drawArc KEYWORD2 5 | getX KEYWORD2 6 | getY KEYWORD2 7 | getMaxX KEYWORD2 8 | getMaxY KEYWORD2 9 | getHBacklash KEYWORD2 10 | getVBacklash KEYWORD2 11 | setHBacklash KEYWORD2 12 | setVBacklash KEYWORD2 13 | turnOffMotors KEYWORD2 14 | turnOffEraseMotor KEYWORD2 15 | turnOffHorzMotor KEYWORD2 16 | turnOffVertMotor KEYWORD2 17 | -------------------------------------------------------------------------------- /nodefiles/client/client.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | EtchABot 10 | 66 | 67 | 68 | 69 |
70 | 71 |     75 | 76 | 77 | 78 |
79 | 80 | 81 |
82 |
83 |
84 | 85 |
86 |

87 |

Backlash

88 |

Horz: ?

89 |

Vert: ?

90 |

91 | 92 |

93 |
94 |
95 |
96 |

97 | 98 | 99 |

100 |
101 | 102 |
103 | 104 |     105 | 106 |
107 | 108 | 109 | 110 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /nodefiles/client/js/app.js: -------------------------------------------------------------------------------- 1 | // Helper function to convert units of length measurement to screen pixels. 2 | // Assumes 96 dpi screen measurement. Takes string as input. 3 | function toPixels(lengthStr) { 4 | var pix; 5 | if (lengthStr.search("in") >= 0) pix = parseFloat(lengthStr) * 96; 6 | else if (lengthStr.search("cm") >= 0) pix = parseFloat(lengthStr) * 96 / 2.2; 7 | else if (lengthStr.search("px") >= 0) pix = parseInt(lengthStr); 8 | else pix = parseInt(lengthStr); 9 | return Math.round(pix); 10 | } 11 | 12 | 13 | // Makes a command string out of a command and two numbers 14 | function makeCmdString(cmd, x, y) { 15 | return cmd + ' ' + Math.round(x) + ' ' + Math.round(y); 16 | } 17 | 18 | // Canvas drawing functions 19 | 20 | // Scope preserving wrapper for canvas callback function 21 | var onCanvasMouseClick = function(canvasPoints) { 22 | return function(e) { 23 | canvasPoints.addPoint(e.pageX - this.offsetLeft, e.pageY - this.offsetTop); 24 | canvasPoints.drawPoints(this); 25 | } 26 | } 27 | 28 | // Scope preserving wrapper for canvas callback function 29 | var onCanvasMouseDown = function(canvasPoints) { 30 | return function(e) { 31 | canvasPoints.setPaint(true); 32 | canvasPoints.drawPoints(this); 33 | } 34 | } 35 | 36 | // Scope preserving wrapper for canvas callback function 37 | var onCanvasMouseUp = function(canvasPoints) { 38 | return function(e) { 39 | canvasPoints.setPaint(false); 40 | canvasPoints.drawPoints(this); 41 | } 42 | } 43 | 44 | // Scope preserving wrapper for canvas callback function 45 | var onCanvasMouseMove = function(canvasPoints) { 46 | return function(e) { 47 | if(canvasPoints.paint) { 48 | canvasPoints.addPoint(e.pageX - this.offsetLeft, e.pageY - this.offsetTop); 49 | canvasPoints.drawPoints(this); 50 | } 51 | } 52 | } 53 | 54 | // Scope preserving wrapper for canvas callback function 55 | var onCanvasMouseLeave = function(canvasPoints) { 56 | return function(e) { 57 | canvasPoints.setPaint(false); 58 | } 59 | } 60 | 61 | // Scope preserving wrapper for canvas callback functions 62 | var onClickErase = function(canvas, canvasPoints) { 63 | return function(e) { 64 | canvasPoints.reset(); 65 | canvasPoints.setPaint(false); 66 | canvasPoints.drawPoints(canvas); 67 | } 68 | } 69 | 70 | var onClickEraseLast = function(canvas, canvasPoints) { 71 | return function(e) { 72 | if (canvasPoints.nPoints() > 1 ) { 73 | canvasPoints.popPoint(); 74 | canvasPoints.drawPoints(canvas); 75 | } 76 | } 77 | } 78 | 79 | // Check for integer. Function lifted from: 80 | // http://stackoverflow.com/questions/14636536/how-to-check-if-a-variable-is-an-integer-in-javascript 81 | function isInt(value) { 82 | return !isNaN(value) && (function(x) { return (x | 0) === x; })(parseFloat(value)) 83 | } 84 | 85 | // Helper function for setting backlash to valid values 86 | function validateBacklashValue(val, min, max) { 87 | if (isInt(val) && val >= min && val <= max) return true; 88 | else return false; 89 | } 90 | // Scope preserving function - get new backlash values and send them to the EtchABot 91 | var onClickSetBacklash = function(socket) { 92 | return function(e) { 93 | var bMin = 0; 94 | var bMax = 500; 95 | // Get and validate values for backlash 96 | var h = prompt("Horz backlash: [" + bMin + "-" + bMax + "]", document.getElementById('xBacklash').value); 97 | if (!validateBacklashValue(h, bMin, bMax)) { 98 | alert("Invalid backlash value " + h + " Exiting."); 99 | return; 100 | } 101 | var v = prompt("Horz backlash: [" + bMin + "-" + bMax + "]", document.getElementById('xBacklash').value); 102 | if (!validateBacklashValue(h, bMin, bMax)) { 103 | alert("Invalid backlash value " + h + " Exiting."); 104 | return; 105 | } 106 | // Now send valid values to Etch-a-Sketch 107 | socket.emit('cmds', makeCmdString('B', parseInt(h), parseInt(v))); 108 | } 109 | } 110 | 111 | // Sets browser HTML elements for drawing mode 112 | var setDrawMode = function() { 113 | // Set button visibility and display canvas 114 | document.getElementById('filechooser').style.display = 'none'; 115 | document.getElementById('imageDisplay').style.display = 'none'; 116 | document.getElementById('eraseLast').style.display = 'initial'; 117 | document.getElementById('erase').style.display = 'initial'; 118 | document.getElementById('theCanvas').style.display = 'block'; 119 | document.getElementById('imToData').value = 'Drawing to Data'; 120 | } 121 | 122 | // Sets browser HTML elements for image mode 123 | var setImageMode = function() { 124 | // Set button visibility and hide the canvas 125 | document.getElementById('filechooser').style.display = 'initial'; 126 | document.getElementById('eraseLast').style.display = 'none'; 127 | document.getElementById('erase').style.display = 'none'; 128 | document.getElementById('theCanvas').style.display = 'none'; 129 | document.getElementById('imageDisplay').style.display = 'block'; 130 | document.getElementById('imToData').value = 'Image to Data'; 131 | } 132 | 133 | // Event handler for selecting drawing mode 134 | var switchModes = function(e) { 135 | //alert(this.value + " was selected"); 136 | if (this.value === "draw") setDrawMode(); 137 | else if (this.value === "image") setImageMode(); 138 | else alert("Unknown mode " + this.value + " selected"); 139 | } 140 | 141 | // Returns either "draw" or "image" currently 142 | var getDisplayMode = function() { 143 | return document.getElementById('drawMode').value; 144 | } 145 | 146 | // Returns true if the currently displayed image is SVG 147 | var isDisplayingSVG = function() { 148 | if ((getDisplayMode() === "image") && (document.getElementsByTagName('svg').length > 0)) return true; 149 | else return false; 150 | } 151 | 152 | // Reads in the contents of an image raster (.jpg, .gif, .png) file or vector (.svg) file 153 | var onFileSelect = function(img) { 154 | return function(evt) { 155 | if (evt.target.files.length == 0) return; 156 | var file = evt.target.files[0]; // FileList object 157 | 158 | var imDisp = document.getElementById('imageDisplay'); 159 | 160 | // Treat vector (SVG) files differently from raster images 161 | if (file.type == "image/svg+xml") { 162 | 163 | var reader = new FileReader(); 164 | // Callback function runs when SVG file is loaded 165 | reader.onload = function(e) { 166 | // Read in SVG and scale it to fit in the display 167 | // First remove any existing child images 168 | imDisp.innerHTML = e.target.result; 169 | var svg = document.getElementsByTagName('svg')[0]; 170 | // Set the viewbox if we don't have one 171 | if (!svg.getAttribute('viewBox')) { 172 | var w = toPixels(svg.getAttribute("width")); 173 | var h = toPixels(svg.getAttribute("height")); 174 | svg.setAttribute('viewBox', '0 0 ' + parseInt(w) + ' ' + parseInt(h)); 175 | } 176 | svg.setAttribute('class', "svgDisplay"); 177 | } 178 | reader.readAsText(file); 179 | } else { 180 | var reader = new FileReader(); 181 | // Callback function runs when file is completely loaded 182 | reader.onload = function(e) { 183 | // Clear any old images from the imDisplay element 184 | imDisp.innerHTML = ""; 185 | 186 | // need to reset any old image values for height/width and copy the image 187 | img.setAttribute("height", "auto"); 188 | img.setAttribute("width", "auto"); 189 | 190 | img.src = reader.result; 191 | 192 | // Scale to fit inside the divbox 193 | var style = getComputedStyle(imDisp); 194 | var hDiv = parseInt(style.getPropertyValue('height')); 195 | var wDiv = parseInt(style.getPropertyValue('width')); 196 | //alert("Hdiv, wdiv = " + hDiv + " " + wDiv); 197 | // Get image dimensions, and rescale to fit in the divbox while maintaining 198 | // the original aspect ratio of the image 199 | var h = parseFloat(img.naturalHeight); 200 | var w = parseFloat(img.naturalWidth); 201 | if (h/w > 1.0*hDiv/wDiv) { 202 | img.setAttribute('height', hDiv + 'px'); 203 | img.setAttribute('width', Math.round(w*hDiv/h) + 'px'); 204 | } else { 205 | img.setAttribute('width', wDiv + 'px'); 206 | img.setAttribute('height', Math.round(h*wDiv/w) + 'px'); 207 | } 208 | // Put image on the screen 209 | imDisp.appendChild(img); 210 | } 211 | reader.readAsDataURL(file); 212 | } 213 | 214 | } 215 | } 216 | 217 | // Helper functions to extract one or more integers from a string. Returns list 218 | // of integers in the string. Assumes the first character in the string is part 219 | // of the first integer. Integers must be separated by spaces. 220 | function getIntegersFromString(str) { 221 | var numList = []; 222 | while (str.length > 0) { 223 | while (str[0] == ' ') { // Eliminate leading spaces 224 | str = str.substring(1); 225 | } 226 | var space = str.indexOf(' '); // Find next space separator 227 | if (space == -1) { 228 | numList.push(parseInt(str)); 229 | str = ""; 230 | } else { 231 | numList.push(parseInt(str.substring(0,space))); 232 | str = str.substring(space); 233 | } 234 | } 235 | return numList; 236 | } 237 | 238 | // Function to display what is currently being drawn on the screen. 239 | // TBD - write this function 240 | var drawStylusPosition = function(data) { 241 | // Get the context to draw the line at 242 | var imDisp = document.getElementById('imDisplay'); 243 | 244 | } 245 | 246 | // Event handler for data received from server 247 | var onSocketNotification = function(display) { 248 | return function(data) { 249 | // Display recevied notifications in text area 250 | var cmd = ' '; 251 | if (data.length > 2 && data[0] == '#') { cmd = data[1]; } 252 | if (cmd == 'D') { // Set dimensions 253 | var numList = getIntegersFromString(data.substring(2)); 254 | if (numList.length) { 255 | var w = numList[0]; var h = numList[1]; 256 | } 257 | //alert("Setting display size to [" + w + ", " + h + "]"); 258 | display.size = [w,h]; 259 | display.center = [(display.origin[0] + w)/2, (display.origin[1] + h)/2]; 260 | } else if (cmd == 'b' || cmd == 'B') { //Receiving backlash info 261 | var numList = getIntegersFromString(data.substring(2)); 262 | if (numList.length) { 263 | document.getElementById('xBacklash').innerHTML = "Horz: " + numList[0]; 264 | document.getElementById('yBacklash').innerHTML = "Vert: " + numList[1]; 265 | } 266 | } else if (cmd == 's') { // Receiving size info 267 | document.getElementById('etchSize').innerHTML = "

Etch A Sketch

" + data.substring(2); 268 | } else if (cmd == 'L' || cmd == 'M' || cmd == 'l' || cmd == 'm') { 269 | // Mark the currently drawn line in red if we're drawing an svg or the currently 270 | // drawn pixel if it is an image. 271 | drawStylusPosition(data); 272 | } 273 | document.getElementById('receivedText').value += ('\n' + data); 274 | //alert(data); 275 | } 276 | } 277 | 278 | // If I ever decide to convert polygons to paths, this function will be useful. 279 | // currently it is unused 280 | var convertAllPolysToPaths = function() { 281 | var polys = document.querySelectorAll('polygon,polyline'); 282 | [].forEach.call(polys,convertPolyToPath); 283 | 284 | function convertPolyToPath(poly){ 285 | var svgNS = poly.ownerSVGElement.namespaceURI; 286 | var path = document.createElementNS(svgNS,'path'); 287 | var points = poly.getAttribute('points').split(/\s+|,/); 288 | var x0=points.shift(), y0=points.shift(); 289 | var pathdata = 'M'+x0+','+y0+'L'+points.join(' '); 290 | if (poly.tagName=='polygon') pathdata+='z'; 291 | path.setAttribute('d',pathdata); 292 | poly.parentNode.replaceChild(path,poly); 293 | } 294 | } 295 | 296 | // Appends points in current node and its children to coordList - a list of [x,y] 297 | // coordinate pairs. Returns a bounding box with min/max x,y values of the node points. 298 | var nodeToCoords = function(node, coordList, hOffset, vOffset) { 299 | 300 | var children = node.childNodes; 301 | for (var i = children.length-1; i >= 0; i--) { 302 | var child = children[i]; 303 | if (child.nodeType != 1) continue; // skip anything that isn't an element 304 | var parent = child.parentNode; 305 | switch(child.nodeName) { 306 | case 'g': // Recursively expand child nodes that are part of groups 307 | nodeToCoords(child, coordList, hOffset, vOffset); 308 | break; 309 | case 'circle': 310 | case 'eclipse': 311 | case 'line': 312 | case 'polyline': 313 | case 'polygon': 314 | case 'rect': 315 | alert("can't convert element of type " + child.NodeName + " to path"); 316 | break; 317 | case 'path': 318 | var len = child.getTotalLength(); 319 | var CTM = child.getScreenCTM(); 320 | // p0 = SVG coords, p = screen coords 321 | var p0, p; 322 | // Eventually should put in variable spacing for smaller SVGs 323 | var spacing = 1; 324 | if (len < 50) spacing = len/100.0; // Scale down for small scale SVGs 325 | for (var j = 0; j < len; j += spacing) { 326 | p0 = child.getPointAtLength(j); 327 | p = p0.matrixTransform(CTM); 328 | // Must subtract out window origin 329 | p.x -= hOffset; 330 | p.y -= vOffset; 331 | if (j == 0) { 332 | coordList.push([p.x, p.y, 'M']); //"move" command to get to beginning of each new path 333 | } else { 334 | coordList.push([p.x, p.y, 'L']); //"line" command to traverse path 335 | } 336 | } 337 | parent.removeChild(child); 338 | break; 339 | } 340 | } 341 | } 342 | 343 | // Helper function to return the border width of a page element 344 | var getBorderWidth = function(el) { 345 | var border = getComputedStyle(el, null).border; 346 | if (border === null || border === "") return 0; 347 | var parts = border.split(' '); 348 | for (i = 0; i < parts.length; i++) { 349 | if (parts[i].indexOf('px') > 0) { 350 | return parseInt(parts[i]); 351 | } 352 | } 353 | } 354 | 355 | // Converts the current SVG to a list of drawing commands for the etch a sketch 356 | var svgToCmdList = function(svg, display, cmdList) { 357 | 358 | // Get canvas and context for drawing 359 | // Scale to fit inside the div box - first get div box dimensions 360 | var disp = document.getElementById('imageDisplay'); 361 | var style = getComputedStyle(disp); 362 | var hDiv = parseInt(style.getPropertyValue("height")); 363 | var wDiv = parseInt(style.getPropertyValue("width")); 364 | 365 | // Find origin of image display relative to page 366 | var boundingRect = disp.getBoundingClientRect(); 367 | var borderWidth = getBorderWidth(disp); 368 | var offsetX = boundingRect.left + borderWidth; 369 | var offsetY = boundingRect.top + borderWidth; 370 | 371 | // Reset the cmdlist 372 | cmdList.length = 0; 373 | cmdList.push('E'); // Start drawing commands by erasing Etch-a-sketch 374 | cmdList.push('M 0 0'); // Go to origin 375 | 376 | // Get scale factor between display and Etch A Sketch coordinates 377 | var scaleFactor = getScaleFactor(display); 378 | //alert("ScaleFactor = " + scaleFactor); 379 | 380 | // Compute the new screen coords 381 | var screenCoords = []; 382 | nodeToCoords(svg, screenCoords, offsetX, offsetY); 383 | 384 | // Comvert screen coords to command list 385 | for (var i = 0; i < screenCoords.length; i++) { 386 | cmdList.push(makeCmdString(screenCoords[i][2], screenCoords[i][0]*scaleFactor, screenCoords[i][1]*scaleFactor)); 387 | } 388 | 389 | // Get new drawing context and canvas 390 | disp.innerHTML = ""; 391 | var canvas = document.createElement('canvas'); 392 | canvas.width = wDiv; 393 | canvas.height = hDiv; 394 | disp.appendChild(canvas); 395 | var ctx = canvas.getContext("2d"); 396 | 397 | // Last command returns pen to origin - may want to take a cleaner route back (TBD???) 398 | cmdList.push(makeCmdString('M', 0, 0)); 399 | cmdList.push('O EHV'); // Turn off motors when done 400 | 401 | // Render drawing commands to screen. Red lines indicate 'Move', and 402 | // black lines indicate 'Line' 403 | ctx.clearRect(0, 0, canvas.width, canvas.height); 404 | ctx.beginPath(); 405 | ctx.moveTo(0,0); 406 | for (var i = 0; i < screenCoords.length-1; i++) { 407 | var p = screenCoords[i]; 408 | var curDrawMode = p[2]; 409 | var nextDrawMode = screenCoords[i+1][2]; 410 | if (nextDrawMode == curDrawMode) { 411 | ctx.lineTo(Math.round(p[0]), Math.round(p[1])); 412 | } else { 413 | //alert(curDrawMode); 414 | ctx.lineTo(Math.round(p[0]), Math.round(p[1])); 415 | ctx.strokeStyle = (curDrawMode == 'M' ? 'Red' : 'Black'); 416 | ctx.lineWidth = 0.5; 417 | ctx.stroke(); 418 | ctx.beginPath(); 419 | ctx.moveTo(Math.round(p[0]), Math.round(p[1])); 420 | } 421 | } 422 | var p = screenCoords[screenCoords.length-1]; 423 | ctx.lineTo(Math.round(p[0]), Math.round(p[1])); 424 | ctx.strokeStyle = (p[2] == 'M' ? 'Red' : 'Black'); 425 | ctx.lineWidth = 0.5; 426 | ctx.stroke(); 427 | 428 | return cmdList.length; 429 | } 430 | 431 | 432 | // Returns the relative scale of Etch-a-Sketch display to image 433 | var getScaleFactor = function(display) { 434 | var scaleFactor = 1.0; 435 | var etchCanvas = document.getElementById('theCanvas'); 436 | var h = etchCanvas.height; 437 | var w = etchCanvas.width; 438 | 439 | if (h/w > display.size[1]/display.size[0]) { 440 | scaleFactor = display.size[1]/h; 441 | } else { 442 | scaleFactor = display.size[0]/w; 443 | } 444 | return scaleFactor; 445 | } 446 | 447 | // Turns user drawn points from the screen into a list of commands 448 | var pointsToCmdList = function(points, display, cmdList) { 449 | // Clear out old points from list 450 | cmdList.length = 0; 451 | 452 | // Compute the relative scale of display to image 453 | var scaleFactor = getScaleFactor(display); 454 | 455 | // Loop over the list of lines, and create an instruction string to send 456 | // out via the socket. 457 | // Canvas and display both have origin at (0,0) - don't need to recenter 458 | // points, only need to rescale them. 459 | cmdList.push("E"); // First cmd - erase old points 460 | for (var i = 0; i < points.clickX.length; i++) { 461 | cmdList.push(makeCmdString('L', points.clickX[i]*scaleFactor, points.clickY[i]*scaleFactor)); 462 | } 463 | cmdList.push(makeCmdString('L', 0, 0)); // Last cmd - return to origin 464 | return cmdList.length; 465 | } 466 | 467 | // Resizes a raster image to size (w, h) 468 | function resizeImage(im, w, h) { 469 | var canvas = document.createElement('canvas'); 470 | canvas.width = w; 471 | canvas.height = h; 472 | var context = canvas.getContext('2d'); 473 | context.drawImage(im, 0, 0, w, h); 474 | return canvas.toDataURL(); 475 | } 476 | 477 | // Returns a grayscale version of the raster image passed in 478 | function makeGrayscaleImage(im) { 479 | var canvas = document.createElement('canvas'); 480 | var w = im.naturalWidth; 481 | var h = im.naturalHeight; 482 | canvas.width = w; 483 | canvas.height = h; 484 | var context = canvas.getContext('2d'); 485 | context.drawImage(im, 0, 0 ); 486 | var imgPixels = context.getImageData(0, 0, w, h); 487 | 488 | for (var y = 0; y < h; y++) { 489 | for (var x = 0; x < w; x++) { 490 | var i = (y*4) * w + x*4; 491 | var avg; 492 | if (imgPixels.data[i+3] == 0) { 493 | avg = 0; // If completely transparent, should appear white 494 | } else { 495 | avg = (imgPixels.data[i] + imgPixels.data[i+1] + imgPixels.data[i+2])/3; 496 | } 497 | imgPixels.data[i] = avg; 498 | imgPixels.data[i+1] = avg; 499 | imgPixels.data[i+2] = avg; 500 | // Eliminate transparency in alpha channel 501 | //imgPixels.data[i+3] = 255; 502 | } 503 | } 504 | 505 | // Can probably use image_src tag to resize displayed data (TBD) 506 | context.putImageData(imgPixels, 0, 0, 0, 0, w, h); 507 | return canvas.toDataURL(); 508 | } 509 | 510 | // Converts the raster image to a series of drawing commands 511 | // that renders the current image crosshatch style. Not for 512 | // use with EtchABot, but works well with a V-plotter 513 | var imgToCrosshatchCmds = function(img, display, cmdList) { 514 | // Clear out any old points from cmdList 515 | cmdList.length = 0; 516 | } 517 | 518 | // Converts the raster image to a series of drawing commands 519 | // which render the image in back and forth "jitter" style 520 | // where the height of the "jitter" corresponds to the darkness 521 | // of the pixel. 522 | var imgToCmds = function(img, display, cmdList) { 523 | // Clear out any old points from cmdList 524 | cmdList.length = 0; 525 | 526 | // Image parameters - these give scaled height 527 | var w = img.naturalWidth; 528 | var h = img.naturalHeight; 529 | //alert("width = " + w + " height = " + h ); 530 | 531 | // Create a virtual canvas for image manipulation and access to individual pixels 532 | var canvas = document.createElement('canvas'); 533 | canvas.width = w; 534 | canvas.height = h; 535 | var context = canvas.getContext('2d'); 536 | context.drawImage(img, 0, 0 ); 537 | var imgPixels = context.getImageData(0, 0, w, h); 538 | 539 | // Find the max/min image intensity values 540 | var inMax = imgPixels.data[0]; 541 | var inMin = imgPixels.data[0]; 542 | var nPixels = w*h; 543 | for (var i = 1; i < nPixels; i++) { 544 | var pixVal = imgPixels.data[i*4]; 545 | if (pixVal > inMax) { 546 | inMax = pixVal; 547 | } else if (pixVal < inMin) { 548 | inMin = pixVal; 549 | } 550 | } 551 | // Get range of values 552 | var inRange = inMax - inMin; 553 | //alert("Max = " + inMax + " Min = " + inMin); 554 | 555 | // Compute the relative scale of display to image 556 | // Compare image aspect ratio to canvas aspect ratio. 557 | // Then scaleFactor up the image to MAXIMSIZE pixels 558 | // on the largest side. Add a slight margin for error 559 | // to make sure we don't exceed boundaries 560 | var scaleFactor = 1.0; 561 | var margin = 200.0; 562 | var maxStrokes = 6; // Max density of strokes per pixel. 563 | //alert ("image height = " + h + " image width = " + w); 564 | if (1.0*h/w > 1.0*display.size[1]/display.size[0]) { 565 | scaleFactor = (display.size[1] - margin)/h; 566 | //alert("Height scale factor = " + scaleFactor); 567 | } else { 568 | scaleFactor = (display.size[0] - margin)/w; 569 | //alert("Width scale factor = " + scaleFactor); 570 | } 571 | 572 | // Compute starting point of image 573 | var startx = display.center[0] - w*scaleFactor/2; 574 | var starty = display.center[1] - h*scaleFactor/2; 575 | 576 | // Start by erasing etch-a-sketch and moving to first point 577 | cmdList.push(makeCmdString('E', 0, 0)); 578 | cmdList.push(makeCmdString('L', startx, starty)); 579 | 580 | // Compute lines to draw each pixel in the image 581 | var jh = scaleFactor; // Maximum jitterHeight 582 | //alert("jitterheight = " + jh); 583 | 584 | var LEFT_TO_RIGHT = 1; 585 | var RIGHT_TO_LEFT = -1; 586 | var direction; 587 | 588 | // Iterate over rows 589 | for (var j = 0; j < h; j++) { 590 | 591 | if ((j%2) == 0) direction = LEFT_TO_RIGHT; // First row is always left to right 592 | else direction = RIGHT_TO_LEFT; 593 | 594 | var rowBaseY = starty + j*scaleFactor; // Y-coordinate of row base 595 | // Copy the current row's intensity values into an array 596 | var rowPixels = []; 597 | var rowStart = j*w; 598 | var rowEnd = (j+1)*w; 599 | for (var i = rowStart; i < rowEnd; i++) { 600 | rowPixels.push(imgPixels.data[i*4]); 601 | } 602 | if (direction == RIGHT_TO_LEFT) rowPixels.reverse(); 603 | 604 | // X coordinate of first pixel in row 605 | var rowStartX; 606 | if (direction == LEFT_TO_RIGHT) rowStartX = startx; 607 | else rowStartX = startx + w*scaleFactor; 608 | 609 | // Move to first pixel in new row 610 | cmdList.push(makeCmdString('M', rowStartX, rowBaseY)); 611 | // Traverse the row 612 | for (var i = 0; i < w; i++) { 613 | var val = rowPixels[i]; 614 | var intensity = (inMax - val)/inRange; // White (hightest val) gets smallest jitter 615 | var jitter = Math.round(maxStrokes*intensity); 616 | //alert("j = " + j + " i = " + i + " val = " + val + " jitter = " + jitter); 617 | if (jitter == 0) { 618 | cmdList.push(makeCmdString('L', rowStartX + direction*(i+1)*scaleFactor, rowBaseY)); 619 | } else { 620 | var jitterWidth = scaleFactor/jitter; 621 | var dx = jitterWidth/2; 622 | var jhscale = jh*jitter/maxStrokes; // try scaling jitter height with intensity 623 | //var jhscale = jh; 624 | var pixelStartX = rowStartX + direction*i*scaleFactor; 625 | for (var k = 0; k < jitter; k++) { 626 | cmdList.push(makeCmdString('L', pixelStartX + k*jitterWidth*direction, rowBaseY + jhscale)); 627 | cmdList.push(makeCmdString('L', pixelStartX + (k*jitterWidth +dx)*direction, rowBaseY + jhscale)); 628 | cmdList.push(makeCmdString('L', pixelStartX + (k*jitterWidth+dx)*direction, rowBaseY)); 629 | cmdList.push(makeCmdString('L', pixelStartX + (k*jitterWidth + 2*dx)*direction, rowBaseY)) 630 | } 631 | } 632 | } 633 | } 634 | // When done, send pen back to origin 635 | cmdList.push(makeCmdString('L', display.origin[0], starty + scaleFactor*h)); 636 | cmdList.push(makeCmdString('L', display.origin[0], display.origin[1])); 637 | cmdList.push('O HV'); // Command to turn off motors once we've arrived there 638 | } 639 | 640 | 641 | // Converts the image to Grayscale, downsizes resolution, and obtains drawing commands 642 | var imgToCmdList = function(img, display, cmdList) { 643 | 644 | 645 | // Rescale the image - max resolution = display.pixels 646 | var newHeight, newWidth; 647 | 648 | // Figure out new image dimensions 649 | if (img.naturalWidth > img.naturalHeight) { 650 | newWidth = display.maxPixels; 651 | newHeight = (img.naturalHeight/img.naturalWidth)*display.maxPixels; 652 | } else { 653 | newHeight = display.maxPixels; 654 | newWidth = (img.naturalWidth/img.naturalHeight)*display.maxPixels; 655 | } 656 | // Resize, then grayscale the image 657 | img.src = resizeImage(img, newWidth, newHeight); 658 | img.src = makeGrayscaleImage(img); 659 | 660 | // Display a pixelated version of the image 661 | 662 | // Scale to fit inside the div box - first get div box dimensions 663 | var disp = document.getElementById('imageDisplay'); 664 | var style = getComputedStyle(disp); 665 | var hDiv = parseInt(style.getPropertyValue("height")); 666 | var wDiv = parseInt(style.getPropertyValue("width")); 667 | 668 | // Get drawing context of the canvas 669 | disp.innerHTML = ""; 670 | var canvas = document.createElement('canvas'); 671 | canvas.width = wDiv; 672 | canvas.height = hDiv; 673 | disp.appendChild(canvas); 674 | var ctx = canvas.getContext("2d"); 675 | 676 | // Let user know they might wait while converting image to commands 677 | // TBD - this doesn't work - figure out why 678 | ctx.font = "40px Arial"; 679 | ctx.fillStyle = "red"; 680 | ctx.textAlign = "center"; 681 | ctx.fillText("Please wait. Converting image...", canvas.width/2, canvas.height/2); 682 | 683 | // Convert image to list of commands 684 | imgToCmds(img, display, cmdList); 685 | 686 | // Draw pixelated grayscale image 687 | // Use a context to scale it up without interpolation 688 | ctx.imageSmoothingEnabled = false; 689 | ctx.webkitImageSmoothingEnabled = false; 690 | ctx.mozImageSmoothingEnabled = false; 691 | 692 | ctx.clearRect(0, 0, canvas.width, canvas.height); 693 | ctx.drawImage(img, (wDiv - img.width)/2 , (hDiv - img.height)/2, img.width, img.height); 694 | 695 | // return 696 | return cmdList.length; 697 | } 698 | 699 | // Convert an image to a list of drawing commands 700 | var convertToData = function(img, points, cmdList, display) { 701 | return function(evt) { 702 | var nPts = 0; 703 | 704 | // Warn user about wait - this might take a while 705 | if (getDisplayMode() == "draw" && points.nPoints() > 1) { 706 | nPts = pointsToCmdList(points, display, cmdList); 707 | } else if (isDisplayingSVG(display)) { 708 | nPts = svgToCmdList(document.getElementsByTagName('svg')[0], display, cmdList); 709 | } else if (img.src != "" && img.complete) { 710 | nPts = imgToCmdList(img, display, cmdList); 711 | } else { 712 | alert("No drawing in display area"); 713 | return; 714 | } 715 | 716 | // Set the value of the textarea to the drawing instructions 717 | var cmdString = ""; 718 | for (var i = 0; i < cmdList.length; i++) { 719 | cmdString += (cmdList[i] + '\n'); 720 | } 721 | document.getElementById('sentText').value = "Commands to Send\n" + cmdString; 722 | if (nPts > 0) { 723 | document.getElementById('sendData').disabled = false; 724 | } 725 | } 726 | } 727 | 728 | // Send list of commands to the browser 729 | var sendDrawingData = function(socket, cmdList, canvasPoints) { 730 | return function(evt) { 731 | var cmdString = ""; 732 | for (var i = 0; i < cmdList.length; i++) { 733 | cmdString += (cmdList[i] + '\n'); 734 | } 735 | socket.emit('cmds', cmdString); 736 | 737 | // Reset cmdList once sent 738 | cmdList.length = 0; 739 | // Reset the list of points if any 740 | canvasPoints.clearPoints(); 741 | // Deactivate the send button 742 | document.getElementById('sendData').disabled = true; 743 | // Clear out any old received commands 744 | document.getElementById('receivedText').value = "Received Response"; 745 | } 746 | } 747 | 748 | // Main Function - will run when the page loads 749 | window.onload = function () { 750 | 751 | // Create object canvasPoints to hold list of points, with member 752 | // functions to draw them into the canvas context 753 | var canvasPoints = { 754 | clickX : [0], 755 | clickY : [0], 756 | paint : false, 757 | drawPoints : function(cvs) { 758 | //cvs = document.getElementById('theCanvas'); 759 | var ctx = cvs.getContext("2d"); 760 | ctx.clearRect(0, 0, cvs.width, cvs.height); 761 | ctx.strokeStyle = 'black'; 762 | ctx.beginPath(); 763 | ctx.moveTo(this.clickX[0], this.clickY[0]); 764 | for (var i = 0; i < this.clickX.length; i++) { 765 | ctx.lineTo(this.clickX[i], this.clickY[i]); 766 | ctx.moveTo(this.clickX[i], this.clickY[i]); 767 | } 768 | ctx.stroke(); 769 | ctx.closePath(); 770 | }, 771 | // Only add new point if it differs from the last one 772 | addPoint : function(x, y) { if (x != this.clickX[this.nPoints()-1] || y != this.clickY[this.nPoints()-1]) { 773 | this.clickX.push(x); 774 | this.clickY.push(y); 775 | } 776 | }, 777 | clearPoints : function() { this.clickX = [0]; this.clickY = [0]; this.paint = false; }, 778 | nPoints : function() { return this.clickX.length; }, 779 | popPoint : function() { return [this.clickX.pop(), this.clickY.pop()];}, 780 | reset : function() { this.clickX = [0]; this.clickY = [0]; }, 781 | setPaint : function(p) { this.paint = p; } 782 | }; 783 | 784 | // Register some listeners for the canvas and the erase button 785 | var canvas = document.getElementById('theCanvas'); 786 | canvas.addEventListener('click', onCanvasMouseClick(canvasPoints)); 787 | canvas.addEventListener('mousedown', onCanvasMouseDown(canvasPoints)); 788 | canvas.addEventListener('mouseup', onCanvasMouseUp(canvasPoints)); 789 | canvas.addEventListener('mousemove', onCanvasMouseMove(canvasPoints)); 790 | canvas.addEventListener('mouseleave', onCanvasMouseLeave(canvasPoints)); 791 | document.getElementById('eraseLast').addEventListener('click', onClickEraseLast(canvas, canvasPoints)); 792 | document.getElementById('erase').addEventListener('click', onClickErase(canvas, canvasPoints)); 793 | 794 | // Start in drawing mode 795 | setDrawMode(); 796 | 797 | // Register event listener for the select element 798 | document.getElementById('drawMode').addEventListener('change', switchModes); 799 | 800 | var img = new Image(); 801 | var cmdList = []; // List of drawing commands to send to plotter 802 | var display = { // Display parameters 803 | type:"Etch-a-Sketch", 804 | origin:[0,0], // Upper left 805 | size:[6000,4000], // Actual size in steps with 2048 steps/rev 806 | maxPixels:48 // Maximum pixels resolution on a side of the image 807 | }; 808 | display.center = [(display.origin[0] + display.size[0])/2, (display.origin[1] + display.size[1])/2]; 809 | 810 | // Create socket. User server's local ip address if connecting from remote machine, 811 | // otherwise use localhost if connecting from this machine. 812 | //var socket = io.connect('http://192.168.1.22:8000') 813 | var socket = io.connect('http://localhost:8000'); 814 | // Set socket listener 815 | socket.on('notification', onSocketNotification(display)); 816 | socket.on('connect', function(s) { 817 | this.emit('cmds', "D"); // Request drawing dimensions 818 | this.emit('cmds', "b"); // Request backlash values 819 | this.emit('cmds', "s"); // Request size 820 | }); 821 | 822 | 823 | // Event handler for file select element 824 | document.getElementById('files').addEventListener('change', onFileSelect(img)); 825 | document.getElementById('imToData').addEventListener('click', convertToData(img, canvasPoints, cmdList, display)); 826 | document.getElementById('sendData').addEventListener('click', sendDrawingData(socket, cmdList, canvasPoints)); 827 | document.getElementById('setBacklash').addEventListener('click', onClickSetBacklash(socket));} 828 | -------------------------------------------------------------------------------- /nodefiles/client/pencil.cur: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekmomprojects/EtchABot/9870f4fb5c0cb0173605493a97f10e4406e8544f/nodefiles/client/pencil.cur -------------------------------------------------------------------------------- /nodefiles/client/pencil.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekmomprojects/EtchABot/9870f4fb5c0cb0173605493a97f10e4406e8544f/nodefiles/client/pencil.png -------------------------------------------------------------------------------- /nodefiles/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "etchabot", 3 | "version": "1.0.0", 4 | "description": "EtchABot image drawing application", 5 | "main": "server/server.js", 6 | "scripts": { 7 | "start":"node ./server/server.js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/geekmomprojects/EtchABot.git" 13 | }, 14 | "author": "GeekMomProjects ", 15 | "license": "ISC", 16 | "bugs": { 17 | "url": "https://github.com/geekmomprojects/EtchABot/issues" 18 | }, 19 | "homepage": "https://github.com/geekmomprojects/EtchABot#readme", 20 | "dependencies": { 21 | "serialport": "^2.0.5", 22 | "socket.io": "~1.3.7" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /nodefiles/server/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekmomprojects/EtchABot/9870f4fb5c0cb0173605493a97f10e4406e8544f/nodefiles/server/favicon.ico -------------------------------------------------------------------------------- /nodefiles/server/server.js: -------------------------------------------------------------------------------- 1 | var app = require('http').createServer(handler), 2 | io = require('socket.io').listen(app), 3 | fs = require('fs'), 4 | url = require('url'), 5 | rl = require('readline'), 6 | path = require('path'), 7 | serialport = require("serialport"), 8 | // Cache of commands to send to arduino 9 | cmdList = []; 10 | 11 | 12 | // User should have specified portname as an argument 13 | var portName = null; 14 | var useSerialPort = false; 15 | if (process.argv.length > 2) { 16 | portName = process.argv[2]; 17 | useSerialPort = true; 18 | } else { 19 | console.log("Please provide serial port name as argument. Starting without port access."); 20 | //process.exit(1); 21 | } 22 | 23 | // localize object constructor for serial port 24 | var SerialPort = serialport.SerialPort; 25 | if (useSerialPort) { 26 | var sp = new SerialPort(portName, { 27 | baudRate: 57600, 28 | parser: serialport.parsers.readline("\r\n") 29 | }, false); // Don't open serial port immediately so we can check for errors when we do 30 | 31 | sp.open(function(err) { 32 | if (err) { 33 | console.log('Serial port ' + portName +' failed to open: ' + err); 34 | //process.exit(1); 35 | } else { 36 | console.log('Successfully opened serial port ' + portName); 37 | // Register event handlers 38 | sp.on('data', onSpData); 39 | sp.on('error', function(err) { 40 | console.error('Serial port error', err); 41 | }); 42 | sp.on('close', function(err) { 43 | console.log('Port closed!'); 44 | }); 45 | } 46 | }); 47 | } 48 | var arduinoMessage = ''; // Contains the message string dispatched by Arduino 49 | var STATUS_WAITING = 1; 50 | var STATUS_NOT_WAITING = 0; 51 | var status = STATUS_NOT_WAITING; // Whether or not the Arduino is ready to receive commands 52 | var browserSocket = null; // Will initialize this variable once connected to a browser 53 | 54 | /** 55 | * helper function to load any app file required by client.html 56 | * @param { String } pathname: path of the file requested to the nodejs server 57 | * @param { Object } res: http://nodejs.org/api/http.html#http_class_http_serverresponse 58 | */ 59 | var readFile = function(pathname, res) { 60 | // an empty path returns client.html 61 | if (pathname === '/') 62 | pathname = 'client.html'; 63 | 64 | // right now there is only the client.html file, but we could serve others in the future 65 | fs.readFile('.' + path.sep + 'client' + path.sep + pathname, function(err, data) { 66 | if (err) { 67 | console.log(err); 68 | res.writeHead(500); 69 | return res.end('Error loading client.html'); 70 | } 71 | res.writeHead(200); 72 | res.end(data); 73 | }); 74 | }; 75 | 76 | /** 77 | * 78 | * This function is used as proxy to print the arduino messages into the nodejs console and on the page 79 | * @param { Buffer } buffer: buffer data sent via serialport 80 | * @param { Object } socket: it's the socket.io instance managing the connections with the client.html page 81 | * 82 | */ 83 | var sendMessage = function(buffer, socket) { 84 | 85 | // send the message to the client 86 | socket.volatile.emit('notification', arduinoMessage); 87 | 88 | }; 89 | 90 | /** 91 | * 92 | * This function receives the list of drawing commands from the client and adds them to the current list 93 | * if there is one. 94 | * 95 | * @param { List of String } cmds: list of commands to send to the arduino, in newline separated form 96 | * 97 | */ 98 | var handleCmdList = function(cmds) { 99 | 100 | //console.log("in handle cmd List"); 101 | if (cmdList.length) { 102 | cmdList.concat(cmds.split('\n')); 103 | } else { 104 | cmdList = cmds.split('\n'); 105 | } 106 | //console.log("holding " + cmdList.length + " commands "); 107 | if (status == STATUS_WAITING) { 108 | sendNextCommand(); 109 | } else { 110 | // TBD - replace this with something more elegant - for now, check 111 | // to see if the command queue is empty 112 | //sp.write("S;"); 113 | } 114 | }; 115 | 116 | function sendNextCommand() { 117 | //console.log("in send next command"); 118 | if (cmdList.length) { 119 | var cmd = cmdList.shift(); 120 | if (cmd.length) { 121 | //console.log("Sending command: " + cmd); 122 | sp.write(cmd + ";"); 123 | status = STATUS_NOT_WAITING; 124 | } 125 | } else { 126 | // TBD - add a check so that we don't hang if there are no commands in the queue 127 | console.log("No commands in queue to send"); 128 | } 129 | } 130 | 131 | // Creating a new websocket - we need to maintain the command list, and dispatch commands to the Arduino 132 | // when there is demand. Data comes from Arduino - commands come from client. 133 | io.sockets.on('connection', function(socket) { 134 | console.log('Socket connection'); 135 | // Store the connection to the most recent browser to connect. 136 | // Right now I think this means we can only connect to one browser at a time 137 | browserSocket = socket; 138 | // listen to all the messages coming from the client.html page 139 | socket.on('cmds', handleCmdList); 140 | // register handlers 141 | socket.on('disconnect', function() { 142 | console.log("Socket disconnected"); 143 | }); 144 | }); 145 | 146 | // Handles data from the serial port 147 | var onSpData = function(data) { 148 | // TBD - Parse data from Arduino, see if it is request for commands 149 | arduinoMessage += data.toString(); 150 | if (arduinoMessage[0] == '#') { // Informational message from arduino 151 | console.log("Arduino message: " + arduinoMessage); 152 | // Only send message to browser if we are connected 153 | if (browserSocket && (browserSocket.connected)) { 154 | sendMessage(arduinoMessage, browserSocket); 155 | } 156 | } else if (arduinoMessage == "OK") { 157 | //console.log("Arduino OK"); 158 | status = STATUS_WAITING; 159 | sendNextCommand(); 160 | } else { 161 | console.log("Non-matching Arduino message->" + arduinoMessage + "<-"); 162 | } 163 | arduinoMessage = ''; 164 | } 165 | 166 | 167 | // Time to run! 168 | // creating the server ( localhost:8000 if client is on server, or 'server-ip':8000 on remote client ) 169 | app.listen(8000); 170 | console.log("Server started on port 8000"); 171 | // server handler 172 | function handler(req, res) { 173 | // Serve favicon file when requested 174 | if (req.url === '/favicon.ico') { 175 | res.writeHead(200, {'Content-Type': 'image/x-icon'} ); 176 | var img = fs.readFileSync('.' + path.sep + 'server' + path.sep + 'favicon.ico'); 177 | res.end(img, 'binary'); 178 | //console.log('favicon requested'); 179 | return; 180 | } 181 | // Serve the requested page 182 | readFile(url.parse(req.url).pathname, res); 183 | } --------------------------------------------------------------------------------