Coder Social home page Coder Social logo

njh / ruby-mqtt Goto Github PK

View Code? Open in Web Editor NEW
528.0 23.0 133.0 554 KB

Pure Ruby gem that implements the MQTT protocol, a lightweight protocol for publish/subscribe messaging.

Home Page: http://www.rubydoc.info/gems/mqtt

License: MIT License

Ruby 100.00%
ruby mqtt rubygems

ruby-mqtt's Introduction

Build Status

ruby-mqtt

Pure Ruby gem that implements the MQTT protocol, a lightweight protocol for publish/subscribe messaging.

Also includes a class for parsing and generating MQTT-SN packets.

Table of Contents

Installation

You may get the latest stable version from Rubygems:

$ gem install mqtt

Alternatively, to use a development snapshot from GitHub using Bundler, add this to your Gemfile:

    gem 'mqtt', :github => 'njh/ruby-mqtt'

Quick Start

require 'mqtt'

# Publish example
MQTT::Client.connect('test.mosquitto.org') do |c|
  c.publish('test', 'message')
end

# Subscribe example
MQTT::Client.connect('test.mosquitto.org') do |c|
  # If you pass a block to the get method, then it will loop
  c.get('test') do |topic,message|
    puts "#{topic}: #{message}"
  end
end

Library Overview

Connecting

A new client connection can be created by passing either a MQTT URI, a host and port or by passing a hash of attributes.

client = MQTT::Client.connect('mqtt://myserver.example.com')
client = MQTT::Client.connect('mqtts://user:[email protected]')
client = MQTT::Client.connect('myserver.example.com')
client = MQTT::Client.connect('myserver.example.com', 18830)
client = MQTT::Client.connect(:host => 'myserver.example.com', :port => 1883 ... )

TLS/SSL is not enabled by default, to enabled it, pass :ssl => true:

client = MQTT::Client.connect(
  :host => 'test.mosquitto.org',
  :port => 8883,
  :ssl => true
)

Alternatively you can create a new Client object and then configure it by setting attributes. This example shows setting up client certificate based authentication:

client = MQTT::Client.new
client.host = 'myserver.example.com'
client.ssl = true
client.cert_file = path_to('client.pem')
client.key_file  = path_to('client.key')
client.ca_file   = path_to('root-ca.pem')
client.connect()

The connection can either be made without the use of a block:

client = MQTT::Client.connect('test.mosquitto.org')
# perform operations
client.disconnect()

Or, if using a block, with an implicit disconnection at the end of the block.

MQTT::Client.connect('test.mosquitto.org') do |client|
  # perform operations
end

For more information, see and list of attributes for the MQTT::Client class and the MQTT::Client.connect method.

Publishing

To send a message to a topic, use the publish method:

client.publish(topic, payload, retain=false)

The method will return once the message has been sent to the MQTT server.

For more information see the MQTT::Client#publish method.

Subscribing

You can send a subscription request to the MQTT server using the subscribe method. One or more Topic Filters may be passed in:

client.subscribe( 'topic1' )
client.subscribe( 'topic1', 'topic2' )
client.subscribe( 'foo/#' )

For more information see the MQTT::Client#subscribe method.

Receiving Messages

To receive a message, use the get method. This method will block until a message is available. The topic is the name of the topic the message was sent to. The message is a string:

topic,message = client.get

Alternatively, you can give the get method a block, which will be called for every message received and loop forever:

client.get do |topic,message|
  # Block is executed for every message received
end

For more information see the MQTT::Client#get method.

Parsing and serialising of packets

The parsing and serialising of MQTT and MQTT-SN packets is a separate lower-level API. You can use it to build your own clients and servers, without using any of the rest of the code in this gem.

# Parse a string containing a binary packet into an object
packet_obj = MQTT::Packet.parse(binary_packet)
    
# Write a PUBACK packet to an IO handle
ios << MQTT::Packet::Puback(:id => 20)
    
# Write an MQTT-SN Publish packet with QoS -1 to a UDP socket
socket = UDPSocket.new
socket.connect('localhost', MQTT::SN::DEFAULT_PORT)
socket << MQTT::SN::Packet::Publish.new(
  :topic_id => 'TT',
  :topic_id_type => :short,
  :data => "The time is: #{Time.now}",
  :qos => -1
)
socket.close

Limitations

  • QoS 2 is not currently supported by client
  • Automatic re-connects to the server are not supported
  • No local persistence for packets

Resources

License

The mqtt ruby gem is licensed under the terms of the MIT license. See the file LICENSE for details.

Contact

ruby-mqtt's People

Contributors

alejanderl avatar anubisss avatar ashie avatar avellable avatar clockwerx avatar cuvoodoo avatar cwingate avatar englishm avatar evgeni avatar jamgraham avatar jasiek avatar jhiemer avatar kidlab avatar larsin avatar leoarnold avatar levent avatar lucaong avatar madale avatar michaelklishin avatar nagachika avatar njh avatar no6v avatar noefroidevaux avatar olleolleolle avatar sborsje avatar stephenwetzel avatar syoder avatar tenderlove avatar troygnichols avatar ytti avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

ruby-mqtt's Issues

Get subscription topics

I'm thinking about implementing a method to get all topics that the client subscribes to. Use case is as bellow:

  1. Client subscribes to topics [A,B,C,D]
  2. A new update comes in. Client has to subscribe to [B,C,D] ONLY.
  3. Therefore, if I can retrieve the topics that client has subscribed to before ie. [A,B,C,D], I only have to ask client to unsubscribe to [A] since I know that by [A,B,C,D] - [B,C,D] = [A]. Otherwise, I have to ask client to unsubscribe to [A,B,C,D], and then subscribe back to [B,C,D].

Any thought?

mqtt gem error No such file or directory @ rb_sysopen

I am getting this error when running ruby app.rb please find a solution for it

pi@raspberrypi:~/waterpi-web $ ruby app.rb
/home/pi/.rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/mqtt-0.3.1/lib/mqtt/client.rb:198:in initialize': No such file or directory @ rb_sysopen - /home/pi/waterpi-web/certs/home/pi/waterpi-web/certs/private.pem.key (Errno::ENOENT) from /home/pi/.rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/mqtt-0.3.1/lib/mqtt/client.rb:198:inopen'
from /home/pi/.rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/mqtt-0.3.1/lib/mqtt/client.rb:198:in key_file=' from /home/pi/.rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/mqtt-0.3.1/lib/mqtt/client.rb:168:inblock in initialize'
from /home/pi/.rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/mqtt-0.3.1/lib/mqtt/client.rb:167:in each_pair' from /home/pi/.rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/mqtt-0.3.1/lib/mqtt/client.rb:167:ininitialize'
from /home/pi/.rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/mqtt-0.3.1/lib/mqtt/client.rb:92:in new' from /home/pi/.rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/mqtt-0.3.1/lib/mqtt/client.rb:92:inconnect'
from /home/pi/waterpi-web/boot.rb:40:in ' from /home/pi/.rbenv/versions/2.2.3/lib/ruby/2.2.0/rubygems/core_ext/kernel_require.rb:54:inrequire'
from /home/pi/.rbenv/versions/2.2.3/lib/ruby/2.2.0/rubygems/core_ext/kernel_require.rb:54:in require' from app.rb:3:in

'
source code-https://github.com/demirhanaydin/waterpi-web

setting qos to 1 always sending the message

I test a sample code and set QOS to 1. Why is the server is always sending the message to the subscribe client even the subscribe client already received the message and as I search #58 this should solve the problem because in MQTT specs the subscribe client should send PUBACK after receiving the message to tag that message is received. Did I missed something? Any tips?

sub.rb

require 'rubygems'
require 'mqtt'

MQTT::Client.connect('192.168.9.105') do |client|
  client.get_packet('test'=>1) do |packet|
    puts packet.inspect
  end
end

pub.rb

require 'rubygems'
require 'mqtt'

# Publish example
MQTT::Client.connect('192.168.9.105') do |c|
  c.publish('test', 'message 3', 0, 1)
end

Dynamic Proxying of MQTT based on client auth.

Here is a challenge that's been troubling me for sometime.

Let's say we have multiple mqtt clients and multiple mqtt backends BUT only one public facing server/port.

We want to identify a user via say their client's SSL Cert or username/password, then proxy all requests for that Client Id (source port/address) to a specific broker based on a lookup table.

Would we be able to configure your tool to support this requirement?

Thank you.

Version bump.

Hi! Could you bump the version and push to rubygems? I rely on the last_ping_response time to ensure connection status. Thanks in advance :)

failed to read byte from socket MQTT::ProtocolException

I keep getting the following issue when I try to run the example code:

/Users/rolandjitsu/Projects/Pulsr/Client/vendor/bundle/gems/mqtt-0.2.0/lib/mqtt/packet.rb:271:in `read_byte': Failed to read byte from socket (MQTT::ProtocolException)
from /Users/rolandjitsu/Projects/Pulsr/Client/vendor/bundle/gems/mqtt-0.2.0/lib/mqtt/packet.rb:32:in `read'
from /Users/rolandjitsu/Projects/Pulsr/Client/vendor/bundle/gems/mqtt-0.2.0/lib/mqtt/client.rb:440:in `receive_packet'
from /Users/rolandjitsu/Projects/Pulsr/Client/vendor/bundle/gems/mqtt-0.2.0/lib/mqtt/client.rb:272:in `block in connect'

I can successfully subscribe and publish with the MQTTKit Objective-C implementation of the mosquito lib. But when I run the ruby code, I can see on my device that it's actually trying to publish something (instead of subscribing) and it fails afterwards.

All I am doing is:

ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __FILE__)
ENV['RACK_ENV'] ||= 'development'

require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
Bundler.require :default, ENV['RACK_ENV'].to_sym


# Subscribe example
MQTT::Client.connect(:remote_host => 'iot.eclipse.org', :client_id => 'FBF68BA8') do |client|

    client.get('/pulsr') do |topic, message|
        puts "#{topic}: #{message}"
    end

end

Change License

I see that this is licensed under the GPL. Since the GPL doesn't play well with commercial software, could you please relicense it under something friendlier like MIT, BSD or Apache 2? I'd be satisfied with a duel license arrangement.

I'd like to offer this gem as an option for people to use, but I can't as it stands now. I wouldn't want to inadvertently cause people to break laws because they didn't understand how the software was licensed. If it were licensed under something I could use at work, I would also feel encouraged to contribute back.

client_id bit size to short?

Why does the bit size of the :client_id have to be shorter than 23?

I'm using the C implementation and that is not limited to 23 bits, I use it on a iOS device and I use the app identifier which is of the form: FBF68BA8-BAFB-41DE-8201-CDD73B7793FF.

Can that limit be increased?

spec test errors under Ruby 2.0

Running this under Ruby 2.0.0 on Cloud Foundry talking to iot.eclipse.org I occasionally encounter:

2014-03-03T16:18:45.63+0000 [App/3]   ERR /home/vcap/app/vendor/bundle/ruby/2.0.0/gems/mqtt-0.1.0/lib/mqtt/client.rb:305:in `select': no implicit conversion of nil into IO (TypeError)
2014-03-03T16:18:45.63+0000 [App/3]   ERR   from /home/vcap/app/vendor/bundle/ruby/2.0.0/gems/mqtt-0.1.0/lib/mqtt/client.rb:305:in `receive_packet'
2014-03-03T16:18:45.63+0000 [App/3]   ERR   from /home/vcap/app/vendor/bundle/ruby/2.0.0/gems/mqtt-0.1.0/lib/mqtt/client.rb:151:in `block (2 levels) in connect'
2014-03-03T16:18:45.63+0000 [App/3]   ERR   from /home/vcap/app/vendor/bundle/ruby/2.0.0/gems/mqtt-0.1.0/lib/mqtt/client.rb:151:in `loop'
2014-03-03T16:18:45.63+0000 [App/3]   ERR   from /home/vcap/app/vendor/bundle/ruby/2.0.0/gems/mqtt-0.1.0/lib/mqtt/client.rb:151:in `block in connect'

Add username and password support

For security reasons more and more we need to authenticate the topics. Most of the clients (node, arduino) implemented this feauture that has been introduced in the latest MQTT draft. Do you guys have any intention on making this? Thanks.

Parse port number from URI

Given

MQTT::Client.new('mqtt://user:[email protected]:13858').connect

the library assumes a default port of 1883; even though I've specified something different.

Exception during check: Connection refused - connect(2) for "m10.cloudmqtt.com" port 1883 -- [
"/home/clockwerx/.rbenv/versions/2.1.1/lib/ruby/gems/2.1.0/gems/mqtt-0.2.0/lib/mqtt/client.rb:234:in `initialize'",
"/home/clockwerx/.rbenv/versions/2.1.1/lib/ruby/gems/2.1.0/gems/mqtt-0.2.0/lib/mqtt/client.rb:234:in `new'",
 "/home/clockwerx/.rbenv/versions/2.1.1/lib/ruby/gems/2.1.0/gems/mqtt-0.2.0/lib/mqtt/client.rb:234:in `connect'",
...
]

what is purpose of "message" variable ?

i'm using this code:

MQTT::Client.connect(:remote_host => 'broker.mqttdashboard.com', :remote_port => 1883) do |client| client.get_packet('#') do |topic,message| puts topic end end

output:

csquare/cubes/1/temperatureTemp: 22.46

i think
"22.46" is real message.. it mean topic variable printing topic + message .
when i tried to print 'message' variable then it output nothing
what is purpose of "message" variable ?

Support URI connections

The client does not seem to support URI connections. While there is no common MQTT URI scheme, suporting mqtt:// and tcp:// URI's would make it easier to
use this client in PaaS environments (Heroku, Cloud Foundry, OpenShift, โ€ฆ) that primarily expose credentials as URI's.

incompatible character encodings: UTF-8 and ASCII-8BIT (Encoding::CompatibilityError)

May be related to #6?

Running https://gist.github.com/andypiper/5866586 against test.mosquitto.org the script eventually crashes with:

/A/_info_/Test/PIC32/uart/2/commErrors: {  "name" : "uart/2/commErrors" ,"type" : "UINT32" ,"mode" : "R" }
/Global/Units/Unit1: {"tst":"1383862016","lat":"35.397300","_type":"location","lon":"-97.495081","acc":"165m"}
/Global/Units/Unit1/deviceToken: {"tst":"1383851685","dev":"<82087c3c d90f15e9 adf15ecc 8ec85bb6 7e84e890 b09a076d a6bbc304 6a92f429>","_type":"deviceToken"}
/Merckx/STM32F100_UART/__get/system/alive: false
/Merckx/STM32F100_UART/__meta/system/eventCounter: { type : 2, mode : 3 , count total number of events processed}
/Merckx/STM32F100_UART/__meta/system/alive: { type : 8, mode : 1 , }
list-topics.rb:8:in `block (2 levels) in <main>': incompatible character encodings: UTF-8 and ASCII-8BIT (Encoding::CompatibilityError)
    from /Users/andyp/gems/gems/mqtt-0.1.0/lib/mqtt/client.rb:242:in `block in get'
    from /Users/andyp/gems/gems/mqtt-0.1.0/lib/mqtt/client.rb:240:in `loop'
    from /Users/andyp/gems/gems/mqtt-0.1.0/lib/mqtt/client.rb:240:in `get'
    from list-topics.rb:7:in `block in <main>'
    from /Users/andyp/gems/gems/mqtt-0.1.0/lib/mqtt/client.rb:157:in `connect'
    from /Users/andyp/gems/gems/mqtt-0.1.0/lib/mqtt/client.rb:50:in `connect'
    from list-topics.rb:6:in `<main>'

Timeouts in client.get

MQTT::Client#get currently has no timeout applied to it. This is very inconvenient in certain situations. From what I see in the code, even network exceptions won't
cause it to unblock.

It's true that Ruby's Queue implementation is very limited and arguably not a great
example of engineering but libraries such as Bunny still provide
timeout functionality for similar operations (using Timeout, which surely has it's own
issues).

I'd recommend this client to support optional timeouts, too.

Coerce Client#unsubscribe argument better

When Client#unsubscribe argument is passed as an array, the server will get

"[\"c/topic\"]"]

which is incorrect. This client should coerce provided values to an array better.

Sample in README.MD is wrong

Hi,
I think the sample in the README.MD should be:

MQTT::Client.connect(endpoint) do |c|
  c.publish('test', 'message')
end

Instead of 'topic'.

Failed to read byte from socket(MQTT::ProtocolException)

hi
please @michaelklishin would can help me.
I have problems with a mqtt client but not happen with amqp client, I use a broker rabbitmq.
my stage are three pc in a network lan

This error with mqtt client

railpc@railpc-VirtualBox:/media/sf_ProjectRubyLinux/Ruby/Semillero$ ruby producer_mqtt.rb 
=> Published Message mqtt sended @ 1430402156 
/home/railpc/.rbenv/versions/2.1.1/lib/ruby/gems/2.1.0/gems/mqtt-0.3.1/lib/mqtt/packet.rb:283:in `read_byte': Failed to read byte from socket (MQTT::ProtocolException)
    from /home/railpc/.rbenv/versions/2.1.1/lib/ruby/gems/2.1.0/gems/mqtt-0.3.1/lib/mqtt/packet.rb:31:in `read'
    from /home/railpc/.rbenv/versions/2.1.1/lib/ruby/gems/2.1.0/gems/mqtt-0.3.1/lib/mqtt/client.rb:454:in `receive_packet'
    from /home/railpc/.rbenv/versions/2.1.1/lib/ruby/gems/2.1.0/gems/mqtt-0.3.1/lib/mqtt/client.rb:283:in `block in connect'

The code of mqtt client :

require "mqtt"

c = MQTT::Client.connect(
  host: '192.168.1.5', 
  port: 1883,
  username: 'guest',
  password: 'guest')

loop do
  m = "Message mqtt sended @ #{Time.now.to_i} " 
  c.publish("builds", m)
  puts "=> Published #{m}"
  sleep 0.2
end
The broker have is configuration in rabbitmq.config :
[
 {rabbit, [
        %%=====================================
        %%----Configuration of AMQP protocol----
        %% =====================================

        {tcp_listeners, [{"192.168.1.5", 5672}]},
        {default_user, <<"guest">>},
        {default_pass, <<"guest">>},
        {default_permissions, [<<".*">>, <<".*">>, <<".*">>]},
        {default_vhost, <<"/">>},
        {loopback_users, []}
 ]},
        %%-----------------------------------------------------

        %%======================================
        %%----Configuration Of MQTT protocol----
        %%======================================
 {rabbitmq_mqtt,[
        {vhost,<<"semillero">>},
        {default_user, << "guest">>},
        {default_pass, <<"guest">>},
        {exchange, <<"amp.topic">>},
        {tcp_listeners,[{"192.168.1.5", 1883}]},
        {allow_anonymous, true}

 ]}
        %%-----------------------------------------------------
].

sorry to my english.
thanks @michaelklishin ๐Ÿ‘

Failed to read byte from socket(MQTT::ProtocolException)

hi @njh
sorry to comment in other issues.

the code of mqtt client :
require "mqtt"

c = MQTT::Client.connect(
  host: '192.168.1.5', 
  port: 1883,
  username: 'guest',
  password: 'guest')

loop do
  m = "Message mqtt sended @ #{Time.now.to_i} " 
  c.publish("builds", m)
  puts "=> Published #{m}"
  sleep 0.2
end
the error is :
railpc@railpc-VirtualBox:/media/sf_ProjectRubyLinux/Ruby/Semillero$ ruby producer_mqtt.rb 
=> Published Message mqtt sended @ 1430402156 
/home/railpc/.rbenv/versions/2.1.1/lib/ruby/gems/2.1.0/gems/mqtt-0.3.1/lib/mqtt/packet.rb:283:in `read_byte': Failed to read byte from socket (MQTT::ProtocolException)
    from /home/railpc/.rbenv/versions/2.1.1/lib/ruby/gems/2.1.0/gems/mqtt-0.3.1/lib/mqtt/packet.rb:31:in `read'
    from /home/railpc/.rbenv/versions/2.1.1/lib/ruby/gems/2.1.0/gems/mqtt-0.3.1/lib/mqtt/client.rb:454:in `receive_packet'
    from /home/railpc/.rbenv/versions/2.1.1/lib/ruby/gems/2.1.0/gems/mqtt-0.3.1/lib/mqtt/client.rb:283:in `block in connect'
the broker have is configuration in rabbitmq.config :
[
 {rabbit, [
        %%=====================================
        %%----Configuration of AMQP protocol----
        %% =====================================

        {tcp_listeners, [{"192.168.1.5", 5672}]},
        {default_user, <<"guest">>},
        {default_pass, <<"guest">>},
        {default_permissions, [<<".*">>, <<".*">>, <<".*">>]},
        {default_vhost, <<"/">>},
        {loopback_users, []}
 ]},
        %%-----------------------------------------------------

        %%======================================
        %%----Configuration Of MQTT protocol----
        %%======================================
 {rabbitmq_mqtt,[
        {vhost,<<"semillero">>},
        {default_user, << "guest">>},
        {default_pass, <<"guest">>},
        {exchange, <<"amp.topic">>},
        {tcp_listeners,[{"192.168.1.5", 1883}]},
        {allow_anonymous, true}

 ]}
        %%-----------------------------------------------------
].

Thanks @njh .

Add support for client persistence

Types of storage for persistence:

  • in memory
  • flat file on disk
  • Redis
  • Relational Databases
    • SQLite
    • MySQL

I am not sure if it would be possible to use Memcache - I think a key quality would be to iterate through the persisted messages.

This is a pre-requisite for QoS 1 and 2.

MQTT 3.1 Support

Looks like there is no username/password support...planning on adding this? I see a lot of 3.0 clients, but not really useful in a multi-tenant environment...

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.