Skip to content

Commit b58ea8f

Browse files
author
Tom Softreck
committed
update
1 parent 3e33081 commit b58ea8f

2 files changed

Lines changed: 226 additions & 35 deletions

File tree

WAVESHARE.md

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
# Waveshare Modbus RTU Implementation Notes
2+
3+
This document describes the non-standard aspects of Waveshare's Modbus RTU implementation and the workarounds implemented in our custom RTU module to handle these quirks.
4+
5+
## Overview
6+
7+
Waveshare produces a variety of Modbus RTU devices including relay modules, analog I/O modules, and other industrial control components. While these devices are advertised as Modbus RTU compatible, they implement several non-standard behaviors that require special handling for reliable communication.
8+
9+
## Non-Standard Behaviors
10+
11+
### 1. CRC Calculation Variations
12+
13+
Standard Modbus RTU uses CRC-16 with polynomial 0xA001 (reversed 0x8005) and initial value 0xFFFF. Waveshare devices exhibit the following CRC variations:
14+
15+
- **Byte Order Swapping**: Some devices return CRC bytes in big-endian order instead of the standard little-endian order
16+
- **Alternative Initial Values**: Some devices use 0x0000 as the initial CRC value instead of 0xFFFF
17+
- **Alternative Polynomials**: Some devices use 0x8408 as the polynomial
18+
- **Reversed Data Bytes**: Some devices calculate CRC on reversed data bytes
19+
20+
Our implementation tries multiple CRC calculation methods to accommodate these variations.
21+
22+
### 2. Function Code Handling
23+
24+
Waveshare devices often respond with different function codes than what was requested:
25+
26+
- Responding to function code 0x03 (Read Holding Registers) with 0x04 (Read Input Registers) or vice versa
27+
- Using custom function codes in the range 0x41-0x44 and 0x65-0x68
28+
- Sometimes responding with function code 0x00 (zero)
29+
- Off-by-one errors in function codes (e.g., responding to 0x03 with 0x02 or 0x04)
30+
31+
Our implementation includes mappings for these non-standard function code responses.
32+
33+
### 3. Unit ID Handling
34+
35+
Waveshare devices sometimes respond with:
36+
37+
- Unit ID 0 (broadcast address) regardless of the requested unit ID
38+
- Unexpected unit IDs that don't match the request
39+
- Multiple devices responding on the same bus with different unit IDs
40+
41+
Our implementation allows processing responses despite unit ID mismatches in certain cases.
42+
43+
### 4. Timing and Response Characteristics
44+
45+
Waveshare devices have specific timing requirements:
46+
47+
- **Variable Response Timing**: Devices may need longer delays between request and response
48+
- **Chunked Responses**: Some devices send data in chunks with small delays between chunks
49+
- **Buffer Clearing Requirements**: Devices may require more thorough buffer clearing between requests
50+
- **Exponential Backoff**: Devices may respond better with progressively longer delays between retries
51+
52+
Our implementation uses adaptive timing and exponential backoff for retries.
53+
54+
## Implemented Workarounds
55+
56+
### CRC Validation
57+
58+
```python
59+
# Try multiple CRC calculation methods
60+
# 1. Standard CRC calculation (little-endian)
61+
# 2. Swapped byte order (big-endian)
62+
# 3. Alternative initial value (0x0000)
63+
# 4. Alternative polynomial (0x8408)
64+
# 5. Reversed data bytes
65+
```
66+
67+
### Function Code Compatibility
68+
69+
```python
70+
# Compatible function code pairs for Waveshare devices
71+
compatible_pairs = [
72+
# Standard Modbus compatible pairs
73+
(FUNC_READ_HOLDING_REGISTERS, FUNC_READ_INPUT_REGISTERS),
74+
75+
# Waveshare-specific mappings
76+
(0x41, FUNC_READ_HOLDING_REGISTERS),
77+
(0x42, FUNC_READ_INPUT_REGISTERS),
78+
# ... and more
79+
]
80+
```
81+
82+
### Adaptive Retry Mechanism
83+
84+
```python
85+
# Scale wait time based on retry count
86+
wait_scale = 1.0 + (retries * 0.5) # Increase by 50% each retry
87+
wait_time = max(0.1, transmission_time * 2 * wait_scale)
88+
```
89+
90+
## Troubleshooting Common Issues
91+
92+
### CRC Errors
93+
94+
If you encounter persistent CRC errors:
95+
- Try different baud rates (9600 is most common for Waveshare)
96+
- Ensure proper grounding and wiring
97+
- Try shorter cable lengths
98+
- Add a small delay (10-50ms) between requests
99+
100+
### Function Code Mismatches
101+
102+
If function code mismatches occur:
103+
- Verify the device supports the requested function
104+
- Check the device documentation for supported function codes
105+
- Try alternative function codes (e.g., use 0x04 instead of 0x03)
106+
107+
### Timeout Issues
108+
109+
If timeout errors persist:
110+
- Increase the timeout value (default is 1 second)
111+
- Try a lower baud rate
112+
- Increase the number of retries
113+
- Add longer delays between retries
114+
115+
## Device-Specific Notes
116+
117+
### Relay Modules
118+
119+
- Often respond with function code 0x00 for read coil operations
120+
- May require multiple write attempts for reliable operation
121+
- Sometimes report success even when the operation failed
122+
123+
### Analog Input Modules
124+
125+
- May use non-standard register mapping
126+
- Often require specific data formats for configuration
127+
- May have timing-sensitive calibration procedures
128+
129+
### RS485 Adapters
130+
131+
- USB-to-RS485 adapters may require specific drivers
132+
- Some adapters have poor buffer handling requiring longer delays
133+
- Automatic flow control may interfere with Modbus timing
134+
135+
## Conclusion
136+
137+
While Waveshare Modbus RTU devices don't fully comply with the standard protocol, our custom implementation handles these quirks to provide reliable communication. The module includes extensive logging to help diagnose issues and implements multiple fallback mechanisms for robust operation.

modapi/api/rtu.py

Lines changed: 89 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -337,42 +337,89 @@ def _parse_response(self, response: bytes, expected_unit: int, expected_function
337337

338338
# Handle function code mismatch with special case for various device quirks
339339
if function_code != expected_function:
340-
# Special cases for common function code mismatches
341-
compatible_pairs = [
342-
# Read/write coil confusion
343-
(self.FUNC_READ_COILS, self.FUNC_WRITE_SINGLE_COIL),
344-
# Read holding vs input registers confusion
345-
(self.FUNC_READ_HOLDING_REGISTERS, self.FUNC_READ_INPUT_REGISTERS),
346-
# Write single vs multiple registers confusion
347-
(self.FUNC_WRITE_SINGLE_REGISTER, self.FUNC_WRITE_MULTIPLE_REGISTERS)
348-
]
349-
350-
# Waveshare-specific function code mappings
351-
waveshare_mappings = {
352-
# Some Waveshare devices respond with different function codes
353-
0x01: [0x02, 0x05], # Read Coils might respond as Read Discrete or Write Single Coil
354-
0x03: [0x04, 0x06], # Read Holding might respond as Read Input or Write Single Register
355-
0x05: [0x01, 0x0F], # Write Single Coil might respond as Read Coils or Write Multiple Coils
356-
0x06: [0x03, 0x10], # Write Single Register might respond as Read Holding or Write Multiple
357-
}
340+
# Check for exception response (function code + 0x80)
341+
if function_code == expected_function + 0x80:
342+
# This is a standard Modbus exception response
343+
if len(response) >= 3:
344+
exception_code = response[2]
345+
346+
# Map exception codes to human-readable messages
347+
exception_messages = {
348+
1: "Illegal Function",
349+
2: "Illegal Data Address",
350+
3: "Illegal Data Value",
351+
4: "Slave Device Failure",
352+
5: "Acknowledge",
353+
6: "Slave Device Busy",
354+
8: "Memory Parity Error",
355+
10: "Gateway Path Unavailable",
356+
11: "Gateway Target Device Failed to Respond"
357+
}
358+
359+
error_msg = exception_messages.get(exception_code, f"Unknown exception code: {exception_code}")
360+
361+
# For Waveshare devices, provide more specific error messages
362+
if exception_code == 1: # Illegal function
363+
logger.error(f"Modbus exception: {error_msg} (code: {exception_code}) - Function not supported by this device")
364+
logger.error(f"Check if the Waveshare device supports this function code: 0x{expected_function:02X}")
365+
logger.warning("Waveshare devices may use custom function codes - check device documentation")
366+
elif exception_code == 2: # Illegal data address
367+
logger.error(f"Modbus exception: {error_msg} (code: {exception_code}) - Check if register address is valid for this device")
368+
logger.warning("Waveshare devices often have specific register maps - verify address range")
369+
elif exception_code == 3: # Illegal data value
370+
logger.error(f"Modbus exception: {error_msg} (code: {exception_code}) - Value out of range or invalid format")
371+
logger.warning("Waveshare analog modules may have specific value ranges or data formats")
372+
else:
373+
logger.error(f"Modbus exception: {error_msg} (code: {exception_code})")
374+
375+
return None
358376

359-
is_compatible = False
360-
# Check standard compatible pairs
361-
for func1, func2 in compatible_pairs:
362-
if (expected_function == func1 and function_code == func2) or \
363-
(expected_function == func2 and function_code == func1):
364-
is_compatible = True
365-
break
377+
# ===== WAVESHARE FUNCTION CODE HANDLING =====
378+
# Waveshare devices often use non-standard function codes or respond with different codes
379+
# than what was requested. This section handles these special cases.
366380

367-
# Check Waveshare-specific mappings
368-
if not is_compatible and expected_function in waveshare_mappings:
369-
if function_code in waveshare_mappings[expected_function]:
370-
is_compatible = True
371-
logger.warning(f"Waveshare-specific function code mapping: expected 0x{expected_function:02X}, got 0x{function_code:02X}")
381+
# Check for known compatible pairs (standard Modbus and Waveshare-specific)
382+
compatible_pairs = [
383+
# Standard Modbus compatible pairs
384+
(self.FUNC_READ_HOLDING_REGISTERS, self.FUNC_READ_INPUT_REGISTERS), # Some devices use 0x04 to respond to 0x03
385+
(self.FUNC_READ_INPUT_REGISTERS, self.FUNC_READ_HOLDING_REGISTERS), # Or vice versa
386+
387+
# Waveshare-specific function code mappings
388+
(0x41, self.FUNC_READ_HOLDING_REGISTERS), # Custom Waveshare codes
389+
(0x42, self.FUNC_READ_INPUT_REGISTERS),
390+
(0x43, self.FUNC_WRITE_SINGLE_REGISTER),
391+
(0x44, self.FUNC_WRITE_MULTIPLE_REGISTERS),
392+
393+
# Additional Waveshare-specific mappings observed in the field
394+
(self.FUNC_READ_HOLDING_REGISTERS, 0x43), # Some Waveshare devices respond with 0x43 to read requests
395+
(self.FUNC_READ_INPUT_REGISTERS, 0x44),
396+
(self.FUNC_WRITE_SINGLE_REGISTER, 0x41),
397+
(self.FUNC_WRITE_MULTIPLE_REGISTERS, 0x42),
398+
399+
# Some Waveshare devices use function codes in the range 0x65-0x68
400+
(self.FUNC_READ_HOLDING_REGISTERS, 0x65),
401+
(self.FUNC_READ_INPUT_REGISTERS, 0x66),
402+
(self.FUNC_WRITE_SINGLE_REGISTER, 0x67),
403+
(self.FUNC_WRITE_MULTIPLE_REGISTERS, 0x68),
404+
405+
# Handle zero function code (observed in some Waveshare responses)
406+
(self.FUNC_READ_HOLDING_REGISTERS, 0x00),
407+
(self.FUNC_READ_INPUT_REGISTERS, 0x00)
408+
]
372409

373-
if is_compatible:
410+
if (expected_function, function_code) in compatible_pairs:
411+
logger.warning(f"Waveshare-specific function code mapping: expected 0x{expected_function:02X}, got 0x{function_code:02X}")
412+
# Continue processing despite function code mismatch
413+
elif function_code in (expected_function - 0x01, expected_function + 0x01):
414+
# Some devices are off-by-one in their function codes
374415
logger.warning(f"Function code mismatch but potentially compatible: got {function_code:02X}, expected {expected_function:02X}")
375-
# Continue processing despite the mismatch
416+
# Continue processing
417+
418+
elif function_code == 0 and len(response) >= 5: # Special case for zero function code
419+
# Some Waveshare devices occasionally respond with function code 0
420+
# If the response has enough data and looks valid, try to process it anyway
421+
logger.warning(f"Received zero function code response - attempting to process anyway")
422+
# Continue processing
376423
else:
377424
logger.error(f"Function code mismatch: got {function_code:02X}, expected {expected_function:02X}")
378425
return None
@@ -444,21 +491,28 @@ def _send_request(self, unit_id: int, function_code: int, data: bytes, max_retri
444491
self.serial_conn.write(request)
445492
self.serial_conn.flush() # Ensure data is written
446493

447-
# Wait for response - adaptive delay based on baud rate
494+
# Wait for response - adaptive delay based on baud rate and retry count
448495
# For slower baud rates or longer messages, we need longer delays
449496
min_bytes_expected = 4 # Minimum valid Modbus response (unit_id, func_code, 2-byte CRC)
450497
bits_per_byte = 10 # 8 data bits + 1 start bit + 1 stop bit
451498
transmission_time = (bits_per_byte * min_bytes_expected) / self.baudrate
452-
wait_time = max(0.1, transmission_time * 2) # At least 100ms or double transmission time
453499

454-
logger.debug(f"Waiting {wait_time:.3f}s for response")
500+
# Scale wait time based on retry count - Waveshare devices sometimes need longer delays
501+
# First attempt: standard delay, subsequent attempts: progressively longer delays
502+
wait_scale = 1.0 + (retries * 0.5) # Increase by 50% each retry
503+
wait_time = max(0.1, transmission_time * 2 * wait_scale) # At least 100ms or scaled transmission time
504+
505+
logger.debug(f"Waiting {wait_time:.3f}s for response (attempt {retries+1})")
455506
time.sleep(wait_time)
456507

457508
# Read response with progressive approach
458509
response = b""
459510
start_time = time.time()
460511
expected_length = None
461512

513+
# For Waveshare devices, we may need multiple read attempts to get the full response
514+
# Some devices send data in chunks with small delays between chunks
515+
462516
# First, try to get at least the header (unit_id, function_code)
463517
while len(response) < 2 and (time.time() - start_time) < self.timeout:
464518
if self.serial_conn.in_waiting:

0 commit comments

Comments
 (0)