package main import ( "context" "time" "github.com/simonvetter/modbus" "k8s.io/klog" ) func modbusValuesToFloat(v uint16) float32 { return float32(v) / 10.0 } func (d *daemon) modbusRestart() error { d.modbusClient.Close() return d.modbusClient.Open() } // There are currently two devices connected to the modbus. // The first one (slave 1) is a temperature/humidity sensor. // The second one (slave 2) is a PTA8D08 transmitter // // Returns whether modbus should restart (only in case of an underlying network error) func (d *daemon) modbusUpdate() bool { var err error var numDevicesNotResponding int // Switch to slave 1 (BTA1) d.modbusClient.SetUnitId(1) // Read temperature and humidity var registersBTA1 []uint16 // temperature, humidity registersBTA1, err = d.modbusClient.ReadRegisters(1, 2, modbus.INPUT_REGISTER) if err != nil { numDevicesNotResponding += 1 klog.Warningf("error while reading registers from BTA1 %v", err) } else if len(registersBTA1) != 2 { klog.Warningf("expected two registers from modbus slave 1, but got %d", len(registersBTA1)) } else { d.mu.Lock() d.daemonState.tempSEM = modbusValuesToFloat(registersBTA1[0]) d.daemonState.humiditySEM = modbusValuesToFloat(registersBTA1[1]) d.mu.Unlock() } // Switch to slave 2 (KEC2) d.modbusClient.SetUnitId(2) // PT100 mapping // Channel 0: Cable -WGA6, Sensor "dp bottom" // Channel 1: Cable -WGA8, Sensor "dp inlet" // Channel 2: Cable WGA7, Sensor "dp top" var registersKEC2 []uint16 // temperatures dp registersKEC2, err = d.modbusClient.ReadRegisters(0, 3, modbus.HOLDING_REGISTER) if err != nil { numDevicesNotResponding += 1 klog.Warningf("error while reading registers from KEC2 %v", err) } else if len(registersKEC2) != 3 { klog.Warningf("expected three registers from modbus slave 2, but got %d", len(registersKEC2)) } else { d.mu.Lock() d.daemonState.tempDPBottom = modbusValuesToFloat(registersKEC2[0]) d.daemonState.tempDPInlet = modbusValuesToFloat(registersKEC2[1]) d.daemonState.tempDPTop = modbusValuesToFloat(registersKEC2[2]) d.mu.Unlock() } // Switch to slave 3 (KEC1) d.modbusClient.SetUnitId(3) // Do a read first to avoid side-effects from the subsequent write to the relay states var digitalInputs [8]bool var digitalInputRegisters []uint16 digitalInputRegisters, err = d.modbusClient.ReadRegisters(0x81, 8, modbus.HOLDING_REGISTER) if err != nil { numDevicesNotResponding += 1 klog.Warningf("error while reading digital inputs from KEC1 %v", err) } else { // Convert MODBUS words into bools for idx, value := range digitalInputRegisters { if value != 0 { digitalInputs[idx] = true } else { digitalInputs[idx] = false } } // TODO: Input mapping goes here } // We must wait between reading and writing to the -KEC1 relay board // because otherwise it chokes and times out the write registers // command. time.Sleep(time.Millisecond * 10) // KFA1-KFA8 var relayState [8]bool d.mu.Lock() // -KFA1 Roughing Pump (normally closed contact) relayState[0] = !d.daemonState.rpOn // -KFA2 Diffusion Pump relayState[1] = d.daemonState.dpOn // -KFA4 Button Vent relayState[3] = d.daemonState.vent.output // -KFA5 Button Pump-Down relayState[4] = d.daemonState.pumpdown.output // -KFA6 Fake-Pirani Rough (normally closed contact) relayState[5] = !d.aboveRough.output // -KFA7 Fake-Pirani High (normally closed contact) relayState[6] = !d.aboveHigh.output d.mu.Unlock() // The KEC1 module uses a non-standard MODBUS interface // instead of coils // 0x0100 is the open command // 0x0200 is the close command // We write 8 words (16-bit) to address 0x01 to update the relays var registerValuesKEC1 [8]uint16 // Convert the boolean values to the commands for idx, state := range relayState { if state { registerValuesKEC1[idx] = 0x0100 } else { registerValuesKEC1[idx] = 0x0200 } } err = d.modbusClient.WriteRegisters(0x01, registerValuesKEC1[:]) if err != nil { numDevicesNotResponding += 1 klog.Warningf("error while updating registers %v", err) } if numDevicesNotResponding >= 4 { klog.Warningf("no device did respond to our request. Probably a network timeout.") return true } return false } // Call modbusUpdate every 100 milliseconds func (d *daemon) modbusProcess(ctx context.Context) { for { select { case <-ctx.Done(): return default: shouldRestart := d.modbusUpdate() // the modbus library does not reopen the tcp socket in case of // a connection loss. if shouldRestart { klog.Infof("restarting modbus connection...") err := d.modbusRestart() if err != nil { klog.Warningf("failed to restart modbus %v", err) } } time.Sleep(time.Millisecond * 100) } } }