Created
February 13, 2022 05:01
-
-
Save kquinsland/4c459ab14202921df6247beb775945c8 to your computer and use it in GitHub Desktop.
Decoding modbus data from generic chineese PH Controller pH-1800
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
This is a simple script to communicate with a "generic" ph / ORP controller as seen on Ali Express | |
The listings for these devices will usually have a title like: | |
PH Controller pH-1800 Industrial On-line Ph Meter Electrode pH Tester Transmitter | |
E.G.: https://www.aliexpress.com/item/32993041598.html | |
The only documentation that I could get from the seller is a microsoft word file and a microsoft windows program that appears to | |
need an older copy of 32 bit windows. I was not able to get the program to run in a VM to sniff the modbus traffic and a quick look | |
with Ghidra didn't reveal anything immediately obvious about how the program interacted with the controller. | |
In testing, I was able to get the controller to respond to commands 0x03 and 0x06. | |
I did not explore the WRITE SINGLE REGISTER (0x06) command as I do not need to set anything remotely. | |
As for 0x03, this command does support a "start register" and a "number of registers to read" paramater. | |
The controller ignores them. You will always get back `0x0c` (12) bytes. | |
Example exchange: | |
Req >> : 01 03 00 00 00 32 c4 1f | |
<< Resp: 01 03 0c 30 36 2e 39 37 01 20 32 37 2e 32 00 7b fd | |
Req >> : To address 1, command 3 start at 0x00 and read 50 coils (0x32). | |
The controller responds with the useful payload: | |
30 36 2e 39 37 01 20 32 37 2e 32 00 | |
This can be decoded directly as ascii characters | |
30 36 2e 39 37 01 20 32 37 2e 32 00 | |
0 6 . 9 7 SH SP 2 7 . 2 NL | |
Where | |
SH: start of header | |
SP: space | |
NL: null terminator | |
Strip out the control/non-printable characters as you get: | |
6.97 27.2 | |
which is a ph of 6.97 at 27.2 c. | |
Simple :). | |
""" | |
# pip install minimalmodbus | |
import minimalmodbus | |
import logging | |
import string | |
logging.basicConfig(level=logging.DEBUG) | |
UART = '/dev/ttyUSB0' | |
# The only things that I do know from the GUI on the controller | |
PH_ADDR = 1 | |
PH_BAUD = 9600 | |
# Despite sending payloads in ASCII, the controller does not respond to modbus ASCII commands, only RTU | |
PROBE_MODE = minimalmodbus.MODE_RTU | |
logging.debug("Setting up modbus probe") | |
probe = minimalmodbus.Instrument(UART, PH_ADDR, mode=PROBE_MODE, debug=True) | |
probe.serial.baudrate = PH_BAUD | |
probe.serial.bytesize = 8 | |
probe.serial.parity = minimalmodbus.serial.PARITY_NONE | |
probe.serial.stopbits = 1 | |
# Controller *usually* responds faster than this but sometimes it takes a tic or two. | |
# This is about the lowest value I could _reliably_ use with my USB <-> RS485 converter | |
probe.serial.timeout = 0.2 | |
logging.info("probe setup") | |
logging.debug(f"getting string...") | |
# Need to get this right otherwise err | |
# minimalmodbus.InvalidResponseError: The register data length is wrong. Registerdata: 12 bytes. Expected: 20. | |
raw_payload = probe.read_string(0,6) | |
# TODO: capture: minimalmodbus.InvalidResponseError for when the controller does not reply completely / in time | |
logging.debug(f"raw_payload: {raw_payload}") | |
# We will get back a string of bytes that should be interperated as ascii characters. | |
# Not all of them are printable. We filter out the non printable and get back a list | |
## | |
# TODO: might be faster to split on the ' ' / 0x20 and then just float() on both? | |
raw_string = list(filter(lambda x: x in string.printable, raw_payload)) | |
logging.debug(f"raw_string: {raw_string}") | |
# Should have something like | |
# ['0', '7', '.', '2', '0', ' ', '4', '2', '.', '5'] | |
# for a PH of 7.2 and a temp of 42.5c | |
## | |
_space_idx = raw_string.index(' ') | |
ph = float("".join(raw_string[0:_space_idx])) | |
temp = float("".join(raw_string[_space_idx+1:])) | |
logging.info(f"ph: {ph} temp: {temp}") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment