Powered by AppSignal & Oban Pro

Connection Request Server: ACS-Initiated Device Contact

08_connection_request_server.livemd

Connection Request Server: ACS-Initiated Device Contact

> This notebook demonstrates the ConnectionRequestServer for ACS-initiated connections.

Setup

Mix.install([
  {:caretaker, path: "."}
])

alias Caretaker.CPE.{ConnectionRequestServer, Fleet, DynamicBehavior}
require Logger

Start Connection Request Server (No Auth)

The ConnectionRequestServer provides a single HTTP endpoint for ACS-initiated connection requests. Devices are identified by path: /cr/:serial_number

{:ok, server} = ConnectionRequestServer.start_link(
  port: 7547,
  on_connection_request: fn serial_number ->
    Logger.info("Connection request received for: #{serial_number}")
    :ok
  end
)

# Get server URLs
base_url = ConnectionRequestServer.base_url(server)
device_url = ConnectionRequestServer.device_url(server, "DEVICE001")

Logger.info("Server running at: #{base_url}")
Logger.info("Device URL example: #{device_url}")

%{base_url: base_url, device_url: device_url}

Test Health Check Endpoint

{:ok, resp} = Finch.build(:get, "http://localhost:7547/health")
  |> Finch.request(Caretaker.Finch)

Logger.info("Health check: #{resp.status}")
resp.body

Test Connection Request (Device Not Found)

When the server can’t find the device, it returns 404.

{:ok, resp} = Finch.build(:get, "http://localhost:7547/cr/UNKNOWN_DEVICE")
  |> Finch.request(Caretaker.Finch)

Logger.info("Unknown device response: #{resp.status}")
{resp.status, resp.body}

Stop First Server

ConnectionRequestServer.stop(server)
Logger.info("Server stopped")
:ok

Start Server with Authentication

The server supports Basic and Digest authentication.

{:ok, auth_server} = ConnectionRequestServer.start_link(
  port: 7548,
  auth: %{username: "admin", password: "secret123"},
  on_connection_request: fn serial_number ->
    Logger.info("Authenticated request for: #{serial_number}")
    :ok
  end
)

Logger.info("Authenticated server running on port 7548")
ConnectionRequestServer.base_url(auth_server)

Test Unauthenticated Request (Should Fail)

{:ok, resp} = Finch.build(:get, "http://localhost:7548/cr/DEVICE001")
  |> Finch.request(Caretaker.Finch)

Logger.info("Unauthenticated response: #{resp.status}")

# Should be 401 Unauthorized
{resp.status, Enum.find(resp.headers, fn {k, _} -> k == "www-authenticate" end)}

Test with Basic Authentication

# Create Basic auth header
credentials = Base.encode64("admin:secret123")
auth_header = {"authorization", "Basic #{credentials}"}

{:ok, resp} = Finch.build(:get, "http://localhost:7548/cr/DEVICE001", [auth_header])
  |> Finch.request(Caretaker.Finch)

Logger.info("Authenticated response: #{resp.status}")
{resp.status, resp.body}

Stop Auth Server

ConnectionRequestServer.stop(auth_server)
Logger.info("Auth server stopped")
:ok

Integration with Fleet

The most powerful use case is integrating the connection request server with a Fleet.

# Start a fleet of devices
{:ok, fleet} = Fleet.start_link(
  acs_url: "http://localhost:4000/cwmp",
  count: 5,
  oui_prefix: "FLEET",
  connection_delay: 0,
  behaviors: [value_change_events: true]
)

Fleet.spawn_devices(fleet)
devices = Fleet.list_devices(fleet)
Logger.info("Fleet devices: #{inspect(devices)}")

devices

Trigger Connection Request via Fleet

The Fleet module provides trigger_connection_request/2 to add a “6 CONNECTION REQUEST” event.

# Pick a device
[device_serial | _] = Fleet.list_devices(fleet)

# Trigger connection request event
:ok = Fleet.trigger_connection_request(fleet, device_serial)
Logger.info("Triggered connection request for: #{device_serial}")

# Verify the event was added
{:ok, device_info} = Fleet.get_device(fleet, device_serial)
behavior_pid = device_info.behavior

if behavior_pid do
  events = DynamicBehavior.pending_events(behavior_pid)
  Logger.info("Pending events: #{inspect(events)}")
  events
else
  Logger.info("No behavior manager attached")
  []
end

Test Error Handling

# Try to trigger on non-existent device
result = Fleet.trigger_connection_request(fleet, "NONEXISTENT")
Logger.info("Non-existent device result: #{inspect(result)}")

result

Cleanup

Fleet.stop_all(fleet)
Logger.info("Fleet stopped")
:ok

Full Integration Example

This example shows a complete setup with ACS server, Fleet, and Connection Request Server working together.

# This would be a full integration in production:
# 
# 1. Start ACS Server on port 4000
# 2. Start Connection Request Server on port 7547
# 3. Start Fleet with devices that report ConnectionRequestURL
# 4. ACS sends HTTP GET to /cr/:serial_number
# 5. Device receives "6 CONNECTION REQUEST" event
# 6. Device initiates new Inform session to ACS
#
# Example flow:
#   ACS -> GET http://cpe-host:7547/cr/FLEET-000001
#   CPE -> POST http://acs:4000/cwmp (Inform with "6 CONNECTION REQUEST")
#   ACS -> InformResponse
#   ... normal session continues

Logger.info("See test/connection_request_server_test.exs for full integration tests")
:ok