├── Mod2Bac.py ├── Mod2Bac_pre-autopep8.py ├── README.md ├── bac.py ├── mod.py ├── mstp_client.py └── test.py /Mod2Bac.py: -------------------------------------------------------------------------------- 1 | 2 | import tkinter as tk 3 | from tkinter import * 4 | from tkinter import ttk 5 | from tkinter import messagebox 6 | import mod 7 | from mod import * 8 | from mod import ModScanner 9 | from bac import BAC0_Converter as bacon 10 | import test 11 | import re 12 | import threading 13 | from time import sleep 14 | 15 | # Main Application/GUI class 16 | 17 | points = 0 18 | point_values = {} 19 | present_values = {} 20 | av_list = [] 21 | mapped_list = [] 22 | connection_established = False 23 | tk_terminate = False 24 | 25 | 26 | class Application(tk.Frame): 27 | 28 | def __init__(self, master): 29 | super().__init__(master) 30 | self.master = master 31 | master.title('Mod2Bac') 32 | # Width height 33 | master.geometry("1200x600") 34 | # Analog Value Map Initializer 35 | self.av = 0 36 | # Binary Value Map Initializer 37 | self.bv = 0 38 | # Create widgets/grid 39 | self.create_dropdown_widgets() 40 | # Create entry widgets() 41 | self.create_entry_widgets() 42 | # Create button widgets() 43 | self.create_button_widgets() 44 | # Create listbox widgets() 45 | self.create_listbox_widgets() 46 | # Init selected item var 47 | self.selected_item = 0 48 | # Get Input Protocol dropdown selection 49 | self.change_input_dropdown() 50 | # Get Comm Port dropdown selection 51 | self.change_comm_dropdown() 52 | # Get Baud Rate dropdown selection 53 | self.change_baud_dropdown() 54 | # Fix row width 55 | self.rowconfigure(2, weight=10) 56 | 57 | def create_dropdown_widgets(self): 58 | # Set up Input dropdown 59 | self.input = StringVar(self.master) 60 | self.input_list = ('', 'rtu', 'mstp') 61 | self.input_choices = sorted(self.input_list) 62 | self.input.set('rtu') 63 | # Input Selection 64 | self.input_text = tk.StringVar() 65 | self.input_label = tk.Label( 66 | self.master, text='Input Protocol', font=('bold', 14), pady=20) 67 | self.input_label.grid(row=0, column=0, sticky=tk.E) 68 | self.input_menu = tk.ttk.OptionMenu( 69 | self.master, self.input, *self.input_choices) 70 | self.input_menu.grid(row=0, column=1, sticky=tk.W) 71 | # Set up Comm Port dropdown 72 | self.comm_port = StringVar(self.master) 73 | self.comm_port_list = ('', 'COM1', 'COM2', 'COM3', 'COM4', 74 | 'COM5', 'COM6', 'COM7', 'COM8', '/dev/ttyUSB0') 75 | self.comm_choices = sorted(self.comm_port_list) 76 | self.comm_port.set('/dev/ttyUSB0') 77 | # Comm Port 78 | self.comm_text = tk.StringVar() 79 | self.comm_label = tk.Label( 80 | self.master, text='COM Port', font=('bold', 14), pady=20) 81 | self.comm_label.grid(row=1, column=0, sticky=tk.E) 82 | self.comm_menu = tk.ttk.OptionMenu( 83 | self.master, self.comm_port, *self.comm_choices) 84 | self.comm_menu.grid(row=1, column=1, sticky=tk.W) 85 | # Set up Baud Rate dropdown 86 | self.baud_rate = IntVar(self.master) 87 | self.baud_rate_list = (0, 1200, 4800, 9600, 88 | 19200, 38400, 76800, 115200) 89 | self.baud_choices = sorted(self.baud_rate_list) 90 | self.baud_rate.set(9600) 91 | # Baud Rate 92 | self.baud_text = tk.StringVar() 93 | self.baud_label = tk.Label( 94 | self.master, text='Baud Rate', font=('bold', 14), pady=20) 95 | self.baud_label.grid(row=0, column=2, sticky=tk.E) 96 | self.baud_menu = tk.ttk.OptionMenu( 97 | self.master, self.baud_rate, *self.baud_choices) 98 | self.baud_menu.grid(row=0, column=3, sticky=tk.W) 99 | # Set up Parity dropdown 100 | self.parity = StringVar(self.master) 101 | self.parity_list = ('', 'N', 'E', 'O') 102 | self.parity_choices = sorted(self.parity_list) 103 | self.parity.set('N') 104 | # Parity 105 | self.parity_text = tk.StringVar() 106 | self.parity_label = tk.Label(self.master, text='Parity', font=( 107 | 'bold', 14), pady=20, width=6, padx=0) 108 | self.parity_label.grid(row=0, column=4, sticky=tk.E) 109 | self.parity_menu = tk.ttk.OptionMenu( 110 | self.master, self.parity, *self.parity_choices) 111 | self.parity_menu.grid(row=0, column=5, sticky=tk.W) 112 | # Set up Bytesize dropdown 113 | self.bytesize = IntVar(self.master) 114 | self.bytesize_list = (0, 8, 16, 32) 115 | self.bytesize_choices = sorted(self.bytesize_list) 116 | self.bytesize.set(8) 117 | # Bytesize 118 | self.bytesize_text = tk.StringVar() 119 | self.bytesize_label = tk.Label( 120 | self.master, text='Bytesize', font=('bold', 14), pady=20) 121 | self.bytesize_label.grid(row=0, column=6, sticky=tk.E) 122 | self.bytesize_menu = tk.ttk.OptionMenu( 123 | self.master, self.bytesize, *self.bytesize_choices) 124 | self.bytesize_menu.grid(row=0, column=7, sticky=tk.W) 125 | # Set up Output Interface dropdown 126 | self.iface = tk.StringVar() 127 | self.iface_list = test.ethernet_keys 128 | self.iface_choices = sorted(self.iface_list) 129 | self.iface.set(test.ethernet_keys[1]) 130 | # Output Interface 131 | self.iface_text = tk.StringVar() 132 | self.iface_label = tk.Label( 133 | self.master, text='Bacnet Iface', font=('bold', 14), pady=20) 134 | self.iface_label.grid(row=3, column=4, sticky=tk.E) 135 | self.iface_menu = tk.ttk.OptionMenu( 136 | self.master, self.iface, *self.iface_choices) 137 | self.iface_menu.grid(row=3, column=5, sticky=tk.W) 138 | # Set up Databits dropdown 139 | self.databits = IntVar(self.master) 140 | self.databits_list = (0, 1, 2, 3, 4, 5, 6, 7, 8) 141 | self.databits_choices = sorted(self.databits_list) 142 | self.databits.set(8) 143 | # Databits 144 | self.databits_text = tk.StringVar() 145 | self.databits_label = tk.Label( 146 | self.master, text='Data bits', font=('bold', 14), pady=20) 147 | self.databits_label.grid(row=1, column=2, sticky=tk.E) 148 | self.databits_menu = tk.ttk.OptionMenu( 149 | self.master, self.databits, *self.databits_choices) 150 | self.databits_menu.grid(row=1, column=3, sticky=tk.W) 151 | # Set up Stopbits dropdown 152 | self.stopbits = IntVar(self.master) 153 | self.stopbits_list = (0, 1, 2, 3, 4, 5, 6, 7, 8) 154 | self.stopbits_choices = sorted(self.stopbits_list) 155 | self.stopbits.set(1) 156 | # Stopbits 157 | self.stopbits_text = tk.StringVar() 158 | self.stopbits_label = tk.Label( 159 | self.master, text='Stop bits', font=('bold', 14), pady=20) 160 | self.stopbits_label.grid(row=1, column=4, sticky=tk.E) 161 | self.stopbits_menu = tk.ttk.OptionMenu( 162 | self.master, self.stopbits, *self.stopbits_choices) 163 | self.stopbits_menu.grid(row=1, column=5, sticky=tk.W) 164 | 165 | def create_entry_widgets(self): 166 | # Device ID 167 | self.device_text = tk.IntVar(self.master) 168 | self.device_label = tk.Label(self.master, text='Device ID', font=( 169 | 'bold', 14), pady=20, padx=4, width=8) 170 | self.device_label.grid(row=2, column=0, sticky=tk.E) 171 | self.device_entry = tk.Entry( 172 | self.master, textvariable=self.device_text) 173 | self.device_entry.insert(END, 1) 174 | self.device_entry.grid(row=2, column=1, sticky=tk.W) 175 | # Starting Register 176 | self.start_text = tk.IntVar(self.master) 177 | self.start_label = tk.Label(self.master, text='Starting', font=( 178 | 'bold', 14), pady=20, padx=4, width=8) 179 | self.start_label.grid(row=2, column=2, sticky=tk.E) 180 | self.start_entry = tk.Entry(self.master, textvariable=self.start_text) 181 | self.start_entry.insert(END, 0) 182 | self.start_entry.grid(row=2, column=3, sticky=tk.W) 183 | # Amount of registers 184 | self.amount_text = tk.IntVar(self.master) 185 | self.amount_label = tk.Label(self.master, text='Amount', font=( 186 | 'bold', 14), pady=20, padx=4, width=8) 187 | self.amount_label.grid(row=2, column=4, sticky=tk.E) 188 | self.amount_entry = tk.Entry( 189 | self.master, textvariable=self.amount_text) 190 | # self.amount_entry.insert(END,10) 191 | self.amount_entry.grid(row=2, column=5, sticky=tk.W) 192 | # Scan time 193 | self.time_text = tk.IntVar(self.master) 194 | self.time_label = tk.Label(self.master, text='Scantime(s)', font=( 195 | 'bold', 14), pady=20, padx=4, width=12) 196 | self.time_label.grid(row=2, column=6, sticky=tk.E) 197 | self.time_entry = tk.Entry(self.master, textvariable=self.time_text) 198 | self.time_entry.insert(END, .1) 199 | self.time_entry.grid(row=2, column=7, sticky=tk.W) 200 | # Output Instance 201 | self.instance_text = tk.StringVar() 202 | self.instance_label = tk.Label(self.master, text='Instance', font=( 203 | 'bold', 14), pady=20, padx=4, width=7) 204 | self.instance_label.grid(row=3, column=6, sticky=tk.E) 205 | self.instance_entry = tk.Entry( 206 | self.master, textvariable=self.instance_text) 207 | self.instance_entry.insert(END, "7200") 208 | self.instance_entry.grid(row=3, column=7, sticky=tk.W) 209 | 210 | def create_button_widgets(self): 211 | # Scan Button 212 | self.scan_button = tk.ttk.Button( 213 | self.master, text='Scan', width=6, command=self.scan) 214 | self.scan_button.grid(row=4, column=3, pady=30) 215 | # Map Analog Value Button 216 | self.map_analog_button = tk.ttk.Button( 217 | self.master, text='Map to Analog', width=12, command=self.map_analog) 218 | self.map_analog_button.grid(row=5, column=3, pady=25) 219 | # Map Binary Value Button 220 | self.map_binary_button = tk.ttk.Button( 221 | self.master, text='Map to Binary', width=12, command=self.map_binary) 222 | self.map_binary_button.grid(row=6, column=3, pady=25) 223 | 224 | def create_listbox_widgets(self): 225 | # Create listbox for input registers/points 226 | self.input_point_list = tk.Listbox( 227 | self.master, selectmode=MULTIPLE, exportselection=1, height=15, width=25, border=1) 228 | self.input_point_list.grid( 229 | row=4, column=0, columnspan=2, rowspan=5, pady=20, sticky=tk.E) 230 | 231 | # Create scrollbar for input listbox 232 | self.input_scrollbar = tk.Scrollbar(self.master) 233 | self.input_scrollbar.grid( 234 | row=4, column=2, rowspan=5, pady=20, sticky=N+S+W) 235 | 236 | # Attach scrollbar to input listbox 237 | self.input_point_list.config(yscrollcommand=self.input_scrollbar.set) 238 | self.input_scrollbar.config(command=self.input_point_list.yview) 239 | 240 | # Create listbox for output points 241 | self.output_point_list = tk.Listbox( 242 | self.master, selectmode=MULTIPLE, exportselection=1, height=15, width=25, border=1) 243 | self.output_point_list.grid( 244 | row=4, column=5, columnspan=2, rowspan=5, pady=20, sticky=tk.E) 245 | 246 | # Create scrollbar for output listbox 247 | self.output_scrollbar = tk.Scrollbar(self.master) 248 | self.output_scrollbar.grid( 249 | row=4, column=7, rowspan=5, pady=20, sticky=N+S+W) 250 | 251 | # Attach scrollbar to output listbox 252 | self.output_point_list.config(yscrollcommand=self.output_scrollbar.set) 253 | self.output_scrollbar.config(command=self.output_point_list.yview) 254 | 255 | def scan(self): 256 | global points 257 | global point_values 258 | global connection_established 259 | self.start = self.start_text.get() 260 | self.amount = self.amount_text.get() 261 | self.scan_time = self.time_text.get() 262 | self.device = self.device_text.get() 263 | if self.register.get() == 'Coil Status 0x': 264 | self.mode = 1 265 | elif self.register.get() == 'Input Status 1x': 266 | self.mode = 2 267 | elif self.register.get() == 'Input Register 3x': 268 | self.mode = 3 269 | elif self.register.get() == 'Holding Register 4x': 270 | self.mode = 4 271 | # Set up Bacnet connection 272 | print(test.ethernet_address[self.iface.get()]) 273 | self.kevin = bacon(str(test.ethernet_address[self.iface.get( 274 | )]), self.instance_text.get(), 'Raspberry Pi3') 275 | attempt_connection = True 276 | initial_port = 47809 277 | while attempt_connection == True: 278 | try: 279 | self.kevin.start_device(initial_port) 280 | attempt_connection = False 281 | except: 282 | initial_port += 1 283 | sleep(1) 284 | 285 | if self.input.get() != self.protocol: 286 | self.input_point_list.delete(0, tk.END) 287 | points = 0 288 | self.protocol = self.input.get() 289 | # Create a new Modbus Scanner using pymodbus 290 | self.scanner = ModScanner( 291 | self.register.get(), 292 | self.input.get(), 293 | self.comm_port.get(), 294 | self.baud_rate.get(), 295 | self.stopbits.get(), 296 | self.databits.get(), 297 | self.parity.get(), 298 | 1 299 | ) 300 | # Connect to the desired device 301 | self.connection = self.scanner.connect() 302 | if self.connection: 303 | print("Establishing connection...") 304 | connection_established = True 305 | sleep(1) 306 | self.device_list = self.device_entry.get().split(',') 307 | for device in self.device_list: 308 | print(f"device is {device}") 309 | self.scanner.scan(self.start, self.amount, self.mode, self.device) 310 | self.total_point_list = mod.point_list 311 | print(f"point list is {self.total_point_list}") 312 | for point in range(self.amount): 313 | self.input_point_list.insert( 314 | END, "device{} point{}".format(device, point)) 315 | try: 316 | point_values[str(point)] = self.total_point_list[point] 317 | except IndexError: 318 | print("No Device connected") 319 | pass 320 | threading.Thread(target=self.scan_threader).start() 321 | 322 | def scan_threader(self): 323 | global connection_established 324 | global av_list 325 | global present_values 326 | global point_values 327 | global tk_terminate 328 | while connection_established == True and tk_terminate == False: 329 | self.total_point_list = mod.point_list 330 | for point in range(self.amount): 331 | try: 332 | point_values[str(point)] = self.total_point_list[point] 333 | except IndexError: 334 | print("No Device connected") 335 | pass 336 | t = threading.Thread(target=self.scanner.scan, args=( 337 | self.start, self.amount, self.mode, self.device)) 338 | t.start() 339 | t.join() 340 | for i in av_list: 341 | present_values[str(i)] = point_values[str(i)] 342 | self.kevin.update_analogs( 343 | f"register{i}", present_values[str(i)]) 344 | sleep(self.scan_time) 345 | if tk_terminate: 346 | connection_established = False 347 | break 348 | 349 | def map_analog(self): 350 | global point_values 351 | global mapped_list 352 | global av_list 353 | self.analog_point_list = self.input_point_list.curselection() 354 | device_name = "" 355 | point = "" 356 | print(point_values) 357 | for k in point_values.keys(): 358 | mapped_list.append(k) 359 | for i in self.analog_point_list: 360 | #self.output_point_list.insert(END,"Mapping point{} to AV_{}".format(point_values[str(i)],self.av)) 361 | self.output_point_list.insert( 362 | END, "Mapping point{} to AV_{}".format(mapped_list[i], self.av)) 363 | av_list.append(mapped_list[i]) 364 | device_name = "{}".format(self.total_point_list[i]) 365 | point = "register{}".format(mapped_list[i]) 366 | self.av += 1 367 | print(point) 368 | self.kevin.build_analog(device_name, point) 369 | 370 | def map_binary(self): 371 | pass 372 | 373 | def select(self): 374 | pass 375 | 376 | def change_input_dropdown(self, *args): 377 | self.input.get() 378 | self.protocol = self.input.get() 379 | if self.protocol == 'rtu': 380 | # Set up Register dropdown 381 | self.register = tk.StringVar() 382 | self.register_list = ( 383 | '', 'Coil Status 0x', 'Input Status 1x', 'Input Register 3x', 'Holding Register 4x') 384 | self.register_choices = sorted(self.register_list) 385 | self.register.set('Holding Register 4x') 386 | # Register dropdown 387 | self.register_text = tk.StringVar() 388 | self.register_label = tk.Label( 389 | self.master, text='Input Scan', font=('bold', 14), pady=20) 390 | self.register_label.grid(row=1, column=6, sticky=tk.E) 391 | self.register_menu = tk.ttk.OptionMenu( 392 | self.master, self.register, *self.register_choices) 393 | self.register_menu.grid(row=1, column=7, sticky=tk.W) 394 | 395 | def change_comm_dropdown(self, *args): 396 | self.comm_port.get() 397 | 398 | def change_baud_dropdown(self, *args): 399 | self.baud_rate.get() 400 | 401 | # Handle window closure by terminating all threads 402 | 403 | 404 | def _delete_window(): 405 | global tk_terminate 406 | print("deleting frame") 407 | tk_terminate = True 408 | root.destroy() 409 | 410 | 411 | def _destroy(): 412 | print("destroy") 413 | 414 | 415 | root = tk.Tk() 416 | app = Application(master=root) 417 | # bind function for thread termination to Frame closure 418 | root.protocol("WM_DELETE_WINDOW", _delete_window) 419 | root.bind("", _destroy) 420 | app.mainloop() 421 | -------------------------------------------------------------------------------- /Mod2Bac_pre-autopep8.py: -------------------------------------------------------------------------------- 1 | 2 | import tkinter as tk 3 | from tkinter import * 4 | from tkinter import ttk 5 | from tkinter import messagebox 6 | import mod 7 | from mod import * 8 | from mod import ModScanner 9 | from bac import BAC0_Converter as bacon 10 | import test 11 | import re 12 | import threading 13 | from time import sleep 14 | 15 | # Main Application/GUI class 16 | 17 | points = 0 18 | point_values = {} 19 | present_values = {} 20 | av_list = [] 21 | mapped_list = [] 22 | connection_established = False 23 | tk_terminate = False 24 | 25 | class Application(tk.Frame): 26 | 27 | def __init__(self, master): 28 | super().__init__(master) 29 | self.master = master 30 | master.title('Mod2Bac') 31 | # Width height 32 | master.geometry("1200x600") 33 | # Analog Value Map Initializer 34 | self.av = 0 35 | # Binary Value Map Initializer 36 | self.bv = 0 37 | # Create widgets/grid 38 | self.create_dropdown_widgets() 39 | # Create entry widgets() 40 | self.create_entry_widgets() 41 | # Create button widgets() 42 | self.create_button_widgets() 43 | # Create listbox widgets() 44 | self.create_listbox_widgets() 45 | # Init selected item var 46 | self.selected_item = 0 47 | # Get Input Protocol dropdown selection 48 | self.change_input_dropdown() 49 | # Get Comm Port dropdown selection 50 | self.change_comm_dropdown() 51 | # Get Baud Rate dropdown selection 52 | self.change_baud_dropdown() 53 | # Fix row width 54 | self.rowconfigure(2,weight=10) 55 | 56 | def create_dropdown_widgets(self): 57 | # Set up Input dropdown 58 | self.input = StringVar(self.master) 59 | self.input_list = ('','rtu','mstp') 60 | self.input_choices = sorted(self.input_list) 61 | self.input.set('rtu') 62 | # Input Selection 63 | self.input_text = tk.StringVar() 64 | self.input_label = tk.Label(self.master, text='Input Protocol', font=('bold', 14), pady=20) 65 | self.input_label.grid(row=0, column=0,sticky=tk.E) 66 | self.input_menu = tk.ttk.OptionMenu(self.master,self.input,*self.input_choices) 67 | self.input_menu.grid(row=0,column=1,sticky=tk.W) 68 | # Set up Comm Port dropdown 69 | self.comm_port = StringVar(self.master) 70 | self.comm_port_list = ('','COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7','COM8','/dev/ttyUSB0') 71 | self.comm_choices = sorted(self.comm_port_list) 72 | self.comm_port.set('/dev/ttyUSB0') 73 | # Comm Port 74 | self.comm_text = tk.StringVar() 75 | self.comm_label = tk.Label(self.master, text='COM Port', font=('bold', 14), pady=20) 76 | self.comm_label.grid(row=1, column=0, sticky=tk.E) 77 | self.comm_menu = tk.ttk.OptionMenu(self.master,self.comm_port,*self.comm_choices) 78 | self.comm_menu.grid(row=1,column=1,sticky=tk.W) 79 | # Set up Baud Rate dropdown 80 | self.baud_rate = IntVar(self.master) 81 | self.baud_rate_list = (0,1200, 4800, 9600, 19200, 38400, 76800, 115200) 82 | self.baud_choices = sorted(self.baud_rate_list) 83 | self.baud_rate.set(9600) 84 | # Baud Rate 85 | self.baud_text = tk.StringVar() 86 | self.baud_label = tk.Label(self.master, text='Baud Rate', font=('bold', 14), pady=20) 87 | self.baud_label.grid(row=0, column=2, sticky=tk.E) 88 | self.baud_menu = tk.ttk.OptionMenu(self.master,self.baud_rate,*self.baud_choices) 89 | self.baud_menu.grid(row=0,column=3,sticky=tk.W) 90 | # Set up Parity dropdown 91 | self.parity = StringVar(self.master) 92 | self.parity_list = ('','N','E','O') 93 | self.parity_choices = sorted(self.parity_list) 94 | self.parity.set('N') 95 | # Parity 96 | self.parity_text = tk.StringVar() 97 | self.parity_label = tk.Label(self.master, text='Parity', font=('bold', 14), pady=20,width=6,padx=0) 98 | self.parity_label.grid(row=0, column=4, sticky=tk.E) 99 | self.parity_menu = tk.ttk.OptionMenu(self.master,self.parity,*self.parity_choices) 100 | self.parity_menu.grid(row=0,column=5, sticky=tk.W) 101 | # Set up Bytesize dropdown 102 | self.bytesize = IntVar(self.master) 103 | self.bytesize_list = (0,8,16,32) 104 | self.bytesize_choices = sorted(self.bytesize_list) 105 | self.bytesize.set(8) 106 | # Bytesize 107 | self.bytesize_text = tk.StringVar() 108 | self.bytesize_label = tk.Label(self.master, text='Bytesize', font=('bold', 14), pady=20) 109 | self.bytesize_label.grid(row=0, column=6, sticky=tk.E) 110 | self.bytesize_menu = tk.ttk.OptionMenu(self.master,self.bytesize,*self.bytesize_choices) 111 | self.bytesize_menu.grid(row=0,column=7, sticky=tk.W) 112 | # Set up Output Interface dropdown 113 | self.iface = tk.StringVar() 114 | self.iface_list = test.ethernet_keys 115 | self.iface_choices = sorted(self.iface_list) 116 | self.iface.set(test.ethernet_keys[1]) 117 | # Output Interface 118 | self.iface_text = tk.StringVar() 119 | self.iface_label = tk.Label(self.master, text='Bacnet Iface', font=('bold', 14), pady=20) 120 | self.iface_label.grid(row=3, column=4, sticky=tk.E) 121 | self.iface_menu = tk.ttk.OptionMenu(self.master,self.iface,*self.iface_choices) 122 | self.iface_menu.grid(row=3,column=5, sticky=tk.W) 123 | # Set up Databits dropdown 124 | self.databits = IntVar(self.master) 125 | self.databits_list = (0,1,2,3,4,5,6,7,8) 126 | self.databits_choices = sorted(self.databits_list) 127 | self.databits.set(8) 128 | # Databits 129 | self.databits_text = tk.StringVar() 130 | self.databits_label = tk.Label(self.master, text='Data bits', font=('bold', 14), pady=20) 131 | self.databits_label.grid(row=1, column=2, sticky=tk.E) 132 | self.databits_menu = tk.ttk.OptionMenu(self.master,self.databits,*self.databits_choices) 133 | self.databits_menu.grid(row=1,column=3, sticky=tk.W) 134 | # Set up Stopbits dropdown 135 | self.stopbits = IntVar(self.master) 136 | self.stopbits_list = (0,1,2,3,4,5,6,7,8) 137 | self.stopbits_choices = sorted(self.stopbits_list) 138 | self.stopbits.set(1) 139 | # Stopbits 140 | self.stopbits_text = tk.StringVar() 141 | self.stopbits_label = tk.Label(self.master, text='Stop bits', font=('bold', 14), pady=20) 142 | self.stopbits_label.grid(row=1, column=4, sticky=tk.E) 143 | self.stopbits_menu = tk.ttk.OptionMenu(self.master,self.stopbits,*self.stopbits_choices) 144 | self.stopbits_menu.grid(row=1,column=5, sticky=tk.W) 145 | 146 | def create_entry_widgets(self): 147 | # Device ID 148 | self.device_text = tk.IntVar(self.master) 149 | self.device_label = tk.Label(self.master, text='Device ID', font=('bold', 14), pady=20, padx=4, width=8) 150 | self.device_label.grid(row=2, column=0,sticky=tk.E) 151 | self.device_entry = tk.Entry(self.master, textvariable=self.device_text) 152 | self.device_entry.insert(END,1) 153 | self.device_entry.grid(row=2, column=1,sticky=tk.W) 154 | # Starting Register 155 | self.start_text = tk.IntVar(self.master) 156 | self.start_label = tk.Label(self.master, text='Starting', font=('bold', 14), pady=20, padx=4, width=8) 157 | self.start_label.grid(row=2, column=2,sticky=tk.E) 158 | self.start_entry = tk.Entry(self.master, textvariable=self.start_text) 159 | self.start_entry.insert(END,0) 160 | self.start_entry.grid(row=2, column=3,sticky=tk.W) 161 | # Amount of registers 162 | self.amount_text = tk.IntVar(self.master) 163 | self.amount_label = tk.Label(self.master, text='Amount', font=('bold', 14), pady=20, padx=4, width=8) 164 | self.amount_label.grid(row=2, column=4,sticky=tk.E) 165 | self.amount_entry = tk.Entry(self.master, textvariable=self.amount_text) 166 | # self.amount_entry.insert(END,10) 167 | self.amount_entry.grid(row=2, column=5,sticky=tk.W) 168 | # Scan time 169 | self.time_text = tk.IntVar(self.master) 170 | self.time_label = tk.Label(self.master, text='Scantime(s)', font=('bold', 14), pady=20, padx=4, width=12) 171 | self.time_label.grid(row=2, column=6,sticky=tk.E) 172 | self.time_entry = tk.Entry(self.master, textvariable=self.time_text) 173 | self.time_entry.insert(END,.1) 174 | self.time_entry.grid(row=2, column=7,sticky=tk.W) 175 | # Output Instance 176 | self.instance_text = tk.StringVar() 177 | self.instance_label = tk.Label(self.master, text='Instance', font=('bold', 14), pady=20, padx=4, width=7) 178 | self.instance_label.grid(row=3, column=6, sticky=tk.E) 179 | self.instance_entry = tk.Entry(self.master, textvariable=self.instance_text) 180 | self.instance_entry.insert(END,"7200") 181 | self.instance_entry.grid(row=3, column=7,sticky=tk.W) 182 | 183 | def create_button_widgets(self): 184 | # Scan Button 185 | self.scan_button = tk.ttk.Button(self.master,text='Scan',width=6,command=self.scan) 186 | self.scan_button.grid(row=4,column=3,pady=30) 187 | # Map Analog Value Button 188 | self.map_analog_button = tk.ttk.Button(self.master,text='Map to Analog',width=12,command=self.map_analog) 189 | self.map_analog_button.grid(row=5,column=3,pady=25) 190 | # Map Binary Value Button 191 | self.map_binary_button = tk.ttk.Button(self.master,text='Map to Binary',width=12,command=self.map_binary) 192 | self.map_binary_button.grid(row=6,column=3,pady=25) 193 | 194 | def create_listbox_widgets(self): 195 | # Create listbox for input registers/points 196 | self.input_point_list = tk.Listbox(self.master, selectmode=MULTIPLE, exportselection=1, height=15, width=25, border=1) 197 | self.input_point_list.grid(row=4, column=0, columnspan=2,rowspan=5, pady=20,sticky=tk.E) 198 | 199 | # Create scrollbar for input listbox 200 | self.input_scrollbar = tk.Scrollbar(self.master) 201 | self.input_scrollbar.grid(row=4, column=2, rowspan=5,pady=20,sticky=N+S+W) 202 | 203 | # Attach scrollbar to input listbox 204 | self.input_point_list.config(yscrollcommand=self.input_scrollbar.set) 205 | self.input_scrollbar.config(command=self.input_point_list.yview) 206 | 207 | # Create listbox for output points 208 | self.output_point_list = tk.Listbox(self.master, selectmode=MULTIPLE, exportselection=1,height=15, width=25, border=1) 209 | self.output_point_list.grid(row=4, column=5, columnspan=2,rowspan=5, pady=20,sticky=tk.E) 210 | 211 | # Create scrollbar for output listbox 212 | self.output_scrollbar = tk.Scrollbar(self.master) 213 | self.output_scrollbar.grid(row=4, column=7, rowspan=5,pady=20,sticky=N+S+W) 214 | 215 | # Attach scrollbar to output listbox 216 | self.output_point_list.config(yscrollcommand=self.output_scrollbar.set) 217 | self.output_scrollbar.config(command=self.output_point_list.yview) 218 | 219 | 220 | def scan(self): 221 | global points 222 | global point_values 223 | global connection_established 224 | self.start = self.start_text.get() 225 | self.amount = self.amount_text.get() 226 | self.scan_time = self.time_text.get() 227 | self.device = self.device_text.get() 228 | if self.register.get() == 'Coil Status 0x': 229 | self.mode = 1 230 | elif self.register.get() == 'Input Status 1x': 231 | self.mode = 2 232 | elif self.register.get() == 'Input Register 3x': 233 | self.mode = 3 234 | elif self.register.get() == 'Holding Register 4x': 235 | self.mode = 4 236 | # Set up Bacnet connection 237 | print(test.ethernet_address[self.iface.get()]) 238 | self.kevin = bacon(str(test.ethernet_address[self.iface.get()]),self.instance_text.get(),'Raspberry Pi3') 239 | attempt_connection = True 240 | initial_port = 47809 241 | while attempt_connection == True: 242 | try: 243 | self.kevin.start_device(initial_port) 244 | attempt_connection = False 245 | except: 246 | initial_port += 1 247 | sleep(1) 248 | 249 | if self.input.get() != self.protocol: 250 | self.input_point_list.delete(0,tk.END) 251 | points = 0 252 | self.protocol = self.input.get() 253 | # Create a new Modbus Scanner using pymodbus 254 | self.scanner = ModScanner( 255 | self.register.get(), 256 | self.input.get(), 257 | self.comm_port.get(), 258 | self.baud_rate.get(), 259 | self.stopbits.get(), 260 | self.databits.get(), 261 | self.parity.get(), 262 | 1 263 | ) 264 | # Connect to the desired device 265 | self.connection = self.scanner.connect() 266 | if self.connection: 267 | print("Establishing connection...") 268 | connection_established = True 269 | sleep(1) 270 | self.device_list = self.device_entry.get().split(',') 271 | for device in self.device_list: 272 | print(f"device is {device}") 273 | self.scanner.scan(self.start,self.amount,self.mode,self.device) 274 | self.total_point_list = mod.point_list 275 | print(f"point list is {self.total_point_list}") 276 | for point in range(self.amount): 277 | self.input_point_list.insert(END,"device{} point{}".format(device,point)) 278 | try: 279 | point_values[str(point)]=self.total_point_list[point] 280 | except IndexError: 281 | print("No Device connected") 282 | pass 283 | threading.Thread(target=self.scan_threader).start() 284 | 285 | def scan_threader(self): 286 | global connection_established 287 | global av_list 288 | global present_values 289 | global point_values 290 | global tk_terminate 291 | while connection_established == True and tk_terminate == False: 292 | self.total_point_list = mod.point_list 293 | for point in range(self.amount): 294 | try: 295 | point_values[str(point)]=self.total_point_list[point] 296 | except IndexError: 297 | print("No Device connected") 298 | pass 299 | t = threading.Thread(target=self.scanner.scan,args=(self.start,self.amount,self.mode,self.device)) 300 | t.start() 301 | t.join() 302 | for i in av_list: 303 | present_values[str(i)]=point_values[str(i)] 304 | self.kevin.update_analogs(f"register{i}",present_values[str(i)]) 305 | sleep(self.scan_time) 306 | if tk_terminate: 307 | connection_established = False 308 | break 309 | 310 | def map_analog(self): 311 | global point_values 312 | global mapped_list 313 | global av_list 314 | self.analog_point_list = self.input_point_list.curselection() 315 | device_name = "" 316 | point = "" 317 | print(point_values) 318 | for k in point_values.keys(): 319 | mapped_list.append(k) 320 | for i in self.analog_point_list: 321 | #self.output_point_list.insert(END,"Mapping point{} to AV_{}".format(point_values[str(i)],self.av)) 322 | self.output_point_list.insert(END,"Mapping point{} to AV_{}".format(mapped_list[i],self.av)) 323 | av_list.append(mapped_list[i]) 324 | device_name = "{}".format(self.total_point_list[i]) 325 | point = "register{}".format(mapped_list[i]) 326 | self.av += 1 327 | print(point) 328 | self.kevin.build_analog(device_name,point) 329 | 330 | def map_binary(self): 331 | pass 332 | 333 | def select(self): 334 | pass 335 | 336 | def change_input_dropdown(self,*args): 337 | self.input.get() 338 | self.protocol = self.input.get() 339 | if self.protocol == 'rtu': 340 | # Set up Register dropdown 341 | self.register = tk.StringVar() 342 | self.register_list = ('','Coil Status 0x', 'Input Status 1x', 'Input Register 3x', 'Holding Register 4x') 343 | self.register_choices = sorted(self.register_list) 344 | self.register.set('Holding Register 4x') 345 | # Register dropdown 346 | self.register_text = tk.StringVar() 347 | self.register_label = tk.Label(self.master, text='Input Scan', font=('bold', 14), pady=20) 348 | self.register_label.grid(row=1, column=6, sticky=tk.E) 349 | self.register_menu = tk.ttk.OptionMenu(self.master,self.register,*self.register_choices) 350 | self.register_menu.grid(row=1,column=7, sticky=tk.W) 351 | 352 | 353 | def change_comm_dropdown(self,*args): 354 | self.comm_port.get() 355 | 356 | def change_baud_dropdown(self,*args): 357 | self.baud_rate.get() 358 | 359 | # Handle window closure by terminating all threads 360 | def _delete_window(): 361 | global tk_terminate 362 | print("deleting frame") 363 | tk_terminate = True 364 | root.destroy() 365 | 366 | def _destroy(): 367 | print("destroy") 368 | 369 | root = tk.Tk() 370 | app = Application(master=root) 371 | # bind function for thread termination to Frame closure 372 | root.protocol("WM_DELETE_WINDOW",_delete_window) 373 | root.bind("",_destroy) 374 | app.mainloop() -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mod2Bac 2 | A Python3 Tkinter GUI application on Raspberry Pi for converting Modbus RTU to Bacnet/IP 3 | 4 | How-To: https://youtu.be/chHRp8q5mbU 5 | 6 | Utilizes pymodbus by Sanjay and BAC0 by Christian Tremblay 7 | 8 | 3-23-2020 Tested on Modbus RTU holding registers to Bacnet IP only. 9 | -------------------------------------------------------------------------------- /bac.py: -------------------------------------------------------------------------------- 1 | import BAC0 2 | from BAC0.core.utils.notes import note_and_log 3 | 4 | from bacpypes.local.object import (AnalogValueCmdObject,BinaryValueCmdObject) 5 | from bacpypes.object import register_object_type 6 | from bacpypes.basetypes import EngineeringUnits 7 | from bacpypes.primitivedata import CharacterString 8 | 9 | import time 10 | 11 | objects = [] 12 | 13 | class BAC0_Converter(): 14 | 15 | analog_value_num = 0 16 | analog_value_list = [] 17 | binary_value_num = 0 18 | binary_value_list = [] 19 | 20 | def __init__(self,ip,instance_number,obj_name): 21 | self.ip = ip 22 | self.instance_number = instance_number 23 | self.obj_name = obj_name 24 | 25 | def start_device(self,init_port): 26 | self.device = BAC0.lite( 27 | ip=self.ip, 28 | deviceId=self.instance_number, 29 | localObjName=self.obj_name, 30 | port=init_port 31 | ) 32 | 33 | def build_analog(self,input_device,input_dev_point): 34 | global objects 35 | self.input_device = input_device 36 | self.input_dev_point = input_dev_point 37 | register_object_type(AnalogValueCmdObject, vendor_id=842) 38 | av_object = AnalogValueCmdObject( 39 | objectIdentifier=("analogValue",BAC0_Converter.analog_value_num), 40 | objectName=self.input_dev_point, 41 | presentValue=666, 42 | description=CharacterString(f"imported from {self.input_device}: {self.input_dev_point} ")) 43 | BAC0_Converter.analog_value_num += 1 44 | BAC0_Converter.analog_value_list.append(av_object) 45 | self.device.this_application.add_object(av_object) 46 | return av_object 47 | 48 | def update_analogs(self,name,value): 49 | global objects 50 | av = self.device.this_application.get_object_name(name) 51 | if av: 52 | av.presentValue = value 53 | print(f"AV present Value: {av.presentValue}") 54 | 55 | 56 | def build_binary(self,input_device,input_dev_point): 57 | self.input_device = input_device 58 | self.input_dev_point = input_dev_point 59 | register_object_type(BinaryValueCmdObject, vendor_id=842) 60 | bv_object = BinaryValueCmdObject( 61 | objectIdentifier=("binaryValue",BAC0_Converter.binary_value_num), 62 | objectName=self.input_dev_point, 63 | presentValue='inactive', 64 | description=CharacterString(f"imported from {self.input_device}: {self.input_dev_point} ")) 65 | BAC0_Converter.binary_value_num += 1 66 | BAC0_Converter.binary_value_list.append(bv_object) 67 | self.device.this_application.add_object(bv_object) 68 | return bv_object 69 | 70 | def start(): 71 | Toast = BAC0_Converter('192.168.1.151/24',7002,'Pi') 72 | Toast.start_device() 73 | Toast.build_analog('modbus_dev_1','register_1') 74 | Toast.build_analog('modbus_dev_1','register_2') 75 | Toast.build_binary('modbus_dev_1','register_3') 76 | Toast.build_binary('modbus_dev_1','register_4') 77 | for i in Toast.analog_value_list: 78 | print(i) 79 | for i in Toast.binary_value_list: 80 | print(i) 81 | while True: 82 | time.sleep(10) 83 | 84 | if __name__=='__main__': 85 | start() 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /mod.py: -------------------------------------------------------------------------------- 1 | from pymodbus.client.sync import ModbusSerialClient as ModbusClient 2 | from pymodbus.register_read_message import * 3 | from time import sleep 4 | import threading 5 | 6 | point_list = [] 7 | 8 | class ModScanner: 9 | def __init__(self,function=4,protocol_input='rtu',comm_port='/dev/ttyUSB0',baudrate=9600,stop_bits=1,bytesize=8,parity='N',timeout=1): 10 | self.protocol_input = protocol_input 11 | self.comm_port = comm_port 12 | self.baud_rate = baudrate 13 | self.stop_bits = stop_bits 14 | self.bytesize = bytesize 15 | self.parity = parity 16 | self.function = function 17 | self.timeout = timeout 18 | 19 | def __repr__(self): 20 | return "Connecting via comm_port {} at a baudrate of {}".format(self.comm_port,self.baudrate) 21 | 22 | def connect(self): 23 | print(self.protocol_input) 24 | self.client = ModbusClient( 25 | method=self.protocol_input, 26 | port=self.comm_port, 27 | stopbits=self.stop_bits, 28 | bytesize=self.bytesize, 29 | parity=self.parity, 30 | baudrate=self.baud_rate, 31 | timeout=self.timeout 32 | ) 33 | self.connection = self.client.connect() 34 | return self.connection 35 | 36 | def scan(self,start,amount,mode,device): 37 | global point_list 38 | if mode == 1: 39 | try: 40 | value = self.client.read_coil_status(start,amount,unit=device) 41 | print(value.status) 42 | point_list = value.status 43 | except AttributeError: 44 | print("device is down") 45 | elif mode == 2: 46 | try: 47 | value = self.client.read_input_status(start,amount,unit=device) 48 | print(value.status) 49 | point_list = value.status 50 | except AttributeError: 51 | print("device is down") 52 | elif mode == 3: 53 | try: 54 | value = self.client.read_input_registers(start,amount,unit=device) 55 | print(value.registers) 56 | point_list = value.registers 57 | except AttributeError: 58 | print("device is down") 59 | elif mode == 4: 60 | try: 61 | value = self.client.read_holding_registers(start,amount,unit=device) 62 | point_list = value.registers 63 | print(point_list) 64 | except AttributeError: 65 | print("device is down") -------------------------------------------------------------------------------- /mstp_client.py: -------------------------------------------------------------------------------- 1 | mstp_client.py -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | from get_nic import getnic 2 | 3 | interfaces = getnic.interfaces() 4 | ip = getnic.ipaddr(interfaces) 5 | 6 | interface_list = [] 7 | ethernet_address = {} 8 | ethernet_keys = [] 9 | ethernet_values = [] 10 | interface_count = 0 11 | 12 | for i in ip.keys(): 13 | interface_list.append(i) 14 | for x in ip[i].keys(): 15 | if interface_list[interface_count] == 'lo': 16 | if x == 'inet4': 17 | ethernet_address.update(lo=ip[i][x]) 18 | elif interface_list[interface_count] == 'eth0': 19 | if x == 'inet4': 20 | ethernet_address.update(eth0=ip[i][x]) 21 | elif interface_list[interface_count] == 'wlan0': 22 | if x == 'inet4': 23 | ethernet_address.update(wlan0=ip[i][x]) 24 | interface_count += 1 25 | 26 | for k,v in ethernet_address.items(): 27 | ethernet_keys.append(k) 28 | ethernet_values.append(v) 29 | --------------------------------------------------------------------------------