Nerves从Hello World到点亮LED

官网 https://nerves-project.org/

文档 https://hexdocs.pm/nerves/getting-started.html


上一篇 Nerves环境配置 中我们在 Ubuntu 虚拟机上配置好了 Nerves 的开发环境。正所谓万事开头难,又所谓事事难开头。既然开了头,那就走两步。

如果是新安装 Erlang 和 Elixir 环境的话,首先我们需要安装 Elixir 和 Erlang 的包管理器:

elixir 复制代码
mix local.hex
mix local.rebar

hex 是 Elixir 的包管理器,rebar3 是 Erlang 的包管理器。上面的命令既可以用来安装 hexrebar3,也可以用来更新它们。

然后从 hex 源安装 nerves 包:

elixir 复制代码
mix archive.install hex nerves_bootstrap

严格来说,这里只是安装 nerves 的 mix 任务,而不是 nerves 库,这和 Phoenix 是一样的。之后我们也可以通过下面的命令来更新 nerves。

elixir 复制代码
mix local.nerves

Hello World

找一个合适的地方,在命令行运行下面的命令创建 nerves 工程:

elixir 复制代码
mix nerves.new hello_nerves

进入 hello_nerves 目录,可以看到它也只是一个普通的带监督树的 Elixir 工程而已。在开始之前,我们需要先生成 SSH 公钥,如果已有公钥的话,可以跳过这一步。

bash 复制代码
ssh-keygen -t rsa -b 4096 -C "your_email@example.com"

邮箱换成你自己的,然后一直回车保持默认选项就可以了,公钥会用于树莓派和主机之间的网络通信。如果这里你修改了公钥名称或者存放路径,可能会导致 nerves 找不到公钥,那时就需要去修改 config/target.exs 配置文件了,默认配置如下:

elixir 复制代码
keys =
  System.user_home!()
  |> Path.join(".ssh/id_{rsa,ecdsa,ed25519}.pub")
  |> Path.wildcard()

下一步下载依赖。

bash 复制代码
cd hello_nerves
MIX_TARGET=rpi4 mix deps.get

Nerves 需要通过环境变量指定目标平台,我的开发板是树莓派4,所以是 rpi4,其他平台可以参考官方文档👈点击直达👈。

环境变量也可以使用 export MIX_TARGET=rpi4 来设置。如果这一步报错,提示你"需要依赖 nerves",那可能就是 Erlang 版本太低了,参考Nerves环境配置安装一个最新版的 Erlang 应该就可以了。

接下来构建镜像。

bash 复制代码
MIX_TARGET=rpi4 mix firmware

如果是首次使用的话,需要将生成的镜像写到SD卡里面。取下树莓派上的SD卡,插入电脑里面,如果电脑没有SD卡接口,需要准备一个读卡器。将SD卡插入电脑后,用下面的命令写入固件。

bash 复制代码
MIX_TARGET=rpi4 mix burn

如果使用的是虚拟机的话,插入USB后,会有弹窗提示,选择将USB设备连接到虚拟机。固件烧录时,会自动识别出SD卡。

烧录完成后,取出SD卡插回树莓派,连上电源和HDMI显示器,系统开机后,屏幕上会显示一些信息和 iex> 命令提示符,这和我们在电脑上运行 iex 时是一样的。将键盘连接到树莓派,就可以和 Elixir 交互式 shell 愉快的玩耍了。

树莓派4的 type c 接口既可以供电,也支持数据传输,将他和电脑主机相连,就得到了一个树莓派和主机之间的USB网络连接。树莓派4既可以连接WiFi,也可以连接网线,Nerves 默认还提供了基于USB的网络接口,这在开发调试阶段非常有用。

Nerves 在树莓派和主机之间使用 mDNS 做为内网主机发现服务,我们不需要知道树莓派的具体IP地址,而是可以通过 nerves.local 主机名(在config/target.exs中配置)来访问树莓派。比如我们可以在虚拟机的 shell 中通过 ssh 来连接到树莓派。

bash 复制代码
ssh nerves.local

不出意外的话,我们会看到和连接到树莓派的屏幕上相同的内容。我们既可以在这里与树莓派交互,也可以将键盘连接到树莓派,选择一种你喜欢的方式就好。

我们在 iex shell 中输入 HelloNerves.hello() 回车,不出意外的话,会看到输出 :world,这正是 lib/hello_nerves.ex 的内容。

只要烧录过一次镜像,修改代码并运行 mix firmware 重新生成镜像之后,我们就不再需要取出SD卡 mix burn 了,而是可以直接 mix upload 上传新固件。

bash 复制代码
MIX_TARGET=rpi4 mix upload

点亮 LED

Nerves 官方文档是通过 GPIO 点亮外接 LED 灯,作为 Hello World 还有有点超纲了,所以我们先从点亮板载 LED 开始。

原理

Linux 将一切设备都抽象成了文件,板载 LED 也不例外。树莓派4有两颗板载 LED:一颗红色电源指示灯;一颗绿色SD卡活动指示灯。分别对应 /sys/class/leds/ 目录下的 PWRACT 两个目录。它俩都是软链接,链接到的是 /sys/devices/platform/leds/leds/ 目录下的 PWRACT 目录。

具体控制 LED 的是 PWRACT 目录下的文件。比如brightness 文件用来控制 LED 灯的亮度,它里面是一个数字,0 表示熄灭,1(或255) 表示点亮。trigger 文件用来控制 LED 灯的行为,比如闪烁,或者根据某些条件亮灭。打开 ACT 目录下的 trigger 文件,可以看到下面的内容:

复制代码
none rfkill-any rfkill-none timer oneshot heartbeat backlight default-on transient input panic pattern mmc1 [mmc0] rfkill0

它是一个空格分隔的 LED 行为列表,[] 表示当前选中的行为模式。

模式 说明
none 不触发任何模式,LED不会自动亮起或闪烁。
rfkill-any 当任何无线设备被禁用时,LED会亮起。
rfkill-none 当没有无线设备被禁用时,LED会亮起。
timer LED会按照设定的时间间隔闪烁。
oneshot LED会闪烁一次。
heartbeat LED会按照心跳模式闪烁。
backlight LED的亮度会根据背光设置变化。
default-on LED默认保持亮起。
transient LED会在特定事件发生时短暂亮起。
input LED会在接收到输入事件时亮起。
panic 在系统崩溃时,LED会闪烁。
pattern LED会按照预设的模式闪烁。
mmc1 当SD卡槽1有活动时,LED会亮起。
mmc0 表示当SD卡槽0有活动时,LED会亮起。
rfkill0 当无线设备0被禁用时,LED会亮起。

如果我们希望通过 brightness 文件来控制 LED 灯,需要将模式设置为 none

复制代码
echo none > brightness

brightness 文件比较特殊,对它写入 none 会自动变成 [none] ...

复制代码
[none] rfkill-any rfkill-none timer oneshot heartbeat backlight default-on transient input panic pattern mmc1 mmc0 rfkill0

LED控制

以上是板载 LED 控制的原理,对于编程,我们可以使用 nerves_leds 库,它封装了板载 LED 的控制接口。首先我们 mix.exs 文件中添加依赖:

elixir 复制代码
defp deps do
  [
    ... ...
    # Dependencies for all targets except :host
    {:nerves_leds, "~> 0.8", targets: @all_targets},
    ... ...
  ]
end

然后获取依赖:

bash 复制代码
MIX_TARGET=rpi4 mix deps.get

随后在 config.exs 文件中添加一行配置:

elixir 复制代码
config :nerves_leds, names: [green: "ACT"]

这里的配置是可选的,有这个配置,后面我们可以通过 :green 来访问 LED,不配置的话也可以直接通过 "ACT" 来访问这个 LED。

接下来我们用一个 GenServer 来控制 LED,在 lib/hello_nerves/ 目录下创建 blinker.ex 文件,输入以下内容:

elixir 复制代码
defmodule HelloNerves.Blinker do
  use GenServer

  require Logger
  alias Nerves.Leds

  def start_link(state \\ []) do
    GenServer.start_link(__MODULE__, state, name: __MODULE__)
  end

  def init(state) do
    enable()

    {:ok, state}
  end

  def handle_cast(:enable, state) do
    Logger.info("Enabling LED")
    Leds.set(green: true)

    {:noreply, state}
  end

  def handle_cast(:disable, state) do
    Logger.info("Disabling LED")
    Leds.set(green: false)

    {:noreply, state}
  end

  def enable() do
    GenServer.cast(__MODULE__, :enable)
  end

  def disable() do
    GenServer.cast(__MODULE__, :disable)
  end
end

这里我们通过 Leds.set 来控制 LED 灯的亮灭,true 表示点亮,false 表示熄灭。:green 就是前面我们配置的 LED 灯的名字。如果没配置,我们也可以直接使用 "ACT" 来访问它。最后我们在 lib/hello_nerves/application.ex 中将 HelloNerves.Blinker 添加到监督树。

elixir 复制代码
defp target_children() do
  [
    # Children for all targets except host
    # Starts a worker by calling: Target.Worker.start_link(arg)
    # {Target.Worker, arg},
    {HelloNerves.Blinker, name: HelloNerves.Blinker}
  ]
end

注意,Nerves 有两个监督树,我们要将它添加到设备端的监督树中。将树莓派链接到电脑,我们就可以重新编译并上传新固件了。

bash 复制代码
MIX_TARGET=rpi4 mix firmware
MIX_TARGET=rpi4 mix upload

等待上传完成,树莓派会重新启动,运行 ssh nerves.local 连接到树莓派,然后我们就可以通过 HelloNerves.Blinker.enable()HelloNerves.Blinker.disable() 来控制 LED 灯的亮灭了。

网络接口

既然 Nerves 也只是一个普通的 Elixir 工程而已,那么添加一个网络接口,通过网络远程遥控 LED 灯想必不是一件难事。编写 HTTP 接口需要用到 cowboy 库,首先还是添加依赖:

elixir 复制代码
defp deps do
  [
    # Dependencies for all targets
    {:plug_cowboy, "~> 2.0"},
    ... ...
  ]
end

然后获取依赖:

bash 复制代码
MIX_TARGET=rpi4 mix deps.get

lib/hello_nerves/ 下添加 http.ex 文件,输入以下内容:

elixir 复制代码
defmodule HelloNerves.Http do
  use Plug.Router

  plug(:match)
  plug(:dispatch)

  get("/", do: send_resp(conn, 200, "Feel free to use API endpoints!"))

  get "/enable" do
    HelloNerves.Blinker.enable()
    send_resp(conn, 200, "LED enabled")
  end

  get "/disable" do
    HelloNerves.Blinker.disable()
    send_resp(conn, 200, "LED disabled")
  end

  match(_, do: send_resp(conn, 404, "Oops!"))
end

同样,它也需要添加到监督树中:

elixir 复制代码
defp target_children() do
[
    # Children for all targets except host
    # Starts a worker by calling: Target.Worker.start_link(arg)
    # {Target.Worker, arg},
    {HelloNerves.Blinker, name: HelloNerves.Blinker},
    {Plug.Cowboy, scheme: :http, plug: HelloNerves.Http, options: [port: 80]}
  ]
end

最后编译并上传固件:

bash 复制代码
MIX_TARGET=rpi4 mix firmware
MIX_TARGET=rpi4 mix upload

上传完成后,打开浏览器,输入 nerves.local 回车,应该能看到 Feel free to use API endpoints! 的响应。

访问 nerves.local/enablenerves.local/disable 可以点亮或熄灭 LED。

既然 HTTP 接口可以,那么 Phoenix 和 Live View 呢?自然也不在话下,但那已经不是本期的主题了。

Nerves工程 vs Elixir工程

Nerves 工程就是一个带监督树的 Elixir 工程,所以从结构和语法来说,它和 Elixir 工程是没有区别的,只是内容上和我们常看到的 Elixir 工程不太一样。

首先是 mix.exs 的依赖部分,有些依赖多了 :target 选项,而且整个依赖库也被清晰的分成了三大部分:

  1. 通用依赖:平台和主机都需要的依赖;比如 plug_cowboy
  2. 平台依赖:除主机以外所有平台需要的依赖;比如 nerves_leds
  3. 特定平台依赖:特定于目标平台的依赖;比如目标平台预编译镜像 nerves_system_xxx

其次配置文件也分成了主机配置 host.exs 和目标平台配置 target.exs,由 config.exs 根据平台加载。

最后是 application.ex 中的监督树也分成了主机端 target_children 和目标平台端 target_children

这些只是在 Elixir 工程上做的一些小小的变化,整体来说,它还是一个带有监督树的 Elixir 工程。

iex vs bash

与普通 Linux 系统不同,Nerves 系统启动后,提供的用户交互接口并不是 bash,而是 iex。Nerves 定制的 Linux 系统并没有将 bash 包含进去,因此在 Nerves 系统中,bash 确实是无法使用的,Nerves 官网也说了,如果你一定需要 bash,那么 Nerves 将不会是你的最佳选择。虽然 bash 用不了,但是对于熟悉 Erlang 和 Elixir 的观众来说,iex 也未尝不利。

首先是 Erlang 的标准库 c 提供了一些命令行接口函数,这里 "c" 是 "command" 的缩写。比如我们熟知的 cdlspwd 等都是来自这个库。需要注意的是,这些都是 Erlang 函数,由于 Erlang 函数调用是可以省略括号的,因此对于无参函数,如 pwdls 我们可以像在 bash 中一样使用。但是对于有参数的情况就有所不同了,比如 cd "/bin"ls "/bin" 等,他们的参数是字符串,需要加上 ""

其次是 Toolshed 库也提供了一些常用的命令,使用之前需要先 use Toolshed 导入这个库,但是 Nerves 启动 iex 之前已经帮我们导入好了。它为我们提供了 unamepinguptimedatelsof 等常用命令,我们也可以通过 h Toolshed 来查看帮助文档。如果提示函数不存在,可以手动导入一下 Toolshed

最后还有兜底的 cmd 函数,Erlang 的 os 库和 Toolshed 都提供了这个函数,后者调用的是 Elixir 的 System.cmd/3cmd 也是函数,我们可以将想要调用的命令作为参数传递给它,但是要加上 "",因为它的参数是一个字符串。比如 cmd "ls -l"cmd "echo 255 > brightness" 等。

命令总结

命令 说明
mix nerves.new 生成 nerves 工程
mix firmware 构建镜像
mix firmware.gen.script 生成固件上传脚本 upload.sh
mix burn 烧录固件
mix upload 上传固件
相关推荐
无垠的广袤3 天前
ChatECNU 大语言模型与 PicoClaw 部署
人工智能·语言模型·自然语言处理·嵌入式·树莓派
无垠的广袤3 天前
【工业树莓派 CM0 NANO 单板计算机】MLX90640 热成像仪
linux·python·树莓派·传感器
Yogurt_cry6 天前
[树莓派4B] 闲置近10年的爱普生 L310 打印机爆改无线打印机
linux·物联网·树莓派
喾颛顼1 个月前
树莓派部署OpenClaw
树莓派·openclaw·bvc混合氛围编程
五蕴非空1 个月前
AI工具实践日记(一):在树莓派上搭建OpenClaw,一个后端开发者的真实踩坑记录
树莓派·ai助手·openclaw
Java烘焙师1 个月前
Java烘焙师的2025年总结
java·架构·树莓派
南风~~1 个月前
树莓派(Raspberry Pi )系统的烧录
嵌入式·树莓派
袁煦丞 cpolar内网穿透实验室1 个月前
精准模拟各种弱网场景!树莓派+ATC打造便携弱网网关。cpolar 内网穿透实验室第781个成功挑战
远程工作·树莓派·内网穿透·cpolar·弱网网关
爱吃肉的鹏2 个月前
树莓派上部署YOLOv5:从零实现实时目标检测
深度学习·yolo·树莓派