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
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks!
esphome/components/rs485/rs485.cpp
Outdated
} | ||
} | ||
|
||
void RS485::parse_rs485_frame_() { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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/rs485/rs485.cpp
Outdated
|
||
void RS485::loop() { | ||
const uint32_t now = millis(); | ||
if (now - this->last_rs485_byte_ > 50) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 🤦
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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 :).
The base
|
I fail to see why these platforms specifically require the use of The way I see it is that |
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? |
No it's not a thing at the moment... |
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. |
What does this implement/fix?
Types of changes
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
Example entry for
config.yaml
: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:
tests/
folder).If user exposed functionality or configuration variables are added/changed: