DEV Community

Cover image for From Wi-Fi to Li-Fi, sending data via light using Arduino and JavaScript
Charlie Gerard for Stripe

Posted on • Originally published at charliegerard.dev

From Wi-Fi to Li-Fi, sending data via light using Arduino and JavaScript

There’s a big chance you’re reading this post on a device connected to the internet via Wi-Fi. Your router broadcasts data that is received by a small antenna in your computer, phone or tablet. This data is transmitted over radio waves at a frequency of either 2.4GHz or 5GHz. However, other parts of the electromagnetic spectrum can be used to transmit information. Using visible light, data can be encoded and transmitted using a technology called Li-Fi which aims at using your existing lights for wireless communication.

In this post, I’ll explain how it works by building a prototype of a Li-Fi project using JavaScript and Arduino.

If you prefer tutorials in video formats, you can check out this video on Youtube.

Demo

First, here’s the final outcome of my experiments. Data is transmitted using visible light between two devices. This demo shows how a Stripe payment link can be sent but this technology also works to transmit audio files, videos, and more.

Demo of a Stripe payment link sent via light

Material

There are a lot of different ways to build this. Here’s the list of components I used for my prototype:

They are then assembled following these schematics:

Schematics showing the 2 Arduinos set up

The board shown at the top is used as the transmitter and will use the Neopixel Jewel connected to pin 7 of the Arduino to send data via light. The board below is the receiver and uses the phototransistor connected to pin A0 to convert the light intensity back into data.

Schematics showing the assembly of both Arduino boards

Now, let’s dive deeper into how it works.

Deep dive

Converting data

If you’re used to working with computers, you have probably heard many times that, at the lowest level, computers deal with data as a bunch of 1s and 0s. Using light as a medium to send information is quite convenient, as the only state a light can be in is either “on” or “off”. As a result, for this experiment, we’re going to encode 1 as the “on” state and 0 as the “off” one.

For the rest of this post, let’s consider that we want to transmit the string “Hello, world”.

Strings are made of characters, and a single character is 1 byte of data. As a byte is 8 bits, each letter in this string can be converted to 8 bits.

Illustration showing the conversion from ASCII to binary
The decimal representation of the ASCII letter “H” is the integer 72, which can be converted to binary as 01001000.

The complete string “Hello, world” represented in binary is the following:
01001000 01100101 01101100 01101100 01101111 00101100 00100000 01110111 01101111 01110010 01101100 01100100

To do this conversion using JavaScript, you can use the built-in methods charCodeAt, toString and padStart.

// This function takes in a string to convert
const convertToBinary = (string) => {
   // The string is split into an array of characters
   return string.split('').map(function (char) {
       // For each character, charCodeAt(0) is called to get its decimal representation, followed by .toString(2) to get its binary representation and .padStart(8, ‘0’) to make sure the leading 0s are kept and each byte has 8 digits. 
       return char.charCodeAt(0).toString(2).padStart(8, '0');
     // Finally, join all converted characters together into a single string.
   }).join(' ');
}
Enter fullscreen mode Exit fullscreen mode

Now that I’ve covered how the data is converted, let’s talk about how it is transmitted.

Transmitting the data

As mentioned above, a string can be converted to binary. The 1s can be associated with the “on” state of a light, and the 0s with the “off”. At first, you might think that a solution would be to loop through the whole binary code, turning the light on when the bit is equal to 1 and turning it off when the bit is 0. A receiver set up as a light sensor could then decode the messages by turning the light states back to 1s and 0s.

While this is how it works at its core, this is where the details get really interesting.

Because it’s important that both the transmitter and receiver stay in sync, we need to create a custom communication protocol.

First, why do they need to stay in sync? I mentioned in the previous part of this post that the binary equivalent of “Hello, world” is
01001000 01100101 01101100 01101100 01101111 00101100 00100000 01110111 01101111 01110010 01101100 01100100

If the receiver starts decoding the data at the first bit, then it will be able to retrieve the right information; however that might not always be the case. If the receiver is out of sync by even a single bit, the information it will decode will be incorrect.

For example, if instead of the first 8 bits “01001000”, it gets “10010000”, it will decode “�” instead of “H” as this value is not a valid ASCII character, and all subsequent characters will also be wrongly decoded.

Besides, as this technology aims at being used as part of the lights people already have set up in their homes or offices, the lights will likely already be on by the time they’re also used to transmit information.

As a result, when the lights are on but not transmitting information, the receiver will read an input equal to “111111111111…”, so a communication protocol is needed to define when a message is starting to be sent so the receiver can start the decoding process.

Setting up a communication protocol

A light might be on simply to illuminate a space, not to transmit information, so there needs to be some kind of preamble to indicate to the receiver that a message is about to be transmitted. This preamble will be a change from “on” to “off” state.

Also, we need to pick a unit of time to define how long the light should reflect the value of each bit transferred. First, let’s say that each bit changes the state of the light for 100 milliseconds, so when the bit is equal to 1, the light stays on for 100 milliseconds, and if the bit is 0, the light turns off for 100 milliseconds.

Finally, when the 8 bits have been transferred, the light will be brought back to its original “on” state.

It can be graphically represented like this:

Graphical representation of the transmission protocol

Then, on the receiver side (represented as the second row of intervals below), we need to detect the preamble when the light changes state from “on” to “off”. Then, we need to wait 1.5x the interval as we don’t want to sample the preamble but we want to make sure we sample our data within the next 100ms where data starts to be transmitted, and sample it 8 times to get the value of each bit.

Graphical representation of the transmitter and receiver protocol

Implementation

I decided to use the Johnny-Five JavaScript framework for this. After installing it , I started by declaring a few variables and instantiating the transmitter board.

// Import the required packages
const five = require("johnny-five");
const pixel = require("node-pixel");
// Instantiate a new board using the first Arduino’s port
var board = new five.Board({ port: "/dev/cu.usbmodem11101" });
// Declare variables to store the pin number that the light sensor is connected to, the value of the interval and the string to transmit.
const LED_PIN = 9;
const INTERVAL = 100;
const string = "Hello, world";
const strLength = string.length;
Enter fullscreen mode Exit fullscreen mode

Then, when the board is ready to receive instructions, I instantiate the Neopixel strip with the pin it is connected to on the Arduino as well as the number of LEDs, turn the light on and call my sendBytes function.

board.on("ready", async function () {
   const strip = new pixel.Strip({
       board: this,
       controller: "FIRMATA",
       strips: [{ pin: 7, length: 7 },],
       gamma: 2.8,
   });
   strip.on("ready", function () {
       strip.color('#fff');
       strip.show();
   });
   await delay(3000);
   sendBytes(strip);
});
Enter fullscreen mode Exit fullscreen mode

This function implements the communication protocol defined in the previous section.

const sendBytes = async (strip) => {
   for (var i = 0; i < strLength; i++) {
       strip.off();
       strip.show();
       await delay(INTERVAL);

       const bits = convertToBinary(string[i]);

       for (var y = 0; y < 8; y++) {
           const ledState = bits[y];

           if (ledState === '1') {
               strip.color('#fff');
               strip.show();
           } else {
               strip.off();
               strip.show();
           }
           await delay(INTERVAL);
       }

       strip.color('#fff');
       strip.show();
       await delay(INTERVAL);
   }
   await delay(INTERVAL);
   sendBytes(strip);
}
Enter fullscreen mode Exit fullscreen mode

For each letter in the string transmitted, it goes through the following steps:

  1. Start by turning the light off
  2. Apply a delay of 100ms
  3. Convert the letter into binary
  4. Loop through each bit
    • If its value is 1, turn the light on and if it is 0, turn it off
    • Apply the delay of 100ms
  5. When it has gone through the 8 bits, turn the light back on and apply the delay again
  6. Once all letters are sent, call sendBytes recursively to continuously send the data.

The delay function is simply a setTimeout function inside a Promise.

const delay = (ms) => {
   return new Promise(resolve => {
       setTimeout(resolve, ms);
   });
}
Enter fullscreen mode Exit fullscreen mode

Before running this code, you need to install the right firmware onto the board. To do this, you can follow the instructions on the node-pixel repository.

Then, it should result in the light flashing like this:

Transmitter light flashing

Now that the transmitter is able to change the state of the light depending on the bits sent, let’s set up the receiver side.

Decoding the data

To set up the receiver, I first declared some variables and instantiated the second board.

var five = require("johnny-five");
var board = new five.Board({ port: "/dev/cu.usbmodem11201" });
const SENSOR_PIN = "A0";
const THRESHOLD = 400;
const INTERVAL = 100;
let previousState;
let currentState;
let lightValue;
let detectionStarted = false;
let testString = "";
let decodedText = "";
Enter fullscreen mode Exit fullscreen mode

Then, when the board is ready to receive instructions, I instantiate the light sensor, store the brightness value in the lightValue variable, and call the main decode function.

board.on("ready", function () {
   var sensor = new five.Sensor(SENSOR_PIN);
   sensor.on("data", async function () {
       lightValue = this.value;
   })
  // Calling the built-in loop function to recursively trigger the decoding logic every 10 milliseconds.
   this.loop(10, () => {
       if (!detectionStarted) {
           decode();
       }
   })
});
Enter fullscreen mode Exit fullscreen mode

This function starts by calling getLDRState to return 1 or 0 if the brightness is over or under the threshold specified (this threshold will depend on the amount of light already present in your environment).

const getLDRState = (value) => {
   return value > THRESHOLD ? 1 : 0;
}
Enter fullscreen mode Exit fullscreen mode

Then, it will call the getByte function only if it has detected the preamble, meaning if the current state of the light is off and the previous state was on.

const decode = () => {
   currentState = getLDRState(lightValue);
   if (!currentState && previousState) {
       detectionStarted = true;
       getByte();
   }
   previousState = currentState;
}
Enter fullscreen mode Exit fullscreen mode

This getByte function starts by waiting 1.5x the interval chosen, then calls getLDRState 8 times to convert the brightness into a bit value, converts that byte into an ASCII character and logs it.

const getByte = async () => {
   let ret = 0;
   await delay(INTERVAL * 1.5);

   for (var i = 0; i < 8; i++) {
       const newValue = getLDRState(lightValue)
       testString += newValue
       await delay(INTERVAL);
   }

   decodedText += convertBinaryToASCII(testString)
   console.log(decodedText)
   testString = ""
   detectionStarted = false;
}
Enter fullscreen mode Exit fullscreen mode

The conversion between binary and ASCII is done with the following code.

const convertBinaryToASCII = (str) => {
   var bits = str.split(" ");
   var text = [];

   for (i = 0; i < bits.length; i++) {
       text.push(String.fromCharCode(parseInt(bits[i], 2)));
   }
   return text.join("");
}
Enter fullscreen mode Exit fullscreen mode

Before running this code, you need to install StandardFirmata onto the receiver board. To do this, follow the steps in this tutorial.

Running both the transmitter and receiver will give something like this.

Demo of the NeoPixel Jewel sending the text "Hello, world" to the transmitter via light

It works! 🎉

Make it work, then make it fast

If you look at the demo above, you’ll see that the transmission is rather slow. This is a problem not only because data will take a long time to transmit but also because the flickering of the light is very noticeable. The goal of Li-Fi is to fully integrate with existing lighting setups to transmit data in a way that wouldn’t be detectable by the human eye. For this, we need to speed up the transmission and reception.

So far in this post, I’ve started by choosing to update and read the state of the light every 100ms. Using JavaScript, the fastest speed I’ve managed to work with is 60ms. Under that, the detection seems to be failing. This was expected as I do not think JavaScript is the right tool to work with very time-sensitive hardware projects.

Instead, I decided to switch to using the Arduino library and running the code natively on the board.

Contrary to JavaScript, the Arduino library runs synchronously which means I didn’t have to find hacks with setTimeout to apply the delays.

If you’re interested in looking at the Arduino code, you can find it in my GitHub repository.

This way, I managed to decrease the interval to 4ms. As this delay is applied 10 times per byte (once for the preamble, then once for each bit, and once when setting back the light to its initial state), it means a character is sent every 40ms, and the string “Hello, world!” can be transmitted in 520ms, or about half a second, instead of 7.8s in JavaScript!

Similar to the demo above but using Arduino code so transmitting much faster.

The transmission is still noticeable at this speed – but it’s much better!

To get to a point where it could be invisible to the human eye, I would have to experiment with faster microcontrollers, maybe different light sensors and overall approaches, especially as the goal is to also transmit images and videos using this technology.

Applications

Li-Fi is not a new technology. This great TED talk from 2011 and this one from 2015 show that researchers have been working on this for more than 10 years.

It provides a few advantages such as faster data transmission capabilities, intrinsic security, and energy efficiency to mention a few.

Indeed, as the receiver device needs to be directly placed under the light source, it could ensure that data would not be intercepted by a potential malicious actor, unless they happen to be in direct line of sight. Additionally, in terms of energy savings, it could allow us to save electricity by using our lights as a router instead of using separate devices for lighting and connectivity.

Additionally, it could present a solution to provide better in-flight Internet connection as Li-Fi would not interfere with radio signals the same way Wi-Fi does.

In the payment space, terminal devices could be equipped with light detection sensors to allow customers to pay using their phone’s flashlight, instead of using NFC chips. This would allow contactless payments to be done from a larger distance than the maximum 4 cm (1.5 in) currently enabled by NFC and would provide an additional layer of security against drive-by attacks.

Conclusion

In this post, I went through the steps I took to build a small prototype and experiment with Arduino and JavaScript to send text via light. There are a lot of different aspects of this project that I would be really interested in diving deeper into. In addition to trying out faster microcontrollers, I would like to try to transmit a video file using a MIMO (Multiple Input Multiple Output) approach, but that will be for another time.

Thanks for reading!

You can stay up to date with Stripe developer updates on the following platforms:
📣 Follow @StripeDev and our team on Twitter
📺 Subscribe to our Youtube channel
💬 Join the official Discord server
📧 Sign up for the Dev Digest

About the author

Charlie has long brown hair and is wearing glasses. She is standing in front of a white wall with purple lights.

Charlie Gerard is a Developer Advocate at Stripe, a published author and a creative technologist. She loves researching and experimenting with technologies. When she’s not coding, she enjoys spending time outdoors, reading and setting herself random challenges.

Latest comments (7)

Collapse
 
devbambhaniya profile image
Devshi bambhaniya

@devdevcharlie

Thank you for sharing yes that is interesting!

Collapse
 
vngcduynguyn1 profile image
Vương Đức Duy Nguyễn

1

Collapse
 
cubiclesocial profile image
cubiclesocial

You should switch to infrared emitters and sensors, which will do the same thing but be outside the spectrum of visible light (at least visible light that humans see). As a bonus, phone cameras and webcams can "see" infrared light, which is essentially how cheap night vision works. More fun stuff to play with. TV/DVR remotes among many older technologies work on this principle to send data to the TV or the DVR box.

Fiber optic cables also use light to transmit data. They are more sensitive in that there is little to no tolerance for physical problems in the cable, the connectors, or the connection to the device. A single piece of dust or microfracture will wreak havoc.

Transmitting data via light pulses also sounds very much like old modem tech. Analog audio signals can carry more information by spreading out which value is which frequency.

So there are at least three different avenues to explore.

If the end goal is to somehow improve contactless payment, displaying a QR code that takes the user to a payment app would probably be better. That way there's no NFC involved and the payment happens on the user's network-connected device. QR codes can also be scanned from several feet away. If someone else snaps the QR code, the only thing it should be good for is to actually pay for the final Total of the goods/services and, once they are paid for, the code can be made to not work after that. So the only thing "stealing" the QR code would do is enable someone else to pay for your stuff. The humble QR code might solve the problem you seem to be trying to solve with flashing lights.

Collapse
 
koha profile image
Joshua Omobola

Cooool!

Collapse
 
antonhaxor profile image
anton-haxor

Brilliant! Great work Charlie

Collapse
 
vngcduynguyn1 profile image
Vương Đức Duy Nguyễn

Hello

Collapse
 
vngcduynguyn1 profile image
Vương Đức Duy Nguyễn

hello 1.