# `NervesHubLink.Client`
[🔗](https://github.com/nerves-hub/nerves_hub_link/blob/v2.12.0/lib/nerves_hub_link/client.ex#L11)

The primary integration point for customizing your applications connection with [NervesHub](https://github.com/nerves-hub/nerves_hub_web).

The following callbacks are supported:

- `c:archive_available/1` - an archive is available to download from NervesHub
- `c:archive_ready/2` - an archive has been downloaded and is available for use
- `c:connected/0` - a connection to NervesHub has been established
- `c:firmware_auto_revert_detected?/0` - checks if a firmware revert occurred
- `c:firmware_validated?/0` - checks if the current firmware has been validated
- `c:handle_error/1` - a firmware update has failed
- `c:handle_fwup_message/1` - a message has been received by `NervesHubLink.UpdateManager`
- `c:identify/0` - a request received from NervesHub to identify the device (eg. blink leds)
- `c:reboot/0` - a request received from NervesHub to reboot the device
- `c:reconnect_backoff/0` - how NervesHubLink should handle reconnection backoffs
- `c:update_available/1` - should a firmware update be applied

A default Client is included (`NervesHubLink.Client.Default`) which `:apply`s firmware
updates, `:ignore`s archives, logs firmware update messages, and logs a message when
the `identify/0` callback is used.

The recommended way to implement your own `Client` is to create your own module and add
`use NervesHubLink.Client`, which will allow you to use the same defaults included in
`NervesHubLink.Client.Default`, while also being able to customize any of the current
callback implementations.

Otherwise you can add `@behaviour NervesHubLink.Client`, but you will need to implement
the following required functions:

- `c:archive_available/1`
- `c:archive_ready/2`
- `c:handle_error/1`
- `c:handle_fwup_message/1`
- `c:identify/0`
- `c:update_available/1`

# Example

```elixir
defmodule MyApp.NervesHubLinkClient do
  use NervesHubLink.Client

  # override only the functions you want to customize

  @impl NervesHubLink.Client
  def update_available(data) do
    if SomeInternalAPI.is_now_a_good_time_to_update?(data) do
      :apply
    else
      {:reschedule, 60_000}
    end
  end
end
```

To have NervesHubLink use your client, add the following to your `config.exs`:

```elixir
config :nerves_hub_link, client: MyApp.NervesHubLinkClient
```

# `archive_data`

```elixir
@type archive_data() :: NervesHubLink.Message.ArchiveInfo.t()
```

Archive that comes over a socket.

# `archive_response`

```elixir
@type archive_response() :: :download | :ignore | {:reschedule, pos_integer()}
```

Supported responses from `archive_available/1`

# `fwup_message`

```elixir
@type fwup_message() ::
  {:ok, non_neg_integer(), String.t()}
  | {:warning, non_neg_integer(), String.t()}
  | {:error, non_neg_integer(), String.t()}
  | {:progress, 0..100}
```

Firmware update progress, completion or error report

# `update_data`

```elixir
@type update_data() :: NervesHubLink.Message.UpdateInfo.t()
```

Update that comes over a socket.

# `update_response`

```elixir
@type update_response() ::
  :apply
  | :ignore
  | {:ignore, String.t()}
  | {:reschedule, pos_integer()}
  | {:reschedule, pos_integer(), String.t()}
```

Supported responses from `update_available/1`

# `archive_available`

```elixir
@callback archive_available(archive_data()) :: archive_response()
```

Called when an archive is available for download

May return one of:

* `download` - Download the archive right now
* `ignore` - Don't download this archive.
* `{:reschedule, timeout}` - Defer making a decision. Call this function again in `timeout` milliseconds.

# `archive_ready`

```elixir
@callback archive_ready(archive_data(), Path.t()) :: :ok
```

Called when an archive has been downloaded and is available for the application to do something

# `connected`
*optional* 

```elixir
@callback connected() :: any()
```

Called when the connection to NervesHub has been established.

The return value of this function is not checked.

# `firmware_auto_revert_detected?`
*optional* 

```elixir
@callback firmware_auto_revert_detected?() :: boolean()
```

Optional callback to check if an auto firmware revert just occurred.

The default behavior is to check if the previous firmware slots:
- `nerves_fw_validated` value is `0`
- and `nerves_fw_platform` is not empty
- and `nerves_fw_architecture` is not empty

If there is custom logic built into `fwup-ops.conf` around `prevent-revert`, this should be
reflected here.

# `firmware_validated?`
*optional* 

```elixir
@callback firmware_validated?() :: boolean()
```

Optional callback to check if the current firmware has been validated.

The default behavior is to delegate to `Nerves.Runtime.firmware_valid?/0`.

If there is custom logic built into your `fwup.conf` and `fwup-ops.conf`
files, you should implement this callback in your `NervesHubLink.Client`.

# `handle_error`

```elixir
@callback handle_error(any()) :: :ok
```

Called when downloading a firmware update fails.

The return value of this function is not checked.

# `handle_fwup_message`

```elixir
@callback handle_fwup_message(fwup_message()) :: :ok
```

Called on firmware update reports.

The return value of this function is not checked.

# `identify`

```elixir
@callback identify() :: :ok
```

Callback to identify the device from NervesHub.

# `reboot`
*optional* 

```elixir
@callback reboot() :: no_return()
```

Optional callback to reboot the device when a firmware update completes

The default behavior is to call `Nerves.Runtime.reboot/0` after a successful update. This
is useful for testing and for doing additional work like notifying users in a UI that a reboot
will happen soon. It is critical that a reboot does happen.

# `reconnect_backoff`
*optional* 

```elixir
@callback reconnect_backoff() :: [integer()] | nil
```

Optional callback when the socket disconnected, before starting to reconnect.

The return value is used to reset the next socket's retry timeout. `nil` asks NervesHubLink
to calculate a set of random backoffs to use.

You may wish to use this to dynamically change the reconnect backoffs. For instance,
during a NervesHub deploy you may wish to change the reconnect based on your
own logic to not create a thundering herd of reconnections. If you have a particularly
flaky connection you can increase how fast the reconnect happens to avoid overloading
your server.

# `update_available`

```elixir
@callback update_available(update_data()) :: update_response()
```

Called to find out what to do when a firmware update is available.

May return one of:

* `apply` - Download and apply the update right now.
* `ignore` - Don't download and apply this update.
* `{:reschedule, timeout}` - Defer making a decision. Call this function again in `timeout` milliseconds.

# `archive_available`

```elixir
@spec archive_available(archive_data()) :: archive_response()
```

# `archive_ready`

```elixir
@spec archive_ready(archive_data(), Path.t()) :: :ok
```

# `connected`

```elixir
@spec connected() :: any()
```

# `firmware_auto_revert_detected?`

```elixir
@spec firmware_auto_revert_detected?() :: boolean()
```

The common logic to determine if an auto revert occurred is to check if the previous
firmware is not validated. This is because, for example, if a device boots into
firmware slot A and isn't able to validate the slot within the initialization
callback time, the device will reboot into the previous firmware slot, B, and now
firmware slot A will be shown as not validated.

We also need to account for the logic used by `prevent-revert` in `fwup-ops.conf`,
which can be different/custom per Nerves system. The common pattern is to unset
`nerves_fw_platform` and `nerves_fw_architecture`.

The default implementation checks if the previous firmware slot is not validated,
and that `nerves_fw_platform` and `nerves_fw_architecture` are not empty.

Clears platform and architecture uboot env vars
- https://github.com/nerves-project/nerves_system_rpi4/blob/main/fwup-ops.conf#L51-L52
- https://github.com/nerves-project/nerves_system_rpi5/blob/main/fwup-ops.conf#L51-L52

Clears platform, architecture, and validated uboot env vars
- https://github.com/nerves-project/nerves_system_rpi4/blob/tryboot-compatible/fwup-ops.conf#L50-L55

# `firmware_validated?`

```elixir
@spec firmware_validated?() :: boolean()
```

A wrapper function which calls `firmware_validated?/0` on the configured `NervesHubLink.Client`.

If the function isn't implemented, the default logic of delegating to
`Nerves.Runtime.firmware_valid?/0` is used.

# `handle_error`

```elixir
@spec handle_error(any()) :: :ok
```

This function is called internally by NervesHubLink to notify clients of fwup errors.

# `handle_fwup_message`

```elixir
@spec handle_fwup_message(fwup_message()) :: :ok
```

This function is called internally by NervesHubLink to notify clients of fwup progress.

# `identify`

```elixir
@spec identify() :: :ok
```

This function is called internally by NervesHubLink to identify a device.

# `initiate_reboot`

```elixir
@spec initiate_reboot() :: :ok
```

This function is called internally by NervesHubLink to initiate a reboot.

After a successful firmware update, NervesHubLink calls this to start the
reboot process. It calls `c:reboot/0` if supplied or
`Nerves.Runtime.reboot/0`.

# `reconnect_backoff`

```elixir
@spec reconnect_backoff() :: [integer()]
```

This function is called internally by NervesHubLink to notify clients of disconnects.

# `update_available`

```elixir
@spec update_available(update_data()) :: update_response()
```

This function is called internally by NervesHubLink to notify clients.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
