const { QLog, undefinedOrNull } = require('quanto-commons'); const player = require('node-wav-player'); const TelegramBot = require('node-telegram-bot-api'); const fs = require('fs'); const { exec } = require("child_process"); if(process.platform !== 'darwin') var gpio = require('rpi-gpio'); const telegramBotToken = '5472909337:AAGH269wNGa9u99ekJqHqXEHfi0knpV7IFU'; //const groupid='@HotSoup'; const groupid='-1001703800044'; const GlobalLog = QLog.scope('Global'); const TelLog = QLog.scope('Telegram'); GlobalLog.enableLogs(['debug']); TelLog.enableLogs(['debug']); GlobalLog.headPadding = 30; TelLog.headPadding = 30; // const bot = new Telegraf(telegramBotToken); const bot = new TelegramBot(telegramBotToken, {polling: true}); // debounce der buttons in ms const bounceMS = 800; // Game Reset Start after ms const startReset = 4000; // ist an wenn im kind mode (erw LED is aus) const childLED = 37; // ist and wenn im erwachsenen Mode (kind LED ist aus) const erwLED = 38; // status : ist an wenn in ruhe position, blinkt, wenn gestartet, ist aus bei fail const statusLED = 40; // setze auf HIGH wenn fail (klingel relais) const failSwitch = 35; // warte auf spielstart wenn auf HIGH const startButton = 32; const startButton_GPIO = 12; // beende spiel wenn auf HIGH // const finishButton = 31; const finishButton = 22; const finishButton_GPIO = 25; // faile spiel wenn auf HIGH // const failButton = 29; const failButton = 21; const failButton_GPIO = 9; // wechsle kind/erwachen Modus + LED const toggleButton = 33; const toggleButton_GPIO = 13; // wird je nach modus gesetzt und abgespielt wenn start const backgroundGrown = "jaws.wav"; const jeopardy = "jeopardy.wav"; const backgroundChild = "northsouth.wav"; // wird zufallig zu zufaelligen momenten abgespielt (erw mode) const erschreckSounds = [ "ziege.wav", "scream.wav", "laugh.wav", "cavallerie.wav", "gulp.wav" ]; // wird nach fester zeit + random oder spaeter nach der besten zeit abgespielt (erw mode) // const stresserSound = "sharks.wav"; // wird bei fail abgespielt (zonk) const failSound = "zonk.wav"; // hurra wird bei finish abgespielt const finishSound = "kidscheering.wav"; const GROWN=1; const CHILD=2; // LOW EDGE let preStart = true; // game running? let isRunning = false; const STATES = { STARTED: 'started', FINISH: 'finshed', FAIL: 'failed' }; let best; const bounce = {}; var STATE = STATES.FINISH; var MODE = GROWN; var GAME = { events: [], runTime: 0 }; var chatId; function sendBotGroupImg(chat_id, photo, caption) { GlobalLog.info( chat_id, photo); bot.sendPhoto(chat_id, photo, { caption: caption }); } function sendBotMsg(msg) { if(chatId) { bot.sendMessage(chatId, msg); } } function setup() { // bot.start((ctx) => { // let message = `Bot aktiv und beobachtet. Benutze /stats um die Bestenliste zu sehen`; // ctx.reply(message); // TelLog.note('Bot started'); // botCTX=ctx; // }); // bot.command('stats', async (ctx) => { // await ctx.replyWithPhoto({ source: '/root/hotwire-js/img/bestgrown.png' }); // await ctx.reply(`Bester Erwachsener mit nur ${best.grown} sek`); // await ctx.replyWithPhoto({ source: '/root/hotwire-js/img/bestchild.png' }) // await ctx.reply(`Bestes Kind mit nur ${best.child} sek`); // }); // bot.launch(); fs.readFile('stats.json', 'utf8', (err, res) => { if(err) { GlobalLog.info("No stats found. Using defaults"); best = { grown: 25, child: 37 } } else { GlobalLog.info("Stats loaded."); best = JSON.parse(res); } }); GlobalLog.note('Setting up GPIOs'); if(process.platform === 'darwin') { setTimeout(start, 500); setTimeout(fail, 3500); setTimeout(start, 4000); setTimeout(finish, 9000); setTimeout(fail, 10000); setTimeout(finish, 11000); return; } gpio.setup(failSwitch, gpio.OUT, () => { gpio.output(failSwitch, false); }); gpio.setup(statusLED, gpio.OUT, () => { gpio.output(statusLED, true); }); gpio.setup(childLED, gpio.OUT, () => { gpio.output(childLED, false); }); gpio.setup(erwLED, gpio.OUT, () => { gpio.output(erwLED, true); }); gpio.setup(finishButton, gpio.DIR_IN, gpio.EDGE_FALLING, () => { exec(`raspi-gpio set ${finishButton_GPIO} pu`); }); gpio.setup(toggleButton, gpio.DIR_IN, gpio.EDGE_FALLING, () => { exec(`raspi-gpio set ${toggleButton_GPIO} pu`); }); gpio.setup(startButton, gpio.DIR_IN, gpio.EDGE_BOTH, () => { exec(`raspi-gpio set ${startButton_GPIO} pu`); }); gpio.setup(failButton, gpio.DIR_IN, gpio.EDGE_FALLING, () => { exec(`raspi-gpio set ${failButton_GPIO} pu`); }); gpio.on('change', (channel, value) => { if(bounce[channel] === true) return; GlobalLog.debug('Change',channel, value); bounce[channel] = true; setTimeout(() => { bounce[channel] = false; GlobalLog.debug('Debounced', channel); }, bounceMS); switch(channel) { case startButton: start(value); break; case finishButton: finish(value); break; case failButton: fail(value); break; case toggleButton: toggle(value); break; default: GlobalLog.warn('Alert! Unknown button change',channel); } }); GlobalLog.note('Done. Game logic started. Waiting for input'); } function gameTimer() { GlobalLog.debug('game timer called'); GAME.events.forEach((el) => { if(el.start === GAME.runTime) { GlobalLog.debug('Event :', el); playSound(el.sound); } }); GAME.runTime++; GAME.led = !GAME.led; gpio.output(statusLED, GAME.led); } function failTimer() { GAME.led = !GAME.led; gpio.output(statusLED, GAME.led); gpio.output(childLED, GAME.led); gpio.output(erwLED, GAME.led); } function successTimer() { GAME.led = !GAME.led; gpio.output(statusLED, GAME.led); gpio.output(childLED, !GAME.led); gpio.output(erwLED, GAME.led); } function getRandomInt(max) { return Math.floor(Math.random() * max); } function start(value) { if(preStart === false || isRunning) { GlobalLog.note('Prestart detect.'); preStart = true; if(GAME.startplayer) GAME.startplayer.stop(); if(GAME.failTimer) clearInterval(GAME.failTimer); if(GAME.successTimer) clearInterval(GAME.successTimer); if(GAME.countTimer) clearInterval(GAME.countTimer) if(player) player.stop(); if(MODE === GROWN) { gpio.output(childLED, false); gpio.output(erwLED, true); } else { gpio.output(childLED, true); gpio.output(erwLED, false); } if(preStart === false) return; } if(isRunning === true) { GlobalLog.note('Game reset.'); if(GAME.startplayer) GAME.startplayer.stop(); if(player) player.stop(); STATE = STATES.FINISH; clearInterval(GAME.countTimer) preStart = false; isRunning = false; if(GAME.failTimer) clearInterval(GAME.failTimer); if(GAME.successTimer) clearInterval(GAME.successTimer); gpio.output(statusLED, true); GAME = { events: [], runTime: 0 } } if(GAME.failTimer) clearInterval(GAME.failTimer); if(GAME.successTimer) clearInterval(GAME.successTimer); if(MODE === GROWN) { gpio.output(erwLED, true); } else { gpio.output(childLED, true); } GAME.runTime = 0; if(STATE === STATES.FINISH || STATE === STATES.FAIL) { STATE = STATES.STARTED; // set all params for next game, then start counter and event timer if(MODE === GROWN) { if(getRandomInt(7) > 2) { GAME.events.push({ sound: jeopardy, start: 1}); } else { GAME.events.push({ sound: backgroundGrown, start: 1}); } GAME.events.push({ sound: erschreckSounds[getRandomInt(5)], start: 15+getRandomInt(10) }); GAME.events.push({ sound: erschreckSounds[getRandomInt(5)], start: 30+getRandomInt(10) }); GAME.events.push({ sound: erschreckSounds[getRandomInt(5)], start: 50+getRandomInt(10) }); GAME.mode = GROWN; GAME.fail = failSound; GAME.finish = finishSound; GAME.runtime = 0; } if(MODE === CHILD) { GAME.events.push({ sound: backgroundChild, start: 0}); GAME.events.push({ sound: erschreckSounds[getRandomInt(2)], start: 30+getRandomInt(10)}); GAME.mode = CHILD; GAME.fail = failSound; GAME.finish = finishSound; GAME.runtime = 0; } GlobalLog.note('Starting new game', JSON.stringify(GAME, 2, null)); isRunning = true; GAME.countTimer = setInterval(gameTimer, 1000); sendBotMsg('new game startet'); } else { GlobalLog.warn('START not in state', STATE); } } function finish(value) { GlobalLog.debug('Finish'); if(STATE === STATES.STARTED) { GlobalLog.note('You made it in', GAME.runTime, 'seconds'); GlobalLog.note(GAME); player.stop(); playSound(GAME.finish); STATE = STATES.FINISH; clearInterval(GAME.countTimer) GAME.successTimer = setInterval(failTimer, 600); GlobalLog.note('Getting new image!') exec(`fswebcam -r 1024x768 /tmp/last.png`, (err) => { let rt = GAME.runTime; if(err) { GlobalLog.error('Could not get image', err); } GAME.events = []; var msg; var img; if(MODE === GROWN) { if(GAME.runTime < best.grown) { best.grown = rt; msg = `Gewinner in nur ${rt} Sekunden!\nNeue Bestzeit bei den Erwachsenen!`; fs.copyFile('/tmp/last.png', '/tmp/bestgrown.png', (err) => {}); } else { msg = `Gewinner in nur ${rt} Sekunden!`; } } else { if(GAME.runTime < best.child) { sendBotMsg(`Neue Bestzeit bei den Kindern!`); best.child = rt; msg = `Gewinner in nur ${rt} Sekunden!\nNeue Bestzeit bei den Kindern!`; fs.copyFile('/tmp/last.png', '/tmp/bestchild.png', (err) => {}); } else { msg = `Gewinner in nur ${rt} Sekunden!`; } } sendBotGroupImg(groupid, '/tmp/last.png', msg); GAME.led = true; }); preStart = false; isRunning = false; GAME = { events: [], runTime: 0 } } else { GlobalLog.warn('FINISH not in state', STATE); } // exec(`killall aplay`); } function fail(value) { if(STATE === STATES.STARTED) { GlobalLog.note('You failed after', GAME.runTime, 'seconds'); GAME.startplayer.stop(); player.stop(); playSound(GAME.fail); STATE = STATES.FAIL; clearInterval(GAME.countTimer) GAME.failTimer = setInterval(failTimer, 100); GAME.events = []; preStart = false; isRunning = false; GAME = { events: [], runTime: 0 } } else { GlobalLog.debug('FAIL not in state', STATE); } } function toggle(value) { if(STATE === STATES.FINISH || STATE === STATES.FAIL) { if(GAME.failTimer) clearInterval(GAME.failTimer); if(GAME.successTimer) clearInterval(GAME.successTimer); if(MODE === GROWN) { gpio.output(erwLED, true); } else { gpio.output(childLED, true); } GlobalLog.info('Toggle, old', MODE); if(MODE === CHILD) { MODE = GROWN; gpio.output(childLED, false); gpio.output(erwLED, true); // set all values for GROWN mode } else { MODE = CHILD; gpio.output(childLED, true); gpio.output(erwLED, false); // set all values for CHILD mode } } else { GlobalLog.debug('Toggle not in state', STATE); } } function playSound(file, runTime){ if(!file) { GlobalLog.error('Sound not defined.'); return; } GlobalLog.info("Play sound : ", file); if(GAME.playing) { GlobalLog.debug("Stopping old soundfile", GAME.playing); player.stop(); } // Do not stop background sound if(GAME.runTime > 1) { GAME.playing = file; } else { GAME.startplayer = player; } player.play({ path: `wav/${file}`, sync: true }).then(() => { delete GAME.playing }) } setup(); TelLog.start('Starting bot...'); bot.on('message', function (msg) { const chatId = msg.chat.id; const resp = msg; TelLog.info(`${chatId}: ${resp.text}`); if(resp.text === '/stats') { TelLog.info(`Dump stats to channel ${groupid}`); if (fs.existsSync('/tmp/bestchild.png')) { sendBotGroupImg(groupid, '/tmp/bestchild.png', `Bestes Kind mit ${best.child} Sekunden.`); } if (fs.existsSync('/tmp/bestgrown.png')) { sendBotGroupImg(groupid, '/tmp/bestgrown.png', `Bester Erwachsener mit ${best.grown} Sekunden.`); } } });