kopia lustrzana https://github.com/projecthorus/chasemapper
Add habitat chase-car position upload.
rodzic
3686e84581
commit
5a10945543
|
@ -60,6 +60,11 @@ def parse_config_file(filename):
|
||||||
chase_config['car_serial_port'] = config.get('gps_serial', 'gps_port')
|
chase_config['car_serial_port'] = config.get('gps_serial', 'gps_port')
|
||||||
chase_config['car_serial_baud'] = config.getint('gps_serial', 'gps_baud')
|
chase_config['car_serial_baud'] = config.getint('gps_serial', 'gps_baud')
|
||||||
|
|
||||||
|
# Habitat Settings
|
||||||
|
chase_config['habitat_upload_enabled'] = config.getboolean('habitat', 'habitat_upload_enabled')
|
||||||
|
chase_config['habitat_call'] = config.get('habitat', 'habitat_call')
|
||||||
|
chase_config['habitat_update_rate'] = config.getint('habitat', 'habitat_update_rate')
|
||||||
|
|
||||||
# Predictor
|
# Predictor
|
||||||
chase_config['pred_enabled'] = config.getboolean('predictor', 'predictor_enabled')
|
chase_config['pred_enabled'] = config.getboolean('predictor', 'predictor_enabled')
|
||||||
chase_config['pred_burst'] = config.getfloat('predictor', 'default_burst')
|
chase_config['pred_burst'] = config.getfloat('predictor', 'default_burst')
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
#
|
#
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
|
|
|
@ -0,0 +1,213 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
#
|
||||||
|
# Project Horus - Browser-Based Chase Mapper
|
||||||
|
# Habitat Communication (Chase car position upload)
|
||||||
|
#
|
||||||
|
# Copyright (C) 2018 Mark Jessop <vk5qi@rfhead.net>
|
||||||
|
# Released under GNU GPL v3 or later
|
||||||
|
#
|
||||||
|
import datetime
|
||||||
|
import logging
|
||||||
|
import requests
|
||||||
|
import time
|
||||||
|
import traceback
|
||||||
|
import json
|
||||||
|
from base64 import b64encode
|
||||||
|
from hashlib import sha256
|
||||||
|
from threading import Thread, Lock
|
||||||
|
try:
|
||||||
|
# Python 2
|
||||||
|
from Queue import Queue
|
||||||
|
except ImportError:
|
||||||
|
# Python 3
|
||||||
|
from queue import Queue
|
||||||
|
|
||||||
|
|
||||||
|
HABITAT_URL = "http://habitat.habhub.org/"
|
||||||
|
|
||||||
|
url_habitat_uuids = HABITAT_URL + "_uuids?count=%d"
|
||||||
|
url_habitat_db = HABITAT_URL + "habitat/"
|
||||||
|
uuids = []
|
||||||
|
|
||||||
|
|
||||||
|
def ISOStringNow():
|
||||||
|
return "%sZ" % datetime.datetime.utcnow().isoformat()
|
||||||
|
|
||||||
|
def postListenerData(doc, timeout=10):
|
||||||
|
global uuids, url_habitat_db
|
||||||
|
# do we have at least one uuid, if not go get more
|
||||||
|
if len(uuids) < 1:
|
||||||
|
fetchUuids()
|
||||||
|
|
||||||
|
# Attempt to add UUID and time data to document.
|
||||||
|
try:
|
||||||
|
doc['_id'] = uuids.pop()
|
||||||
|
except IndexError:
|
||||||
|
logging.error("Habitat - Unable to post listener data - no UUIDs available.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
doc['time_uploaded'] = ISOStringNow()
|
||||||
|
|
||||||
|
try:
|
||||||
|
_r = requests.post(url_habitat_db, json=doc, timeout=timeout)
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logging.error("Habitat - Could not post listener data - %s" % str(e))
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def fetchUuids(timeout=10):
|
||||||
|
global uuids, url_habitat_uuids
|
||||||
|
|
||||||
|
_retries = 5
|
||||||
|
|
||||||
|
while _retries > 0:
|
||||||
|
try:
|
||||||
|
_r = requests.get(url_habitat_uuids % 10, timeout=timeout)
|
||||||
|
uuids.extend(_r.json()['uuids'])
|
||||||
|
logging.debug("Habitat - Got UUIDs")
|
||||||
|
return
|
||||||
|
except Exception as e:
|
||||||
|
logging.error("Habitat - Unable to fetch UUIDs, retrying in 10 seconds - %s" % str(e))
|
||||||
|
time.sleep(10)
|
||||||
|
_retries = _retries - 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
logging.error("Habitat - Gave up trying to get UUIDs.")
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def initListenerCallsign(callsign):
|
||||||
|
doc = {
|
||||||
|
'type': 'listener_information',
|
||||||
|
'time_created' : ISOStringNow(),
|
||||||
|
'data': {
|
||||||
|
'callsign': callsign
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp = postListenerData(doc)
|
||||||
|
|
||||||
|
if resp is True:
|
||||||
|
logging.debug("Habitat - Listener Callsign Initialized.")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logging.error("Habitat - Unable to initialize callsign.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def uploadListenerPosition(callsign, lat, lon):
|
||||||
|
""" Upload Listener Position """
|
||||||
|
|
||||||
|
doc = {
|
||||||
|
'type': 'listener_telemetry',
|
||||||
|
'time_created': ISOStringNow(),
|
||||||
|
'data': {
|
||||||
|
'callsign': callsign,
|
||||||
|
'chase': True,
|
||||||
|
'latitude': lat,
|
||||||
|
'longitude': lon,
|
||||||
|
'altitude': 0,
|
||||||
|
'speed': 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# post position to habitat
|
||||||
|
resp = postListenerData(doc)
|
||||||
|
if resp is True:
|
||||||
|
logging.debug("Habitat - Listener information uploaded.")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logging.error("Habitat - Unable to upload listener information.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class HabitatChaseUploader(object):
|
||||||
|
''' Upload supplied chase car positions to Habitat on a regular basis '''
|
||||||
|
def __init__(self,
|
||||||
|
update_rate = 30,
|
||||||
|
callsign = "N0CALL",
|
||||||
|
upload_enabled = True):
|
||||||
|
''' Initialise the Habitat Chase uploader, and start the update thread '''
|
||||||
|
|
||||||
|
self.update_rate = update_rate
|
||||||
|
self.callsign = callsign
|
||||||
|
self.callsign_init = False
|
||||||
|
self.upload_enabled = upload_enabled
|
||||||
|
|
||||||
|
self.car_position = None
|
||||||
|
self.car_position_lock = Lock()
|
||||||
|
|
||||||
|
self.uploader_thread_running = True
|
||||||
|
self.uploader_thread = Thread(target=self.upload_thread)
|
||||||
|
self.uploader_thread.start()
|
||||||
|
|
||||||
|
logging.info("Habitat - Chase-Car Position Uploader Started")
|
||||||
|
|
||||||
|
|
||||||
|
def update_position(self, position):
|
||||||
|
''' Update the chase car position state
|
||||||
|
This function accepts and stores a copy of the same dictionary structure produced by both
|
||||||
|
Horus UDP broadcasts, and the serial GPS and GPSD modules
|
||||||
|
'''
|
||||||
|
|
||||||
|
with self.car_position_lock:
|
||||||
|
self.car_position = position.copy()
|
||||||
|
|
||||||
|
|
||||||
|
def upload_thread(self):
|
||||||
|
''' Uploader thread '''
|
||||||
|
while self.uploader_thread_running:
|
||||||
|
|
||||||
|
# Grab a copy of the most recent car position.
|
||||||
|
with self.car_position_lock:
|
||||||
|
if self.car_position != None:
|
||||||
|
_position = self.car_position.copy()
|
||||||
|
else:
|
||||||
|
_position = None
|
||||||
|
|
||||||
|
if self.upload_enabled and _position != None:
|
||||||
|
try:
|
||||||
|
# If the listener callsign has not been initialized, init it.
|
||||||
|
# We only need to do this once per callsign.
|
||||||
|
if self.callsign_init != self.callsign:
|
||||||
|
_resp = initListenerCallsign(self.callsign)
|
||||||
|
if _resp:
|
||||||
|
self.callsign_init = self.callsign
|
||||||
|
|
||||||
|
# Upload the listener position.
|
||||||
|
uploadListenerPosition(self.callsign, _position['latitude'], _position['longitude'])
|
||||||
|
except Exception as e:
|
||||||
|
logging.error("Habitat - Error uploading chase-car position - %s" % str(e))
|
||||||
|
|
||||||
|
# Wait for next update.
|
||||||
|
_i = 0
|
||||||
|
while (_i < self.update_rate) and self.uploader_thread_running:
|
||||||
|
time.sleep(1)
|
||||||
|
_i += 1
|
||||||
|
|
||||||
|
def set_update_rate(self, rate):
|
||||||
|
''' Set the update rate '''
|
||||||
|
self.update_rate = int(rate)
|
||||||
|
|
||||||
|
|
||||||
|
def set_callsign(self, call):
|
||||||
|
''' Set the callsign '''
|
||||||
|
self.callsign = call
|
||||||
|
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
self.uploader_thread_running = False
|
||||||
|
try:
|
||||||
|
self.uploader_thread.join()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
logging.info("Habitat - Chase-Car Position Uploader Closed")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -124,4 +124,21 @@ tile_server_enabled = False
|
||||||
tile_server_path = /home/pi/Maps/
|
tile_server_path = /home/pi/Maps/
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Habitat Chase-Car Position Upload
|
||||||
|
# If you want, this application can upload your chase-car position to the Habhub tracker,
|
||||||
|
# for those follwing along at home.
|
||||||
|
# The settings below can be modified from the web interface, but they will default to what is set below on startup.
|
||||||
|
#
|
||||||
|
[habitat]
|
||||||
|
# Enable uploading of chase-car position to Habitat (True / False)
|
||||||
|
habitat_upload_enabled = False
|
||||||
|
|
||||||
|
# Callsign to use when uploading. Note that _chase is automatically appended to this callsign
|
||||||
|
# i.e. N0CALL will show up as N0CALL_chase on tracker.habhub.org
|
||||||
|
habitat_call = 'N0CALL'
|
||||||
|
|
||||||
|
# Attempt to upload position to habitat every x seconds.
|
||||||
|
habitat_update_rate = 30
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,7 @@ from chasemapper.gps import SerialGPS, GPSDGPS
|
||||||
from chasemapper.atmosphere import time_to_landing
|
from chasemapper.atmosphere import time_to_landing
|
||||||
from chasemapper.listeners import OziListener, UDPListener
|
from chasemapper.listeners import OziListener, UDPListener
|
||||||
from chasemapper.predictor import predictor_spawn_download, model_download_running
|
from chasemapper.predictor import predictor_spawn_download, model_download_running
|
||||||
|
from chasemapper.habitat import HabitatChaseUploader
|
||||||
|
|
||||||
|
|
||||||
# Define Flask Application, and allow automatic reloading of templates for dev work
|
# Define Flask Application, and allow automatic reloading of templates for dev work
|
||||||
|
@ -60,6 +61,9 @@ current_payload_tracks = {} # Store of payload Track objects which are used to c
|
||||||
# Chase car position
|
# Chase car position
|
||||||
car_track = GenericTrack()
|
car_track = GenericTrack()
|
||||||
|
|
||||||
|
# Habitat Chase-Car uploader object
|
||||||
|
habitat_uploader = None
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Flask Routes
|
# Flask Routes
|
||||||
|
@ -101,7 +105,7 @@ def flask_emit_event(event_name="none", data={}):
|
||||||
|
|
||||||
@socketio.on('client_settings_update', namespace='/chasemapper')
|
@socketio.on('client_settings_update', namespace='/chasemapper')
|
||||||
def client_settings_update(data):
|
def client_settings_update(data):
|
||||||
global chasemapper_config
|
global chasemapper_config, habitat_uploader
|
||||||
|
|
||||||
_predictor_change = "none"
|
_predictor_change = "none"
|
||||||
if (chasemapper_config['pred_enabled'] == False) and (data['pred_enabled'] == True):
|
if (chasemapper_config['pred_enabled'] == False) and (data['pred_enabled'] == True):
|
||||||
|
@ -109,6 +113,13 @@ def client_settings_update(data):
|
||||||
elif (chasemapper_config['pred_enabled'] == True) and (data['pred_enabled'] == False):
|
elif (chasemapper_config['pred_enabled'] == True) and (data['pred_enabled'] == False):
|
||||||
_predictor_change = "stop"
|
_predictor_change = "stop"
|
||||||
|
|
||||||
|
|
||||||
|
_habitat_change = "none"
|
||||||
|
if (chasemapper_config['habitat_upload_enabled'] == False) and (data['habitat_upload_enabled'] == True):
|
||||||
|
_habitat_change = "start"
|
||||||
|
elif (chasemapper_config['habitat_upload_enabled'] == True) and (data['habitat_upload_enabled'] == False):
|
||||||
|
_habitat_change = "stop"
|
||||||
|
|
||||||
# Overwrite local config data with data from the client.
|
# Overwrite local config data with data from the client.
|
||||||
chasemapper_config = data
|
chasemapper_config = data
|
||||||
|
|
||||||
|
@ -125,6 +136,21 @@ def client_settings_update(data):
|
||||||
|
|
||||||
predictor = None
|
predictor = None
|
||||||
|
|
||||||
|
# Start or Stop the Habitat Chase-Car Uploader.
|
||||||
|
if _habitat_change == "start":
|
||||||
|
if habitat_uploader == None:
|
||||||
|
habitat_uploader = HabitatChaseUploader(update_rate = chasemapper_config['habitat_update_rate'],
|
||||||
|
callsign=chasemapper_config['habitat_call'])
|
||||||
|
|
||||||
|
elif _habitat_change == "stop":
|
||||||
|
habitat_uploader.close()
|
||||||
|
habitat_uploader = None
|
||||||
|
|
||||||
|
# Update the habitat uploader with a new update rate, if one has changed.
|
||||||
|
if habitat_uploader != None:
|
||||||
|
habitat_uploader.set_update_rate(chasemapper_config['habitat_update_rate'])
|
||||||
|
habitat_uploader.set_callsign(chasemapper_config['habitat_call'])
|
||||||
|
|
||||||
|
|
||||||
# Push settings back out to all clients.
|
# Push settings back out to all clients.
|
||||||
flask_emit_event('server_settings_update', chasemapper_config)
|
flask_emit_event('server_settings_update', chasemapper_config)
|
||||||
|
@ -511,7 +537,7 @@ def udp_listener_car_callback(data):
|
||||||
''' Handle car position data '''
|
''' Handle car position data '''
|
||||||
# TODO: Make a generic car position function, and have this function pass data into it
|
# TODO: Make a generic car position function, and have this function pass data into it
|
||||||
# so we can add support for other chase car position inputs.
|
# so we can add support for other chase car position inputs.
|
||||||
global car_track
|
global car_track, habitat_uploader
|
||||||
_lat = data['latitude']
|
_lat = data['latitude']
|
||||||
_lon = data['longitude']
|
_lon = data['longitude']
|
||||||
_alt = data['altitude']
|
_alt = data['altitude']
|
||||||
|
@ -537,6 +563,10 @@ def udp_listener_car_callback(data):
|
||||||
# Push the new car position to the web client
|
# Push the new car position to the web client
|
||||||
flask_emit_event('telemetry_event', {'callsign': 'CAR', 'position':[_lat,_lon,_alt], 'vel_v':0.0, 'heading': _heading, 'speed':_speed})
|
flask_emit_event('telemetry_event', {'callsign': 'CAR', 'position':[_lat,_lon,_alt], 'vel_v':0.0, 'heading': _heading, 'speed':_speed})
|
||||||
|
|
||||||
|
# Update the Habitat Uploader, if one exists.
|
||||||
|
if habitat_uploader != None:
|
||||||
|
habitat_uploader.update_position(data)
|
||||||
|
|
||||||
|
|
||||||
# Add other listeners here...
|
# Add other listeners here...
|
||||||
|
|
||||||
|
@ -727,10 +757,15 @@ if __name__ == "__main__":
|
||||||
# Start listeners using the default profile selection.
|
# Start listeners using the default profile selection.
|
||||||
start_listeners(chasemapper_config['profiles'][chasemapper_config['selected_profile']])
|
start_listeners(chasemapper_config['profiles'][chasemapper_config['selected_profile']])
|
||||||
|
|
||||||
|
# Start up the predictor, if enabled.
|
||||||
if chasemapper_config['pred_enabled']:
|
if chasemapper_config['pred_enabled']:
|
||||||
initPredictor()
|
initPredictor()
|
||||||
|
|
||||||
|
# Start up the Habitat Chase-Car Uploader, if enabled
|
||||||
|
if chasemapper_config['habitat_upload_enabled']:
|
||||||
|
habitat_uploader = HabitatChaseUploader(update_rate = chasemapper_config['habitat_update_rate'],
|
||||||
|
callsign=chasemapper_config['habitat_call'])
|
||||||
|
|
||||||
# Start up the data age monitor thread.
|
# Start up the data age monitor thread.
|
||||||
_data_age_monitor = Thread(target=check_data_age)
|
_data_age_monitor = Thread(target=check_data_age)
|
||||||
_data_age_monitor.start()
|
_data_age_monitor.start()
|
||||||
|
@ -742,6 +777,9 @@ if __name__ == "__main__":
|
||||||
predictor_thread_running = False
|
predictor_thread_running = False
|
||||||
data_monitor_thread_running = False
|
data_monitor_thread_running = False
|
||||||
|
|
||||||
|
if habitat_uploader != None:
|
||||||
|
habitat_uploader.close()
|
||||||
|
|
||||||
# Attempt to close the running listeners.
|
# Attempt to close the running listeners.
|
||||||
for _thread in data_listeners:
|
for _thread in data_listeners:
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -108,7 +108,10 @@
|
||||||
$('#burstAlt').val(chase_config.pred_burst.toFixed(0));
|
$('#burstAlt').val(chase_config.pred_burst.toFixed(0));
|
||||||
$('#descentRate').val(chase_config.pred_desc_rate.toFixed(1));
|
$('#descentRate').val(chase_config.pred_desc_rate.toFixed(1));
|
||||||
$('#predUpdateRate').val(chase_config.pred_update_rate.toFixed(0));
|
$('#predUpdateRate').val(chase_config.pred_update_rate.toFixed(0));
|
||||||
|
$('#habitatUpdateRate').val(chase_config.habitat_update_rate.toFixed(0));
|
||||||
$("#predictorEnabled").prop('checked', chase_config.pred_enabled);
|
$("#predictorEnabled").prop('checked', chase_config.pred_enabled);
|
||||||
|
$("#habitatUploadEnabled").prop('checked', chase_config.habitat_upload_enabled);
|
||||||
|
$("#habitatCall").val(chase_config.habitat_call);
|
||||||
$("#abortPredictionEnabled").prop('checked', chase_config.show_abort);
|
$("#abortPredictionEnabled").prop('checked', chase_config.show_abort);
|
||||||
|
|
||||||
// Clear and populate the profile selection.
|
// Clear and populate the profile selection.
|
||||||
|
@ -143,6 +146,8 @@
|
||||||
updateSettings = function(){
|
updateSettings = function(){
|
||||||
chase_config.pred_enabled = document.getElementById("predictorEnabled").checked;
|
chase_config.pred_enabled = document.getElementById("predictorEnabled").checked;
|
||||||
chase_config.show_abort = document.getElementById("abortPredictionEnabled").checked;
|
chase_config.show_abort = document.getElementById("abortPredictionEnabled").checked;
|
||||||
|
chase_config.habitat_upload_enabled = document.getElementById("habitatUploadEnabled").checked;
|
||||||
|
chase_config.habitat_call = $('#habitatCall').val()
|
||||||
|
|
||||||
// Attempt to parse the text field values.
|
// Attempt to parse the text field values.
|
||||||
var _burst_alt = parseFloat($('#burstAlt').val());
|
var _burst_alt = parseFloat($('#burstAlt').val());
|
||||||
|
@ -153,12 +158,16 @@
|
||||||
if (isNaN(_desc_rate) == false){
|
if (isNaN(_desc_rate) == false){
|
||||||
chase_config.pred_desc_rate = _desc_rate
|
chase_config.pred_desc_rate = _desc_rate
|
||||||
}
|
}
|
||||||
|
|
||||||
var _update_rate = parseInt($('#predUpdateRate').val());
|
var _update_rate = parseInt($('#predUpdateRate').val());
|
||||||
if (isNaN(_update_rate) == false){
|
if (isNaN(_update_rate) == false){
|
||||||
chase_config.pred_update_rate = _update_rate
|
chase_config.pred_update_rate = _update_rate
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var _habitat_update_rate = parseInt($('#habitatUpdateRate').val());
|
||||||
|
if (isNaN(_habitat_update_rate) == false){
|
||||||
|
chase_config.habitat_update_rate = _habitat_update_rate
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
socket.emit('client_settings_update', chase_config);
|
socket.emit('client_settings_update', chase_config);
|
||||||
};
|
};
|
||||||
|
@ -176,6 +185,12 @@
|
||||||
$("#predUpdateRate").change(function(){
|
$("#predUpdateRate").change(function(){
|
||||||
updateSettings();
|
updateSettings();
|
||||||
});
|
});
|
||||||
|
$("#habitatUpdateRate").change(function(){
|
||||||
|
updateSettings();
|
||||||
|
});
|
||||||
|
$("#habitatCall").change(function(){
|
||||||
|
updateSettings();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
$("#profileSelect").change(function(){
|
$("#profileSelect").change(function(){
|
||||||
|
@ -869,6 +884,17 @@
|
||||||
<b>Update Rate</b><input type="text" class="paramEntry" id="predUpdateRate"><br/>
|
<b>Update Rate</b><input type="text" class="paramEntry" id="predUpdateRate"><br/>
|
||||||
</div>
|
</div>
|
||||||
</hr>
|
</hr>
|
||||||
|
<h3>Habitat</h3>
|
||||||
|
<div class="paramRow">
|
||||||
|
<b>Enable Chase-Car Position Upload</b> <input type="checkbox" class="paramSelector" id="habitatUploadEnabled" onclick='updateSettings();'>
|
||||||
|
</div>
|
||||||
|
<div class="paramRow">
|
||||||
|
<b>Habitat Call:</b><input type="text" class="paramEntry" id="habitatCall"><br/>
|
||||||
|
</div>
|
||||||
|
<div class="paramRow">
|
||||||
|
<b>Update Rate (seconds):</b><input type="text" class="paramEntry" id="habitatUpdateRate"><br/>
|
||||||
|
</div>
|
||||||
|
</hr>
|
||||||
<h3>Other</h3>
|
<h3>Other</h3>
|
||||||
<div class="paramRow">
|
<div class="paramRow">
|
||||||
<button type="button" class="paramSelector" id="clearPayloadData">Clear Payload Data</button></br>
|
<button type="button" class="paramSelector" id="clearPayloadData">Clear Payload Data</button></br>
|
||||||
|
|
Ładowanie…
Reference in New Issue