Chore: Updating documentation

This commit is contained in:
Halcy0nic
2024-10-11 09:10:25 -06:00
parent 07b668c21f
commit ee28952a8a
9 changed files with 335 additions and 29 deletions

View File

@@ -58,6 +58,27 @@ python app.py
![scanner-main](./doc/img/lora-main.png)
## Configuration
![config](./doc/img/config.png)
From the configuration page, you can connect to a LoRa transmitter to start sending and receiving messages. Select the desired frequency to begin. Click the 'Select Device' button to connect to a serial port on your computer (where your Feather is attached). Once connected, traffic from your LoRa receiver will automatically stream to the web page for analysis. To disconnect a receiver, click the 'Disconnect Device' button. You can connect up to three LoRa transmitters at once.
![config](./doc/img/select-device.png)
#### Configure LoRaWAN Gateways
The 'Configure LoRaWAN Gateway' section allows you to set up to ten Dragino LPS8N LoRaWAN gateways.
- Click the "Configure Gateway" button to start configuring each gateway's IP address.
- Skip configuring a gateway or disconnect an existing one by leaving the input field empty.
- All entered IP addresses are validated for correct formatting.
- Once configured, the application automatically retrieves and stores LoRaWAN traffic from each active gateway.
- Access and analyze stored traffic in 'survey mode'.
![gateways](./doc/img/gateways.png)
## Analysis Mode
Analyze LoRa traffic received at 433, 868, or 915 MHz with Analysis Mode. Click the desired frequency to get started. Once you are on the appropriate page, click the 'Connect Serial Port' button to connect to a serial port on your computer (the one your Feather is attached to). Once connected to your LoRa receiver, traffic will automatically be streamed to the web page for analysis. To disconnect a receiver, click the 'Disconnect Serial Port' button.
@@ -106,10 +127,3 @@ To download all packets captured by the LoRa Scanner, click the Download Pack
![export-packets](./doc/img/download-packets.png)
## LoRaWAN
To capture LoRaWAN traffic, you have to connect to an active Dragino LPS8N Indoor LoRaWAN gateway (915 or 868 MHz). You can connect to your gateway from the main screen by entering in the IP address of the gateway. Currently the application allows you to simultaneously connect to three gateways at once.
![gateway](./doc/img/lorawan-gateway.png)
![gateway-config](./doc/img/lorawan-gateway-configure.png)

336
app.py
View File

@@ -42,12 +42,23 @@ surveydata = {}
parsed_entries = set()
def read_serial_data(port, ser, buffer):
"""
This function reads data from a serial port and processes it.
Parameters:
port (str): The name of the serial port (e.g., 'port1', 'port2', 'port3').
ser (Serial): The serial object representing the serial port.
buffer (deque): A deque object to store the received data.
Returns:
None
"""
global surveydata
rssi_pattern = r"RSSI: (-?\d+)"
decoded_value_pattern = r"Decoded Value: (.+)"
rssi = None
decoded_value = None
while True:
try:
if ser.in_waiting > 0:
@@ -67,7 +78,7 @@ def read_serial_data(port, ser, buffer):
if rssi is not None and decoded_value is not None:
freq = frequency(port)
key = f'Raw LoRa Device {freq} MHz'
# Initialize the list for the frequency if not already done
if key not in surveydata:
surveydata[key] = []
@@ -97,28 +108,61 @@ def read_serial_data(port, ser, buffer):
pass
def convert_dict_to_csv(data):
"""
Converts a dictionary of survey data into a CSV format.
Parameters:
data (dict): A dictionary where keys are device identifiers and values are lists of data entries.
Each data entry is a list containing frequency, RSSI, and a decoded value.
Returns:
StringIO: A StringIO object containing the CSV representation of the input data.
"""
output = StringIO()
writer = csv.writer(output)
# Write the header
writer.writerow(['Device', 'Frequency', 'RSSI', 'Decoded Value'])
# Write the data
for key, values in data.items():
for value in values:
writer.writerow([key] + value)
output.seek(0)
return output
def is_valid_ip(ip):
"""
Validates whether a given string is a valid IP address.
Parameters:
ip (str): The string representation of the IP address to validate.
Returns:
bool: True if the input string is a valid IP address, False otherwise.
"""
try:
ip_address(ip)
return True
except AddressValueError:
return False
return False
def parse_and_store_data():
"""
Fetches data from configured gateway URLs, parses the data, and stores it in a global survey data structure.
This function constructs URLs for gateways, sends HTTP GET requests to fetch data, parses the HTML response
to extract relevant information, and stores the parsed data in a global dictionary. It also schedules itself
to run periodically.
Global Variables:
- surveydata (dict): A dictionary to store parsed survey data.
- parsed_entries (set): A set to keep track of already parsed entries to avoid duplicates.
- gateway_ips (dict): A dictionary containing IP addresses of gateways.
The function does not take any parameters or return any values.
"""
global surveydata
global parsed_entries
global gateway_ips
@@ -151,19 +195,19 @@ def parse_and_store_data():
table = soup.find('table')
if table:
rows = table.find_all('tr')[1:] # Skip the header row
for row in rows:
# Skip hidden rows in this iteration
if row.get('style') == 'display: none;':
continue
cells = row.find_all('td')
if not cells:
continue
# Prepare formatted_row from visible cells, skipping the first cell for Chevron icon
formatted_row = ' | '.join(cell.text.strip() for cell in cells[1:])
# Extract dev_id and freq from the visible row
dev_id = extract_dev_id(formatted_row)
freq = extract_freq(formatted_row)
@@ -200,8 +244,19 @@ def parse_and_store_data():
def extract_dev_id(formatted_row):
# Assuming DevEui or DevAddr is in the 'Content' part of the formatted_row
# and it's formatted like 'Dev Addr: {DevEui}, Size: {Size}'
"""
Extracts the device identifier (DevEui or DevAddr) from a formatted row string.
The function assumes that the device identifier is located in the 'Content' part of the formatted row,
formatted as 'Dev Addr: {DevEui}, Size: {Size}'.
Parameters:
formatted_row (str): A string representing a row of data, where the last part contains the 'Content'
with the device identifier.
Returns:
str or None: The extracted device identifier (DevEui or DevAddr) if successful, or None if extraction fails.
"""
try:
content_part = formatted_row.split('|')[-1].strip() # Get the last part of the formatted_row, which is 'Content'
dev_id = content_part.split(',')[0].split(':')[-1].strip() # Extract the DevEui or DevAddr
@@ -212,7 +267,18 @@ def extract_dev_id(formatted_row):
def extract_freq(formatted_row):
# Assuming 'Freq' is a standalone field in the formatted_row
"""
Extracts the frequency value from a formatted row string.
The function assumes that the frequency is a standalone field within the formatted row,
specifically located at a fixed position.
Parameters:
formatted_row (str): A string representing a row of data, where fields are separated by '|'.
Returns:
float or None: The extracted frequency as a float if successful, or None if extraction fails.
"""
try:
freq_part = formatted_row.split('|')[3].strip() # Get the 'Freq' part (assuming it's the fifth field)
freq = float(freq_part) # Convert the frequency to float
@@ -222,7 +288,18 @@ def extract_freq(formatted_row):
return None # Return None or some default value if extraction fails
def connect_serial(port,frequency):
def connect_serial(port, frequency):
"""
Establishes a serial connection to a specified port and frequency, and starts a thread to read data.
Parameters:
port (str): The name of the serial port to connect to.
frequency (int): The frequency in MHz for which the serial connection is being established.
Supported frequencies are 433, 868, and 915 MHz.
Returns:
None
"""
global ser1
global ser2
global ser3
@@ -252,12 +329,21 @@ def connect_serial(port,frequency):
serial_threads['port3'] = threading.Thread(target=read_serial_data, args=('port3', ser3, serial_buffers['port3']))
serial_threads['port3'].daemon = True
serial_threads['port3'].start()
except:
print('\n\nPort for 915 MHz not available\n\n')
def disconnect_serial(port):
"""
Disconnects the serial connection for a specified port and cleans up associated resources.
Parameters:
port (str): The name of the serial port to disconnect. Expected values are 'port1', 'port2', or 'port3'.
Returns:
None
"""
global ser1
global ser2
global ser3
@@ -279,74 +365,194 @@ def disconnect_serial(port):
@app.route('/')
def index():
"""
Renders the main index page of the application.
This function handles the root URL of the application and returns the rendered HTML
template for the index page.
Returns:
str: The rendered HTML content for the index page.
"""
return render_template('index.html')
@app.route('/analysis')
def analysis():
"""
Renders the analysis page of the application with initial serial data.
This function handles the '/analysis' URL route and returns the rendered HTML
template for the analysis page. It includes initial data from the serial buffers
for each port.
Returns:
str: The rendered HTML content for the analysis page, with initial data for each port.
"""
return render_template('analysis.html', initial_data={port: list(buffer) for port, buffer in serial_buffers.items()})
@app.route('/survey')
def survey():
"""
Renders the survey page of the application.
This function handles the '/survey' URL route and returns the rendered HTML
template for the survey page.
Returns:
str: The rendered HTML content for the survey page.
"""
return render_template('survey.html')
@app.route('/tracking')
def tracking():
"""
Renders the tracking page of the application with initial serial data.
This function handles the '/tracking' URL route and returns the rendered HTML
template for the tracking page. It includes initial data from the serial buffers
for each port.
Returns:
str: The rendered HTML content for the tracking page, with initial data for each port.
"""
return render_template('tracking.html', initial_data={port: list(buffer) for port, buffer in serial_buffers.items()})
@app.route('/attach_serial_433', methods=['GET'])
def attach_serial_433():
"""
Attaches a serial connection to the 433 MHz port.
This function handles the '/attach_serial_433' URL route and establishes a serial
connection to the specified port for 433 MHz frequency.
Parameters:
None
Returns:
Response: A JSON response indicating the result of the operation.
"""
user_input = escape(request.args.get('user_input'))
port1_status = True
connect_serial(str(user_input), 433)
# Process the input as needed
result = f'Serial Port Requested for 433 MHz'
return jsonify(result=result)
@app.route('/delete_serial_433', methods=['GET'])
def delete433():
# Add your logic here to handle the confirmation
"""
Disconnects the serial connection for the 433 MHz port.
This function handles the '/delete_serial_433' URL route and disconnects
the serial connection associated with the 433 MHz frequency.
Returns:
Response: A JSON response indicating the result of the disconnection operation.
"""
disconnect_serial("port1")
result = "Port Disconnected!"
return jsonify(result=result)
@app.route('/attach_serial_868', methods=['GET'])
def attach_serial_868():
"""
Attaches a serial connection to the 868 MHz port.
This function handles the '/attach_serial_868' URL route and establishes a serial
connection to the specified port for 868 MHz frequency.
Parameters:
None
Returns:
Response: A JSON response indicating the result of the operation.
"""
user_input = escape(request.args.get('user_input'))
port2_status = True
connect_serial(str(user_input), 868)
# Process the input as needed
result = f'Serial Port Requested for 868 MHz'
return jsonify(result=result)
@app.route('/delete_serial_868', methods=['GET'])
def delete868():
# Add your logic here to handle the confirmation
"""
Disconnects the serial connection for the 868 MHz port.
This function handles the '/delete_serial_868' URL route and disconnects
the serial connection associated with the 868 MHz frequency.
Returns:
Response: A JSON response indicating the result of the disconnection operation.
"""
disconnect_serial("port2")
result = "Port Disconnected!"
return jsonify(result=result)
@app.route('/attach_serial_915', methods=['GET'])
def attach_serial_915():
"""
Attaches a serial connection to the 915 MHz port.
This function handles the '/attach_serial_915' URL route and establishes a serial
connection to the specified port for 915 MHz frequency.
Parameters:
None
Returns:
Response: A JSON response indicating the result of the operation.
"""
user_input = escape(request.args.get('user_input'))
connect_serial(str(user_input), 915)
# Process the input as needed
result = f'Serial Port Requested for 915 MHz'
return jsonify(result=result)
@app.route('/delete_serial_915', methods=['GET'])
def delete915():
# Add your logic here to handle the confirmation
"""
Disconnects the serial connection for the 915 MHz port.
This function handles the '/delete_serial_915' URL route and disconnects
the serial connection associated with the 915 MHz frequency.
Returns:
Response: A JSON response indicating the result of the disconnection operation.
"""
disconnect_serial("port3")
result = "Port Disconnected!"
return jsonify(result=result)
@socketio.on('connect')
def handle_connect():
"""
Handles a new client connection to the SocketIO server.
This function is triggered when a client connects to the server via SocketIO.
It emits the initial serial data for each port to the connected client.
Parameters:
None
Returns:
None
"""
for port, buffer in serial_buffers.items():
emit(f'initial_serial_data_{port}', {'data': list(buffer)})
@app.route('/transmit433', methods=['POST'])
def transmit433():
"""
Transmits a message over the 433 MHz serial connection.
This function handles the '/transmit433' URL route and sends a user-provided
message to the connected serial device operating at 433 MHz frequency.
Parameters:
None
Returns:
Response: A JSON response indicating the result of the transmission operation.
"""
global ser1
data = request.json # Get the data from the POST request
user_input = data.get('user_input') # Extract the user input
@@ -356,11 +562,32 @@ def transmit433():
@app.route('/get_serial_ports')
def get_serial_ports():
"""
Retrieves a list of available serial ports on the system.
This function handles the '/get_serial_ports' URL route and returns a JSON
response containing a list of serial port device names available on the system.
Returns:
Response: A JSON response with a list of available serial port device names.
"""
ports = [port.device for port in serial.tools.list_ports.comports()]
return jsonify(ports=ports)
@app.route('/transmit868', methods=['POST'])
def transmit868():
"""
Transmits a message over the 868 MHz serial connection.
This function handles the '/transmit868' URL route and sends a user-provided
message to the connected serial device operating at 868 MHz frequency.
Parameters:
None
Returns:
Response: A JSON response indicating the result of the transmission operation.
"""
global ser2
data = request.json # Get the data from the POST request
user_input = data.get('user_input') # Extract the user input
@@ -370,6 +597,18 @@ def transmit868():
@app.route('/transmit915', methods=['POST'])
def transmit915():
"""
Transmits a message over the 915 MHz serial connection.
This function handles the '/transmit915' URL route and sends a user-provided
message to the connected serial device operating at 915 MHz frequency.
Parameters:
None
Returns:
Response: A JSON response indicating the result of the transmission operation.
"""
global ser3
data = request.json # Get the data from the POST request
user_input = data.get('user_input') # Extract the user input
@@ -379,6 +618,19 @@ def transmit915():
@app.route('/checkSer', methods=['GET'])
def checkSer():
"""
Checks the status of a specified serial port to determine if it is open.
This function handles the '/checkSer' URL route and checks whether the specified
serial port is currently open. It returns a JSON response indicating the status.
Parameters:
None
Returns:
Response: A JSON response with the result "True" if the specified port is open,
otherwise "False".
"""
data = request.args.get('port')
if data == 'port1':
try:
@@ -402,6 +654,19 @@ def checkSer():
@app.route('/get_table_data')
def get_table_data():
"""
Retrieves and returns the cleaned survey data in JSON format.
This function processes the global survey data to remove any entries with empty device identifiers
and returns the cleaned data as a JSON response.
Parameters:
None
Returns:
Response: A JSON response containing the cleaned survey data, where each key is a device identifier
and the value is a list of associated data entries.
"""
global surveydata
cleaned_data = {}
@@ -409,12 +674,26 @@ def get_table_data():
if dev_id: # Check if dev_id is not empty
cleaned_data[dev_id] = data
#print(cleaned_data) # For debugging
return jsonify(cleaned_data)
@app.route('/set_gateways', methods=['POST'])
def set_gateways():
"""
Updates the IP addresses of the gateways based on the provided form data.
This function processes a POST request containing IP addresses for up to 10 gateways.
It validates each IP address and updates the global `gateway_ips` dictionary with the
new values if they are valid.
Parameters:
None
Returns:
Response: A JSON response indicating the success or failure of the operation.
- On success, returns a message confirming the update of gateway IPs with a 200 status code.
- On failure, returns an error message specifying the invalid IP address with a 400 status code.
"""
global gateway_ips
data = request.form
for key in [f'gateway{i}' for i in range(1, 11)]:
@@ -434,6 +713,19 @@ def set_gateways():
@app.route('/downloadPackets', methods=['GET'])
def downloadPackets():
"""
Initiates the download of survey data in CSV format.
This function handles the '/downloadPackets' URL route and converts the global
survey data into a CSV format. It then prepares the CSV data for download as a file.
Parameters:
None
Returns:
Response: A Flask response object that sends the CSV file as an attachment to the client.
The file is named 'surveydata.csv' and is sent with a 'text/csv' MIME type.
"""
csv_data = convert_dict_to_csv(surveydata)
# Convert StringIO to BytesIO for send_file compatibility
bytes_data = BytesIO()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 193 KiB

After

Width:  |  Height:  |  Size: 134 KiB

BIN
doc/img/config.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

BIN
doc/img/gateways.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 458 KiB

After

Width:  |  Height:  |  Size: 243 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 MiB

BIN
doc/img/select-device.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB