1564 Packet Capture Analyzer
Interactive Wireshark-Style Network Capture Analysis for IoT Protocols
animation
networking
protocols
wireshark
packet-analysis
mqtt
coap
ble
zigbee
1564.1 Packet Capture Analyzer
This interactive tool simulates network packet capture analysis, similar to Wireshark. Analyze IoT protocol traffic, inspect packet details at each protocol layer, and understand network communication patterns.
1564.2 Learning Objectives
By using this interactive tool, you will be able to:
- Analyze packet captures: Read and interpret network traffic like a protocol analyst
- Understand protocol layers: See how Application, Transport, Network, and Link layers encapsulate data
- Identify IoT protocols: Recognize MQTT, CoAP, HTTP, BLE, and Zigbee packet structures
- Filter traffic: Apply display filters to isolate specific conversations
- Detect anomalies: Spot retransmissions, errors, and suspicious patterns
- Visualize conversations: Understand request-response sequences through diagrams
NoteTool Overview
The Packet Capture Analyzer provides a Wireshark-like experience for learning IoT protocols:
- Packet List View: Scrollable table of captured packets with key information
- Packet Details Panel: Protocol layer breakdown with field-by-field parsing
- Hex Dump View: Raw packet bytes with ASCII interpretation
- Display Filters: Protocol, source, destination, and time filters
- Statistics Dashboard: Protocol distribution, top talkers, conversation pairs
- Scenario Library: Pre-built captures for common IoT scenarios
- Sequence Diagrams: Visual request-response flow representation
TipHow to Use This Tool
- Select a scenario from the library (e.g., “Normal MQTT Session”)
- Browse packets in the list view - click a row to inspect
- Examine details in the protocol breakdown panel
- Apply filters to narrow down traffic of interest
- View statistics to understand traffic patterns
- Check the sequence diagram to visualize message flow
- Hover over fields for educational explanations
Show code
// ============================================
// Packet Capture Analyzer
// Wireshark-Style IoT Protocol Analysis Tool
// ============================================
{
// IEEE Color Palette
const colors = {
navy: "#2C3E50",
teal: "#16A085",
orange: "#E67E22",
gray: "#7F8C8D",
lightGray: "#ECF0F1",
white: "#FFFFFF",
red: "#E74C3C",
green: "#27AE60",
purple: "#9B59B6",
yellow: "#F1C40F",
blue: "#3498DB",
darkGray: "#34495E",
pink: "#E91E63",
cyan: "#00BCD4",
lime: "#8BC34A",
indigo: "#3F51B5"
};
// Protocol color mapping
const protocolColors = {
mqtt: colors.purple,
coap: colors.teal,
http: colors.blue,
ble: colors.cyan,
zigbee: colors.green,
tcp: colors.orange,
udp: colors.lime,
ipv4: colors.navy,
ethernet: colors.gray
};
// Protocol definitions with layer information
const protocolInfo = {
mqtt: {
name: "MQTT",
fullName: "Message Queuing Telemetry Transport",
layer: "Application",
port: 1883,
description: "Lightweight pub/sub messaging protocol for IoT",
color: protocolColors.mqtt
},
coap: {
name: "CoAP",
fullName: "Constrained Application Protocol",
layer: "Application",
port: 5683,
description: "RESTful protocol for constrained devices over UDP",
color: protocolColors.coap
},
http: {
name: "HTTP",
fullName: "Hypertext Transfer Protocol",
layer: "Application",
port: 80,
description: "Standard web protocol for request-response communication",
color: protocolColors.http
},
ble: {
name: "BLE",
fullName: "Bluetooth Low Energy",
layer: "Application/Link",
port: null,
description: "Short-range wireless for wearables and sensors",
color: protocolColors.ble
},
zigbee: {
name: "Zigbee",
fullName: "IEEE 802.15.4 Zigbee",
layer: "Application/Network",
port: null,
description: "Mesh networking for home automation and industrial IoT",
color: protocolColors.zigbee
}
};
// Pre-built capture scenarios
const scenarios = {
mqtt_normal: {
name: "Normal MQTT Session",
description: "Complete MQTT connection with publish/subscribe",
icon: "chat",
packets: generateMQTTSession()
},
coap_retransmit: {
name: "CoAP with Retransmissions",
description: "CoAP confirmable messages with packet loss",
icon: "refresh",
packets: generateCoAPRetransmit()
},
ble_discovery: {
name: "BLE Device Discovery",
description: "BLE advertising and connection setup",
icon: "bluetooth",
packets: generateBLEDiscovery()
},
zigbee_join: {
name: "Zigbee Network Join",
description: "Zigbee device association and data exchange",
icon: "mesh",
packets: generateZigbeeJoin()
},
http_rest: {
name: "HTTP REST API",
description: "RESTful sensor data retrieval",
icon: "api",
packets: generateHTTPRest()
},
mixed_traffic: {
name: "Mixed IoT Traffic",
description: "Multiple protocols in a smart home",
icon: "home",
packets: generateMixedTraffic()
}
};
// Packet generation functions
function generateMQTTSession() {
const baseTime = Date.now() - 5000;
return [
createPacket(baseTime, "192.168.1.100", "192.168.1.1", "mqtt", "TCP", 1883, {
type: "CONNECT",
info: "CONNECT Client: sensor-001, Clean Session",
details: {
packetType: 1,
flags: 0x02,
protocolName: "MQTT",
protocolLevel: 4,
keepAlive: 60,
clientId: "sensor-001",
cleanSession: true
},
payload: null,
isRequest: true
}),
createPacket(baseTime + 15, "192.168.1.1", "192.168.1.100", "mqtt", "TCP", 1883, {
type: "CONNACK",
info: "CONNACK Return Code: 0 (Accepted)",
details: {
packetType: 2,
sessionPresent: false,
returnCode: 0,
returnCodeText: "Connection Accepted"
},
payload: null,
isRequest: false
}),
createPacket(baseTime + 50, "192.168.1.100", "192.168.1.1", "mqtt", "TCP", 1883, {
type: "SUBSCRIBE",
info: "SUBSCRIBE Topic: sensors/+/status, QoS: 1",
details: {
packetType: 8,
packetId: 1,
topics: [{ topic: "sensors/+/status", qos: 1 }]
},
payload: null,
isRequest: true
}),
createPacket(baseTime + 65, "192.168.1.1", "192.168.1.100", "mqtt", "TCP", 1883, {
type: "SUBACK",
info: "SUBACK Packet ID: 1, Granted QoS: 1",
details: {
packetType: 9,
packetId: 1,
grantedQos: [1]
},
payload: null,
isRequest: false
}),
createPacket(baseTime + 200, "192.168.1.100", "192.168.1.1", "mqtt", "TCP", 1883, {
type: "PUBLISH",
info: "PUBLISH Topic: sensors/temp/data, QoS: 1",
details: {
packetType: 3,
dup: false,
qos: 1,
retain: false,
topic: "sensors/temp/data",
packetId: 2
},
payload: '{"temperature": 23.5, "humidity": 65}',
isRequest: true
}),
createPacket(baseTime + 215, "192.168.1.1", "192.168.1.100", "mqtt", "TCP", 1883, {
type: "PUBACK",
info: "PUBACK Packet ID: 2",
details: {
packetType: 4,
packetId: 2
},
payload: null,
isRequest: false
}),
createPacket(baseTime + 500, "192.168.1.1", "192.168.1.100", "mqtt", "TCP", 1883, {
type: "PUBLISH",
info: "PUBLISH Topic: sensors/door/status, QoS: 1",
details: {
packetType: 3,
dup: false,
qos: 1,
retain: false,
topic: "sensors/door/status",
packetId: 3
},
payload: '{"state": "closed", "battery": 85}',
isRequest: true
}),
createPacket(baseTime + 515, "192.168.1.100", "192.168.1.1", "mqtt", "TCP", 1883, {
type: "PUBACK",
info: "PUBACK Packet ID: 3",
details: {
packetType: 4,
packetId: 3
},
payload: null,
isRequest: false
}),
createPacket(baseTime + 1000, "192.168.1.100", "192.168.1.1", "mqtt", "TCP", 1883, {
type: "PINGREQ",
info: "PINGREQ Keep-alive ping",
details: {
packetType: 12
},
payload: null,
isRequest: true
}),
createPacket(baseTime + 1010, "192.168.1.1", "192.168.1.100", "mqtt", "TCP", 1883, {
type: "PINGRESP",
info: "PINGRESP Keep-alive response",
details: {
packetType: 13
},
payload: null,
isRequest: false
}),
createPacket(baseTime + 2000, "192.168.1.100", "192.168.1.1", "mqtt", "TCP", 1883, {
type: "DISCONNECT",
info: "DISCONNECT Clean disconnect",
details: {
packetType: 14
},
payload: null,
isRequest: true
})
];
}
function generateCoAPRetransmit() {
const baseTime = Date.now() - 5000;
return [
createPacket(baseTime, "192.168.1.50", "192.168.1.200", "coap", "UDP", 5683, {
type: "CON GET",
info: "CON GET /temperature MID:0x1234",
details: {
version: 1,
type: 0,
tokenLength: 2,
code: "0.01",
codeText: "GET",
messageId: 0x1234,
token: "0xABCD",
options: [{ number: 11, name: "Uri-Path", value: "temperature" }]
},
payload: null,
isRequest: true
}),
createPacket(baseTime + 2000, "192.168.1.50", "192.168.1.200", "coap", "UDP", 5683, {
type: "CON GET",
info: "CON GET /temperature MID:0x1234 [Retransmission]",
details: {
version: 1,
type: 0,
tokenLength: 2,
code: "0.01",
codeText: "GET",
messageId: 0x1234,
token: "0xABCD",
options: [{ number: 11, name: "Uri-Path", value: "temperature" }],
retransmission: true,
retransmitCount: 1
},
payload: null,
isRequest: true,
isRetransmit: true
}),
createPacket(baseTime + 2050, "192.168.1.200", "192.168.1.50", "coap", "UDP", 5683, {
type: "ACK 2.05",
info: "ACK 2.05 Content MID:0x1234",
details: {
version: 1,
type: 2,
tokenLength: 2,
code: "2.05",
codeText: "Content",
messageId: 0x1234,
token: "0xABCD",
options: [{ number: 12, name: "Content-Format", value: "50 (JSON)" }]
},
payload: '{"temp": 22.3, "unit": "C"}',
isRequest: false
}),
createPacket(baseTime + 3000, "192.168.1.50", "192.168.1.200", "coap", "UDP", 5683, {
type: "CON PUT",
info: "CON PUT /led MID:0x1235",
details: {
version: 1,
type: 0,
tokenLength: 2,
code: "0.03",
codeText: "PUT",
messageId: 0x1235,
token: "0xBCDE",
options: [
{ number: 11, name: "Uri-Path", value: "led" },
{ number: 12, name: "Content-Format", value: "0 (text/plain)" }
]
},
payload: "on",
isRequest: true
}),
createPacket(baseTime + 3100, "192.168.1.200", "192.168.1.50", "coap", "UDP", 5683, {
type: "ACK 2.04",
info: "ACK 2.04 Changed MID:0x1235",
details: {
version: 1,
type: 2,
tokenLength: 2,
code: "2.04",
codeText: "Changed",
messageId: 0x1235,
token: "0xBCDE"
},
payload: null,
isRequest: false
}),
createPacket(baseTime + 4000, "192.168.1.50", "192.168.1.200", "coap", "UDP", 5683, {
type: "NON GET",
info: "NON GET /humidity MID:0x1236",
details: {
version: 1,
type: 1,
tokenLength: 2,
code: "0.01",
codeText: "GET",
messageId: 0x1236,
token: "0xCDEF",
options: [{ number: 11, name: "Uri-Path", value: "humidity" }]
},
payload: null,
isRequest: true
}),
createPacket(baseTime + 4080, "192.168.1.200", "192.168.1.50", "coap", "UDP", 5683, {
type: "NON 2.05",
info: "NON 2.05 Content MID:0x5678",
details: {
version: 1,
type: 1,
tokenLength: 2,
code: "2.05",
codeText: "Content",
messageId: 0x5678,
token: "0xCDEF",
options: [{ number: 12, name: "Content-Format", value: "50 (JSON)" }]
},
payload: '{"humidity": 58, "unit": "%"}',
isRequest: false
}),
createPacket(baseTime + 5000, "192.168.1.50", "192.168.1.200", "coap", "UDP", 5683, {
type: "CON POST",
info: "CON POST /data MID:0x1237",
details: {
version: 1,
type: 0,
tokenLength: 2,
code: "0.02",
codeText: "POST",
messageId: 0x1237,
token: "0xDEF0",
options: [
{ number: 11, name: "Uri-Path", value: "data" },
{ number: 12, name: "Content-Format", value: "50 (JSON)" }
]
},
payload: '{"sensor": "motion", "detected": true}',
isRequest: true
}),
createPacket(baseTime + 5120, "192.168.1.200", "192.168.1.50", "coap", "UDP", 5683, {
type: "ACK 2.01",
info: "ACK 2.01 Created MID:0x1237",
details: {
version: 1,
type: 2,
tokenLength: 2,
code: "2.01",
codeText: "Created",
messageId: 0x1237,
token: "0xDEF0",
options: [{ number: 8, name: "Location-Path", value: "/data/123" }]
},
payload: null,
isRequest: false
})
];
}
function generateBLEDiscovery() {
const baseTime = Date.now() - 5000;
return [
createPacket(baseTime, "AA:BB:CC:DD:EE:01", "FF:FF:FF:FF:FF:FF", "ble", "LL", null, {
type: "ADV_IND",
info: "ADV_IND Advertising: IoT-Sensor-01",
details: {
pduType: "ADV_IND",
txAddress: "AA:BB:CC:DD:EE:01",
txAddType: "Public",
advData: [
{ type: 0x01, name: "Flags", value: "0x06 (LE General, BR/EDR not supported)" },
{ type: 0x09, name: "Complete Local Name", value: "IoT-Sensor-01" },
{ type: 0x03, name: "Complete 16-bit UUIDs", value: "0x180F (Battery), 0x181A (Environment)" }
],
rssi: -45
},
payload: null,
isRequest: true
}),
createPacket(baseTime + 100, "AA:BB:CC:DD:EE:01", "FF:FF:FF:FF:FF:FF", "ble", "LL", null, {
type: "ADV_IND",
info: "ADV_IND Advertising: IoT-Sensor-01",
details: {
pduType: "ADV_IND",
txAddress: "AA:BB:CC:DD:EE:01",
txAddType: "Public",
advData: [
{ type: 0x01, name: "Flags", value: "0x06" },
{ type: 0x09, name: "Complete Local Name", value: "IoT-Sensor-01" }
],
rssi: -47
},
payload: null,
isRequest: true
}),
createPacket(baseTime + 150, "11:22:33:44:55:66", "AA:BB:CC:DD:EE:01", "ble", "LL", null, {
type: "SCAN_REQ",
info: "SCAN_REQ from Central",
details: {
pduType: "SCAN_REQ",
scannerAddress: "11:22:33:44:55:66",
scannerAddType: "Random",
advAddress: "AA:BB:CC:DD:EE:01"
},
payload: null,
isRequest: true
}),
createPacket(baseTime + 151, "AA:BB:CC:DD:EE:01", "11:22:33:44:55:66", "ble", "LL", null, {
type: "SCAN_RSP",
info: "SCAN_RSP Extended data",
details: {
pduType: "SCAN_RSP",
txAddress: "AA:BB:CC:DD:EE:01",
advData: [
{ type: 0xFF, name: "Manufacturer Data", value: "Company: 0x004C (Apple), Data: 0x0215..." },
{ type: 0x0A, name: "TX Power Level", value: "-10 dBm" }
]
},
payload: null,
isRequest: false
}),
createPacket(baseTime + 300, "11:22:33:44:55:66", "AA:BB:CC:DD:EE:01", "ble", "LL", null, {
type: "CONNECT_IND",
info: "CONNECT_IND Initiating connection",
details: {
pduType: "CONNECT_IND",
initiatorAddress: "11:22:33:44:55:66",
advAddress: "AA:BB:CC:DD:EE:01",
accessAddress: "0x8E89BED6",
connInterval: "50ms",
slaveLatency: 0,
supervisionTimeout: "2000ms"
},
payload: null,
isRequest: true
}),
createPacket(baseTime + 400, "11:22:33:44:55:66", "AA:BB:CC:DD:EE:01", "ble", "ATT", null, {
type: "ATT",
info: "ATT Exchange MTU Request, MTU: 247",
details: {
opcode: 0x02,
opcodeName: "Exchange MTU Request",
clientMtu: 247
},
payload: null,
isRequest: true
}),
createPacket(baseTime + 420, "AA:BB:CC:DD:EE:01", "11:22:33:44:55:66", "ble", "ATT", null, {
type: "ATT",
info: "ATT Exchange MTU Response, MTU: 247",
details: {
opcode: 0x03,
opcodeName: "Exchange MTU Response",
serverMtu: 247
},
payload: null,
isRequest: false
}),
createPacket(baseTime + 500, "11:22:33:44:55:66", "AA:BB:CC:DD:EE:01", "ble", "ATT", null, {
type: "ATT",
info: "ATT Read By Group Type Request (Primary Services)",
details: {
opcode: 0x10,
opcodeName: "Read By Group Type Request",
startHandle: "0x0001",
endHandle: "0xFFFF",
uuid: "0x2800 (Primary Service)"
},
payload: null,
isRequest: true
}),
createPacket(baseTime + 540, "AA:BB:CC:DD:EE:01", "11:22:33:44:55:66", "ble", "ATT", null, {
type: "ATT",
info: "ATT Read By Group Type Response (3 services)",
details: {
opcode: 0x11,
opcodeName: "Read By Group Type Response",
attributeDataLength: 6,
services: [
{ startHandle: "0x0001", endHandle: "0x0005", uuid: "0x1800 (Generic Access)" },
{ startHandle: "0x0006", endHandle: "0x000A", uuid: "0x180F (Battery Service)" },
{ startHandle: "0x000B", endHandle: "0x0015", uuid: "0x181A (Environmental Sensing)" }
]
},
payload: null,
isRequest: false
}),
createPacket(baseTime + 700, "11:22:33:44:55:66", "AA:BB:CC:DD:EE:01", "ble", "ATT", null, {
type: "ATT",
info: "ATT Read Request (Battery Level)",
details: {
opcode: 0x0A,
opcodeName: "Read Request",
handle: "0x0008"
},
payload: null,
isRequest: true
}),
createPacket(baseTime + 730, "AA:BB:CC:DD:EE:01", "11:22:33:44:55:66", "ble", "ATT", null, {
type: "ATT",
info: "ATT Read Response: 87%",
details: {
opcode: 0x0B,
opcodeName: "Read Response",
value: "0x57 (87%)"
},
payload: null,
isRequest: false
})
];
}
function generateZigbeeJoin() {
const baseTime = Date.now() - 5000;
return [
createPacket(baseTime, "0xFFFF", "0x0000", "zigbee", "802.15.4", null, {
type: "Beacon Request",
info: "MAC Beacon Request (Broadcast)",
details: {
frameType: "MAC Command",
srcPanId: "0xFFFF",
dstPanId: "0xFFFF",
srcAddr: "0xFFFF",
dstAddr: "0xFFFF",
commandId: 0x07,
commandName: "Beacon Request"
},
payload: null,
isRequest: true
}),
createPacket(baseTime + 50, "0x0000", "0xFFFF", "zigbee", "802.15.4", null, {
type: "Beacon",
info: "MAC Beacon PAN: 0x1234, Coordinator",
details: {
frameType: "Beacon",
srcPanId: "0x1234",
srcAddr: "0x0000",
beaconOrder: 15,
superframeOrder: 15,
permitJoining: true,
routerCapacity: true,
endDeviceCapacity: true
},
payload: null,
isRequest: false
}),
createPacket(baseTime + 200, "0xFFFF", "0x0000", "zigbee", "NWK", null, {
type: "Association Request",
info: "MAC Association Request to Coordinator",
details: {
frameType: "MAC Command",
srcPanId: "0xFFFF",
dstPanId: "0x1234",
srcAddr: "00:11:22:33:44:55:66:77",
dstAddr: "0x0000",
commandId: 0x01,
commandName: "Association Request",
capabilityInfo: {
alternatePanCoord: false,
deviceType: "FFD",
powerSource: "Battery",
rxOnWhenIdle: false,
securityCapability: true,
allocateAddress: true
}
},
payload: null,
isRequest: true
}),
createPacket(baseTime + 300, "0x0000", "0x1234", "zigbee", "NWK", null, {
type: "Association Response",
info: "MAC Association Response, Addr: 0x1234",
details: {
frameType: "MAC Command",
srcPanId: "0x1234",
dstPanId: "0x1234",
srcAddr: "0x0000",
dstAddr: "00:11:22:33:44:55:66:77",
commandId: 0x02,
commandName: "Association Response",
shortAddress: "0x1234",
associationStatus: "Association Successful"
},
payload: null,
isRequest: false
}),
createPacket(baseTime + 500, "0x1234", "0x0000", "zigbee", "APS", null, {
type: "Device Announce",
info: "ZDP Device Announce, NWK: 0x1234",
details: {
frameType: "ZDP",
clusterId: "0x0013",
clusterName: "Device_annce",
nwkAddress: "0x1234",
ieeeAddress: "00:11:22:33:44:55:66:77",
capability: {
alternatePanCoord: false,
deviceType: "Router",
powerSource: "Battery",
rxOnWhenIdle: false
}
},
payload: null,
isRequest: true
}),
createPacket(baseTime + 700, "0x1234", "0x0000", "zigbee", "ZCL", null, {
type: "Report Attributes",
info: "ZCL Report: Temperature 23.5C",
details: {
frameType: "ZCL",
frameControl: 0x18,
sequenceNumber: 1,
commandId: 0x0A,
commandName: "Report Attributes",
clusterId: "0x0402",
clusterName: "Temperature Measurement",
attributes: [
{ id: "0x0000", name: "MeasuredValue", type: "int16", value: 2350, formatted: "23.5 C" }
]
},
payload: null,
isRequest: true
}),
createPacket(baseTime + 800, "0x0000", "0x1234", "zigbee", "ZCL", null, {
type: "Default Response",
info: "ZCL Default Response: Success",
details: {
frameType: "ZCL",
frameControl: 0x18,
sequenceNumber: 1,
commandId: 0x0B,
commandName: "Default Response",
responseCommandId: 0x0A,
statusCode: 0x00,
statusText: "Success"
},
payload: null,
isRequest: false
}),
createPacket(baseTime + 1000, "0x1234", "0x0000", "zigbee", "ZCL", null, {
type: "Read Attributes",
info: "ZCL Read Attributes: On/Off State",
details: {
frameType: "ZCL",
frameControl: 0x00,
sequenceNumber: 2,
commandId: 0x00,
commandName: "Read Attributes",
clusterId: "0x0006",
clusterName: "On/Off",
attributes: [{ id: "0x0000", name: "OnOff" }]
},
payload: null,
isRequest: true
}),
createPacket(baseTime + 1080, "0x0000", "0x1234", "zigbee", "ZCL", null, {
type: "Read Attributes Response",
info: "ZCL Read Response: OnOff = 0x01 (On)",
details: {
frameType: "ZCL",
frameControl: 0x18,
sequenceNumber: 2,
commandId: 0x01,
commandName: "Read Attributes Response",
attributes: [
{ id: "0x0000", name: "OnOff", status: "Success", type: "bool", value: 1, formatted: "On" }
]
},
payload: null,
isRequest: false
})
];
}
function generateHTTPRest() {
const baseTime = Date.now() - 5000;
return [
createPacket(baseTime, "192.168.1.100", "192.168.1.1", "http", "TCP", 80, {
type: "GET",
info: "GET /api/sensors HTTP/1.1",
details: {
method: "GET",
uri: "/api/sensors",
version: "HTTP/1.1",
headers: {
"Host": "192.168.1.1",
"Accept": "application/json",
"Authorization": "Bearer eyJhbGc...",
"User-Agent": "IoT-Client/1.0"
}
},
payload: null,
isRequest: true
}),
createPacket(baseTime + 50, "192.168.1.1", "192.168.1.100", "http", "TCP", 80, {
type: "200 OK",
info: "HTTP/1.1 200 OK (application/json)",
details: {
status: 200,
statusText: "OK",
version: "HTTP/1.1",
headers: {
"Content-Type": "application/json",
"Content-Length": "156",
"Cache-Control": "no-cache",
"X-Request-Id": "abc123"
}
},
payload: '[{"id":1,"type":"temperature","value":23.5},{"id":2,"type":"humidity","value":65}]',
isRequest: false
}),
createPacket(baseTime + 200, "192.168.1.100", "192.168.1.1", "http", "TCP", 80, {
type: "POST",
info: "POST /api/sensors/1/data HTTP/1.1",
details: {
method: "POST",
uri: "/api/sensors/1/data",
version: "HTTP/1.1",
headers: {
"Host": "192.168.1.1",
"Content-Type": "application/json",
"Content-Length": "45",
"Authorization": "Bearer eyJhbGc..."
}
},
payload: '{"timestamp":1704067200,"temperature":24.2}',
isRequest: true
}),
createPacket(baseTime + 280, "192.168.1.1", "192.168.1.100", "http", "TCP", 80, {
type: "201 Created",
info: "HTTP/1.1 201 Created",
details: {
status: 201,
statusText: "Created",
version: "HTTP/1.1",
headers: {
"Content-Type": "application/json",
"Location": "/api/sensors/1/data/456",
"Content-Length": "32"
}
},
payload: '{"id":456,"status":"recorded"}',
isRequest: false
}),
createPacket(baseTime + 500, "192.168.1.100", "192.168.1.1", "http", "TCP", 80, {
type: "PUT",
info: "PUT /api/sensors/1/config HTTP/1.1",
details: {
method: "PUT",
uri: "/api/sensors/1/config",
version: "HTTP/1.1",
headers: {
"Host": "192.168.1.1",
"Content-Type": "application/json",
"Content-Length": "38"
}
},
payload: '{"interval":30,"unit":"celsius"}',
isRequest: true
}),
createPacket(baseTime + 580, "192.168.1.1", "192.168.1.100", "http", "TCP", 80, {
type: "200 OK",
info: "HTTP/1.1 200 OK",
details: {
status: 200,
statusText: "OK",
version: "HTTP/1.1",
headers: {
"Content-Type": "application/json",
"Content-Length": "52"
}
},
payload: '{"message":"Configuration updated","sensor_id":1}',
isRequest: false
}),
createPacket(baseTime + 800, "192.168.1.100", "192.168.1.1", "http", "TCP", 80, {
type: "DELETE",
info: "DELETE /api/sensors/2 HTTP/1.1",
details: {
method: "DELETE",
uri: "/api/sensors/2",
version: "HTTP/1.1",
headers: {
"Host": "192.168.1.1",
"Authorization": "Bearer eyJhbGc..."
}
},
payload: null,
isRequest: true
}),
createPacket(baseTime + 860, "192.168.1.1", "192.168.1.100", "http", "TCP", 80, {
type: "204 No Content",
info: "HTTP/1.1 204 No Content",
details: {
status: 204,
statusText: "No Content",
version: "HTTP/1.1",
headers: {}
},
payload: null,
isRequest: false
})
];
}
function generateMixedTraffic() {
const baseTime = Date.now() - 5000;
const mqttPackets = generateMQTTSession().slice(0, 6);
const coapPackets = generateCoAPRetransmit().slice(0, 4);
const blePackets = generateBLEDiscovery().slice(0, 4);
mqttPackets.forEach((p, i) => p.timestamp = baseTime + i * 150);
coapPackets.forEach((p, i) => p.timestamp = baseTime + 100 + i * 200);
blePackets.forEach((p, i) => p.timestamp = baseTime + 50 + i * 250);
const all = [...mqttPackets, ...coapPackets, ...blePackets];
all.sort((a, b) => a.timestamp - b.timestamp);
all.forEach((p, i) => p.id = i + 1);
return all;
}
function createPacket(timestamp, src, dst, protocol, transport, port, appData) {
const id = Math.floor(Math.random() * 100000);
const frameLength = 60 + Math.floor(Math.random() * 200);
return {
id: id,
timestamp: timestamp,
relativeTime: 0,
source: src,
destination: dst,
protocol: protocol,
transport: transport,
port: port,
length: frameLength,
info: appData.info,
appData: appData,
layers: buildProtocolLayers(protocol, transport, src, dst, port, appData, frameLength),
hexDump: generateHexDump(frameLength),
isRetransmit: appData.isRetransmit || false,
isError: appData.isError || false
};
}
function buildProtocolLayers(protocol, transport, src, dst, port, appData, frameLength) {
const layers = [];
if (protocol === "ble") {
layers.push({
name: "Bluetooth Low Energy Link Layer",
shortName: "BLE LL",
color: protocolColors.ble,
expanded: false,
fields: [
{ name: "Access Address", value: "0x8E89BED6", description: "Advertising channel access address" },
{ name: "PDU Type", value: appData.details.pduType || "DATA", description: "Protocol Data Unit type" },
{ name: "TxAdd", value: appData.details.txAddType || "Public", description: "Transmitter address type" },
{ name: "Length", value: `${frameLength - 10} bytes`, description: "PDU length" },
{ name: "CRC", value: "0x123456", description: "24-bit CRC" }
]
});
} else if (protocol === "zigbee") {
layers.push({
name: "IEEE 802.15.4",
shortName: "802.15.4",
color: protocolColors.zigbee,
expanded: false,
fields: [
{ name: "Frame Type", value: appData.details.frameType || "Data", description: "MAC frame type" },
{ name: "Dest PAN ID", value: appData.details.dstPanId || "0x1234", description: "Destination PAN identifier" },
{ name: "Dest Address", value: dst, description: "Destination address" },
{ name: "Src PAN ID", value: appData.details.srcPanId || "0x1234", description: "Source PAN identifier" },
{ name: "Src Address", value: src, description: "Source address" },
{ name: "Sequence Number", value: Math.floor(Math.random() * 256), description: "Frame sequence" },
{ name: "FCS", value: "0xABCD", description: "Frame Check Sequence" }
]
});
} else {
layers.push({
name: "Ethernet II",
shortName: "Ethernet",
color: protocolColors.ethernet,
expanded: false,
fields: [
{ name: "Destination MAC", value: "00:11:22:33:44:55", description: "Destination hardware address" },
{ name: "Source MAC", value: "66:77:88:99:AA:BB", description: "Source hardware address" },
{ name: "Type", value: "0x0800 (IPv4)", description: "Ethertype field" }
]
});
layers.push({
name: "Internet Protocol Version 4",
shortName: "IPv4",
color: protocolColors.ipv4,
expanded: false,
fields: [
{ name: "Version", value: "4", description: "IP version" },
{ name: "Header Length", value: "20 bytes", description: "IHL field" },
{ name: "Total Length", value: `${frameLength - 14} bytes`, description: "Total datagram length" },
{ name: "Identification", value: `0x${Math.floor(Math.random() * 65536).toString(16)}`, description: "Fragment identification" },
{ name: "TTL", value: "64", description: "Time to live" },
{ name: "Protocol", value: transport === "TCP" ? "6 (TCP)" : "17 (UDP)", description: "Upper layer protocol" },
{ name: "Source IP", value: src, description: "Source address" },
{ name: "Destination IP", value: dst, description: "Destination address" },
{ name: "Header Checksum", value: "0x1234 [valid]", description: "Header checksum" }
]
});
if (transport === "TCP") {
layers.push({
name: "Transmission Control Protocol",
shortName: "TCP",
color: protocolColors.tcp,
expanded: false,
fields: [
{ name: "Source Port", value: `${40000 + Math.floor(Math.random() * 10000)}`, description: "Source port number" },
{ name: "Destination Port", value: `${port}`, description: "Destination port number" },
{ name: "Sequence Number", value: Math.floor(Math.random() * 1000000), description: "Sequence number" },
{ name: "Ack Number", value: Math.floor(Math.random() * 1000000), description: "Acknowledgment number" },
{ name: "Flags", value: "PSH, ACK", description: "TCP flags" },
{ name: "Window", value: "65535", description: "Window size" },
{ name: "Checksum", value: "0x5678 [valid]", description: "TCP checksum" }
]
});
} else if (transport === "UDP") {
layers.push({
name: "User Datagram Protocol",
shortName: "UDP",
color: protocolColors.udp,
expanded: false,
fields: [
{ name: "Source Port", value: `${40000 + Math.floor(Math.random() * 10000)}`, description: "Source port number" },
{ name: "Destination Port", value: `${port}`, description: "Destination port number" },
{ name: "Length", value: `${frameLength - 34} bytes`, description: "UDP length" },
{ name: "Checksum", value: "0x9ABC", description: "UDP checksum" }
]
});
}
}
const appLayer = buildAppLayer(protocol, appData);
if (appLayer) {
layers.push(appLayer);
}
return layers;
}
function buildAppLayer(protocol, appData) {
const layer = {
name: protocolInfo[protocol]?.fullName || protocol.toUpperCase(),
shortName: protocol.toUpperCase(),
color: protocolColors[protocol],
expanded: true,
fields: []
};
switch (protocol) {
case "mqtt":
layer.fields = buildMQTTFields(appData);
break;
case "coap":
layer.fields = buildCoAPFields(appData);
break;
case "http":
layer.fields = buildHTTPFields(appData);
break;
case "ble":
layer.fields = buildBLEFields(appData);
break;
case "zigbee":
layer.fields = buildZigbeeFields(appData);
break;
}
return layer;
}
function buildMQTTFields(appData) {
const fields = [];
const d = appData.details;
fields.push({ name: "Packet Type", value: `${d.packetType} (${appData.type})`, description: "MQTT control packet type" });
if (d.protocolName) {
fields.push({ name: "Protocol Name", value: d.protocolName, description: "Protocol identifier" });
fields.push({ name: "Protocol Level", value: d.protocolLevel, description: "MQTT version (4 = 3.1.1, 5 = 5.0)" });
}
if (d.clientId) {
fields.push({ name: "Client ID", value: d.clientId, description: "Unique client identifier" });
fields.push({ name: "Clean Session", value: d.cleanSession ? "Yes" : "No", description: "Start fresh session" });
}
if (d.keepAlive) {
fields.push({ name: "Keep Alive", value: `${d.keepAlive} seconds`, description: "Keep-alive interval" });
}
if (d.returnCode !== undefined) {
fields.push({ name: "Session Present", value: d.sessionPresent ? "Yes" : "No", description: "Session already exists" });
fields.push({ name: "Return Code", value: `${d.returnCode} (${d.returnCodeText})`, description: "Connection result" });
}
if (d.packetId !== undefined) {
fields.push({ name: "Packet ID", value: d.packetId, description: "Message identifier for QoS > 0" });
}
if (d.topic) {
fields.push({ name: "Topic Name", value: d.topic, description: "MQTT topic string" });
}
if (d.topics) {
d.topics.forEach((t, i) => {
fields.push({ name: `Topic Filter ${i + 1}`, value: t.topic, description: "Subscription filter" });
fields.push({ name: `Requested QoS ${i + 1}`, value: t.qos, description: "Quality of Service level" });
});
}
if (d.qos !== undefined) {
fields.push({ name: "QoS Level", value: d.qos, description: "Quality of Service (0, 1, or 2)" });
fields.push({ name: "DUP Flag", value: d.dup ? "Yes" : "No", description: "Duplicate delivery" });
fields.push({ name: "Retain Flag", value: d.retain ? "Yes" : "No", description: "Retain message" });
}
if (d.grantedQos) {
fields.push({ name: "Granted QoS", value: d.grantedQos.join(", "), description: "Granted QoS levels" });
}
if (appData.payload) {
fields.push({ name: "Payload", value: appData.payload, description: "Message payload data", isPayload: true });
}
return fields;
}
function buildCoAPFields(appData) {
const fields = [];
const d = appData.details;
fields.push({ name: "Version", value: d.version, description: "CoAP version (always 1)" });
const typeNames = ["CON (Confirmable)", "NON (Non-confirmable)", "ACK (Acknowledgement)", "RST (Reset)"];
fields.push({ name: "Type", value: typeNames[d.type], description: "Message type" });
fields.push({ name: "Token Length", value: `${d.tokenLength} bytes`, description: "Length of token field" });
fields.push({ name: "Code", value: `${d.code} (${d.codeText})`, description: "Request method or response code" });
fields.push({ name: "Message ID", value: `0x${d.messageId.toString(16).toUpperCase()}`, description: "Message identifier for matching" });
if (d.token) {
fields.push({ name: "Token", value: d.token, description: "Request-response matching token" });
}
if (d.options) {
d.options.forEach(opt => {
fields.push({ name: `Option: ${opt.name}`, value: opt.value, description: `CoAP option #${opt.number}` });
});
}
if (d.retransmission) {
fields.push({ name: "[Retransmission]", value: `Attempt ${d.retransmitCount + 1}`, description: "This is a retransmitted message", isAnomaly: true });
}
if (appData.payload) {
fields.push({ name: "Payload", value: appData.payload, description: "Message payload data", isPayload: true });
}
return fields;
}
function buildHTTPFields(appData) {
const fields = [];
const d = appData.details;
if (d.method) {
fields.push({ name: "Method", value: d.method, description: "HTTP request method" });
fields.push({ name: "Request URI", value: d.uri, description: "Requested resource path" });
fields.push({ name: "Version", value: d.version, description: "HTTP version" });
} else {
fields.push({ name: "Version", value: d.version, description: "HTTP version" });
fields.push({ name: "Status Code", value: d.status, description: "Response status code" });
fields.push({ name: "Reason Phrase", value: d.statusText, description: "Status description" });
}
if (d.headers) {
Object.entries(d.headers).forEach(([key, value]) => {
fields.push({ name: `Header: ${key}`, value: value, description: `HTTP header field` });
});
}
if (appData.payload) {
fields.push({ name: "Body", value: appData.payload, description: "HTTP message body", isPayload: true });
}
return fields;
}
function buildBLEFields(appData) {
const fields = [];
const d = appData.details;
if (d.pduType) {
fields.push({ name: "PDU Type", value: d.pduType, description: "Advertising PDU type" });
if (d.txAddress) {
fields.push({ name: "Advertiser Address", value: d.txAddress, description: "Advertising device address" });
}
if (d.scannerAddress) {
fields.push({ name: "Scanner Address", value: d.scannerAddress, description: "Scanning device address" });
}
if (d.initiatorAddress) {
fields.push({ name: "Initiator Address", value: d.initiatorAddress, description: "Connection initiator address" });
}
if (d.advAddress) {
fields.push({ name: "Advertiser Address", value: d.advAddress, description: "Target advertiser" });
}
if (d.advData) {
d.advData.forEach(ad => {
fields.push({ name: `AD: ${ad.name}`, value: ad.value, description: `Advertising data type 0x${ad.type.toString(16).toUpperCase()}` });
});
}
if (d.connInterval) {
fields.push({ name: "Connection Interval", value: d.connInterval, description: "Time between connection events" });
fields.push({ name: "Slave Latency", value: d.slaveLatency, description: "Number of events slave can skip" });
fields.push({ name: "Supervision Timeout", value: d.supervisionTimeout, description: "Link supervision timeout" });
}
if (d.rssi) {
fields.push({ name: "RSSI", value: `${d.rssi} dBm`, description: "Received signal strength" });
}
}
if (d.opcode !== undefined) {
fields.push({ name: "ATT Opcode", value: `0x${d.opcode.toString(16).toUpperCase()} (${d.opcodeName})`, description: "Attribute Protocol opcode" });
if (d.clientMtu) {
fields.push({ name: "Client Rx MTU", value: d.clientMtu, description: "Maximum Transmission Unit" });
}
if (d.serverMtu) {
fields.push({ name: "Server Rx MTU", value: d.serverMtu, description: "Maximum Transmission Unit" });
}
if (d.handle) {
fields.push({ name: "Handle", value: d.handle, description: "Attribute handle" });
}
if (d.startHandle) {
fields.push({ name: "Starting Handle", value: d.startHandle, description: "Start of handle range" });
fields.push({ name: "Ending Handle", value: d.endHandle, description: "End of handle range" });
}
if (d.uuid) {
fields.push({ name: "UUID", value: d.uuid, description: "Attribute type UUID" });
}
if (d.value) {
fields.push({ name: "Value", value: d.value, description: "Attribute value" });
}
if (d.services) {
d.services.forEach((svc, i) => {
fields.push({ name: `Service ${i + 1}`, value: `${svc.startHandle}-${svc.endHandle}: ${svc.uuid}`, description: "Discovered service" });
});
}
}
return fields;
}
function buildZigbeeFields(appData) {
const fields = [];
const d = appData.details;
if (d.commandName) {
fields.push({ name: "MAC Command", value: d.commandName, description: "MAC layer command type" });
}
if (d.beaconOrder !== undefined) {
fields.push({ name: "Beacon Order", value: d.beaconOrder, description: "Beacon interval exponent" });
fields.push({ name: "Permit Joining", value: d.permitJoining ? "Yes" : "No", description: "Network allows new devices" });
}
if (d.capabilityInfo) {
fields.push({ name: "Device Type", value: d.capabilityInfo.deviceType, description: "FFD or RFD" });
fields.push({ name: "Power Source", value: d.capabilityInfo.powerSource, description: "Mains or battery powered" });
}
if (d.shortAddress) {
fields.push({ name: "Short Address", value: d.shortAddress, description: "Assigned network address" });
fields.push({ name: "Association Status", value: d.associationStatus, description: "Join result" });
}
if (d.clusterName) {
fields.push({ name: "Cluster ID", value: `${d.clusterId} (${d.clusterName})`, description: "ZCL cluster identifier" });
}
if (d.commandId !== undefined && d.commandName) {
fields.push({ name: "Command", value: `0x${d.commandId.toString(16).toUpperCase()} (${d.commandName})`, description: "ZCL command" });
}
if (d.sequenceNumber !== undefined) {
fields.push({ name: "Sequence Number", value: d.sequenceNumber, description: "Transaction sequence" });
}
if (d.nwkAddress) {
fields.push({ name: "Network Address", value: d.nwkAddress, description: "16-bit network address" });
fields.push({ name: "IEEE Address", value: d.ieeeAddress, description: "64-bit extended address" });
}
if (d.attributes) {
d.attributes.forEach(attr => {
const statusStr = attr.status ? ` [${attr.status}]` : "";
fields.push({
name: `Attr: ${attr.name} (${attr.id})`,
value: attr.formatted || attr.value,
description: `Attribute type: ${attr.type}${statusStr}`
});
});
}
if (d.statusCode !== undefined) {
fields.push({ name: "Status", value: `0x${d.statusCode.toString(16).toUpperCase()} (${d.statusText})`, description: "Command status" });
}
return fields;
}
function generateHexDump(length) {
const bytes = [];
for (let i = 0; i < length; i++) {
bytes.push(Math.floor(Math.random() * 256));
}
return bytes;
}
function formatHexDump(bytes) {
let output = "";
for (let i = 0; i < bytes.length; i += 16) {
const offset = i.toString(16).padStart(4, "0");
const chunk = bytes.slice(i, Math.min(i + 16, bytes.length));
const hex = chunk.map(b => b.toString(16).padStart(2, "0")).join(" ");
const ascii = chunk.map(b => (b >= 32 && b <= 126) ? String.fromCharCode(b) : ".").join("");
output += `${offset} ${hex.padEnd(48)} ${ascii}\n`;
}
return output;
}
// State management
let state = {
selectedScenario: "mqtt_normal",
packets: [],
filteredPackets: [],
selectedPacketId: null,
filters: {
protocol: "",
source: "",
destination: "",
timeStart: 0,
timeEnd: Infinity,
searchText: ""
},
expandedLayers: {},
showSequenceDiagram: false,
statsView: "distribution"
};
state.packets = scenarios[state.selectedScenario].packets;
state.filteredPackets = [...state.packets];
if (state.packets.length > 0) {
state.selectedPacketId = state.packets[0].id;
const baseTime = Math.min(...state.packets.map(p => p.timestamp));
state.packets.forEach(p => p.relativeTime = ((p.timestamp - baseTime) / 1000).toFixed(6));
}
const container = d3.create("div")
.style("font-family", "system-ui, -apple-system, sans-serif")
.style("max-width", "1400px")
.style("margin", "0 auto")
.style("background", colors.darkGray)
.style("border-radius", "8px")
.style("overflow", "hidden");
const header = container.append("div")
.style("background", `linear-gradient(135deg, ${colors.navy} 0%, ${colors.darkGray} 100%)`)
.style("padding", "16px 20px")
.style("color", colors.white)
.style("display", "flex")
.style("justify-content", "space-between")
.style("align-items", "center");
header.append("div")
.html(`<h3 style="margin:0;font-size:20px;">Packet Capture Analyzer</h3>
<p style="margin:4px 0 0;opacity:0.8;font-size:13px;">Wireshark-style IoT protocol analysis</p>`);
const scenarioSelector = header.append("div")
.style("display", "flex")
.style("align-items", "center")
.style("gap", "10px");
scenarioSelector.append("label")
.style("font-size", "13px")
.text("Load Scenario:");
const scenarioSelect = scenarioSelector.append("select")
.style("padding", "8px 12px")
.style("border-radius", "4px")
.style("border", "none")
.style("background", colors.white)
.style("font-size", "13px")
.style("cursor", "pointer")
.style("min-width", "200px");
Object.entries(scenarios).forEach(([key, scenario]) => {
scenarioSelect.append("option")
.attr("value", key)
.attr("selected", key === state.selectedScenario ? true : null)
.text(scenario.name);
});
const filterBar = container.append("div")
.style("background", colors.lightGray)
.style("padding", "10px 20px")
.style("display", "flex")
.style("flex-wrap", "wrap")
.style("gap", "12px")
.style("align-items", "center")
.style("border-bottom", `1px solid ${colors.gray}`);
filterBar.append("span")
.style("font-weight", "bold")
.style("font-size", "13px")
.style("color", colors.navy)
.text("Filter:");
const filterInput = filterBar.append("input")
.attr("type", "text")
.attr("placeholder", "e.g., mqtt, ip.src==192.168.1.100, coap.type==CON")
.style("flex", "1")
.style("min-width", "300px")
.style("padding", "8px 12px")
.style("border-radius", "4px")
.style("border", `1px solid ${colors.gray}`)
.style("font-size", "13px")
.style("font-family", "monospace");
const protocolFilter = filterBar.append("select")
.style("padding", "8px 12px")
.style("border-radius", "4px")
.style("border", `1px solid ${colors.gray}`)
.style("font-size", "13px")
.style("cursor", "pointer");
protocolFilter.append("option").attr("value", "").text("All Protocols");
["mqtt", "coap", "http", "ble", "zigbee"].forEach(p => {
protocolFilter.append("option").attr("value", p).text(p.toUpperCase());
});
const applyFilterBtn = filterBar.append("button")
.attr("type", "button")
.attr("aria-label", "Apply filter")
.text("Apply")
.style("padding", "8px 16px")
.style("background", colors.teal)
.style("color", colors.white)
.style("border", "none")
.style("border-radius", "4px")
.style("cursor", "pointer")
.style("font-size", "13px")
.style("font-weight", "bold");
const clearFilterBtn = filterBar.append("button")
.attr("type", "button")
.attr("aria-label", "Clear filter")
.text("Clear")
.style("padding", "8px 16px")
.style("background", colors.gray)
.style("color", colors.white)
.style("border", "none")
.style("border-radius", "4px")
.style("cursor", "pointer")
.style("font-size", "13px");
const packetCount = filterBar.append("span")
.attr("class", "packet-count")
.style("margin-left", "auto")
.style("font-size", "13px")
.style("color", colors.navy);
const mainContent = container.append("div")
.style("display", "grid")
.style("grid-template-rows", "300px 1fr")
.style("height", "900px");
const packetListPanel = mainContent.append("div")
.style("background", colors.white)
.style("overflow", "auto")
.style("border-bottom", `2px solid ${colors.navy}`);
const packetTable = packetListPanel.append("table")
.style("width", "100%")
.style("border-collapse", "collapse")
.style("font-size", "12px");
const tableHead = packetTable.append("thead");
const headerRow = tableHead.append("tr")
.style("background", colors.navy)
.style("color", colors.white)
.style("position", "sticky")
.style("top", "0")
.style("z-index", "10");
["No.", "Time", "Source", "Destination", "Protocol", "Length", "Info"].forEach(col => {
headerRow.append("th")
.style("padding", "10px 8px")
.style("text-align", "left")
.style("font-size", "12px")
.style("font-weight", "600")
.style("white-space", "nowrap")
.text(col);
});
const tableBody = packetTable.append("tbody")
.attr("class", "packet-list-body");
const bottomPanels = mainContent.append("div")
.style("display", "grid")
.style("grid-template-columns", "1fr 1fr")
.style("gap", "2px")
.style("background", colors.gray);
const detailsPanel = bottomPanels.append("div")
.style("background", colors.white)
.style("overflow", "auto")
.style("display", "flex")
.style("flex-direction", "column");
const detailsHeader = detailsPanel.append("div")
.style("background", colors.lightGray)
.style("padding", "10px 15px")
.style("font-weight", "bold")
.style("font-size", "14px")
.style("color", colors.navy)
.style("border-bottom", `1px solid ${colors.gray}`)
.style("display", "flex")
.style("justify-content", "space-between")
.style("align-items", "center");
detailsHeader.append("span").text("Packet Details");
const viewToggle = detailsHeader.append("div")
.style("display", "flex")
.style("gap", "5px");
const expandAllBtn = viewToggle.append("button")
.attr("type", "button")
.attr("aria-label", "Expand all layers")
.text("Expand All")
.style("padding", "4px 8px")
.style("background", colors.teal)
.style("color", colors.white)
.style("border", "none")
.style("border-radius", "3px")
.style("cursor", "pointer")
.style("font-size", "11px");
const collapseAllBtn = viewToggle.append("button")
.attr("type", "button")
.attr("aria-label", "Collapse all layers")
.text("Collapse All")
.style("padding", "4px 8px")
.style("background", colors.gray)
.style("color", colors.white)
.style("border", "none")
.style("border-radius", "3px")
.style("cursor", "pointer")
.style("font-size", "11px");
const detailsContent = detailsPanel.append("div")
.attr("class", "details-content")
.style("flex", "1")
.style("overflow", "auto")
.style("padding", "10px");
const rightPanel = bottomPanels.append("div")
.style("background", colors.white)
.style("display", "flex")
.style("flex-direction", "column");
const tabBar = rightPanel.append("div")
.style("background", colors.lightGray)
.style("padding", "0 15px")
.style("display", "flex")
.style("gap", "0")
.style("border-bottom", `1px solid ${colors.gray}`);
const tabs = ["Hex Dump", "Statistics", "Sequence Diagram"];
let activeTab = "Hex Dump";
tabs.forEach(tabName => {
tabBar.append("button")
.attr("type", "button")
.attr("aria-label", `Switch to ${tabName} view`)
.attr("class", `tab-btn tab-${tabName.replace(/\s/g, "-").toLowerCase()}`)
.text(tabName)
.style("padding", "10px 20px")
.style("background", tabName === activeTab ? colors.white : "transparent")
.style("color", tabName === activeTab ? colors.navy : colors.gray)
.style("border", "none")
.style("border-bottom", tabName === activeTab ? `2px solid ${colors.teal}` : "2px solid transparent")
.style("cursor", "pointer")
.style("font-size", "13px")
.style("font-weight", tabName === activeTab ? "bold" : "normal")
.on("click", function() {
activeTab = tabName;
tabBar.selectAll("button")
.style("background", "transparent")
.style("color", colors.gray)
.style("border-bottom", "2px solid transparent")
.style("font-weight", "normal");
d3.select(this)
.style("background", colors.white)
.style("color", colors.navy)
.style("border-bottom", `2px solid ${colors.teal}`)
.style("font-weight", "bold");
updateRightPanel();
});
});
const rightContent = rightPanel.append("div")
.attr("class", "right-content")
.style("flex", "1")
.style("overflow", "auto")
.style("padding", "15px");
const tooltip = container.append("div")
.attr("class", "field-tooltip")
.style("position", "absolute")
.style("background", colors.navy)
.style("color", colors.white)
.style("padding", "8px 12px")
.style("border-radius", "4px")
.style("font-size", "12px")
.style("max-width", "300px")
.style("pointer-events", "none")
.style("opacity", 0)
.style("z-index", "1000")
.style("box-shadow", "0 4px 12px rgba(0,0,0,0.3)");
function loadScenario(scenarioKey) {
state.selectedScenario = scenarioKey;
state.packets = scenarios[scenarioKey].packets;
const baseTime = Math.min(...state.packets.map(p => p.timestamp));
state.packets.forEach(p => p.relativeTime = ((p.timestamp - baseTime) / 1000).toFixed(6));
applyFilters();
if (state.filteredPackets.length > 0) {
state.selectedPacketId = state.filteredPackets[0].id;
} else {
state.selectedPacketId = null;
}
renderPacketList();
renderPacketDetails();
updateRightPanel();
}
function applyFilters() {
const searchText = filterInput.property("value").toLowerCase().trim();
const protocolFilterValue = protocolFilter.property("value");
state.filteredPackets = state.packets.filter(packet => {
if (protocolFilterValue && packet.protocol !== protocolFilterValue) {
return false;
}
if (searchText) {
if (searchText.includes("==")) {
const [field, value] = searchText.split("==").map(s => s.trim());
if (field === "ip.src" && !packet.source.includes(value)) return false;
if (field === "ip.dst" && !packet.destination.includes(value)) return false;
if (field.includes(".type") && !packet.appData.type.toLowerCase().includes(value.toLowerCase())) return false;
} else {
const searchable = `${packet.source} ${packet.destination} ${packet.protocol} ${packet.info}`.toLowerCase();
if (!searchable.includes(searchText)) return false;
}
}
return true;
});
updatePacketCount();
}
function updatePacketCount() {
const total = state.packets.length;
const filtered = state.filteredPackets.length;
const text = filtered === total
? `Displaying ${total} packets`
: `Displaying ${filtered} of ${total} packets`;
container.select(".packet-count").text(text);
}
function renderPacketList() {
const tbody = container.select(".packet-list-body");
tbody.selectAll("tr").remove();
state.filteredPackets.forEach((packet, idx) => {
const isSelected = packet.id === state.selectedPacketId;
const bgColor = isSelected ? colors.blue :
packet.isRetransmit ? colors.yellow :
packet.isError ? colors.red :
idx % 2 === 0 ? colors.white : colors.lightGray;
const textColor = isSelected || packet.isError ? colors.white : colors.navy;
const row = tbody.append("tr")
.style("background", bgColor)
.style("color", textColor)
.style("cursor", "pointer")
.style("border-bottom", `1px solid ${colors.lightGray}`)
.on("click", function() {
state.selectedPacketId = packet.id;
renderPacketList();
renderPacketDetails();
updateRightPanel();
})
.on("mouseover", function() {
if (!isSelected) {
d3.select(this).style("background", colors.lightGray);
}
})
.on("mouseout", function() {
if (!isSelected) {
d3.select(this).style("background", idx % 2 === 0 ? colors.white : colors.lightGray);
}
});
row.append("td")
.style("padding", "6px 8px")
.style("font-family", "monospace")
.text(idx + 1);
row.append("td")
.style("padding", "6px 8px")
.style("font-family", "monospace")
.text(packet.relativeTime);
row.append("td")
.style("padding", "6px 8px")
.style("font-family", "monospace")
.style("max-width", "150px")
.style("overflow", "hidden")
.style("text-overflow", "ellipsis")
.text(packet.source);
row.append("td")
.style("padding", "6px 8px")
.style("font-family", "monospace")
.style("max-width", "150px")
.style("overflow", "hidden")
.style("text-overflow", "ellipsis")
.text(packet.destination);
row.append("td")
.style("padding", "6px 8px")
.append("span")
.style("background", protocolColors[packet.protocol] || colors.gray)
.style("color", colors.white)
.style("padding", "2px 8px")
.style("border-radius", "3px")
.style("font-size", "11px")
.style("font-weight", "bold")
.text(packet.protocol.toUpperCase());
row.append("td")
.style("padding", "6px 8px")
.style("text-align", "right")
.text(packet.length);
const infoCell = row.append("td")
.style("padding", "6px 8px")
.style("max-width", "400px")
.style("overflow", "hidden")
.style("text-overflow", "ellipsis")
.style("white-space", "nowrap");
if (packet.isRetransmit) {
infoCell.append("span")
.style("background", colors.orange)
.style("color", colors.white)
.style("padding", "1px 4px")
.style("border-radius", "2px")
.style("font-size", "10px")
.style("margin-right", "5px")
.text("Retransmit");
}
infoCell.append("span").text(packet.info);
});
}
function renderPacketDetails() {
const content = container.select(".details-content");
content.selectAll("*").remove();
const packet = state.packets.find(p => p.id === state.selectedPacketId);
if (!packet) {
content.append("div")
.style("color", colors.gray)
.style("padding", "20px")
.style("text-align", "center")
.text("Select a packet to view details");
return;
}
const frameSummary = content.append("div")
.style("background", colors.lightGray)
.style("padding", "10px 12px")
.style("border-radius", "4px")
.style("margin-bottom", "10px")
.style("font-size", "12px");
frameSummary.append("div")
.style("font-weight", "bold")
.style("color", colors.navy)
.style("margin-bottom", "5px")
.text(`Frame ${state.filteredPackets.findIndex(p => p.id === packet.id) + 1}: ${packet.length} bytes on wire`);
frameSummary.append("div")
.style("color", colors.gray)
.html(`Arrival Time: ${new Date(packet.timestamp).toISOString()}<br>
Protocols: ${packet.layers.map(l => l.shortName).join(" > ")}`);
packet.layers.forEach((layer, layerIdx) => {
const layerDiv = content.append("div")
.style("margin-bottom", "8px")
.style("border", `1px solid ${colors.lightGray}`)
.style("border-radius", "4px")
.style("overflow", "hidden");
const layerHeader = layerDiv.append("div")
.attr("role", "button")
.attr("tabindex", "0")
.attr("aria-expanded", "true")
.attr("aria-label", `Toggle ${layer.name} layer details`)
.style("background", layer.color)
.style("color", colors.white)
.style("padding", "8px 12px")
.style("cursor", "pointer")
.style("display", "flex")
.style("justify-content", "space-between")
.style("align-items", "center")
.on("click", function() {
const key = `${packet.id}-${layerIdx}`;
state.expandedLayers[key] = !state.expandedLayers[key];
renderPacketDetails();
});
const expandKey = `${packet.id}-${layerIdx}`;
const isExpanded = state.expandedLayers[expandKey] !== false;
layerHeader.append("span")
.style("font-weight", "bold")
.style("font-size", "13px")
.text(layer.name);
layerHeader.append("span")
.style("font-size", "18px")
.text(isExpanded ? "-" : "+");
if (isExpanded) {
const fieldsDiv = layerDiv.append("div")
.style("padding", "8px 12px")
.style("background", colors.white);
layer.fields.forEach(field => {
const fieldRow = fieldsDiv.append("div")
.style("display", "flex")
.style("padding", "4px 0")
.style("border-bottom", `1px solid ${colors.lightGray}`)
.style("font-size", "12px")
.style("cursor", "help");
fieldRow.append("span")
.style("flex", "0 0 200px")
.style("color", field.isAnomaly ? colors.red : colors.navy)
.style("font-weight", field.isAnomaly ? "bold" : "normal")
.text(field.name + ":");
const valueSpan = fieldRow.append("span")
.style("flex", "1")
.style("color", field.isPayload ? colors.teal : colors.darkGray)
.style("font-family", "monospace")
.style("word-break", "break-all");
if (field.isPayload && typeof field.value === "string" && field.value.length > 50) {
valueSpan.text(field.value.substring(0, 50) + "...");
} else {
valueSpan.text(String(field.value));
}
fieldRow.on("mouseenter", function(event) {
tooltip
.style("opacity", 1)
.html(`<strong>${field.name}</strong><br><span style="color:${colors.lightGray}">${field.description}</span>`)
.style("left", `${event.pageX + 15}px`)
.style("top", `${event.pageY - 10}px`);
})
.on("mousemove", function(event) {
tooltip
.style("left", `${event.pageX + 15}px`)
.style("top", `${event.pageY - 10}px`);
})
.on("mouseleave", function() {
tooltip.style("opacity", 0);
});
});
}
});
}
function updateRightPanel() {
const content = container.select(".right-content");
content.selectAll("*").remove();
if (activeTab === "Hex Dump") {
renderHexDump(content);
} else if (activeTab === "Statistics") {
renderStatistics(content);
} else if (activeTab === "Sequence Diagram") {
renderSequenceDiagram(content);
}
}
function renderHexDump(content) {
const packet = state.packets.find(p => p.id === state.selectedPacketId);
if (!packet) {
content.append("div")
.style("color", colors.gray)
.style("text-align", "center")
.style("padding", "20px")
.text("Select a packet to view hex dump");
return;
}
const pre = content.append("pre")
.style("background", "#1a1a2e")
.style("color", "#00ff00")
.style("padding", "15px")
.style("border-radius", "4px")
.style("font-family", "'Consolas', 'Monaco', monospace")
.style("font-size", "12px")
.style("line-height", "1.4")
.style("overflow-x", "auto")
.style("margin", "0");
pre.text(formatHexDump(packet.hexDump));
const copyBtn = content.append("button")
.attr("type", "button")
.attr("aria-label", "Copy hex dump to clipboard")
.text("Copy Hex Dump")
.style("margin-top", "10px")
.style("padding", "8px 16px")
.style("background", colors.teal)
.style("color", colors.white)
.style("border", "none")
.style("border-radius", "4px")
.style("cursor", "pointer")
.style("font-size", "12px")
.on("click", function() {
navigator.clipboard.writeText(formatHexDump(packet.hexDump)).then(() => {
d3.select(this).text("Copied!").style("background", colors.green);
setTimeout(() => {
d3.select(this).text("Copy Hex Dump").style("background", colors.teal);
}, 2000);
});
});
}
function renderStatistics(content) {
const packets = state.filteredPackets;
if (packets.length === 0) {
content.append("div")
.style("color", colors.gray)
.style("text-align", "center")
.text("No packets to analyze");
return;
}
const protocolCounts = {};
packets.forEach(p => {
protocolCounts[p.protocol] = (protocolCounts[p.protocol] || 0) + 1;
});
content.append("h4")
.style("margin", "0 0 15px 0")
.style("color", colors.navy)
.text("Protocol Distribution");
const pieContainer = content.append("div")
.style("display", "flex")
.style("gap", "20px")
.style("margin-bottom", "20px");
const pieSvg = pieContainer.append("svg")
.attr("width", 200)
.attr("height", 200)
.attr("role", "img")
.attr("aria-label", "Protocol distribution pie chart");
const pieData = Object.entries(protocolCounts).map(([name, value]) => ({
name,
value,
color: protocolColors[name] || colors.gray
}));
const total = pieData.reduce((sum, d) => sum + d.value, 0);
let currentAngle = 0;
pieData.forEach(d => {
const startAngle = currentAngle;
const endAngle = currentAngle + (d.value / total) * 2 * Math.PI;
currentAngle = endAngle;
const x1 = 100 + 80 * Math.cos(startAngle);
const y1 = 100 + 80 * Math.sin(startAngle);
const x2 = 100 + 80 * Math.cos(endAngle);
const y2 = 100 + 80 * Math.sin(endAngle);
const largeArc = endAngle - startAngle > Math.PI ? 1 : 0;
pieSvg.append("path")
.attr("d", `M 100 100 L ${x1} ${y1} A 80 80 0 ${largeArc} 1 ${x2} ${y2} Z`)
.attr("fill", d.color)
.attr("stroke", colors.white)
.attr("stroke-width", 2);
});
const legend = pieContainer.append("div");
pieData.forEach(d => {
const item = legend.append("div")
.style("display", "flex")
.style("align-items", "center")
.style("gap", "8px")
.style("margin-bottom", "8px");
item.append("div")
.style("width", "16px")
.style("height", "16px")
.style("background", d.color)
.style("border-radius", "3px");
item.append("span")
.style("font-size", "13px")
.style("color", colors.navy)
.text(`${d.name.toUpperCase()}: ${d.value} (${((d.value / total) * 100).toFixed(1)}%)`);
});
content.append("h4")
.style("margin", "20px 0 15px 0")
.style("color", colors.navy)
.text("Top Talkers");
const addressCounts = {};
packets.forEach(p => {
addressCounts[p.source] = (addressCounts[p.source] || 0) + 1;
addressCounts[p.destination] = (addressCounts[p.destination] || 0) + 1;
});
const topTalkers = Object.entries(addressCounts)
.sort((a, b) => b[1] - a[1])
.slice(0, 5);
const talkersTable = content.append("table")
.style("width", "100%")
.style("border-collapse", "collapse")
.style("font-size", "12px");
const talkersHeader = talkersTable.append("tr")
.style("background", colors.navy)
.style("color", colors.white);
["Address", "Packets", "Percentage"].forEach(h => {
talkersHeader.append("th")
.style("padding", "8px")
.style("text-align", "left")
.text(h);
});
topTalkers.forEach(([addr, count], i) => {
const row = talkersTable.append("tr")
.style("background", i % 2 === 0 ? colors.white : colors.lightGray);
row.append("td")
.style("padding", "6px 8px")
.style("font-family", "monospace")
.text(addr);
row.append("td")
.style("padding", "6px 8px")
.text(count);
row.append("td")
.style("padding", "6px 8px")
.text(`${((count / (packets.length * 2)) * 100).toFixed(1)}%`);
});
content.append("h4")
.style("margin", "20px 0 15px 0")
.style("color", colors.navy)
.text("Conversation Pairs");
const conversations = {};
packets.forEach(p => {
const key = [p.source, p.destination].sort().join(" <-> ");
conversations[key] = (conversations[key] || 0) + 1;
});
const topConvs = Object.entries(conversations)
.sort((a, b) => b[1] - a[1])
.slice(0, 5);
const convsTable = content.append("table")
.style("width", "100%")
.style("border-collapse", "collapse")
.style("font-size", "12px");
const convsHeader = convsTable.append("tr")
.style("background", colors.navy)
.style("color", colors.white);
["Conversation", "Packets"].forEach(h => {
convsHeader.append("th")
.style("padding", "8px")
.style("text-align", "left")
.text(h);
});
topConvs.forEach(([conv, count], i) => {
const row = convsTable.append("tr")
.style("background", i % 2 === 0 ? colors.white : colors.lightGray);
row.append("td")
.style("padding", "6px 8px")
.style("font-family", "monospace")
.style("font-size", "11px")
.text(conv);
row.append("td")
.style("padding", "6px 8px")
.text(count);
});
content.append("h4")
.style("margin", "20px 0 15px 0")
.style("color", colors.navy)
.text("Packets Over Time");
const timelineSvg = content.append("svg")
.attr("width", "100%")
.attr("height", 120)
.attr("viewBox", "0 0 400 120")
.attr("role", "img")
.attr("aria-label", "Packets over time timeline");
const timestamps = packets.map(p => p.timestamp);
const minTime = Math.min(...timestamps);
const maxTime = Math.max(...timestamps);
const timeRange = maxTime - minTime || 1;
const barWidth = 380 / packets.length;
packets.forEach((p, i) => {
const x = 10 + ((p.timestamp - minTime) / timeRange) * 380;
timelineSvg.append("rect")
.attr("x", x)
.attr("y", 20)
.attr("width", Math.max(2, barWidth - 1))
.attr("height", 60)
.attr("fill", protocolColors[p.protocol] || colors.gray)
.attr("opacity", 0.8);
});
timelineSvg.append("line")
.attr("x1", 10)
.attr("y1", 90)
.attr("x2", 390)
.attr("y2", 90)
.attr("stroke", colors.gray);
timelineSvg.append("text")
.attr("x", 10)
.attr("y", 105)
.attr("font-size", "10px")
.attr("fill", colors.gray)
.text("0s");
timelineSvg.append("text")
.attr("x", 390)
.attr("y", 105)
.attr("text-anchor", "end")
.attr("font-size", "10px")
.attr("fill", colors.gray)
.text(`${(timeRange / 1000).toFixed(2)}s`);
}
function renderSequenceDiagram(content) {
const packets = state.filteredPackets;
if (packets.length === 0) {
content.append("div")
.style("color", colors.gray)
.style("text-align", "center")
.text("No packets to visualize");
return;
}
const participants = [...new Set(packets.flatMap(p => [p.source, p.destination]))];
const svgWidth = Math.max(600, participants.length * 150);
const svgHeight = Math.max(400, packets.length * 40 + 100);
const svg = content.append("svg")
.attr("width", "100%")
.attr("height", svgHeight)
.attr("viewBox", `0 0 ${svgWidth} ${svgHeight}`)
.attr("role", "img")
.attr("aria-label", "Sequence diagram showing packet flow between participants")
.style("display", "block");
const participantWidth = svgWidth / participants.length;
participants.forEach((p, i) => {
const x = participantWidth * i + participantWidth / 2;
svg.append("rect")
.attr("x", x - 60)
.attr("y", 10)
.attr("width", 120)
.attr("height", 35)
.attr("fill", colors.navy)
.attr("rx", 4);
svg.append("text")
.attr("x", x)
.attr("y", 32)
.attr("text-anchor", "middle")
.attr("fill", colors.white)
.attr("font-size", "11px")
.attr("font-weight", "bold")
.text(p.length > 15 ? p.substring(0, 12) + "..." : p);
svg.append("line")
.attr("x1", x)
.attr("y1", 45)
.attr("x2", x)
.attr("y2", svgHeight - 20)
.attr("stroke", colors.gray)
.attr("stroke-width", 1)
.attr("stroke-dasharray", "4,4");
});
packets.forEach((packet, idx) => {
const srcIdx = participants.indexOf(packet.source);
const dstIdx = participants.indexOf(packet.destination);
const srcX = participantWidth * srcIdx + participantWidth / 2;
const dstX = participantWidth * dstIdx + participantWidth / 2;
const y = 70 + idx * 35;
const color = protocolColors[packet.protocol] || colors.gray;
svg.append("line")
.attr("x1", srcX)
.attr("y1", y)
.attr("x2", dstX)
.attr("y2", y)
.attr("stroke", color)
.attr("stroke-width", 2)
.attr("stroke-dasharray", packet.isRetransmit ? "4,2" : "none");
const arrowDir = dstX > srcX ? 1 : -1;
svg.append("polygon")
.attr("points", `${dstX - 8 * arrowDir},${y - 5} ${dstX},${y} ${dstX - 8 * arrowDir},${y + 5}`)
.attr("fill", color);
const labelX = (srcX + dstX) / 2;
svg.append("rect")
.attr("x", labelX - 80)
.attr("y", y - 18)
.attr("width", 160)
.attr("height", 14)
.attr("fill", colors.white)
.attr("opacity", 0.9);
svg.append("text")
.attr("x", labelX)
.attr("y", y - 8)
.attr("text-anchor", "middle")
.attr("font-size", "10px")
.attr("fill", colors.navy)
.text(packet.appData.type || packet.info.substring(0, 25));
if (packet.id === state.selectedPacketId) {
svg.append("rect")
.attr("x", Math.min(srcX, dstX) - 5)
.attr("y", y - 20)
.attr("width", Math.abs(dstX - srcX) + 10)
.attr("height", 28)
.attr("fill", "none")
.attr("stroke", colors.blue)
.attr("stroke-width", 2)
.attr("rx", 4);
}
});
const legendY = svgHeight - 30;
svg.append("text")
.attr("x", 10)
.attr("y", legendY)
.attr("font-size", "11px")
.attr("fill", colors.gray)
.text("Legend:");
let legendX = 70;
Object.entries(protocolColors).forEach(([protocol, color]) => {
if (packets.some(p => p.protocol === protocol)) {
svg.append("line")
.attr("x1", legendX)
.attr("y1", legendY - 5)
.attr("x2", legendX + 30)
.attr("y2", legendY - 5)
.attr("stroke", color)
.attr("stroke-width", 2);
svg.append("text")
.attr("x", legendX + 35)
.attr("y", legendY)
.attr("font-size", "10px")
.attr("fill", colors.navy)
.text(protocol.toUpperCase());
legendX += 80;
}
});
}
scenarioSelect.on("change", function() {
loadScenario(this.value);
});
applyFilterBtn.on("click", function() {
applyFilters();
renderPacketList();
updatePacketCount();
});
clearFilterBtn.on("click", function() {
filterInput.property("value", "");
protocolFilter.property("value", "");
state.filters = {
protocol: "",
source: "",
destination: "",
timeStart: 0,
timeEnd: Infinity,
searchText: ""
};
applyFilters();
renderPacketList();
});
expandAllBtn.on("click", function() {
const packet = state.packets.find(p => p.id === state.selectedPacketId);
if (packet) {
packet.layers.forEach((layer, idx) => {
state.expandedLayers[`${packet.id}-${idx}`] = true;
});
renderPacketDetails();
}
});
collapseAllBtn.on("click", function() {
const packet = state.packets.find(p => p.id === state.selectedPacketId);
if (packet) {
packet.layers.forEach((layer, idx) => {
state.expandedLayers[`${packet.id}-${idx}`] = false;
});
renderPacketDetails();
}
});
filterInput.on("keypress", function(event) {
if (event.key === "Enter") {
applyFilters();
renderPacketList();
}
});
updatePacketCount();
renderPacketList();
renderPacketDetails();
updateRightPanel();
return container.node();
}1564.3 Learn More
For comprehensive understanding of packet capture analysis, explore these focused chapters:
| Chapter | Description | Difficulty |
|---|---|---|
| Protocol Layers and Filtering | Understanding network layers and display filters | Beginner |
| IoT Protocol Scenarios | Analyzing MQTT, CoAP, BLE, and Zigbee captures | Intermediate |
| Advanced Techniques | Hex dump analysis and statistics interpretation | Intermediate |
1564.4 Summary
ImportantKey Takeaways
- Packet captures reveal protocol behavior at every layer
- Display filters are essential for finding relevant traffic
- Sequence diagrams visualize request-response patterns
- Retransmissions indicate network reliability issues
- Statistics help identify traffic patterns and anomalies
- Understanding hex dumps enables deep protocol analysis
1564.5 What’s Next
- Protocol Layers and Filtering - Understand network layers and display filters
- IoT Protocol Scenarios - Analyze MQTT, CoAP, BLE, and Zigbee captures
- Advanced Techniques - Hex dump analysis and statistics interpretation
- Interactive Packet Analyzer - Build and parse IoT packets
- Network Conditions Emulator - Test protocol behavior under stress