├── LICENSE.txt ├── README.md ├── img ├── Multichannel.png ├── Singlechannel.png └── plot_line.png ├── seedlink_plotter ├── __init__.py ├── favicon.gif └── seedlink_plotter.py └── setup.py /LICENSE.txt: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Seedlink-Plotter 2 | 3 | A python script to plot real time seismic data from a seedlink server in drum style or line style 4 | 5 | This version works with at least stable ObsPy version 1.3.0 6 | 7 | On some linux box, the time zone must be set to UTC and not GMT 8 | 9 | ### Installation 10 | 11 | pip install https://github.com/bonaime/seedlink_plotter/archive/master.zip 12 | 13 | ### Upgrade 14 | 15 | pip install --upgrade https://github.com/bonaime/seedlink_plotter/archive/master.zip 16 | 17 | ### Usage examples 18 | 19 | Drum plots (with longer time range) and with events greater than 5.5: 20 | 21 | seedlink-plotter -s "G_FDFM:00BHZ" --x_position 200 --y_position 50 --x_size 800 --y_size 600 -b 24h --scale 20000 --seedlink_server "rtserver.ipgp.fr:18000" --x_scale 60m --events 5.5 22 | 23 | ![Singlechannel](/img/Singlechannel.png) 24 | 25 | 26 | Line plot with single station (with shorter time range): 27 | 28 | seedlink-plotter -s "G_IVI:00BHZ" -b 10m --seedlink_server "rtserver.ipgp.fr:18000" --line_plot 29 | 30 | ![Plot_line](/img/plot_line.png) 31 | 32 | Line plots with multiple stations (with shorter time range): 33 | 34 | seedlink-plotter -s "G_FDFM:00BHZ,G_SSB:00BHZ" --x_position 200 --y_position 50 --x_size 800 --y_size 600 -b 30m --seedlink_server "rtserver.ipgp.fr:18000" --update_time 2s 35 | seedlink-plotter -s "G_FDFM:00BHZ 00BHN 00BHE" --x_position 200 --y_position 50 --x_size 800 --y_size 600 -b 30m --seedlink_server "rtserver.ipgp.fr:18000" --update_time 2s 36 | 37 | ![Multichannel](/img/Multichannel.png) 38 | 39 | ### Keyboard Controls 40 | 41 | Keyboard controls only work without option `--without-decoration`! 42 | 43 | - `f`: toggle fullscreen 44 | - `` or `q`: close window 45 | 46 | ### Dependencies 47 | - Python 3.6 48 | - ObsPy 1.3.0 49 | - matplolib (>= 1.3.0) 50 | - scipy 51 | - numpy 52 | -------------------------------------------------------------------------------- /img/Multichannel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbonaime/seedlink_plotter/300ae9057e8bca38a9246acc666fa6ae4e4d2778/img/Multichannel.png -------------------------------------------------------------------------------- /img/Singlechannel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbonaime/seedlink_plotter/300ae9057e8bca38a9246acc666fa6ae4e4d2778/img/Singlechannel.png -------------------------------------------------------------------------------- /img/plot_line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbonaime/seedlink_plotter/300ae9057e8bca38a9246acc666fa6ae4e4d2778/img/plot_line.png -------------------------------------------------------------------------------- /seedlink_plotter/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbonaime/seedlink_plotter/300ae9057e8bca38a9246acc666fa6ae4e4d2778/seedlink_plotter/__init__.py -------------------------------------------------------------------------------- /seedlink_plotter/favicon.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbonaime/seedlink_plotter/300ae9057e8bca38a9246acc666fa6ae4e4d2778/seedlink_plotter/favicon.gif -------------------------------------------------------------------------------- /seedlink_plotter/seedlink_plotter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from __future__ import print_function 3 | 4 | import matplotlib 5 | # Set the backend for matplotlib. 6 | matplotlib.use("TkAgg") 7 | matplotlib.rc('figure.subplot', hspace=0) 8 | matplotlib.rc('font', family="monospace") 9 | import tkinter 10 | 11 | from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg 12 | from matplotlib.figure import Figure 13 | from matplotlib.ticker import MaxNLocator 14 | from matplotlib.patheffects import withStroke 15 | from matplotlib.dates import date2num 16 | import matplotlib.pyplot as plt 17 | from obspy import Stream, Trace 18 | from obspy import __version__ as OBSPY_VERSION 19 | from obspy.core import UTCDateTime 20 | from obspy.core.event import Catalog 21 | from obspy.core.util import MATPLOTLIB_VERSION 22 | from argparse import ArgumentParser,ArgumentDefaultsHelpFormatter 23 | from math import sin 24 | import threading 25 | import time 26 | import warnings 27 | import os 28 | import sys 29 | from urllib.request import URLError 30 | import logging 31 | import numpy as np 32 | 33 | 34 | range_func = range 35 | input_func = input 36 | 37 | 38 | OBSPY_VERSION = [int(x) for x in OBSPY_VERSION.split(".")[:2]] 39 | # check obspy version and warn if it's below 0.10.0, which means that a memory 40 | # leak is present in the used seedlink client (unless working on some master 41 | # branch version after obspy/obspy@5ce975c3710ca, which is impossible to check 42 | # reliably). see #7 and obspy/obspy#918. 43 | # imports depend of the obspy version 44 | if OBSPY_VERSION < [0, 10]: 45 | warning_msg = ( 46 | "ObsPy version < 0.10.0 has a memory leak in SeedLink Client. " 47 | "Please update your ObsPy installation to avoid being affected by " 48 | "the memory leak (see " 49 | "https://github.com/bonaime/seedlink_plotter/issues/7).") 50 | warnings.warn(warning_msg) 51 | sys.exit() 52 | 53 | from obspy.clients.seedlink.slpacket import SLPacket 54 | from obspy.clients.seedlink import SLClient 55 | from obspy.clients.fdsn import Client 56 | 57 | # Compatibility checks 58 | # UTCDateTime 59 | try: 60 | UTCDateTime.format_seedlink 61 | except AttributeError: 62 | # create the new format_seedlink fonction using the old formatSeedLink 63 | # method 64 | def format_seedlink(self): 65 | return self.formatSeedLink() 66 | # add the function in the class 67 | setattr(UTCDateTime, 'format_seedlink', format_seedlink) 68 | # SLPacket 69 | try: 70 | SLPacket.get_type 71 | except AttributeError: 72 | # create the new get_type fonction using the old getType method 73 | def get_type(self): 74 | return self.getType() 75 | # add the function in the class 76 | setattr(SLPacket, 'get_type', get_type) 77 | 78 | try: 79 | SLPacket.get_trace 80 | except AttributeError: 81 | # create the new get_trace fonction using the old getTrace method 82 | def get_trace(self): 83 | return self.getTrace() 84 | # add the function in the class 85 | setattr(SLPacket, 'get_trace', get_trace) 86 | 87 | 88 | class SeedlinkPlotter(tkinter.Tk): 89 | 90 | """ 91 | This module plots realtime seismic data from a Seedlink server 92 | """ 93 | 94 | def __init__(self, stream=None, events=None, myargs=None, lock=None, 95 | drum_plot=True, trace_ids=None, *args, **kwargs): 96 | tkinter.Tk.__init__(self, *args, **kwargs) 97 | favicon = tkinter.PhotoImage( 98 | file=os.path.join(os.path.dirname(os.path.abspath(__file__)), 99 | "favicon.gif")) 100 | self.tk.call('wm', 'iconphoto', self._w, favicon) 101 | self.wm_title("seedlink-plotter {}".format(myargs.seedlink_server)) 102 | self.focus_set() 103 | self._bind_keys() 104 | args = myargs 105 | self.lock = lock 106 | ### size and position 107 | self.geometry(str(args.x_size) + 'x' + str(args.y_size) + '+' + str( 108 | args.x_position) + '+' + str(args.y_position)) 109 | w, h, pad = self.winfo_screenwidth(), self.winfo_screenheight(), 3 110 | self._geometry = ("%ix%i+0+0" % (w - pad, h - pad)) 111 | # hide the window decoration 112 | if args.without_decoration: 113 | self.wm_overrideredirect(True) 114 | if args.fullscreen: 115 | self._toggle_fullscreen(None) 116 | 117 | # main figure 118 | self.figure = Figure() 119 | canvas = FigureCanvasTkAgg(self.figure, master=self) 120 | 121 | if MATPLOTLIB_VERSION[:2] >= [2, 2]: 122 | canvas.draw() 123 | else: 124 | canvas.show() 125 | canvas.get_tk_widget().pack(fill=tkinter.BOTH, expand=1) 126 | 127 | self.backtrace = args.backtrace_time 128 | self.canvas = canvas 129 | self.scale = args.scale 130 | self.args = args 131 | self.stream = stream 132 | self.events = events 133 | self.drum_plot = drum_plot 134 | self.ids = trace_ids 135 | 136 | # Colors 137 | if args.rainbow: 138 | # Rainbow colors ! 139 | self.color = self.rainbow_color_generator( 140 | int(args.nb_rainbow_colors)) 141 | else: 142 | # Regular colors: Black, Red, Blue, Green 143 | self.color = ('#000000', '#e50000', '#0000e5', '#448630') 144 | 145 | self.plot_graph() 146 | 147 | def _quit(self, event): 148 | event.widget.quit() 149 | 150 | def _bind_keys(self): 151 | self.bind('', self._quit) 152 | self.bind('q', self._quit) 153 | self.bind('f', self._toggle_fullscreen) 154 | 155 | def _toggle_fullscreen(self, event): 156 | g = self.geometry() 157 | self.geometry(self._geometry) 158 | self._geometry = g 159 | 160 | def plot_graph(self): 161 | now = UTCDateTime() 162 | if self.drum_plot: 163 | self.stop_time = UTCDateTime( 164 | now.year, now.month, now.day, now.hour, 0, 0) + 3600 165 | self.start_time = self.stop_time - self.args.backtrace_time 166 | else: 167 | self.start_time = now - self.backtrace 168 | self.stop_time = now 169 | 170 | with self.lock: 171 | # leave some data left of our start for possible processing 172 | self.stream.trim( 173 | starttime=self.start_time - 120, nearest_sample=False) 174 | stream = self.stream.copy() 175 | 176 | try: 177 | logging.info(str(stream.split())) 178 | if not stream: 179 | raise Exception("Empty stream for plotting") 180 | 181 | if self.drum_plot : 182 | stream.merge() 183 | stream.trim(starttime=self.start_time, endtime=self.stop_time, 184 | pad=True, nearest_sample=False) 185 | else: 186 | stream.merge(-1) 187 | stream.trim(starttime=self.start_time, endtime=self.stop_time) 188 | if self.drum_plot: 189 | self.plot_drum(stream) 190 | else: 191 | self.plot_lines(stream) 192 | except Exception as e: 193 | logging.error(e) 194 | pass 195 | self.after(int(self.args.update_time * 1000), self.plot_graph) 196 | 197 | def plot_drum(self, stream): 198 | title = stream[0].id 199 | if self.scale: 200 | title += ' - scale: ' + str(self.scale) + ' -' 201 | else: 202 | title += ' - autoscale -' 203 | title += " without filtering" 204 | self.figure.clear() 205 | stream.plot( 206 | fig=self.figure, type='dayplot', interval=self.args.x_scale, 207 | number_of_ticks=self.args.time_tick_nb, tick_format=self.args.tick_format, 208 | size=(self.args.x_size, self.args.y_size), 209 | x_labels_size=8, y_labels_size=8, 210 | title=title, title_size=14, 211 | linewidth=0.5, right_vertical_labels=False, 212 | vertical_scaling_range=self.args.scale, 213 | subplots_adjust_left=0.04, subplots_adjust_right=0.99, 214 | subplots_adjust_top=0.95, subplots_adjust_bottom=0.05, 215 | one_tick_per_line=True, 216 | color=self.color, 217 | show_y_UTC_label=False, 218 | events=self.events) 219 | 220 | def plot_lines(self, stream): 221 | for id_ in self.ids: 222 | if not any([tr.id == id_ for tr in stream]): 223 | net, sta, loc, cha = id_.split(".") 224 | header = {'network': net, 'station': sta, 'location': loc, 225 | 'channel': cha, 'starttime': self.start_time} 226 | data = np.zeros(2) 227 | stream.append(Trace(data=data, header=header)) 228 | stream.sort() 229 | self.figure.clear() 230 | fig = self.figure 231 | # avoid the differing trace.processing attributes prohibiting to plot 232 | # single traces of one id together. 233 | for tr in stream: 234 | tr.stats.processing = [] 235 | stream.plot(fig=fig, method="fast", draw=False, equal_scale=False, 236 | size=(self.args.x_size, self.args.y_size), title="", 237 | color='Blue', tick_format=self.args.tick_format, 238 | number_of_ticks=self.args.time_tick_nb) 239 | fig.subplots_adjust(left=0, right=1, top=1, bottom=0) 240 | bbox = dict(boxstyle="round", fc="w", alpha=0.8) 241 | path_effects = [withStroke(linewidth=4, foreground="w")] 242 | pad = 10 243 | for ax in fig.axes[::2]: 244 | if MATPLOTLIB_VERSION[0] >= 2: 245 | ax.set_facecolor("0.8") 246 | else: 247 | ax.set_axis_bgcolor("0.8") 248 | for id_, ax in zip(self.ids, fig.axes): 249 | ax.set_title("") 250 | 251 | try: 252 | text = ax.texts[0] 253 | # we should always have a single text, which is the stream 254 | # label of the axis, but catch index errors just in case 255 | except IndexError: 256 | pass 257 | else: 258 | text.set_fontsize(self.args.title_size) 259 | xlabels = ax.get_xticklabels() 260 | ylabels = ax.get_yticklabels() 261 | plt.setp(ylabels, ha="left", path_effects=path_effects) 262 | ax.yaxis.set_tick_params(pad=-pad) 263 | # treatment for bottom axes: 264 | if ax is fig.axes[-1]: 265 | plt.setp( 266 | xlabels, va="bottom", size=self.args.time_legend_size, bbox=bbox) 267 | 268 | ax.xaxis.set_tick_params(pad=-pad) 269 | # all other axes 270 | else: 271 | plt.setp(xlabels, visible=False) 272 | locator = MaxNLocator(nbins=4, prune="both") 273 | ax.yaxis.set_major_locator(locator) 274 | ax.yaxis.grid(False) 275 | ax.grid(True, axis="x") 276 | if len(ax.lines) == 1: 277 | ydata = ax.lines[0].get_ydata() 278 | # if station has no data we add a dummy trace and we end up in 279 | # a line with either 2 or 4 zeros (2 if dummy line is cut off 280 | # at left edge of time axis) 281 | if len(ydata) in [4, 2] and not ydata.any(): 282 | plt.setp(ylabels, visible=False) 283 | if MATPLOTLIB_VERSION[0] >= 2: 284 | ax.set_facecolor("#ff6666") 285 | else: 286 | ax.set_axis_bgcolor("#ff6666") 287 | fig.axes[0].set_xlim(right=date2num(self.stop_time.datetime)) 288 | fig.axes[0].set_xlim(left=date2num(self.start_time.datetime)) 289 | if len(fig.axes) > 5: 290 | bbox["alpha"] = 0.6 291 | fig.text(0.99, 0.97, self.stop_time.strftime("%Y-%m-%d %H:%M:%S UTC"), 292 | ha="right", va="top", bbox=bbox, fontsize="medium") 293 | fig.canvas.draw() 294 | 295 | def rgb_to_hex(self, red_value, green_value, blue_value): 296 | """ 297 | converter for the colors gradient 298 | """ 299 | return '#%02X%02X%02X' % (red_value, green_value, blue_value) 300 | 301 | def rainbow_color_generator(self, max_color): 302 | """ 303 | Rainbow color generator 304 | """ 305 | color_list = [] 306 | frequency = 0.3 307 | for compteur_lignes in range_func(max_color): 308 | 309 | red = sin(frequency * compteur_lignes * 2 + 0) * 127 + 128 310 | green = sin(frequency * compteur_lignes * 2 + 2) * 127 + 128 311 | blue = sin(frequency * compteur_lignes * 2 + 4) * 127 + 128 312 | 313 | color_list.append( 314 | self.rgb_to_hex(red_value=red, green_value=green, blue_value=blue)) 315 | 316 | return tuple(color_list) 317 | 318 | 319 | class SeedlinkUpdater(SLClient): 320 | 321 | def __init__(self, stream, myargs=None, lock=None): 322 | # loglevel NOTSET delegates messages to parent logger 323 | super(SeedlinkUpdater, self).__init__() 324 | self.stream = stream 325 | self.lock = lock 326 | self.args = myargs 327 | 328 | 329 | def packet_handler(self, count, slpack): 330 | """ 331 | for compatibility with obspy 0.10.3 renaming 332 | """ 333 | self.packetHandler(count, slpack) 334 | 335 | def packetHandler(self, count, slpack): 336 | """ 337 | Processes each packet received from the SeedLinkConnection. 338 | :type count: int 339 | :param count: Packet counter. 340 | :type slpack: :class:`~obspy.seedlink.SLPacket` 341 | :param slpack: packet to process. 342 | :return: Boolean true if connection to SeedLink server should be 343 | closed and session terminated, false otherwise. 344 | """ 345 | 346 | # check if not a complete packet 347 | if slpack is None or (slpack == SLPacket.SLNOPACKET) or \ 348 | (slpack == SLPacket.SLERROR): 349 | return False 350 | 351 | # get basic packet info 352 | type = slpack.get_type() 353 | 354 | # process INFO packets here 355 | if type == SLPacket.TYPE_SLINF: 356 | return False 357 | if type == SLPacket.TYPE_SLINFT: 358 | logging.info("Complete INFO:" + self.slconn.getInfoString()) 359 | if self.infolevel is not None: 360 | return True 361 | else: 362 | return False 363 | 364 | # process packet data 365 | trace = slpack.get_trace() 366 | if trace is None: 367 | logging.info( 368 | self.__class__.__name__ + ": blockette contains no trace") 369 | return False 370 | 371 | # new samples add to the main stream which is then trimmed 372 | with self.lock: 373 | self.stream += trace 374 | self.stream.merge(-1) 375 | for tr in self.stream: 376 | tr.stats.processing = [] 377 | return False 378 | 379 | def getTraceIDs(self): 380 | """ 381 | Return a list of SEED style Trace IDs that the SLClient is trying to 382 | fetch data for. 383 | """ 384 | ids = [] 385 | streams = self.slconn.get_streams() 386 | for stream in streams: 387 | net = stream.net 388 | sta = stream.station 389 | selectors = stream.get_selectors() 390 | for selector in selectors: 391 | if len(selector) == 3: 392 | loc = "" 393 | else: 394 | loc = selector[:2] 395 | cha = selector[-3:] 396 | ids.append(".".join((net, sta, loc, cha))) 397 | ids.sort() 398 | return ids 399 | 400 | 401 | class EventUpdater(): 402 | """ 403 | Fetch list of seismic events 404 | """ 405 | def __init__(self, stream, events, myargs=None, lock=None): 406 | self.stream = stream 407 | self.events = events 408 | self.args = myargs 409 | self.lock = lock 410 | warn_msg = "The resource identifier already exists and points to " + \ 411 | "another object. It will now point to the object " + \ 412 | "referred to by the new resource identifier." 413 | warnings.filterwarnings("ignore", warn_msg) 414 | 415 | def run(self): 416 | """ 417 | Endless execution to update events. Does not terminate, to be run in a 418 | (daemon) thread. 419 | """ 420 | while True: 421 | # no stream, reschedule event update in 20 seconds 422 | if not self.stream: 423 | time.sleep(20) 424 | continue 425 | try: 426 | events = self.get_events() 427 | except URLError as error: 428 | msg = "%s: %s\n" % (error.__class__.__name__, error) 429 | sys.stderr.write(msg) 430 | except Exception as error: 431 | msg = "%s: %s\n" % (error.__class__.__name__, error) 432 | sys.stderr.write(msg) 433 | else: 434 | self.update_events(events) 435 | time.sleep(self.args.events_update_time * 60) 436 | 437 | def get_events(self): 438 | """ 439 | Method to fetch updated list of events to use in plot. 440 | """ 441 | with self.lock: 442 | start = min([tr.stats.starttime for tr in self.stream]) 443 | end = max([tr.stats.endtime for tr in self.stream]) 444 | neries_emsc = Client("EMSC") 445 | events = neries_emsc.get_events(starttime=start, endtime=end, 446 | minmagnitude=self.args.events) 447 | return events 448 | 449 | def update_events(self, events): 450 | """ 451 | Method to insert new events into list of events shared with the GUI. 452 | """ 453 | with self.lock: 454 | self.events.clear() 455 | self.events.extend(events) 456 | 457 | 458 | def _parse_time_with_suffix_to_seconds(timestring): 459 | """ 460 | Parse a string to seconds as float. 461 | 462 | If string can be directly converted to a float it is interpreted as 463 | seconds. Otherwise the following suffixes can be appended, case 464 | insensitive: "s" for seconds, "m" for minutes, "h" for hours, "d" for days. 465 | 466 | >>> _parse_time_with_suffix_to_seconds("12.6") 467 | 12.6 468 | >>> _parse_time_with_suffix_to_seconds("12.6s") 469 | 12.6 470 | >>> _parse_time_with_suffix_to_minutes("12.6m") 471 | 756.0 472 | >>> _parse_time_with_suffix_to_seconds("12.6h") 473 | 45360.0 474 | 475 | :type timestring: str 476 | :param timestring: "s" for seconds, "m" for minutes, "h" for hours, "d" for 477 | days. 478 | :rtype: float 479 | """ 480 | try: 481 | return float(timestring) 482 | except: 483 | timestring, suffix = timestring[:-1], timestring[-1].lower() 484 | mult = {'s': 1.0, 'm': 60.0, 'h': 3600.0, 'd': 3600.0 * 24}[suffix] 485 | return float(timestring) * mult 486 | 487 | 488 | def _parse_time_with_suffix_to_minutes(timestring): 489 | """ 490 | Parse a string to minutes as float. 491 | 492 | If string can be directly converted to a float it is interpreted as 493 | minutes. Otherwise the following suffixes can be appended, case 494 | insensitive: "s" for seconds, "m" for minutes, "h" for hours, "d" for days. 495 | 496 | >>> _parse_time_with_suffix_to_minutes("12.6") 497 | 12.6 498 | >>> _parse_time_with_suffix_to_minutes("12.6s") 499 | 0.21 500 | >>> _parse_time_with_suffix_to_minutes("12.6m") 501 | 12.6 502 | >>> _parse_time_with_suffix_to_minutes("12.6h") 503 | 756.0 504 | 505 | :type timestring: str 506 | :param timestring: "s" for seconds, "m" for minutes, "h" for hours, "d" for 507 | days. 508 | :rtype: float 509 | """ 510 | try: 511 | return float(timestring) 512 | except: 513 | seconds = _parse_time_with_suffix_to_seconds(timestring) 514 | return seconds / 60.0 515 | 516 | 517 | def main(): 518 | parser = ArgumentParser(prog='seedlink_plotter', 519 | description='Plot a realtime seismogram of a station', 520 | formatter_class=ArgumentDefaultsHelpFormatter) 521 | parser.add_argument( 522 | '-s', '--seedlink_streams', type=str, required=True, 523 | help='The seedlink stream selector string. It has the format ' 524 | '"stream1[:selectors1],stream2[:selectors2],...", with "stream" ' 525 | 'in "NETWORK"_"STATION" format and "selector" a space separated ' 526 | 'list of "LOCATION""CHANNEL", e.g. ' 527 | '"IU_KONO:BHE BHN,MN_AQU:HH?.D".') 528 | parser.add_argument( 529 | '--scale', type=int, help='the scale to apply on data ex:50000', required=False) 530 | 531 | # Real-time parameters 532 | parser.add_argument('--seedlink_server', type=str, 533 | help='the seedlink server to connect to with port. "\ 534 | "ex: rtserver.ipgp.fr:18000 ', required=True) 535 | parser.add_argument( 536 | '--x_scale', type=_parse_time_with_suffix_to_minutes, 537 | help='the number of minute to plot per line' 538 | ' The following suffixes can be used as well: "s" for seconds, ' 539 | '"m" for minutes, "h" for hours and "d" for days.', 540 | default='60m') 541 | parser.add_argument('-b', '--backtrace_time', 542 | help='the number of seconds to plot (3600=1h,86400=24h). The ' 543 | 'following suffixes can be used as well: "m" for minutes, ' 544 | '"h" for hours and "d" for days.', required=True, 545 | type=_parse_time_with_suffix_to_seconds,default='24h') 546 | parser.add_argument('--x_position', type=int, 547 | help='the x position of the graph', required=False, default=0) 548 | parser.add_argument('--y_position', type=int, 549 | help='the y position of the graph', required=False, default=0) 550 | parser.add_argument( 551 | '--x_size', type=int, help='the x size of the graph', required=False, default=800) 552 | parser.add_argument( 553 | '--y_size', type=int, help='the y size of the graph', required=False, default=600) 554 | parser.add_argument( 555 | '--title_size', type=int, help='the title size of each station in multichannel', required=False, default=10) 556 | parser.add_argument( 557 | '--time_legend_size', type=int, help='the size of time legend in multichannel', required=False, default=10) 558 | parser.add_argument( 559 | '--tick_format', type=str, help='the tick format of time legend ', required=False, default=None) 560 | parser.add_argument( 561 | '--time_tick_nb', type=int, help='the number of time tick', required=False) 562 | parser.add_argument( 563 | '--without-decoration', required=False, action='store_true', 564 | help=('the graph window will have no decorations. that means the ' 565 | 'window is not controlled by the window manager and can only ' 566 | 'be closed by killing the respective process.')) 567 | parser.add_argument( 568 | '--line_plot', help='regular real time plot for single station', required=False, action='store_true') 569 | parser.add_argument( 570 | '--rainbow', help='', required=False, action='store_true') 571 | parser.add_argument( 572 | '--nb_rainbow_colors', help='the numbers of colors for rainbow mode', required=False, default=10) 573 | parser.add_argument( 574 | '--update_time', 575 | help='time in seconds between each graphic update.' 576 | ' The following suffixes can be used as well: "s" for seconds, ' 577 | '"m" for minutes, "h" for hours and "d" for days.', 578 | required=False, default=10, 579 | type=_parse_time_with_suffix_to_seconds) 580 | parser.add_argument('--events', required=False, default=None, type=float, 581 | help='plot events using obspy.neries, specify minimum magnitude') 582 | parser.add_argument( 583 | '--events_update_time', required=False, default=10, 584 | help='time in minutes between each event data update. ' 585 | ' The following suffixes can be used as well: "s" for seconds, ' 586 | '"m" for minutes, "h" for hours and "d" for days.', 587 | type=_parse_time_with_suffix_to_minutes) 588 | parser.add_argument('-f', '--fullscreen', default=False, 589 | action="store_true", 590 | help='set to full screen on startup') 591 | parser.add_argument('-v', '--verbose', default=False, 592 | action="store_true", dest="verbose", 593 | help='show verbose debugging output') 594 | parser.add_argument('--force', default=False, action="store_true", 595 | help='skip warning message and confirmation prompt ' 596 | 'when opening a window without decoration') 597 | # parse the arguments 598 | args = parser.parse_args() 599 | 600 | if args.verbose: 601 | loglevel = logging.DEBUG 602 | else: 603 | loglevel = logging.CRITICAL 604 | logging.basicConfig(level=loglevel) 605 | 606 | # before anything else: warn user about window without decoration 607 | if args.without_decoration and not args.force: 608 | warning_ = ("Warning: You are about to open a window without " 609 | "decoration that is not controlled via your Window " 610 | "Manager. You can exit with -C (as long as you do " 611 | "not switch to another window with e.g. -)." 612 | "\n\nType 'y' to continue.. ") 613 | if input_func(warning_) != "y": 614 | print("Aborting.") 615 | sys.exit() 616 | 617 | now = UTCDateTime() 618 | stream = Stream() 619 | events = Catalog() 620 | lock = threading.Lock() 621 | 622 | # cl is the seedlink client 623 | seedlink_client = SeedlinkUpdater(stream, myargs=args, lock=lock) 624 | seedlink_client.slconn.set_sl_address(args.seedlink_server) 625 | seedlink_client.multiselect = args.seedlink_streams 626 | 627 | # tes if drum plot or line plot 628 | if any([x in args.seedlink_streams for x in ", ?*"]) or args.line_plot: 629 | drum_plot = False 630 | if args.time_tick_nb is None: 631 | args.time_tick_nb = 5 632 | if args.tick_format is None: 633 | args.tick_format = '%H:%M:%S' 634 | round_start = UTCDateTime(now.year, now.month, now.day, now.hour, 0, 0) 635 | round_start = round_start + 3600 - args.backtrace_time 636 | seedlink_client.begin_time = (round_start).format_seedlink() 637 | 638 | else: 639 | drum_plot = True 640 | if args.time_tick_nb is None: 641 | args.time_tick_nb = 13 642 | if args.tick_format is None: 643 | args.tick_format = '%d/%m/%y %Hh' 644 | seedlink_client.begin_time = (now - args.backtrace_time).format_seedlink() 645 | 646 | seedlink_client.initialize() 647 | ids = seedlink_client.getTraceIDs() 648 | # start cl in a thread 649 | thread = threading.Thread(target=seedlink_client.run) 650 | thread.setDaemon(True) 651 | thread.start() 652 | 653 | # start another thread for event updating if requested 654 | if args.events is not None: 655 | event_updater = EventUpdater( 656 | stream=stream, events=events, myargs=args, lock=lock) 657 | thread = threading.Thread(target=event_updater.run) 658 | thread.setDaemon(True) 659 | thread.start() 660 | 661 | # Wait few seconds to get data for the first plot 662 | time.sleep(2) 663 | 664 | master = SeedlinkPlotter(stream=stream, events=events, myargs=args, 665 | lock=lock, drum_plot=drum_plot, 666 | trace_ids=ids) 667 | master.mainloop() 668 | 669 | if __name__ == '__main__': 670 | main() 671 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | INSTALL_REQUIRES = [ 4 | 'numpy', 5 | 'scipy', 6 | 'matplotlib>=2.0.0', 7 | 'obspy>=1.3.0'] 8 | 9 | setup( 10 | name="seedlink-plotter", 11 | version="0.2.1", 12 | description="Plot data acquired in realtime in from a seedlink server.", 13 | author="Sebastien Bonaime, Lion Krischer, Tobias Megies", 14 | author_email="bonaime@ipgp.fr", 15 | url="https://github.com/bonaime/seedlink_plotter", 16 | download_url="https://github.com/bonaime/seedlink_plotter.git", 17 | install_requires=INSTALL_REQUIRES, 18 | python_requires='>3.7.0', 19 | keywords=["Seedlink", "ObsPy", "Seismology", "Plotting", "Realtime"], 20 | packages=["seedlink_plotter"], 21 | package_data={'seedlink_plotter': ['favicon.gif']}, 22 | entry_points={ 23 | 'console_scripts': 24 | ['seedlink-plotter = seedlink_plotter.seedlink_plotter:main'], 25 | }, 26 | classifiers=[ 27 | "Programming Language :: Python", 28 | "Development Status :: 4 - Beta", 29 | "Intended Audience :: Developers", 30 | "License :: OSI Approved :: GNU Library or " + 31 | "Lesser General Public License (LGPL)", 32 | "Operating System :: OS Independent", 33 | "Topic :: Software Development :: Libraries :: Python Modules", 34 | ], 35 | long_description="""\ 36 | Seedlink-plotter is a tool to plot seismological waveform data acquired from a 37 | Seedlink server. The plots are continuously updated as recent data is acquired 38 | in realtime. 39 | """ 40 | ) 41 | --------------------------------------------------------------------------------