Giter VIP home page Giter VIP logo

Comments (26)

johnzbesko avatar johnzbesko commented on May 28, 2024 1

Well, after dealing with custom hardware issues, I again tackled the issue of multithreading, per the examples given. My app redraws the plot every 2 seconds. It seems the user buttons are unresponsive during the 2 seconds, I have to click multiple times or doubleclick to get a response. Current to the electric heater is pulsed for 0-8 seconds, so the GUI can be unresponsive longer than 2 seconds. I tried to use multithreading to control the heat on/off, but the heat stayed on continually- maybe each 2 seconds another call to the heater thread?

I've attached two programs- one has a single thread for the flow meter (Hall effect, it counts the number of interrupts in 2 seconds) and another where the numbers of seconds the heat is on is a thread as well as the flow thread. Please forgive my spaghetti code!

#!/usr/bin/env python3
"""
One thread for flow meter

@author: pi
"""
import sys
import time
import datetime
import RPi.GPIO as GPIO
import pigpio
import threading

import board
import busio
import digitalio

import adafruit_max31865

import PySimpleGUI as sg
#import tkinter as tk
import pyscreenshot as ImageGrab
import io

# will be adding graph of temperatures and flow
#import matplotlib.backends.backend_tkagg as tkagg
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasAgg

FLOW_GPIO = 22

pi = pigpio.pi()  # connect to Pi
if not pi.connected:
    exit()
pi.set_mode(FLOW_GPIO, pigpio.INPUT)
pi.set_pull_up_down(FLOW_GPIO, pigpio.PUD_UP)

GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)
GPIO.setup(27, GPIO.OUT)

# Initialize SPI bus and sensor.
spi = busio.SPI(board.SCK, MOSI=board.MOSI, MISO=board.MISO)
csm = digitalio.DigitalInOut(board.D5)  # Chip select of the MAX31865 board.
csr = digitalio.DigitalInOut(board.D6)
# Note you can optionally provide the thermocouple RTD nominal, the reference
# resistance, and the number of wires for the sensor (2 the default, 3, or 4)
# with keyword args:
mash = adafruit_max31865.MAX31865(
    spi, csm, rtd_nominal=100, ref_resistor=435, wires=3)
rims = adafruit_max31865.MAX31865(
    spi, csr, rtd_nominal=100, ref_resistor=430, wires=3)

#best size for touchscreen
SIZE = (800, 450)

def printNiceTimeDelta(stime, etime):
    delay = datetime.timedelta(seconds=(etime - stime))
    if (delay.days > 0):
        out = str(delay).replace(" days, ", ":")
    else:
        out = "0:" + str(delay)
    outAr = out.split(':')
    outAr = ["%02d" % (int(float(x))) for x in outAr]
    out = ":".join(outAr)
    return out


def save_element_as_file(element, filename):
    """
    Saves any element as an image file.  Element needs to have an underlyiong Widget available (almost if not all of them do)
    :param element: The element to save
    :param filename: The filename to save to. The extension of the filename determines the format (jpg, png, gif, ?)
    """
    widget = element.Widget
    box = (widget.winfo_rootx(), widget.winfo_rooty(), widget.winfo_rootx(
    ) + widget.winfo_width(), widget.winfo_rooty() + widget.winfo_height())
    grab = ImageGrab.grab(bbox=box)
    grab.save(filename)


class MyGlobals:
    axis = None
    secondAxis = None
    t_array = []
    x_array = []
    y_array = []
    y2array = []
    f_array = []
    s_array = []


g_my_globals = MyGlobals()

# ================================================================================
#       Performs *** PING! ***
# ================================================================================


def run_graph():
    # graphs are global so that can be retained across multiple calls to this callback
    global g_my_globals, flowAxis

    # ===================== Store current ping in historical array =====================#
    g_my_globals.x_array.append(len(g_my_globals.x_array))
    g_my_globals.t_array.append(datetime.datetime.now())
    g_my_globals.y_array.append(fahrMash)
    g_my_globals.y2array.append(fahrRIMS)
    g_my_globals.f_array.append(flow)
    g_my_globals.s_array.append(setTemp)

    t_array = g_my_globals.t_array
    y_array = g_my_globals.y_array
    y2array = g_my_globals.y2array
    f_array = g_my_globals.f_array
    s_array = g_my_globals.s_array

    # ===================== Call graphinc functions =====================#
    g_my_globals.axis.clear()

    g_my_globals.axis.set_ylim([50, 175])     # clear before graphing
    g_my_globals.axis.grid()
    g_my_globals.axis.plot(t_array, y_array)
    g_my_globals.axis.plot(t_array, y2array)
    g_my_globals.axis.plot(t_array, s_array)
    g_my_globals.axis.set_xlabel('Time', fontsize=8)
    for label in g_my_globals.axis.get_xticklabels():
        label.set_rotation(90)
        label.set_horizontalalignment('right')

    g_my_globals.axis.set_ylabel('Temperature (F)', fontsize=10)
    g_my_globals.axis.set_title('Mash RIMS Flow', fontsize=8)
    g_my_globals.axis.plot(t_array, f_array)  # graph the ping values
    g_my_globals.axis.legend(['Mash', 'RIMS', 'Set', 'Flow'], loc='upper left')

# ================================================================================
#   Function:   Set graph titles and Axis labels
#       Sets the text for the subplots
#       Have to do this in 2 places... initially when creating and when updating
#       So, putting into a function so don't have to duplicate code
# ================================================================================


def set_chart_labels():
    global g_my_globals

    g_my_globals.axis.set_xlabel('Time', fontsize=8)
    g_my_globals.axis.set_ylabel('Temperature (F)', fontsize=8)
    g_my_globals.axis.set_title('Mash RIMS Flow', fontsize=8)
    g_my_globals.axis.legend(['Mash', 'RIMS', 'Set', 'Flow'], loc='best') 

def draw(element, figure):
    """
    Draws the previously created "figure" in the supplied Image Element

    :param element: an Image Element
    :param figure: a Matplotlib figure
    :return: The figure canvas
    """

    # plt.close('all')  # erases previously drawn plots
    canv = FigureCanvasAgg(figure)
    buf = io.BytesIO()
    canv.print_figure(buf, format='png')
    if buf is not None:
        buf.seek(0)
        element.update(data=buf.read())
        return canv
    else:
        return None


def sensor_thread(flowRate,window):
    while True:

        callback = pi.callback(FLOW_GPIO)  # default tally callback
        callback.reset_tally()
        time.sleep(2)
        flowRate = callback.tally()*2.1      
        window.write_event_value('-FLOW-', flowRate)


# ================================================================================
#   Function:   MAIN
# ================================================================================


def main():
    global g_my_globals, fixMash, fixRIMs
    datalogfile = open("/home/pi/BrewPi/datalog.csv", "w")
    datalogfile.write("Time"+time.strftime("%Y%m%d") +
                      ", RIMS, Mash, Flow, SetTemp, ElapsedTime, secon"+"\n")
    datalogfile.close()
    datalogfile = open("/home/pi/BrewPi/datalog.csv", "a")

    global setTemp, setTime, count, start_counter
    setTime = time.time()
    setTemp = 32
    fixMash = 0
    fixRIMs = 0

    # define the form layout
    dataframe = [
        [sg.Text('Mash (F):', size=(10, 1), font='Courier 10'),
         sg.Text('     ', font='Courier 16', text_color='red', key='mashF'),
         sg.Text('RIMS (F):', size=(10, 1), font='Courier 10'),
         sg.Text('     ', font='Courier 16', text_color='red', key='rimsF'),
         sg.Text('Set (F):', size=(10, 1), font='Courier 10'),
         sg.Text('     ', font='Courier 16', text_color='red', key='setF'),
         sg.Text('Flow L/hr:', size=(10, 1), font='Courier 10'),
         sg.Text('     ', font='Courier 16', text_color='red', key='flowRate')]
    ]

    layout = [
        [sg.Canvas( background_color='white', key='canvas',expand_x=True,expand_y=True)],
        [sg.Image(key='-IMAGE-',expand_x=True,expand_y=True,size=[int(SIZE[0] / 2), int(SIZE[1] / 2)])],
#        [sg.Image(key='-IMAGE-',size=SIZE)],
        [sg.Frame('Mash/RIMS/Flow', layout=dataframe,
                  font='Any 10', title_color='blue')],
        [sg.Button('fix Mash', size=(6, 2), font='Any 10'),
         sg.Button('fix RIMs', size=(6, 2), font='Any 10'),
         sg.Button('Set Temp', size=(6, 2), font='Any 10'),
         sg.Button('Reset Time', size=(6, 2), font='Any 10'), sg.Text(
             '     ', size=(10, 1), font='Any 16', key='setTime'),
         sg.Button('Exit', size=(6, 2), font='Any 10')]
    ]

    # create the form and show it without the plot
    window = sg.Window('Mash Control', layout, background_color='white',resizable=True, size=SIZE, 
                       grab_anywhere=True, use_default_focus=False, location=(0, 0)).Finalize()
    #window.maximize()
    #print(window.size)
    window['setTime'](0)
#    fig = plt.figure()
    fig = plt.figure(figsize=(6.0, 3), tight_layout={'pad': 0})
    fig.set_tight_layout
    g_my_globals.axis = fig.add_subplot(1, 1, 1)

    plt.rcParams['xtick.labelsize'] = 8
    plt.rcParams['ytick.labelsize'] = 8
    plt.tight_layout()

    keypad_active = False
    newSetTemp = 0
    image_element = window['-IMAGE-']

    threading.Thread(target=sensor_thread, args=('TempsFlow', window),  daemon=True).start()    
   
    while True:
        global fahrMash, fahrRIMS, flow, flowGal, secon

        window['Reset Time'].Widget.config(takefocus=1)
        event, values = window.read(timeout=0)

        keypad_layout= [
            [sg.Input('', size=(10, 3), key='input')],
            [sg.Button('1', font='Helvetica 8'), sg.Button('2', font='Helvetica 8'), sg.Button(
                    '3', font='Helvetica 8'), sg.Button('4', font='Helvetica 8')],
            [sg.Button('5', font='Helvetica 8'), sg.Button('6', font='Helvetica 8'), sg.Button(
                    '7', font='Helvetica 8'), sg.Button('8', font='Helvetica 8')],
            [sg.Button('9', font='Helvetica 8'), sg.Button('0', font='Helvetica 8'), sg.Button(
                    'Enter', font='Helvetica 8', key='Submit'), sg.Button('Clear', font='Helvetica 8')],
            [sg.Text('', size=(10, 3), font='Helvetica 12',
                         text_color='red', key='out')],
            ]

        if event in (None, 'Exit'):
            GPIO.output(27, GPIO.LOW)
            GPIO.remove_event_detect(27)
#            callback.cancel()  # cancel callback
            pi.stop()  # disconnect from Pi
            GPIO.remove_event_detect(21)
            GPIO.remove_event_detect(22)
            GPIO.cleanup()
            save_element_as_file(image_element, '/home/pi/BrewPi/plot.png')
            window.close()
            del window
            datalogfile.close()
            sys.exit
            break

        if not keypad_active and event == 'Set Temp':
            keypad_active = True

            keypad = sg.Window('Keypad', keypad_layout,
                               default_button_element_size=(20, 10),
                               auto_size_buttons=False,
                               grab_anywhere=False)

            # Loop forever reading the form's values, updating the Input field
            keys_entered = ''
            while True:
                event, values = keypad.read()  # read the form
                if event is None:  # if the X button clicked, just exit
                    break
                if event == 'Clear':  # clear keys if clear button
                    keys_entered = ''
                elif event in '1234567890':
                    # get what's been entered so far
                    keys_entered = values['input']
                    keys_entered += event  # add the new digit
                elif event == 'Submit':
                    keys_entered = values['input']
     #               window_elem.update(keys_entered)# output the final string
                    window['setF'].update(keys_entered)
                    break
                # change the form to reflect current key string
                keypad['input'].update(keys_entered)
            setTemp = float(keys_entered)
            setTime = time.time()
            newSetTemp = 1
            keypad.close()
            keypad_active = False
            window.un_hide()

        if not keypad_active and event == 'fix Mash':
            keypad_active = True

            keypad = sg.Window('Keypad', keypad_layout,
                               default_button_element_size=(20, 10),
                               auto_size_buttons=False,
                               grab_anywhere=False)

            # Loop forever reading the form's values, updating the Input field
            keys_entered = ''
            while True:
                event, values = keypad.read()  # read the form
                if event is None:  # if the X button clicked, just exit
                    break
                if event == 'Clear':  # clear keys if clear button
                    keys_entered = ''
                elif event in '1234567890':
                    # get what's been entered so far
                    keys_entered = values['input']
                    keys_entered += event  # add the new digit
                elif event == 'Submit':
                    keys_entered = values['input']
     #               window_elem.update(keys_entered)# output the final string
                    window['setF'].update(keys_entered)
                    break
                # change the form to reflect current key string
                keypad['input'].update(keys_entered)
            fixMash = keys_entered
            keypad.close()
            keypad_active = False
            window.un_hide()

        if not keypad_active and event == 'fix RIMs':
            keypad_active = True

            keypad = sg.Window('Keypad', keypad_layout,
                               default_button_element_size=(20, 10),
                               auto_size_buttons=False,
                               grab_anywhere=False)

            # Loop forever reading the form's values, updating the Input field
            keys_entered = ''
            while True:
                event, values = keypad.read()  # read the form
                if event is None:  # if the X button clicked, just exit
                    break
                if event == 'Clear':  # clear keys if clear button
                    keys_entered = ''
                elif event in '1234567890':
                    # get what's been entered so far
                    keys_entered = values['input']
                    keys_entered += event  # add the new digit
                elif event == 'Submit':
                    keys_entered = values['input']
     #               window_elem.update(keys_entered)# output the final string
                    window['setF'].update(keys_entered)
                    break
                # change the form to reflect current key string
                keypad['input'].update(keys_entered)
            fixRIMs = keys_entered
            keypad.close()
            keypad_active = False
            window.un_hide()

        if event == 'Reset Time':
            setTime = time.time()

        if event == 'Close':
            GPIO.output(27, GPIO.LOW)
            GPIO.remove_event_detect(27)
            GPIO.remove_event_detect(FLOW_GPIO)
            GPIO.remove_event_detect(21)
            GPIO.remove_event_detect(22)
            GPIO.cleanup()
            save_element_as_file(image_element, '/home/pi/Raspberry/plot.png')
            window.close()
            del window
            datalogfile.close()
            sys.exit
            break

        # read temperatures     
        event, values = window.read()
        
        if event == '-FLOW-':
            flow =  values[event]
        # if event == '-HEAT-':
        #     flow =  values[event]
        

        clcsMash = mash.temperature 
        fahrMash = round(9/5*clcsMash + 32 +float(fixMash),1)
        clcsRIMS = rims.temperature 
        fahrRIMS = round(9/5*clcsRIMS + 32 +float(fixRIMs),1)
        if (fahrMash>(float(setTemp)-1)) and (newSetTemp==1):
            setTime = time.time()
            newSetTemp=0
        secon = 0


        if (fahrMash > (float(setTemp)-1)) and (newSetTemp == 1):
            setTime = time.time()
            newSetTemp = 0
        secon = 0

        # thermostat control
        # was 50, now bigger safety margin
        if (fahrMash < float(setTemp)) and (fahrRIMS < (float(setTemp)+5)) and (flow > 75) and ((fahrRIMS-fahrMash) < 11):
            #        if (True):
            maxSecOn = min(8, flow/15)  # was 12.5 in V1. 13 in V2
            secon = min(maxSecOn, (float(setTemp)-fahrMash)*2)
            GPIO.output(27, GPIO.HIGH)
            time.sleep(secon)
            GPIO.output(27, GPIO.LOW)
        else:
            GPIO.output(27, GPIO.LOW)

        run_graph()
        window['mashF'](fahrMash)
        window['rimsF'](fahrRIMS)
        window['flowRate'](flow)
        window['setF'](setTemp)
        window['setTime'](printNiceTimeDelta(setTime, time.time()))
        #print("Temperature: {0:0.3f}F".format(fahrMash))
        datalogfile.write(time.strftime("%H:%M:%S") +
                          ","+format(fahrRIMS, '.1f')+","+format(fahrMash, '.1f') +
                          ","+format(flow)+","+format(setTemp) +
                          ","+printNiceTimeDelta(setTime, time.time()) +
                          ","+format(secon)+"\n")


        draw(image_element,fig)
        count = 0
        # window.close()


if __name__ == '__main__':
    main()
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Multi-threaded for flow and heater circuit

@author: pi
"""
import sys
import time
import datetime
import RPi.GPIO as GPIO
import pigpio
import threading

import board
import busio
import digitalio

import adafruit_max31865

import PySimpleGUI as sg
#import tkinter as tk
import pyscreenshot as ImageGrab
import io

# will be adding graph of temperatures and flow
#import matplotlib.backends.backend_tkagg as tkagg
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasAgg

FLOW_GPIO = 22

pi = pigpio.pi()  # connect to Pi
if not pi.connected:
    exit()
pi.set_mode(FLOW_GPIO, pigpio.INPUT)
pi.set_pull_up_down(FLOW_GPIO, pigpio.PUD_UP)

GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)
#the pin for turning on/off heat
GPIO.setup(27, GPIO.OUT)


# Initialize SPI bus and sensor.
spi = busio.SPI(board.SCK, MOSI=board.MOSI, MISO=board.MISO)
csm = digitalio.DigitalInOut(board.D5)  # Chip select of the MAX31865 board.
csr = digitalio.DigitalInOut(board.D6)
# Note you can optionally provide the thermocouple RTD nominal, the reference
# resistance, and the number of wires for the sensor (2 the default, 3, or 4)
# with keyword args:
mash = adafruit_max31865.MAX31865(
    spi, csm, rtd_nominal=100, ref_resistor=435, wires=3)
rims = adafruit_max31865.MAX31865(
    spi, csr, rtd_nominal=100, ref_resistor=430, wires=3)

#best size for touchscreen
SIZE = (800, 450)

def printNiceTimeDelta(stime, etime):
    delay = datetime.timedelta(seconds=(etime - stime))
    if (delay.days > 0):
        out = str(delay).replace(" days, ", ":")
    else:
        out = "0:" + str(delay)
    outAr = out.split(':')
    outAr = ["%02d" % (int(float(x))) for x in outAr]
    out = ":".join(outAr)
    return out


def save_element_as_file(element, filename):
    """
    Saves any element as an image file.  Element needs to have an underlyiong Widget available (almost if not all of them do)
    :param element: The element to save
    :param filename: The filename to save to. The extension of the filename determines the format (jpg, png, gif, ?)
    """
    widget = element.Widget
    box = (widget.winfo_rootx(), widget.winfo_rooty(), widget.winfo_rootx(
    ) + widget.winfo_width(), widget.winfo_rooty() + widget.winfo_height())
    grab = ImageGrab.grab(bbox=box)
    grab.save(filename)


class MyGlobals:
    axis = None
    secondAxis = None
    t_array = []
    x_array = []
    y_array = []
    y2array = []
    f_array = []
    s_array = []


g_my_globals = MyGlobals()

# ================================================================================
#       Performs *** PING! ***
# ================================================================================


def run_graph():
    # graphs are global so that can be retained across multiple calls to this callback
    global g_my_globals, flowAxis

    # ===================== Store current ping in historical array =====================#
    g_my_globals.x_array.append(len(g_my_globals.x_array))
    g_my_globals.t_array.append(datetime.datetime.now())
    g_my_globals.y_array.append(fahrMash)
    g_my_globals.y2array.append(fahrRIMS)
    g_my_globals.f_array.append(flow)
    g_my_globals.s_array.append(setTemp)

    t_array = g_my_globals.t_array
    y_array = g_my_globals.y_array
    y2array = g_my_globals.y2array
    f_array = g_my_globals.f_array
    s_array = g_my_globals.s_array

    # ===================== Call graphinc functions =====================#
    g_my_globals.axis.clear()

    g_my_globals.axis.set_ylim([50, 175])     # clear before graphing
    g_my_globals.axis.grid()
    g_my_globals.axis.plot(t_array, y_array)
    g_my_globals.axis.plot(t_array, y2array)
    g_my_globals.axis.plot(t_array, s_array)
    g_my_globals.axis.set_xlabel('Time', fontsize=8)
    for label in g_my_globals.axis.get_xticklabels():
        label.set_rotation(90)
        label.set_horizontalalignment('right')

    g_my_globals.axis.set_ylabel('Temperature (F)', fontsize=10)
    g_my_globals.axis.set_title('Mash RIMS Flow', fontsize=8)
    g_my_globals.axis.plot(t_array, f_array)  # graph the ping values
    g_my_globals.axis.legend(['Mash', 'RIMS', 'Set', 'Flow'], loc='upper left')

# ================================================================================
#   Function:   Set graph titles and Axis labels
#       Sets the text for the subplots
#       Have to do this in 2 places... initially when creating and when updating
#       So, putting into a function so don't have to duplicate code
# ================================================================================


def set_chart_labels():
    global g_my_globals

    g_my_globals.axis.set_xlabel('Time', fontsize=8)
    g_my_globals.axis.set_ylabel('Temperature (F)', fontsize=8)
    g_my_globals.axis.set_title('Mash RIMS Flow', fontsize=8)
    g_my_globals.axis.legend(['Mash', 'RIMS', 'Set', 'Flow'], loc='best') 

def draw(element, figure):
    """
    Draws the previously created "figure" in the supplied Image Element

    :param element: an Image Element
    :param figure: a Matplotlib figure
    :return: The figure canvas
    """

    # plt.close('all')  # erases previously drawn plots
    canv = FigureCanvasAgg(figure)
    buf = io.BytesIO()
    canv.print_figure(buf, format='png')
    if buf is not None:
        buf.seek(0)
        element.update(data=buf.read())
        return canv
    else:
        return None


def sensor_thread(flowRate,window):
    while True:

        callback = pi.callback(FLOW_GPIO)  # default tally callback
        callback.reset_tally()
        time.sleep(2)
        flowRate = callback.tally()*2.1      
        window.write_event_value('-FLOW-', flowRate)

def power_thread(power,seconds,window):
    GPIO.setwarnings(False)
    GPIO.setup(27, GPIO.OUT)
 
    while True:
        GPIO.output(27, GPIO.HIGH)
        time.sleep(seconds)
        GPIO.output(27, GPIO.LOW)
    else:
        GPIO.output(27, GPIO.LOW)
     
        window.write_event_value('-HEAT-')


# ================================================================================
#   Function:   MAIN
# ================================================================================


def main():
    global g_my_globals, fixMash, fixRIMs
    datalogfile = open("/home/pi/BrewPi/datalog.csv", "w")
    datalogfile.write("Time"+time.strftime("%Y%m%d") +
                      ", RIMS, Mash, Flow, SetTemp, ElapsedTime, secon"+"\n")
    datalogfile.close()
    datalogfile = open("/home/pi/BrewPi/datalog.csv", "a")

    global setTemp, setTime, count, start_counter
    setTime = time.time()
    setTemp = 32
    fixMash = 0
    fixRIMs = 0

    # define the form layout
    dataframe = [
        [sg.Text('Mash (F):', size=(10, 1), font='Courier 10'),
         sg.Text('     ', font='Courier 16', text_color='red', key='mashF'),
         sg.Text('RIMS (F):', size=(10, 1), font='Courier 10'),
         sg.Text('     ', font='Courier 16', text_color='red', key='rimsF'),
         sg.Text('Set (F):', size=(10, 1), font='Courier 10'),
         sg.Text('     ', font='Courier 16', text_color='red', key='setF'),
         sg.Text('Flow L/hr:', size=(10, 1), font='Courier 10'),
         sg.Text('     ', font='Courier 16', text_color='red', key='flowRate')]
    ]

    layout = [
        [sg.Canvas( background_color='white', key='canvas',expand_x=True,expand_y=True)],
        [sg.Image(key='-IMAGE-',expand_x=True,expand_y=True,size=[int(SIZE[0] / 2), int(SIZE[1] / 2)])],
#        [sg.Image(key='-IMAGE-',size=SIZE)],
        [sg.Frame('Mash/RIMS/Flow', layout=dataframe,
                  font='Any 10', title_color='blue')],
        [sg.Button('fix Mash', size=(6, 2), font='Any 10'),
         sg.Button('fix RIMs', size=(6, 2), font='Any 10'),
         sg.Button('Set Temp', size=(6, 2), font='Any 10'),
         sg.Button('Reset Time', size=(6, 2), font='Any 10'), sg.Text(
             '     ', size=(10, 1), font='Any 16', key='setTime'),
         sg.Button('Exit', size=(6, 2), font='Any 10')]
    ]

    # create the form and show it without the plot
    window = sg.Window('Mash Control', layout, background_color='white',resizable=True, size=SIZE, 
                       grab_anywhere=True, use_default_focus=False, location=(0, 0)).Finalize()
    #window.maximize()
    #print(window.size)
    window['setTime'](0)
#    fig = plt.figure()
    fig = plt.figure(figsize=(6.0, 3), tight_layout={'pad': 0})
    fig.set_tight_layout
    g_my_globals.axis = fig.add_subplot(1, 1, 1)

    plt.rcParams['xtick.labelsize'] = 8
    plt.rcParams['ytick.labelsize'] = 8
    plt.tight_layout()

    keypad_active = False
    newSetTemp = 0
    image_element = window['-IMAGE-']

    threading.Thread(target=sensor_thread, args=('TempsFlow', window),  daemon=True).start()    
   
    while True:
        global fahrMash, fahrRIMS, flow, flowGal, secon

        window['Reset Time'].Widget.config(takefocus=1)
        event, values = window.read(timeout=0)

        keypad_layout= [
            [sg.Input('', size=(10, 3), key='input')],
            [sg.Button('1', font='Helvetica 8'), sg.Button('2', font='Helvetica 8'), sg.Button(
                    '3', font='Helvetica 8'), sg.Button('4', font='Helvetica 8')],
            [sg.Button('5', font='Helvetica 8'), sg.Button('6', font='Helvetica 8'), sg.Button(
                    '7', font='Helvetica 8'), sg.Button('8', font='Helvetica 8')],
            [sg.Button('9', font='Helvetica 8'), sg.Button('0', font='Helvetica 8'), sg.Button(
                    'Enter', font='Helvetica 8', key='Submit'), sg.Button('Clear', font='Helvetica 8')],
            [sg.Text('', size=(10, 3), font='Helvetica 12',
                         text_color='red', key='out')],
            ]

        if event in (None, 'Exit'):
            GPIO.output(27, GPIO.LOW)
            GPIO.remove_event_detect(27)
#            callback.cancel()  # cancel callback
            pi.stop()  # disconnect from Pi
            GPIO.remove_event_detect(21)
            GPIO.remove_event_detect(22)
            GPIO.cleanup()
            save_element_as_file(image_element, '/home/pi/BrewPi/plot.png')
            window.close()
            del window
            datalogfile.close()
            sys.exit
            break

        if not keypad_active and event == 'Set Temp':
            keypad_active = True

            keypad = sg.Window('Keypad', keypad_layout,
                               default_button_element_size=(20, 10),
                               auto_size_buttons=False,
                               grab_anywhere=False)

            # Loop forever reading the form's values, updating the Input field
            keys_entered = ''
            while True:
                event, values = keypad.read()  # read the form
                if event is None:  # if the X button clicked, just exit
                    break
                if event == 'Clear':  # clear keys if clear button
                    keys_entered = ''
                elif event in '1234567890':
                    # get what's been entered so far
                    keys_entered = values['input']
                    keys_entered += event  # add the new digit
                elif event == 'Submit':
                    keys_entered = values['input']
     #               window_elem.update(keys_entered)# output the final string
                    window['setF'].update(keys_entered)
                    break
                # change the form to reflect current key string
                keypad['input'].update(keys_entered)
            setTemp = float(keys_entered)
            setTime = time.time()
            newSetTemp = 1
            keypad.close()
            keypad_active = False
            window.un_hide()

        if not keypad_active and event == 'fix Mash':
            keypad_active = True

            keypad = sg.Window('Keypad', keypad_layout,
                               default_button_element_size=(20, 10),
                               auto_size_buttons=False,
                               grab_anywhere=False)

            # Loop forever reading the form's values, updating the Input field
            keys_entered = ''
            while True:
                event, values = keypad.read()  # read the form
                if event is None:  # if the X button clicked, just exit
                    break
                if event == 'Clear':  # clear keys if clear button
                    keys_entered = ''
                elif event in '1234567890':
                    # get what's been entered so far
                    keys_entered = values['input']
                    keys_entered += event  # add the new digit
                elif event == 'Submit':
                    keys_entered = values['input']
     #               window_elem.update(keys_entered)# output the final string
                    window['setF'].update(keys_entered)
                    break
                # change the form to reflect current key string
                keypad['input'].update(keys_entered)
            fixMash = keys_entered
            keypad.close()
            keypad_active = False
            window.un_hide()

        if not keypad_active and event == 'fix RIMs':
            keypad_active = True

            keypad = sg.Window('Keypad', keypad_layout,
                               default_button_element_size=(20, 10),
                               auto_size_buttons=False,
                               grab_anywhere=False)

            # Loop forever reading the form's values, updating the Input field
            keys_entered = ''
            while True:
                event, values = keypad.read()  # read the form
                if event is None:  # if the X button clicked, just exit
                    break
                if event == 'Clear':  # clear keys if clear button
                    keys_entered = ''
                elif event in '1234567890':
                    # get what's been entered so far
                    keys_entered = values['input']
                    keys_entered += event  # add the new digit
                elif event == 'Submit':
                    keys_entered = values['input']
     #               window_elem.update(keys_entered)# output the final string
                    window['setF'].update(keys_entered)
                    break
                # change the form to reflect current key string
                keypad['input'].update(keys_entered)
            fixRIMs = keys_entered
            keypad.close()
            keypad_active = False
            window.un_hide()

        if event == 'Reset Time':
            setTime = time.time()

        if event == 'Close':
            GPIO.output(27, GPIO.LOW)
            GPIO.remove_event_detect(27)
            GPIO.remove_event_detect(FLOW_GPIO)
            GPIO.remove_event_detect(21)
            GPIO.remove_event_detect(22)
            GPIO.cleanup()
            save_element_as_file(image_element, '/home/pi/Raspberry/plot.png')
            window.close()
            del window
            datalogfile.close()
            sys.exit
            break

        # read flow     
        event, values = window.read()
        
        if event == '-FLOW-':
            flow =  values[event]
        # if event == '-HEAT-':
        #     flow =  values[event]
        

        clcsMash = mash.temperature 
        fahrMash = round(9/5*clcsMash + 32 +float(fixMash),1)
        clcsRIMS = rims.temperature 
        fahrRIMS = round(9/5*clcsRIMS + 32 +float(fixRIMs),1)
        if (fahrMash>(float(setTemp)-1)) and (newSetTemp==1):
            setTime = time.time()
            newSetTemp=0
        secon = 0


        if (fahrMash > (float(setTemp)-1)) and (newSetTemp == 1):
            setTime = time.time()
            newSetTemp = 0
        secon = 0

        # thermostat control
        # was 50, now bigger safety margin
        if (fahrMash < float(setTemp)) and (fahrRIMS < (float(setTemp)+5)) and (flow > 75) and ((fahrRIMS-fahrMash) < 11):

            maxSecOn = min(8, flow/15)  # was 12.5 in V1. 13 in V2
            secon = min(maxSecOn, (float(setTemp)-fahrMash)*2)
            print(secon,maxSecOn)
            threading.Thread(target=power_thread, args=('Seconds',secon, window),  daemon=True).start()
        else:
            GPIO.output(27, GPIO.LOW)

        run_graph()
        window['mashF'](fahrMash)
        window['rimsF'](fahrRIMS)
        window['flowRate'](flow)
        window['setF'](setTemp)
        window['setTime'](printNiceTimeDelta(setTime, time.time()))
        #print("Temperature: {0:0.3f}F".format(fahrMash))
        datalogfile.write(time.strftime("%H:%M:%S") +
                          ","+format(fahrRIMS, '.1f')+","+format(fahrMash, '.1f') +
                          ","+format(flow)+","+format(setTemp) +
                          ","+printNiceTimeDelta(setTime, time.time()) +
                          ","+format(secon)+"\n")


        draw(image_element,fig)
        count = 0
        # window.close()


if __name__ == '__main__':
    main()

from pysimplegui.

johnzbesko avatar johnzbesko commented on May 28, 2024 1

Thanks for all your help! I'll study the examples further and, if necessary, I'll ask simple, direct questions in future.

BTW, can you comment of the use of multithreading vs. asyncio for my application?

from pysimplegui.

jason990420 avatar jason990420 commented on May 28, 2024 1

Long script may stop people to help.

I didn't spend long time to look inside your script, but come out something which may help.

  • Main thread mainly for GUI, and initialize your sub-thread for your GPIO once.
  • Generate events from sub-thread to main thread by calling window.write_event_valu, and update the GUI under your event loop.
  • Waiting in other event loop of sub-window will stop the GUI update for the main window.
# Pseudo Code
1. Set a status field/column in your window, so no popup window required and wait there.
2. Generate a window with your layout
3. Initialize GPIO
4. Starting sub-thread which can be stopped by a global flag/variable, to manage about pulling data from GPIO
5. Event loop for GUI
    a. Exit or Close window - set the flag to stop sub-thread first before your close/destroy the window
    b. Get sub-thread stopped event to close GPIO and the window
    c. Get event from sub-thread to generate matplotlib figure and redraw to update the GUI
    d. Other event from main window interface

Of course, you can use multiple sub-threads for each hardware interface. Better not to start new sub-thread again and again under your while event loop.

from pysimplegui.

johnzbesko avatar johnzbesko commented on May 28, 2024 1

Again, thank you for your help! Your advice will take me some time to understand and implement. My experience of programming has been mostly linear- starting with punched cards in the early '70's. ;-)

from pysimplegui.

jason990420 avatar jason990420 commented on May 28, 2024
# Line 3 in Demo_Matplotlib_Ping_Graph_Large.py
import matplotlib.backends.tkagg as tkagg
# Line 7
import ping
Traceback (most recent call last):
  File "D:\Demo_Matplotlib_Ping_Graph_Large.py", line 3, in <module>
    import matplotlib.backends.tkagg as tkagg
ModuleNotFoundError: No module named 'matplotlib.backends.tkagg'

API Changes for 3.2.0

The following API elements have been removed:

the matplotlib.backends.tkagg, matplotlib.backends.windowing, matplotlib.backends.wx_compat, and matplotlib.compat.subprocess modules

I don't have library ping installed, and don't know how to install it. Tried to install lot of similar libraries, but all failed.
So I cannit run this demo script.

What version of matplotlib you installed ?

from pysimplegui.

PySimpleGUI avatar PySimpleGUI commented on May 28, 2024

I try to run a variety of versions of packages like matplotlib. Here's what I've got installed on various interpreters.

3.6 3.3.4
3.7 3.4.3
3.8  
3.9 3.6.2
3.10 3.8.2
3.11 3.8.0
3.12  
3.13  
Other 1 3.5.3
Other 2  

from pysimplegui.

jason990420 avatar jason990420 commented on May 28, 2024

Following statement work on your installed matplotlib ?

import matplotlib.backends.tkagg as tkagg

I tested it with python 3.9.13 and matplotlib 3.6.2, it failed.

d:\>python
Python 3.9.13 (tags/v3.9.13:6de2ca5, May 17 2022, 16:36:42) [MSC v.1929 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> from matplotlib import _get_version
>>> _get_version()
'3.6.2'
>>> import matplotlib.backends.tkagg as tkagg
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ModuleNotFoundError: No module named 'matplotlib.backends.tkagg'

from pysimplegui.

johnzbesko avatar johnzbesko commented on May 28, 2024

I tried something similar to this:
https://stackoverflow.com/questions/61847663/matplotlib-backends-backend-tkagg-is-throwing-an-attribute-error-for-blit
but using an older version created other problems. The "ping" module apparently doesn't exist anymore and I used ping3. Unfortunately, its return arguments are different from ping, and so the demo also doesn't work because of that.

from pysimplegui.

PySimpleGUI avatar PySimpleGUI commented on May 28, 2024

Oh! I knew ping went away, but didn't realize a demo depended on it. I'll get on that so we've got an example that runs. Thank you for mentioning it.

Our Matplotlib usage is likely inconsistent with some demos using a different technique. The Demo Browser can show which use a particular call.

I'll get on that fix. Thanks much!

from pysimplegui.

johnzbesko avatar johnzbesko commented on May 28, 2024

So, the original code is:

response = ping.quiet_ping('google.com', timeout=1000)
if response[0] == 0:
    ping_time = 1000
else:
    ping_time = response[0]

In ping3, there is no quiet_ping, so I tried just:
import ping3 as ping
response = ping..ping('google.com', timeout=1000)

However, response comes back as a single value, hence, response[0] generates an error. Changing all instances of response[0] to response gets us back to the original error:

module 'matplotlib.backends.backend_tkagg' has no attribute 'blit'

BTW, in the spyder IDE, the statement:
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, FigureCanvasAgg
has a little warning sign, "imported, but not used"

Same with line 124:
photo = draw(fig, canvas)

local variable 'photo' is assigned but never used.

When I comment out line 124 and run the program, the app window appears, but without the embedded plot. The "exit" button DOES work (and no error messages) and when pressed, the window disappears and a plot with data appears in the spyder console pane. This behavior is similar to what I got in my brewery application.

The other method for embedding a real-time plot seems to use a function called animate. If this "ping" program were adapted to a new method, I'm sure I could adapt my own program.

Thanks for your help.

from pysimplegui.

PySimpleGUI avatar PySimpleGUI commented on May 28, 2024

I changed the demo program to use the ping3 module.

https://github.com/PySimpleGUI/PySimpleGUI/blob/master/DemoPrograms/Demo_Matplotlib_Ping_Graph_Large.py

Additionally, I made these changes:

  • The ping is run as a thread
  • For the animated drawing using Matplotlib, I used the code that you'll find in the Spectrogram Animated Demo
  • An Image element is used in the layout

You'll need to make a few simple changes to your code to match. Note that the order of parameters changed in the draw function because it's copied from the other demo.

Here's the result.

ping.demo.mp4

from pysimplegui.

PySimpleGUI avatar PySimpleGUI commented on May 28, 2024

I've got a bit more cleanup to do. It's an extremely old Demo Program. My programming skills in Python was just starting at the time, it was one of the first Matplotlib Demo Programs and used an older technique.

from pysimplegui.

johnzbesko avatar johnzbesko commented on May 28, 2024

Thanks! I can confirm that this demo program runs both on my PC and Pi4. May I request a simple change- remove the threaded feature. I tried to adapt the current code to my application, but with multiple sensors and the heating power subroutine, both threading and plotting is too big a bite to chew. (I will keep the current code for a next step.)

from pysimplegui.

johnzbesko avatar johnzbesko commented on May 28, 2024

Well, I am finally able to adapt this demo to my app. The key change was adding:
[sg.Image( size=SIZE,key='-IMAGE-')]
to my window definition and then using:
image_element = window['-IMAGE-']
and finally:
draw(image_element,fig)

In the demo, the size of the window and the plot are given hard coded sizes. How would one create a window that could be expanded to the whole screen with the various elements sized proportionally? I develop the app usimg a PC monitor, but want to deploy the app on a 7" touchscreen.

from pysimplegui.

PySimpleGUI avatar PySimpleGUI commented on May 28, 2024

The Pi can be a little touchy about full-screen (maximized windows). You may want to try a no-titlebar window that's positioned at (0,0) and sized to fit your screen. You can add some code so that it only does this on your Pi and not other development machines like Windows.

from pysimplegui.

johnzbesko avatar johnzbesko commented on May 28, 2024

Thanks again for your help. After much trial and error with the window and widget display sizes, I have a working useful program.

Back to the thread issue. My app is linear- I use:
time.sleep(2)
to give some time for the flow sensor to count interrupts; I use time.sleep(seconds_on) to supply a measured amount of electric heat power as part of the thermostat circuit. I believe these sleep times interfere/make unresponsive the user interface. Am I right? Would any subroutine that uses time.sleep be best put into a thread? Hate to bother you- a link to a good reference would suffice.

from pysimplegui.

PySimpleGUI avatar PySimpleGUI commented on May 28, 2024

I believe these sleep times interfere/make unresponsive the user interface. Am I right?

You are indeed right.

The reason I put the call to "ping" into a thread was because it can take a long time, in some cases many seconds. It costs very little CPU to use a thread and it's not difficult to add a thread to a PySimpleGUI program and have it communicate with your GUI via an event that your thread generates as well as adding an entry to the values dictionary when that event is sent.

Would any subroutine that uses time.sleep be best put into a thread?

Again you are correct.

Hate to bother you- a link to a good reference would suffice.

You are most certainly not bothering anyone! We're here to help.

Main documentation section on threading

https://docs.pysimplegui.com/en/latest/documentation/module/multithreading/

Cookbook entry about "Long Operations"

https://docs.pysimplegui.com/en/latest/cookbook/ecookbook/advanced/multi-threaded/window-perform_long_operation-method/

If you search the documentation and search for "thread" (there's a search input in the upper right corner of our documentation) then you'll find more information about using threading with PySimpleGUI.

from pysimplegui.

johnzbesko avatar johnzbesko commented on May 28, 2024

The markdown did some funny things to my code.

from pysimplegui.

PySimpleGUI avatar PySimpleGUI commented on May 28, 2024

Hi @johnzbesko

I'm sympathetic to the frustrations of developing software. I know it all too well.

We're not set up to debug over 900 lines of user code. I'm not ignoring your post, I am simply unable to provide this level of consulting as part of supporting PySimpleGUI.

from pysimplegui.

PySimpleGUI avatar PySimpleGUI commented on May 28, 2024

Thank you so much for the "thanks"! image

I enjoy helping. Jason does too, clearly. It's really difficult to help someone with a huge amount of code though.

On multithreading versus asyncio, I'm definitely not a good one to answer that kind of question. I use multi-threading and multi-processing, but haven't done anything with Python's asyncio. I've not needed it I guess. I'm not a Python expert, that's certain.

I did notice that you've got an event loop with a timeout=0. There's a discussion in the documentation about the problems that can happen when you use a timeout value that's really small. The result is that unless you've got something else in the event loop that pends, you'll chew through 100% of the CPU time available to that program's CPU core. That can cause all kinds of troubles.

For projects that need to handle hardware on a periodic basis, say every 100ms, then a thread is ideal, especially if part of that processing can pend on another device, or use a lot of CPU time. This will starve your GUI from getting CPU time to run and can cause missed events, or the window to appear as if it's locked up.

There are a good number of Demo Programs and Cookbook (check out the interactive ones) that demonstrate using threading with PySimpleGUI. I built threading into PySimpleGUI itself so that it is simpler to understand and use. It's one less module to have to import and learn.

Integrating with hardware, matplotlib, realtime processing and networking is a lot for a program to do. Expect that kind of complexity to take time to design, develop, debug, rewrite, rework, debug, and lots of experimenting. It's rare to get it right the first time in my experience.

Thanks again for the thanks and for understanding that simpler = better when it comes to questions. As you know for your extensive experience, programming is largely a solitary activity. It's you, the keyboard, and the computer. It can feel overwhelming and frustrating. Be kind and patient with yourself.

from pysimplegui.

johnzbesko avatar johnzbesko commented on May 28, 2024

So, I tried experimenting with the timeout variable. It did not help with my multithreading version above. I was getting strange behavior with the thread that controlled the power. It was flickering, ie. like perhaps multiple heating threads were being started every the main() loop iterated. Is there a way to insure only one thread (of that particular routine) runs at a time, ie. a new thread can only be started when the previous one has ended?

Again, the objective here is to make the GUI window responsive even though the app is continuing to monitor temperatures and pulsing the heater circuit. The frequency of the plot redrawing seems to be determined by the time.sleep in the flow meter routine; the frequency of the heating circuit can be as long as 8 seconds.

Maybe this is a fool's task- that I'm dealing with a RPi4, not an AMD Ryzen?

from pysimplegui.

PySimpleGUI avatar PySimpleGUI commented on May 28, 2024

I keep this Gadget / Desktop Widget (whatever you want to call it) running all the time on my system and on Linux environments...
image

It's a Demo Program, and it's also part of the psggadgets repo:
https://github.com/PySimpleGUI/psggadgets/blob/main/psggadgets/gadget_CPU_Dashboard.py

Maybe try running it and seeing how things look. I'm guessing your core is maxed out. A timeout of 0 will do that. Set it to 100 instead so that your thread can run.

from pysimplegui.

PySimpleGUI avatar PySimpleGUI commented on May 28, 2024

My experience of programming has been mostly linear- starting with punched cards in the early '70's. ;-)

image

You and me both! I used punched cards all the way through college. Here's a picture of my dorm room desk

image

I was the only person on campus with a terminal in my room. I dialed into a computer that was at a larger university (using the acoustic coupler modem on top of the terminal).

PySimpleGUI has a linear architecture perhaps because of my background being similar to yours.

from pysimplegui.

PySimpleGUI avatar PySimpleGUI commented on May 28, 2024

Here's a radical idea for you that I'm betting will both simplify and speed up your code. Instead of using Matplotlib for your graphs, use PySimpleGUI. It's actually very easy once you get an example or two done. It's buffering the data to be displayed that can be a tiny bit tricky, but you likely already have to do that.

This older demo program draws a graph including axis that are labelled.

https://github.com/PySimpleGUI/PySimpleGUI/blob/master/DemoPrograms/Demo_Graph_Element_Sine_Wave.py

image

from pysimplegui.

johnzbesko avatar johnzbesko commented on May 28, 2024

I thought of an alternative way to frame the application. The GUI window and plot are structured as the ping demo, that is, the GUI waits for user input and the plot is updated everytime the flow rate calculation iterates (time.sleep(2)). Meanwhile, a different thread also reads the flowrate, the temperatures and has an input of the set thermostat temp. The thread then turns on/off the power as appropriate and does so in an infinite loop, unless an event from the GUI window stops this thread and restarts it with a new user input set thermostat temp.

Will both the plot thread and thermostat thread be able to read the temp sensors and do the flowrate calculation independently of each other (nearly) simultaneously? They're both reading off of the same GPIO pins.

from pysimplegui.

johnzbesko avatar johnzbesko commented on May 28, 2024

Answered my own question. Yes, both threads, window(with plot) and the thermostat, can read hardware GPIO inputs and act accordingly. An added bonus is that because the timing of the main loop is regular, ie. not waiting/interrupted by the thermostat process, the time axis in the plot actually displays the correct time.

from pysimplegui.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.