mirror of
https://github.com/DJ2LS/FreeDATA
synced 2024-05-14 08:04:33 +00:00
code cleanup
This commit is contained in:
parent
129e0c0645
commit
1969349b50
6 changed files with 158 additions and 107 deletions
41
arq.py
41
arq.py
|
@ -13,36 +13,18 @@ from random import randrange
|
|||
|
||||
import static
|
||||
import modem
|
||||
modem = modem.RF()
|
||||
import helpers
|
||||
|
||||
#import tnc
|
||||
|
||||
|
||||
|
||||
|
||||
modem = modem.RF()
|
||||
|
||||
static.ARQ_PAYLOAD_PER_FRAME = static.FREEDV_DATA_PAYLOAD_PER_FRAME - 3 #6?!
|
||||
static.ARQ_ACK_PAYLOAD_PER_FRAME = 14 - 2#
|
||||
|
||||
|
||||
def data_received(data_in):
|
||||
|
||||
|
||||
# arqframe = frame_type + \ # 1 [:1] # frame type and current number of arq frame of (current) burst
|
||||
# bytes([static.ARQ_TX_N_FRAMES_PER_BURST]) + \ # 1 [1:2] # total number of arq frames per (current) burst
|
||||
# static.ARQ_N_CURRENT_ARQ_FRAME + \ # 2 [2:4] # current arq frame number
|
||||
# static.ARQ_N_TOTAL_ARQ_FRAMES + \ # 2 [4:6] # total number arq frames
|
||||
# static.ARQ_BURST_PAYLOAD_CRC + \ # 2 [6:8] # arq crc
|
||||
# payload_data # N [8:N] # payload data
|
||||
|
||||
|
||||
|
||||
static.ARQ_N_FRAME = int.from_bytes(bytes(data_in[:1]), "big") - 10 #get number of burst frame
|
||||
static.ARQ_N_RX_FRAMES_PER_BURSTS = int.from_bytes(bytes(data_in[1:2]), "big") #get number of bursts from received frame
|
||||
static.ARQ_RX_N_CURRENT_ARQ_FRAME = int.from_bytes(bytes(data_in[2:4]), "big") #get current number of total frames
|
||||
static.ARQ_N_ARQ_FRAMES_PER_DATA_FRAME = int.from_bytes(bytes(data_in[4:6]), "big") # get get total number of frames
|
||||
static.ARQ_BURST_PAYLOAD_CRC = data_in[6:8]
|
||||
|
||||
|
||||
logging.debug("----------------------------------------------------------------")
|
||||
|
@ -50,7 +32,6 @@ def data_received(data_in):
|
|||
logging.debug("ARQ_N_RX_FRAMES_PER_BURSTS: " + str(static.ARQ_N_RX_FRAMES_PER_BURSTS))
|
||||
logging.debug("ARQ_RX_N_CURRENT_ARQ_FRAME: " + str(static.ARQ_RX_N_CURRENT_ARQ_FRAME))
|
||||
logging.debug("ARQ_N_ARQ_FRAMES_PER_DATA_FRAME: " + str(static.ARQ_N_ARQ_FRAMES_PER_DATA_FRAME))
|
||||
logging.debug("static.ARQ_BURST_PAYLOAD_CRC: " + str(static.ARQ_BURST_PAYLOAD_CRC))
|
||||
logging.debug("----------------------------------------------------------------")
|
||||
|
||||
|
||||
|
@ -63,7 +44,6 @@ def data_received(data_in):
|
|||
|
||||
#allocate ARQ_RX_FRAME_BUFFER as a list with "None" if not already done. This should be done only once per burst!
|
||||
# here we will save the N frame of a data frame to N list position so we can explicit search for it
|
||||
|
||||
# delete frame buffer if first frame to make sure the buffer is cleared and no junks of a old frame is remaining
|
||||
if static.ARQ_RX_N_CURRENT_ARQ_FRAME == 1:
|
||||
static.ARQ_RX_FRAME_BUFFER = []
|
||||
|
@ -93,11 +73,8 @@ def data_received(data_in):
|
|||
|
||||
static.ARQ_RX_BURST_BUFFER[static.ARQ_N_FRAME] = bytes(data_in)
|
||||
|
||||
#for i in range(len(static.ARQ_RX_BURST_BUFFER)):
|
||||
# print(static.ARQ_RX_BURST_BUFFER[i])
|
||||
|
||||
# - ------------------------- ARQ BURST CHECKER
|
||||
|
||||
# run only if we recieved all ARQ FRAMES per ARQ BURST
|
||||
if static.ARQ_RX_BURST_BUFFER.count(None) == 1: #count nones
|
||||
logging.info("ARQ | TX | BURST ACK")
|
||||
|
@ -116,7 +93,6 @@ def data_received(data_in):
|
|||
elif static.ARQ_N_FRAME == static.ARQ_N_RX_FRAMES_PER_BURSTS and static.ARQ_RX_BURST_BUFFER.count(None) != 1:
|
||||
|
||||
# --------------- CHECK WHICH BURST FRAMES WE ARE MISSING -------------------------------------------
|
||||
|
||||
missing_frames = b''
|
||||
for burstnumber in range(1,len(static.ARQ_RX_BURST_BUFFER)):
|
||||
|
||||
|
@ -140,9 +116,7 @@ def data_received(data_in):
|
|||
modem.transmit_arq_ack(rpt_frame)
|
||||
|
||||
|
||||
|
||||
# ---------------------------- FRAME MACHINE
|
||||
|
||||
# --------------- IF LIST NOT CONTAINS "None" stick everything together
|
||||
complete_data_frame = bytearray()
|
||||
#print("static.ARQ_RX_FRAME_BUFFER.count(None)" + str(static.ARQ_RX_FRAME_BUFFER.count(None)))
|
||||
|
@ -203,7 +177,6 @@ def data_received(data_in):
|
|||
|
||||
#print("----------------------------------------------------------------")
|
||||
#print(static.RX_BUFFER[-1])
|
||||
#tnc.request.sendall(bytes(static.RX_BUFFER[-1]))
|
||||
#print("----------------------------------------------------------------")
|
||||
|
||||
else:
|
||||
|
@ -215,8 +188,8 @@ def data_received(data_in):
|
|||
|
||||
def transmit(data_out):
|
||||
|
||||
static.ARQ_PAYLOAD_PER_FRAME = static.FREEDV_DATA_PAYLOAD_PER_FRAME - 8 #3 ohne ARQ_TX_N_FRAMES_PER_BURST
|
||||
frame_header_length = 4
|
||||
static.ARQ_PAYLOAD_PER_FRAME = static.FREEDV_DATA_PAYLOAD_PER_FRAME - 8
|
||||
frame_header_length = 6 #4
|
||||
|
||||
n_arq_frames_per_data_frame = (len(data_out)+frame_header_length) // static.ARQ_PAYLOAD_PER_FRAME + ((len(data_out)+frame_header_length) % static.ARQ_PAYLOAD_PER_FRAME > 0)
|
||||
|
||||
|
@ -368,7 +341,6 @@ def transmit(data_out):
|
|||
logging.debug("static.TX_BUFFER_SIZE " + str(static.TX_BUFFER_SIZE))
|
||||
logging.debug("static.TX_N_RETRIES " + str(static.TX_N_RETRIES))
|
||||
logging.debug("static.TX_N_MAX_RETRIES " + str(static.TX_N_MAX_RETRIES))
|
||||
|
||||
logging.debug("static.ARQ_STATE " + str(static.ARQ_STATE))
|
||||
logging.debug("static.ARQ_FRAME_ACK_RECEIVED " + str(static.ARQ_FRAME_ACK_RECEIVED))
|
||||
logging.debug("static.ARQ_RX_FRAME_TIMEOUT " + str(static.ARQ_RX_FRAME_TIMEOUT))
|
||||
|
@ -419,12 +391,8 @@ def transmit(data_out):
|
|||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# BURST MACHINE TO DEFINE N BURSTS PER FRAME ---> LATER WE CAN USE CHANNEL MESSUREMENT TO SET FRAMES PER BURST
|
||||
def get_n_frames_per_burst():
|
||||
|
||||
#n_frames_per_burst = randrange(1,10)
|
||||
n_frames_per_burst = 2
|
||||
return n_frames_per_burst
|
||||
|
@ -432,19 +400,16 @@ def get_n_frames_per_burst():
|
|||
|
||||
|
||||
def burst_ack_received():
|
||||
|
||||
static.ARQ_ACK_RECEIVED = True #Force data loops of TNC to stop and continue with next frame
|
||||
|
||||
|
||||
|
||||
def frame_ack_received():
|
||||
|
||||
static.ARQ_FRAME_ACK_RECEIVED = True #Force data loops of TNC to stop and continue with next frame
|
||||
|
||||
|
||||
|
||||
def burst_rpt_received(data_in):
|
||||
|
||||
static.ARQ_RPT_RECEIVED = True
|
||||
static.ARQ_RPT_FRAMES = []
|
||||
|
||||
|
|
34
helpers.py
34
helpers.py
|
@ -9,8 +9,8 @@ Created on Fri Dec 25 21:25:14 2020
|
|||
import time
|
||||
import threading
|
||||
import logging
|
||||
from colorlog import ColoredFormatter
|
||||
import crcengine
|
||||
import pyaudio
|
||||
|
||||
import static
|
||||
|
||||
|
@ -63,3 +63,35 @@ def arq_reset_frame_machine():
|
|||
static.ARQ_TX_N_FRAMES_PER_BURST = 0
|
||||
|
||||
|
||||
def setup_logging():
|
||||
|
||||
logging.basicConfig(format='%(asctime)s.%(msecs)03d %(levelname)s:\t%(message)s', datefmt='%H:%M:%S', level=logging.INFO)
|
||||
|
||||
logging.addLevelName( logging.DEBUG, "\033[1;36m%s\033[1;0m" % logging.getLevelName(logging.DEBUG))
|
||||
logging.addLevelName( logging.INFO, "\033[1;37m%s\033[1;0m" % logging.getLevelName(logging.INFO))
|
||||
logging.addLevelName( logging.WARNING, "\033[1;33m%s\033[1;0m" % logging.getLevelName(logging.WARNING))
|
||||
logging.addLevelName( logging.ERROR, "\033[1;31m%s\033[1;0m" % "FAILED")
|
||||
#logging.addLevelName( logging.ERROR, "\033[1;31m%s\033[1;0m" % logging.getLevelName(logging.ERROR))
|
||||
logging.addLevelName( logging.CRITICAL, "\033[1;41m%s\033[1;0m" % logging.getLevelName(logging.CRITICAL))
|
||||
|
||||
logging.addLevelName( 25, "\033[1;32m%s\033[1;0m" % "SUCCESS")
|
||||
logging.addLevelName( 24, "\033[1;34m%s\033[1;0m" % "DATA")
|
||||
|
||||
|
||||
# https://stackoverflow.com/questions/384076/how-can-i-color-python-logging-output
|
||||
#'DEBUG' : 37, # white
|
||||
#'INFO' : 36, # cyan
|
||||
#'WARNING' : 33, # yellow
|
||||
#'ERROR' : 31, # red
|
||||
#'CRITICAL': 41, # white on red bg
|
||||
|
||||
|
||||
|
||||
def list_audio_devices():
|
||||
p = pyaudio.PyAudio()
|
||||
devices = []
|
||||
for x in range(0, p.get_device_count()):
|
||||
devices.append(f"{x} - {p.get_device_info_by_index(x)['name']}")
|
||||
|
||||
for line in devices:
|
||||
print(line)
|
||||
|
|
48
main.py
48
main.py
|
@ -11,24 +11,18 @@ import socketserver
|
|||
import argparse
|
||||
import logging
|
||||
import threading
|
||||
import pyaudio
|
||||
|
||||
#import tnc
|
||||
import static
|
||||
import helpers
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def start_cmd_socket():
|
||||
|
||||
try:
|
||||
logging.info("SRV | STARTING TCP/IP CMD ON PORT: " + str(static.PORT))
|
||||
socketserver.TCPServer.allow_reuse_address = True #https://stackoverflow.com/a/16641793
|
||||
cmdserver = socketserver.TCPServer((static.HOST, static.PORT), tnc.CMDTCPRequestHandler)
|
||||
cmdserver = socketserver.TCPServer((static.HOST, static.PORT), sock.CMDTCPRequestHandler)
|
||||
cmdserver.serve_forever()
|
||||
|
||||
finally:
|
||||
|
@ -40,7 +34,7 @@ def start_data_socket():
|
|||
try:
|
||||
logging.info("SRV | STARTING TCP/IP DATA ON PORT: " + str(static.PORT + 1))
|
||||
socketserver.TCPServer.allow_reuse_address = True #https://stackoverflow.com/a/16641793
|
||||
dataserver = socketserver.TCPServer((static.HOST, static.PORT + 1), tnc.DATATCPRequestHandler)
|
||||
dataserver = socketserver.TCPServer((static.HOST, static.PORT + 1), sock.DATATCPRequestHandler)
|
||||
dataserver.serve_forever()
|
||||
|
||||
finally:
|
||||
|
@ -49,13 +43,10 @@ def start_data_socket():
|
|||
|
||||
|
||||
|
||||
p = pyaudio.PyAudio()
|
||||
devices = []
|
||||
for x in range(0, p.get_device_count()):
|
||||
devices.append(f"{x} - {p.get_device_info_by_index(x)['name']}")
|
||||
|
||||
for line in devices:
|
||||
print(line)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -63,6 +54,14 @@ for line in devices:
|
|||
|
||||
if __name__ == '__main__':
|
||||
|
||||
# config logging
|
||||
helpers.setup_logging()
|
||||
|
||||
# list audio devices
|
||||
helpers.list_audio_devices()
|
||||
|
||||
|
||||
|
||||
static.MYCALLSIGN = b'DJ2LS'
|
||||
static.MYCALLSIGN_CRC8 = helpers.get_crc_8(static.MYCALLSIGN)
|
||||
|
||||
|
@ -93,28 +92,9 @@ if __name__ == '__main__':
|
|||
static.AUDIO_OUTPUT_DEVICE = args.audio_output_device
|
||||
static.PORT = args.socket_port
|
||||
|
||||
import tnc # we need to wait until we got all parameters from argparse
|
||||
|
||||
#-------------------------------------------- DEFINE LOGGING
|
||||
logging.basicConfig(format='%(asctime)s.%(msecs)03d %(levelname)s:\t%(message)s', datefmt='%H:%M:%S', level=logging.INFO)
|
||||
|
||||
logging.addLevelName( logging.DEBUG, "\033[1;36m%s\033[1;0m" % logging.getLevelName(logging.DEBUG))
|
||||
logging.addLevelName( logging.INFO, "\033[1;37m%s\033[1;0m" % logging.getLevelName(logging.INFO))
|
||||
logging.addLevelName( logging.WARNING, "\033[1;33m%s\033[1;0m" % logging.getLevelName(logging.WARNING))
|
||||
logging.addLevelName( logging.ERROR, "\033[1;31m%s\033[1;0m" % "FAILED")
|
||||
#logging.addLevelName( logging.ERROR, "\033[1;31m%s\033[1;0m" % logging.getLevelName(logging.ERROR))
|
||||
logging.addLevelName( logging.CRITICAL, "\033[1;41m%s\033[1;0m" % logging.getLevelName(logging.CRITICAL))
|
||||
|
||||
logging.addLevelName( 25, "\033[1;32m%s\033[1;0m" % "SUCCESS")
|
||||
logging.addLevelName( 24, "\033[1;34m%s\033[1;0m" % "DATA")
|
||||
import sock # we need to wait until we got all parameters from argparse
|
||||
|
||||
|
||||
# https://stackoverflow.com/questions/384076/how-can-i-color-python-logging-output
|
||||
#'DEBUG' : 37, # white
|
||||
#'INFO' : 36, # cyan
|
||||
#'WARNING' : 33, # yellow
|
||||
#'ERROR' : 31, # red
|
||||
#'CRITICAL': 41, # white on red bg
|
||||
|
||||
|
||||
|
||||
|
|
5
modem.py
5
modem.py
|
@ -10,7 +10,8 @@ import ctypes
|
|||
from ctypes import *
|
||||
import pathlib
|
||||
import pyaudio
|
||||
import sys
|
||||
import audioop
|
||||
#import sys
|
||||
import logging
|
||||
import time
|
||||
import threading
|
||||
|
@ -19,7 +20,7 @@ import helpers
|
|||
import static
|
||||
import arq
|
||||
|
||||
import audioop
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
84
sock.py
Normal file
84
sock.py
Normal file
|
@ -0,0 +1,84 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Created on Fri Dec 25 21:25:14 2020
|
||||
|
||||
@author: DJ2LS
|
||||
"""
|
||||
|
||||
import socketserver
|
||||
import threading
|
||||
import logging
|
||||
|
||||
|
||||
import static
|
||||
import arq
|
||||
|
||||
class DATATCPRequestHandler(socketserver.BaseRequestHandler):
|
||||
|
||||
def handle(self):
|
||||
|
||||
self.data = bytes()
|
||||
while True:
|
||||
chunk = self.request.recv(8192)#.strip()
|
||||
self.data += chunk
|
||||
if chunk.endswith(b'\n'):
|
||||
break
|
||||
|
||||
|
||||
|
||||
class CMDTCPRequestHandler(socketserver.BaseRequestHandler):
|
||||
|
||||
def handle(self):
|
||||
|
||||
self.data = bytes()
|
||||
while True:
|
||||
chunk = self.request.recv(8192)#.strip()
|
||||
self.data += chunk
|
||||
if chunk.endswith(b'\n'):
|
||||
break
|
||||
|
||||
|
||||
|
||||
# self.request is the TCP socket connected to the client
|
||||
#self.data = self.request.recv(1024).strip()
|
||||
### self.data = self.request.recv(1000000).strip()
|
||||
|
||||
# interrupt listening loop "while true" by setting MODEM_RECEIVE to False
|
||||
#if len(self.data) > 0:
|
||||
# static.MODEM_RECEIVE = False
|
||||
|
||||
|
||||
####print("{} wrote:".format(self.client_address[0]))
|
||||
####print(self.data)
|
||||
|
||||
# just send back the same data, but upper-cased
|
||||
#####self.request.sendall(self.data.upper())
|
||||
|
||||
#if self.data == b'TEST':
|
||||
#logging.info("DER TEST KLAPPT! HIER KOMMT DER COMMAND PARSER HIN!")
|
||||
if self.data.startswith(b'SHOWBUFFERSIZE'):
|
||||
self.request.sendall(bytes(static.RX_BUFFER[-1]))
|
||||
print(static.RX_BUFFER_SIZE)
|
||||
|
||||
# BROADCAST PARSER -----------------------------------------------------------
|
||||
|
||||
if self.data.startswith(b'BC:'):
|
||||
#import modem
|
||||
#modem = modem.RF()
|
||||
|
||||
data = self.data.split(b'BC:')
|
||||
#modem.Transmit(data[1])
|
||||
|
||||
|
||||
|
||||
# SEND AN ARQ FRAME -----------------------------------------------------------
|
||||
|
||||
if self.data.startswith(b'ARQ:'):
|
||||
|
||||
data = self.data.split(b'ARQ:')
|
||||
data_out = data[1]
|
||||
|
||||
TRANSMIT_ARQ = threading.Thread(target=arq.transmit, args=[data_out], name="TRANSMIT_ARQ")
|
||||
TRANSMIT_ARQ.start()
|
||||
|
43
static.py
43
static.py
|
@ -5,23 +5,21 @@ Created on Wed Dec 23 11:13:57 2020
|
|||
|
||||
@author: DJ2LS
|
||||
"""
|
||||
# ADDITION MESSUREMENT:
|
||||
#AUDIO TIME: 7.451462268829346 #12 # 1 FRAME + PREAMBLE
|
||||
#MODULATION TIME: 0.002051115036010742 #12 # 1 FRAME + PREAMBLE
|
||||
|
||||
#MODULATION TIME: 0.004580974578857422 #12 # 2 FRAME + PREAMBLE
|
||||
#AUDIO TIME: 14.750595331192017 #12 # 2 FRAME + PREAMBLE
|
||||
|
||||
|
||||
# Operator Defaults
|
||||
MYCALLSIGN = b''
|
||||
MYCALLSIGN_CRC8 = b''
|
||||
|
||||
DXCALLSIGN = b''
|
||||
DXCALLSIGN_CRC8 = b''
|
||||
|
||||
MYGRID = b''
|
||||
#---------------------------------
|
||||
|
||||
|
||||
|
||||
# Server Defaults
|
||||
HOST = "localhost"
|
||||
PORT = 3000
|
||||
#---------------------------------
|
||||
|
||||
# FreeDV Defaults
|
||||
FREEDV_RECEIVE = True
|
||||
|
@ -33,13 +31,9 @@ FREEDV_DATA_BYTES_PER_FRAME = 0
|
|||
FREEDV_DATA_PAYLOAD_PER_FRAME = 0
|
||||
FREEDV_SIGNALLING_BYTES_PER_FRAME = 0
|
||||
FREEDV_SIGNALLING_PAYLOAD_PER_FRAME = 0
|
||||
#---------------------------------
|
||||
|
||||
# Server Defaults
|
||||
HOST = "localhost"
|
||||
PORT = 3000
|
||||
|
||||
|
||||
#AUdio Defaults
|
||||
#Audio Defaults
|
||||
AUDIO_INPUT_DEVICE = 1
|
||||
AUDIO_OUTPUT_DEVICE = 1
|
||||
#TX_SAMPLE_STATE = None
|
||||
|
@ -50,20 +44,17 @@ AUDIO_OUTPUT_DEVICE = 1
|
|||
MODEM_SAMPLE_RATE = 8000 #8000
|
||||
AUDIO_FRAMES_PER_BUFFER = 2048
|
||||
AUDIO_CHANNELS = 1
|
||||
#---------------------------------
|
||||
|
||||
|
||||
#TNC DEFAULTS
|
||||
# ARQ
|
||||
TX_N_MAX_RETRIES = 3
|
||||
#ARQ DEFAULTS
|
||||
TX_N_MAX_RETRIES = 10
|
||||
TX_N_RETRIES = 0
|
||||
|
||||
|
||||
|
||||
ARQ_TX_N_FRAMES_PER_BURST = 0
|
||||
ARQ_TX_N_BURSTS = 0
|
||||
|
||||
ARQ_PAYLOAD_PER_FRAME = 0
|
||||
ARQ_ACK_WAITING_FOR_ID = 0
|
||||
|
||||
ARQ_RX_BURST_BUFFER = []
|
||||
ARQ_RX_FRAME_BUFFER = []
|
||||
ARQ_RX_FRAME_N_BURSTS = 0
|
||||
|
@ -79,22 +70,20 @@ ARQ_RX_N_CURRENT_ARQ_FRAME = 0
|
|||
##
|
||||
|
||||
ARQ_N_RX_ARQ_FRAMES = 0 # total number of received frames
|
||||
|
||||
ARQ_N_RX_FRAMES_PER_BURSTS = 0 # NUMBER OF FRAMES WE ARE WAITING FOR --> GOT DATA FROM RECEIVED FRAME
|
||||
ARQ_ACK_PAYLOAD_PER_FRAME = 0 # PAYLOAD per ACK frame
|
||||
|
||||
ARQ_ACK_RECEIVED = False # set to 1 if ACK received
|
||||
ARQ_RX_ACK_TIMEOUT = False # set to 1 if timeut reached
|
||||
ARQ_RX_ACK_TIMEOUT_SECONDS = 5.0 #timeout for waiting for ACK frames
|
||||
|
||||
ARQ_RX_ACK_TIMEOUT_SECONDS = 10.0 #timeout for waiting for ACK frames
|
||||
|
||||
ARQ_FRAME_ACK_RECEIVED = False # set to 1 if FRAME ACK received
|
||||
ARQ_RX_FRAME_TIMEOUT = False
|
||||
ARQ_RX_FRAME_TIMEOUT_SECONDS = 5.0
|
||||
ARQ_RX_FRAME_TIMEOUT_SECONDS = 10.0
|
||||
|
||||
|
||||
ARQ_RX_RPT_TIMEOUT = False
|
||||
ARQ_RX_RPT_TIMEOUT_SECONDS = 5.0
|
||||
ARQ_RX_RPT_TIMEOUT_SECONDS = 10.0
|
||||
ARQ_RPT_RECEIVED = False #indicate if RPT frame has been received
|
||||
ARQ_RPT_FRAMES = [] #buffer for frames which are requested to repeat
|
||||
|
||||
|
|
Loading…
Reference in a new issue