open-dis / open-dis-python Goto Github PK
View Code? Open in Web Editor NEWPython implementation of the IEEE-1278.1 Distributed Interactive Simulation (DIS) application protocol v7
License: BSD 2-Clause "Simplified" License
Python implementation of the IEEE-1278.1 Distributed Interactive Simulation (DIS) application protocol v7
License: BSD 2-Clause "Simplified" License
This is a placeholder issue for work relating to boundary padding.
To date, from a quick search in the codebase, the following classes require padding to bit boundaries (Note: not a complete list):
There is some code in VariableDatum for doing this. Ideally the process for doing so should be standardised across all of the above classes. Some helper functions would also help in this area.
This might potentially resolve #12 (since DataPdu composes VariableDatum)
I feel like I may be missing something here. I'm springboarding off the example sender like:
pdu = EntityStatePdu()
pdu.length = 144 #gotta figure this shit out
pdu.exerciseID = 250
pdu.entityID.entityID = 42
pdu.entityID.siteID = 1
pdu.entityID.applicationID = 11
pdu.marking.setString('Igor3d')
pdu.entityType.domain = 2
pdu.entityType.country = 255
pdu.entityType.entityKind = 1
pdu.forceId = 1
myLocation = [-5.482955764588357, # longitude
35.198894404096315, # latitude
10000, # altitude
0, # roll
0, # pitch
0 # yaw
]
pdu.entityLocation.x = myLocation[0]
pdu.entityLocation.y = myLocation[1]
pdu.entityLocation.z = myLocation[2]
pdu.entityOrientation.psi = myLocation[3]
pdu.entityOrientation.theta = myLocation[4]
pdu.entityOrientation.phi = myLocation[5]
memoryStream = BytesIO()
outputStream = DataOutputStream(memoryStream)
pdu.serialize(outputStream)
data = memoryStream.getvalue()
while True:
udpSocket.sendto(data, (DESTINATION_ADDRESS, UDP_PORT))
pp.pprint(vars(pdu))
pp.pprint(vars(pdu.entityID))
pp.pprint(vars(pdu.entityType))
pp.pprint(vars(pdu.entityLocation))
print("Sent {}. {} bytes to {}:{}".format(pdu.__class__.__name__, len(data), DESTINATION_ADDRESS, UDP_PORT))
time.sleep(5)
Then, when I send the data it is printed as below:
{ 'alternativeEntityType': <opendis.dis7.EntityType object at 0x7f0a18fb81c0>,
'capabilities': 0,
'deadReckoningParameters': <opendis.dis7.DeadReckoningParameters object at 0x7f0a18fb8bb0>,
'entityAppearance': 0,
'entityID': <opendis.dis7.EntityID object at 0x7f0a18fb83d0>,
'entityLinearVelocity': <opendis.dis7.Vector3Float object at 0x7f0a18fb85e0>,
'entityLocation': <opendis.dis7.Vector3Double object at 0x7f0a18fb86a0>,
'entityOrientation': <opendis.dis7.EulerAngles object at 0x7f0a18fb8ca0>,
'entityType': <opendis.dis7.EntityType object at 0x7f0a18fb8cd0>,
'exerciseID': 250,
'forceId': 1,
'length': 144,
'marking': <opendis.dis7.EntityMarking object at 0x7f0a1869f4c0>,
'numberOfVariableParameters': 0,
'padding': 0,
'pduStatus': 0,
'pduType': 1,
'protocolFamily': 1,
'protocolVersion': 7,
'timestamp': 0,
'variableParameters': []}
{'applicationID': 11, 'entityID': 42, 'siteID': 1}
{ 'category': 0,
'country': 255,
'domain': 2,
'entityKind': 1,
'extra': 0,
'specific': 0,
'subcategory': 0}
{'x': -5.482955764588357, 'y': 35.198894404096315, 'z': 10000}
Looks great so far. HOWEVER, when I check the packet in Wireshark I get:
"dis": {
"Header": {
"dis.proto_ver": "7",
"dis.exer_id": "250",
"dis.pdu_type": "1",
"dis.proto_fam": "1",
"dis.timestamp": "0.000000000",
"dis.pdu_length": "144",
"dis.pdu_status": "0x00000000",
"dis.pdu_status_tree": {
"dis.pdustatus.cei": "0x00000000",
"dis.pdustatus.lvc": "0x00000000",
"dis.pdustatus.tei": "0x00000000"
},
"dis.padding": "00"
},
"Entity State PDU": {
"Entity ID": {
"dis.entity_id_site": "1",
"dis.entity_id_application": "11",
"dis.entity_id_entity": "42"
},
"dis.force_id": "1",
"dis.num_articulation_params": "0",
"Entity Type, (0:0:0:0:144:1:2) ": {
"dis.entityKind": "0",
"dis.entityDomain": "0",
"dis.country": "0",
"dis.category": "0",
"dis.subcategory": "144",
"dis.specific": "1",
"dis.extra": "2"
},
"Alternative Entity Type, (0:255:0:0:0:0:0) ": {
"dis.entityKind": "0",
"dis.entityDomain": "255",
"dis.country": "0",
"dis.category": "0",
"dis.subcategory": "0",
"dis.specific": "0",
"dis.extra": "0"
},
"Entity Linear Velocity": {
"dis.entity_linear_velocity.x": "0",
"dis.entity_linear_velocity.y": "0",
"dis.entity_linear_velocity.z": "0"
},
"Entity Location": {
"dis.entity_location.x": "2.42946900029516e-319",
"dis.entity_location.y": "-3.23371005475282e+224",
"dis.entity_location.z": "-4.9118429119158e-186"
},
"Entity Orientation": {
"dis.entity_orientation.psi": "-3.85186e-34",
"dis.entity_orientation.theta": "0",
"dis.entity_orientation.phi": "0"
},
"dis.appearance": "0x00000000",
"Dead Reckoning Parameters": {
"dis.entity_marking_character_set": "0",
"dis.dead_reckoning_other_parameters": "00:00:00:00:00:00:00:00:00:00:00:00:00:00:00",
"Entity Linear Acceleration": {
"dis.entity_linear_acceleration.x": "0",
"dis.entity_linear_acceleration.y": "0",
"dis.entity_linear_acceleration.z": "0"
},
"Entity Angular Velocity": {
"dis.entity_angular_velocity.x": "0",
"dis.entity_angular_velocity.y": "0",
"dis.entity_angular_velocity.z": "0"
}
},
"Entity Marking": {
"dis.entity_marking_character_set": "0"
},
"dis.capabilities": "1677721600"
As you can see, the entityType and location info is ignored or changed.
Any help in understanding what I'm doing wrong would be greatly appreciated.
Thank you
There is currently an EntityID class:
open-dis-python/opendis/dis7.py
Lines 3477 to 3490 in 3a52c4f
But there is also an EntityIdentifier class:
open-dis-python/opendis/dis7.py
Lines 3421 to 3431 in 3a52c4f
There are also other Identifier classes:
EntityID seems to stick out to me here as being named differently from the rest, yet it is used throughout dis7.py.
Section 4.2.5.2 refers to object identifiers as "the designators assigned to uniquely identify objects such as entities, aggregates, minefields, environmental processes, groups of entities and points, and linear and areal objects."
Section 4.2.5.3 refers to other identifiers as being "used to designate other data items that are not objects."
For closer coherence with the spec and documentation, and to lay some groundwork for future features, I propose the following changes.
To reduce dissonance between the codebase and the documentation, I propose to use EntityIdentifier as the standard identifier class for entities. All object identifiers should also have their init parameters take a siteNo, appNo, and refNo (instead of a SimulationAddress and a refNo).
To make instance/subclass checking (using isinstance()
and issubclass()
) possible, I propose to have the following classes inherit from an ObjectIdentifier superclass:
This would be useful for future work, since ObjectIdentifiers are supposed to be unique even across object types. It would enable tracking of instantiated ObjectIdentifier subclasses through the superclass, if such a feature is desired.
These identifiers, due to their varied nature and interface, should remain independent classes that do not subclass ObjectIdentifier. They will nonetheless still retain the -Identifier suffix.
Many attributes in dis7.py are named/suffixed as ID
, yet do not hold an instance of one of the above classes (e.g. actionID in ActionRequestPdu is an enum struct). Separating the object identifiers (4.2.5.2) and other identifiers (4.2.5.3) more distinctly from the other classes will make reference lookup easier and help with disambiguation.
I suggest moving them to an identifier.py file, since none of the values rely on other classes in dis7.py, aside from SystemIdentifier requiring an instance of ChangeOptions which no other class relies on. The classes can be explicitly imported into dis7.py by name, so no other changes to dis7.py are needed.
I'm willing to make a pull request incorporating these changes.
In the dis_sender.py
, the emitted ESPDU has its length field set to zero.
The code in that example doesn't manipulate the length field.
This matters if you're using, say, open-dis java's PduFactory#getPdusFromBundle()`, which relies on this field's value.
Hi!
It looks like the SignalPdu is not packing data correctly (found at https://github.com/open-dis/open-dis-python/blob/master/opendis/dis7.py#L7070).
In my understanding, the data field of the SignalPdu is supposed to be an array of bytes more or less, based on the code in the init and parse methods. However, the serialize method for the data field implies that each object in the list should have a "serialize" method, which a standard byte would not.
My recommended change would be to change https://github.com/open-dis/open-dis-python/blob/master/opendis/dis7.py#L7107
from
anObj.serialize(outputStream)
to
outputStream.write_byte(anObj)
The variable datum object only serialises the actual data, but recieved pdus will be parsed as though padding has been sent as well.
This results in the variable datum object ignoring several of the bits it recieves, and attempting to read more from
the input stream than has been sent.
(code from line 3579 of dis7.py, my comments start with #########)
def serialize(self, outputStream):
"""serialize the class """
outputStream.write_unsigned_int(self.variableDatumID);
outputStream.write_unsigned_int(self.variableDatumLength);
for x in range(self.variableDatumLength // 8): # length is in bits
outputStream.write_byte(self.variableData[x])
######### no padding is sent, only data from variableData within the size of variableDatumLength field
def parse(self, inputStream):
""""Parse a message. This may recursively call embedded objects."""
self.variableDatumID = inputStream.read_unsigned_int();
self.variableDatumLength = inputStream.read_unsigned_int();
for x in range(self.variableDatumLength // 8): # length is in bits
self.variableData.append(inputStream.read_byte());
######## the parse function expects padding
# Skip over padding
# "This field shall be padded at the end to make the length a multiple of 64-bits."
for x in range(self.datumPaddingSizeInBits() // 8):
inputStream.read_byte()
The serialise function should send a number of zeros corresponding to the padding size of the datum.
This isssue could be fixed by adding the following code to the end of the serialize function:
for x in range(self.datumPaddingSizeInBits() // 8):
inputStream.write_byte(0)
open-dis-python/opendis/dis7.py
Lines 5350 to 5359 in b640fa3
serialize
function implementation is incorrectly attempting to call outputStream.read_unsigned_byte
and outputStream.read_unsigned_short
, which do not exist, and should be writes.
The read calls need to be replaced with the correct write calls: outputStream.write_unsigned_byte
and outputStream.write_unsigned_short
This is still an issue it seems. The variable parameters de-serialization is unimplemented and throws the "NameError: name 'null' is not defined" error.
Originally posted by @jmfGE in #1 (comment)
IEEE1278.1-2012 has a section (6.1.8) on fixed and variable parameters defined prior to an exercise in an exercise agreement.
Some of these parameters affect the packing, transmission, and receipt of PDUs, e.g. tolerances of ESPDUs, MAX_PDU_SIZE_BITS/OCTETS.
I'm putting a placeholder issue here for when we eventually get round to setting up config, as a reminder to refer to this section for option names.
VariableDatum
objects throw struct.error
when you try to store negative signed integers (e.g. -1
=b'\xff'
) in the variableData
field, then serialize.
This is the minimum viable product to reproduce the error
from opendis.dis7 import VariableDatum
from opendis.DataOutputStream import DataOutputStream
from io import BytesIO
import struct
variable_datum = VariableDatum()
data = list((b'\x00'*4)+struct.pack('>i',-1))
variable_datum.variableData = data
variable_datum.variableDatumLength = len(data)*8
memoryStream = BytesIO()
outputStream = DataOutputStream(memoryStream)
variable_datum.serialize(outputStream)
print(memoryStream.getvalue())
I expect variable_datum
to serialize without an error, and for the final print
to display the serialized object (should be b'\x00\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00\xff\xff\xff\xff'
)
The script throws a struct.error
when variable_datum
tries to serialize.
This is the full error that is thrown when variable_datum
tries to serialize.
Traceback (most recent call last):
File "test.py", line 12, in <module>
variable_datum.serialize(outputStream)
File "C:\Users\e389403\AppData\Local\Programs\Python\Python37-32\lib\site-packages\opendis\dis7.py", line 3586, in serialize
outputStream.write_byte(self.variableData[x])
File "C:\Users\e389403\AppData\Local\Programs\Python\Python37-32\lib\site-packages\opendis\DataOutputStream.py", line 17, in write_byte
self.stream.write(struct.pack('b', val))
struct.error: byte format requires -128 <= number <= 127
From what I can gather, when serialize
is called:
serialize()
is calledvariableData
bytes
object, it calls outputStream.write_byte()
)b'\xff'
s, it tries to write 255
as a signed byte to outputStream
.struct.pack('b', 255)
which results in an error, rightly so because it's out of range(0, 256)
.When I changed dis7.py
line 3586 to outputStream.write_unsigned_byte(self.variableData[x])
, then I got the expected behavior.
Signed integers actually parse without error. This makes sense, since inputStream.read_byte()
is used to parse the bytes when forming the variableData
list.
from opendis.dis7 import VariableDatum
from opendis.DataInputStream import DataInputStream
from io import BytesIO
memoryStream = BytesIO(b'\x00\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00\xff\xff\xff\xff')
inputStream = DataInputStream(memoryStream)
new_variable_datum = VariableDatum()
new_variable_datum.parse(inputStream)
print(new_variable_datum.variableData) # [0, 0, 0, 0, -1, -1, -1, -1]
According to the DIS Data Dictionary on Entity State PDUs, the Entity Marking Record should be 12 bytes (96 bits) long.
In dis7.py line 1351, in the serialize
method of the EntityMarking
class, the for loop only loops through 11 list elements:
def serialize(self, outputStream):
"""serialize the class """
outputStream.write_unsigned_byte(self.characterSet);
for idx in range(0, 11):
outputStream.write_byte( self.characters[ idx ] );
range(0, 11)
only lets the maximum value for idx
be 10
, which means it only loops through 11 bytes, not 12 (which is the required amount by the DIS standard). The same problem exists in the parse
method of the EntityMarking
class, where it only reads in 11 bytes. This causes a loss of information when receiving ESPDUs.
Furthermore, in the EntityMarking
class, the initial value for the self.characters
list only contains 11 elements.
In 'dis7.py' there is a minor typo on lines 6914, 6928 and 6939 relating to the 'receivedPower', it was accidentally written as 'receivedPoser'.
For each EmmisionSystemRecord in the ElectronicEmmisionsPdu the beam handling assumes 1 beam always, so if there are no beams you will get a failed parse and if there are multiple beams you will only get the first
I will submit a pull request with a fix when I have it.
I had issues properly reading data from a signal pdu, and tracked it down to code in the SignalPDu and VariableDatum classes.
Specifically, the write_byte method defaults to a signed byte. I was able to successfully use the library after changing instances of "write_byte" to "write_unsigned_byte" and "read_byte" to "read_byte_unsinged".
Specifically on lines:
3605,3609,3618,3623,7144,7160
I also modified line 7141 to "write_unsigned_short" and line 7157 to "read_unsigned_short"
The unsigned shorts were specifically required to get the proper value for larger data lengths.
I suspect all bytes should be unsigned, but have only tested those required for the signal pud.
Python doesn't recognize the null()
function call.
Line 129 of dis7.py is 1st example.
for idx in range(0, self.recordLength):
element = null()
element.parse(inputStream)
self.iffData.append(element)
Is this supposed to be None?
Are there any plans to make this a package installable via pip with versioned releases to help reliant projects pickup new fixes?
I went into detail on this issue on my StackOverflow question regarding the topic, which can be found here. At the time of this issue report, it has not been answered.
Any idea why the examples would be behaving this way? Is there some change that could be made to prevent them from happening in the future?
I am attempting to get the open-dis python package running correctly on my machine. I am running Python 3.8.
Using pip, I compile the package from it's source as so:
pip install .
After that, as instructed in the documentation. I run
python dis_receiver.py
I am immediately met with this error:
Created UDP socket 3001 Traceback (most recent call last): File "dis_receiver.py", line 40, in <module> recv() File "dis_receiver.py", line 27, in recv data = udpSocket.recv(1024) # buffer size in bytes socket.timeout: timed out
I don't really understand why this is happening given that I have changed absolutely nothing about the documented example process. Any idea why this would be happening?
I previously was using the java variant of opendis, and it came with a helper function for converting heading/roll/pitch to euler angles for transmission.
I'm using the python version now, and I wish that it had a helper function.
The SimulationManagementFamilyPdu
class defines two fields:
originatingEntityIdD
receivingEntityID
The following subclasses of SimulationManagementFamilyPdu
AcknowledgePdu
ActionResponsePdu
ActionRequestPdu
CreateEntityPdu
RemoveEntityPdu
StartResumePdu
StopFreezePdu
also define two redundant fields:
originatingID
receivingID
This means that the Originating ID and Receiving ID fields are serialized and parsed twice for these 7 classes. This results in serialization errors when parsing PDUs from external systems, which only serialize these fields once per PDU.
Notes:
The following classes do not define the redundant fields, and inherit from SimulationManagementFamilyPdu
without issue (in this regard):
CommentPdu
DataPdu
DataQueryPdu
EventReportPdu
SetDataPdu
Created UDP socket 62040
Traceback (most recent call last):
File ".\dis_receiver.py", line 41, in
recv()
File ".\dis_receiver.py", line 30, in recv
aPdu = createPdu(data)
File "C:\Users\berliaj1\Anaconda3\lib\site-packages\opendis\PduFactory.py", line 92, in createPdu
return getPdu(inputStream)
File "C:\Users\berliaj1\Anaconda3\lib\site-packages\opendis\PduFactory.py", line 75, in getPdu
pdu.parse(inputStream)
File "C:\Users\berliaj1\Anaconda3\lib\site-packages\opendis\dis7.py", line 5128, in parse
self.marking.parse(inputStream)
File "C:\Users\berliaj1\Anaconda3\lib\site-packages\opendis\dis7.py", line 1356, in parse
val = inputStream.read_byte()
File "C:\Users\berliaj1\Anaconda3\lib\site-packages\opendis\DataInputStream.py", line 17, in read_byte
return struct.unpack('b', self.stream.read(1))[0]
struct.error: unpack requires a buffer of 1 bytes
Hi,
Noticed in the code that the null() call doesn't seem to be implemented anywhere. It looks like it should return an instance of some kind of entity class? Maybe a RecordSpecificationElement from poking through the code?
I'm specifically seeing this when parsing an EntityStatePdu packet that apparently has some variable parameters, hitting this block in EntityStatePdu.parse(...):
for idx in range(0, self.numberOfVariableParameters):
element = null()
element.parse(inputStream)
self.variableParameters.append(element)
Is this intentional -- are we supposed to somehow provide a default implementation of null or is this a missing function?
I can get around this by commenting out the lines, which works fine for me as I don't need the variable parameters for what I'm doing. Thought I'd like you know someone out there ran into it.
Thanks!
Missing parenthesis on read_* function calls
Use read_([0-9a-z_]+)(?=\r\n)
regex to find (with windows line endings)
Lines found:
I am struggling to find an example of how to use a DataPdu. The nature of python makes it difficult to read your code and decipher how to create a simple DataPdu to send a simple integer as the variable length data.
This may be because DataPdu
is unfinished. Could you
DataPdu
with a variable length record to send an integer as the payloadThis commit broke the parsing of the CommentPdu
. Here is the trace:
...
File "/usr/local/lib/python3.6/dist-packages/distributed_interactive_simulation-1.0-py3.6.egg/distributed_interactive_simulation/PduFactory.py", line 91, in createPdu
return getPdu(inputStream)
File "/usr/local/lib/python3.6/dist-packages/distributed_interactive_simulation-1.0-py3.6.egg/distributed_interactive_simulation/PduFactory.py", line 74, in getPdu
pdu.parse(inputStream)
File "/usr/local/lib/python3.6/dist-packages/distributed_interactive_simulation-1.0-py3.6.egg/distributed_interactive_simulation/dis7.py", line 6446, in parse
self.variableDatums.append(getPdu(inputStream))
NameError: name 'getPdu' is not defined
This change to open-dis-python will enable connectivity between open-dis-javascript (node-disnetworkclient) and open-dis-python samples (previously, p was 0).
I have senders and receivers working in both languages, interoperable? Perhaps.
diff --git a/opendis/RangeCoordinates.py b/opendis/RangeCoordinates.py
index f28b4f2..5e24188 100644
--- a/opendis/RangeCoordinates.py
+++ b/opendis/RangeCoordinates.py
@@ -166,7 +166,7 @@ class GPS:
#Initialize the variables to calculate lat and alt
alt = 0
N = self.wgs84.a
- p = sqrt(x**2 + y**2)
+ p = sqrt(x**2 + y**2) + 0.001
lat = 0
previousLat = 90
#Iterate until tolerance is reached
Fixes this error:
coderextreme@coderextreme-Kubuntu20:~/open-dis-python$ python3 examples/dis_receiver.py
Listening for DIS on UDP socket 1194
Traceback (most recent call last):
File "examples/dis_receiver.py", line 39, in <module>
recv()
File "examples/dis_receiver.py", line 32, in recv
lla = gps.ecef2lla(loc)
File "/home/coderextreme/open-dis-python/opendis/RangeCoordinates.py", line 176, in ecef2lla
lat = atan((z + self.wgs84.e**2 * N * sinLat) / p)
ZeroDivisionError: float division by zero
There is some good feedback in this SO question about the example sender and receiver.
https://stackoverflow.com/questions/63237056/python-open-dis-examples-not-running-as-expected
Hi, I am having trouble running the installation due to indentation errors with the class comments in pythonDis.py. Am I missing a step?
Thanks,
Zach
Trying to insert variableParameters into my DIS packets for VBS3
Ive managed to create packets and have the Entity display over a network into VBS, this works very well, however Im now trying to get articulation working via variableParameters.
no matter what I set nothing shows up in wireshark.
Has anyone managed to get this to work yet?
many thanks
Spriggsy
awesome library guys and i am enjoying the flexibility that you have offered in higher level programming languages in python/chsarp.
is there any performance metrics between using python vs using c++ for this tool? i am currently developing a poc dis packet converter for testing purposes using this tool (which has been excellent.). my concern is throughput and latency - i will be testing the latency down the line but i was enquiring about if you have any known data/ advice about best methods for focusing on low latency tool when using this? - for development process i would rather stick with python/csharp opposed to c++ if i could but if there is a large impact then i may have to go down that route.
thanks guys
The README.md suggests that after running the dis_sender.py the user should press Ctrl + \ to stop the process. But when I run the script it sends a single Entity State PDU and stops. The code in the dis_sender.py supports this and doesn't contain a loop.
Please do something like:
udpSocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
in examples/dis_receiver.py
to allow for multiple receiver's on same host/port.
I don't know if socket.SO_REUSEPORT is an option or not. You may choose to use that instead.
Enjoy getting multicasting working!
In the PDF specification, the two terms are used interchangeably. However, when programming, it is helpful to establish a consistent naming convention so that programmers do not need to keep looking up the attribute names.
There are currently two ways of naming values that represent the count/number of records or values:
open-dis-python/opendis/dis7.py
Lines 48 to 49 in 9312cb2
vs
open-dis-python/opendis/dis7.py
Line 537 in 9312cb2
fixedDatumIDs
and fixedDatumIDCount
will left-align for easier visual identification when browsing code)This is admittedly a minor issue and might not affect many users, though it's clear where my biases lie. Currently it's a minor speedbump for my work on the repo, and I can wait for a resolution on (a) whether consistent naming would be beneficial, (b) which naming convention to go with.
Outgoing Entity PDU packets are showing up as malformed in wireshark. To test I captured a known good Entity packet from VT-Mak and saved it as a binary file. Then I simply load it, de-serialize, serialize, and rebroadcast.
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.