Fix DX Cluster spot formatting - emit fixed 75-character DXSpider-compatible lines - align callsign, comment, and UTC fields - support frequencies up to 24 GHz - reject overlong callsigns instead of truncating them - add protocol tests and update documentation Fixes #86

This commit is contained in:
Marc Froehlich
2026-09-02 20:03:55 +02:00
parent 6b0d699a98
commit 51aa04bfb5
15 changed files with 422 additions and 77 deletions
+9
View File
@@ -66,6 +66,15 @@ Known integration areas include:
CR/LF framing, XML framing, ports/transports, callsign normalization and frequency formatting are protocol behaviour and must not be changed as incidental cleanup.
### Local DX Cluster output
- Local spots use a fixed 75-character, DXSpider-compatible payload line followed by two BEL characters and CRLF.
- The DX callsign begins in column 27 and occupies up to 12 characters. The 30-character comment begins in column 40, and the five-character UTC time begins in column 71.
- Spotter and frequency padding is calculated dynamically so frequencies from 50 MHz through 24 GHz do not shift the following fields.
- Comments are padded or truncated to exactly 30 characters. Automatic AirScout comments retain the locator first and use the compact form `JO51HK AP 1m/100%;4m/75%`.
- A DX callsign longer than 12 characters is rejected and logged rather than truncated.
- Trigger conditions, QRG recognition and normalisation, login, keepalive, multi-client delivery and the local-only trust boundary remain separate from line formatting.
### Logging and Worked-state persistence
- The Simplelogfile interpreter reads the selected text file after connection startup and then once per minute using a fixed built-in callsign pattern.
+2
View File
@@ -44,6 +44,8 @@ v1.42 führt mehrere bisher getrennte Auswertungen zusammen. Bandinformationen,
### Geändert
- **DX-Cluster-Zeilenformat vereinheitlicht:** Lokale Spots verwenden jetzt eine feste, DXSpider-kompatible 75-Zeichen-Nutzzeile mit dem DX-Rufzeichen ab Spalte 27, einem 30 Zeichen breiten Kommentarfeld und der UTC-Zeit ab Spalte 71. Das Format bleibt auch bei Mikrowellenfrequenzen bis 24 GHz stabil. Überlange DX-Rufzeichen werden verworfen und protokolliert, statt still abgeschnitten zu werden; AirScout- und Testkommentare sind entsprechend kompakter.
- **Sessionbezogene ON4KST-Verbindungssteuerung:** Socket, Reader, Writer, Messagebus und Warteschlangen gehören jetzt zu einer eindeutig identifizierten Verbindungssession. Veraltete Threads einer abgelösten Verbindung können dadurch keine Daten mehr verarbeiten oder die neue Verbindung schließen. `ONLINE` wird erst nach bestätigtem Login und vollständig empfangenen Benutzerlisten gemeldet. Verbindungsaufbau, Login und Synchronisation besitzen feste Zeitlimits; Heartbeats, ausbleibende Eingangsdaten, EOF sowie Lese- und Schreibfehler werden überwacht und lösen bei Bedarf einen kontrollierten Neuaufbau mit Backoff aus.
- **ON4KST-Protokollbefehle abgesichert:** Ausgehende Befehle werden zentral aufgebaut und auf gültige Kategorien, Locatoren und unerlaubte Frame-Trennzeichen geprüft. Da ON4KST pro TCP-Session nur einen Locator verwaltet, wird für beide Chat-Kategorien der Hauptlocator verwendet und eine abweichende zweite Konfiguration protokolliert, statt widersprüchliche Befehle an den Server zu senden.
+11 -1
View File
@@ -163,7 +163,7 @@ Die Schaltfläche **Send test spot** erzeugt folgenden Testeintrag:
```text
Spotted callsign: DO5AMF
Comment: Testing DXC-Spot: Congrats, you donated $100!
Comment: DXC test: You donated $100!
Frequency: .300 des konfigurierten Fallback-Bandes
```
@@ -191,8 +191,18 @@ Ein Spot enthält:
- den Locator,
- die aktuelle UTC-Zeit.
Die Nutzzeile folgt einem festen, DXSpider-kompatiblen 75-Zeichen-Format. Das DX-Rufzeichen beginnt in Spalte 27, das Kommentarfeld umfasst genau 30 Zeichen und die UTC-Zeit beginnt in Spalte 71. Kurze Kommentare werden mit Leerzeichen aufgefüllt, längere kontrolliert auf 30 Zeichen begrenzt. Unterschiedlich lange Spotter-Rufzeichen und Frequenzen bis 24 GHz verschieben die nachfolgenden Felder nicht.
Das vollständige DX-Rufzeichen wird nicht abgeschnitten. Ist es länger als zwölf Zeichen, verwirft KST4Contest den Spot stattdessen kontrolliert und protokolliert den Grund.
Bei automatisch erzeugten Richtungs-Spots kann KST4Contest bis zu zwei aktuelle AirScout-Einträge als zusätzliche AP-Information in den Kommentar aufnehmen. Fehlende AirScout-Daten verhindern den Spot nicht. Ein manuell über die Stationskarte ausgelöster Spot verwendet den Locator der ausgewählten Station ohne diese optionale Ergänzung.
Ein kompakter Kommentar mit AirScout-Information sieht beispielsweise so aus:
```text
JO51HK AP 1m/100%;4m/75%
```
---
## Wenn kein Spot erscheint
+3 -1
View File
@@ -379,12 +379,14 @@ Folgende Einstellungen und Schaltflächen gehören zur lokalen DX-Cluster-Ausgab
```text
Spotted callsign: DO5AMF
Comment: Testing DXC-Spot: Congrats, you donated $100!
Comment: DXC test: You donated $100!
Frequency: .300 des ausgewählten Fallback-Bandes
```
Bei einem Fallback-Band von `144 MHz` wird daraus beispielsweise eine Frequenz von ungefähr `144.300 MHz`.
Alle Spots verwenden eine feste, DXSpider-kompatible 75-Zeichen-Nutzzeile mit einem 30 Zeichen breiten Kommentarfeld. Längere Kommentare werden an dieser Protokollgrenze kontrolliert gekürzt; das DX-Rufzeichen wird dagegen nicht abgeschnitten. Rufzeichen mit mehr als zwölf Zeichen führen dazu, dass der betreffende Spot verworfen und protokolliert wird.
Der Kommentar des Testspots ist ein bewusst beibehaltenes Easteregg. Er hat keine technische Bedeutung und löst trotz seiner erfreulich konkreten Formulierung keine Zahlung aus. Entscheidend ist, dass der Spot im verbundenen Logprogramm erscheint.
Der Test funktioniert nur, wenn
+2
View File
@@ -44,6 +44,8 @@ v1.42 brings several previously separate calculations together. Band information
### Changed
- **Unified DX Cluster line format:** Local spots now use a fixed, DXSpider-compatible 75-character payload line with the DX callsign in column 27, a 30-character comment field and UTC time in column 71. The layout remains stable for microwave frequencies up to 24 GHz. Overlong DX callsigns are rejected and logged instead of being silently truncated; AirScout and test comments are correspondingly more compact.
- **Session-based ON4KST connection lifecycle:** Each socket, reader, writer, message bus and queue now belongs to an explicitly identified connection session. Delayed threads from an obsolete connection can therefore no longer process data or close its replacement. `ONLINE` is reported only after the login has been accepted and all requested user lists have been received. Connection setup, login and synchronisation use bounded timeouts, while heartbeats, missing inbound traffic, EOF and read or write failures trigger controlled reconnect attempts with backoff where appropriate.
- **Validated ON4KST protocol commands:** Outgoing frames are built centrally and checked for valid categories, locators and prohibited frame delimiters. Because ON4KST maintains one locator per TCP session, the main locator is used for both chat categories and a conflicting secondary configuration is logged instead of sending contradictory commands to the server.
+3 -1
View File
@@ -428,12 +428,14 @@ The following settings and controls belong to the local DX cluster output:
```text
Spotted callsign: DO5AMF
Comment: Testing DXC-Spot: Congrats, you donated $100!
Comment: DXC test: You donated $100!
Frequency: .300 on the selected fallback band
```
With `144 MHz` selected as the fallback band, the resulting frequency is approximately `144.300 MHz`.
All spots use a fixed, DXSpider-compatible 75-character payload line with a 30-character comment field. Longer comments are deliberately truncated at this protocol boundary; the DX callsign is not. A callsign longer than twelve characters causes the affected spot to be rejected and logged.
The comment is a deliberately retained Easter egg. It has no technical meaning and, despite being remarkably specific, does not initiate a payment. Its practical purpose is to make the test spot easy to identify in the logging software.
The test works only if
+6 -2
View File
@@ -163,7 +163,7 @@ The **Send test spot** button creates the following test entry:
```text
Spotted callsign: DO5AMF
Comment: Testing DXC-Spot: Congrats, you donated $100!
Comment: DXC test: You donated $100!
Frequency: .300 on the configured fallback band
```
@@ -191,12 +191,16 @@ A spot contains:
- the locator; and
- the current UTC time.
The payload line uses a fixed, DXSpider-compatible 75-character format. The DX callsign starts in column 27, the comment field is exactly 30 characters wide and the UTC time starts in column 71. Short comments are padded with spaces; longer ones are deliberately limited to 30 characters. Different spotter-callsign lengths and frequencies up to 24 GHz do not move the following fields.
KST4Contest does not truncate the complete DX callsign. If it exceeds twelve characters, the spot is rejected in a controlled manner and the reason is logged.
For automatically generated directional spots, KST4Contest can add up to two current AirScout entries to the comment. Missing AirScout data does not prevent the spot from being sent. A spot triggered manually from the station map uses the selected station's locator without this optional addition.
An automatic comment with AirScout information may look like this:
```text
JN49GL , AP: 1min, 100%; 4min, 75%
JO51HK AP 1m/100%;4m/75%
```
---
@@ -0,0 +1,165 @@
package kst4contest.controller;
import java.nio.charset.StandardCharsets;
import java.util.Locale;
/**
* Formats local DX Cluster spots using the fixed-column layout emitted by
* DXSpider and accepted by common logging programs.
*/
final class DXClusterSpotFormatter {
/** Length of the DX Cluster line before BEL and CRLF framing. */
/* package */
static final int LINE_LENGTH = 75;
/** One-based column in which the spotted callsign starts. */
/* package */
static final int DX_CALL_COLUMN = 27;
/** Width of the fixed comment field. */
/* package */
static final int COMMENT_LENGTH = 30;
/** One-based column in which the UTC time starts. */
/* package */
static final int TIME_COLUMN = 71;
/** Zero-based exclusive end position of the frequency field. */
private static final int FREQUENCY_END = 24;
/** Maximum width of the spotted callsign field. */
private static final int DX_CALL_LENGTH = 12;
/** Required width of the HHMMZ time field. */
private static final int TIME_LENGTH = 5;
/** Minimum separator width between spotter and frequency. */
private static final int MIN_FREQUENCY_GAP = 1;
/** Wire framing appended to every formatted line. */
private static final String PAYLOAD_SUFFIX = "\u0007\u0007\r\n";
private DXClusterSpotFormatter() {
}
/** Builds the fixed 75-character payload line without wire framing. */
/* package */
static String formatLine(
final String spotterCallSign,
final String frequency,
final String dxCallSign,
final String comment,
final String time
) {
final String spotter = requireValue(
spotterCallSign,
"spotter callsign"
)
.toUpperCase(Locale.ROOT);
final String frequencyValue = requireValue(frequency, "frequency");
final String dxCall = requireValue(dxCallSign, "DX callsign")
.toUpperCase(Locale.ROOT);
final String timeValue = requireValue(time, "time");
validateDxCall(dxCall);
validateTime(timeValue);
final String prefix = "DX de " + spotter + ":";
final int frequencyPadding = calculateFrequencyPadding(
prefix,
frequencyValue
);
final String normalizedComment = normalizeComment(comment);
final String line = prefix
+ " ".repeat(frequencyPadding)
+ frequencyValue
+ " "
+ padRight(dxCall, DX_CALL_LENGTH)
+ " "
+ padRight(normalizedComment, COMMENT_LENGTH)
+ " "
+ timeValue;
if (line.length() != LINE_LENGTH) {
throw new IllegalStateException(
"DX Cluster formatter produced "
+ line.length()
+ " characters instead of "
+ LINE_LENGTH
);
}
return line;
}
/** Builds one complete ASCII spot payload including BEL and CRLF framing. */
/* package */
static byte[] formatPayload(
final String spotterCallSign,
final String frequency,
final String dxCallSign,
final String comment,
final String time
) {
return (formatLine(
spotterCallSign,
frequency,
dxCallSign,
comment,
time
) + PAYLOAD_SUFFIX).getBytes(StandardCharsets.US_ASCII);
}
private static void validateDxCall(final String dxCall) {
if (dxCall.length() > DX_CALL_LENGTH) {
throw new IllegalArgumentException(
"DX callsign exceeds 12 characters: " + dxCall
);
}
}
private static void validateTime(final String time) {
if (time.length() != TIME_LENGTH) {
throw new IllegalArgumentException(
"DX Cluster time must contain exactly five characters"
);
}
}
private static int calculateFrequencyPadding(
final String prefix,
final String frequency
) {
final int padding = FREQUENCY_END
- prefix.length()
- frequency.length();
if (padding < MIN_FREQUENCY_GAP) {
throw new IllegalArgumentException(
"Spotter callsign and frequency do not fit the DX Cluster prefix"
);
}
return padding;
}
private static String normalizeComment(final String comment) {
final String normalized = comment == null ? "" : comment.trim();
return normalized.length() > COMMENT_LENGTH
? normalized.substring(0, COMMENT_LENGTH)
: normalized;
}
private static String requireValue(
final String value,
final String fieldName
) {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException(
"DX Cluster " + fieldName + " is missing"
);
}
return value.trim();
}
private static String padRight(final String value, final int length) {
return value + " ".repeat(length - value.length());
}
}
@@ -152,6 +152,7 @@ public class DXClusterThreadPooledServer implements Runnable {
public boolean broadcastSingleDXClusterEntryToLoggers(
ChatMember chatMember
) {
final byte[] clusterPayload;
final String clusterMessage;
try {
@@ -162,24 +163,27 @@ public class DXClusterThreadPooledServer implements Runnable {
.getNotify_optionalFrequencyPrefix()
);
clusterMessage =
"DX de "
+ chatController
clusterPayload = DXClusterSpotFormatter.formatPayload(
chatController
.getChatPreferences()
.getNotify_DXCSrv_SpottersCallSign()
.getValue()
+ ": "
+ frequency
+ " "
+ chatMember.getCallSign().toUpperCase()
+ " "
+ chatMember.getQra().toUpperCase()
+ " "
+ new Utils4KST()
.getValue(),
frequency,
chatMember.getCallSign(),
chatMember.getQra(),
new Utils4KST()
.time_generateCurrenthhmmZTimeStringForClusterMessage()
+ ((char) 7)
+ ((char) 7)
+ "\r\n";
);
clusterMessage = new String(
clusterPayload,
StandardCharsets.US_ASCII
);
} catch (IllegalArgumentException exception) {
LOGGER.log(
Level.WARNING,
"DX Cluster spot rejected: " + exception.getMessage()
);
return false;
} catch (Exception exception) {
LOGGER.log(
Level.SEVERE,
@@ -204,11 +208,7 @@ public class DXClusterThreadPooledServer implements Runnable {
try {
OutputStream output = socket.getOutputStream();
output.write(
clusterMessage.getBytes(
StandardCharsets.US_ASCII
)
);
output.write(clusterPayload);
output.flush();
deliveredClients++;
} catch (IOException exception) {
@@ -360,4 +360,4 @@ class DXClusterServerWorkerRunnable implements Runnable {
}
}
}
}
}
@@ -1,44 +0,0 @@
package kst4contest.controller;
import javafx.beans.property.SimpleStringProperty;
import kst4contest.model.ChatMember;
import kst4contest.model.ChatPreferences;
public class DXClusterThreadPooledServerTest {
public static void main(String[] args) {
ChatController client = new ChatController();
ChatPreferences testPreferences = new ChatPreferences();
testPreferences.setStn_loginCallSign("DM5M");
client.setChatPreferences(testPreferences);
DXClusterThreadPooledServer dxClusterServer = new DXClusterThreadPooledServer(8000, client, client);
new Thread(dxClusterServer).start();
try {
Thread.sleep(10 * 1000);
System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>ready.....go!");
} catch (InterruptedException e) {
e.printStackTrace();
}
ChatMember test = new ChatMember();
test.setCallSign("DL5ASG");
test.setQra("JO51HK");
test.setFrequency(new SimpleStringProperty("144776.0"));
dxClusterServer.broadcastSingleDXClusterEntryToLoggers(test);
// try {
// Thread.sleep(20 * 3333);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// System.out.println("Stopping Server");
// server.stop();
}
}
@@ -1847,14 +1847,14 @@ public class MessageBusManagementThread extends Thread {
* @param sender station for which the DX Cluster spot is generated
* @return locator with up to two optional AP entries
*/
private String buildDxClusterSpotComment(ChatMember sender) {
static String buildDxClusterSpotComment(ChatMember sender) {
if (sender == null) {
return "";
}
String locator = sender.getQra() == null
? ""
: sender.getQra().trim();
: sender.getQra().trim().toUpperCase(Locale.ROOT);
AirPlaneReflectionInfo reflectionInfo =
sender.getAirPlaneReflectInfo();
@@ -1882,7 +1882,7 @@ public class MessageBusManagementThread extends Thread {
aircraftComments.add(
aircraft.getArrivingDurationMinutes()
+ "min, "
+ "m/"
+ aircraft.getPotential()
+ "%"
);
@@ -1893,11 +1893,11 @@ public class MessageBusManagementThread extends Thread {
}
String apComment =
"AP: " + String.join("; ", aircraftComments);
"AP " + String.join(";", aircraftComments);
return locator.isEmpty()
? apComment
: locator + " , " + apComment;
: locator + " " + apComment;
}
/**
@@ -10736,7 +10736,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
testSpot.setFrequency(
new SimpleStringProperty("300")
);
testSpot.setQra("Testing DXC-Spot: Congrats, you donated $100!");
testSpot.setQra("DXC test: You donated $100!");
testSpot.setCallSign("DO5AMF");
if (!dxClusterServer
@@ -0,0 +1,138 @@
package kst4contest.controller;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.nio.charset.StandardCharsets;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
class DXClusterSpotFormatterTest {
@ParameterizedTest
@MethodSource("supportedFrequencies")
void keepsFixedColumnsAcrossSupportedFrequencies(
String spotter,
String frequency
) {
String line = DXClusterSpotFormatter.formatLine(
spotter,
frequency,
"DL5ASG",
"JO51HK",
"1234Z"
);
assertEquals(DXClusterSpotFormatter.LINE_LENGTH, line.length());
assertEquals(
"DL5ASG",
line.substring(
DXClusterSpotFormatter.DX_CALL_COLUMN - 1,
DXClusterSpotFormatter.DX_CALL_COLUMN - 1 + 6
)
);
assertEquals(
"JO51HK",
line.substring(39, 45)
);
assertEquals(
"1234Z",
line.substring(DXClusterSpotFormatter.TIME_COLUMN - 1)
);
assertEquals(
frequency,
line.substring(0, 24).trim().replaceFirst("^DX de .+?:\\s*", "")
);
}
@Test
void padsShortCommentsAndTruncatesLongCommentsToThirtyCharacters() {
String shortLine = DXClusterSpotFormatter.formatLine(
"DM5M",
"144205.0",
"DL5ASG",
"JO51HK",
"1234Z"
);
String longLine = DXClusterSpotFormatter.formatLine(
"DM5M",
"144205.0",
"DL5ASG",
"123456789012345678901234567890EXTRA",
"1234Z"
);
assertEquals(
"JO51HK" + " ".repeat(24),
shortLine.substring(39, 69)
);
assertEquals(
"123456789012345678901234567890",
longLine.substring(39, 69)
);
}
@Test
void keepsVariableDxCallsignsWithoutMovingTheComment() {
String twelveCharacterLine = DXClusterSpotFormatter.formatLine(
"DO5AMF",
"24048100.0",
"ABCDEFGHIJKL",
"JO51HK AP 1m/100%;4m/75%",
"2359Z"
);
assertEquals("ABCDEFGHIJKL", twelveCharacterLine.substring(26, 38));
assertEquals(
"JO51HK AP 1m/100%;4m/75%" + " ".repeat(6),
twelveCharacterLine.substring(39, 69)
);
assertThrows(
IllegalArgumentException.class,
() -> DXClusterSpotFormatter.formatLine(
"DO5AMF",
"144205.0",
"ABCDEFGHIJKLM",
"JO51HK",
"2359Z"
)
);
}
@Test
void appendsExactlyTwoBellCharactersAndCrlf() {
byte[] payload = DXClusterSpotFormatter.formatPayload(
"DM5M",
"50200.0",
"DL5ASG",
"JO51HK",
"0000Z"
);
assertEquals(DXClusterSpotFormatter.LINE_LENGTH + 4, payload.length);
assertEquals(7, payload[75]);
assertEquals(7, payload[76]);
assertEquals('\r', payload[77]);
assertEquals('\n', payload[78]);
assertEquals(
75,
new String(payload, 0, 75, StandardCharsets.US_ASCII).length()
);
}
private static Stream<Arguments> supportedFrequencies() {
return Stream.of(
Arguments.of("DM5M", "50200.0"),
Arguments.of("DO5AMF", "70250.0"),
Arguments.of("DM5M", "144205.0"),
Arguments.of("DO5AMF", "432088.0"),
Arguments.of("DM5M", "1296338.0"),
Arguments.of("DO5AMF", "10368100.0"),
Arguments.of("DO5AMF", "24048100.0")
);
}
}
@@ -0,0 +1,51 @@
package kst4contest.controller;
import static org.junit.jupiter.api.Assertions.assertEquals;
import javafx.collections.FXCollections;
import kst4contest.model.AirPlane;
import kst4contest.model.AirPlaneReflectionInfo;
import kst4contest.model.ChatMember;
import org.junit.jupiter.api.Test;
class MessageBusManagementThreadDxClusterCommentTest {
@Test
void keepsLocatorAndAddsCompactAirScoutInformation() {
ChatMember sender = new ChatMember();
sender.setQra("jo51hk");
AirPlane firstAircraft = new AirPlane();
firstAircraft.setArrivingDurationMinutes(1);
firstAircraft.setPotential(100);
AirPlane secondAircraft = new AirPlane();
secondAircraft.setArrivingDurationMinutes(4);
secondAircraft.setPotential(75);
AirPlaneReflectionInfo reflectionInfo = new AirPlaneReflectionInfo();
reflectionInfo.setRisingAirplanes(
FXCollections.observableArrayList(
firstAircraft,
secondAircraft
)
);
sender.setAirPlaneReflectInfo(reflectionInfo);
assertEquals(
"JO51HK AP 1m/100%;4m/75%",
MessageBusManagementThread.buildDxClusterSpotComment(sender)
);
}
@Test
void returnsLocatorWhenAirScoutInformationIsMissing() {
ChatMember sender = new ChatMember();
sender.setQra("JO51HK");
assertEquals(
"JO51HK",
MessageBusManagementThread.buildDxClusterSpotComment(sender)
);
}
}
+5 -1
View File
@@ -141,10 +141,12 @@ Every generated spot contains:
Automatically generated directional spots can additionally include up to two current AirScout entries. A manually triggered map spot uses the selected station's locator without this optional addition.
The payload is a fixed, DXSpider-compatible 75-character line. The DX callsign begins in column 27, the 30-character comment begins in column 40 and the UTC time begins in column 71. Short comments are padded; longer comments are limited to the available field. A DX callsign longer than twelve characters is rejected rather than silently truncated.
An example comment with AirScout information may look like this:
```text
JN49GL , AP: 1min, 100%; 4min, 75%
JO51HK AP 1m/100%;4m/75%
```
AirScout information is optional. A missing AirScout response does not prevent an automatic directional spot from being sent.
@@ -199,6 +201,8 @@ A missing password is acceptable inside the intended station network. It is not
Use **Send test spot** after the logger has connected.
The test uses `DO5AMF` with the comment `DXC test: You donated $100!` and `.300` on the configured fallback band.
A successful test confirms that at least one client received the generated spot. If the test works but real spots do not appear, the TCP connection is probably not the problem. In that case, check the conditions used for the actual directional opportunity:
- Were valid locators available?