diff --git a/constants.js b/constants.js index 75254cc..8966ffe 100644 --- a/constants.js +++ b/constants.js @@ -1,6 +1,6 @@ const path = require('path') -require('dotenv').config({ path: path.join(__dirname, '..', '.env') }) +require('dotenv').config({ path: path.join(__dirname, '.env') }) const ENV = process.env.NODE_ENV || 'development' const USB_MOUNT_PATH = process.env.USB_MOUNT_PATH || '/mnt/sda1' diff --git a/doorlock.js b/doorlock.js index 4063a60..ba92aa5 100644 --- a/doorlock.js +++ b/doorlock.js @@ -1,27 +1,170 @@ -const chalk = require('chalk') -const Cobot = require('./cobot') +'use strict' + +// const axios = require('axios') +const https = require('https') const fs = require('fs') const SerialPort = require('serialport') const tessel = require('tessel') const { CARD_UPDATE_INTERVAL, CARDS_PATH, + COBOT_CARDS_API, + COBOT_CLIENT_ID, + COBOT_CLIENT_SECRET, + COBOT_SCOPE, + COBOT_USER_EMAIL, + COBOT_USER_PASSWORD, DOOR_OPEN_DELAY, + ENV, RFID_READER_SERIAL_NUMBER, } = require('./constants') -const TEST = process.env.NODE_ENV === 'test' +const TEST = ENV === 'test' + +class Cobot { + constructor(token) { + this.token = token + } + + cards() { + if (!COBOT_CARDS_API) + throw new Error('missing "COBOT_CARDS_API" env variable!') + return new Promise((resolve, reject) => { + const req = https.request( + { + headers: { + Authorization: `Bearer ${this.token}`, + }, + hostname: 'chimera.cobot.me', + method: 'GET', + path: '/api/check_in_tokens', + }, + res => { + const { statusCode, headers } = res + console.log('\n----------------------------------------------------') + console.log('COBOT CARDS RESPONSE:') + console.log(JSON.stringify({ statusCode, headers }, null, 2)) + res.setEncoding('utf8') + + let cards = '' + res.on('data', chunk => { + cards += chunk + }) + + res.on('end', () => { + cards = JSON.parse(cards) + console.log(JSON.stringify(cards, null, 2)) + if (!cards || !cards.length) { + throw new Error('No cards received from API!') + } + resolve( + cards.map(card => ({ + name: card.membership.name, + number: card.token, + })) + ) + console.log( + '----------------------------------------------------\n' + ) + }) + } + ) + req.on('data', console.log) + req.on('error', e => { + console.error(e) + reject(e) + }) + req.end() + }) + // return axios + // .get(COBOT_CARDS_API, { + // headers: { + // Authorization: `Bearer ${this.token}`, + // }, + // }) + // .then(resp => + // resp.data.map(card => ({ + // name: card.membership.name, + // number: card.token, + // })) + // ) + } + + static authorize() { + console.log('Authorizing Cobot application...') + if (!COBOT_SCOPE) throw new Error('missing "COBOT_SCOPE" env variable!') + if (!COBOT_USER_EMAIL) + throw new Error('missing "COBOT_USER_EMAIL" env variable!') + if (!COBOT_USER_PASSWORD) + throw new Error('missing "COBOT_USER_PASSWORD" env variable!') + if (!COBOT_CLIENT_ID) + throw new Error('missing "COBOT_CLIENT_ID" env variable!') + if (!COBOT_CLIENT_SECRET) + throw new Error('missing "COBOT_CLIENT_SECRET" env variable!') + + const qs = [ + `scope=${COBOT_SCOPE}`, + `grant_type=password`, + `username=${COBOT_USER_EMAIL}`, + `password=${encodeURI(COBOT_USER_PASSWORD)}`, + `client_id=${COBOT_CLIENT_ID}`, + `client_secret=${COBOT_CLIENT_SECRET}`, + ].join('&') + + return new Promise((resolve, reject) => { + const req = https.request( + { + hostname: 'www.cobot.me', + method: 'POST', + path: `/oauth/access_token?${qs}`, + }, + res => { + const { statusCode, headers } = res + console.log('\n----------------------------------------------------') + console.log('COBOT AUTHORIZATION RESPONSE:') + console.log(JSON.stringify({ statusCode, headers }, null, 2)) + + res.setEncoding('utf8') + + let cobot + res.on('data', chunk => { + const body = JSON.parse(chunk) + console.log(JSON.stringify({ body }, null, 2)) + const token = body.access_token + if (!token) { + console.error(`Expected access token, got: ${body}`) + throw new Error('Error getting access token') + } + cobot = new Cobot(token) + }) + + res.on('end', () => { + resolve(cobot) + console.log( + '----------------------------------------------------\n' + ) + }) + } + ) + req.on('error', e => reject(e)) + req.end() + }) + // return axios + // .post(`https://www.cobot.me/oauth/access_token?${qs}`) + // .then(resp => new Cobot(resp.data.access_token)) + } +} class DoorLock { constructor() { - this.log(chalk.gray('Initializing doorlock')) + this.log('Initializing doorlock') if (!RFID_READER_SERIAL_NUMBER) { throw new Error('No serial number set!') } this.cards = [] - // this.fetchCardListFromCobot() + this.fetchCardListFromCobot() this.initializeRFIDReader() } @@ -37,8 +180,8 @@ class DoorLock { getListOfSerialDevices() { return (SerialPort.list() || Promise.resolve([])).then(devices => { - this.log(chalk.green('Available serial devices:')) - this.log(chalk.gray(JSON.stringify(devices.map(d => d.comName), null, 2))) + this.log('Available serial devices:') + this.log(JSON.stringify(devices.map(d => d.comName), null, 2)) return devices }) } @@ -56,7 +199,7 @@ class DoorLock { } connectToRFIDReaderDevice(port) { - this.log(chalk.green('Connected to device:'), chalk.gray(port)) + this.log('Connected to device:', port) const device = new SerialPort(port, { baudRate: 9600 }) const Readline = SerialPort.parsers.Readline const parser = new Readline() @@ -76,24 +219,24 @@ class DoorLock { 16 ) - this.log(chalk.green('Scanned card:'), chalk.gray(scanned)) + this.log('Scanned card:', scanned) return this.readCardsFromSDCard().then(cards => { const card = cards.find(c => parseInt(c.number) === scanned) if (card) { const name = card.name.split(' ')[0] - this.log(chalk.green(`Welcome in ${name}!`), chalk.gray(scanned)) + this.log(`Welcome in ${name}!`, scanned) this.openDoor() } else { - this.log(chalk.red('Card is invalid:'), chalk.gray(scanned)) + this.log('Card is invalid:', scanned) } }) } openDoor() { return new Promise(resolve => { - this.log(chalk.green('Opening door!')) + this.log('Opening door!') // TODO: trigger door opening... if (tessel.led) tessel.led[3].on() @@ -107,26 +250,24 @@ class DoorLock { } closeDoor() { - this.log(chalk.green('Closing door!')) + this.log('Closing door!') if (tessel.led) tessel.led[3].off() } fetchCardListFromCobot() { - this.log(chalk.gray('Updating cards...')) + this.log('Updating cards...') Cobot.authorize() .then(cobot => cobot.cards()) .then(cards => { - this.log(chalk.green('UPDATED CARDS:', cards.length, 'cards')) + this.log('UPDATED CARDS:', cards.length, 'cards') this.writeCardsToSDCard(cards) this.cards = cards }) .then(() => { this.log( - chalk.gray( - 'Updating card list in', - CARD_UPDATE_INTERVAL / 1000, - 'seconds...' - ) + 'Updating card list in', + CARD_UPDATE_INTERVAL / 1000, + 'seconds...' ) setTimeout(this.fetchCardListFromCobot.bind(this), CARD_UPDATE_INTERVAL) }) @@ -137,7 +278,6 @@ class DoorLock { const sorted = cards.sort( (a, b) => (a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1) ) - this.log(chalk.green('CARDS:'), sorted) return sorted } @@ -145,8 +285,6 @@ class DoorLock { const json = JSON.stringify(this.sortCardsByName(cards)) return new Promise((resolve, reject) => { fs.writeFile(CARDS_PATH, json, err => { - console.log(json) - console.log('JSON', json) if (err) return reject(err) resolve() }) @@ -164,8 +302,8 @@ class DoorLock { logErrorMessage(error) { if (TEST) return - console.error(chalk.red(error.message)) - console.error(chalk.gray(error.stack)) + console.error(error.message) + console.error(error.stack) process.exit } diff --git a/package.json b/package.json index 3bbfb0e..3443ad2 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,10 @@ { "name": "doorlock", "version": "0.1.0", - "main": "src/boot.js", + "main": "doorlock.js", "scripts": { - "deploy": "t2 run src/boot.js", - "start": "node src/boot.js", + "deploy": "t2 run doorlock.js", + "start": "node doorlock.js", "test": "jest", "watch-test": "npm test -- --watch" },