├── .env ├── .gitignore ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── screenshot-desktop-full.png ├── screenshot-desktop.png ├── screenshot-mobile-full.png ├── screenshot-mobile.png ├── screenshot-tablet-full.png ├── screenshot-tablet.png ├── src ├── App.css ├── App.js ├── App.test.js ├── components │ └── card.js ├── index.css ├── index.js ├── logo.svg ├── reportWebVitals.js └── setupTests.js └── tmo-live-graph-logo.png /.env: -------------------------------------------------------------------------------- 1 | # This file can be used to avoid runtime configuration 2 | # Uncomment settings as desired 3 | # 4 | # Gateway Model: { 'NOK5G21' | 'ARCKVD21' } 5 | # - Silver Cylindrical Nokia Gateway is 'NOK5G21' 6 | # - Black Rectangular Arcadyan Gateway is 'ARCKVD21' 7 | # REACT_APP_GATEWAY_MODEL= 8 | # Gateway Username 9 | REACT_APP_USER=admin 10 | # Gateway Password 11 | # REACT_APP_PASSWORD= -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # production 41 | /build 42 | 43 | # Dependency directories 44 | node_modules/ 45 | jspm_packages/ 46 | 47 | # TypeScript v1 declaration files 48 | typings/ 49 | 50 | # TypeScript cache 51 | *.tsbuildinfo 52 | 53 | # Optional npm cache directory 54 | .npm 55 | 56 | # Optional eslint cache 57 | .eslintcache 58 | 59 | # Microbundle cache 60 | .rpt2_cache/ 61 | .rts2_cache_cjs/ 62 | .rts2_cache_es/ 63 | .rts2_cache_umd/ 64 | 65 | # Optional REPL history 66 | .node_repl_history 67 | 68 | # Output of 'npm pack' 69 | *.tgz 70 | 71 | # Yarn Integrity file 72 | .yarn-integrity 73 | 74 | # dotenv environment variables file 75 | .env*.local 76 | 77 | # parcel-bundler cache (https://parceljs.org/) 78 | .cache 79 | 80 | # Next.js build output 81 | .next 82 | 83 | # Nuxt.js build / generate output 84 | .nuxt 85 | dist 86 | 87 | # Gatsby files 88 | .cache/ 89 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 90 | # https://nextjs.org/blog/next-9-1#public-directory-support 91 | # public 92 | 93 | # vuepress build output 94 | .vuepress/dist 95 | 96 | # Serverless directories 97 | .serverless/ 98 | 99 | # FuseBox cache 100 | .fusebox/ 101 | 102 | # DynamoDB Local files 103 | .dynamodb/ 104 | 105 | # TernJS port file 106 | .tern-port 107 | 108 | # dependencies 109 | /.pnp 110 | .pnp.js 111 | 112 | # misc 113 | .DS_Store -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 highvolt-dev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | tmo-live-graph logo 3 |

4 | 5 | # tmo-live-graph 6 | 7 | A simpe react app that plots a live view of the T-Mobile Home Internet Nokia 5G Gateway signal stats, helpful for optimizing signal. 8 | 9 | This project should be considered to be in a pre-release state. 10 | 11 | desktop screenshot of tmo-live-graph 12 | 13 | 14 | ## Getting Started 15 | 16 | Ensure that you have nodejs installed on your machine, then run: 17 | 18 | ``` 19 | npm install 20 | ``` 21 | 22 | In order to properly fetch API responses from the Nokia Gateway in-browser, they must be proxied to work around CORS restrictions. 23 | 24 | This is handled automatically when running the project in development mode using webpack-dev-server with the following command: 25 | 26 | ``` 27 | npm start 28 | ``` 29 | 30 | This will start the project at http://localhost:3000/ 31 | 32 | This project has not been prepared to handle proxying in a production-ready release mode. 33 | 34 | ### Configuration 35 | 36 | You can avoid specifying your gateway model and admin username/password by making use of an *environment file*. 37 | 38 | The default environment configuration is specified in the `.env` file. 39 | 40 | It is suggested that you make a copy as `.env.local`. 41 | 42 | Inside `.env.local` you can specify your desired configuration, e.g.: 43 | 44 | ``` 45 | REACT_APP_GATEWAY_MODEL=NOK5G21 46 | REACT_APP_USER=admin 47 | REACT_APP_PASSWORD=yourpassword 48 | ``` 49 | 50 | Note that the file has further instructions (helpful if you have the Arcadyan gateway). 51 | 52 | If necessary, you may need to enclose your value in single (`'`) or double quotes (`"`). 53 | 54 | When your environment file is fully configured, the application will not prompt you for your gateway model (Nokia vs Arcadyan) and will automatically attempt to log you into your gateway with the provided username and password, enabling advanced functionality that requires authentication against your administrative credentials. 55 | 56 | 57 | ## Cell Info 58 | 59 | This section requires authentication with admin username and password - login form appears at top of app. 60 | 61 | - Connection Type 62 | - Operator (PLMN/MCC-MNC) 63 | - eNB ID (Cell Site) 64 | - Cell ID 65 | - Link to cellmapper.net for given operator. Zoomed to geographic center of US. To respect cellmapper.net, you must manually copy and paste the enNB ID to find your cell site. 66 | 67 | ## Summarized Statistics 68 | 69 | ### 4G LTE 70 | - Connected band 71 | - Carrier Aggregation (Download, Upload, Bands) 72 | - Current/Best RSRP 73 | - Current/Best SNR 74 | - Current/Best RSRQ 75 | 76 | ### 5G NR 77 | - Connected band 78 | - Current/Best RSRP 79 | - Current/Best SNR 80 | - Current/Best RSRQ 81 | 82 | ## Visualized Statistics 83 | 84 | ### 4G LTE 85 | - RSRP value with Min/Max reference lines 86 | - SNR value with Min/Max reference lines 87 | - RSRQ value with Min/Max reference lines 88 | 89 | ### 5G NR 90 | - RSRP value with Min/Max reference lines 91 | - SNR value with Min/Max reference lines 92 | - RSRQ value with Min/Max reference lines 93 | 94 | ## Screenshots 95 | 96 | ### Desktop 97 | desktop screenshot of tmo-live-graph 98 | 99 | ### Tablet 100 | tablet screenshot of tmo-live-graph 101 | 102 | ### Mobile 103 | mobile screenshot of tmo-live-graph 104 | 105 | ## Available Scripts 106 | 107 | In the project directory, you can run: 108 | 109 | ### `npm start` 110 | 111 | Runs the app in the development mode.\ 112 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 113 | 114 | The page will reload if you make edits.\ 115 | You will also see any lint errors in the console. 116 | 117 | ### `npm test` 118 | 119 | Launches the test runner in the interactive watch mode.\ 120 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 121 | 122 | ### `npm run build` 123 | 124 | Builds the app for production to the `build` folder.\ 125 | It correctly bundles React in production mode and optimizes the build for the best performance. 126 | 127 | The build is minified and the filenames include the hashes.\ 128 | Your app is ready to be deployed! 129 | 130 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 131 | 132 | ### `npm run eject` 133 | 134 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 135 | 136 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 137 | 138 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 139 | 140 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 141 | 142 | ## Learn More 143 | 144 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 145 | 146 | To learn React, check out the [React documentation](https://reactjs.org/). 147 | 148 | ### Code Splitting 149 | 150 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) 151 | 152 | ### Analyzing the Bundle Size 153 | 154 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) 155 | 156 | ### Making a Progressive Web App 157 | 158 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) 159 | 160 | ### Advanced Configuration 161 | 162 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) 163 | 164 | ### Deployment 165 | 166 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) 167 | 168 | ### `npm run build` fails to minify 169 | 170 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) 171 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tmo-live-graph", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.14.1", 7 | "@testing-library/react": "^11.2.7", 8 | "@testing-library/user-event": "^12.8.3", 9 | "react": "^17.0.2", 10 | "react-dom": "^17.0.2", 11 | "react-scripts": "4.0.3", 12 | "recharts": "^2.1.4", 13 | "web-vitals": "^1.1.2" 14 | }, 15 | "scripts": { 16 | "start": "react-scripts start", 17 | "build": "react-scripts build", 18 | "test": "react-scripts test", 19 | "eject": "react-scripts eject" 20 | }, 21 | "eslintConfig": { 22 | "extends": [ 23 | "react-app", 24 | "react-app/jest" 25 | ] 26 | }, 27 | "browserslist": { 28 | "production": [ 29 | ">0.2%", 30 | "not dead", 31 | "not op_mini all" 32 | ], 33 | "development": [ 34 | "last 1 chrome version", 35 | "last 1 firefox version", 36 | "last 1 safari version" 37 | ] 38 | }, 39 | "proxy": "http://192.168.12.1" 40 | } 41 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/highvolt-dev/tmo-live-graph/8caa356aec99b1965cc8331d7c117c0f571ac4b6/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | Tmo Live Graph 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/highvolt-dev/tmo-live-graph/8caa356aec99b1965cc8331d7c117c0f571ac4b6/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/highvolt-dev/tmo-live-graph/8caa356aec99b1965cc8331d7c117c0f571ac4b6/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /screenshot-desktop-full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/highvolt-dev/tmo-live-graph/8caa356aec99b1965cc8331d7c117c0f571ac4b6/screenshot-desktop-full.png -------------------------------------------------------------------------------- /screenshot-desktop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/highvolt-dev/tmo-live-graph/8caa356aec99b1965cc8331d7c117c0f571ac4b6/screenshot-desktop.png -------------------------------------------------------------------------------- /screenshot-mobile-full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/highvolt-dev/tmo-live-graph/8caa356aec99b1965cc8331d7c117c0f571ac4b6/screenshot-mobile-full.png -------------------------------------------------------------------------------- /screenshot-mobile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/highvolt-dev/tmo-live-graph/8caa356aec99b1965cc8331d7c117c0f571ac4b6/screenshot-mobile.png -------------------------------------------------------------------------------- /screenshot-tablet-full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/highvolt-dev/tmo-live-graph/8caa356aec99b1965cc8331d7c117c0f571ac4b6/screenshot-tablet-full.png -------------------------------------------------------------------------------- /screenshot-tablet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/highvolt-dev/tmo-live-graph/8caa356aec99b1965cc8331d7c117c0f571ac4b6/screenshot-tablet.png -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | min-height: 100vh; 4 | background-color: #282c34; 5 | display: flex; 6 | flex-direction: column; 7 | justify-content: center; 8 | } 9 | 10 | .App-header { 11 | display: flex; 12 | flex-direction: column; 13 | align-items: center; 14 | justify-content: center; 15 | font-size: calc(10px + 2vmin); 16 | color: white; 17 | margin-bottom: 1rem; 18 | } 19 | .App-header h1 { 20 | margin: 0 0 0.5rem 0; 21 | } 22 | @media( min-width: 768px ) { 23 | .App-header h1 { 24 | margin: 0 0 1rem 0; 25 | } 26 | } 27 | @media( min-width: 1024px ) { 28 | .App-header h1 { 29 | margin: 0 0 2rem 0; 30 | } 31 | } 32 | .App-header a.login { 33 | text-decoration: underline; 34 | cursor: pointer; 35 | margin-bottom: 1rem; 36 | } 37 | .App-header .login-state { 38 | margin-bottom: 1rem; 39 | } 40 | .App-header label { 41 | font-size: 75%; 42 | } 43 | .App-header a.login:hover { 44 | text-decoration: none; 45 | } 46 | .App-header h2 { 47 | padding-bottom: 0.5rem; 48 | border-bottom: 1px solid #ccc; 49 | margin-top: 0; 50 | } 51 | .App-header h3 { 52 | margin-bottom: 0; 53 | } 54 | .App-header form { 55 | margin-bottom: 1rem; 56 | } 57 | .App-header label, .App-header input { 58 | margin-right: 1ch; 59 | } 60 | .App-header .ext-link { 61 | color: #fff; 62 | margin-bottom: 1rem; 63 | } 64 | .App-header .summary { 65 | font-size: 80%; 66 | display: flex; 67 | } 68 | @media( min-width: 768px ) { 69 | .App-header .summary { 70 | font-size: 60%; 71 | } 72 | } 73 | @media( min-width: 1200px ) { 74 | .App-header .summary { 75 | font-size: 50%; 76 | } 77 | } 78 | .App-header .lte, 79 | .App-header .nr { 80 | background-color: rgba(255,255,255,0.1); 81 | padding: 1rem; 82 | } 83 | .App-header .lte { 84 | margin-left: 2rem; 85 | margin-right: 1rem; 86 | } 87 | .App-header .lte h2 { 88 | color: #a65ef3; 89 | } 90 | 91 | .App-header .nr { 92 | margin-left: 1rem; 93 | margin-right: 2em; 94 | } 95 | .App-header .nr h2 { 96 | color: #6668eb; 97 | } 98 | dl { 99 | display: flex; 100 | flex-wrap: wrap; 101 | text-align: right; 102 | margin-top: 0.25rem; 103 | } 104 | dl dt { 105 | flex-basis: 50%; 106 | width: 40%; 107 | font-weight: bold; 108 | } 109 | dl dd { 110 | width: auto; 111 | flex-shrink: 1; 112 | } 113 | .unit { 114 | display: none; 115 | color: #aaa; 116 | font-size: 80%; 117 | font-style: italic; 118 | } 119 | @media (min-width: 768px) { 120 | .unit { 121 | display: inline; 122 | } 123 | } 124 | /* LTE SNR MIN */ 125 | #composed-chart .recharts-reference-line:nth-child(10) text { 126 | transform: translate(-50px, -10px); 127 | } 128 | /* LTE SNR MAX */ 129 | #composed-chart .recharts-reference-line:nth-child(11) text { 130 | transform: translate(-50px, 10px); 131 | } 132 | /* LTE RSRP MIN */ 133 | #composed-chart .recharts-reference-line:nth-child(12) text { 134 | transform: translate(-100px, -10px); 135 | } 136 | /* LTE RSRP MAX */ 137 | #composed-chart .recharts-reference-line:nth-child(13) text { 138 | transform: translate(-100px, 10px); 139 | } 140 | /* LTE RSRQ MIN */ 141 | #rsrq-chart .recharts-reference-line:nth-child(7) text { 142 | transform: translate(-100px, -10px); 143 | } 144 | /* LTE RSRQ MAX */ 145 | #rsrq-chart .recharts-reference-line:nth-child(8) text { 146 | transform: translate(-100px, 10px); 147 | } 148 | /* NR SNR MIN */ 149 | #composed-chart .recharts-reference-line:nth-child(14) text { 150 | transform: translate(50px, -10px); 151 | } 152 | /* NR SNR MAX */ 153 | #composed-chart .recharts-reference-line:nth-child(15) text { 154 | transform: translate(50px, 10px); 155 | } 156 | /* NR RSRP MIN */ 157 | #composed-chart .recharts-reference-line:nth-child(16) text { 158 | transform: translate(100px, -10px); 159 | } 160 | /* NR RSRP MAX */ 161 | #composed-chart .recharts-reference-line:nth-child(17) text { 162 | transform: translate(100px, 10px); 163 | } 164 | /* NR RSRQ MIN */ 165 | #rsrq-chart .recharts-reference-line:nth-child(9) text { 166 | transform: translate(100px, -10px); 167 | } 168 | /* NR RSRQ MAX */ 169 | #rsrq-chart .recharts-reference-line:nth-child(10) text { 170 | transform: translate(100px, 10px); 171 | } 172 | 173 | @media( min-width: 768px) { 174 | /* LTE SNR MIN */ 175 | #composed-chart .recharts-reference-line:nth-child(10) text { 176 | transform: translate(-100px, -10px); 177 | } 178 | /* LTE SNR MAX */ 179 | #composed-chart .recharts-reference-line:nth-child(11) text { 180 | transform: translate(-100px, 10px); 181 | } 182 | /* LTE RSRP MIN */ 183 | #composed-chart .recharts-reference-line:nth-child(12) text { 184 | transform: translate(-200px, -10px); 185 | } 186 | /* LTE RSRP MAX */ 187 | #composed-chart .recharts-reference-line:nth-child(13) text { 188 | transform: translate(-200px, 10px); 189 | } 190 | /* LTE RSRQ MIN */ 191 | #rsrq-chart .recharts-reference-line:nth-child(7) text { 192 | transform: translate(-200px, -10px); 193 | } 194 | /* LTE RSRQ MAX */ 195 | #rsrq-chart .recharts-reference-line:nth-child(8) text { 196 | transform: translate(-200px, 10px); 197 | } 198 | /* NR SNR MIN */ 199 | #composed-chart .recharts-reference-line:nth-child(14) text { 200 | transform: translate(100px, -10px); 201 | } 202 | /* NR SNR MAX */ 203 | #composed-chart .recharts-reference-line:nth-child(15) text { 204 | transform: translate(100px, 10px); 205 | } 206 | /* NR RSRP MIN */ 207 | #composed-chart .recharts-reference-line:nth-child(16) text { 208 | transform: translate(200px, -10px); 209 | } 210 | /* NR RSRP MAX */ 211 | #composed-chart .recharts-reference-line:nth-child(17) text { 212 | transform: translate(200px, 10px); 213 | } 214 | /* NR RSRQ MIN */ 215 | #rsrq-chart .recharts-reference-line:nth-child(9) text { 216 | transform: translate(200px, -10px); 217 | } 218 | /* NR RSRQ MAX */ 219 | #rsrq-chart .recharts-reference-line:nth-child(10) text { 220 | transform: translate(200px, 10px); 221 | } 222 | } 223 | .App-body { 224 | background-color: #282c34; 225 | display: flex; 226 | flex-direction: column; 227 | align-items: center; 228 | justify-content: center; 229 | color: white; 230 | } 231 | .App-body .yAxis text { 232 | fill: white; 233 | } 234 | .App-body .recharts-tooltip-wrapper { 235 | color: #282c34; 236 | } 237 | .App-body .recharts-reference-line text { 238 | fill: white; 239 | } -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from 'react'; 2 | import { Area, ComposedChart, Line, CartesianGrid, ReferenceLine, XAxis, YAxis, Tooltip } from 'recharts'; 3 | import Card from './components/card'; 4 | import './App.css'; 5 | 6 | // https://stackoverflow.com/a/62798382 7 | function useInterval(callback, delay) { 8 | const savedCallback = useRef(); 9 | 10 | // Remember the latest callback. 11 | useEffect(() => { 12 | savedCallback.current = callback; 13 | }, [callback]); 14 | 15 | // Set up the interval. 16 | useEffect(() => { 17 | function tick() { 18 | savedCallback.current(); 19 | } 20 | if (delay !== null) { 21 | let id = setInterval(tick, delay); 22 | return () => clearInterval(id); 23 | } 24 | }, [delay]); 25 | } 26 | 27 | function App() { 28 | const [model, setModel] = useState(process.env.REACT_APP_GATEWAY_MODEL || ''); 29 | const [data, setData] = useState([]); 30 | const [login, setLogin] = useState({username: process.env.REACT_APP_USER || 'admin', password: process.env.REACT_APP_PASSWORD || '', error: ''}); 31 | const [loggedIn, setLoggedIn] = useState(false); 32 | const [cellData, setCellData] = useState({}); 33 | // Automatically login if saved in env 34 | useEffect(() => { 35 | if ( login.username && login.password) { 36 | doLogin(); 37 | } 38 | }); 39 | 40 | const renderComposedChart = ( 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | plot.lte.SNRCurrent).reduce((max, val) => val > max ? val : max, -5) : ''} label="Max LTE SNR" stroke="red" strokeDasharray="3 3" isFront /> 56 | plot.lte.SNRCurrent).reduce((min, val) => val < min ? val : min, 40) : ''} label="Min LTE SNR" stroke="red" strokeDasharray="3 3" isFront /> 57 | plot.lte.RSRPCurrent).reduce((max, val) => val > max ? val : max, -140) : ''} label="Max LTE RSRP" stroke="crimson" strokeDasharray="3 3" isFront /> 58 | plot.lte.RSRPCurrent).reduce((min, val) => val < min ? val : min, -44) : ''} label="Min LTE RSRP" stroke="crimson" strokeDasharray="3 3" isFront /> 59 | plot.nr.SNRCurrent).reduce((max, val) => val > max ? val : max, -5) : ''} label="Max NR SNR" stroke="blue" strokeDasharray="3 3" isFront /> 60 | plot.nr.SNRCurrent).reduce((min, val) => val < min ? val : min, 40) : ''} label="Min NR SNR" stroke="blue" strokeDasharray="3 3" isFront /> 61 | plot.nr.RSRPCurrent).reduce((max, val) => val > max ? val : max, -140) : ''} label="Max NR RSRP" stroke="aqua" strokeDasharray="3 3" isFront /> 62 | plot.nr.RSRPCurrent).reduce((min, val) => val < min ? val : min, -44) : ''} label="Min NR RSRP" stroke="aqua" strokeDasharray="3 3" isFront /> 63 | 64 | 65 | ); 66 | 67 | const renderRSRQChart = ( 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | plot.lte.RSRQCurrent).reduce((max, val) => val > max ? val : max, -19.5) : ''} label="Max LTE RSRQ" stroke="red" strokeDasharray="3 3" isFront /> 78 | plot.lte.RSRQCurrent).reduce((min, val) => val < min ? val : min, -3) : ''} label="Min LTE RSRQ" stroke="red" strokeDasharray="3 3" isFront /> 79 | plot.nr.RSRQCurrent).reduce((max, val) => val > max ? val : max, -19.5) : ''} label="Max NR RSRQ" stroke="blue" strokeDasharray="3 3" isFront /> 80 | plot.nr.RSRQCurrent).reduce((min, val) => val < min ? val : min, -3) : ''} label="Min NR RSRQ" stroke="blue" strokeDasharray="3 3" isFront /> 81 | 82 | 83 | ); 84 | 85 | useInterval(async () => { 86 | if (!model) return; 87 | if (model === 'ARCKVD21') { 88 | const res = await fetch('/TMI/v1/gateway?get=all', { 89 | headers: { 90 | 'Accept': 'application/json' 91 | } 92 | }); 93 | const json = await res.json(); 94 | const date = new Date(); 95 | const primary = {...json.signal['4g']}; 96 | const secondary = {...json.signal['5g']}; 97 | 98 | const lte = { 99 | "PhysicalCellID": primary.cid, 100 | "RSSICurrent": primary.rssi, 101 | "SNRCurrent": primary.sinr, 102 | "RSRPCurrent": primary.rsrp, 103 | "RSRPStrengthIndexCurrent": primary.bars, 104 | "RSRQCurrent": primary.rsrq, 105 | "DownlinkEarfcn": null, // Only available in authenticated telemetry endpoint 106 | "SignalStrengthLevel":0, 107 | "Band": primary.bands.length ? primary.bands.map(b => b.toUpperCase()).join(", ") : null 108 | }; 109 | 110 | const nr = { 111 | "PhysicalCellID": secondary.cid, 112 | "SNRCurrent": secondary.sinr, 113 | "RSRPCurrent": secondary.rsrp, 114 | "RSRPStrengthIndexCurrent": secondary.bars, 115 | "RSRQCurrent": secondary.rsrq, 116 | "Downlink_NR_ARFCN": null, // Only available in authenticated telemetry endpoint 117 | "SignalStrengthLevel":0, 118 | "Band": secondary?.bands?.length ? secondary.bands.join(", ") : null 119 | }; 120 | 121 | if (primary['RSRPStrengthIndexCurrent'] === 0) { 122 | primary['SNRCurrent'] = null; 123 | primary['RSRPCurrent'] = null; 124 | primary['RSRQCurrent'] = null; 125 | } 126 | if (secondary['RSRPStrengthIndexCurrent'] === 0) { 127 | secondary['SNRCurrent'] = null; 128 | secondary['RSRPCurrent'] = null; 129 | secondary['RSRQCurrent'] = null; 130 | } 131 | setData(data => [...data.slice(-24), {date, time: `${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}` , lte, nr, ca: null }]); 132 | } else { 133 | const res = await fetch('/fastmile_radio_status_web_app.cgi', { 134 | headers: { 135 | 'Accept': 'application/json' 136 | } 137 | }); 138 | const json = await res.json(); 139 | let ca = json['cell_CA_stats_cfg'][0]; 140 | // Older firmware 141 | if ('ca' in ca) { 142 | ca = ca.ca; 143 | const numericKeys = Object.keys(ca).filter(key => ca.hasOwnProperty(key) && !isNaN(key)); 144 | ca.carriers = { 145 | unspecified: numericKeys.map(key => ca[key]) 146 | }; 147 | ca.download = ca.X_ALU_COM_DLCarrierAggregationNumberOfEntries; 148 | ca.upload = ca.X_ALU_COM_ULCarrierAggregationNumberOfEntries; 149 | for (const key of numericKeys) { 150 | delete ca[key]; 151 | } 152 | delete ca.X_ALU_COM_DLCarrierAggregationNumberOfEntries; 153 | delete ca.X_ALU_COM_ULCarrierAggregationNumberOfEntries; 154 | } else { 155 | ca.carriers = {}; 156 | ca.download = ca.X_ALU_COM_DLCarrierAggregationNumberOfEntries; 157 | ca.upload = ca.X_ALU_COM_ULCarrierAggregationNumberOfEntries; 158 | for (const group of ['ca4GDL', 'ca4GUL']) { 159 | const numericKeys = Object.keys(ca[group]).filter(key => ca[group].hasOwnProperty(key) && !isNaN(key)); 160 | ca.carriers[group === 'ca4GDL' ? 'download' : 'upload'] = numericKeys.map(key => ca[group][key]); 161 | } 162 | delete ca.X_ALU_COM_DLCarrierAggregationNumberOfEntries; 163 | delete ca.X_ALU_COM_ULCarrierAggregationNumberOfEntries; 164 | delete ca.ca4GDL; 165 | delete ca.ca4GUL; 166 | } 167 | 168 | const date = new Date(); 169 | const primary = {...json['cell_LTE_stats_cfg'][0]['stat']}; 170 | const secondary = {...json['cell_5G_stats_cfg'][0]['stat']}; 171 | if (primary['RSRPStrengthIndexCurrent'] === 0) { 172 | primary['SNRCurrent'] = null; 173 | primary['RSRPCurrent'] = null; 174 | primary['RSRQCurrent'] = null; 175 | } 176 | if (secondary['RSRPStrengthIndexCurrent'] === 0) { 177 | secondary['SNRCurrent'] = null; 178 | secondary['RSRPCurrent'] = null; 179 | secondary['RSRQCurrent'] = null; 180 | } 181 | setData(data => [...data.slice(-24), {date, time: `${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}` , lte: primary, nr: secondary, ca }]); 182 | } 183 | }, 2000); 184 | 185 | const doLogin = async (e=undefined) => { 186 | if (!model) return; 187 | if (loggedIn) return; 188 | 189 | if (e) e.target.disabled = true; 190 | 191 | if (model === 'ARCKVD21') { 192 | const res = await fetch('/TMI/v1/auth/login', { 193 | method: 'POST', 194 | headers: { 195 | 'Content-Type': 'application/json' 196 | }, 197 | body: JSON.stringify({ 198 | username: login.username, 199 | password: login.password 200 | }) 201 | }); 202 | const json = await res.json(); 203 | if (!json.hasOwnProperty('auth')) { 204 | setField('error', 'Problem logging in'); 205 | } else { 206 | setField('error', ''); 207 | setLoggedIn(true); 208 | getCellInfoArcadyan(json.auth.token); 209 | } 210 | } else { 211 | const body = new URLSearchParams({ 212 | name: login.username, 213 | pswd: login.password 214 | }).toString(); 215 | 216 | const res = await fetch('/login_app.cgi', { 217 | method: 'POST', 218 | headers: { 219 | 'Accept': 'application/json', 220 | 'Content-Type': 'application/x-www-form-urlencoded' 221 | }, 222 | body 223 | }); 224 | const json = await res.json(); 225 | if (json.result !== 0) { 226 | setField('error', 'Problem logging in'); 227 | } else { 228 | setField('error', ''); 229 | setLoggedIn(true); 230 | getCellInfoNokia(); 231 | } 232 | } 233 | if (e) e.target.disabled = false; 234 | }; 235 | 236 | const getCellInfoNokia = async () => { 237 | const res = await fetch('/cell_status_app.cgi', { 238 | headers: { 239 | 'Accept': 'application/json' 240 | } 241 | }); 242 | const json = await res.json(); 243 | 244 | const data = { 245 | AccessTechnology: json.cell_stat_generic[0].CurrentAccessTechnology, 246 | eNBID: json.cell_stat_lte[0].eNBID, 247 | CellId: json.cell_stat_lte[0].Cellid, 248 | MCC: json.cell_stat_lte[0].MCC, 249 | MNC: json.cell_stat_lte[0].MNC 250 | } 251 | 252 | setCellData({...data, plmn: `${json.cell_stat_lte[0].MCC}-${json.cell_stat_lte[0].MNC}`}); 253 | }; 254 | 255 | const getCellInfoArcadyan = async (token) => { 256 | const res = await fetch('/TMI/v1/network/telemetry?get=all', { 257 | headers: { 258 | 'Authorization': 'Bearer ' + token 259 | } 260 | }); 261 | const json = await res.json(); 262 | 263 | const data = { 264 | eNBID: Math.floor(parseInt(json.cell['4g'].ecgi.substring(6)) / 256), 265 | CellId: json.cell['4g'].sector.cid, 266 | MCC: json.cell['4g'].mcc, 267 | MNC: json.cell['4g'].mnc 268 | }; 269 | 270 | setCellData({...data, plmn: `${json.cell['4g'].mcc}-${json.cell['4g'].mnc}`, gps: json.cell.gps}); 271 | }; 272 | 273 | const setField = (prop, value) => { setLogin({...login, [prop]: value}) }; 274 | 275 | const plmn = { 276 | '310-260': 'T-Mobile USA', 277 | '311-490': 'Sprint', 278 | '312-250': 'Sprint Keep Site' 279 | } 280 | 281 | return ( 282 |
283 |
284 |

Tmo Live Graph

285 |
286 |

Model Selection

287 | {model ? 288 | <>Selected: {model} 289 | : 290 | 295 | } 296 |
297 | {model ? <> 298 | {loggedIn ? Logged In : <> 299 |
300 | 301 | {setField('username', e.target.value)}} /> 302 | 303 | 304 | {setField('password', e.target.value)}} /> 305 | 306 | {login.error ?

{login.error}

: ''} 307 |
308 | } 309 | {'plmn' in cellData ? 310 | <> 311 |
312 |
Connection Type
313 | {cellData.AccessTechnology ? 314 |
{cellData.AccessTechnology}
315 | :
Connection Information
} 316 |
Operator (PLMN/MCC-MNC)
317 |
{cellData.plmn} ({cellData.plmn in plmn ? plmn[cellData.plmn] : 'Unknown'})
318 |
eNB ID (Cell Site)
319 |
{cellData.eNBID}
320 |
Cell ID (Cell Site)
321 |
{cellData.CellId}
322 |
323 | Cellmapper link 324 |

In left menu, expand "Tower Search" and copy and paste the eNB ID for your cell site ({cellData.eNBID})

325 | 326 | : ''} 327 |
328 | plot.lte.RSRPCurrent).filter(val => val !== null).reduce((best, val) => val > best ? val : best, -140) : null} 335 | SNRCurrent={data.length ? data.slice(-1)[0].lte.SNRCurrent : null} 336 | SNRBest={data.length ? data.map(plot => plot.lte.SNRCurrent).filter(val => val !== null).reduce((best, val) => val > best ? val : best, -19.5) : null} 337 | CA={data.length ? data.slice(-1)[0].ca /* "ca":{ "X_ALU_COM_DLCarrierAggregationNumberOfEntries":1, "X_ALU_COM_ULCarrierAggregationNumberOfEntries":0 ,"1":{"PhysicalCellID":49, "ScellBand":"B2", "ScellChannel":675 }} }]} */ : null } 338 | /> 339 | plot.nr.RSRPCurrent).filter(val => val !== null).reduce((best, val) => val > best ? val : best, -140) : null} 346 | SNRCurrent={data.length ? data.slice(-1)[0].nr.SNRCurrent : null} 347 | SNRBest={data.length ? data.map(plot => plot.nr.SNRCurrent).filter(val => val !== null).reduce((best, val) => val > best ? val : best, -19.5) : null} 348 | /> 349 |
350 | : ''} 351 |
352 | { model ? <> 353 |
354 | {renderComposedChart} 355 |
356 |
357 |
358 | plot.lte.RSRQCurrent).filter(val => val !== null).reduce((best, val) => val > best ? val : best, -19.5) : null} 364 | /> 365 | plot.nr.RSRQCurrent).filter(val => val !== null).reduce((best, val) => val > best ? val : best, -19.5) : null} 371 | /> 372 |
373 |
374 |
375 | {renderRSRQChart} 376 |
377 | : '' } 378 |
379 | ); 380 | } 381 | 382 | export default App; 383 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /src/components/card.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function Card(props) { 4 | 5 | return ( 6 |
7 |

{props.title}

8 | {props.main ? ( 9 | <> 10 |

Band{props?.band?.indexOf(",") >= 0 ? 's' : ''} {props.band ? props.band : 'N/A'}

11 | {props.signal === 'lte' && props.CA ? ( 12 | <> 13 |

Carrier Aggregation

14 |
15 |
Download
16 |
{props.CA.download ? `+${props.CA.download}` : 'None'}
17 | {props.CA.carriers.hasOwnProperty('download') ? <> 18 | {props.CA.carriers.download.map((carrier, i) => <> 19 |
DL CA Add({i + 1})
20 |
Band {carrier['ScellBand']}
21 | )} 22 | : ''} 23 |
Upload
24 |
{props.CA.upload ? `+${props.CA.upload}` : 'None'}
25 | {props.CA.carriers.hasOwnProperty('upload') ? <> 26 | {props.CA.carriers.upload.map((carrier, i) => <> 27 |
UL CA Add({i + 1})
28 |
Band {carrier['ScellBand']}
29 | )} 30 | : ''} 31 | {props.CA.carriers.hasOwnProperty('unspecified') ? <> 32 | {props.CA.carriers.unspecified.map((carrier, i) => <> 33 |
CA Add({i + 1})
34 |
Band {carrier['ScellBand']}
35 | )} 36 | : ''} 37 |
38 | 39 | ) : props.signal === 'lte' ? 'NO CA' : ''} 40 |

RSRP

41 |
42 |
Current:
43 |
44 | {props.RSRPCurrent ? ( 45 | <> 46 | {props.RSRPCurrent} dBm 47 | 48 | ) : 'N/A'} 49 |
50 |
Best:
51 |
52 | {props.RSRPBest ? ( 53 | <> 54 | {props.RSRPBest} dBm 55 | 56 | ) : 'N/A'} 57 |
58 |
59 |

SNR

60 |
61 |
Current:
62 |
63 | {props.SNRCurrent ? ( 64 | <> 65 | {props.SNRCurrent} dB 66 | 67 | ) : 'N/A'} 68 |
69 |
Best:
70 |
71 | {props.SNRBest ? ( 72 | <> 73 | {props.SNRBest} dB 74 | 75 | ) : 'N/A'} 76 |
77 |
78 | 79 | ) : ( 80 | <> 81 |

RSRQ

82 |
83 |
Current:
84 |
85 | {props.RSRQCurrent ? ( 86 | <> 87 | {props.RSRQCurrent} dB 88 | 89 | ) : 'N/A'} 90 |
91 |
Best:
92 |
93 | {props.RSRQBest ? ( 94 | <> 95 | {props.RSRQBest} dB 96 | 97 | ) : 'N/A'} 98 |
99 |
100 | 101 | )} 102 |
103 | ); 104 | } 105 | 106 | export default Card; 107 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root') 12 | ); 13 | 14 | // If you want to start measuring performance in your app, pass a function 15 | // to log results (for example: reportWebVitals(console.log)) 16 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 17 | reportWebVitals(); 18 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /tmo-live-graph-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/highvolt-dev/tmo-live-graph/8caa356aec99b1965cc8331d7c117c0f571ac4b6/tmo-live-graph-logo.png --------------------------------------------------------------------------------