├── .gitignore
├── ServoSimulator
├── anims
│ └── .gitignore
├── corrected-anims
│ └── .gitignore
├── format_anim_json.js
├── Platform.pde
└── ServoSimulator.pde
├── electron
├── app
│ ├── config
│ │ ├── common.res
│ │ ├── dialogflow-agent.zip
│ │ └── config-dev.js
│ ├── media
│ │ ├── sounds
│ │ │ └── alert.wav
│ │ ├── responses
│ │ │ ├── ok
│ │ │ │ └── ok.mp4
│ │ │ ├── alarm
│ │ │ │ └── alarm.mp4
│ │ │ ├── bye
│ │ │ │ ├── byebye.mp4
│ │ │ │ └── take-care.mp4
│ │ │ └── confused
│ │ │ │ ├── shrug.gif
│ │ │ │ ├── shrug.mp4
│ │ │ │ ├── no-idea.png
│ │ │ │ ├── dont-know.jpg
│ │ │ │ └── who-knows.webp
│ │ ├── imgs
│ │ │ └── glasses
│ │ │ │ ├── glass-star.png
│ │ │ │ ├── glass-circle.png
│ │ │ │ ├── glass-pointy.png
│ │ │ │ ├── glass-rayban.png
│ │ │ │ ├── glass-regular.png
│ │ │ │ ├── glass-square.png
│ │ │ │ └── glass-rectangle.png
│ │ └── servo_anims
│ │ │ ├── jiggle.json
│ │ │ ├── look-up.json
│ │ │ ├── look-up-slow.json
│ │ │ └── alert.json
│ ├── js
│ │ ├── events
│ │ │ ├── events.js
│ │ │ └── listeners.js
│ │ ├── senses
│ │ │ ├── text.js
│ │ │ ├── mic.js
│ │ │ ├── buttons.js
│ │ │ ├── camera.js
│ │ │ ├── speak.js
│ │ │ ├── listen.js
│ │ │ ├── servo.js
│ │ │ └── leds.js
│ │ ├── power
│ │ │ └── power.js
│ │ ├── face
│ │ │ ├── glasses.js
│ │ │ └── eyes.js
│ │ ├── skills
│ │ │ ├── timer.js
│ │ │ └── weather.js
│ │ ├── intent-engines
│ │ │ ├── dialogflow-intents.js
│ │ │ └── dialogflow.js
│ │ ├── actions
│ │ │ └── actions.js
│ │ ├── responses
│ │ │ └── responses.js
│ │ ├── global.js
│ │ ├── helpers
│ │ │ ├── media.js
│ │ │ └── common.js
│ │ └── lib
│ │ │ └── dotstar.js
│ ├── index.html
│ └── css
│ │ └── style.css
├── .gitignore
├── package.json
└── main.js
├── scripts
└── launch.sh
├── README.md
└── python
└── zero.py
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 |
--------------------------------------------------------------------------------
/ServoSimulator/anims/.gitignore:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/ServoSimulator/corrected-anims/.gitignore:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/electron/app/config/common.res:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shekit/peeqo/HEAD/electron/app/config/common.res
--------------------------------------------------------------------------------
/electron/app/media/sounds/alert.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shekit/peeqo/HEAD/electron/app/media/sounds/alert.wav
--------------------------------------------------------------------------------
/electron/app/config/dialogflow-agent.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shekit/peeqo/HEAD/electron/app/config/dialogflow-agent.zip
--------------------------------------------------------------------------------
/electron/app/media/responses/ok/ok.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shekit/peeqo/HEAD/electron/app/media/responses/ok/ok.mp4
--------------------------------------------------------------------------------
/electron/app/media/responses/alarm/alarm.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shekit/peeqo/HEAD/electron/app/media/responses/alarm/alarm.mp4
--------------------------------------------------------------------------------
/electron/app/media/responses/bye/byebye.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shekit/peeqo/HEAD/electron/app/media/responses/bye/byebye.mp4
--------------------------------------------------------------------------------
/electron/app/media/imgs/glasses/glass-star.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shekit/peeqo/HEAD/electron/app/media/imgs/glasses/glass-star.png
--------------------------------------------------------------------------------
/electron/app/media/responses/bye/take-care.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shekit/peeqo/HEAD/electron/app/media/responses/bye/take-care.mp4
--------------------------------------------------------------------------------
/electron/app/media/responses/confused/shrug.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shekit/peeqo/HEAD/electron/app/media/responses/confused/shrug.gif
--------------------------------------------------------------------------------
/electron/app/media/responses/confused/shrug.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shekit/peeqo/HEAD/electron/app/media/responses/confused/shrug.mp4
--------------------------------------------------------------------------------
/electron/app/media/imgs/glasses/glass-circle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shekit/peeqo/HEAD/electron/app/media/imgs/glasses/glass-circle.png
--------------------------------------------------------------------------------
/electron/app/media/imgs/glasses/glass-pointy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shekit/peeqo/HEAD/electron/app/media/imgs/glasses/glass-pointy.png
--------------------------------------------------------------------------------
/electron/app/media/imgs/glasses/glass-rayban.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shekit/peeqo/HEAD/electron/app/media/imgs/glasses/glass-rayban.png
--------------------------------------------------------------------------------
/electron/app/media/imgs/glasses/glass-regular.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shekit/peeqo/HEAD/electron/app/media/imgs/glasses/glass-regular.png
--------------------------------------------------------------------------------
/electron/app/media/imgs/glasses/glass-square.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shekit/peeqo/HEAD/electron/app/media/imgs/glasses/glass-square.png
--------------------------------------------------------------------------------
/electron/app/media/responses/confused/no-idea.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shekit/peeqo/HEAD/electron/app/media/responses/confused/no-idea.png
--------------------------------------------------------------------------------
/electron/app/media/imgs/glasses/glass-rectangle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shekit/peeqo/HEAD/electron/app/media/imgs/glasses/glass-rectangle.png
--------------------------------------------------------------------------------
/electron/app/media/responses/confused/dont-know.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shekit/peeqo/HEAD/electron/app/media/responses/confused/dont-know.jpg
--------------------------------------------------------------------------------
/electron/app/media/responses/confused/who-knows.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shekit/peeqo/HEAD/electron/app/media/responses/confused/who-knows.webp
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/electron/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 | package-lock.json
4 | .env
5 | npm-debug.log
6 | build
7 | app/config/config.js
8 | app/config/dialogflow.json
9 | app/config/Peeqo.pmdl
10 |
--------------------------------------------------------------------------------
/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/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/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/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/index.html:
--------------------------------------------------------------------------------
1 |
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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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/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/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/css/style.css:
--------------------------------------------------------------------------------
1 | /* reset */
2 | html, body, div {
3 | padding: 0;
4 | margin: 0;
5 | overflow: hidden;
6 | }
7 |
8 | /* wrapper class for all major divs */
9 | .wrapper {
10 | display: none;
11 | position: absolute;
12 | top:0;
13 | left: 0;
14 | height: 480px; /* height defined in main.js*/
15 | width: 800px; /* width defined in main.js*/
16 | overflow: none;
17 | }
18 |
19 | #gifWrapper, #videoWrapper {
20 | background: #000;
21 | }
22 |
23 | /* video & gif display divs */
24 | #video, #gif{
25 | width: 800px;
26 | height: 480px;
27 | object-fit: cover;
28 | }
29 |
30 | /* svg animated eyes */
31 | #eyeWrapper #eyes {
32 | width: 100%;
33 | height: 100%;
34 | background: white;
35 | }
36 |
37 | /* peeqo glasses */
38 | #eyeWrapper #glasses {
39 | position: absolute;
40 | top:0;
41 | left: 0;
42 | z-index: 10;
43 | }
44 |
45 | #textWrapper {
46 | display: block;
47 | }
48 |
49 | /* text overlayed on screen */
50 | #textOverlay {
51 | font-weight: bold;
52 | font-family: 'Helvetica','sans-serif';
53 | position: absolute;
54 | color: #fff;
55 | -webkit-text-stroke-width: 3px;
56 | -webkit-text-stroke-color: black;
57 | font-size: 60px;
58 | z-index:10000;
59 | top:50%;
60 | left:50%;
61 | text-align: center;
62 | transform: translate(-50%,-50%);
63 | pointer-events: none;
64 | text-transform: capitalize;
65 | }
66 |
67 | /* Debug hotword link */
68 | #wakeword{
69 | position: absolute;
70 | z-index: 100;
71 | }
--------------------------------------------------------------------------------
/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()
--------------------------------------------------------------------------------
/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/config/config-dev.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | giphy:{
3 | key:'', // get your own from https://developers.giphy.com/docs/
4 | max_gif_size: 800000, // max gif size it should try to download
5 | max_mp4_size: 700000 // max video size it should try to download
6 | },
7 | speech: {
8 | projectId: 'peeqo', // your dialogflow project name
9 | dialogflowKey: 'dialogflow.json', // *.json - name of your dialogflow key file - should be stored in app/config/
10 | wakeword: "peeqo", // you can change this wakeword if you record a differnt one on snowboy.kitt.ai
11 | language: "en-US", // find supported language codes - https://cloud.google.com/dialogflow-enterprise/docs/reference/language
12 | model: "Peeqo.pmdl", // The name of your model - name model downloaded from snowboy.kitt.ai - should be stored in app/config
13 | sensitivity: 0.5, // Keyword getting too many false positives or not detecting? Change this.
14 | continuous: false // After a keyword is detected keep listening until speech is not heard
15 | },
16 | fileExtensions: [".gif", ".mp4", ".webp"], // list of supported file types
17 | server: "", //"http://localhost:3000"
18 | openweather: {
19 | key: "", // please get api key from https://openweathermap.org/api
20 | city: 'New York' // default city to search - change it to your city of choice
21 | },
22 | spotify:{
23 | clientId:"", // get from https://developer.spotify.com/dashboard/applications
24 | clientSecret:""
25 | },
26 | vlipsy:{
27 | key:"" // request for api key by emailing api@vlipsy.com
28 | }
29 | }
--------------------------------------------------------------------------------
/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{
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/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/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/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/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/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/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/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/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/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/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/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/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/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------