From ad4bbf52e8abfd079b73206ad3ed6b3dfff68e0d Mon Sep 17 00:00:00 2001 From: Sudarsan Balakrishnan Date: Fri, 6 Feb 2026 14:17:59 -0500 Subject: [PATCH] First commit, working skeleton Tested to perform smoothly over a 'ssh -XYC' connection. Hard-coded /dev/ttyUSBn location, ideally it isn't changed too often for a given setup. --- CrateControl.py | 138 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 CrateControl.py diff --git a/CrateControl.py b/CrateControl.py new file mode 100644 index 0000000..84f0222 --- /dev/null +++ b/CrateControl.py @@ -0,0 +1,138 @@ +''' +v0.1 notes +--------- +Relies on /dev/ttyUSB channel being available to the user. Do it by running the command + +sudo adduser dialout + +and logging out, then logging back in. Look at the output of ls /dev/ttyUSB* on a terminal before +and after plugging in the USB cable to the PC, to find out which channel to use in the input below. + +''' + +import serial +import time +import tkinter as tk +import ttkbootstrap as ttk +from ttkbootstrap.constants import * + +port_name = '/dev/ttyUSB0' +baud_rate = 9600 +timeout = 2 + +commands = { + "turn on": "$CMD:SET,CH:8,PAR:ON\n".encode('utf-8'), + "turn off": "$CMD:SET,CH:8,PAR:OFF\n".encode('utf-8'), + "get name": "$CMD:MON,CH:8,PAR:CRNAME\n".encode('utf-8') +} + +class CrateControl(ttk.Frame): + def __init__(self,master): + super().__init__(master, padding=20) + try: + self.ser = serial.Serial(port=port_name,baudrate=baud_rate,timeout=timeout) + except serial.SerialException as e: + print(f"Error opening serial port: {e}") + self.destroy() + quit() + except Exception as e: + print(f"An unexpected error occurred: {e}") + self.destroy() + quit() + print(self.ser) + + self.freeze_frame=False + self.last_return_string="" + self.style = ttk.Style() + self.style.configure('big.success-round-toggle.TCheckbutton',font=('Verdana',18),indicatorsize=40,padding=10) + + self.name_of_crate = self.poll(8,"CRNAME","") + + self.onoff_status = tk.IntVar(value=0) + self.status_label = ttk.Label(self, text="Ready",bootstyle="info",justify=LEFT) + self.onoff_button = ttk.Checkbutton(self, + text=self.name_of_crate, + variable=self.onoff_status, + onvalue=1, + offvalue=0, + width=20, + #style='success-round-toggle.Checkbutton', + command=self.toggle_onoff, + bootstyle='big success-round-toggle') + self.status_label.pack(expand=YES,fill=X,pady=10) + self.onoff_button.pack(expand=YES,fill=X,pady=10) + self.refresh_monitors() + + #print('test') + #self.after(200,self.eventloop) + #self.refresh_monitors() + + def poll(self, channel, polstring,replystring=""): + #while(self.ser.in_waiting > 0): + # self.ser.readline() + self.ser.write(str("$CMD:MON,CH:"+str(channel)+",PAR:"+polstring+"\n").encode('utf-8')) + retval = self.ser.readline().decode("utf-8").rstrip() + #retval = self.read_reply() + retval = retval.replace("#CMD:OK,VAL:","") + return "%s\t%s\t"%(retval,replystring) + #return self.read_reply() + + def refresh_monitors(self): + #fullstatus="name: " + fullstatus="name\t\tset1\t\tmon1\t\tmon2\n" + for i in range(0,3): + reply="" + name = self.poll(i,"NAME") + setv = self.poll(i,"VSET","V") + monv = self.poll(i,"VMON","V") + moni = self.poll(i,"IMON","A") + #reply = name #.replace("#CMD:OK,VAL:"," ") + #reply = reply+"\n" + fullstatus += (name+setv+monv+moni+"\n") + fullstatus+="\t" + fullstatus+=self.poll(8,"FAN1","rpm\n") + fullstatus+=self.poll(8,"FAN2","rpm\n") + fullstatus+=self.poll(8,"FAN3","rpm\n") + fullstatus+=self.poll(8,"FUTEMP","C\n") + fullstatus+=self.poll(8,"PSTEMP","C\n") + status = self.poll(8,"CRST","\n") + fanstat = (int(status)&0x200==512) + crstat = int(status)&0x1 + fullstatus+=("fan: %d\t crate: %d"%(fanstat,crstat)) + if(crstat == 1 and fanstat == 1 and self.onoff_status.get() == 0): + self.onoff_status.set(1) + self.onoff_button.state(['selected']) + while(self.freeze_frame): + fullstatus = fullstatus + #print(fullstatus,self.freeze_frame) + self.status_label.config(text=fullstatus, bootstyle="info") + self.after(4000,self.refresh_monitors) + + def read_reply(self): + totresp = "" + while(self.ser.in_waiting > 0): + response = self.ser.readline().decode('utf-8').rstrip() #rstrip gets rid of trailing whitespace + totresp = totresp+response + self.last_return_string=totresp + return totresp + + def toggle_onoff(self): + self.freeze_frame=True + if(self.onoff_status.get()==1): + print("Turn ON") + self.ser.write(commands['turn on']) + time.sleep(0.2) + elif(self.onoff_status.get()==0): + print("Turn OFF") + self.ser.write(commands['turn off']) + time.sleep(0.2) + #print(self.read_reply()) + while(self.ser.in_waiting > 0): + self.ser.readline() + self.freeze_frame=False + +if __name__ == "__main__": + root = ttk.Window(themename='flatly',title="CrateControl") + app = CrateControl(root) #ttk.Window("Crate Control") + app.pack(expand=True,fill=BOTH) + root.mainloop()