diff --git a/.gitignore b/.gitignore index 65d326a70304165cbb700f42fe7d27b13ba926e6..9055c5a38dc53cc269bcc45cbe7c3632ad433bcd 100644 --- a/.gitignore +++ b/.gitignore @@ -2,10 +2,12 @@ node_modules/ build/ .cache/ .DS_Store +.idea *.log server/config.js front/src/config.js +scripts/nfc-poll-wrapper-dummy.sh # *.pub # *.key diff --git a/README.md b/README.md index f466ae2ff1b499d5bed52554e9240d25c5e1d444..f952a63a62f26ca2ce3c585fb7d8a5df3fa773d8 100644 --- a/README.md +++ b/README.md @@ -4,20 +4,29 @@ ## Installation ### Beginning -- Create user `hermod`: `adduser hermod` +- Create user `hermod`: (as root or `sudo`) `adduser hermod` - Put it in the same groups as pi (other than the `pi` group): `groups pi | sed 's/ /,/g'`, `usermod -a -G group1,group2,... hermod` -change `pi` and `root` passwords +- Change `pi` and `root` passwords - `sudo apt remove wolfram-engine minecraft-pi` - `sudo apt autoremove` - `sudo apt update && sudo apt upgrade && sudo reboot` - `sudo apt install git vim screen htop` -- `sudo date -s '2018-02-15 2:26:00'` if needed +- `sudo date -s '2018-02-15 2:26:00'` if needed (change the date to current date ofc) - Generate RSA key `ssh-keygen -t rsa -C "hermod.inno@gmail.com" -b 4096` and add it to gitlab - Clone project in `~`: `git clone git@gitlab.viarezo.fr:hermod/tv_panel.git` - `cp front/src/config.template.js front/src/config.js` - `cp server/config.template.js server/config.js` - Useful links: `ln -s ~/tv_panel/scripts/.screenrc ~/`, `ln -s ~/tv_panel/scripts/.vimrc ~/` +### Telegram message on statup to get IP address +- `cp scripts/waitForNetwork /root/scripts/waitForNetwork` +- `cp scripts/startup-telegram-message /root/scripts/startup-telegram-message` +- `sudo vim /root/scripts/startup-telegram-message` to add the token and chat id +- Add in root crontab (`sudo crontab -e`): +``` +@reboot sleep 1 && /root/scripts/waitForNetwork && /root/scripts/startup-telegram-message +``` + ### Boot config `vim /boot/config.txt` to comment out `dtparam=audio=on` -> `#dtparam=audio=on` and add ``` @@ -50,14 +59,14 @@ After reboot or log out - log in, `npm install -g yarn` - `sudo ./test` to test - `follow ./python/README.md for python installation` -Script without password for sudo: -- `sudo mkdir /hermod_bin` -- `cp tv_panel/scripts/statusRGB.py /hermod_bin/` -- `sudo visudo` to add +To display a status LED without root +- `cp scripts/statusRGB_listener.py /root/scripts/statusRGB_listener.py` (from `tv_panel` directory) +- Add in root crontab (`sudo crontab -e`): ``` -# Allow the scripts here to be run without sudo password -hermod ALL=(ALL) NOPASSWD: /hermod_bin/statusRGB.py +@reboot /usr/bin/python3 /root/scripts/statusRGB_listener.py ``` +- Reboot or run `/usr/bin/python3 /root/scripts/statusRGB_listener.py &` as root +- To display a color for t seconds (float): `echo "55ee33 1" > /dev/rgb_pipe` ### Disable screen sleeping @@ -72,7 +81,8 @@ hermod ALL=(ALL) NOPASSWD: /hermod_bin/statusRGB.py - Wiring: VCC -> 5V, GND -> GND, RX -> TX, TX -> RX - In `sudo raspi-config`, disable serial messages but enable serial interface -```wget https://github.com/nfc-tools/libnfc/releases/download/libnfc-1.7.1/libnfc-1.7.1.tar.bz2 +``` +wget https://github.com/nfc-tools/libnfc/releases/download/libnfc-1.7.1/libnfc-1.7.1.tar.bz2 tar xvjf libnfc-1.7.1.tar.bz2 cd libnfc-1.7.1 ./configure --prefix=/usr --sysconfdir=/etc diff --git a/front/src/style.css b/front/src/style.css index e0eaae40575493d59809380c5ab97a136e5a585b..29ef3ccc37d2d7821f18938dd90d8e761e5bc17c 100644 --- a/front/src/style.css +++ b/front/src/style.css @@ -55,10 +55,8 @@ justify-content: space-between; } -.image-text-bloc > .text-section > .text-element:first-child { - flex-grow: 1; -} - .image-text-bloc > .text-section > .text-element:last-child { white-space: nowrap; + flex-grow: 1; + text-align: right; } diff --git a/scripts/.screenrc_tv_panel b/scripts/.screenrc_tv_panel index 5688bc404564e47e733f01a208b10835b717e09e..20d6f0c20205718bf9ea1bba784e81d31680be60 100755 --- a/scripts/.screenrc_tv_panel +++ b/scripts/.screenrc_tv_panel @@ -29,8 +29,6 @@ bind f eval "hardstatus ignore" bind F eval "hardstatus alwayslastline" # Windows at startup -screen -t "tvp_back" 0 bash -c 'cd /home/hermod/tv_panel && yarn start:prod' -screen -t "tvp_front" 1 bash -c 'cd /home/hermod/tv_panel && yarn start:front' -screen -t "tvp_chromium" 2 bash -c 'cd /home/hermod/tv_panel && DISPLAY=:0 ./front/start.sh' -screen -t bash 3 bash -select 3 +screen -t "tvp_chromium" 0 bash -c 'DISPLAY=:0 xdotool mousemove 10000 10000 && DISPLAY=:0 chromium-browser --kiosk --incognito --disable-gpu http://localhost:5000' +screen -t bash 1 bash +select 1 diff --git a/scripts/ecosystem.config.json b/scripts/ecosystem.config.json new file mode 100644 index 0000000000000000000000000000000000000000..a7576e3bf4741563faab7d9c70709267ec050e41 --- /dev/null +++ b/scripts/ecosystem.config.json @@ -0,0 +1,14 @@ +{ + "apps": [ + { + "script": "server/index.js", + "name": "tv-panel", + "instances": "1", + "exec_mode": "cluster", + "watch": false, + "env": { + "NODE_ENV": "production" + } + } + ] +} diff --git a/scripts/nfc-poll-wrapper.sh b/scripts/nfc-poll-wrapper.sh new file mode 100755 index 0000000000000000000000000000000000000000..21ef17ef29b47cf5497408edb33afabac662a2dc --- /dev/null +++ b/scripts/nfc-poll-wrapper.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +while : +do + res=$(nfc-poll | grep "UID " | cut -d: -f2 | sed 's/ //g' | xargs) + if [[ ! -z "$res" ]] + then + echo $res + echo "11ff11 0.2" > /dev/rgb_pipe + sleep 1 + else + echo "ff1111 0.2" > /dev/rgb_pipe + fi +done diff --git a/scripts/statusRGB_listener.py b/scripts/statusRGB_listener.py new file mode 100755 index 0000000000000000000000000000000000000000..fb7564a95678cd6fd38c981fb456d5379bcb6f51 --- /dev/null +++ b/scripts/statusRGB_listener.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import os +import sys +import time +import subprocess + +from neopixel import Adafruit_NeoPixel, ws + + +""" +This script opens a pipe to access the RGB status LED by writing into it. +When the script is running, you can use 'echo "11ee55 3" > PIPE_PATH' +(PIPE_PATH defined below) to display the color 11ee55 for 3 seconds. + +It must be run as root. +""" + + +# LED strip configuration: +LED_COUNT = 1 # Number of LED pixels. +LED_PIN = 18 # GPIO pin connected to the pixels (18 uses PWM!). +LED_FREQ_HZ = 800000 # LED signal frequency in hertz (usually 800khz) +LED_DMA = 10 # DMA channel to use for generating signal (try 10) +LED_BRIGHTNESS = 255 # Set to 0 for darkest and 255 for brightest +LED_INVERT = False # True to invert the signal (when using NPN transistor level shift) +LED_CHANNEL = 0 # set to '1' for GPIOs 13, 19, 41, 45 or 53 +LED_STRIP = ws.WS2811_STRIP_GRB # Strip type and colour ordering + +# Pipe where the code listen for color commands +PIPE_PATH = '/dev/rgb_pipe' + + +def sanitize_color(s): + """Return (r, g, b) levels from html color format""" + hex_color = s.lstrip('#') + if len(hex_color) == 3: + hex_color = '{0}{0}{1}{1}{2}{2}'.format(*hex_color) + if len(hex_color) != 6: + return None + + (r, g, b) = (int(x, 16) for x in [hex_color[i:i+2] for i in (0, 2, 4)]) + return r, g, b + + +def command_string_to_display(cs, strip): + """Display the color based on the 'color time' string""" + data = data0.split(' ') + colors = sanitize_color(data[0]) + if colors is None: + return 1 + r, g, b = colors + try: + t = float(data[1]) + except: + t = -1 + + strip.setPixelColorRGB(0, r, g, b) + strip.show() + if t > 0: + time.sleep(t) + strip.setPixelColorRGB(0, 0, 0, 0) + strip.show() + return 0 + + +if __name__ == '__main__': + if os.path.exists(PIPE_PATH): + os.remove(PIPE_PATH) + + os.mkfifo(PIPE_PATH) + os.system('chown root:hermod {0} && chmod g+rw {0}'.format(PIPE_PATH)) # rw access for hermod group + + # Create NeoPixel object with appropriate configuration. + strip = Adafruit_NeoPixel(LED_COUNT, LED_PIN, LED_FREQ_HZ, LED_DMA, + LED_INVERT, LED_BRIGHTNESS, LED_CHANNEL, LED_STRIP) + + # Intialize the library (must be called once before other functions). + strip.begin() + + # Listen to the pipe and display colors + while True: + data0 = subprocess.check_output(['cat', PIPE_PATH]).decode('utf-8') + command_string_to_display(data0, strip) + + command_string_to_display('000000', strip) diff --git a/scripts/update_server.sh b/scripts/update_server.sh new file mode 100755 index 0000000000000000000000000000000000000000..31cff824509f91ca97832d96183eac539eb37ffc --- /dev/null +++ b/scripts/update_server.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +if [[ "$NODE_ENV" != "production" ]] +then + read -p "The env if not production, this script can remove uncommitted changes. Are you sure you want to continue? [y/N] " prompt + if [[ $prompt == "y" || $prompt == "Y" || $prompt == "yes" || $prompt == "Yes" ]] + then + echo "Pursuing..." + else + echo "Aborted." + exit 1 + fi +fi + +# Pull code +echo "Fetching the code..." +git fetch +# git reset --hard origin/master + +# Install deps +echo "Installing dependencies..." +yarn install --pure-lockfile + +# Build front +echo "Building front-end..." +yarn build:front + +# Restart server +echo "Restarting the back-end server..." +pm2 startOrRestart ./scripts/ecosystem.config.json + +# Restart screen +echo "Restarting the screen..." +screen -S TVPanelScreen -X quit +./scripts/start_tv_panel.sh diff --git a/scripts/waitForNetwork b/scripts/waitForNetwork index 3c8192a1f97b26665a4df79a0b9d0100f38cfd21..8d0e610722484bc05b65e3a762e354d139b892e6 100755 --- a/scripts/waitForNetwork +++ b/scripts/waitForNetwork @@ -2,11 +2,13 @@ while true do + echo "1111ff 0.1" > /dev/rgb_pipe ping 8.8.8.8 -c 3 -W 15 -q > /dev/null res=$? if [ "$res" = "0" ] then echo "Connection established" + echo "1111ff 3" > /dev/rgb_pipe break else echo "Connection failed" @@ -14,5 +16,4 @@ do sleep 5 done - exit 0 diff --git a/server/index.js b/server/index.js index a9a740d2bb915c42911b6e26bbf0ccc144bb43cf..9361359b10407328dde9f07c8c4c55b56f09383c 100644 --- a/server/index.js +++ b/server/index.js @@ -1,46 +1,34 @@ -const fetch = require('node-fetch'); - -const { - uuid, fontSize, rowHeight, port, api, -} = require('./config'); - -const dummyResponse = require('./dummyResponse.json'); - -const io = require('socket.io')(port || 3000); -const { createSignedJWT, interval } = require('./utils'); - -const useDummy = false; - -io.of('/').on('connection', (socket) => { - socket.emit('config', { fontSize, rowHeight }); +const http = require('http'); +const fs = require('fs'); +const path = require('path'); +const socketIO = require('socket.io'); +const { port } = require('./config'); +const socket = require('./socket'); + +const indexPath = path.resolve(__dirname, '../front/build/index.html'); + +const server = (req, res) => { + let url = req.url.replace(/(^\/|\/$)/g, '').trim(); + if (url === '') { + url = 'index.html'; + } + const askedPath = path.resolve(__dirname, `../front/build/${url}`); + const filePath = fs.existsSync(askedPath) ? askedPath : indexPath; + fs.readFile(filePath, (err, data) => { + if (err) { + res.writeHead(500); + res.end('Error loading index.html'); + return; + } + + res.writeHead(200); + res.end(data); + }); +}; - const chrono = useDummy - ? interval(() => { - socket.emit('panel_data', dummyResponse); - return 15000; - }, 0) - : interval( - () => - fetch(`${api.url}/${api.version}/screen/${uuid}`, { - headers: { - Autorization: `Bearer ${createSignedJWT()}`, - }, - }) - .then(rawRes => rawRes.json()) - .then((res) => { - socket.emit('panel_data', res); - return res.ttl; - }) - .catch(console.log), - 0, - ); +const app = http.createServer(server); +app.listen(port || 3000); - // Respond to date message with the date - socket.on('date', () => { - socket.emit('date', { date: Date.now() }); - }); +const io = socketIO(app); - socket.on('disconnect', () => { - chrono.clear(); - }); -}); +io.of('/').on('connection', socket); diff --git a/server/socket.js b/server/socket.js new file mode 100644 index 0000000000000000000000000000000000000000..dfe5cfe3a4b64bddfcaa532f71296c7da5e7eaa5 --- /dev/null +++ b/server/socket.js @@ -0,0 +1,75 @@ +const fetch = require('node-fetch'); +const path = require('path'); + +const { + uuid, fontSize, rowHeight, api, +} = require('./config'); + +const { spawn } = require('child_process'); + +const dummyResponse = require('./dummyResponse.json'); + +const { createSignedJWT, interval } = require('./utils'); + +const useDummy = false; + +let version; + +const checkVersion = (newVersion) => { + if (version && version !== newVersion) { + console.log('Update code'); + const updateServer = spawn(path.resolve(__dirname, '../scripts/update_server.sh')); + updateServer.stdout.on('data', (data) => { + process.stdout.write(data); + }); + } + version = newVersion; +}; + +const doScreenApiRequest = (socket, userid = null) => { + const query = userid ? `?userid=${userid}` : ''; + return fetch(`${api.url}/${api.version}/screen/${uuid}${query}`, { + headers: { + Autorization: `Bearer ${createSignedJWT()}`, + }, + }) + .then(rawRes => rawRes.json()) + .then(async (res) => { + checkVersion(res.version); + socket.emit('panel_data', res); + return res.ttl; + }) + .catch(console.log); +}; + +const setChrono = (socket) => { + if (useDummy) { + return interval(() => socket.emit('panel_data', dummyResponse), 10000); + } + return interval(() => doScreenApiRequest(socket), 10000); +}; + +module.exports = (socket) => { + socket.emit('config', { fontSize, rowHeight }); + + const chrono = setChrono(socket); + chrono.startNow(); + + // Respond to date message with the date + socket.on('date', () => { + socket.emit('date', { date: Date.now() }); + }); + + socket.on('disconnect', () => { + chrono.stop(); + }); + + const badgeChild = spawn(path.resolve(__dirname, '../scripts/nfc-poll-wrapper.sh')); + + badgeChild.stdout.on('data', (data0) => { + const userid = data0.toString().trim(); + console.log(`child stdout: ${userid}`); + + doScreenApiRequest(socket, userid).then(ttl => chrono.restart(ttl)); + }); +}; diff --git a/server/utils.js b/server/utils.js index 0716563e9fb2361df198888890de647cfe70f282..119080646c2956a563dfe87c3ff9deb354f7a8f3 100644 --- a/server/utils.js +++ b/server/utils.js @@ -2,14 +2,6 @@ const jwt = require('jsonwebtoken'); const { cert } = require('./config'); -const initialOutput = { id: null, clear: () => undefined, trigger: () => undefined }; - -const replace = (toReplace, replacer) => { - Object.keys(replacer).forEach((x) => { - toReplace[x] = replacer[x]; - }); -}; - function Timeout(fn, interval) { this.id = setTimeout(fn, interval); this.cleared = false; @@ -19,7 +11,7 @@ function Timeout(fn, interval) { }; } -const interval = (fn, initialTTL, output = initialOutput) => { +const interval = (fn, initialTTL, output = {}) => { const getTimeout = ttl => new Timeout(async () => { let TTL; @@ -27,10 +19,7 @@ const interval = (fn, initialTTL, output = initialOutput) => { if (this.cleared) { return; } - TTL = fn(); - if (TTL instanceof Promise) { - TTL = await TTL; - } + TTL = await fn(); } catch (error) { console.error(error); TTL = initialTTL; @@ -41,18 +30,48 @@ const interval = (fn, initialTTL, output = initialOutput) => { const nextTTL = parseInt(TTL, 10) || initialTTL; interval(fn, nextTTL, output); }, ttl); - const getOutput = timeout => ({ - id: timeout.id, - clear: () => timeout.clear(), - trigger: () => { - timeout.clear(); - const newTimeout = getTimeout(0); - replace(output, getOutput(newTimeout)); - }, - }); - const timeout = getTimeout(initialTTL); - replace(output, getOutput(timeout)); + const getOutput = (timeout) => { + const stop = () => { + if (timeout) { + timeout.clear(); + } + }; + return { + start: (ttl) => { + if (timeout && !timeout.cleared) { + throw new Error('Interval is already started'); + } + const newOutput = getOutput(getTimeout(ttl || initialTTL)); + Object.assign(output, getOutput(newOutput)); + return output; + }, + startNow: () => { + if (timeout && !timeout.cleared) { + throw new Error('Interval is already started'); + } + Object.assign(output, getOutput(getTimeout(0))); + return output; + }, + stop, + restart: (ttl) => { + stop(); + Object.assign(output, getOutput(getTimeout(ttl || initialTTL))); + return output; + }, + restartNow: () => { + stop(); + Object.assign(output, getOutput(getTimeout(0))); + return output; + }, + }; + }; + + if (Object.keys(output).length === 0) { + Object.assign(output, getOutput(null)); + } else { + Object.assign(output, getOutput(getTimeout(initialTTL))); + } return output; };