Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP PR] How to Deploy New Firmware issue #3 #4

Draft
wants to merge 7 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
/deps
erl_crash.dump
*.ez
.env
60 changes: 47 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,50 +2,74 @@

# Nerves LED Blinking tutorial

A complete tutorial of how to deploy a Nerves application on a Raspberry Pi
A complete beginners' tutorial showing
how to deploy an Internet of Things (IoT) project
to a Raspberry Pi
using the Nerves frawework.

</div>

<br></br>

## Why?

We need a reliable way of deploying


Nerves is a simple to use (IoT) framework
that is rock-solid thanks to it running on the BEAM (Erlang virtual machine).
However, in order to start building
and working on systems it's useful to have a knowledge
of how the whole framework fits together
and how it interacts with the wider Elixir ecosystem.

Although I have refered to Nerves as a framework, Nerves is better described as a **platform**, in that you write pure Elixir code and almost never
call Nerves functions. Nerves packages up your code and creates Linux firmware image that includes everything you need and nothing more. When the Raspberry
Pi has finished booting Nerves starts your Elixir application and its dependencies.
Although I have refered to Nerves as a framework,
Nerves is better described as a **platform**
in that you write pure Elixir code and almost never
call Nerves functions directly.
Nerves packages up your code and creates Linux firmware image
that includes everything you need and nothing more.
When the Raspberry Pi has finished booting
Nerves starts your Elixir application and its dependencies.

Nerves includes lots of features such as Over-The-Air firmware updates that means you can truly "fire and forget".
Nerves includes lots of features such as Over-The-Air firmware updates
that means you can easily update your application once it's deployed
to an inaccessible location.

## What?

This application is deliberately built with extra features expandability in mind. *There are simpler tutorials* for
Blinking lights, but we're aiming to give a broad overview of Nerves and Elixir.
This application is deliberately built
with extra features expandability in mind.
*There are simpler tutorials* for Blinking lights,
but we're aiming to give a broad overview of Nerves and Elixir.

A simple step-by-step that will show you how to:
- **Create** a Nerves application from scratch
- **Add** a simple module to blink an LED
- **Deploy** your application on a Raspberry Pi
- **Tweaking** your application and redeploying Over-The-Air

*For simple Nerves applications, thats it! But (**Intermediate knowledge of Elixir recommended)** we can also add a web-based GUI by*:
- **Refractoring** the blinking LED control so we can call it from another BEAM application
*For simple Nerves applications, thats it!
For more advanced Nerves apps, we can also add a web-based GUI by*:
- **Refactoring** the blinking LED control so we can call it from another BEAM application
- **Creating** a Nerves **poncho** project structure.
- **Creating** a simple Phoenix web application.
- **Implementing** a GUI that switches your light on and off.
- **Configuring** networking so you can access your application.

## Who?
This example is for people who are ***complete beginners*** with Nerves but some Elixir knowledge will be useful for understanding whats going on.
For the second part of the guide a basic knowledge of how `GenServers` and the BEAM works is recommened, although you should still be able to follow

This example is for people who are ***complete beginners*** with Nerves
but have some Elixir knowledge.
For the second part of the guide
a basic knowledge of how `GenServers` and the BEAM works is recommended,
although you should still be able to follow
along and work out whats going on.

If you get stuck, open an *issue* on this GitHub repository and we'll try and fix it. If you get stuck, it's probaly an issue with our guide!
If you get stuck, please open an
[*issue*](https://github.com/dwyl/learn-nerves/issues)
on this GitHub repository
and we'll try and help any way we can.

## How?

Expand Down Expand Up @@ -515,4 +539,14 @@ it the LED should start to blink!

# TODO: Add networking and GUI

See: https://github.com/dwyl/learn-nerves/issues/3
See: https://github.com/dwyl/learn-nerves/issues/3


## References and Recommended Reading


+ From 0 to 11 with Nerves:
https://nerves.build/posts/nerves-0-11
(last updated 2017, still has lots of useful info)
+ Get the /dev/tty??? reference for Raspberry PI plugged in via USB:
https://raspberrypi.stackexchange.com/questions/88079/get-dev-tty-for-raspberry-pi
3 changes: 3 additions & 0 deletions smart_led/.env_sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export MIX_TARGET=rpi0
export NERVES_NETWORK_SSID=wifinetwork
export NERVES_NETWORK_PSK=wifipassword
15 changes: 15 additions & 0 deletions smart_led/config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,18 @@ config :logger, backends: [RingLogger]
if Mix.target() != :host do
import_config "target.exs"
end


# https://elixirschool.com/en/lessons/advanced/nerves/#setting-up-networking
# Statically assign an address
config :nerves_network, :default,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be in config/target.exs so it only gets configured on the target device?

Also, I think nerves doesn't use nerves_network anymore and has switched to vintage_net, which means the config will be slightly different.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it seems the there's a default vintage_net config in config/target.exs.

I don't know why they changed it, nerves_network seems a lot simpler.

For reference, this is how I setup VintageNet on a previous project: https://github.com/th0mas/Fancy-Lights/blob/master/fancy_lights_firmware/config/target.exs#L44

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@th0mas thanks for sharing direct link to your working code. 🚀

eth0: [
ipv4_address_method: :static,
ipv4_address: "192.168.1.101",
ipv4_subnet_mask: "255.255.255.0",
nameservers: ["8.8.8.8", "8.8.4.4"]
],
wlan0: [
ssid: System.get_env("NERVES_NETWORK_SSID"),
psk: System.get_env("NERVES_NETWORK_PSK")
]
9 changes: 5 additions & 4 deletions smart_led/lib/smart_led/led_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,17 @@ defmodule SmartLed.LedController do
end

def handle_info(:blink, state) do
Process.send_after(self(), :blink, 2000)
blink_led()
delay = :rand.uniform(2000)
Process.send_after(self(), :blink, 2 * delay)
blink_led(delay)

{:noreply, state}
end

defp blink_led() do
defp blink_led(delay) do
{:ok, gpio} = GPIO.open(18, :output)
GPIO.write(gpio, 1)
:timer.sleep(1000)
:timer.sleep(delay)
GPIO.write(gpio, 0)
GPIO.close(gpio)
end
Expand Down
4 changes: 4 additions & 0 deletions smart_led/mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,13 @@ defmodule SmartLed.MixProject do
[
# Dependencies for all targets
{:nerves, "~> 1.6.0", runtime: false},
# https://github.com/nerves-project/shoehorn (terrible name. necessary to init VM)
{:shoehorn, "~> 0.6"},
# https://github.com/nerves-project/ring_logger in-memory logger with IEx access.
{:ring_logger, "~> 0.6"},
# https://github.com/fhunleth/toolshed collection of useful commands for IEx prompts
{:toolshed, "~> 0.2"},
# https://github.com/elixir-circuits/circuits_gpio control or read from GPIO pins.
{:circuits_gpio, "~> 0.4"},

# Dependencies for all targets except :host
Expand Down
154 changes: 154 additions & 0 deletions smart_led/upload.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
#!/bin/sh

#
# Upload new firmware to a target running nerves_firmware_ssh
#
# Usage:
# upload.sh [destination IP] [Path to .fw file]
#
# If unspecifed, the destination is nerves.local and the .fw file is naively
# guessed
#
# You may want to add the following to your `~/.ssh/config` to avoid recording
# the IP addresses of the target:
#
# Host nerves.local
# UserKnownHostsFile /dev/null
# StrictHostKeyChecking no
#
# The firmware update protocol is:
#
# 1. Connect to the nerves_firmware_ssh service running on port 8989
# 2. Send "fwup:$FILESIZE,reboot\n" where `$FILESIZE` is the size of the file
# being uploaded
# 3. Send the firmware file
# 4. The response from the device is a progress bar from fwup that can either
# be ignored or shown to the user.
# 5. The ssh connection is closed with an exit code to indicate success or
# failure
#
# Feel free to copy this script wherever is convenient. The template is at
# https://github.com/nerves-project/nerves_firmware_ssh/blob/master/priv/templates/script.upload.eex
#

set -e

DESTINATION=$1
FILENAME="$2"

help() {
echo
echo "upload.sh [destination IP] [Path to .fw file]"
echo
echo "Default destination IP is 'nerves.local'"
echo "Default firmware bundle is the first .fw file in '_build/\${MIX_TARGET}_\${MIX_ENV}/nerves/images'"
echo
echo "MIX_TARGET=$MIX_TARGET"
echo "MIX_ENV=$MIX_ENV"
exit 1
}

[ -n "$DESTINATION" ] || DESTINATION=nerves.local
[ -n "$MIX_TARGET" ] || MIX_TARGET=rpi0
[ -n "$MIX_ENV" ] || MIX_ENV=dev
if [ -z "$FILENAME" ]; then
FIRMWARE_PATH="./_build/${MIX_TARGET}_${MIX_ENV}/nerves/images"
if [ ! -d "$FIRMWARE_PATH" ]; then
# Try the Nerves 1.4 path if the user hasn't upgraded their mix.exs
FIRMWARE_PATH="./_build/${MIX_TARGET}/${MIX_TARGET}_${MIX_ENV}/nerves/images"
if [ ! -d "$FIRMWARE_PATH" ]; then
# Try the pre-Nerves 1.4 path
FIRMWARE_PATH="./_build/${MIX_TARGET}/${MIX_ENV}/nerves/images"
if [ ! -d "$FIRMWARE_PATH" ]; then
echo "Can't find the build products."
echo
echo "Nerves environment"
echo "MIX_TARGET: ${MIX_TARGET}"
echo "MIX_ENV: ${MIX_ENV}"
echo
echo "Make sure your Nerves environment is correct."
echo
echo "If the Nerves environment is correct make sure you have built the firmware"
echo "using 'mix firmware'."
echo
echo "If you are uploading a .fw file from a custom path you can specify the"
echo "path like so:"
echo
echo " $0 <device hostname or IP address> </path/to/my/firmware.fw>"
echo
exit 1
fi
fi
fi

FILENAME=$(ls "$FIRMWARE_PATH/"*.fw 2> /dev/null | head -n 1)
fi

[ -n "$FILENAME" ] || (echo "Error: error determining firmware bundle."; help)
[ -f "$FILENAME" ] || (echo "Error: can't find '$FILENAME'"; help)

# Check the flavor of stat for sending the filesize
if stat --version 2>/dev/null | grep GNU >/dev/null; then
# The QNU way
FILESIZE=$(stat -c%s "$FILENAME")
else
# Else default to the BSD way
FILESIZE=$(stat -f %z "$FILENAME")
fi

FIRMWARE_METADATA=$(fwup -m -i "$FILENAME" || echo "meta-product=Error reading metadata!")
FIRMWARE_PRODUCT=$(echo "$FIRMWARE_METADATA" | grep -E "^meta-product=" -m 1 2>/dev/null | cut -d '=' -f 2- | tr -d '"')
FIRMWARE_VERSION=$(echo "$FIRMWARE_METADATA" | grep -E "^meta-version=" -m 1 2>/dev/null | cut -d '=' -f 2- | tr -d '"')
FIRMWARE_PLATFORM=$(echo "$FIRMWARE_METADATA" | grep -E "^meta-platform=" -m 1 2>/dev/null | cut -d '=' -f 2- | tr -d '"')
FIRMWARE_UUID=$(echo "$FIRMWARE_METADATA" | grep -E "^meta-uuid=" -m 1 2>/dev/null | cut -d '=' -f 2- | tr -d '"')

echo "Path: $FILENAME"
echo "Product: $FIRMWARE_PRODUCT $FIRMWARE_VERSION"
echo "UUID: $FIRMWARE_UUID"
echo "Platform: $FIRMWARE_PLATFORM"
echo
echo "Uploading to $DESTINATION..."

# Don't fall back to asking for passwords, since that won't work
# and it's easy to misread the message thinking that it's asking
# for the private key password
SSH_OPTIONS="-o PreferredAuthentications=publickey"

if [ "$(uname -s)" = "Darwin" ]; then
DESTINATION_IP=$(arp -n $DESTINATION | sed 's/.* (\([0-9.]*\).*/\1/' || exit 0)
if [ -z "$DESTINATION_IP" ]; then
echo "Can't resolve $DESTINATION"
exit 1
fi
TEST_DESTINATION_IP=$(printf "$DESTINATION_IP" | head -n 1)
if [ "$DESTINATION_IP" != "$TEST_DESTINATION_IP" ]; then
echo "Multiple destination IP addresses for $DESTINATION found:"
echo "$DESTINATION_IP"
echo "Guessing the first one..."
DESTINATION_IP=$TEST_DESTINATION_IP
fi

IS_DEST_LL=$(echo $DESTINATION_IP | grep '^169\.254\.' || exit 0)
if [ -n "$IS_DEST_LL" ]; then
LINK_LOCAL_IP=$(ifconfig | grep 169.254 | sed 's/.*inet \([0-9.]*\) .*/\1/')
if [ -z "$LINK_LOCAL_IP" ]; then
echo "Can't find an interface with a link local address?"
exit 1
fi
TEST_LINK_LOCAL_IP=$(printf "$LINK_LOCAL_IP" | tail -n 1)
if [ "$LINK_LOCAL_IP" != "$TEST_LINK_LOCAL_IP" ]; then
echo "Multiple interfaces with link local addresses:"
echo "$LINK_LOCAL_IP"
echo "Guessing the last one, but YMMV..."
LINK_LOCAL_IP=$TEST_LINK_LOCAL_IP
fi

# If a link local address, then force ssh to bind to the link local IP
# when connecting. This fixes an issue where the ssh connection is bound
# to another Ethernet interface. The TCP SYN packet that goes out has no
# chance of working when this happens.
SSH_OPTIONS="$SSH_OPTIONS -b $LINK_LOCAL_IP"
fi
fi

printf "fwup:$FILESIZE,reboot\n" | cat - $FILENAME | ssh -s -p 8989 $SSH_OPTIONS $DESTINATION nerves_firmware_ssh