From e4594ae6645a1652de48877afcab87bba785d1db Mon Sep 17 00:00:00 2001 From: Halcy0nic Date: Wed, 31 Jan 2024 11:29:52 -0700 Subject: [PATCH] Feat: Integrating Dragino lps8n --- app.py | 89 +- draginoReq.py | 58 ++ examplereq | 1767 +++++++++++++++++++++++++++++++++++++++ lorawan.py | 122 +++ rn2903-lorawan.txt | 1 + templates/survey.html | 29 +- templates/tracking.html | 71 +- udp-listener.py | 22 + 8 files changed, 2108 insertions(+), 51 deletions(-) create mode 100644 draginoReq.py create mode 100644 examplereq create mode 100644 lorawan.py create mode 100644 rn2903-lorawan.txt create mode 100644 udp-listener.py diff --git a/app.py b/app.py index 7a54506..fe7f2c6 100644 --- a/app.py +++ b/app.py @@ -3,10 +3,12 @@ from markupsafe import escape from flask_socketio import SocketIO, emit import serial import threading +from threading import Timer import time from collections import deque -import pandas as pd import re +import requests +from bs4 import BeautifulSoup app = Flask(__name__) socketio = SocketIO(app) @@ -18,7 +20,6 @@ port3_status = True global ser1 global ser2 global ser3 -global_dataframe = pd.DataFrame(columns=['Device Name', 'Frequency', 'Signal Strength', 'Plaintext']) frequency = lambda port: {'port1': 433, 'port2': 868,'port3': 915}.get(port, None) surveydata = {} @@ -75,8 +76,78 @@ def read_serial_data(port, ser, buffer): print(f"Error: {e}") pass +def parse_and_store_data(): + global surveydata + url = "http://10.130.1.1/cgi-bin/log-traffic.has" # Your target URL + headers = { + "Host": "10.130.1.1", + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:122.0) Gecko/20100101 Firefox/122.0", + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", + "Accept-Language": "en-US,en;q=0.5", + "Accept-Encoding": "gzip, deflate", + "DNT": "1", + "Sec-GPC": "1", + "Authorization": "Basic cm9vdDpkcmFnaW5v", + "Connection": "keep-alive", + "Referer": "http://10.130.1.1/cgi-bin/log-lora.has", + "Upgrade-Insecure-Requests": "1" + } + + response = requests.get(url, headers=headers) + + if response.status_code == 200: + soup = BeautifulSoup(response.text, 'html.parser') + table = soup.find('table') + rows = table.find_all('tr') + headers = [header.text.strip() for header in rows[0].find_all('th')][1:] + + for row in rows[1:]: + cells = row.find_all('td') + cell_data = [cell.text.strip() for cell in cells[1:] if cells.index(cell) < len(headers) + 1] + formatted_row = ' | '.join(cell_data) + + # Extract DevEui or DevAddr from the response + dev_id = extract_dev_id(formatted_row) # Implement this function based on your data format + freq = extract_freq(formatted_row) # Implement this function based on your data format + + # Initialize dictionary for dev_id if not present + if dev_id not in surveydata: + surveydata[dev_id] = [] + + # Append new data to the list associated with the DevEui or DevAddr + surveydata[dev_id].append([freq, 0, formatted_row]) + #surveydata[dev_id]['decoded_values'].append(formatted_row) + + print("Data parsed and stored.") + + else: + print(f"Request failed with status code: {response.status_code}") + + # Schedule the next call to this function + Timer(60, parse_and_store_data).start() # Call this function every 60 seconds +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}' + 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 + return dev_id + except Exception as e: + print(f"Error extracting DevEui/DevAddr: {e}") + return None # Return None or some default value if extraction fails + + +def extract_freq(formatted_row): + # Assuming 'Freq' is a standalone field in the formatted_row + 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 + return freq + except Exception as e: + print(f"Error extracting frequency: {e}") + return None # Return None or some default value if extraction fails def connect_serial(port,frequency): @@ -144,7 +215,7 @@ def analysis(): @app.route('/survey') def survey(): - return render_template('survey.html', data=global_dataframe) + return render_template('survey.html') @app.route('/tracking') def tracking(): @@ -255,8 +326,16 @@ def checkSer(): @app.route('/get_table_data') def get_table_data(): global surveydata - print(surveydata) - return jsonify(surveydata) + cleaned_data = {} + + for dev_id, data in surveydata.items(): + if dev_id: # Check if dev_id is not empty + cleaned_data[dev_id] = data + + #print(cleaned_data) # For debugging + return jsonify(cleaned_data) + if __name__ == '__main__': + Timer(60, parse_and_store_data).start() socketio.run(app, debug=True) diff --git a/draginoReq.py b/draginoReq.py new file mode 100644 index 0000000..4b9f5f6 --- /dev/null +++ b/draginoReq.py @@ -0,0 +1,58 @@ +import requests +from bs4 import BeautifulSoup + +# Define the URL and headers +url = "http://10.130.1.1/cgi-bin/log-traffic.has" +headers = { + "Host": "10.130.1.1", + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:122.0) Gecko/20100101 Firefox/122.0", + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", + "Accept-Language": "en-US,en;q=0.5", + "Accept-Encoding": "gzip, deflate", + "DNT": "1", + "Sec-GPC": "1", + "Authorization": "Basic cm9vdDpkcmFnaW5v", + "Connection": "keep-alive", + "Referer": "http://10.130.1.1/cgi-bin/log-lora.has", + "Upgrade-Insecure-Requests": "1" +} + +# Send the GET request +response = requests.get(url, headers=headers) + +if response.status_code == 200: + # Parse the HTML content using BeautifulSoup + soup = BeautifulSoup(response.text, 'html.parser') + + # Find the table + table = soup.find('table') + + # Initialize an empty list to store formatted strings for each row + formatted_rows = [] + + # Find all table rows + rows = table.find_all('tr') + + # Get column headers from the first row + # Using .text.strip() to clean the text and [1:] to skip the empty first column + headers = [header.text.strip() for header in rows[0].find_all('th')][1:] + + # Iterate through each row (skipping the first row with the headers) + for row in rows[1:]: + # Find all data cells (td tags) in the row + cells = row.find_all('td') + + # Extract text from each cell + # Using [1:] to skip the first cell with the arrow icon + cell_data = [cell.text.strip() for cell in cells[1:] if cells.index(cell) < len(headers) + 1] + + # Format the row data into a neat line + formatted_row = ' | '.join(cell_data) + + # Append the formatted string to the list + formatted_rows.append(formatted_row) + + # Print the formatted string to display the row + print(formatted_row) +else: + print("Request failed with status code:", response.status_code) \ No newline at end of file diff --git a/examplereq b/examplereq new file mode 100644 index 0000000..eb0ad18 --- /dev/null +++ b/examplereq @@ -0,0 +1,1767 @@ + + + + + + + + LoRa Gateway + + + + + + + + + + + + + + + + + + + + + +
+ Please wait...

+
+
+ + + +
+ + + +
+ +
+ +
+ + + + + + + + + + + + + Custom + + + + + + + + Home + + Logout + + + + + +
+ +
+

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TimeMessage TypeModFreqData RateCNTContent
01/30-13:53:13Data Unconfirmed UpLoRa914.1SF10 BW12512Dev Addr: 27FD3566, Size: 20
01/30-13:51:34Data Unconfirmed UpLoRa914.9SF10 BW1257Dev Addr: 27FD3566, Size: 18
01/30-13:51:34Data Unconfirmed UpLoRa913.9SF10 BW1257Dev Addr: 27FD3566, Size: 18
+ + + + + + + + + + + diff --git a/lorawan.py b/lorawan.py new file mode 100644 index 0000000..ddc3c98 --- /dev/null +++ b/lorawan.py @@ -0,0 +1,122 @@ +##!/usr/bin/env python3 +import io +import sys +import time +import datetime +import argparse +from enum import IntEnum +import serial +from serial.threaded import LineReader, ReaderThread + +parser = argparse.ArgumentParser(description='Connect to LoRaWAN network') +parser.add_argument('port', help="Serial port of LoStik") +parser.add_argument('--joinmode', '-j', help="otaa, abp", default="otaa") + +# ABP credentials +parser.add_argument('--appskey', help="App Session Key", default="") +parser.add_argument('--nwkskey', help="Network Session Key", default="") +parser.add_argument('--devaddr', help="Device Address", default="") + +# OTAA credentials +parser.add_argument('--appeui', help="App EUI", default="") +parser.add_argument('--appkey', help="App Key", default="") +parser.add_argument('--deveui', help="Device EUI", default="") + +args = parser.parse_args() + +OTAA_RETRIES = 5 + +class MaxRetriesError(Exception): + pass + +class ConnectionState(IntEnum): + SUCCESS = 0 + CONNECTING = 100 + CONNECTED = 200 + FAILED = 500 + TO_MANY_RETRIES = 520 + + +class PrintLines(LineReader): + + retries = 0 + state = ConnectionState.CONNECTING + + def retry(self, action): + if(self.retries >= OTAA_RETRIES): + print("Too many retries, exiting") + self.state = ConnectionState.TO_MANY_RETRIES + return + self.retries = self.retries + 1 + action() + + def get_var(self, cmd): + self.send_cmd(cmd) + return self.transport.serial.readline() + + def join(self): + if args.joinmode == "abp": + self.join_abp() + else: + self.join_otaa() + + def join_otaa(self): + if len(args.appeui): + self.send_cmd('mac set appeui %s' % args.appeui) + if len(args.appkey): + self.send_cmd('mac set appkey %s' % args.appkey) + if len(args.deveui): + self.send_cmd('mac set deveui %s' % args.deveui) + self.send_cmd('mac join otaa') + + def join_abp(self): + if len(args.devaddr): + self.send_cmd('mac set devaddr %s' % args.devaddr) + if len(args.appskey): + self.send_cmd('mac set appskey %s' % args.appskey) + if len(args.nwkskey): + self.send_cmd('mac set nwkskey %s' % args.nwkskey) + self.send_cmd('mac join abp') + + def connection_made(self, transport): + """ + Fires when connection is made to serial port device + """ + print("Connection to LoStik established") + self.transport = transport + self.retry(self.join) + + def handle_line(self, data): + # if data == "ok" or data == 'busy': + # return + print("STATUS: %s" % data) + if data.strip() == "denied" or data.strip() == "no_free_ch": + print("Retrying OTAA connection") + self.retry(self.join) + elif data.strip() == "accepted": + print("UPDATING STATE to connected") + self.state = ConnectionState.CONNECTED + + def connection_lost(self, exc): + """ + Called when serial connection is severed to device + """ + if exc: + print(exc) + print("Lost connection to serial device") + + def send_cmd(self, cmd, delay=.5): + print(cmd) + self.transport.write(('%s\r\n' % cmd).encode('UTF-8')) + time.sleep(delay) + + +ser = serial.Serial(args.port, baudrate=57600) +with ReaderThread(ser, PrintLines) as protocol: + while protocol.state < ConnectionState.FAILED: + if protocol.state != ConnectionState.CONNECTED: + time.sleep(1) + continue + protocol.send_cmd("mac tx uncnf 1 %d" % int(time.time())) + time.sleep(10) + exit(protocol.state) \ No newline at end of file diff --git a/rn2903-lorawan.txt b/rn2903-lorawan.txt new file mode 100644 index 0000000..0dacd47 --- /dev/null +++ b/rn2903-lorawan.txt @@ -0,0 +1 @@ +python3 lorawan.py --joinmode otaa --appkey "814BFA6E589EE49376628B0CDD642E1F" --deveui "70B3D57ED80022D3" --appeui 0000000000000000 /dev/tty.usbserial-1140 diff --git a/templates/survey.html b/templates/survey.html index 84ddffb..db57e18 100644 --- a/templates/survey.html +++ b/templates/survey.html @@ -81,8 +81,11 @@ let cell4 = mainRow.insertCell(); cell1.innerHTML = key; cell2.innerHTML = data[key][0][0]; // Frequency (from the first entry) - cell3.innerHTML = data[key][0][1]; // Signal Strength (from the first entry) - + + // Check for RSSI value and display 'unknown' if it is 0 + let rssiValue = data[key][0][1]; + cell3.innerHTML = rssiValue === 0 ? 'unknown' : rssiValue; // Signal Strength (from the first entry) + // Create a Bootstrap styled button let expandBtn = document.createElement('button'); expandBtn.classList.add('btn', 'btn-secondary', 'btn-sm'); // Bootstrap button classes @@ -90,21 +93,21 @@ expandBtn.onclick = function() { let deviceKey = key; // Capture the current device key let isExpanded = !expandedRows[deviceKey]; // Toggle the expanded state - + // Show or hide the expandable rows let nextRow = mainRow.nextSibling; while (nextRow && nextRow.classList.contains('expandable-row')) { nextRow.style.display = isExpanded ? 'table-row' : 'none'; nextRow = nextRow.nextSibling; } - + // Update the button text and expandedRows object expandBtn.innerHTML = isExpanded ? 'Hide Values' : 'Show Values'; expandedRows[deviceKey] = isExpanded; }; cell4.appendChild(expandBtn); cell4.style.textAlign = 'center'; // Center align the button - + // Add expandable rows for each decoded value data[key].forEach(entry => { let expandableRow = tableBody.insertRow(); @@ -114,7 +117,7 @@ cell.colSpan = "4"; cell.innerHTML = `Decoded Value: ${entry[2]}`; }); - + if (expandedRows[key]) { let nextRow = mainRow.nextSibling; while (nextRow && nextRow.classList.contains('expandable-row')) { @@ -126,15 +129,15 @@ }) .catch(error => console.error('Error:', error)); } - - // Initial update of the table - updateTableData(); - - // Periodically update the table every 30 seconds (30000 milliseconds) - setInterval(updateTableData, 30000); - + + // Initial update of the table + updateTableData(); + + // Periodically update the table every 30 seconds (30000 milliseconds) + setInterval(updateTableData, 30000); +
diff --git a/templates/tracking.html b/templates/tracking.html index ddac759..21cfa13 100644 --- a/templates/tracking.html +++ b/templates/tracking.html @@ -79,46 +79,51 @@ let selectedDeviceName = null; // Global variable to store the selected device name let dataCache = {}; // Cache for storing fetched data - function updateTableData() { - fetch('/get_table_data') - .then(response => response.json()) - .then(data => { - const tableBody = document.getElementById('data-table').getElementsByTagName('tbody')[0]; - tableBody.innerHTML = ''; // Clear existing table rows + - let isRowSelected = selectedDeviceName !== null; + function updateTableData() { + fetch('/get_table_data') + .then(response => response.json()) + .then(data => { + const tableBody = document.getElementById('data-table').getElementsByTagName('tbody')[0]; + tableBody.innerHTML = ''; // Clear existing table rows - for (let key in data) { - let row = tableBody.insertRow(); - row.addEventListener('click', function() { - // Handle row click event for selection - handleRowSelection(row, key); - }); + let isRowSelected = selectedDeviceName !== null; - let cell1 = row.insertCell(); - let cell2 = row.insertCell(); - let cell3 = row.insertCell(); - cell1.innerHTML = key; // Device Name - - // Assuming that the latest data is the most relevant - let latestData = data[key][data[key].length - 1]; - cell2.innerHTML = latestData[0]; // Frequency - cell3.innerHTML = latestData[1]; // Signal Strength + for (let key in data) { + let row = tableBody.insertRow(); + row.addEventListener('click', function() { + // Handle row click event for selection + handleRowSelection(row, key); + }); - // Apply hiding logic based on selection - if (isRowSelected) { - if (key !== selectedDeviceName) { - row.classList.add('hidden-row'); - } else { - row.classList.add('selected-row'); - } + let cell1 = row.insertCell(); + let cell2 = row.insertCell(); + let cell3 = row.insertCell(); + cell1.innerHTML = key; // Device Name + + // Assuming that the latest data is the most relevant + let latestData = data[key][data[key].length - 1]; + cell2.innerHTML = latestData[0]; // Frequency + + // Check for RSSI value and display 'unknown' if it is 0 + let rssiValue = latestData[1]; + cell3.innerHTML = rssiValue === 0 ? 'unknown' : rssiValue; // Signal Strength + + // Apply hiding logic based on selection + if (isRowSelected) { + if (key !== selectedDeviceName) { + row.classList.add('hidden-row'); + } else { + row.classList.add('selected-row'); } } + } - updateSelectedDataDisplay(); // Update the selected data display after refreshing the table - }) - .catch(error => console.error('Error:', error)); - } + updateSelectedDataDisplay(); // Update the selected data display after refreshing the table + }) + .catch(error => console.error('Error:', error)); + } function handleRowSelection(row, key) { // Reset all rows diff --git a/udp-listener.py b/udp-listener.py new file mode 100644 index 0000000..90cfb64 --- /dev/null +++ b/udp-listener.py @@ -0,0 +1,22 @@ +import socket + +# Define the IP address and the port number to listen on. +# '' means the script will listen on all available IPs. +IP = '10.130.1.235' +PORT = 1700 + +def main(): + # Create a UDP socket + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s: + # Bind the socket to the address and port + s.bind((IP, PORT)) + print(f"Listening for UDP traffic on port {PORT}...") + + # Continuously listen for UDP packets + while True: + # Receive data from the client + data, addr = s.recvfrom(1024) # buffer size is 1024 bytes + print(f"Received message from {addr}: {data}") + +if __name__ == "__main__": + main()