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.
Material
There are a lot of different ways to build this. Here’s the list of components I used for my prototype:
- 2 Arduino UNO
- A breadboard
- Jumper wires
- A 10k resistor
- A Neopixel Jewel (a standard LED will work too but you will need an extra resistor and the transmitter and receiver will have to be set up much closer as an LED is less bright than the Jewel).
- A phototransistor
They are then assembled following these schematics:
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.
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.
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
.
const convertToBinary = (string) => {
return string.split('').map(function (char) {
return char.charCodeAt(0).toString(2).padStart(8, '0');
}).join(' ');
}
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:
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.
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.
const five = require("johnny-five");
const pixel = require("node-pixel");
var board = new five.Board({ port: "/dev/cu.usbmodem11101" });
const LED_PIN = 9;
const INTERVAL = 100;
const string = "Hello, world";
const strLength = string.length;
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