├── .gitignore ├── README.md ├── ServoSimulator ├── Platform.pde ├── ServoSimulator.pde ├── anims │ └── .gitignore ├── corrected-anims │ └── .gitignore └── format_anim_json.js ├── electron ├── .gitignore ├── app │ ├── config │ │ ├── common.res │ │ ├── config-dev.js │ │ └── dialogflow-agent.zip │ ├── css │ │ └── style.css │ ├── index.html │ ├── js │ │ ├── actions │ │ │ └── actions.js │ │ ├── events │ │ │ ├── events.js │ │ │ └── listeners.js │ │ ├── face │ │ │ ├── eyes.js │ │ │ └── glasses.js │ │ ├── global.js │ │ ├── helpers │ │ │ ├── common.js │ │ │ └── media.js │ │ ├── intent-engines │ │ │ ├── dialogflow-intents.js │ │ │ └── dialogflow.js │ │ ├── lib │ │ │ └── dotstar.js │ │ ├── power │ │ │ └── power.js │ │ ├── responses │ │ │ └── responses.js │ │ ├── senses │ │ │ ├── buttons.js │ │ │ ├── camera.js │ │ │ ├── leds.js │ │ │ ├── listen.js │ │ │ ├── mic.js │ │ │ ├── servo.js │ │ │ ├── speak.js │ │ │ └── text.js │ │ └── skills │ │ │ ├── timer.js │ │ │ └── weather.js │ └── media │ │ ├── imgs │ │ └── glasses │ │ │ ├── glass-circle.png │ │ │ ├── glass-pointy.png │ │ │ ├── glass-rayban.png │ │ │ ├── glass-rectangle.png │ │ │ ├── glass-regular.png │ │ │ ├── glass-square.png │ │ │ └── glass-star.png │ │ ├── responses │ │ ├── alarm │ │ │ └── alarm.mp4 │ │ ├── bye │ │ │ ├── byebye.mp4 │ │ │ └── take-care.mp4 │ │ ├── confused │ │ │ ├── dont-know.jpg │ │ │ ├── no-idea.png │ │ │ ├── shrug.gif │ │ │ ├── shrug.mp4 │ │ │ └── who-knows.webp │ │ └── ok │ │ │ └── ok.mp4 │ │ ├── servo_anims │ │ ├── alert.json │ │ ├── jiggle.json │ │ ├── look-up-slow.json │ │ └── look-up.json │ │ └── sounds │ │ └── alert.wav ├── main.js └── package.json ├── python └── zero.py └── scripts └── launch.sh /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Discord 2 | 3 | Join us on [discord](http://bit.ly/2HLtxez) 4 | 5 | # Assembly Instructions 6 | 7 | Full assembly instructions can be found in the wiki [here](https://github.com/shekit/peeqo/wiki/Assembly) 8 | 9 | # Setup your Dev Machine 10 | 11 | Instructions to get the application running on your dev machine for development can be found [here](https://github.com/shekit/peeqo/wiki/setting-up-dev-machine) 12 | 13 | # Creating Custom Commands 14 | 15 | Create your own commands (even if you don't have a Peeqo) by following the tutorial [here](https://github.com/shekit/peeqo/wiki/creating-a-custom-command) 16 | 17 | # Wiki 18 | 19 | The growing documentation can be found in the wiki [here](https://github.com/shekit/peeqo/wiki) 20 | 21 | # Get a Peeqo Dev Kit 22 | Get on the waitlist for a peeqo dev kit. Enter your email [here](http://bit.ly/2TyocgY) 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /ServoSimulator/Platform.pde: -------------------------------------------------------------------------------- 1 | 2 | 3 | class Platform { 4 | private PVector translation, rotation, initialHeight; 5 | private PVector[] baseJoint, platformJoint, q, l, A; 6 | private float[] alpha; 7 | private float baseRadius, platformRadius, hornLength, legLength; 8 | private String[] prevAllowableAngleValues = new String [3]; 9 | 10 | private int servoUpperRangeAngle = 180; 11 | private int servoLowerRangeAngle = 0; 12 | 13 | private int servoUpperRangeMicros = 2560; 14 | private int servoLowerRangeMicros = 500; 15 | 16 | public boolean isRecording = false; 17 | public boolean shouldSave = false; 18 | 19 | private JSONArray animation = new JSONArray(); 20 | private String animationFileName; 21 | 22 | public boolean setFileName = false; 23 | 24 | int time; 25 | int wait = 33; // every 33 ms or roughly 30fps 26 | // REAL ANGLES 27 | 28 | //new angles new small platform, need to invert the servo horns 29 | private final float baseAngles[] = { 30 | 0, 120, 240 }; // angle positions on base platform 31 | 32 | private final float platformAngles[] = { 33 | 0, 120, 240}; // corresponding angles on top platform 34 | 35 | private final float beta[] = { 36 | 0, 2*PI/3, 4*PI/3}; // angles the servo arms point in. This makes them point outward like our setup 37 | 38 | // REAL MEASUREMENTS 39 | private final float SCALE_INITIAL_HEIGHT = 125; // height of platform above base 40 | private final float SCALE_BASE_RADIUS = 65; // radius of base platform. radius at point at center of servo axis 41 | private final float SCALE_PLATFORM_RADIUS = 85; //radius of top platform. radius at Point where the dof arms connect 42 | private final float SCALE_HORN_LENGTH = 13; // length of servo arm 43 | private final float SCALE_LEG_LENGTH = 125; // length of leg connecting end of servo arm to top of platform 44 | 45 | public Platform(float s) { 46 | translation = new PVector(); 47 | initialHeight = new PVector(0, 0, s*SCALE_INITIAL_HEIGHT); 48 | rotation = new PVector(); 49 | baseJoint = new PVector[3]; 50 | platformJoint = new PVector[3]; 51 | alpha = new float[3]; 52 | q = new PVector[3]; 53 | l = new PVector[3]; 54 | A = new PVector[3]; 55 | baseRadius = s*SCALE_BASE_RADIUS; 56 | platformRadius = s*SCALE_PLATFORM_RADIUS; 57 | hornLength = s*SCALE_HORN_LENGTH; 58 | legLength = s*SCALE_LEG_LENGTH; 59 | 60 | for (int i=0; i<3; i++) { 61 | float mx = baseRadius*cos(radians(baseAngles[i])); 62 | float my = baseRadius*sin(radians(baseAngles[i])); 63 | baseJoint[i] = new PVector(mx, my, 0); 64 | } 65 | 66 | for (int i=0; i<3; i++) { 67 | float mx = platformRadius*cos(radians(platformAngles[i])); 68 | float my = platformRadius*sin(radians(platformAngles[i])); 69 | 70 | platformJoint[i] = new PVector(mx, my, 0); 71 | q[i] = new PVector(0, 0, 0); 72 | l[i] = new PVector(0, 0, 0); 73 | A[i] = new PVector(0, 0, 0); 74 | } 75 | calcQ(); 76 | } 77 | 78 | public void applyTranslationAndRotation(PVector t, PVector r) { 79 | 80 | rotation.set(r); 81 | translation.set(t); 82 | 83 | calcQ(); 84 | calcAlpha(); 85 | 86 | } 87 | 88 | private void calcQ() { 89 | for (int i=0; i<3; i++) { 90 | // rotation 91 | q[i].x = cos(rotation.z)*cos(rotation.y)*platformJoint[i].x + 92 | (-sin(rotation.z)*cos(rotation.x)+cos(rotation.z)*sin(rotation.y)*sin(rotation.x))*platformJoint[i].y + 93 | (sin(rotation.z)*sin(rotation.x)+cos(rotation.z)*sin(rotation.y)*cos(rotation.x))*platformJoint[i].z; 94 | 95 | q[i].y = sin(rotation.z)*cos(rotation.y)*platformJoint[i].x + 96 | (cos(rotation.z)*cos(rotation.x)+sin(rotation.z)*sin(rotation.y)*sin(rotation.x))*platformJoint[i].y + 97 | (-cos(rotation.z)*sin(rotation.x)+sin(rotation.z)*sin(rotation.y)*cos(rotation.x))*platformJoint[i].z; 98 | 99 | q[i].z = -sin(rotation.y)*platformJoint[i].x + 100 | cos(rotation.y)*sin(rotation.x)*platformJoint[i].y + 101 | cos(rotation.y)*cos(rotation.x)*platformJoint[i].z; 102 | 103 | // translation 104 | q[i].add(PVector.add(translation, initialHeight)); 105 | l[i] = PVector.sub(q[i], baseJoint[i]); 106 | } 107 | } 108 | 109 | private void calcAlpha() { 110 | for (int i=0; i<3; i++) { 111 | float L = l[i].magSq()-(legLength*legLength)+(hornLength*hornLength); 112 | float M = 2*hornLength*(q[i].z-baseJoint[i].z); 113 | float N = 2*hornLength*(cos(beta[i])*(q[i].x-baseJoint[i].x) + sin(beta[i])*(q[i].y-baseJoint[i].y)); 114 | alpha[i] = asin(L/sqrt(M*M+N*N)) - atan2(N, M); 115 | 116 | A[i].set(hornLength*cos(alpha[i])*cos(beta[i]) + baseJoint[i].x, 117 | hornLength*cos(alpha[i])*sin(beta[i]) + baseJoint[i].y, 118 | hornLength*sin(alpha[i]) + baseJoint[i].z); 119 | 120 | //float xqxb = (q[i].x-baseJoint[i].x); 121 | //float yqyb = (q[i].y-baseJoint[i].y); 122 | //float h0 = sqrt((legLength*legLength)+(hornLength*hornLength)-(xqxb*xqxb)-(yqyb*yqyb)) - q[i].z; 123 | 124 | //float L0 = 2*hornLength*hornLength; 125 | //float M0 = 2*hornLength*(h0+q[i].z); 126 | //float a0 = asin(L0/sqrt(M0*M0+N*N)) - atan2(N, M0); 127 | 128 | //println(i+":"+alpha[i]+" h0:"+h0+" a0:"+a0); 129 | } 130 | } 131 | 132 | public float[] getAlpha(){ 133 | return alpha; 134 | } 135 | 136 | public void draw() { 137 | // draw Base 138 | noStroke(); 139 | fill(128); 140 | ellipse(0, 0, 2*baseRadius, 2*baseRadius); 141 | for (int i=0; i<3; i++) { 142 | pushMatrix(); 143 | translate(baseJoint[i].x, baseJoint[i].y, baseJoint[i].z); 144 | noStroke(); 145 | fill(0); 146 | ellipse(0, 0, 5, 5); //ellipse at joint of red servo arm to circular base 147 | textSize(32); 148 | text(str(i), 15, 65 ,15); 149 | textSize(16); 150 | text(String.format("%.2f", degrees(alpha[i])), 5,5,5); 151 | popMatrix(); 152 | 153 | stroke(245,0,0); // draw servo arms 154 | line(baseJoint[i].x, baseJoint[i].y, baseJoint[i].z, A[i].x, A[i].y, A[i].z); 155 | 156 | PVector rod = PVector.sub(q[i], A[i]); 157 | rod.setMag(legLength); 158 | rod.add(A[i]); 159 | 160 | //draw leg attachments 161 | stroke(0,100,0); 162 | strokeWeight(3); 163 | line(A[i].x, A[i].y, A[i].z, rod.x, rod.y, rod.z); 164 | } 165 | 166 | // draw phone jointss and rods 167 | for (int i=0; i<3; i++) { 168 | pushMatrix(); 169 | translate(q[i].x, q[i].y, q[i].z); 170 | noStroke(); 171 | fill(0); 172 | ellipse(0, 0, 5, 5); 173 | popMatrix(); 174 | 175 | stroke(0,0,254); 176 | strokeWeight(1); 177 | // draw vertical debug line 178 | line(baseJoint[i].x, baseJoint[i].y, baseJoint[i].z, q[i].x, q[i].y, q[i].z); 179 | } 180 | 181 | // sanity check 182 | pushMatrix(); 183 | translate(initialHeight.x, initialHeight.y, initialHeight.z); 184 | translate(translation.x, translation.y, translation.z); 185 | rotateZ(rotation.z); 186 | rotateY(rotation.y); 187 | rotateX(rotation.x); 188 | stroke(245); 189 | noFill(); 190 | ellipse(0, 0, 2*platformRadius, 2*platformRadius); 191 | popMatrix(); 192 | 193 | String [] str = new String[3]; 194 | 195 | for(int i=0;i<3;i++){ 196 | if(Float.isNaN(alpha[i])){ 197 | // if any value is NaN, then send previous non NaN Angle Values 198 | sendSerial(prevAllowableAngleValues); 199 | return; 200 | } 201 | 202 | if(degrees(alpha[i]) <=0){ 203 | // if negative make positive add to 90 204 | int mappedToMicro = int(map(int(90 + abs(degrees(alpha[i]))),servoLowerRangeAngle,servoUpperRangeAngle,servoLowerRangeMicros,servoUpperRangeMicros)); 205 | str[i] = str(mappedToMicro); 206 | } else if(degrees(alpha[i]) > 0){ 207 | // if positive, then subtract from 90 208 | int mappedToMicro = int(map(int(90 - degrees(alpha[i])),servoLowerRangeAngle,servoUpperRangeAngle,servoLowerRangeMicros,servoUpperRangeMicros)); 209 | str[i] = str(mappedToMicro); 210 | } 211 | } 212 | 213 | sendSerial(str); 214 | } 215 | 216 | public void saveAnimationDetails(String fileName){ 217 | println("Ready to save: "+fileName); 218 | animation = new JSONArray(); 219 | setFileName = true; 220 | animationFileName = fileName; 221 | } 222 | 223 | public void saveAnimationToFile(){ 224 | String date = str(day())+"-"+str(hour())+"-"+str(minute())+"-"+str(second()); 225 | saveJSONArray(animation, "anims/"+animationFileName+"-"+date+".json"); 226 | setFileName = false; 227 | } 228 | 229 | void sendSerial(String[] str){ 230 | 231 | if(str.length != 3){ 232 | return; 233 | } 234 | 235 | JSONArray angles = new JSONArray(); 236 | 237 | if(millis() - time >= wait){ 238 | 239 | for(int i=0;i<3;i++){ 240 | if(str[i]!=null || str[i]!=""){ 241 | if(port != null){ 242 | port.write(str[i] + '@'); 243 | delay(1); 244 | } 245 | 246 | if(isRecording){ 247 | angles.setInt(i, int(str[i])); 248 | } 249 | } 250 | } 251 | 252 | time = millis(); 253 | 254 | if(isRecording){ 255 | animation.append(angles); 256 | } 257 | 258 | prevAllowableAngleValues = str; 259 | } 260 | } 261 | 262 | } 263 | -------------------------------------------------------------------------------- /ServoSimulator/ServoSimulator.pde: -------------------------------------------------------------------------------- 1 | import peasy.*; //<>// 2 | import controlP5.*; 3 | 4 | import processing.serial.*; 5 | 6 | Serial port; 7 | 8 | float MAX_TRANSLATION = 50; 9 | float MAX_ROTATION = PI/2; 10 | 11 | ControlP5 cp5; 12 | PeasyCam camera; 13 | 14 | Platform mPlatform; 15 | 16 | Button recordBtn; 17 | Button saveBtn; 18 | 19 | Textfield animFileName; 20 | Textlabel recordingText; 21 | Textlabel savedFileName; 22 | 23 | // reference to slider elements on screen 24 | Slider posZSlider; 25 | Slider rotXSlider; 26 | Slider rotYSlider; 27 | Slider2D rotXYSlider; 28 | 29 | // named references to values of 1D sliders 30 | float posX=0, posY=0, posZ=0, rotX=0, rotY=0, rotZ=0; 31 | 32 | float rotXMain = rotX; 33 | float rotYMain = rotY; 34 | float posZMain = posZ; 35 | 36 | float prevRotX = rotX; 37 | float prevRotY = rotY; 38 | float prevPosZ = posZ; 39 | 40 | //float camRotX=-1.0, camRotY=0.0, camRotZ=0.0; 41 | float camRotX=-0.685, camRotY=1.0, camRotZ=-0.88; 42 | 43 | boolean ctlPressed = false; 44 | 45 | CallbackListener rotXcb; 46 | CallbackListener rotYcb; 47 | 48 | float currentXMax=1; 49 | float currentYMax=1; 50 | float currentXMin=-1; 51 | float currentYMin=-1; 52 | float currentZMax = 1; 53 | float currentZMin = -1; 54 | 55 | boolean hitXLimit = false; 56 | boolean hitYLimit = false; 57 | boolean hitZLimit = false; 58 | 59 | float previousAngles[]; 60 | 61 | void setup() { 62 | size(1024, 768, P3D); 63 | smooth(); 64 | frameRate(60); 65 | textSize(20); 66 | 67 | String portName = "/dev/cu.usbmodem1421"; 68 | //port = new Serial(this, portName, 115200); 69 | 70 | camera = new PeasyCam(this, 333); 71 | camera.setRotations(camRotX, camRotY, camRotZ); 72 | camera.lookAt(8.0, -50.0, 80.0); 73 | 74 | // create platform and set to default start position 75 | mPlatform = new Platform(1); //pass scale of 1 76 | mPlatform.applyTranslationAndRotation(new PVector(), new PVector()); 77 | 78 | //create an instance of control p5 79 | cp5 = new ControlP5(this); 80 | 81 | 82 | //recordBtn = cp5.addButton("Not Recording - Hit 'r' to start/stop") 83 | // .setPosition(20,20) 84 | // .setSize(200,100); 85 | 86 | recordingText = cp5.addTextlabel("rec") 87 | .setText("Not Recording") 88 | .setPosition(20,20) 89 | .setColorValue(0xffffff00) 90 | .setFont(createFont("arial", 30)); 91 | 92 | cp5.addTextlabel("tip") 93 | .setText("Press '~' key to start/stop recording") 94 | .setPosition(20,60) 95 | .setFont(createFont("arial", 16)); 96 | 97 | savedFileName = cp5.addTextlabel("No File Name Entered") 98 | .setPosition(20,120) 99 | .setFont(createFont("arial", 18)); 100 | 101 | animFileName = cp5.addTextfield("animation Name") 102 | .setPosition(20,150) 103 | .setFocus(true) 104 | .setAutoClear(false); 105 | 106 | saveBtn = cp5.addButton("Set File Name") 107 | .setPosition(20,200) 108 | .setSize(200,20); 109 | 110 | //cp5.addSlider("posX") 111 | // .setPosition(20, 20) 112 | // .setSize(180, 40).setRange(-1, 1); 113 | //cp5.addSlider("posY") 114 | // .setPosition(20, 70) 115 | // .setSize(180, 40).setRange(-1, 1); 116 | posZSlider = cp5.addSlider("posZ") 117 | .setPosition(width-250, 20) 118 | .setSize(180, 40).setRange(-1, 1); 119 | 120 | rotXSlider = cp5.addSlider("rotX") 121 | .setPosition(width-250, 70) 122 | .setSize(180, 40).setRange(-1, 1); 123 | rotYSlider = cp5.addSlider("rotY") 124 | .setPosition(width-250, 120) 125 | .setSize(180, 40).setRange(-1, 1); 126 | //cp5.addSlider("rotZ") 127 | // .setPosition(width-200, 120) 128 | // .setSize(180, 40).setRange(-1, 1); 129 | rotXYSlider = cp5.addSlider2D("x+y") 130 | .setPosition(width-250,170) 131 | .setSize(200,200) 132 | .setMinMax(-1,-1,1,1) 133 | .setValue(0,0); 134 | 135 | rotXcb = new CallbackListener(){ 136 | public void controlEvent(CallbackEvent theEvent){ 137 | switch(theEvent.getAction()){ 138 | case(ControlP5.ACTION_RELEASED): 139 | println("Released X slider"); 140 | rotXYSlider.setValue(rotXMain, rotYMain); 141 | break; 142 | case(ControlP5.ACTION_RELEASEDOUTSIDE): 143 | println("Released X Slider outside"); 144 | rotXYSlider.setValue(rotXMain, rotYMain); 145 | break; 146 | } 147 | } 148 | }; 149 | 150 | rotYcb = new CallbackListener(){ 151 | public void controlEvent(CallbackEvent theEvent){ 152 | switch(theEvent.getAction()){ 153 | case(ControlP5.ACTION_RELEASED): 154 | println("Released Y slider"); 155 | rotXYSlider.setValue(rotXMain, rotYMain); 156 | break; 157 | case(ControlP5.ACTION_RELEASEDOUTSIDE): 158 | println("Released Y Slider outside"); 159 | rotXYSlider.setValue(rotXMain, rotYMain); 160 | break; 161 | } 162 | } 163 | }; 164 | 165 | rotXSlider.addCallback(rotXcb); 166 | rotYSlider.addCallback(rotYcb); 167 | 168 | rotXSlider.onDrag(rotXcb); 169 | 170 | cp5.setAutoDraw(false); 171 | camera.setActive(true); 172 | } 173 | 174 | void draw() { 175 | background(200); 176 | //if(!hitMax){ 177 | mPlatform.applyTranslationAndRotation(PVector.mult(new PVector(posX, posY, posZMain), MAX_TRANSLATION), 178 | PVector.mult(new PVector(rotXMain, rotYMain, rotZ), MAX_ROTATION)); 179 | //} else { 180 | // //println("MAMAMA"); 181 | // mPlatform.applyTranslationAndRotation(PVector.mult(new PVector(posX, posY, posZ), MAX_TRANSLATION), 182 | // PVector.mult(new PVector(prevRotX, rotYMain, rotZ), MAX_ROTATION)); 183 | //} 184 | mPlatform.draw(); 185 | 186 | hint(DISABLE_DEPTH_TEST); 187 | camera.beginHUD(); 188 | cp5.draw(); 189 | camera.endHUD(); 190 | hint(ENABLE_DEPTH_TEST); 191 | 192 | } 193 | 194 | void checkXLimits(){ 195 | 196 | float[] angles = mPlatform.getAlpha(); 197 | 198 | boolean hasHitLimit = false; 199 | 200 | for (float f : angles) { 201 | if (Float.isNaN(f)) { 202 | hasHitLimit = true; 203 | break; 204 | } 205 | 206 | } 207 | 208 | if(hasHitLimit){ 209 | hitXLimit = true; 210 | 211 | // if value is positive and we have received a NaN value 212 | // set the previous nonNan giving value as the acceptable max limit 213 | if(rotXSlider.getValue()>0){ 214 | currentXMax = prevRotX; 215 | 216 | } 217 | 218 | // if value is negative and we have received a NaN value 219 | // set the previous nonNan giving value as the acceptable min limit 220 | if(rotXSlider.getValue()<0){ 221 | currentXMin = prevRotX; 222 | } 223 | 224 | } else { 225 | hitXLimit = false; 226 | currentXMax = 1; 227 | currentXMin = -1; 228 | } 229 | } 230 | 231 | void checkYLimits(){ 232 | 233 | float[] angles = mPlatform.getAlpha(); 234 | 235 | boolean hasHitLimit = false; 236 | 237 | for (float f : angles) { 238 | if (Float.isNaN(f)) { 239 | hasHitLimit = true; 240 | break; 241 | } 242 | } 243 | 244 | if(hasHitLimit){ 245 | hitYLimit = true; 246 | 247 | // if value is positive and we have received a NaN value 248 | // set the previous nonNan giving value as the acceptable max limit 249 | if(rotYSlider.getValue()>0){ 250 | currentYMax = prevRotY; 251 | } 252 | 253 | // if value is negative and we have received a NaN value 254 | // set the previous nonNan giving value as the acceptable min limit 255 | if(rotYSlider.getValue()<0){ 256 | currentYMin = prevRotY; 257 | } 258 | 259 | } else { 260 | hitYLimit = false; 261 | currentYMax = 1; 262 | currentYMin = -1; 263 | } 264 | } 265 | 266 | void checkZLimits(){ 267 | float[] angles = mPlatform.getAlpha(); 268 | 269 | boolean hasHitLimit = false; 270 | 271 | for (float f : angles) { 272 | if (Float.isNaN(f)) { 273 | hasHitLimit = true; 274 | break; 275 | } 276 | } 277 | 278 | if(hasHitLimit){ 279 | hitZLimit = true; 280 | 281 | // if value is positive and we have received a NaN value 282 | // set the previous nonNan giving value as the acceptable max limit 283 | if(posZSlider.getValue()>0){ 284 | currentZMax = prevPosZ; 285 | } 286 | 287 | // if value is negative and we have received a NaN value 288 | // set the previous nonNan giving value as the acceptable min limit 289 | if(posZSlider.getValue()<0){ 290 | currentZMin = prevPosZ; 291 | } 292 | 293 | } else { 294 | hitZLimit = false; 295 | currentZMax = 1; 296 | currentZMin = -1; 297 | } 298 | } 299 | 300 | void controlEvent(ControlEvent theEvent) { 301 | camera.setActive(false); 302 | 303 | if(theEvent.isFrom(rotXSlider)){ 304 | // if value is currently within acceptable limits 305 | if(rotX < currentXMax && rotX > currentXMin){ 306 | rotXMain = rotX; 307 | prevRotX = rotXMain; 308 | } 309 | 310 | checkXLimits(); 311 | } 312 | 313 | if(theEvent.isFrom(rotYSlider)){ 314 | if(rotY < currentYMax && rotY > currentYMin){ 315 | rotYMain = rotY; 316 | prevRotY = rotYMain; 317 | } 318 | 319 | checkYLimits(); 320 | } 321 | 322 | if(theEvent.isFrom(posZSlider)){ 323 | if(posZ < currentZMax && posZ > currentZMin){ 324 | posZMain = posZ; 325 | prevPosZ = posZMain; 326 | } 327 | 328 | checkZLimits(); 329 | } 330 | 331 | if(theEvent.isFrom(rotXYSlider)){ 332 | 333 | if(rotXYSlider.getArrayValue()[0] < currentXMax && rotXYSlider.getArrayValue()[0] > currentXMin){ 334 | rotXMain = rotXYSlider.getArrayValue()[0]; 335 | rotXSlider.setValue(rotXMain); 336 | } 337 | 338 | if(rotXYSlider.getArrayValue()[1] < currentYMax && rotXYSlider.getArrayValue()[1] > currentYMin){ 339 | rotYMain = rotXYSlider.getArrayValue()[1]; 340 | rotYSlider.setValue(rotYMain); 341 | } 342 | } 343 | 344 | if(theEvent.isFrom(saveBtn)){ 345 | 346 | if(animFileName.getText().trim().length() > 0){ 347 | println("save file name"); 348 | println(animFileName.getText().trim()); 349 | mPlatform.saveAnimationDetails(animFileName.getText().trim()); 350 | savedFileName.setText(animFileName.getText().trim()+".json"); 351 | animFileName.clear(); 352 | } else { 353 | println("Enter file name"); 354 | } 355 | } 356 | } 357 | void mouseReleased() { 358 | camera.setActive(true); 359 | } 360 | 361 | void keyPressed() { 362 | if (key == ' ') { 363 | resetEverything(); 364 | } else if (key == '`') { 365 | 366 | if(!mPlatform.setFileName){ 367 | println("Enter file name first"); 368 | return; 369 | } 370 | 371 | mPlatform.isRecording = !mPlatform.isRecording; 372 | 373 | if(mPlatform.isRecording){ 374 | //show record btn 375 | recordingText.setText("RECORDING").setColor(0xffff0000); 376 | } else { 377 | // show not recording btn 378 | recordingText.setText("Not Recording").setColor(0xffffff00); 379 | mPlatform.saveAnimationToFile(); 380 | savedFileName.setText("Saved!"); 381 | } 382 | } 383 | } 384 | 385 | void resetEverything(){ 386 | // RESET EVERYTHING 387 | camera.setRotations(camRotX, camRotY, camRotZ); 388 | camera.lookAt(8.0, -50.0, 80.0); 389 | camera.setDistance(333); 390 | posZ=0; 391 | rotX=0; 392 | rotY=0; 393 | posZSlider.setValue(0); 394 | rotXSlider.setValue(0); 395 | rotYSlider.setValue(0); 396 | rotXYSlider.setValue(0, 0); 397 | } 398 | 399 | void keyReleased() { 400 | if (keyCode == CONTROL) { 401 | camera.setActive(true); 402 | ctlPressed = false; 403 | } 404 | } 405 | -------------------------------------------------------------------------------- /ServoSimulator/anims/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shekit/peeqo/825872a41b3db46795d262fa5a986f082e3eafc0/ServoSimulator/anims/.gitignore -------------------------------------------------------------------------------- /ServoSimulator/corrected-anims/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shekit/peeqo/825872a41b3db46795d262fa5a986f082e3eafc0/ServoSimulator/corrected-anims/.gitignore -------------------------------------------------------------------------------- /ServoSimulator/format_anim_json.js: -------------------------------------------------------------------------------- 1 | // inverts servo anims 1 -> 0, 2 -> 1, 0 -> 2 to correct orientation 2 | // pass in anim file name without .json as argument 3 | 4 | 5 | var fs = require('fs') 6 | var path = require('path') 7 | 8 | // var anim = [ 9 | // [0,1,2], 10 | // [3,4,5], 11 | // [6,7,8] 12 | // ] 13 | 14 | anim_file = process.argv[2] 15 | 16 | //var variant = process.argv[3] 17 | 18 | var anim = JSON.parse(fs.readFileSync(path.join(process.cwd(),'anims', `${anim_file}.json` )), 'utf8') 19 | 20 | var new_array =[] 21 | 22 | for (var i=0; i 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Wakeword 10 |
11 | 12 | 13 |
14 | 15 |
16 | 17 |
18 | 19 |
20 | 21 |
22 | 23 |
24 |

25 |
26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /electron/app/js/actions/actions.js: -------------------------------------------------------------------------------- 1 | const event = require('js/events/events') 2 | const common = require('js/helpers/common') 3 | const media = require('js/helpers/media') 4 | const responses = require('js/responses/responses') 5 | 6 | 7 | async function setAnswer(ans=null, overrides={}){ 8 | 9 | // @param {obj} ans - the response object as defined in responses.js 10 | // @param {obj} overrides - new keys to be added or overriden in ans param 11 | console.log("RESPONSE > START") 12 | 13 | // merge overriden values and new values 14 | Object.assign(ans, overrides) 15 | 16 | if(ans.hasOwnProperty('sound') && ans.sound !== null){ 17 | event.emit('play-sound', ans.sound) 18 | } 19 | 20 | let q = await common.setQuery(ans) 21 | console.log(`LOCAL FILE OR SEARCH QUERY > ${q}`) 22 | 23 | let r = null 24 | 25 | if(ans.type == 'remote'){ 26 | r = await media.findRemoteGif(q) 27 | console.log(`MEDIA URL > ${r}`) 28 | } else { 29 | // local response 30 | r = q 31 | } 32 | 33 | let mediaType = await media.findMediaType(r) 34 | let d = await media.findMediaDuration(r) 35 | 36 | console.log(`MEDIA DURATION > ${d}`) 37 | 38 | if(ans.hasOwnProperty('led') && Object.keys(ans.led).length != 0){ 39 | // run led animation 40 | event.emit('led-on', {anim: ans.led.anim , color: ans.led.color }) 41 | } 42 | 43 | if(ans.hasOwnProperty('servo') && ans.servo !== null){ 44 | // move servo 45 | event.emit('servo-move', ans.servo) 46 | } 47 | 48 | if(ans.hasOwnProperty('cbBefore')){ 49 | ans.cbBefore() 50 | } 51 | 52 | let showMedia = common.transitionToMedia(d, mediaType) 53 | 54 | if(ans.hasOwnProperty('text') && ans.text){ 55 | text.showText(ans.text) 56 | } 57 | 58 | if(ans.hasOwnProperty('cbDuring')){ 59 | ans.cbDuring() 60 | } 61 | 62 | let o = await common.transitionFromMedia(d) 63 | 64 | if(ans.hasOwnProperty('text')){ 65 | text.removeText() 66 | } 67 | 68 | console.log(`RESPONSE > END`) 69 | 70 | // callback 71 | if(ans.hasOwnProperty('cbAfter')){ 72 | ans.cbAfter() 73 | } 74 | } 75 | 76 | function wakeword(){ 77 | setAnswer(responses.wakeword, {type:'wakeword'}) 78 | } 79 | 80 | 81 | module.exports = { 82 | wakeword, 83 | setAnswer 84 | } -------------------------------------------------------------------------------- /electron/app/js/events/events.js: -------------------------------------------------------------------------------- 1 | const events = require('events') 2 | const eventEmitter = new events.EventEmitter() 3 | 4 | eventEmitter.setMaxListeners(Infinity) 5 | 6 | module.exports = eventEmitter -------------------------------------------------------------------------------- /electron/app/js/events/listeners.js: -------------------------------------------------------------------------------- 1 | const event = require('js/events/events') 2 | const action = require('js/actions/actions') 3 | const common = require('js/helpers/common') 4 | const power = require('js/power/power') 5 | const speak = require('js/senses/speak') 6 | const dialogflow = require('js/intent-engines/dialogflow') 7 | const dialogflowIntents = require('js/intent-engines/dialogflow-intents') 8 | const mic = require('js/senses/mic') 9 | 10 | module.exports = () => { 11 | 12 | event.on('wakeword', action.wakeword) 13 | 14 | // passes on response object from STT engine 15 | event.on('final-command', dialogflowIntents.parseIntent) 16 | 17 | event.on('no-command', () => { 18 | event.emit("led-on", {anim:'fadeOutError',color:'red'}) 19 | }) 20 | 21 | event.on('speech-to-text', dialogflow.start) 22 | 23 | event.on('end-speech-to-text', () =>{ 24 | 25 | if(process.env.OS == "unsupported"){ 26 | document.getElementById("wakeword").style.backgroundColor = "" 27 | } 28 | 29 | event.emit('pipe-to-wakeword') 30 | 31 | }) 32 | 33 | // passes id of div to show 34 | event.on('show-div', common.showDiv) 35 | 36 | 37 | // POWER CONTROL 38 | event.on('shutdown', power.shutdown) 39 | 40 | event.on('reboot', power.reboot) 41 | 42 | event.on('refresh', power.refresh) 43 | 44 | 45 | // AUDIO PLAYBACK 46 | event.on('play-sound', speak.playSound) 47 | 48 | event.on('set-volume', speak.setVolume) 49 | 50 | // BUTTON PRESSES 51 | event.on('btn-4-short-press',()=>{ 52 | console.log('btn 4 short press') 53 | }) 54 | event.on('btn-4-long-press',()=>{ 55 | console.log('btn 4 long press') 56 | }) 57 | 58 | event.on('btn-16-short-press',()=>{ 59 | console.log('btn 16 short press') 60 | power.refresh() 61 | }) 62 | event.on('btn-16-long-press',()=>{ 63 | console.log('btn 16 long press') 64 | power.shutdown() 65 | }) 66 | 67 | event.on('btn-17-short-press',()=>{ 68 | console.log('btn 17 short press') 69 | }) 70 | event.on('btn-17-long-press',()=>{ 71 | console.log('btn 17 long press') 72 | }) 73 | 74 | event.on('btn-23-short-press',()=>{ 75 | console.log('btn 23 short press') 76 | }) 77 | event.on('btn-23-long-press',()=>{ 78 | console.log('btn 23 long press') 79 | }) 80 | 81 | } -------------------------------------------------------------------------------- /electron/app/js/face/eyes.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const event = require('js/events/events') 4 | const Snap = require('snapsvg') 5 | const snap = Snap("#eyes") 6 | 7 | class Eyes { 8 | 9 | constructor(color="#000000"){ 10 | 11 | this.eyeSize = 87.5 12 | this.closedEye = 1 13 | this.blinkDuration = 120 14 | this.leftEye = snap.ellipse(202.5, 330, this.eyeSize, this.eyeSize) 15 | this.rightEye = snap.ellipse(604.5, 330, this.eyeSize, this.eyeSize) 16 | this.isBlinking = false 17 | this.blinkTimer = null 18 | this.eyes = snap.group(this.leftEye, this.rightEye) 19 | this.blinkIntervals = [4000, 6000, 10000, 1000, 500, 8000] 20 | this.transitionSize = 1000 21 | this.transitionSpeed = 100 22 | 23 | this.eyes.attr({ 24 | fill: color 25 | }) 26 | 27 | this.startBlinking = this.startBlinking.bind(this) 28 | this.stopBlinking = this.stopBlinking.bind(this) 29 | this.transitionToMedia = this.transitionToMedia.bind(this) 30 | this.transitionFromMedia = this.transitionFromMedia.bind(this) 31 | this.blink = this.blink.bind(this) 32 | 33 | event.on('start-blinking', this.startBlinking) 34 | event.on('stop-blinking', this.stopBlinking) 35 | event.on('transition-eyes-away', this.transitionToMedia) 36 | event.on('transition-eyes-back', this.transitionFromMedia) 37 | } 38 | 39 | getRandomBlinkInterval(){ 40 | return this.blinkIntervals[Math.floor(this.blinkIntervals.length * Math.random())] 41 | } 42 | 43 | startBlinking() { 44 | this.isBlinking = true 45 | let duration = this.getRandomBlinkInterval() 46 | this.blinkTimer = setTimeout(this.blink, duration) 47 | } 48 | 49 | transitionFromMedia(){ 50 | 51 | // eye animation when transitioning after displaying media 52 | 53 | this.leftEye.animate({ry:this.eyeSize, rx:this.eyeSize}, this.transitionSpeed, mina.easein()) 54 | this.rightEye.animate({ry:this.eyeSize, rx:this.eyeSize}, this.transitionSpeed, mina.easein(), ()=>{ 55 | console.log("transitioned back") 56 | this.startBlinking() 57 | }) 58 | } 59 | 60 | transitionToMedia(cb){ 61 | 62 | // eye animation when transitioning to display media 63 | 64 | if(this.isBlinking){ 65 | this.stopBlinking() 66 | } 67 | 68 | this.leftEye.animate({ry:this.transitionSize, rx:this.transitionSize}, this.transitionSpeed, mina.elastic()) 69 | this.rightEye.animate({ry:this.transitionSize, rx:this.transitionSize}, this.transitionSpeed, mina.elastic(), ()=>{ 70 | console.log("transitioned away") 71 | cb() 72 | }) 73 | } 74 | 75 | blink() { 76 | 77 | let eyes = ['leftEye','rightEye'] 78 | 79 | for(const i in eyes){ 80 | this[eyes[i]].animate({ry: this.closedEye}, this.blinkDuration, mina.elastic(), () => { 81 | this[eyes[i]].animate({ry:this.eyeSize}, this.blinkDuration, mina.easein()) 82 | }) 83 | } 84 | 85 | clearTimeout(this.blinkTimer) 86 | 87 | this.blinkTimer = null 88 | let duration = this.getRandomBlinkInterval() 89 | this.blinkTimer = setTimeout(this.blink, duration) 90 | 91 | } 92 | 93 | stopBlinking(){ 94 | this.eyes.isBlinking = false 95 | clearTimeout(this.blinkTimer) 96 | this.blinkTimer = null 97 | } 98 | } 99 | 100 | module.exports = Eyes -------------------------------------------------------------------------------- /electron/app/js/face/glasses.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const event = require('js/events/events') 3 | 4 | class Glasses{ 5 | 6 | constructor(){ 7 | this.glasses = document.getElementById("glasses") 8 | this.currentGlass = 0 9 | this.glassList = ["glass-regular.png","glass-pointy.png","glass-square.png","glass-circle.png","glass-rectangle.png","glass-rayban.png"] 10 | 11 | this.changeGlasses = this.changeGlasses.bind(this) 12 | 13 | event.on('change-glasses', this.changeGlasses) 14 | } 15 | 16 | changeGlasses(){ 17 | this.currentGlass++ 18 | 19 | console.log(this) 20 | if(this.currentGlass == this.glassList.length){ 21 | this.currentGlass = 0 22 | } 23 | 24 | let imgPath = path.join(process.cwd(),'app','media','imgs','glasses', this.glassList[this.currentGlass]) 25 | 26 | this.glasses.src = imgPath 27 | } 28 | 29 | 30 | } 31 | 32 | module.exports = Glasses -------------------------------------------------------------------------------- /electron/app/js/global.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | require('app-module-path').addPath(__dirname) 4 | 5 | const event = require('js/events/events') 6 | const mic = require('js/senses/mic') 7 | 8 | let listen = null 9 | 10 | if(process.env.OS !== 'unsupported'){ 11 | // only include snowboy for supported OS 12 | listen = require('js/senses/listen') 13 | } 14 | 15 | const Eyes = require('js/face/eyes') 16 | const Glasses = require('js/face/glasses') 17 | const speak = require('js/senses/speak') 18 | const buttons = require('js/senses/buttons') 19 | const weather = require('js/skills/weather') 20 | 21 | const listeners = require('js/events/listeners')() 22 | 23 | // keyboard shortcuts 24 | const remote = require('electron').remote 25 | 26 | document.addEventListener("keydown", (e)=>{ 27 | if(e.which == 123){ 28 | 29 | // F12 - show js console 30 | remote.getCurrentWindow().toggleDevTools() 31 | 32 | } else if(e.which == 116){ 33 | 34 | // F5 - refresh page 35 | // make sure page is in focus, not console 36 | location.reload() 37 | 38 | } 39 | }) 40 | 41 | // initiate eyes and glasses 42 | const eyes = new Eyes() 43 | event.emit('show-div', 'eyeWrapper') 44 | event.emit('start-blinking') 45 | const glasses = new Glasses() 46 | 47 | 48 | setTimeout(()=>{ 49 | 50 | },3000) 51 | 52 | // initiate buttons 53 | buttons.initializeButtons() 54 | 55 | //initiate leds and run initial animation 56 | const leds = require('js/senses/leds') 57 | event.emit('led-on', {anim: 'circle', color: 'aqua'}) 58 | 59 | // initiate camera 60 | const Camera = require('js/senses/camera') 61 | const camera = new Camera() 62 | 63 | // initiate servos 64 | const Servo = require('js/senses/servo') 65 | const servo = new Servo() 66 | 67 | // initiate text 68 | const text = require('js/senses/text') 69 | 70 | // set audio volume level. 0 - mute; 1-max 71 | event.emit('set-volume',0.4) 72 | 73 | // initiate listening or show wakeword button 74 | if(process.env.OS == 'unsupported'){ 75 | // on certain linux systems and windows snowboy offline keyword detection does not work 76 | // pass in OS=unsupported when starting application to show a clickable wakeword button instead 77 | document.getElementById("wakeword").addEventListener('click', (e) => { 78 | e.preventDefault() 79 | document.getElementById("wakeword").style.backgroundColor = "red" 80 | event.emit('wakeword') 81 | }) 82 | } else { 83 | listen.startListening() 84 | document.getElementById("wakeword").style.display = "none" 85 | } 86 | -------------------------------------------------------------------------------- /electron/app/js/helpers/common.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const fs = require('fs') 3 | const util = require('util') 4 | 5 | const readdir = util.promisify(fs.readdir) 6 | const exists = util.promisify(fs.stat) 7 | 8 | function showDiv(id){ 9 | 10 | // Hides all divs in majorDivs array and shows div passed as param 11 | // @param {string} id - id of div to show 12 | 13 | const majorDivs = ["eyeWrapper", "cameraWrapper", "gifWrapper", "pictureWrapper", "videoWrapper"] 14 | 15 | if(!majorDivs.includes(id)){ 16 | console.error(`Div with id ${id} is not included in array`) 17 | return 18 | } 19 | 20 | for(var i in majorDivs){ 21 | if(majorDivs[i] != id){ 22 | let div = document.getElementById(majorDivs[i]) 23 | if(div){ 24 | // only if div is found in DOM 25 | div.style.display = "none" 26 | } 27 | } else { 28 | document.getElementById(id).style.display = "block" 29 | } 30 | } 31 | } 32 | 33 | async function setQuery(answer){ 34 | 35 | // returns path to local file or remote query terms 36 | 37 | if(answer.type == 'local'){ 38 | // search from local folder 39 | let file = await pickFile(path.join(process.cwd(),'app','media','responses',answer.localFolder)) 40 | console.log(`Picked File: ${file}`) 41 | return file 42 | 43 | } else if(answer.type == 'remote'){ 44 | // use remote query terms array to search online service 45 | let searchTerm = pickRandom(answer.queryTerms) 46 | return searchTerm 47 | } else { 48 | // if type is wakeword or something else 49 | return null 50 | } 51 | } 52 | 53 | async function findLocalMediaType(filepath){ 54 | 55 | if(!filepath){ 56 | return null 57 | } 58 | 59 | let imgFiles = [".png", ".jpg", ".jpeg"] 60 | let videoFiles = [".mp4", ".webp"] 61 | let gifFiles = [".gif"] 62 | 63 | if(imgFiles.includes(path.extname(filepath).toLowerCase())){ 64 | return "image" 65 | } 66 | 67 | if(videoFiles.includes(path.extname(filepath).toLowerCase())){ 68 | return "video" 69 | } 70 | 71 | if(gifFiles.includes(path.extname(filepath).toLowerCase())){ 72 | return "gif" 73 | } 74 | } 75 | 76 | async function pickFile(folderPath){ 77 | // picks random media file from folder 78 | // @param {string} folderPath - path of folder to pick file from 79 | 80 | let isValid = false 81 | try{ 82 | isValid = await exists(folderPath) 83 | } catch (e){ 84 | console.log(e) 85 | } 86 | 87 | if(!isValid){ 88 | console.error(`Folder at path ${folderPath} does not exist. Create this folder and add some media to it`) 89 | return null 90 | } 91 | 92 | console.log("PICKING FILE") 93 | const fileExtensions = [".gif",".mp4",".webp",".png",".jpg",".jpeg"] //acceptable file extensions 94 | 95 | const files = await readdir(folderPath) 96 | 97 | let mediaFiles = files.filter((file)=>{ 98 | return fileExtensions.includes(path.extname(file).toLowerCase()) 99 | }) 100 | 101 | if(mediaFiles.length === 0){ 102 | console.log(`No media files found in ${folderPath}`) 103 | return null 104 | } 105 | 106 | let chosenFile = mediaFiles[Math.floor(Math.random()*mediaFiles.length)] 107 | 108 | return path.join(folderPath,chosenFile) 109 | } 110 | 111 | function pickRandom(array){ 112 | // shuffles and picks random element from array 113 | // @param {array} array 114 | if(!array){ 115 | return null 116 | } 117 | 118 | var m = array.length, t, i; 119 | 120 | // While there remain elements to shuffle… 121 | while (m) { 122 | 123 | // Pick a remaining element… 124 | i = Math.floor(Math.random() * m--); 125 | 126 | // And swap it with the current element. 127 | t = array[m]; 128 | array[m] = array[i]; 129 | array[i] = t; 130 | } 131 | 132 | var randomNumber = Math.floor(Math.random()*array.length) 133 | 134 | return array[randomNumber]; 135 | } 136 | 137 | async function transitionFromMedia(ms){ 138 | return new Promise((resolve) => { 139 | let wait = setTimeout(()=>{ 140 | clearTimeout(wait) 141 | event.emit('show-div','eyeWrapper') 142 | event.emit('transition-eyes-back') 143 | resolve(true) 144 | }, ms) 145 | }) 146 | } 147 | 148 | function transitionToMedia(duration, type){ 149 | if(!duration){ 150 | return null 151 | } 152 | 153 | duration = parseInt(duration) 154 | let loop = 1 155 | 156 | let afterTransition = () => { 157 | if(type == 'video'){ 158 | let video = document.getElementById("video") 159 | event.emit('show-div','videoWrapper') 160 | video.play() 161 | } else if(type == 'gif' || type == 'img'){ 162 | let img = document.getElementById("gif") 163 | event.emit('show-div', 'gifWrapper') 164 | } 165 | } 166 | 167 | event.emit('transition-eyes-away', afterTransition) 168 | } 169 | 170 | 171 | async function setTimer(duration, type){ 172 | 173 | if(!duration){ 174 | return null 175 | } 176 | 177 | duration = parseInt(duration) 178 | let loop = 1 179 | 180 | let afterTransition = () => { 181 | if(type == 'video'){ 182 | let video = document.getElementById("video") 183 | event.emit('show-div','videoWrapper') 184 | video.play() 185 | } else if(type == 'gif' || type == 'img'){ 186 | let img = document.getElementById("gif") 187 | event.emit('show-div', 'gifWrapper') 188 | } 189 | } 190 | 191 | event.emit('transition-eyes-away', afterTransition) 192 | 193 | let done = await transitionFromMedia(duration*loop) 194 | 195 | return done 196 | } 197 | 198 | module.exports = { 199 | showDiv, 200 | pickFile, 201 | setQuery, 202 | setTimer, 203 | transitionToMedia, 204 | transitionFromMedia, 205 | findLocalMediaType 206 | } -------------------------------------------------------------------------------- /electron/app/js/helpers/media.js: -------------------------------------------------------------------------------- 1 | const config = require('config/config.js') 2 | const giphy = require('giphy-api')(config.giphy.key); 3 | const path = require('path') 4 | 5 | 6 | function findRemoteGif(query){ 7 | if(!query){ 8 | return null 9 | } 10 | 11 | return new Promise((resolve, reject)=>{ 12 | giphy.translate(query, (err,res)=>{ 13 | 14 | if(err || !res) reject(`Got error or no response when searching for "${query}" from Giphy`); 15 | 16 | //console.log(res.data.images) 17 | 18 | const gif = res.data.images.original_mp4.mp4 19 | 20 | resolve(gif) 21 | 22 | }) 23 | }) 24 | } 25 | 26 | async function findRemoteVideo(query){ 27 | 28 | query = encodeURI(query) 29 | // let json = null 30 | try { 31 | let response = await fetch(`https://apiv2.vlipsy.com/v1/vlips/search?q=${query}&key=${config.vlipsy.key}`) 32 | if(!response.ok){ 33 | throw new Error(`Error accessing vlipsy. Check api key or query`) 34 | } 35 | var json = await response.json() 36 | } catch(e){ 37 | console.error(e) 38 | return 39 | } 40 | 41 | const acceptableDuration = 5.5; 42 | let acceptableVlips = [] 43 | 44 | for(let i=0; i { 127 | video.addEventListener('canplay', (e)=>{ 128 | resolve(e.returnValue) 129 | }) 130 | }) 131 | 132 | if(!canplay){ 133 | return 0 134 | } 135 | 136 | let duration = video.duration*1000+endPauseDuration 137 | return duration 138 | } 139 | 140 | 141 | module.exports = { 142 | findRemoteGif, 143 | findRemoteVideo, 144 | findMediaType, 145 | findMediaDuration 146 | } -------------------------------------------------------------------------------- /electron/app/js/intent-engines/dialogflow-intents.js: -------------------------------------------------------------------------------- 1 | const actions = require('js/actions/actions') 2 | const weather = require('js/skills/weather') 3 | const Timer = require('js/skills/timer') 4 | const event = require('js/events/events') 5 | const responses = require('js/responses/responses') 6 | 7 | function parseIntent(cmd){ 8 | 9 | /* param {cmd} - response object from speech to text engine */ 10 | 11 | // this one is for google dialogflow, you might need to make adjustments for a different engine 12 | 13 | console.log(cmd) 14 | 15 | switch(cmd.intent){ 16 | 17 | case "greeting": 18 | actions.setAnswer(responses.greeting, {type: 'remote'}) 19 | break 20 | 21 | case "camera": 22 | event.emit(`camera-${cmd.params.on.stringValue}`) 23 | break 24 | 25 | case "timer": 26 | let timer = new Timer(cmd.params.time.numberValue, cmd.params.timeUnit.stringValue) 27 | timer.startTimer() 28 | break 29 | 30 | case "weather": 31 | weather.getWeather(cmd.params.city.stringValue) 32 | break 33 | 34 | case "changeGlasses": 35 | event.emit("change-glasses") 36 | break 37 | 38 | case "goodbye": 39 | actions.setAnswer(responses.bye, {type: 'local'}) 40 | break 41 | default: 42 | actions.setAnswer(responses.confused, {type:'local'}) 43 | break 44 | } 45 | 46 | // setAnswer(responses[cmd.intent], {type:'remote'}) 47 | } 48 | 49 | module.exports = { 50 | parseIntent 51 | } -------------------------------------------------------------------------------- /electron/app/js/intent-engines/dialogflow.js: -------------------------------------------------------------------------------- 1 | const dialogflow = require('dialogflow') 2 | const through2 = require('through2') 3 | const path = require('path') 4 | const uuid = require('uuid') 5 | const config = require('config/config') 6 | const event = require('js/events/events') 7 | const mic = require('js/senses/mic') 8 | 9 | function setup(){ 10 | // DIALOGFLOW 11 | 12 | //create unique id for new dialogflow session 13 | const sessionId = uuid.v4() 14 | 15 | //create a dialogflow session 16 | const sessionClient = new dialogflow.SessionsClient({ 17 | projectId: config.speech.projectId, 18 | keyFilename: path.join(process.cwd(), 'app', 'config', config.speech.dialogflowKey) 19 | }) 20 | 21 | const sessionPath = sessionClient.sessionPath(config.speech.projectId, sessionId) 22 | 23 | // the dialogflow request 24 | const dialogflowRequest = { 25 | session: sessionPath, 26 | queryParams: { 27 | session: sessionClient.sessionPath(config.speech.projectId, sessionId) 28 | }, 29 | queryInput:{ 30 | audioConfig:{ 31 | audioEncoding: "AUDIO_ENCODING_LINEAR_16", 32 | sampleRateHertz: 16000, 33 | languageCode: config.speech.language 34 | } 35 | }, 36 | singleUtterance: true, 37 | interimResults: false 38 | } 39 | 40 | return {sessionClient, dialogflowRequest} 41 | } 42 | 43 | function start(){ 44 | const {sessionClient, dialogflowRequest} = setup() 45 | 46 | let stt = new DialogflowSpeech(sessionClient, dialogflowRequest) 47 | 48 | event.emit('start-stt') 49 | } 50 | 51 | class DialogflowSpeech { 52 | 53 | constructor(client, request){ 54 | this.request = request 55 | this.stream = client.streamingDetectIntent() 56 | this.result = '' 57 | this.unpipeTimer = null 58 | this.listenFor = 4000 59 | this.intentObj = {} 60 | this.sttStream = null 61 | // this.wakewordDetector = wakewordDetector 62 | 63 | this.stream.write(this.request) 64 | 65 | this.startStream = this.startStream.bind(this) 66 | 67 | event.once('start-stt', this.startStream) 68 | } 69 | 70 | startSttStream(){ 71 | this.sttStream = through2.obj((obj,_,next)=>{ 72 | next(null, {inputAudio: obj}) 73 | }) 74 | } 75 | 76 | startStream(){ 77 | const self = this 78 | 79 | this.startSttStream() 80 | 81 | this.stream.once('pipe', () => { 82 | console.log('PIPING > DIALOGFLOW') 83 | 84 | self.unpipeTimer = setTimeout(()=>{ 85 | console.log('UNPIPING DIALOGFLOW > QUERY TIME EXCEEDED') 86 | self.sttStream.unpipe(self.stream) 87 | mic.getMic().unpipe(self.sttStream) 88 | self.unpipeTimer = null 89 | }, self.listenFor) 90 | }) 91 | 92 | this.stream.on('data', (data) => { 93 | if(data.queryResult != null){ 94 | 95 | if(data.queryResult.queryText == ""){ 96 | return 97 | } 98 | 99 | self.intentObj.intent = data.queryResult.intent.displayName 100 | self.intentObj.params = data.queryResult.parameters.fields 101 | self.intentObj.queryText = data.queryResult.queryText 102 | self.intentObj.responseText = data.queryResult.fulfillmentText 103 | 104 | self.result = self.intentObj 105 | 106 | if(self.sttStream == null){ 107 | return 108 | } 109 | 110 | self.sttStream.unpipe(self.stream) 111 | mic.getMic().unpipe(self.sttStream) 112 | } 113 | }) 114 | 115 | this.stream.once('error', (err) => { 116 | console.error('ERROR > DIALOGFLOW', err) 117 | }) 118 | 119 | this.stream.once('close', function(){ 120 | console.log('DIALOGFLOW PIPE > CLOSED') 121 | }) 122 | 123 | this.stream.once('unpipe', function(src){ 124 | console.log('UNPIPING > DIALOGFLOW') 125 | self.sttStream.end() 126 | self.stream.end() 127 | }) 128 | 129 | this.stream.once('finish', () => { 130 | 131 | console.log("FINISHED > DIALOGFLOW") 132 | if(self.unpipeTimer != null){ 133 | // timer is running but result has returned already 134 | clearTimeout(self.unpipeTimer) 135 | self.unpipeTimer = null 136 | 137 | console.log("CLEARING TIMEOUT > RESULT RETURNED") 138 | } 139 | 140 | if(self.result){ 141 | console.log("DIALOGFLOW > SENDING RESULT") 142 | event.emit('final-command', self.result) 143 | } else { 144 | console.log("DIALOGFLOW > NO RESULT/NTN HEARD") 145 | event.emit('no-command') 146 | } 147 | 148 | self.request = null 149 | self.stream = null 150 | self.result = null 151 | self.intentObj = {} 152 | self.sttStream = null 153 | 154 | event.emit('end-speech-to-text') 155 | 156 | // self.mic.pipe(self.wakewordDetector) 157 | 158 | // self.mic = null 159 | // self.wakewordDetector = null 160 | 161 | event.removeListener('start-speech-to-text', self.startStream) 162 | 163 | }) 164 | 165 | mic.startMic().pipe(this.sttStream).pipe(this.stream) 166 | } 167 | } 168 | 169 | module.exports = { 170 | setup, 171 | start, 172 | DialogflowSpeech 173 | } -------------------------------------------------------------------------------- /electron/app/js/lib/dotstar.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var Dotstar = (function () { 3 | function Dotstar(spi, options) { 4 | if (options === void 0) { options = {}; } 5 | this.length = options.length || Dotstar.defaultOptions.length; 6 | var fullBufferLength = Dotstar.startBytesLength + this.length * Dotstar.bytesPerLed + Dotstar.endBytesLength; 7 | this.ledBuffer = new Buffer(fullBufferLength); 8 | this.ledBuffer.fill(0); 9 | this.ledBuffer.fill(255, this.ledBuffer.length - Dotstar.endBytesLength); 10 | // Create buffer which is subset of the full buffer represetenting only the LEDs 11 | this.colorBuffer = this.ledBuffer.slice(Dotstar.startBytesLength, -Dotstar.endBytesLength); 12 | this.clear(); 13 | this.offBuffer = new Buffer(fullBufferLength); 14 | this.ledBuffer.copy(this.offBuffer); 15 | this.device = spi; 16 | this.write(this.offBuffer); 17 | } 18 | /** 19 | * Set every LED in the colorBuffer to the RGBA value. 20 | */ 21 | Dotstar.prototype.all = function (r, g, b, a) { 22 | if (a === void 0) { a = 1; } 23 | var singleLedBuffer = this.convertRgbaToLedBuffer(r, g, b, a); 24 | for (var led = 0; led < this.length; led++) { 25 | singleLedBuffer.copy(this.colorBuffer, Dotstar.bytesPerLed * led); 26 | } 27 | }; 28 | /** 29 | * Set every LED in the colorBuffer to black/off. 30 | */ 31 | Dotstar.prototype.clear = function () { 32 | this.all(0, 0, 0, 0); 33 | }; 34 | /** 35 | * Turn off every LED without having to update the color buffer. 36 | * This is slightly faster and useful when you want to resume with the previous color. 37 | */ 38 | Dotstar.prototype.off = function () { 39 | this.write(this.offBuffer); 40 | }; 41 | /** 42 | * Set a specific LED in the colorBuffer to RGBA value. 43 | */ 44 | Dotstar.prototype.set = function (led, r, g, b, a) { 45 | if (a === void 0) { a = 1; } 46 | if (led < 0) { 47 | throw new Error("led value must be a positive integer. You passed " + led); 48 | } 49 | if (led > this.length) { 50 | throw new Error("led value must not be greater than the maximum length of the led strip. The max length is: " + this.length + ". You passed: " + led); 51 | } 52 | var ledBuffer = this.convertRgbaToLedBuffer(r, g, b, a); 53 | var ledOffset = Dotstar.bytesPerLed * led; 54 | ledBuffer.copy(this.colorBuffer, ledOffset); 55 | }; 56 | /** 57 | * Update DotStar LED strip with current data in led buffer. 58 | */ 59 | Dotstar.prototype.sync = function () { 60 | this.write(this.ledBuffer); 61 | }; 62 | /** 63 | * Convert RGBA value to Buffer 64 | */ 65 | Dotstar.prototype.convertRgbaToLedBuffer = function (r, g, b, a) { 66 | if (a === void 0) { a = 1; } 67 | var brightnessValue = Math.floor(31 * a) + 224; 68 | var ledBuffer = new Buffer(Dotstar.bytesPerLed); 69 | ledBuffer.writeUInt8(brightnessValue, 0); 70 | ledBuffer.writeUInt8(b, 1); 71 | ledBuffer.writeUInt8(g, 2); 72 | ledBuffer.writeUInt8(r, 3); 73 | return ledBuffer; 74 | }; 75 | /** 76 | * Wrapper around device.write which rethrows errors 77 | */ 78 | Dotstar.prototype.write = function (buffer) { 79 | this.device.write(buffer, function (error) { 80 | if (error) { 81 | throw error; 82 | } 83 | }); 84 | }; 85 | Dotstar.defaultOptions = { 86 | length: 10 87 | }; 88 | Dotstar.startBytesLength = 4; 89 | Dotstar.endBytesLength = 4; 90 | Dotstar.bytesPerLed = 4; 91 | return Dotstar; 92 | }()); 93 | exports.Dotstar = Dotstar; 94 | //# sourceMappingURL=dotstar.js.map -------------------------------------------------------------------------------- /electron/app/js/power/power.js: -------------------------------------------------------------------------------- 1 | const event = require('js/events/events') 2 | const execSync = require('child_process').execSync 3 | 4 | function shutdown() { 5 | event.emit("reset") 6 | setTimeout(()=>{ 7 | execSync('sudo shutdown -h now') 8 | },1000) 9 | } 10 | 11 | function reboot() { 12 | event.emit("reset") 13 | setTimeout(() => { 14 | execSync('sudo reboot -h now') 15 | }, 1000) 16 | } 17 | 18 | function refresh(){ 19 | location.reload() 20 | } 21 | 22 | module.exports = { 23 | shutdown, 24 | reboot, 25 | refresh 26 | } -------------------------------------------------------------------------------- /electron/app/js/responses/responses.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | /* 4 | Obj Structure: 5 | 6 | intentName: { 7 | localFolder: 'xxx' <- Local folder in app/media/responses/ where you are storing local media responses 8 | queryTerms: ['a','b','c'] <- what terms to use to query media from online sources like giphy.com 9 | servo: 'ccc' <- name of servo animation stored in app/media/servo_anims/ (without the .json) 10 | led: { 11 | anim: 'eee' <- name of animation, must be a function in app/js/senses/leds.js 12 | color: 'red' <- color leds, must be defined in app/js/senses/leds.js 13 | } 14 | sound: 'cccc.wav/mp3' <- mp3 or wav file located in app/media/sounds/ 15 | cbBefore: function <- callback function before media playback 16 | cbDuring: function <- callback function during media playback 17 | cbAfter: function <- callback function after media playback 18 | text: 'string' <- what text should be overlayed on the screen 19 | } 20 | */ 21 | 22 | 23 | confused: { 24 | localFolder: 'confused', 25 | queryTerms: ['shrug', 'confused', 'dont know'], 26 | servo: null, 27 | led: { 28 | anim: 'blink', 29 | color: 'orange' 30 | }, 31 | sound: null 32 | }, 33 | 34 | greeting: { 35 | localFolder: 'greeting', 36 | queryTerms: ['hello','hi','howdy','sup','whatsup'], 37 | servo: 'look-up', 38 | led: { 39 | anim: 'blink', 40 | color: 'green' 41 | }, 42 | sound: null 43 | }, 44 | 45 | bye: { 46 | localFolder: "bye", 47 | queryTerms:["bye","see you","goodbye","ciao","so long"], 48 | servo: "look-up-slow", 49 | led: { 50 | anim: "blink", 51 | color: "blue" 52 | }, 53 | sound:null 54 | }, 55 | 56 | wakeword: { 57 | localFolder: null, 58 | queryTerms: null, 59 | servo: 'alert', 60 | led: { 61 | anim:'circle', 62 | color: 'aqua' 63 | }, 64 | sound: 'alert.wav', 65 | cbAfter: function(){ 66 | event.emit('speech-to-text') 67 | } 68 | }, 69 | 70 | ok: { 71 | localFolder: 'ok', 72 | queryTerms:["ok","okay","you got it"], 73 | servo: "look-up", 74 | led: { 75 | anim: "blink", 76 | color: "green" 77 | }, 78 | sound: null 79 | }, 80 | 81 | alarm: { 82 | localFolder: "alarm", 83 | queryTerms:["alarm","ringing","party"], 84 | servo: "jiggle", 85 | led: { 86 | anim: "blink", 87 | color: "yellow" 88 | }, 89 | sound:null 90 | }, 91 | } -------------------------------------------------------------------------------- /electron/app/js/senses/buttons.js: -------------------------------------------------------------------------------- 1 | const event = require('js/events/events') 2 | const os = require('os') 3 | 4 | let gpio = null 5 | 6 | if(os.arch() == 'arm'){ 7 | // only include on raspberry pi 8 | gpio = require('rpi-gpio') 9 | gpio.setMode(gpio.MODE_BCM) 10 | let gpios = [4,16,17,23] 11 | 12 | for(var i in gpios){ 13 | gpio.setup(gpios[i], gpio.DIR_IN, gpio.EDGE_BOTH) 14 | } 15 | } 16 | 17 | 18 | function initializeButtons(){ 19 | 20 | if(gpio == null){ 21 | return 22 | } 23 | 24 | const longPressDuration = 3000 25 | let btnTimer = null 26 | let longPressEventSent = false 27 | let pressed = false 28 | 29 | gpio.on('change', (channel, value) => { 30 | 31 | 32 | 33 | 34 | if(value == false){ 35 | console.log(`Btn ${channel} released`) 36 | clearTimeout(btnTimer) 37 | btnTimer = null 38 | pressed = false 39 | 40 | if(!longPressEventSent){ 41 | event.emit(`btn-${channel}-short-press`) 42 | } 43 | 44 | longPressEventSent = false 45 | 46 | } else if(value == true){ 47 | console.log(`Btn ${channel} pressed`) 48 | 49 | if(!pressed){ 50 | btnTimer = setTimeout(()=>{ 51 | event.emit(`btn-${channel}-long-press`) 52 | longPressEventSent = true 53 | btnTimer = null 54 | }, longPressDuration) 55 | } 56 | 57 | pressed = true 58 | 59 | } 60 | }) 61 | } 62 | 63 | module.exports = { 64 | initializeButtons 65 | } -------------------------------------------------------------------------------- /electron/app/js/senses/camera.js: -------------------------------------------------------------------------------- 1 | const zerorpc = require('zerorpc') 2 | const spawn = require('child_process').spawn 3 | const event = require('js/events/events') 4 | 5 | class Camera{ 6 | 7 | constructor(){ 8 | this.connected = false 9 | this.client = new zerorpc.Client() 10 | this.client.connect("tcp://127.0.0.1:4242") 11 | this.client.invoke("hello", (err, res, more) => { 12 | if(res){ 13 | console.log(`Connected to camera: ${res}`) 14 | this.connected = true 15 | } else { 16 | console.log('Not connected to camera') 17 | } 18 | }) 19 | 20 | this.startCamera = this.startCamera.bind(this) 21 | this.stopCamera = this.stopCamera.bind(this) 22 | this.startRecording = this.startRecording.bind(this) 23 | this.stopRecording = this.stopRecording.bind(this) 24 | 25 | event.on('camera-on', this.startCamera) 26 | event.on('camera-off', this.stopCamera) 27 | event.on('camera-record', this.startRecording) 28 | event.on('camera-stop', this.stopRecording) 29 | 30 | } 31 | 32 | startCamera(){ 33 | if(this.connected){ 34 | this.client.invoke("startCamera") 35 | } 36 | } 37 | 38 | stopCamera(){ 39 | if(this.connected){ 40 | this.client.invoke("stopCamera") 41 | } 42 | } 43 | 44 | startRecording(){ 45 | if(this.connected){ 46 | this.client.invoke("startRecording") 47 | } 48 | } 49 | 50 | stopRecording(){ 51 | if(this.connected){ 52 | this.client.invoke("stopRecording") 53 | } 54 | } 55 | } 56 | 57 | module.exports = Camera -------------------------------------------------------------------------------- /electron/app/js/senses/leds.js: -------------------------------------------------------------------------------- 1 | const dotstar = require('js/lib/dotstar') 2 | const os = require('os') 3 | const event = require('js/events/events') 4 | 5 | let SPI = null 6 | let spi = null 7 | 8 | if(os.arch == "arm"){ 9 | SPI = require('pi-spi') 10 | spi = SPI.initialize('/dev/spidev0.0') 11 | } 12 | 13 | class Leds { 14 | 15 | constructor() { 16 | 17 | this.brightness = 0.5 18 | this.length = 12 19 | this.colors = { 20 | "red":[255,0,0], 21 | "green":[0,255,0], 22 | "blue":[0,0,255], 23 | "aqua":[0,255,255], 24 | "purple":[190,64,242], 25 | "orange":[239,75,36], 26 | "yellow":[255,215,18], 27 | "pink":[244,52,239], 28 | "black":[0,0,0] 29 | } 30 | this.currentlyOn = [] 31 | this.trailLength = 3 32 | 33 | this.strip = null 34 | 35 | if(spi != null){ 36 | // only available on pi 37 | this.strip = new dotstar.Dotstar(spi, { 38 | length: this.length 39 | }) 40 | } 41 | 42 | this.playAnimation = this.playAnimation.bind(this) 43 | this.off = this.off.bind(this) 44 | 45 | if(this.strip){ 46 | // only listen for led events on pi 47 | event.on('led-on', this.playAnimation) 48 | event.on('led-off', this.off) 49 | } 50 | } 51 | 52 | playAnimation(anim){ 53 | // @param {obj} anim - contains keys for anim type and color 54 | console.log(`LED anim: ${anim.anim} with color ${anim.color}`) 55 | 56 | this[anim.anim](anim.color) 57 | } 58 | 59 | blink(color='red', time=500, count=5, brightness=0.5) { 60 | let blinkCount = 0 61 | 62 | let blinkInterval = setInterval(() => { 63 | if(blinkCount%2==0){ 64 | this.on(color, brightness) 65 | } else { 66 | this.off() 67 | } 68 | 69 | blinkCount++ 70 | 71 | if(blinkCount>count){ 72 | clearInterval(blinkInterval) 73 | blinkInterval = null 74 | } 75 | }, time) 76 | } 77 | 78 | fade (){ 79 | 80 | } 81 | 82 | circle(color="aqua"){ 83 | this.trail(color, 0,5) 84 | this.trail(color, 11,6) 85 | 86 | setTimeout(() => { 87 | this.trail(color, 5,0,false) 88 | this.trail(color,6,11,false) 89 | }, 1000) 90 | 91 | setTimeout(()=>{ 92 | this.off() 93 | }, 2500) 94 | } 95 | 96 | circleOut(color="green"){ 97 | this.trail(color, 0, 5) 98 | this.trail(color, 11,6) 99 | } 100 | 101 | trail(color, start, finish, overshoot=true, brightness=0.5, time=100, trailLength=3){ 102 | 103 | if((start < 0 || finish < 0) || (start > this.length || finish > this.length)){ 104 | console.error(`Led values are outside permissible range of 0-11`) 105 | return 106 | } 107 | 108 | var firstLed = start 109 | var currentlyOn = [] 110 | 111 | let moveInterval = setInterval(() => { 112 | currentlyOn.push(firstLed) 113 | 114 | if(currentlyOn.length > trailLength){ 115 | // remove first led to maintain trail length 116 | let removeLed = currentlyOn.shift() 117 | this.strip.set(removeLed, ...this.colors["black"],0) 118 | } 119 | 120 | for(let i=0;i finish){ 133 | clearInterval(moveInterval) 134 | moveInterval = null 135 | if(overshoot){ 136 | this.clearLedTrail(currentlyOn, time) 137 | } 138 | } 139 | } else if (start > finish){ 140 | // move in anticlockwise direction 141 | firstLed-- 142 | 143 | if(firstLed < finish){ 144 | clearInterval(moveInterval) 145 | moveInterval = null 146 | if(overshoot){ 147 | this.clearLedTrail(currentlyOn, time) 148 | } 149 | } 150 | } 151 | }, time) 152 | } 153 | 154 | fadeOutError(color='red', time=100){ 155 | let increment = 0.1 156 | let fadeOutInterval = null 157 | let brightness = 0.5 158 | 159 | this.on(color, brightness) 160 | 161 | fadeOutInterval = setInterval(() => { 162 | this.strip.all(...this.colors[color], brightness) 163 | this.strip.sync() 164 | 165 | brightness-=increment 166 | 167 | if(brightness<0){ 168 | clearInterval(fadeOutInterval) 169 | fadeOutInterval = null 170 | this.strip.clear() 171 | this.strip.sync() 172 | } 173 | }, time) 174 | } 175 | 176 | clearLedTrail(onLeds, time=100){ 177 | let removeInterval = setInterval(() => { 178 | if(onLeds.length != 0){ 179 | let offLed = onLeds.shift() 180 | 181 | this.strip.set(offLed, ...this.colors["black"],0) 182 | } else { 183 | clearInterval(removeInterval) 184 | removeInterval = null 185 | 186 | this.strip.clear() 187 | } 188 | 189 | this.strip.sync() 190 | }, time) 191 | } 192 | 193 | 194 | 195 | on(color, brightness=0.5){ 196 | if(!this.colors.hasOwnProperty(color)){ 197 | console.error(`Color ${color} has not been set`) 198 | return 199 | } 200 | 201 | this.strip.all(...this.colors[color], brightness) 202 | this.strip.sync() 203 | } 204 | 205 | off(){ 206 | this.strip.clear() 207 | this.strip.sync() 208 | } 209 | } 210 | 211 | // make singleton 212 | const leds = new Leds() 213 | Object.freeze(leds) 214 | 215 | module.exports = leds -------------------------------------------------------------------------------- /electron/app/js/senses/listen.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const record = require('node-record-lpcm16') 4 | const path = require('path') 5 | const os = require('os') 6 | const config = require('config/config') 7 | 8 | const {Detector, Models} = require('snowboy') 9 | 10 | const event = require('js/events/events') 11 | const mic = require('js/senses/mic') 12 | 13 | // const dialogflow = require('js/intent-engines/dialogflow') 14 | 15 | function setupSnowboy(){ 16 | //SNOWBOY WAKEWORD DETECTOR 17 | 18 | const models = new Models() 19 | 20 | models.add({ 21 | file: path.join(process.cwd(),'app','config',config.speech.model), 22 | sensitivity: config.speech.sensitivity, // adjust sensitivity if you are getting too many false positive or negatives 23 | hotwords: config.speech.wakeword 24 | }) 25 | 26 | const wakewordDetector = new Detector({ 27 | resource: path.join(process.cwd(), 'app', 'config', 'common.res'), 28 | models: models, 29 | audioGain: 2.0 30 | }) 31 | 32 | return wakewordDetector 33 | } 34 | 35 | function setupRecorder(){ 36 | // MIC RECORDER 37 | 38 | const recorder = (os.arch()=='arm')?'arecord':'rec' // use arecord on pi, rec on laptop 39 | 40 | const recorderOpts={ 41 | verbose: false, 42 | threshold:0, 43 | recordProgram: recorder, 44 | sampleRateHertz: 16000 45 | } 46 | 47 | return {recorder, recorderOpts} 48 | } 49 | 50 | 51 | function startListening(){ 52 | 53 | const wakewordDetector = setupSnowboy() 54 | 55 | const {recorder, recorderOpts} = setupRecorder() 56 | 57 | // WAKEWORD SNOWBOY EVENTS 58 | wakewordDetector.on('unpipe', (src) => { 59 | console.log("STOPPED PIPING > WAKEWORD") 60 | }) 61 | 62 | wakewordDetector.on('pipe', (src) => { 63 | console.log("PIPING > WAKEWORD") 64 | }) 65 | 66 | wakewordDetector.on('error', (err) => { 67 | console.error("WAKEWORD ERROR: ", err) 68 | }) 69 | 70 | wakewordDetector.on('close', () => { 71 | console.log("WAKEWORD PIPE CLOSED") 72 | }) 73 | 74 | wakewordDetector.on('hotword', (index, hotword) => { 75 | 76 | console.log("WAKEWORD > DETECTED") 77 | 78 | //unpipe recording from wakeword listener 79 | mic.getMic().unpipe(wakewordDetector) 80 | event.emit("wakeword") 81 | }) 82 | 83 | event.on('pipe-to-wakeword', () => { 84 | // prevent bug in arecord. WAV has 2gb file limit. After streaming 2GB it starts 85 | // sending headers with no data 86 | // possible short term solution: restart mic everytime after a response 87 | mic.startMic().pipe(wakewordDetector) 88 | }) 89 | 90 | mic.getMic().pipe(wakewordDetector) 91 | } 92 | 93 | module.exports = { 94 | startListening 95 | } 96 | 97 | 98 | -------------------------------------------------------------------------------- /electron/app/js/senses/mic.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const record = require('node-record-lpcm16') 4 | const os = require('os') 5 | 6 | class Mic { 7 | constructor(){ 8 | this.recorder = (os.arch()=='arm')?'arecord':'rec' // use arecord on pi, rec on os/linux 9 | 10 | this.recorderOpts={ 11 | verbose: false, 12 | threshold:0, 13 | recordProgram: this.recorder, 14 | sampleRateHertz: 16000 15 | } 16 | 17 | this.mic = null 18 | this.startMic() 19 | } 20 | 21 | startMic(){ 22 | this.mic = null 23 | this.mic = record.start(this.recorderOpts) 24 | return this.mic 25 | } 26 | 27 | getMic(){ 28 | return this.mic 29 | } 30 | } 31 | 32 | const mic = new Mic() 33 | 34 | module.exports = mic -------------------------------------------------------------------------------- /electron/app/js/senses/servo.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const fs = require('fs') 3 | const os = require('os') 4 | const event = require('js/events/events') 5 | 6 | let i2cBus = null 7 | let PCA9685 = null 8 | let options = null 9 | 10 | if(os.arch() == 'arm'){ 11 | //only setup on pi 12 | 13 | i2cBus = require('i2c-bus') 14 | 15 | PCA9685 = require('pca9685').Pca9685Driver 16 | 17 | options = { 18 | i2c: i2cBus.openSync(1), 19 | address: 0x40, 20 | frequency: 50, 21 | debug: false 22 | } 23 | } 24 | 25 | class Servo { 26 | 27 | constructor(){ 28 | this.pwm = null 29 | this.servoTimer = null 30 | this.playbackRate = 33 //ms 31 | this.servoRestAngle = 1500 32 | 33 | this.animate = this.animate.bind(this) 34 | this.reset = this.reset.bind(this) 35 | 36 | if(PCA9685 != null){ 37 | this.pwm = new PCA9685(options, (err) => { 38 | if(err) console.error(`Error initializing PCA9685 for servos`); 39 | 40 | for(var i=0;i<3;i++){ 41 | this.pwm.setPulseLength(i, this.servoRestAngle) 42 | } 43 | }) 44 | 45 | event.on('servo-move', this.animate) 46 | event.on('servo-reset', this.reset) 47 | } 48 | 49 | } 50 | 51 | animate(animName){ 52 | 53 | console.log(`SERVO > ${animName}.json`) 54 | 55 | let filepath = path.join(process.cwd(),'app','media','servo_anims',`${animName}.json`) 56 | 57 | fs.readFile(filepath, 'utf8', (err, contents) => { 58 | if(err){ 59 | console.error(`Error reading animation file`) 60 | console.log(err) 61 | return 62 | } 63 | 64 | try { 65 | const data = JSON.parse(contents) 66 | this.servoPlayback(data) 67 | 68 | } catch(error){ 69 | console.error(`Error playing servo from anim file`) 70 | console.error(error) 71 | } 72 | }) 73 | } 74 | 75 | reset(){ 76 | if(this.servoTimer != null){ 77 | clearInterval(this.servoTimer) 78 | this.servoTimer=null 79 | } 80 | for(let i=0;i<3;i++){ 81 | this.pwm.setPulseLength(i, this.servoRestAngle) 82 | } 83 | } 84 | 85 | servoPlayback(animData){ 86 | var index = 0 87 | 88 | this.servoTimer = setInterval(() => { 89 | for(var i=0;i<3;i++){ 90 | this.pwm.setPulseLength(i, animData[index][i]) 91 | //console.log(i,animData[index][i]) 92 | } 93 | 94 | index ++ 95 | 96 | if(index >= animData.length){ 97 | console.log(`Finished playing servo animation`) 98 | clearInterval(this.servoTimer) 99 | this.servoTimer = null 100 | this.reset() 101 | } 102 | }, this.playbackRate) 103 | } 104 | } 105 | 106 | module.exports = Servo -------------------------------------------------------------------------------- /electron/app/js/senses/speak.js: -------------------------------------------------------------------------------- 1 | const spawn = require('child_process').spawn 2 | const os = require('os') 3 | const path = require('path') 4 | const event = require('js/events/events') 5 | 6 | const tts = (os.arch() == 'arm')?'flite':'say' 7 | 8 | function speak(text){ 9 | // speaks out the given text using the system voice 10 | // @param {string} text - the text to be spoken 11 | 12 | let speechProcess = null 13 | 14 | if(tts === 'flite'){ 15 | speechProcess = spawn(tts, ['-voice','awb','-t',text],{detached:false}) 16 | 17 | 18 | } else if(tts === 'say'){ 19 | speechProcess = spawn(tts, [text], {detached: false}) 20 | } 21 | 22 | speechProcess.on('close', ()=>{ 23 | event.emit("finished-speaking") 24 | }) 25 | } 26 | 27 | function playSound(filename){ 28 | // plays passed in file located in app/media/sounds 29 | // @param {string} filename - accepts .wav & .mp3 files located in app/media/sounds 30 | console.log(`FILE: ${filename}`) 31 | 32 | if(!filename.endsWith('.wav') && !filename.endsWith('.mp3')){ 33 | console.error(`File ${filename} is not supported`) 34 | return 35 | } 36 | let audio = document.getElementById("sound") 37 | audio.currentTime = 0 38 | audio.src = path.join(process.cwd(),'app','media','sounds',filename) 39 | audio.play() 40 | } 41 | 42 | function stopSound(){ 43 | // stop sound playback 44 | 45 | let audio = document.getElementById("sound") 46 | audio.currentTime = 0 47 | audio.pause() 48 | audio.src = '' 49 | } 50 | 51 | function setVolume(vol){ 52 | // sets volume level for audio and video playback 53 | // @param {float} vol - range 0-1 54 | 55 | if(vol < 0){ 56 | vol = 0 57 | } else if(vol > 1){ 58 | vol = 1 59 | } 60 | 61 | const video = document.getElementById("video") 62 | const audio = document.getElementById("sound") 63 | 64 | video.volume = vol 65 | audio.volume = vol 66 | } 67 | 68 | module.exports = { 69 | speak, 70 | playSound, 71 | stopSound, 72 | setVolume 73 | } -------------------------------------------------------------------------------- /electron/app/js/senses/text.js: -------------------------------------------------------------------------------- 1 | const event = require('js/events/events') 2 | 3 | class Text{ 4 | constructor(){ 5 | this.text = document.getElementById("textOverlay") 6 | 7 | event.on('show-text', this.showText) 8 | event.on('remove-text', this.removeText) 9 | } 10 | 11 | showText(content){ 12 | this.text.innerHTML = content 13 | } 14 | 15 | removeText(){ 16 | this.text.innerHTML = '' 17 | } 18 | } 19 | 20 | // make singleton 21 | const text = new Text() 22 | Object.freeze(text) 23 | 24 | module.exports = text -------------------------------------------------------------------------------- /electron/app/js/skills/timer.js: -------------------------------------------------------------------------------- 1 | const event = require('js/events/events') 2 | const actions = require('js/actions/actions') 3 | const responses = require('js/responses/responses') 4 | 5 | class Timer { 6 | constructor(time, units){ 7 | 8 | this.time = time 9 | this.unit = units 10 | this.timer = null 11 | this.multiplier = 1000 12 | 13 | if(this.unit == "hour" || this.unit == "hours"){ 14 | this.multiplier *= 3600 15 | } else if(this.unit == "minute" || this.unit == "minutes"){ 16 | this.multiplier *= 60 17 | } 18 | 19 | this.time = this.time * this.multiplier 20 | 21 | this.clearTimer = this.clearTimer.bind(this) 22 | 23 | event.once('stop-timer', this.clearTimer) 24 | } 25 | 26 | startTimer(){ 27 | 28 | actions.setAnswer(responses.ok, {type: 'local'}) 29 | 30 | this.timer = setTimeout(()=>{ 31 | actions.setAnswer(responses.alarm, {type: 'local'}) 32 | console.log("timer over") 33 | this.timer = null 34 | }, this.time) 35 | } 36 | 37 | clearTimer(){ 38 | if(this.timer !== null){ 39 | clearTimeout(this.timer) 40 | this.timer = null 41 | } 42 | } 43 | } 44 | 45 | module.exports = Timer -------------------------------------------------------------------------------- /electron/app/js/skills/weather.js: -------------------------------------------------------------------------------- 1 | const config = require('config/config') 2 | const actions = require('js/actions/actions') 3 | const speak = require('js/senses/speak') 4 | 5 | function getWeather(city){ 6 | 7 | // @param {string} city - city to find weather of 8 | 9 | if(!city){ 10 | // enter your default city here 11 | city = config.openweather.city 12 | } 13 | 14 | let query = encodeURI(city) 15 | 16 | fetch(`http://api.openweathermap.org/data/2.5/weather?q=${query}&units=imperial&APPID=${config.openweather.key}`) 17 | .then((response)=> response.json()) 18 | .then((json)=>{ 19 | console.log(json) 20 | if(json.cod == '404'){ 21 | console.error(`Cant find city ${query}`) 22 | return 23 | } 24 | 25 | displayWeather(json) 26 | }) 27 | } 28 | 29 | function displayWeather(data){ 30 | 31 | let cbDuring = () => { 32 | speak.speak(`The temperature in ${data.name} is ${data.main.temp} degrees with ${data.weather[0].description}`) 33 | } 34 | 35 | actions.setAnswer({type:'remote', queryTerms: [data.weather[0].description], cbDuring: cbDuring, text: `${data.main.temp} \n ${data.weather[0].description}`}) 36 | //console.log(`The temperature in ${data.name} is ${data.main.temp} degrees with ${data.weather[0].description}`) 37 | } 38 | 39 | module.exports = { 40 | getWeather 41 | } -------------------------------------------------------------------------------- /electron/app/media/imgs/glasses/glass-circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shekit/peeqo/825872a41b3db46795d262fa5a986f082e3eafc0/electron/app/media/imgs/glasses/glass-circle.png -------------------------------------------------------------------------------- /electron/app/media/imgs/glasses/glass-pointy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shekit/peeqo/825872a41b3db46795d262fa5a986f082e3eafc0/electron/app/media/imgs/glasses/glass-pointy.png -------------------------------------------------------------------------------- /electron/app/media/imgs/glasses/glass-rayban.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shekit/peeqo/825872a41b3db46795d262fa5a986f082e3eafc0/electron/app/media/imgs/glasses/glass-rayban.png -------------------------------------------------------------------------------- /electron/app/media/imgs/glasses/glass-rectangle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shekit/peeqo/825872a41b3db46795d262fa5a986f082e3eafc0/electron/app/media/imgs/glasses/glass-rectangle.png -------------------------------------------------------------------------------- /electron/app/media/imgs/glasses/glass-regular.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shekit/peeqo/825872a41b3db46795d262fa5a986f082e3eafc0/electron/app/media/imgs/glasses/glass-regular.png -------------------------------------------------------------------------------- /electron/app/media/imgs/glasses/glass-square.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shekit/peeqo/825872a41b3db46795d262fa5a986f082e3eafc0/electron/app/media/imgs/glasses/glass-square.png -------------------------------------------------------------------------------- /electron/app/media/imgs/glasses/glass-star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shekit/peeqo/825872a41b3db46795d262fa5a986f082e3eafc0/electron/app/media/imgs/glasses/glass-star.png -------------------------------------------------------------------------------- /electron/app/media/responses/alarm/alarm.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shekit/peeqo/825872a41b3db46795d262fa5a986f082e3eafc0/electron/app/media/responses/alarm/alarm.mp4 -------------------------------------------------------------------------------- /electron/app/media/responses/bye/byebye.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shekit/peeqo/825872a41b3db46795d262fa5a986f082e3eafc0/electron/app/media/responses/bye/byebye.mp4 -------------------------------------------------------------------------------- /electron/app/media/responses/bye/take-care.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shekit/peeqo/825872a41b3db46795d262fa5a986f082e3eafc0/electron/app/media/responses/bye/take-care.mp4 -------------------------------------------------------------------------------- /electron/app/media/responses/confused/dont-know.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shekit/peeqo/825872a41b3db46795d262fa5a986f082e3eafc0/electron/app/media/responses/confused/dont-know.jpg -------------------------------------------------------------------------------- /electron/app/media/responses/confused/no-idea.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shekit/peeqo/825872a41b3db46795d262fa5a986f082e3eafc0/electron/app/media/responses/confused/no-idea.png -------------------------------------------------------------------------------- /electron/app/media/responses/confused/shrug.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shekit/peeqo/825872a41b3db46795d262fa5a986f082e3eafc0/electron/app/media/responses/confused/shrug.gif -------------------------------------------------------------------------------- /electron/app/media/responses/confused/shrug.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shekit/peeqo/825872a41b3db46795d262fa5a986f082e3eafc0/electron/app/media/responses/confused/shrug.mp4 -------------------------------------------------------------------------------- /electron/app/media/responses/confused/who-knows.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shekit/peeqo/825872a41b3db46795d262fa5a986f082e3eafc0/electron/app/media/responses/confused/who-knows.webp -------------------------------------------------------------------------------- /electron/app/media/responses/ok/ok.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shekit/peeqo/825872a41b3db46795d262fa5a986f082e3eafc0/electron/app/media/responses/ok/ok.mp4 -------------------------------------------------------------------------------- /electron/app/media/servo_anims/alert.json: -------------------------------------------------------------------------------- 1 | [ 2 | [ 3 | 1518, 4 | 1518, 5 | 1518 6 | ], 7 | [ 8 | 1518, 9 | 1518, 10 | 1518 11 | ], 12 | [ 13 | 1518, 14 | 1518, 15 | 1518 16 | ], 17 | [ 18 | 1518, 19 | 1518, 20 | 1518 21 | ], 22 | [ 23 | 1518, 24 | 1518, 25 | 1518 26 | ], 27 | [ 28 | 1518, 29 | 1518, 30 | 1518 31 | ], 32 | [ 33 | 1484, 34 | 1484, 35 | 1484 36 | ], 37 | [ 38 | 1427, 39 | 1427, 40 | 1427 41 | ], 42 | [ 43 | 1221, 44 | 1221, 45 | 1221 46 | ], 47 | [ 48 | 1221, 49 | 1221, 50 | 1221 51 | ], 52 | [ 53 | 1186, 54 | 1186, 55 | 1186 56 | ], 57 | [ 58 | 1186, 59 | 1186, 60 | 1186 61 | ], 62 | [ 63 | 1118, 64 | 1118, 65 | 1118 66 | ], 67 | [ 68 | 1118, 69 | 1118, 70 | 1118 71 | ], 72 | [ 73 | 1118, 74 | 1118, 75 | 1118 76 | ], 77 | [ 78 | 1118, 79 | 1118, 80 | 1118 81 | ], 82 | [ 83 | 1118, 84 | 1118, 85 | 1118 86 | ], 87 | [ 88 | 1118, 89 | 1118, 90 | 1118 91 | ], 92 | [ 93 | 1118, 94 | 1118, 95 | 1118 96 | ], 97 | [ 98 | 1118, 99 | 1118, 100 | 1118 101 | ], 102 | [ 103 | 1118, 104 | 1118, 105 | 1118 106 | ], 107 | [ 108 | 1118, 109 | 1118, 110 | 1118 111 | ], 112 | [ 113 | 1118, 114 | 1118, 115 | 1118 116 | ], 117 | [ 118 | 1118, 119 | 1118, 120 | 1118 121 | ], 122 | [ 123 | 1118, 124 | 1118, 125 | 1118 126 | ], 127 | [ 128 | 1118, 129 | 1118, 130 | 1118 131 | ], 132 | [ 133 | 1118, 134 | 1118, 135 | 1118 136 | ], 137 | [ 138 | 1118, 139 | 1118, 140 | 1118 141 | ], 142 | [ 143 | 1118, 144 | 1118, 145 | 1118 146 | ], 147 | [ 148 | 1152, 149 | 1152, 150 | 1152 151 | ], 152 | [ 153 | 1186, 154 | 1186, 155 | 1186 156 | ], 157 | [ 158 | 1221, 159 | 1221, 160 | 1221 161 | ], 162 | [ 163 | 1255, 164 | 1255, 165 | 1255 166 | ], 167 | [ 168 | 1278, 169 | 1278, 170 | 1278 171 | ], 172 | [ 173 | 1312, 174 | 1312, 175 | 1312 176 | ], 177 | [ 178 | 1346, 179 | 1346, 180 | 1346 181 | ], 182 | [ 183 | 1369, 184 | 1369, 185 | 1369 186 | ], 187 | [ 188 | 1404, 189 | 1404, 190 | 1404 191 | ], 192 | [ 193 | 1427, 194 | 1427, 195 | 1427 196 | ], 197 | [ 198 | 1461, 199 | 1461, 200 | 1461 201 | ], 202 | [ 203 | 1461, 204 | 1461, 205 | 1461 206 | ], 207 | [ 208 | 1484, 209 | 1484, 210 | 1484 211 | ], 212 | [ 213 | 1484, 214 | 1484, 215 | 1484 216 | ], 217 | [ 218 | 1484, 219 | 1484, 220 | 1484 221 | ] 222 | ] -------------------------------------------------------------------------------- /electron/app/media/servo_anims/jiggle.json: -------------------------------------------------------------------------------- 1 | [[1518,1518,1518],[1518,1518,1518],[1518,1518,1518],[1518,1518,1518],[1518,1518,1518],[1518,1518,1518],[1518,1518,1518],[1518,1518,1518],[1518,1518,1518],[1518,1518,1518],[1575,1518,1449],[1644,1518,1381],[1644,1518,1381],[1713,1518,1312],[1781,1518,1243],[1781,1518,1243],[1713,1518,1312],[1518,1518,1518],[1312,1518,1713],[1312,1518,1713],[1312,1518,1713],[1449,1518,1575],[1575,1518,1449],[1575,1518,1449],[1575,1518,1449],[1575,1518,1449],[1575,1518,1449],[1518,1518,1518],[1312,1518,1713],[1312,1518,1713],[1312,1518,1713],[1518,1518,1518],[1644,1518,1381],[1644,1518,1381],[1644,1518,1381],[1449,1518,1575],[1381,1518,1644],[1381,1518,1644],[1381,1518,1644],[1575,1518,1449],[1644,1518,1381],[1644,1518,1381],[1644,1518,1381],[1644,1518,1381],[1449,1518,1575],[1381,1518,1644],[1381,1518,1644],[1381,1518,1644]] -------------------------------------------------------------------------------- /electron/app/media/servo_anims/look-up-slow.json: -------------------------------------------------------------------------------- 1 | [[1518,1518,1518],[1518,1518,1518],[1518,1518,1518],[1518,1518,1518],[1518,1518,1518],[1518,1518,1518],[1518,1518,1518],[1518,1518,1518],[1552,1438,1552],[1552,1438,1552],[1552,1438,1552],[1552,1438,1552],[1552,1438,1552],[1552,1438,1552],[1587,1358,1587],[1587,1358,1587],[1621,1278,1621],[1667,1198,1667],[1667,1198,1667],[1701,1106,1701],[1736,1003,1736],[1736,1003,1736],[1736,1003,1736],[1781,866,1781],[1781,866,1781],[1781,866,1781],[1781,866,1781],[1781,866,1781],[1781,866,1781],[1781,866,1781],[1781,866,1781],[1781,866,1781],[1781,866,1781],[1781,866,1781],[1781,866,1781],[1781,866,1781],[1781,866,1781],[1781,866,1781],[1781,866,1781],[1781,866,1781],[1781,866,1781],[1781,866,1781],[1781,866,1781],[1781,866,1781],[1781,866,1781],[1781,866,1781],[1781,866,1781],[1781,866,1781],[1781,866,1781],[1781,866,1781],[1736,1003,1736],[1736,1003,1736],[1736,1003,1736],[1701,1106,1701],[1701,1106,1701],[1667,1198,1667],[1667,1198,1667],[1667,1198,1667],[1667,1198,1667],[1621,1278,1621],[1621,1278,1621],[1587,1358,1587],[1587,1358,1587],[1587,1358,1587],[1587,1358,1587],[1552,1438,1552],[1552,1438,1552],[1552,1438,1552],[1552,1438,1552],[1552,1438,1552],[1518,1518,1518],[1518,1518,1518],[1518,1518,1518],[1518,1518,1518]] -------------------------------------------------------------------------------- /electron/app/media/servo_anims/look-up.json: -------------------------------------------------------------------------------- 1 | [[1518,1518,1518],[1518,1518,1518],[1518,1518,1518],[1518,1518,1518],[1518,1518,1518],[1518,1518,1518],[1518,1518,1518],[1587,1358,1587],[1587,1358,1587],[1621,1278,1621],[1621,1278,1621],[1667,1198,1667],[1701,1106,1701],[1701,1106,1701],[1736,1003,1736],[1736,1003,1736],[1781,866,1781],[1781,866,1781],[1781,866,1781],[1781,866,1781],[1781,866,1781],[1781,866,1781],[1781,866,1781],[1781,866,1781],[1781,866,1781],[1781,866,1781],[1781,866,1781],[1781,866,1781],[1781,866,1781],[1781,866,1781],[1781,866,1781],[1781,866,1781],[1781,866,1781],[1781,866,1781],[1781,866,1781],[1781,866,1781],[1736,1003,1736],[1736,1003,1736],[1701,1106,1701],[1701,1106,1701],[1701,1106,1701],[1667,1198,1667],[1667,1198,1667],[1667,1198,1667],[1621,1278,1621],[1621,1278,1621],[1587,1358,1587],[1587,1358,1587],[1552,1438,1552],[1552,1438,1552],[1552,1438,1552],[1552,1438,1552],[1552,1438,1552],[1552,1438,1552],[1552,1438,1552],[1518,1518,1518],[1518,1518,1518],[1518,1518,1518],[1518,1518,1518],[1518,1518,1518],[1518,1518,1518],[1518,1518,1518],[1518,1518,1518],[1518,1518,1518],[1518,1518,1518],[1518,1518,1518]] -------------------------------------------------------------------------------- /electron/app/media/sounds/alert.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shekit/peeqo/825872a41b3db46795d262fa5a986f082e3eafc0/electron/app/media/sounds/alert.wav -------------------------------------------------------------------------------- /electron/main.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const {app, BrowserWindow } = require('electron') 4 | const os = require('os') 5 | 6 | 7 | var createWindow = () => { 8 | let mainWindow = new BrowserWindow({ 9 | width: 800, 10 | height: 480 11 | }) 12 | 13 | // display index.html 14 | mainWindow.loadURL('file://'+__dirname+'/app/index.html') 15 | 16 | if(os.arch() == 'arm'){ 17 | 18 | // For Raspberry Pi 19 | 20 | if(process.env.NODE_ENV == "debug"){ 21 | // open console only if NODE_ENV=debug is set 22 | mainWindow.webContents.openDevTools(); 23 | } 24 | 25 | // make application full screen 26 | mainWindow.setMenu(null); 27 | mainWindow.setFullScreen(true); 28 | mainWindow.maximize(); 29 | 30 | } else { 31 | 32 | // For Desktop OS - Mac, Windows, Linux 33 | 34 | // always open console on dev machine 35 | mainWindow.webContents.openDevTools(); 36 | 37 | } 38 | } 39 | 40 | app.on('ready', createWindow) 41 | 42 | app.on('window-all-closed', ()=>{ 43 | app.quit() 44 | }) -------------------------------------------------------------------------------- /electron/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "peeqo", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "main.js", 6 | "scripts": { 7 | "start": "electron .", 8 | "debug": "NODE_ENV=debug electron .", 9 | "rebuild": "electron-rebuild" 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "devDependencies": { 14 | "electron": "2.0.18", 15 | "electron-rebuild": "1.8.1" 16 | }, 17 | "dependencies": { 18 | "app-module-path": "^2.2.0", 19 | "dialogflow": "^0.8.0", 20 | "giphy-api": "^2.0.1", 21 | "i2c-bus": "^4.0.1", 22 | "node-record-lpcm16": "^0.3.1", 23 | "pca9685": "^4.0.3", 24 | "pi-spi": "^1.0.2", 25 | "rpi-gpio": "^2.1.3", 26 | "snapsvg": "^0.5.1", 27 | "snowboy": "1.2.0", 28 | "spotify-web-api-node": "^4.0.0", 29 | "through2": "^3.0.0", 30 | "zerorpc": "^0.9.7" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /python/zero.py: -------------------------------------------------------------------------------- 1 | import zerorpc 2 | from time import sleep 3 | import os 4 | import logging 5 | import atexit 6 | logging.basicConfig() 7 | 8 | isPi = False 9 | 10 | print(os.uname()) 11 | 12 | if os.uname()[4].lower().startswith("arm"): 13 | isPi = True 14 | 15 | if isPi: 16 | from picamera import PiCamera 17 | try: 18 | camera = PiCamera() 19 | camera.resolution = (800,480) 20 | except: 21 | print "There is no camera connected" 22 | 23 | def exit_handler(): 24 | if camera: 25 | camera.close() 26 | print "closing camera" 27 | 28 | atexit.register(exit_handler) 29 | # set pi camera settings 30 | #camera = PiCamera() 31 | #camera.resolution = (640, 480) 32 | 33 | full_path = os.path.realpath(__file__) 34 | 35 | # RPC class that can be called from node client 36 | class ControlRPC(object): 37 | 38 | def hello(self): 39 | print("connected") 40 | return "connected" 41 | 42 | def startCamera(self): 43 | if isPi and camera: 44 | camera.start_preview() 45 | 46 | print "start preview" 47 | 48 | def stopCamera(self): 49 | if isPi and camera: 50 | camera.stop_preview() 51 | print "stop preview" 52 | 53 | #def startRecording(self): 54 | #camera.start_recording(os.path.join(os.path.dirname(full_path),'gif.h264'), resize=(320,240)) 55 | 56 | #def stopRecording(self): 57 | #camera.stop_recording() 58 | 59 | # start zerorpc server and accept client connections at port 60 | s = zerorpc.Server(ControlRPC()) 61 | s.bind("tcp://0.0.0.0:4242") 62 | s.run() -------------------------------------------------------------------------------- /scripts/launch.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # starts bg script to access camera 4 | cd ~/peeqo/python 5 | python zero.py & 6 | 7 | 8 | cd ~/peeqo/electron 9 | ./node_modules/.bin/electron main.js --------------------------------------------------------------------------------