Dev
This commit is contained in:
parent
bd01209283
commit
9b2bf554b1
18 changed files with 760 additions and 0 deletions
64
.gitignore
vendored
Normal file
64
.gitignore
vendored
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# Typescript v1 declaration files
|
||||||
|
typings/
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variables file
|
||||||
|
.env
|
||||||
|
|
||||||
|
# Local development config file
|
||||||
|
config.json
|
||||||
|
package-lock.json
|
||||||
|
|
||||||
|
public/assets/app.js
|
1
.npmrc
Normal file
1
.npmrc
Normal file
|
@ -0,0 +1 @@
|
||||||
|
package-lock=false
|
24
api/config.mjs
Normal file
24
api/config.mjs
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import Nconf from 'nconf-lite'
|
||||||
|
|
||||||
|
const nconf = new Nconf()
|
||||||
|
|
||||||
|
// Helper method for global usage.
|
||||||
|
nconf.inTest = () => nconf.get('NODE_ENV') === 'test'
|
||||||
|
|
||||||
|
// Load env
|
||||||
|
nconf.overrides({
|
||||||
|
NODE_ENV: (process.env.NODE_ENV || 'development').toLocaleLowerCase()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Load empty overrides that can be overwritten later
|
||||||
|
nconf.overrides({})
|
||||||
|
|
||||||
|
nconf.defaults({
|
||||||
|
"frontend": {
|
||||||
|
"url": "http://streamer.filadelfia.is"
|
||||||
|
},
|
||||||
|
"jwtsecret": "w2bkdWAButfdfEkCs8dpE3L2n6QzCfhna0T4"
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
export default nconf
|
14
api/encoder/routes.mjs
Normal file
14
api/encoder/routes.mjs
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
|
||||||
|
export default class EncoderRoutes {
|
||||||
|
constructor(opts = {}) {
|
||||||
|
Object.assign(this, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
registerIo(server, ctx) {
|
||||||
|
ctx.socket.safeOn('encoder.status', this.status.bind(this))
|
||||||
|
}
|
||||||
|
|
||||||
|
status(ctx) {
|
||||||
|
ctx.socket.emit('encoder.status', { running: false })
|
||||||
|
}
|
||||||
|
}
|
59
api/io.mjs
Normal file
59
api/io.mjs
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
import socket from 'socket.io-serveronly'
|
||||||
|
|
||||||
|
export default class SocketServer {
|
||||||
|
constructor(db, log, routes) {
|
||||||
|
this.db = db
|
||||||
|
this.log = log
|
||||||
|
this.routes = routes
|
||||||
|
}
|
||||||
|
|
||||||
|
init(server) {
|
||||||
|
this.io = new socket(server)
|
||||||
|
this.io.on('connection', this.onNewConnection.bind(this))
|
||||||
|
}
|
||||||
|
|
||||||
|
onNewConnection(socket) {
|
||||||
|
let ctx = {
|
||||||
|
io: this.io,
|
||||||
|
socket: socket,
|
||||||
|
log: this.log.child({ id: socket.id }),
|
||||||
|
db: this.db
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.log.info('New socket connection', { id: socket.id })
|
||||||
|
|
||||||
|
ctx.socket.safeOn = this.socketSafeOn.bind(this, ctx)
|
||||||
|
|
||||||
|
this.register(ctx)
|
||||||
|
|
||||||
|
ctx.socket.emit('data', { bla: 'test' })
|
||||||
|
}
|
||||||
|
|
||||||
|
register(ctx) {
|
||||||
|
let keys = Object.keys(this.routes)
|
||||||
|
for (let key of keys) {
|
||||||
|
if (this.routes[key].registerIo) {
|
||||||
|
this.routes[key].registerIo(this, ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
socketSafeOn(ctx, name, fn) {
|
||||||
|
ctx.socket.on(name, data => {
|
||||||
|
ctx.log.info('IO: ' + name)
|
||||||
|
let res;
|
||||||
|
try {
|
||||||
|
res = fn(ctx, data)
|
||||||
|
} catch (err) {
|
||||||
|
ctx.log.error(error, `Error processing ${name}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res && typeof(res.then) === 'function') {
|
||||||
|
res.then(
|
||||||
|
() => {},
|
||||||
|
error => ctx.log.error(error, `Error processing ${name}`)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
61
api/serve.mjs
Normal file
61
api/serve.mjs
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
import path from 'path'
|
||||||
|
import { FileResponse } from 'flaska'
|
||||||
|
import fs from 'fs/promises'
|
||||||
|
import fsSync from 'fs'
|
||||||
|
|
||||||
|
export default class ServeHandler {
|
||||||
|
constructor(opts = {}) {
|
||||||
|
Object.assign(this, opts)
|
||||||
|
Object.assign(this, {
|
||||||
|
fs: this.fs || fs,
|
||||||
|
fsSync: this.fsSync || fsSync,
|
||||||
|
frontend: this.frontend || 'http://localhost:4000',
|
||||||
|
version: this.version || 'version',
|
||||||
|
})
|
||||||
|
|
||||||
|
this.index = fsSync.readFileSync(path.join(this.root, 'index.html'))
|
||||||
|
}
|
||||||
|
|
||||||
|
register(server) {
|
||||||
|
server.flaska.get('/::file', this.serve.bind(this))
|
||||||
|
}
|
||||||
|
|
||||||
|
/** GET: /::file */
|
||||||
|
serve(ctx) {
|
||||||
|
if (ctx.params.file.startsWith('api/')) {
|
||||||
|
return this.serveIndex(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
let file = path.resolve(path.join(this.root, ctx.params.file ? ctx.params.file : 'index.html'))
|
||||||
|
|
||||||
|
if (!ctx.params.file
|
||||||
|
|| ctx.params.file === 'index.html') {
|
||||||
|
return this.serveIndex(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!file.startsWith(this.root)) {
|
||||||
|
return this.serveIndex(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.fs.stat(file)
|
||||||
|
.then(function(stat) {
|
||||||
|
ctx.headers['Cache-Control'] = 'no-store'
|
||||||
|
ctx.body = new FileResponse(file, stat)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
if (err.code === 'ENOENT') {
|
||||||
|
return this.serveIndex(ctx)
|
||||||
|
}
|
||||||
|
return Promise.reject(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async serveIndex(ctx) {
|
||||||
|
ctx.body = this.index
|
||||||
|
ctx.type = 'text/html; charset=utf-8'
|
||||||
|
}
|
||||||
|
|
||||||
|
serveErrorPage(ctx) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
139
api/server.mjs
Normal file
139
api/server.mjs
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
import { Flaska, QueryHandler, JsonHandler, HttpError } from 'flaska'
|
||||||
|
|
||||||
|
import config from './config.mjs'
|
||||||
|
import StaticRoutes from './static_routes.mjs'
|
||||||
|
import ServeHandler from './serve.mjs'
|
||||||
|
import SocketServer from './io.mjs'
|
||||||
|
import EncoderRoutes from './encoder/routes.mjs'
|
||||||
|
|
||||||
|
export default class Server {
|
||||||
|
constructor(http, port, core, opts = {}) {
|
||||||
|
Object.assign(this, opts)
|
||||||
|
Object.assign(this, {
|
||||||
|
http, port, core,
|
||||||
|
})
|
||||||
|
let localUtil = new this.core.sc.Util(import.meta.url)
|
||||||
|
|
||||||
|
this.flaskaOptions = {
|
||||||
|
appendHeaders: {
|
||||||
|
'Content-Security-Policy': `default-src 'self'; style-src 'self' 'unsafe-inline'; img-src * data: blob:; font-src 'self' data:; object-src 'none'; frame-ancestors 'none'; connect-src 'self' https://media.nfp.is/; media-src 'self' https://cdn.nfp.is/`,
|
||||||
|
},
|
||||||
|
nonce: [],
|
||||||
|
log: this.core.log,
|
||||||
|
}
|
||||||
|
this.jsonHandler = JsonHandler
|
||||||
|
|
||||||
|
this.routes = {
|
||||||
|
static: new StaticRoutes(),
|
||||||
|
encoder: new EncoderRoutes(),
|
||||||
|
}
|
||||||
|
this.routes.serve = new ServeHandler({
|
||||||
|
root: localUtil.getPathFromRoot('../public'),
|
||||||
|
version: this.core.app.running,
|
||||||
|
frontend: config.get('frontend:url'),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
runCreateServer() {
|
||||||
|
// Create our server
|
||||||
|
this.flaska = new Flaska(this.flaskaOptions, this.http)
|
||||||
|
|
||||||
|
// configure our server
|
||||||
|
if (config.get('NODE_ENV') === 'development') {
|
||||||
|
this.flaska.devMode()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.flaska.onerror((err, ctx) => {
|
||||||
|
if (err instanceof HttpError && err.status !== 500) {
|
||||||
|
ctx.status = err.status
|
||||||
|
ctx.log.warn(err.message)
|
||||||
|
} else {
|
||||||
|
ctx.log.error(err.inner || err)
|
||||||
|
if (err.extra) {
|
||||||
|
ctx.log.error({ extra: err.extra }, 'Database parameters')
|
||||||
|
}
|
||||||
|
ctx.status = 500
|
||||||
|
}
|
||||||
|
ctx.body = {
|
||||||
|
status: ctx.status,
|
||||||
|
message: err.message,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.flaska.before(function(ctx) {
|
||||||
|
ctx.state.started = new Date().getTime()
|
||||||
|
ctx.req.ip = ctx.req.headers['x-forwarded-for'] || ctx.req.connection.remoteAddress
|
||||||
|
ctx.log = ctx.log.child({
|
||||||
|
id: Math.random().toString(36).substring(2, 14),
|
||||||
|
})
|
||||||
|
ctx.db = this.pool
|
||||||
|
}.bind(this))
|
||||||
|
this.flaska.before(QueryHandler())
|
||||||
|
|
||||||
|
let healthChecks = 0
|
||||||
|
let healthCollectLimit = 60 * 60 * 12
|
||||||
|
|
||||||
|
this.flaska.after(function(ctx) {
|
||||||
|
if (ctx.aborted && ctx.status === 200) {
|
||||||
|
ctx.status = 299
|
||||||
|
}
|
||||||
|
let ended = new Date().getTime()
|
||||||
|
var requestTime = ended - ctx.state.started
|
||||||
|
|
||||||
|
let status = ''
|
||||||
|
let level = 'info'
|
||||||
|
if (ctx.status >= 400) {
|
||||||
|
status = ctx.status + ' '
|
||||||
|
level = 'warn'
|
||||||
|
}
|
||||||
|
if (ctx.status >= 500) {
|
||||||
|
level = 'error'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctx.url === '/health' || ctx.url === '/api/health') {
|
||||||
|
healthChecks++
|
||||||
|
if (healthChecks >= healthCollectLimit) {
|
||||||
|
ctx.log[level]({
|
||||||
|
duration: Math.round(ended),
|
||||||
|
status: ctx.status,
|
||||||
|
}, `<-- ${status}${ctx.method} ${ctx.url} {has happened ${healthChecks} times}`)
|
||||||
|
healthChecks = 0
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.log[level]({
|
||||||
|
duration: requestTime,
|
||||||
|
status: ctx.status,
|
||||||
|
ip: ctx.req.ip,
|
||||||
|
}, (ctx.aborted ? '[ABORT]' : '<--') + ` ${status}${ctx.method} ${ctx.url}`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
runRegisterRoutes() {
|
||||||
|
let keys = Object.keys(this.routes)
|
||||||
|
for (let key of keys) {
|
||||||
|
if (this.routes[key].register) {
|
||||||
|
this.routes[key].register(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
runCreateSocket() {
|
||||||
|
this.io = new SocketServer(this.db, this.core.log, this.routes)
|
||||||
|
this.io.init(this.flaska.server)
|
||||||
|
}
|
||||||
|
|
||||||
|
runStartListen() {
|
||||||
|
return this.flaska.listenAsync(this.port).then(() => {
|
||||||
|
this.core.log.info('Server is listening on port ' + this.port)
|
||||||
|
this.runCreateSocket()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
run() {
|
||||||
|
this.runCreateServer()
|
||||||
|
this.runRegisterRoutes()
|
||||||
|
return this.runStartListen()
|
||||||
|
}
|
||||||
|
}
|
17
api/static_routes.mjs
Normal file
17
api/static_routes.mjs
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import config from './config.mjs'
|
||||||
|
|
||||||
|
export default class StaticRoutes {
|
||||||
|
constructor(opts = {}) {
|
||||||
|
Object.assign(this, { })
|
||||||
|
}
|
||||||
|
|
||||||
|
register(server) {
|
||||||
|
server.flaska.get('/api/health', this.health.bind(this))
|
||||||
|
}
|
||||||
|
|
||||||
|
health(ctx) {
|
||||||
|
ctx.body = {
|
||||||
|
environment: config.get('NODE_ENV'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
78
app/api/client.js
Normal file
78
app/api/client.js
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
const m = require('mithril')
|
||||||
|
var io = require('./socket')
|
||||||
|
var socket = io()
|
||||||
|
|
||||||
|
class Client {
|
||||||
|
constructor(socket) {
|
||||||
|
this.socket = socket
|
||||||
|
this.isConnected = false
|
||||||
|
|
||||||
|
this.socket.on('disconnect', this.disconnected.bind(this))
|
||||||
|
this.socket.on('connect', this.connected.bind(this))
|
||||||
|
this.components = new Map()
|
||||||
|
}
|
||||||
|
|
||||||
|
registerComponent(component) {
|
||||||
|
this.components.set(component, [])
|
||||||
|
if (component.ioInit) {
|
||||||
|
component.ioInit()
|
||||||
|
}
|
||||||
|
if (this.isConnected && component.ioConnected) {
|
||||||
|
component.ioConnected()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
safeGetComponent(component) {
|
||||||
|
if (typeof(component) === 'string') {
|
||||||
|
throw new Error('io.on was called without specifying active component')
|
||||||
|
}
|
||||||
|
let arr = this.components.get(component)
|
||||||
|
if (!arr) {
|
||||||
|
throw new Error('Registered component was missing but was attempting to add socket listener')
|
||||||
|
}
|
||||||
|
return arr
|
||||||
|
}
|
||||||
|
|
||||||
|
on(component, name, fn) {
|
||||||
|
let arr = this.safeGetComponent(component)
|
||||||
|
arr.push([name, fn])
|
||||||
|
|
||||||
|
this.socket.on(name, (data) => {
|
||||||
|
Promise.resolve().then(() => {
|
||||||
|
return fn(data)
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error('Error running io handler for ' + name, err)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(name, data) {
|
||||||
|
this.socket.emit(name, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
unregisterComponent(component) {
|
||||||
|
let arr = this.safeGetComponent(component)
|
||||||
|
for (let item of arr) {
|
||||||
|
this.socket.off(item[0], item[1])
|
||||||
|
}
|
||||||
|
this.components.delete(component)
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnected() {
|
||||||
|
this.isConnected = false
|
||||||
|
m.redraw()
|
||||||
|
}
|
||||||
|
|
||||||
|
connected() {
|
||||||
|
this.isConnected = true
|
||||||
|
for (let component of this.components) {
|
||||||
|
if (component[0].ioConnected) {
|
||||||
|
component[0].ioConnected()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.redraw()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new Client(socket)
|
8
app/api/socket.js
Normal file
8
app/api/socket.js
Normal file
File diff suppressed because one or more lines are too long
21
app/index.js
Normal file
21
app/index.js
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
const m = require('mithril')
|
||||||
|
const Status = require('./page_status')
|
||||||
|
window.m = m
|
||||||
|
|
||||||
|
m.route.setOrig = m.route.set
|
||||||
|
m.route.set = function(path, data, options){
|
||||||
|
m.route.setOrig(path, data, options)
|
||||||
|
window.scrollTo(0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.route.linkOrig = m.route.link
|
||||||
|
m.route.link = function(vnode){
|
||||||
|
m.route.linkOrig(vnode)
|
||||||
|
window.scrollTo(0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.route.prefix = ''
|
||||||
|
|
||||||
|
m.route(document.getElementById('main'), '/', {
|
||||||
|
'/': Status,
|
||||||
|
})
|
30
app/page_status.js
Normal file
30
app/page_status.js
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
const m = require('mithril')
|
||||||
|
const client = require('./api/client')
|
||||||
|
|
||||||
|
const Status = {
|
||||||
|
oninit: function(vnode) {
|
||||||
|
client.registerComponent(this)
|
||||||
|
},
|
||||||
|
|
||||||
|
onremove: function(vnode) {
|
||||||
|
client.unregisterComponent(this)
|
||||||
|
},
|
||||||
|
|
||||||
|
ioInit: function() {
|
||||||
|
client.on(this, 'encoder.status', status => {
|
||||||
|
console.log('status', status)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
ioConnected: function() {
|
||||||
|
client.emit('encoder.status')
|
||||||
|
},
|
||||||
|
|
||||||
|
view: function(vnode) {
|
||||||
|
return [
|
||||||
|
m('div', `Hello world, connection status: ${client.isConnected}`),
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Status
|
103
arduino/arduino.ino
Normal file
103
arduino/arduino.ino
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
/*
|
||||||
|
Blink
|
||||||
|
|
||||||
|
The circuit:
|
||||||
|
* VFD Data to digital pin 2 (blue)
|
||||||
|
* VFD Clock to digital pin 3 (yellow)
|
||||||
|
* VFD Chip select to digital pin 4 (green)
|
||||||
|
* VFD VCC (power) to 5V
|
||||||
|
* VFD Ground (power) to Ground
|
||||||
|
*/
|
||||||
|
|
||||||
|
// include the library code:
|
||||||
|
#include <SPI_VFD.h>
|
||||||
|
|
||||||
|
// initialize the library with the numbers of the interface pins
|
||||||
|
SPI_VFD vfd(2, 3, 4);
|
||||||
|
|
||||||
|
// | 1 2|
|
||||||
|
// |12345678901234567890|
|
||||||
|
String vfd_line_top = "Booting up... ";
|
||||||
|
String vfd_line_bottom = "Waiting for stream. ";
|
||||||
|
unsigned int vfd_line_position = 0;
|
||||||
|
bool vfd_line_print = false;
|
||||||
|
unsigned long millis_last_incoming_ping = 0;
|
||||||
|
unsigned long millis_last_outgoing_ping = 0;
|
||||||
|
|
||||||
|
// the setup function runs once when you press reset or power the board
|
||||||
|
void setup() {
|
||||||
|
// initialize digital pin LED_BUILTIN as an output.
|
||||||
|
pinMode(LED_BUILTIN, OUTPUT);
|
||||||
|
digitalWrite(LED_BUILTIN, LOW);
|
||||||
|
|
||||||
|
Serial.begin(9600);
|
||||||
|
|
||||||
|
// set up the VFD's number of columns and rows:
|
||||||
|
vfd.begin(20, 2);
|
||||||
|
update_display();
|
||||||
|
}
|
||||||
|
|
||||||
|
void update_display() {
|
||||||
|
vfd.setCursor(0, 0);
|
||||||
|
vfd.print(vfd_line_top);
|
||||||
|
vfd.setCursor(0, 1);
|
||||||
|
vfd.print(vfd_line_bottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
void check_serial() {
|
||||||
|
while (Serial.available() > 0) {
|
||||||
|
millis_last_incoming_ping = millis();
|
||||||
|
int incomingByte = Serial.read();
|
||||||
|
if (incomingByte == 0 || incomingByte == '^') {
|
||||||
|
incomingByte = vfd_line_position = 0;
|
||||||
|
}
|
||||||
|
if (incomingByte < 32) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vfd_line_position < 20) {
|
||||||
|
vfd_line_top.setCharAt(vfd_line_position, incomingByte);
|
||||||
|
} else {
|
||||||
|
vfd_line_bottom.setCharAt(vfd_line_position - 20, incomingByte);
|
||||||
|
}
|
||||||
|
vfd_line_position = (vfd_line_position + 1) % 40;
|
||||||
|
vfd_line_print = vfd_line_position == 0;
|
||||||
|
if (vfd_line_print) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// the loop function runs over and over again forever
|
||||||
|
void loop() {
|
||||||
|
// We've been running for over 50 days and wrapped to zero. Reset all our timers
|
||||||
|
if (millis() < 10000 && (millis_last_outgoing_ping > 10000 || millis_last_incoming_ping > 10000)) {
|
||||||
|
millis_last_outgoing_ping = 0;
|
||||||
|
millis_last_incoming_ping = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
check_serial();
|
||||||
|
|
||||||
|
// If it's been 10 seconds since last incoming data
|
||||||
|
if (millis() - millis_last_incoming_ping > 30000) {
|
||||||
|
// |12345678901234567890|
|
||||||
|
vfd_line_top = "Error stream is down";
|
||||||
|
vfd_line_bottom = "Verify or restart...";
|
||||||
|
vfd_line_print = true;
|
||||||
|
millis_last_incoming_ping = millis();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send outgoing ping every 5 seconds if we can.
|
||||||
|
if (millis() - millis_last_outgoing_ping > 30000 && Serial.availableForWrite()) {
|
||||||
|
Serial.write('^');
|
||||||
|
millis_last_outgoing_ping = millis();
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have something to print, print it
|
||||||
|
if (vfd_line_print) {
|
||||||
|
vfd_line_print = false;
|
||||||
|
update_display();
|
||||||
|
}
|
||||||
|
|
||||||
|
delay(100);
|
||||||
|
}
|
38
index.mjs
Normal file
38
index.mjs
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import fs from 'fs'
|
||||||
|
import { pathToFileURL } from 'url'
|
||||||
|
import config from './api/config.mjs'
|
||||||
|
|
||||||
|
export function start(http, port, ctx) {
|
||||||
|
config.sources[1].store = ctx.config
|
||||||
|
|
||||||
|
return import('./api/server.mjs')
|
||||||
|
.then(function(module) {
|
||||||
|
let server = new module.default(http, port, ctx)
|
||||||
|
return server.run()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||||
|
import('service-core').then(core => {
|
||||||
|
const port = 4510
|
||||||
|
|
||||||
|
var core = new core.ServiceCore('filadelfia_streamer', import.meta.url, port, '')
|
||||||
|
|
||||||
|
let config = {
|
||||||
|
frontend: {
|
||||||
|
url: 'http://localhost:' + port
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
config = JSON.parse(fs.readFileSync('./config.json'))
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
config.port = port
|
||||||
|
|
||||||
|
core.setConfig(config)
|
||||||
|
core.init({ start }).then(function() {
|
||||||
|
return core.run()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
49
package.json
Normal file
49
package.json
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
{
|
||||||
|
"name": "church_streamer",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.mjs",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node index.mjs",
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"build": "esbuild app/index.js --bundle --outfile=public/assets/app.js",
|
||||||
|
"dev:build": "eltro --watch build --npm build",
|
||||||
|
"dev:server": "eltro --watch server --npm server",
|
||||||
|
"dev:build:old": "npm-watch build",
|
||||||
|
"dev:server:old": "npm-watch server",
|
||||||
|
"server": "node index.mjs | bunyan"
|
||||||
|
},
|
||||||
|
"watch": {
|
||||||
|
"server": {
|
||||||
|
"patterns": [
|
||||||
|
"api"
|
||||||
|
],
|
||||||
|
"extensions": "js,mjs",
|
||||||
|
"quiet": true,
|
||||||
|
"inherit": true
|
||||||
|
},
|
||||||
|
"build": {
|
||||||
|
"patterns": [
|
||||||
|
"app"
|
||||||
|
],
|
||||||
|
"extensions": "js",
|
||||||
|
"quiet": true,
|
||||||
|
"inherit": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"author": "Jonatan Nilsson",
|
||||||
|
"license": "WTFPL",
|
||||||
|
"dependencies": {
|
||||||
|
"bunyan-lite": "^1.2.1",
|
||||||
|
"flaska": "^1.3.4",
|
||||||
|
"nconf-lite": "^2.0.0",
|
||||||
|
"serialport": "^12.0.0",
|
||||||
|
"socket.io-serveronly": "^2.3.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"eltro": "^1.4.4",
|
||||||
|
"esbuild": "^0.19.5",
|
||||||
|
"mithril": "^2.2.2",
|
||||||
|
"service-core": "^3.0.0-beta.17"
|
||||||
|
}
|
||||||
|
}
|
25
public/assets/app.css
Normal file
25
public/assets/app.css
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
|
||||||
|
[hidden] { display: none !important; }
|
||||||
|
|
||||||
|
:root { --bg: #fff; --bg-component: #f3f7ff; --bg-component-half: #f3f7ff77; --bg-component-alt: #ffd99c; --color: #031131; --color-alt: #7a9ad3; --main: #18597d; --main-fg: #fff; --error: red; --error-bg: hsl(0, 75%, 80%); } /* Box sizing rules */
|
||||||
|
*, *::before, *::after { box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Remove default margin */
|
||||||
|
body, h1, h2, h3, h4, p, figure, blockquote, dl, dd { margin: 0; }
|
||||||
|
|
||||||
|
body { min-height: 100vh; text-rendering: optimizeSpeed; line-height: 1.5; font-size: 16px; font-family: 'Inter var', Helvetica, Arial, sans-serif; font-variation-settings: "slnt" 0; font-feature-settings: "case", "frac", "tnum", "ss02", "calt", "ccmp", "kern"; background: var(--bg); color: var(--color); display: flex; flex-direction: column; }
|
||||||
|
|
||||||
|
.italic { font-variation-settings: "slnt" 10deg; }
|
||||||
|
|
||||||
|
input, button, textarea, select { font: inherit; }
|
||||||
|
|
||||||
|
h1 { font-size: 1.88rem; }
|
||||||
|
h2 { font-size: 1.66rem; }
|
||||||
|
h3 { font-size: 1.44rem; }
|
||||||
|
h4 { font-size: 1.22rem; }
|
||||||
|
h5 { font-size: 1.0rem; }
|
||||||
|
|
||||||
|
.row { display: flex; }
|
||||||
|
.column { display: flex; flex-direction: column; }
|
||||||
|
.filler { flex-grow: 2; }
|
13
public/assets/font.css
Normal file
13
public/assets/font.css
Normal file
File diff suppressed because one or more lines are too long
16
public/index.html
Normal file
16
public/index.html
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Filadelfia Streamer</title>
|
||||||
|
<base href="/">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="icon" type="image/png" href="/assets/favicon.png">
|
||||||
|
<link rel="stylesheet" type="text/css" href="/assets/font.css">
|
||||||
|
<link rel="stylesheet" type="text/css" href="/assets/app.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main id="main"></main>
|
||||||
|
<script type="text/javascript" src="/assets/app.js?v=2"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in a new issue