从 Wi-Fi 到 Li-Fi,使用 Arduino 和 JavaScript 通过光传输数据
由 Mux 赞助的 DEV 全球展示挑战赛:展示你的项目!
你很可能正在使用通过 Wi-Fi 连接互联网的设备阅读这篇文章。你的路由器会广播数据,这些数据会被你电脑、手机或平板电脑上的小型天线接收。这些数据通过频率为 2.4GHz 或 5GHz 的无线电波传输。然而,电磁频谱的其他部分也可以用来传输信息。利用可见光,数据可以通过一种名为 Li-Fi 的技术进行编码和传输,该技术旨在利用你现有的光源进行无线通信。
在这篇文章中,我将通过使用 JavaScript 和 Arduino 构建一个 Li-Fi 项目原型来解释它的工作原理。
如果您更喜欢视频形式的教程,可以观看 YouTube 上的这个视频。
演示
首先,这是我实验的最终结果。数据通过可见光在两个设备之间传输。这个演示展示了如何发送Stripe 支付链接,但这项技术也适用于传输音频文件、视频等。
材料
有很多不同的方法可以实现这个功能。以下是我在原型中使用的组件列表:
- 2 个Arduino UNO
- 面包板
- 跳线
- 一个10k 电阻
- Neopixel Jewel(标准 LED 也可以,但需要额外的电阻,并且发射器和接收器必须设置得更近,因为 LED 的亮度不如 Jewel)。
- 光电晶体管
然后按照以下示意图进行组装:
上方所示的电路板用作发射器,它使用连接到 Arduino 引脚 7 的 Neopixel Jewel 像素灯,通过光信号发送数据。下方的电路板是接收器,它使用连接到引脚 A0 的光电晶体管将光强转换回数据。
现在,让我们深入了解一下它是如何运作的。
深度探索
数据转换
如果你经常使用电脑,你可能已经多次听说过,电脑在底层处理数据时,实际上是用一串串的 1 和 0 来表示的。用光作为信息传输媒介非常方便,因为光只有两种状态:“开”或“关”。因此,在这个实验中,我们将用 1 表示“开”,用 0 表示“关”。
在本文的其余部分,我们假设我们要传输字符串“Hello, world”。
字符串由字符组成,一个字符占用 1 个字节的数据。由于一个字节是 8 位,因此该字符串中的每个字母都可以转换为 8 位。

ASCII 字母“H”的十进制表示是整数 72,可以转换为二进制 01001000。
完整的字符串“Hello, world”的二进制表示如下:01001000 01100101 01101100 01101100 01101111 00101100 00100000 01110111 01101111 01110010 01101100 01100100
要使用 JavaScript 进行此转换,您可以使用内置方法charCodeAt、toString和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(' ');
}
既然我已经讲解了数据是如何转换的,那么我们来谈谈数据是如何传输的。
数据传输
如上所述,字符串可以转换为二进制。1 可以表示灯的“开”状态,0 表示灯的“关”状态。起初,你可能会想到一个解决方案:遍历整个二进制代码,当位为 1 时打开灯,当位为 0 时关闭灯。然后,一个设置为光传感器的接收器可以通过将灯的状态转换回 1 和 0 来解码这些信息。
虽然其核心运作方式如此,但细节之处才真正有趣。
因为发射器和接收器保持同步非常重要,所以我们需要创建一个自定义通信协议。
首先,为什么它们需要保持同步?我在本文前一部分提到过,“Hello, world”的二进制等效代码是01001000 01100101 01101100 01101100 01101111 00101100 00100000 01110111 01101111 01110010 01101100 01100100
如果接收器从第一个比特开始解码数据,就能获取正确的信息;然而,情况并非总是如此。即使接收器只延迟一个比特,解码后的信息也会是错误的。
例如,如果前 8 位不是“01001000”,而是“10010000”,则会解码为“�”而不是“H”,因为该值不是有效的 ASCII 字符,并且所有后续字符也都会被错误解码。
此外,由于这项技术旨在与人们在家中或办公室中已经安装的照明设备配合使用,因此当它们也被用于传输信息时,这些灯可能已经亮着了。
因此,当灯亮着但没有传输信息时,接收器将读取到等于“111111111111…”的输入,所以需要一个通信协议来定义何时开始发送消息,以便接收器可以开始解码过程。
建立通信协议
灯亮着可能仅仅是为了照亮空间,而不是为了传递信息,因此需要某种前导信号来告知接收者即将发送信息。这种前导信号就是灯的状态从“开”变为“关”。
此外,我们需要选择一个时间单位来定义灯光反映每个传输比特值的时间。首先,假设每个比特改变灯光状态100毫秒,那么当比特值为1时,灯光保持亮100毫秒;当比特值为0时,灯光熄灭100毫秒。
最后,当 8 位数据传输完毕后,灯将恢复到原来的“开启”状态。
它可以图形化地表示如下:
然后,在接收端(如下方第二行区间所示),我们需要检测指示灯状态从“亮”变为“灭”时的前导码。之后,我们需要等待1.5倍的时间间隔,因为我们不想对前导码进行采样,而是要确保在接下来的100毫秒内(数据开始传输的时间段)对数据进行采样,并采样8次以获取每个比特的值。
执行
我决定使用Johnny-Five JavaScript框架来实现这个功能。安装完成后,我首先声明了一些变量并实例化了发射器板。
// 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;
然后,当电路板准备好接收指令时,我使用 Arduino 上连接的引脚以及 LED 的数量来实例化 Neopixel 灯条,打开灯并调用我的sendBytes函数。
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);
});
该函数实现了上一节中定义的通信协议。
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);
}
对于传输字符串中的每个字母,它都会执行以下步骤:
- 首先关掉灯
- 应用 100 毫秒的延迟
- 将字母转换为二进制
- 遍历每一位
- 如果该值为 1,则打开灯;如果该值为 0,则关闭灯。
- 应用 100 毫秒的延迟
- 当它处理完 8 位后,重新打开灯并再次应用延迟。
- 所有信件发送完毕后,递归调用 sendBytes 函数继续发送数据。
该delay函数只是一个setTimeout嵌套在……中的函数Promise。
const delay = (ms) => {
return new Promise(resolve => {
setTimeout(resolve, ms);
});
}
运行此代码之前,您需要在开发板上安装正确的固件。为此,您可以按照node-pixel 代码库中的说明进行操作。
然后,指示灯应该像这样闪烁:
既然发射器能够根据发送的比特改变灯的状态,那么我们来设置接收器端。
数据解码
为了设置接收器,我首先声明了一些变量并实例化了第二个板。
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 = "";
然后,当电路板准备好接收指令时,我实例化光传感器,将亮度值存储在lightValue变量中,并调用主decode函数。
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();
}
})
});
该函数首先调用函数,getLDRState如果亮度超过或低于指定的阈值,则返回 1,如果亮度低于指定的阈值,则返回 0(此阈值取决于环境中已有的光量)。
const getLDRState = (value) => {
return value > THRESHOLD ? 1 : 0;
}
然后,只有当检测到前导码时,才会调用该getByte函数,也就是说,如果灯的当前状态为,并且先前的状态为。offon
const decode = () => {
currentState = getLDRState(lightValue);
if (!currentState && previousState) {
detectionStarted = true;
getByte();
}
previousState = currentState;
}
该getByte函数首先等待所选间隔的 1.5 倍,然后调用getLDRState8 次将亮度转换为位值,将该字节转换为 ASCII 字符并将其记录下来。
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;
}
二进制和ASCII之间的转换通过以下代码完成。
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("");
}
运行此代码之前,需要将 StandardFirmata 安装到接收器板上。为此,请按照此教程中的步骤操作。
同时运行发射器和接收器会得到类似这样的结果。
成功了!🎉
先让它奏效,然后加快速度
如果你看一下上面的演示,你会发现传输速度相当慢。这不仅是因为数据传输需要很长时间,还因为灯光闪烁非常明显。Li-Fi 的目标是与现有的照明系统完全集成,以人眼无法察觉的方式传输数据。为此,我们需要加快传输和接收速度。
到目前为止,我在这篇文章中首先选择每 100 毫秒更新并读取一次灯的状态。使用 JavaScript,我能达到的最快速度是 60 毫秒。低于这个速度,检测似乎就失败了。这也在意料之中,因为我认为 JavaScript 并不是处理对时间要求极高的硬件项目的最佳工具。
我决定改用 Arduino 库,并在开发板上直接运行代码。
与 JavaScript 不同,Arduino 库是同步运行的,这意味着我不需要使用 setTimeout 等技巧来施加延迟。
如果您有兴趣查看 Arduino 代码,可以在我的GitHub 存储库中找到它。
这样,我成功地将间隔缩短到了 4 毫秒。由于每个字节都要应用 10 次延迟(一次用于前导码,一次用于每个比特,一次用于将灯恢复到初始状态),这意味着每 40 毫秒发送一个字符,字符串“Hello, world!”可以在 520 毫秒(约半秒)内传输完毕,而不是像 JavaScript 那样需要 7.8 秒!
在这个速度下,变速箱的顿挫感仍然很明显——但已经好多了!
为了达到肉眼不可见的程度,我需要尝试使用速度更快的微控制器、不同的光传感器以及整体方法,尤其因为我们的目标还是要利用这项技术传输图像和视频。
应用程序
Li-Fi并非一项新技术。2011年的这段精彩TED演讲和2015年的这段演讲都表明,研究人员已经在这方面投入了十多年的时间。
它具有一些优势,例如更快的数据传输能力、固有的安全性以及能源效率等等。
事实上,由于接收器设备需要直接放置在光源下方,因此可以确保数据不会被潜在的恶意攻击者截获,除非他们恰好处于接收器的视线范围内。此外,就节能而言,我们可以利用灯具作为路由器,而不是使用单独的照明和网络连接设备,从而节省电力。
此外,Li-Fi 不会像 Wi-Fi 那样干扰无线电信号,因此它有望成为提供更好机上互联网连接的解决方案。
在支付领域,终端设备可以配备光线检测传感器,让顾客使用手机闪光灯而非NFC芯片进行支付。这将使非接触式支付的距离超过目前NFC最大4厘米(1.5英寸)的限制,并提供额外的安全保障,防止驾车式攻击。
结论
在这篇文章中,我详细介绍了如何搭建一个小型原型,并使用 Arduino 和 JavaScript 实现通过光发送文本的实验。这个项目有很多方面都让我很感兴趣,我很想深入研究。除了尝试使用速度更快的微控制器之外,我还想尝试使用 MIMO(多输入多输出)技术传输视频文件,但这可以留待以后再做。
感谢阅读!
您可以通过以下平台了解 Stripe 开发者的最新动态: 📣在Twitter
上 关注@StripeDev和我们的团队 📺 订阅我们的YouTube 频道 💬 加入官方Discord 服务器 📧 注册订阅开发者简报
关于作者
查理·杰拉德是 Stripe 的一名开发者布道师,也是一位出版作家和富有创意的技术专家。她热爱研究和尝试各种技术。除了编程之外,她还喜欢户外活动、阅读以及给自己设定一些随机挑战。
文章来源:https://dev.to/4thzoa/from-wi-fi-to-li-fi-sending-data-via-light-using-arduino-and-javascript-4mpb








