Summary

Network control spec for the Linn 360 flagship loudspeaker over the Linn LPEC (Linn Private Ethernet Control) protocol. LPEC is a text protocol carried over a raw TCP socket session on port 23, exposing UPnP-style services (Product, Preamp, MediaRenderer/AVTransport, MediaRenderer/RenderingControl, Pins) via ACTION, SUBSCRIBE, and UNSUBSCRIBE messages with RESPONSE/EVENT replies.

Transport

protocols:
  - tcp
addressing:
  port: 23  # raw socket session (LPEC uses TCP port 23 but is NOT RFC telnet)
auth:
  type: none  # inferred: no auth procedure in source
# Session limits stated in source:
#   - max 4 simultaneous LPEC sessions; sessions 5+ are ignored
#   - each product supports only one concurrent LPEC session (see Other Considerations - note conflicts with the 4-session statement; left as documented)
# UNRESOLVED: no baud/serial config applicable (TCP only). No keepalive/heartbeat spec.

Traits

traits:
  - queryable   # inferred: GetVolume, Volume, Source, GetIdArray query examples in source
  - routable    # inferred: SetSourceIndex / SetSourceBySystemName / SetSourceIndexByName source-selection examples
  - levelable   # inferred: SetVolume volume-control example
  # powerable NOT added: source shows ProductStandby only as an evented variable; no explicit standby/power ACTION is documented.

Actions

# Source documents LPEC by example, not as an exhaustive command catalogue.
# "LPEC does not implement a service discovery protocol. The specific set of
# services and actions implemented on each device is dependent on the device
# type." The entries below are the distinct ACTION/operation examples the
# source documents verbatim. The 360's full action set must be discovered via
# UPnP (device.xml -> service.xml) and mapped onto LPEC ACTIONs.
actions:
  # --- RenderingControl (MediaRenderer) ---
  - id: set_volume
    label: Set Volume
    kind: action
    service: MediaRenderer/RenderingControl
    command: 'ACTION MediaRenderer/RenderingControl 1 SetVolume "0" "Master" "50"'
    params:
      - name: instance
        type: string
        description: 'Instance ID (source uses "0")'
      - name: channel
        type: string
        description: 'Channel name (source uses "Master")'
      - name: volume
        type: integer
        description: Volume level (0-100 range typical; exact range UNRESOLVED - not stated in source)
    notes: Source example sets volume to 50. No output args.

  - id: get_volume
    label: Get Volume
    kind: query
    service: MediaRenderer/RenderingControl
    command: 'ACTION MediaRenderer/RenderingControl 1 GetVolume "0" "Master"'
    params:
      - name: instance
        type: string
        description: 'Instance ID (source uses "0")'
      - name: channel
        type: string
        description: 'Channel name (source uses "Master")'
    notes: Returns RESPONSE "<volume>".

  # --- AVTransport (MediaRenderer) ---
  - id: play
    label: Play
    kind: action
    service: MediaRenderer/AVTransport
    command: 'ACTION MediaRenderer/AVTransport 1 Play "0" "1"'
    params:
      - name: instance
        type: string
        description: 'Instance ID (source uses "0")'
      - name: speed
        type: string
        description: 'Play speed (source uses "1")'

  - id: pause
    label: Pause
    kind: action
    service: MediaRenderer/AVTransport
    command: 'ACTION MediaRenderer/AVTransport 1 Pause "0"'
    params:
      - name: instance
        type: string
        description: 'Instance ID (source uses "0")'

  - id: next
    label: Next Track
    kind: action
    service: MediaRenderer/AVTransport
    command: 'ACTION MediaRenderer/AVTransport 1 Next "0"'
    params:
      - name: instance
        type: string
        description: 'Instance ID (source uses "0")'

  - id: previous
    label: Previous Track
    kind: action
    service: MediaRenderer/AVTransport
    command: 'ACTION MediaRenderer/AVTransport 1 Previous "0"'
    params:
      - name: instance
        type: string
        description: 'Instance ID (source uses "0")'

  # --- Preamp ---
  - id: volume_query
    label: Preamp Volume Query
    kind: query
    service: Preamp/Preamp
    command: 'ACTION Preamp/Preamp 1 Volume'
    params: []
    notes: Returns RESPONSE "<volume>" (source example RESPONSE "40").

  - id: set_mute
    label: Set Mute
    kind: action
    service: Preamp/Preamp
    command: 'ACTION Preamp/Preamp 1 SetMute "true"'
    params:
      - name: mute
        type: boolean
        description: '"true" to mute, "false" to unmute (XML-escaped, quoted)'
    notes: No output args (empty RESPONSE).

  # --- Product (source selection) ---
  - id: set_source_index
    label: Set Source By Index
    kind: action
    service: Ds/Product
    command: 'ACTION Ds/Product 1 SetSourceIndex "0"'
    params:
      - name: index
        type: integer
        description: 'Source index (source: "Index numbers are not guaranteed and may change with different versions of Linn firmware or if sources are disabled"). Prefer SetSourceBySystemName.'
    notes: Version 1. Indices are NOT stable across firmware/invisibility changes.

  - id: set_source_by_systemname
    label: Set Source By System Name
    kind: action
    service: Ds/Product
    command: 'ACTION Ds/Product 2 SetSourceBySystemName "Balanced"'
    params:
      - name: system_name
        type: string
        description: 'System name (always fixed): e.g. Playlist, Radio, Songcast, NetAux, UpnpAv, Analog1, Phono, Balanced, Front Aux, HDMI1, SPDIF1, TOSLINK1'
    notes: Version 2. System names are "always fixed" per source table.

  - id: set_source_index_by_name
    label: Set Source By Name (Konfig)
    kind: action
    service: Ds/Product
    command: 'ACTION Ds/Product 2 SetSourceIndexByName "CD12"'
    params:
      - name: name
        type: string
        description: 'Konfig-adjusted input name (examples: Playlist, Radio, Airplay, UpnpAv, Analog1, LP12, CD12, iPod, SKY, DAT Player, CD Player)'
    notes: Version 2. Name adjusted by Konfig input name.

  - id: source_query
    label: Source Info Query
    kind: query
    service: Ds/Product
    command: 'ACTION Ds/Product 2 Source "9"'
    params:
      - name: index
        type: integer
        description: Source index to look up
    notes: Returns SourceIndex, Systemname, and ByName for the given index.

  # --- Pins ---
  - id: pins_invoke_id
    label: Invoke PIN
    kind: action
    service: Ds/Pins
    command: 'ACTION Ds/Pins 1 InvokeId "1"'
    params:
      - name: pin_id
        type: integer
        description: 'PIN id (NOT guaranteed; may change after reboot if recently altered)'
    notes: Requires Davaar 67+.

  - id: pins_read_list
    label: Read PIN List
    kind: query
    service: Ds/Pins
    command: 'ACTION Ds/Pins 1 ReadList "[ 1]"'
    params:
      - name: json_array
        type: string
        description: 'JSON-style array of PIN ids (source example "[ 1]")'

  - id: pins_get_id_array
    label: Get PIN Id Array
    kind: query
    service: Ds/Pins
    command: 'ACTION Ds/Pins 1 GetIdArray'
    params: []
    notes: Returns the live PIN id array.

  # --- Protocol primitives (documented command forms) ---
  - id: subscribe
    label: Subscribe To Service Events
    kind: action
    command: 'SUBSCRIBE [sub-device]/[service]'
    params:
      - name: service_path
        type: string
        description: 'e.g. Ds/Product, MediaRenderer/AVTransport'
    notes: Device replies SUBSCRIBE <subscription-id> then sends an initial EVENT with current state. Max 16 subscriptions per session.

  - id: unsubscribe_id
    label: Unsubscribe By Subscription Id
    kind: action
    command: 'UNSUBSCRIBE [subscription-id]'
    params:
      - name: subscription_id
        type: integer
        description: Subscription id returned by SUBSCRIBE

  - id: unsubscribe_service
    label: Unsubscribe By Service Path
    kind: action
    command: 'UNSUBSCRIBE [sub-device]/[service]'
    params:
      - name: service_path
        type: string
        description: 'e.g. Ds/Product'

  - id: unsubscribe_all
    label: Unsubscribe All
    kind: action
    command: 'UNSUBSCRIBE'
    params: []
    notes: Multiple UNSUBSCRIBE responses may be received (one per active subscription).

Feedbacks

# Observable state arrives via RESPONSE (synchronous, to an ACTION query) and
# EVENT (unsolicited, for evented variables). Documented evented variables and
# response shapes from source examples:
feedbacks:
  - id: action_response
    type: string
    description: 'RESPONSE "<outarg1>" "<outarg2>" ... - output args of the invoked ACTION (empty RESPONSE when no output args). All args XML-escaped and double-quoted.'

  - id: product_name
    type: string
    description: 'Evented Product service variable. Example value "Sneaky Music DS".'

  - id: product_room
    type: string
    description: 'Evented Product service variable. Example value "Main Room".'

  - id: product_standby
    type: boolean
    description: 'Evented Product service variable (standby state). Example "true".'
    values: ["true", "false"]

  - id: product_source_index
    type: integer
    description: 'Evented Product service variable (currently selected source index).'

  - id: volume
    type: integer
    description: 'Returned by GetVolume / Preamp Volume query. Source example RESPONSE "40".'
# UNRESOLVED: full set of evented variables per service not enumerated in source (discover via UPnP service.xml).

Variables

# All settable parameters in source appear as discrete ACTIONs (SetVolume,
# SetMute, SetSource*). No separate parameter-table variables documented.
# UNRESOLVED: device exposes additional settable variables via UPnP services
# not enumerated in this source.

Events

# Unsolicited notifications the device sends (all <CR><LF> terminated):
events:
  - id: event_state_change
    type: event
    command: 'EVENT [subscription-id] [sequence-no] [var1-name] "[var1]" [var2-name] "[var2]" ... [varn-name] "[varn]"'
    description: 'Sent on subscription (sequence-no 0 = initial full state) and on subsequent variable changes (sequence-no increments, 32-bit unsigned, wraps to 1). Only changed variables are reported.'

  - id: alive
    type: event
    command: 'ALIVE [sub-device] [udn]'
    description: 'Sent on connect for each enabled sub-device, and when a sub-device is (re)enabled. Example: ALIVE Ds 4c494e4e-0050-c221-71e5-df000003013f'

  - id: byebye
    type: event
    command: 'BYEBYE [sub-device] [udn]'
    description: 'Sent when a sub-device is disabled. Commonly preceded by unsolicited UNSUBSCRIBE messages (subscriptions forcibly revoked). Sent for all sub-devices before the device closes the LPEC connection on reboot.'

  - id: error
    type: event
    command: 'ERROR [code] "[description]"'
    description: 'Error reply to an ACTION/SUBSCRIBE/UNSUBSCRIBE. Defined codes: 101 Command not recognised; 102 Service not specified; 103 Service not found; 104 Version invalid; 105 Version not specified; 106 Version not supported; 107 Method not specified; 108 Method execution exception; 201 Boolean argument invalid; 202 String argument invalid; 203 Unsigned numeric argument invalid; 204 Signed numeric invalid; 205 Binary argument invalid; 206 Invalid argument escaping; 301 Argument list incomplete; 302 Argument not quoted; 303 Argument incomplete; 401 Already subscribed; 402 Client has too many subscriptions; 403 Service has too many subscriptions; 404 Subscription not found; 405 Service not subscribed; 406 Invalid XML escaping. Service-specific ERROR messages may also be sent.'

  - id: subscribe_ack
    type: event
    command: 'SUBSCRIBE [subscription-id]'
    description: 'Acknowledgement of a SUBSCRIBE; carries the assigned subscription id.'

  - id: unsubscribe_ack
    type: event
    command: 'UNSUBSCRIBE [subscription-id]'
    description: 'Acknowledgement of an UNSUBSCRIBE. For unsubscribe-all, one ack per active subscription may arrive.'

Macros

# No multi-step sequences described explicitly in source.
# UNRESOLVED: populate from source if a 360-specific macro section exists elsewhere.

Safety

confirmation_required_for: []
interlocks: []
# No safety warnings, interlock procedures, or power-on sequencing requirements stated in source.

Notes

  • Transport nuance: LPEC uses TCP port 23 but is a raw socket session, not RFC 854 telnet. Do not negotiate telnet options.
  • Session limits: Source states both "up to 4 simultaneous sessions" (Connection section) and "each product supports only one concurrent LPEC session" (Other Considerations). This contradiction is left as-documented — verify against the target 360 device.
  • Connection hygiene: Source recommends control devices close the TELNET/LPEC connection when the touchpanel is asleep (Crestron processors are known to re-open ports without closing existing ones).
  • Known bug: The first command sent immediately after the initial ALIVE messages generates a spurious ERROR. Workaround: send a blank/no-op command right after ALIVEs to absorb the error, then proceed normally.
  • Argument encoding: All input/output arguments are XML-escaped and enclosed in double-quotes.
  • Subscription limits: Up to 16 services may be subscribed simultaneously per session. Subscriptions are auto-cleaned when the control point closes the session, and forcibly revoked on sub-device disable (BYEBYE).
  • Source indices are unstable: SetSourceIndex numeric indices can change with firmware version or when sources are made invisible (Davaar 50+). Prefer SetSourceBySystemName (always-fixed system names).
  • PIN ids are unstable: PIN ids can change after a reboot if recently altered. Requires Davaar 67+.
  • Service discovery: LPEC has no built-in service discovery. Discover available services/actions via UPnP (http://<ip>:<port>/<device>/device.xmlservice.xml) and map onto LPEC ACTIONs. Example device URL form: http://192.168.0.123:55178/Ds/device.xml.

---

## Provenance

```yaml
source_domains:
  - docs.linn.co.uk
source_urls:
  - https://docs.linn.co.uk/wiki/index.php/Developer:LPEC
  - https://docs.linn.co.uk/wiki/index.php/Developer:Documentation
  - https://docs.linn.co.uk/wiki/index.php/Linn_DS/DSM_control
  - https://docs.linn.co.uk/wiki/index.php/RS232
retrieved_at: 2026-06-30T10:57:14.429Z
last_checked_at: 2026-07-07T11:46:02.599Z

Verification Summary

verdict: verified
checked_at: 2026-07-07T11:46:02.599Z
matched_actions: 19
action_count: 19
confidence: medium
summary: "All 19 spec actions matched verbatim in source; complete bidirectional coverage of LPEC command set and transport parameters. (5 unresolved item(s) noted in Known Gaps.)"

Known Gaps

- "Source is the general Linn LPEC Protocol Reference. It does not document 360-specific services/actions; the concrete service/action set for the 360 is device-dependent and is normally discovered via UPnP (device.xml / service.xml). Firmware/protocol version ranges, power specs, and the full 360 action catalogue are not stated."
- "no baud/serial config applicable (TCP only). No keepalive/heartbeat spec."
- "full set of evented variables per service not enumerated in source (discover via UPnP service.xml)."
- "device exposes additional settable variables via UPnP services"
- "populate from source if a 360-specific macro section exists elsewhere."

From the AI4AV catalog (https://ai4av.net) · ODbL-1.0