mirror of
https://github.com/DJ2LS/FreeDATA
synced 2024-05-14 08:04:33 +00:00
WIP ARQ
This commit is contained in:
parent
ad11332d16
commit
000702740f
92
modem/arq_session_irs.py
Normal file
92
modem/arq_session_irs.py
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
import threading
|
||||||
|
import data_frame_factory
|
||||||
|
import queue
|
||||||
|
|
||||||
|
class ARQSessionIRS():
|
||||||
|
|
||||||
|
STATE_CONN_REQ_RECEIVED = 0
|
||||||
|
STATE_WAITING_DATA = 1
|
||||||
|
STATE_FAILED = 2
|
||||||
|
STATE_ENDED = 10
|
||||||
|
|
||||||
|
RETRIES_CONNECT = 3
|
||||||
|
RETRIES_TRANSFER = 3
|
||||||
|
|
||||||
|
TIMEOUT_DATA = 2
|
||||||
|
|
||||||
|
def __init__(self, config: dict, tx_frame_queue: queue.Queue, dxcall: str, session_id: int):
|
||||||
|
self.config = config
|
||||||
|
self.tx_frame_queue = tx_frame_queue
|
||||||
|
self.dxcall = dxcall
|
||||||
|
self.session_id = session_id
|
||||||
|
|
||||||
|
self.received_data = b''
|
||||||
|
|
||||||
|
self.state = self.STATE_CONN_REQ_RECEIVED
|
||||||
|
|
||||||
|
self.event_data_received = threading.Event()
|
||||||
|
|
||||||
|
self.frame_factory = data_frame_factory.DataFrameFactory(self.config)
|
||||||
|
|
||||||
|
def generate_id(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def log(self, message):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def set_state(self, state):
|
||||||
|
self.log(f"ARQ Session {self.id} state {self.state}")
|
||||||
|
self.state = state
|
||||||
|
|
||||||
|
def transmit_frame(self, frame: bytearray):
|
||||||
|
self.tx_frame_queue.put(frame)
|
||||||
|
|
||||||
|
def set_modem_decode_modes(self, modes):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def runner(self, request):
|
||||||
|
isWideband = True
|
||||||
|
speed = 1
|
||||||
|
version = 1
|
||||||
|
|
||||||
|
ack_frame = self.frame_factory.build_arq_connect_ack(isWideband, self.session_id, speed, version)
|
||||||
|
self.transmit_frame(ack_frame)
|
||||||
|
|
||||||
|
self.set_modem_decode_modes(None)
|
||||||
|
|
||||||
|
self.state = self.STATE_WAITING_DATA
|
||||||
|
while self.state == self.STATE_WAITING_DATA:
|
||||||
|
if not self.event_data_received.wait(self.TIMEOUT_DATA):
|
||||||
|
self.log("Timeout waiting for data")
|
||||||
|
self.state = self.STATE_FAILED
|
||||||
|
return
|
||||||
|
|
||||||
|
self.log("Finished ARQ IRS session")
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
self.thread = threading.Thread(target=self.runner, name=f"ARQ IRS Session {self.id}", daemon=True)
|
||||||
|
self.thread.run()
|
||||||
|
|
||||||
|
def on_data_received(self, data_frame):
|
||||||
|
if self.state != self.STATE_WAITING_DATA:
|
||||||
|
raise RuntimeError(f"ARQ Session: Received data while in state {self.state}")
|
||||||
|
|
||||||
|
self.event_data_received.set()
|
||||||
|
|
||||||
|
|
||||||
|
def on_transfer_ack_received(self, ack):
|
||||||
|
self.event_transfer_ack_received.set()
|
||||||
|
self.speed_level = ack['speed_level']
|
||||||
|
|
||||||
|
def on_transfer_nack_received(self, nack):
|
||||||
|
self.speed_level = nack['speed_level']
|
||||||
|
|
||||||
|
def on_disconnect_received(self):
|
||||||
|
self.abort()
|
||||||
|
|
||||||
|
def abort(self):
|
||||||
|
self.state = self.STATE_DISCONNECTED
|
||||||
|
self.event_connection_ack_received.set()
|
||||||
|
self.event_connection_ack_received.clear()
|
||||||
|
self.event_transfer_feedback.set()
|
||||||
|
self.event_transfer_feedback.clear()
|
135
modem/arq_session_iss.py
Normal file
135
modem/arq_session_iss.py
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
import threading
|
||||||
|
import data_frame_factory
|
||||||
|
import queue
|
||||||
|
import random
|
||||||
|
|
||||||
|
class ARQSessionISS():
|
||||||
|
|
||||||
|
STATE_DISCONNECTED = 0
|
||||||
|
STATE_CONNECTING = 1
|
||||||
|
STATE_CONNECTED = 2
|
||||||
|
STATE_SENDING = 3
|
||||||
|
|
||||||
|
STATE_ENDED = 10
|
||||||
|
|
||||||
|
RETRIES_CONNECT = 3
|
||||||
|
RETRIES_TRANSFER = 3
|
||||||
|
|
||||||
|
TIMEOUT_CONNECT_ACK = 5
|
||||||
|
TIMEOUT_TRANSFER = 2
|
||||||
|
|
||||||
|
def __init__(self, config: dict, tx_frame_queue: queue.Queue, dxcall: str, data: bytearray):
|
||||||
|
self.config = config
|
||||||
|
self.tx_frame_queue = tx_frame_queue
|
||||||
|
self.dxcall = dxcall
|
||||||
|
self.data = data
|
||||||
|
|
||||||
|
self.state = self.STATE_DISCONNECTED
|
||||||
|
self.id = self.generate_id()
|
||||||
|
|
||||||
|
self.event_connection_ack_received = threading.Event()
|
||||||
|
self.event_transfer_ack_received = threading.Event()
|
||||||
|
self.frame_factory = data_frame_factory.DataFrameFactory(self.config)
|
||||||
|
|
||||||
|
def generate_id(self):
|
||||||
|
return random.randint(1,255)
|
||||||
|
|
||||||
|
def log(self, message):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def set_state(self, state):
|
||||||
|
self.log(f"ARQ Session {self.id} state {self.state}")
|
||||||
|
self.state = state
|
||||||
|
|
||||||
|
def transmit_frame(self, frame: bytearray):
|
||||||
|
modem_queue_item = {
|
||||||
|
'mode': self.mode,
|
||||||
|
'repeat': 1,
|
||||||
|
'repeat_delay': 1,
|
||||||
|
'frame': frame,
|
||||||
|
}
|
||||||
|
self.tx_frame_queue.put(modem_queue_item)
|
||||||
|
|
||||||
|
def runner(self):
|
||||||
|
if not self.connect():
|
||||||
|
return False
|
||||||
|
|
||||||
|
return self.send_data()
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
self.thread = threading.Thread(target=self.runner, name=f"ARQ ISS Session {self.id}", daemon=True)
|
||||||
|
self.thread.run()
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
self.set_state(self.STATE_CONNECTING)
|
||||||
|
|
||||||
|
connect_frame = self.frame_factory.build_arq_session_connect(True, self.dxcall, self.id)
|
||||||
|
|
||||||
|
retries = self.RETRIES_CONNECT
|
||||||
|
while retries > 0:
|
||||||
|
self.transmit_frame(connect_frame)
|
||||||
|
if self.event_connection_ack_received.wait(self.TIMEOUT_CONNECT_ACK):
|
||||||
|
self.setState(self.STATE_CONNECTED)
|
||||||
|
return True
|
||||||
|
retries = retries - 1
|
||||||
|
|
||||||
|
self.setState(self.STATE_DISCONNECTED)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def on_connection_ack_received(self, ack):
|
||||||
|
if self.state != self.STATE_CONNECTING:
|
||||||
|
raise RuntimeError(f"ARQ Session: Received connection ACK while in state {self.state}")
|
||||||
|
|
||||||
|
self.speed_level = ack['speed_level']
|
||||||
|
self.event_connection_ack_received.set()
|
||||||
|
|
||||||
|
# Sends the full payload in multiple frames
|
||||||
|
def send_data(self):
|
||||||
|
offset = 0
|
||||||
|
while offset < len(self.payload):
|
||||||
|
max_size = self.get_max_size_for_speed_level(self.speed_level)
|
||||||
|
end_offset = min(len(self.payload), max_size)
|
||||||
|
frame_payload = self.payload[offset:end_offset]
|
||||||
|
data_frame = self.frame_factory.build_arq_session_send(self.speed_level,
|
||||||
|
self.dxcall,
|
||||||
|
frame_payload)
|
||||||
|
self.set_state(self.STATE_SENDING)
|
||||||
|
if not self.send_arq(data_frame):
|
||||||
|
return False
|
||||||
|
offset = end_offset + 1
|
||||||
|
|
||||||
|
# Send part of the payload using ARQ
|
||||||
|
def send_arq(self, frame):
|
||||||
|
retries = self.RETRIES_TRANSFER
|
||||||
|
while retries > 0:
|
||||||
|
# to know later if it has changed
|
||||||
|
speed_level = self.speed_level
|
||||||
|
self.transmit_frame(frame)
|
||||||
|
# wait for ack
|
||||||
|
if self.event_transfer_ack_received.wait(self.TIMEOUT_TRANSFER):
|
||||||
|
speed_level = self.speed_level
|
||||||
|
return True
|
||||||
|
|
||||||
|
# don't decrement retries if speed level is changing
|
||||||
|
if self.speed_level == speed_level:
|
||||||
|
retries = retries - 1
|
||||||
|
|
||||||
|
self.set_state(self.STATE_DISCONNECTED)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def on_transfer_ack_received(self, ack):
|
||||||
|
self.event_transfer_ack_received.set()
|
||||||
|
self.speed_level = ack['speed_level']
|
||||||
|
|
||||||
|
def on_transfer_nack_received(self, nack):
|
||||||
|
self.speed_level = nack['speed_level']
|
||||||
|
|
||||||
|
def on_disconnect_received(self):
|
||||||
|
self.abort()
|
||||||
|
|
||||||
|
def abort(self):
|
||||||
|
self.state = self.STATE_DISCONNECTED
|
||||||
|
self.event_connection_ack_received.set()
|
||||||
|
self.event_connection_ack_received.clear()
|
||||||
|
self.event_transfer_feedback.set()
|
||||||
|
self.event_transfer_feedback.clear()
|
|
@ -2,10 +2,11 @@ from data_frame_factory import DataFrameFactory
|
||||||
import queue
|
import queue
|
||||||
from codec2 import FREEDV_MODE
|
from codec2 import FREEDV_MODE
|
||||||
import structlog
|
import structlog
|
||||||
|
from state_manager import StateManager
|
||||||
|
|
||||||
class TxCommand():
|
class TxCommand():
|
||||||
|
|
||||||
def __init__(self, config, state_manager, modem_events, apiParams = {}):
|
def __init__(self, config: dict, state_manager: StateManager, modem_events: queue.Queue, apiParams:dict = {}):
|
||||||
self.config = config
|
self.config = config
|
||||||
self.logger = structlog.get_logger("Command")
|
self.logger = structlog.get_logger("Command")
|
||||||
self.state_manager = state_manager
|
self.state_manager = state_manager
|
||||||
|
|
|
@ -1,36 +1,23 @@
|
||||||
|
import queue
|
||||||
from command import TxCommand
|
from command import TxCommand
|
||||||
import api_validations
|
import api_validations
|
||||||
from protocol_arq_iss import ISS
|
import base64
|
||||||
from protocol_arq import ARQ
|
from queue import Queue
|
||||||
|
from arq_session_iss import ARQSessionISS
|
||||||
class ARQRawCommand(TxCommand):
|
class ARQRawCommand(TxCommand):
|
||||||
|
|
||||||
def __int__(self, state_manager):
|
|
||||||
# open a new arq instance here
|
|
||||||
self.initialize_arq_instance()
|
|
||||||
|
|
||||||
def set_params_from_api(self, apiParams):
|
def set_params_from_api(self, apiParams):
|
||||||
self.dxcall = apiParams['dxcall']
|
self.dxcall = apiParams['dxcall']
|
||||||
if not api_validations.validate_freedata_callsign(self.dxcall):
|
if not api_validations.validate_freedata_callsign(self.dxcall):
|
||||||
self.dxcall = f"{self.dxcall}-0"
|
self.dxcall = f"{self.dxcall}-0"
|
||||||
return super().set_params_from_api(apiParams)
|
|
||||||
|
|
||||||
def initialize_arq_transmission_iss(self, data):
|
self.data = base64.b64decode(apiParams['data'])
|
||||||
if id := self.get_id_from_frame(data):
|
|
||||||
instance = self.initialize_arq_instance()
|
|
||||||
self.states.register_arq_instance_by_id(id, instance)
|
|
||||||
instance['arq_irs'].arq_received_data_channel_opener()
|
|
||||||
|
|
||||||
|
def run(self, event_queue: Queue, tx_frame_queue: Queue):
|
||||||
|
self.emit_event(event_queue)
|
||||||
|
self.logger.info(self.log_message())
|
||||||
|
|
||||||
def initialize_arq_instance(self):
|
iss = ARQSessionISS(self.config, tx_frame_queue, self.dxcall, self.data)
|
||||||
self.arq = ARQ(self.config, self.event_queue, self.state_manager)
|
self.state_manager.register_arq_iss_session(iss)
|
||||||
self.arq_iss = ISS(self.config, self.event_queue, self.state_manager)
|
iss.run()
|
||||||
|
return iss
|
||||||
return {
|
|
||||||
'arq': self.arq,
|
|
||||||
'arq_irs': self.arq_irs,
|
|
||||||
'arq_iss': self.arq_iss,
|
|
||||||
'arq_session': self.arq_session
|
|
||||||
}
|
|
||||||
def build_frame(self):
|
|
||||||
return self.frame_factory.build_arq_connect(destination=self.dxcall, session_id=b'', isWideband=True)
|
|
||||||
|
|
|
@ -329,7 +329,8 @@ class DataFrameFactory:
|
||||||
"origin": helpers.callsign_to_bytes(self.myfullcall),
|
"origin": helpers.callsign_to_bytes(self.myfullcall),
|
||||||
"session_id": session_id.to_bytes(1, 'big'),
|
"session_id": session_id.to_bytes(1, 'big'),
|
||||||
}
|
}
|
||||||
return self.construct(FR_TYPE.ARQ_SESSION_OPEN, payload)
|
channel_type = FR_TYPE.ARQ_DC_OPEN_W if isWideband else FR_TYPE.ARQ_DC_OPEN_N
|
||||||
|
return self.construct(channel_type, payload)
|
||||||
|
|
||||||
def build_arq_burst_ack(self, session_id: bytes, snr: int, speed_level: int, len_arq_rx_frame_buffer: int):
|
def build_arq_burst_ack(self, session_id: bytes, snr: int, speed_level: int, len_arq_rx_frame_buffer: int):
|
||||||
# ack_frame = bytearray(self.length_sig1_frame)
|
# ack_frame = bytearray(self.length_sig1_frame)
|
||||||
|
|
|
@ -9,15 +9,13 @@ from codec2 import FREEDV_MODE
|
||||||
class FrameHandler():
|
class FrameHandler():
|
||||||
|
|
||||||
def __init__(self, name: str, config, states: StateManager, event_manager: EventManager,
|
def __init__(self, name: str, config, states: StateManager, event_manager: EventManager,
|
||||||
tx_frame_queue: Queue,
|
tx_frame_queue: Queue) -> None:
|
||||||
arq_sessions: list) -> None:
|
|
||||||
|
|
||||||
self.name = name
|
self.name = name
|
||||||
self.config = config
|
self.config = config
|
||||||
self.states = states
|
self.states = states
|
||||||
self.event_manager = event_manager
|
self.event_manager = event_manager
|
||||||
self.tx_frame_queue = tx_frame_queue
|
self.tx_frame_queue = tx_frame_queue
|
||||||
self.arq_sessions = arq_sessions
|
|
||||||
self.logger = structlog.get_logger("Frame Handler")
|
self.logger = structlog.get_logger("Frame Handler")
|
||||||
|
|
||||||
self.details = {
|
self.details = {
|
||||||
|
|
17
modem/frame_handler_arq.py
Normal file
17
modem/frame_handler_arq.py
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
from queue import Queue
|
||||||
|
import frame_handler
|
||||||
|
from modem.event_manager import EventManager
|
||||||
|
from modem.state_manager import StateManager
|
||||||
|
from modem_frametypes import FRAME_TYPE as FR
|
||||||
|
from arq_session_irs import ARQSessionIRS
|
||||||
|
|
||||||
|
class ARQFrameHandler(frame_handler.FrameHandler):
|
||||||
|
|
||||||
|
def follow_protocol(self):
|
||||||
|
frame = self.details['frame']
|
||||||
|
|
||||||
|
# ARQ session open received
|
||||||
|
if frame.frame_type in [FR.ARQ_DC_OPEN_N.value, FR.ARQ_DC_OPEN_W]:
|
||||||
|
session = ARQSessionIRS(self.config, self.tx_frame_queue, frame.origin, frame.session_id)
|
||||||
|
self.states.register_arq_irs_session(session)
|
||||||
|
session.run()
|
|
@ -33,7 +33,9 @@ class StateManager:
|
||||||
self.heard_stations = [] # TODO remove it... heard stations list == deprecated
|
self.heard_stations = [] # TODO remove it... heard stations list == deprecated
|
||||||
self.activities_list = {}
|
self.activities_list = {}
|
||||||
|
|
||||||
self.arq_instance_table = {}
|
self.arq_iss_sessions = {}
|
||||||
|
self.arq_irs_sessions = {}
|
||||||
|
|
||||||
self.arq_session_state = 'disconnected'
|
self.arq_session_state = 'disconnected'
|
||||||
self.arq_speed_level = 0
|
self.arq_speed_level = 0
|
||||||
self.arq_total_bytes = 0
|
self.arq_total_bytes = 0
|
||||||
|
@ -112,19 +114,33 @@ class StateManager:
|
||||||
def waitForTransmission(self):
|
def waitForTransmission(self):
|
||||||
self.transmitting_event.wait()
|
self.transmitting_event.wait()
|
||||||
|
|
||||||
def register_arq_instance_by_id(self, id, instance):
|
def register_arq_iss_session(self, session):
|
||||||
self.arq_instance_table[id] = instance
|
if session.id in self.arq_iss_sessions:
|
||||||
|
raise RuntimeError(f"ARQ ISS Session '{session.id}' already exists!")
|
||||||
|
self.arq_iss_sessions[session.id] = session
|
||||||
|
|
||||||
def get_arq_instance_by_id(self, id):
|
def register_arq_irs_session(self, session):
|
||||||
return self.arq_instance_table.get(id)
|
if session.id in self.arq_irs_sessions:
|
||||||
|
raise RuntimeError(f"ARQ IRS Session '{session.id}' already exists!")
|
||||||
|
self.arq_irs_sessions[session.id] = session
|
||||||
|
|
||||||
def delete_arq_instance_by_id(self, id):
|
def get_arq_iss_session(self, id):
|
||||||
instances = self.arq_instance_table.pop(id, None)
|
if id not in self.arq_iss_sessions:
|
||||||
if None not in instances:
|
raise RuntimeError(f"ARQ ISS Session '{id}' not found!")
|
||||||
for key in instances:
|
|
||||||
del instances[key]
|
def get_arq_irs_session(self, id):
|
||||||
return True
|
if id not in self.arq_irs_sessions:
|
||||||
return False
|
raise RuntimeError(f"ARQ IRS Session '{id}' not found!")
|
||||||
|
|
||||||
|
def remove_arq_iss_session(self, id):
|
||||||
|
if id not in self.arq_iss_sessions:
|
||||||
|
raise RuntimeError(f"ARQ ISS Session '{id}' not found!")
|
||||||
|
del self.arq_iss_sessions[id]
|
||||||
|
|
||||||
|
def remove_arq_irs_session(self, id):
|
||||||
|
if id not in self.arq_irs_sessions:
|
||||||
|
raise RuntimeError(f"ARQ ISS Session '{id}' not found!")
|
||||||
|
del self.arq_irs_sessions[id]
|
||||||
|
|
||||||
def add_activity(self, activity_data):
|
def add_activity(self, activity_data):
|
||||||
# Generate a random 8-byte string as hex
|
# Generate a random 8-byte string as hex
|
||||||
|
|
71
tests/test_arq_session.py
Normal file
71
tests/test_arq_session.py
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
import sys
|
||||||
|
sys.path.append('modem')
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
from config import CONFIG
|
||||||
|
import helpers
|
||||||
|
import queue
|
||||||
|
import threading
|
||||||
|
import base64
|
||||||
|
from command_arq_raw import ARQRawCommand
|
||||||
|
from state_manager import StateManager
|
||||||
|
from frame_dispatcher import DISPATCHER
|
||||||
|
|
||||||
|
class TestARQSession(unittest.TestCase):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
config_manager = CONFIG('modem/config.ini.example')
|
||||||
|
cls.config = config_manager.read()
|
||||||
|
|
||||||
|
# ISS
|
||||||
|
cls.iss_modem_transmit_queue = queue.Queue()
|
||||||
|
cls.iss_state_manager = StateManager(queue.Queue())
|
||||||
|
cls.iss_event_queue = queue.Queue()
|
||||||
|
cls.iss_frame_dispatcher = DISPATCHER(cls.config,
|
||||||
|
cls.iss_event_queue,
|
||||||
|
cls.iss_state_manager,
|
||||||
|
queue.Queue(),
|
||||||
|
cls.iss_modem_transmit_queue)
|
||||||
|
|
||||||
|
# IRS
|
||||||
|
cls.irs_modem_transmit_queue = queue.Queue()
|
||||||
|
cls.irs_state_manager = StateManager(queue.Queue())
|
||||||
|
cls.irs_event_queue = queue.Queue()
|
||||||
|
cls.irs_frame_dispatcher = DISPATCHER(cls.config,
|
||||||
|
cls.irs_event_queue,
|
||||||
|
cls.irs_state_manager,
|
||||||
|
queue.Queue(),
|
||||||
|
cls.irs_modem_transmit_queue)
|
||||||
|
|
||||||
|
|
||||||
|
def channelWorker(self, modem_transmit_queue: queue, frame_dispatcher: DISPATCHER):
|
||||||
|
while True:
|
||||||
|
transmission_item = modem_transmit_queue.get()
|
||||||
|
frame_bytes = bytes(transmission_item['frame'])
|
||||||
|
frame_dispatcher.new_process_data(frame_bytes, None, len(frame_bytes), 0, 0)
|
||||||
|
|
||||||
|
def establishChannels(self):
|
||||||
|
self.iss_to_irs_channel = threading.Thread(target=self.channelWorker,
|
||||||
|
args=[self.iss_modem_transmit_queue,
|
||||||
|
self.irs_frame_dispatcher],
|
||||||
|
name = "IRS to ISS channel")
|
||||||
|
self.iss_to_irs_channel.start()
|
||||||
|
|
||||||
|
self.irs_to_iss_channel = threading.Thread(target=self.channelWorker,
|
||||||
|
args=[self.irs_modem_transmit_queue,
|
||||||
|
self.iss_frame_dispatcher],
|
||||||
|
name = "IRS to ISS channel")
|
||||||
|
self.irs_to_iss_channel.start()
|
||||||
|
|
||||||
|
def testARQSession(self):
|
||||||
|
self.establishChannels()
|
||||||
|
params = {
|
||||||
|
'dxcall': "DJ2LS-3",
|
||||||
|
'data': base64.b64encode(bytes("Hello world!", encoding="utf-8")),
|
||||||
|
}
|
||||||
|
cmd = ARQRawCommand(self.config, self.iss_state_manager, self.iss_event_queue, params)
|
||||||
|
cmd.run(self.iss_event_queue, self.iss_modem_transmit_queue)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
Loading…
Reference in a new issue