Figure out how to control Lyngdorf amplifier using UART from ESP32

The thing is, I don’t know that you can expect an ACK unless you send a packet to the correct address.

Thinking on this further, I wonder if using a higher level language may make things easier. Perhaps CircuitPython or something.

Hmm, I might be wrong, but my impression was that the dymmy_reviever will create a “fake” response by emulating an endpoint? Think I have seen examples in the forum where the response turns up with the debug active but no dummy_reciever.

Nope, just connected the ESP directly to the amp using a modified RJ12.

Have the same idea, the amp will not respond if it is not the correct address.

Might be missing something fundamental, but the example they give in the beginning does not make sense to me:

My interpretation is that this is the address. Checking my amp it has the link address 1 set (I checked this using the menu). Do not understand the PROD_DEV_CODE above the table, cannot find any other references to this in the documentation, my impression was that this would be the addressing to use, but seems to be unrelated.

Right now I´m going back to square one, not sure if the pinout config for the RJ12 is correct, though that this was a standard for RS232, but googling this it seems to be all over the place, so have asked Lyngdorf for the correct pinout used.

The address is sent in the 2nd and 3rd bytes, so there are 65535 possible addresses.

The PRODUCT_DEV_CODE is returned by command 200, which puts it in byte 23 of the answer - see page 46.

The example given in the quote in your last post does exactly that. It sends C8 hex (200 dec) to device with address 0001 and in the return 07 is in byte 23. 7 tells that it is a TDA2200+ RoomP.

1 Like

Thanks for the explanation. I think I might be in over my head :slight_smile: .
Got the pin-out from Lyngdorf for the RS232 cable and have connected the cabling to the ESP32 according to this:
RJ12 DB9 Pinout
(my notes in blue)

The config for UART is as follows:

# UART Serial interface
uart:
  baud_rate: 57600
  tx_pin: GPIO17
  rx_pin: GPIO16
  rx_buffer_size: 256
  data_bits: 8
  parity: NONE
  stop_bits: 1
  debug:
    direction: BOTH
    dummy_receiver: false
    sequence:
      - lambda: UARTDebug::log_binary(direction, bytes, ',');

Not 100% on this conf. but verified the baud rate on the amp at least, the rest is more or less the defaults.

Set up a switch that send the following command:

#test serial hex input, request for dev info

switch:

  - platform: uart
    name: "Comm test"
    data: [0x05, 0x01, 0x00, 0xc8, 0xce]

The amp reports having the “link address” 1, so I guess that 0x01 0x00 should be correct to address it.
This gives me the following log:

[21:22:46][VV][api.service:558]: on_switch_command_request: SwitchCommandRequest {
  key: 3250616424
  state: YES
}
[21:22:46][D][switch:013]: 'Comm test' Turning ON.
[21:22:46][D][switch:037]: 'Comm test': Sending state ON
[21:22:46][VV][api.service:156]: send_switch_state_response: SwitchStateResponse {
  key: 3250616424
  state: YES
}
[21:22:46][D][uart.switch:020]: 'Comm test': Sending data...
[21:22:46][D][switch:037]: 'Comm test': Sending state OFF
[21:22:46][VV][api.service:156]: send_switch_state_response: SwitchStateResponse {
  key: 3250616424
  state: NO
}
[21:22:46][D][uart_debug:196]: >>> 0b00000101 (0x05),0b00000001 (0x01),0b00000000 (0x00),0b11001000 (0xC8),0b11001110 (0xCE)
[21:22:50][VV][api.service:470]: on_ping_request: PingRequest {}
[21:22:50][VV][api.service:043]: send_ping_response: PingResponse {}

My expectation would be to get something back from the amp, but nothing. Just to test I also reversed the Tx/Rx connection, same result.

Any ideas on what to test to try to figure this out?

How? Looks from the manual that there is a method to set the baud rate on various devices (eg command code 204), but it doesn’t say a default. I would try 9600.

I really think you should be using a TTL/RS232 interface.

Have you tried the Show Address command?

The amp lets you set the baud rate using the standard interface (buttons) and display, so verified the rate and that is also where the “link address” is showed.
Looking to get hold of the original cable for RS232 from Lyndorf, I can then connect from my laptop and verify that it is working in default setup, can use portsniffer to look at the actual commands as well over the wire.

Will check the “show address” command, missed that one.

Also asked them to clarify the pinout on the RJ12 as well, realized late yesterday that it is possible to interpret in different ways.

@mwitt : I’m not sure whether you are still working on this, but I have a similar setup for my TDAi2200 (commands are different compared to SDAi but the way of serial control is identical)

My code (which creates switches in HA (should convert this to buttons once, they did not exist at the time of creating this):

esphome:
  name: esp-lyngdorf-serial
  includes:
    - parse_serial_data.h
  on_boot:
    priority: -100
    then:
      - switch.turn_on: lyngdorf_get_setup_data #retrieve current amp states after reboot

esp32:
  board: lolin32_lite
  framework:
    type: arduino

# Enable logging
logger:
  #Retrieve '19' messages from log
  on_message:
    level: DEBUG
    then:
    - if:
        condition:
          lambda: 'return strstr(message,"<<<");'
        then:
          - text_sensor.template.publish:
              id: lyngdorf_raw_msg #zoek naar level 5 tag uart_debug in string <<<
              state: !lambda 
                return remove_up_to_arrows(message);
#            return "Triggered on_message with level " + to_string(level) + ", tag " + tag + " and message " + message;

# Enable Home Assistant API
api:

ota:


wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Esphome-Web-XYZ"
    password: "XYZXYZ"

captive_portal:

uart:
  baud_rate: 9600
  id: uart_lyngdorf
  tx_pin:
    number: 17
    inverted: true
  rx_pin:
    number: 16
    inverted: true
  debug:
    direction: BOTH
    dummy_receiver: true
    after:
      delimiter: "\n"
    sequence:
      - lambda: UARTDebug::log_hex(direction, bytes, ',');

# To parse serial output and report to HA
text_sensor:
  - platform: template
    name: "RAW Lyngdorf message"
    id: "lyngdorf_raw_msg"
    icon: "mdi:card-text-outline"


binary_sensor:
  - platform: status #Connection status of ESP
    name: "Lyngdorf serial connection status"

##########
# Serial sent stuff and GPIO
##########
switch:
  - platform: gpio
    pin: #OnBoard led 
      number: 22
      inverted: true
    name: "Set Onboard LED"
    id: onboard_led
  - platform: uart
    name: "Lyngdorf: Set Volume 30"
    id: lyngdorf_volume_30
    icon: "mdi:volume-plus"
    data: [0x07, 0x01, 0x00, 0x73, 0x2c, 0x01, 0xA8] # return 02 aa
  - platform: uart
    name: "Lyngdorf: Set Digital 1"
    id: lyngdorf_set_digital_1
    icon: "mdi:import"
    data: [0x06, 0x01, 0x00, 0x71, 0x01, 0x79] # return 02 aa 
    on_turn_off:
      then:
        - delay: 1s
        - switch.turn_on: lyngdorf_get_setup_data
  - platform: uart
    name: "Lyngdorf: Set Digital 2"
    id: lyngdorf_set_digital_2
    icon: "mdi:import"
    data: [0x06, 0x01, 0x00, 0x71, 0x02, 0x7A] # return 02 aa 
    on_turn_off:
      then:
        - delay: 1s
        - switch.turn_on: lyngdorf_get_setup_data
  - platform: uart
    name: "Lyngdorf: Set Digital 3"
    id: lyngdorf_set_digital_3
    icon: "mdi:import"
    data: [0x06, 0x01, 0x00, 0x71, 0x03, 0x7B] # return 02 aa 
    on_turn_off:
      then:
        - delay: 1s
        - switch.turn_on: lyngdorf_get_setup_data
  - platform: uart
    name: "Lyngdorf: Set Digital 4"
    id: lyngdorf_set_digital_4
    icon: "mdi:import"
    data: [0x06, 0x01, 0x00, 0x71, 0x04, 0x7C] # return 02 aa 
    on_turn_off:
      then:
        - delay: 1s
        - switch.turn_on: lyngdorf_get_setup_data
  - platform: uart
    name: "Lyngdorf: Set Digital 5"
    id: lyngdorf_set_digital_5
    icon: "mdi:import"
    data: [0x06, 0x01, 0x00, 0x71, 0x05, 0x7D] # return 02 aa 
    on_turn_off:
      then:
        - delay: 1s
        - switch.turn_on: lyngdorf_get_setup_data
  - platform: uart
    name: "Lyngdorf: Set Power On"
    id: lyngdorf_set_power_on
    icon: "mdi:power-on"
    data: [0x06, 0x01, 0x00, 0x75, 0x01, 0x7D] # return 02 aa 
  - platform: uart
    name: "Lyngdorf: Set Power Off"
    id: lyngdorf_set_power_off
    icon: "mdi:power-off"
    data: [0x06, 0x01, 0x00, 0x75, 0x00, 0x7C] # No return
  - platform: uart
    name: "Lyngdorf: Comm test"
    id: lyngdorf_send_comm_test
    icon: "mdi:test-tube"
    data: [0x05, 0x01, 0x00, 0x01, 0x07] # return 02 aa
  - platform: uart
    name: "Lyngdorf: Get setup data"
    id: lyngdorf_get_setup_data
    icon: "mdi:cog"
    data: [0x05, 0x01, 0x00, 0xC8, 0xCE] # Returns setup data

My ESP code, creates switches in HA (i should convert this to buttons once, they did not exist at the time of creating this) and puts the reply by amplifier into the raw message, this is parsed by a separate piece of code in AppDaemon (out of this I get information like power state, volume and selected input back into HA)

AppDaemon code:


  def parse_Lyngdorf_state(self, NewData):
    if (len(self.LyngdorfDataBuffer) > 0):
      self.LyngdorfDataBuffer = self.LyngdorfDataBuffer + "," + NewData
    else:
      self.LyngdorfDataBuffer = NewData

    while (len(self.LyngdorfDataBuffer) > 15):
      state_array = self.LyngdorfDataBuffer.split(",")
      if (state_array[0] == "19"):
        #self.log("parsing 19, status message")
        if (len(state_array) > 24): #25 parts in complete msg
          self.parse_Lyngdorf_msg19(state_array)
          self.LyngdorfDataBuffer = self.LyngdorfDataBuffer[75:len(self.LyngdorfDataBuffer)]
        else:
          self.log("message 19 too short")
          break
      elif (state_array[3] == "73"): #73:	07,..,..,73,C5,01,..	Volume, actual volume C5,01
        if (len(state_array) > 6): #7 parts in complete msg
          #self.log("Msg 73, volume, received")
          self.parse_Lyngdorf_73_volume(state_array)
          self.LyngdorfDataBuffer = self.LyngdorfDataBuffer[21:len(self.LyngdorfDataBuffer)]
        else:
          self.log("message 73 too short")
          break
      elif (state_array[3] == "74"): #74:	06,..,..,74,01,..	Mute, 1=on
        if (len(state_array) > 5): #6 parts in complete msg
          #self.log("Msg 74, mute,  received")
          self.parse_Lyngdorf_74_mute(state_array)
          self.LyngdorfDataBuffer = self.LyngdorfDataBuffer[18:len(self.LyngdorfDataBuffer)]
        else:
          self.log("message 74 too short")
          break
      elif (state_array[3] == "75"): #75	06,..,..,75,00,..	Power, 0 is off
        if (len(state_array) > 5): #6 parts in complete msg
          #self.log("Msg 75, power, received")
          self.parse_Lyngdorf_75_power(state_array)
          self.LyngdorfDataBuffer = self.LyngdorfDataBuffer[18:len(self.LyngdorfDataBuffer)]
        else:
          self.log("message 75 too short")
          break
      elif (state_array[3] == "89"): #89	07,..,..,89,00,64,..	Delta volume -- IGNORE
        if (len(state_array) > 6): #7 parts in complete msg
          #self.log("Msg 89 received, ignoring volume delta message")
          self.LyngdorfDataBuffer = self.LyngdorfDataBuffer[21:len(self.LyngdorfDataBuffer)]
        else:
          self.log("message 89 too short")
          break
      elif ( (state_array[0] == "02") and (state_array[1] == "AA")): #ACK message - ignore
        self.LyngdorfDataBuffer = self.LyngdorfDataBuffer[6:len(self.LyngdorfDataBuffer)]
      else:
        self.log("Invalid message received from Lyngdorf " + self.LyngdorfDataBuffer)
        #ignore all remaining
        self.LyngdorfDataBuffer = ""
        break

  def parse_Lyngdorf_msg19(self, state_array):
    if (len(state_array) >= 9): #only focus on parsed data max field=9
      #Power (not ignored here, in case I accidentally mis a message), komt wel vaak als hij uit staat, niet als hij aan staat
      #self.log("power " + state_array[1])
      if (state_array[1] == "01"):
        self.set_state("input_boolean.lyngdorf_power_state", state="on")
      else:
        self.set_state("input_boolean.lyngdorf_power_state", state="off")
      
      #Volume
      #ignore volume here, will be sent regularly (and has offset....)
      #self.parse_Lyngdorf_volume(state_array[3], state_array[2])
      #calculated_vol = ((256*(int(state_array[3], 16)) + (int(state_array[2], 16)) -100))/10
      #self.set_state("input_text.lyngdorf_volume_state", state=str(calculated_vol))
      
      #Mute
      #ignore mute, will be sent regularly in seperate message 
      #self.log("mute " + state_array[4])
      #if (state_array[4] == "01"):
      #  self.set_state("input_boolean.lyngdorf_mute_on_state", state="on")
      #else:
      #  self.set_state("input_boolean.lyngdorf_mute_on_state", state="off")
      
      #Input This is not sent regulary by Lyngdorf themselves
      input=state_array[9]
      if (input == "05"): #Order optimized for performance
        self.set_state("input_text.lyngdorf_input_state", state="Digital 1")
      elif (input == "09"):
        self.set_state("input_text.lyngdorf_input_state", state="Digital 5")
      elif (input == "06"):
        self.set_state("input_text.lyngdorf_input_state", state="Digital 2")
      elif (input == "07"):
        self.set_state("input_text.lyngdorf_input_state", state="Digital 3")
      elif (input == "08"):
        self.set_state("input_text.lyngdorf_input_state", state="Digital 4")
      elif (input == "01"):
        self.set_state("input_text.lyngdorf_input_state", state="Analog 1")
      elif (input == "02"):
        self.set_state("input_text.lyngdorf_input_state", state="Analog 2")
      elif (input == "03"):
        self.set_state("input_text.lyngdorf_input_state", state="Analog 3")
      elif (input == "04"):
        self.set_state("input_text.lyngdorf_input_state", state="Analog 4")
      else:
        self.set_state("input_text.lyngdorf_input_state", state="unknown")

      #0  19: nr of bytes                                n.i. (not interesting)
      #1  01: 1 power on                                  implemented
      #2  26: 38 vol LSB                                 implemented
      #3  01: 1 vol MSB                                  implemented
      #4  00: MUTE (off)                                 implemented
      #5  5E:def vol LSB  (dit moet samen 35 zijn?)      n.i.
      #6  01,def vol MSB                                 n.i.
      #7  52,max vol lsb                                 n.i.
      #8  03,max vol msb                                 n.i.
      #9  09,SRC (digital 5)                             implemented
      #10 01,PReset 1                                    n.i. ???
      #11 01,Display max intens                          n.i.
      #12 00,POL                                         n.i.
      #13 00,P1                                         n.i.
      #14 00,P2                                         n.i.
      #15 00,P3                                         n.i.
      #16 00,P4                                         n.i.
      #17 00,Remote setting both                         n.i.
      #18 00,remote no disable                           n.i.
      #19 01,master                                      n.i.
      #20 00,balance                                     n.i.
      #21 00,version                                     n.i.
      #22 26,version                                     n.i.
      #23 07,dev code                                    n.i.
      #24 2E,checksum                                    n.i.

  def parse_Lyngdorf_73_volume(self, state_array): ##73:	07,..,..,73,C5,01,..	Volume, actual volume C5,01
    #self.log("parsing 73 volume")
    self.parse_Lyngdorf_volume(state_array[5], state_array[4])

  def parse_Lyngdorf_74_mute(self, state_array): #74:	06,..,..,74,01,..	Mute, 1=on
    #self.log("parsing 74 mute " + state_array[4])
    if (state_array[4] == "01"):
      self.set_state("input_boolean.lyngdorf_mute_on_state", state="on")
    else:
      self.set_state("input_boolean.lyngdorf_mute_on_state", state="off")

  def parse_Lyngdorf_75_power(self, state_array): #75	06,..,..,75,00,..	Power, 0 is off right?
    #self.log("parsing 75 power " + state_array[4])
    if (state_array[4] == "01"):
      self.set_state("input_boolean.lyngdorf_power_state", state="on")
    else:
      self.set_state("input_boolean.lyngdorf_power_state", state="off")

  def parse_Lyngdorf_volume(self, MSB, LSB):
    calculated_vol = ((256*(int(MSB, 16)) + (int(LSB, 16)) -100))/10
    self.set_state("input_text.lyngdorf_volume_state", state=str(calculated_vol))
    #self.log("calculated volume " + str(calculated_vol))