1#!/usr/local/bin/python3.8 2 3# python3 status: compatible 4 5import sys, os 6 7# system libraries : test, then import as local symbols 8from afnipy import module_test_lib 9testlibs = ['signal', 'time'] 10if module_test_lib.num_import_failures(testlibs): sys.exit(1) 11import signal, time 12 13# AFNI libraries (besides module_test_lib) 14from afnipy import option_list as OL 15from afnipy import lib_realtime as RT 16from afnipy import afni_util as UTIL 17 18# ---------------------------------------------------------------------- 19# globals 20 21g_help_string = """ 22============================================================================= 23realtime_receiver.py - program to receive and display real-time plugin data 24 25 This program receives motion parameters and optionally ROI averages 26 or voxel data each TR from the real-time plugin to afni. Which data 27 will get sent is controlled by the real-time plugin. All data is 28 sent as floats. 29 30 Motion parameters: 6 values per TR 31 ROI averages: N values per TR, where N is the number of ROIs 32 All voxel data: 8 values per voxel per TR (might be a lot of data!) 33 The 8 values include voxel index, 3 ijk indices, 34 the 3 xyz coordinates, and oh yes, the data 35 36 Examples: 37 38 1a. Run in test mode to display verbose data on the terminal window. 39 40 realtime_receiver.py -show_data yes 41 42 1b. Run in test mode to just display motion to the terminal. 43 44 realtime_receiver.py -write_text_data stdout 45 46 1c. Write all 'extra' parameters to file my_data.txt, one set 47 per line. 48 49 realtime_receiver.py -write_text_data my_data.txt \\ 50 -data_choice all_extras 51 52 2. Provide a serial port, sending the Euclidean norm of the motion params. 53 54 realtime_receiver.py -show_data yes -serial_port /dev/ttyS0 \\ 55 -data_choice motion_norm 56 57 3. Run a feedback demo. Assume that the realtime plugin will send 2 58 values per TR. Request the receiver to plot (a-b)/(a+b), scaled 59 to some small integral range. 60 61 realtime_receiver.py -show_demo_gui yes -data_choice diff_ratio 62 63 4. Adjust the defaults of the -data_choice diff_ratio parameters from 64 those for AFNI_data6/realtime.demos/demo.2.fback.1.receiver, to those 65 for the s620 demo: 66 67 realtime_receiver.py -show_demo_gui yes -data_choice diff_ratio \ 68 -dc_params 0.008 43.5 69 70 TESTING NOTE: 71 72 This following setup can be tested off-line using Dimon, afni and this 73 realtime_receiver.py program. Note that while data passes from Dimon 74 to afni to realtime_receiver.py, the programs essentially should be 75 started in the reverse order (so that the listener is always ready for 76 the talker, say). 77 78 See the sample scripts: 79 80 AFNI_data6/realtime.demos/demo.2.fback.* 81 82 step 1. start the receiver: demo.2.fback.1.receiver 83 84 realtime_receiver.py -show_data yes -show_demo_gui yes \\ 85 -data_choice diff_ratio 86 87 step 2. start realtime afni: demo.2.fback.2.afni 88 89 Note: func_slim+orig is only loaded to ensure a multiple 90 volume overlay dataset, so that the rtfeedme command 91 "DRIVE_AFNI SET_SUBBRICKS 0 1 1" finds sub-brick 1. 92 93 # set many REALTIME env vars or in afni's realtime plugin 94 setenv AFNI_REALTIME_Registration 3D:_realtime 95 setenv AFNI_REALTIME_Base_Image 2 96 setenv AFNI_REALTIME_Graph Realtime 97 setenv AFNI_REALTIME_MP_HOST_PORT localhost:53214 98 setenv AFNI_REALTIME_SEND_VER YES 99 setenv AFNI_REALTIME_SHOW_TIMES YES 100 setenv AFNI_REALTIME_Mask_Vals ROI_means 101 setenv AFNI_REALTIME_Function FIM 102 103 cd ../afni 104 afni -rt -yesplugouts \\ 105 -com "SWITCH_UNDERLAY epi_r1+orig" \\ 106 -com "SWITCH_OVERLAY func_slim+orig" & 107 108 # at this point, the user should open a graph window and: 109 # FIM->Ignore->2 110 # FIM->Pick Ideal->epi_r1_ideal.1D 111 112 step 3. feed data to afni (can be repeated): demo.2.fback.3.feedme 113 114 cd ../afni 115 set episet = epi_r1+orig 116 set maskset = mask.left.vis.aud+orig 117 118 plugout_drive -com "SETENV AFNI_REALTIME_Mask_Dset $maskset" -quit 119 120 rtfeedme \\ 121 -drive 'DRIVE_AFNI OPEN_WINDOW axialimage geom=285x285+3+533' \\ 122 -drive 'DRIVE_AFNI OPEN_WINDOW axialgraph keypress=A' \\ 123 -drive 'DRIVE_AFNI SET_SUBBRICKS 0 1 1' \\ 124 -drive 'DRIVE_AFNI SET_DICOM_XYZ 52 4 12' \\ 125 -drive 'DRIVE_AFNI SET_FUNC_RANGE 0.9' \\ 126 -drive 'DRIVE_AFNI SET_THRESHNEW 0.4' \\ 127 -dt 200 -3D $episet 128 129 130 COMMUNICATION NOTE: 131 132 This program listens for connections at TCP port 53214, unless an 133 alternate port is specified. The real-time plugin (or some other 134 program) connects at that point, opening a new data socket. There 135 is a "handshake" on the data socket, and then data is recieved until 136 a termination signal is received (or the socket goes bad). 137 138 Data is sent per run, meaning the connection should be terminated 139 and restarted at the end of each run. 140 141 The handshake should be the first data on the data socket (per run). 142 The real-time plugin (or other program) will send the hello bytes: 143 0xabcdefab, where the final byte may be incremented by 0, 1 or 2 144 to set the version number, e.g. use 0xabcdefac for version 1. 145 146 Version 0: only motion will be sent 147 Version 1: motion plus N ROI averages will be sent 148 Version 2: motion plus all voxel data for N voxels will be sent 149 - this is dense - 8 values per voxel 150 - 1Dindex, i, j, k, x, y, z, value 151 Version 3: motion plus voxel data for N voxels will be sent 152 - "light" version of 2, only send one 'value' per voxel 153 Version 4: mix of 1 and 3: motion, N ROI aves, M voxel values 154 155 If the version is 1, 2 or 3, the 4-byte handshake should be followed 156 by a 4-byte integer, specifying the value of N. Hence, the 157 combination of the version number and any received N will determine 158 how much data will be sent to the program each TR. 159 160 For version 4, the 4-byte handshake should be followed by 2 4-byte 161 integers, one to specify N (# ROI aves), one to specify M (# vox). 162 163 At the end of the run, the sending program should send the 4-byte 164 good-bye sequence: 0xdeaddead. 165 166 This program is based on the structure of serial_helper, but because 167 it is meant as a replacement, it will have different options. 168 169 ------------------------------------------ 170 Options: 171 172 terminal options: 173 174 -help : show this help 175 -hist : show module history 176 -show_valid_opts : list valid options 177 -ver : show current version 178 179 other options 180 -data_choice CHOICE : pick which data to send as feedback 181 motion : send the 6 motion parameters 182 motion_norm : send the Euclidean norm of them 183 all_extras : send all 'extra' values (ROI or voxel values) 184 diff_ratio : (a-b)/(abs(a)+abs(b)) for 2 'extra' values 185 * To add additional CHOICE methods, see the function compute_TR_data(). 186 -dc_params P1 P2 ... : set data_choice parameters 187 e.g. for diff_ratio, parmas P1 P2 188 P1 = dr low limit, P2 = scalar -> [0,1] 189 result is (dr-P1)*P2 {applied in [0,1]} 190 -serial_port PORT : specify serial port file for feedback data 191 -show_comm_times : display communication times 192 -show_data yes/no : display incoming data in terminal window 193 -show_demo_data : display feedback data in terminal window 194 -show_demo_gui : demonstrate a feedback GUI 195 -swap : swap bytes incoming data 196 -tcp_port PORT : specify TCP port for incoming connections 197 -verb LEVEL : set the verbosity level 198 -write_text_data FNAME : write data to text file 'FNAME' 199 200----------------------------------------------------------------------------- 201R Reynolds July 2009 202============================================================================= 203""" 204g_history = """ 205 realtime_receiver.py history: 206 207 0.0 Jul 06, 2009 : initial version (show data, no serial, little help) 208 0.1 Jul 16, 2009 : includes optional serial connection 209 0.2 Aug 04, 2009 : added basic demo interface and itemized exception traps 210 0.3 Sep 08, 2009 : bind to open host (so /etc/hosts entry is not required) 211 0.4 Jul 26, 2012 : added -show_comm_times 212 0.5 Jan 16, 2013 : added -dc_params 213 0.6 Sep 16, 2016 : proceed even if requested GUI fails to load 214 1.0 Jan 01, 2018 : python3 compatible, added -write_text_data 215 1.1 Jan 22, 2020 : added handling of magic version 3 (all data light) 216 1.2 Jan 23, 2020 : added handling of magic version 4 (ROIs and data) 217""" 218 219g_version = "realtime_receiver.py version 1.2, January 23, 2020" 220 221g_RTinterface = None # global reference to main class (for signal handler) 222 223# ---------------------------------------------------------------------- 224# In this module, handing signals and options. Try to keep other 225# operations in separate libraries (e.g. lib_realtime.py). 226# ---------------------------------------------------------------------- 227 228class ReceiverInterface: 229 """main interface for realtime_receiver.py""" 230 def __init__(self): 231 232 self.valid_opts = None 233 self.user_opts = None 234 self.data_choice = 'motion' 235 self.TR_data = [] # store computed TR data 236 self.verb = 1 237 self.serial_port = None # serial port (filename) 238 239 # lib_realtime.py class instances 240 self.RTI = None # real-time interface RTInterface 241 self.SER = None # serial port interface Serial 242 self.TEXT = None # text file interface 243 244 # data choice parameters 245 self.dc_params = [] 246 247 # demo attributes 248 self.show_demo_data = 0 249 self.demo_frame = None # for demo plot 250 self.wx_app = None # wx App for demo plot 251 252 self.valid_opts = self.init_options() 253 254 def init_options(self): 255 """return an option list instance""" 256 257 valid_opts = OL.OptionList('valid opts') 258 259 # short, terminal arguments 260 valid_opts.add_opt('-help', 0, [], 261 helpstr='display program help') 262 valid_opts.add_opt('-hist', 0, [], 263 helpstr='display the modification history') 264 valid_opts.add_opt('-show_valid_opts', 0, [], 265 helpstr='display all valid options') 266 valid_opts.add_opt('-ver', 0, [], 267 helpstr='display the current version number') 268 269 # general options 270 valid_opts.add_opt('-verb', 1, [], 271 helpstr='set the verbose level (default is 1)') 272 273 valid_opts.add_opt('-data_choice', 1, [], 274 helpstr='which data to send (motion, motion_norm,...)') 275 valid_opts.add_opt('-dc_params', -2, [], 276 helpstr='set parameters for data_choice processing') 277 valid_opts.add_opt('-serial_port', 1, [], 278 helpstr='serial port filename (e.g. /dev/ttyS0 or COM1)') 279 valid_opts.add_opt('-show_data', 1, [], 280 acplist=['no', 'yes'], 281 helpstr='whether to display received data in terminal') 282 valid_opts.add_opt('-show_comm_times', 0, [], 283 helpstr='display communication times') 284 valid_opts.add_opt('-write_text_data', 1, [], 285 helpstr='write data to text file') 286 287 # demo options 288 valid_opts.add_opt('-show_demo_data', 1, [], 289 acplist=['no', 'yes'], 290 helpstr='whether to display demo data in terminal') 291 valid_opts.add_opt('-show_demo_gui', 1, [], 292 acplist=['no', 'yes'], 293 helpstr='whether to display demo data in a GUI') 294 295 valid_opts.add_opt('-swap', 0, [], 296 helpstr='byte-swap numerical reads') 297 valid_opts.add_opt('-tcp_port', 1, [], 298 helpstr='TCP port for incoming connections') 299 300 return valid_opts 301 302 303 def check_terminal_opts(self): 304 """check argv for terminal options, start with a global library 305 call to check_special_opts""" 306 307 self.valid_opts.check_special_opts(sys.argv) 308 309 # if no arguments are given, apply -help 310 if len(sys.argv) < 2 or '-help' in sys.argv: 311 print(g_help_string) 312 return 1 313 314 if '-hist' in sys.argv: 315 print(g_history) 316 return 1 317 318 if '-show_valid_opts' in sys.argv: 319 self.valid_opts.show('', 1) 320 return 1 321 322 if '-ver' in sys.argv: 323 print(g_version) 324 return 1 325 326 return 0 327 328 def process_options(self): 329 """process all options, applying to interfaces where appropriate""" 330 331 # ================================================== 332 # first fire up the TCP interface 333 334 self.RTI = RT.RTInterface() 335 if not self.RTI: return None 336 337 # and store globally 338 global g_RTinterface 339 g_RTinterface = self.RTI # global for signal handler 340 341 # ================================================== 342 # gather and process the user options 343 self.user_opts = OL.read_options(sys.argv, self.valid_opts) 344 uopts = self.user_opts # for convenience 345 if not uopts: return 1 346 347 # process -verb first 348 val, err = uopts.get_type_opt(int, '-verb') 349 if val != None and not err: 350 self.verb = val 351 self.RTI.verb = val 352 353 # ================================================== 354 # --- serial options --- 355 356 # port first: if set, create SerialInterface 357 val, err = uopts.get_string_opt('-serial_port') 358 if val != None and not err: 359 self.SER = RT.SerialInterface(val, verb=self.verb) 360 if not self.SER: return 1 361 362 # ================================================== 363 # --- feedback options --- 364 365 val, err = uopts.get_string_opt('-data_choice') 366 if val != None and not err: self.data_choice = val 367 368 val, err = uopts.get_type_list(float, '-dc_params') 369 if val != None and not err: self.dc_params = val 370 371 # ================================================== 372 # --- tcp options --- 373 374 val, err = uopts.get_string_opt('-show_data') 375 if val != None and not err: 376 if val == 'no': self.RTI.show_data = 0 377 else: self.RTI.show_data = 1 378 379 if uopts.find_opt('-show_comm_times'): self.RTI.show_times = 1 380 if uopts.find_opt('-swap'): self.RTI.swap = 1 381 382 val, err = uopts.get_type_opt(int, '-tcp_port') 383 if val != None and not err: self.RTI.server_port = val 384 385 # ================================================== 386 # --- text file writing options --- 387 388 # open text file 389 val, err = uopts.get_string_opt('-write_text_data') 390 if val != None and not err: 391 self.TEXT = RT.TextFileInterface(val, verb=self.verb) 392 if not self.TEXT: return 1 393 394 # ================================================== 395 # --- demo options --- 396 397 val, err = uopts.get_string_opt('-show_demo_data') 398 if val != None and not err: 399 if val == 'no': self.show_demo_data = 0 400 else: self.show_demo_data = 1 401 402 val, err = uopts.get_string_opt('-show_demo_gui') 403 if val != None and not err: 404 if val == 'yes': 405 if self.set_demo_gui(): 406 print('\n** GUI demo failed, proceeding without GUI...\n') 407 408 return 0 # so continue and listen 409 410 def set_demo_gui(self): 411 """create the GUI for display of the demo data""" 412 testlibs = ['numpy', 'wx'] 413 if module_test_lib.num_import_failures(testlibs): 414 return 1 415 try: 416 import numpy as N, wx 417 from afnipy import lib_RR_plot as LPLOT 418 except: 419 return 1 420 421 self.wx_app = wx.App() 422 self.demo_frame = LPLOT.CanvasFrame(title='receiver demo') 423 self.demo_frame.EnableCloseButton(True) 424 self.demo_frame.Show(True) 425 self.demo_frame.style = 'bar' 426 self.demo_frame.xlabel = 'most recent 10 TRs' 427 self.demo_frame.ylabel = 'scaled diff_ratio' 428 429 # for the current demo, set an ranges for 10 numbers in [0,10] 430 if self.demo_frame.style == 'graph': 431 self.demo_frame.set_limits(0,9.1,-0.1,10.1) 432 elif self.demo_frame.style == 'bar': 433 self.demo_frame.set_limits(0,10.1,-0.1,10.1) 434 435 def set_signal_handlers(self): 436 """capture common termination signals, to properly close ports""" 437 438 if self.verb > 1: print('++ setting signals') 439 440 slist = [ signal.SIGHUP, signal.SIGINT, signal.SIGQUIT, signal.SIGTERM ] 441 if self.verb > 2: print(' signals are %s' % slist) 442 443 for sig in slist: signal.signal(sig, clean_n_exit) 444 445 return 446 447 def close_data_ports(self): 448 """close TCP and socket ports, except for server port""" 449 450 if self.RTI: self.RTI.close_data_ports() 451 if self.SER: self.SER.close_data_ports() 452 if self.TEXT: self.TEXT.close_text_file() 453 454 def process_demo_data(self): 455 456 length = len(self.TR_data) 457 if length == 0: return 458 459 if self.show_demo_data: 460 print('-- TR %d, demo value: ' % length, self.TR_data[length-1][0]) 461 if self.demo_frame: 462 if length > 10: bot = length-10 463 else: bot = 0 464 pdata = [self.TR_data[ind][0] for ind in range(bot,length)] 465 self.demo_frame.plot_data(pdata) 466 467 def process_one_TR(self): 468 """return 0 to continue, 1 on valid termination, -1 on error""" 469 470 if self.verb>2: 471 print('-- process_one_TR, show_demo_data = %d,' % self.show_demo_data) 472 473 rv = self.RTI.read_TR_data() 474 if rv: 475 if self.verb > 3: print('** process 1 TR: read data failure') 476 return rv 477 478 rv, data = compute_TR_data(self) # PROCESS DATA HERE 479 if rv or len(data) == 0: return rv 480 self.TR_data.append(data) 481 482 if self.SER: self.SER.write_4byte_data(data) 483 if self.TEXT: self.TEXT.write_data_line(data) 484 if self.show_demo_data or self.demo_frame: self.process_demo_data() 485 486 return rv 487 488 def process_one_run(self): 489 """repeatedly: process all incoming data for a single run 490 return 0 on success and 1 on error 491 """ 492 493 # clear any old data 494 if len(self.TR_data) > 0: 495 del(self.TR_data) 496 self.TR_data = [] 497 498 # wait for the real-time plugin to talk to us 499 if self.RTI.wait_for_new_run(): return 1 500 501 # possibly open a serial port 502 if self.SER: 503 if self.SER.open_data_port(): return 1 504 505 # possibly open a text file 506 if self.TEXT: 507 if self.TEXT.open_text_file(): return 1 508 509 # process one TR at a time until 510 if self.verb > 1: 511 print('-- incoming data, data_choice = %s' % self.data_choice) 512 513 rv = self.process_one_TR() 514 while rv == 0: 515 rv = self.process_one_TR() 516 sys.stdout.flush() 517 518 if self.verb > 1: 519 if rv > 0: vstr = '(terminating on success)' 520 else: vstr = '(terminating on error)' 521 print('-- processed %d TRs of data %s' % (self.RTI.nread, vstr)) 522 if self.verb > 0: print('-'*60) 523 sys.stdout.flush() 524 525 if rv > 0: return 0 # success for one run 526 else: return 1 # some error 527 528def clean_n_exit(signum, frame): 529 530 verb = g_RTinterface.verb 531 532 if verb > 1: print('++ signal handler called with signal', signum) 533 534 g_RTinterface.close_data_ports() 535 try: sys.stdout.flush() 536 except: pass 537 538 # at last, close server port 539 if g_RTinterface.server_sock: 540 if g_RTinterface.verb > 1: print('closing server port...') 541 try: g_RTinterface.server_sock.close() 542 except (RT.socket.error, RT.socket.timeout): pass 543 544 if g_RTinterface.verb > 0: print('-- exiting on signal %d...' % signum) 545 sys.exit(signum) 546 547def compute_TR_data(rec): 548 """If writing to the serial port, this is the main function to compute 549 results from rec.motion and/or rec.extras for the current TR and 550 return it as an array of floats. 551 552 Note that motion and extras are lists of time series of length nread, 553 so processing a time series is easy, but a single TR requires extracting 554 the data from the end of each list. 555 556 The possible computations is based on data_choice, specified by the user 557 option -data_choice. If you want to send data that is not listed, just 558 add a condition. 559 560 ** Please add each data_choice to the -help. Search for motion_norm to 561 find all places to edit. 562 563 return 2 items: 564 error code: 0 on success, -1 on error 565 data array: (possibly empty) array of data to send 566 """ 567 568 rti = rec.RTI # for convenience 569 if not rec.data_choice: return 0, [] 570 571 # case 'motion': send all motion 572 if rec.data_choice == 'motion': 573 if rti.nread > 0: 574 return 0, [rti.motion[ind][rti.nread-1] for ind in range(6)] 575 else: return -1, [] 576 577 # case 'motion_norm': send Euclidean norm of motion params 578 # --> sqrt(sum of squared motion params) 579 elif rec.data_choice == 'motion_norm': 580 if rti.nread > 0: 581 motion = [rti.motion[ind][rti.nread-1] for ind in range(6)] 582 return 0, [UTIL.euclidean_norm(motion)] 583 else: return -1, [] 584 585 # case 'all_extras': send all extra data 586 elif rec.data_choice == 'all_extras': 587 if rti.nextra > 0: 588 return 0, [rti.extras[i][rti.nread-1] for i in range(rti.nextra)] 589 else: return -1, [] 590 591 # case 'diff_ratio': (a-b)/(abs(a)+abs(b)) 592 elif rec.data_choice == 'diff_ratio': 593 npairs = rti.nextra//2 594 if npairs > 0: 595 vals = [rti.extras[i][rti.nread-1] for i in range(rti.nextra)] 596 # modify vals array, setting the first half to diff_ratio 597 for ind in range(npairs): 598 a = vals[2*ind] 599 b = vals[2*ind+1] 600 if a == 0 and b == 0: newval = 0.0 601 else: newval = (a-b)/float(abs(a)+abs(b)) 602 603 # -------------------------------------------------------------- 604 # VERY data dependent: convert from diff_ratio to int in {0..10} 605 # assume AFNI_data6 demo 15 Jan 2013 606 607 # now scale [bot,inf) to {0..10}, where val>=top -> 10 608 # AD6: min = -0.1717, mean = -0.1605, max = -0.1490 609 610 bot = -0.17 # s620: bot = 0.008, scale = 43.5 611 scale = 55.0 # =~ 1.0/(0.1717-0.149), rounded up 612 if len(rec.dc_params) == 2: 613 bot = rec.dc_params[0] 614 scale = rec.dc_params[1] 615 616 val = newval-bot 617 if val < 0.0: val = 0.0 618 ival = int(10*val*scale) 619 if ival > 10: ival = 10 620 621 vals[ind] = ival 622 623 if rti.verb > 1: 624 if rti.verb > 2: pstr = ', (params = %s)' % rec.dc_params 625 else: pstr = '' 626 print('++ diff_ratio: ival = %d (from %s)%s'%(ival,newval,pstr)) 627 628 return 0, vals[0:npairs] # return the partial list 629 630 else: 631 if rti.verb > 0 and rti.nread < 2: 632 print('** no pairs to compute diff_ratio from...') 633 return 0, [] 634 635 # failure! 636 else: 637 print("** invalid data_choice '%s', shutting down ..." % rec.data_choice) 638 return -1, [] 639 640def main(): 641 642 # create main interface 643 receiver = ReceiverInterface() 644 if receiver == None: return 1 645 646 # set options and look for early termination 647 if receiver.check_terminal_opts(): return 0 # then exit 648 649 # read and process user options 650 if receiver.process_options(): return 1 651 652 # ---------------------------------------------------------------------- 653 # ready to rock: set signal handlers and look for data 654 655 receiver.set_signal_handlers() # require signal to exit 656 657 # prepare for incoming connections 658 if receiver.RTI.open_incoming_socket(): return 1 659 660 # repeatedly: process all incoming data for a single run 661 while 1: 662 rv = receiver.process_one_run() 663 if rv: time.sleep(1) # on error, ponder life briefly 664 receiver.close_data_ports() 665 666 return -1 # should not be reached 667 668if __name__ == '__main__': 669 sys.exit(main()) 670 671