Code Monkey home page Code Monkey logo

loragw-setup's Introduction

Lora Gateway base setup for SX1301 based concentrators

This setup is used for some LoraWAN concentrators based on small computers such as Raspberry PI or others. For example it works fine with the RAK831 PI Zero shield

RAK831 Shield

And for the iC880a sield for Raspberry PI V2 or V3.

iC880a Shield

Installation

Download Raspbian lite image and flash it to your SD card using etcher.

Prepare SD to your environement

Once flashed, you need to do some changes on boot partition (windows users, remove and then replug SD card)

Enable SSH

Create a dummy ssh file on this partition. By default SSH is now disabled so this is required to enable it. Windows users, make sure your file doesn't have an extension like .txt etc.

Enable USB OTG (Pi Zero Only)

If you need to be able to use OTG (Console access for any computer by conecting the PI to computer USB port) Open up the file cmdline.txt. Be careful with this file, it is very picky with its formatting! Each parameter is seperated by a single space (it does not use newlines). Insert modules-load=dwc2,g_ether after rootwait quiet.

The new file cmdline.txt should looks like this

dwc_otg.lpm_enable=0 console=serial0,115200 console=tty1 root=PARTUUID=37665771-02 rootfstype=ext4 elevator=deadline fsck.repair=yes rootwait quiet modules-load=dwc2,g_ether init=/usr/lib/raspi-config/init_resize.sh

For OTG, add also the bottom of the config.txt file, on a new line

dtoverlay=dwc2

Optionnal, disable Auto Resize of SD Card

And since I don't like the Auto Resize SD function (I prefer do do it manually from raspi-config), remove also from the file cmdline.txt auto resize by deleting the following

init=/usr/lib/raspi-config/init_resize.sh

The new file cmdline.txt should looks like this

dwc_otg.lpm_enable=0 console=serial0,115200 console=tty1 root=PARTUUID=37665771-02 rootfstype=ext4 elevator=deadline fsck.repair=yes rootwait quiet modules-load=dwc2,g_ether

Pre Connect to your WiFi AP

Finally, on same partition (boot), to allow your PI to connect to your WiFi after first boot, create a file named wpa_supplicant.conf to allow the PI to be connected on your WiFi network.

country=FR
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1
network={
  ssid="YOUR-WIFI-SSID"
  psk="YOUR-WIFI-PASSWORD"
}

Of course change country, ssid and psk with your own WiFi settings.

That's it, eject the SD card from your computer, put it in your Raspberry Pi Zero . It will take up to 90s to boot up (shorter on subsequent boots). You then can SSH into it using raspberrypi.local as the address. If WiFi does not work, connect it via USB to your computer It should then appear as a USB Ethernet device.

Now connect to raspberry PI with ssh or via USB otg

Remember default login/paswword (ssh or serial console) is pi/raspberry.

So please for security reasons, you should change this default password

passwd 

Launch PI configuration script

This 1_Pi_Config.sh script will prepare your Pi environnment, create and configure a loragw user, add access to SPI, I2C, Uart. It will reduce video memory to 16MB to allow max memory for the stuff need to be done. It also enable excellent log2ram SD card preservation.

wget https://raw.githubusercontent.com/ch2i/LoraGW-Setup/master/1_Pi_Config.sh
chmod ug+x 1_Pi_Config.sh
sudo ./1_Pi_Config.sh

Reconnect after reboot

Log back with loragw user and if you changed hostname to loragw-xxyy, use this command

Get CH2i Gateway Install repository

git clone https://github.com/ch2i/LoraGW-Setup
cd LoraGW-Setup

Configure Gateway on TTN console

Now you need to register your new GW on TTN before next step, see gateway registration, the GW_ID and GW_KEY will be asked by the script

Then launch script to install all stuff

sudo ./2_Setup.sh

That's it, If you are using PI Zero shield, the 2 LED should be blinking green and you should be able to see your brand new gateway on TTN

Usefull information

Startup

Check all is fine also at startup, reboot your gateway.

sudo reboot

LED Blinking colors (RAK831 Shied with 2 WS2812B Leds)

WS2812B driver use DMA channel, and with new Raspbian version, using DMA 5 will corrupt your SD card. see this issue. It's now solved but if you have old GW with old scripts, be sure to update the line of script /opt/loragw/monitor_ws2812.py from

strip = Adafruit_NeoPixel(2, gpio_led, 800000, 5, False, 64, 0, ws.WS2811_STRIP_GRB)

to (using DMA channel 10 instead of 5)

strip = Adafruit_NeoPixel(2, gpio_led, 800000, 10, False, 64, 0, ws.WS2811_STRIP_GRB)

LED 1

  • green => connected to Internet
  • blue => No Internet connexion but gateway WiFi AP is up
  • red => No Internet, no WiFi Access Point

LED 2

  • green => packet forwarder is started and running
  • blue => no packed forwarder but local LoRaWAN server is started
  • red => No packet forwarder nor LoRaWAN server

LED Blinking colors (iC880a with 4 GPIO Leds)

- GPIO 4  (Blue) Blink => Internet access OK
- GPIO 18 (Yellow) Blink => local web server up & running
- GPIO 24 (Green)
	- Blink => packet forwarder is running
	- Fixed => Shutdown OK, can remove power
- GPIO 23 (Red) 
	- Blink every second, one of the previous service down (local web, internet, )
  - Middle bink on every bad LoRaWAN packet received
  - Lot of short blink => Activity on SD Card (seen a boot for example)

Change behaviour

You can change LED code behaviour at the end of script /opt/loragw/monitor.py

Shutdown

You can press (and let it pressed) the switch push button, leds well become RED and after 2s start blinking in blue. If you release button when they blink blue, the Pi will initiate a shutdown. So let it 30s before removing power.

Shutdown LED display (for RPI 3 and iC880a only)

If you have a raspberry PI 3 with this iC880A shield, then the /boot/config.txt file has been enhanced with the following lines:

# When system if Halted/OFF Light Green LED
dtoverlay=gpio-poweroff,gpiopin=24

The Green LED (gpio24) will stay on when you can remove the power of the gateway. It's really a great indicator.

You can also select which GPIO LED is used to replace activity LED if you need it.

# Activity LED
dtoverlay=pi3-act-led,gpio=23

The Red LED (gpio23) will blink on activity.

Shutdown or Activity LED (for RPI Zero Shield V1.5+)

If you have a raspberry PI Zero with this RAK831, then you can change the /boot/config.txt file to choose one of the two following features:

# When system is Halted/OFF Light Green LED
dtoverlay=gpio-poweroff,gpiopin=26

The Green LED (gpio26) will stay on when you can remove the power of the gateway. It's really a great indicator.

You can also choose select this LED to replace activity LED if you need it.

# Activity LED
dtparam=act_led_gpio=26

The Greend LED (gpio26) will blink on activity.

You can select only one of the 2 options, not both at the same time.

Detailled information

The installed sofware is located on /opt/loragw, I changed this name (original was ttn-gateway) just because not all my gateways are connected to TTN so I wanted to have a more generic setup.

ls -al /opt/loragw/
total 344
drwxr-xr-x 3 root root   4096 Jan 21 03:15 .
drwxr-xr-x 5 root root   4096 Jan 21 01:01 ..
drwxr-xr-x 9 root root   4096 Jan 21 01:03 dev
-rw-r--r-- 1 root root   6568 Jan 21 01:15 global_conf.json
-rwxr-xr-- 1 root root   3974 Jan 21 01:15 monitor-gpio.py
-rwxr-xr-- 1 root root   3508 Jan 21 03:15 monitor.py
-rwxr-xr-- 1 root root   4327 Jan 21 01:15 monitor-ws2812.py
-rwxr-xr-x 1 root root 307680 Jan 21 01:14 mp_pkt_fwd
-rwxr-xr-- 1 root root    642 Jan 21 01:36 start.sh

LED blinking and push button functions are done with the monitor.py service (launched by systemd at startup). There are 2 versions of this service (with symlink), one with WS2812B led and another for classic GPIO LED such as the one on this IC880A shield. So if you want to change you can do it like that

stop the service

sudo systemctl stop monitor

If you have ic880a shield, change monitor service

In this case you do not have WS2812B RGB LED on the shield, but GPIO classic one. The push button GPIO to power off the PI is also not on the same GPIO, so you need to setup the correct monitor service.

sudo rm /opt/loragw/monitor.py
sudo ln -s /opt/loragw/monitor-gpio.py /opt/loragw/monitor.py

start the service

sudo systemctl start monitor

Check packed forwarder log

sudo journalctl -f -u loragw
-- Logs begin at Sun 2018-01-21 14:57:08 CET. --
Jan 22 01:00:41 loragw loragw[240]: ### GPS IS DISABLED!
Jan 22 01:00:41 loragw loragw[240]: ### [PERFORMANCE] ###
Jan 22 01:00:41 loragw loragw[240]: # Upstream radio packet quality: 100.00%.
Jan 22 01:00:41 loragw loragw[240]: # Semtech status report send.
Jan 22 01:00:41 loragw loragw[240]: ##### END #####
Jan 22 01:00:41 loragw loragw[240]: 01:00:41  INFO: [TTN] bridge.eu.thethings.network RTT 52
Jan 22 01:00:41 loragw loragw[240]: 01:00:41  INFO: [TTN] send status success for bridge.eu.thethings.network
Jan 22 01:00:53 loragw loragw[240]: 01:00:53  INFO: Disabling GPS mode for concentrator's counter...
Jan 22 01:00:53 loragw loragw[240]: 01:00:53  INFO: host/sx1301 time offset=(1516578208s:159048µs) - drift=-55µs
Jan 22 01:00:53 loragw loragw[240]: 01:00:53  INFO: Enabling GPS mode for concentrator's counter.
Jan 22 01:01:11 loragw loragw[240]: ##### 2018-01-22 00:01:11 GMT #####
Jan 22 01:01:11 loragw loragw[240]: ### [UPSTREAM] ###
Jan 22 01:01:11 loragw loragw[240]: # RF packets received by concentrator: 0
Jan 22 01:01:11 loragw loragw[240]: # CRC_OK: 0.00%, CRC_FAIL: 0.00%, NO_CRC: 0.00%
Jan 22 01:01:11 loragw loragw[240]: # RF packets forwarded: 0 (0 bytes)
Jan 22 01:01:11 loragw loragw[240]: # PUSH_DATA datagrams sent: 0 (0 bytes)
Jan 22 01:01:11 loragw loragw[240]: # PUSH_DATA acknowledged: 0.00%
Jan 22 01:01:11 loragw loragw[240]: ### [DOWNSTREAM] ###
Jan 22 01:01:11 loragw loragw[240]: # PULL_DATA sent: 0 (0.00% acknowledged)
Jan 22 01:01:11 loragw loragw[240]: # PULL_RESP(onse) datagrams received: 0 (0 bytes)
Jan 22 01:01:11 loragw loragw[240]: # RF packets sent to concentrator: 0 (0 bytes)
Jan 22 01:01:11 loragw loragw[240]: # TX errors: 0
Jan 22 01:01:11 loragw loragw[240]: ### BEACON IS DISABLED!
Jan 22 01:01:11 loragw loragw[240]: ### [JIT] ###
Jan 22 01:01:11 loragw loragw[240]: # INFO: JIT queue contains 0 packets.
Jan 22 01:01:11 loragw loragw[240]: # INFO: JIT queue contains 0 beacons.
Jan 22 01:01:11 loragw loragw[240]: ### GPS IS DISABLED!
Jan 22 01:01:11 loragw loragw[240]: ### [PERFORMANCE] ###
Jan 22 01:01:11 loragw loragw[240]: # Upstream radio packet quality: 0.00%.
Jan 22 01:01:11 loragw loragw[240]: # Semtech status report send.
Jan 22 01:01:11 loragw loragw[240]: ##### END #####
Jan 22 01:01:11 loragw loragw[240]: 01:01:11  INFO: [TTN] bridge.eu.thethings.network RTT 53
Jan 22 01:01:11 loragw loragw[240]: 01:01:11  INFO: [TTN] send status success for bridge.eu.thethings.network

Use legacy Packet Forwarder (not needed)

First build it

./build_legacy.sh

If you want to use the legacy packet forwarder, you'll need to change file /opt/loragw/start.sh to replace the last line

./mp_pkt_fwd.sh

by

./poly_pkt_fwd.sh
sudo systemctl stop loragw
sudo systemctl start loragw

Adjust log2ram

if you chose log2ram to reduce SD card write, you need to change some log file rotation to avoid RAM Disk to be full.

For this you need to edit each file in /etc/logrotate.d/, and on each file:

  • remove line(s) containing delaycompress (this avoid uncompressed old log)
  • change line(s) containing rotate n by rotate 12 (this this the max log file history)
  • change line(s) containing daily by hourly (rotate log each hour)
  • change line(s) containing monthly by daily (rotate log each day)

In this case we got last 12H with 1 file per hour. Of course, you can adjust these paramaters to feet you need, it's just an example,

file /etc/logrotate.d/rsyslog

/var/log/syslog
{
        rotate 12
        hourly
        missingok
        notifempty
        compress
        postrotate
        invoke-rc.d rsyslog rotate > /dev/null
        endscript
}

/var/log/mail.info
/var/log/mail.warn
/var/log/mail.err
/var/log/mail.log
/var/log/daemon.log
/var/log/kern.log
/var/log/auth.log
/var/log/user.log
/var/log/lpr.log
/var/log/cron.log
/var/log/debug
/var/log/messages
{
        rotate 12
        hourly
        missingok
        notifempty
        compress
        sharedscripts
        postrotate
        invoke-rc.d rsyslog rotate > /dev/null
        endscript
}

And here is the final result

Click on image to see the video

CH2i RAK831 GW

Add some other features

Here are other feature I use sometime on my gateways:

loragw-setup's People

Contributors

hallard 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

Watchers

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

loragw-setup's Issues

OLED Display on 1.5

Hello,
Any special thing needs to be done if I wan't to connect OLED in shield 1.5 ?
I have connected OLED to I2C-3 ... it smoked when I plug the RPi in :D ( 5V via USB power )

monitor_gpio.py outcommented line 138 wrong

Hello,
in Python script monitor_gpio.py line 138 is commented out. With this selection the function to shutdown GW with the push-button does not work in my case. If I remove the comment character # it works. Please can you check.

setup_config.py bug

Running setup_config.py fails as router is None.

I fixed by changing line 96 from:

router = router.hostname

to

router = router.path

LoraGW Bridge

I'm trying to follow this deployment guide but I'm stuck a little, more confused though.

From what I gathered this deployment script does not count with using own Loara Server and does not install lora gateway bridge on the rpi, which would be more secure to let it talk to mqtt server directly ( with ssl and pass ).

Own installed loraserver will not give you: serv_gw_id and serv_gw_key

2_setup.sh wrong reset pin selection for ch2i V1.2 - ic880A - Raspberry PI

Hello,
I have lora gw with ch2i V1.2 board, ic880A and a RPI 3B. During running 2_setup.sh there is question, which HW combination is used for the gateway (see line 91 of 2_setup.sh). I choose "CH2i ic880a". This will select GPIO5 as reset pin for ic880A board. But this is wrong. It has to be GPIO17 for this combination. Please can you correct script. I do not know, if the selection for the other HW combinations is correct.

Monitor doesn't start due to bad/not compiled _rpi_ws281x.so file

Hi,
1st of all, thanks for the great work you've done (and are still doing), the main function, working as a TTN Lorawan gateway, is working perfectly. I have the Pi0W + RAK831 board, PCB version 1.3 (small PCB). There is one issue though, the monitor doesn't properly start because the WS281X lib is not properly compiled. The file is in the expected location but is 0 bytes. A bit too short :) Below is the log for the monitor service.

What could I check/do to make it work? Compile something again differently/by itself?

Thanks,
Geert

Oct 03 18:48:42 loragw-ac6d systemd[1]: Started LoraGW monitoring service.
Oct 03 18:48:46 loragw-ac6d monitor[1438]: Traceback (most recent call last):
Oct 03 18:48:46 loragw-ac6d monitor[1438]:   File "/opt/loragw/monitor.py", line 25, in <module>
Oct 03 18:48:46 loragw-ac6d monitor[1438]:     from neopixel import *
Oct 03 18:48:46 loragw-ac6d monitor[1438]:   File "build/bdist.linux-armv6l/egg/neopixel.py", line 5, in <module>
Oct 03 18:48:46 loragw-ac6d monitor[1438]:   File "build/bdist.linux-armv6l/egg/_rpi_ws281x.py", line 7, in <module>
Oct 03 18:48:46 loragw-ac6d monitor[1438]:   File "build/bdist.linux-armv6l/egg/_rpi_ws281x.py", line 6, in __bootstrap__
Oct 03 18:48:46 loragw-ac6d monitor[1438]: ImportError: /root/.cache/Python-Eggs/rpi_ws281x-1.0.0-py2.7-linux-armv6l.egg-tmp/_rpi_ws281x.so: file too short
Oct 03 18:48:46 loragw-ac6d systemd[1]: monitor.service: Main process exited, code=exited, status=1/FAILURE
Oct 03 18:48:46 loragw-ac6d systemd[1]: monitor.service: Unit entered failed state.
Oct 03 18:48:47 loragw-ac6d systemd[1]: monitor.service: Failed with result 'exit-code'.
Oct 03 18:48:52 loragw-ac6d systemd[1]: monitor.service: Service hold-off time over, scheduling restart.
Oct 03 18:48:52 loragw-ac6d systemd[1]: Stopped LoraGW monitoring service.

SNR and RSSI of raspberry pi LoRa Gateway

Hi, I was trying to read out both SNR and RSSI values to display on the OLED display from oled.py script and it was unsuccessful. I did add "lora_snr" variable in both global and UDPhandler method.

So I am wondering if I want to get the SNR value, is it to do with oled.py or other files?

Thanks!

router.eu.thethings.network in global_conf.json missing

I have installed my gateway according to instructions. But when I called sudo journalctl -f -u loragw.service it only displayed

Jun 14 17:08:51 loragw-9883 loragw[613]: 17:08:51  INFO: Packet logger is disabled
Jun 14 17:08:51 loragw-9883 loragw[613]: 17:08:51  INFO: Flush output after statistics is disabled
Jun 14 17:08:51 loragw-9883 loragw[613]: 17:08:51  INFO: Flush after each line of output is disabled
Jun 14 17:08:51 loragw-9883 loragw[613]: 17:08:51  INFO: Watchdog is disabled
Jun 14 17:08:51 loragw-9883 loragw[613]: 17:08:51  INFO: Contact email configured to ""
Jun 14 17:08:51 loragw-9883 loragw[613]: 17:08:51  INFO: Description configured to ""
Jun 14 17:08:51 loragw-9883 loragw[613]: 17:08:51  INFO: [Transports] Initializing protocol for 2 servers
Jun 14 17:08:51 loragw-9883 loragw[613]: 17:08:51  ERROR: [TTN] Connection to server "" failed, retry in 30 seconds
Jun 14 17:09:35 loragw-9883 loragw[613]: 17:09:35  ERROR: [TTN] Connection to server "" failed, retry in 60 seconds
Jun 14 17:10:35 loragw-9883 loragw[613]: 17:10:35  ERROR: [TTN] Connection to server "" failed, retry in 120 seconds

The server address in /opt/loragw/global_conf.json was empty. "server_address": "",
I had to change it to "server_address": "router.eu.thethings.network",

shouldn't this work out of the box?

Script does not work anymore with newest Raspberry OS (V1.3a board)

Hi Charles,
I wanted to give my gateway to a colleague and try to install the newest software. Unfortunately, the script does no more work and creates many errors (e.g. node-js but also some python errors.)

Would it be possible to adapt it? Or at least give a workaround that it still can be used. I like this small gateway...

Regards
Andreas

how to upgrade the software for TTN v3 compliance?

Hi,
I've been happily using your designs and solution for the last years, on and off, depending on my (mini) project focus.

Now, with the upcoming disabling of the The Things stack v2, I would like to upgrade the software on the Raspberry towards The Things Community Edition and also change Things Stack version.

I saw the steps in the link below, those are probably doable without changing the software. https://www.thethingsnetwork.org/docs/the-things-stack/migrate-to-v3/migrate-gateways/

But I suspect that fully migrating to the TTN Stack V3 will require code changes.
How would we go about to extend the life of your (still perfect working) gateway?

Thanks

SocketServer.UDPServer(("127.0.0.1", 1688), MyUDPHandler)

I promise this is the last issue I have :D But what should be on port 1688 ? The script did not put anything there. Its not open and there is no traffic on that port either....OLED therefore does not show LoraWan Data Status... any idea ?

Node data transmission ends abruptly

Hey Charles!
I have used your awesome script up boot up my ttn gateway.. it's working well.
I have kept it On for around a week now.
For testing purposes, I've kept nodes transmitting at intervals of 20 secs.
The problem is that after a continuous transmission of a lot of packages for an hour or so, the data transmission stops abruptly. I can't see node data on the Gateway traffic on ttn, neither on my node-red setup which i have installed. TTN problem is because of the fair usage policy, but why a problem in my private network?
Restarting solves this problem for the moment. What could be the issue? Is it some memory problems?
Btw, am using the loraserver.io also with your gateway setup.

Detect the GPS module of the official RAK831 shield for Raspberry Pi 3

Thanks for the awesome setup procedure. I've successfully used it with a RAK831 board and RAK's official shield (converter board) for the Raspberry Pi 3. However the GPS module soldered onto the shield was not configured.

With manual work, I was able to get the GPS module running. The relevant steps are:

Serial port

On the Raspberry Pi 3, the serial port /dev/ttyAMA0 is used for the bluetooth module. The GPS module is found on /dev/ttyS0. The path /dev/serial0 is linked to that device as well. The Raspbian distribution configures it as a login shell, which is in conflict with the use for the GPS module. So it must be changed, either by removing console=serial0,115200 from /boot/cmdline.txt or by using raspbi-config:

  • Interfacing Options
  • Serial
  • Would you like a login shell to be accessible over serial? No
  • Would you like the serial port hardware to be enabled? Yes

Permissions for serial port

In order to access the serial port (without being root), the user must belong to the group dialout. The user pi belongs to it, the user loragw doesn't. So:

sudo usermod -a -G dialout loragw

Discovery

Even after these changes, the setup script did not properly discover the GPS module. It said:

Has hardware GPS:	False
Hardware GPS port:	/dev/ttyAMA0
Using fake GPS

I have no idea how the discovery works. Is the GPS port a default value? Anyhow, I've modified the global_conf.json manually:

        "fake_gps": false, 
        "gps": true,
	"gps_tty_path": "/dev/ttyS0"

After a reboot, the GPS module is now working. It would be cool if no manual work was needed and the setup scripts would automatically detect and configure the GPS module.

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.