andrewfraley / arris_cable_modem_stats Goto Github PK
View Code? Open in Web Editor NEWRetrieves stats from Arris cable modems and sends to InfluxDB
Retrieves stats from Arris cable modems and sends to InfluxDB
Hi Mate,
Been racking my brain trying to get the python script to pull data from another location on the modem without any luck. I actually have zero programming experience and basically just modify stuff to get myself out of trouble however I've come pretty stuck here.
I've linked below what I've added building upon your dashboard.
https://imgur.com/a/OwTW3ug
Below is also a link to the HTML of the page in question
https://pastebin.com/FirkBjzg
I can also share the .json when I figure out how upload it as I just did some minor housekeeping and unit measurements etc.
Appreciate any help you can give. I'm new to all of this so apologies if this is the wrong way to go about it.
TIA
Hey Andrew:
Current version does not supported the updated 2.x client from Influxdata (https://docs.influxdata.com/influxdb/v2.0/api-guide/client-libraries/python/)
Migration guide here: https://influxdb-client.readthedocs.io/en/stable/migration.html
Breaking Change: the 2.x client requires token based auth so the username/password and a few other options will require adjustment
PR incoming for changes.
I'm working on a module to support writing to Amazon Timestream as well - I don't have a local / managed Influx but Amazon Timestream is basically free for the volume of data I'll push to it for this monitoring.
Would it be possible to write to a flat file or send to Splunk HTTP Event Collector (HEC) directly? It looks like the way you've architected the app that a new arris_stats_splunk.py destination could be created.
I have the script running and if I use InfluxQL I can see that data is being successfully written to the InfluxDB database, but I'm not getting any results from your provided sb8200_grafana.json. It successfully imports the dashboard via file but no data displays. I tried restarting InfluxDB and Grafana but it didn't make a difference. Honestly, your queries are beyond what I normally use for my own dashboards so I am not sure what is wrong.
It looks like I have a new firmware from Comcast where it is 1.) allowing me to change the password after manually logging in and 2.) it is changing the auth_url
pattern. Instead of auth_url = url + '?' + auth_hash.decode()
, it is inserting login_
so the code needs to be updated to auth_url = url + '?login_' + auth_hash.decode()
. I am not 100% sure of the behavior but when I inspect the credential
variable, it's getting the login page without it.
Here is the cmconnectionstatus.html page that I am currently receiving. It looks like on line 99 that it shows login_
being included but I also do not have a saved page from what it looked like before.
Adding login_
now returns what looks to be a valid credential but I am still not getting stats and am seeing this after getting what appears to be a valid credential:
2021-10-14 12:56:38,288 INFO Retreiving stats from https://192.168.100.1/cmconnectionstatus.html
2021-10-14 12:56:38,290 DEBUG Starting new HTTPS connection (1): 192.168.100.1:443
2021-10-14 12:56:38,368 DEBUG https://192.168.100.1:443 "GET /cmconnectionstatus.html HTTP/1.1" 200 None
2021-10-14 12:56:38,416 ERROR Authentication error, received login page. Check username / password. SB8200 has some kind of bug that can cause this after too many authentications, the only known fix is to reboot the modem.
2021-10-14 12:56:38,416 ERROR No HTML to parse, giving up until next interval
2021-10-14 12:56:38,417 INFO clear_auth_token_on_html_error is true, clearing credential token
2021-10-14 12:56:38,417 INFO Sleeping for 120 second
It seems like something has changed where it might be sending an additional cookie. I see in dev tools in Chrome when trying to figure out what it is doing, it is sending a request header like: Cookie: HttpOnly: true, Secure: true; sessionId=W4y7qJL2bap3Ai3WSixrGU1PHSfXKiE
. Not sure if that was the same as before but a quick test with curl shows that this is required For example, this is what it takes to hit the page and get stats:
curl -kvL \
-H "Cookie: HttpOnly: true, Secure: true; sessionId=W4y7qJL2bap3Ai3WSixrGU1PHSfXKiE"\
"https://192.168.100.1/cmconnectionstatus.html?ct_EpuyMr65P3xW8q9wtZxrv3158yzpfUt"
Also, it looks like it may be adding ct_
in front of the cookie. I didn't see that in the code. I'll try to plug away at it a little bit to see if I can figure out how to adapt it to work but even then, I'll probably need some help with the logic to work with this different auth method without breaking the previous.
@andrewfraley The login is admin and the password is the last 8 digits of the modem's serial number. I was able to make some really ugly changes to the script to make it work, but it doesn't seem to work reliably. It works for a few minutes and then goes back to not accepting the credential cookie required to auth to it. I'll submit a PR once I'm more confident with my changes, but I thought I would give you a heads up.
Cox did a firwmare upgrade last night that has broken auth/login. The status page now returns 404. When I login in via web browser I'm seeing the url has changed and is now using php.
New status page URL now looks like this:
https://192.168.100.1/cmconnectionstatus.php?ct_83e452a103e3ea40437328e67750d735
2022-06-08 23:41:56,085 INFO Getting config from: /home/bbird/arris_cable_modem_stats/src/config.ini
2022-06-08 23:41:56,164 INFO Obtaining login session from modem
2022-06-08 23:41:56,167 DEBUG Starting new HTTPS connection (1): 192.168.100.1:443
2022-06-08 23:41:56,191 DEBUG https://192.168.100.1:443 "GET /cmconnectionstatus.html?login_YWRtaW46Qkc0MDQ3NTQ= HTTP/1.1" 404 341
2022-06-08 23:41:56,192 ERROR Error authenticating with https://192.168.100.1/cmconnectionstatus.html
2022-06-08 23:41:56,193 ERROR Status code: 404
2022-06-08 23:41:56,193 ERROR Reason: Not Found
2022-06-08 23:41:56,193 INFO Unable to obtain valid login session, sleeping for: 10s
Nice work on adding auth to this script, but I'm not having much luck. It works for exactly one cycle and then it dies. Not sure if this is something with the behavior of the modem not allowing auth so frequently, or something in this code.
I'm running this in a docker container, and i've enabled debug and this is what I see. I wonder if there needs to be some behavior changed in the auth code.
2020-11-07 19:17:23,345 INFO Obtaining login session from modem
2020-11-07 19:17:23,345 DEBUG auth_url: https://192.168.100.1/cmconnectionstatus.html?YWRtaW46ODU2MDI5NTA=
2020-11-07 19:17:23,346 DEBUG Starting new HTTPS connection (1): 192.168.100.1:443
2020-11-07 19:17:23,406 DEBUG https://192.168.100.1:443 "GET /cmconnectionstatus.html?YWRtaW46ODU2MDI5NTA= HTTP/1.1" 200 None
2020-11-07 19:17:23,445 INFO Retreiving stats from https://192.168.100.1/cmconnectionstatus.html
2020-11-07 19:17:23,448 DEBUG Starting new HTTPS connection (1): 192.168.100.1:443
2020-11-07 19:17:29,652 DEBUG https://192.168.100.1:443 "GET /cmconnectionstatus.html HTTP/1.1" 200 None
2020-11-07 19:17:30,011 INFO Parsing HTML for modem model sb8200
2020-11-07 19:17:30,104 DEBUG downstream stats: [{'channel_id': '2', 'frequency': '483000000', 'power': '0.3', 'snr': '42.4', 'corrected': '0', 'uncorrectables': '0'}, {'channel_id': '1', 'frequency': '477000000', 'power': '-0.1', 'snr': '42.2', 'corrected': '0', 'uncorrectables': '0'}, {'channel_id': '3', 'frequency': '489000000', 'power': '0.2', 'snr': '42.3', 'corrected': '0', 'uncorrectables': '0'}, {'channel_id': '4', 'frequency': '495000000', 'power': '0.5', 'snr': '42.4', 'corrected': '0', 'uncorrectables': '0'}, {'channel_id': '5', 'frequency': '501000000', 'power': '0.2', 'snr': '42.2', 'corrected': '0', 'uncorrectables': '0'}, {'channel_id': '6', 'frequency': '507000000', 'power': '0.5', 'snr': '42.2', 'corrected': '0', 'uncorrectables': '0'}, {'channel_id': '7', 'frequency': '513000000', 'power': '0.3', 'snr': '42.2', 'corrected': '0', 'uncorrectables': '0'}, {'channel_id': '8', 'frequency': '519000000', 'power': '0.3', 'snr': '41.6', 'corrected': '0', 'uncorrectables': '0'}, {'channel_id': '9', 'frequency': '525000000', 'power': '0.2', 'snr': '42.1', 'corrected': '0', 'uncorrectables': '0'}, {'channel_id': '10', 'frequency': '531000000', 'power': '0.4', 'snr': '42.1', 'corrected': '0', 'uncorrectables': '0'}, {'channel_id': '11', 'frequency': '537000000', 'power': '0.4', 'snr': '42.1', 'corrected': '0', 'uncorrectables': '0'}, {'channel_id': '12', 'frequency': '543000000', 'power': '0.5', 'snr': '42.1', 'corrected': '0', 'uncorrectables': '0'}, {'channel_id': '13', 'frequency': '555000000', 'power': '0.0', 'snr': '41.9', 'corrected': '0', 'uncorrectables': '0'}, {'channel_id': '14', 'frequency': '561000000', 'power': '-0.6', 'snr': '41.8', 'corrected': '0', 'uncorrectables': '0'}, {'channel_id': '15', 'frequency': '567000000', 'power': '-0.3', 'snr': '41.8', 'corrected': '0', 'uncorrectables': '0'}, {'channel_id': '16', 'frequency': '573000000', 'power': '0.5', 'snr': '41.8', 'corrected': '0', 'uncorrectables': '0'}, {'channel_id': '17', 'frequency': '579000000', 'power': '1.0', 'snr': '42.1', 'corrected': '0', 'uncorrectables': '0'}, {'channel_id': '18', 'frequency': '585000000', 'power': '0.8', 'snr': '42.0', 'corrected': '0', 'uncorrectables': '0'}, {'channel_id': '19', 'frequency': '591000000', 'power': '1.3', 'snr': '42.2', 'corrected': '0', 'uncorrectables': '0'}, {'channel_id': '20', 'frequency': '597000000', 'power': '1.0', 'snr': '42.0', 'corrected': '0', 'uncorrectables': '0'}, {'channel_id': '21', 'frequency': '603000000', 'power': '1.3', 'snr': '42.1', 'corrected': '0', 'uncorrectables': '0'}, {'channel_id': '22', 'frequency': '609000000', 'power': '1.4', 'snr': '42.1', 'corrected': '0', 'uncorrectables': '0'}, {'channel_id': '23', 'frequency': '615000000', 'power': '0.8', 'snr': '41.9', 'corrected': '0', 'uncorrectables': '0'}, {'channel_id': '24', 'frequency': '621000000', 'power': '1.2', 'snr': '42.0', 'corrected': '0', 'uncorrectables': '0'}, {'channel_id': '25', 'frequency': '627000000', 'power': '1.3', 'snr': '42.0', 'corrected': '0', 'uncorrectables': '0'}, {'channel_id': '26', 'frequency': '633000000', 'power': '1.1', 'snr': '42.0', 'corrected': '0', 'uncorrectables': '0'}, {'channel_id': '27', 'frequency': '639000000', 'power': '1.6', 'snr': '42.1', 'corrected': '0', 'uncorrectables': '0'}, {'channel_id': '33', 'frequency': '453000000', 'power': '-0.2', 'snr': '42.2', 'corrected': '0', 'uncorrectables': '0'}, {'channel_id': '34', 'frequency': '459000000', 'power': '-0.2', 'snr': '42.2', 'corrected': '0', 'uncorrectables': '0'}, {'channel_id': '35', 'frequency': '465000000', 'power': '-0.5', 'snr': '42.1', 'corrected': '0', 'uncorrectables': '0'}, {'channel_id': '36', 'frequency': '471000000', 'power': '-0.3', 'snr': '42.2', 'corrected': '0', 'uncorrectables': '0'}, {'channel_id': '37', 'frequency': '690000000', 'power': '1.6', 'snr': '40.2', 'corrected': '57280518', 'uncorrectables': '0'}]
2020-11-07 19:17:30,106 DEBUG upstream stats: [{'channel_id': '1', 'frequency': '35600000', 'power': '51.0'}, {'channel_id': '2', 'frequency': '29200000', 'power': '51.0'}, {'channel_id': '3', 'frequency': '22800000', 'power': '52.0'}, {'channel_id': '4', 'frequency': '16400000', 'power': '51.0'}, {'channel_id': '5', 'frequency': '39600000', 'power': '51.0'}]
2020-11-07 19:17:30,106 INFO Sending stats to InfluxDB (unraid.home:8086)
2020-11-07 19:17:30,124 DEBUG Starting new HTTP connection (1): unraid.home:8086
2020-11-07 19:17:30,128 DEBUG http://unraid.home:8086 "POST /write?db=cable_modem_stats HTTP/1.1" 204 0
2020-11-07 19:17:30,128 INFO Successfully wrote data to InfluxDB
2020-11-07 19:17:30,128 DEBUG Influx series sent to db:
2020-11-07 19:17:30,128 DEBUG [{'measurement': 'downstream_statistics', 'time': '2020-11-07T19:17:30Z', 'fields': {'frequency': 483000000, 'power': 0.3, 'snr': 42.4, 'corrected': 0, 'uncorrectables': 0}, 'tags': {'channel_id': 2}}, {'measurement': 'downstream_statistics', 'time': '2020-11-07T19:17:30Z', 'fields': {'frequency': 477000000, 'power': -0.1, 'snr': 42.2, 'corrected': 0, 'uncorrectables': 0}, 'tags': {'channel_id': 1}}, {'measurement': 'downstream_statistics', 'time': '2020-11-07T19:17:30Z', 'fields': {'frequency': 489000000, 'power': 0.2, 'snr': 42.3, 'corrected': 0, 'uncorrectables': 0}, 'tags': {'channel_id': 3}}, {'measurement': 'downstream_statistics', 'time': '2020-11-07T19:17:30Z', 'fields': {'frequency': 495000000, 'power': 0.5, 'snr': 42.4, 'corrected': 0, 'uncorrectables': 0}, 'tags': {'channel_id': 4}}, {'measurement': 'downstream_statistics', 'time': '2020-11-07T19:17:30Z', 'fields': {'frequency': 501000000, 'power': 0.2, 'snr': 42.2, 'corrected': 0, 'uncorrectables': 0}, 'tags': {'channel_id': 5}}, {'measurement': 'downstream_statistics', 'time': '2020-11-07T19:17:30Z', 'fields': {'frequency': 507000000, 'power': 0.5, 'snr': 42.2, 'corrected': 0, 'uncorrectables': 0}, 'tags': {'channel_id': 6}}, {'measurement': 'downstream_statistics', 'time': '2020-11-07T19:17:30Z', 'fields': {'frequency': 513000000, 'power': 0.3, 'snr': 42.2, 'corrected': 0, 'uncorrectables': 0}, 'tags': {'channel_id': 7}}, {'measurement': 'downstream_statistics', 'time': '2020-11-07T19:17:30Z', 'fields': {'frequency': 519000000, 'power': 0.3, 'snr': 41.6, 'corrected': 0, 'uncorrectables': 0}, 'tags': {'channel_id': 8}}, {'measurement': 'downstream_statistics', 'time': '2020-11-07T19:17:30Z', 'fields': {'frequency': 525000000, 'power': 0.2, 'snr': 42.1, 'corrected': 0, 'uncorrectables': 0}, 'tags': {'channel_id': 9}}, {'measurement': 'downstream_statistics', 'time': '2020-11-07T19:17:30Z', 'fields': {'frequency': 531000000, 'power': 0.4, 'snr': 42.1, 'corrected': 0, 'uncorrectables': 0}, 'tags': {'channel_id': 10}}, {'measurement': 'downstream_statistics', 'time': '2020-11-07T19:17:30Z', 'fields': {'frequency': 537000000, 'power': 0.4, 'snr': 42.1, 'corrected': 0, 'uncorrectables': 0}, 'tags': {'channel_id': 11}}, {'measurement': 'downstream_statistics', 'time': '2020-11-07T19:17:30Z', 'fields': {'frequency': 543000000, 'power': 0.5, 'snr': 42.1, 'corrected': 0, 'uncorrectables': 0}, 'tags': {'channel_id': 12}}, {'measurement': 'downstream_statistics', 'time': '2020-11-07T19:17:30Z', 'fields': {'frequency': 555000000, 'power': 0.0, 'snr': 41.9, 'corrected': 0, 'uncorrectables': 0}, 'tags': {'channel_id': 13}}, {'measurement': 'downstream_statistics', 'time': '2020-11-07T19:17:30Z', 'fields': {'frequency': 561000000, 'power': -0.6, 'snr': 41.8, 'corrected': 0, 'uncorrectables': 0}, 'tags': {'channel_id': 14}}, {'measurement': 'downstream_statistics', 'time': '2020-11-07T19:17:30Z', 'fields': {'frequency': 567000000, 'power': -0.3, 'snr': 41.8, 'corrected': 0, 'uncorrectables': 0}, 'tags': {'channel_id': 15}}, {'measurement': 'downstream_statistics', 'time': '2020-11-07T19:17:30Z', 'fields': {'frequency': 573000000, 'power': 0.5, 'snr': 41.8, 'corrected': 0, 'uncorrectables': 0}, 'tags': {'channel_id': 16}}, {'measurement': 'downstream_statistics', 'time': '2020-11-07T19:17:30Z', 'fields': {'frequency': 579000000, 'power': 1.0, 'snr': 42.1, 'corrected': 0, 'uncorrectables': 0}, 'tags': {'channel_id': 17}}, {'measurement': 'downstream_statistics', 'time': '2020-11-07T19:17:30Z', 'fields': {'frequency': 585000000, 'power': 0.8, 'snr': 42.0, 'corrected': 0, 'uncorrectables': 0}, 'tags': {'channel_id': 18}}, {'measurement': 'downstream_statistics', 'time': '2020-11-07T19:17:30Z', 'fields': {'frequency': 591000000, 'power': 1.3, 'snr': 42.2, 'corrected': 0, 'uncorrectables': 0}, 'tags': {'channel_id': 19}}, {'measurement': 'downstream_statistics', 'time': '2020-11-07T19:17:30Z', 'fields': {'frequency': 597000000, 'power': 1.0, 'snr': 42.0, 'corrected': 0, 'uncorrectables': 0}, 'tags': {'channel_id': 20}}, {'measurement': 'downstream_statistics', 'time': '2020-11-07T19:17:30Z', 'fields': {'frequency': 603000000, 'power': 1.3, 'snr': 42.1, 'corrected': 0, 'uncorrectables': 0}, 'tags': {'channel_id': 21}}, {'measurement': 'downstream_statistics', 'time': '2020-11-07T19:17:30Z', 'fields': {'frequency': 609000000, 'power': 1.4, 'snr': 42.1, 'corrected': 0, 'uncorrectables': 0}, 'tags': {'channel_id': 22}}, {'measurement': 'downstream_statistics', 'time': '2020-11-07T19:17:30Z', 'fields': {'frequency': 615000000, 'power': 0.8, 'snr': 41.9, 'corrected': 0, 'uncorrectables': 0}, 'tags': {'channel_id': 23}}, {'measurement': 'downstream_statistics', 'time': '2020-11-07T19:17:30Z', 'fields': {'frequency': 621000000, 'power': 1.2, 'snr': 42.0, 'corrected': 0, 'uncorrectables': 0}, 'tags': {'channel_id': 24}}, {'measurement': 'downstream_statistics', 'time': '2020-11-07T19:17:30Z', 'fields': {'frequency': 627000000, 'power': 1.3, 'snr': 42.0, 'corrected': 0, 'uncorrectables': 0}, 'tags': {'channel_id': 25}}, {'measurement': 'downstream_statistics', 'time': '2020-11-07T19:17:30Z', 'fields': {'frequency': 633000000, 'power': 1.1, 'snr': 42.0, 'corrected': 0, 'uncorrectables': 0}, 'tags': {'channel_id': 26}}, {'measurement': 'downstream_statistics', 'time': '2020-11-07T19:17:30Z', 'fields': {'frequency': 639000000, 'power': 1.6, 'snr': 42.1, 'corrected': 0, 'uncorrectables': 0}, 'tags': {'channel_id': 27}}, {'measurement': 'downstream_statistics', 'time': '2020-11-07T19:17:30Z', 'fields': {'frequency': 453000000, 'power': -0.2, 'snr': 42.2, 'corrected': 0, 'uncorrectables': 0}, 'tags': {'channel_id': 33}}, {'measurement': 'downstream_statistics', 'time': '2020-11-07T19:17:30Z', 'fields': {'frequency': 459000000, 'power': -0.2, 'snr': 42.2, 'corrected': 0, 'uncorrectables': 0}, 'tags': {'channel_id': 34}}, {'measurement': 'downstream_statistics', 'time': '2020-11-07T19:17:30Z', 'fields': {'frequency': 465000000, 'power': -0.5, 'snr': 42.1, 'corrected': 0, 'uncorrectables': 0}, 'tags': {'channel_id': 35}}, {'measurement': 'downstream_statistics', 'time': '2020-11-07T19:17:30Z', 'fields': {'frequency': 471000000, 'power': -0.3, 'snr': 42.2, 'corrected': 0, 'uncorrectables': 0}, 'tags': {'channel_id': 36}}, {'measurement': 'downstream_statistics', 'time': '2020-11-07T19:17:30Z', 'fields': {'frequency': 690000000, 'power': 1.6, 'snr': 40.2, 'corrected': 57280518, 'uncorrectables': 0}, 'tags': {'channel_id': 37}}, {'measurement': 'upstream_statistics', 'time': '2020-11-07T19:17:30Z', 'fields': {'frequency': 35600000, 'power': 51.0}, 'tags': {'channel_id': 1}}, {'measurement': 'upstream_statistics', 'time': '2020-11-07T19:17:30Z', 'fields': {'frequency': 29200000, 'power': 51.0}, 'tags': {'channel_id': 2}}, {'measurement': 'upstream_statistics', 'time': '2020-11-07T19:17:30Z', 'fields': {'frequency': 22800000, 'power': 52.0}, 'tags': {'channel_id': 3}}, {'measurement': 'upstream_statistics', 'time': '2020-11-07T19:17:30Z', 'fields': {'frequency': 16400000, 'power': 51.0}, 'tags': {'channel_id': 4}}, {'measurement': 'upstream_statistics', 'time': '2020-11-07T19:17:30Z', 'fields': {'frequency': 39600000, 'power': 51.0}, 'tags': {'channel_id': 5}}]
2020-11-07 19:17:30,129 INFO Sleeping for 600 seconds
2020-11-07 19:27:30,229 INFO Retreiving stats from https://192.168.100.1/cmconnectionstatus.html
2020-11-07 19:27:30,231 DEBUG Starting new HTTPS connection (1): 192.168.100.1:443
2020-11-07 19:27:30,268 DEBUG https://192.168.100.1:443 "GET /cmconnectionstatus.html HTTP/1.1" 200 None
2020-11-07 19:27:30,310 ERROR Authentication error, received login page. Check username / password.
2020-11-07 19:27:30,310 ERROR No HTML to parse, giving up until next interval
2020-11-07 19:27:30,310 INFO Sleeping for 600 seconds
2020-11-07 19:37:30,410 INFO Retreiving stats from https://192.168.100.1/cmconnectionstatus.html
2020-11-07 19:37:30,412 DEBUG Starting new HTTPS connection (1): 192.168.100.1:443
2020-11-07 19:37:30,484 DEBUG https://192.168.100.1:443 "GET /cmconnectionstatus.html HTTP/1.1" 200 None
2020-11-07 19:37:30,525 ERROR Authentication error, received login page. Check username / password.
2020-11-07 19:37:30,525 ERROR No HTML to parse, giving up until next interval
2020-11-07 19:37:30,525 INFO Sleeping for 600 seconds
Why not setup a container auto build and publish it via github? Would make it easier for people like me that use kubernetes for hosting these types of things.
Rather than have Debug be a setting when you run the program, this should be moved (or added) to the configuration file. When running this in a docker container, it's rather difficult to temporarily enable debug. I've had to actually have a separate docker container that has the Dockerfile modified for the run command to be --debug. If it was in the config file, you could simply share the config file with your docker container, modify the variable in config.ini, restart the docker container and then you have debug running.
I followed your instructions and got the docker container and telegraf config working, very nice!
However, the grafana page for the internet dashboard seems to have an issue extrapolating the different hosts that are a part of the ping test and building out the graphs like it does in your screenshots. I'm not sure if theres a variable I need to configure or something else to change, here is what I get.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.