gonzojive / heatpump

Tools for communicating with the Chiltrix CX34 heat pump using a Raspberry Pi
Apache License 2.0
5 stars 2 forks source link

Multiple outdoor units #24

Open sodabrew opened 2 years ago

sodabrew commented 2 years ago

I installed two CX34 units. At a high level, I can see two options for talking to them:

  1. Two RS-485 busses, each machine is ID 1 on its own bus, run two copies of cx34collector.

    • cx34dash would be best with one web interface and either overlaid or side-by-side charts so any differences can be quickly observed. If there are two collector instances this means a list of grpc ports to talk to.
  2. One RS-485 bus, change the ID of one of the machines (it's a setting on the touch screen controller), modify cx34collector to accept a list of bus IDs to collect from.

    • Reading the Connect method in cx34.go, the slaveId variable seems bound up with the serial port instance. Would need to separate these so that one serial port instance is used by multiple modbus instances ... making sure frames aren't clobbered on the wire at the same time.

Other thoughts:

gonzojive commented 2 years ago

2 would be best. Some sort of configuration file that specifies what devices are available and what serial device should be used to connect to them sounds appropriate. The same config file could be used for the fan coil units, too. It'd be good to also assign a UUID to each device. AFAIK there is no serial number info available through modbus commands.

// config.proto

message Config {
  repeated HeatPump heat_pumps = 1;
  repeated FanCoilUnit fan_coil_units = 2;
}

message HeatPump {
  // Address of the device.
  ModbusServerSpec address = 1;
}

message FanCoilUnit {
  // Address of the device.
  ModbusServerSpec address = 1;
}

message ModbusServerSpec {
  // Modbus address of the device. 2 bytes.
  uint32 server_id = 1;

  // Serial device used to connect to the server. Value of this ifeld is something like "/dev/ttyX"
  string serial_device= 2;
}

I plan to remove the offensive terms from the library at some point (replace slaveID with serverID).

sodabrew commented 2 years ago

+1. Just confirming that I understand: in the Modbus model, the device is the server and this software on the RPi is the client?

Is the protobuf text format sufficiently human-readable for a config file? I've never used it as such, but if you want to stick to gRPC I'm not about to roll in with a PR to make everything JSON. https://developers.google.com/protocol-buffers/docs/text-format-spec

gonzojive commented 2 years ago

+1. Just confirming that I understand: in the Modbus model, the device is the server and this software on the RPi is the client?

Yes, that's my understanding of the how it works.

prototext is the Go library for the text format, and it's decently readable. What's nice vs. JSON is there is more validation. https://marketplace.visualstudio.com/items?itemName=thesofakillers.vscode-pbtxt

Protos have a JSON encoding that would allow storing the config in JSON or YAML, but proto text format is simpler.

sodabrew commented 2 years ago

Agreed, protobuf-json format is rough. I'll take a look at proto-text.

sodabrew commented 2 years ago

Something that's not totally clear to me: can you run the fancoil client on the same RS-485 device as the cx34 client? Each sets up its own instance of handler := modbus.NewRTUClientHandler(p.TTYDevice) -- is there an underlying locking mechanism that prevents the two from clobbering each other on the same serial bus?

Looking around at the modbus implementations in Go (as I'm sure you did! but I'm getting myself up to speed...) I found one with an explicit method of switching device id:

https://github.com/simonvetter/modbus

    client.SetUnitId(4)

The goburrow library in use here has an awkward reach into the handler structure to change the id and hasn't followed up on PRs to improve in recent years: https://github.com/goburrow/modbus/pull/36

if handler, ok := q.handler.(*modbus.RTUClientHandler); ok {
    handler.SlaveId = deviceid
} else if handler, ok := q.handler.(*modbus.TCPClientHandler); ok {
    handler.SlaveId = deviceid
}

It looks like there is a fork of goburrow/modbus that has adopted this PR: https://github.com/grid-x/modbus/pull/8

    handler.SetSlave(4)

There is a totally different proposal to move the id into each method call: https://github.com/goburrow/modbus/pull/87 e.g.

-   ReadCoils(address, quantity uint16) (results []byte, err error)
+   ReadCoils(slaveId uint8, address, quantity uint16) (results []byte, err error)

And a very minimalist implementation that follows the same pattern of id in each method call: https://github.com/dpapathanasiou/go-modbus (IMO this implementation too minimalist, leaving most of the modbus parsing to the calling code - but maybe that's helpful for Omron protocol!? -- anyways, the device id in method call idea is what I was looking for)

Only https://github.com/simonvetter/modbus has taken up Modbus org's offensive terms changes, albeit using yet a different naming ("unit id" vs. "server id" -- I'll avoid the tangent where I argue into the wind that I, a random person on the Internet who started using Modbus a few days ago but has been writing network software for a couple of decades, boldly proclaims that the words "client/server" make no sense in this context and something like "controller/device" would be way more sensible! oops)

gonzojive commented 1 year ago

Something that's not totally clear to me: can you run the fancoil client on the same RS-485 device as the cx34 client? Each sets up its own instance of handler := modbus.NewRTUClientHandler(p.TTYDevice) -- is there an underlying locking mechanism that prevents the two from clobbering each other on the same serial bus?

I'm not sure. I don't think there is any locking mechanism and suspect using the same device would be unsafe. Both Go processes would need to be able to read the same bytes from the same device to process traffic on the bus. Writes would also require locking.

I was a little paranoid myself and bought two USB RS485 dongles just in case... one for the fan coils and one for the heat pump.

--

Using a different modbus library would be fine with me if upstream isn't accepting patches.

gonzojive commented 1 year ago

It looks like serial port locking comes up and two processes will cause problems. https://stackoverflow.com/questions/17980725/locking-linux-serial-port/17983721#17983721

One software solution is to update this code to use a single process for reading/writing from the serial device. Another is probably closing the serial device when not in use and protecting opens with a lock. I would just grab another USB to RS485 converter for now. The Waveshare ones seem pretty high quality and are ~$20.

njmckenzie commented 1 year ago

FYI that I got some really weird behavior using a single RS485 even with a single process (I think). I'm using the logic from this project but putting it in OpenHab instead of Google. I'm not 100% sure the openHab Modbus code is actually using a single process but it was easy rationale to stick with two RS-485 devices. When I polled certain addresses (no discernible pattern) on the CX34, it would cause all the fan coils to start blinking gibberish and stop working. The instant I deactivated the CX34 polling, they returned to normal.

sodabrew commented 1 year ago

What other devices were on your RS-485 bus? In theory, one bus could have many different components from different vendors; but I wonder if building automation folks know that in practice there are known issues?

It's been a couple of months since I had time to hack on this, but I think the path to using the same bus will be factoring out a "bus manager" from the other components of the system. The more I sketched my idea, the more I realized I was reinventing Modbus over TCP -- that is, the "bus manager" would sit on the serial port and expose a TCP port and client programs would use Modbus over TCP. That seems to be where the industry is, definitely feel like the hobbyist playing catch-up :)

This codebase mentions using Omron format. I wasn't able to find any descriptions on the web of how that interacts with Modbus over TCP.

gonzojive commented 1 year ago

I think the issue is that there are multiple processes using the same serial device on your raspberry pi. They are likely reading or writing in incompatible ways to the serial device.

Modbus itself can handle lots of devices as long as they are well behaving and all using the same baud rate and other network parameters.

On Mon, Jan 16, 2023, 4:27 PM Aaron Stone @.***> wrote:

What other devices were on your RS-485 bus? In theory, one bus could have many different components from different vendors; but I wonder if building automation folks know that in practice there are known issues?

It's been a couple of months since I had time to hack on this, but I think the path to using the same bus will be factoring out a "bus manager" from the other components of the system. The more I sketched my idea, the more I realized I was reinventing Modbus over TCP https://en.wikipedia.org/wiki/Modbus#Protocol_versions -- that is, the "bus manager" would sit on the serial port and expose a TCP port and client programs would use Modbus over TCP. That seems to be where the industry is, definitely feel like the hobbyist playing catch-up :)

This codebase mentions using Omron format. I wasn't able to find any descriptions on the web of how that interacts with Modbus over TCP.

— Reply to this email directly, view it on GitHub https://github.com/gonzojive/heatpump/issues/24#issuecomment-1384695325, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAAUO5ZUBSZ6PGTM5P7CQBLWSXRNNANCNFSM6AAAAAASFOTYJU . You are receiving this because you commented.Message ID: @.***>

sodabrew commented 1 year ago

It's finally spring season, which means switching between heating and cooling modes every few days. Time to dust off my Pi and get this working! I tinkered with the collector but realized I wanted a more minimal starting point. So I started ripping down to the bare bones. I have a small command line output now. I haven't committed to my fork yet.

The mode and target temperatures are the things I need to be able to change remotely, so I'll work on adding write support to those registers from the command line. Here's the first light I have so far:

~/chilcmd $ ./chilcmd -tty /dev/ttyUSB0 -unit 2
Summary for CX34 unit 2:
  Mode: Cooling + DHW
  COP: 0.00 (stopped)
  Power: 0.00 Watts
  Outdoor Temp: 55.04 °F
  Active Target Temp: 87.80 °F
  Heating Target Temp: 87.80 °F
  Inlet Temp: 55.76 °F
  Outlet Temp: 55.40 °F
  Pump Speed: 0.01 l/s
  Useful Heat Rate: 0.1163kg/s * -0.2°K * 4kJ/(kg * °K) = -93J/s = -0.09kW