diff --git a/.gitignore b/.gitignore index 5db995f3..33ada764 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,10 @@ .vs *.sln *.vcxproj* +dashboard/node_modules/ +**/node_modules/ +dashboard/package-lock.json +dashboard/package.json NerdMinerLog.txt bin/0x10000_firmware.bin logs diff --git a/dashboard/Dashboard.PNG b/dashboard/Dashboard.PNG new file mode 100644 index 00000000..59f61eb6 Binary files /dev/null and b/dashboard/Dashboard.PNG differ diff --git a/dashboard/Dashboard2.PNG b/dashboard/Dashboard2.PNG new file mode 100644 index 00000000..156ce59e Binary files /dev/null and b/dashboard/Dashboard2.PNG differ diff --git a/dashboard/Dockerfile b/dashboard/Dockerfile new file mode 100644 index 00000000..6dcfd9ee --- /dev/null +++ b/dashboard/Dockerfile @@ -0,0 +1,17 @@ +FROM node:18-alpine + +# Create app directory +WORKDIR /app + +# Install app dependencies +COPY package*.json ./ +RUN npm install --only=production + +# Bundle app source +COPY . . + +# Expose ports +EXPOSE 3000 +EXPOSE 33333/udp + +CMD [ "node", "server.js" ] diff --git a/dashboard/EditConfig.PNG b/dashboard/EditConfig.PNG new file mode 100644 index 00000000..d1afc835 Binary files /dev/null and b/dashboard/EditConfig.PNG differ diff --git a/dashboard/README.md b/dashboard/README.md new file mode 100644 index 00000000..bd9b3fb1 --- /dev/null +++ b/dashboard/README.md @@ -0,0 +1,109 @@ +# NerdMiner Dashboard + +This is a local dashboard to monitor and configure your NerdMiner fleet via UDP, and Rest API. + +> **Updated Firmware Required**: In order to use the dashboard you must update the firmware on your specific device, The firmware has been updated to broadcast a json payload via UDP including the miner's IP address. Broadcasting the miner's IP resolves connectivity issues when running in Docker/Umbrel where the source IP is masked. Please flash the latest firmware to use Remote Configuration. + +## Firmware Configuration + +The NerdMiner firmware has been modified to broadcast stats to `255.255.255.255` on port `33333` via UDP. +Ensure your miners and this computer are on the same network/subnet. you can then expose this via Tailscail + +## Remote Configuration + +To configure a miner remotely: +1. Ensure you are running the **latest firmware** with `api.cpp` enabled. +2. On the dashboard, click the **Gear Icon (⚙️)** on the miner card. +3. A modal will appear showing the current settings. +4. Update your Pool, Wallet, etc., and click **Save Changes**. +5. The miner will save settings to NVS and restart automatically. + +**Note:** This uses a secure proxy through the dashboard backend to communicate with the miner's local API. + +## Tips / donations. + +Found this useful? buy me a beer or send a tip, In no way required what so ever, but much appreciated + +**BTC:** bc1qjqhg5c2f6da8y4qr7upegwhkvl2376xzlpwf5p\ +**ETH:** +0x1c054d43c8b6452ceb5d9fe773cc7da66764c283\ +**SOL:** +GTMphvuZU3QsHbieCwWutf1gRGmLWWEVY5dPq73pkgnz\ +**USDC on Ethereum:** +0x1c054d43c8b6452ceb5d9fe773cc7da66764c283 + +## Features + +* **Real-time Monitoring**: Live updates via UDP Broadcast (port 33333). +* **Auto-Discovery**: Miners appear automatically when they start hashing. +* **Detailed Stats**: + * Hashrate (Dynamic units: H/s, KH/s, MH/s) + * Miner Name (extracted from wallet address) + * Pool Address & Port + * Uptime, Temperature, Valid Shares, Templates, Best Difficulty +* **Remote Configuration Rest API**: + * **Settings**: Change Pool, Port, Address, and Password remotely. + * **Persistence**: Settings saved to flash memory (NVS). + * **Auto-Restart**: Miner applies settings automatically. +* **Bitcoin Network Stats**: + * Real-time Price (USD) + * Network Hashrate & Difficulty + * Halving Progress Bar + * Block Height & Fees +* **Fleet Performance**: + * **24-Hour Graph**: Real-time line chart showing total hashrate history. + * **24h Average**: Rolling average calculation. + * **Smart Formatting**: Difficulty (k, M, G, T, P, E) and Hashrate auto-scaling. +* **Zero Configuration**: No IP setup needed on miners; just flash and run. + +## Non docker setup + +1. **Install Dependencies**: + ```bash + npm install + ``` + +2. **Start the Server**: + ```bash + node server.js + ``` + +3. **Access Dashboard**: + Open your browser and navigate to `http://localhost:3000`. + +## Docker & Umbrel Support + +You can run this dashboard as a Docker container. + +### Linux / Umbrel (Recommended for Production) +```bash +docker-compose up -d --build +``` +*Uses `network_mode: "host"` for proper UDP broadcast reception.* + +### Windows (Testing/Development) +```bash +docker-compose -f docker-compose.windows.yml up -d --build +``` +*Uses port mapping. Access at `http://localhost:3000`* + +**Note:** UDP broadcasts from miners may not reach the container on Windows due to Docker's networking limitations. For full functionality, deploy on Linux/Umbrel. + +### Umbrel +This app is ready for Umbrel. +1. install Portainer from the umbrel app store +2. Once in portainer, navigate to the environment you want to add this to, then click on "Add Container" +3. for docker.io image, use `ocybress/nerdminer-dashboard-linux:r0.0.8` +4. for the ports, add `3000` TCP, and `33333` UDP +5. click "diploy the container" +6. wait for the container to start +7. navigate to `http://localhost:3000` to access the dashboard +8. you can expose this via tailscail if you want to access it from other devices + +Dashboard +Dashboard2 +EditConfig + + + + diff --git a/dashboard/docker-compose.windows.yml b/dashboard/docker-compose.windows.yml new file mode 100644 index 00000000..85f59f80 --- /dev/null +++ b/dashboard/docker-compose.windows.yml @@ -0,0 +1,12 @@ +version: '3.7' + +services: + app: + image: nerdminer-dashboard-windows + build: . + restart: unless-stopped + ports: + - "3000:3000" + - "33333:33333/udp" + volumes: + - ./data:/app/data diff --git a/dashboard/docker-compose.yml b/dashboard/docker-compose.yml new file mode 100644 index 00000000..52e7ed77 --- /dev/null +++ b/dashboard/docker-compose.yml @@ -0,0 +1,10 @@ +version: '3.7' + +services: + app: + image: nerdminer-dashboard-linux + build: . + restart: unless-stopped + network_mode: "host" + volumes: + - ./data:/app/data diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json new file mode 100644 index 00000000..9c44229a --- /dev/null +++ b/dashboard/package-lock.json @@ -0,0 +1,1093 @@ +{ + "name": "nerdminer-dashboard", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "nerdminer-dashboard", + "version": "1.0.0", + "dependencies": { + "express": "^4.18.2", + "socket.io": "^4.7.2" + } + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "25.0.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz", + "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/engine.io": { + "version": "6.6.5", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.5.tgz", + "integrity": "sha512-2RZdgEbXmp5+dVbRm0P7HQUImZpICccJy7rN7Tv+SFa55pH+lxnuw6/K1ZxxBfHoYpSkHLAO92oa8O4SwFXA2A==", + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.18.3" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/socket.io": { + "version": "4.8.2", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.2.tgz", + "integrity": "sha512-wMAICvNHJNtnd3Jq97xROyRyFjMQ2G8QsVF6V+K6+6lztP3GaTcIaos+6E7+8jD/NoY++/vCvU9AI+bvRBNXVw==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "license": "MIT", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-adapter/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/socket.io-adapter/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/dashboard/package.json b/dashboard/package.json new file mode 100644 index 00000000..d7e56d66 --- /dev/null +++ b/dashboard/package.json @@ -0,0 +1,9 @@ +{ + "name": "nerdminer-dashboard", + "version": "1.0.0", + "main": "server.js", + "dependencies": { + "express": "^4.18.2", + "socket.io": "^4.7.2" + } +} \ No newline at end of file diff --git a/dashboard/public/btc-qr.png b/dashboard/public/btc-qr.png new file mode 100644 index 00000000..2c7cfe1a Binary files /dev/null and b/dashboard/public/btc-qr.png differ diff --git a/dashboard/public/config-modal.css b/dashboard/public/config-modal.css new file mode 100644 index 00000000..2b697681 --- /dev/null +++ b/dashboard/public/config-modal.css @@ -0,0 +1,144 @@ +/* Modal Overlay */ +.modal-overlay { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.7); + backdrop-filter: blur(5px); + z-index: 1000; + align-items: center; + justify-content: center; + opacity: 0; + transition: opacity 0.3s ease; +} + +.modal-overlay.active { + display: flex; + opacity: 1; +} + +/* Modal Content */ +.modal-content { + background: rgba(30, 41, 59, 0.95); + width: 100%; + max-width: 500px; + border-radius: 1rem; + border: 1px solid rgba(255, 255, 255, 0.1); + box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5); + transform: scale(0.95); + transition: transform 0.3s ease; + overflow: hidden; +} + +.modal-overlay.active .modal-content { + transform: scale(1); +} + +/* Modal Header */ +.modal-header { + padding: 1.5rem; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + display: flex; + justify-content: space-between; + align-items: center; +} + +.modal-title { + font-size: 1.25rem; + font-weight: 600; + color: #f8fafc; +} + +.modal-close { + background: none; + border: none; + color: #94a3b8; + cursor: pointer; + font-size: 1.5rem; + padding: 0.5rem; + transition: color 0.2s; +} + +.modal-close:hover { + color: #f8fafc; +} + +/* Modal Body */ +.modal-body { + padding: 1.5rem; +} + +.form-group { + margin-bottom: 1.25rem; +} + +.form-label { + display: block; + margin-bottom: 0.5rem; + color: #94a3b8; + font-size: 0.875rem; +} + +.form-input { + width: 100%; + padding: 0.75rem; + background: rgba(15, 23, 42, 0.6); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 0.5rem; + color: #f1f5f9; + font-family: inherit; + transition: all 0.2s; +} + +.form-input:focus { + outline: none; + border-color: #3b82f6; + background: rgba(15, 23, 42, 0.8); +} + +/* Modal Footer */ +.modal-footer { + padding: 1.5rem; + border-top: 1px solid rgba(255, 255, 255, 0.1); + display: flex; + justify-content: flex-end; + gap: 1rem; +} + +.btn { + padding: 0.75rem 1.5rem; + border-radius: 0.5rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; + border: none; +} + +.btn-secondary { + background: rgba(255, 255, 255, 0.1); + color: #f1f5f9; +} + +.btn-secondary:hover { + background: rgba(255, 255, 255, 0.2); +} + +.btn-primary { + background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%); + color: white; + box-shadow: 0 4px 6px -1px rgba(37, 99, 235, 0.2); +} + +.btn-primary:hover { + box-shadow: 0 10px 15px -3px rgba(37, 99, 235, 0.3); + transform: translateY(-1px); +} + +.btn:disabled { + opacity: 0.5; + cursor: not-allowed; + transform: none; +} diff --git a/dashboard/public/index.html b/dashboard/public/index.html new file mode 100644 index 00000000..c85f69ae --- /dev/null +++ b/dashboard/public/index.html @@ -0,0 +1,564 @@ + + + + + + + NerdMiner Dashboard + + + + + + + + + +
+
+

NerdMiner Fleet

+
0 KH/s
+
+ + + + + +
+
+
+
Total Hashrate (Last 24 Hours)
+
Fleet Performance
+
+
+
+
+ +
+
+
+ 24h Average
+
-
+
+
+
+ +
+ +
+ + +
+
+
+

⚡ Support This Project

+

Dust or tips BTC appreciated!

+
+
+
+
Bitcoin Address
+
+ bc1qjqhg5c2f6da8y4qr7upegwhkvl2376xzlpwf5p +
+ +
+
+ BTC Donation QR Code +
+
+
+
+
+ + + + + + + + + + + \ No newline at end of file diff --git a/dashboard/public/script.js b/dashboard/public/script.js new file mode 100644 index 00000000..76c648e9 --- /dev/null +++ b/dashboard/public/script.js @@ -0,0 +1,357 @@ +const socket = io(); +const grid = document.getElementById('miners-grid'); +const totalHashEl = document.getElementById('total-hashrate'); + +const miners = {}; + +function formatTime(seconds) { + if (seconds >= 86400) { + const d = Math.floor(seconds / 86400); + const h = Math.floor((seconds % 86400) / 3600); + const m = Math.floor((seconds % 3600) / 60); + return `${d}d ${h}h ${m}m`; + } + const h = Math.floor(seconds / 3600); + const m = Math.floor((seconds % 3600) / 60); + const s = Math.floor(seconds % 60); + return `${h}h ${m}m ${s}s`; +} + +function formatCount(val) { + if (val >= 1e12) return (val / 1e12).toFixed(2) + ' T'; + if (val >= 1e9) return (val / 1e9).toFixed(2) + ' B'; + if (val >= 1e6) return (val / 1e6).toFixed(2) + ' M'; + if (val >= 1e3) return (val / 1e3).toFixed(2) + ' k'; + return Math.floor(val).toLocaleString(); +} + +function createCard(key) { + const div = document.createElement('div'); + div.className = 'miner-card'; + div.id = `card-${key}`; + return div; +} + +function formatHashrate(strValue) { + const val = parseFloat(strValue); + if (isNaN(val)) return '0 H/s'; + + if (val >= 1e18) return `${(val / 1e18).toFixed(3)} ZH/s`; + if (val >= 1e15) return `${(val / 1e15).toFixed(2)} EH/s`; + if (val >= 1e12) return `${(val / 1e12).toFixed(2)} PH/s`; + if (val >= 1e9) return `${(val / 1e9).toFixed(2)} TH/s`; + if (val >= 1e6) return `${(val / 1e6).toFixed(2)} GH/s`; + if (val >= 1e3) return `${(val / 1e3).toFixed(2)} MH/s`; + if (val >= 1) return `${val.toFixed(2)} KH/s`; + return `${(val * 1000).toFixed(0)} H/s`; +} + +function updateCardHTML(miner) { + const tempClass = parseFloat(miner.temp) > 70 ? 'temp-high' : ''; + const displayName = (miner.miner && miner.miner !== 'Unknown') ? miner.miner : (miner.id || 'Unknown'); + const hashrateDisplay = formatHashrate(miner.hashrate); + + return ` +
+
+
${displayName}
+
${miner.ip}
+
+
+ +
+ Online +
+
+
+
+
Pool: ${miner.pool || 'Unknown'}
+ ${miner.address ? `
Addr: ...${miner.address.split('.')[0].slice(-8)}
` : ''} + ${(miner.miner && miner.miner !== 'Unknown') ? `
ID: ${miner.id}
` : ''} +
+
+
+ Hashrate + ${hashrateDisplay} +
+
+ Valid Shares + ${miner.valid} +
+
+ Templates + ${formatCount(parseFloat(miner.templates) || 0)} +
+
+ Best Diff + ${formatDifficulty(parseFloat(miner.bestDiff) || 0)} +
+
+ Temp + ${miner.temp}°C +
+
+ Uptime + ${formatTime(miner.uptime)} +
+
+ `; +} + +function render() { + let totalHash = 0; + + // Sort miners by IP or ID + const sortedKeys = Object.keys(miners).sort(); + + sortedKeys.forEach(key => { + const miner = miners[key]; + totalHash += parseFloat(miner.hashrate) || 0; + + let card = document.getElementById(`card-${key}`); + if (!card) { + card = createCard(key); + grid.appendChild(card); + } + card.innerHTML = updateCardHTML(miner); + }); + + // Remove old cards + const currentIds = new Set(sortedKeys.map(k => `card-${k}`)); + Array.from(grid.children).forEach(child => { + if (!currentIds.has(child.id)) { + child.remove(); + } + }); + + totalHashEl.innerHTML = `${formatHashrate(totalHash)} Total`; +} + +socket.on('init_miners', (data) => { + Object.assign(miners, data); + render(); +}); + +socket.on('miner_update', (data) => { + const key = data.id || data.ip; + miners[key] = data; + render(); +}); + +socket.on('miner_remove', (id) => { + delete miners[id]; + render(); +}); + +socket.on('bitcoin_stats', (data) => { + document.getElementById('bitcoin-card').style.display = 'block'; + + document.getElementById('btc-price').innerText = `$${data.price.toLocaleString()}`; + document.getElementById('btc-height').innerText = data.height.toLocaleString(); + + document.getElementById('btc-halving').innerText = `${data.halvingProgress}%`; + document.getElementById('btc-halving-bar').style.width = `${data.halvingProgress}%`; + + const diffVal = data.difficulty; // Now validated + let diffStr = diffVal.toLocaleString(); + if (diffVal > 1e12) diffStr = (diffVal / 1e12).toFixed(2) + ' T'; + document.getElementById('btc-diff').innerText = diffStr; + + // Use pre-fetched network hashrate (convert H/s to KH/s) + document.getElementById('btc-network-hash').innerHTML = formatHashrate(data.networkHashrate / 1000); + + if (data.fees) { + document.getElementById('btc-fees').innerHTML = ` + ${data.fees.fastestFee} / + ${data.fees.hourFee} + `; + } +}); + +// Chart.js Setup +const ctx = document.getElementById('hashrateChart').getContext('2d'); +const hashrateChart = new Chart(ctx, { + type: 'line', + data: { + labels: [], + datasets: [{ + label: 'Total Hashrate (KH/s)', + data: [], + borderColor: '#10b981', + backgroundColor: 'rgba(16, 185, 129, 0.1)', + borderWidth: 2, + fill: true, + tension: 0.4, + pointRadius: 0 + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { display: false } + }, + scales: { + x: { display: false }, + y: { + grid: { color: 'rgba(255, 255, 255, 0.05)' }, + ticks: { color: '#94a3b8' } + } + } + } +}); + +function updateGraph(history) { + const labels = history.map(p => new Date(p.timestamp).toLocaleTimeString()); + const data = history.map(p => p.hashrate); + + hashrateChart.data.labels = labels; + hashrateChart.data.datasets[0].data = data; + hashrateChart.update(); + + // Calculate Average + if (data.length > 0) { + const sum = data.reduce((a, b) => a + b, 0); + const avg = sum / data.length; + document.getElementById('hourly-avg').innerHTML = formatHashrate(avg); + } +} + +socket.on('init_history', (history) => { + updateGraph(history); +}); + +socket.on('history_update', (point) => { + // Add new point + hashrateChart.data.labels.push(new Date(point.timestamp).toLocaleTimeString()); + hashrateChart.data.datasets[0].data.push(point.hashrate); + + // Limit to 1440 points (24h) + if (hashrateChart.data.labels.length > 1440) { + hashrateChart.data.labels.shift(); + hashrateChart.data.datasets[0].data.shift(); + } + hashrateChart.update(); + + // Re-calculate Average + const data = hashrateChart.data.datasets[0].data; + const sum = data.reduce((a, b) => a + b, 0); + const avg = sum / data.length; + document.getElementById('hourly-avg').innerHTML = formatHashrate(avg); +}); + +function formatDifficulty(val) { + if (val >= 1e18) return (val / 1e18).toFixed(2) + ' E'; + if (val >= 1e15) return (val / 1e15).toFixed(2) + ' P'; + if (val >= 1e12) return (val / 1e12).toFixed(2) + ' T'; + if (val >= 1e9) return (val / 1e9).toFixed(2) + ' G'; + if (val >= 1e6) return (val / 1e6).toFixed(2) + ' M'; + if (val >= 1e3) return (val / 1e3).toFixed(2) + ' k'; + return val.toFixed(2); +} + +// Copy BTC address function +function copyBtcAddress() { + const addressEl = document.getElementById('btc-donation-address'); + const address = addressEl.innerText; + + navigator.clipboard.writeText(address).then(() => { + const btn = event.target.closest('.copy-btn'); + const originalText = btn.innerHTML; + btn.innerHTML = ' Copied!'; + btn.style.background = '#059669'; + + setTimeout(() => { + btn.innerHTML = originalText; + btn.style.background = ''; + }, 2000); + }).catch(err => { + console.error('Failed to copy:', err); + alert('Failed to copy address. Please copy manually.'); + }); +} + +// Configuration Modal Logic +const modal = document.getElementById('configModal'); + +async function openConfigModal(ip) { + document.getElementById('configIp').value = ip; + document.getElementById('configPool').value = 'Loading...'; + document.getElementById('configPort').value = ''; + document.getElementById('configAddr').value = ''; + document.getElementById('configPass').value = ''; + document.getElementById('configTz').value = ''; + + modal.classList.add('active'); + + // Find miner name + const minerKey = Object.keys(miners).find(key => miners[key].ip === ip); + const miner = miners[minerKey]; + const minerName = (miner && miner.miner && miner.miner !== 'Unknown') ? miner.miner : 'Miner'; + document.getElementById('configModalTitle').innerText = `${minerName} Configuration`; + + try { + const res = await fetch(`/miners/${ip}/config`); + if (!res.ok) throw new Error('Failed to fetch config'); + const data = await res.json(); + + document.getElementById('configPool').value = data.pool || ''; + document.getElementById('configPort').value = data.port || ''; + document.getElementById('configAddr').value = data.address || ''; + document.getElementById('configPass').value = data.password || ''; + document.getElementById('configTz').value = data.timezone || 0; + } catch (e) { + alert('Error loading configuration: ' + e.message); + closeConfigModal(); + } +} + +function closeConfigModal() { + modal.classList.remove('active'); +} + +async function saveConfig() { + const ip = document.getElementById('configIp').value; + const btn = document.querySelector('#configModal .btn-primary'); + const originalText = btn.innerText; + + const config = { + pool: document.getElementById('configPool').value, + port: parseInt(document.getElementById('configPort').value), + address: document.getElementById('configAddr').value, + password: document.getElementById('configPass').value, + timezone: parseInt(document.getElementById('configTz').value) + }; + + btn.disabled = true; + btn.innerText = 'Saving...'; + + try { + const res = await fetch(`/miners/${ip}/config`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(config) + }); + + const data = await res.json(); + if (!data.success) throw new Error(data.message || 'Save failed'); + + alert('Configuration saved! Miner is restarting...'); + closeConfigModal(); + } catch (e) { + alert('Error saving configuration: ' + e.message); + } finally { + btn.disabled = false; + btn.innerText = originalText; + } +} + +// Close modal when clicking outside +modal.addEventListener('click', (e) => { + if (e.target === modal) closeConfigModal(); +}); diff --git a/dashboard/server.js b/dashboard/server.js new file mode 100644 index 00000000..36c8c6a0 --- /dev/null +++ b/dashboard/server.js @@ -0,0 +1,199 @@ +const dgram = require('dgram'); +const express = require('express'); +const http = require('http'); +const { Server } = require("socket.io"); +const path = require('path'); + +const app = express(); +const server = http.createServer(app); +const io = new Server(server); +const udpSocket = dgram.createSocket('udp4'); + +const UDP_PORT = 33333; +const HTTP_PORT = 3000; + +app.use(express.static(path.join(__dirname, 'public'))); +app.use(express.json()); + +const miners = {}; + +// UDP Listener +udpSocket.on('error', (err) => { + console.log(`UDP error:\n${err.stack}`); + udpSocket.close(); +}); + +udpSocket.on('message', (msg, rinfo) => { + console.log(`UDP received: ${msg} from ${rinfo.address}:${rinfo.port}`); + try { + const data = JSON.parse(msg.toString()); + const id = data.id || rinfo.address; + const ip = data.ip || rinfo.address; // Prefer reported IP to bypass Docker NAT + + // Initialize or update miner + if (!miners[id]) { + miners[id] = { ...data, lastSeen: Date.now(), ip: ip }; + // New miner discovered: Fetch full config to get BTC address + fetchMinerConfig(ip, id); + } else { + // Preserve existing address if already fetched + const existingAddr = miners[id].address; + miners[id] = { ...data, lastSeen: Date.now(), ip: ip }; + if (existingAddr) miners[id].address = existingAddr; + else fetchMinerConfig(ip, id); // Try fetching again if we missed it + } + + io.emit('miner_update', miners[id]); + } catch (e) { + console.error('Invalid JSON from', rinfo.address, msg.toString()); + } +}); + +// Helper to fetch config and update miner state +async function fetchMinerConfig(ip, id) { + try { + // console.log(`Fetching config for ${id} (${ip})...`); + const response = await fetch(`http://${ip}/api/config`, { signal: AbortSignal.timeout(3000) }); + if (response.ok) { + const config = await response.json(); + if (miners[id]) { + miners[id].address = config.address; // Store address + io.emit('miner_update', miners[id]); // Push update + } + } + } catch (e) { + // console.error(`Failed to fetch config for ${ip}:`, e.message); + } +} + +udpSocket.bind(UDP_PORT, () => { + console.log(`UDP socket listening on port ${UDP_PORT}`); +}); + +// Clean up old miners +setInterval(() => { + const now = Date.now(); + for (const id in miners) { + if (now - miners[id].lastSeen > 30000) { // 30 seconds timeout + console.log(`Miner ${id} timed out`); + delete miners[id]; + io.emit('miner_remove', id); + } + } +}, 5000); + +// Bitcoin Stats +let bitcoinStats = {}; + +async function fetchBitcoinStats() { + try { + const [priceRes, heightRes, diffRes, feesRes, miningRes] = await Promise.all([ + fetch('https://mempool.space/api/v1/prices'), + fetch('https://mempool.space/api/blocks/tip/height'), + fetch('https://mempool.space/api/v1/difficulty-adjustment'), + fetch('https://mempool.space/api/v1/fees/recommended'), + fetch('https://mempool.space/api/v1/mining/hashrate/3d') + ]); + + const prices = await priceRes.json(); + const height = parseInt(await heightRes.text()); + const diffData = await diffRes.json(); + const fees = await feesRes.json(); + const miningData = await miningRes.json(); + + // Calculate Halving Progress + const blocksPerHalving = 210000; + const currentHalvingCycle = Math.floor(height / blocksPerHalving); + const nextHalvingBlock = (currentHalvingCycle + 1) * blocksPerHalving; + const blocksUntilHalving = nextHalvingBlock - height; + const halvingProgress = ((blocksPerHalving - blocksUntilHalving) / blocksPerHalving) * 100; + + bitcoinStats = { + price: prices.USD, + height: height, + difficulty: miningData.currentDifficulty, // Corrected source + networkHashrate: miningData.currentHashrate, // Direct from API + blocksUntilHalving: blocksUntilHalving, + halvingProgress: halvingProgress.toFixed(2), + fees: fees + }; + + io.emit('bitcoin_stats', bitcoinStats); + // console.log('Updated Bitcoin Stats:', bitcoinStats); + } catch (e) { + console.error('Error fetching Bitcoin stats:', e.message); + } +} + +// Fetch stats every 60 seconds +setInterval(fetchBitcoinStats, 60000); +// Miner Config Proxy +app.get('/miners/:ip/config', async (req, res) => { + try { + const { ip } = req.params; + console.log(`Proxying GET config to http://${ip}/api/config`); + const response = await fetch(`http://${ip}/api/config`, { signal: AbortSignal.timeout(5000) }); // 5s timeout + if (!response.ok) throw new Error(`Miner returned ${response.status}`); + const data = await response.json(); + res.json(data); + } catch (e) { + console.error(`Proxy Error (GET ${req.params.ip}):`, e.cause || e.message); + res.status(502).json({ error: e.message }); + } +}); + +app.post('/miners/:ip/config', async (req, res) => { + try { + const { ip } = req.params; + console.log(`Proxying POST config to http://${ip}/api/config`); + const response = await fetch(`http://${ip}/api/config`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(req.body), + signal: AbortSignal.timeout(5000) + }); + if (!response.ok) throw new Error(`Miner returned ${response.status}`); + const data = await response.json(); + res.json(data); + } catch (e) { + console.error(`Proxy Error (POST ${req.params.ip}):`, e.cause || e.message); + res.status(502).json({ error: e.message }); + } +}); + +fetchBitcoinStats(); // Initial fetch + +const hashrateHistory = []; +const MAX_HISTORY = 1440; // 24 hours (1440 minutes) + +// Update History every 60 seconds +setInterval(() => { + let totalHashrate = 0; + for (const id in miners) { + totalHashrate += parseFloat(miners[id].hashrate) || 0; + } + + const point = { + timestamp: Date.now(), + hashrate: totalHashrate + }; + + hashrateHistory.push(point); + if (hashrateHistory.length > MAX_HISTORY) { + hashrateHistory.shift(); + } + + io.emit('history_update', point); +}, 60000); + +io.on('connection', (socket) => { + socket.emit('init_miners', miners); + socket.emit('init_history', hashrateHistory); + if (bitcoinStats.price) { + socket.emit('bitcoin_stats', bitcoinStats); + } +}); + +server.listen(HTTP_PORT, () => { + console.log(`Dashboard running at http://localhost:${HTTP_PORT}`); +}); diff --git a/dashboard/test_udp.js b/dashboard/test_udp.js new file mode 100644 index 00000000..ca19fbef --- /dev/null +++ b/dashboard/test_udp.js @@ -0,0 +1,37 @@ +const dgram = require('dgram'); +const client = dgram.createSocket('udp4'); +client.bind(0, () => { + client.setBroadcast(true); +}); + +const message = JSON.stringify({ + id: "TEST_MINER_01", + miner: "Miner_Alpha", + pool: "pool.nerdminers.org:3333", + bestDiff: "42.00001", + hashrate: "123.45", + shares: 10, + valid: 9, + temp: "45.0", + uptime: 100 +}); + +console.log("Sending test packet..."); +client.send(message, 33333, '127.0.0.1', (err) => { + if (err) { + console.error(err); + client.close(); + } else { + console.log("Packet sent to 127.0.0.1:33333"); + } +}); + +client.send(message, 33333, '255.255.255.255', (err) => { + if (err) { + console.error(err); + client.close(); + } else { + console.log("Packet sent to 255.255.255.255:33333"); + client.close(); + } +}); diff --git a/dashboard/umbrel-app.yml b/dashboard/umbrel-app.yml new file mode 100644 index 00000000..0b1b454c --- /dev/null +++ b/dashboard/umbrel-app.yml @@ -0,0 +1,29 @@ +manifestVersion: 1 +id: nerdminer-dashboard +category: finance +name: NerdMiner Dashboard +version: "1.0.0" +tagline: Monitor your NerdMiner fleet in real-time +description: >- + A lightweight, real-time dashboard for your NerdMiners. + Features include: + - Auto-discovery of miners via UDP + - Real-time Hashrate & Temperature + - 24-Hour Historical Graph + - Bitcoin Network Stats (Price, Fees, Difficulty) +developer: NerdMiner +website: https://github.com/BitMaker-hub/NerdMiner_v2 +dependencies: [] +repo: https://github.com/BitMaker-hub/NerdMiner_v2 +support: https://github.com/BitMaker-hub/NerdMiner_v2/issues +port: 3000 +gallery: + - 1.jpg + - 2.jpg + - 3.jpg +path: "" +defaultPassword: "" +releaseNotes: >- + Initial release with UDP auto-discovery and Bitcoin stats. +submitter: NerdMiner +submission: https://github.com/BitMaker-hub/NerdMiner_v2 diff --git a/firmware/nerdminer--V1.8.3-1-g91930d3-dirty/ESP32-S2-mini-wemos_factory.bin b/firmware/nerdminer--V1.8.3-1-g91930d3-dirty/ESP32-S2-mini-wemos_factory.bin new file mode 100644 index 00000000..8885332b Binary files /dev/null and b/firmware/nerdminer--V1.8.3-1-g91930d3-dirty/ESP32-S2-mini-wemos_factory.bin differ diff --git a/firmware/nerdminer--V1.8.3-1-g91930d3-dirty/ESP32-S2-mini-wemos_firmware.bin b/firmware/nerdminer--V1.8.3-1-g91930d3-dirty/ESP32-S2-mini-wemos_firmware.bin new file mode 100644 index 00000000..b6372316 Binary files /dev/null and b/firmware/nerdminer--V1.8.3-1-g91930d3-dirty/ESP32-S2-mini-wemos_firmware.bin differ diff --git a/firmware/nerdminer--V1.8.3-1-g91930d3-dirty/ESP32-S3-mini-wemos_factory.bin b/firmware/nerdminer--V1.8.3-1-g91930d3-dirty/ESP32-S3-mini-wemos_factory.bin new file mode 100644 index 00000000..027cf99e Binary files /dev/null and b/firmware/nerdminer--V1.8.3-1-g91930d3-dirty/ESP32-S3-mini-wemos_factory.bin differ diff --git a/firmware/nerdminer--V1.8.3-1-g91930d3-dirty/ESP32-S3-mini-wemos_firmware.bin b/firmware/nerdminer--V1.8.3-1-g91930d3-dirty/ESP32-S3-mini-wemos_firmware.bin new file mode 100644 index 00000000..d9fb0971 Binary files /dev/null and b/firmware/nerdminer--V1.8.3-1-g91930d3-dirty/ESP32-S3-mini-wemos_firmware.bin differ diff --git a/firmware/nerdminer--V1.8.3-1-g91930d3-dirty/TTGO-T-Display_factory.bin b/firmware/nerdminer--V1.8.3-1-g91930d3-dirty/TTGO-T-Display_factory.bin new file mode 100644 index 00000000..4b6fbae6 Binary files /dev/null and b/firmware/nerdminer--V1.8.3-1-g91930d3-dirty/TTGO-T-Display_factory.bin differ diff --git a/firmware/nerdminer--V1.8.3-1-g91930d3-dirty/TTGO-T-Display_firmware.bin b/firmware/nerdminer--V1.8.3-1-g91930d3-dirty/TTGO-T-Display_firmware.bin new file mode 100644 index 00000000..fa32e5f8 Binary files /dev/null and b/firmware/nerdminer--V1.8.3-1-g91930d3-dirty/TTGO-T-Display_firmware.bin differ diff --git a/firmware/nerdminer--V1.8.3-1-g91930d3-dirty/m5stack-core2_factory.bin b/firmware/nerdminer--V1.8.3-1-g91930d3-dirty/m5stack-core2_factory.bin new file mode 100644 index 00000000..f7585f01 Binary files /dev/null and b/firmware/nerdminer--V1.8.3-1-g91930d3-dirty/m5stack-core2_factory.bin differ diff --git a/firmware/nerdminer--V1.8.3-1-g91930d3-dirty/m5stack-core2_firmware.bin b/firmware/nerdminer--V1.8.3-1-g91930d3-dirty/m5stack-core2_firmware.bin new file mode 100644 index 00000000..5f770360 Binary files /dev/null and b/firmware/nerdminer--V1.8.3-1-g91930d3-dirty/m5stack-core2_firmware.bin differ diff --git a/firmware/nerdminer--V1.8.3-11-g7fcb474-dirty/ESP32-S3-mini-wemos_factory.bin b/firmware/nerdminer--V1.8.3-11-g7fcb474-dirty/ESP32-S3-mini-wemos_factory.bin new file mode 100644 index 00000000..9e6dc713 Binary files /dev/null and b/firmware/nerdminer--V1.8.3-11-g7fcb474-dirty/ESP32-S3-mini-wemos_factory.bin differ diff --git a/firmware/nerdminer--V1.8.3-11-g7fcb474-dirty/ESP32-S3-mini-wemos_firmware.bin b/firmware/nerdminer--V1.8.3-11-g7fcb474-dirty/ESP32-S3-mini-wemos_firmware.bin new file mode 100644 index 00000000..53ac5e7f Binary files /dev/null and b/firmware/nerdminer--V1.8.3-11-g7fcb474-dirty/ESP32-S3-mini-wemos_firmware.bin differ diff --git a/firmware/nerdminer--V1.8.3-11-g7fcb474-dirty/TTGO-T-Display_factory.bin b/firmware/nerdminer--V1.8.3-11-g7fcb474-dirty/TTGO-T-Display_factory.bin new file mode 100644 index 00000000..55cf4b9c Binary files /dev/null and b/firmware/nerdminer--V1.8.3-11-g7fcb474-dirty/TTGO-T-Display_factory.bin differ diff --git a/firmware/nerdminer--V1.8.3-11-g7fcb474-dirty/TTGO-T-Display_firmware.bin b/firmware/nerdminer--V1.8.3-11-g7fcb474-dirty/TTGO-T-Display_firmware.bin new file mode 100644 index 00000000..eafdeaab Binary files /dev/null and b/firmware/nerdminer--V1.8.3-11-g7fcb474-dirty/TTGO-T-Display_firmware.bin differ diff --git a/firmware/nerdminer--V1.8.3-14-g9740c26-dirty/ESP32-S3-mini-wemos_factory.bin b/firmware/nerdminer--V1.8.3-14-g9740c26-dirty/ESP32-S3-mini-wemos_factory.bin new file mode 100644 index 00000000..935564c2 Binary files /dev/null and b/firmware/nerdminer--V1.8.3-14-g9740c26-dirty/ESP32-S3-mini-wemos_factory.bin differ diff --git a/firmware/nerdminer--V1.8.3-14-g9740c26-dirty/ESP32-S3-mini-wemos_firmware.bin b/firmware/nerdminer--V1.8.3-14-g9740c26-dirty/ESP32-S3-mini-wemos_firmware.bin new file mode 100644 index 00000000..d638ab3c Binary files /dev/null and b/firmware/nerdminer--V1.8.3-14-g9740c26-dirty/ESP32-S3-mini-wemos_firmware.bin differ diff --git a/firmware/nerdminer--V1.8.3-14-g9740c26-dirty/TTGO-T-Display_factory.bin b/firmware/nerdminer--V1.8.3-14-g9740c26-dirty/TTGO-T-Display_factory.bin new file mode 100644 index 00000000..76f22ced Binary files /dev/null and b/firmware/nerdminer--V1.8.3-14-g9740c26-dirty/TTGO-T-Display_factory.bin differ diff --git a/firmware/nerdminer--V1.8.3-14-g9740c26-dirty/TTGO-T-Display_firmware.bin b/firmware/nerdminer--V1.8.3-14-g9740c26-dirty/TTGO-T-Display_firmware.bin new file mode 100644 index 00000000..259113db Binary files /dev/null and b/firmware/nerdminer--V1.8.3-14-g9740c26-dirty/TTGO-T-Display_firmware.bin differ diff --git a/firmware/nerdminer--V1.8.3-14-g9740c26-dirty/m5stack-core2_factory.bin b/firmware/nerdminer--V1.8.3-14-g9740c26-dirty/m5stack-core2_factory.bin new file mode 100644 index 00000000..17d367dd Binary files /dev/null and b/firmware/nerdminer--V1.8.3-14-g9740c26-dirty/m5stack-core2_factory.bin differ diff --git a/firmware/nerdminer--V1.8.3-14-g9740c26-dirty/m5stack-core2_firmware.bin b/firmware/nerdminer--V1.8.3-14-g9740c26-dirty/m5stack-core2_firmware.bin new file mode 100644 index 00000000..37a3375f Binary files /dev/null and b/firmware/nerdminer--V1.8.3-14-g9740c26-dirty/m5stack-core2_firmware.bin differ diff --git a/firmware/nerdminer--V1.8.3-25-gb1839b2-dirty/m5stack-core2_factory.bin b/firmware/nerdminer--V1.8.3-25-gb1839b2-dirty/m5stack-core2_factory.bin new file mode 100644 index 00000000..3e774941 Binary files /dev/null and b/firmware/nerdminer--V1.8.3-25-gb1839b2-dirty/m5stack-core2_factory.bin differ diff --git a/firmware/nerdminer--V1.8.3-25-gb1839b2-dirty/m5stack-core2_firmware.bin b/firmware/nerdminer--V1.8.3-25-gb1839b2-dirty/m5stack-core2_firmware.bin new file mode 100644 index 00000000..236c1be8 Binary files /dev/null and b/firmware/nerdminer--V1.8.3-25-gb1839b2-dirty/m5stack-core2_firmware.bin differ diff --git a/firmware/nerdminer--V1.8.3-25-gb1839b2/m5stack-core2_factory.bin b/firmware/nerdminer--V1.8.3-25-gb1839b2/m5stack-core2_factory.bin new file mode 100644 index 00000000..c5191c01 Binary files /dev/null and b/firmware/nerdminer--V1.8.3-25-gb1839b2/m5stack-core2_factory.bin differ diff --git a/firmware/nerdminer--V1.8.3-25-gb1839b2/m5stack-core2_firmware.bin b/firmware/nerdminer--V1.8.3-25-gb1839b2/m5stack-core2_firmware.bin new file mode 100644 index 00000000..97a0e0f0 Binary files /dev/null and b/firmware/nerdminer--V1.8.3-25-gb1839b2/m5stack-core2_firmware.bin differ diff --git a/firmware/nerdminer--V1.8.3-dirty/ESP32-S3-mini-wemos_factory.bin b/firmware/nerdminer--V1.8.3-dirty/ESP32-S3-mini-wemos_factory.bin new file mode 100644 index 00000000..e69de29b diff --git a/firmware/nerdminer--V1.8.3-dirty/ESP32-S3-mini-wemos_firmware.bin b/firmware/nerdminer--V1.8.3-dirty/ESP32-S3-mini-wemos_firmware.bin new file mode 100644 index 00000000..37f21ff8 Binary files /dev/null and b/firmware/nerdminer--V1.8.3-dirty/ESP32-S3-mini-wemos_firmware.bin differ diff --git a/firmware/nerdminer--V1.8.3-dirty/TTGO-T-Display_factory.bin b/firmware/nerdminer--V1.8.3-dirty/TTGO-T-Display_factory.bin new file mode 100644 index 00000000..6d662f5f Binary files /dev/null and b/firmware/nerdminer--V1.8.3-dirty/TTGO-T-Display_factory.bin differ diff --git a/firmware/nerdminer--V1.8.3-dirty/TTGO-T-Display_firmware.bin b/firmware/nerdminer--V1.8.3-dirty/TTGO-T-Display_firmware.bin new file mode 100644 index 00000000..0f3acb11 Binary files /dev/null and b/firmware/nerdminer--V1.8.3-dirty/TTGO-T-Display_firmware.bin differ diff --git a/firmware/nerdminer--V1.8.3-dirty/han_m5stack_factory.bin b/firmware/nerdminer--V1.8.3-dirty/han_m5stack_factory.bin new file mode 100644 index 00000000..872b7ebc Binary files /dev/null and b/firmware/nerdminer--V1.8.3-dirty/han_m5stack_factory.bin differ diff --git a/firmware/nerdminer--V1.8.3-dirty/han_m5stack_firmware.bin b/firmware/nerdminer--V1.8.3-dirty/han_m5stack_firmware.bin new file mode 100644 index 00000000..551f5182 Binary files /dev/null and b/firmware/nerdminer--V1.8.3-dirty/han_m5stack_firmware.bin differ diff --git a/firmware/nerdminer--V1.8.3-dirty/m5stack-core2_factory.bin b/firmware/nerdminer--V1.8.3-dirty/m5stack-core2_factory.bin new file mode 100644 index 00000000..2c3fd1bc Binary files /dev/null and b/firmware/nerdminer--V1.8.3-dirty/m5stack-core2_factory.bin differ diff --git a/firmware/nerdminer--V1.8.3-dirty/m5stack-core2_firmware.bin b/firmware/nerdminer--V1.8.3-dirty/m5stack-core2_firmware.bin new file mode 100644 index 00000000..5d1977c0 Binary files /dev/null and b/firmware/nerdminer--V1.8.3-dirty/m5stack-core2_firmware.bin differ diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..0dcb73a7 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "NerdMiner_v2 - Copy", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/platformio.ini b/platformio.ini index 8f8b9f24..e48f701c 100644 --- a/platformio.ini +++ b/platformio.ini @@ -265,6 +265,50 @@ lib_ignore = TFT_eSPI HANSOLOminerv2 +;-------------------------------------------------------------------- + +[env:m5stack-core2] +platform = espressif32 +board = m5stack-core2 +framework = arduino +extra_scripts = + pre:auto_firmware_version.py + post:post_build_merge.py +monitor_filters = + esp32_exception_decoder + time + log2file + +build_flags = + -D HAN=1 + -D M5STACKCORE2_BOARD=1 + -O3 + -Ofast + -flto + -fdata-sections + -ffunction-sections + -fexceptions + -ffast-math + -fno-lto + -fno-exceptions + -Wno-strict-aliasing + +lib_deps = + https://github.com/tzapu/WiFiManager.git#v2.0.17 + mathertel/oneButton@^2.6.1 + arduino-libraries/NTPClient@^3.2.1 + m5stack/M5Core2 @ ^0.1.9 + bblanchon/ArduinoJson @ ^6.21.5 + +lib_ignore = + TFT_eSPI + HANSOLOminerv2 + +# 2 x 4.5MB app, 6.875MB SPIFFS +board_build.partitions = huge_app.csv + +monitor_speed = 115200 +upload_speed = 1500000 ;-------------------------------------------------------------------- diff --git a/src/NerdMinerV2.ino.cpp b/src/NerdMinerV2.ino.cpp index 548740c8..88464728 100644 --- a/src/NerdMinerV2.ino.cpp +++ b/src/NerdMinerV2.ino.cpp @@ -9,6 +9,7 @@ #include "mbedtls/md.h" #include "wManager.h" +#include "api.h" #include "mining.h" #include "monitor.h" #include "drivers/displays/display.h" @@ -105,7 +106,16 @@ void setup() #endif /******** INIT NERDMINER ************/ - Serial.println("NerdMiner v2 starting......"); + Serial.println("NerdMiner v2 UDP ENABLED......"); + + /******** CPU FREQUENCY OPTIMIZATION ************/ + uint32_t currentFreq = getCpuFrequencyMhz(); + Serial.printf("CPU Frequency: %d MHz\n", currentFreq); + if (currentFreq < 240) { + Serial.println("Boosting CPU to 240MHz for maximum hashrate..."); + setCpuFrequencyMhz(240); + Serial.printf("CPU Frequency after boost: %d MHz\n", getCpuFrequencyMhz()); + } /******** INIT DISPLAY ************/ initDisplay(); @@ -122,9 +132,13 @@ void setup() SDCrd.initSDcard(); #endif + /******** INIT WIFI ************/ /******** INIT WIFI ************/ init_WifiManager(); + /******** INIT API ************/ + setupAPI(); + /******** CREATE TASK TO PRINT SCREEN *****/ //tft.pushImage(0, 0, MinerWidth, MinerHeight, MinerScreen); // Higher prio monitor task @@ -214,6 +228,7 @@ void loop() { touchHandler.isTouched(); #endif wifiManagerProcess(); // avoid delays() in loop when non-blocking and other long running code + api_loop(); vTaskDelay(50 / portTICK_PERIOD_MS); } diff --git a/src/api.cpp b/src/api.cpp new file mode 100644 index 00000000..bc62dc61 --- /dev/null +++ b/src/api.cpp @@ -0,0 +1,88 @@ +#include "api.h" + +WebServer apiServer(80); +extern TSettings Settings; +extern nvMemory nvMem; + +void handleConfigGet() { + StaticJsonDocument<512> doc; + + doc["pool"] = Settings.PoolAddress; + doc["port"] = Settings.PoolPort; + doc["password"] = Settings.PoolPassword; + doc["address"] = Settings.BtcWallet; + doc["timezone"] = Settings.Timezone; + + String response; + serializeJson(doc, response); + apiServer.send(200, "application/json", response); +} + +void handleConfigPost() { + if (apiServer.hasArg("plain") == false) { + apiServer.send(400, "application/json", "{\"error\":\"Body not received\"}"); + return; + } + + String body = apiServer.arg("plain"); + DynamicJsonDocument doc(1024); + DeserializationError error = deserializeJson(doc, body); + + if (error) { + apiServer.send(400, "application/json", "{\"error\":\"Invalid JSON\"}"); + return; + } + + bool changed = false; + + if (doc.containsKey("pool")) { + Settings.PoolAddress = doc["pool"].as(); + changed = true; + } + + if (doc.containsKey("port")) { + Settings.PoolPort = doc["port"].as(); + changed = true; + } + + if (doc.containsKey("password")) { + String pass = doc["password"].as(); + strncpy(Settings.PoolPassword, pass.c_str(), sizeof(Settings.PoolPassword)); + changed = true; + } + + if (doc.containsKey("address")) { + String addr = doc["address"].as(); + strncpy(Settings.BtcWallet, addr.c_str(), sizeof(Settings.BtcWallet)); + changed = true; + } + + if (doc.containsKey("timezone")) { + Settings.Timezone = doc["timezone"].as(); + changed = true; + } + + if (changed) { + nvMem.saveConfig(&Settings); + apiServer.send(200, "application/json", "{\"success\":true, \"message\":\"Configuration saved. Restarting...\"}"); + delay(1000); + ESP.restart(); + } else { + apiServer.send(200, "application/json", "{\"success\":true, \"message\":\"No changes detected\"}"); + } +} + +void setupAPI() { + apiServer.on("/api/config", HTTP_GET, handleConfigGet); + apiServer.on("/api/config", HTTP_POST, handleConfigPost); + + // Enable CORS + apiServer.enableCORS(true); + + apiServer.begin(); + Serial.println("API Server started"); +} + +void api_loop() { + apiServer.handleClient(); +} diff --git a/src/api.h b/src/api.h new file mode 100644 index 00000000..3b3f9550 --- /dev/null +++ b/src/api.h @@ -0,0 +1,12 @@ +#ifndef API_H +#define API_H + +#include +#include +#include "wManager.h" +#include "drivers/storage/nvMemory.h" + +void setupAPI(); +void api_loop(); + +#endif // API_H diff --git a/src/drivers/devices/device.h b/src/drivers/devices/device.h index 49ec045f..d28e837f 100644 --- a/src/drivers/devices/device.h +++ b/src/drivers/devices/device.h @@ -45,6 +45,8 @@ #include "esp32S3MiniWeact.h" #elif defined(M5STACK_BOARD) #include "m5stack.h" +#elif defined(M5STACKCORE2_BOARD) +#include "m5stackCore2.h" #elif defined(WT32_BOARD) #include "wt32.h" #elif defined(NERDMINER_S3_GEEK) diff --git a/src/drivers/devices/m5stackCore2.h b/src/drivers/devices/m5stackCore2.h new file mode 100644 index 00000000..b844b603 --- /dev/null +++ b/src/drivers/devices/m5stackCore2.h @@ -0,0 +1,6 @@ +#ifndef _M5_STACKCORE2_H +#define _M5_STACKCORE2_H + +#define M5STACKCORE2_DISPLAY + +#endif \ No newline at end of file diff --git a/src/drivers/displays/display.cpp b/src/drivers/displays/display.cpp index 5c559b83..04b2fc7d 100644 --- a/src/drivers/displays/display.cpp +++ b/src/drivers/displays/display.cpp @@ -8,6 +8,10 @@ DisplayDriver *currentDisplayDriver = &noDisplayDriver; DisplayDriver *currentDisplayDriver = &m5stackDisplayDriver; #endif +#ifdef M5STACKCORE2_DISPLAY +DisplayDriver *currentDisplayDriver = &m5stackCore2DisplayDriver; +#endif + #ifdef WT32_DISPLAY DisplayDriver *currentDisplayDriver = &wt32DisplayDriver; #endif diff --git a/src/drivers/displays/displayDriver.h b/src/drivers/displays/displayDriver.h index 9594ea19..800a6fa8 100644 --- a/src/drivers/displays/displayDriver.h +++ b/src/drivers/displays/displayDriver.h @@ -29,6 +29,7 @@ typedef struct extern DisplayDriver *currentDisplayDriver; extern DisplayDriver m5stackDisplayDriver; +extern DisplayDriver m5stackCore2DisplayDriver; extern DisplayDriver wt32DisplayDriver; extern DisplayDriver noDisplayDriver; extern DisplayDriver ledDisplayDriver; diff --git a/src/drivers/displays/m5stackCore2DisplayDriver.cpp b/src/drivers/displays/m5stackCore2DisplayDriver.cpp new file mode 100644 index 00000000..24f71ce2 --- /dev/null +++ b/src/drivers/displays/m5stackCore2DisplayDriver.cpp @@ -0,0 +1,296 @@ +#include "displayDriver.h" + +#ifdef M5STACKCORE2_DISPLAY + +#include +#include +#include + +#include "Free_Fonts.h" +#include "monitor.h" +#include "drivers/storage/storage.h" +#include "wManager.h" + +extern monitor_data mMonitor; +extern TSettings Settings; + +#define WIDTH 320 +#define HEIGHT 240 + +void m5stackDisplay_Init(void) +{ + Serial.println("M5Stack Core2 display driver initialized"); + M5.begin(); // Init M5Stack Core2 + M5.Lcd.setRotation(1); // Landscape mode + M5.Lcd.fillScreen(BLACK); +} + +int screen_state = 1; +void m5stackDisplay_AlternateScreenState(void) +{ + screen_state == 1 ? M5.Lcd.sleep() : M5.Lcd.wakeup(); + screen_state ^= 1; +} + +int screen_rotation = 1; +void m5stackDisplay_AlternateRotation(void) +{ + screen_rotation = (screen_rotation == 1) ? 3 : 1; + M5.Lcd.setRotation(screen_rotation); +} + +void m5stackDisplay_MinerScreen(unsigned long mElapsed) +{ + mining_data data = getMiningData(mElapsed); + + Serial.printf(">>> Completed %s share(s), %s Khashes, avg. hashrate %s KH/s\n", + data.completedShares.c_str(), data.totalKHashes.c_str(), data.currentHashRate.c_str()); + + M5.Lcd.fillScreen(TFT_BLACK); + M5.Lcd.setTextColor(TFT_WHITE); + M5.Lcd.setTextDatum(TL_DATUM); + + // Header + M5.Lcd.setTextFont(4); + M5.Lcd.drawString("NerdMiner", 10, 5); + M5.Lcd.drawString(data.currentTime, 240, 5); + M5.Lcd.drawLine(0, 30, 320, 30, TFT_DARKGREY); + + // Main hashrate display + M5.Lcd.setTextFont(7); + M5.Lcd.setTextColor(TFT_YELLOW); + M5.Lcd.drawString(data.currentHashRate, 20, 45); + M5.Lcd.setTextFont(4); + M5.Lcd.drawString("KH/s", 200, 70); + + // Stats column + M5.Lcd.setTextFont(2); + M5.Lcd.setTextColor(TFT_CYAN); + M5.Lcd.drawString("Templates:", 20, 120); + M5.Lcd.setTextColor(TFT_WHITE); + M5.Lcd.drawString(data.templates, 130, 120); + + M5.Lcd.setTextColor(TFT_CYAN); + M5.Lcd.drawString("Best Diff:", 20, 140); + M5.Lcd.setTextColor(TFT_WHITE); + M5.Lcd.drawString(data.bestDiff, 130, 140); + + M5.Lcd.setTextColor(TFT_CYAN); + M5.Lcd.drawString("Shares:", 20, 160); + M5.Lcd.setTextColor(TFT_WHITE); + M5.Lcd.drawString(data.completedShares, 130, 160); + + M5.Lcd.setTextColor(TFT_CYAN); + M5.Lcd.drawString("Valids:", 20, 180); + M5.Lcd.setTextColor(TFT_GREEN); + M5.Lcd.drawString(data.valids, 130, 180); + + // Right column + M5.Lcd.setTextColor(TFT_CYAN); + M5.Lcd.drawString("Total:", 180, 120); + M5.Lcd.setTextColor(TFT_WHITE); + M5.Lcd.drawString(data.totalKHashes + " KH", 230, 120); + + M5.Lcd.setTextColor(TFT_CYAN); + M5.Lcd.drawString("Time:", 180, 140); + M5.Lcd.setTextColor(TFT_WHITE); + M5.Lcd.drawString(data.timeMining, 180, 160); + + // Footer + M5.Lcd.drawLine(0, 205, 320, 205, TFT_DARKGREY); + M5.Lcd.setTextFont(1); + M5.Lcd.setTextColor(TFT_GREENYELLOW); + M5.Lcd.drawString("Temp: " + data.temp + "C", 10, 215); +} + +void m5stackDisplay_ClockScreen(unsigned long mElapsed) +{ + clock_data data = getClockData(mElapsed); + + Serial.printf(">>> Completed %s share(s), %s Khashes, avg. hashrate %s KH/s\n", + data.completedShares.c_str(), data.totalKHashes.c_str(), data.currentHashRate.c_str()); + + M5.Lcd.fillScreen(TFT_BLACK); + M5.Lcd.setTextDatum(TC_DATUM); + + // Large time display + M5.Lcd.setTextFont(7); + M5.Lcd.setTextColor(TFT_CYAN); + M5.Lcd.drawString(data.currentTime, 160, 50); + + // Date + M5.Lcd.setTextFont(4); + M5.Lcd.setTextColor(TFT_WHITE); + M5.Lcd.drawString(data.currentDate, 160, 110); + + // BTC Price + M5.Lcd.setTextFont(4); + M5.Lcd.setTextColor(TFT_YELLOW); + M5.Lcd.drawString("BTC: " + data.btcPrice, 160, 145); + + // Block Height + M5.Lcd.setTextFont(2); + M5.Lcd.setTextColor(TFT_GREENYELLOW); + M5.Lcd.drawString("Block: " + data.blockHeight, 160, 180); + + // Hashrate + M5.Lcd.setTextFont(2); + M5.Lcd.setTextColor(TFT_ORANGE); + M5.Lcd.drawString(data.currentHashRate + " KH/s", 160, 200); + + // Footer info + M5.Lcd.setTextFont(1); + M5.Lcd.setTextColor(TFT_DARKGREY); + M5.Lcd.drawString("Shares: " + data.completedShares, 160, 220); +} + +void m5stackDisplay_GlobalHashScreen(unsigned long mElapsed) +{ + coin_data data = getCoinData(mElapsed); + + Serial.printf(">>> Completed %s share(s), %s Khashes, avg. hashrate %s KH/s\n", + data.completedShares.c_str(), data.totalKHashes.c_str(), data.currentHashRate.c_str()); + + M5.Lcd.fillScreen(TFT_BLACK); + M5.Lcd.setTextDatum(TL_DATUM); + + // Header + M5.Lcd.setTextFont(2); + M5.Lcd.setTextColor(TFT_WHITE); + M5.Lcd.drawString("NETWORK STATS", 10, 5); + M5.Lcd.setTextColor(TFT_YELLOW); + M5.Lcd.drawString("BTC: " + data.btcPrice, 200, 5); + M5.Lcd.setTextColor(TFT_CYAN); + M5.Lcd.drawString(data.currentTime, 10, 20); + M5.Lcd.drawLine(0, 35, 320, 35, TFT_DARKGREY); + + // Block Height - Large + M5.Lcd.setTextFont(7); + M5.Lcd.setTextColor(TFT_ORANGE); + M5.Lcd.setTextDatum(TC_DATUM); + M5.Lcd.drawString(data.blockHeight, 160, 45); + + M5.Lcd.setTextFont(2); + M5.Lcd.setTextColor(TFT_WHITE); + M5.Lcd.drawString("Block Height", 110, 95); + + // Network stats + M5.Lcd.setTextDatum(TL_DATUM); + M5.Lcd.setTextFont(2); + + M5.Lcd.setTextColor(TFT_CYAN); + M5.Lcd.drawString("Global Hash:", 10, 120); + M5.Lcd.setTextColor(TFT_WHITE); + M5.Lcd.drawString(data.globalHashRate + " EH/s", 120, 120); + + M5.Lcd.setTextColor(TFT_CYAN); + M5.Lcd.drawString("Difficulty:", 10, 140); + M5.Lcd.setTextColor(TFT_WHITE); + M5.Lcd.drawString(data.netwrokDifficulty, 120, 140); + + M5.Lcd.setTextColor(TFT_CYAN); + M5.Lcd.drawString("Fee (30min):", 10, 160); + M5.Lcd.setTextColor(TFT_WHITE); + M5.Lcd.drawString(data.halfHourFee, 120, 160); + + // Halving progress bar + M5.Lcd.drawLine(0, 185, 320, 185, TFT_DARKGREY); + M5.Lcd.setTextFont(1); + M5.Lcd.setTextColor(TFT_GREENYELLOW); + M5.Lcd.drawString("Next Halving Progress:", 10, 195); + + // Progress bar + int barWidth = 300; + int barHeight = 20; + int barX = 10; + int barY = 210; + + M5.Lcd.drawRect(barX, barY, barWidth, barHeight, TFT_WHITE); + int fillWidth = (barWidth - 4) * data.progressPercent / 100; + M5.Lcd.fillRect(barX + 2, barY + 2, fillWidth, barHeight - 4, TFT_ORANGE); + + // Progress text + M5.Lcd.setTextDatum(TC_DATUM); + M5.Lcd.setTextColor(TFT_BLACK); + M5.Lcd.drawString(data.remainingBlocks, 160, 213); +} + +void m5stackDisplay_LoadingScreen(void) +{ + Serial.println("M5Stack Core2 Initializing..."); + + M5.Lcd.fillScreen(BLACK); + M5.Lcd.setTextColor(TFT_CYAN); + M5.Lcd.setTextDatum(MC_DATUM); + M5.Lcd.setTextFont(4); + M5.Lcd.drawString("NerdMiner", 160, 80); + + M5.Lcd.setTextFont(2); + M5.Lcd.setTextColor(TFT_WHITE); + M5.Lcd.drawString("M5Stack Core2", 160, 120); + M5.Lcd.drawString("Initializing...", 160, 140); + + M5.Lcd.setTextFont(1); + M5.Lcd.setTextColor(TFT_DARKGREY); +} + +void m5stackDisplay_SetupScreen(void) +{ + Serial.println("M5Stack Core2 Setup Mode"); + + M5.Lcd.fillScreen(BLACK); + M5.Lcd.setTextColor(TFT_ORANGE); + M5.Lcd.setTextDatum(MC_DATUM); + M5.Lcd.setTextFont(4); + M5.Lcd.drawString("SETUP MODE", 160, 60); + + M5.Lcd.setTextFont(2); + M5.Lcd.setTextColor(TFT_WHITE); + M5.Lcd.drawString("Connect to WiFi:", 160, 100); + + M5.Lcd.setTextColor(TFT_CYAN); + M5.Lcd.drawString("HanSoloAP", 160, 120); + + M5.Lcd.setTextColor(TFT_WHITE); + M5.Lcd.drawString("Password:", 160, 145); + M5.Lcd.setTextColor(TFT_CYAN); + M5.Lcd.drawString("MineYourCoins", 160, 165); + + M5.Lcd.setTextFont(1); + M5.Lcd.setTextColor(TFT_GREENYELLOW); + M5.Lcd.drawString("Pool: " + String(Settings.PoolAddress), 160, 195); + M5.Lcd.drawString("IP: " + WiFi.localIP().toString(), 160, 210); +} + +void m5stackDisplay_DoLedStuff(unsigned long frame) +{ + // M5Stack Core2 doesn't have external LEDs like the original + // Could add touch feedback or vibration motor effects here if desired +} + +void m5stackDisplay_AnimateCurrentScreen(unsigned long frame) +{ + // Add any screen animations here if desired +} + +CyclicScreenFunction m5stackDisplayCyclicScreens[] = { + m5stackDisplay_MinerScreen, + m5stackDisplay_ClockScreen, + m5stackDisplay_GlobalHashScreen +}; + +DisplayDriver m5stackCore2DisplayDriver = { + m5stackDisplay_Init, + m5stackDisplay_AlternateScreenState, + m5stackDisplay_AlternateRotation, + m5stackDisplay_LoadingScreen, + m5stackDisplay_SetupScreen, + m5stackDisplayCyclicScreens, + m5stackDisplay_AnimateCurrentScreen, + m5stackDisplay_DoLedStuff, + SCREENS_ARRAY_SIZE(m5stackDisplayCyclicScreens), + 0, + WIDTH, + HEIGHT +}; +#endif \ No newline at end of file diff --git a/src/mining.cpp b/src/mining.cpp index 8fd73163..5f4f86aa 100644 --- a/src/mining.cpp +++ b/src/mining.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include @@ -653,7 +654,7 @@ void minerWorkerSw(void * task_id) #if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C3) -static inline void nerd_sha_ll_fill_text_block_sha256(const void *input_text, uint32_t nonce) +static inline IRAM_ATTR void nerd_sha_ll_fill_text_block_sha256(const void *input_text, uint32_t nonce) { uint32_t *data_words = (uint32_t *)input_text; uint32_t *reg_addr_buf = (uint32_t *)(SHA_TEXT_BASE); @@ -693,7 +694,7 @@ static inline void nerd_sha_ll_fill_text_block_sha256(const void *input_text, ui #endif } -static inline void nerd_sha_ll_fill_text_block_sha256_inter() +static inline IRAM_ATTR void nerd_sha_ll_fill_text_block_sha256_inter() { uint32_t *reg_addr_buf = (uint32_t *)(SHA_TEXT_BASE); @@ -718,7 +719,7 @@ static inline void nerd_sha_ll_fill_text_block_sha256_inter() REG_WRITE(®_addr_buf[15], 0x00010000); } -static inline void nerd_sha_ll_read_digest(void* ptr) +static inline IRAM_ATTR void nerd_sha_ll_read_digest(void* ptr) { DPORT_INTERRUPT_DISABLE(); ((uint32_t*)ptr)[0] = DPORT_SEQUENCE_REG_READ(SHA_H_BASE + 0 * 4); @@ -733,19 +734,16 @@ static inline void nerd_sha_ll_read_digest(void* ptr) } -static inline bool nerd_sha_ll_read_digest_if(void* ptr) +static inline IRAM_ATTR bool nerd_sha_ll_read_digest_if(void* ptr) { - DPORT_INTERRUPT_DISABLE(); - uint32_t last = DPORT_SEQUENCE_REG_READ(SHA_H_BASE + 7 * 4); - #if 1 - if ( (uint16_t)(last >> 16) != 0) - { - DPORT_INTERRUPT_RESTORE(); + // AGGRESSIVE: Read without barrier first for early rejection + uint32_t last_quick = REG_READ(SHA_H_BASE + 7 * 4); + if ((uint8_t)last_quick != 0 || (uint8_t)(last_quick >> 8) != 0) return false; - } - #endif - ((uint32_t*)ptr)[7] = last; + // Only use expensive barrier for full read + DPORT_INTERRUPT_DISABLE(); + ((uint32_t*)ptr)[7] = DPORT_SEQUENCE_REG_READ(SHA_H_BASE + 7 * 4); ((uint32_t*)ptr)[0] = DPORT_SEQUENCE_REG_READ(SHA_H_BASE + 0 * 4); ((uint32_t*)ptr)[1] = DPORT_SEQUENCE_REG_READ(SHA_H_BASE + 1 * 4); ((uint32_t*)ptr)[2] = DPORT_SEQUENCE_REG_READ(SHA_H_BASE + 2 * 4); @@ -757,7 +755,7 @@ static inline bool nerd_sha_ll_read_digest_if(void* ptr) return true; } -static inline void nerd_sha_ll_write_digest(void *digest_state) +static inline IRAM_ATTR void nerd_sha_ll_write_digest(void *digest_state) { uint32_t *digest_state_words = (uint32_t *)digest_state; uint32_t *reg_addr_buf = (uint32_t *)(SHA_H_BASE); @@ -772,7 +770,7 @@ static inline void nerd_sha_ll_write_digest(void *digest_state) REG_WRITE(®_addr_buf[7], digest_state_words[7]); } -static inline void nerd_sha_hal_wait_idle() +static inline IRAM_ATTR void nerd_sha_hal_wait_idle() { while (REG_READ(SHA_BUSY_REG)) {} @@ -835,50 +833,30 @@ void minerWorkerHw(void * task_id) uint32_t nend = job->nonce_start + job->nonce_count; for (uint32_t n = job->nonce_start; n < nend; ++n) { - //nerd_sha_hal_wait_idle(); nerd_sha_ll_write_digest(digest_mid); - //nerd_sha_hal_wait_idle(); nerd_sha_ll_fill_text_block_sha256(sha_buffer, n); - //sha_ll_continue_block(SHA2_256); REG_WRITE(SHA_CONTINUE_REG, 1); sha_ll_load(SHA2_256); nerd_sha_hal_wait_idle(); nerd_sha_ll_fill_text_block_sha256_inter(); - //sha_ll_start_block(SHA2_256); REG_WRITE(SHA_START_REG, 1); sha_ll_load(SHA2_256); nerd_sha_hal_wait_idle(); if (nerd_sha_ll_read_digest_if(hash)) { - //Serial.printf("Hw 16bit Share, nonce=0x%X\n", n); -#ifdef VALIDATION - //Validation - ((uint32_t*)(job->sha_buffer+64+12))[0] = n; - nerd_sha256d_baked(diget_mid, job->sha_buffer+64, bake, doubleHash); - for (int i = 0; i < 32; ++i) - { - if (hash[i] != doubleHash[i]) - { - Serial.println("***HW sha256 esp32s3 bug detected***"); - break; - } - } -#endif - //~5 per second double diff_hash = diff_from_target(hash); if (diff_hash > result->difficulty) { - if (isSha256Valid(hash)) - { - result->difficulty = diff_hash; - result->nonce = n; - memcpy(result->hash, hash, sizeof(hash)); - } + // AGGRESSIVE: Removed redundant isSha256Valid() check + result->difficulty = diff_hash; + result->nonce = n; + memcpy(result->hash, hash, sizeof(hash)); } } + // AGGRESSIVE: Check job validity every 4096 nonces instead of 256 if ( - (uint8_t)(n & 0xFF) == 0 && + (uint16_t)(n & 0xFFF) == 0 && s_working_current_job_id != job_in_work) { result->nonce_count = n-job->nonce_start+1; @@ -887,7 +865,7 @@ void minerWorkerHw(void * task_id) } esp_sha_release_hardware(); } else - vTaskDelay(2 / portTICK_PERIOD_MS); + vTaskDelay(1 / portTICK_PERIOD_MS); // AGGRESSIVE: Reduced from 2ms to 1ms wdt_counter++; if (wdt_counter >= 8) @@ -904,14 +882,14 @@ void minerWorkerHw(void * task_id) static inline bool nerd_sha_ll_read_digest_swap_if(void* ptr) { - DPORT_INTERRUPT_DISABLE(); - uint32_t fin = DPORT_SEQUENCE_REG_READ(SHA_TEXT_BASE + 7 * 4); - if ( (uint32_t)(fin & 0xFFFF) != 0) - { - DPORT_INTERRUPT_RESTORE(); + // AGGRESSIVE: Read without barrier first for early rejection + uint32_t fin_quick = DPORT_REG_READ(SHA_TEXT_BASE + 7 * 4); + if ((uint8_t)(fin_quick >> 24) != 0 || (uint8_t)(fin_quick >> 16) != 0) return false; - } - ((uint32_t*)ptr)[7] = __builtin_bswap32(fin); + + // Only use expensive barrier for full read + DPORT_INTERRUPT_DISABLE(); + ((uint32_t*)ptr)[7] = __builtin_bswap32(DPORT_SEQUENCE_REG_READ(SHA_TEXT_BASE + 7 * 4)); ((uint32_t*)ptr)[0] = __builtin_bswap32(DPORT_SEQUENCE_REG_READ(SHA_TEXT_BASE + 0 * 4)); ((uint32_t*)ptr)[1] = __builtin_bswap32(DPORT_SEQUENCE_REG_READ(SHA_TEXT_BASE + 1 * 4)); ((uint32_t*)ptr)[2] = __builtin_bswap32(DPORT_SEQUENCE_REG_READ(SHA_TEXT_BASE + 2 * 4)); @@ -943,7 +921,7 @@ static inline void nerd_sha_hal_wait_idle() {} } -static inline void nerd_sha_ll_fill_text_block_sha256(const void *input_text) +static inline IRAM_ATTR void nerd_sha_ll_fill_text_block_sha256(const void *input_text) { uint32_t *data_words = (uint32_t *)input_text; uint32_t *reg_addr_buf = (uint32_t *)(SHA_TEXT_BASE); @@ -966,7 +944,7 @@ static inline void nerd_sha_ll_fill_text_block_sha256(const void *input_text) reg_addr_buf[15] = data_words[15]; } -static inline void nerd_sha_ll_fill_text_block_sha256_upper(const void *input_text, uint32_t nonce) +static inline IRAM_ATTR void nerd_sha_ll_fill_text_block_sha256_upper(const void *input_text, uint32_t nonce) { uint32_t *data_words = (uint32_t *)input_text; uint32_t *reg_addr_buf = (uint32_t *)(SHA_TEXT_BASE); @@ -1004,7 +982,7 @@ static inline void nerd_sha_ll_fill_text_block_sha256_upper(const void *input_te #endif } -static inline void nerd_sha_ll_fill_text_block_sha256_double() +static inline IRAM_ATTR void nerd_sha_ll_fill_text_block_sha256_double() { uint32_t *reg_addr_buf = (uint32_t *)(SHA_TEXT_BASE); @@ -1097,16 +1075,15 @@ void minerWorkerHw(void * task_id) double diff_hash = diff_from_target(hash); if (diff_hash > result->difficulty) { - if (isSha256Valid(hash)) - { - result->difficulty = diff_hash; - result->nonce = job->nonce_start+n; - memcpy(result->hash, hash, sizeof(hash)); - } + // AGGRESSIVE: Removed redundant isSha256Valid() check + result->difficulty = diff_hash; + result->nonce = job->nonce_start+n; + memcpy(result->hash, hash, sizeof(hash)); } } + // AGGRESSIVE: Check job validity every 4096 nonces instead of 256 if ( - (uint8_t)(n & 0xFF) == 0 && + (uint16_t)(n & 0xFFF) == 0 && s_working_current_job_id != job_in_work) { result->nonce_count = n+1; @@ -1115,7 +1092,7 @@ void minerWorkerHw(void * task_id) } esp_sha_unlock_engine(SHA2_256); } else - vTaskDelay(2 / portTICK_PERIOD_MS); + vTaskDelay(1 / portTICK_PERIOD_MS); // AGGRESSIVE: Reduced from 2ms to 1ms esp_task_wdt_reset(); } @@ -1206,7 +1183,17 @@ void runMonitor(void *name) { Serial.println("[MONITOR] started"); + Serial.println("DEBUG: CUSTOM FIRMWARE ACTIVE - UDP ENABLED"); restoreStat(); + + WiFiUDP udp; + + String fullWallet = Settings.BtcWallet; + String minerName = "Unknown"; + int dotIndex = fullWallet.indexOf('.'); + if (dotIndex != -1) { + minerName = fullWallet.substring(dotIndex + 1); + } unsigned long mLastCheck = 0; @@ -1265,7 +1252,37 @@ void runMonitor(void *name) seconds_elapsed = 0; if(currentIntervalIndex < saveIntervalsSize - 1) currentIntervalIndex++; - } + + } + + // UDP Reporting + if (WiFi.status() == WL_CONNECTED) { + StaticJsonDocument<512> doc; + doc["id"] = WiFi.macAddress(); + doc["miner"] = minerName; + doc["pool"] = Settings.PoolAddress + ":" + String(Settings.PoolPort); + doc["bestDiff"] = String(best_diff, 5); + doc["hashrate"] = String((double)elapsedKHs * 1000.0 / (double)mElapsed, 2); + doc["shares"] = shares; + doc["templates"] = templates; + doc["valid"] = valids; + doc["temp"] = String(temperatureRead(), 1); + doc["uptime"] = upTime; + doc["ip"] = WiFi.localIP().toString(); // Required for Docker NAT traversal + + + String jsonString; + serializeJson(doc, jsonString); + + // Use subnet-specific broadcast address for better compatibility + udp.beginPacket(WiFi.broadcastIP(), 33333); + size_t bytesSent = udp.print(jsonString); + if (udp.endPacket()) { + Serial.printf("UDP Packet Sent: %s\n", jsonString.c_str()); + } else { + Serial.println("UDP Packet Send Failed"); + } + } } animateCurrentScreen(frame); doLedStuff(frame); diff --git a/src/monitor.cpp b/src/monitor.cpp index f9db1bb3..68e92f51 100644 --- a/src/monitor.cpp +++ b/src/monitor.cpp @@ -65,58 +65,76 @@ void updateGlobalData(void){ //Make first API call to get global hash and current difficulty HTTPClient http; http.setTimeout(10000); - try { - http.begin(getGlobalHash); + + if (!http.begin(getGlobalHash)) { + Serial.println("Global data HTTP begin failed"); + return; + } + int httpCode = http.GET(); if (httpCode == HTTP_CODE_OK) { String payload = http.getString(); StaticJsonDocument<1024> doc; - deserializeJson(doc, payload); - String temp = ""; - if (doc.containsKey("currentHashrate")) temp = String(doc["currentHashrate"].as()); - if(temp.length()>18 + 3) //Exahashes more than 18 digits + 3 digits decimals - gData.globalHash = temp.substring(0,temp.length()-18 - 3); - if (doc.containsKey("currentDifficulty")) temp = String(doc["currentDifficulty"].as()); - if(temp.length()>10 + 3){ //Terahash more than 10 digits + 3 digit decimals - temp = temp.substring(0,temp.length()-10 - 3); - gData.difficulty = temp.substring(0,temp.length()-2) + "." + temp.substring(temp.length()-2,temp.length()) + "T"; + DeserializationError error = deserializeJson(doc, payload); + + if (!error) { + String temp = ""; + if (doc.containsKey("currentHashrate")) temp = String(doc["currentHashrate"].as()); + if(temp.length()>18 + 3) //Exahashes more than 18 digits + 3 digits decimals + gData.globalHash = temp.substring(0,temp.length()-18 - 3); + if (doc.containsKey("currentDifficulty")) temp = String(doc["currentDifficulty"].as()); + if(temp.length()>10 + 3){ //Terahash more than 10 digits + 3 digit decimals + temp = temp.substring(0,temp.length()-10 - 3); + gData.difficulty = temp.substring(0,temp.length()-2) + "." + temp.substring(temp.length()-2,temp.length()) + "T"; + } + doc.clear(); + + mGlobalUpdate = millis(); + } else { + Serial.println("Global data JSON parse failed"); } - doc.clear(); - - mGlobalUpdate = millis(); + } else { + Serial.printf("Global data HTTP error: %d\n", httpCode); } http.end(); //Make third API call to get fees - http.begin(getFees); + if (!http.begin(getFees)) { + Serial.println("Fees HTTP begin failed"); + return; + } + httpCode = http.GET(); if (httpCode == HTTP_CODE_OK) { String payload = http.getString(); StaticJsonDocument<1024> doc; - deserializeJson(doc, payload); - String temp = ""; - if (doc.containsKey("halfHourFee")) gData.halfHourFee = doc["halfHourFee"].as(); + DeserializationError error = deserializeJson(doc, payload); + + if (!error) { + String temp = ""; + if (doc.containsKey("halfHourFee")) gData.halfHourFee = doc["halfHourFee"].as(); #ifdef SCREEN_FEES_ENABLE - if (doc.containsKey("fastestFee")) gData.fastestFee = doc["fastestFee"].as(); - if (doc.containsKey("hourFee")) gData.hourFee = doc["hourFee"].as(); - if (doc.containsKey("economyFee")) gData.economyFee = doc["economyFee"].as(); - if (doc.containsKey("minimumFee")) gData.minimumFee = doc["minimumFee"].as(); + if (doc.containsKey("fastestFee")) gData.fastestFee = doc["fastestFee"].as(); + if (doc.containsKey("hourFee")) gData.hourFee = doc["hourFee"].as(); + if (doc.containsKey("economyFee")) gData.economyFee = doc["economyFee"].as(); + if (doc.containsKey("minimumFee")) gData.minimumFee = doc["minimumFee"].as(); #endif - doc.clear(); + doc.clear(); - mGlobalUpdate = millis(); + mGlobalUpdate = millis(); + } else { + Serial.println("Fees JSON parse failed"); + } + } else { + Serial.printf("Fees HTTP error: %d\n", httpCode); } http.end(); - } catch(...) { - Serial.println("Global data HTTP error caught"); - http.end(); - } } } @@ -130,8 +148,12 @@ String getBlockHeight(void){ HTTPClient http; http.setTimeout(10000); - try { - http.begin(getHeightAPI); + + if (!http.begin(getHeightAPI)) { + Serial.println("Height HTTP begin failed"); + return current_block; + } + int httpCode = http.GET(); if (httpCode == HTTP_CODE_OK) { @@ -141,15 +163,14 @@ String getBlockHeight(void){ current_block = payload; mHeightUpdate = millis(); - } - http.end(); - } catch(...) { - Serial.println("Height HTTP error caught"); - http.end(); + } else { + Serial.printf("Height HTTP error: %d\n", httpCode); } + + http.end(); } - return current_block; + return current_block; } unsigned long mBTCUpdate = 0; @@ -166,37 +187,41 @@ String getBTCprice(void){ HTTPClient http; http.setTimeout(10000); - bool priceUpdated = false; - try { - http.begin(getBTCAPI); + if (!http.begin(getBTCAPI)) { + Serial.println("BTC price HTTP begin failed"); + static char price_buffer[16]; + snprintf(price_buffer, sizeof(price_buffer), "$%u", bitcoin_price); + return String(price_buffer); + } + int httpCode = http.GET(); if (httpCode == HTTP_CODE_OK) { String payload = http.getString(); StaticJsonDocument<1024> doc; - deserializeJson(doc, payload); + DeserializationError error = deserializeJson(doc, payload); - if (doc.containsKey("bitcoin") && doc["bitcoin"].containsKey("usd")) { - bitcoin_price = doc["bitcoin"]["usd"]; + if (!error) { + if (doc.containsKey("bitcoin") && doc["bitcoin"].containsKey("usd")) { + bitcoin_price = doc["bitcoin"]["usd"]; + } + doc.clear(); + mBTCUpdate = millis(); + } else { + Serial.println("BTC price JSON parse failed"); } - - doc.clear(); - - mBTCUpdate = millis(); + } else { + Serial.printf("BTC price HTTP error: %d\n", httpCode); } http.end(); - } catch(...) { - Serial.println("BTC price HTTP error caught"); - http.end(); - } } - static char price_buffer[16]; - snprintf(price_buffer, sizeof(price_buffer), "$%u", bitcoin_price); - return String(price_buffer); + static char price_buffer[16]; + snprintf(price_buffer, sizeof(price_buffer), "$%u", bitcoin_price); + return String(price_buffer); } unsigned long mTriggerUpdate = 0; @@ -443,73 +468,86 @@ pool_data getPoolData(void){ //Make first API call to get global hash and current difficulty HTTPClient http; http.setTimeout(10000); - try { - String btcWallet = Settings.BtcWallet; - // Serial.println(btcWallet); - if (btcWallet.indexOf(".")>0) btcWallet = btcWallet.substring(0,btcWallet.indexOf(".")); + + String btcWallet = Settings.BtcWallet; + // Serial.println(btcWallet); + if (btcWallet.indexOf(".")>0) btcWallet = btcWallet.substring(0,btcWallet.indexOf(".")); + #ifdef SCREEN_WORKERS_ENABLE - Serial.println("Pool API : " + poolAPIUrl+btcWallet); - http.begin(poolAPIUrl+btcWallet); + Serial.println("Pool API : " + poolAPIUrl+btcWallet); + if (!http.begin(poolAPIUrl+btcWallet)) { + Serial.println("####### Pool HTTP begin failed!"); + pData.bestDifficulty = "P"; + pData.workersHash = "Error"; + pData.workersCount = 0; + return pData; + } #else - http.begin(String(getPublicPool)+btcWallet); + if (!http.begin(String(getPublicPool)+btcWallet)) { + Serial.println("####### Pool HTTP begin failed!"); + pData.bestDifficulty = "P"; + pData.workersHash = "Error"; + pData.workersCount = 0; + return pData; + } #endif - int httpCode = http.GET(); - if (httpCode == HTTP_CODE_OK) { - String payload = http.getString(); - // Serial.println(payload); - StaticJsonDocument<300> filter; - filter["bestDifficulty"] = true; - filter["workersCount"] = true; - filter["workers"][0]["sessionId"] = true; - filter["workers"][0]["hashRate"] = true; - StaticJsonDocument<2048> doc; - deserializeJson(doc, payload, DeserializationOption::Filter(filter)); - //Serial.println(serializeJsonPretty(doc, Serial)); - if (doc.containsKey("workersCount")) pData.workersCount = doc["workersCount"].as(); - const JsonArray& workers = doc["workers"].as(); - float totalhashs = 0; - for (const JsonObject& worker : workers) { - totalhashs += worker["hashRate"].as(); - /* Serial.print(worker["sessionId"].as()+": "); - Serial.print(" - "+worker["hashRate"].as()+": "); - Serial.println(totalhashs); */ - } - char totalhashs_s[16] = {0}; - suffix_string(totalhashs, totalhashs_s, 16, 0); - pData.workersHash = String(totalhashs_s); - - double temp; - if (doc.containsKey("bestDifficulty")) { - temp = doc["bestDifficulty"].as(); - char best_diff_string[16] = {0}; - suffix_string(temp, best_diff_string, 16, 0); - pData.bestDifficulty = String(best_diff_string); - } - doc.clear(); - mPoolUpdate = millis(); - Serial.println("\n####### Pool Data OK!"); - } else { - Serial.println("\n####### Pool Data HTTP Error!"); - /* Serial.println(httpCode); - String payload = http.getString(); - Serial.println(payload); */ - // mPoolUpdate = millis(); - pData.bestDifficulty = "P"; - pData.workersHash = "E"; - pData.workersCount = 0; - http.end(); - return pData; - } - http.end(); - } catch(...) { - Serial.println("####### Pool Error!"); - // mPoolUpdate = millis(); - pData.bestDifficulty = "P"; - pData.workersHash = "Error"; - pData.workersCount = 0; - http.end(); - return pData; - } + + int httpCode = http.GET(); + + if (httpCode == HTTP_CODE_OK) { + String payload = http.getString(); + // Serial.println(payload); + StaticJsonDocument<300> filter; + filter["bestDifficulty"] = true; + filter["workersCount"] = true; + filter["workers"][0]["sessionId"] = true; + filter["workers"][0]["hashRate"] = true; + StaticJsonDocument<2048> doc; + DeserializationError error = deserializeJson(doc, payload, DeserializationOption::Filter(filter)); + + if (!error) { + //Serial.println(serializeJsonPretty(doc, Serial)); + if (doc.containsKey("workersCount")) pData.workersCount = doc["workersCount"].as(); + const JsonArray& workers = doc["workers"].as(); + float totalhashs = 0; + for (const JsonObject& worker : workers) { + totalhashs += worker["hashRate"].as(); + /* Serial.print(worker["sessionId"].as()+": "); + Serial.print(" - "+worker["hashRate"].as()+": "); + Serial.println(totalhashs); */ + } + char totalhashs_s[16] = {0}; + suffix_string(totalhashs, totalhashs_s, 16, 0); + pData.workersHash = String(totalhashs_s); + + double temp; + if (doc.containsKey("bestDifficulty")) { + temp = doc["bestDifficulty"].as(); + char best_diff_string[16] = {0}; + suffix_string(temp, best_diff_string, 16, 0); + pData.bestDifficulty = String(best_diff_string); + } + doc.clear(); + mPoolUpdate = millis(); + Serial.println("\n####### Pool Data OK!"); + } else { + Serial.println("####### Pool JSON parse failed!"); + pData.bestDifficulty = "P"; + pData.workersHash = "Error"; + pData.workersCount = 0; + } + } else { + Serial.println("\n####### Pool Data HTTP Error!"); + Serial.printf("HTTP Code: %d\n", httpCode); + /* String payload = http.getString(); + Serial.println(payload); */ + pData.bestDifficulty = "P"; + pData.workersHash = "E"; + pData.workersCount = 0; + } + + http.end(); } + return pData; -} +} \ No newline at end of file