pwm_client 7.55 KB
#!/usr/bin/python3

"""
author    Benoit Dubois
copyright FEMTO ENGINEERING
license   GPL v3.0+
brief     Basic GUI to deal with "PWM controller" of STM32 board.
          Need to connect USB cable to CN5 to open a serial link with board
          (usually /dev/ttyACM0).
"""

# Ctrl-c closes the application
import signal
signal.signal(signal.SIGINT, signal.SIG_DFL)

import os
import sys
import struct
import argparse
import configparser
import tkinter as tk
from tkinter import messagebox
import serial

APPLICATION = "pwm_client"
BASE_CONFIG_DIR = os.path.expanduser("~") + "/.config/" + APPLICATION


PARAMETERS = (
    {'mnemo': "value", 'default': 0, 'min': -100, 'max': 100, 'format': None},
)

#==============================================================================
class ClientStmGui(tk.Frame):
    """Basic GUI to deal with "PWM handler" on STM32 board.
    """

    def __init__(self, master=None):
        """
        master: the parent window of the tk.Frame
        """
        super().__init__(master)
        self.cctrl = tk.IntVar()
        self._build_ui()

    def _build_ui(self):
        lbl = (dict(), dict())
        self.sbox = (dict(), dict())
        self.box_val = (dict(), dict())
        self.btn = (dict(), dict())
        for pwmh in range(2):
            for param in PARAMETERS:
                lbl[pwmh][param['mnemo']] = tk.Label(text=param['mnemo'])
                self.box_val[pwmh][param['mnemo']] = tk.IntVar()
                self.sbox[pwmh][param['mnemo']] = tk.Spinbox(from_=param['min'],
                            to=param['max'],
                            textvariable=self.box_val[pwmh][param['mnemo']],
                            format=param['format'])
                self.btn[pwmh][param['mnemo']] = tk.Button(text="Set")
        # Layout
        tk.Label(text='Controller 1').grid(row=0, column=1)
        tk.Label(text='Controller 2').grid(row=0, column=4)
        for pwmh in range(2):
            for idx, param in enumerate(PARAMETERS):
                lbl[pwmh][param['mnemo']].grid(row=idx+len(PARAMETERS)+1,
                                               column=pwmh*3+0)
                self.sbox[pwmh][param['mnemo']].grid(row=idx+len(PARAMETERS)+1,
                                                     column=pwmh*3+1)
                self.btn[pwmh][param['mnemo']].grid(row=idx+len(PARAMETERS)+1,
                                                    column=pwmh*3+2)
        # Global look
        self.config(bd=3, relief='groove')


#==============================================================================
class ClientPwm(object):

    def __init__(self, port, config_file, master=None):
        """
        port: serial port device file (e.g. /dev/ttyACMx)
        config_file: a configuration file used to initialise the client
        master: the parent window (see ClientStmGui())
        """
        self.stm = serial.Serial(port=port,
                                 baudrate=9600,
                                 timeout=1,
                                 xonxoff=False,
                                 rtscts=False,
                                 dsrdtr=True)
        self.stm.reset_input_buffer()
        self.stm.reset_output_buffer()
        self.ui = ClientStmGui(master)
        for pwmh in range(2):
            for param in PARAMETERS:
                self.ui.btn[pwmh][param['mnemo']].configure(
                    command=lambda x=pwmh, y=param['mnemo']:
                    self.btn_clicked(x, y))
        self.config_file = config_file
        self.config_to_device()
        self.config_to_ui()
        self.ui.mainloop()

    def btn_clicked(self, pwmh, mnemo):
        """Action when button is clicked
        """
        try:
            value = self.ui.box_val[pwmh][mnemo].get()
        except Exception as ex:
            print("Value error: {}".format(ex))
            return
        self.send_cmd(mnemo, pwmh, value)
        print("Send to STM:", mnemo, pwmh, value)
        retval = self.get_data()
        print("STM returns:", retval)
        self.save_setting('pwm_handler'+str(pwmh), mnemo, value)

    def config_to_device(self):
        """Get value of parameters from configuration file and
        set them to device.
        """
        for pwmh in range(2):
            config = self.get_pwmh_config(pwmh)
            for param in PARAMETERS:
                mnemo = param['mnemo']
                value = config[param['mnemo']]
                self.send_cmd(mnemo, pwmh, value)

    def config_to_ui(self):
        """Get value of parameters of the controller from configuration
        file and set them to UI.
        """
        for pwmh in range(2):
            config = self.get_pwmh_config(pwmh)
            for param in PARAMETERS:
                self.ui.box_val[pwmh][param['mnemo']]. \
                    set(config[param['mnemo']])

    def send_cmd(self, mnemo, pwmh, value):
        """High level data sending process
        """
        msg = "{} {} {}".format(mnemo, pwmh, value)
        header = struct.pack('<L', len(msg))
        self.stm.write(header + msg.encode('utf-8'))

    def get_data(self):
        try:
            data = self.stm.readline().decode('utf-8')
        except:
            print("Get data error: serial read error")
            return
        return data

    def get_pwmh_config(self, pwmh):
        configp = configparser.ConfigParser()
        configp.read(self.config_file)
        pwmh_cfg = dict()
        for param in PARAMETERS:
            pwmh_cfg[param['mnemo']] = configp.getint('pwm_handler'+str(pwmh),
                                                       param['mnemo'])
        return pwmh_cfg

    def save_setting(self, section, parameter, value):
        """Write a specific setting to the configuration file.
        """
        configp = configparser.ConfigParser()
        configp.read(self.config_file)
        configp.set(section, parameter, str(value))
        with open(self.config_file, 'w') as fd:
            configp.write(fd)


#==============================================================================
def reset_settings(settings_file):
    """Resets the "settings" file with default values.
    :param settings_file: file containing setting data value (str)
    :returns: None
    """
    config = configparser.ConfigParser()
    for pwmh in range(2):
        config.add_section('pwm_handler'+str(pwmh))
        for param in PARAMETERS:
            config.set('pwm_handler'+str(pwmh),
                       param['mnemo'],
                       str(param['default']))
    # Write modification to setting file
    with open(settings_file, 'w') as fd:
        fd.truncate(0) # Reset file contents
        config.write(fd)


#==============================================================================
def handle_args():
    parser = argparse.ArgumentParser(
        prog="client_stm",
        usage="%(prog)s [options]",
        description="Client for \"double PID\" on STM32",
    )
    parser.add_argument(
        "-p", "--port",
        dest="port",
        action="store",
        help="Set the port to connect to",
        default="/dev/ttyACM0"
    )
    return parser.parse_args(sys.argv[1:])


#==============================================================================
def main():
    args = handle_args()
    ini_file = BASE_CONFIG_DIR + '/' + APPLICATION + ".ini"
    if not os.path.isfile(ini_file):
        os.makedirs(BASE_CONFIG_DIR, exist_ok=True)
        reset_settings(ini_file)
    root = tk.Tk()
    root.resizable(0, 0)
    root.title("PWM handler client")
    app = ClientPwm(args.port, ini_file, root)


#==============================================================================
main()