├── README.md
├── .eslintrc
├── config.js
├── cars
├── index.html
└── index.js
├── index.html
├── package.json
├── visual
├── visual-utils.js
└── vu.js
├── LICENSE
├── .gitignore
└── index.js
/README.md:
--------------------------------------------------------------------------------
1 | # train-brake-smell
2 |
3 | ## RPI
4 |
5 | `touch $HOME/.asoundrc`
6 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "airbnb-base",
3 | "rules": {
4 | "indent": ["error", 4]
5 | }
6 | }
--------------------------------------------------------------------------------
/config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | portals: {
3 | portal3bar: {
4 | PORT: 2342,
5 | LENGTH: 300,
6 | HOST: 'portal3.bar'
7 | },
8 | portal2bar: {
9 | PORT: 2342,
10 | LENGTH: 300,
11 | HOST: 'portal2.bar'
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/cars/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
15 |
16 |
17 |
18 | Laps: 0
19 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "train-brake-smell",
3 | "version": "0.2.0",
4 | "description": "open pixel control opc udp esp8266",
5 | "main": "index.js",
6 | "repository": "git@github.com:olso/train-brake-smell.git",
7 | "author": "olso ",
8 | "license": "MIT",
9 | "dependencies": {
10 | "blockchain.info": "^2.10.1",
11 | "commander": "^2.11.0",
12 | "cubic-bezier": "^0.1.2",
13 | "lodash": "^4.17.4",
14 | "mic": "git+https://github.com/lorenzoromagnoli/mic.git",
15 | "node-fetch": "^1.7.3",
16 | "opc-via-udp": "^1.0.6",
17 | "redux": "^3.7.2",
18 | "through2": "^2.0.3",
19 | "tinycolor2": "^1.4.1",
20 | "tinygradient": "^0.3.1",
21 | "vu-meter": "^0.1.0",
22 | "ws": "^3.2.0"
23 | },
24 | "devDependencies": {
25 | "eslint": "^4.9.0",
26 | "eslint-config-airbnb-base": "^12.1.0",
27 | "eslint-plugin-import": "^2.8.0"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/visual/visual-utils.js:
--------------------------------------------------------------------------------
1 | const tinycolor = require('tinycolor2')
2 | const tinygradient = require('tinygradient')
3 |
4 | function toRgbArray(rgbObject) {
5 | return [ rgbObject.r, rgbObject.g, rgbObject.b ]
6 | }
7 |
8 | function randomRgbColor() {
9 | return toRgbArray(tinycolor.random().toRgb())
10 | }
11 |
12 | function makeRgbGradientArray(startColor, endColor, steps) {
13 | const colors = tinygradient([startColor, endColor]).rgb(steps)
14 |
15 | return colors.map(color => toRgbArray(color.toRgb()))
16 | }
17 |
18 | function convertHexColorToRgbArray(color) {
19 | return toRgbArray(tinycolor(color).toRgb())
20 | }
21 |
22 | function fillPixelsWithSingleColor(stripLength = 1, color = randomRgbColor()) {
23 | return new Array(stripLength).fill(color)
24 | }
25 |
26 | function clearPixels(stripLength) {
27 | return fillPixelsWithSingleColor(stripLength, [0, 0, 0])
28 | }
29 |
30 | module.exports = {
31 | clearPixels,
32 | toRgbArray,
33 | convertHexColorToRgbArray,
34 | fillPixelsWithSingleColor,
35 | makeRgbGradientArray,
36 | randomRgbColor,
37 | }
38 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 ProgressBar HackerSpace
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 |
3 | # Logs
4 | logs
5 | *.log
6 | npm-debug.log*
7 | yarn-debug.log*
8 | yarn-error.log*
9 |
10 | # Runtime data
11 | pids
12 | *.pid
13 | *.seed
14 | *.pid.lock
15 |
16 | # Directory for instrumented libs generated by jscoverage/JSCover
17 | lib-cov
18 |
19 | # Coverage directory used by tools like istanbul
20 | coverage
21 |
22 | # nyc test coverage
23 | .nyc_output
24 |
25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
26 | .grunt
27 |
28 | # Bower dependency directory (https://bower.io/)
29 | bower_components
30 |
31 | # node-waf configuration
32 | .lock-wscript
33 |
34 | # Compiled binary addons (http://nodejs.org/api/addons.html)
35 | build/Release
36 |
37 | # Dependency directories
38 | node_modules/
39 | jspm_packages/
40 |
41 | # Typescript v1 declaration files
42 | typings/
43 |
44 | # Optional npm cache directory
45 | .npm
46 |
47 | # Optional eslint cache
48 | .eslintcache
49 |
50 | # Optional REPL history
51 | .node_repl_history
52 |
53 | # Output of 'npm pack'
54 | *.tgz
55 |
56 | # Yarn Integrity file
57 | .yarn-integrity
58 |
59 | # dotenv environment variables file
60 | .env
61 |
62 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | const { getSendPixels } = require('opc-via-udp')
2 | const WebSocket = require('ws')
3 | const { flatten } = require('lodash')
4 | const { clearPixels, fillPixelsWithSingleColor, randomRgbColor } = require('./visual/visual-utils')
5 | const { getVuMicPacketStream, virtualMeter, meter, easeIn } = require('./visual/vu')
6 |
7 | const portalConfig = require('./config').portals.portal3bar
8 |
9 | const sendPixels = getSendPixels({
10 | port: portalConfig.PORT,
11 | length: portalConfig.LENGTH,
12 | host: portalConfig.HOST
13 | })
14 |
15 | // easeIn(portalConfig.LENGTH, sendPixels)
16 |
17 | // const vuMicPacketStream = getVuMicPacketStream(portalConfig.LENGTH, virtualMeter)
18 | const vuMicPacketStream = getVuMicPacketStream(portalConfig.LENGTH, meter)
19 | vuMicPacketStream.on('data', pixels => sendPixels(flatten(pixels)))
20 |
21 | // const wss = new WebSocket.Server({ port: 8080 })
22 | // wss.on('connection', (ws) => {
23 | // ws.on('message', (message) => {
24 | // const pixels = fillPixelsWithSingleColor(portalConfig.LENGTH, [0, 0, 0])
25 | // const pixelPos = (message/100) * portalConfig.LENGTH
26 | //
27 | // for (let i = 0; i < 5; i++) {
28 | // pixels[pixelPos] = [255, 255, 255]
29 | // }
30 | // sendPixels(flatten(pixels))
31 | // })
32 | // })
33 |
--------------------------------------------------------------------------------
/visual/vu.js:
--------------------------------------------------------------------------------
1 | const { flatten } = require('lodash')
2 | const { createOpcPacket } = require('opc-via-udp')
3 | const { makeRgbGradientArray, fillPixelsWithSingleColor, convertHexColorToRgbArray, clearPixels, randomRgbColor } = require('./visual-utils')
4 | const VUmeter = require('vu-meter')
5 | const through2 = require('through2')
6 | const mic = require('mic')
7 | const _ = require('lodash')
8 | const bezier = require('cubic-bezier')
9 | const http = require('http')
10 | const fetch = require('node-fetch')
11 |
12 | function getVuBgPixels(stripLength) {
13 | const startColor = '#00FF00'
14 | const endColor = '#FF0000'
15 |
16 | return makeRgbGradientArray(startColor, endColor, stripLength)
17 | }
18 |
19 | function normalizeVolume(volume) {
20 | return volume > 100 ? 100 : Math.floor(volume)
21 | }
22 |
23 | function calcMicStrength(micData) {
24 | const magicNumber = 50; // 60
25 | const strength = (Math.max(micData + magicNumber, 0) / magicNumber) * 100;
26 |
27 | // return strength < 3 ? 0 : strength;
28 | return strength;
29 | }
30 |
31 | function calcNeededStripLength(stripLength, volume) {
32 | return Math.floor((stripLength)*(volume/100))
33 | }
34 |
35 | function meter(stripLength, rawVolume) {
36 | const volume = normalizeVolume(rawVolume)
37 | const pixels = getVuBgPixels(stripLength)
38 | const neededStripLength = calcNeededStripLength(stripLength, volume)
39 |
40 | pixels.fill([0, 0 ,0], neededStripLength, stripLength)
41 |
42 | return pixels;
43 | }
44 |
45 | function virtualMeter(stripLength, rawVolume) {
46 | const virtualLength = stripLength/2
47 | const volume = normalizeVolume(rawVolume)
48 | const pixels = getVuBgPixels(virtualLength)
49 | const neededStripLength = calcNeededStripLength(stripLength, volume)
50 |
51 | pixels.fill([0, 0, 0], neededStripLength, stripLength)
52 |
53 | return [ ...pixels, ...pixels.reverse() ]
54 | }
55 |
56 | function getMicVuOpcPipe(stripLength, meter) {
57 | return through2.obj({ objectMode: true }, (data, enc, cb) => {
58 |
59 | const micStrength = calcMicStrength(data[0])
60 | const pixels = meter(stripLength, micStrength)
61 |
62 | cb(null, pixels)
63 | })
64 | }
65 |
66 | function getVuMicPacketStream(stripLength, meter) {
67 | const vuMeter = new VUmeter()
68 | const micVuOpcPipe = getMicVuOpcPipe(stripLength, meter)
69 | const micInstance = mic({
70 | debug: false,
71 | rate: '32000',
72 | bitwidth: '16',
73 | buffer: 300,
74 | device: 'hw:1,0',
75 | })
76 | const micStream = micInstance.getAudioStream()
77 |
78 | micInstance.start()
79 |
80 | return micStream.pipe(vuMeter).pipe(micVuOpcPipe)
81 | }
82 |
83 | function easeIn(stripLength, sendPixels) {
84 | // const easeIn = bezier(0.42, 0, 1.0, 1.0, 1000)
85 | // const easeIn = bezier(0.455, 0, 0.515, 0.955)
86 | const easeIn = bezier(0, 0, 1, 1, 1000)
87 | for (let t = 0; t <= 1; t += 0.001) {
88 | const pixels = clearPixels(stripLength)
89 | const timing = Math.round(easeIn(t) * 100)
90 | const position = (stripLength / 100) * timing
91 | console.log(position)
92 | pixels[position] = [255, 255, 0];
93 | sendPixels(flatten(pixels))
94 | }
95 | }
96 |
97 |
98 | module.exports = {
99 | meter,
100 | virtualMeter,
101 | getVuMicPacketStream,
102 | easeIn
103 | }
104 |
--------------------------------------------------------------------------------
/cars/index.js:
--------------------------------------------------------------------------------
1 | const { flatten, isFunction } = require('lodash')
2 | const { getSendPixels } = require('opc-via-udp')
3 | const { randomRgbColor } = require('../visual/visual-utils')
4 | const WebSocketServer = require('ws').Server
5 | const { portal3bar, portal2bar } = require('../config').portals
6 | const { EventEmitter } = require('events')
7 |
8 | const TRACK_LENGTH = portal3bar.LENGTH + portal2bar.LENGTH
9 | const IS_PLAYING_TIMEOUT = 5000
10 |
11 | const sendPortal3Pixels = getSendPixels({
12 | port: portal3bar.PORT,
13 | length: portal3bar.LENGTH,
14 | host: portal3bar.HOST
15 | })
16 |
17 | const sendPortal2Pixels = getSendPixels({
18 | port: portal2bar.PORT,
19 | length: portal2bar.LENGTH,
20 | host: portal2bar.HOST
21 | })
22 |
23 | const DANGERS = [
24 | {
25 | start: 90,
26 | length: 20,
27 | triggerSpeed: 7,
28 | color: [255, 0, 0],
29 | },
30 | {
31 | start: 190,
32 | length: 20,
33 | triggerSpeed: 8,
34 | color: [255, 0, 0],
35 | },
36 | {
37 | start: 390,
38 | length: 20,
39 | triggerSpeed: 7,
40 | color: [255, 0, 0],
41 | },
42 | {
43 | start: 440,
44 | length: 20,
45 | triggerSpeed: 8,
46 | color: [255, 0, 0],
47 | },
48 | {
49 | start: 490,
50 | length: 20,
51 | triggerSpeed: 8,
52 | color: [255, 0, 0],
53 | }
54 | ]
55 | let cars = []
56 |
57 | class Car extends EventEmitter {
58 | constructor(props) {
59 | super(props)
60 | const { id, color } = props
61 |
62 | this.id = id
63 | this.color = color
64 | this.health = 15
65 | this.position = 0
66 | this.lap = 0
67 | this.acceleration = 2
68 | this.speed = 0
69 | this.speedDecay = 0.98
70 | this.isBeingKilled = false
71 | this.maxSpeed = 8
72 | this.lastReceivedActionAt = Date.now()
73 | }
74 |
75 | isMoving() {
76 | return this.speed > 0
77 | }
78 |
79 | getPosition() {
80 | return Math.round(this.position)
81 | }
82 |
83 | updatePosition() {
84 | this.position += this.speed
85 |
86 | if (this.position > TRACK_LENGTH) {
87 | this.position = 0
88 | this.lap++
89 | this.emit('lap-change')
90 | }
91 |
92 | return this
93 | }
94 |
95 | shouldBeKicked() {
96 | let should = false
97 |
98 | DANGERS.forEach((danger) => {
99 | if (this.position >= danger.start &&
100 | this.position <= (danger.start + danger.length)) {
101 | if (this.speed > danger.triggerSpeed) {
102 | should = true
103 | }
104 | }
105 | })
106 |
107 | return should
108 | }
109 |
110 | takeHealth() {
111 | this.health -= 1
112 | }
113 |
114 | killCar() {
115 | if (this.isBeingKilled) {
116 | return
117 | }
118 |
119 | this.isBeingKilled = true
120 |
121 | const stop = setInterval(() => {
122 | this.takeHealth()
123 |
124 | if (this.health < 0) {
125 | if (isFunction(stop)) {
126 | stop()
127 | console.log('Killed car')
128 | }
129 | }
130 | }, 100)
131 | }
132 |
133 | isDead() {
134 | return this.health <= 0
135 | }
136 |
137 | receivedAction() {
138 | this.lastReceivedActionAt = Date.now()
139 | }
140 |
141 | isPlaying() {
142 | return (Date.now() - this.lastReceivedActionAt) < IS_PLAYING_TIMEOUT
143 | }
144 |
145 | accelerate() {
146 | this.receivedAction()
147 |
148 | if (this.speed > this.maxSpeed) {
149 | return this
150 | }
151 |
152 | if (this.speed === 0) {
153 | this.speed = 0.5
154 | }
155 |
156 | if (this.speed > 0) {
157 | this.speed *= this.acceleration
158 | }
159 |
160 | return this
161 | }
162 |
163 | decelarate() {
164 | if (this.speed < 0.1) {
165 | this.speed = 0
166 |
167 | return this
168 | }
169 |
170 | this.speed *= this.speedDecay
171 |
172 | return this
173 | }
174 | }
175 |
176 | function getCar(id) {
177 | return cars.find(car => car.id === id)
178 | }
179 |
180 | function addCar(id, color) {
181 | const car = new Car({ id, color })
182 | cars.push(car)
183 | return car
184 | }
185 |
186 | function removeCar(id) {
187 | cars = cars.filter(car => car.id !== id)
188 | }
189 |
190 | function step(car) {
191 | if (!car.isMoving()) {
192 | car.speed = 0
193 | } else {
194 | car.decelarate()
195 | }
196 |
197 | car.updatePosition()
198 |
199 | if (car.shouldBeKicked()) {
200 | car.takeHealth()
201 | car.position = 0
202 | }
203 |
204 | if (car.isDead()) {
205 | removeCar(car.id)
206 | }
207 |
208 | if (!car.isPlaying()) {
209 | car.killCar()
210 | }
211 | }
212 |
213 | function drawDangers(pixels) {
214 | DANGERS.forEach((danger) => {
215 | for (let i = danger.start; i <= (danger.start + danger.length); i++) {
216 | pixels[i] = danger.color
217 | }
218 | })
219 |
220 | return pixels
221 | }
222 |
223 | function getPixels() {
224 | let pixels = new Array(TRACK_LENGTH).fill([0, 0, 0])
225 |
226 | pixels = drawDangers(pixels)
227 |
228 | cars.forEach((car) => {
229 | const pos = car.getPosition()
230 |
231 | for (let i = 0; i < car.health; i++) {
232 | pixels[pos + i] = car.color
233 | }
234 | })
235 |
236 | return pixels
237 | }
238 |
239 | function updateGame() {
240 | cars.forEach(car => step(car))
241 | // console.log(cars.length)
242 | }
243 |
244 | const SV_TICK_RATE = 10
245 | setInterval(() => {
246 | updateGame()
247 | const pixels = flatten(getPixels())
248 | const middle = pixels.length / 2
249 | const firstHalf = pixels.slice(0, middle)
250 | const secondHalf = pixels.slice(middle, pixels.length)
251 |
252 | sendPortal3Pixels(firstHalf)
253 | sendPortal2Pixels(secondHalf)
254 | }, SV_TICK_RATE)
255 |
256 | const wss = new WebSocketServer({ port: 1337 })
257 | wss.on('connection', (ws) => {
258 | const id = ws._ultron.id
259 |
260 | const mainCar = addCar(id, randomRgbColor())
261 |
262 | ws.on('message', (data) => {
263 | const car = getCar(id)
264 |
265 | if (car) {
266 | car.accelerate()
267 | }
268 | })
269 |
270 | const handleLapChange = () => {
271 | const car = getCar(id)
272 | if (car) {
273 | ws.send(car.lap, (e) => {
274 | if (e) {
275 | console.error('ERR:', e.message)
276 | }
277 | })
278 | }
279 | }
280 | mainCar.on('lap-change', handleLapChange)
281 |
282 | })
283 |
--------------------------------------------------------------------------------