Remeha, I2C, MCBA en het protocol (deel 2)

In deel 1 zette ik de eerste stappen naar het uitzoeken van het protocol. In dit deel volgt de uitwerking.

In het eerste deel gaf ik de generieke vorm van de seriële opdrachten al weer, telkens per byte: <lengte><opdracht><I²C-address><I²C-register><data…><…><checksum>

De lengte is de gehele lengte van het bericht, inclusief de lengte-byte zelf.

De antwoorden hebben een vergelijkbare opzet, maar variëren per opdracht enigszins. De generieke vorm is: <lengte><data…><checksum>

Er zijn – zo ver ik kan nagaan – vier mogelijkheden om met de ketel te communiceren. Als master of als slave; en in beide gevallen: lezen of schrijven. Die vier methoden hebben allemaal een eigen opdracht, als volgt:

gezien vanuit de PC-interfacelezenschrijven
master0x420x43
slave0x400x41
de vier mogelijke opdrachten

Als de PC (of de ketelmonitor, of whatever) dus als busmaster iets wil lezen uit de ketel, stuurt de PC 0x42 als seriële opdracht aan de RMI1414.

Het (7-bits) I²C-bus-adres wordt op de seriële bus shifted, als 8-bits-adres dus, doorgegeven. Adressen 0x50 en 0x57 worden dus respectievelijk serieel 0xA0 en 0xAE.

Master/lezen

Dan de antwoorden. Het eenvoudigste geval is master/lezen. De master (dat is de kant van de computer) stuurt bijvoorbeeld: <lengte: 0x07> <lezen: 0x42> <slave addr 0x50: 0xA0> <register: 0x40> <aantal te lezen bytes: 0x08> <onbekend 0x40> <checksum 0x8F>

Op de I²C-bus komt achtereenvolgens 0x50W0x40, 0x50R – de ketel stuurt in antwoord bijvoorbeeld de volgende 8 bytes: 0x37 0x0D 0x3C 0x59 0x6E 0x2F 0x00 0x0F en daar maakt de seriële interface het volgende van: <lengte: 0x0B> <onbekend: 00> <data: 37 0D 3C 59 6E 2F 00 0F> <checksum: 70>

Master / schrijven

Dan busmaster/schrijven. Die gaat als volgt. De PC of de ketelmonitor schrijft op de seriële poort bijvoorbeeld: <lengte: 0x0A> <schrijven: 0x43> <I²C addr 0x50: 0xA0> <register: 0x40> <data: 0x38 0x0D 0x3C 0x59> <onbekend: 0x50> <checksum: 0xA9>

Op de I²C-bus komt nu: 0x50W0x40 0x38 0x0D 0x3C 0x59. De ketel stuurt niks terug (dat kan ook niet), anders dan correcte ACK-bits voor elke geschreven byte. De seriële interface meldt terug dat de gegevens op de bus gezet zijn: <Lengte: 0x04> <onbekend: 0x10> <onbekend: 0x06> <checksum: 0xE6>

Let dus op dat lezen telkens in series van 8 bytes gebeurt, maar dat bij schrijven slechts vier bytes naar hetzelfde register geschreven worden. De serie 0x37 0x0D 0x3C 0x59 0x6E 0x2F 0x00 0x0F (gelezen) wordt dus in twee separate opdrachten weer weggeschreven, eerst 0x37 0x0D 0x3C 0x59, daarna 0x6E 0x2F 0x00 0x0F.

Slave / schrijven

Na zo’n schrijfsessie schrijft Recom opeens twee bytes als slave, middels de opdracht (serieel) <0x07> <0x41> <slave 0x57: 0xAE> <register: 0x40> 0x00 0x00 <checksum: 0xCA>. De ketel probeert inderdaad regelmatig registers van slave 0x57 te lezen. Zonder GMI1414 lukt dat helemaal niet, niemand beantwoordt deze vragen: 0x57W – NACK. Wanneer de GMI1414 de opdracht heeft om register 0x40 “aan te bieden”, antwoordt hij op slave-address 0x57; maar nog steeds alleen voor het juiste register. Schrijfopdrachten voor andere registers stuiten opnieuw op een NACK; het slave-address wordt wel beantwoord, maar het register niet: 0x57W0x18 – NACK; 0x57W0x20 – NACK. Alleen schrijven op 0x40 gaat goed: 0x57W0x40 – ack 0x00 0x00 0xFF 0xFF 0xFF 0xFF 0xFF en tenslotte een laatste read (0xFF) met een NACK. Protocolgeleerden zien volgens mij, dat de RMI1414 twee bytes zendt; daarna nog vijf bytes “ACKt” en pas bij de achtste byte niet meer reageert (een NACK op de bus is in dit geval geen actief proces, maar juist de afwezigheid van een ACK). Het antwoord op de seriële bus verschijnt trouwens op het moment dat de laatste bytes nog geschreven moeten worden, dus het lijkt erop dat de ACK op die vijf bytes meer een uit vriendelijkheid zijn dan dat de RMI1414 er iets mee beoogt. Het seriële antwoord luidt <lengte: 0x04> <onbekend 0x10> <onbekend 0x02> <checksum 0xEA>.

Wanneer de ketel bovenstaande sequentie niet beantwoordt, laat Recom een popup zien dat je de ketel moet resetten om de nieuwe waarden te implementeren. Vermoedelijk is dit dus een soort “flush”, een reload van de ketelvariabelen.

Na een schrijfsessie (die overigens de volledige registerruimte van 0x40 tot en met 0x7F beslaat) en de flush, gaat Recom alle variabelen opnieuw inlezen, dus 0x0A 0x42 0xA0 [0x40 t/m 0x78 in stappen van 8] 0x08 enz. De opgevraagde waarden hoeven verder niet gelijk te zijn aan de zojuist weggeschreven waarden: als je bijvoorbeeld een maximumtemperatuur van 0x37 hebt weggeschreven, maar direct daarna een waarde van 0x38 terugkrijgt, vindt Recom dat niet vreemd of onjuist.

Lezen als slave

Tenslotte nog het lezen als slave. In de vorige posting zagen we al, dat op adres 0x57 diverse ketelwaarden worden weggeschreven door de ketel, als master. Ik interpreteerde destijds, hoewel nog niet in het bezit van een GMI1414, correct dat dit de actuele meetwaarden zijn van de ketelbesturing. Recom leest dus deze waarden en hoewel ik eigenlijk vooral in het I²C-protocol geïnteresseerd ben, is het voor de volledigheid wel netjes om ook de bijbehorende seriële communicatie hier uit te werken.

Recom begint – zoals zo vaak – met de standaard “hallo wie is daar”-riedel, als master: 0x07 0x42 0xA0 0x00 0x05 0x40 0xD2 en krijgt 0x08 0x00 0xAA 0x02 0x24 0x01 0x00 0x27 terug.

Daarna geht’s los: <lengte 0x06> <lezen: 0x40> <I²C address 0x57: 0xAE> <register: 0x00> <aantal bytes: 0x08> <checksum: 0x04>

Recom gaat nu staan wachten tot de ketel een keer <0x57W0x00> schrijft. Schrijfopdrachten naar andere registers worden niet gehonoreerd: 0x57W18 – NACK; 0x57W40 – NACK… enzovoorts. Tot: 0x57W00 <ACK> <0x37 0x35 0xDB 0xDB 0xDB 0x00 0x00 0x14>. Serieel wordt dat teruggemeld met het slave-adres en register opnieuw in het bericht: <lengte 0x0D> <onbekend: 0x00> <Slave-adr 0x57: 0xAE> <register: 0x00> <data: 0x37 0x35 0xDB 0xDB 0xDB 0x00 0x00 0x14> <checksum: 0x34>

Recom leest alleen telkens 8 bytes uit registers 0x00, 0x08, 0x10 en 0x28. Daarna opnieuw de hallo-wie-is-daar en de bovenstaande reeks gegevens.

Conclusie

Samengevat samengevat: we kunnen op de I²C-bus volstaan met eenmalig lezen als master op adres 0x50 voor de ketelparameters; daarna lezen we telkens 32 bytes als slave om de actuele ketelwaarden (aanvoer- en retourtemperatuur) uit te lezen. De precieze benodigdheden uit deze reeks van bytes zal ik nog nader uitwerken.

Ketelsimulatie

Tenslotte. Een ketelcommunicatieprotocol uitdokteren is natuurlijk niet volledig zonder de bijbehorende ketelsimulator. In python. Ik draaide het op een Raspberry Pi, die met behulp van een ouderwetsche nullmodem-kabel (ja heus!) op de PC met Recom aangesloten zit. Recom detecteert een Quinta-P1 en gaat vrolijk waarden uitlezen. De “ketel” is overigens gewoon een tabelletje met vaste waarden, er gebeurt dus bitter weinig.

#!/usr/bin/python3
import serial 
prt = serial.Serial('/dev/ttyUSB0', 4800)
#prt.open()
#prt.reset_input_buffer()

# msg moet een list zijn.
def validmessage(msg):
 # correct als: msg.len = blah[0]
 # msg[0,1,2,3,4....]++ = 0
 if (len(msg) < msg[0]): return []
 checksum=0
 for val in msg[:msg[0]]:
   checksum+=val
 if (checksum%256): return []
 else: return msg[:msg[0]]

def sendmessage(msg):
 # correct als: msg.len = blah[0]
 # msg[0,1,2,3,4....]++ = 0
 newmsg=[len(msg)+3, 0] + msg
# print('newmsg: ', newmsg)
 checksum=0
 for val in newmsg:
   checksum+=val
 newmsg.append(256 - ( checksum % 256 ))
# print('resultaat na checksum: ',newmsg)
 return newmsg

# vertalen met bytes.fromhex('.....')
# of zelfs: list(bytes.fromhex('....')
slave50 = {
 0x00: 'AA 02 24 01 00',
 # dit is: configurations: failure.
 # byte 0: locking.code; 1 status.code; 2 aanvoer, 3 retour, 4 boiler, 5 rookgas, 6 en 7: a.1 + b.0 x 0,05 zegt de xml, da's 6 MSB, 7 LSB
 0x08: '02 01 43 E3 DB DB 2D 1F', # storing 1, status, aanvoer, retour, boiler, rookgas, tijd (2 bytes)
 0x10: '0B 00 46 3F DB DB 35 E8', # storing 2, status, aanvoer, retour, boiler, rookgas, tijd (2 bytes)
 0x18: '02 02 20 20 DB DB 7D DE',
 0x20: '02 02 34 33 DB DB 1E CF',
 0x28: '02 02 28 26 7F DB 15 B7',
 0x30: '02 02 1A 17 7F DB 14 FA',
 
 0x38: '0B 00 00 00 00 00 00 00',
#byte 0: max aanvoer CV 0x37 = 55; byte=1: nadraaitijd; byte=2: WWtemp; byte=3 gedwongen deellast max. aanvoer; byte=4 mx aanvoer service (0x6e=110°C);
#byte=5: max vent toertal hondertallen 4700; byte=6 min vent toertal (0?) byte=7 deellast vent rpm (1500).
 0x40: '37 0D 3C 59 6E 2F 00 0F',
#byte=9 dt boven X terugmoduleren; byte=10 intfselectie 0=opentherm,1=extern; byte=11 XX; 12: toestel uit bij T1>... enz; 
# byte = 13 max vent toertal; 
 0x48: '00 19 00 1F 01 2F 00 01',
 0x50: '50 26 03 26 14 00 49 02',
 0x58: '09 15 00 0F 14 00 64 0A',
 0x60: '1E 23 23 00 63 02 05 5C',
 0x68: '1E 14 0A 01 19 1F 1F 00',
 0x70: '00 00 00 00 00 00 00 00',
 0x78: '00 00 00 00 00 00 00 00' }

slave57 = {
# dit is configurations: sample
# byte 0: aanvoer; 1: retour; 2: boiler; 3: buiten; 4: rookgas; 5: toeren 7: setpunt (20°C)
 0x00: '37 35 DB DB DB 00 00 14',
# byte 9: bit 0: boilervraag; bit 1 XX; bit 2 LDS; bit 3 Warmtevraag; bit 6 min.gasdruk; bit 7 ionisatie
# byte 10: bit 0: gasklep; bit 2: Driewegklep; bit 6 Pomp
# byte 12: br. toerental: RPM; byte 12 is MSB, 13 is LSB (A.1 + B.0)
 0x08: '00 F2 BF 20 00 00 00 00', 
 # 16: "Pomp" - %. Whatever that may be. Ik weet ook niet of dat verandert.
 # - de rest blijft verder ongebruikt.
 0x10: '00 00 2B 12 5C 12 5C 05', 
 0x18: 'DC 64 0A 02 08 F3 24 00',
 0x20: '00 00 37 00 4B 14 00 00',
 0x28: '02 02 28 26 7F DB 15 B7' }

opdrachten = {
 0x40: 'slave - ketel leest uit',
 0x41: 'slave - ketel schrijft',
 0x42: 'busmaster; lees slave',
 0x43: 'busmaster; ketel leest' }
 
#lees byte. voeg toe aan ingevoerde bytes. probeer bericht te ontcijferen. ingevoerde bytes altijd kleiner dan 16 houden.
message=[]
while (True):
  char=prt.read(1)
  message.append(ord(char))
  if (len(message) > 16):
    message=message[1:]
    print(message)

  # check validity
  for begin in range(len(message)):
#    print(message[begin])
    valide=validmessage(message[begin:])
    if (valide):
      slaveadr=valide[2]
      slavereg=valide[3]
      slavelen=valide[4]
      print('Valide. lengte: 0x%x, opdracht 0x%x, i2c addr 0x%x (0x%x), register 0x%x' % (valide[0] , valide[1], slaveadr, int(slaveadr / 2), slavereg))
      if (valide[1] == 0x42):
        print('Lees slave %x (%x)/register %x/%x bytes' % (slaveadr, int(slaveadr/2), slavereg, slavelen))
        if (slaveadr == 0xa0):
#          print('slave 50')
          if slavereg in slave50:
            print('-->  we zenden: ', slave50[slavereg])
#            print (bytes.fromhex(slave50[slavereg]))
#            print (list(bytes.fromhex(slave50[slavereg])))
#            print (bytes.fromhex(slave50[slavereg]))
            prt.write(sendmessage(list(bytes.fromhex(slave50[slavereg]))))
          else:
            print('###register niet gevonden')
      elif (valide[1] == 0x43):
        print('Opdracht 0x43: schrijf naar slave')
        print('Bericht: ', message)
        prt.write(bytes.fromhex('04 10 06 E6'))
        # 04 lengte; 10 is "klaar", 06 is "6 bytes geschreven"
      elif (valide[1] == 0x41):
        print('Opdracht 0x41: schrijf als slave (ketel leest waarden) - ik zeg: prima, 2 waarden geschreven 04 10 02 EA')
        # de KETEL zet bijv. op de bus: schrijf 0x57 0x40 - en dan zeg ik: prima, hier komen de waarden.
        prt.write(bytes.fromhex('04 10 02 EA'))
      elif (valide[1] == 0x40):
        print('Opdracht 0x40: lees als slave (ketel schrijft waarden); register %x, lengte %x' %(slavereg, slavelen))
        if (slaveadr == 0xae):
          if slavereg in slave57:
            print('--> we zenden: ', slave57[slavereg])
            print('---> in rs232: ', sendmessage([ 0xae ] + [ slavereg ] + list(bytes.fromhex(slave57[slavereg]))))
#            print([ 0xae ] + [ slavereg ] + list(bytes.fromhex(slave57[slavereg])))
            prt.write(sendmessage([ 0xae ] + [ slavereg ] + list(bytes.fromhex(slave57[slavereg]))))
          else:
            print('### register niet gevonden')
#        prt.write(bytes.fromhex('04 10 02 EA'))
        # zie boven.
      else:
        print('### opdracht nog niet geïmplementeerd: 0x%x' % valide[1])
        print('Bericht: ', message)
      message=[]
      break