SnmpKit Interactive Tour ๐Ÿš€

Section

Welcome to SnmpKit v0.3.5!

This interactive Livebook will take you on a comprehensive tour of SnmpKit's new unified API. We'll start by creating a simulated SNMP device and then demonstrate all the powerful features against our own simulation - no external network required!

What you'll learn:

  • ๐ŸŽฏ Unified API - Clean, context-based modules
  • ๐Ÿ“ก SNMP Operations - get, walk, bulk, multi-target
  • ๐Ÿ“š MIB Management - resolution, compilation, tree navigation
  • ๐Ÿงช Device Simulation - realistic testing environments
  • โšก Advanced Features - streaming, performance, analytics

Let's get started! ๐Ÿš€

Setup

First, let's install SnmpKit and configure our environment:

Mix.install([
  {:snmpkit, "~> 0.3.5"}
])

# Configure logging for our tour
Logger.configure(level: :info)

# Import the unified API modules for convenience
alias SnmpKit.{SNMP, MIB, Sim}

IO.puts("๐ŸŽ‰ SnmpKit v0.3.5 loaded successfully!")
IO.puts("๐Ÿ“š Ready to explore the unified API!")

Chapter 1: Start Our Simulated Network ๐Ÿ–ฅ๏ธ

Before we can demonstrate SNMP operations, let's create our own simulated network! This is one of SnmpKit's most powerful features - realistic device simulation for testing and development.

Create a Cable Modem Simulation

# Create a realistic DOCSIS cable modem with essential OIDs
cable_modem_oids = %{
  # System Group
  "1.3.6.1.2.1.1.1.0" => "ARRIS SURFboard SB8200 DOCSIS 3.1 Cable Modem",
  "1.3.6.1.2.1.1.2.0" => "1.3.6.1.4.1.4115.1.20.1.1.2.25",
  "1.3.6.1.2.1.1.3.0" => %{type: "TimeTicks", value: 0},
  "1.3.6.1.2.1.1.4.0" => "admin@example.com",
  "1.3.6.1.2.1.1.5.0" => "cm-001",
  "1.3.6.1.2.1.1.6.0" => "Home Network",

  # Interface Group
  "1.3.6.1.2.1.2.1.0" => 2,
  "1.3.6.1.2.1.2.2.1.1.1" => 1,
  "1.3.6.1.2.1.2.2.1.1.2" => 2,
  "1.3.6.1.2.1.2.2.1.2.1" => "cable-downstream0",
  "1.3.6.1.2.1.2.2.1.2.2" => "cable-upstream0",
  "1.3.6.1.2.1.2.2.1.3.1" => 127,  # docsCableMaclayer
  "1.3.6.1.2.1.2.2.1.3.2" => 127,

  # DOCSIS Specific OIDs
  "1.3.6.1.2.1.10.127.1.1.1.1.3.2" => %{type: "INTEGER", value: 3},  # docsIfCmtsUpChannelId
  "1.3.6.1.2.1.10.127.1.1.1.1.6.2" => %{type: "INTEGER", value: 6400000},  # docsIfCmtsUpChannelFrequency
  "1.3.6.1.2.1.10.127.1.2.2.1.1.2" => %{type: "INTEGER", value: 1},  # docsIfCmStatusValue
  "1.3.6.1.2.1.10.127.1.2.2.1.12.2" => %{type: "Counter32", value: 1000},  # docsIfCmStatusUnerroreds

  # Cable Modem specific counters
  "1.3.6.1.2.1.2.2.1.10.1" => %{type: "Counter32", value: 1500000},  # ifInOctets
  "1.3.6.1.2.1.2.2.1.16.1" => %{type: "Counter32", value: 900000},   # ifOutOctets
  "1.3.6.1.2.1.2.2.1.11.1" => %{type: "Counter32", value: 12000},    # ifInUcastPkts
  "1.3.6.1.2.1.2.2.1.17.1" => %{type: "Counter32", value: 8000},     # ifOutUcastPkts
}

# Create the cable modem profile
{:ok, cable_modem_profile} = SnmpKit.SnmpSim.ProfileLoader.load_profile(
  :cable_modem,
  {:manual, cable_modem_oids},
  behaviors: [:counter_increment, :time_based_changes]
)

# Start our simulated cable modem on port 1161
{:ok, cable_modem} = Sim.start_device(cable_modem_profile, [
  port: 1161,
  community: "public"
])

# Define our target for easy reference
cable_modem_target = "127.0.0.1:1161"

IO.puts("โœ… Cable modem simulation started on #{cable_modem_target}")
IO.puts("๐ŸŽฏ Ready for SNMP operations!")

# Make target available to other cells
cable_modem_target

Create a Router Simulation

Let's also create a router simulation to demonstrate multi-target operations:

# Create a realistic enterprise router with essential OIDs
router_oids = %{
  # System Group
  "1.3.6.1.2.1.1.1.0" => "Cisco IOS Software, C2900 Software, Version 15.1(4)M12a",
  "1.3.6.1.2.1.1.2.0" => "1.3.6.1.4.1.9.1.576",
  "1.3.6.1.2.1.1.3.0" => %{type: "TimeTicks", value: 0},
  "1.3.6.1.2.1.1.4.0" => "IT Department <it@company.com>",
  "1.3.6.1.2.1.1.5.0" => "router-001",
  "1.3.6.1.2.1.1.6.0" => "Main Office - Server Room",

  # Interface Group (more interfaces for a router)
  "1.3.6.1.2.1.2.1.0" => 5,  # ifNumber (more interfaces)

  # FastEthernet0/0 (WAN)
  "1.3.6.1.2.1.2.2.1.1.1" => 1,
  "1.3.6.1.2.1.2.2.1.2.1" => "FastEthernet0/0",
  "1.3.6.1.2.1.2.2.1.3.1" => 6,  # ethernetCsmacd
  "1.3.6.1.2.1.2.2.1.5.1" => %{type: "Gauge32", value: 100000000},  # 100Mbps
  "1.3.6.1.2.1.2.2.1.8.1" => 1,  # up

  # FastEthernet0/1 (LAN)
  "1.3.6.1.2.1.2.2.1.1.2" => 2,
  "1.3.6.1.2.1.2.2.1.2.2" => "FastEthernet0/1",
  "1.3.6.1.2.1.2.2.1.3.2" => 6,
  "1.3.6.1.2.1.2.2.1.5.2" => %{type: "Gauge32", value: 100000000},
  "1.3.6.1.2.1.2.2.1.8.2" => 1,

  # Serial0/0/0 (WAN backup)
  "1.3.6.1.2.1.2.2.1.1.3" => 3,
  "1.3.6.1.2.1.2.2.1.2.3" => "Serial0/0/0",
  "1.3.6.1.2.1.2.2.1.3.3" => 22,  # propPointToPointSerial
  "1.3.6.1.2.1.2.2.1.5.3" => %{type: "Gauge32", value: 1544000},  # T1 speed
  "1.3.6.1.2.1.2.2.1.8.3" => 2,  # down (backup interface)

  # Loopback0
  "1.3.6.1.2.1.2.2.1.1.4" => 4,
  "1.3.6.1.2.1.2.2.1.2.4" => "Loopback0",
  "1.3.6.1.2.1.2.2.1.3.4" => 24,  # softwareLoopback
  "1.3.6.1.2.1.2.2.1.8.4" => 1,

  # Traffic counters for active interfaces
  "1.3.6.1.2.1.2.2.1.10.1" => %{type: "Counter32", value: 0},  # ifInOctets WAN
  "1.3.6.1.2.1.2.2.1.16.1" => %{type: "Counter32", value: 0},  # ifOutOctets WAN
  "1.3.6.1.2.1.2.2.1.10.2" => %{type: "Counter32", value: 0},  # ifInOctets LAN
  "1.3.6.1.2.1.2.2.1.16.2" => %{type: "Counter32", value: 0},  # ifOutOctets LAN

  # IP routing info
  "1.3.6.1.2.1.4.1.0" => 1,  # ipForwarding (enabled)
  "1.3.6.1.2.1.4.2.0" => 30, # ipDefaultTTL

  # SNMP community info
  "1.3.6.1.2.1.11.1.0" => %{type: "Counter32", value: 0},  # snmpInPkts
  "1.3.6.1.2.1.11.2.0" => %{type: "Counter32", value: 0},  # snmpOutPkts
}

# Create the router profile
{:ok, router_profile} = SnmpKit.SnmpSim.ProfileLoader.load_profile(
  :router,
  {:manual, router_oids},
  behaviors: [:counter_increment, :time_based_changes]
)

{:ok, router} = Sim.start_device(router_profile, [
  port: 1162,
  community: "public"
])

router_target = "127.0.0.1:1162"

IO.puts("โœ… Router simulation started on #{router_target}")
IO.puts("๐ŸŒ Now we have a complete simulated network!")
IO.puts("๐Ÿ“Š Loaded #{map_size(router_oids)} router OIDs")

# Make targets available to other cells
{cable_modem_target, router_target}

Quick Connectivity Test

Let's verify our simulated devices are responding:

# Get targets from previous cells
cable_modem_target = "127.0.0.1:1161"
router_target = "127.0.0.1:1162"

# Test cable modem
case SNMP.get(cable_modem_target, "sysDescr.0") do
  {:ok, description} ->
    IO.puts("๐Ÿ“ก Cable Modem: #{description}")
  {:error, reason} ->
    IO.puts("โŒ Cable modem error: #{inspect(reason)}")
end

# Test router
case SNMP.get(router_target, "sysDescr.0") do
  {:ok, description} ->
    IO.puts("๐Ÿ”€ Router: #{description}")
  {:error, reason} ->
    IO.puts("โŒ Router error: #{inspect(reason)}")
end

IO.puts("\n๐ŸŽ‰ Both devices are responding! Let's explore the API...")

Chapter 2: SnmpKit.SNMP - Protocol Operations ๐Ÿ“ก

Now let's explore the comprehensive SNMP operations available through SnmpKit.SNMP. All operations will work against our simulated devices!

Basic GET Operations

# Set targets for this cell
cable_modem_target = "127.0.0.1:1161"

IO.puts("=== Basic SNMP GET Operations ===\n")

# Standard GET operation
{:ok, system_desc} = SNMP.get(cable_modem_target, "sysDescr.0")
IO.puts("System Description: #{system_desc}")

# GET with type information
{:ok, {oid, type, value}} = SNMP.get_with_type(cable_modem_target, "sysUpTime.0")
IO.puts("System Uptime: #{value} (#{type}) at OID #{oid}")  # Remove Enum.join

# GET with pretty formatting
{:ok, formatted_uptime} = SNMP.get_pretty(cable_modem_target, "sysUpTime.0")
IO.puts("Formatted Uptime: #{formatted_uptime}")

# GET system contact
{:ok, contact} = SNMP.get(cable_modem_target, "sysContact.0")
IO.puts("System Contact: #{contact}")

WALK Operations

WALK operations traverse the SNMP tree to get multiple related values:

# Set targets for this cell
cable_modem_target = "127.0.0.1:1161"
router_target = "127.0.0.1:1162"

IO.puts("\n=== SNMP WALK Operations ===\n")

# Walk the system group
{:ok, system_info} = SNMP.walk(cable_modem_target, "1.3.6.1.2.1.1.1")
IO.puts("System group contains #{length(system_info)} objects:")

# Display first few system objects
system_info
|> Enum.take(5)
|> Enum.each(fn {oid, type, value} ->
  IO.puts("  #{oid} (#{type}) = #{inspect(value)}")
end)

# Walk with pretty formatting
{:ok, pretty_system} = SNMP.walk_pretty(cable_modem_target, "system")
IO.puts("\nPretty formatted system info:")
pretty_system
|> Enum.take(3)
|> Enum.each(fn {name, value} ->
  IO.puts("  #{name}: #{value}")
end)

Interface Information

Let's explore interface data, which is crucial for network monitoring:

# Set targets for this cell
cable_modem_target = "127.0.0.1:1161"
router_target = "127.0.0.1:1162"

IO.puts("\n=== Interface Information ===\n")

# Get interface count from cable modem
{:ok, cm_if_count} = SNMP.get(cable_modem_target, "ifNumber.0")
IO.puts("Cable Modem interfaces: #{cm_if_count}")

# Get interface count from router
{:ok, router_if_count} = SNMP.get(router_target, "ifNumber.0")
IO.puts("Router interfaces: #{router_if_count}")

# Get interface descriptions
{:ok, cm_if1_desc} = SNMP.get(cable_modem_target, "ifDescr.1")
{:ok, cm_if2_desc} = SNMP.get(cable_modem_target, "ifDescr.2")
IO.puts("\nCable Modem Interface Details:")
IO.puts("  Interface 1: #{cm_if1_desc}")
IO.puts("  Interface 2: #{cm_if2_desc}")

{:ok, router_if1_desc} = SNMP.get(router_target, "ifDescr.1")
{:ok, router_if2_desc} = SNMP.get(router_target, "ifDescr.2")
IO.puts("\nRouter Interface Details:")
IO.puts("  Interface 1: #{router_if1_desc}")
IO.puts("  Interface 2: #{router_if2_desc}")

Bulk Operations

For large amounts of data, bulk operations are much more efficient:

# Set targets for this cell
cable_modem_target = "127.0.0.1:1161"
router_target = "127.0.0.1:1162"

IO.puts("\n=== Bulk Operations ===\n")

# Standard bulk walk
{:ok, bulk_results} = SNMP.bulk_walk(cable_modem_target, "interfaces")
IO.puts("Bulk walk of interfaces returned #{length(bulk_results)} objects")

# Adaptive bulk walk (auto-optimizes performance)
{:ok, adaptive_results} = SNMP.adaptive_walk(cable_modem_target, "interfaces")
IO.puts("Adaptive walk returned #{length(adaptive_results)} objects")

# Get bulk with specific parameters
{:ok, bulk_specific} = SNMP.get_bulk(cable_modem_target, "interfaces", [
  max_repetitions: 5,
  timeout: 2000
])
IO.puts("Targeted bulk operation returned #{length(bulk_specific)} objects")

# Show some bulk results
bulk_results
|> Enum.take(3)
|> Enum.each(fn {oid, type, value} ->
  IO.puts("  #{oid} (#{type}): #{inspect(value)}")
end)

Multi-Target Operations

One of SnmpKit's powerful features is querying multiple devices simultaneously:

# Set targets for this cell
cable_modem_target = "127.0.0.1:1161"
router_target = "127.0.0.1:1162"

IO.puts("\n=== Multi-Target Operations ===\n")

# Query both devices for system information
multi_targets = [
  {cable_modem_target, "sysDescr.0"},
  {router_target, "sysDescr.0"},
  {cable_modem_target, "sysUpTime.0"},
  {router_target, "sysContact.0"}
]

multi_results = SNMP.get_multi(multi_targets)
IO.puts("Multi-target query results:")
IO.puts("Raw results: #{inspect(multi_results)}")

# Process results based on actual format
multi_results
|> Enum.with_index()
|> Enum.each(fn {result, index} ->
  {target, oid} = Enum.at(multi_targets, index)
  case result do
    {:ok, value} ->
      IO.puts("  โœ… #{target} #{oid}: #{inspect(value)}")
    {:error, reason} ->
      IO.puts("  โŒ #{target} #{oid}: #{inspect(reason)}")
    _ ->
      IO.puts("  ? #{target} #{oid}: #{inspect(result)}")
  end
end)

# Multi-target walk operations
walk_targets = [
  {cable_modem_target, "system"},
  {router_target, "system"}
]

multi_walk_results = SNMP.walk_multi(walk_targets)
IO.puts("\nMulti-target walk completed for #{length(multi_walk_results)} targets")

# Display walk results
multi_walk_results
|> Enum.with_index()
|> Enum.each(fn {result, index} ->
  {target, oid} = Enum.at(walk_targets, index)
  case result do
    {:ok, walk_data} ->
      IO.puts("  โœ… #{target} #{oid}: #{length(walk_data)} objects")
      # Show first few objects
      walk_data
      |> Enum.take(3)
      |> Enum.each(fn {obj_oid, type, value} ->
        oid_str = if is_list(obj_oid), do: Enum.join(obj_oid, "."), else: obj_oid
        IO.puts("    #{oid_str} (#{type}) = #{inspect(value)}")
      end)
    {:error, reason} ->
      IO.puts("  โŒ #{target} #{oid}: #{inspect(reason)}")
    _ ->
      IO.puts("  ? #{target} #{oid}: #{inspect(result)}")
  end
end)

Chapter 3: SnmpKit.MIB - MIB Management ๐Ÿ“š

The MIB (Management Information Base) system is the heart of SNMP. It defines the structure and meaning of SNMP data. Let's explore SnmpKit's powerful MIB capabilities!

OID Name Resolution

IO.puts("=== MIB Name Resolution ===\n")

# Resolve common SNMP object names to OIDs
common_objects = [
  "sysDescr.0",
  "sysUpTime.0",
  "sysContact.0",
  "ifNumber.0",
  "ifDescr.1",
  "ifInOctets.1",
  "system",
  "interfaces"
]

IO.puts("Common SNMP objects and their OIDs:")
Enum.each(common_objects, fn name ->
  case MIB.resolve(name) do
    {:ok, oid} ->
      IO.puts("  #{name} โ†’ #{Enum.join(oid, ".")}")
    {:error, reason} ->
      IO.puts("  #{name} โ†’ Error: #{reason}")
  end
end)

Reverse OID Lookup

IO.puts("\n=== Reverse OID Lookup ===\n")

# Convert OIDs back to names
test_oids = [
  [1, 3, 6, 1, 2, 1, 1, 1, 0],
  [1, 3, 6, 1, 2, 1, 1, 3, 0],
  [1, 3, 6, 1, 2, 1, 2, 1, 0],
  [1, 3, 6, 1, 2, 1, 2, 2, 1, 2, 1]
]

IO.puts("OID to name resolution:")
Enum.each(test_oids, fn oid ->
  case MIB.reverse_lookup(oid) do
    {:ok, name} ->
      IO.puts("  #{Enum.join(oid, ".")} โ†’ #{name}")
    {:error, reason} ->
      IO.puts("  #{Enum.join(oid, ".")} โ†’ #{reason}")
  end
end)

MIB Tree Navigation

IO.puts("\n=== MIB Tree Navigation ===\n")

# Get children of the system group
{:ok, system_oid} = MIB.resolve("system")
{:ok, system_children} = MIB.children(system_oid)

system_oid_str = if is_list(system_oid), do: Enum.join(system_oid, "."), else: inspect(system_oid)
IO.puts("System group (#{system_oid_str}) has #{length(system_children)} children:")
system_children
|> Enum.take(5)
|> Enum.each(fn child_oid ->
  oid_str = if is_list(child_oid), do: Enum.join(child_oid, "."), else: inspect(child_oid)
  case MIB.reverse_lookup(child_oid) do
    {:ok, name} ->
      IO.puts("  #{oid_str} (#{name})")
    {:error, _} ->
      IO.puts("  #{oid_str}")
  end
end)

# Get parent of a specific OID
{:ok, sys_descr_oid} = MIB.resolve("sysDescr.0")
{:ok, parent_oid} = MIB.parent(sys_descr_oid)
{:ok, parent_name} = MIB.reverse_lookup(parent_oid)
parent_oid_str = if is_list(parent_oid), do: Enum.join(parent_oid, "."), else: inspect(parent_oid)
IO.puts("\nParent of sysDescr.0: #{parent_oid_str} (#{parent_name})")

Chapter 4: Creating Custom Device Simulations ๐Ÿ› ๏ธ

One of the most powerful features of SnmpKit is creating realistic device simulations without needing walk files. Let's explore different approaches:

Enterprise Switch Simulation

IO.puts("=== Creating Enterprise Switch Simulation ===\n")

# Define a realistic 24-port enterprise switch
switch_oids = %{
  # System Group
  "1.3.6.1.2.1.1.1.0" => "Cisco IOS Software, C3560CX Software, Version 15.2(4)E10",
  "1.3.6.1.2.1.1.2.0" => "1.3.6.1.4.1.9.1.1208",
  "1.3.6.1.2.1.1.3.0" => %{type: "TimeTicks", value: 0},
  "1.3.6.1.2.1.1.4.0" => "Network Operations <netops@company.com>",
  "1.3.6.1.2.1.1.5.0" => "switch-core-01",
  "1.3.6.1.2.1.1.6.0" => "Main Office - Network Closet A",

  # Switch has many interfaces (24 ports + management)
  "1.3.6.1.2.1.2.1.0" => 25,  # ifNumber
}

# Add interfaces programmatically
switch_oids = Enum.reduce(1..24, switch_oids, fn port, acc ->
  Map.merge(acc, %{
    # Interface descriptions
    "1.3.6.1.2.1.2.2.1.2.#{port}" => "GigabitEthernet0/#{port}",
    "1.3.6.1.2.1.2.2.1.3.#{port}" => 6,  # ethernetCsmacd
    "1.3.6.1.2.1.2.2.1.5.#{port}" => %{type: "Gauge32", value: 1000000000},  # 1Gbps
    "1.3.6.1.2.1.2.2.1.8.#{port}" => if(port <= 12, do: 1, else: 2),  # First 12 up, rest down
    # Traffic counters for active ports
    "1.3.6.1.2.1.2.2.1.10.#{port}" => %{type: "Counter32", value: 0},
    "1.3.6.1.2.1.2.2.1.16.#{port}" => %{type: "Counter32", value: 0},
  })
end)

# Add management interface
switch_oids = Map.merge(switch_oids, %{
  "1.3.6.1.2.1.2.2.1.2.25" => "Management0",
  "1.3.6.1.2.1.2.2.1.3.25" => 6,
  "1.3.6.1.2.1.2.2.1.5.25" => %{type: "Gauge32", value: 100000000},  # 100Mbps
  "1.3.6.1.2.1.2.2.1.8.25" => 1,
})

# Create and start the switch
{:ok, switch_profile} = SnmpKit.SnmpSim.ProfileLoader.load_profile(
  :switch,
  {:manual, switch_oids},
  behaviors: [:counter_increment, :time_based_changes]
)

{:ok, switch_device} = Sim.start_device(switch_profile, port: 1163)
switch_target = "127.0.0.1:1163"

IO.puts("โœ… Enterprise switch simulation started on #{switch_target}")

# Test the switch
{:ok, switch_desc} = SNMP.get(switch_target, "sysDescr.0")
{:ok, switch_interfaces} = SNMP.get(switch_target, "ifNumber.0")
IO.puts("Switch: #{switch_desc}")
IO.puts("Interfaces: #{switch_interfaces}")

# Check a few interface statuses
Enum.each([1, 5, 15, 25], fn port ->
  case SNMP.get(switch_target, "ifDescr.#{port}") do
    {:ok, desc} ->
      case SNMP.get(switch_target, "ifOperStatus.#{port}") do
        {:ok, status} ->
          status_text = if status == 1, do: "UP", else: "DOWN"
          IO.puts("  Port #{port}: #{desc} - #{status_text}")
        _ -> nil
      end
    _ -> nil
  end
end)

Wireless Access Point Simulation

IO.puts("\n=== Creating Wireless Access Point Simulation ===\n")

# Define a realistic dual-band wireless AP
wireless_ap_oids = %{
  # System Group
  "1.3.6.1.2.1.1.1.0" => "Ubiquiti UniFi AP AC Pro, Version 4.3.21.11325",
  "1.3.6.1.2.1.1.2.0" => "1.3.6.1.4.1.41112.1.4.7",
  "1.3.6.1.2.1.1.3.0" => %{type: "TimeTicks", value: 0},
  "1.3.6.1.2.1.1.4.0" => "Wireless Admin <wireless@company.com>",
  "1.3.6.1.2.1.1.5.0" => "ap-lobby-01",
  "1.3.6.1.2.1.1.6.0" => "Main Building - Lobby",

  # Wireless interfaces: Management + 2.4GHz + 5GHz
  "1.3.6.1.2.1.2.1.0" => 3,

  # Management interface (Ethernet)
  "1.3.6.1.2.1.2.2.1.2.1" => "eth0 (Management)",
  "1.3.6.1.2.1.2.2.1.3.1" => 6,  # ethernetCsmacd
  "1.3.6.1.2.1.2.2.1.5.1" => %{type: "Gauge32", value: 1000000000},  # 1Gbps
  "1.3.6.1.2.1.2.2.1.8.1" => 1,

  # 2.4GHz radio
  "1.3.6.1.2.1.2.2.1.2.2" => "wlan0 (2.4GHz)",
  "1.3.6.1.2.1.2.2.1.3.2" => 71,  # ieee80211
  "1.3.6.1.2.1.2.2.1.5.2" => %{type: "Gauge32", value: 300000000},  # 300Mbps
  "1.3.6.1.2.1.2.2.1.8.2" => 1,

  # 5GHz radio
  "1.3.6.1.2.1.2.2.1.2.3" => "wlan1 (5GHz)",
  "1.3.6.1.2.1.2.2.1.3.3" => 71,  # ieee80211
  "1.3.6.1.2.1.2.2.1.5.3" => %{type: "Gauge32", value: 1300000000},  # 1.3Gbps
  "1.3.6.1.2.1.2.2.1.8.3" => 1,

  # Wireless-specific OIDs (simplified)
  "1.3.6.1.4.1.41112.1.4.1.1.4.1" => 6,    # 2.4GHz channel
  "1.3.6.1.4.1.41112.1.4.1.1.4.2" => 36,   # 5GHz channel
  "1.3.6.1.4.1.41112.1.4.1.1.5.1" => 20,   # TX power (dBm)
  "1.3.6.1.4.1.41112.1.4.1.1.5.2" => 23,   # TX power (dBm)
  "1.3.6.1.4.1.41112.1.4.1.1.6.1" => 15,   # Connected clients 2.4GHz
  "1.3.6.1.4.1.41112.1.4.1.1.6.2" => 8,    # Connected clients 5GHz

  # Traffic counters
  "1.3.6.1.2.1.2.2.1.10.2" => %{type: "Counter32", value: 0},  # 2.4GHz RX
  "1.3.6.1.2.1.2.2.1.16.2" => %{type: "Counter32", value: 0},  # 2.4GHz TX
  "1.3.6.1.2.1.2.2.1.10.3" => %{type: "Counter32", value: 0},  # 5GHz RX
  "1.3.6.1.2.1.2.2.1.16.3" => %{type: "Counter32", value: 0}   # 5GHz TX
}

# Create the wireless AP profile
{:ok, wireless_ap_profile} = SnmpKit.SnmpSim.ProfileLoader.load_profile(
  :wireless_ap,
  {:manual, wireless_ap_oids},
  behaviors: [:counter_increment, :time_based_changes]
)

# NOTE: If you get port conflicts, restart your Livebook runtime to clean up previous devices

# Try to find an available port starting from 1164
{wireless_ap, wireless_ap_target} =
  Enum.reduce_while(1164..1170, nil, fn port, _acc ->
    case Sim.start_device(wireless_ap_profile, [port: port, community: "public"]) do
      {:ok, device} ->
        target = "127.0.0.1:#{port}"
        {:halt, {device, target}}
      {:error, :eaddrinuse} ->
        IO.puts("Port #{port} in use, trying next...")
        {:cont, nil}
      {:error, reason} ->
        IO.puts("Port #{port} failed: #{inspect(reason)}")
        {:cont, nil}
    end
  end) ||
  raise "Could not find available port for wireless AP. Try restarting runtime."

IO.puts("โœ… Wireless AP simulation started on #{wireless_ap_target}")

# Test the wireless AP
{:ok, ap_desc} = SNMP.get(wireless_ap_target, "sysDescr.0")
{:ok, ap_interfaces} = SNMP.get(wireless_ap_target, "ifNumber.0")
IO.puts("Wireless AP: #{ap_desc}")
IO.puts("Interfaces: #{ap_interfaces}")

# Check wireless-specific data
case SNMP.get(wireless_ap_target, "1.3.6.1.4.1.41112.1.4.1.1.6.1") do
  {:ok, clients_24} -> IO.puts("2.4GHz Clients: #{clients_24}")
  _ -> IO.puts("2.4GHz Clients: Not available")
end

case SNMP.get(wireless_ap_target, "1.3.6.1.4.1.41112.1.4.1.1.6.2") do
  {:ok, clients_5} -> IO.puts("5GHz Clients: #{clients_5}")
  _ -> IO.puts("5GHz Clients: Not available")
end

wireless_ap_target

Congratulations! ๐ŸŽ‰

You've completed the SnmpKit Interactive Tour! You've learned how to:

  • ๐ŸŽฏ Use the Unified API - Clean, context-based modules for different operations
  • ๐Ÿ“ก Perform SNMP Operations - GET, WALK, bulk operations, and multi-target queries
  • ๐Ÿ“š Work with MIBs - Resolve OIDs, navigate the MIB tree, and understand SNMP data
  • ๐Ÿงช Create Device Simulations - Build realistic test environments without real hardware
  • โšก Leverage Advanced Features - Streaming, performance optimization, and analytics

Next Steps

  1. Explore the Documentation: https://hexdocs.pm/snmpkit
  2. Try the Examples: Check out the examples/ directory for more practical use cases
  3. Read the Guides:
  4. Build Something Cool: Use SnmpKit in your own projects!

Community

Happy SNMP monitoring with SnmpKit! ๐Ÿš€