ttlappalainen / nmea0183 Goto Github PK
View Code? Open in Web Editor NEWLibrary for handling NMEA0183 messages
Library for handling NMEA0183 messages
on a linux box working with RMC
input:
$GPRMC,203253.00,A,2642.08438,N,07859.51125,W,0.065,,061220,,,A*67
return from
if ( !NMEA0183ParseRMC_nc(NMEA0183Msg, GPSTime, Latitude, Longitude, COG, SOG, DaysSince1970, Variation, &DateTime)) {
return;
}
RMC: 06/12/2020 20:32:53, 26.701406, -78.991854, 0.000000, 0.065000, 18603, 0.000000, 1607304773
timezone set on my machine is EDT (GMT-4).
the issue is that DateTime == 1607304773 corresponds to
Monday, December 7, 2020 1:32:53 AM
and not 20:32:53 06/12/20
the problem is that makeTime is defined as
static inline time_t makeTime(tmElements_t &TimeElements) { return mktime(&TimeElements); }
which uses mktime which according to the docs
"The mktime() function converts a broken-down time structure, expressed as local time, to calendar time representation. "
so it uses timezone. since GPS time is always in UTC it seems that the correct definition for makeTime should be
static inline time_t makeTime(tmElements_t &TimeElements) { return mktime(&TimeElements) - timezone; }
i.e. subtract out the timezone to get back to GMT.
Hi Timo
Maybe a silly problem, but I am unable to compile die examples of the NMEA0183 library:
C:\Users\Uwe\Documents\Arduino\libraries\NMEA0183/NMEA0183Messages.h: In member function 'char* tRTE::operator const':
C:\Users\Uwe\Documents\Arduino\libraries\NMEA0183/NMEA0183Messages.h:50:11: warning: comparison between signed and unsigned integer expressions [-Wsign-compare]
if (i > nrOfwp || i < 0) {
^
C:\Users\Uwe\Documents\Arduino\libraries\NMEA0183/NMEA0183Messages.h:53:11: error: invalid conversion from 'const char*' to 'char*' [-fpermissive]
return _wp;
^
C:\Users\Uwe\Documents\Arduino\libraries\NMEA0183/NMEA0183Messages.h:58:21: error: invalid conversion from 'const char*' to 'char*' [-fpermissive]
return _wp + k + 1;
Any idea?
best Regards Uwe
BTW, the example in the NMEA2000 lib work fine
A new field (12, FAA Mode Indicator) was added to the RMC sentence in NMEA v2.3. The Set and Parse for this sentence should support this new field.
The content of this field is a single status character: A - Data Valid, or V - Data Invalid. Some devices expecting this field will ignore the entire message if field 12 is missing.
Reference: OpenCPN:
https://www.opencpn.org/wiki/dokuwiki/doku.php?id=opencpn:opencpn_user_manual:advanced_features:nmea_sentences
This library is superb, and together with NMEA2000 is just such a great contribution.
Unless there is something we are not understanding (which is not uncommon), the NMEA0183SetGGA() method should be setting strings of "M" (for meters) as fields 9 and 11. Currently it seems to generate GGA messages without those units fields for the related altitude and geoseparation fields.
Happy to do a pull request, but just wanted to make sure there is not something obvious we are failing to see.
this family of routines should return NMEA183xxxxNA when given arguments of "". They mostly all return 0.
NMEA0183GPSDateTimetotime_t(NMEA0183Msg.Field(8),0) is particularly bad since it returns bogus time instead of NMEA0183time_tNA (which does not exist).
$GPGGA,,,,,,0,00,99.99,,,,,,*48
for example returns 0 for Time, Lat, Lon, Alt the fields are really NA.
same is true for
$GPGLL,,,,,,V,N64
$GPRMC,,V,,,,,,,,,,N53
although these have status flags that tag the data as invalid.
RMC returns bogus data time data since it calls
lDT=NMEA0183GPSDateTimetotime_t(NMEA0183Msg.Field(8),0)+floor(GPSTime);
and NMEA0183GPSDateTimetotime_t returns bogus data when passed "" for arguments.
Do not know if this is the right place for this:
Is it somewhere already, or are there any plans, or is there somebody working on NMEA2000 AIS PGNs converting to "NMEA" !AIVDM sentences yet?
should this
bool tNMEA0183Msg::Init(const char *_MessageCode, const char *_Sender, char _Prefix) {
Clear();
size_t nSender=2;
size_t nMessageCode=0;
if ( _Sender==0 && (nSender=strlen(_Sender))>7 ) return false;
did you mean
if ( _Sender==0 || (nSender=strlen(_Sender))>7 ) return false;
g++ is complaining...
It seems that the library returns SI units for everithing including headings and angles. Lat/Lon are in deg which makes sense. COG is in radians.... looking at the code in RMC we see
SOG=atof(NMEA0183Msg.Field(6))*knToms;
TrueCOG=atof(NMEA0183Msg.Field(7))*degToRad;
so it would seem that WindAngle should also be in radians... but its in degrees. Here is the code
bool NMEA0183ParseMWV_nc(const tNMEA0183Msg &NMEA0183Msg,double &WindAngle, tNMEA0183WindReference &Reference, double &WindSpeed) {
bool result=( NMEA0183Msg.FieldCount()>=4 );
if ( result ) {
WindAngle=atof(NMEA0183Msg.Field(0));
it would be nice if the definitions in NMEA0183Messages.h would list the units of the arguments in the commets as is done in the NMEA2000 library.
I think there is an Issue in your Code.
in File "NMEA0183.cpp" in Function "SendBuf"
181 // Could not send immediately, so buffer message
182 if ( strlen(buf)-iBuf >= MsgOutBufFreeSize() ) return false; // No room for message
183
184 size_t wp=MsgOutWritePos;
185
186 for (size_t temp = (MsgOutWritePos + 1) % MsgOutBufSize;
187 buf[iBuf]!=0 && temp!=MsgOutReadPos;
188 temp = (MsgOutWritePos + 1) % MsgOutBufSize ) {
189 MsgOutBuf[MsgOutWritePos]=buf[iBuf];
190 MsgOutWritePos=temp;
191 iBuf++; // <------ Please add this here !!****
192 }
193
194 if ( buf[iBuf]!=0 ) {
......
Am I right ?
greetings from Hamburg
Hi Timo,
After updating NMEA0183 lib from 1.7.1 to 1.8.2 in Code/Platformio two compile errors for missing definitions popped up. Fixed by adding class name to breaktime() call on line 128 of NMEA0183Messages.cpp.
changed breakTime(defDate,TimeElements); to tNMEA0183Msg::breakTime(defDate,TimeElements);
second issue was due to missing Macro for NMEA0183IsNA(time_t v) in NMEA0183Msgs.h. Fixed by inserting macro definition:
inline bool NMEA0183IsNA(time_t v) { return v==NMEA0183time_tNA; }
at line 50.
Thanks for all your wonderful efforts!
Mike Kenny, nz9p
Hi, I wanted to get the GSV - Satellites in View to plot a basic map of satellites so I have added a basic parser for GSV messages.
Timo,
When I use "PrintNMEA" example from NMEA0183 library, with Chrome NMEA Simulator (https://chrome.google.com/webstore/detail/nmea-simulator/dfhcgoinjchfcfnnkecjpjcnknlipcll/related), sentences are displayed successfully.
If I change the source from Chrome Simulator to the RTL_AIS receiver (https://github.com/dgiardini/rtl-ais), "PrintNMEA" appear to be unable to parse the sentences (none are displayed).
The main difference between sentences appear to be :
"!AIDVO" for sentences that work fine (Chrome NMEA Simulator). First image with 2 sentences
"!AIVDM" for sentences that are not displayed (RTL_AIS). Second image with 9 sentences
Any idea about the source of the problem ?
Thank you !
Currently when you parse an NMEA0183 message (tNMEA0183Msg::SetMessage) there is no check against the field count.
So if the message (accidently) contains more then 20 fields it will overwrite data leading to crashes.
So there should be something like
if (_FieldCount >= MAX_NMEA0183_MSG_FIELDS){
Clear();
return false;
}
before line 101 of NMEA0183Message.cpp.
First, huge thanks for this library. I am trying to make use of it in a N2K>0183 project that is based on a NMEA0183 multiplexer.
This previous multiplexer code optionally allowed the acceptance of 0183 messages with incorrect checksums to be parsed. (This sometimes useful if you are trying handwritten 0183 messages and cannot run a checksum generator).
Am I correct that the 0183 handlers in the library ALL require the checksum to be correct? Or is there a "checkChecksum" boolean somewhere that I can turn on and off to allow parsing of incorrectly checksummed messages?
---I have tried to see where the checksum is "checked" but have failed.---- As far as I can see the NMEA decoders do not check checksums..!
One option I can see to "allow" incorrect checksums would be to parse all incoming messages and deliberately set (correct) checksums before passing them to the handler. But this seems a rather extreme approach!
Many thanks
Dagnall
Hi Timo. How can I get satellite data using your library glonass? Are they supported?
It seams that some manufacturers are sending the NMEA checksum using lower case characters for a...f.
So It would be good of the library would also accept them in the checksum.
left out status. its in tRMC but not used anywhere. Simple enought to add in.
http://caxapa.ru/thumbs/214299/NMEA0183_.pdf
MWV is referenced as Apparent wind OR Theoretical wind. I quote
When the reference field is set to T (Theoretical, calculated actual wind), data is provided giving the wind angle in relation to the vessel's bow/centerline and the wind speed as if the vessel was stationary. On a moving ship these data can be calculated by combining the measured relative wind with the vessel's own speed.
The code assumes that 'T' refers to True which is misleading. There is nothing wrong with the way things are written but an implementor might be mislead sending true wind values instead of Theoretical.
The library should probably be modified to change references to NMEA0183Wind_True to NMEA0183Wind_Theoretical
enum tNMEA0183WindReference {
NMEA0183Wind_True=0,
// Apparent Wind (relative to the vessel centerline)
NMEA0183Wind_Apparent=1
};
//*****************************************************************************
// MWV - Wind Speed and Angle
//$IIMWV,120.1,R,9.5,M,A,a*hh
bool NMEA0183ParseMWV_nc(const tNMEA0183Msg &NMEA0183Msg,double &WindAngle, tNMEA0183WindReference &Reference, double &WindSpeed) {
bool result=( NMEA0183Msg.FieldCount()>=4 );
if ( result ) {
WindAngle=atof(NMEA0183Msg.Field(0))*degToRad;
switch ( NMEA0183Msg.Field(1)[0] ) {
case 'T' : Reference=NMEA0183Wind_True; break;
case 'R' :
default : Reference=NMEA0183Wind_Apparent; break;
}
WindSpeed=atof(NMEA0183Msg.Field(2));
switch ( NMEA0183Msg.Field(3)[0] ) {
case 'K' : WindSpeed*=kmhToms; break;
case 'N' : WindSpeed*=knToms; break;
case 'M' :
default : ;
}
}
return result;
}
Hi Timo,
in example NMEA0183/Examples/NMEA2000ToNMEA0183/
there is:
void tN2kDataToNMEA0183::HandleMsg(const tN2kMsg &N2kMsg) {
switch (N2kMsg.PGN) {
case 127250UL: HandleHeading(N2kMsg);
case 127258UL: HandleVariation(N2kMsg);
case 128259UL: HandleBoatSpeed(N2kMsg);
case 128267UL: HandleDepth(N2kMsg);
case 129025UL: HandlePosition(N2kMsg);
case 129026UL: HandleCOGSOG(N2kMsg);
case 129029UL: HandleGNSS(N2kMsg);
}
}
I was adding case 129038UL: HandleAISClassAPosReport(N2kMsg); // AIS Class A Position Report
and got many false returns, because every PGN from all cases were sent to on ParseN2kPGN129038
function.
I added break;
to each case
line and now it works.
Hi Timo,
While using this Library ,I identified a compatibility problem with a loogbook app.
It looks like the Longitude, Latttude information is not formatted according to the NMEA specification, which requires leading zeros. Most apps will ignore the missing leading zeros, but not all.
Changing the formatting in NMEA0183Msg.h worked for me (%08.3f and %09.3f).
Regards,
Andreas
// Add Latitude field. Also E/W will be added. Latitude is in degrees. Negative value is W. E.g.
// AddLatitudeField(-5.2345); -> ,5.235,W
bool AddLatitudeField(double Latitude, const char *Format="%08.3f");
// Add Longitude field. Also N/S will be added. Longitude is in degrees. Negative value is S. E.g.
// AddLongitudeField(-5.2345); -> ,514.070,S
bool AddLongitudeField(double Longitude, const char *Format="%09.3f");
Timo,
Thanks for your help, and perhaps I can suggest a small contribution.
I find that the heading conversions (HDM HDT and HDG) to N2K and then back to 0183 all resulted in HDG, which to my thinking is incorrect, as it converts a (eg) HDM to a HDG, and thus presents (HDG) data (e.g. previously sent variation), that was not present (and may therefore be incorrect or outdated) when the 0183 HDM was sent..
The 127250 message seems sufficiently well defined to allow the conversion of a HDM or HDT so that a demonstrably N2K>0183 conversion back to HDM or HDT can be made- avoiding conversion to HDG with possibly incorrect saved variation / deviation parameters.
I have placed below my suggested HandleHDT, HandleHDM, HandleHDG functions from NMEA0183Handlers.cpp, and the N2KDataToNMEA0183.cpp "HandleHeading" (I wanted to avoid making a duplicate issue in N2k)..
I tested with these
$IIHDM,310.1,M21
$IIHDT,320.1,T22
$GPHDG,310.1,,,,5D
$GPHDG,310.1,,,10.0,E07
$GPHDG,310.1,5.0,E,5.0,E*5D
The third case is one I would not expect in practice as it is HDG without deviation or variation, and I would have expected the instrument to send HDM in this case, and it is not possible (I think) to differentiate this 127250 from one sent with the simpler HDM.
The results of 0183>N2K>0183 are shown below. (but change to GP).
$GPHDM,310.1,M36
$GPHDT,320.1,T35
$GPHDM,310.1,M36
$GPHDG,310.1,,,10.0,E07
$GPHDG,310.1,5.0,E,5.0,E*5D
Code shown below.
`void HandleHDT(const tNMEA0183Msg &NMEA0183Msg) { // HDT to PGN 127250
if (pBD == 0) { return; };
if (NMEA0183ParseHDT_nc(NMEA0183Msg, pBD->TrueHeading)) { // get value
if (pNMEA2000 != 0) {
SID = SID + 1;
if (SID >= 100) { SID = 1; } // increment SID to differentiate this "set" of NAV data from any previous "sets"
tN2kMsg N2kMsg;
if (NMEA0183ParseHDT_nc(NMEA0183Msg, pBD->TrueHeading)) { //Use library Parse to get the TRUE heading into the boat data and do the next if that suceeds
SetN2kPGN127250(N2kMsg, SID, pBD->TrueHeading, NMEA0183DoubleNA, NMEA0183DoubleNA, N2khr_true);
SendMessageOnceBuilt(N2kMsg);
}
}
}
}
void HandleHDM(const tNMEA0183Msg &NMEA0183Msg) { // HDM to PGN 127250
if (pBD == 0) {return;}
if (pNMEA2000 != 0) {
tN2kMsg N2kMsg;
SID = SID + 1;
if (SID >= 100) { SID = 1; } // increment SID to differentiate this "set" of NAV data from any previous "sets"
if (NMEA0183ParseHDM_nc(NMEA0183Msg, pBD->MagneticHeading)) { //NOTE do not use saved variation.. we only got a HDM, so send it "honsestly"
SetN2kPGN127250(N2kMsg, SID, pBD->MagneticHeading, NMEA0183DoubleNA, NMEA0183DoubleNA, N2khr_magnetic);
SendMessageOnceBuilt(N2kMsg);
}
}
}
void HandleHDG(const tNMEA0183Msg &NMEA0183Msg) { // HDG to PGN 127250 Save MAGHeading (and variation if received) in boat data. Deviation is just local
if (pBD == 0) return;
if (pNMEA2000 != 0) {
tN2kMsg N2kMsg;
SID = SID + 1;
if (SID >= 100) { SID = 1; } // increment SID to differentiate this "set" of NAV data from any previous "sets"
//Get HDG data (in degrees)
double heading = NMEA0183GetDouble(NMEA0183Msg.Field(0));
pBD->MagneticHeading = DegToRad(heading);
double deviation = NMEA0183GetDouble(NMEA0183Msg.Field(1));
double variation = NMEA0183GetDouble(NMEA0183Msg.Field(3));
// Correct for Deviation and Variation if available to give heading in N2000 format for PGN127250
if ((NMEA0183GetDouble(NMEA0183Msg.Field(1)) != NMEA0183DoubleNA) && (NMEA0183GetDouble(NMEA0183Msg.Field(2)) != NMEA0183DoubleNA)) { // we have deviation data.
if (NMEA0183Msg.Field(2)[0] == 'W') { deviation = -deviation; }
heading = heading + deviation; //Update heading(deg) with Deviation(deg)
deviation = DegToRad(deviation);
}
if ((NMEA0183GetDouble(NMEA0183Msg.Field(3)) != NMEA0183DoubleNA) && (NMEA0183GetDouble(NMEA0183Msg.Field(4)) != NMEA0183DoubleNA)) { // we have deviation data.
if (NMEA0183Msg.Field(4)[0] == 'W') { variation = -variation; }
heading = heading + variation; //Update heading(deg) with Variation(deg)
pBD->Variation = DegToRad(variation); //Save for other functions to use.
} // NB if No variation was sent, do not use an old and potentially incorrect one !
pBD->TrueHeading = DegToRad(heading); //Save for other functions to use. // N2000 uses Radians
SetN2kPGN127250(N2kMsg, SID, pBD->TrueHeading, deviation, DegToRad(variation), N2khr_magnetic); // set N2khr_magnetic but do NOT send pBD->Variation unless we received it! as this MAY be old.
SendMessageOnceBuilt(N2kMsg);
}
}
void tN2kDataToNMEA0183::HandleHeading(const tN2kMsg &N2kMsg) {
/*
1 Sequence ID
2 Heading Sensor Reading
3 Deviation
4 Variation
5 Heading Sensor Reference
6 NMEA Reserved
{"Vessel Heading", https://github.com/canboat/canboat/blob/master/analyzer/pgn.h
127250,
PACKET_COMPLETE,
PACKET_SINGLE,
{UINT8_FIELD("SID"),
ANGLE_U16_FIELD("Heading", NULL),
ANGLE_I16_FIELD("Deviation", NULL),
ANGLE_I16_FIELD("Variation", NULL),
LOOKUP_FIELD("Reference", 2, DIRECTION_REFERENCE),
RESERVED_FIELD(6),
END_OF_FIELDS},
.interval = 100}
*/
unsigned char SID;
tN2kHeadingReference ref;
double Deviation = 0; // not used in other places ?
double _Deviation = 0;
double _Variation;
tNMEA0183Msg NMEA0183Msg;
//Select which message to send! depending on type of data received: If N2khr_magnetic -- send HDG or HDM depending on if DEV/Var were seen.
//if N2khr_true send HDT!
bool SendHDM = true;
if (ParseN2kHeading(N2kMsg, SID, Heading, _Deviation, _Variation, ref)) {
LastHeadingTime = millis();
if (ref == N2khr_magnetic) {
if (!N2kIsNA(_Deviation)) { // Update Deviation, Send HDG
Deviation = _Deviation;
SendHDM = false;
}
if (!N2kIsNA(_Variation)) { // Update Variation, Send HDG
Variation = _Variation;
SendHDM = false;
}
if (!N2kIsNA(Heading) && !N2kIsNA(_Deviation)) { Heading -= Deviation; }
if (!N2kIsNA(Heading) && !N2kIsNA(_Variation)) { Heading -= Variation; }
if (SendHDM) {
if (NMEA0183SetHDM(NMEA0183Msg, Heading)) { SendMessage(NMEA0183Msg); }
} else { //data included Deviation and/or Variation so send as HDG
if (NMEA0183SetHDG(NMEA0183Msg, Heading, _Deviation, Variation)) { SendMessage(NMEA0183Msg); }
}
} else { // data was "true" so send as HDT!
if (NMEA0183SetHDT(NMEA0183Msg, Heading)) { SendMessage(NMEA0183Msg); }
}
}
}`
Hi,
You did a great job.
I have very basic questions if you can answer plz.
I have Arduino UNO and MEGA.
Can it work on both boards?
I simply want to print the data of a compass to serial monitor.
I am using this shield
https://projecthub.arduino.cc/hwhardsoft/how-to-use-nmea-0183-with-arduino-49055a
I am using printNMEA example,
It is giving me this header issue no such file or director. #include <StandardCplusplus.h>
If i comment it then everything fine.
2nd, i believe this example are based on hardware serial port 3 of mega.
I tried but no data show up.
I need 4800 baud rate to receive data.
Thanks
Hi Timo,
in the example https://github.com/ttlappalainen/NMEA0183/tree/master/Examples/NMEA2000ToNMEA0183
you have:
void tN2kDataToNMEA0183::Update() {
SendRMC();
if ( LastHeadingTime+2000<millis() ) Heading=N2kDoubleNA;
if ( LastCOGSOGTime+2000<millis() ) { COG=N2kDoubleNA; SOG=N2kDoubleNA; }
if ( LastPositionTime+4000<millis() ) { Latitude=N2kDoubleNA; Longitude=N2kDoubleNA; }
}
Could you please help me to get me on the right trail, how this is doing the timing of message sending?
And why those values are set to N2kDoubleNA
?
Especially with building NMEA0183 which are built on more of one PGNs (e.g. calculating Leeway or VHW where heading is needed, not coming with ParseN2kBoatSpeed() and so on) I need those "old" values.
What do I miss out here?
Thank you in advance for spending time on this,
Ronnie
Hi everyone,
First, sorry if it's not the right place to ask !
I used this library and example to make a NMEA0183 Wifi gateway. This works perfectly as I can have values on Navionics App.
Now, I'm bulding an Android App to have personalized screen with data.
I already have sentences readed succesfully on device.
My question:
Is there a way to use this library to only parse sentences ?
Traced this to function NMEA0183SetRMC - the AddTimeField call in this function is passing format "%06f", which fails to add leading zeros to the formatted time value. Passing "%06.0f" fixed the problem.
seems i cant find the parser for depth, did it go missing somewhere, or was it never implemented?
Hi Timo
In the example NMEA0183ToN2k you are mentioning "This example reads NMEA0183 messages from one serial port. It is possible to add more serial ports for having NMEA0183 combiner functionality."
I tried to do this, but it was not successfully.
Do you have an eample for na conmbine or can you give me a hint, how the several serial ports must be initialized concerning the setup of the NMEA0183 ports and handlers.
Thanks in advance
Best Regards Uwe
Is there an example of parsing GSV with NMEA0183ParseGSV_nc()?
Typically there are many satellites in view, but the parser takes 4 tGSV args. If field 1 (total number of messages) is >4, should I simply call the parser function repeatedly with different tGSVs until it returns no more satellites?
Hi Timo, I'm using this excellent library in an ESP32 project that I've split into many separate files for easier maintenance and reuse.
https://github.com/sailingfree/n2k-gateway
If I include the NMEA2000.h in more than one source file I get multiple instances of the NMEA2000 object. I see that the NMEA2000_CAN.h selects the appropriate low level class, so its not just a case of moving the definition to the application. I've added a work-around using something like this:
#ifndef defined_NMEA2000
tNMEA2000 &NMEA2000=*(new tNMEA2000_esp32());
#define defined_NMEA2000
#else
extern tNMEA2000 &NMEA2000;
#endif
and defined that in my application files but thats clumsy.
The other thought was to #define the type in the NMEA2000_CAN.h for each board and have the application define the object. Something like
#define NMEA2000_TYPE tNMEA2000_esp32
and in the application:
tNMEA2000 &NMEA2000=*(new NMEA2000_TYPE());
That would break a ton of applications of course.
Any thoughts?
According to NMEA0183Msg.h
MAX_NMEA0183_MSG_LEN
cannot contain multi messages.
Does this mean proper handling of AIS messages are not supported yet in this library?
I'm working on multiplexing NMEA from different sources and send it out to WiFI, so currently I don't need to parse AIS, just properly know if the message is fully red.
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.