mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
feat(takserver): bridge Casevac / Emergency / Task + GeoChat receipts
Bump the protobufs submodule to the takv2_geometry commit that adds
CasevacReport, EmergencyAlert, TaskRequest, and GeoChat receipt
fields. Rebuild meshtastic-tak-0.1.0.jar from the updated
TAKPacket-SDK kotlin artifact and extend TakV2Compressor.kt with the
corresponding wireToSdkData / sdkDataToWire branches so the JVM
takserver round-trip preserves every structured field the SDK parser
now extracts.
- .gitmodules: pin core/proto/src/main/proto to takv2_geometry
- core/proto/src/main/proto: fast-forward to the new protobufs tip
- core/takserver/libs/meshtastic-tak-0.1.0.jar: rebuilt artifact
with the typed CasevacReport / EmergencyAlert / TaskRequest
payload variants and the extended GeoChat receipt fields
- core/takserver/src/jvmAndroidMain/.../TakV2Compressor.kt:
* extend wireToSdkData with casevac / emergency / task / chat
receipt branches, mapping enum .value ints through the SDK
TakPacketV2Data sealed-class payloads
* extend sdkDataToWire with the same three variants plus the
chat receipt_for_uid / receipt_type fields
* import WireCasevacReport / WireEmergencyAlert / WireTaskRequest
from the regenerated Wire proto classes
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
15419aba6c
commit
9d5d516c37
4 changed files with 435 additions and 1 deletions
1
.gitmodules
vendored
1
.gitmodules
vendored
|
|
@ -1,3 +1,4 @@
|
|||
[submodule "app proto submodule"]
|
||||
path = core/proto/src/main/proto
|
||||
url = https://github.com/meshtastic/protobufs.git
|
||||
branch = takv2_geometry
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit 349c1d5c1e3ab716a65d7dab1597923b4542796d
|
||||
Subproject commit 10a86bf0b9c1fc9242363c17e4dfc54185967232
|
||||
BIN
core/takserver/libs/meshtastic-tak-0.1.0.jar
Normal file
BIN
core/takserver/libs/meshtastic-tak-0.1.0.jar
Normal file
Binary file not shown.
|
|
@ -0,0 +1,433 @@
|
|||
/*
|
||||
* Copyright (c) 2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*/
|
||||
|
||||
package org.meshtastic.core.takserver
|
||||
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.meshtastic.proto.AircraftTrack as WireAircraftTrack
|
||||
import org.meshtastic.proto.CasevacReport as WireCasevacReport
|
||||
import org.meshtastic.proto.CotGeoPoint as WireCotGeoPoint
|
||||
import org.meshtastic.proto.DrawnShape as WireDrawnShape
|
||||
import org.meshtastic.proto.EmergencyAlert as WireEmergencyAlert
|
||||
import org.meshtastic.proto.GeoChat as WireGeoChat
|
||||
import org.meshtastic.proto.Marker as WireMarker
|
||||
import org.meshtastic.proto.RangeAndBearing as WireRangeAndBearing
|
||||
import org.meshtastic.proto.Route as WireRoute
|
||||
import org.meshtastic.proto.TAKPacketV2
|
||||
import org.meshtastic.proto.TaskRequest as WireTaskRequest
|
||||
import org.meshtastic.proto.Team as WireTeam
|
||||
import org.meshtastic.tak.TakCompressor as SdkCompressor
|
||||
import org.meshtastic.tak.TakPacketV2Data
|
||||
import org.meshtastic.tak.TakPacketV2Serializer
|
||||
import org.meshtastic.tak.CotTypeMapper
|
||||
|
||||
/**
|
||||
* JVM/Android implementation of TakV2Compressor.
|
||||
* Delegates to TAKPacket-SDK's TakCompressor for zstd dictionary compression.
|
||||
*
|
||||
* The SDK compressor is constructed lazily and its result is cached in a
|
||||
* nullable field so that a native-library failure (e.g. missing Android .so)
|
||||
* does NOT poison this object. Without lazy/try-catch, a failure inside a
|
||||
* top-level `val` initializer runs at class `<clinit>` time, marks the class
|
||||
* ERRONEOUS, and turns every subsequent reference into `NoClassDefFoundError`.
|
||||
*/
|
||||
internal actual object TakV2Compressor {
|
||||
|
||||
actual val MAX_DECOMPRESSED_SIZE: Int = 4096
|
||||
actual val DICT_ID_NON_AIRCRAFT: Int = 0
|
||||
actual val DICT_ID_AIRCRAFT: Int = 1
|
||||
actual val DICT_ID_UNCOMPRESSED: Int = 0xFF
|
||||
|
||||
@Volatile private var sdkCompressorOrNull: SdkCompressor? = null
|
||||
@Volatile private var sdkCompressorInitFailure: Throwable? = null
|
||||
|
||||
@Synchronized
|
||||
private fun getSdkCompressor(): SdkCompressor {
|
||||
sdkCompressorOrNull?.let { return it }
|
||||
sdkCompressorInitFailure?.let { cached ->
|
||||
throw IllegalStateException("zstd-jni unavailable on this platform", cached)
|
||||
}
|
||||
return try {
|
||||
SdkCompressor().also { sdkCompressorOrNull = it }
|
||||
} catch (e: Throwable) {
|
||||
sdkCompressorInitFailure = e
|
||||
throw IllegalStateException("zstd-jni unavailable on this platform", e)
|
||||
}
|
||||
}
|
||||
|
||||
actual fun compress(packet: TAKPacketV2): ByteArray {
|
||||
val data = wireToSdkData(packet)
|
||||
return getSdkCompressor().compress(data)
|
||||
}
|
||||
|
||||
actual fun decompress(wirePayload: ByteArray): TAKPacketV2 {
|
||||
val data = getSdkCompressor().decompress(wirePayload)
|
||||
return sdkDataToWire(data)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Wire-generated TAKPacketV2 → SDK's TakPacketV2Data.
|
||||
*/
|
||||
private fun wireToSdkData(packet: TAKPacketV2): TakPacketV2Data {
|
||||
val cotTypeId = packet.cot_type_id.value
|
||||
val cotTypeStr = if (cotTypeId == 0 && packet.cot_type_str.isNotEmpty()) packet.cot_type_str else null
|
||||
|
||||
val payload = when {
|
||||
packet.pli != null -> TakPacketV2Data.Payload.Pli(true)
|
||||
packet.chat != null -> TakPacketV2Data.Payload.Chat(
|
||||
message = packet.chat!!.message,
|
||||
to = packet.chat!!.to,
|
||||
toCallsign = packet.chat!!.to_callsign,
|
||||
receiptForUid = packet.chat!!.receipt_for_uid,
|
||||
receiptType = packet.chat!!.receipt_type.value,
|
||||
)
|
||||
packet.aircraft != null -> TakPacketV2Data.Payload.Aircraft(
|
||||
icao = packet.aircraft!!.icao,
|
||||
registration = packet.aircraft!!.registration,
|
||||
flight = packet.aircraft!!.flight,
|
||||
aircraftType = packet.aircraft!!.aircraft_type,
|
||||
squawk = packet.aircraft!!.squawk,
|
||||
category = packet.aircraft!!.category,
|
||||
rssiX10 = packet.aircraft!!.rssi_x10,
|
||||
gps = packet.aircraft!!.gps,
|
||||
cotHostId = packet.aircraft!!.cot_host_id,
|
||||
)
|
||||
// Typed geometry variants added by takv2_geometry (tags 34-37).
|
||||
// All GeoPoint fields on the wire are delta-encoded from the
|
||||
// event anchor; the SDK data class stores absolute lat/lon, so
|
||||
// we add packet.latitude_i / longitude_i here.
|
||||
packet.shape != null -> {
|
||||
val s = packet.shape!!
|
||||
TakPacketV2Data.Payload.DrawnShape(
|
||||
kind = s.kind.value,
|
||||
style = s.style.value,
|
||||
majorCm = s.major_cm,
|
||||
minorCm = s.minor_cm,
|
||||
angleDeg = s.angle_deg,
|
||||
strokeColor = s.stroke_color.value,
|
||||
strokeArgb = s.stroke_argb,
|
||||
strokeWeightX10 = s.stroke_weight_x10,
|
||||
fillColor = s.fill_color.value,
|
||||
fillArgb = s.fill_argb,
|
||||
labelsOn = s.labels_on,
|
||||
vertices = s.vertices.map { v ->
|
||||
TakPacketV2Data.Payload.Vertex(
|
||||
latI = packet.latitude_i + v.lat_delta_i,
|
||||
lonI = packet.longitude_i + v.lon_delta_i,
|
||||
)
|
||||
},
|
||||
truncated = s.truncated,
|
||||
bullseyeDistanceDm = s.bullseye_distance_dm,
|
||||
bullseyeBearingRef = s.bullseye_bearing_ref,
|
||||
bullseyeFlags = s.bullseye_flags,
|
||||
bullseyeUidRef = s.bullseye_uid_ref,
|
||||
)
|
||||
}
|
||||
packet.marker != null -> {
|
||||
val m = packet.marker!!
|
||||
TakPacketV2Data.Payload.Marker(
|
||||
kind = m.kind.value,
|
||||
color = m.color.value,
|
||||
colorArgb = m.color_argb,
|
||||
readiness = m.readiness,
|
||||
parentUid = m.parent_uid,
|
||||
parentType = m.parent_type,
|
||||
parentCallsign = m.parent_callsign,
|
||||
iconset = m.iconset,
|
||||
)
|
||||
}
|
||||
packet.rab != null -> {
|
||||
val r = packet.rab!!
|
||||
val anchor = r.anchor
|
||||
TakPacketV2Data.Payload.RangeAndBearing(
|
||||
anchorLatI = packet.latitude_i + (anchor?.lat_delta_i ?: 0),
|
||||
anchorLonI = packet.longitude_i + (anchor?.lon_delta_i ?: 0),
|
||||
anchorUid = r.anchor_uid,
|
||||
rangeCm = r.range_cm,
|
||||
bearingCdeg = r.bearing_cdeg,
|
||||
strokeColor = r.stroke_color.value,
|
||||
strokeArgb = r.stroke_argb,
|
||||
strokeWeightX10 = r.stroke_weight_x10,
|
||||
)
|
||||
}
|
||||
packet.route != null -> {
|
||||
val rt = packet.route!!
|
||||
TakPacketV2Data.Payload.Route(
|
||||
method = rt.method.value,
|
||||
direction = rt.direction.value,
|
||||
prefix = rt.prefix,
|
||||
strokeWeightX10 = rt.stroke_weight_x10,
|
||||
links = rt.links.map { link ->
|
||||
val pt = link.point
|
||||
TakPacketV2Data.Payload.Route.Link(
|
||||
latI = packet.latitude_i + (pt?.lat_delta_i ?: 0),
|
||||
lonI = packet.longitude_i + (pt?.lon_delta_i ?: 0),
|
||||
uid = link.uid,
|
||||
callsign = link.callsign,
|
||||
linkType = link.link_type,
|
||||
)
|
||||
},
|
||||
truncated = rt.truncated,
|
||||
)
|
||||
}
|
||||
packet.casevac != null -> {
|
||||
val c = packet.casevac!!
|
||||
TakPacketV2Data.Payload.CasevacReport(
|
||||
precedence = c.precedence.value,
|
||||
equipmentFlags = c.equipment_flags,
|
||||
litterPatients = c.litter_patients,
|
||||
ambulatoryPatients = c.ambulatory_patients,
|
||||
security = c.security.value,
|
||||
hlzMarking = c.hlz_marking.value,
|
||||
zoneMarker = c.zone_marker,
|
||||
usMilitary = c.us_military,
|
||||
usCivilian = c.us_civilian,
|
||||
nonUsMilitary = c.non_us_military,
|
||||
nonUsCivilian = c.non_us_civilian,
|
||||
epw = c.epw,
|
||||
child = c.child,
|
||||
terrainFlags = c.terrain_flags,
|
||||
frequency = c.frequency,
|
||||
)
|
||||
}
|
||||
packet.emergency != null -> {
|
||||
val e = packet.emergency!!
|
||||
TakPacketV2Data.Payload.EmergencyAlert(
|
||||
type = e.type.value,
|
||||
authoringUid = e.authoring_uid,
|
||||
cancelReferenceUid = e.cancel_reference_uid,
|
||||
)
|
||||
}
|
||||
packet.task != null -> {
|
||||
val t = packet.task!!
|
||||
TakPacketV2Data.Payload.TaskRequest(
|
||||
taskType = t.task_type,
|
||||
targetUid = t.target_uid,
|
||||
assigneeUid = t.assignee_uid,
|
||||
priority = t.priority.value,
|
||||
status = t.status.value,
|
||||
note = t.note,
|
||||
)
|
||||
}
|
||||
packet.raw_detail != null -> TakPacketV2Data.Payload.RawDetail(packet.raw_detail!!.toByteArray())
|
||||
else -> TakPacketV2Data.Payload.None
|
||||
}
|
||||
|
||||
return TakPacketV2Data(
|
||||
cotTypeId = cotTypeId,
|
||||
cotTypeStr = cotTypeStr,
|
||||
how = packet.how.value,
|
||||
callsign = packet.callsign,
|
||||
team = packet.team.value,
|
||||
role = packet.role.value,
|
||||
latitudeI = packet.latitude_i,
|
||||
longitudeI = packet.longitude_i,
|
||||
altitude = packet.altitude,
|
||||
speed = packet.speed,
|
||||
course = packet.course,
|
||||
battery = packet.battery,
|
||||
geoSrc = packet.geo_src.value,
|
||||
altSrc = packet.alt_src.value,
|
||||
uid = packet.uid,
|
||||
deviceCallsign = packet.device_callsign,
|
||||
staleSeconds = packet.stale_seconds,
|
||||
takVersion = packet.tak_version,
|
||||
takDevice = packet.tak_device,
|
||||
takPlatform = packet.tak_platform,
|
||||
takOs = packet.tak_os,
|
||||
endpoint = packet.endpoint,
|
||||
phone = packet.phone,
|
||||
payload = payload,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert SDK's TakPacketV2Data → Wire-generated TAKPacketV2.
|
||||
*/
|
||||
private fun sdkDataToWire(data: TakPacketV2Data): TAKPacketV2 {
|
||||
val cotType = org.meshtastic.proto.CotType.fromValue(data.cotTypeId)
|
||||
?: org.meshtastic.proto.CotType.CotType_Other
|
||||
val how = org.meshtastic.proto.CotHow.fromValue(data.how)
|
||||
?: org.meshtastic.proto.CotHow.CotHow_Unspecified
|
||||
val team = org.meshtastic.proto.Team.fromValue(data.team)
|
||||
?: org.meshtastic.proto.Team.Unspecifed_Color
|
||||
val role = org.meshtastic.proto.MemberRole.fromValue(data.role)
|
||||
?: org.meshtastic.proto.MemberRole.Unspecifed
|
||||
val geoSrc = org.meshtastic.proto.GeoPointSource.fromValue(data.geoSrc)
|
||||
?: org.meshtastic.proto.GeoPointSource.GeoPointSource_Unspecified
|
||||
val altSrc = org.meshtastic.proto.GeoPointSource.fromValue(data.altSrc)
|
||||
?: org.meshtastic.proto.GeoPointSource.GeoPointSource_Unspecified
|
||||
|
||||
return TAKPacketV2(
|
||||
cot_type_id = cotType,
|
||||
cot_type_str = data.cotTypeStr ?: "",
|
||||
how = how,
|
||||
callsign = data.callsign,
|
||||
team = team,
|
||||
role = role,
|
||||
latitude_i = data.latitudeI,
|
||||
longitude_i = data.longitudeI,
|
||||
altitude = data.altitude,
|
||||
speed = data.speed,
|
||||
course = data.course,
|
||||
battery = data.battery,
|
||||
geo_src = geoSrc,
|
||||
alt_src = altSrc,
|
||||
uid = data.uid,
|
||||
device_callsign = data.deviceCallsign,
|
||||
stale_seconds = data.staleSeconds,
|
||||
tak_version = data.takVersion,
|
||||
tak_device = data.takDevice,
|
||||
tak_platform = data.takPlatform,
|
||||
tak_os = data.takOs,
|
||||
endpoint = data.endpoint,
|
||||
phone = data.phone,
|
||||
pli = if (data.payload is TakPacketV2Data.Payload.Pli) true else null,
|
||||
chat = (data.payload as? TakPacketV2Data.Payload.Chat)?.let { chat ->
|
||||
WireGeoChat(
|
||||
message = chat.message,
|
||||
to = chat.to,
|
||||
to_callsign = chat.toCallsign,
|
||||
receipt_for_uid = chat.receiptForUid,
|
||||
receipt_type = WireGeoChat.ReceiptType.fromValue(chat.receiptType)
|
||||
?: WireGeoChat.ReceiptType.ReceiptType_None,
|
||||
)
|
||||
},
|
||||
aircraft = (data.payload as? TakPacketV2Data.Payload.Aircraft)?.let { ac ->
|
||||
WireAircraftTrack(
|
||||
icao = ac.icao,
|
||||
registration = ac.registration,
|
||||
flight = ac.flight,
|
||||
aircraft_type = ac.aircraftType,
|
||||
squawk = ac.squawk,
|
||||
category = ac.category,
|
||||
rssi_x10 = ac.rssiX10,
|
||||
gps = ac.gps,
|
||||
cot_host_id = ac.cotHostId,
|
||||
)
|
||||
},
|
||||
shape = (data.payload as? TakPacketV2Data.Payload.DrawnShape)?.let { s ->
|
||||
WireDrawnShape(
|
||||
kind = WireDrawnShape.Kind.fromValue(s.kind) ?: WireDrawnShape.Kind.Kind_Unspecified,
|
||||
style = WireDrawnShape.StyleMode.fromValue(s.style) ?: WireDrawnShape.StyleMode.StyleMode_Unspecified,
|
||||
major_cm = s.majorCm,
|
||||
minor_cm = s.minorCm,
|
||||
angle_deg = s.angleDeg,
|
||||
stroke_color = WireTeam.fromValue(s.strokeColor) ?: WireTeam.Unspecifed_Color,
|
||||
stroke_argb = s.strokeArgb,
|
||||
stroke_weight_x10 = s.strokeWeightX10,
|
||||
fill_color = WireTeam.fromValue(s.fillColor) ?: WireTeam.Unspecifed_Color,
|
||||
fill_argb = s.fillArgb,
|
||||
labels_on = s.labelsOn,
|
||||
// Delta-encode vertices relative to the event anchor.
|
||||
vertices = s.vertices.map { v ->
|
||||
WireCotGeoPoint(
|
||||
lat_delta_i = v.latI - data.latitudeI,
|
||||
lon_delta_i = v.lonI - data.longitudeI,
|
||||
)
|
||||
},
|
||||
truncated = s.truncated,
|
||||
bullseye_distance_dm = s.bullseyeDistanceDm,
|
||||
bullseye_bearing_ref = s.bullseyeBearingRef,
|
||||
bullseye_flags = s.bullseyeFlags,
|
||||
bullseye_uid_ref = s.bullseyeUidRef,
|
||||
)
|
||||
},
|
||||
marker = (data.payload as? TakPacketV2Data.Payload.Marker)?.let { m ->
|
||||
WireMarker(
|
||||
kind = WireMarker.Kind.fromValue(m.kind) ?: WireMarker.Kind.Kind_Unspecified,
|
||||
color = WireTeam.fromValue(m.color) ?: WireTeam.Unspecifed_Color,
|
||||
color_argb = m.colorArgb,
|
||||
readiness = m.readiness,
|
||||
parent_uid = m.parentUid,
|
||||
parent_type = m.parentType,
|
||||
parent_callsign = m.parentCallsign,
|
||||
iconset = m.iconset,
|
||||
)
|
||||
},
|
||||
rab = (data.payload as? TakPacketV2Data.Payload.RangeAndBearing)?.let { r ->
|
||||
WireRangeAndBearing(
|
||||
anchor = WireCotGeoPoint(
|
||||
lat_delta_i = r.anchorLatI - data.latitudeI,
|
||||
lon_delta_i = r.anchorLonI - data.longitudeI,
|
||||
),
|
||||
anchor_uid = r.anchorUid,
|
||||
range_cm = r.rangeCm,
|
||||
bearing_cdeg = r.bearingCdeg,
|
||||
stroke_color = WireTeam.fromValue(r.strokeColor) ?: WireTeam.Unspecifed_Color,
|
||||
stroke_argb = r.strokeArgb,
|
||||
stroke_weight_x10 = r.strokeWeightX10,
|
||||
)
|
||||
},
|
||||
route = (data.payload as? TakPacketV2Data.Payload.Route)?.let { rt ->
|
||||
WireRoute(
|
||||
method = WireRoute.Method.fromValue(rt.method) ?: WireRoute.Method.Method_Unspecified,
|
||||
direction = WireRoute.Direction.fromValue(rt.direction) ?: WireRoute.Direction.Direction_Unspecified,
|
||||
prefix = rt.prefix,
|
||||
stroke_weight_x10 = rt.strokeWeightX10,
|
||||
links = rt.links.map { link ->
|
||||
WireRoute.Link(
|
||||
point = WireCotGeoPoint(
|
||||
lat_delta_i = link.latI - data.latitudeI,
|
||||
lon_delta_i = link.lonI - data.longitudeI,
|
||||
),
|
||||
uid = link.uid,
|
||||
callsign = link.callsign,
|
||||
link_type = link.linkType,
|
||||
)
|
||||
},
|
||||
truncated = rt.truncated,
|
||||
)
|
||||
},
|
||||
casevac = (data.payload as? TakPacketV2Data.Payload.CasevacReport)?.let { c ->
|
||||
WireCasevacReport(
|
||||
precedence = WireCasevacReport.Precedence.fromValue(c.precedence)
|
||||
?: WireCasevacReport.Precedence.Precedence_Unspecified,
|
||||
equipment_flags = c.equipmentFlags,
|
||||
litter_patients = c.litterPatients,
|
||||
ambulatory_patients = c.ambulatoryPatients,
|
||||
security = WireCasevacReport.Security.fromValue(c.security)
|
||||
?: WireCasevacReport.Security.Security_Unspecified,
|
||||
hlz_marking = WireCasevacReport.HlzMarking.fromValue(c.hlzMarking)
|
||||
?: WireCasevacReport.HlzMarking.HlzMarking_Unspecified,
|
||||
zone_marker = c.zoneMarker,
|
||||
us_military = c.usMilitary,
|
||||
us_civilian = c.usCivilian,
|
||||
non_us_military = c.nonUsMilitary,
|
||||
non_us_civilian = c.nonUsCivilian,
|
||||
epw = c.epw,
|
||||
child = c.child,
|
||||
terrain_flags = c.terrainFlags,
|
||||
frequency = c.frequency,
|
||||
)
|
||||
},
|
||||
emergency = (data.payload as? TakPacketV2Data.Payload.EmergencyAlert)?.let { e ->
|
||||
WireEmergencyAlert(
|
||||
type = WireEmergencyAlert.Type.fromValue(e.type)
|
||||
?: WireEmergencyAlert.Type.Type_Unspecified,
|
||||
authoring_uid = e.authoringUid,
|
||||
cancel_reference_uid = e.cancelReferenceUid,
|
||||
)
|
||||
},
|
||||
task = (data.payload as? TakPacketV2Data.Payload.TaskRequest)?.let { t ->
|
||||
WireTaskRequest(
|
||||
task_type = t.taskType,
|
||||
target_uid = t.targetUid,
|
||||
assignee_uid = t.assigneeUid,
|
||||
priority = WireTaskRequest.Priority.fromValue(t.priority)
|
||||
?: WireTaskRequest.Priority.Priority_Unspecified,
|
||||
status = WireTaskRequest.Status.fromValue(t.status)
|
||||
?: WireTaskRequest.Status.Status_Unspecified,
|
||||
note = t.note,
|
||||
)
|
||||
},
|
||||
raw_detail = (data.payload as? TakPacketV2Data.Payload.RawDetail)?.bytes?.toByteString(),
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue