Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UART Multidrop/Multipoint Support - Add base component, Dooya shades, Chenyang window openers, GM40 curtain motor #1670

Closed
wants to merge 66 commits into from

Conversation

loongyh
Copy link
Contributor

@loongyh loongyh commented Apr 6, 2021

What does this implement/fix?

Types of changes

  • Bugfix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Configuration change (this will require users to update their yaml configuration files to keep working)

Related issue or feature (if applicable): Relevant but does not fix (this PR depends on RS485-TTL drivers with automatic flow control, i.e. no DE/RE control) esphome/feature-requests#421
fixes esphome/feature-requests#1051

Pull request in esphome-docs with documentation (if applicable): esphome/esphome-docs#1090

Test Environment

  • ESP32
  • ESP8266
  • Windows
  • Mac OS
  • Linux

Example entry for config.yaml:

# Example config.yaml
uart:
  tx_pin: 17
  rx_pin: 16
  baud_rate: 9600
cover:
  - platform: dooya
    name: Dooya Shade
    address: 0xFEFE
    device_class: shade
  - platform: chenyang
    name: Chenyang Window
    address: 0xFF
    device_class: window
  - platform: gm40
    name: GM40 Curtain
    address: 0x00
    device_class: curtain

Explain your changes

This PR adds a base UARTMulti 'router' component that forwards received bytes to registered UARTMulti components to support multidrop/multipoint devices from any manufacturer (as long as they have the same serial settings e.g. baud rate etc.). Inspired heavily from the modbus component.
This will add support for multipoint/multidrop buses such as RS485 over UART using a RS485-TTL converter.
Also adds in support for the RS485 Dooya blinds/shades motor, Chenyang window openers, GM40 curtain motor.

Checklist:

  • The code change is tested and works locally.
  • Tests have been added to verify that the new code works (under tests/ folder).

If user exposed functionality or configuration variables are added/changed:

@loongyh loongyh changed the title Add RS485 base component and Dooya RS485 shades RS485 - Add base component, Dooya shades, Chenyang window openers Apr 7, 2021
Copy link
Member

@OttoWinter OttoWinter left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

}
}

void RS485::parse_rs485_frame_() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wikipedia tells me RS485 just defines an electrical interface, not a communication protocol: https://en.wikipedia.org/wiki/RS-485#Protocols - so rs485 would be the wrong name for this component.

Is there some other specification that defines the communication protocl? We should use the name of that protocol for this component.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm theoretically this is protocol-less and can be used for any multidrop/multipoint communication aside from RS485. It is named RS485 as currently I have a bunch of RS485 devices from different manufacturers sporting different protocols that I'd like to string up in a single bus controlled by a single UART on the ESP8266/32. Perhaps a better name may be "multidrop UART" or something?

esphome/components/chenyang/chenyang.cpp Outdated Show resolved Hide resolved
esphome/components/rs485/rs485.cpp Outdated Show resolved Hide resolved

void RS485::loop() {
const uint32_t now = millis();
if (now - this->last_rs485_byte_ > 50) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So if two devices (or even one) on the bus send messages close to each other, this will interpret that as one big message, right?

That sounds like it's just asking for trouble...

Most UART protocols include a start byte to synchronize the reading end - the dooya/chenyang should listen directly for that (like most other UART integrations in ESPHome). The multidrop uart integration could then just serve as a proxy buffering the received data and forwarding it to all registered listeners.

Copy link
Contributor Author

@loongyh loongyh Apr 8, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devices on the bus are slaves and thus they can't initiate sending, it'll only respond with a single frame upon receipt of a frame addressed to it. These frames are usually commands or status requests. Thus the 50ms is the timeout for the slave to send the response.

Copy link
Contributor Author

@loongyh loongyh Apr 8, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To send a 'open' command for the Dooya:

start_code address_h address_l command type crc_h crc_l
0x55 0x12 0x34 0x03 0x01 0xAD 0x8A

Dooya will respond with this frame:

start_code address_h address_l command type crc_h crc_l
0x55 0x12 0x34 0x03 0x01 0xAD 0x8A

To send a 'open' command for the Chenyang:

start_code address command payload xor
0x01 0x01 0x00 0x00 0x00

Chenyang will respond with this frame:

start_code address command payload xor
0x02 0x01 0x00 0x00 0x03

To send a 'get_status' command for the Chenyang:

start_code address command payload xor
0x01 0x01 0x0F 0x00 0x0F

Chenyang will respond with this frame:

start_code address status position speed anti_pinch direction fixed power on close power on open rain sensor enable rain sensor direction fixed xor
0x03 0x01 0x00 0x23 0x01 0x02 0x01 0x78 0x00 0x00 0x00 0x00 0x78 0x23

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With these wildly different frame formats, even the start code may be different in Chenyang's case. To identify the component to forward a received frame to, in Dooya's case it's the combination of start_code, address_h, address_l, but for Chenyang's case we can't use the start_code as they differ, thus we can only use the address (2nd byte). Hence the first byte of Chenyang's this->header_ is nullptr to signify ignore.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, the header can be different for each device. What I mean is: each component (like chenyang) listens directly on the bus. It reads from the bus, waits for the start code and tries to parse the frame. if it's not a valid frame (or for another device), just ignore it. Or expressed another way: the chenyang component should be implemented just like the other esphome uart components, reading in stuff byte-by-byte.

How to do multidrop then? Have a new central component that reads from the uart bus, then sends the received bytes directly to all listening components like chenyang.

This way there's less latency (as we don't need to wait 50ms to ensure we're at the end of the frame), and the individual components are closer to ESPHome's UART abstractions.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So in Ethernet terms, instead of the central component acting as a switch, which routes packets only to the destined interface, we should have it act as a hub instead, which just blindly fires all packets to all interfaces. I was concerned with implementing a switch instead of a hub as it is more 'efficient', and totally overlooked the fact that the delay of 50ms actually makes it less efficient than just firing all received bytes to all registered components 🤦

Copy link
Contributor Author

@loongyh loongyh Apr 8, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the base component should be multidrop UART, then perhaps we can modify uart.h to directly support multidrop devices. Meaning instead of implementing void register_device(Device *device) in a seperate RS485 class, we put this directly in the UARTDevice class. Such that multiple integrations will be able to extend from the same UARTDevice instance, which if I understand correctly that'll mean they'll all have access to the same incoming and outgoing buffers.

Edit: On second thought, we'll need to refactor UARTDevice read_byte() to instead send the rx_buffer data to the components, and then somehow derive a way to flush the rx buffer not before all integrations have done their read_byte().

Edit2: For sending, since current UART integrations assume they have exclusive uart access, they actually implement write_byte() byte-by-byte too, which means if two integrations start to send at the same time, their bytes will be interleaved resulting in garbage.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah implementing in the UARTDevice would of course be ideal. But that's probably not possible given what you mentioned. However, we could convert UARTComponent to an abstract class (which is implemented by "native" UART and multidrop); not sure if that would be so simple though because the multidrop interface would ideally use a callback to send each byte to the listeners (instead of having to children poll themselves)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have reimplemented the RS485 base component into a UARTMulti component, with received bytes being passed to each component via on_uart_multi_byte(). This offloads parsing of the bytes-to-frames to the components themselves and affords a more 'native' access to the received bytes :).

@loongyh
Copy link
Contributor Author

loongyh commented May 13, 2021

The base UARTMulti component now cycles around each child component to trigger its update callback, with a default of 500ms interval. Tested fully working with the following deployments:

  • 2 sets of (2x Chenyang window openers, 1x GM40 curtain motor)
  • 2 sets of (1x Chenyang window openers, 1x GM40 curtain motor)
  • 1 set of (1x Chenyang window opener, 1x Dooya shade motor)

@loongyh loongyh changed the title UART Multidrop/Multipoint Support - Add base component, Dooya shades, Chenyang window openers UART Multidrop/Multipoint Support - Add base component, Dooya shades, Chenyang window openers, GM40 curtain motor May 13, 2021
@jesserockz jesserockz added this to In Review in ESPHome Dev May 19, 2021
@jesserockz
Copy link
Member

I fail to see why these platforms specifically require the use of UARTMulti, based on the code of UARTMulti.

The way I see it is that UARTMulti should be an optional middleware component between uart and each platform so a user can just use a single device without the extra. And this would allow simple adaptation of other uart command/response devices to use the uart_multi bus...

@loongyh
Copy link
Contributor Author

loongyh commented Jul 14, 2021

That's true, I'll see to decoupling the platforms from uart_multi. Looking through the rest of the integrations with middleware, I don't see any optional ones, are there examples?
i.e. is there a way in config yaml to selectively choose to have the component extend the UARTMultiDevice or UARTDevice class?

@jesserockz
Copy link
Member

No it's not a thing at the moment...
Not sure exactly how it could be done either.

@github-actions
Copy link
Contributor

github-actions bot commented Nov 3, 2021

There hasn't been any activity on this pull request recently. This pull request has been automatically marked as stale because of that and will be closed if no further activity occurs within 7 days. Thank you for your contributions.

@github-actions github-actions bot added the stale label Nov 3, 2021
@github-actions github-actions bot closed this Nov 10, 2021
ESPHome Dev automation moved this from In Review to Cancelled Nov 10, 2021
@github-actions github-actions bot locked and limited conversation to collaborators Nov 11, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
ESPHome Dev
Cancelled
Development

Successfully merging this pull request may close these issues.

Abstraction for byte-based raw UART/RS485 communication