Skip to content

Instantly share code, notes, and snippets.

@kquinsland
Created February 13, 2022 05:01
Show Gist options
  • Save kquinsland/4c459ab14202921df6247beb775945c8 to your computer and use it in GitHub Desktop.
Save kquinsland/4c459ab14202921df6247beb775945c8 to your computer and use it in GitHub Desktop.
Decoding modbus data from generic chineese PH Controller pH-1800
"""
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