diff --git a/documentation/01 _ Table of Contents.help.md b/documentation/01 _ Table of Contents.help.md
new file mode 100644
index 0000000000000000000000000000000000000000..336457508cd3e0ae4b77cff1983219fe966dcd98
--- /dev/null
+++ b/documentation/01 _ Table of Contents.help.md	
@@ -0,0 +1,18 @@
+# SOH Station Viewer Documentation
+
+Welcome to the SOH Station Viewer documentation. Here you will find usage guides and other useful information in navigating and using this software.
+
+On the left-hand side you will find a list of currently available help topics.
+
+The home button can be used to return to this page at any time.
+
+# Table of Contents
+
++ [Table of Contents](01%20_%20Table%20of%20Contents.help.md)
+
++ [Shortcuts](02%20_%20Shortcuts.help.md)
+
++ [How to Use Help](03%20_%20How%20to%20Use%20Help.help.md)
+
++ [Search SOH n LOG](04%20_%20Search%20SOH%20n%20LOG.help.md)
+
diff --git a/documentation/02 _ Shortcuts.help.md b/documentation/02 _ Shortcuts.help.md
new file mode 100644
index 0000000000000000000000000000000000000000..2469cc6b745385d36a5b0d03290fd1f7725689b8
--- /dev/null
+++ b/documentation/02 _ Shortcuts.help.md	
@@ -0,0 +1,13 @@
+# Shortcuts
+
+<br />
+
+Below is a list of available keyboard shortcuts for commonly used operations and
+forms.
+
+<br />
+
+| Shortcut | Combination |
+| -------- | ----------- |
+| Ctrl + I | Open Documentation Viewer                               |
+| Ctrl + F | Open folder (Change data directory to another location) |
diff --git a/documentation/03 _ How to Use Help.help.md b/documentation/03 _ How to Use Help.help.md
new file mode 100644
index 0000000000000000000000000000000000000000..603fb1e2bd81333050153e1105f68870c051cf98
--- /dev/null
+++ b/documentation/03 _ How to Use Help.help.md	
@@ -0,0 +1,75 @@
+
+
+# How to Use Help
+
+---------------------------
+---------------------------
+
+## How to open
+Go to Menu - Help and click on "Documentation".
+
+<br />
+
+---------------------------
+## User interface
+
+<br />
+
+### Search box
+The box to enter search text is placed at the top of the dialog.
+
+<br />
+
+### Search Through All Documents button
+The button to search through all documents for search text located on the right of Search box.
+
+<br />
+
+### Toolbar
+
++ <img alt="Navigate to Table of Contents" src="images/help/table_contents.png" width="30" />    **Navigate to Table of Contents**: Go to 'Table of Contents' page.
+
++ <img alt="Recreate Table of Contents" src="images/help/recreate_table_contents.png" width="30" />    **Recreate Table of Contents**: If links in 'Table of Contents' are broken, the page can be recreated by clicking this button.
+
++ <img alt="Search Previous" src="images/help/search_previous.png" width="30" />    **Search Previous**: Highlight the previous found text and the view roll to its position.
+
++ <img alt="Search Next" src="images/help/search_next.png" width="30" />    **Search Next**: Highlight the next found text and the view roll to its position.
+
++ <img alt="Search Next" src="images/help/search_all_doc.png" width="30" />    **Navigate to Search Through All Documents**: Go to 'Search Through All Documents' result page. 
+
+<br />
+
+### Document list
+Is the list of all help documents located on the left of Help Dialog.
+
+<br />
+
+### Document View
+Occupies the largest section of Help Dialog located on the right to display the content of the current selected document.
+
+<br />
+
+---------------------------
+## How to search in Help Dialog
+
+<br />
+
+### Search for a text on the current document
+Type a searched text, the first text found will be highlighted in the Document View and the view scrolls to its location.
+
+User can traverse back and forth the view to look for previous or next search text using **Search Previous** or **Search Next** button.
+
+<br />
+
+### Search for all documents that contain the searched text
+Type a searched text, click the button 'Search Through All Documents'.
+
+A list of all documents that contain the searched text will be created in document 'Search Through All Documents'.
+
+The document will be brought to Document View.
+
+Click on the link of a document in 'Search Through All Documents' will bring its to Document View.
+
+User can go back to 'Search Through All Documents' document any time by clicking on the name on Document List.
+
+<br />
diff --git a/documentation/04 _ Search SOH n LOG.help.md b/documentation/04 _ Search SOH n LOG.help.md
new file mode 100644
index 0000000000000000000000000000000000000000..6d764f3ced4d33324b94b980f2829616bd3d71af
--- /dev/null
+++ b/documentation/04 _ Search SOH n LOG.help.md	
@@ -0,0 +1,67 @@
+# Search Messages
+
+---------------------------
+---------------------------
+
+## How to open
+Search Messages dialog will open by itself after a data set are done with reading and plotting.
+If it is closed by any reason, user can open it by go to Menu - Forms, and click on "Search Messages"
+
+<br />
+
+---------------------------
+## User interface
+
+<br />
+
+### Search box
+The box to enter search text is placed at the top of the dialog.
+
+<br />
+
+### Toolbar
+
++ <img alt="To Selected" src="images/search_messages/to_selected.png" width="30" />    **To Selected**: The current table rolls back to the current selected line.
+
++ <img alt="Search Previous" src="images/search_messages/search_previous.png" width="30" />    **Search Previous**: The current table rolls to the previous line that contains the searched text.
+
++ <img alt="Search Next" src="images/search_messages/search_next.png" width="30" />    **Search Next**: The current table rolls to the next line that contains the searched text.
+
++ <img alt="Save" src="images/search_messages/save.png" width="30" />    **Save**: Save the content of the table to a text file.
+
+<br />
+
+### Tabs
+
++ **Search SOH Lines** tab: to list all lines that includes searched text with its channel 
++ **Processing Logs** tab: to list all processing logs with its log type and color of each line depends on its log type.
++ Each of the following tab is for one of SOH LOG channels
+
+<br />
+
+### Info box
+Is the box to display the info of the selected line.
+
+<br />
+
+---------------------------
+## How to search
+
+<br />
+
+### Search for a text 
+Type a searched text, the searched text will be highlighted through the table and the current table scrolls to the first line that contains the searched text.
+
+User can traverse back and forth the current tab to look for previous or next search text using **Search Previous** or **Search Next** button.
+
+<br />
+
+### Filter all lines with searched text
+The first tab **Search SOH Lines** is for filtering all lines that contains the searched text.
+
+<br />
+
+### Interaction with RT130 SOH channel's data point
+When user click on a clickable data point on a SOH channel of RT130, SOH tab will be focused and the line corresponding to the data point will be brought up to view.
+
+<br />
\ No newline at end of file
diff --git a/documentation/99 _ test.md b/documentation/99 _ test.md
new file mode 100644
index 0000000000000000000000000000000000000000..7ef0655b760ac6880ab28c7b87f54ad34c2bb4ae
--- /dev/null
+++ b/documentation/99 _ test.md	
@@ -0,0 +1,77 @@
+Document Title?
+===
+# Testing markdown features supported by Qt
+To show this file in help, change extension to ".help.md"
+[Links to other documents?](01 _ Table of Contents.md)
+1. Numbered List
+    1. Sublist
+    2. Another Entry
+        1. Another Sublist
+
+* Bulleted list
+    * Sublist
+        * Another Sublist
+
+# Section
+## Subsection
+### Sub-subsection
+#### Subsections all the way down
+
+*Emphasis*
+
+**Bold**
+
+***Bold-Emphasis***
+
+~strikethrough?~
+
+> This is a quote
+
+```
+printf("%s\n", codeFences.doTheyWork ? "Success!" : "Oof.");
+```
+
+```c
+printf("%s\n", syntaxHighlighting.doesItWork ? "Success!" : "Oof.");
+```
+
+---
+^ This is a horizontal line
+
+v This is an image
+![An Image?](images/image.jpg)
+
+---
+Another horizontal line
+
+Is there a footnote? [^1]
+
+[^1]: Maybe.
+
+Definition Lists
+: Do they work?
+
+Task Lists? 
+- [ ] Yes
+- [ ] No
+- [x] Maybe
+
+==Highlighting==? Nope.
+
+Subscripts? x~0 Nope.
+
+Superscripts? ax^2 + bx + c = 0. Also Nope.
+
+Html?
+x<sub>0</sub> Yup.
+x<sup>2</sup> Yup.
+
+Tables?
+
+~Not supported.~
+Just kidding, they totally are, and I just didn't do it right.
+
+| Is this | a table?     |
+| ------- | ------------ |
+| Perhaps | Perhaps not. |
+
diff --git a/documentation/Search Results.md b/documentation/Search Results.md
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/documentation/images/help/home.png b/documentation/images/help/home.png
new file mode 100644
index 0000000000000000000000000000000000000000..2ee44ccfe6905c2567ef9270877c12fa48a6817a
Binary files /dev/null and b/documentation/images/help/home.png differ
diff --git a/documentation/images/help/recreate_table_contents.png b/documentation/images/help/recreate_table_contents.png
new file mode 100644
index 0000000000000000000000000000000000000000..34ab02a858eb4da3d62325cff47e1bd56dc90186
Binary files /dev/null and b/documentation/images/help/recreate_table_contents.png differ
diff --git a/documentation/images/help/search_next.png b/documentation/images/help/search_next.png
new file mode 100644
index 0000000000000000000000000000000000000000..07f642b938b2098dd1e64ff6edd81ca4c94fa6f8
Binary files /dev/null and b/documentation/images/help/search_next.png differ
diff --git a/documentation/images/help/search_previous.png b/documentation/images/help/search_previous.png
new file mode 100644
index 0000000000000000000000000000000000000000..1a4c53854ad24ac1ad8983bd6936a008217cb9c4
Binary files /dev/null and b/documentation/images/help/search_previous.png differ
diff --git a/documentation/images/help/search_results.png b/documentation/images/help/search_results.png
new file mode 100644
index 0000000000000000000000000000000000000000..80f6ab53e137413202a83196a07f66511b09e6cb
Binary files /dev/null and b/documentation/images/help/search_results.png differ
diff --git a/documentation/images/help/table_contents.png b/documentation/images/help/table_contents.png
new file mode 100644
index 0000000000000000000000000000000000000000..a4fa90caa1e7a173fad809c01d46a0fb7b248ed7
Binary files /dev/null and b/documentation/images/help/table_contents.png differ
diff --git a/documentation/images/image.jpg b/documentation/images/image.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..d7b974f761cf912f46030febaf994d10df8a360b
Binary files /dev/null and b/documentation/images/image.jpg differ
diff --git a/documentation/images/search_messages/save.png b/documentation/images/search_messages/save.png
new file mode 100644
index 0000000000000000000000000000000000000000..2a694fe7cdc510b093a22f8428a290ac614f3bf6
Binary files /dev/null and b/documentation/images/search_messages/save.png differ
diff --git a/documentation/images/search_messages/search_next.png b/documentation/images/search_messages/search_next.png
new file mode 100644
index 0000000000000000000000000000000000000000..07f642b938b2098dd1e64ff6edd81ca4c94fa6f8
Binary files /dev/null and b/documentation/images/search_messages/search_next.png differ
diff --git a/documentation/images/search_messages/search_previous.png b/documentation/images/search_messages/search_previous.png
new file mode 100644
index 0000000000000000000000000000000000000000..1a4c53854ad24ac1ad8983bd6936a008217cb9c4
Binary files /dev/null and b/documentation/images/search_messages/search_previous.png differ
diff --git a/documentation/images/search_messages/to_selected.png b/documentation/images/search_messages/to_selected.png
new file mode 100644
index 0000000000000000000000000000000000000000..b43bc4b8411d2b1bbb30cdda1514a5526dca0f50
Binary files /dev/null and b/documentation/images/search_messages/to_selected.png differ
diff --git a/images/home.png b/images/home.png
new file mode 100644
index 0000000000000000000000000000000000000000..2ee44ccfe6905c2567ef9270877c12fa48a6817a
Binary files /dev/null and b/images/home.png differ
diff --git a/images/recreate_table_contents.png b/images/recreate_table_contents.png
new file mode 100644
index 0000000000000000000000000000000000000000..34ab02a858eb4da3d62325cff47e1bd56dc90186
Binary files /dev/null and b/images/recreate_table_contents.png differ
diff --git a/images/search_results.png b/images/search_results.png
new file mode 100644
index 0000000000000000000000000000000000000000..fe2a583e5b7877450c38dba942c24a6d22079358
Binary files /dev/null and b/images/search_results.png differ
diff --git a/images/table_contents.png b/images/table_contents.png
new file mode 100644
index 0000000000000000000000000000000000000000..a4fa90caa1e7a173fad809c01d46a0fb7b248ed7
Binary files /dev/null and b/images/table_contents.png differ
diff --git a/images/to_selected.png b/images/to_selected.png
new file mode 100644
index 0000000000000000000000000000000000000000..b43bc4b8411d2b1bbb30cdda1514a5526dca0f50
Binary files /dev/null and b/images/to_selected.png differ
diff --git a/sohstationviewer/conf/constants.py b/sohstationviewer/conf/constants.py
index a537627f795451653f950600cdc4a571eee93b2b..e9a81414adaed80b306ab58da3dfb1b121c6580e 100644
--- a/sohstationviewer/conf/constants.py
+++ b/sohstationviewer/conf/constants.py
@@ -34,6 +34,11 @@ NO_5M_DAY = 288
 # total of 5 minutes in an hour
 NO_5M_1H = int(60 * 60 / 300)
 
+# name of table of contents file
+TABLE_CONTENTS = "01 _ Table of Contents.help.md"
+
+# name of search through all documents file
+SEARCH_RESULTS = "Search Results.md"
 
 # ================================================================= #
 #                      PLOTTING CONSTANT
diff --git a/sohstationviewer/controller/plottingData.py b/sohstationviewer/controller/plottingData.py
index c0ec746ceca774b0a1159fe030fe5937d9922fe6..6849df2aacb6b9f2eefd077fbe7fba2a8c3d2de0 100755
--- a/sohstationviewer/controller/plottingData.py
+++ b/sohstationviewer/controller/plottingData.py
@@ -6,7 +6,9 @@ import math
 from typing import List, Union, Optional, Tuple, Dict
 
 from obspy import UTCDateTime
+
 from sohstationviewer.conf import constants as const
+from sohstationviewer.view.util.enums import LogType
 
 maxInt = 1E100
 maxFloat = 1.0E100
@@ -42,7 +44,7 @@ def getMassposValueColors(rangeOpt: str, chan_id: str, cMode: str,
                 f"{chan_id}: The current selected Mass Position color range is"
                 f" '{rangeOpt}' isn't allowable. The accept ranges are: "
                 f"{', '.join(MassPosVoltRanges.keys())}",
-                "error"
+                LogType.ERROR
             )
         )
         return
diff --git a/sohstationviewer/controller/processing.py b/sohstationviewer/controller/processing.py
index 81ac94fad9aae398e9961c90add2e9d1d392cacb..4a0e0e8914bf8c11c6c4bafc8a752ed6de96b058 100644
--- a/sohstationviewer/controller/processing.py
+++ b/sohstationviewer/controller/processing.py
@@ -1,11 +1,12 @@
 """
-Function that ignite from MainWindow, Dialogs to read data files for data,
+Function that ignite from main_window, Dialogs to read data files for data,
 channels, datatype
 """
 
 import os
 import json
 import re
+import traceback
 from pathlib import Path
 from typing import List, Set, Optional, Dict, Tuple
 
@@ -14,17 +15,26 @@ from obspy.core import read as read_ms
 from obspy.io.reftek.core import Reftek130Exception
 
 from sohstationviewer.model.mseed.mseed import MSeed
+from sohstationviewer.database.extract_data import get_signature_channels
 from sohstationviewer.model.data_type_model import DataTypeModel
-from sohstationviewer.database.extractData import signatureChannels
+
 from sohstationviewer.controller.util import validateFile, displayTrackingInfo
 
+from sohstationviewer.view.util.enums import LogType
+
 
 def loadData(dataType: str, tracking_box: QTextBrowser, listOfDir: List[str],
              reqWFChans: List[str] = [], reqSOHChans: List[str] = [],
              readStart: Optional[float] = None,
              readEnd: Optional[float] = None) -> DataTypeModel:
     """
-    Go through root dir and read all files in that dir and its subdirs
+    Load the data stored in listOfDir and store it in a DataTypeModel object.
+    The concrete class of the data object is based on dataType. Run on the same
+    thread as its caller, and so will block the GUI if called on the main
+    thread. It is advisable to use model.data_loader.DataLoader to load data
+    unless it is necessary to load data in the main thread (e.g. if there is
+    a need to access the call stack).
+
     :param dataType: str - type of data read
     :param tracking_box: QTextBrowser - widget to display tracking info
     :param listOfDir: [str,] - list of directories selected by users
@@ -43,9 +53,11 @@ def loadData(dataType: str, tracking_box: QTextBrowser, listOfDir: List[str],
                     dataType, tracking_box, d,
                     reqWFChans=reqWFChans, reqSOHChans=reqSOHChans,
                     readStart=readStart, readEnd=readEnd)
-            except Exception as e:
-                msg = f"Dir {d} can't be read due to error: {str(e)}"
-                displayTrackingInfo(tracking_box, msg, "Warning")
+            except Exception:
+                fmt = traceback.format_exc()
+                msg = f"Dir {d} can't be read due to error: {str(fmt)}"
+                displayTrackingInfo(tracking_box, msg, LogType.WARNING)
+
             # if dataObject.hasData():
             #     continue
             # If no data can be read from the first dir, throw exception
@@ -97,8 +109,7 @@ def detectDataType(tracking_box: QTextBrowser, listOfDir: List[str]
             return None with a warning message
         + if data type found, return data_type,
     """
-
-    sign_chan_dataType_dict = signatureChannels()
+    sign_chan_dataType_dict = get_signature_channels()
 
     dirDataTypeDict = {}
     for d in listOfDir:
@@ -126,13 +137,13 @@ def detectDataType(tracking_box: QTextBrowser, listOfDir: List[str]
         msg = (f"There are more than one types of data detected:\n"
                f"{dirDataTypeStr}\n\n"
                f"Please have only data that related to each other.")
-        displayTrackingInfo(tracking_box, msg, "error")
+        displayTrackingInfo(tracking_box, msg, LogType.ERROR)
         return
 
     elif dataTypeList == {'Unknown'}:
         msg = ("There are no known data detected.\n"
                "Please select different folder(s).")
-        displayTrackingInfo(tracking_box, msg, "error")
+        displayTrackingInfo(tracking_box, msg, LogType.ERROR)
         return
 
     return list(dirDataTypeDict.values())[0][0]
diff --git a/sohstationviewer/controller/util.py b/sohstationviewer/controller/util.py
index 6f022a9943228f5d76e0ca8554ba1537e53cb8c8..2e85563750e4afa4a7f8cd2e02e7e870b509cee9 100644
--- a/sohstationviewer/controller/util.py
+++ b/sohstationviewer/controller/util.py
@@ -5,11 +5,14 @@ basic functions: format, validate, display tracking
 import os
 import re
 from datetime import datetime
+
+from PySide2 import QtCore
 from typing import Tuple
 
 from PySide2.QtWidgets import QTextBrowser
 from obspy import UTCDateTime
 import numpy as np
+from sohstationviewer.view.util.enums import LogType
 
 
 def validateFile(path2file: str, fileName: str):
@@ -29,8 +32,8 @@ def validateFile(path2file: str, fileName: str):
     return True
 
 
-def displayTrackingInfo(trackingBox: QTextBrowser, text: str,
-                        type: str = 'info'):
+@QtCore.Slot()
+def displayTrackingInfo(trackingBox: QTextBrowser, text: str, type: LogType = LogType.INFO):
     """
     Display text in the given widget with different background and text colors
     :param trackingBox: QTextBrowser - widget to display tracking info
@@ -40,14 +43,14 @@ def displayTrackingInfo(trackingBox: QTextBrowser, text: str,
     """
 
     if trackingBox is None:
-        print(f"{type}: {text}")
+        print(f"{type.name}: {text}")
         return
 
     msg = {'text': text}
-    if type == 'error':
+    if type == LogType.ERROR:
         msg['color'] = 'white'
         msg['bgcolor'] = '#e46269'
-    elif type == 'warning':
+    elif type == LogType.WARNING:
         msg['color'] = '#ffd966'
         msg['bgcolor'] = 'orange'
     else:
diff --git a/sohstationviewer/database/extractData.py b/sohstationviewer/database/extractData.py
deleted file mode 100755
index f12209bc00bf7cf9410b8d87efd25323cc13dcb2..0000000000000000000000000000000000000000
--- a/sohstationviewer/database/extractData.py
+++ /dev/null
@@ -1,146 +0,0 @@
-"""
-Read DB for necessary information
-"""
-
-from sohstationviewer.database.proccessDB import executeDB_dict, executeDB
-from sohstationviewer.conf.dbSettings import dbConf
-
-
-def getChanPlotInfo(orgChan, data_type):
-    """
-     Given chanID read from raw data file and detected dataType
-     Return plotting info from DB for that channel
-
-    :param orgChan: str - channel ID from raw data file
-    :param data_type: str - data type detected
-    :return chanInfo: dict - plotting info from DB for that channel
-    """
-    chan = orgChan
-    if orgChan.startswith('EX'):
-        chan = 'EX?'
-    if orgChan.startswith('VM'):
-        chan = 'VM?'
-    if orgChan.startswith('MP'):
-        chan = 'MP?'
-    if orgChan.startswith('Event DS'):
-        chan = 'Event DS?'
-    if orgChan.startswith('DS'):
-        chan = 'DS?'
-    if orgChan.startswith('Disk Usage'):
-        chan = 'Disk Usage?'
-    if dbConf['seisRE'].match(chan):
-        chan = 'SEISMIC'
-
-    o_sql = ("SELECT channel, plotType, height, unit, linkedChan,"
-             " convertFactor, label, fixPoint, valueColors "
-             "FROM Channels as C, Parameters as P")
-    if data_type == 'Unknown':
-        sql = f"{o_sql} WHERE channel='{chan}' and C.param=P.param"
-    else:
-        sql = (f"{o_sql} WHERE channel='{chan}' and C.param=P.param"
-               f" and dataType='{data_type}'")
-    # print("SQL:", sql)
-    chanInfo = executeDB_dict(sql)
-
-    if len(chanInfo) == 0:
-        chanInfo = executeDB_dict(
-            f"{o_sql} WHERE channel='DEFAULT' and C.param=P.param")
-    else:
-        if chanInfo[0]['channel'] == 'SEISMIC':
-            chanInfo[0]['label'] = dbConf['seisLabel'][orgChan[-1]]
-        chanInfo[0]['channel'] = orgChan
-
-    chanInfo[0]['label'] = (
-        '' if chanInfo[0]['label'] is None else chanInfo[0]['label'])
-    chanInfo[0]['unit'] = (
-        '' if chanInfo[0]['unit'] is None else chanInfo[0]['unit'])
-    chanInfo[0]['fixPoint'] = (
-        0 if chanInfo[0]['fixPoint'] is None else chanInfo[0]['fixPoint'])
-    if chanInfo[0]['label'].strip() == '':
-        chanInfo[0]['label'] = chanInfo[0]['channel']
-    else:
-        chanInfo[0]['label'] = '-'.join([chanInfo[0]['channel'],
-                                         chanInfo[0]['label']])
-    if chanInfo[0]['label'].strip() == 'DEFAULT':
-        chanInfo[0]['label'] = 'DEFAULT-' + orgChan
-    return chanInfo[0]
-
-
-def getWFPlotInfo(orgChan):
-    """
-    Similar to getChanPlotInfo but for waveform channel (param=Seismic data)
-    :param orgChan: str - channel ID from raw data file
-    :return chanInfo: dict - plotting info from DB for that waveform channel
-    """
-    chanInfo = executeDB_dict(
-        "SELECT * FROM Parameters WHERE param='Seismic data'")[0]
-    chanInfo['label'] = getChanLabel(orgChan)
-    chanInfo['unit'] = ''
-    chanInfo['channel'] = 'SEISMIC'
-    return chanInfo
-
-
-def getChanLabel(chanID):
-    """
-    Get waveform channel label for the given chan_id in which:
-        + RT130's start with DS, remain unchanged
-        + MSEED's need to change last character according to dbConf[seisLabel]
-    :param chanID: str - channel name
-    :return label: str - plot's label for the channel
-    """
-    if chanID.startswith("DS"):
-        label = chanID
-    else:
-        label = chanID + '-' + dbConf['seisLabel'][chanID[-1]]
-    return label
-
-
-def signatureChannels():
-    """
-    Get channels that are unique for datatype.
-    :return: {str: str,} - the dict {channel: dataType,}
-        in which channel is unique for dataType
-    """
-    sql = ("SELECT channel, dataType FROM Channels where channel in"
-           "(SELECT channel FROM Channels GROUP BY channel"
-           " HAVING COUNT(channel)=1)")
-    rows = executeDB_dict(sql)
-    sign_chan_dataType_dict = {r['channel']: r['dataType'] for r in rows}
-    return sign_chan_dataType_dict
-
-
-def getColorDef():
-    """
-    Get TPS Color definition from DB
-    :return [str,]: list of color names
-    """
-    sql = "SELECT color FROM TPS_ColorDefinition ORDER BY name ASC"
-    rows = executeDB(sql)
-    return [r[0] for r in rows]
-
-
-def getColorRanges():
-    """
-    Get TPS color range from DB
-    :return rangeNames: [str,] - 'antarctica'/'low'/'med'/'high'
-    :return allSquareCounts: [[int,],] - time-power-squared range
-        (base_count * 10 ** (level-1)) ** 2
-    :return clrLabels: [str,] - labels that define count range for colors
-    """
-    sql = "SELECT name, baseCounts FROM TPS_ColorRange"
-    rows = executeDB(sql)
-    rangeNames = [r[0] for r in rows]
-    baseCounts = [r[1] for r in rows]
-    allSquareCounts = []
-    clrLabels = []
-    cnt = 0
-    # 7 : number of color definition, not include 'E'
-    for idx, bc in enumerate(baseCounts):
-        allSquareCounts.append([0] * 7)
-        clrLabels.append(['0 counts'])
-        for cidx in range(1, 7):
-            cnt = (bc * 10 ** (cidx - 1))
-            allSquareCounts[idx][cidx] = cnt ** 2
-            clrLabels[idx].append("+/- {:,} counts".format(cnt))
-        clrLabels[idx].append("> {:,} counts".format(cnt))
-    return rangeNames, allSquareCounts, clrLabels
diff --git a/sohstationviewer/database/extract_data.py b/sohstationviewer/database/extract_data.py
new file mode 100755
index 0000000000000000000000000000000000000000..934d3afc7f66eb89cff9110290ddc5982b4355a3
--- /dev/null
+++ b/sohstationviewer/database/extract_data.py
@@ -0,0 +1,114 @@
+
+from sohstationviewer.database.process_db import execute_db_dict, execute_db
+from sohstationviewer.conf.dbSettings import dbConf
+
+
+def get_chan_plot_info(org_chan, data_type):
+    """
+    Given chanID read from raw data file and detected dataType
+    Return plotting info from DB for that channel
+    """
+    chan = org_chan
+    if org_chan.startswith('EX'):
+        chan = 'EX?'
+    if org_chan.startswith('VM'):
+        chan = 'VM?'
+    if org_chan.startswith('MP'):
+        chan = 'MP?'
+    if org_chan.startswith('Event DS'):
+        chan = 'Event DS?'
+    if org_chan.startswith('DS'):
+        chan = 'DS?'
+    if org_chan.startswith('Disk Usage'):
+        chan = 'Disk Usage?'
+    if dbConf['seisRE'].match(chan):
+        chan = 'SEISMIC'
+
+    o_sql = ("SELECT channel, plotType, height, unit, linkedChan,"
+             " convertFactor, label, fixPoint, valueColors "
+             "FROM Channels as C, Parameters as P")
+    if data_type == 'Unknown':
+        sql = f"{o_sql} WHERE channel='{chan}' and C.param=P.param"
+    else:
+        sql = (f"{o_sql} WHERE channel='{chan}' and C.param=P.param"
+               f" and dataType='{data_type}'")
+    # print("SQL:", sql)
+    chan_info = execute_db_dict(sql)
+
+    if len(chan_info) == 0:
+        chan_info = execute_db_dict(
+            f"{o_sql} WHERE channel='DEFAULT' and C.param=P.param")
+    else:
+        if chan_info[0]['channel'] == 'SEISMIC':
+            chan_info[0]['label'] = dbConf['seisLabel'][org_chan[-1]]
+        chan_info[0]['channel'] = org_chan
+
+    chan_info[0]['label'] = (
+        '' if chan_info[0]['label'] is None else chan_info[0]['label'])
+    chan_info[0]['unit'] = (
+        '' if chan_info[0]['unit'] is None else chan_info[0]['unit'])
+    chan_info[0]['fixPoint'] = (
+        0 if chan_info[0]['fixPoint'] is None else chan_info[0]['fixPoint'])
+    if chan_info[0]['label'].strip() == '':
+        chan_info[0]['label'] = chan_info[0]['channel']
+    else:
+        chan_info[0]['label'] = '-'.join([chan_info[0]['channel'],
+                                         chan_info[0]['label']])
+    if chan_info[0]['label'].strip() == 'DEFAULT':
+        chan_info[0]['label'] = 'DEFAULT-' + org_chan
+    return chan_info[0]
+
+
+def get_wf_plot_info(org_chan):
+    chan_info = execute_db_dict(
+        "SELECT * FROM Parameters WHERE param='Seismic data'")
+    chan_info[0]['label'] = get_chan_label(org_chan)
+    chan_info[0]['unit'] = ''
+    chan_info[0]['channel'] = 'SEISMIC'
+    return chan_info[0]
+
+
+def get_chan_label(chan_id):
+    if chan_id.startswith("DS"):
+        label = chan_id
+    else:
+        label = chan_id + '-' + dbConf['seisLabel'][chan_id[-1]]
+    return label
+
+
+def get_signature_channels():
+    """
+    return the dict {channel: dataType} in which channel is unique for dataType
+    """
+    sql = ("SELECT channel, dataType FROM Channels where channel in"
+           "(SELECT channel FROM Channels GROUP BY channel"
+           " HAVING COUNT(channel)=1)")
+    rows = execute_db_dict(sql)
+    sign_chan_data_type_dict = {r['channel']: r['dataType'] for r in rows}
+    return sign_chan_data_type_dict
+
+
+def get_color_def():
+    sql = "SELECT color FROM TPS_ColorDefinition ORDER BY name ASC"
+    rows = execute_db(sql)
+    return [r[0] for r in rows]
+
+
+def get_color_ranges():
+    sql = "SELECT name, baseCounts FROM TPS_ColorRange"
+    rows = execute_db(sql)
+    range_names = [r[0] for r in rows]
+    base_counts = [r[1] for r in rows]
+    all_square_counts = []
+    clr_labels = []
+    cnt = 0
+    # 7 : number of color definition, not include 'E'
+    for idx, bc in enumerate(base_counts):
+        all_square_counts.append([0] * 7)
+        clr_labels.append(['0 counts'])
+        for c_idx in range(1, 7):
+            cnt = (bc * 10 ** (c_idx - 1))
+            all_square_counts[idx][c_idx] = cnt ** 2
+            clr_labels[idx].append("+/- {:,} counts".format(cnt))
+        clr_labels[idx].append("> {:,} counts".format(cnt))
+    return range_names, all_square_counts, clr_labels
diff --git a/sohstationviewer/database/proccessDB.py b/sohstationviewer/database/process_db.py
similarity index 92%
rename from sohstationviewer/database/proccessDB.py
rename to sohstationviewer/database/process_db.py
index b28756639950b3d7c7adf0d8b5880c35d31aafb0..b8b50fdc4c63e07c2c9c45a24fccbca5d3050e13 100755
--- a/sohstationviewer/database/proccessDB.py
+++ b/sohstationviewer/database/process_db.py
@@ -6,7 +6,7 @@ import sqlite3
 from sohstationviewer.conf.dbSettings import dbConf
 
 
-def executeDB(sql):
+def execute_db(sql):
     """
     Execute or fetch data from DB
     :param sql: str - request string to execute or fetch data from database
@@ -25,7 +25,7 @@ def executeDB(sql):
     return rows
 
 
-def executeDB_dict(sql):
+def execute_db_dict(sql):
     """
     Fetch data and return rows in dictionary with fields as keys
     :param sql: str - request string to fetch data from database
@@ -44,11 +44,11 @@ def executeDB_dict(sql):
     return [dict(row) for row in rows]
 
 
-def trunc_addDB(table, sqls):
+def trunc_add_db(table, sql_list):
     """
     Truncate table and refill with new data
     :param table: str - name of data table to process
-    :param sqls: [str, ] - list of INSERT query to add values to table
+    :param sql_list: [str, ] - list of INSERT query to add values to table
     :return: str: error message if not successful
         or bool(True): if successful
     """
@@ -57,7 +57,7 @@ def trunc_addDB(table, sqls):
         cur = conn.cursor()
         cur.execute('BEGIN')
         cur.execute(f'DELETE FROM {table}')
-        for sql in sqls:
+        for sql in sql_list:
             cur.execute(sql)
         cur.execute('COMMIT')
     except sqlite3.Error as e:
diff --git a/sohstationviewer/database/soh.db b/sohstationviewer/database/soh.db
index 7dd0ed988adb7a10d48627f6aa8f6125d99e85ad..8a34e889f0fc4c48d561a774f1f567f82ce6fc9e 100755
Binary files a/sohstationviewer/database/soh.db and b/sohstationviewer/database/soh.db differ
diff --git a/sohstationviewer/model/data_loader.py b/sohstationviewer/model/data_loader.py
new file mode 100644
index 0000000000000000000000000000000000000000..732d8ede8d05369a10b45a1f8a57acb58bba0987
--- /dev/null
+++ b/sohstationviewer/model/data_loader.py
@@ -0,0 +1,212 @@
+"""
+This module provides access to a class that loads data in a separate thread.
+"""
+import traceback
+from pathlib import Path
+from typing import Union, List, Optional
+
+from PySide2 import QtCore, QtWidgets
+
+from sohstationviewer.conf import constants
+from sohstationviewer.controller.util import displayTrackingInfo
+from sohstationviewer.model.data_type_model import DataTypeModel, ThreadStopped
+from sohstationviewer.view.util.enums import LogType
+
+
+class DataLoaderWorker(QtCore.QObject):
+    """
+    The worker class that executes the code to load the data.
+    """
+    finished = QtCore.Signal(DataTypeModel)
+    failed = QtCore.Signal()
+    stopped = QtCore.Signal()
+    notification = QtCore.Signal(QtWidgets.QTextBrowser, str, str)
+    button_dialog = QtCore.Signal(str, list)
+    button_chosen = QtCore.Signal(int)
+
+    def __init__(self, data_type: str, tracking_box: QtWidgets.QTextBrowser,
+                 folder: str, req_wf_chans: Union[List[str], List[int]] = [],
+                 req_soh_chans: List[str] = [], read_start: float = 0,
+                 read_end: float = constants.HIGHEST_INT, parent_thread=None):
+        super().__init__()
+        self.data_type = data_type
+        self.tracking_box = tracking_box
+        self.folder = folder
+        self.req_wf_chans = req_wf_chans
+        self.req_soh_chans = req_soh_chans
+        self.read_start = read_start
+        self.read_end = read_end
+        self.parent_thread = parent_thread
+        # displayTrackingInfo updates a QtWidget, which can only be done in the
+        # main thread. Since self.run runs in a background thread, we need to
+        # use signal-slot mechanism to ensure that displayTrackingInfo runs in
+        # the main thread.
+        self.notification.connect(displayTrackingInfo)
+        self.end_msg = None
+
+    def run(self):
+        try:
+            if self.data_type == 'RT130':
+                from sohstationviewer.model.reftek.reftek import RT130
+                ObjectType = RT130
+            else:
+                from sohstationviewer.model.mseed.mseed import MSeed
+                ObjectType = MSeed
+            # Create data object without loading any data in order to connect
+            # its unpause slot to the loader's unpause signal
+            dataObject = ObjectType.get_empty_instance()
+            self.button_chosen.connect(dataObject.receive_pause_response,
+                                       type=QtCore.Qt.DirectConnection)
+            dataObject.__init__(
+                self.tracking_box, self.folder,
+                reqWFChans=self.req_wf_chans,
+                reqSOHhans=self.req_soh_chans, readStart=self.read_start,
+                readEnd=self.read_end, creator_thread=self.parent_thread,
+                notification_signal=self.notification,
+                pause_signal=self.button_dialog
+            )
+
+        except ThreadStopped:
+            self.end_msg = 'Data loading has been stopped'
+            self.stopped.emit()
+        except Exception:
+            fmt = traceback.format_exc()
+            self.end_msg = (f"Dir {self.folder} can't be read "
+                            f"due to error: {str(fmt)}")
+            self.failed.emit()
+        else:
+            self.end_msg = f'Finished loading data stored in {self.folder}'
+            self.finished.emit(dataObject)
+
+
+class DataLoader:
+    """
+    The class that coordinate the loading of data using multiple threads. The
+    code inside has to be encapsulated in a class because a connection between
+    a signal and a receiver is automatically disconnected when either of them
+    is deleted (e.g. goes out of scope).
+    """
+
+    def __init__(self):
+        self.running = False
+        self.thread: Optional[QtCore.QThread] = None
+        self.worker: Optional[DataLoaderWorker] = None
+
+    def init_loader(self, data_type: str, tracking_box: QtWidgets.QTextBrowser,
+                    list_of_dir: List[Union[str, Path]],
+                    req_wf_chans: Union[List[str], List[int]] = [],
+                    req_soh_chans: List[str] = [], read_start: float = 0,
+                    read_end: float = constants.HIGHEST_INT):
+        """
+        Initialize the data loader. Construct the thread and worker and connect
+        them together. Separated from the actual loading of the data to allow
+        the main window a chance to connect its slots to the data loader.
+
+        :param data_type: the type of data being loaded. 'RT130' for RT130 data
+            and 'Centaur', 'Pegasus', and 'Q330' for MSeed data.
+        :param tracking_box: the widget used to display tracking info
+        :param list_of_dir: list of directories selected by users
+        :param req_wf_chans: list of requested waveform channels
+        :param req_soh_chans: list of requested SOH channel
+        :param read_start: the time before which no data is read
+        :param read_end: the time after which no data is read
+        :return:
+        """
+        if self.running:
+            # TODO: implement showing an error window
+            print('Already running')
+            return False
+
+        self.running = True
+        self.thread = QtCore.QThread()
+        self.worker = DataLoaderWorker(
+            data_type,
+            tracking_box,
+            list_of_dir[0],  # Only work on one directory for now.
+            req_wf_chans=req_wf_chans,
+            req_soh_chans=req_soh_chans,
+            read_start=read_start,
+            read_end=read_end,
+            parent_thread=self.thread
+        )
+
+        self.connect_worker_signals()
+
+        self.worker.moveToThread(self.thread)
+
+    def connect_worker_signals(self):
+        """
+        Connect the signals of the data loader to the appropriate slots.
+        """
+        # Connection order from https://realpython.com/python-pyqt-qthread
+        self.thread.started.connect(self.worker.run)
+
+        self.worker.finished.connect(self.thread.quit)
+        self.worker.failed.connect(self.thread.quit)
+        self.worker.stopped.connect(self.thread.quit)
+
+        self.thread.finished.connect(self.thread.deleteLater)
+        self.thread.finished.connect(self.load_end)
+        self.thread.finished.connect(self.worker.deleteLater)
+
+        self.worker.button_dialog.connect(self.create_button_dialog)
+
+    def load_data(self):
+        """
+        Start the data loading thread.
+        """
+        self.thread.start()
+
+    @QtCore.Slot()
+    def load_end(self):
+        """
+        Cleans up after data loading ended. Called even if the loading fails or
+        is stopped.
+
+        Currently does the following:
+            - Set running state of self to False
+        """
+        displayTrackingInfo(self.worker.tracking_box,
+                            self.worker.end_msg, LogType.INFO)
+        print(self.worker.end_msg)
+        self.running = False
+
+    @QtCore.Slot()
+    def create_button_dialog(self, msg: str, button_labels: List[str]):
+        """
+        Create a modal dialog with buttons. Show the dialog and send the user's
+        choice to the data object being created.
+
+        :param msg: the instruction shown to the user
+        :type msg: str
+        :param button_labels: the list of labels that are shown on the buttons
+        :type button_labels: List[str]
+        """
+        msg_box = QtWidgets.QMessageBox()
+        msg_box.setText(msg)
+        buttons = []
+        for label in button_labels:
+            # RT130's labels have type Tuple[str, int], so we need to convert
+            # them to strings.
+            if not isinstance(label, str):
+                # When we convert a tuple to a string, any strings in the tuple
+                # will be surrounded by quotes in the result string. We remove
+                # those quotes before displaying them to the user for aesthetic
+                # reasons.
+                label = str(label).replace("'", '').replace('"', '')
+            buttons.append(
+                msg_box.addButton(label, QtWidgets.QMessageBox.ActionRole)
+            )
+        abortButton = msg_box.addButton(QtWidgets.QMessageBox.Abort)
+
+        msg_box.exec_()
+
+        if msg_box.clickedButton() == abortButton:
+            # The default choice is the first item, so we default to it if the
+            # user presses the abort button. An alternative choice is to stop
+            # when the user presses the abort button.
+            chosen_idx = 0
+        else:
+            chosen_idx = buttons.index(msg_box.clickedButton())
+
+        self.worker.button_chosen.emit(chosen_idx)
diff --git a/sohstationviewer/model/data_type_model.py b/sohstationviewer/model/data_type_model.py
index 81cb91d8b0f99449d455d221e1fd36f6f8296854..458e350f880c00b0498c5278c9e981d8f655a978 100644
--- a/sohstationviewer/model/data_type_model.py
+++ b/sohstationviewer/model/data_type_model.py
@@ -1,10 +1,16 @@
+from __future__ import annotations
 import os
 
 from tempfile import mkdtemp
 import shutil
+from typing import Optional
+
+from PySide2 import QtCore
 
 from sohstationviewer.controller.util import displayTrackingInfo
 from sohstationviewer.conf import constants
+from sohstationviewer.view.util.enums import LogType
+from sohstationviewer.database.process_db import execute_db
 
 
 class WrongDataTypeError(Exception):
@@ -12,10 +18,22 @@ class WrongDataTypeError(Exception):
         self.args = (args, kwargs)
 
 
+class ThreadStopped(Exception):
+    """
+    An exception that is raised when the user requests for the data loader
+    thread to be stopped.
+    """
+    def __init__(self, *args, **kwargs):
+        self.args = (args, kwargs)
+
+
 class DataTypeModel():
     def __init__(self, trackingBox, folder, readChanOnly=False,
                  reqWFChans=[], reqSOHChans=[],
                  readStart=0, readEnd=constants.HIGHEST_INT,
+                 creator_thread: Optional[QtCore.QThread] = None,
+                 notification_signal: Optional[QtCore.Signal] = None,
+                 pause_signal: Optional[QtCore.Signal] = None,
                  *args, **kwargs):
         """
         Super class for different data type to process data from data files
@@ -27,6 +45,13 @@ class DataTypeModel():
         :param reqSOHChans: list of str - requested SOH channel list
         :param readStart: float - requested start time to read
         :param readEnd: float - requested end time to read
+        :param creator_thread: the thread the current DataTypeModel instance is
+            being created in. If None, the DataTypeModel instance is being
+            created in the main thread
+        :param notification_signal: signal used to send notifications to the
+            main thread. Only not None when creator_thread is not None
+        :param pause_signal: signal used to notify the main thread that the
+            data loader is paused.
         """
         self.trackingBox = trackingBox
         self.dir = folder
@@ -35,7 +60,17 @@ class DataTypeModel():
         self.readChanOnly = readChanOnly
         self.readStart = readStart
         self.readEnd = readEnd
-
+        if creator_thread is None:
+            err_msg = (
+                'A signal is not None while running in main thread'
+            )
+            assert notification_signal is None, err_msg
+            assert pause_signal is None, err_msg
+            self.creator_thread = QtCore.QThread()
+        else:
+            self.creator_thread = creator_thread
+        self.notification_signal = notification_signal
+        self.pause_signal = pause_signal
         """
         processingLog: [(message, type)] - record the progress of processing
         """
@@ -43,8 +78,9 @@ class DataTypeModel():
 
         """
         Log data: info from log channels, soh messages, text file in dict:
-        {chan_id: list of log strings}
-        'TEXT': is the chan_id given by sohview for text only file.
+        {'TEXT': [str,], key:{chan_id: [str,],},}
+        In which 'TEXT': is the chan_id given by sohview for text only file.
+        Note: logData for RT130's dataset has only one channel: SOH
         """
         self.logData = {'TEXT': []}
 
@@ -106,11 +142,14 @@ class DataTypeModel():
                     'end_tm_epoch': end epoch time of the trace - float,
                     'times': data's real time in epoch - np.array of float,
                     'data': data - np.array of float,
+                    'logIdx: soh message line indexes - np.array of int,
                     }
                 times: times that has been trimmed and down- sampled for
                     plotting - np.array of float,
                 data: data that has been trimmed and down-sampled for plotting
                     - np.array of float/int,
+                logIdx: soh message line indexes that has been trimmed and
+                    down-sampled for plotting - np.array of int,
                'chan_db_info': the plotting parameters got from database
                     for this channel - dict,
                 ax: axes to draw the channel in PlottingWidget
@@ -179,12 +218,16 @@ class DataTypeModel():
          Will be deleted when object is deleted
         """
         self.tmpDir = mkdtemp()
+        self.save_temp_data_folder_to_database()
         try:
             os.mkdir(self.tmpDir)
         except FileExistsError:
             shutil.rmtree(self.tmpDir)
             os.mkdir(self.tmpDir)
 
+        self._pauser = QtCore.QSemaphore()
+        self.pause_response = None
+
     def __del__(self):
         print("delete dataType Object")
         try:
@@ -192,7 +235,7 @@ class DataTypeModel():
         except OSError as e:
             self.trackInfo(
                 "Error deleting %s : %s" % (self.tmpDir, e.strerror),
-                "error")
+                LogType.ERROR)
             print("Error deleting %s : %s" % (self.tmpDir, e.strerror))
         print("finish deleting")
 
@@ -206,21 +249,46 @@ class DataTypeModel():
             return False
         return True
 
-    def trackInfo(self, text, type):
+    def trackInfo(self, text: str, type: LogType = LogType.INFO) -> None:
         """
         Display tracking info in tracking_box.
         Add all errors/warnings to processing_log.
         :param text: str - message to display
         :param type: str - type of message (error/warning/info)
         """
-        displayTrackingInfo(self.trackingBox, text, type)
-        if type != 'info':
+        # displayTrackingInfo updates a QtWidget, which can only be done in the
+        # main thread. So, if we are running in a background thread
+        # (i.e. self.creator_thread is not None), we need to use signal slot
+        # mechanism to ensure that displayTrackingInfo is run in the main
+        # thread.
+        if self.notification_signal is None:
+            displayTrackingInfo(self.trackingBox, text, type)
+        else:
+            self.notification_signal.emit(self.trackingBox, text, type)
+        if type != LogType.INFO:
             self.processingLog.append((text, type))
 
     @classmethod
     def create_data_object(cls, data_type, tracking_box, folder,
                            readChanOnly=False, reqWFChans=[], reqSOHChans=[],
                            readStart=0, readEnd=constants.HIGHEST_INT):
+        """
+        Create a DataTypeModel object, with the concrete class being based on
+        data_type. Run on the same thread as its caller, and so will block the
+        GUI if called on the main thread. Do not call this method directly.
+        Instead, call the wrapper controller.processing.loadData.
+
+        :param data_type: str - type of data read
+        :param tracking_box: QTextBrowser - widget to display tracking info
+        :param folder: [str,] - the data directory
+        :param readChanOnly: if True, only read channel name
+        :param reqWFChans: [str,] - requested waveform channel list
+        :param reqSOHChans: [str,] - requested soh channel list
+        :param readStart: [float,] - start time of read data
+        :param readEnd: [float,] - finish time of read data
+        :return: DataTypeModel - object that keep the data read from
+            folder
+        """
         if data_type == 'RT130':
             from sohstationviewer.model.reftek.reftek import RT130
             dataObject = RT130(
@@ -234,3 +302,60 @@ class DataTypeModel():
                 reqWFChans=reqWFChans, reqSOHChans=reqSOHChans,
                 readStart=readStart, readEnd=readEnd)
         return dataObject
+
+    def pause(self) -> None:
+        """
+        Pause the thread this DataTypeModel instance is in. Works by trying
+        to acquire a semaphore that is not available, which causes the thread
+        to block.
+
+        Note: due to how this is implemented, each call to pause will require
+        a corresponding call to unpause. Thus, it is inadvisable to call this
+        method more than once.
+
+        Caution: not safe to call in the main thread. Unless a background
+        thread releases the semaphore, the whole program will freeze.
+        """
+        self._pauser.acquire()
+
+    @QtCore.Slot()
+    def unpause(self):
+        """
+        Unpause the thread this DataTypeModel instance is in. Works by trying
+        to acquire a semaphore that is not available, which causes the thread
+        to block.
+
+        Caution: due to how this is implemented, if unpause is called before
+        pause, the thread will not be paused until another call to pause is
+        made. Also, like pause, each call to unpause must be matched by another
+        call to pause for everything to work.
+        """
+        self._pauser.release()
+
+    @QtCore.Slot()
+    def receive_pause_response(self, response: object):
+        """
+        Receive a response to a request made to another thread and unpause the
+        calling thread.
+
+        :param response: the response to the request made
+        :type response: object
+        """
+        self.pause_response = response
+        self.unpause()
+
+    @classmethod
+    def get_empty_instance(cls) -> DataTypeModel:
+        """
+        Create an empty data object. Useful if a DataTypeModel instance is
+        needed, but it is undesirable to load a data set. Basically wraps
+        __new__().
+
+        :return: an empty data object
+        :rtype: DataTypeModel
+        """
+        return cls.__new__(cls)
+
+    def save_temp_data_folder_to_database(self):
+        execute_db(f'UPDATE PersistentData SET FieldValue="{self.tmpDir}" '
+                   f'WHERE FieldName="tempDataDirectory"')
diff --git a/sohstationviewer/model/handling_data.py b/sohstationviewer/model/handling_data.py
index 87321a559cdc510ba203b75f2ac8d8c1893e2d92..b28f9de0fcd32a71cad163752627fa6e1546f82f 100644
--- a/sohstationviewer/model/handling_data.py
+++ b/sohstationviewer/model/handling_data.py
@@ -15,6 +15,7 @@ from sohstationviewer.model.mseed.blockettes_reader import (
 from sohstationviewer.conf.dbSettings import dbConf
 from sohstationviewer.conf import constants as const
 from sohstationviewer.model.reftek.from_rt2ms import core
+from sohstationviewer.view.util.enums import LogType
 
 
 def readSOHMSeed(path2file, fileName,
@@ -246,7 +247,7 @@ def readASCII(path2file, file, sta_id, chan_id, trace, log_data, track_info):
     """
     byteorder = trace.stats.mseed['byteorder']
     h = trace.stats
-    logText = "\n\n**** STATE OF HEALTH: "
+    logText = "\n\nSTATE OF HEALTH: "
     logText += ("From:%s  To:%s\n" % (h.starttime, h.endtime))
     textFromData = trace.data.tobytes().decode()
     logText += textFromData
@@ -269,7 +270,7 @@ def readASCII(path2file, file, sta_id, chan_id, trace, log_data, track_info):
                     nextBlktByteNo, databytes, byteorder)
                 logText += info
             except ReadBlocketteError as e:
-                track_info(f"{sta_id} - {chan_id}: {e.msg}", 'error')
+                track_info(f"{sta_id} - {chan_id}: {e.msg}", LogType.ERROR)
 
     if sta_id not in log_data:
         log_data[sta_id] = {}
@@ -292,7 +293,7 @@ def readText(path2file, fileName, textLogs, ):
         try:
             content = file.read()
         except UnicodeDecodeError:
-            raise Exception("Can't process file: %s" % fileName, 'error')
+            raise Exception("Can't process file: %s" % fileName, LogType.ERROR)
 
         logText = "\n\n** STATE OF HEALTH: %s\n" % fileName
         logText += content
@@ -430,7 +431,7 @@ def squash_gaps(gaps):
     return squashed_gaps
 
 
-def downsample(times, data, rq_points):
+def downsample(times, data, log_indexes=None, rq_points=0):
     """
     Reduce sample rate of times and data so that times and data return has
         the size around the rq_points.
@@ -440,11 +441,19 @@ def downsample(times, data, rq_points):
         continue to downsample.
     :param times: numpy array - of a waveform channel's times
     :param data: numpy array - of a waveform channel's data
+    :param log_indexes: numpy array - of a waveform channel's soh message line
+        index
     :param rq_points: int - requested size to return.
-    :return np.array, np.array - new times and new data with the requested size
-    """
+    :return np.array, np.array,(np.array) - new times and new data (and new
+        log_indexes) with the requested size
+    """
+    # create a dummy array for log_indexes. However this way may slow down
+    # the performance of waveform downsample because waveform channel are large
+    # and have no log_indexes.
+    if log_indexes is None:
+        log_indexes = np.empty(times.size)
     if times.size <= rq_points:
-        return times, data
+        return times, data, log_indexes
     dataMax = max(abs(data.max()), abs(data.min()))
     dataMean = abs(data.mean())
     indexes = np.where(
@@ -452,9 +461,12 @@ def downsample(times, data, rq_points):
         (dataMax - dataMean) * const.CUT_FROM_MEAN_FACTOR)
     times = times[indexes]
     data = data[indexes]
+    log_indexes = log_indexes[indexes]
+
     if times.size <= rq_points:
-        return times, data
-    return chunk_minmax(times, data, rq_points)
+        return times, data, log_indexes
+
+    return chunk_minmax(times, data, log_indexes, rq_points)
 
 
 def constant_rate(times, data, rq_points):
@@ -478,24 +490,25 @@ def constant_rate(times, data, rq_points):
     return times, data
 
 
-def chunk_minmax(times, data, rq_points):
+def chunk_minmax(times, data, log_indexes, rq_points):
     """
-    Split data into differen chunks, take the min, max of each chunk to add
+    Split data into different chunks, take the min, max of each chunk to add
         to the data return
-    :param times: numpy array - of a waveform channel's times
-    :param data: numpy array - of a waveform channel's data
+    :param times: numpy array - of a channel's times
+    :param data: numpy array - of a channel's data
+    :param log_indexes: numpy array - of a channel's log_indexes
     :param rq_points: int - requested size to return.
     :return times, data: np.array, np.array - new times and new data with the
         requested size
     """
-    x, y = times, data
+    x, y, z = times, data, log_indexes
     final_points = 0
     if x.size <= rq_points:
         final_points += x.size
-        return x, y
+        return x, y, log_indexes
 
     if rq_points < 2:
-        return np.empty((1, 0)), np.empty((1, 0))
+        return np.empty((1, 0)), np.empty((1, 0)), np.empty((1, 0))
 
     # Since grabbing the min and max from each
     # chunk, need to div the requested number of points
@@ -512,36 +525,35 @@ def chunk_minmax(times, data, rq_points):
         # the requested sample size, but not by much.
         x0 = times[:cs * size]
         y0 = data[:cs * size]
+        z0 = log_indexes[:cs * size]
 
         x1 = times[cs * size:]
         y1 = data[cs * size:]
+        z1 = data[cs * size:]
 
-        dx0, dy0 = downsample(x0, y0, rq_points)
+        dx0, dy0, dz0 = downsample(x0, y0, z0, rq_points=rq_points)
 
         # right-most subarray is always smaller than
         # the initially requested number of points.
-        dx1, dy1 = downsample(x1, y1, cs)
+        dx1, dy1, dz1 = downsample(x1, y1, z1, rq_points=cs)
 
         dx = np.zeros(dx0.size + dx1.size)
         dy = np.zeros(dy0.size + dy1.size)
-
-        # print(dx0.size + dx1.size)
+        dz = np.zeros(dz0.size + dz1.size)
 
         dx[:dx0.size] = dx0
         dy[:dy0.size] = dy0
+        dz[:dz0.size] = dz0
 
         dx[dx0.size:] = dx1
         dy[dy0.size:] = dy1
-        del x0
-        del y0
-        del x1
-        del y1
-        del times
-        del data
-        return dx, dy
+        dz[dz0.size:] = dz1
+
+        return dx, dy, dz
 
     x = x.reshape(size, cs)
     y = y.reshape(size, cs)
+    z = z.reshape(size, cs)
 
     imin = np.argmin(y, axis=1)
     imax = np.argmax(y, axis=1)
@@ -554,7 +566,8 @@ def chunk_minmax(times, data, rq_points):
 
     dx = x[mask]
     dy = y[mask]
-    return dx, dy
+    dz = z[mask]
+    return dx, dy, dz
 
 
 def trim_downsample_SOHChan(chan, startTm, endTm, firsttime):
@@ -570,14 +583,17 @@ def trim_downsample_SOHChan(chan, startTm, endTm, firsttime):
     :param endTm: float - end time of zoomed section
     :param firsttime: bool True for original size when channel is not zoomed in
     """
-    # TODO, add logIdx to downsample if using reftex
-    # zoom in to the given time
+    # zoom into the given time
     tr = chan['orgTrace']
-
     indexes = np.where((startTm <= tr['times']) & (tr['times'] <= endTm))
-    chan['times'], chan['data'] = downsample(
-        tr['times'][indexes], tr['data'][indexes],
-        const.CHAN_SIZE_LIMIT)
+    if 'logIdx' in tr.keys():
+        chan['times'], chan['data'], chan['logIdx'] = downsample(
+            tr['times'][indexes], tr['data'][indexes], tr['logIdx'][indexes],
+            rq_points=const.CHAN_SIZE_LIMIT)
+    else:
+        chan['times'], chan['data'], _ = downsample(
+            tr['times'][indexes], tr['data'][indexes],
+            rq_points=const.CHAN_SIZE_LIMIT)
 
 
 def trim_waveform_data(wf_channel_data: Dict, start_time: float,
@@ -661,7 +677,8 @@ def downsample_waveform_data(trimmed_traces_list: List[Dict],
         times = times[indexes]
         data = data[indexes]
         if requested_points != 0:
-            times, data = downsample(times, data, requested_points)
+            times, data, _ = downsample(times, data,
+                                        rq_points=requested_points)
         downsampled_times_list.append(times)
         downsampled_data_list.append(data)
 
diff --git a/sohstationviewer/model/mseed/mseed.py b/sohstationviewer/model/mseed/mseed.py
index 6bc7a2108460a1f0cd128e9fd89699dc8ddb66e1..4c09b26075c92594fada14c4e0352961f0e86cac 100644
--- a/sohstationviewer/model/mseed/mseed.py
+++ b/sohstationviewer/model/mseed/mseed.py
@@ -2,23 +2,17 @@
 MSeed object to hold and process MSeed data
 """
 
-
 import os
 from pathlib import Path
 
-from PySide2 import QtWidgets
-
-from sohstationviewer.view.select_buttons_dialog import (
-    SelectButtonDialog)
-from sohstationviewer.model.data_type_model import DataTypeModel
-
 from sohstationviewer.conf import constants
-
-from sohstationviewer.model.mseed.from_mseedpeek.mseed_header import readHdrs
 from sohstationviewer.controller.util import validateFile
+from sohstationviewer.model.data_type_model import DataTypeModel, ThreadStopped
 from sohstationviewer.model.handling_data import (
-    readWaveformMSeed, squash_gaps, checkWFChan,
-    sortData, readSOHTrace)
+    readWaveformMSeed, squash_gaps, checkWFChan, sortData, readSOHTrace,
+)
+from sohstationviewer.model.mseed.from_mseedpeek.mseed_header import readHdrs
+from sohstationviewer.view.util.enums import LogType
 
 
 class MSeed(DataTypeModel):
@@ -46,10 +40,16 @@ class MSeed(DataTypeModel):
         """
         self.netsProbInFile = {}
 
+        if self.creator_thread.isInterruptionRequested():
+            raise ThreadStopped()
         self.read_soh_and_index_waveform(self.dir)
+
+        if self.creator_thread.isInterruptionRequested():
+            raise ThreadStopped()
         self.selectedKey = self.selectStaID()
+
         if self.selectedKey is None:
-            return
+            raise ThreadStopped()
         if len(self.reqWFChans) != 0:
             self.readWFFiles(self.selectedKey)
 
@@ -89,13 +89,16 @@ class MSeed(DataTypeModel):
 
         for path, sub_dirs, files in os.walk(folder):
             for file_name in files:
+                if self.creator_thread.isInterruptionRequested():
+                    raise ThreadStopped()
+
                 path2file = Path(path).joinpath(file_name)
                 if not validateFile(path2file, file_name):
                     continue
                 count += 1
                 if count % 50 == 0:
                     self.trackInfo(
-                        f'Read {count} file headers/ SOH files', 'info')
+                        f'Read {count} file headers/ SOH files', LogType.INFO)
 
                 ret = readHdrs(
                     path2file, file_name, soh_streams, self.logData,
@@ -148,14 +151,14 @@ class MSeed(DataTypeModel):
         if len(stat_prop) > 0:
             errmsg = (f"More than one stations in a file: {stat_prop}. "
                       f"Will use the first one.")
-            self.trackInfo(errmsg, "error")
+            self.trackInfo(errmsg, LogType.ERROR)
         if len(net_stat_prop) > 0:
             errmsg = "More than one netIDs in a file: %s" % net_stat_prop
-            self.trackInfo(errmsg, "warning")
+            self.trackInfo(errmsg, LogType.WARNING)
         if len(chan_prop) > 0:
             errmsg = (f"More than one channels in a file: {chan_prop} "
                       f"\nThis is a CRITICAL ERROR.")
-            self.trackInfo(errmsg, "error")
+            self.trackInfo(errmsg, LogType.ERROR)
         return waveform_data, soh_streams
 
     def merge_soh_streams(self, soh_streams):
@@ -189,16 +192,16 @@ class MSeed(DataTypeModel):
                 stream = soh_streams[sta_id][chan_id]
 
                 stream.merge()
+
+                tr = stream[0]
                 if len(stream) > 1:
                     nets = [tr.stats['network'].strip() for tr in stream]
                     nets += [f"Combine to {n}" for n in nets]
                     msg = (f"There are more than one net for sta {sta_id}.\n"
                            "Please select one or combine all to one.")
-                    msg_box = SelectButtonDialog(message=msg,
-                                                 button_labels=nets)
-                    msg_box.exec_()
-                    sel_net = nets[msg_box.ret]
-
+                    self.pause_signal.emit(msg, nets)
+                    self.pause()
+                    sel_net = nets[self.pause_response]
                     if "Combine" not in sel_net:
                         tr = [tr for tr in stream
                               if tr.stats['network'] == sel_net][0]
@@ -210,8 +213,6 @@ class MSeed(DataTypeModel):
                             self.nets.add(sel_net)
                         stream.merge()
                         tr = stream[0]
-                else:
-                    tr = stream[0]
 
                 gaps_in_stream = stream.get_gaps()
                 all_gaps += [[g[4].timestamp, g[5].timestamp]
@@ -243,22 +244,12 @@ class MSeed(DataTypeModel):
         selectedStaID = stats[0]
         if len(stats) > 1:
             msg = ("There are more than one stations in the given data.\n"
-                   "Please select one to one to display")
-            msgBox = QtWidgets.QMessageBox()
-            msgBox.setText(msg)
-            staButtons = []
-            for staID in stats:
-                staButtons.append(msgBox.addButton(
-                    staID, QtWidgets.QMessageBox.ActionRole))
-            abortButton = msgBox.addButton(QtWidgets.QMessageBox.Abort)
-
-            msgBox.exec_()
-
-            if msgBox.clickedButton() == abortButton:
-                return selectedStaID
-            selectedIdx = staButtons.index(msgBox.clickedButton())
-            selectedStaID = stats[selectedIdx]
-        self.trackInfo(f'Select Station {selectedStaID}', 'info')
+                   "Please select one to display")
+            self.pause_signal.emit(msg, stats)
+            self.pause()
+            selectedStaID = stats[self.pause_response]
+
+        self.trackInfo(f'Select Station {selectedStaID}', LogType.INFO)
         return selectedStaID
 
     def readWFFiles(self, staID):
@@ -289,6 +280,8 @@ class MSeed(DataTypeModel):
                 'readData'][chanID]['tracesInfo']
 
             for fileInfo in self.waveformData[staID]['filesInfo'][chanID]:
+                if self.creator_thread.isInterruptionRequested():
+                    raise ThreadStopped
                 # file have been read
                 if fileInfo['read']:
                     continue
@@ -307,6 +300,7 @@ class MSeed(DataTypeModel):
                 fileInfo['read'] = True
                 count += 1
                 if count % 50 == 0:
-                    self.trackInfo(f'Read {count} waveform files', 'info')
+                    self.trackInfo(
+                        f'Read {count} waveform files', LogType.INFO)
 
         sortData(self.waveformData)
diff --git a/sohstationviewer/model/reftek/logInfo.py b/sohstationviewer/model/reftek/logInfo.py
index b8f1caf0727d1a41abe07e78cb50be0381f6e969..dc98cbd93976c574ce11c6baa3352eea7c07fd95 100644
--- a/sohstationviewer/model/reftek/logInfo.py
+++ b/sohstationviewer/model/reftek/logInfo.py
@@ -2,6 +2,7 @@
 from sohstationviewer.conf import constants
 from sohstationviewer.controller.util import (
     getTime6, getTime4, getVal, rtnPattern)
+from sohstationviewer.view.util.enums import LogType
 
 
 class LogInfo():
@@ -68,7 +69,7 @@ class LogInfo():
             else:
                 epoch, _ = getTime6(parts[8])
         except AttributeError:
-            self.parent.processingLog.append(line, 'error')
+            self.parent.processingLog.append(line, LogType.ERROR)
             return False
         if epoch > 0:
             self.minEpoch = min(epoch, self.minEpoch)
@@ -90,7 +91,7 @@ class LogInfo():
         try:
             epoch, self.trackYear = getTime6(parts[3])
         except AttributeError:
-            self.parent.processingLog.append(line, 'error')
+            self.parent.processingLog.append(line, LogType.ERROR)
             return False
         self.yAdded = False         # reset yAdded
         self.minEpoch = min(epoch, self.minEpoch)
@@ -101,7 +102,7 @@ class LogInfo():
                    "read, but now there is information for DAS %s in "
                    "the same State Of Health messages.\n"
                    "Skip reading State Of Health." % (self.unitID, unitID))
-            self.trackInfo(msg, 'error')
+            self.trackInfo(msg, LogType.ERROR)
             False
         return epoch
 
@@ -119,7 +120,7 @@ class LogInfo():
             epoch, self.trackYear, self.yAdded = getTime4(
                 parts[0], self.trackYear, self.yAdded)
         except AttributeError:
-            self.parent.processingLog.append(line, 'error')
+            self.parent.processingLog.append(line, LogType.ERROR)
             return False
         self.maxEpoch = max(epoch, self.maxEpoch)
         return parts, epoch
@@ -161,7 +162,7 @@ class LogInfo():
             volts = getVal(parts[4])
             temp = getVal(parts[7])
         except AttributeError:
-            self.parent.processingLog.append(line, 'error')
+            self.parent.processingLog.append(line, LogType.ERROR)
             return False
         if self.model == "RT130":
             bkupV = getVal(parts[10])
@@ -186,7 +187,7 @@ class LogInfo():
             disk = getVal(parts[2])
             val = getVal(parts[4])
         except AttributeError:
-            self.parent.processingLog.append(line, 'error')
+            self.parent.processingLog.append(line, LogType.ERROR)
             return False
         return epoch, disk, val
 
@@ -203,7 +204,7 @@ class LogInfo():
             secs = getVal(parts[4])
             msecs = getVal(parts[-2])
         except AttributeError:
-            self.parent.processingLog.append(line, 'error')
+            self.parent.processingLog.append(line, LogType.ERROR)
             return False
         total = abs(secs) * 1000.0 + abs(msecs)
         if secs < 0.0 or msecs < 0.0:
@@ -231,7 +232,7 @@ class LogInfo():
                 try:
                     epoch, _ = getTime6(parts[-3])
                 except AttributeError:
-                    self.parent.processingLog.append(line, 'error')
+                    self.parent.processingLog.append(line, LogType.ERROR)
                     return False
             else:
                 epoch = self.maxEpoch
@@ -315,10 +316,13 @@ class LogInfo():
         Extract data from each line of log string to add to
         SOH channels's orgTrace using addChanInfo()
         """
-        lines = [ln.strip() for ln in self.logText.splitlines() if ln != '']
+        lines = [ln.strip() for ln in self.logText.splitlines()]
         sohEpoch = 0
 
         for idx, line in enumerate(lines):
+
+            if line == '':
+                continue
             line = line.upper()
             if 'FST' in line:
                 ret = self.readEVT(line)
@@ -329,9 +333,11 @@ class LogInfo():
                             chanName = 'Event DS%s' % DS
                             self.addChanInfo(chanName, epoch, 1, idx)
                         elif epoch == 0:
-                            self.parent.processingLog.append(line, 'warning')
+                            self.parent.processingLog.append(
+                                line, LogType.WARNING)
                         else:
-                            self.parent.processingLog.append(line, 'error')
+                            self.parent.processingLog.append(
+                                line, LogType.ERROR)
 
             elif line.startswith("STATE OF HEALTH"):
                 epoch = self.readSHHeader(line)
@@ -413,7 +419,7 @@ class LogInfo():
             elif "MASS RE-CENTER" in line:
                 epoch = self.simpleRead(line)[1]
                 if epoch:
-                    self.addChanInfo('Mass Re-center', epoch, idx)
+                    self.addChanInfo('Mass Re-center', epoch, 0, idx)
 
             elif any(x in line for x in ["SYSTEM RESET", "FORCE RESET"]):
                 epoch = self.simpleRead(line)[1]
diff --git a/sohstationviewer/model/reftek/reftek.py b/sohstationviewer/model/reftek/reftek.py
index 2cf1d9024c66f7ff1e682740e0c992366e6dad77..6333c44caae4a2924790542572473b8e47e611ce 100755
--- a/sohstationviewer/model/reftek/reftek.py
+++ b/sohstationviewer/model/reftek/reftek.py
@@ -6,13 +6,12 @@ import os
 from pathlib import Path
 import numpy as np
 
-from PySide2 import QtWidgets
 from obspy.core import Stream
 
 from sohstationviewer.model.reftek.from_rt2ms import (
     core, soh_packet, packet)
 from sohstationviewer.model.reftek.logInfo import LogInfo
-from sohstationviewer.model.data_type_model import DataTypeModel
+from sohstationviewer.model.data_type_model import DataTypeModel, ThreadStopped
 from sohstationviewer.model.handling_data import (
     readWaveformReftek, squash_gaps, sortData, readMPTrace, readText)
 
@@ -20,6 +19,8 @@ from sohstationviewer.conf import constants
 
 from sohstationviewer.controller.util import validateFile
 
+from sohstationviewer.view.util.enums import LogType
+
 
 class RT130(DataTypeModel):
     """
@@ -32,11 +33,19 @@ class RT130(DataTypeModel):
         self.keys = set()
         self.reqDSs = self.reqWFChans
         self.massPosStream = {}
+
+        if self.creator_thread.isInterruptionRequested():
+            raise ThreadStopped()
         self.readSOH_indexWaveform(self.dir)
 
+        if self.creator_thread.isInterruptionRequested():
+            raise ThreadStopped()
         self.selectedKey = self.selectKey()
         if self.selectedKey is None:
-            return
+            raise ThreadStopped()
+
+        if self.creator_thread.isInterruptionRequested():
+            raise ThreadStopped()
         if len(self.reqWFChans) != 0:
             self.readWFFiles(self.selectedKey)
 
@@ -49,6 +58,8 @@ class RT130(DataTypeModel):
         count = 0
         for path, subdirs, files in os.walk(folder):
             for fileName in files:
+                if self.creator_thread.isInterruptionRequested():
+                    raise ThreadStopped()
                 path2file = Path(path).joinpath(fileName)
                 if not validateFile(path2file, fileName):
                     continue
@@ -57,7 +68,7 @@ class RT130(DataTypeModel):
                 count += 1
                 if count % 50 == 0:
                     self.trackInfo(
-                        f'Read {count} file headers/ SOH files', 'info')
+                        f'Read {count} file headers/ SOH files', LogType.INFO)
 
         self.combineData()
 
@@ -74,22 +85,12 @@ class RT130(DataTypeModel):
         selectedKey = self.keys[0]
         if len(self.keys) > 1:
             msg = ("There are more than one keys in the given data.\n"
-                   "Please select one to one to display")
-            msgBox = QtWidgets.QMessageBox()
-            msgBox.setText(msg)
-            staButtons = []
-            for key in self.keys:
-                staButtons.append(msgBox.addButton(
-                    key, QtWidgets.QMessageBox.ActionRole))
-            abortButton = msgBox.addButton(QtWidgets.QMessageBox.Abort)
-
-            msgBox.exec_()
-
-            if msgBox.clickedButton() == abortButton:
-                return selectedKey
-            selectedIdx = staButtons.index(msgBox.clickedButton())
-            selectedKey = self.keys[selectedIdx]
-        self.trackInfo(f'Select Key {selectedKey}', 'info')
+                   "Please select one to display")
+            self.pause_signal.emit(msg, self.keys)
+            self.pause()
+            selectedKey = self.keys[self.pause_response]
+
+        self.trackInfo(f'Select Key {selectedKey}', LogType.INFO)
         return selectedKey
 
     def readWFFiles(self, key):
@@ -111,6 +112,8 @@ class RT130(DataTypeModel):
         for DS in self.waveformData[key]['filesInfo']:
             readData = self.waveformData[key]['readData']
             for fileInfo in self.waveformData[key]['filesInfo'][DS]:
+                if self.creator_thread.isInterruptionRequested():
+                    raise ThreadStopped()
                 # file have been read
                 if fileInfo['read']:
                     continue
@@ -127,7 +130,8 @@ class RT130(DataTypeModel):
                 fileInfo['read'] = True
                 count += 1
                 if count % 50 == 0:
-                    self.trackInfo(f'Read {count} waveform files', 'info')
+                    self.trackInfo(
+                        f'Read {count} waveform files', LogType.INFO)
         sortData(self.waveformData)
 
     def addLog(self, chan_pkt, logInfo):
@@ -282,6 +286,8 @@ class RT130(DataTypeModel):
         is too big to consider calculating gaps.
         """
         for k in self.logData:
+            if self.creator_thread.isInterruptionRequested():
+                raise ThreadStopped()
             if k == 'TEXT':
                 continue
             if k not in self.dataTime:
@@ -302,7 +308,7 @@ class RT130(DataTypeModel):
                 except KeyError:
                     pass
             logStr = ''.join(logs)
-            self.logData[k][pktType] = logStr
+            self.logData[k] = {'SOH': [logStr]}
             logObj = LogInfo(
                 self, self.trackInfo, logStr, k, self.reqDSs)
             self.dataTime[k][0] = min(logObj.minEpoch, self.dataTime[k][0])
@@ -317,6 +323,8 @@ class RT130(DataTypeModel):
         self.gaps = {k: [] for k in self.keys}
         self.massPosData = {k: {} for k in self.keys}
         for k in self.massPosStream:
+            if self.creator_thread.isInterruptionRequested():
+                raise ThreadStopped()
             stream = self.massPosStream[k]
             stream.merge()
             for tr in stream:
diff --git a/sohstationviewer/view/channel_prefer_dialog.py b/sohstationviewer/view/channel_prefer_dialog.py
index c50b945b74586243fc9dfd9f74e2b1c4c5714a38..ff2b43769135d314010101497a2adbde31c12519 100755
--- a/sohstationviewer/view/channel_prefer_dialog.py
+++ b/sohstationviewer/view/channel_prefer_dialog.py
@@ -1,10 +1,14 @@
 from PySide2 import QtWidgets, QtCore
 
-from sohstationviewer.database.proccessDB import (
-    executeDB, trunc_addDB, executeDB_dict)
+from sohstationviewer.database.process_db import (
+    execute_db, trunc_add_db, execute_db_dict)
+
 from sohstationviewer.controller.processing import readChannels, detectDataType
 from sohstationviewer.controller.util import displayTrackingInfo
 
+from sohstationviewer.view.util.enums import LogType
+
+
 INSTRUCTION = """
 Place lists of channels to be read in the IDs field.\n
 Select the radiobutton for the list to be used in plotting.
@@ -373,9 +377,9 @@ class ChannelPreferDialog(QtWidgets.QWidget):
             self.parent.data_type = 'Unknown'
             return True
 
-        ret = trunc_addDB('ChannelPrefer', sql_list)
+        ret = trunc_add_db('ChannelPrefer', sql_list)
         if ret is not True:
-            displayTrackingInfo(self.parent, ret, "error")
+            displayTrackingInfo(self.parent, ret, LogType.ERROR)
         self.parent.IDs = [
             t.strip() for t in self.id_widget.text().split(',')]
         self.parent.IDsName = self.name_widget.text().strip()
@@ -425,7 +429,7 @@ class ChannelPreferDialog(QtWidgets.QWidget):
 
         :return: [str, ] - list of data types
         """
-        data_type_rows = executeDB(
+        data_type_rows = execute_db(
             'SELECT * FROM DataTypes ORDER BY dataType ASC')
         return [d[0] for d in data_type_rows]
 
@@ -435,7 +439,7 @@ class ChannelPreferDialog(QtWidgets.QWidget):
 
         :param data_type: str - the given data type
         """
-        channel_rows = executeDB(
+        channel_rows = execute_db(
             f"SELECT channel FROM CHANNELS WHERE dataType='{data_type}' "
             f" ORDER BY dataType ASC")
         return [c[0] for c in channel_rows]
@@ -447,7 +451,7 @@ class ChannelPreferDialog(QtWidgets.QWidget):
 
         :param id_rows: [dict,] - list of data for each row
         """
-        id_rows = executeDB_dict(
+        id_rows = execute_db_dict(
             "SELECT name, IDs, dataType, current FROM ChannelPrefer "
             " ORDER BY name ASC")
         return id_rows
diff --git a/sohstationviewer/view/core/plotting_widget.py b/sohstationviewer/view/core/plotting_widget.py
new file mode 100755
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/sohstationviewer/view/db_config/channel_dialog.py b/sohstationviewer/view/db_config/channel_dialog.py
index 1bc7ebb1c03c67d45e8eeb04710c78203d088a38..a866b9c3250b953f78d8672cf97396d7638f1a21 100755
--- a/sohstationviewer/view/db_config/channel_dialog.py
+++ b/sohstationviewer/view/db_config/channel_dialog.py
@@ -4,7 +4,7 @@ GUI to add/edit/remove channels
 """
 
 from sohstationviewer.view.db_config.db_config_dialog import UiDBInfoDialog
-from sohstationviewer.database.proccessDB import executeDB
+from sohstationviewer.database.process_db import execute_db
 
 
 class ChannelDialog(UiDBInfoDialog):
@@ -30,7 +30,7 @@ class ChannelDialog(UiDBInfoDialog):
         Create list of parameters to used in widget for selecting parameters
             before update item in self.data_table_widgets
         """
-        param_rows = executeDB("SELECT param from parameters")
+        param_rows = execute_db("SELECT param from parameters")
         self.param_choices = [''] + sorted([d[0] for d in param_rows])
         super(ChannelDialog, self).update_data_table_widget_items()
 
@@ -76,7 +76,7 @@ class ChannelDialog(UiDBInfoDialog):
         """
         Get list of data to fill self.data_table_widgets' content
         """
-        channel_rows = executeDB(
+        channel_rows = execute_db(
             f"SELECT channel, label, param, convertFactor, unit, fixPoint "
             f"FROM Channels "
             f"WHERE dataType='{self.data_type}'")
diff --git a/sohstationviewer/view/db_config/data_type_dialog.py b/sohstationviewer/view/db_config/data_type_dialog.py
index 2710cda896785c598c2cf86a6d8b3b43d6242906..75c8aea23f1ef7e22acb6428bf4b8dcf90c92527 100755
--- a/sohstationviewer/view/db_config/data_type_dialog.py
+++ b/sohstationviewer/view/db_config/data_type_dialog.py
@@ -5,7 +5,7 @@ NOTE: Cannot remove or change dataTypes that already have channels.
 """
 
 from sohstationviewer.view.db_config.db_config_dialog import UiDBInfoDialog
-from sohstationviewer.database.proccessDB import executeDB
+from sohstationviewer.database.process_db import execute_db
 
 
 class DataTypeDialog(UiDBInfoDialog):
@@ -30,7 +30,7 @@ class DataTypeDialog(UiDBInfoDialog):
         Get list of data to fill self.data_table_widgets' content
         """
 
-        data_type_rows = executeDB('SELECT * FROM DataTypes')
+        data_type_rows = execute_db('SELECT * FROM DataTypes')
         return [[d[0]] for d in data_type_rows]
 
     def get_row_inputs(self, row_idx):
diff --git a/sohstationviewer/view/db_config/db_config_dialog.py b/sohstationviewer/view/db_config/db_config_dialog.py
index 33c811ad51dec1da72a1d1e66be51a488b12d8e0..9cb182bdaaa05d9873cb4897060a04b521fb65da 100755
--- a/sohstationviewer/view/db_config/db_config_dialog.py
+++ b/sohstationviewer/view/db_config/db_config_dialog.py
@@ -1,6 +1,6 @@
 from PySide2 import QtWidgets, QtGui, QtCore
 
-from sohstationviewer.database.proccessDB import executeDB
+from sohstationviewer.database.process_db import execute_db
 
 
 def set_widget_color(widget, changed=False, read_only=False):
@@ -198,7 +198,7 @@ class UiDBInfoDialog(QtWidgets.QWidget):
 
         if self.need_data_type_choice:
             self.data_type_combo_box = QtWidgets.QComboBox(self)
-            data_type_rows = executeDB('SELECT * FROM DataTypes')
+            data_type_rows = execute_db('SELECT * FROM DataTypes')
             self.data_type_combo_box.addItems([d[0] for d in data_type_rows])
             self.data_type_combo_box.currentTextChanged.connect(
                 self.data_type_changed)
@@ -317,7 +317,7 @@ class UiDBInfoDialog(QtWidgets.QWidget):
             return False
         sql = (f"SELECT {self.col_name} FROM channels "
                f"WHERE {self.col_name}='{val}'")
-        param_rows = executeDB(sql)
+        param_rows = execute_db(sql)
         if len(param_rows) > 0:
             return True
         else:
@@ -442,7 +442,7 @@ class UiDBInfoDialog(QtWidgets.QWidget):
                            f"WHERE {self.col_name}='{org_row[0]}'")
                     if del_sql_add is not None:
                         sql += sql
-                    executeDB(sql)
+                    execute_db(sql)
                     self.data_list.remove(org_row)
                     self.remove_row(widget_idx)
                     self.remove_count += 1
@@ -473,7 +473,7 @@ class UiDBInfoDialog(QtWidgets.QWidget):
                 if result == QtWidgets.QMessageBox.Cancel:
                     return 1
                 else:
-                    executeDB(update_sql % org_row[0])
+                    execute_db(update_sql % org_row[0])
                     self.data_list[list_idx] = row
                     return 0
 
@@ -499,7 +499,7 @@ class UiDBInfoDialog(QtWidgets.QWidget):
             QtWidgets.QMessageBox.information(self, "Error", msg)
             self.remove_row(widget_idx)
             return -1
-        executeDB(insert_sql)
+        execute_db(insert_sql)
         self.data_list.append(row)
         self.insert_count += 1
         return 0
diff --git a/sohstationviewer/view/db_config/param_dialog.py b/sohstationviewer/view/db_config/param_dialog.py
index b55ac1a22de92c1a1de48905afa0dee12e792683..5734067d62bf700e3c28c4b2086d94e1aa6bece6 100755
--- a/sohstationviewer/view/db_config/param_dialog.py
+++ b/sohstationviewer/view/db_config/param_dialog.py
@@ -9,7 +9,7 @@ from PySide2 import QtWidgets
 from sohstationviewer.view.util.plot_func_names import plot_functions
 from sohstationviewer.view.db_config.db_config_dialog import UiDBInfoDialog
 
-from sohstationviewer.database.proccessDB import executeDB
+from sohstationviewer.database.process_db import execute_db
 
 from sohstationviewer.conf.dbSettings import dbConf
 
@@ -46,7 +46,7 @@ class ParamDialog(UiDBInfoDialog):
         """
         Get list of data to fill self.data_table_widgets' content
         """
-        param_rows = executeDB('SELECT * FROM Parameters')
+        param_rows = execute_db('SELECT * FROM Parameters')
         return [[d[0],
                  '' if d[1] is None else d[1],
                  d[2],
diff --git a/sohstationviewer/view/file_list_widget.py b/sohstationviewer/view/file_list_widget.py
index f01499fd52d3cb5e2661217a1de61e50b886bdd8..8cb08e9b929049804fbb021f3c8201bab7a14267 100644
--- a/sohstationviewer/view/file_list_widget.py
+++ b/sohstationviewer/view/file_list_widget.py
@@ -4,9 +4,9 @@ from PySide2 import QtWidgets
 class FileListItem(QtWidgets.QListWidgetItem):
     """
     Widget to select file and save the absolute path of the file under
-    self.filePath variable
+    self.file_path variable
     """
-    def __init__(self, filePath, parent=None):
-        super().__init__(filePath.name, parent,
+    def __init__(self, file_path, parent=None):
+        super().__init__(file_path.name, parent,
                          type=QtWidgets.QListWidgetItem.UserType)
-        self.filePath = filePath
+        self.file_path = file_path
diff --git a/sohstationviewer/view/help_view.py b/sohstationviewer/view/help_view.py
new file mode 100644
index 0000000000000000000000000000000000000000..a13e1b78626f58f1cce594b5c01742bf59ae5f5b
--- /dev/null
+++ b/sohstationviewer/view/help_view.py
@@ -0,0 +1,545 @@
+import sys
+from pathlib import Path
+from typing import Tuple, Callable, Union, List, Dict
+
+from PySide2 import QtCore, QtGui, QtWidgets
+from PySide2.QtWidgets import QStyle
+
+from sohstationviewer.view.util.functions import (
+    create_search_results_file, create_table_of_content_file)
+from sohstationviewer.view.util.color import set_color_mode
+from sohstationviewer.conf import constants as const
+
+
+class HelpBrowserItemDelegate(QtWidgets.QStyledItemDelegate):
+    """
+    To help showing names of files in tree list without extension.
+    """
+    def initStyleOption(self, opt, index):
+        super().initStyleOption(opt, index)
+        if not index.model().isDir(index):
+            base_file_name = index.model().fileInfo(index).baseName()
+            try:
+                display_file_name = base_file_name.split(" _ ")[1]
+            except IndexError:
+                print(f"WARNING: Filename '{base_file_name}' in "
+                      f"'documentation/' is NOT in the "
+                      f"correct format: <order number> _ <text>")
+                display_file_name = base_file_name
+
+            opt.text = display_file_name
+
+    def setEditorData(self, ed, index):
+        if isinstance(index.model(), QtWidgets.QFileSystemModel):
+            if not index.model().isDir(index):
+                ed.setText(index.model().fileInfo(index).baseName())
+            else:
+                super().setEditorData(ed, index)
+
+
+class HelpBrowser(QtWidgets.QWidget):
+    """
+    GUI:
+        + Search box: to enter text to search for, first search is processed
+            with text changed
+        + Search through all documents button: To create the links to all
+            documents that contain search text. Click on a link will go to
+            the link's page with search text highlight at the first position.
+        + Table of Contents button: to go to Table of Contents page.
+        + Recreate Table of Contents button: to recreate Table of Contents
+            when there are any changes in the name of help documents that may
+            make links broken or some pages has no links in Table of Contents.
+        + Backward arrow button: to search backward on the current page.
+        + Forward arrow button: to search forward on the current page.
+        + Go to Search Results page: to continue working on Search Results page
+        + Document list: tree list located on left side to browse for help
+            documents.
+        + Document view: the largest text box located on the right side of the
+            dialog to display the current selected document's content.
+
+    Attributes
+    ----
+    SCREEN_SIZE_SCALAR_X : float
+        Specifies how to scale the width of the window, in relation to total
+        screen size.
+    SCREEN_SIZE_SCALAR_Y : float
+        Specifies how to scale the height of the window, in relation to total
+        screen size.
+    TREE_VIEW_SCALAR: float
+        Specifies how to scale the width of tree_view, in relation to total
+        screen size.
+    """
+
+    SCREEN_SIZE_SCALAR_X = 0.40
+    SCREEN_SIZE_SCALAR_Y = 0.50
+    TREE_VIEW_SCALAR = 0.25
+
+    def __init__(
+            self,
+            parent: Union[QtWidgets.QWidget, QtWidgets.QMainWindow] = None,
+            home_path: str = '.'):
+        """
+        :param parent: The window that call HelpBrowser object
+        :type parent: QWidget/QMainWindow
+        :param home_path: relative path to the folder where app is started
+        :type home_path: str
+        """
+        super().__init__(parent)
+        """
+        home_path: absolute path to the root of the project
+        """
+        self.home_path = Path(home_path).resolve()
+        """
+        docdir_path: path to the folder containing the documentations
+        """
+        self.docdir_path: Path = self.home_path.joinpath('documentation')
+        """
+        images_path: path to the folder containing images
+        """
+        self.images_path: Path = self.home_path.joinpath('images')
+        """
+        contents_table_path: the absolute path to "01 _ Table of Contents.md"
+        """
+        self.contents_table_path: Path = self.docdir_path.joinpath(
+            const.TABLE_CONTENTS)
+        """
+        search_results_path: the absolute path to "Search Results.md"
+        """
+        self.search_results_path: Path = self.docdir_path.joinpath(
+            'Search Results.md')
+
+        # Get screen dimensions
+        geom = QtGui.QGuiApplication.screens()[0].availableGeometry()
+        self.setGeometry(10, 10,
+                         geom.size().width() * self.SCREEN_SIZE_SCALAR_X,
+                         geom.size().height() * self.SCREEN_SIZE_SCALAR_Y
+                         )
+        self.setWindowTitle("Help Documents")
+        # -------------------- PRE-DEFINE WIDGETS ----------------------------
+        """
+        search_box: text box to enter a string to search along the
+            current table.
+        """
+        self.search_box: QtWidgets.QLineEdit = None
+        """
+        search_all_button: button to perform create list of links of documents
+            that contain search text which is called search results
+        """
+        self.search_all_button: QtWidgets.QPushButton = None
+        """
+        go_to_search_results_button: button to navigate to the search results
+        """
+        self.go_to_search_results_button: QtWidgets.QToolButton = None
+        """
+        tree_view: tree list of all documents
+        """
+        self.tree_view: QtWidgets.QTreeView = None
+        """
+        file_system_model: provide data model for document's filesystem
+        """
+        self.file_system_model: QtWidgets.QFileSystemModel = None
+        """
+        help_view: to display selected document's content
+        """
+        self.help_view: QtWidgets.QTextBrowser = None
+        # --------------------- PRE-DEFINED OTHER ATTRIBUTES ----------------
+        """
+        cursor_list: list of cursors of search text found
+        """
+        self.cursor_list: List[QtGui.QTextCursor] = None
+        """
+        current_index: index of the current cursor in cursor_list
+        """
+        self.current_index: int = 0
+        """
+        display_color: {type: color,} - color map for setting background color
+            for different types of processing log or
+             "state of health"/"Events:" labels
+        """
+        self.display_color: Dict[str, str] = set_color_mode("W")
+
+        """
+        highlight_format: to apply to the found text in help view
+        """
+        self.highlight_format: QtGui.QTextCharFormat = \
+            self.get_highlight_format()
+
+        self.setup_ui()
+
+        self.tree_view.setFocus()
+
+    def __del__(self):
+        try:
+            with open(self.docdir_path.joinpath(const.SEARCH_RESULTS), 'w'):
+                pass
+        except NameError:
+            pass
+
+    def get_highlight_format(self):
+        """
+        Setting format to apply for search_text found in document
+
+        :return: format to apply for search_text
+        :rtype:  QtGui.QTextCharFormat
+        """
+        text_format = QtGui.QTextCharFormat()
+        text_format.setForeground(
+            QtGui.QColor(self.display_color['highlight'])
+        )
+        return text_format
+
+    def setup_ui(self):
+        """
+        Setting up GUI including
+            + A search box and a button to search through all documents
+            + A navigation bar to go to Table of Contents, recreate Table of
+                Contents, search back and forth, go to Search Results
+            + A tree to list all documents and select document to view
+            + A view to view the selected documents
+        """
+        # Searching
+        search_layout = QtWidgets.QHBoxLayout()
+        self.search_box = QtWidgets.QLineEdit()
+        self.search_box.setTextMargins(7, 7, 7, 7)
+        self.search_box.setFixedHeight(40)
+        self.search_box.setClearButtonEnabled(True)
+        self.search_box.setPlaceholderText("Enter a string to search")
+        self.search_box.textChanged.connect(self.start_search_on_curr_doc)
+        search_layout.addWidget(self.search_box)
+
+        self.search_all_button = QtWidgets.QPushButton(
+            "Search through\nAll Documents")
+        self.search_all_button.setFixedHeight(50)
+        self.search_all_button.setFixedWidth(150)
+        self.search_all_button.clicked.connect(self.search_through_all_docs)
+        search_layout.addWidget(self.search_all_button)
+
+        navigation = self.create_navigation()
+
+        # Documentation listing
+        self.tree_view, self.file_system_model = self.create_tree_view()
+
+        # Help documentation display
+        self.help_view = QtWidgets.QTextBrowser()
+        # set stylesheet for the selected text in help_view
+        self.help_view.setStyleSheet(
+            f"selection-background-color:"
+            f" {self.display_color['highlight_background']};"
+            f"selection-color: {self.display_color['highlight']};")
+        self.help_view.sourceChanged.connect(self.on_source_changed)
+        split = QtWidgets.QSplitter()
+        split.addWidget(self.tree_view)
+        split.addWidget(self.help_view)
+
+        # --------- set layout -------------------------------------
+        main_layout = QtWidgets.QVBoxLayout()
+        self.setLayout(main_layout)
+        main_layout.setSpacing(10)
+        main_layout.setContentsMargins(5, 5, 5, 5)
+
+        main_layout.addLayout(search_layout)
+        main_layout.addWidget(navigation)
+        main_layout.addWidget(split, 2)
+
+        self.load_file(self.contents_table_path.as_posix())
+
+    def create_navigation(self) -> QtWidgets.QToolBar:
+        """
+        Create navigation bar includes:
+            + A button to go back to Table of Contents document
+            + A button to recreate TAble of Contents document
+            + A button to search backward on the current document
+            + A button to search forward on the current document
+            + A button to go back to Search Results
+
+        :return: Toolbar that includes necessary buttons
+        :rtype:  QtWidgets.QToolBar
+        """
+
+        nav_bar = QtWidgets.QToolBar("Navigation")
+
+        self.add_nav_button(
+            nav_bar,
+            QtGui.QIcon(self.images_path.joinpath(
+                'table_contents.png').as_posix()),
+            'Navigate to Table of Contents', self.go_table_contents)
+
+        self.add_nav_button(
+            nav_bar,
+            QtGui.QIcon(self.images_path.joinpath(
+                'recreate_table_contents.png').as_posix()),
+            'Recreate Table of Contents', self.recreate_table_contents)
+
+        self.add_nav_button(
+            nav_bar, self.style().standardIcon(QStyle.SP_ArrowBack),
+            'Search Backward', self.search_backward)
+
+        self.add_nav_button(
+            nav_bar, self.style().standardIcon(QStyle.SP_ArrowForward),
+            'Search Forward', self.search_forward)
+
+        self.go_to_search_results_button = self.add_nav_button(
+            nav_bar,
+            QtGui.QIcon(self.images_path.joinpath(
+                'search_results.png').as_posix()),
+            'Navigate to Search Results', self.go_search_results)
+        self.go_to_search_results_button.setEnabled(False)
+        return nav_bar
+
+    def add_nav_button(self, nav: QtWidgets.QToolBar,
+                       icon: QtGui.QIcon,
+                       tool_tip_text: str, function: Callable[[], None])\
+            -> QtWidgets.QToolButton:
+        """
+        Add a QToolButton to Navigation QToolBar including: icon, tool tip,
+            function to do
+
+        :param nav: Tool bar to add button
+        :type nav: QToolBar
+        :param icon: icon of the button
+        :type icon: QtGui.QIcon
+        :param tool_tip_text: Description of what the button will do
+        :type tool_tip_text: str
+        :param function: The method that perform the button's action
+        :type function: method
+        :return: The added button
+        :rtype:  QtWidgets.QToolButton
+        """
+        button = QtWidgets.QToolButton()
+        button.setIcon(icon)
+        button.setToolTip(tool_tip_text)
+        button.clicked.connect(function)
+        nav.addWidget(button)
+        return button
+
+    def create_tree_view(self) -> Tuple[QtWidgets.QTreeView,
+                                        QtWidgets.QFileSystemModel]:
+        """
+        Create tree_view associating with file_system_model
+
+        :return: tree view to show list of files in document folder
+        :rtype:  QTreeView
+        :return: file system model for mechanism to access file when it is
+            clicked on tree view
+        :rtype:  QFileSystemModel
+        """
+        tree_view = QtWidgets.QTreeView()
+
+        file_system_model = QtWidgets.QFileSystemModel()
+        file_system_icon_provider = QtWidgets.QFileIconProvider()
+
+        file_system_model.setIconProvider(file_system_icon_provider)
+        index = file_system_model.setRootPath(self.docdir_path.as_posix())
+
+        file_system_model.setFilter(
+            QtCore.QDir.NoDotAndDotDot | QtCore.QDir.Files)
+        file_system_model.setNameFilters(['*.help.md'])
+        file_system_model.setNameFilterDisables(False)  # hide inactive
+
+        tree_view.setItemDelegate(HelpBrowserItemDelegate())
+
+        tree_view.setModel(file_system_model)
+
+        tree_view.setRootIndex(
+            index)
+
+        for i in range(1, file_system_model.columnCount()):
+            tree_view.hideColumn(i)
+
+        width = self.geometry().size().width()
+        tree_view.setMaximumWidth(width * self.TREE_VIEW_SCALAR)
+
+        tree_view.clicked.connect(self.on_tree_view_item_clicked)
+        tree_view.setToolTip('Available documentation pages')
+        return tree_view, file_system_model
+
+    def load_file(self, file_path: str):
+        """
+        Load file from file_path to help_view
+
+        :param file_path:  absolute path to a document
+        :type file_path: str
+        """
+        url = QtCore.QUrl.fromLocalFile(file_path)
+        self.help_view.setSource(url)
+
+    @QtCore.Slot()
+    def on_tree_view_item_clicked(self, index: QtCore.QModelIndex):
+        """
+        Load file to help_view when a file name is click on tree_view
+
+        :param index: index of the file name
+        :type index: QtCore.QModelIndex
+        """
+        path = self.file_system_model.filePath(index)
+        self.load_file(path)
+
+    @QtCore.Slot()
+    def go_table_contents(self):
+        """
+        Select Table of Contents on tree view and bring the file to help view
+        """
+        self._go_to_file(self.contents_table_path)
+
+    @QtCore.Slot()
+    def recreate_table_contents(self):
+        """
+        Recreate Table of Contents when users see any inconsistent with
+            the documents in documentation folder.
+        """
+        create_table_of_content_file(self.docdir_path)
+        QtWidgets.QMessageBox.information(
+            self, "Link fixed!!!", "Table of Contents has been recreated.")
+        self.help_view.clear()
+        self.help_view.setSource(
+            QtCore.QUrl(self.contents_table_path.as_posix()))
+
+    @QtCore.Slot()
+    def go_search_results(self):
+        """
+        Bring "Search Results.md" file to view
+        """
+        self._go_to_file(self.search_results_path)
+
+    @QtCore.Slot(QtCore.QUrl)
+    def on_source_changed(self, url: QtCore.QUrl):
+        """
+        When bringing up a page to help_view from clicking on a link on
+            help_view, this slot is implemented to set the current selection
+            on tree_view and start Search on that page.
+        For Search Results, it's better experience if
+            not perform search.
+
+        :param url: The url emit from clicking on a link on help view
+        :type url: QtCore.QUrl
+        """
+        self.tree_view.setCurrentIndex(self.file_system_model.index(
+            url.path(), 0))
+        if const.SEARCH_RESULTS == url.fileName():
+            return
+        try:
+            self.start_search_on_curr_doc(self.search_box.text(),
+                                          from_entering_search_text=False)
+        except AttributeError:
+            # Error happens because highlight_format not exist when help_view
+            # first open
+            pass
+
+    @QtCore.Slot()
+    def search_backward(self):
+        """
+        When clicking on "Search Backward" button, the cursor_index will go
+            back one on the cursor_list, then highlight text in that cursor.
+        """
+        self.current_index -= 1
+        if self.current_index < 0:
+            self.current_index = len(self.cursor_list) - 1
+
+        self.help_view.setTextCursor(self.cursor_list[self.current_index])
+
+    @QtCore.Slot()
+    def search_forward(self):
+        """
+        When clicking on "Search Forward" button, the cursor_index will go
+            forsard one on the cursor_list, then highlight text in that cursor.
+        """
+        self.current_index += 1
+        if self.current_index > len(self.cursor_list) - 1:
+            self.current_index = 0
+        self.help_view.setTextCursor(self.cursor_list[self.current_index])
+
+    @QtCore.Slot(str)
+    def start_search_on_curr_doc(self, search_text: str,
+                                 from_entering_search_text: bool = True):
+        """
+        If current page is 'Search Results' the content isn't
+            matched with search_text anymore. So, navigate to
+            'Table of Contents' if in 'Search Results' page and disable
+            'Navigate to Search Results' button if the function is called
+            from clicking on go_to_search_results_button
+        Build cursor_list which is the list of cursors of all search texts'
+            occurrences on the current document on help_view.
+        Highlight the first cursor to show the starting of the search to user.
+
+        :param search_text: text to search
+        :type search_text: str
+        :param from_entering_search_text: flag indicate if the method is called
+            from entering text in search box
+        :type from_entering_search_text: bool
+        """
+        if from_entering_search_text:
+            if self.help_view.source().fileName() == const.SEARCH_RESULTS:
+                self.help_view.setSource(
+                    QtCore.QUrl(self.contents_table_path.as_posix()))
+            self.go_to_search_results_button.setEnabled(False)
+
+        self.help_view.setTextCursor(QtGui.QTextCursor())
+        doc = self.help_view.document()
+        self.cursor_list = []
+        cursor = QtGui.QTextCursor()
+        selections = []
+        while 1:
+            cursor = doc.find(search_text, cursor)
+            if cursor.isNull():
+                break
+            self.cursor_list.append(cursor)
+            sel = QtWidgets.QTextEdit.ExtraSelection()
+            sel.cursor = cursor
+            sel.format = self.highlight_format
+            selections.append(sel)
+        self.help_view.setExtraSelections(selections)        # to highlight
+        self.current_index = 0
+        # to roll to the current index and select text
+        if self.cursor_list != []:
+            self.help_view.setTextCursor(self.cursor_list[self.current_index])
+
+    @QtCore.Slot()
+    def search_through_all_docs(self):
+        """
+        Create the links to all documents that contain search text.
+        Bring up the result to view.
+        Allow user to go back to the result page.
+        """
+        self.go_to_search_results_button.setEnabled(True)
+
+        search_text = self.search_box.text()
+        if search_text == '':
+            return
+        search_results_file = create_search_results_file(
+            self.docdir_path, search_text)
+
+        self._go_to_file(search_results_file)
+
+    def _go_to_file(self, filepath):
+        """
+        + Select filename on tree_view,
+        + Bring file's content to help_view
+        """
+        self.tree_view.setCurrentIndex(self.file_system_model.index(
+            filepath.as_posix(), 0, 0))
+        self.tree_view.update()
+        self.help_view.setSource(QtCore.QUrl(filepath.as_posix()))
+
+
+def main():
+    import platform
+    import os
+    # Enable Layer-backing for MacOs version >= 11
+    # Only needed if using the pyside2 library with version>=5.15.
+    # Layer-backing is always enabled in pyside6.
+    os_name, version, *_ = platform.platform().split('-')
+    # if os_name == 'macOS' and version >= '11':
+    # mac OSX 11.6 appear to be 10.16 when read with python and still required
+    # this environment variable
+    if os_name == 'macOS':
+        os.environ['QT_MAC_WANTS_LAYER'] = '1'
+
+    app = QtWidgets.QApplication(sys.argv)
+
+    wnd = HelpBrowser(home_path='../../')
+    wnd.show()
+    sys.exit(app.exec_())
+
+
+if __name__ == '__main__':
+    main()
diff --git a/sohstationviewer/view/main_window.py b/sohstationviewer/view/main_window.py
index a49e5114a56fe37f49e2324a93dfde4d757ebafe..3d083f8b8ab2a69ae9a6727536105b25f0c9b89d 100755
--- a/sohstationviewer/view/main_window.py
+++ b/sohstationviewer/view/main_window.py
@@ -1,30 +1,33 @@
-import pathlib
 import os
-from pathlib import Path
-from datetime import datetime
+import pathlib
+import shutil
 from copy import deepcopy
-from PySide2 import QtCore, QtWidgets
-
-from sohstationviewer.view.ui.main_ui import UIMainWindow
-from sohstationviewer.view.calendar.calendar_dialog import (
-    CalendarDialog)
-from sohstationviewer.view.file_list_widget import FileListItem
-from sohstationviewer.view.plotting.waveform_dialog import WaveformDialog
-from sohstationviewer.view.plotting.time_power_squared_dialog import (
-    TimePowerSquaredDialog)
+from datetime import datetime
+from pathlib import Path
+from PySide2 import QtCore, QtWidgets, QtGui
 
+from sohstationviewer.conf.constants import TM_FORMAT
+from sohstationviewer.controller.processing import detectDataType
+from sohstationviewer.controller.util import displayTrackingInfo
+from sohstationviewer.database.process_db import execute_db_dict, execute_db
+from sohstationviewer.model.data_loader import DataLoader
+from sohstationviewer.model.data_type_model import DataTypeModel
+
+from sohstationviewer.view.calendar.calendar_dialog import CalendarDialog
+from sohstationviewer.view.channel_prefer_dialog import ChannelPreferDialog
+from sohstationviewer.view.db_config.channel_dialog import ChannelDialog
 from sohstationviewer.view.db_config.data_type_dialog import DataTypeDialog
 from sohstationviewer.view.db_config.param_dialog import ParamDialog
-from sohstationviewer.view.db_config.channel_dialog import ChannelDialog
 from sohstationviewer.view.db_config.plot_type_dialog import PlotTypeDialog
-from sohstationviewer.view.channel_prefer_dialog import (
-    ChannelPreferDialog)
-
-from sohstationviewer.controller.processing import loadData, detectDataType
-
-from sohstationviewer.database.proccessDB import executeDB_dict
-
-from sohstationviewer.conf.constants import TM_FORMAT
+from sohstationviewer.view.file_list_widget import FileListItem
+from sohstationviewer.view.plotting.time_power_squared_dialog import (
+    TimePowerSquaredDialog)
+from sohstationviewer.view.plotting.waveform_dialog import WaveformDialog
+from sohstationviewer.view.search_message.search_message_dialog import (
+    SearchMessageDialog)
+from sohstationviewer.view.help_view import HelpBrowser
+from sohstationviewer.view.ui.main_ui import UIMainWindow
+from sohstationviewer.view.util.enums import LogType
 
 
 class MainWindow(QtWidgets.QMainWindow, UIMainWindow):
@@ -42,6 +45,9 @@ class MainWindow(QtWidgets.QMainWindow, UIMainWindow):
         data_type: str - type of data set
         """
         self.data_type = 'Unknown'
+
+        self.data_loader = DataLoader()
+
         """
         req_soh_chans: [str,] - list of State-Of-Health channels to read data
             from. For Reftek, the list of channels is fixed => may not need
@@ -107,6 +113,19 @@ class MainWindow(QtWidgets.QMainWindow, UIMainWindow):
             waveform channels
         """
         self.tps_dlg = TimePowerSquaredDialog(self)
+        """
+        help_browser: HelpBrowser - Display help documents with searching
+            feature.
+        """
+        self.help_browser = HelpBrowser()
+        """
+        search_message_dialog: SearchMessageDialog - Display log, soh message
+            with searching feature.
+        """
+        self.search_message_dialog = SearchMessageDialog()
+
+        self.pull_current_directory_from_db()
+        self.delete_old_temp_data_folder()
 
     @QtCore.Slot()
     def open_data_type(self):
@@ -148,13 +167,21 @@ class MainWindow(QtWidgets.QMainWindow, UIMainWindow):
         calendar = CalendarDialog(self)
         calendar.show()
 
+    @QtCore.Slot()
+    def open_help_browser(self):
+        """
+        Open Help Dialog to view and search help documents
+        """
+        self.help_browser.show()
+        self.help_browser.raise_()
+
     @QtCore.Slot()
     def open_channel_preferences(self):
         """
         Open a dialog to view, select, add, edit, scan for preferred channels
             list.
         """
-        dir_names = [os.path.join(self.cwd_line_edit.text(), item.text())
+        dir_names = [os.path.join(self.curr_dir_line_edit.text(), item.text())
                      for item in self.open_files_list.selectedItems()]
         if dir_names == []:
             msg = "No directories has been selected."
@@ -209,11 +236,11 @@ class MainWindow(QtWidgets.QMainWindow, UIMainWindow):
         """
         Constructs a QFileDialog to select a new working directory from which
             the user can load data. The starting directory is taken from
-            cwdLineEdit.
+            curr_dir_line_edit.
         """
         fd = QtWidgets.QFileDialog(self)
         fd.setFileMode(QtWidgets.QFileDialog.Directory)
-        fd.setDirectory(self.cwd_line_edit.text())
+        fd.setDirectory(self.curr_dir_line_edit.text())
         fd.exec_()
         new_path = fd.selectedFiles()[0]
         self.set_current_directory(new_path)
@@ -233,8 +260,9 @@ class MainWindow(QtWidgets.QMainWindow, UIMainWindow):
                               if not self.all_soh_chan_check_box.isChecked()
                               else [])
 
-        self.dir_names = [Path(self.cwd_line_edit.text()).joinpath(item.text())
-                          for item in self.open_files_list.selectedItems()]
+        self.dir_names = [
+            Path(self.curr_dir_line_edit.text()).joinpath(item.text())
+            for item in self.open_files_list.selectedItems()]
         if self.dir_names == []:
             msg = "No directories has been selected."
             QtWidgets.QMessageBox.warning(self, "Select directory", msg)
@@ -277,13 +305,39 @@ class MainWindow(QtWidgets.QMainWindow, UIMainWindow):
         end_tm_str = self.time_to_date_edit.date().toString(QtCore.Qt.ISODate)
         self.start_tm = datetime.strptime(start_tm_str, TM_FORMAT).timestamp()
         self.end_tm = datetime.strptime(end_tm_str, TM_FORMAT).timestamp()
-        self.data_object = loadData(self.data_type,
-                                    self.tracking_info_text_browser,
-                                    self.dir_names,
-                                    reqWFChans=self.req_wf_chans,
-                                    reqSOHChans=self.req_soh_chans,
-                                    readStart=self.start_tm,
-                                    readEnd=self.end_tm)
+
+        self.data_loader.init_loader(self.data_type,
+                                     self.tracking_info_text_browser,
+                                     self.dir_names,
+                                     req_wf_chans=self.req_wf_chans,
+                                     req_soh_chans=self.req_soh_chans,
+                                     read_start=self.start_tm,
+                                     read_end=self.end_tm)
+        self.data_loader.worker.finished.connect(self.plot_data)
+        self.data_loader.load_data()
+
+    @QtCore.Slot()
+    def stop_load_data(self):
+        # TODO: find a way to stop the data loader without a long wait.
+        """
+        Request the data loader thread to stop. The thread will stop at the
+        earliest possible point, meaning that the wait is variable and can be
+        very long.
+        """
+        if self.data_loader.running:
+            self.data_loader.thread.requestInterruption()
+            displayTrackingInfo(self.tracking_info_text_browser,
+                                'Stopping data loading...',
+                                LogType.INFO)
+
+    @QtCore.Slot()
+    def plot_data(self, data_obj: DataTypeModel):
+        """
+        Process the loaded data and pass control to the plotter.
+
+        :param data_obj: the data object that contains the loaded data.
+        """
+        self.data_object = data_obj
         self.replot_loaded_data()
 
     @QtCore.Slot()
@@ -346,20 +400,33 @@ class MainWindow(QtWidgets.QMainWindow, UIMainWindow):
         self.tps_dlg.plotting_widget.set_peer_plotting_widgets(
             peer_plotting_widgets)
 
+        processing_log = do.processingLog + self.plotting_widget.processing_log
+        self.search_message_dialog.setup_logview(
+            sel_key, do.logData, processing_log)
+        self.search_message_dialog.show()
+
     def set_current_directory(self, path=''):
         """
-        Set all directories under current directory to self.open_files_list
+        Update currentDirectory with path in DB table PersistentData.
+        Set all directories under current directory to self.open_files_list.
+
         :param path: str - absolute path to current directory
         """
-        # Remove entries when cwd changes
+        # Remove entries when current directory changed
         self.open_files_list.clear()
-        # Signal cwd changed, and gather list of files in new cwd
+        # Signal current_directory_changed, and gather list of files in new
+        # current directory
         self.current_directory_changed.emit(path)
-        for dent in pathlib.Path(path).iterdir():
-            if not dent.is_dir() or dent.name.startswith('.'):
-                continue
+        execute_db(f'UPDATE PersistentData SET FieldValue="{path}" WHERE '
+                   'FieldName="currentDirectory"')
+        try:
+            for dent in pathlib.Path(path).iterdir():
+                if not dent.is_dir() or dent.name.startswith('.'):
+                    continue
 
-            self.open_files_list.addItem(FileListItem(dent))
+                self.open_files_list.addItem(FileListItem(dent))
+        except FileNotFoundError:
+            self.set_current_directory()
 
     def get_channel_prefer(self):
         """
@@ -369,8 +436,8 @@ class MainWindow(QtWidgets.QMainWindow, UIMainWindow):
         self.ids_name = ''
         self.ids = []
         self.data_type = 'Unknown'
-        rows = executeDB_dict('SELECT name, IDs, dataType FROM ChannelPrefer '
-                              'WHERE current=1')
+        rows = execute_db_dict('SELECT name, IDs, dataType FROM ChannelPrefer '
+                               'WHERE current=1')
         if len(rows) > 0:
             self.ids_name = rows[0]['name']
             self.ids = [t.strip() for t in rows[0]['IDs'].split(',')]
@@ -378,9 +445,48 @@ class MainWindow(QtWidgets.QMainWindow, UIMainWindow):
 
     def resizeEvent(self, event):
         """
-        When MainWindow is resized, its plotting_widget need to initialize
+        OVERRIDE Qt method.
+        When main_window is resized, its plotting_widget need to initialize
             its size to fit the viewport.
 
         :param event: QResizeEvent - resize event
         """
         self.plotting_widget.init_size()
+
+    def pull_current_directory_from_db(self):
+        """
+        Set current directory with info saved in DB
+        """
+        rows = execute_db_dict(
+            'SELECT FieldName, FieldValue FROM PersistentData '
+            'WHERE FieldName="currentDirectory"')
+        if len(rows) > 0 and rows[0]['FieldValue']:
+            self.set_current_directory(rows[0]['FieldValue'])
+
+    def closeEvent(self, event:  QtGui.QCloseEvent) -> None:
+        """
+        Cleans up when the user exits the program. Currently only clean up
+        running data loaders.
+
+        :param event: parameter of method being overridden
+        """
+        displayTrackingInfo(self.tracking_info_text_browser, 'Cleaning up...',
+                            'info')
+        if self.data_loader.running:
+            self.data_loader.thread.requestInterruption()
+            self.data_loader.thread.quit()
+            self.data_loader.thread.wait()
+
+    def delete_old_temp_data_folder(self):
+        rows = execute_db(
+            'SELECT FieldValue FROM PersistentData '
+            'WHERE FieldName="tempDataDirectory"')
+        temp_data_folder = rows[0][0]
+        try:
+            shutil.rmtree(temp_data_folder)
+            execute_db(
+                f'UPDATE PersistentData SET FieldValue="{None}" WHERE'
+                f' FieldName="tempDataDirectory"'
+            )
+        except (FileNotFoundError, TypeError):
+            pass
diff --git a/sohstationviewer/view/mainwindow.py b/sohstationviewer/view/mainwindow.py
new file mode 100755
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/sohstationviewer/view/param_dialog.py b/sohstationviewer/view/param_dialog.py
new file mode 100755
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/sohstationviewer/view/plotting/plotting_widget/plotting_axes.py b/sohstationviewer/view/plotting/plotting_widget/plotting_axes.py
index b72c95e06d6b04f455f4f1512536a5b8c081e15f..537df02597f9f9f1565b3504c6dadc8979121d93 100644
--- a/sohstationviewer/view/plotting/plotting_widget/plotting_axes.py
+++ b/sohstationviewer/view/plotting/plotting_widget/plotting_axes.py
@@ -33,6 +33,8 @@ class PlottingAxes:
         self.fig = pl.Figure(facecolor='white', figsize=(50, 100))
         self.fig.canvas.mpl_connect('button_press_event',
                                     parent.on_button_press_event)
+        self.fig.canvas.mpl_connect('pick_event',
+                                    parent.on_pick_event)
 
         """
         canvas: matplotlib.backends.backend_qt5agg.FigureCanvasQTAgg - the
diff --git a/sohstationviewer/view/plotting/plotting_widget/plotting_widget.py b/sohstationviewer/view/plotting/plotting_widget/plotting_widget.py
index 364ddd4ad2bc139da4beb0576104ea96b47e5579..701ce9385235a1ce074ac082570da3b708b8a82b 100755
--- a/sohstationviewer/view/plotting/plotting_widget/plotting_widget.py
+++ b/sohstationviewer/view/plotting/plotting_widget/plotting_widget.py
@@ -2,7 +2,7 @@
 Class of which object is used to plot data
 """
 import numpy as np
-
+from matplotlib import pyplot as pl
 from PySide2 import QtCore, QtWidgets
 
 from sohstationviewer.conf import constants
@@ -12,6 +12,9 @@ from sohstationviewer.view.plotting.plotting_widget.plotting_axes import (
     PlottingAxes)
 from sohstationviewer.view.plotting.plotting_widget.plotting import Plotting
 
+from sohstationviewer.controller.plottingData import formatTime
+from sohstationviewer.controller.util import displayTrackingInfo
+
 
 class PlottingWidget(QtWidgets.QScrollArea):
     """
@@ -258,6 +261,49 @@ class PlottingWidget(QtWidgets.QScrollArea):
         self.zoom_marker1.set_visible(False)
         self.zoom_marker2.set_visible(False)
 
+    def on_pick_event(self, event):
+        """
+        When click mouse on a clickable data point (dot with picker=True in
+            Plotting),
+        + Point's info will be displayed in tracking_box
+        + If the chan_data has key 'logIdx', raise the Search Messages dialog,
+            focus SOH tab, roll to the corresponding line.
+        """
+        artist = event.artist
+        ax = artist.axes
+        chan_id = ax.chan
+        if isinstance(artist, pl.Line2D):
+            chan_data = self.plotting_data1[chan_id]
+            # list of x values of the plot
+            x_list = artist.get_xdata()
+            # index of the clicked point on the plot
+            click_plot_index = event.ind[0]
+            # time value of the clicked point
+            clicked_time = x_list[click_plot_index]
+            # indexes of the clicked time in data (one value only)
+            clicked_indexes = np.where(chan_data['times'] == clicked_time)
+            """
+            clicked_indexes and click_plot_index can be different if there
+            are different plots for a channel.
+            """
+            clicked_data = chan_data['data'][clicked_indexes][0]
+
+            if hasattr(ax, 'unit_bw'):
+                clicked_data = ax.unit_bw.format(clicked_data)
+            formatted_clicked_time = formatTime(
+                clicked_time, self.date_mode, 'HH:MM:SS')
+            info_str = (f"<pre>Channel: {chan_id}   "
+                        f"Point:{click_plot_index + 1}   "
+                        f"Time: {formatted_clicked_time}   "
+                        f"Value: {clicked_data}</pre>")
+            displayTrackingInfo(self.tracking_box, info_str)
+
+            if 'logIdx' in chan_data.keys():
+                self.parent.search_message_dialog.show()
+                clicked_log_idx = chan_data['logIdx'][clicked_indexes][0]
+                self.parent.search_message_dialog. \
+                    show_log_entry_from_data_index(clicked_log_idx)
+
     def on_button_press_event(self, event):
         """
         When click mouse on the current plottingWidget, SOHView will loop
diff --git a/sohstationviewer/view/plotting/state_of_health_widget.py b/sohstationviewer/view/plotting/state_of_health_widget.py
index 0d6af0a0ff5bdbf26cfaecaacc9c7f0489ef912e..0b4afecc04f147b6f174572660b9225c0164ee05 100644
--- a/sohstationviewer/view/plotting/state_of_health_widget.py
+++ b/sohstationviewer/view/plotting/state_of_health_widget.py
@@ -1,5 +1,5 @@
 """
-Drawing State-Of-Health channels
+Drawing State-Of-Health channels and mass position
 """
 from sohstationviewer.view.util.plot_func_names import plot_functions
 from sohstationviewer.view.plotting.plotting_widget import plotting_widget
@@ -10,10 +10,12 @@ from sohstationviewer.controller.util import (
 
 from sohstationviewer.conf import constants
 
-from sohstationviewer.database import extractData
+from sohstationviewer.database import extract_data
 
 from sohstationviewer.model.handling_data import trim_downsample_SOHChan
 
+from sohstationviewer.view.util.enums import LogType
+
 
 class SOHWidget(plotting_widget.PlottingWidget):
     def plot_channels(self, start_tm, end_tm, key, data_time,
@@ -62,18 +64,19 @@ class SOHWidget(plotting_widget.PlottingWidget):
         if len(not_found_chan) > 0:
             msg = (f"The following channels is in Channel Preferences but "
                    f"not in the given data: {not_found_chan}")
-            self.processing_log.append((msg, 'warning'))
+            self.processing_log.append((msg, LogType.WARNING))
 
         for chan_id in self.plotting_data1:
-            chan_db_info = extractData.getChanPlotInfo(chan_id,
-                                                       self.parent.data_type)
+            chan_db_info = extract_data.get_chan_plot_info(
+                chan_id, self.parent.data_type
+            )
             if chan_db_info['height'] == 0:
                 # not draw
                 continue
             if chan_db_info['channel'] == 'DEFAULT':
                 msg = (f"Channel {chan_id}'s "
                        f"definition can't be found database.")
-                displayTrackingInfo(self.tracking_box, msg, 'warning')
+                displayTrackingInfo(self.tracking_box, msg, LogType.WARNING)
 
             if chan_db_info['plotType'] == '':
                 continue
@@ -82,7 +85,7 @@ class SOHWidget(plotting_widget.PlottingWidget):
                                self.plotting_data1, True)
 
         for chan_id in self.plotting_data2:
-            chan_db_info = extractData.getChanPlotInfo(
+            chan_db_info = extract_data.get_chan_plot_info(
                 chan_id, self.parent.data_type)
             self.plotting_data2[chan_id]['chan_db_info'] = chan_db_info
             self.get_zoom_data(self.plotting_data2[chan_id], chan_id, True)
diff --git a/sohstationviewer/view/plotting/time_power_squared_dialog.py b/sohstationviewer/view/plotting/time_power_squared_dialog.py
index 1cbcb5c42045d8cb697a5365cfe07d907c97d496..8cc2e383e4e3de736d0c3308bc4828cb950d2dda 100755
--- a/sohstationviewer/view/plotting/time_power_squared_dialog.py
+++ b/sohstationviewer/view/plotting/time_power_squared_dialog.py
@@ -1,4 +1,4 @@
-# UI and connectSignals for MainWindow
+# Display time-power-squared values for waveform data
 from math import sqrt
 import numpy as np
 
@@ -16,8 +16,8 @@ from sohstationviewer.controller.util import (
 from sohstationviewer.model.handling_data import (
     get_trimTPSData, get_eachDay5MinList, findTPSTm)
 
-from sohstationviewer.database.extractData import (
-    getColorDef, getColorRanges, getChanLabel)
+from sohstationviewer.database.extract_data import (
+    get_color_def, get_color_ranges, get_chan_label)
 
 from sohstationviewer.conf import constants as const
 
@@ -57,8 +57,6 @@ class TimePowerSquaredWidget(plotting_widget.PlottingWidget):
         self.tps_t = 0
 
         super().__init__(*args, **kwarg)
-        self.plotting_axes.fig.canvas.mpl_connect(
-            'pick_event', self.on_pick_event)
 
     def plot_channels(self, start_tm=None, end_tm=None, key=None,
                       data_time=None, waveform_data=None):
@@ -109,9 +107,19 @@ class TimePowerSquaredWidget(plotting_widget.PlottingWidget):
 
     def get_plot_data(self, c_data, chan_id):
         """
-        Trim data to minx, max_x and calculate time-power-square for each 5
-            minute into c_data['tps_data'] then draw each 5 minute with the
-            color corresponding to value
+        TPS is plotted in lines of small rectangular, so called bars.
+        Each line is a day so - y value is the order of days
+        Each bar is data represent for 5 minutes so x value is the order of
+            five minute in a day
+        If there is no data in a portion of a day, the bars in the portion
+            will have grey color.
+        For the five minutes that have data, the color of the bars will be
+            based on mapping between tps value of the five minutes against
+            the selected color range.
+
+        This function trim data to minx, max_x and calculate time-power-square
+            for each 5 minute into c_data['tps_data'] then draw each 5 minute
+            with the color corresponding to value.
         Create ruler, zoom_marker1, zoom_marker2 for the channel.
 
         :param c_data: dict - data of the channel which includes down-sampled
@@ -130,7 +138,7 @@ class TimePowerSquaredWidget(plotting_widget.PlottingWidget):
         ax = self.create_axes(self.plotting_bot, plot_h)
         ax.text(
             -0.1, 1.2,
-            f"{getChanLabel(chan_id)} {c_data['samplerate']}",
+            f"{get_chan_label(chan_id)} {c_data['samplerate']}",
             horizontalalignment='left',
             verticalalignment='top',
             rotation='horizontal',
@@ -159,6 +167,7 @@ class TimePowerSquaredWidget(plotting_widget.PlottingWidget):
         square_counts = self.parent.sel_square_counts  # square counts range
         color_codes = self.parent.color_def  # colordef
 
+        # --------------------------- PLOT TPS -----------------------------#
         for dayIdx, y in enumerate(c_data['tps_data']):
             # not draw data out of day range
             color_set = self.get_color_set(y, square_counts, color_codes)
@@ -268,28 +277,34 @@ class TimePowerSquaredWidget(plotting_widget.PlottingWidget):
             xdata = event.mouseevent.xdata
             if xdata is None:
                 return
-            xdata = round(xdata)
+            xdata = round(xdata)                        # x value on the plot
             # when click on outside xrange that close to edge, adjust to edge
             if xdata in [-2, -1]:
                 xdata = 0
             if xdata in [288, 289]:
                 xdata = 287
-            ydata = round(event.mouseevent.ydata)
-
-            y_idx = - ydata
-            x_idx = xdata
-            # identify time for rulers on other plotting widget
-            self.tps_t = self.each_day5_min_list[y_idx, x_idx]
-            format_t = formatTime(self.tps_t, self.date_mode, 'HH:MM:SS')
-            info_str += f"{format_t}:"
-            for chan_id in self.plotting_data1:
-                c_data = self.plotting_data1[chan_id]
-                data = c_data['tps_data'][y_idx, x_idx]
-                info_str += (f"  {chan_id}:"
-                             f"{add_thousand_separator_to_int(sqrt(data))}")
-            info_str += "  (counts)"
-            displayTrackingInfo(self.tracking_box, info_str)
-            self.draw()
+            ydata = round(event.mouseevent.ydata)       # y value on the plot
+
+            # refer to description in get_plot_data to understand x,y vs
+            # day_index, five_min_index
+            day_index = - ydata
+            five_min_index = xdata
+            try:
+                # identify time for rulers on other plotting widget
+                self.tps_t = self.each_day5_min_list[day_index, five_min_index]
+                format_t = formatTime(self.tps_t, self.date_mode, 'HH:MM:SS')
+                info_str += f"<pre>{format_t}:"
+                for chan_id in self.plotting_data1:
+                    c_data = self.plotting_data1[chan_id]
+                    data = c_data['tps_data'][day_index, five_min_index]
+                    info_str += f"   {chan_id}:{fmti(sqrt(data))}"
+                info_str += "  (counts)</pre>"
+                displayTrackingInfo(self.tracking_box, info_str)
+                self.draw()
+            except IndexError:
+                # exclude the extra points added to the 2 sides of x axis to
+                # show the entire highlight box
+                pass
 
     def on_ctrl_cmd_click(self, xdata):
         """
@@ -398,7 +413,7 @@ class TimePowerSquaredDialog(QtWidgets.QWidget):
         color_def: [str,] -  list of color codes in order of values to be
             displayed
         """
-        self.color_def = getColorDef()
+        self.color_def = get_color_def()
         """
         sel_square_counts: [int,] - selected time-power-square ranges
         """
@@ -416,7 +431,7 @@ class TimePowerSquaredDialog(QtWidgets.QWidget):
         """
         (self.color_ranges,
          self.all_square_counts,
-         self.color_label) = getColorRanges()
+         self.color_label) = get_color_ranges()
 
         """
         color_range_choice: QComboBox - dropdown box for user to choose a
@@ -449,6 +464,7 @@ class TimePowerSquaredDialog(QtWidgets.QWidget):
 
     def resizeEvent(self, event):
         """
+        OVERRIDE Qt method.
         When TimePowerDialog is resized, its plotting_widget need to initialize
             its size to fit the viewport.
 
diff --git a/sohstationviewer/view/plotting/waveform_dialog.py b/sohstationviewer/view/plotting/waveform_dialog.py
index 2893a16f6e035454ac6084ae8879a13f1335303d..75fb9343af8c19f297762047ff3c567ef258fbf5 100755
--- a/sohstationviewer/view/plotting/waveform_dialog.py
+++ b/sohstationviewer/view/plotting/waveform_dialog.py
@@ -10,7 +10,7 @@ from sohstationviewer.model.handling_data import trim_downsample_WFChan
 from sohstationviewer.controller.plottingData import getTitle
 from sohstationviewer.controller.util import apply_convert_factor
 
-from sohstationviewer.database import extractData
+from sohstationviewer.database import extract_data
 
 from sohstationviewer.conf import constants as const
 
@@ -56,13 +56,13 @@ class WaveformWidget(plotting_widget.PlottingWidget):
         self.plotting_axes.set_title(title)
 
         for chan_id in self.plotting_data1:
-            chan_db_info = extractData.getWFPlotInfo(chan_id)
+            chan_db_info = extract_data.get_wf_plot_info(chan_id)
             if chan_db_info['plotType'] == '':
                 continue
             self.plotting_data1[chan_id]['chan_db_info'] = chan_db_info
             self.get_zoom_data(self.plotting_data1[chan_id], chan_id, True)
         for chan_id in self.plotting_data2:
-            chan_db_info = extractData.getChanPlotInfo(
+            chan_db_info = extract_data.get_chan_plot_info(
                 chan_id, self.parent.data_type)
             self.plotting_data2[chan_id]['chan_db_info'] = chan_db_info
             self.get_zoom_data(self.plotting_data2[chan_id], chan_id, True)
@@ -193,6 +193,7 @@ class WaveformDialog(QtWidgets.QWidget):
 
     def resizeEvent(self, event):
         """
+        OVERRIDE Qt method.
         When WaveformDialog is resized, its plotting_widget need to initialize
             its size to fit the viewport.
 
diff --git a/sohstationviewer/view/search_message/__init__.py b/sohstationviewer/view/search_message/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/sohstationviewer/view/search_message/highlight_delegate.py b/sohstationviewer/view/search_message/highlight_delegate.py
new file mode 100644
index 0000000000000000000000000000000000000000..cda425ca1c27191f8c0376f048fc36c06ce8d2ef
--- /dev/null
+++ b/sohstationviewer/view/search_message/highlight_delegate.py
@@ -0,0 +1,120 @@
+"""
+Credit: https://stackoverflow.com/questions/53353450/how-to-highlight-a-words-in-qtablewidget-from-a-searchlist  # noqa
+"""
+from typing import List, Dict
+from PySide2 import QtCore, QtGui, QtWidgets
+from PySide2.QtGui import QPen, QTextCursor, QTextCharFormat, QColor
+
+
+class HighlightDelegate(QtWidgets.QStyledItemDelegate):
+    """
+    Help with highlighting search text in SOH Message tables.
+    """
+    def __init__(self, parent=None, display_color=None):
+        super(HighlightDelegate, self).__init__(parent)
+        """
+        doc: container to format soh table's item
+        """
+        self.doc: QtGui.QTextDocument = QtGui.QTextDocument(self)
+        """
+        filters: list of texts to apply text highlighted
+        """
+        self.filters: List[str] = []
+        """
+        current_row: the row index of the item to apply border highlighted
+        """
+        self.current_row: int = -1
+        """
+        display_color: {type: color,} - color map for setting highlight color
+        """
+        self.display_color: Dict[str, str] = display_color
+
+    def paint(self, painter: QtGui.QPainter,
+              option: QtWidgets.QStyleOptionViewItem,
+              index: QtCore.QModelIndex):
+        """
+        Custom rendering which is a reimplementation of the abstract function
+        paint()
+
+        Parameters
+        ----
+        painter: painter of the table
+        option: the item widget
+        index: location of the item on the table widget
+        """
+        painter.save()
+        options = QtWidgets.QStyleOptionViewItem(option)
+        self.initStyleOption(options, index)
+        self.doc.setPlainText(options.text)
+        if index.column() != 0:
+            # not apply highlight for column 0
+            self.apply_highlight(painter, option, index.row())
+        options.text = ""
+        style = QtWidgets.QApplication.style() if options.widget is None \
+            else options.widget.style()
+        style.drawControl(QtWidgets.QStyle.CE_ItemViewItem, options, painter)
+
+        context = QtGui.QAbstractTextDocumentLayout.PaintContext()
+        if option.state & QtWidgets.QStyle.State_Selected:
+            role = QtGui.QPalette.HighlightedText
+        else:
+            role = QtGui.QPalette.Text
+        context.palette.setColor(
+            QtGui.QPalette.Text,
+            option.palette.color(QtGui.QPalette.Active, role)
+        )
+
+        text_rect = style.subElementRect(
+            QtWidgets.QStyle.SE_ItemViewItemText, options)
+
+        if index.column() != 0:
+            text_rect.adjust(5, 0, 0, 0)
+
+        the_constant = 4
+        margin = (option.rect.height() - options.fontMetrics.height()) // 2
+        margin = margin - the_constant
+        text_rect.setTop(text_rect.top() + margin)
+
+        painter.translate(text_rect.topLeft())
+        painter.setClipRect(text_rect.translated(-text_rect.topLeft()))
+        self.doc.documentLayout().draw(painter, context)
+
+        painter.restore()
+
+    def apply_highlight(self, painter, option, row):
+        """
+        Look through doc to find the word matched with all text in filters
+        to highlight it.
+        """
+        cursor = QTextCursor(self.doc)
+        cursor.beginEditBlock()
+        fmt = QTextCharFormat()
+        fmt.setForeground(QColor(self.display_color['highlight']))
+        fmt.setFontWeight(700)
+        done_task = False
+        for f in self.filters:
+            highlight_cursor = QTextCursor(self.doc)
+            while (not highlight_cursor.isNull() and
+                   not highlight_cursor.atEnd()):
+                highlight_cursor = self.doc.find(f, highlight_cursor)
+                if not highlight_cursor.isNull():
+                    highlight_cursor.mergeCharFormat(fmt)
+                    if not done_task and row == self.current_row:
+                        painter.setPen(
+                            QPen(QColor(self.display_color['highlight']), 3))
+                        painter.drawRect(option.rect)
+                        done_task = True
+        cursor.endEditBlock()
+
+    def set_filters(self, filters: List[str]):
+        """
+        Set texts to highlight
+
+        Parameters
+        ----
+        filters: list of texts
+        """
+        self.filters = filters
+
+    def set_current_row(self, current_row):
+        self.current_row = current_row
diff --git a/sohstationviewer/view/search_message/search_message_dialog.py b/sohstationviewer/view/search_message/search_message_dialog.py
new file mode 100644
index 0000000000000000000000000000000000000000..2766d4064083a53d1def28acda7ea9c7e9d32ba7
--- /dev/null
+++ b/sohstationviewer/view/search_message/search_message_dialog.py
@@ -0,0 +1,710 @@
+import sys
+import os
+from pathlib import PosixPath, Path
+from typing import Dict, List, Tuple, Callable, Union, Optional
+
+from PySide2 import QtGui, QtCore, QtWidgets
+from PySide2.QtWidgets import QStyle
+
+from sohstationviewer.view.search_message.highlight_delegate import (
+    HighlightDelegate)
+from sohstationviewer.view.util.functions import (
+    get_soh_messages_for_view, log_str)
+from sohstationviewer.view.util.color import set_color_mode
+from sohstationviewer.view.util.enums import LogType
+
+
+class SearchMessageDialog(QtWidgets.QWidget):
+    """
+    GUI:
+        + "Search SOH Lines" tab: to list all lines that includes searched text
+            with its channel
+        + "Processing Logs" tab: to list all processing logs with its log type
+            and color of each line depends on its log type.
+        + Each SOH LOG channel has its own tab
+    For the last two types of tabs, user can use the buttons in the tool bar
+        to process the following tasks:
+        + The 1st button: The current table rolls back to the current selected
+            line.
+        + Type a searched text, the searched text will be highlighted through
+            the table and the current table scrolls to the first line that
+            contains the searched text.
+        + The 2nd button: The current table rolls to the next line that
+            contains the searched text.
+        + The 3rd button: The current table rolls to the previous line that
+            contains the searched text.
+        + The 4th button: Save the content of the table to a text file
+    Interaction: When user click on a clickable data point on a SOH channel
+        of RT130, SOH tab will be focus and the line corresponding to the
+        data point will be highlighted and rolled to view.
+
+    Attributes
+    ----
+    SCREEN_SIZE_SCALAR_X : float
+        Specifies how to scale the width of the window, in relation to total
+        screen size.
+    SCREEN_SIZE_SCALAR_Y : float
+        Specifies how to scale the height of the window, in relation to total
+        screen size.
+    """
+    SCREEN_SIZE_SCALAR_X = 0.40
+    SCREEN_SIZE_SCALAR_Y = 0.50
+
+    def __init__(self, home_path: str = '.'):
+        super().__init__()
+        # Get screen dimensions
+        geom = QtGui.QGuiApplication.screens()[0].availableGeometry()
+        self.setGeometry(10, 10,
+                         geom.size().width() * self.SCREEN_SIZE_SCALAR_X,
+                         geom.size().height() * self.SCREEN_SIZE_SCALAR_Y
+                         )
+        self.setWindowTitle("Search Messages")
+
+        """
+        images_path: path to the folder containing images
+        """
+        self.images_path: PosixPath = Path(
+            home_path).resolve().joinpath('images')
+
+        # -------------------- PRE-DEFINE WIDGETS ----------------------------
+        """
+        search_box: QLineEdit - text box to enter a string to search along the
+            current table.
+        """
+        self.search_box: QtWidgets.QLineEdit = None
+        """
+        tab_widget: QTabWidget - widget to set up tabs for tables.
+            All soh tables in this widget will be deleted before setting up new
+            tables for new data reading because channels may be different for
+            each data set.
+        """
+        self.tab_widget: QtWidgets.QTabWidget = None
+        """
+        info_display: QTextEdit - text box to show info of selected line
+        """
+        self.info_display: QtWidgets.QTextEdit = None
+        """
+        filter_soh_lines_table: QTableWidget - table to display all lines that
+            contains search text
+        """
+        self.filter_soh_lines_table: QtWidgets.QTableWidget = None
+        """
+        processing_log_table: QTableWidget - table to display processing log
+        """
+        self.processing_log_table: QtWidgets.QTableWidget = None
+        """
+        current_table: QTableWidget - The active table (widget of a tab for
+            this dialog)
+        """
+        self.current_table: QtWidgets.QTableWidget = None
+        """
+        selected_item: QTableWidgetItem - The last selected cell widget in a
+            table. There is only one selected_item over all tables
+        """
+        self.selected_item: QtWidgets.QTableWidgetItem = None
+        """
+        active_table_for_new_dataset: QTableWidget - table to be set active
+            when a new dataset is load
+        """
+        self.active_table_for_new_dataset: QtWidgets.QTableWidget = None
+        """
+        save_button: QToolButton - Button to save log messages on current
+            tab in a text file
+        """
+        self.save_button: QtWidgets.QToolButton = None
+        """
+        delegate: HighlightDelegate - delegate that help format current table
+            items that contain search_text
+        """
+        self.delegate: HighlightDelegate = None
+        # --------------------- PRE-DEFINED OTHER ATTRIBUTES ----------------
+        """
+        search_rowidx: int - The last row index found during searching
+        """
+        self.search_rowidx: int = 0
+        """
+        search_text: str - text to search for
+        """
+        self.search_text: str = ""
+        """
+        display_color: {type: color,} - color map for setting background color
+            for different types of processing log or
+             "state of health"/"Events:" labels
+        """
+        self.display_color: Dict[str, str] = set_color_mode("W")
+        """
+        processing_logs: [(message, type),] - record of processing progress
+        """
+        self.processing_logs: List[(str, str)] = []
+        """
+        soh_dict: {chan_id: [str,]} - dict of list of soh message lines
+            for each soh channel
+        """
+        self.soh_dict: Dict[str, List[str]] = {}
+        """
+        soh_tables_dict: {chan_id: QTableWidget,} - dict of table for
+            each soh channel
+        """
+        self.soh_tables_dict: Dict[str, QtWidgets.QTableWidget] = {}
+
+        self.setup_ui()
+
+    def setup_ui(self):
+        """
+        Set up GUI includes:
+            + A search box to enter words for searching
+            + A toolbar allow user to go back to last selected, search next,
+                search previous, save file
+            + Tabs each of which is a table to show message for processing
+                log or a state-of-health channel's message
+            + A text box to show selected line's text
+        """
+        # ------------------------- search box ------------------------
+        self.search_box = QtWidgets.QLineEdit()
+        self.search_box.setTextMargins(7, 7, 7, 7)
+        self.search_box.setFixedHeight(40)
+        self.search_box.setClearButtonEnabled(True)
+        self.search_box.setPlaceholderText("Enter a string to search")
+        self.search_box.textChanged.connect(self.on_search_text_changed)
+        # -------------- Create tool buttons -------------------------
+
+        navigation = QtWidgets.QToolBar('Navigation')
+        self.add_nav_button(
+            navigation,
+            QtGui.QIcon(
+                self.images_path.joinpath('to_selected.png').as_posix()),
+            'Scroll to selected line of the current table',
+            self.scroll_to_selected)
+        self.add_nav_button(
+            navigation,
+            self.style().standardIcon(QStyle.SP_ArrowBack),
+            'Scroll to previous search text',
+            self.scroll_to_previous_search_text)
+        self.add_nav_button(
+            navigation,
+            self.style().standardIcon(QStyle.SP_ArrowForward),
+            'Scroll to next search text',
+            self.scroll_to_next_search_text)
+        self.save_button = self.add_nav_button(
+            navigation,
+            self.style().standardIcon(QStyle.SP_DialogSaveButton),
+            'Save messages in current tab to text file',
+            self.save_messages)
+
+        # --------- display info message ------------------
+        self.info_display = QtWidgets.QLineEdit()
+        self.info_display.setTextMargins(7, 7, 7, 7)
+        self.info_display.setFixedHeight(40)
+        self.info_display.setReadOnly(True)
+
+        # --------- set layout -------------------------------------
+        main_layout = QtWidgets.QVBoxLayout()
+        self.setLayout(main_layout)
+        main_layout.setSpacing(10)
+        main_layout.setContentsMargins(5, 5, 5, 5)
+
+        main_layout.addWidget(self.search_box)
+
+        main_layout.addWidget(navigation)
+
+        tab_widget_layout = QtWidgets.QVBoxLayout()
+        main_layout.addLayout(tab_widget_layout)
+
+        self.tab_widget = QtWidgets.QTabWidget()
+        tab_widget_layout.addWidget(self.tab_widget)
+        self.tab_widget.currentChanged.connect(self.set_current_tab)
+
+        self.filter_soh_lines_table = self.create_table(0, 2)
+        self.tab_widget.addTab(
+            self.filter_soh_lines_table, 'Filter SOH Lines')
+        self.processing_log_table = self.create_table(0, 2, [0])
+        self.tab_widget.addTab(
+            self.processing_log_table, 'Processing Logs')
+        main_layout.addWidget(self.info_display)
+
+    def setup_logview(
+            self, key: Union[str, Tuple[str, str]],
+            soh_messages: Dict[str, Union[List[str], Dict[str, List[str]]]],
+            processing_logs: List[Tuple[str, str]]):
+        """
+        When a dataset is loaded,
+            + processing_logs need to be refilled in processing_log_table
+            + Tabs need to be recreated for soh channels since they are
+                different for each dataset
+            + If there is any info in processing_log_table, it will be
+                activated. Otherwise, the first soh table will be activated
+
+        Parameters
+        ----
+        key: str or (str, str): key to identify the data set
+        soh_messages: {'TEXT': [str,], key:{chan_id: [str,],},} - info from log
+            channels, soh messages, text file
+        processing_logs: [(message, type),] - record of processing progress
+        """
+        for i in range(self.tab_widget.count() - 1, 1, -1):
+            # delete all soh tabs in self.tab_widget
+            widget = self.tab_widget.widget(i)
+            self.tab_widget.removeTab(i)
+            widget.setParent(None)
+
+        self.soh_tables_dict = {}
+
+        self.add_processing_log_table(processing_logs)
+        self.add_soh_tables(key, soh_messages)
+        self.tab_widget.setCurrentWidget(self.active_table_for_new_dataset)
+
+    def add_processing_log_table(self, processing_logs: List[Tuple[str, str]]):
+        """
+        Adding info to Processing Logs table
+
+        Parameters
+        ----
+        processing_logs: [(message, type),] - record of processing progress
+        """
+
+        self.processing_logs = processing_logs
+        if processing_logs == []:
+            self.active_table_for_new_dataset = None
+        else:
+            self.active_table_for_new_dataset = self.processing_log_table
+
+        self.processing_log_table.setRowCount(0)
+        count = 0
+        processing_logs = (
+            [("There are no processing logs to display.", LogType.INFO)]
+            if processing_logs == [] else processing_logs)
+        for log_line in processing_logs:
+            self.add_line(self.processing_log_table, text1=count,
+                          text2=log_line[0], log_type=log_line[1])
+
+    def add_soh_tables(
+            self, key: Union[str, Tuple[str, str]],
+            soh_messages: Dict[str, Union[List[str], Dict[str, List[str]]]]):
+        """
+        Adding SOH Channel tables to tab_widget and their info
+
+        Parameters
+        ----
+        key: str or (str, str): key to identify the data set
+        soh_messages: {'TEXT': [str,], key:{chan_id: [str,],},} - info from log
+            channels, soh messages, text file
+        """
+        self.soh_dict = get_soh_messages_for_view(key, soh_messages)
+        for chan_id in self.soh_dict:
+            self.soh_tables_dict[chan_id] = self.create_table(0, 2, [0])
+            if self.active_table_for_new_dataset is None:
+                self.active_table_for_new_dataset = self.soh_tables_dict[
+                    chan_id]
+            self.tab_widget.addTab(self.soh_tables_dict[chan_id], chan_id)
+            count = 0
+            for log_line in self.soh_dict[chan_id]:
+                self.add_line(
+                    self.soh_tables_dict[chan_id], text1=count, text2=log_line)
+                count += 1
+
+    def add_nav_button(self, nav: QtWidgets.QToolBar,
+                       icon: QtGui.QIcon,
+                       tool_tip_text: str, function: Callable[[], None])\
+            -> QtWidgets.QToolButton:
+        """
+        Add a QToolButton to Navigation QToolBar including: icon, tool tip,
+            function to do
+
+        :param nav: Tool bar to add button
+        :type nav: QToolBar
+        :param icon_pic: Pix map of the picture of the button
+        :type icon_pic: QStyle.StandardPixMap
+        :param tool_tip_text: Description of what the button will do
+        :type tool_tip_text: str
+        :param function: The method that perform the button's action
+        :type function: method
+        :return: The added button
+        :rtype:  QtWidgets.QToolButton
+        """
+        button = QtWidgets.QToolButton()
+        button.setIcon(icon)
+        button.setToolTip(tool_tip_text)
+        button.clicked.connect(function)
+        nav.addWidget(button)
+        return button
+
+    def add_line(self, table: QtWidgets.QTableWidget,
+                 text1: Union[str, int], text2: str,
+                 log_type: LogType = LogType.INFO):
+        """
+        Add a line of message to a row of table and color it according to
+            log_type (red for error, orange for warning) or blue header of
+            block of soh messages
+        Parameters
+        ----
+        table: QTableWidget - table to add row
+        text1: str/int - index of row for soh tables, log_type for
+            Processing Logs table, channel id for Search SOH Lines table
+        text2: str - content of line
+        log_type: LogType - type of log line
+        """
+        count = table.rowCount()
+        table.setRowCount(count + 1)
+
+        # Add data index to column 0
+        it = QtWidgets.QTableWidgetItem(f'{text1}')
+        table.setItem(count, 0, it)
+
+        # Add log message to column 1
+        it = QtWidgets.QTableWidgetItem(f'{text2}')
+        if log_type is not None:
+            # set background color for error/warning processing log
+            if log_type == LogType.ERROR:
+                it.setBackground(QtGui.QColor(self.display_color['error']))
+            elif log_type == LogType.WARNING:
+                it.setBackground(QtGui.QColor(self.display_color['warning']))
+
+        if "state of health" in text2.lower() or text2 == "Events:":
+            # set background color for header of a block of soh messages
+            it.setBackground(
+                QtGui.QColor(self.display_color['state_of_health']))
+        it.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+        table.setItem(count, 1, it)
+
+    def create_table(self, rows: int, cols: int, hidden: List[int] = [])\
+            -> QtWidgets.QTableWidget:
+        """
+        Creates and returns a QTableWidget with the specified number
+            of rows and columns.
+        The first column should be the index of the line
+        The second column should be the text of the line
+
+        Parameters
+        ----
+        rows : int
+            Number of rows in the created table
+        cols : int
+            Number of columns in the created table
+        hidden : iterable object
+            An iterable containing >= 0 int's. Each integer should
+            correspond to the index of a column which should be hidden
+            from display.
+
+        Returns
+        ----
+        QTableWidget
+            The constructed QTableWidget
+        """
+        # add 1 extra column to show scroll bar (+ 1)
+        table = QtWidgets.QTableWidget(rows, cols + 1)
+        delegate = HighlightDelegate(table, self.display_color)
+        table.setItemDelegate(delegate)
+        # Hide header cells
+        table.verticalHeader().setVisible(False)
+        table.horizontalHeader().setVisible(False)
+
+        # Expand horizontally
+        table.horizontalHeader().setStretchLastSection(True)
+
+        table.horizontalHeader().setSectionResizeMode(
+            1, QtWidgets.QHeaderView.ResizeToContents)
+
+        # Hide cell grid
+        table.setShowGrid(False)
+
+        table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
+        table.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection)
+        table.itemClicked.connect(self.on_log_entry_clicked)
+
+        for index in hidden:
+            table.setColumnHidden(index, True)
+
+        return table
+
+    def search(self, col: int = 1, direction: str = 'next',
+               start_search: bool = False)\
+            -> Optional[Tuple[QtWidgets.QTableWidgetItem, int]]:
+        """
+        Searches all rows of a table for search_text and returns the item that
+            contains search_text and its row index
+
+        Parameters
+        ----
+        col: int - index of the column to search text from
+        direction: str - next/previous: show which direction to search
+        start_search: bool - if this search start from typing to search box
+
+        Returns
+        ----
+        item: QTableWidgetItem, row: int
+            A QTableWidgetItem is returned if there is an item matching the
+            query in table, otherwise None is returned
+            Index of the row where search_text is found
+        Or None if no text found
+        """
+        self.delegate.set_current_row(-1)
+        self.info_display.setText('')
+        self.current_table.viewport().update()
+
+        if self.search_text == '':
+            return
+
+        if direction == "next":
+            if not start_search:
+                self.search_rowidx += 1
+            total_rows = self.current_table.rowCount()
+            search_range = range(self.search_rowidx, total_rows)
+        else:
+            search_range = range(self.search_rowidx - 2, -1, -1)
+        for row in search_range:
+            item = self.current_table.item(row, col)
+            if item is None:
+                continue
+            if self.search_text.lower() in item.text().lower():
+                self.delegate.set_current_row(row)
+                self.current_table.viewport().update()
+                message_index = self.current_table.item(item.row(), 0).text()
+                message_text = self.current_table.item(item.row(), 1).text()
+                self.info_display.setText(
+                    f"Line {message_index}:   {message_text}")
+                if direction == "next":
+                    row += 1
+                return item, row
+        if not start_search:
+            opposite_direction = 'previous' if direction == 'next' else 'next'
+            self.info_display.setText(
+                f"'{self.search_text}' not found in '{direction}' direction. "
+                f"Try click '{opposite_direction}' instead."
+            )
+        return
+
+    def _show_log_message(self, item: QtWidgets.QTableWidgetItem):
+        """
+        Showing the given item on the current table by:
+            + Scrolling to the given item on the current table.
+            + Set focus on the current table so that the item will
+                be highlight instead of grey out.
+
+        Parameters
+        ----
+        item : QTableWidgetItem
+            A valid QTableWidgetIem
+        """
+        self.current_table.scrollToItem(item)
+        self.current_table.setFocus()
+
+    def show_log_entry_from_data_index(self, data_index: int):
+        """
+        This is called when clicking a clickable data point on a SOH channel
+            of RT130, data_index, which represent for row index in 'SOH' table,
+            will be given. This method will:
+            + set current tab to soh_table_dict['SOH']
+            + make a search on soh_table_dict['SOH']
+            + scroll to and highlight the row corresponding to the data point
+
+        Parameters
+        ----
+        data_index : int
+            The index (of the data point that has been loaded from disk)
+            to be selected and scrolled to.
+        """
+        if 'SOH' not in self.soh_tables_dict:
+            return
+        self.tab_widget.setCurrentWidget(self.soh_tables_dict['SOH'])
+        self.search_text = str(data_index)
+        self.search_rowidx = 0
+        ret = self.search(col=0, start_search=True)
+        if ret is None:
+            raise ValueError(f'Not found line: ({data_index})')
+        it, r = ret
+        self.on_log_entry_clicked(it)
+        self._show_log_message(it)
+        self.setWindowState(QtCore.Qt.WindowState.WindowActive)
+        self.raise_()
+        self.activateWindow()
+
+    @QtCore.Slot()
+    def set_current_tab(self):
+        """
+        When a tab is selected,
+            + Reset selected_item
+            + Reset search_text to text in search_box because search text.
+                may still keep data_index from RT130 SOH data point clicked
+                interaction.
+            + Set current_table.
+            + Redo the search on search_text.
+            + Disable save button if table is filter_soh_lines_table, or if
+                table is processing_log_table but have no information.
+        """
+        self.selected_item = None
+        self.search_text = self.search_box.text()   # reset data_index
+        self.current_table = self.tab_widget.currentWidget()
+        self.on_search_text_changed(self.search_text)
+
+        if (self.current_table == self.filter_soh_lines_table or
+                (self.current_table == self.processing_log_table and
+                 self.processing_logs == [(
+                    "There are no processing logs to display.", LogType.INFO)]
+                 )):
+            self.save_button.setEnabled(False)
+        else:
+            self.save_button.setEnabled(True)
+
+    @QtCore.Slot()
+    def scroll_to_selected(self):
+        """
+        Scroll back to the selected item on the current_table
+        """
+        it = self.selected_item
+        if not it:
+            return
+        self._show_log_message(it)
+
+    @QtCore.Slot()
+    def scroll_to_previous_search_text(self):
+        """
+        Scroll to the search_text before the last search roll
+        """
+        ret = self.search(direction="previous")
+        if ret is None:
+            return
+        self.selected_item, self.search_rowidx = ret
+        self.scroll_to_selected()
+
+    @QtCore.Slot()
+    def scroll_to_next_search_text(self):
+        """
+        Scroll to the search_text after the last search roll
+        """
+        ret = self.search()
+        if ret is None:
+            return
+        self.selected_item, self.search_rowidx = ret
+        self.scroll_to_selected()
+
+    @QtCore.Slot()
+    def on_log_entry_clicked(self, item: QtWidgets.QTableWidgetItem):
+        """
+        Set selectRow for item on the current_table, display the info on
+            info_display and set selected_item = item
+
+        Parameters
+        ----
+        item : QTableWidgetItem
+            A valid QTableWidgetIem to select
+        """
+        self.current_table.selectRow(item.row())
+
+        # # Display full text of message
+        message_index = self.current_table.item(item.row(), 0).text()
+        message_text = self.current_table.item(item.row(), 1).text()
+        self.info_display.setText(f"Line {message_index}:   {message_text}")
+        self.selected_item = item
+
+    @QtCore.Slot()
+    def on_search_text_changed(self, text: str):
+        """
+        When text in search_box is changed,
+            + Set search_text
+            + Highlight text in all tables if it is not a search for data_index
+            + If the current_table is Processing Logs or a SOH channel table,
+                jump to the first search text.
+            + If the current_table is Search SOH Lines, filter all SOH lines to
+                display the lines contain the search_text only.
+        """
+        self.search_text = text
+        self.delegate = self.current_table.itemDelegate()
+        # check to highlight text when searching for text in search_box but
+        # not highlight when searching for data_index
+        if self.search_box.text() == self.search_text:
+            self.delegate.set_filters([text])
+            self.current_table.viewport().update()
+
+        if self.current_table != self.filter_soh_lines_table:
+            self._jumpto_search_text_on_current_table()
+        else:
+            self._filter_lines_with_search_text_from_soh_messages()
+
+    def _jumpto_search_text_on_current_table(self):
+        """
+        Jump to the first search text by search forward for search_text on the
+            current_table from top of the table then scroll to that row.
+        """
+        self.search_rowidx = 0
+        ret = self.search(start_search=True)
+        if ret is None:
+            return
+        self.selected_item, self.search_rowidx = ret
+        self.current_table.scrollToItem(self.selected_item)
+
+    def _filter_lines_with_search_text_from_soh_messages(self):
+        """
+        Filter all SOH lines to display the lines contain the search_text only.
+        """
+        self.filter_soh_lines_table.setRowCount(0)
+        if self.search_text == '':
+            return
+
+        for chan_id in self.soh_dict:
+            for line in self.soh_dict[chan_id]:
+                if self.search_text in line:
+                    self.add_line(self.filter_soh_lines_table,
+                                  text1=chan_id, text2=line)
+
+    @QtCore.Slot()
+    def save_messages(self):
+        """
+        Save text on the current_table to a text file.
+        """
+        tab_index = self.tab_widget.currentIndex()
+        tab_name = self.tab_widget.tabText(tab_index)
+        default_name = os.path.join(QtCore.QDir.homePath(), f'{tab_name}.txt')
+        file_name = QtWidgets.QFileDialog.getSaveFileName(
+            self, 'Save File', default_name, "text(*.txt)")[0]
+        if file_name == '':
+            return
+        with open(file_name, 'w') as file:
+            if self.current_table == self.processing_log_table:
+                file.write('\n'.join(map(log_str, self.processing_logs)))
+            else:
+                file.write('\n'.join(self.soh_dict[tab_name]))
+
+
+def main():
+    import platform
+    import os
+    # Enable Layer-backing for MacOs version >= 11
+    # Only needed if using the pyside2 library with version>=5.15.
+    # Layer-backing is always enabled in pyside6.
+    os_name, version, *_ = platform.platform().split('-')
+    # if os_name == 'macOS' and version >= '11':
+    # mac OSX 11.6 appear to be 10.16 when read with python and still required
+    # this environment variable
+    if os_name == 'macOS':
+        os.environ['QT_MAC_WANTS_LAYER'] = '1'
+
+    app = QtWidgets.QApplication(sys.argv)
+    app.setStyleSheet(
+        "QTableView::item:selected {"
+        "  selection-background-color: #93CAFF;}"
+    )
+    soh_messages = {
+        'TEXT': ['this is a text message\nThis is a different line'],
+        '0895': {
+            'SOH': ["\n\n**** STATE OF HEALTH: From:2018-02-07T17:48:24.000000Z  To:2018-02-07T17:48:24.000000Z"   # noqa
+                    "\n\nVCO Correction: 50.0\nTime of exception: 2018:38:17:48:24:0\nMicro sec: 0\nReception Quality: 0\nException Count: 1", # noqa
+                    "\n\n**** STATE OF HEALTH: From:2018-02-07T17:58:35.000000Z  To:2018-02-07T17:58:35.000000Z" # noqa
+                    "\n\nVCO Correction: 55.6396484375\nTime of exception: 2018:38:17:58:35:0\nMicro sec: 0\nReception Quality: 0\nException Count: 1"]}}  # noqa
+
+    processing_logs = [
+        ("info line 1", LogType.INFO),
+        ("warning line 1", LogType.WARNING),
+        ("info line 2", LogType.INFO),
+        ("error line 1", LogType.ERROR)
+    ]
+
+    wnd = SearchMessageDialog(home_path='../../../')
+    wnd.setup_logview('0895', soh_messages, processing_logs)
+    wnd.show()
+    # wnd.show_log_entry_from_data_index(10)
+
+    sys.exit(app.exec_())
+
+
+if __name__ == '__main__':
+    main()
diff --git a/sohstationviewer/view/ui/main_ui.py b/sohstationviewer/view/ui/main_ui.py
index 63756d70d94ee14a020a4beba25829c2eaef6f20..2e1bd1c07da186e8672eeb80efd972af4431623b 100755
--- a/sohstationviewer/view/ui/main_ui.py
+++ b/sohstationviewer/view/ui/main_ui.py
@@ -1,10 +1,11 @@
-# UI and connectSignals for MainWindow
+# UI and connectSignals for main_window
 
 from PySide2 import QtCore, QtGui, QtWidgets
 
 from sohstationviewer.view.calendar.calendar_widget import CalendarWidget
 
 from sohstationviewer.view.plotting.state_of_health_widget import SOHWidget
+
 from sohstationviewer.conf import constants
 
 
@@ -22,7 +23,7 @@ def add_separation_line(layout):
 class UIMainWindow(object):
     def __init__(self):
         """
-        Class that create widgets, menus and connect signals for MainWindow.
+        Class that create widgets, menus and connect signals for main_window.
         """
         super().__init__()
         """
@@ -42,14 +43,15 @@ class UIMainWindow(object):
         self.tracking_info_text_browser = None
         # =================== top row ========================
         """
-        cwd_button: QPushButton - Button that helps browse to the directory for
-            selecting data set
+        curr_dir_button: QPushButton - Button that helps browse to the
+            directory for selecting data set
         """
-        self.cwd_button = None
+        self.curr_dir_button = None
         """
-        cwd_line_edit: QLineEdit - textbox that display the current directory
+        curr_dir_line_edit: QLineEdit - textbox that display the current
+            directory
         """
-        self.cwd_line_edit = None
+        self.curr_dir_line_edit = None
         """
         time_from_date_edit: QDateEdit - to help user select start day to read
             from the data set
@@ -243,14 +245,19 @@ class UIMainWindow(object):
         self.view_plot_type_action = None
         # ========================= Help Menu =============================
         """
-        calendarAction: QAction - Open Calendar Dialog as a helpful tool
+        calendar_action: QAction - Open Calendar Dialog as a helpful tool
         """
-        self.calendarAction = None
+        self.calendar_action = None
         """
         about_action: QAction - Open About Dialog to give information about
             SOHView
         """
         self.about_action = None
+        """
+        doc_action: QAction - Open a display allowing user to browse to the
+            help documents in the folder Documentation/
+        """
+        self.doc_action = None
 
     def setup_ui(self, main_window):
         """
@@ -277,6 +284,7 @@ class UIMainWindow(object):
         main_layout.addWidget(self.tracking_info_text_browser)
         self.create_menu_bar(main_window)
         self.connect_signals(main_window)
+        self.create_shortcuts(main_window)
 
     def set_first_row(self, main_layout):
         """
@@ -290,13 +298,13 @@ class UIMainWindow(object):
         h_layout.setSpacing(8)
         main_layout.addLayout(h_layout)
 
-        self.cwd_button = QtWidgets.QPushButton(
+        self.curr_dir_button = QtWidgets.QPushButton(
             "Main Data Directory", self.central_widget)
-        h_layout.addWidget(self.cwd_button)
+        h_layout.addWidget(self.curr_dir_button)
 
-        self.cwd_line_edit = QtWidgets.QLineEdit(
+        self.curr_dir_line_edit = QtWidgets.QLineEdit(
             self.central_widget)
-        h_layout.addWidget(self.cwd_line_edit, 1)
+        h_layout.addWidget(self.curr_dir_line_edit, 1)
 
         h_layout.addSpacing(40)
 
@@ -388,15 +396,21 @@ class UIMainWindow(object):
 
         search_grid.addWidget(self.replot_button, 1, 3, 1, 1)
 
+        color_tip_fmt = ('Set the background color of the plot '
+                         ' to {0}')
         background_layout = QtWidgets.QHBoxLayout()
         # background_layout.setContentsMargins(0, 0, 0, 0)
         left_layout.addLayout(background_layout)
         background_layout.addWidget(QtWidgets.QLabel('Background:     '))
         self.background_black_radio_button = QtWidgets.QRadioButton(
             'B', self.central_widget)
+        self.background_black_radio_button.setToolTip(
+            color_tip_fmt.format('black'))
         background_layout.addWidget(self.background_black_radio_button)
         self.background_white_radio_button = QtWidgets.QRadioButton(
             'W', self.central_widget)
+        self.background_white_radio_button.setToolTip(
+            color_tip_fmt.format('white'))
         background_layout.addWidget(self.background_white_radio_button)
 
         add_separation_line(left_layout)
@@ -477,16 +491,25 @@ class UIMainWindow(object):
         submit_layout.setSpacing(5)
         left_layout.addLayout(submit_layout)
         self.read_button = QtWidgets.QPushButton('Read', self.central_widget)
+        self.read_button.setToolTip('Read selected files')
         submit_layout.addWidget(self.read_button)
         self.stop_button = QtWidgets.QPushButton('Stop', self.central_widget)
+        self.stop_button.setToolTip('Halt ongoing read')
         submit_layout.addWidget(self.stop_button)
         self.save_plot_button = QtWidgets.QPushButton(
             'Save plot', self.central_widget)
+        self.save_plot_button.setToolTip('Save plots to disk')
         submit_layout.addWidget(self.save_plot_button)
 
         self.info_list_widget = QtWidgets.QListWidget(self.central_widget)
         left_layout.addWidget(self.info_list_widget, 1)
 
+    def create_shortcuts(self, main_window):
+        seq = QtGui.QKeySequence('Ctrl+F')
+        chdir_shortcut = QtWidgets.QShortcut(seq, main_window)
+        chdir_shortcut.activated.connect(
+            main_window.change_current_directory)
+
     def create_menu_bar(self, main_window):
         """
         Setting up menu bar
@@ -505,6 +528,7 @@ class UIMainWindow(object):
         self.create_file_menu(main_window, file_menu)
         self.create_command_menu(main_window, command_menu)
         self.create_option_menu(main_window, option_menu)
+
         self.create_database_menu(main_window, database_menu)
         self.create_help_menu(main_window, help_menu)
 
@@ -603,14 +627,17 @@ class UIMainWindow(object):
         :param main_window: QMainWindow - main GUI for user to interact with
         :param menu: QMenu - Help Menu
         """
-        self.calendarAction = QtWidgets.QAction(
+        self.calendar_action = QtWidgets.QAction(
             'Calendar', main_window)
-        menu.addAction(self.calendarAction)
+        menu.addAction(self.calendar_action)
 
         self.about_action = QtWidgets.QAction(
             'About', main_window)
         menu.addAction(self.about_action)
 
+        self.doc_action = QtWidgets.QAction('Documentation', main_window)
+        menu.addAction(self.doc_action)
+
     def connect_signals(self, main_window):
         """
         Connect widgets what they do
@@ -657,18 +684,21 @@ class UIMainWindow(object):
         # Form
 
         # Help
-        self.calendarAction.triggered.connect(main_window.open_calendar)
+        self.calendar_action.triggered.connect(main_window.open_calendar)
+        self.doc_action.triggered.connect(self.open_help_browser)
 
     def connect_widget_signals(self, main_window):
         main_window.current_directory_changed.connect(
-            self.cwd_line_edit.setText)
+            self.curr_dir_line_edit.setText)
+
         # first Row
         self.time_from_date_edit.setCalendarWidget(CalendarWidget(main_window))
         self.time_from_date_edit.setDate(QtCore.QDate.fromString(
             constants.DEFAULT_START_TIME, QtCore.Qt.ISODate
         ))
 
-        self.cwd_button.clicked.connect(main_window.change_current_directory)
+        self.curr_dir_button.clicked.connect(
+            main_window.change_current_directory)
         self.time_to_date_edit.setCalendarWidget(CalendarWidget(main_window))
         self.time_to_date_edit.setDate(QtCore.QDate.currentDate())
 
@@ -683,3 +713,4 @@ class UIMainWindow(object):
         self.prefer_soh_chan_button.clicked.connect(
             main_window.open_channel_preferences)
         self.read_button.clicked.connect(main_window.read_selected_files)
+        self.stop_button.clicked.connect(main_window.stop_load_data)
diff --git a/sohstationviewer/view/util/color.py b/sohstationviewer/view/util/color.py
index 216dec14c46cf0be5d11c03f02f7d1da092ef8a3..76a29a3442e6bc41132ac6b0e746508f28c40b2f 100644
--- a/sohstationviewer/view/util/color.py
+++ b/sohstationviewer/view/util/color.py
@@ -14,7 +14,7 @@ clr = {"B": "#000000", "C": "#00FFFF", "G": "#00FF00", "M": "#FF00FF",
        "E": "#DFDFDF", "A": "#8F8F8F", "K": "#3F3F3F", "U": "#0070FF",
        "N": "#007F00", "S": "#7F0000", "y": "#7F7F00", "u": "#ADD8E6",
        "s": "#FA8072", "p": "#FFB6C1", "g": "#90EE90", "r": "#EFEFEF",
-       "P": "#AA22FF", "b": "#0000FF"}
+       "P": "#AA22FF", "b": "#0000FF", "o": "#f7e5a8"}
 
 # This is just if the program wants to let the user know what the possibilities
 # are.
@@ -24,7 +24,7 @@ clr_desc = {"B": "black", "C": "cyan", "G": "green", "M": "magenta",
             "N": "dark green", "S": "dark red", "y": "dark yellow",
             "u": "light blue", "s": "salmon", "p": "light pink",
             "g": "light green", "r": "very light gray", "P": "purple",
-            "b": "dark blue"}
+            "b": "dark blue", "o": "light orange"}
 
 
 def set_color_mode(mode):
@@ -41,6 +41,10 @@ def set_color_mode(mode):
         display_color["time_ruler"] = clr["Y"]
         display_color["zoom_marker"] = clr["O"]
         display_color["warning"] = clr["O"]
+        display_color["error"] = clr["R"]
+        display_color["state_of_health"] = clr["u"]
+        display_color["highlight"] = clr['P']
+        display_color["highlight_background"] = clr['u']
     elif mode == "W":
         display_color["background"] = clr["W"]
         display_color["basic"] = clr["B"]
@@ -49,4 +53,8 @@ def set_color_mode(mode):
         display_color["time_ruler"] = clr["U"]
         display_color["zoom_marker"] = clr["O"]
         display_color["warning"] = clr["O"]
+        display_color["error"] = clr["s"]
+        display_color["state_of_health"] = clr["u"]
+        display_color["highlight"] = clr['P']
+        display_color["highlight_background"] = clr['u']
     return display_color
diff --git a/sohstationviewer/view/util/enums.py b/sohstationviewer/view/util/enums.py
new file mode 100644
index 0000000000000000000000000000000000000000..a7749fed9038da3eeef6f157205e059318974c4a
--- /dev/null
+++ b/sohstationviewer/view/util/enums.py
@@ -0,0 +1,7 @@
+import enum
+
+
+class LogType(enum.Enum):
+    INFO = 0
+    WARNING = 1
+    ERROR = 2
diff --git a/sohstationviewer/view/util/functions.py b/sohstationviewer/view/util/functions.py
new file mode 100644
index 0000000000000000000000000000000000000000..c99075ab74fa409b367e4dd71d491f4aad9f4663
--- /dev/null
+++ b/sohstationviewer/view/util/functions.py
@@ -0,0 +1,164 @@
+from pathlib import Path
+from typing import Dict, List, Tuple, Union
+from sohstationviewer.view.util.enums import LogType
+from sohstationviewer.conf import constants as const
+
+
+def is_doc_file(file: Path, include_table_of_contents: bool = False)\
+        -> bool:
+    """
+    Check if file is a document which is an '.help.md' file
+
+    :param file: Absolute path to file
+    :type file: Path
+    :param include_table_of_contents: if Table of Contents file is considered
+        as doc file or not
+    :type include_table_of_contents: bool
+    :return: True if file is a document, False otherwise
+    :rtype: bool
+    """
+    if not file.is_file():
+        return False
+    if not file.name.endswith('.help.md'):
+        return False
+    if not include_table_of_contents and file.name == const.TABLE_CONTENTS:
+        return False
+    return True
+
+
+def create_search_results_file(base_path: Path, search_text: str)\
+        -> Path:
+    """
+    Create 'Search Results.md' file if not exist.
+    Write file content which includes all links to '.help.md' files of which
+        content contains search_text, excluding Table of Contents file.
+    Format of each link is [name](URL)
+
+    :param base_path: directory where document files are located
+    :type base_path: Path
+    :param search_text: text to search in each file
+    :type search_text: str
+    :return: path to search through file
+    :rtype: Path
+    """
+    all_files = [f for f in list(base_path.iterdir()) if is_doc_file(f)]
+
+    search_header = "# Search results\n\n"
+    search_results = ""
+    for file in sorted(all_files):
+        with open(file) as f:
+            content = f.read()
+            if search_text in content:
+                # space in URL must be replace with %20
+                url_name = file.name.replace(" ", "%20")
+                base_file_name = file.stem.split('.help')[0]
+                try:
+                    display_file_name = base_file_name.split(" _ ")[1]
+                except IndexError:
+                    display_file_name = base_file_name
+                search_results += (f"+ [{display_file_name}]"
+                                   f"({url_name})\n\n")
+    if search_results == "":
+        search_notfound = f"Text '{search_text}' not found."
+        search_results = search_header + search_notfound
+    else:
+        search_found = (f"Text '{search_text}' found in the following files:"
+                        f"\n\n---------------------------\n\n")
+        search_results = search_header + search_found + search_results
+
+    search_through_file = Path(base_path).joinpath(const.SEARCH_RESULTS)
+    with open(search_through_file, "w") as f:
+        f.write(search_results)
+    return search_through_file
+
+
+def create_table_of_content_file(base_path: Path) -> None:
+    """
+    Creating Table of Contents which includes all links to '.help.md' files.
+    Format of each link is [name](URL)
+    This function is added to __main__. So run functions.py to create "Table
+        of Contents" file.
+
+    :param base_path: directory where document files are located
+    :type base_path: Path
+    """
+    all_files = [f for f in list(base_path.iterdir())
+                 if is_doc_file(f, include_table_of_contents=True)]
+
+    header = (
+        "# SOH Station Viewer Documentation\n\n"
+        "Welcome to the SOH Station Viewer documentation. Here you will find "
+        "usage guides and other useful information in navigating and using "
+        "this software.\n\n"
+        "On the left-hand side you will find a list of currently available"
+        " help topics.\n\n"
+        "The home button can be used to return to this page at any time.\n\n"
+        "# Table of Contents\n\n")
+    links = ""
+
+    for file in sorted(all_files):
+        # space in URL must be replace with %20
+        url_name = file.name.replace(" ", "%20")
+        base_file_name = file.stem.split('.help')[0]
+        try:
+            display_file_name = base_file_name.split(" _ ")[1]
+        except IndexError:
+            display_file_name = base_file_name
+        links += f"+ [{display_file_name}]({url_name})\n\n"
+
+    contents = header + links
+
+    contents_table_file = Path(base_path).joinpath(const.TABLE_CONTENTS)
+    with open(contents_table_file, "w") as f:
+        f.write(contents)
+        print(f"{contents_table_file.absolute().as_posix()} has been created.")
+
+
+def get_soh_messages_for_view(
+        key: Union[str, Tuple[str, str]],
+        soh_messages: Dict[str, Union[List[str], Dict[str, List[str]]]]) ->\
+        Dict[str, List[str]]:
+    """
+    Convert SOH message of the selected key and TEXT log to dict of list of str
+        to display
+
+    :param key: key of selected data set: station_id
+        or (experiment_number, serial)
+    :type key: str/ (str, str)
+    :param soh_messages: {'TEXT': log_text, key:{chan_id: sos_messages,},}
+    :type soh_messages: Dict
+    :return: {'TEXT'/chan_id: [str,]
+    :rtype: Dict
+    """
+
+    soh_message_view = {}
+
+    if 'TEXT' in soh_messages.keys() and soh_messages['TEXT'] != []:
+        soh_message_view['TEXT'] = []
+        for msg_list in soh_messages['TEXT']:
+            for msg in msg_list.split('\n'):
+                soh_message_view['TEXT'].append(msg)
+
+    for chan_id in soh_messages[key]:
+        soh_message_view[chan_id] = []
+
+        for msg_lines in soh_messages[key][chan_id]:
+            for msg in msg_lines.split('\n'):
+                soh_message_view[chan_id].append(msg)
+    return soh_message_view
+
+
+def log_str(log_info: Tuple[str, LogType]) -> str:
+    """
+    Convert log_info to string that showing log line in saved file
+    :param log_info: tuple include text and type of a log line
+    :type log_info: Tuple[str, LogType]
+    :return: line of log to save in file
+    :rtype: str
+    """
+    log_text, log_type = log_info
+    return f"{log_type.name}: {log_text}"
+
+
+if __name__ == '__main__':
+    create_table_of_content_file(Path('../../../documentation'))
diff --git a/tests/test_controller/test_processing.py b/tests/test_controller/test_processing.py
index dc79d6fa912e7da56e5b43262f621833f9d41676..3ee8345a7119abfb9201ee7b2a4fee883ffe2e96 100644
--- a/tests/test_controller/test_processing.py
+++ b/tests/test_controller/test_processing.py
@@ -3,6 +3,8 @@ from pathlib import Path
 
 from unittest import TestCase
 from unittest.mock import patch
+from contextlib import redirect_stdout
+import io
 
 from sohstationviewer.controller.processing import (
     loadData,
@@ -10,7 +12,7 @@ from sohstationviewer.controller.processing import (
     detectDataType,
     getDataTypeFromFile
 )
-from sohstationviewer.database.extractData import signatureChannels
+from sohstationviewer.database.extract_data import get_signature_channels
 from PySide2 import QtWidgets
 from sohstationviewer.model.mseed.mseed import MSeed
 from sohstationviewer.model.reftek.reftek import RT130
@@ -121,6 +123,30 @@ class TestLoadDataAndReadChannels(TestCase):
         self.assertIsNone(
             loadData(self.mseed_dtype, self.widget_stub, [rt130_dir]))
 
+    def test_load_data_data_traceback_error(self):
+        """
+        Test basic functionality of loadData - when there is an error
+        on loading data, the traceback info will be printed out
+        """
+        f = io.StringIO()
+        with redirect_stdout(f):
+            self.assertIsNone(loadData('RT130', None, [q330_dir]))
+        output = f.getvalue()
+        self.assertIn(
+            f"WARNING: Dir {q330_dir} "
+            f"can't be read due to error: Traceback",
+            output
+        )
+        with redirect_stdout(f):
+            self.assertIsNone(
+                loadData(self.mseed_dtype, None, [rt130_dir]))
+        output = f.getvalue()
+        self.assertIn(
+            f"WARNING: Dir {rt130_dir} "
+            f"can't be read due to error: Traceback",
+            output
+        )
+
     def test_read_channels_mseed_dir(self):
         """
         Test basic functionality of loadData - the given directory contains
@@ -288,7 +314,7 @@ class TestGetDataTypeFromFile(TestCase):
             '92EB/0/000000000_00000000')
         expected_data_type = ('RT130', '_')
         self.assertTupleEqual(
-            getDataTypeFromFile(rt130_file, signatureChannels()),
+            getDataTypeFromFile(rt130_file, get_signature_channels()),
             expected_data_type
         )
 
@@ -299,7 +325,7 @@ class TestGetDataTypeFromFile(TestCase):
         """
         test_file = NamedTemporaryFile()
         self.assertIsNone(
-            getDataTypeFromFile(test_file.name, signatureChannels()))
+            getDataTypeFromFile(test_file.name, get_signature_channels()))
 
     def test_mseed_data(self):
         """
@@ -315,7 +341,7 @@ class TestGetDataTypeFromFile(TestCase):
         centaur_data_type = ('Centaur', 'GEL')
         pegasus_data_type = ('Pegasus', 'VE1')
 
-        sig_chan = signatureChannels()
+        sig_chan = get_signature_channels()
 
         self.assertTupleEqual(getDataTypeFromFile(q330_file, sig_chan),
                               q330_data_type)
@@ -332,6 +358,6 @@ class TestGetDataTypeFromFile(TestCase):
         empty_name_file = ''
         non_existent_file = 'non_existent_dir'
         with self.assertRaises(FileNotFoundError):
-            getDataTypeFromFile(empty_name_file, signatureChannels())
+            getDataTypeFromFile(empty_name_file, get_signature_channels())
         with self.assertRaises(FileNotFoundError):
-            getDataTypeFromFile(non_existent_file, signatureChannels())
+            getDataTypeFromFile(non_existent_file, get_signature_channels())
diff --git a/tests/test_database/test_extract_data.py b/tests/test_database/test_extract_data.py
index 735c0548359f2444cbac63a65d8e8451bafdd2ac..8cd0ab3c039eeaf6453a1743c8c2a433165f24d5 100644
--- a/tests/test_database/test_extract_data.py
+++ b/tests/test_database/test_extract_data.py
@@ -1,19 +1,19 @@
 import unittest
 
-from sohstationviewer.database.extractData import (
-    getChanPlotInfo,
-    getWFPlotInfo,
-    getChanLabel,
-    signatureChannels,
-    getColorDef,
-    getColorRanges,
+from sohstationviewer.database.extract_data import (
+    get_chan_plot_info,
+    get_wf_plot_info,
+    get_chan_label,
+    get_signature_channels,
+    get_color_def,
+    get_color_ranges,
 )
 
 
 class TestExtractData(unittest.TestCase):
     def test_get_chan_plot_info_good_channel_and_data_type(self):
         """
-        Test basic functionality of getChanPlotInfo - channel and data type
+        Test basic functionality of get_chan_plot_info - channel and data type
         combination exists in database table `Channels`
         """
         expected_result = {'channel': 'SOH/Data Def',
@@ -25,13 +25,13 @@ class TestExtractData(unittest.TestCase):
                            'label': 'SOH/Data Def',
                            'fixPoint': 0,
                            'valueColors': '0:W|1:C'}
-        self.assertDictEqual(getChanPlotInfo('SOH/Data Def', 'RT130'),
+        self.assertDictEqual(get_chan_plot_info('SOH/Data Def', 'RT130'),
                              expected_result)
 
     def test_get_chan_plot_info_data_type_is_unknown(self):
         """
-        Test basic functionality of getChanPlotInfo - data type is the string
-        'Unknown'.
+        Test basic functionality of get_chan_plot_info - data type is the
+        string 'Unknown'.
         """
         # Channel does not exist in database
         expected_result = {'channel': 'DEFAULT',
@@ -43,7 +43,7 @@ class TestExtractData(unittest.TestCase):
                            'label': 'DEFAULT-Bad Channel ID',
                            'fixPoint': '0',
                            'valueColors': None}
-        self.assertDictEqual(getChanPlotInfo('Bad Channel ID', 'Unknown'),
+        self.assertDictEqual(get_chan_plot_info('Bad Channel ID', 'Unknown'),
                              expected_result)
 
         # Channel exist in database
@@ -55,13 +55,15 @@ class TestExtractData(unittest.TestCase):
                            'convertFactor': 1,
                            'label': 'LCE-PhaseError',
                            'fixPoint': 0,
-                           'valueColors': 'L:W'}
-        self.assertDictEqual(getChanPlotInfo('LCE', 'Unknown'),
+                           'valueColors': 'L:W|D:Y'}
+        self.assertDictEqual(get_chan_plot_info('LCE', 'Unknown'),
+                             expected_result)
+        self.assertDictEqual(get_chan_plot_info('LCE', 'Unknown'),
                              expected_result)
 
     def test_get_chan_plot_info_bad_channel_or_data_type(self):
         """
-        Test basic functionality of getChanPlotInfo - channel and data type
+        Test basic functionality of get_chan_plot_info - channel and data type
         combination does not exist in database table Channels and data type is
         not the string 'Unknown'.
         """
@@ -79,74 +81,76 @@ class TestExtractData(unittest.TestCase):
         # Data type has None value. None value comes from
         # controller.processing.detectDataType.
         expected_result['label'] = 'DEFAULT-SOH/Data Def'
-        self.assertDictEqual(getChanPlotInfo('SOH/Data Def', None),
+        self.assertDictEqual(get_chan_plot_info('SOH/Data Def', None),
                              expected_result)
 
         # Channel and data type are empty strings
         expected_result['label'] = 'DEFAULT-'
-        self.assertDictEqual(getChanPlotInfo('', ''), expected_result)
+        self.assertDictEqual(get_chan_plot_info('', ''), expected_result)
 
         # Channel exists in database but data type does not
         expected_result['label'] = 'DEFAULT-SOH/Data Def'
-        self.assertDictEqual(getChanPlotInfo('SOH/Data Def', 'Bad Data Type'),
-                             expected_result)
+        self.assertDictEqual(
+            get_chan_plot_info('SOH/Data Def', 'Bad Data Type'),
+            expected_result
+        )
 
         # Data type exists in database but channel does not
         expected_result['label'] = 'DEFAULT-Bad Channel ID'
-        self.assertDictEqual(getChanPlotInfo('Bad Channel ID', 'RT130'),
+        self.assertDictEqual(get_chan_plot_info('Bad Channel ID', 'RT130'),
                              expected_result)
 
         # Both channel and data type exists in database but not their
         # combination
         expected_result['label'] = 'DEFAULT-SOH/Data Def'
-        self.assertDictEqual(getChanPlotInfo('SOH/Data Def', 'Q330'),
+        self.assertDictEqual(get_chan_plot_info('SOH/Data Def', 'Q330'),
                              expected_result)
 
     def test_get_wf_plot_info(self):
         """
-        Test basic functionality of getWFPlotInfo - ensures returned dictionary
-        contains all the needed key. Bad channel IDs cases are handled in tests
-        for getChanLabel.
+        Test basic functionality of get_wf_plot_info - ensures returned
+        dictionary contains all the needed key. Bad channel IDs cases are
+        handled in tests for get_chan_label.
         """
-        result = getWFPlotInfo('CH1')
+        result = get_wf_plot_info('CH1')
         expected_keys = ('param', 'plotType', 'valueColors', 'height',
                          'label', 'unit', 'channel')
         self.assertTupleEqual(tuple(result.keys()), expected_keys)
 
     def test_get_chan_label_good_channel_id(self):
         """
-        Test basic functionality of getChanLabel - channel ID ends in one
+        Test basic functionality of get_chan_label - channel ID ends in one
         of the keys in conf.dbSettings.dbConf['seisLabel'] or starts with 'DS'
         """
         # Channel ID does not start with 'DS'
-        self.assertEqual(getChanLabel('CH1'), 'CH1-NS')
-        self.assertEqual(getChanLabel('CH2'), 'CH2-EW')
+        self.assertEqual(get_chan_label('CH1'), 'CH1-NS')
+        self.assertEqual(get_chan_label('CH2'), 'CH2-EW')
 
         # Channel ID starts with 'DS'
-        self.assertEqual(getChanLabel('DS-TEST-CHANNEL'), 'DS-TEST-CHANNEL')
+        self.assertEqual(get_chan_label('DS-TEST-CHANNEL'), 'DS-TEST-CHANNEL')
 
     def test_get_chan_label_bad_channel_id(self):
         """
-        Test basic functionality of getChanLabel - channel ID does not end in
+        Test basic functionality of get_chan_label - channel ID does not end in
         one of the keys in conf.dbSettings.dbConf['seisLabel'] or is the empty
         string.
         """
-        self.assertRaises(KeyError, getChanLabel, 'CHG')
-        self.assertRaises(IndexError, getChanLabel, '')
+        self.assertRaises(KeyError, get_chan_label, 'CHG')
+        self.assertRaises(IndexError, get_chan_label, '')
 
-    def test_signature_channels(self):
-        """Test basic functionality of signatureChannels"""
-        self.assertIsInstance(signatureChannels(), dict)
+    def test_get_signature_channels(self):
+        """Test basic functionality of get_signature_channels"""
+        self.assertIsInstance(get_signature_channels(), dict)
 
     def test_get_color_def(self):
-        """Test basic functionality of getColorDef"""
-        colors = getColorDef()
+        """Test basic functionality of get_color_def"""
+        colors = get_color_def()
         expected_colors = ['K', 'U', 'C', 'G', 'Y', 'R', 'M', 'E']
         self.assertListEqual(colors, expected_colors)
 
     def test_get_color_ranges(self):
-        """Test basic functionality of getColorDef"""
-        names, all_counts, all_display_strings = getColorRanges()
+        """Test basic functionality of get_color_ranges"""
+        names, all_counts, all_display_strings = get_color_ranges()
         num_color_def = 7
 
         expected_names = ['antarctica', 'low', 'med', 'high']
diff --git a/tests/test_model/test_handling_data.py b/tests/test_model/test_handling_data.py
deleted file mode 100644
index 935a25af25c5c71bda10b292b039b356718d04be..0000000000000000000000000000000000000000
--- a/tests/test_model/test_handling_data.py
+++ /dev/null
@@ -1,289 +0,0 @@
-from pathlib import Path
-from tempfile import TemporaryDirectory
-
-from unittest import TestCase
-from unittest.mock import patch
-
-import numpy as np
-
-from sohstationviewer.conf import constants as const
-from sohstationviewer.model.handling_data import (
-    downsample,
-    trim_downsample_WFChan,
-    trim_waveform_data,
-    downsample_waveform_data
-)
-
-ORIGINAL_CHAN_SIZE_LIMIT = const.CHAN_SIZE_LIMIT
-ORIGINAL_RECAL_SIZE_LIMIT = const.RECAL_SIZE_LIMIT
-
-
-class TestTrimWfData(TestCase):
-    def setUp(self) -> None:
-        self.channel_data = {}
-        self.traces_info = []
-        self.channel_data['tracesInfo'] = self.traces_info
-
-        for i in range(100):
-            trace_size = 100
-            start_time = i * trace_size
-            trace = {}
-            trace['startTmEpoch'] = start_time
-            trace['endTmEpoch'] = start_time + trace_size - 1
-            self.traces_info.append(trace)
-        self.start_time = 2500
-        self.end_time = 7500
-
-    def test_data_is_trimmed_neither_start_nor_end_time_is_trace_start_or_end_time(self):  # noqa: E501
-        self.start_time = 2444
-        self.end_time = 7444
-        trimmed_traces_list = trim_waveform_data(
-            self.channel_data, self.start_time, self.end_time
-        )
-
-        self.assertTrue(
-            trimmed_traces_list[0]['startTmEpoch'] <= self.start_time)
-        self.assertTrue(
-            trimmed_traces_list[0]['endTmEpoch'] > self.start_time
-        )
-        trimmed_traces_list.pop(0)
-        trimmed_traces_list.pop()
-        is_left_trimmed = all(trace['startTmEpoch'] > self.start_time
-                              for trace in trimmed_traces_list)
-        is_right_trimmed = all(trace['endTmEpoch'] <= self.end_time
-                               for trace in trimmed_traces_list)
-        self.assertTrue(is_left_trimmed and is_right_trimmed)
-
-    def test_data_out_of_range(self):
-        with self.subTest('test_start_time_later_than_data_end_time'):
-            self.start_time = 12500
-            self.end_time = 17500
-            self.assertFalse(
-                trim_downsample_WFChan(self.channel_data, self.start_time,
-                                       self.end_time, True)
-            )
-        with self.subTest('test_end_time_earlier_than_data_start_time'):
-            self.start_time = -7500
-            self.end_time = -2500
-            self.assertFalse(
-                trim_downsample_WFChan(self.channel_data, self.start_time,
-                                       self.end_time, True)
-            )
-
-    def test_no_data(self):
-        self.channel_data['tracesInfo'] = []
-        with self.assertRaises(IndexError):
-            trim_waveform_data(
-                self.channel_data, self.start_time, self.end_time
-            )
-
-    def test_end_time_earlier_than_start_time(self):
-        self.start_time, self.end_time = self.end_time, self.start_time
-        trimmed_traces_list = trim_waveform_data(
-            self.channel_data, self.start_time, self.end_time
-        )
-        self.assertListEqual(trimmed_traces_list, [])
-
-    def test_data_does_not_need_to_be_trimmed(self):
-        with self.subTest('test_start_time_earlier_than_trace_earliest_time'):
-            self.start_time = -2500
-            self.end_time = 7500
-            trimmed_traces_list = trim_waveform_data(
-                self.channel_data, self.start_time, self.end_time
-            )
-            self.assertEqual(len(trimmed_traces_list), 76)
-        with self.subTest('test_end_time_later_than_trace_latest_time'):
-            self.start_time = 2500
-            self.end_time = 12500
-            trimmed_traces_list = trim_waveform_data(
-                self.channel_data, self.start_time, self.end_time
-            )
-            self.assertEqual(len(trimmed_traces_list), 75)
-        with self.subTest('test_data_contained_in_time_range'):
-            self.start_time = self.traces_info[0]['startTmEpoch']
-            self.end_time = self.traces_info[-1]['endTmEpoch']
-            trimmed_traces_list = trim_waveform_data(
-                self.channel_data, self.start_time, self.end_time
-            )
-            self.assertEqual(len(trimmed_traces_list), len(self.traces_info))
-
-
-class TestDownsampleWaveformData(TestCase):
-    def no_file_memmap(self, file_path: Path, **kwargs):
-        # Data will look the same as times. This has two benefits:
-        # - It is a lot easier to inspect what data remains after trimming
-        # and downsampling, seeing as the remaining data would be the same
-        # as the remaining times.
-        # - It is a lot easier to reproducibly create a test data set.
-        array_size = 100
-        file_idx = int(file_path.name.split('-')[-1])
-        start = file_idx * array_size
-        end = start + array_size
-        return np.arange(start, end)
-
-    def setUp(self) -> None:
-        memmap_patcher = patch.object(np, 'memmap',
-                                      side_effect=self.no_file_memmap)
-        self.addCleanup(memmap_patcher.stop)
-        memmap_patcher.start()
-
-        self.channel_data = {}
-        self.traces_info = []
-        self.channel_data['tracesInfo'] = self.traces_info
-        self.data_folder = TemporaryDirectory()
-        for i in range(100):
-            trace_size = 100
-            start_time = i * trace_size
-            trace = {}
-            trace['startTmEpoch'] = start_time
-            trace['endTmEpoch'] = start_time + trace_size - 1
-            trace['size'] = trace_size
-
-            times_file_name = Path(self.data_folder.name) / f'times-{i}'
-            trace['times_f'] = times_file_name
-
-            data_file_name = Path(self.data_folder.name) / f'data-{i}'
-            trace['data_f'] = data_file_name
-
-            self.traces_info.append(trace)
-        self.start_time = 2550
-        self.end_time = 7550
-        self.trimmed_traces_list = trim_waveform_data(
-            self.channel_data, self.start_time, self.end_time
-        )
-
-    @patch('sohstationviewer.model.handling_data.downsample', wraps=downsample)
-    def test_data_is_downsampled(self, mock_downsample):
-        const.CHAN_SIZE_LIMIT = 1000
-        downsample_waveform_data(self.trimmed_traces_list,
-                                 self.start_time, self.end_time)
-        self.assertTrue(mock_downsample.called)
-        const.CHAN_SIZE_LIMIT = ORIGINAL_CHAN_SIZE_LIMIT
-
-    def test_all_traces_handled(self):
-        downsampled_times, downsampled_data = downsample_waveform_data(
-            self.trimmed_traces_list,
-            self.start_time, self.end_time
-        )
-        self.assertEqual(len(downsampled_times), 51)
-        self.assertEqual(len(downsampled_data), 51)
-
-    def test_downsampling_not_needed(self):
-        downsampled_times, downsampled_data = downsample_waveform_data(
-            self.trimmed_traces_list,
-            self.start_time, self.end_time
-        )
-        with self.subTest('test_data_points_outside_time_range_removed'):
-            self.assertEqual(downsampled_times.pop(0).size, 50)
-            self.assertEqual(downsampled_times.pop(-1).size, 51)
-            self.assertEqual(downsampled_data.pop(0).size, 50)
-            self.assertEqual(downsampled_data.pop(-1).size, 51)
-
-        with self.subTest('test_intermediate_data_points_not_removed'):
-            self.assertTrue(
-                all(times.size == 100 for times in downsampled_times)
-            )
-            self.assertTrue(
-                all(data.size == 100 for data in downsampled_times)
-            )
-
-    def test_trace_list_empty(self):
-        self.trimmed_traces_list = []
-        downsampled_times, downsampled_data = downsample_waveform_data(
-            self.trimmed_traces_list,
-            self.start_time, self.end_time
-        )
-        self.assertListEqual(downsampled_times, [])
-        self.assertListEqual(downsampled_data, [])
-
-    def test_end_time_earlier_than_start_time(self):
-        self.start_time, self.end_time = self.end_time, self.start_time
-        downsampled_times, downsampled_data = downsample_waveform_data(
-            self.trimmed_traces_list,
-            self.start_time, self.end_time
-        )
-        self.assertTrue(all(times.size == 0 for times in downsampled_times))
-        self.assertTrue(all(data.size == 0 for data in downsampled_data))
-
-
-class TestTrimDownsampleWfChan(TestCase):
-    def no_file_memmap(self, file_path: Path, **kwargs):
-        # Data will look the same as times. This has two benefits:
-        # - It is a lot easier to inspect what data remains after trimming
-        # and downsampling, seeing as the remaining data would be the same
-        # as the remaining times.
-        # - It is a lot easier to reproducibly create a test data set.
-        array_size = 100
-        file_idx = int(file_path.name.split('-')[-1])
-        start = file_idx * array_size
-        end = start + array_size
-        return np.arange(start, end)
-
-    def setUp(self) -> None:
-        memmap_patcher = patch.object(np, 'memmap',
-                                      side_effect=self.no_file_memmap)
-        self.addCleanup(memmap_patcher.stop)
-        memmap_patcher.start()
-
-        self.channel_data = {}
-        self.traces_info = []
-        self.channel_data['tracesInfo'] = self.traces_info
-        self.data_folder = TemporaryDirectory()
-        for i in range(100):
-            trace_size = 100
-            start_time = i * trace_size
-            trace = {}
-            trace['startTmEpoch'] = start_time
-            trace['endTmEpoch'] = start_time + trace_size - 1
-            trace['size'] = trace_size
-
-            times_file_name = Path(self.data_folder.name) / f'times-{i}'
-            trace['times_f'] = times_file_name
-
-            data_file_name = Path(self.data_folder.name) / f'data-{i}'
-            trace['data_f'] = data_file_name
-
-            self.traces_info.append(trace)
-        self.start_time = 2500
-        self.end_time = 7500
-
-    def test_result_is_stored(self):
-        trim_downsample_WFChan(self.channel_data, self.start_time,
-                               self.end_time, True)
-        self.assertTrue('times' in self.channel_data)
-        self.assertGreater(len(self.channel_data['times']), 0)
-        self.assertTrue('data' in self.channel_data)
-        self.assertGreater(len(self.channel_data['data']), 0)
-
-    def test_data_small_enough_after_first_trim_flag_is_set(self):
-        trim_downsample_WFChan(self.channel_data, self.start_time,
-                               self.end_time, True)
-        self.assertTrue('fulldata' in self.channel_data)
-
-    def test_no_additional_work_if_data_small_enough_after_first_trim(self):
-        trim_downsample_WFChan(self.channel_data, self.start_time,
-                               self.end_time, True)
-        current_times = self.channel_data['times']
-        current_data = self.channel_data['data']
-        trim_downsample_WFChan(self.channel_data, self.start_time,
-                               self.end_time, True)
-        self.assertIs(current_times, self.channel_data['times'])
-        self.assertIs(current_data, self.channel_data['data'])
-
-    def test_data_too_large_after_trimming(self):
-        const.RECAL_SIZE_LIMIT = 1
-        trim_downsample_WFChan(self.channel_data, self.start_time,
-                               self.end_time, False)
-        self.assertTrue('times' not in self.channel_data)
-        self.assertTrue('data' not in self.channel_data)
-        const.RECAL_SIZE_LIMIT = ORIGINAL_RECAL_SIZE_LIMIT
-
-    @patch('sohstationviewer.model.handling_data.trim_waveform_data',
-           wraps=trim_waveform_data)
-    @patch('sohstationviewer.model.handling_data.downsample_waveform_data',
-           wraps=downsample_waveform_data)
-    def test_data_trim_and_downsampled(self, mock_downsample, mock_trim):
-        trim_downsample_WFChan(self.channel_data, self.start_time,
-                               self.end_time, False)
-        self.assertTrue(mock_trim.called)
-        self.assertTrue(mock_downsample.called)
diff --git a/tests/test_model/test_handling_data_trim_downsample.py b/tests/test_model/test_handling_data_trim_downsample.py
new file mode 100644
index 0000000000000000000000000000000000000000..244588c8f501e1003b14111b9a0760f927239b48
--- /dev/null
+++ b/tests/test_model/test_handling_data_trim_downsample.py
@@ -0,0 +1,599 @@
+from pathlib import Path
+from tempfile import TemporaryDirectory
+
+from unittest import TestCase
+from unittest.mock import patch
+
+from obspy.core import UTCDateTime
+import numpy as np
+
+from sohstationviewer.conf import constants as const
+from sohstationviewer.model.handling_data import (
+    downsample,
+    chunk_minmax,
+    trim_downsample_SOHChan,
+    trim_downsample_WFChan,
+    trim_waveform_data,
+    downsample_waveform_data
+)
+
+ORIGINAL_CHAN_SIZE_LIMIT = const.CHAN_SIZE_LIMIT
+ORIGINAL_RECAL_SIZE_LIMIT = const.RECAL_SIZE_LIMIT
+ZERO_EPOCH_TIME = UTCDateTime(1970, 1, 1, 0, 0, 0).timestamp
+
+
+class TestTrimWfData(TestCase):
+    def setUp(self) -> None:
+        self.channel_data = {}
+        self.traces_info = []
+        self.channel_data['tracesInfo'] = self.traces_info
+
+        for i in range(100):
+            trace_size = 100
+            start_time = i * trace_size
+            trace = {}
+            trace['startTmEpoch'] = start_time
+            trace['endTmEpoch'] = start_time + trace_size - 1
+            self.traces_info.append(trace)
+        self.start_time = 2500
+        self.end_time = 7500
+
+    def test_data_is_trimmed_neither_start_nor_end_time_is_trace_start_or_end_time(self):  # noqa: E501
+        self.start_time = 2444
+        self.end_time = 7444
+        trimmed_traces_list = trim_waveform_data(
+            self.channel_data, self.start_time, self.end_time
+        )
+
+        self.assertTrue(
+            trimmed_traces_list[0]['startTmEpoch'] <= self.start_time)
+        self.assertTrue(
+            trimmed_traces_list[0]['endTmEpoch'] > self.start_time
+        )
+        trimmed_traces_list.pop(0)
+        trimmed_traces_list.pop()
+        is_left_trimmed = all(trace['startTmEpoch'] > self.start_time
+                              for trace in trimmed_traces_list)
+        is_right_trimmed = all(trace['endTmEpoch'] <= self.end_time
+                               for trace in trimmed_traces_list)
+        self.assertTrue(is_left_trimmed and is_right_trimmed)
+
+    def test_data_out_of_range(self):
+        with self.subTest('test_start_time_later_than_data_end_time'):
+            self.start_time = 12500
+            self.end_time = 17500
+            self.assertFalse(
+                trim_downsample_WFChan(self.channel_data, self.start_time,
+                                       self.end_time, True)
+            )
+        with self.subTest('test_end_time_earlier_than_data_start_time'):
+            self.start_time = -7500
+            self.end_time = -2500
+            self.assertFalse(
+                trim_downsample_WFChan(self.channel_data, self.start_time,
+                                       self.end_time, True)
+            )
+
+    def test_no_data(self):
+        self.channel_data['tracesInfo'] = []
+        with self.assertRaises(IndexError):
+            trim_waveform_data(
+                self.channel_data, self.start_time, self.end_time
+            )
+
+    def test_end_time_earlier_than_start_time(self):
+        self.start_time, self.end_time = self.end_time, self.start_time
+        trimmed_traces_list = trim_waveform_data(
+            self.channel_data, self.start_time, self.end_time
+        )
+        self.assertListEqual(trimmed_traces_list, [])
+
+    def test_data_does_not_need_to_be_trimmed(self):
+        with self.subTest('test_start_time_earlier_than_trace_earliest_time'):
+            self.start_time = -2500
+            self.end_time = 7500
+            trimmed_traces_list = trim_waveform_data(
+                self.channel_data, self.start_time, self.end_time
+            )
+            self.assertEqual(len(trimmed_traces_list), 76)
+        with self.subTest('test_end_time_later_than_trace_latest_time'):
+            self.start_time = 2500
+            self.end_time = 12500
+            trimmed_traces_list = trim_waveform_data(
+                self.channel_data, self.start_time, self.end_time
+            )
+            self.assertEqual(len(trimmed_traces_list), 75)
+        with self.subTest('test_data_contained_in_time_range'):
+            self.start_time = self.traces_info[0]['startTmEpoch']
+            self.end_time = self.traces_info[-1]['endTmEpoch']
+            trimmed_traces_list = trim_waveform_data(
+                self.channel_data, self.start_time, self.end_time
+            )
+            self.assertEqual(len(trimmed_traces_list), len(self.traces_info))
+
+
+class TestDownsampleWaveformData(TestCase):
+    def no_file_memmap(self, file_path: Path, **kwargs):
+        # Data will look the same as times. This has two benefits:
+        # - It is a lot easier to inspect what data remains after trimming
+        # and downsampling, seeing as the remaining data would be the same
+        # as the remaining times.
+        # - It is a lot easier to reproducibly create a test data set.
+        array_size = 100
+        file_idx = int(file_path.name.split('-')[-1])
+        start = file_idx * array_size
+        end = start + array_size
+        return np.arange(start, end)
+
+    def setUp(self) -> None:
+        memmap_patcher = patch.object(np, 'memmap',
+                                      side_effect=self.no_file_memmap)
+        self.addCleanup(memmap_patcher.stop)
+        memmap_patcher.start()
+
+        self.channel_data = {}
+        self.traces_info = []
+        self.channel_data['tracesInfo'] = self.traces_info
+        self.data_folder = TemporaryDirectory()
+        for i in range(100):
+            trace_size = 100
+            start_time = i * trace_size
+            trace = {}
+            trace['startTmEpoch'] = start_time
+            trace['endTmEpoch'] = start_time + trace_size - 1
+            trace['size'] = trace_size
+
+            times_file_name = Path(self.data_folder.name) / f'times-{i}'
+            trace['times_f'] = times_file_name
+
+            data_file_name = Path(self.data_folder.name) / f'data-{i}'
+            trace['data_f'] = data_file_name
+
+            self.traces_info.append(trace)
+        self.start_time = 2550
+        self.end_time = 7550
+        self.trimmed_traces_list = trim_waveform_data(
+            self.channel_data, self.start_time, self.end_time
+        )
+
+    @patch('sohstationviewer.model.handling_data.downsample', wraps=downsample)
+    def test_data_is_downsampled(self, mock_downsample):
+        const.CHAN_SIZE_LIMIT = 1000
+        downsample_waveform_data(self.trimmed_traces_list,
+                                 self.start_time, self.end_time)
+        self.assertTrue(mock_downsample.called)
+        const.CHAN_SIZE_LIMIT = ORIGINAL_CHAN_SIZE_LIMIT
+
+    def test_all_traces_handled(self):
+        downsampled_times, downsampled_data = downsample_waveform_data(
+            self.trimmed_traces_list,
+            self.start_time, self.end_time
+        )
+        self.assertEqual(len(downsampled_times), 51)
+        self.assertEqual(len(downsampled_data), 51)
+
+    def test_downsampling_not_needed(self):
+        downsampled_times, downsampled_data = downsample_waveform_data(
+            self.trimmed_traces_list,
+            self.start_time, self.end_time
+        )
+        with self.subTest('test_data_points_outside_time_range_removed'):
+            self.assertEqual(downsampled_times.pop(0).size, 50)
+            self.assertEqual(downsampled_times.pop(-1).size, 51)
+            self.assertEqual(downsampled_data.pop(0).size, 50)
+            self.assertEqual(downsampled_data.pop(-1).size, 51)
+
+        with self.subTest('test_intermediate_data_points_not_removed'):
+            self.assertTrue(
+                all(times.size == 100 for times in downsampled_times)
+            )
+            self.assertTrue(
+                all(data.size == 100 for data in downsampled_times)
+            )
+
+    def test_trace_list_empty(self):
+        self.trimmed_traces_list = []
+        downsampled_times, downsampled_data = downsample_waveform_data(
+            self.trimmed_traces_list,
+            self.start_time, self.end_time
+        )
+        self.assertListEqual(downsampled_times, [])
+        self.assertListEqual(downsampled_data, [])
+
+    def test_end_time_earlier_than_start_time(self):
+        self.start_time, self.end_time = self.end_time, self.start_time
+        downsampled_times, downsampled_data = downsample_waveform_data(
+            self.trimmed_traces_list,
+            self.start_time, self.end_time
+        )
+        self.assertTrue(all(times.size == 0 for times in downsampled_times))
+        self.assertTrue(all(data.size == 0 for data in downsampled_data))
+
+    @patch('sohstationviewer.model.handling_data.downsample')
+    def test_arguments_sent_to_downsample(self, mock_downsample):
+        mock_downsample.return_value = (1, 2, 3)
+        const.CHAN_SIZE_LIMIT = 1000
+        downsample_waveform_data(self.trimmed_traces_list,
+                                 self.start_time, self.end_time)
+
+        positional_args, named_args = mock_downsample.call_args
+        self.assertEqual(len(positional_args), 2)
+
+
+class TestDownsample(TestCase):
+    def setUp(self) -> None:
+        patcher = patch('sohstationviewer.model.handling_data.chunk_minmax')
+        self.addCleanup(patcher.stop)
+        self.mock_chunk_minmax = patcher.start()
+        self.times = np.arange(1000)
+        self.data = np.arange(1000)
+        self.log_idx = np.arange(1000)
+
+    def test_first_downsample_step_remove_enough_points(self):
+        req_points = 999
+        downsample(self.times, self.data, rq_points=req_points)
+        self.assertFalse(self.mock_chunk_minmax.called)
+
+    def test_first_downsample_step_remove_enough_points_with_logidx(self):
+        req_points = 999
+        downsample(self.times, self.data, self.log_idx, rq_points=req_points)
+        self.assertFalse(self.mock_chunk_minmax.called)
+
+    def test_second_downsample_step_required(self):
+        req_points = 1
+        downsample(self.times, self.data, rq_points=req_points)
+        self.assertTrue(self.mock_chunk_minmax.called)
+        times, data, _, rq_points = self.mock_chunk_minmax.call_args[0]
+        self.assertIsNot(times, self.times)
+        self.assertIsNot(data, self.data)
+        self.assertEqual(rq_points, req_points)
+
+    def test_second_downsample_step_required_with_logidx(self):
+        req_points = 1
+        downsample(self.times, self.data, self.log_idx, rq_points=req_points)
+        self.assertTrue(self.mock_chunk_minmax.called)
+        times, data, log_idx, rq_points = self.mock_chunk_minmax.call_args[0]
+        self.assertIsNot(times, self.times)
+        self.assertIsNot(data, self.data)
+        self.assertIsNot(log_idx, self.log_idx)
+        self.assertEqual(rq_points, req_points)
+
+    def test_requested_points_greater_than_data_size(self):
+        req_points = 10000
+        times, data, _ = downsample(
+            self.times, self.data, rq_points=req_points)
+        self.assertFalse(self.mock_chunk_minmax.called)
+        # Check that we did not do any processing on the times and data arrays.
+        # This ensures that we don't do two unneeded copy operations.
+        self.assertIs(times, self.times)
+        self.assertIs(data, self.data)
+
+    def test_requested_points_greater_than_data_size_with_logidx(self):
+        req_points = 10000
+        times, data, log_idx = downsample(
+            self.times, self.data, self.log_idx, rq_points=req_points)
+        self.assertFalse(self.mock_chunk_minmax.called)
+        # Check that we did not do any processing on the times and data arrays.
+        # This ensures that we don't do two unneeded copy operations.
+        self.assertIs(times, self.times)
+        self.assertIs(data, self.data)
+        self.assertIs(log_idx, self.log_idx)
+
+    def test_requested_points_is_zero(self):
+        req_points = 0
+        downsample(self.times, self.data, rq_points=req_points)
+        self.assertTrue(self.mock_chunk_minmax.called)
+        times, data, _, rq_points = self.mock_chunk_minmax.call_args[0]
+        self.assertIsNot(times, self.times)
+        self.assertIsNot(data, self.data)
+        self.assertEqual(rq_points, req_points)
+
+    def test_requested_points_is_zero_with_logidx(self):
+        req_points = 0
+        downsample(self.times, self.data, self.log_idx, rq_points=req_points)
+        self.assertTrue(self.mock_chunk_minmax.called)
+        times, data, log_idx, rq_points = self.mock_chunk_minmax.call_args[0]
+        self.assertIsNot(times, self.times)
+        self.assertIsNot(data, self.data)
+        self.assertIsNot(log_idx, self.log_idx)
+        self.assertEqual(rq_points, req_points)
+
+    def test_empty_times_and_data(self):
+        req_points = 1000
+        self.times = np.empty((0, 0))
+        self.data = np.empty((0, 0))
+        times, data, _ = downsample(
+            self.times, self.data, rq_points=req_points)
+        self.assertFalse(self.mock_chunk_minmax.called)
+        # Check that we did not do any processing on the times and data arrays.
+        # This ensures that we don't do two unneeded copy operations.
+        self.assertIs(times, self.times)
+        self.assertIs(data, self.data)
+
+    def test_empty_times_and_data_with_logidx(self):
+        req_points = 1000
+        self.times = np.empty((0, 0))
+        self.data = np.empty((0, 0))
+        self.log_idx = np.empty((0, 0))
+        times, data, log_idx = downsample(
+            self.times, self.data, self.log_idx, rq_points=req_points)
+        self.assertFalse(self.mock_chunk_minmax.called)
+        # Check that we did not do any processing on the times and data arrays.
+        # This ensures that we don't do two unneeded copy operations.
+        self.assertIs(times, self.times)
+        self.assertIs(data, self.data)
+        self.assertIs(log_idx, self.log_idx)
+
+
+class TestChunkMinmax(TestCase):
+    def setUp(self):
+        self.times = np.arange(1000)
+        self.data = np.arange(1000)
+        self.log_idx = np.arange(1000)
+
+    def test_data_size_is_multiple_of_requested_points(self):
+        req_points = 100
+        times, data, log_idx = chunk_minmax(
+            self.times, self.data, self.log_idx, req_points)
+        self.assertEqual(times.size, req_points)
+        self.assertEqual(data.size, req_points)
+        self.assertEqual(log_idx.size, req_points)
+
+    @patch('sohstationviewer.model.handling_data.downsample', wraps=downsample)
+    def test_data_size_is_not_multiple_of_requested_points(
+            self, mock_downsample):
+        req_points = 102
+        chunk_minmax(self.times, self.data, self.log_idx, req_points)
+        self.assertTrue(mock_downsample.called)
+
+    def test_requested_points_too_small(self):
+        small_req_points_list = [0, 1]
+        for req_points in small_req_points_list:
+            with self.subTest(f'test_requested_points_is_{req_points}'):
+                times, data, log_idx = chunk_minmax(
+                    self.times, self.data, self.log_idx, rq_points=req_points)
+                self.assertEqual(times.size, 0)
+                self.assertEqual(data.size, 0)
+                self.assertEqual(data.size, 0)
+
+
+class TestTrimDownsampleSohChan(TestCase):
+    @staticmethod
+    def downsample(times, data, log_indexes=None, rq_points=0):
+        return times, data, log_indexes
+
+    def setUp(self) -> None:
+        self.channel_info = {}
+        self.org_trace = {
+            'times': np.arange(1000),
+            'data': np.arange(1000)
+        }
+        self.channel_info['orgTrace'] = self.org_trace
+        self.start_time = 250
+        self.end_time = 750
+        self.first_time = False
+
+        patcher = patch('sohstationviewer.model.handling_data.downsample')
+        self.addCleanup(patcher.stop)
+        self.mock_downsample = patcher.start()
+        self.mock_downsample.side_effect = self.downsample
+
+    def num_points_outside_time_range(self, start_time, end_time):
+        return len([data_point
+                    for data_point in self.org_trace['times']
+                    if not start_time <= data_point <= end_time])
+
+    def test_start_time_later_than_times_data(self):
+        self.start_time = 250
+        self.end_time = 1250
+        trim_downsample_SOHChan(self.channel_info, self.start_time,
+                                self.end_time, self.first_time)
+        self.assertGreaterEqual(self.channel_info['times'].min(),
+                                self.start_time)
+        self.assertEqual(
+            self.org_trace['times'].size - self.channel_info['times'].size,
+            self.num_points_outside_time_range(self.start_time, self.end_time)
+        )
+
+    def test_end_time_earlier_than_times_data(self):
+        self.start_time = -250
+        self.end_time = 750
+        trim_downsample_SOHChan(self.channel_info, self.start_time,
+                                self.end_time, self.first_time)
+        self.assertLessEqual(self.channel_info['times'].max(),
+                             self.end_time)
+        self.assertEqual(
+            self.org_trace['times'].size - self.channel_info['times'].size,
+            self.num_points_outside_time_range(self.start_time, self.end_time)
+        )
+
+    def test_start_time_earlier_than_times_data(self):
+        self.start_time = -250
+        self.end_time = 750
+        trim_downsample_SOHChan(self.channel_info, self.start_time,
+                                self.end_time, self.first_time)
+        self.assertEqual(self.channel_info['times'].min(),
+                         self.channel_info['orgTrace']['times'].min())
+        self.assertEqual(
+            self.org_trace['times'].size - self.channel_info['times'].size,
+            self.num_points_outside_time_range(self.start_time, self.end_time)
+        )
+
+    def test_end_time_later_than_times_data(self):
+        self.start_time = 250
+        self.end_time = 1250
+        trim_downsample_SOHChan(self.channel_info, self.start_time,
+                                self.end_time, self.first_time)
+        self.assertEqual(self.channel_info['times'].max(),
+                         self.channel_info['orgTrace']['times'].max())
+        self.assertEqual(
+            self.org_trace['times'].size - self.channel_info['times'].size,
+            self.num_points_outside_time_range(self.start_time, self.end_time)
+        )
+
+    def test_times_data_contained_in_time_range(self):
+        self.start_time = -250
+        self.end_time = 1250
+        trim_downsample_SOHChan(self.channel_info, self.start_time,
+                                self.end_time, self.first_time)
+        np.testing.assert_array_equal(self.channel_info['times'],
+                                      self.org_trace['times'])
+
+    def test_time_range_is_the_same_as_times_data(self):
+        self.start_time = ZERO_EPOCH_TIME
+        self.end_time = 999
+        trim_downsample_SOHChan(self.channel_info, self.start_time,
+                                self.end_time, self.first_time)
+        np.testing.assert_array_equal(self.channel_info['times'],
+                                      self.org_trace['times'])
+
+    def test_time_range_does_not_overlap_times_data(self):
+        self.start_time = 2000
+        self.end_time = 3000
+        trim_downsample_SOHChan(self.channel_info, self.start_time,
+                                self.end_time, self.first_time)
+        self.assertEqual(self.channel_info['times'].size, 0)
+        self.assertEqual(self.channel_info['data'].size, 0)
+
+    def test_data_is_downsampled(self):
+        trim_downsample_SOHChan(self.channel_info, self.start_time,
+                                self.end_time, self.first_time)
+        self.assertTrue(self.mock_downsample.called)
+
+    def test_processed_data_is_stored_in_appropriate_location(self):
+        trim_downsample_SOHChan(self.channel_info, self.start_time,
+                                self.end_time, self.first_time)
+        expected_keys = ('orgTrace', 'times', 'data')
+        self.assertTupleEqual(tuple(self.channel_info.keys()),
+                              expected_keys)
+
+    @patch('sohstationviewer.model.handling_data.downsample')
+    def test_arguments_sent_to_downsample(self, mock_downsample):
+        mock_downsample.return_value = (1, 2, 3)
+        trim_downsample_SOHChan(self.channel_info, self.start_time,
+                                self.end_time, self.first_time)
+
+        positional_args, named_args = mock_downsample.call_args
+        self.assertEqual(len(positional_args), 2)
+
+
+class TestTrimDownsampleSohChanWithLogidx(TestCase):
+    def setUp(self) -> None:
+        self.channel_info = {}
+        self.org_trace = {
+            'times': np.arange(1000),
+            'data': np.arange(1000),
+            'logIdx': np.arange(1000)
+        }
+        self.channel_info['orgTrace'] = self.org_trace
+        self.start_time = 250
+        self.end_time = 750
+        self.first_time = False
+
+    def test_time_range_does_not_overlap_times_data(self):
+        self.start_time = 2000
+        self.end_time = 3000
+        trim_downsample_SOHChan(self.channel_info, self.start_time,
+                                self.end_time, self.first_time)
+        self.assertEqual(self.channel_info['times'].size, 0)
+        self.assertEqual(self.channel_info['data'].size, 0)
+        self.assertEqual(self.channel_info['logIdx'].size, 0)
+
+    def test_processed_data_is_stored_in_appropriate_location(self):
+        trim_downsample_SOHChan(self.channel_info, self.start_time,
+                                self.end_time, self.first_time)
+        expected_keys = ('orgTrace', 'times', 'data', 'logIdx')
+        self.assertTupleEqual(tuple(self.channel_info.keys()),
+                              expected_keys)
+
+    @patch('sohstationviewer.model.handling_data.downsample')
+    def test_arguments_sent_to_downsample(self, mock_downsample):
+        mock_downsample.return_value = (1, 2, 3)
+        trim_downsample_SOHChan(self.channel_info, self.start_time,
+                                self.end_time, self.first_time)
+
+        positional_args, named_args = mock_downsample.call_args
+        self.assertEqual(len(positional_args), 3)
+
+
+class TestTrimDownsampleWfChan(TestCase):
+    def no_file_memmap(self, file_path: Path, **kwargs):
+        # Data will look the same as times. This has two benefits:
+        # - It is a lot easier to inspect what data remains after trimming
+        # and downsampling, seeing as the remaining data would be the same
+        # as the remaining times.
+        # - It is a lot easier to reproducibly create a test data set.
+        array_size = 100
+        file_idx = int(file_path.name.split('-')[-1])
+        start = file_idx * array_size
+        end = start + array_size
+        return np.arange(start, end)
+
+    def setUp(self) -> None:
+        memmap_patcher = patch.object(np, 'memmap',
+                                      side_effect=self.no_file_memmap)
+        self.addCleanup(memmap_patcher.stop)
+        memmap_patcher.start()
+
+        self.channel_data = {}
+        self.traces_info = []
+        self.channel_data['tracesInfo'] = self.traces_info
+        self.data_folder = TemporaryDirectory()
+        for i in range(100):
+            trace_size = 100
+            start_time = i * trace_size
+            trace = {}
+            trace['startTmEpoch'] = start_time
+            trace['endTmEpoch'] = start_time + trace_size - 1
+            trace['size'] = trace_size
+
+            times_file_name = Path(self.data_folder.name) / f'times-{i}'
+            trace['times_f'] = times_file_name
+
+            data_file_name = Path(self.data_folder.name) / f'data-{i}'
+            trace['data_f'] = data_file_name
+
+            self.traces_info.append(trace)
+        self.start_time = 2500
+        self.end_time = 7500
+
+    def test_result_is_stored(self):
+        trim_downsample_WFChan(self.channel_data, self.start_time,
+                               self.end_time, True)
+        self.assertTrue('times' in self.channel_data)
+        self.assertGreater(len(self.channel_data['times']), 0)
+        self.assertTrue('data' in self.channel_data)
+        self.assertGreater(len(self.channel_data['data']), 0)
+
+    def test_data_small_enough_after_first_trim_flag_is_set(self):
+        trim_downsample_WFChan(self.channel_data, self.start_time,
+                               self.end_time, True)
+        self.assertTrue('fulldata' in self.channel_data)
+
+    def test_no_additional_work_if_data_small_enough_after_first_trim(self):
+        trim_downsample_WFChan(self.channel_data, self.start_time,
+                               self.end_time, True)
+        current_times = self.channel_data['times']
+        current_data = self.channel_data['data']
+        trim_downsample_WFChan(self.channel_data, self.start_time,
+                               self.end_time, True)
+        self.assertIs(current_times, self.channel_data['times'])
+        self.assertIs(current_data, self.channel_data['data'])
+
+    def test_data_too_large_after_trimming(self):
+        const.RECAL_SIZE_LIMIT = 1
+        trim_downsample_WFChan(self.channel_data, self.start_time,
+                               self.end_time, False)
+        self.assertTrue('times' not in self.channel_data)
+        self.assertTrue('data' not in self.channel_data)
+        const.RECAL_SIZE_LIMIT = ORIGINAL_RECAL_SIZE_LIMIT
+
+    @patch('sohstationviewer.model.handling_data.trim_waveform_data',
+           wraps=trim_waveform_data)
+    @patch('sohstationviewer.model.handling_data.downsample_waveform_data',
+           wraps=downsample_waveform_data)
+    def test_data_trim_and_downsampled(self, mock_downsample, mock_trim):
+        trim_downsample_WFChan(self.channel_data, self.start_time,
+                               self.end_time, False)
+        self.assertTrue(mock_trim.called)
+        self.assertTrue(mock_downsample.called)
diff --git a/tests/test_view/__init__.py b/tests/test_view/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/tests/test_view/test_util_functions.py b/tests/test_view/test_util_functions.py
new file mode 100644
index 0000000000000000000000000000000000000000..3b9c894ec77ebc4aadbff4522dc3c9555d57950a
--- /dev/null
+++ b/tests/test_view/test_util_functions.py
@@ -0,0 +1,210 @@
+import io
+from contextlib import redirect_stdout
+from pathlib import Path
+import tempfile
+from unittest import TestCase
+
+from sohstationviewer.view.util.functions import (
+    get_soh_messages_for_view, log_str, is_doc_file,
+    create_search_results_file, create_table_of_content_file)
+
+from sohstationviewer.view.util.enums import LogType
+from sohstationviewer.conf import constants as const
+
+
+class TestGetSOHMessageForView(TestCase):
+
+    def test_no_or_empty_textlog(self):
+        soh_msg_channels = {"ACE": ["test1\ntest2", "test3"],
+                            "LOG": ["test4"]}
+        soh_msg_for_view = {'ACE': ['test1', 'test2', 'test3'],
+                            'LOG': ['test4']}
+
+        with self.subTest('test_no_TEXT_str_dataset_key'):
+            soh_messages = {"key1": soh_msg_channels}
+            ret = get_soh_messages_for_view("key1", soh_messages)
+            self.assertNotIn('TEXT', list(ret.keys()))
+            self.assertEqual(ret, soh_msg_for_view)
+
+        with self.subTest('test_empty_TEXT_tupple_dataset_key'):
+            soh_messages = {"TEXT": [], ("key1", "key2"):  soh_msg_channels}
+            ret = get_soh_messages_for_view(("key1", "key2"), soh_messages)
+            self.assertNotIn('TEXT', list(ret.keys()))
+            self.assertEqual(ret, soh_msg_for_view)
+
+        # no key "TEXT", dataset has no channels
+        with self.subTest('test_no_TEXT_no_SOH_channels_for_dataset'):
+            soh_messages = {"key1": {}}
+            ret = get_soh_messages_for_view("key1", soh_messages)
+            self.assertEqual(ret, {})
+
+    def test_some_empty_soh_message(self):
+        soh_messages = {"TEXT": ['text1', 'text2\ntext3'],
+                        "key1": {"ACE": ["test1\ntest2", "test3"],
+                                 "LOG": []}}
+        # channel LOG is empty
+        ret = get_soh_messages_for_view("key1", soh_messages)
+        self.assertEqual(ret,
+                         {'TEXT': ['text1', 'text2', 'text3'],
+                          'ACE': ['test1', 'test2', 'test3'],
+                          'LOG': []})
+
+
+class TestLogStr(TestCase):
+    def test_log_str(self):
+        log = ('info line 1', LogType.INFO)
+        ret = log_str(log)
+        self.assertEqual(ret, 'INFO: info line 1')
+
+
+class TestIsDocFile(TestCase):
+    @classmethod
+    def setUpClass(cls) -> None:
+        cls.temp_dir = tempfile.TemporaryDirectory()
+
+    def _run_is_doc_file(self, filename, include_table_of_contents=False):
+        test_file = Path(self.temp_dir.name).joinpath(filename)
+        with open(test_file, 'w'):
+            pass
+        return is_doc_file(
+            test_file, include_table_of_contents=include_table_of_contents)
+
+    def test_not_md_file(self):
+        self.assertFalse(self._run_is_doc_file("doc.md"))
+
+    def test_table_of_contents_file(self):
+        self.assertFalse(self._run_is_doc_file(
+            "./" + const.TABLE_CONTENTS,
+            include_table_of_contents=False))
+
+        self.assertTrue(self._run_is_doc_file(
+            "./" + const.TABLE_CONTENTS,
+            include_table_of_contents=True))
+
+    def test_doc_file(self):
+        self.assertTrue(self._run_is_doc_file('doc.help.md'))
+
+
+class TestCreateSearchResultFile(TestCase):
+    @classmethod
+    def setUpClass(cls) -> None:
+        cls.temp_dir = tempfile.TemporaryDirectory()
+        cls.temp_dir_path = Path(cls.temp_dir.name)
+        with open(cls.temp_dir_path.joinpath('file1.help.md'), 'w') as file1:
+            file1.write('exist1')
+        with open(cls.temp_dir_path.joinpath(
+                '01 _ file2.help.md'), 'w') as file2:
+            file2.write('exist2')
+        with open(cls.temp_dir_path.joinpath('file3.md'), 'w') as file3:
+            file3.write('exist')
+        with open(cls.temp_dir_path.joinpath(const.SEARCH_RESULTS), 'w'):
+            pass
+        with open(cls.temp_dir_path.joinpath(const.TABLE_CONTENTS), 'w'):
+            pass
+
+    def test_search_text_in_no_files(self):
+        search_result_filename = create_search_results_file(
+            self.temp_dir_path, 'non_exist')
+        self.assertEqual(search_result_filename.name, const.SEARCH_RESULTS)
+        with open(search_result_filename, 'r') as file:
+            content = file.read()
+            self.assertEqual(
+                content,
+                "# Search results\n\nText 'non_exist' not found.")
+
+    def test_search_text_in_one_file(self):
+        search_result_filename = create_search_results_file(
+            self.temp_dir_path, 'exist1')
+        self.assertEqual(search_result_filename.name, const.SEARCH_RESULTS)
+        with open(search_result_filename, 'r') as file:
+            content = file.read()
+            self.assertEqual(
+                content,
+                "# Search results\n\n"
+                "Text 'exist1' found in the following files:\n\n"
+                "---------------------------\n\n"
+                "+ [file1](file1.help.md)\n\n")
+
+    def test_search_text_in_all_files(self):
+        search_result_filename = create_search_results_file(
+            self.temp_dir_path, 'exist')
+        self.assertEqual(search_result_filename.name, const.SEARCH_RESULTS)
+        with open(search_result_filename, 'r') as file:
+            content = file.read()
+            self.assertEqual(
+                content,
+                "# Search results\n\n"
+                "Text 'exist' found in the following files:\n\n"
+                "---------------------------\n\n"
+                "+ [file2](01%20_%20file2.help.md)\n\n"
+                "+ [file1](file1.help.md)\n\n")
+
+    def test_empty_search_text(self):
+        # This case is excluded in help_view
+        search_result_filename = create_search_results_file(
+            self.temp_dir_path, '')
+        self.assertEqual(search_result_filename.name, const.SEARCH_RESULTS)
+        with open(search_result_filename, 'r') as file:
+            content = file.read()
+            self.assertEqual(
+                content,
+                "# Search results\n\n"
+                "Text '' found in the following files:\n\n"
+                "---------------------------\n\n"
+                "+ [file2](01%20_%20file2.help.md)\n\n"
+                "+ [file1](file1.help.md)\n\n")
+
+    def test_no_search_result_file_exist(self):
+        search_result_file_path = self.temp_dir_path.joinpath(
+            const.SEARCH_RESULTS)
+        search_result_file_path.unlink()   # remove file
+        search_result_filename = create_search_results_file(
+            self.temp_dir_path, 'exist2')
+        self.assertEqual(search_result_filename.name, const.SEARCH_RESULTS)
+        with open(search_result_filename, 'r') as file:
+            content = file.read()
+            self.assertEqual(
+                content,
+                "# Search results\n\n"
+                "Text 'exist2' found in the following files:\n\n"
+                "---------------------------\n\n"
+                "+ [file2](01%20_%20file2.help.md)\n\n")
+
+
+class TestCreateTableOfContentFile(TestCase):
+    @classmethod
+    def setUpClass(cls) -> None:
+        cls.temp_dir = tempfile.TemporaryDirectory()
+        cls.temp_dir_path = Path(cls.temp_dir.name)
+        with open(cls.temp_dir_path.joinpath('file1.help.md'), 'w') as file1:
+            file1.write('exist1')
+        with open(cls.temp_dir_path.joinpath(
+                '01 _ file2.help.md'), 'w') as file2:
+            file2.write('exist2')
+        with open(cls.temp_dir_path.joinpath('file3.md'), 'w') as file3:
+            file3.write('exist')
+        with open(cls.temp_dir_path.joinpath(const.SEARCH_RESULTS), 'w'):
+            pass
+        with open(cls.temp_dir_path.joinpath(const.TABLE_CONTENTS), 'w'):
+            pass
+
+    def test_create_table_of_contents_file(self):
+        table_contents_path = self.temp_dir_path.joinpath(const.TABLE_CONTENTS)
+        f = io.StringIO()
+        with redirect_stdout(f):
+            create_table_of_content_file(self.temp_dir_path)
+        output = f.getvalue()
+        self.assertEqual(
+            f"{table_contents_path.as_posix()} has been created.",
+            output.strip())
+        self.assertIn(table_contents_path, list(self.temp_dir_path.iterdir()))
+        with open(table_contents_path, 'r') as file:
+            content = file.read()
+            self.assertTrue(content.endswith(
+                "# Table of Contents\n\n"
+                "+ [Table of Contents](01%20_%20Table%20of%20Contents.help.md)"
+                "\n\n"
+                "+ [file2](01%20_%20file2.help.md)\n\n"
+                "+ [file1](file1.help.md)\n\n",
+                )
+            )