366 lines
9.8 KiB
JavaScript
366 lines
9.8 KiB
JavaScript
// server.js
|
||
const express = require('express')
|
||
const cors = require('cors')
|
||
const { WhatsMiner, ResponseError } = require('@thermocline-labs/whats-miner')
|
||
|
||
const app = express()
|
||
const PORT = process.env.PORT || 3000
|
||
const MINER_IP = process.env.MINER_IP || '192.168.31.174'
|
||
const MINER_PORT = parseInt(process.env.MINER_PORT || '4028')
|
||
|
||
// Middleware
|
||
app.use(cors())
|
||
app.use(express.json())
|
||
app.use(express.urlencoded({ extended: true }))
|
||
|
||
// Логирование запросов
|
||
app.use((req, res, next) => {
|
||
console.log(`[${new Date().toISOString()}] ${req.method} ${req.path}`)
|
||
next()
|
||
})
|
||
|
||
// Инициализация клиента майнера
|
||
const client = new WhatsMiner(MINER_IP, MINER_PORT)
|
||
|
||
// Обработчик ошибок
|
||
const errorHandler = (err, res, customMessage = 'Internal server error') => {
|
||
console.error('Error:', err)
|
||
|
||
if (err instanceof ResponseError) {
|
||
return res.status(502).json({
|
||
success: false,
|
||
error: {
|
||
type: 'ResponseError',
|
||
message: err.message,
|
||
code: err.code
|
||
}
|
||
})
|
||
}
|
||
|
||
if (err.name === 'AbortError') {
|
||
return res.status(504).json({
|
||
success: false,
|
||
error: {
|
||
type: 'TimeoutError',
|
||
message: 'Request timeout'
|
||
}
|
||
})
|
||
}
|
||
|
||
return res.status(500).json({
|
||
success: false,
|
||
error: {
|
||
type: err.name || 'UnknownError',
|
||
message: customMessage,
|
||
details: process.env.NODE_ENV === 'development' ? err.message : undefined
|
||
}
|
||
})
|
||
}
|
||
|
||
// Эндпоинт: получение общей информации
|
||
app.get('/api/summary', async (req, res) => {
|
||
const abortController = new AbortController()
|
||
const timeout = setTimeout(() => abortController.abort(), 5000) // 5 сек таймаут
|
||
|
||
try {
|
||
const summary = await client.summary(abortController.signal)
|
||
|
||
clearTimeout(timeout)
|
||
|
||
res.json({
|
||
success: true,
|
||
timestamp: new Date().toISOString(),
|
||
data: summary
|
||
})
|
||
} catch (err) {
|
||
clearTimeout(timeout)
|
||
errorHandler(err, res, 'Failed to fetch summary')
|
||
}
|
||
})
|
||
|
||
// Эндпоинт: информация об устройствах (чипах)
|
||
app.get('/api/devs', async (req, res) => {
|
||
const abortController = new AbortController()
|
||
const timeout = setTimeout(() => abortController.abort(), 5000)
|
||
|
||
try {
|
||
const devs = await client.edevs(abortController.signal)
|
||
|
||
clearTimeout(timeout)
|
||
|
||
res.json({
|
||
success: true,
|
||
timestamp: new Date().toISOString(),
|
||
data: devs
|
||
})
|
||
} catch (err) {
|
||
clearTimeout(timeout)
|
||
errorHandler(err, res, 'Failed to fetch devices')
|
||
}
|
||
})
|
||
|
||
app.get('/api/devdetails', async (req, res) => {
|
||
const abortController = new AbortController()
|
||
const timeout = setTimeout(() => abortController.abort(), 5000)
|
||
|
||
try {
|
||
const devs = await client.devDetails(abortController.signal)
|
||
|
||
clearTimeout(timeout)
|
||
|
||
res.json({
|
||
success: true,
|
||
timestamp: new Date().toISOString(),
|
||
data: devs
|
||
})
|
||
} catch (err) {
|
||
clearTimeout(timeout)
|
||
errorHandler(err, res, 'Failed to fetch devices')
|
||
}
|
||
})
|
||
|
||
// Эндпоинт: информация о пулах
|
||
app.get('/api/pools', async (req, res) => {
|
||
const abortController = new AbortController()
|
||
const timeout = setTimeout(() => abortController.abort(), 5000)
|
||
|
||
try {
|
||
const pools = await client.pools(abortController.signal)
|
||
|
||
clearTimeout(timeout)
|
||
|
||
res.json({
|
||
success: true,
|
||
timestamp: new Date().toISOString(),
|
||
data: pools
|
||
})
|
||
} catch (err) {
|
||
clearTimeout(timeout)
|
||
errorHandler(err, res, 'Failed to fetch pools')
|
||
}
|
||
})
|
||
|
||
// Эндпоинт: температура и вентиляторы
|
||
app.get('/api/temperature', async (req, res) => {
|
||
const abortController = new AbortController()
|
||
const timeout = setTimeout(() => abortController.abort(), 5000)
|
||
|
||
try {
|
||
const devs = await client.edevs(abortController.signal)
|
||
|
||
clearTimeout(timeout)
|
||
|
||
// Извлечение температурных данных
|
||
const temperatureData = {
|
||
chip1: {
|
||
temp_current: devs[0]['Temperature'] || null,
|
||
temp_awg: devs[0]['Chip Temp Avg'] || null,
|
||
temp_min: devs[0]['Chip Temp Min'] || null,
|
||
temp_max: devs[0]['Chip Temp Max'] || null
|
||
},
|
||
chip2: {
|
||
temp_current: devs[1]['Temperature'] || null,
|
||
temp_awg: devs[1]['Chip Temp Avg'] || null,
|
||
temp_min: devs[1]['Chip Temp Min'] || null,
|
||
temp_max: devs[1]['Chip Temp Max'] || null
|
||
}
|
||
}
|
||
|
||
res.json({
|
||
success: true,
|
||
timestamp: new Date().toISOString(),
|
||
data: temperatureData
|
||
})
|
||
} catch (err) {
|
||
clearTimeout(timeout)
|
||
errorHandler(err, res, 'Failed to fetch temperature data')
|
||
}
|
||
})
|
||
|
||
// Эндпоинт: хешрейт
|
||
app.get('/api/hashrate', async (req, res) => {
|
||
const abortController = new AbortController()
|
||
const timeout = setTimeout(() => abortController.abort(), 5000)
|
||
function format(num) {
|
||
const del = 1_000_000
|
||
return Math.round(num / del * 100) / 100
|
||
}
|
||
try {
|
||
const summary = await client.summary(abortController.signal)
|
||
|
||
clearTimeout(timeout)
|
||
|
||
const hashrateData = {
|
||
current: format(summary[0]['HS RT']),
|
||
average: format(summary[0]['MHS av']),
|
||
'1_sec': format(summary[0]['MHS 5s']),
|
||
'1_min': format(summary[0]['MHS 1m']),
|
||
'5_min': format(summary[0]['MHS 5m']),
|
||
'15_min': format(summary[0]['MHS 15m']),
|
||
unit: 'TH/s',
|
||
power_watt: summary[0]['Power'],
|
||
'watt/th': summary[0]['Power Rate']
|
||
}
|
||
|
||
res.json({
|
||
success: true,
|
||
timestamp: new Date().toISOString(),
|
||
data: hashrateData
|
||
})
|
||
} catch (err) {
|
||
clearTimeout(timeout)
|
||
errorHandler(err, res, 'Failed to fetch hashrate')
|
||
}
|
||
})
|
||
|
||
// Эндпоинт: перезагрузка майнера
|
||
app.post('/api/reboot', async (req, res) => {
|
||
const abortController = new AbortController()
|
||
const timeout = setTimeout(() => abortController.abort(), 10000)
|
||
|
||
try {
|
||
const result = await client.reboot(abortController.signal)
|
||
|
||
clearTimeout(timeout)
|
||
|
||
res.json({
|
||
success: true,
|
||
message: 'Miner reboot initiated',
|
||
data: result
|
||
})
|
||
} catch (err) {
|
||
clearTimeout(timeout)
|
||
errorHandler(err, res, 'Failed to reboot miner')
|
||
}
|
||
})
|
||
|
||
// Эндпоинт: проверка состояния
|
||
app.get('/api/health', async (req, res) => {
|
||
const abortController = new AbortController()
|
||
const timeout = setTimeout(() => abortController.abort(), 3000)
|
||
|
||
try {
|
||
const summary = await client.summary(abortController.signal)
|
||
|
||
clearTimeout(timeout)
|
||
|
||
res.json({
|
||
success: true,
|
||
status: 'healthy',
|
||
miner: {
|
||
ip: MINER_IP,
|
||
port: MINER_PORT,
|
||
connected: true,
|
||
uptime: summary.Uptime || null,
|
||
hashrate: summary.GHS_5s || 0
|
||
},
|
||
timestamp: new Date().toISOString()
|
||
})
|
||
} catch (err) {
|
||
clearTimeout(timeout)
|
||
|
||
res.status(503).json({
|
||
success: false,
|
||
status: 'unhealthy',
|
||
miner: {
|
||
ip: MINER_IP,
|
||
port: MINER_PORT,
|
||
connected: false
|
||
},
|
||
error: err.message,
|
||
timestamp: new Date().toISOString()
|
||
})
|
||
}
|
||
})
|
||
|
||
// Эндпоинт: все данные (агрегированный)
|
||
app.get('/api/all', async (req, res) => {
|
||
const abortController = new AbortController()
|
||
const timeout = setTimeout(() => abortController.abort(), 10000)
|
||
|
||
try {
|
||
const [summary, stats, devs, pools] = await Promise.all([
|
||
client.summary(abortController.signal),
|
||
client.stats(abortController.signal),
|
||
client.devs(abortController.signal),
|
||
client.pools(abortController.signal)
|
||
])
|
||
|
||
clearTimeout(timeout)
|
||
|
||
const aggregatedData = {
|
||
summary,
|
||
stats,
|
||
devs,
|
||
pools,
|
||
quick_stats: {
|
||
hashrate: summary.GHS_5s || 0,
|
||
temperature: summary.BoardTemp || summary.temp || null,
|
||
uptime: summary.Uptime || null,
|
||
chips_active: devs?.DEVS?.reduce((sum, dev) => sum + (dev.ChipsActive || 0), 0) || null
|
||
}
|
||
}
|
||
|
||
res.json({
|
||
success: true,
|
||
timestamp: new Date().toISOString(),
|
||
data: aggregatedData
|
||
})
|
||
} catch (err) {
|
||
clearTimeout(timeout)
|
||
errorHandler(err, res, 'Failed to fetch all data')
|
||
}
|
||
})
|
||
|
||
// Главная страница (документация)
|
||
app.get('/', (req, res) => {
|
||
res.json({
|
||
api: 'WhatsMiner REST API',
|
||
version: '1.0.0',
|
||
miner: {
|
||
ip: MINER_IP,
|
||
port: MINER_PORT
|
||
},
|
||
endpoints: {
|
||
'GET /api/summary': 'Общая информация о майнере',
|
||
'GET /api/devs': 'Информация об устройствах (чипах)',
|
||
'GET /api/devdetails': 'Информация об устройствах',
|
||
'GET /api/pools': 'Информация о пулах',
|
||
'GET /api/temperature': 'Температура и вентиляторы',
|
||
'GET /api/hashrate': 'Текущий хешрейт',
|
||
'GET /api/health': 'Проверка состояния',
|
||
'GET /api/all': 'Все данные (агрегированные)',
|
||
'POST /api/reboot': 'Перезагрузка майнера'
|
||
}
|
||
})
|
||
})
|
||
|
||
// Обработчик 404
|
||
app.use((req, res) => {
|
||
res.status(404).json({
|
||
success: false,
|
||
error: {
|
||
type: 'NotFoundError',
|
||
message: 'Endpoint not found',
|
||
path: req.path
|
||
}
|
||
})
|
||
})
|
||
|
||
// Запуск сервера
|
||
app.listen(PORT, () => {
|
||
console.log(`🚀 WhatsMiner API запущен на порту ${PORT}`)
|
||
console.log(`📍 IP майнера: ${MINER_IP}:${MINER_PORT}`)
|
||
console.log(`📚 Документация: http://localhost:${PORT}`)
|
||
})
|
||
|
||
// Обработка завершения
|
||
process.on('SIGTERM', () => {
|
||
console.log('SIGTERM received, shutting down gracefully')
|
||
process.exit(0)
|
||
})
|
||
|
||
process.on('SIGINT', () => {
|
||
console.log('SIGINT received, shutting down gracefully')
|
||
process.exit(0)
|
||
}) |