cpe-labs speaks TR-369 (USP) alongside TR-069. Same simulator, same vendor profile, same parameter tree; the USP Agent is just a second transport adapter over the in-memory tree.
This page covers the USP-side surface: how the Agent connects to a Controller, which MTPs are supported, and how Subscribe / Notify drives outbound traffic.
Architectural fit¶
The USP Agent reads from and writes to the same paramtree.Tree that the CWMP stack uses. Per-CPE state is shared:
- One parameter tree per simulated CPE / Agent instance.
- One profile (the YAML schema is transport-agnostic).
- One generator runner (counters / drift / enum / uptime / wallclock leaves move regardless of which protocol reads them).
- One scheduler entry per CPE (USP
Subscriptionperiodic notify reuses the same timer plumbing as periodic Inform).
cpe-labs runs in one process with many simulated Agents in goroutines (see Architecture anchor #5). One outbound MTP client (one MQTT broker connection, one WebSocket session, one STOMP session) is shared across the fleet where the protocol allows it; per-Agent USP records multiplex over that shared client.
Picking an MTP¶
USP rides one of three MTPs in cpe-labs:
| MTP | When to use | cpe-labs flag |
|---|---|---|
| MQTT | Most common in production deployments. Broker-mediated, NAT-friendly. | --usp-mtp=mqtt |
| WebSocket | Direct Controller↔Agent over a single TCP connection. | --usp-mtp=websocket |
| STOMP | Legacy / message-broker shops with existing STOMP infra. | --usp-mtp=stomp |
Each MTP has its own connection knobs but the USP record framing on top is identical. Switching MTPs does not require profile changes.
MQTT¶
bin/cpe-sim \
--profile=profile.yaml \
--usp-mtp=mqtt \
--usp-mqtt-addr=broker.example.com:1883 \
--usp-mqtt-user=cpe-user \
--usp-mqtt-password=cpe-pass
| Flag | Env | Default | Notes |
|---|---|---|---|
--usp-mqtt-addr |
CPE_SIM_USP_MQTT_ADDR |
(required when --usp-mtp=mqtt) |
host:port. |
--usp-mqtt-user |
CPE_SIM_USP_MQTT_USER |
"" | Optional broker auth. |
--usp-mqtt-password |
CPE_SIM_USP_MQTT_PASSWORD |
"" | |
--usp-mqtt-tls |
CPE_SIM_USP_MQTT_TLS |
false |
Wrap the connection in TLS. |
--usp-mqtt-ca-cert-file |
CPE_SIM_USP_MQTT_CA_CERT_FILE |
"" | PEM bundle for broker cert verification. |
USP MQTT topic shapes are not fixed by the spec; the Agent and Controller agree on a topic prefix per deployment via Device.LocalAgent.MTP.{i}.MQTT.SubscribeTopic and ResponseTopicConfigured. Configure those leaves in the profile to match what your Controller expects.
The endpoint ID is derived from the per-CPE serial: os::<oui>-<productClass>-<serial>. For example, with (OUI=AABBCC, ProductClass=GenericGateway, Serial=GATEWAY-0001) the Agent registers as os::AABBCC-GenericGateway-GATEWAY-0001.
WebSocket¶
bin/cpe-sim \
--profile=profile.yaml \
--usp-mtp=websocket \
--usp-ws-url=ws://controller.example.com:8080/ws/agent
| Flag | Env | Default | Notes |
|---|---|---|---|
--usp-ws-url |
CPE_SIM_USP_WS_URL |
(required when --usp-mtp=websocket) |
Full ws:// or wss:// URL. |
--usp-ws-tls-skip-verify |
CPE_SIM_USP_WS_TLS_SKIP_VERIFY |
false |
Insecure; for self-signed test Controllers. |
--usp-ws-ca-cert-file |
CPE_SIM_USP_WS_CA_CERT_FILE |
"" | PEM bundle for wss:// cert verification. |
Each Agent opens its own WebSocket session. The shared transport pool reuses TCP-level resources where the protocol allows.
STOMP¶
bin/cpe-sim \
--profile=profile.yaml \
--usp-mtp=stomp \
--usp-stomp-addr=stomp.example.com:61613 \
--usp-stomp-user=cpe-user \
--usp-stomp-password=cpe-pass
Same auth and TLS flag pattern as MQTT.
Endpoint identity¶
USP Agents identify themselves with an Endpoint ID. cpe-labs builds it deterministically from the parameter tree:
os::<DeviceInfo.ManufacturerOUI>-<DeviceInfo.ProductClass>-<DeviceInfo.SerialNumber>
Per-CPE differentiation works the same as CWMP: fleet.serialPattern and the {cpe:*} placeholders stamp unique serials, and the Endpoint ID follows automatically.
Subscribe / Notify¶
The USP equivalent of TR-069's periodic Inform is Device.LocalAgent.Subscription.{i}. driven outbound notifications. A Controller creates a Subscription with NotifType=Periodic and a Periodic.Period; the Agent fires Notify records on that interval.
cpe-labs reuses the same scheduler the CWMP stack uses (see Periodic Inform Scheduler). Per-CPE jitter is the same ±10% default, derived from the same per-CPE RNG. --seed=N reproduces both stacks byte-for-byte.
Other NotifType values:
ValueChange: fires when a leaf the subscription references mutates. Generators that move tree state (counter, drift, enum) trigger this.ObjectCreation/ObjectDeletion: fires when an Agent-side AddObject / DeleteObject runs.OperationComplete: fires when a long-running Operate (USP equivalent of CWMP's TransferComplete) finishes.
All Subscription state lives in the parameter tree under Device.LocalAgent.Subscription.{i}.. The profile loads it the same way it loads any other multi-instance object.
No Connection Request¶
USP has no equivalent of TR-069 §3.2.2 Connection Request. The Agent maintains a persistent connection to the Controller via the chosen MTP, and the Controller sends Get / Set / Operate / Add / Delete records over that session whenever it needs to. There is no "wake up the Agent" round-trip.
This means --cr-bind-addr is a no-op for USP-only deployments. In dual-stack deployments (the Agent speaks both CWMP and USP), the CR listener serves the CWMP side only.
Same profile, both stacks¶
Nothing in the vendor profile is CWMP-specific. The same profiles/example-tr181-gateway/ works against an ACS over CWMP or a Controller over USP. Toggle between them with the relevant --acs-url / --usp-mtp flags.
Dual-stack mode (Agent speaks both protocols simultaneously) is supported by combining the flag sets:
bin/cpe-sim \
--profile=profile.yaml \
--acs-url=http://acs.example.com:7547/cwmp \
--usp-mtp=mqtt \
--usp-mqtt-addr=broker.example.com:1883
Reads from the ACS and reads from the Controller see the same tree. Writes from either side mutate the same tree. Generators move state regardless of who's looking.
Determinism across protocols¶
USP record framing is deterministic given a seed: the per-CPE RNG drives Subscription jitter, generator jitter, and any vendor-quirky reordering hooks the profile enables. Pass --seed=N and an entire dual-stack run replays byte-for-byte.
What's next¶
- Smoke-test against a Controller you own.
- Build a profile that mirrors a real device's
Device.LocalAgent.MTP.{i}.andDevice.LocalAgent.Controller.{i}.shape so the Controller sees the topology it expects. - Use the same fleet placeholders documented in Multi-CPE Fleets to differentiate Endpoint IDs, MTP credentials, and per-Agent topic subscriptions.