Visible Light Communication System

by darthrappers in Circuits > LEDs

479 Views, 9 Favorites, 0 Comments

Visible Light Communication System

introduction.png

Our goal is to revolutionize underwater exploration by developing a high-speed communication system that uses light to transmit data, enabling real-time access to sensor data. This innovative solution allows researchers to capture and transmit underwater images, such as those of coral reefs, with unprecedented speed and efficiency. By relying exclusively on light for data transmission, our system ensures rapid, reliable communication in underwater environments.

We hope this instructable provides clear instructions for anyone to build a visible light based communication device. There are many comments in the code, which will help you change any parameters to suit your needs.

Supplies

  1. Teensy 4.1 - Qty 1
  2. Arduino Uno R3 - Qty 1
  3. Green LED - Qty 1
  4. 220 Ohm Resistor - Qty 1
  5. OPT101 Photodiode - Qty 1
  6. 3x Magnification Fresnel Lens - Qty 1
  7. Breadboards - As needed
  8. Dupont wires - As needed
  9. PCs/laptops - Qty 2

Downloads

Build Transmitter

Blink_bb.png
PXL_20241224_224340752.jpg
  1. Bend the LED leads at 90 degrees and mount to a bread board.
  2. Connect Arduino GND to the negative (shorter) lead of the Green LED
  3. Connect a 220ohm resistor to the positive (longer) lead of the Green LED
  4. Connect the other end of the resistor to Digital Pin 9 of Arduino
  5. Mount the Fresnel lens at about 6.5cm from the LED. You may have to move the Fresnel lens closer or farther until the LED forms a clear spotlight on the receiver.

Build Receiver

PXL_20241210_223707033.MP.jpg
PXL_20241225_220803657.jpg
  1. Mount the OPT101 photodiode on the breadboard.
  2. Mount the Teensy on the breadboard.
  3. OPT101 connections
  4. Connect Pin 1 to Teensy 3.3V 
  5. Connect Pin 3 and Pin 8 to ground 
  6. Connect Pin 4 to Pin 5 
  7. Connect Pin 5 to A0 on Teensy


Software Environment Setup

  1. Install Arduino IDE on both laptops.
  2. Install the Teensduino software add-on using these instructions.

Transmission Software

This Arduino code enables an LED to transmit data using visible light communication using simple ON-OFF keying method. It stores a preloaded binary message in program memory and transmits it bit by bit by toggling the LED on and off at precise intervals. The transmission timing is controlled by a configurable bit duration, ensuring accurate encoding of the message. Prior to transmitting the message a long 1 (3 seconds) followed by a 0 (1 second) is sent.

#include <avr/pgmspace.h>
#include <stdint.h>

// Pin where the LED is connected
const int ledPin = 9;

// Time interval (in microseconds) for each bit
const unsigned long bitDuration = 50; // in microseconds

// Preloaded message stored in an array of uint64_t
const uint64_t binaryMessage[] PROGMEM = {
0b1101100010100111101101110111000100010111010111000100101010001100,
0b1000100010011110010101110111010101001100110100010110101010011100,
0b0110101011011010101011010010010011010101011101010111010010101010,
0b1111000011110000111100001111000011110000111100001111000011110000,
0b1010101010101010101010101010101010101010101010101010101010101010,
0b0101010101010101010101010101010101010101010101010101010101010101,
0b1110001110001110001110001110001110001110001110001110001110001110,
0b0001110001110001110001110001110001110001110001110001110001110001,
0b1010010110100101101001011010010110100101101001011010010110100101,
0b0101101001011010010110100101101001011010010110100101101001011010
};

const int messageLength = sizeof(binaryMessage) / sizeof(binaryMessage[0]);

// Variables to manage timing and state
unsigned long previousMicros = 0; // Tracks the last time a bit was processed
int currentBitIndex = 0; // Tracks the current bit being transmitted
int currentArrayIndex = 0; // Tracks which uint64_t is being transmitted
int messageCount = 0; // Tracks the number of times the message is sent
bool transmitting = false; // Whether transmission has started

void setup() {
pinMode(ledPin, OUTPUT);
Serial.begin(115200); // Set Serial Monitor speed to 115200
Serial.println("Type S to begin transmission.");
}

void loop() {
// Check for command to start transmission
if (!transmitting && Serial.available()) {
String command = Serial.readStringUntil('\n');
command.trim();
if (command.equalsIgnoreCase("S")) {
transmitting = true;
Serial.println("Starting transmission...");

// Send a long 1 (LED ON) for 5 seconds
digitalWrite(ledPin, HIGH);
delay(2000);

// Send a long 0 (LED OFF) for 1 seconds
digitalWrite(ledPin, LOW);
delay(1000);

previousMicros = micros(); // Reset timing
}
}

// Transmit message if started
if (transmitting) {
unsigned long currentMicros = micros();

// Check if it's time to process the next bit
if (currentMicros - previousMicros >= bitDuration) {
previousMicros = currentMicros; // Update the timestamp

// Transmit the current bit
if (currentArrayIndex < messageLength) {
// Read the current uint64_t from program memory
uint64_t currentWord = pgm_read_qword_near(binaryMessage + currentArrayIndex);

// Extract the current bit
int bitValue = (currentWord >> (63 - currentBitIndex)) & 1;

// Set the LED state based on the bit value
digitalWrite(ledPin, bitValue);

// Print the bit to the Serial Monitor
// Serial.print(bitValue);

// Move to the next bit
currentBitIndex++;
if (currentBitIndex >= 64) {
currentBitIndex = 0; // Reset to the first bit of the next uint64_t
currentArrayIndex++; // Move to the next uint64_t
}
} else {
// End of message: reset for next transmission or stop
Serial.println(); // Newline after full message
currentBitIndex = 0;
currentArrayIndex = 0;
messageCount++;

if (messageCount >= 5) { // Change this value for multiple transmissions
Serial.println("Transmission complete.");
transmitting = false; // Stop transmitting
}
}
}
}
}

// Helper function to read 64-bit integers from PROGMEM
uint64_t pgm_read_qword_near(const void* addr) {
uint64_t value = 0;
for (int i = 0; i < 8; i++) {
value |= ((uint64_t)pgm_read_byte_near((const uint8_t*)addr + i)) << (i * 8);
}
return value;
}

Downloads

Receiving Software

This Arduino code facilitates the reception of visible light communication signals using an analog sensor. It samples input from a photodiode connected to an analog pin at precise intervals, storing the time and signal values in memory arrays for further processing. The sampling parameters, such as interval and additional metadata like timestamps, can be configured. The data collected is saved to an SD card for later analysis, making this setup ideal for decoding signals transmitted using light.

#include <SD.h>
#include <SPI.h>
#include <ADC.h>
#include <ADC_util.h>

// Define the array size
const int numSamples = 120000;

//ADC object
ADC *adc = new ADC();
const int readPin = A0;

// Arrays to store time and analog values
DMAMEM short timeArray[numSamples];
DMAMEM short valueArray[numSamples];

// Variables for time tracking
unsigned long previousMicros = 0; // Last time an analog read was taken
unsigned long interval = 20; // Interval between reads (default 100 microseconds)

int sampleIndex = 0;
bool startSampling = false; // Flag to track when to start sampling
bool descriptionReceived = false; // Flags for sequential inputs
bool intervalReceived = false;
bool timestampReceived = false;

String descriptionText = ""; // Text input from user
String timestamp = ""; // Timestamp received from PC

File dataFile;

void setup() {
pinMode(readPin, INPUT);
adc->adc0->setAveraging(0); // number of averages
adc->adc0->setResolution(10); // ADC resolution
adc->adc0->setConversionSpeed(ADC_CONVERSION_SPEED::MED_SPEED); //conversion speed
adc->adc0->setSamplingSpeed(ADC_SAMPLING_SPEED::MED_SPEED); // sampling speed

Serial.begin(115200); // Initialize serial communication
while (!Serial); // Wait for serial port to connect (for boards like Teensy 4.1)

// Initialize the SD card
if (!SD.begin(BUILTIN_SDCARD)) { // Assuming the SD card is connected to pin 10 (can vary)
Serial.println("SD card initialization failed!");
return;
}
Serial.println("SD card initialized.");

// Print instructions
Serial.println("Enter a description for the data and press ENTER.");
}

void loop() {
// Check for serial input
if (!startSampling && Serial.available() > 0) {
String input = Serial.readStringUntil('\n');
input.trim(); // Remove any extra whitespace or newline characters

if (!descriptionReceived) {
// First input is the description
descriptionText = input;
descriptionReceived = true;
Serial.println("Description received. Enter interval (in microseconds) and press ENTER.");
} else if (!intervalReceived) {
// Second input is the interval
interval = input.toInt(); // Convert input to integer
if (interval > 0) {
intervalReceived = true;
Serial.println("Interval set to " + String(interval) + " microseconds. Enter timestamp in the format 'TS:yyyy-mm-dd hh:mm:ss' or type 'SKIP'.");
} else {
Serial.println("Invalid interval. Please enter a positive number.");
}
} else if (!timestampReceived) {
// Third input is the timestamp
if (input.equalsIgnoreCase("SKIP")) {
timestampReceived = true;
Serial.println("Timestamp skipped. Send 'R' to start data collection.");
} else if (input.startsWith("TS:")) {
timestamp = input.substring(3).replace(" ", "-").replace(":", "-");
timestampReceived = true;
Serial.println("Timestamp received: " + timestamp + ". Send 'R' to start data collection.");
} else {
Serial.println("Invalid timestamp. Please use the format 'TS:yyyy-mm-dd hh:mm:ss' or type 'SKIP'.");
}
} else if (input.equalsIgnoreCase("R")) {
// 'R' starts the sampling
startSampling = true;
Serial.println("Data collection started.");
} else {
Serial.println("Invalid input. Follow the sequence: description, interval, timestamp, then 'R'.");
}
}

// Sampling loop
if (startSampling) {
unsigned long currentMicros = micros();

if (currentMicros - previousMicros >= interval) {
if (sampleIndex < numSamples) {
// Store the timestamp and analog value
timeArray[sampleIndex] = (short)(currentMicros&0xFFFF);
valueArray[sampleIndex] = adc->adc0->analogRead(readPin); // Fast read
sampleIndex++;
previousMicros = currentMicros;
} else {
// All samples collected, save to SD card
delay(1000); // Allow some time before writing

// Use timestamp or fallback filename
String filename = (timestamp.length() > 0 ? timestamp : String(millis())) + ".csv";
dataFile = SD.open(filename.c_str(), FILE_WRITE);

if (dataFile) {
// Write description, interval, and header
dataFile.println("Description: " + descriptionText);
dataFile.println("Interval (us): " + String(interval));
dataFile.println("Index,Time_us,Value");

// Write the collected data
for (int i = 0; i < numSamples; i++) {
dataFile.print(i);
dataFile.print(", ");
dataFile.print(timeArray[i]);
dataFile.print(", ");
dataFile.println(valueArray[i]);
Serial.println(valueArray[i]);
}

// Close the file
dataFile.close();
Serial.println("Data written to SD card as: " + filename);
} else {
Serial.println("Error opening file for writing.");
}

// Calculate and print max/min values
calculateAndPrintMaxMin();

// Reset the sampling flag and inputs for another run
Serial.println("Data collection complete. Enter a new description to start again.");
startSampling = false;
descriptionReceived = false;
intervalReceived = false;
timestampReceived = false;
sampleIndex = 0;
}
}
}
}

void calculateAndPrintMaxMin() {
int maxValue = valueArray[0];
int minValue = valueArray[0];

for (int i = 1; i < numSamples; i++) {
if (valueArray[i] > maxValue) {
maxValue = valueArray[i];
}
if (valueArray[i] < minValue) {
minValue = valueArray[i];
}
}

Serial.print("Maximum Value: ");
Serial.println(maxValue);
Serial.print("Minimum Value: ");
Serial.println(minValue);
}


Downloads

Decoding Software

This Python script processes CSV data containing photodiode signal measurements to decode a transmitted binary message. It calculates a threshold to distinguish between high and low signal values and generates a sequence of bits based on this threshold. The script analyzes transitions in the bitstream, rounds their durations to a specified multiple, and reconstructs the message. It then compares the decoded bit sequence against predefined 64-bit binary constants to verify accuracy.

import csv
import numpy as np
import sys
import itertools
import math

def round_to_nearest_multiple(number, multiple):
return int(round((number-0.001) / multiple) * multiple)

def process_csv(file_path, rounding_multiple):

with open(file_path, 'r') as csv_file:
# Read all lines from the file
lines = list(csv_file)
# Extract the first two rows
row1, row2 = lines[0], lines[1]
# Use csv.DictReader on the remaining lines
reader = csv.DictReader(lines[2:])
data = [row for row in reader]

# Extract the "Value" column as a list of floats
values = [float(row['Value']) for row in data]

# Find min, max, and calculate the threshold
min_value = min(values)
max_value = max(values)
threshold = (min_value + max_value) / 2

# Compare values to the threshold and generate the "Bit" column
bits = [1 if value > threshold else 0 for value in values]

# Ignore leading zeros and start decoding from the first '1'
first_one_index = next((i for i, bit in enumerate(bits) if bit == 1), None)
if first_one_index is None:
print("No '1' found in the data. Cannot start decoding.")
return

bits = bits[first_one_index:]

# Find transitions, count 0s or 1s between transitions, and store bits with counts
bit_count_pairs = []
count = 1
for i in range(1, len(bits)):
if bits[i] != bits[i - 1]: # Transition detected
bit_count_pairs.append((bits[i - 1], count))
count = 1 # Reset count
else:
count += 1
bit_count_pairs.append((bits[-1], count)) # Append the final bit and its count

# Round counts to the nearest multiple of the rounding_multiple
rounded_counts = [round_to_nearest_multiple(count, rounding_multiple) for _, count in bit_count_pairs]

# Generate the bit sequence "DecodedBits"
decoded_bits = []
for (bit, _), rounded_count in zip(bit_count_pairs, rounded_counts):
num_bits = rounded_count // rounding_multiple
if num_bits>4:
num_bits=4
decoded_bits.extend([bit] * num_bits)

# BinaryMessage constants
binary_message = [
0b1101100010100111101101110111000100010111010111000100101010001100, #1
0b1000100010011110010101110111010101001100110100010110101010011100,
0b0110101011011010101011010010010011010101011101010111010010101010,
0b1111000011110000111100001111000011110000111100001111000011110000,
0b1010101010101010101010101010101010101010101010101010101010101010,
0b0101010101010101010101010101010101010101010101010101010101010101,
0b1110001110001110001110001110001110001110001110001110001110001110,
0b0001110001110001110001110001110001110001110001110001110001110001,
0b1010010110100101101001011010010110100101101001011010010110100101,
0b0101101001011010010110100101101001011010010110100101101001011010,
0b1101100010100111101101110111000100010111010111000100101010001100, #2
0b1000100010011110010101110111010101001100110100010110101010011100,
0b0110101011011010101011010010010011010101011101010111010010101010,
0b1111000011110000111100001111000011110000111100001111000011110000,
0b1010101010101010101010101010101010101010101010101010101010101010,
0b0101010101010101010101010101010101010101010101010101010101010101,
0b1110001110001110001110001110001110001110001110001110001110001110,
0b0001110001110001110001110001110001110001110001110001110001110001,
0b1010010110100101101001011010010110100101101001011010010110100101,
0b0101101001011010010110100101101001011010010110100101101001011010,
0b1101100010100111101101110111000100010111010111000100101010001100,
0b1000100010011110010101110111010101001100110100010110101010011100,
0b0110101011011010101011010010010011010101011101010111010010101010,
0b1111000011110000111100001111000011110000111100001111000011110000,
0b1010101010101010101010101010101010101010101010101010101010101010,
0b0101010101010101010101010101010101010101010101010101010101010101,
0b1110001110001110001110001110001110001110001110001110001110001110,
0b0001110001110001110001110001110001110001110001110001110001110001,
0b1010010110100101101001011010010110100101101001011010010110100101,
0b0101101001011010010110100101101001011010010110100101101001011010,
0b1101100010100111101101110111000100010111010111000100101010001100,
0b1000100010011110010101110111010101001100110100010110101010011100,
0b0110101011011010101011010010010011010101011101010111010010101010,
0b1111000011110000111100001111000011110000111100001111000011110000,
0b1010101010101010101010101010101010101010101010101010101010101010,
0b0101010101010101010101010101010101010101010101010101010101010101,
0b1110001110001110001110001110001110001110001110001110001110001110,
0b0001110001110001110001110001110001110001110001110001110001110001,
0b1010010110100101101001011010010110100101101001011010010110100101,
0b0101101001011010010110100101101001011010010110100101101001011010,
0b1101100010100111101101110111000100010111010111000100101010001100,
0b1000100010011110010101110111010101001100110100010110101010011100,
0b0110101011011010101011010010010011010101011101010111010010101010,
0b1111000011110000111100001111000011110000111100001111000011110000,
0b1010101010101010101010101010101010101010101010101010101010101010,
0b0101010101010101010101010101010101010101010101010101010101010101,
0b1110001110001110001110001110001110001110001110001110001110001110,
0b0001110001110001110001110001110001110001110001110001110001110001,
0b1010010110100101101001011010010110100101101001011010010110100101,
0b0101101001011010010110100101101001011010010110100101101001011010
]

# Print the file name and rounding multiple
print(row1.strip())
print(row2.strip())
print(f"File Name: {file_path}")
print(f"Photodiode Samples per Cycle: {rounding_multiple}")
print(f"Minimum : {min_value}")
print(f"Maximum : {max_value}")
print(f"Threshold : {threshold}\n")
# Compare DecodedBits to the binaryMessage constants and print results
for i, constant in enumerate(binary_message):
constant_bits = [(constant >> bit) & 1 for bit in range(63, -1, -1)] # Convert to bit array
decoded_segment = decoded_bits[i * 64 + math.floor(i/10):(i + 1) * 64 + math.floor(i/10)]
match = decoded_segment == constant_bits # Compare segment to constant
print(f"Original 64-bit Sequence {i + 1}: {''.join(map(str, constant_bits))}")
print(f"Decoded 64-bit Sequence {i + 1}: {''.join(map(str, decoded_segment))}")
print(f"Match: {match}\n")

if __name__ == "__main__":
if len(sys.argv) < 3:
print("Usage: python script.py <csv_file> <rounding_multiple>")
sys.exit(1)

file_path = sys.argv[1]
rounding_multiple = int(sys.argv[2])

process_csv(file_path, rounding_multiple)

Downloads

Testing

To test the visible light transmission system you need two users to operate the two different laptops connected to the transmitter and the receiver. Synchronization between the transmitter and receiver is currently manual. The user operating the receiver needs to look for the long 3 second LED ON from the transmitter and as soon as it goes OFF (for 1 second) they need to press R to start collecting data. For accurate decoding the photodiode should be sampled at least 5 times for every LED bit duration. Using this system we can currently send data at about 20 kbps.