diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..834eb3f --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +9.8.0 diff --git a/miner-test-server.js b/miner-test-server.js new file mode 100644 index 0000000..ca455eb --- /dev/null +++ b/miner-test-server.js @@ -0,0 +1,217 @@ +'use strict'; + +const sampleData = { + "summary": [ + { + "STATUS": [ + { + "STATUS": "S", + "When": 1541144892, + "Code": 11, + "Msg": "Summary", + "Description": "bfgminer 5.4.2" + } + ], + "SUMMARY": [ + { + "Elapsed": 847, + "MHS av": 3.364, + "MHS 20s": 3.324, + "Found Blocks": 0, + "Getworks": 30, + "Accepted": 168, + "Rejected": 0, + "Hardware Errors": 2, + "Utility": 11.9, + "Discarded": 78, + "Stale": 0, + "Get Failures": 0, + "Local Work": 138, + "Remote Failures": 0, + "Network Blocks": 5, + "Total MH": 2849.3488, + "Diff1 Work": 0.67822266, + "Work Utility": 0.048, + "Difficulty Accepted": 0.65625, + "Difficulty Rejected": 0, + "Difficulty Stale": 0, + "Best Share": 1.03726997, + "Device Hardware%": 0.1438, + "Device Rejected%": 0, + "Pool Rejected%": 0, + "Pool Stale%": 0, + "Last getwork": 1541144888 + } + ], + "id": 1 + } + ], + "devs": [ + { + "STATUS": [ + { + "STATUS": "S", + "When": 1541144892, + "Code": 9, + "Msg": "1 PGA(s)", + "Description": "bfgminer 5.4.2" + } + ], + "DEVS": [ + { + "PGA": 0, + "Name": "MLD", + "ID": 0, + "Enabled": "Y", + "Status": "Alive", + "Device Elapsed": 847, + "MHS av": 3.363, + "MHS 20s": 3.373, + "MHS rolling": 3.373, + "Accepted": 168, + "Rejected": 0, + "Hardware Errors": 2, + "Utility": 11.897, + "Stale": 0, + "Last Share Pool": 1, + "Last Share Time": 1541144887, + "Total MH": 2849.3488, + "Diff1 Work": 0.67822266, + "Work Utility": 0.048, + "Difficulty Accepted": 0.65625, + "Difficulty Rejected": 0, + "Difficulty Stale": 0, + "Last Share Difficulty": 0.00390625, + "Last Valid Work": 1541144891, + "Device Hardware%": 0.1438, + "Device Rejected%": 0 + } + ], + "id": 1 + } + ], + "pools": [ + { + "STATUS": [ + { + "STATUS": "S", + "When": 1541144892, + "Code": 7, + "Msg": "2 Pool(s)", + "Description": "bfgminer 5.4.2" + } + ], + "POOLS": [ + { + "POOL": 0, + "URL": "stratum+tcp://us.litecoinpool.org:3333", + "Status": "Alive", + "Priority": 1, + "Quota": 1, + "Mining Goal": "default", + "Long Poll": "N", + "Getworks": 3, + "Accepted": 0, + "Rejected": 0, + "Works": 0, + "Discarded": 0, + "Stale": 0, + "Get Failures": 0, + "Remote Failures": 0, + "User": "jstefanop.1", + "Last Share Time": 0, + "Diff1 Shares": 0, + "Proxy": "", + "Difficulty Accepted": 0, + "Difficulty Rejected": 0, + "Difficulty Stale": 0, + "Last Share Difficulty": 0, + "Has Stratum": true, + "Stratum Active": false, + "Stratum URL": "", + "Best Share": 0, + "Pool Rejected%": 0, + "Pool Stale%": 0 + }, + { + "POOL": 1, + "URL": "stratum+tcp://us.litecoinpool.org:3333", + "Status": "Alive", + "Priority": 0, + "Quota": 1, + "Mining Goal": "default", + "Long Poll": "N", + "Getworks": 27, + "Accepted": 168, + "Rejected": 0, + "Works": 30, + "Discarded": 78, + "Stale": 0, + "Get Failures": 0, + "Remote Failures": 0, + "User": "jstefanop.1", + "Last Share Time": 1541144887, + "Diff1 Shares": 0.67822266, + "Proxy": "", + "Difficulty Accepted": 0.65625, + "Difficulty Rejected": 0, + "Difficulty Stale": 0, + "Last Share Difficulty": 0.00390625, + "Has Stratum": true, + "Stratum Active": true, + "Stratum URL": "us.litecoinpool.org", + "Best Share": 1.03726997, + "Pool Rejected%": 0, + "Pool Stale%": 0 + } + ], + "id": 1 + } + ], + "id": 1 +} + +const net = require('net'); +const PORT = 4028; +const HOST = 'localhost'; + +class Server { + constructor(port, address) { + this.port = port || PORT; + this.address = address || HOST; + + this.init(); + } + + init() { + let server = this; + + let onClientConnected = (sock) => { + + let clientName = `${sock.remoteAddress}:${sock.remotePort}`; + console.log(`new client connected: ${clientName}`); + + sock.on('data', (data) => { + console.log(`${clientName} Says: ${data}`); + sock.write(JSON.stringify(sampleData)); + // sock.write('exit'); + }); + + sock.on('close', () => { + console.log(`connection from ${clientName} closed`); + }); + + sock.on('error', (err) => { + console.log(`Connection ${clientName} error: ${err.message}`); + }); + } + + server.connection = net.createServer(onClientConnected); + + server.connection.listen(PORT, HOST, function() { + console.log(`Server started at: ${HOST}:${PORT}`); + }); + } +} + +new Server(); \ No newline at end of file diff --git a/package.json b/package.json index 8c2c30a..4c32962 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "express": "^4.16.4", "knex": "^0.15.2", "luxon": "^1.4.4", + "normalize-object": "^1.1.5", "sqlite3": "^4.0.3" }, "devDependencies": { diff --git a/src/app/minerClient.js b/src/app/minerClient.js new file mode 100644 index 0000000..93d2cee --- /dev/null +++ b/src/app/minerClient.js @@ -0,0 +1,27 @@ +'use strict'; + +const net = require('net'); +const PORT = 4028; +const HOST = 'localhost'; + +class Client { + constructor(port, address) { + this.socket = new net.Socket(); + this.address = address || HOST; + this.port = port || PORT; + this.init(); + } + + init() { + var client = this; + client.socket.connect(client.port, client.address, () => { + console.log(`Client connected to: ${client.address} : ${client.port}`); + }); + + client.socket.on('close', () => { + console.log('Client closed'); + }); + } +} + +module.exports = Client; \ No newline at end of file diff --git a/src/graphql/graphqlModules/Miner/Miner.js b/src/graphql/graphqlModules/Miner/Miner.js new file mode 100644 index 0000000..9500a65 --- /dev/null +++ b/src/graphql/graphqlModules/Miner/Miner.js @@ -0,0 +1,13 @@ +module.exports.typeDefs = ` + type Query { + Miner: MinerActions + } +` + +module.exports.resolvers = { + Query: { + Miner () { + return {} + } + } +} diff --git a/src/graphql/graphqlModules/Miner/MinerRestart.js b/src/graphql/graphqlModules/Miner/MinerRestart.js new file mode 100644 index 0000000..b0450e4 --- /dev/null +++ b/src/graphql/graphqlModules/Miner/MinerRestart.js @@ -0,0 +1,13 @@ +module.exports.typeDefs = ` + type MinerActions { + restart: EmptyOutput! + } +` + +module.exports.resolvers = { + MinerActions: { + restart (root, args, { dispatch }) { + return dispatch('api/miner/restart') + } + } +} diff --git a/src/graphql/graphqlModules/Miner/MinerStart.js b/src/graphql/graphqlModules/Miner/MinerStart.js new file mode 100644 index 0000000..d9f6bfd --- /dev/null +++ b/src/graphql/graphqlModules/Miner/MinerStart.js @@ -0,0 +1,13 @@ +module.exports.typeDefs = ` + type MinerActions { + start: EmptyOutput! + } +` + +module.exports.resolvers = { + MinerActions: { + start (root, args, { dispatch }) { + return dispatch('api/miner/start') + } + } +} diff --git a/src/graphql/graphqlModules/Miner/MinerStats.js b/src/graphql/graphqlModules/Miner/MinerStats.js new file mode 100644 index 0000000..66cec6e --- /dev/null +++ b/src/graphql/graphqlModules/Miner/MinerStats.js @@ -0,0 +1,142 @@ +module.exports.typeDefs = ` + type MinerActions { + stats: MinerStatsOutput! + } + + type MinerStatsOutput { + result: MinerStatsResult + error: Error + } + + type MinerStatsResult { + stats: MinerStats! + } + + type MinerStats { + summary: MinerStatsSummary + devs: MinerStatsDevs + pools: MinerStatsPools + } + + type MinerStatsSummary { + status: MinerStatsStatus + data: MinerStatsSummaryData + } + + type MinerStatsDevs { + status: MinerStatsStatus + data: MinerStatsDevsData + } + + type MinerStatsPools { + status: MinerStatsStatus + data: MinerStatsPoolsData + } + + type MinerStatsStatus { + status: String + when: Int + code: Int + msg: String + description: String + } + + type MinerStatsSummaryData { + elapsed: Int + mHSAv: Float + mHS20s: Float + foundBlocks: Int + getworks: Int + accepted: Int + rejected: Int + hardwareErrors: Int + utility: Float + discarded: Int + stale: Int + getFailures: Int + localWork: Int + remoteFailures: Int + networkBlocks: Int + totalMH: Float + diff1Work: Float + workUtility: Float + difficultyAccepted: Float + difficultyRejected: Int + difficultyStale: Int + bestShare: Float + deviceHardware: Float + deviceRejected: Int + poolRejected: Int + poolStale: Int + lastGetwork: Int + } + + type MinerStatsDevsData { + pga: Int + name: String + id: Int + enabled: String + status: String + deviceElapsed: Int + mHSAv: Float + mHS20s: Float + mHSRolling: Float + accepted: Int + rejected: Int + hardwareErrors: Int + utility: Float + stale: Int + lastSharePool: Int + lastShareTime: Int + totalMH: Float + diff1Work: Float + workUtility: Float + difficultyAccepted: Float + difficultyRejected: Int + difficultyStale: Int + lastShareDifficulty: Float + lastValidWork: Int + deviceHardware: Float + deviceRejected: Int + } + + type MinerStatsPoolsData { + pool: Int + url: String + status: String + priority: Int + quota: Int + miningGoal: String + longPoll: String + getworks: Int + accepted: Int + rejected: Int + works: Int + discarded: Int + stale: Int + getFailures: Int + remoteFailures: Int + user: String + lastShareTime: Int + diff1Shares: Int + proxy: String + difficultyAccepted: Int + difficultyRejected: Int + difficultyStale: Int + lastShareDifficulty: Int + hasStratum: Boolean + stratumActive: Boolean + stratumURL: String + bestShare: Int + poolRejected: Int + poolStale: Int + } +` + +module.exports.resolvers = { + MinerActions: { + stats (root, args, { dispatch }) { + return dispatch('api/miner/stats') + } + } +} diff --git a/src/graphql/graphqlModules/Miner/MinerStop.js b/src/graphql/graphqlModules/Miner/MinerStop.js new file mode 100644 index 0000000..f01a1b1 --- /dev/null +++ b/src/graphql/graphqlModules/Miner/MinerStop.js @@ -0,0 +1,13 @@ +module.exports.typeDefs = ` + type MinerActions { + stop: EmptyOutput! + } +` + +module.exports.resolvers = { + MinerActions: { + stop (root, args, { dispatch }) { + return dispatch('api/miner/stop') + } + } +} diff --git a/src/graphql/graphqlModules/Settings/Settings.js b/src/graphql/graphqlModules/Settings/Settings.js index e169ffd..9a26d16 100644 --- a/src/graphql/graphqlModules/Settings/Settings.js +++ b/src/graphql/graphqlModules/Settings/Settings.js @@ -7,9 +7,11 @@ module.exports.typeDefs = ` enum TemperatureUnit { f, c } type Settings { + id: Int! + createdAt: String! minerMode: MinerMode! - voltage: Float!, - frequency: Int!, + voltage: Float! + frequency: Int! fan: Int! connectedWifi: String leftSidebarVisibility: Boolean! diff --git a/src/graphql/graphqlModules/Settings/SettingsList.js b/src/graphql/graphqlModules/Settings/SettingsList.js new file mode 100644 index 0000000..d48f621 --- /dev/null +++ b/src/graphql/graphqlModules/Settings/SettingsList.js @@ -0,0 +1,22 @@ +module.exports.typeDefs = ` + type SettingsActions { + list: SettingListOutput! + } + + type SettingListOutput { + result: SettingListResult + error: Error + } + + type SettingListResult { + settings: [Settings!]! + } +` + +module.exports.resolvers = { + SettingsActions: { + list (root, args, { dispatch }) { + return dispatch('api/settings/list', args.input) + } + } +} diff --git a/src/store/api/miner/minerRestart.js b/src/store/api/miner/minerRestart.js new file mode 100644 index 0000000..43d786f --- /dev/null +++ b/src/store/api/miner/minerRestart.js @@ -0,0 +1,9 @@ +const { exec } = require('child_process') + +module.exports = ({ define }) => { + define('restart', async (payload, { knex, errors, utils }) => { + exec('sudo systemctl restart bfgminer') + }, { + auth: true + }) +} diff --git a/src/store/api/miner/minerStart.js b/src/store/api/miner/minerStart.js new file mode 100644 index 0000000..fd7567b --- /dev/null +++ b/src/store/api/miner/minerStart.js @@ -0,0 +1,9 @@ +const { exec } = require('child_process') + +module.exports = ({ define }) => { + define('start', async (payload, { knex, errors, utils }) => { + exec('sudo systemctl start bfgminer') + }, { + auth: true + }) +} diff --git a/src/store/api/miner/minerStats.js b/src/store/api/miner/minerStats.js new file mode 100644 index 0000000..8d45e1a --- /dev/null +++ b/src/store/api/miner/minerStats.js @@ -0,0 +1,59 @@ +const { join } = require('path') +const { exec } = require('child_process') +const normalize = require('normalize-object') +const Client =require('../../../app/minerClient'); + +module.exports = ({ define }) => { + define('stats', async (payload, { knex, errors, utils }) => { + const stats = await getMinerStats() + return { stats } + }, { + auth: true + }) +} + +function getMinerStats () { + const client = new Client(); + return new Promise((resolve, reject) => { + + client.socket.write('{"command":"summary+devs+pools"}'); + + client.socket.on('data', (data) => { + let received = data.toString('utf8').trim(); + try { + received = JSON.parse(received); + const summary = (received.summary && received.summary[0]) ? received.summary[0] : null; + const devs = (received.devs && received.devs[0]) ? received.devs[0] : null; + const pools = (received.pools && received.pools[0]) ? received.pools[0] : null; + + let results = { + summary: { + status: (summary.STATUS && summary.STATUS[0]) ? summary.STATUS[0] : null, + data: (summary.SUMMARY && summary.SUMMARY[0]) ? summary.SUMMARY[0] : null + }, + devs: { + status: (devs.STATUS && devs.STATUS[0]) ? devs.STATUS[0] : null, + data: (devs.DEVS && devs.DEVS[0]) ? devs.DEVS[0] : null + }, + pools: { + status: (pools.STATUS && pools.STATUS[0]) ? pools.STATUS[0] : null, + data: (pools.POOLS && pools.POOLS[0]) ? pools.POOLS[0] : null + } + } + + results = normalize(results, 'camel'); + + resolve(results) + } catch (err) { + reject(err); + } + + client.socket.destroy(); + }); + + client.socket.on('error', (err) => { + reject(err); + }); + + }); +} diff --git a/src/store/api/miner/minerStop.js b/src/store/api/miner/minerStop.js new file mode 100644 index 0000000..a7ca850 --- /dev/null +++ b/src/store/api/miner/minerStop.js @@ -0,0 +1,9 @@ +const { exec } = require('child_process') + +module.exports = ({ define }) => { + define('stop', async (payload, { knex, errors, utils }) => { + exec('sudo systemctl stop bfgminer') + }, { + auth: true + }) +} diff --git a/src/store/api/settings/collection/settingsList.js b/src/store/api/settings/collection/settingsList.js new file mode 100644 index 0000000..abb66e8 --- /dev/null +++ b/src/store/api/settings/collection/settingsList.js @@ -0,0 +1,48 @@ +module.exports = ({ define }) => { + define('list', async ({ + where = {}, + one, + forUpdate + }, { + context: { trx } = {}, + knex + }) => { + const readQ = (trx || knex)('settings') + + if (where.id) { + readQ.where('id', where.id) + } + + readQ.select( + 'id', + 'created_at as createdAt', + 'miner_mode as minerMode', + 'voltage', + 'frequency', + 'fan', + 'connected_wifi as connectedWifi', + 'left_sidebar_visibility as leftSidebarVisibility', + 'left_sidebar_extended as leftSidebarExtended', + 'right_sidebar_visibility as rightSidebarVisibility', + 'temperature_unit as temperatureUnit' + ) + + readQ.orderBy('created_at', 'desc') + + readQ.limit(10) + + if (forUpdate) { + readQ.forUpdate() + } + + const items = await readQ + + if (one) { + return items[0] || null + } + + return { + items + } + }) +} diff --git a/src/store/api/settings/settingsList.js b/src/store/api/settings/settingsList.js new file mode 100644 index 0000000..32a059e --- /dev/null +++ b/src/store/api/settings/settingsList.js @@ -0,0 +1,10 @@ +module.exports = ({ define }) => { + define('list', async (payload, { dispatch, errors, utils }) => { + const { items: settings } = await dispatch('api/settings/collection/list', {}) + return { + settings + } + }, { + auth: true + }) +} diff --git a/yarn.lock b/yarn.lock index 4a807c6..feae418 100644 --- a/yarn.lock +++ b/yarn.lock @@ -367,6 +367,11 @@ call-me-maybe@^1.0.1: resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.1.tgz#26d208ea89e37b5cbde60250a15f031c16a4d66b" integrity sha1-JtII6onje1y95gJQoV8DHBak1ms= +case@^1.4.1: + version "1.5.5" + resolved "https://registry.yarnpkg.com/case/-/case-1.5.5.tgz#8e832ef0329609a4f3198dee37c029645dc27c72" + integrity sha512-tQm8bxc8L9Dk/6FGhtBtV89rrRzqytUbdLqGZxmGwYKqeAD0VmLc8kYSqm0GXOTsf9r1tc0bzq+CDLqtrjiuHw== + caseless@~0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" @@ -1945,6 +1950,15 @@ nopt@^4.0.1: abbrev "1" osenv "^0.1.4" +normalize-object@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/normalize-object/-/normalize-object-1.1.5.tgz#c8f767851d04d6f28ef863af61ff083ba24affee" + integrity sha1-yPdnhR0E1vKO+GOvYf8IO6JK/+4= + dependencies: + case "^1.4.1" + underscore "^1.7.0" + underscore.string "^3.3.4" + npm-bundled@^1.0.1: version "1.0.5" resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.0.5.tgz#3c1732b7ba936b3a10325aef616467c0ccbcc979" @@ -2602,6 +2616,11 @@ split-string@^3.0.1, split-string@^3.0.2: dependencies: extend-shallow "^3.0.0" +sprintf-js@^1.0.3: + version "1.1.1" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.1.tgz#36be78320afe5801f6cea3ee78b6e5aab940ea0c" + integrity sha1-Nr54Mgr+WAH2zqPueLblqrlA6gw= + sqlite3@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/sqlite3/-/sqlite3-4.0.3.tgz#da8c167a87941657fd22e27b248aa371e192b715" @@ -2818,6 +2837,19 @@ unc-path-regex@^0.1.2: resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" integrity sha1-5z3T17DXxe2G+6xrCufYxqadUPo= +underscore.string@^3.3.4: + version "3.3.5" + resolved "https://registry.yarnpkg.com/underscore.string/-/underscore.string-3.3.5.tgz#fc2ad255b8bd309e239cbc5816fd23a9b7ea4023" + integrity sha512-g+dpmgn+XBneLmXXo+sGlW5xQEt4ErkS3mgeN2GFbremYeMBSJKr9Wf2KJplQVaiPY/f7FN6atosWYNm9ovrYg== + dependencies: + sprintf-js "^1.0.3" + util-deprecate "^1.0.2" + +underscore@^1.7.0: + version "1.9.1" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.9.1.tgz#06dce34a0e68a7babc29b365b8e74b8925203961" + integrity sha512-5/4etnCkd9c8gwgowi5/om/mYO5ajCaOgdzj/oW+0eQV9WxKBDZw5+ycmKmeaTXjInS/W0BzpGLo2xR2aBwZdg== + union-value@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.0.tgz#5c71c34cb5bad5dcebe3ea0cd08207ba5aa1aea4" @@ -2865,7 +2897,7 @@ use@^3.1.0: resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== -util-deprecate@~1.0.1: +util-deprecate@^1.0.2, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=