Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
Has anybody experience with MODBUS to read temp sensors?
#21
(01-14-2026, 02:00 PM)Petr Wrote: The program reads temperatures. That's a clear reason to go buy a beer  Big Grin   Thank you for the information. If you need anything else, write it here, please.
Thank You Petr!!!
While running Your (USB-connected) SensorProgram, I did also a test with the USB-using-relaisbord,
this to see if both software did not came in conflict. And: Both stay working perfect!
I'm trying to store the temperatures in a little global array:
Dim shared MaxSensors : MaxSensors% = 4  (in this case 4 sensors...).  or possible also CONST MaxSensors = 4
Dim shared Temp!(MaxSensors%) ... and store the T of each sensor in his variable.
If I'm so far I publish it here on the forum.
---------------------------------------------------------------------------
Have you solved the switching of outputs according to temperatures with the second board? What is the current load capacity of those relays, what is the power consumption of the heaters, will you power it directly from the second board, or will you use contactors or other auxiliary relays for higher current? Have you solved this yet?
Yes, I did this already to switch ON OFF an Induction-heater-plate of 2000 Watt.
I use this big one to start heating or boiling water in a big vessel.
Thereafter I switch to 480 and 750 Watt heaterpads to keep the temperature constant (p.e. for the maishing proces in beer fabrication).
(Rem: Yes it are the same heaterpads one use in printer devices...).
To switch the power of bigger heater elements the relais contacts are to weak!
So I use SSR-40 DA modules (Aliexpress) to switch heater-elements ON and OFF.
Input: 3 - 32 VDC
Output: 24 - 380 VAC
I used them Input directly connected to GPIO pins of a RaspberryPI
When the relais is on You can see burning a red LED.


[Image: SSR-aliexpress.png]



Warning for everybody who want also do this:
Aliexpress sellers announce always mention the peak current or power values of these SSR's
So always buy and use an SSR with at least double the current or power you want to drive, and use also the aluminium heatsink. (I even use little PC ventilators to keep T below 40°C)For my safety I even use a separate differential and all metal parts of my installation are grounded.

That's it for the non-QB64 electrical info, but for safety reasons I tried to answer more comprehensively, for which I apologize. There's no point in creating software that will cause the installation to burn down afterward, right?
---------------------------------------------------------------------------

Greatings, and thank You very much for Your help.
Rudy M
Reply
#22
Hello everyone, 
Here's a small update regarding the power supply for the PT100 sensor module:
- Power the module before starting up your PC. 
- Only then run the PC and the program to read the sensors. 
Doing it the other way around doesn't work... you'll end up with a terminal without text! 

Therefore, I'm also going to make a small UPS (uninterruptible power supply) for this module.
(A short "Power bracke" ... and reading of the sensors stops!) 
I'll easily do this using the original battery charger from my previous drill. Charger: Output 21 Volt 2 Amps, 
Battery: Li-ion 20 Volt 2 Ah 

If 20 Volts proves insufficient for proper operation of the module, I would recommend what's called a "DC-DC Step-Up Converter 2-24V to 3-30V 4A 80W Adj Regulated power supply." This is an inverter that increases the 20 Volts from the battery to a stable 30 Volt voltage (adjustable and short-circuit-proof) and costs only a few euros on AliExpress, eBay.... 
Once it's finished, I'll publish the schematic here,
(anyone can connect those inverter themselves... only -Vin +Vin and -Vout +Vout).

Greatings,
Rudy M
Reply
#23
Hi everybody,

To be able to use Petr's program in my own programs, I performed several tests with the QB64 "on timer" statement. 
However, it made the program quite unreadable. 
Therefore, I created a SHARED ARRAY Temp!() to store the temperatures of multiple sensors, and two subs.
Because the T-info are "shared," it can be used in all sub-functions of the QB64. 

The first sub: "SUB PT100Read start%" The second sub: "SUB PT100Stop" 

To start the module only once without having to create a separate SUB, I added a simple IF THEN ELSE END IF sequence. This if then is controlled by the variable "start%". 

With what I created, you can still clearly see the structure of the program Petr created for this purpose.
Once again, my thanks to Petr for the cleverly programmed program for reading PT100 sensors via MODBUS. 

Na de start van de module voegde ik ook een kleine controle (T=0 °C... or T>320°C indicates problems with one or more sensors (The test from this You can found it at the end of this reply.

And... I'm certainly open to your comments and improvements. Best, Rudy

Code: (Select All)
 OPTION _EXPLICIT

CONST TRUE = -1, FALSE = 0

DIM SHARED Byte1 AS STRING * 1
DIM SHARED h%: h% = FREEFILE
DIM SHARED MaxSensors%: MaxSensors% = 4 'Number of sensors (may be < then real present nr of sensors)
DIM SHARED Temp!(3) 'To read T in all subs en functions

'User-configurable settings (edit these)
DIM SHARED cmd$
DIM SHARED dev$: dev$ = "/dev/ttyACM0"
DIM SHARED dipRaw&
DIM SHARED pollHz!: pollHz! = 2
DIM SHARED rawS%
DIM SHARED req$, resp$
DIM SHARED showFrames%: showFrames% = -1
DIM SHARED slave%: slave% = 1
DIM SHARED timeout!: timeout! = 1.0
DIM start%: start% = TRUE '              (First loop start% =-1 'TRUE'  thereafter= 0 'FALSE')
DIM test%: test% = TRUE

'for programflow
DIM i%
DIM key$

PT100Read start% 'start% = true so START UP SENSORMODUEL

'Test all PT100 sensortest:
key$ = INKEY$
IF key$ = CHR$(27) THEN END

PT100Read start% 'start = false so read T in shared variable Temp!()

'Safety first: Before cooking test all sensors:
'==============================================
FOR i% = 0 TO MaxSensors% - 1
  PRINT USING "PT100 sensor CH# = ####.# C"; i%; Temp!(i%),
  IF Temp!(i%) = 0 THEN '        Connected but T = 0 degrees Celcius is NOT normal.
    PRINT " indicates this sensor is possible defect."
    PLAY "a8e8g8a8g8e8c4f8f8f4d8d8d4a8e8g8a8g8e8c4f8f8f4d8d8d4" 'Make much noice
    test% = FALSE
  ELSEIF Temp!(i%) > 300 THEN '  Not connected temperature = +-320 degrees Celcius...
    PRINT " is not connected."
    PLAY "a8e8g8a8g8e8c4f8f8f4d8d8d4a8e8g8a8g8e8c4f8f8f4d8d8d4" 'Make much noice
    test% = FALSE
  ELSE
    PRINT
  END IF
NEXT i%

IF test% = FALSE THEN
  PRINT
  PRINT "*********************************************"
  PRINT "*  Replace a zero indicting sensor,        *"
  PRINT "*  or reconnect some sensors if necessary. *"
  PRINT "*-------------------------------------------*"
  PRINT "*  If all sensors indicte ZERO degrees:    *"
  PRINT "*  Possible the PT100 module did not start.*"
  PRINT "*  Switch PC and senormodule OFF          *"
  PRINT "*  First Power UP sensormodule.            *"
  PRINT "*  Thereafter Power UP PC                  *"
  PRINT "*  Only RESTART PC this does not work!    *"
  PRINT "*********************************************"
  SLEEP

  PT100Stop
  END

END IF

'Place your cooking or temperature controlled program here.
'And call sub 'PT100Read start%' to store actuel T in shared variable Temp!()

PT100Stop 'Close sensor-module

END





'==============================================================================
SUB PT100Read (start%)
  'Start and read T storage in shared variable Temp!()

  DIM i%

  IF start% = -1 THEN 'only need once at program startup, control with variable "start%".

    ' Open the device as a binary file
    ' LEN=1 ensures PUT/GET operate on single bytes correctly.
    OPEN dev$ FOR BINARY AS #h% LEN = 1

    SerialFlush h%

    req$ = ModbusBuildRead$(255, &H00FD, 1)
    IF showFrames% THEN PRINT "TX addr (FF 00FD): "; HexDump$(req$)
    SerialWrite h%, req$

    resp$ = SerialReadFrame03$(h%, 0, timeout!)
    IF showFrames% THEN PRINT "RX addr:          "; HexDump$(resp$)

    IF LEN(resp$) > 0 AND ModbusCheckCrc%(resp$) THEN
      slave% = ASC(resp$, 1) AND &HFF
      dipRaw& = GetU16BE&(resp$, 4)
      PRINT "Detected station address from reply ="; slave%
      PRINT "00FD raw register value (hex) = &H"; HEX$(dipRaw&)
      PRINT
    ELSE
      PRINT "Could not read station address (00FD)."
      PRINT "This usually means: no real MODBUS reply is arriving."
      PRINT "Focus on: VIN power, A/B wiring swap, GND reference, correct device path, permissions."
      PRINT "Continuing with slave address ="; slave%; "(default)."
      PRINT
    END IF

    start% = 0

  ELSE 'Read (again) actuel T of all sensors in the shared variable Temp()

    SerialFlush h%

    req$ = ModbusBuildRead$(slave%, &H0000, 8)
    'IF showFrames% THEN PRINT "TX temps:          "; HexDump$(req$)
    SerialWrite h%, req$

    resp$ = SerialReadFrame03$(h%, slave%, timeout!)

    ' Decode and print temperatures
    ' Each channel is a signed 16-bit big-endian integer in 0.1°C.

    FOR i% = 0 TO MaxSensors% - 1 'pe: Option base 0 count from 0 to 3 for 4 sensors
      rawS% = GetS16BE%(resp$, 4 + i% * 2)
      Temp!(i%) = rawS% / 10! 'Temp!() is shared to read T everywhere
      'PRINT USING "CH# = ####.# C"; i%; Temp!(i%)
    NEXT i%

    _LIMIT pollHz!

  END IF

END SUB 'PT100Read (start%) -------------------------------

'==============================================================================
SUB PT100Stop
  CLOSE #h%
  PRINT "Reading PT100-sensors finished."
END SUB

'==============================================================================                                                                              '==============================================================================
FUNCTION Crc16Modbus& (s$) 'moet blijven
  ' MODBUS RTU CRC16:
  ' - init 0xFFFF
  ' - polynomial 0xA001 (reflected)
  DIM crc&, i%, j%
  crc& = &HFFFF&
  FOR i% = 1 TO LEN(s$)
    crc& = crc& XOR ASC(s$, i%)
    FOR j% = 1 TO 8
      IF (crc& AND 1) THEN
        crc& = (crc& \ 2) XOR &HA001&
      ELSE
        crc& = crc& \ 2
      END IF
    NEXT j%
  NEXT i%
  Crc16Modbus& = (crc& AND &HFFFF&)
END FUNCTION

FUNCTION U16BE$ (v&)
  ' Convert 0..65535 into 2 bytes big-endian.
  U16BE$ = CHR$((v& \ 256) AND &HFF) + CHR$(v& AND &HFF)
END FUNCTION 'Crc16Modbus& (s$) ------------------------

'==============================================================================
FUNCTION HexDump$ (s$)
  ' Render bytes as hex pairs "01 03 00 ..."
  DIM i%, out$
  out$ = ""
  FOR i% = 1 TO LEN(s$)
    out$ = out$ + RIGHT$("0" + HEX$(ASC(s$, i%)), 2) + " "
  NEXT i%
  HexDump$ = out$
END FUNCTION '-----------

'==============================================================================
FUNCTION ModbusBuildRead$ (slave%, reg&, count%)
  ' Build MODBUS RTU request frame:
  ' [slave][03][regHi][regLo][cntHi][cntLo][crcLo][crcHi]
  DIM req$, crc&
  req$ = CHR$(slave% AND &HFF) + CHR$(&H03) + U16BE$(reg&) + U16BE$(count%)
  crc& = Crc16Modbus&(req$)
  req$ = req$ + CHR$(crc& AND &HFF) + CHR$((crc& \ 256) AND &HFF)
  ModbusBuildRead$ = req$
END FUNCTION '-------------

'==============================================================================
FUNCTION ModbusCheckCrc% (frame$) 'moet blijven
  ' Verify CRC for a complete MODBUS RTU frame.
  ' CRC is stored as [crcLo][crcHi] in the last two bytes.
  DIM n%, crcRx&, crcCalc&
  n% = LEN(frame$)
  IF n% < 5 THEN ModbusCheckCrc% = 0: EXIT FUNCTION
  crcRx& = ASC(frame$, n% - 1) + 256& * ASC(frame$, n%)
  crcCalc& = Crc16Modbus&(LEFT$(frame$, n% - 2))
  ModbusCheckCrc% = (crcRx& = crcCalc&)
END FUNCTION '--------------

'==============================================================================
FUNCTION GetU16BE& (s$, position%)
  ' Read unsigned 16-bit big-endian from s$ at 1-based position.
  DIM hi&, lo&
  hi& = ASC(s$, position%) AND &HFF
  lo& = ASC(s$, position% + 1) AND &HFF
  GetU16BE& = hi& * 256& + lo&
END FUNCTION '--------------

'==============================================================================
FUNCTION GetS16BE% (s$, position%)
  'Read signed 16-bit big-endian (two's complement).
  DIM v&
  v& = GetU16BE&(s$, position%)
  IF v& >= 32768& THEN v& = v& - 65536&
  GetS16BE% = v&
END FUNCTION '--------------

'==============================================================================
SUB SleepMs (ms!)
  ' Simple portable sleep using QB64 _DELAY (seconds).
  _DELAY ms! / 1000!
END SUB '-------------------

'==============================================================================
SUB SerialWrite (h%, s$)
  ' Write raw bytes one-by-one using PUT (LEN=1 record).
  DIM i%
  FOR i% = 1 TO LEN(s$)
    Byte1 = MID$(s$, i%, 1)
    PUT #h%, , Byte1
  NEXT i%
END SUB 'SerialWrite (h%, s$) -------------------------

'==============================================================================
SUB SerialFlush (h%)
  ' Linux-friendly RX flush:
  ' Read and discard bytes for a short time window, without LOC().
  DIM t0!, b$
  t0! = TIMER

  DO
    b$ = INPUT$(1, # h%)
    IF LEN(b$) = 0 THEN
      IF (TIMER - t0!) > 0.2 THEN EXIT DO
      SleepMs 5
    ELSE
      ' Keep flushing while bytes arrive; extend the window slightly.
      t0! = TIMER
    END IF
  LOOP
END SUB 'SerialFlus (h%) --------------------------

'==============================================================================
FUNCTION SerialReadFrame03$ (h%, expectedSlave%, timeoutSec!)
  ' Read and extract a valid MODBUS function 03 response frame.
  DIM t0!, buf$, b$
  DIM i%, n%, addr%, fn%, bc%, frameLen%
  DIM candidate$

  t0! = TIMER
  buf$ = ""
  SerialReadFrame03$ = ""

  DO
    b$ = INPUT$(1, # h%)
    IF LEN(b$) > 0 THEN
      buf$ = buf$ + b$
      IF LEN(buf$) > 512 THEN buf$ = RIGHT$(buf$, 512)
    ELSE
      SleepMs 2
    END IF

    n% = LEN(buf$)
    IF n% >= 5 THEN
      FOR i% = 1 TO n% - 4
        addr% = ASC(buf$, i%) AND &HFF

        IF expectedSlave% <> 0 THEN
          IF addr% <> (expectedSlave% AND &HFF) THEN GOTO NextI
        END IF

        fn% = ASC(buf$, i% + 1) AND &HFF
        IF fn% <> &H03 THEN GOTO NextI

        bc% = ASC(buf$, i% + 2) AND &HFF
        frameLen% = 5 + bc% ' addr+fn+bc + data(bc) + crc(2)

        IF (i% + frameLen% - 1) <= n% THEN
          candidate$ = MID$(buf$, i%, frameLen%)
          IF ModbusCheckCrc%(candidate$) THEN
            SerialReadFrame03$ = candidate$
            EXIT FUNCTION
          END IF
        END IF

        NextI:
      NEXT i%
    END IF

    IF (TIMER - t0!) >= timeoutSec! THEN EXIT DO
  LOOP
END FUNCTION 'SerialReadFrame03$ -------------------


[Image: Test-OK.png]

(The difference between T can be eliminated by placing a resistance in one of the leads of the PT100 sensors)

[Image: Testfalse.png]
Reply
#24
Hi Petr,

I'm busy migrating from the Raspberry Pi to mini PCs (Aliexpress...),
 where I'm replacing the Windows operating system with Linux Mint 22.x (Wilma) with an xfce4 desktop. 
This is, of course, only possible thanks to your beautifully designed software for reading PT100 sensors. 

The relays for switching heating elements, pumps, and valves ON or OFF are now controlled via "usbrelay" software (I already posted the latter on this forum). 

But on the mini PC I'm programming on, the PT100 software crashed again.
I performed the following test: In QB64, 
I placed a few statements: PRINT "AAAA test" SLEEP and I noticed that the program hung in the "SerialFlush" subroutine
So I out-commanded your first call in the programlistings to the "SerialFlush h%" routine
I pressed F5... 
Your listing immediately started displaying the temperatures

Question: What I've done now is act like a "headless chicken"... 
Can I leave this first call to SerialFlush disabled? 

(Incidentally: If my (brewing) setup works as expected, 
I'll definitely post a schematic of the SSR power control via USB BRELAY software and my home-made UPS system here. 
OK... it's a bit off-topic, but our PCs and software can't work without a good voltage and power supply...).

Regards, Rudy
Reply
#25
Hi @Rudy M,

SerialFlush reads via INPUT$(1, #h%) in a situation where there is often nothing flowing on the line yet.
If for some reason stty ... min 0 time 1 is not applied on that particular mini PC (or the port is reset back after OPEN), then INPUT$ can be blocking and waits for the first byte "forever".
As soon as you skip the first SerialFlush, the program immediately sends a request (TX), the sensor responds (RX), so INPUT$ already has something to read and the program runs.

So the answer to your question:
Yes, you can leave the first SerialFlush h% call disabled, if it is already reading stably for you now.

Try this version:

Code: (Select All)

Option _Explicit


' PTA8D08 (8x PT100) - MODBUS RTU over RS-485 - Linux Mint
' QB64PE Linux-friendly implementation
'
' Key design goals:
' - Reliable on Linux when opening /dev/ttyUSBx or /dev/ttyACMx
' - Avoid LOC()/GET() polling (often unreliable for /dev/tty* as files)
' - Use stty to force "raw" serial mode + non-blocking-ish reads
' - Read bytes via INPUT$(1,#) into a buffer
' - Extract MODBUS function 03 frames by CRC check
'
' IMPORTANT FIX (for "hangs in SerialFlush" on some systems):
' - Apply stty BEFORE open AND AGAIN AFTER open.
'  Some drivers reset or override tty settings at open-time.
'
' Hardware notes:
' - Board must be powered via VIN+GND (8..30V DC). RS-485 does not power it.
' - A/B labeling is not consistent; if no reply, swap A and B.
' - Connecting adapter GND to module GND can improve stability (reference).
'

Dim Shared Byte1 As String * 1

'===========================================================
' MAIN PROGRAM (must be first)
'===========================================================

'User-configurable settings
Dim dev$: dev$ = "/dev/ttyUSB0"
' TIP: Prefer /dev/serial/by-id/... if available (stable naming across reboots).
' NOTE: On some adapters it can be /dev/ttyACM0 instead.

Dim pollHz!: pollHz! = 2
Dim showFrames%: showFrames% = -1
Dim timeout!: timeout! = 1.0

'--- Variables (no DIM inside loops)
Dim h%: h% = FreeFile
Dim sttyCmd$
Dim req$, resp$
Dim slave%: slave% = 1
Dim i%
Dim rawS%
Dim t!
Dim dipRaw&
Dim key$


' Print what to send back for debugging
Print "============================================================"
Print "PTA8D08 MODBUS RTU tester (Linux, QB64PE)"
Print
Print "If you need help debugging, send back:"
Print "1) ls -l /dev/serial/by-id/"
Print "2) ls -l /dev/ttyUSB* /dev/ttyACM*"
Print "3) groups"
Print "4) lsof "; dev$
Print "5) stty -F "; dev$; " -a"
Print "6) Confirm VIN power 8..30V DC and whether A/B swap was needed."
Print "7) Paste TX/RX hex dumps printed by this program."
Print "============================================================"
Print


' Configure serial port via stty
'----------------------------------------------------------------------------------------
' Explanation:
' - raw:            disable canonical processing
' - -echo:          no echo
' - -ixon -ixoff:  disable software flow control
' - -crtscts:      disable hardware flow control
' - clocal:        ignore modem control lines (important on USB serial)
' - cread:          enable receiver
' - min 0 time 1:  reads return quickly even if no bytes are available (time in 0.1s units)
sttyCmd$ = "stty -F " + dev$ + " 9600 cs8 -cstopb -parenb -ixon -ixoff -crtscts raw -echo clocal cread min 0 time 1"
Shell sttyCmd$


' Open serial device
' LEN=1 ensures byte-accurate PUT/INPUT$ behavior.
'--------------------------------------------------------------------------------------------
Open dev$ For Binary As #h% Len = 1

' CRITICAL FIX: apply stty again AFTER OPEN (prevents INPUT$ blocking on some systems)
Shell sttyCmd$

Print "Opened "; dev$; " (USB-RS485). Press ESC to exit."
Print


' STEP 0: Broadcast read 0x00FD to discover station address/DIP.
' Vendor example request: FF 03 00 FD 00 01 00 24
' We build it dynamically by CRC.
'
' expectedSlave = 0 => accept any slave address in the reply.

SerialFlushSoft h%

req$ = ModbusBuildRead$(255, &H00FD, 1)
If showFrames% Then Print "TX addr (FF 00FD): "; HexDump$(req$)
SerialWrite h%, req$

resp$ = SerialReadFrame03$(h%, 0, timeout!)
If showFrames% Then Print "RX addr:          "; HexDump$(resp$)

If Len(resp$) > 0 And ModbusCheckCrc%(resp$) Then
    slave% = Asc(resp$, 1) And &HFF
    dipRaw& = GetU16BE&(resp$, 4)
    Print "Detected station address from reply ="; slave%
    Print "00FD raw register value (hex) = &H"; Hex$(dipRaw&)
    Print
Else
    Print "Could not read station address (00FD)."
    Print "If temperatures still read fine later, ignore this. Otherwise check power/wiring/device path."
    Print "Continuing with slave address ="; slave%; "(default)."
    Print
End If


' MAIN LOOP: Read 8 temperatures (0x0000..0x0007) repeatedly
' Response should look like:
' [addr][03][16][16 data bytes][crcLo][crcHi]

Do
    key$ = InKey$
    If key$ = Chr$(27) Then Exit Do

    SerialFlushSoft h%

    req$ = ModbusBuildRead$(slave%, &H0000, 8)
    If showFrames% Then Print "TX temps:          "; HexDump$(req$)
    SerialWrite h%, req$

    resp$ = SerialReadFrame03$(h%, slave%, timeout!)
    If showFrames% Then Print "RX temps:          "; HexDump$(resp$)

    If Len(resp$) = 0 Then
        Print "No MODBUS reply (temps)."
        Print "If RX LED never blinks: power/wiring/A-B swap/device path."
        Print
        _Limit pollHz!
        GoTo ContinueLoop
    End If

    If ModbusCheckCrc%(resp$) = 0 Then
        Print "CRC FAIL (temps frame)."
        Print
        _Limit pollHz!
        GoTo ContinueLoop
    End If

    If Asc(resp$, 2) <> &H03 Then
        Print "Unexpected function code:"; Asc(resp$, 2); " (expected 03)."
        Print "If you see 0x83, it is a MODBUS exception response."
        Print
        _Limit pollHz!
        GoTo ContinueLoop
    End If

    If Asc(resp$, 3) <> 16 Then
        Print "Unexpected bytecount:"; Asc(resp$, 3); " (expected 16)."
        Print
        _Limit pollHz!
        GoTo ContinueLoop
    End If

    For i% = 0 To 7
        rawS% = GetS16BE%(resp$, 4 + i% * 2)
        t! = rawS% / 10!
        Print Using "CH# = ####.# C"; i%; t!
    Next i%
    Print "----------------------------"
    Print

    ContinueLoop:
    _Limit pollHz!
Loop

Close #h%
Print "Done."
End


Function Crc16Modbus& (s$)
    Dim crc&, i%, j%
    crc& = &HFFFF&
    For i% = 1 To Len(s$)
        crc& = crc& Xor Asc(s$, i%)
        For j% = 1 To 8
            If (crc& And 1) Then
                crc& = (crc& \ 2) Xor &HA001&
            Else
                crc& = crc& \ 2
            End If
        Next j%
    Next i%
    Crc16Modbus& = (crc& And &HFFFF&)
End Function

Function U16BE$ (v&)
    U16BE$ = Chr$((v& \ 256) And &HFF) + Chr$(v& And &HFF)
End Function

Function HexDump$ (s$)
    Dim i%, out$
    out$ = ""
    For i% = 1 To Len(s$)
        out$ = out$ + Right$("0" + Hex$(Asc(s$, i%)), 2) + " "
    Next i%
    HexDump$ = out$
End Function

Function ModbusBuildRead$ (slave%, reg&, count%)
    Dim req$, crc&
    req$ = Chr$(slave% And &HFF) + Chr$(&H03) + U16BE$(reg&) + U16BE$(count%)
    crc& = Crc16Modbus&(req$)
    req$ = req$ + Chr$(crc& And &HFF) + Chr$((crc& \ 256) And &HFF)
    ModbusBuildRead$ = req$
End Function

Function ModbusCheckCrc% (frame$)
    Dim n%, crcRx&, crcCalc&
    n% = Len(frame$)
    If n% < 5 Then ModbusCheckCrc% = 0: Exit Function
    crcRx& = Asc(frame$, n% - 1) + 256& * Asc(frame$, n%)
    crcCalc& = Crc16Modbus&(Left$(frame$, n% - 2))
    ModbusCheckCrc% = (crcRx& = crcCalc&)
End Function

Function GetU16BE& (s$, position%)
    Dim hi&, lo&
    hi& = Asc(s$, position%) And &HFF
    lo& = Asc(s$, position% + 1) And &HFF
    GetU16BE& = hi& * 256& + lo&
End Function

Function GetS16BE% (s$, position%)
    Dim v&
    v& = GetU16BE&(s$, position%)
    If v& >= 32768& Then v& = v& - 65536&
    GetS16BE% = v&
End Function

Sub SleepMs (ms!)
    _Delay ms! / 1000!
End Sub

Sub SerialWrite (h%, s$)
    Dim i%
    For i% = 1 To Len(s$)
        Byte1 = Mid$(s$, i%, 1)
        Put #h%, , Byte1
    Next i%
End Sub

Sub SerialFlushSoft (h%)
    ' Soft flush with a bounded time window.
    ' We do NOT use LOC() here. We just read/discard for a short time.
    '
    ' If INPUT$ is configured correctly (stty min 0 time 1), this should never block forever.
    ' If it STILL blocks on a specific machine, it almost always means tty settings did not apply.
    Dim t0!, b$
    t0! = Timer

    Do
        b$ = Input$(1, # h%)
        If Len(b$) = 0 Then
            If (Timer - t0!) > 0.2 Then Exit Do
            SleepMs 5
        Else
            ' Keep flushing while bytes arrive; extend window slightly.
            t0! = Timer
        End If
    Loop
End Sub

Function SerialReadFrame03$ (h%, expectedSlave%, timeoutSec!)
    ' Accumulate bytes until timeout and extract a valid MODBUS 03 response frame by CRC.
    ' Format searched:
    ' [addr][03][bytecount][...data...][crcLo][crcHi]
    '
    ' expectedSlave%:
    ' - 0 => accept any address (useful for broadcast queries)
    ' - else => accept only that slave address
    Dim t0!, buf$, b$
    Dim i%, n%, addr%, fn%, bc%, frameLen%
    Dim candidate$

    t0! = Timer
    buf$ = ""
    SerialReadFrame03$ = ""

    Do
        b$ = Input$(1, # h%)
        If Len(b$) > 0 Then
            buf$ = buf$ + b$
            If Len(buf$) > 512 Then buf$ = Right$(buf$, 512)
        Else
            SleepMs 2
        End If

        n% = Len(buf$)
        If n% >= 5 Then
            For i% = 1 To n% - 4
                addr% = Asc(buf$, i%) And &HFF

                If expectedSlave% <> 0 Then
                    If addr% <> (expectedSlave% And &HFF) Then GoTo NextI
                End If

                fn% = Asc(buf$, i% + 1) And &HFF
                If fn% <> &H03 Then GoTo NextI

                bc% = Asc(buf$, i% + 2) And &HFF
                frameLen% = 5 + bc%

                If (i% + frameLen% - 1) <= n% Then
                    candidate$ = Mid$(buf$, i%, frameLen%)
                    If ModbusCheckCrc%(candidate$) Then
                        SerialReadFrame03$ = candidate$
                        Exit Function
                    End If
                End If

                NextI:
            Next i%
        End If

        If (Timer - t0!) >= timeoutSec! Then Exit Do
    Loop
End Function

If that fails, then the quickest way is to send me the output:

stty -F /dev/ttyUSB0 -a (or your actual dev$)

and those debug dumps + first TX/RX dumps


Reply
#26
Hi Petr, 
Thanks again for sharing your knowledge. 
In the meantime, I've replaced the Raspberry Pi with a miniPC running Linux Mint 22 (Wilma) and an XFCE desktop. 

The miniPCs are identical (the one replacing the Raspberry Pi only has 8GB of RAM). 
All hardware and software are now the samel. 
What works on this PC works on the other... 

I tested your new listing, and... it runs 100% perfectly from the first start! 
Stopping and restarting multiple times is no longer a problem. 

In the beer installation, I've already replaced the DS18B20 with PT100 sensors (but I still need to connect them). 
One more question: 
As you may have noticed, I stored the sensor temperatures in an array labeled Temp!(x%). 

Should I have your listing read the sensors continuously? 
Or ON TIMER (e.g., 0.5 seconds) gosub...? 

(I would then create a small subroutine to print the three temperatures somewhere on the screen. 
(pe.:
[Image: Result-new-listing-2026-Petr.png]

brewkettle = xxx.x °C, water heater = xxx.x °C, cooling water valve = xxx.x °C) 

Regards,
Rudy M
Reply
#27
Hi Petr, 
I did some more testing and discovered that I need to add the user "rudy" to the "dialout" group again. 
As you wrote, "Typical error on Mint/Ubuntu." 

So, I ran the Terminal command you suggested: 
sudo usermod -a -G dialout $user (in my case, $user = rudy). 
-Turn off the PT100 module's power supply and turn it back on after 20-30 seconds. 
-Turn the PC off and back on (or restart it). 
And the PT100 routines are working perfectly again! 
Regards,
Rudy
Reply
#28
@Rudy M

Your current approach (continuous loop + _LIMIT pollHz!) is perfectly fine and is the simplest, most robust way to poll MODBUS sensors on a PC. For MODBUS RTU there is no “push” unless you enable auto-report, so polling is the normal model.

I would not use ON TIMER ... GOSUB for serial MODBUS polling. Reason: TIMER events can fire while you’re still reading/writing the port, which can create re-entrancy problems (overlapping requests, partial frames, CRC fails) unless you add a lock/busy flag and very careful buffering. A single-threaded main loop is safer.

Recommended pattern:

Keep a main loop that polls at a fixed rate: e.g. 2 Hz (every 0.5 s) or 1 Hz.

After each successful read, copy results into Temp!(0..7) (and maybe store a timestamp).

The rest of your program (control logic / GUI) just reads the latest Temp!() values. If a read fails, keep the old values and set an error flag or “age” counter.

So: if you want “every 0.5 seconds”, don’t use ON TIMER. Just set:

pollHz! = 2 (because 2 polls per second = 0.5 s)

or use Timer (not ON Timer but Timer only):

pseudocode:

If timer < oldtimer then

oldtimer = timer + .5
read and print temperatures

end if

Extra practical notes:

MODBUS requests should not be too fast; 0.5–1.0 s is a good start for stability.

If you later run multiple devices (relays + sensors) on the same RS485 bus, keep polling orderly: one request at a time, wait for reply, then next request.


Reply
#29
Hi Petr, Thanks for your advice. 
And... you're right, that ON TIMER would mess up your carefully timed routine.
So I stopped using the ON TIMER (nonsense in this case...). 

So what did I do? 
- I defined a global variable PT100start% At the start, I set PT100start% = TRUE 
- I named your main routine PT100Read. 

I placed your startup or initiation routines between IF THEN END IF statements. 
- If the startup routines AND after an initial read test of the PT100 sensors return no errors: 
  Only then will PT100start% = FALSE From this point on, the next time your routine is called, only the reading of the PT100 sensors will be performed.

OK, so far...
Yesterday I ran the above 50-60 times (also to clean up some minor bugs in my software). 
AND: Your routine didn't fail a single time! 
Congratulations on your wonderful piece of software!!! 

I launched the program until I had to heat up the brewing kettle.
(With the USB relay, I trick the PC into thinking a brewing system is connected.)
Result: The PC switches on the induction hob (via the USB relay). 
Then my subroutine checks... Temperature too high or too low, then switches the induction hob OFF or ON. 

I could clearly see that the reading speed (of the PT100 sensors) was 100% controlled by your routine (POLHz...).
(because the powerled on the MODBUSboard "blinks" every time the module reads the sensors.) 
I'll do some more testing in the coming days, and if it continues to work without bugs,
I'll publish your routine with my (minor) changes here on the forum.

All in all... I'm truly grateful to you; without your knowledge, 
I couldn't have achieved this. 

Thank you, Rudy M
Reply


Possibly Related Threads…
Thread Author Replies Views Last Post
Question How to read Google Calendar events? Ikerkaz 10 1,884 07-10-2023, 11:06 AM
Last Post: Ultraman
  SHELL creates unicode file, can't read correctly with LINE INPUT thesolarcode 3 1,248 05-06-2023, 09:41 PM
Last Post: thesolarcode
  Read / Allocate Cores dISP 10 2,234 04-25-2023, 12:23 PM
Last Post: dISP
  Can images be read from a file into an array? PhilOfPerth 11 2,563 02-17-2023, 03:31 AM
Last Post: TerryRitchie

Forum Jump:


Users browsing this thread: 1 Guest(s)