Skip to content

Commit 3a64fb9

Browse files
committed
fromWSPRFields() <-> toWSPRFields()
1 parent 48f629d commit 3a64fb9

8 files changed

Lines changed: 462 additions & 3 deletions

File tree

Codex/src/main/java/org/operatorfoundation/codex/symbols/CallLetter.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,20 @@ class CallLetter(val value: Char) : Symbol {
2626
?: throw IllegalArgumentException("Unknown value $numericValue for CallLetter")
2727
return CallLetter(char)
2828
}
29+
30+
/**
31+
* Creates a CallLetter from a character value.
32+
*
33+
* @param char The character (A-Z)
34+
* @return CallLetter instance
35+
* @throws IllegalArgumentException if char is not a valid CallLetter
36+
*/
37+
fun fromChar(char: Char): CallLetter
38+
{
39+
val upperChar = char.uppercaseChar()
40+
if (charToValue.containsKey(upperChar)) return CallLetter(upperChar)
41+
throw IllegalArgumentException("Invalid CallLetter character: $char")
42+
}
2943
}
3044

3145
override fun toString(): String = "CallLetter($value)"

Codex/src/main/java/org/operatorfoundation/codex/symbols/CallNumber.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,19 @@ class CallNumber(val value: Char) : Symbol {
2323
}
2424
throw IllegalArgumentException("Unknown value $numericValue for Number")
2525
}
26+
27+
/**
28+
* Creates a CallNumber from a character value.
29+
*
30+
* @param char The character (0-9)
31+
* @return CallNumber instance
32+
* @throws IllegalArgumentException if char is not a valid digit
33+
*/
34+
fun fromChar(char: Char): CallNumber
35+
{
36+
if (charToValue.containsKey(char)) return CallNumber(char)
37+
throw IllegalArgumentException("Invalid CallNumber character: $char")
38+
}
2639
}
2740

2841
override fun toString(): String = "Number($value)"

Codex/src/main/java/org/operatorfoundation/codex/symbols/GridLetter.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,20 @@ class GridLetter(val value: Char) : Symbol {
2525
}
2626
throw IllegalArgumentException("Unknown value $numericValue for GridLetter")
2727
}
28+
29+
/**
30+
* Creates a GridLetter from a character value.
31+
*
32+
* @param char The character (A-R)
33+
* @return GridLetter instance
34+
* @throws IllegalArgumentException if char is not a valid GridLetter
35+
*/
36+
fun fromChar(char: Char): GridLetter
37+
{
38+
val upperChar = char.uppercaseChar()
39+
if (charToValue.containsKey(upperChar)) return GridLetter(upperChar)
40+
throw IllegalArgumentException("Invalid GridLetter character: $char")
41+
}
2842
}
2943

3044
override fun toString(): String = "GridLetter($value)"

Codex/src/main/java/org/operatorfoundation/codex/symbols/GridNumber.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,19 @@ class GridNumber(val value: Char) : Symbol {
2323
}
2424
throw IllegalArgumentException("Unknown value $numericValue for Number")
2525
}
26+
27+
/**
28+
* Creates a GridNumber from a character value.
29+
*
30+
* @param char The character (0-9)
31+
* @return GridNumber instance
32+
* @throws IllegalArgumentException if char is not a valid digit
33+
*/
34+
fun fromChar(char: Char): GridNumber
35+
{
36+
if (charToValue.containsKey(char)) return GridNumber(char)
37+
throw IllegalArgumentException("Invalid GridNumber character: $char")
38+
}
2639
}
2740

2841
override fun toString(): String = "Number($value)"

Codex/src/main/java/org/operatorfoundation/codex/symbols/Power.kt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,31 @@ class Power(val value: Int) : Symbol {
1313
)
1414
private val powerToIndex = indexToPower.map { (k, v) -> v to k }.toMap()
1515

16+
/** Set of valid WSPR power levels in dBm */
17+
val validPowerLevels: Set<Int> = powerToIndex.keys
18+
1619
override fun size(): BigInteger = 19.toBigInteger()
1720

1821
override fun encode(numericValue: BigInteger): Power {
1922
val powerValue = indexToPower[numericValue.toInt()]
2023
?: throw IllegalArgumentException("Invalid index: $numericValue")
2124
return Power(powerValue)
2225
}
26+
27+
/**
28+
* Creates a Power from a dBm value.
29+
*
30+
* @param dbm The power level in dBm (must be a valid WSPR power level)
31+
* @return Power instance
32+
* @throws IllegalArgumentException if dbm is not a valid WSPR power level
33+
*/
34+
fun fromDbm(dbm: Int): Power
35+
{
36+
if (powerToIndex.containsKey(dbm)) return Power(dbm)
37+
throw IllegalArgumentException(
38+
"Invalid power level: $dbm dBm. Valid levels: ${validPowerLevels.sorted()}"
39+
)
40+
}
2341
}
2442

2543
override fun toString(): String = "Power($value)"

Codex/src/main/java/org/operatorfoundation/codex/symbols/WSPRMessage.kt

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,61 @@ class WSPRMessage(
9999

100100
return WSPRMessage(required, callsign1, callsign2, callsign3, callsign4, callsign5, grid1, grid2, grid3, grid4, power)
101101
}
102+
103+
/**
104+
* Creates a WSPRMessage from WSPR transmission fields.
105+
*
106+
* This is the inverse of toWSPRFields() - it reconstructs a WSPRMessage
107+
* from the callsign, grid square, and power level received from a WSPR decode.
108+
*
109+
* @param callsign The 6-character callsign (e.g., "QA0BCD") - must start with 'Q'
110+
* @param gridSquare The 4-character grid square (e.g., "FN31")
111+
* @param powerDbm The power level in dBm (must be a valid WSPR power level)
112+
* @return WSPRMessage instance
113+
* @throws IllegalArgumentException if any parameter is invalid
114+
*/
115+
fun fromWSPRFields(callsign: String, gridSquare: String, powerDbm: Int): WSPRMessage
116+
{
117+
// Validate and parse callsign
118+
val trimmedCallsign = callsign.trim().uppercase()
119+
120+
require(trimmedCallsign.length == 6) {
121+
"Callsign must be exactly 6 characters, got: '$callsign' (${trimmedCallsign.length})"
122+
}
123+
require(trimmedCallsign[0] == 'Q') {
124+
"Encoded WSPR callsign must start with 'Q', got: '$callsign'"
125+
}
126+
127+
// Validate and parse grid square
128+
val trimmedGrid = gridSquare.trim().uppercase()
129+
require(trimmedGrid.length == 4) {
130+
"Grid square must be exactly 4 characters, got: '$gridSquare' (${trimmedGrid.length})"
131+
}
132+
133+
// Build the WSPRMessage from parsed values
134+
val prefix = Required('Q')
135+
val callsign1 = CallLetter.fromChar(trimmedCallsign[1])
136+
val callsign2 = CallNumber.fromChar(trimmedCallsign[2])
137+
val callsign3 = CallLetter.fromChar(trimmedCallsign[3])
138+
val callsign4 = CallLetter.fromChar(trimmedCallsign[4])
139+
val callsign5 = CallLetter.fromChar(trimmedCallsign[5])
140+
141+
val grid1 = GridLetter.fromChar(trimmedGrid[0])
142+
val grid2 = GridLetter.fromChar(trimmedGrid[1])
143+
val grid3 = GridNumber.fromChar(trimmedGrid[2])
144+
val grid4 = GridNumber.fromChar(trimmedGrid[3])
145+
146+
val power = Power.fromDbm(powerDbm)
147+
148+
return WSPRMessage(
149+
prefix, callsign1, callsign2, callsign3, callsign4, callsign5,
150+
grid1, grid2, grid3, grid4, power
151+
)
152+
}
153+
154+
fun isEncodedMessage(callsign: String): Boolean {
155+
return callsign.trim().uppercase().startsWith("Q")
156+
}
102157
}
103158

104159
override fun toString(): String = "WSPRMessage(${prefix.value}, " +
@@ -152,7 +207,7 @@ class WSPRMessage(
152207
return result
153208
}
154209

155-
fun extractValues(): Triple<String, String, Int> {
210+
fun toWSPRFields(): Triple<String, String, Int> {
156211
val callsign = "Q${callsign1.value}${callsign2.value}${callsign3.value}${callsign4.value}${callsign5.value}"
157212
val grid = "${grid1.value}${grid2.value}${grid3.value}${grid4.value}"
158213
val power = power.value

Codex/src/main/java/org/operatorfoundation/codex/symbols/WSRPMessageSequence.kt

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,40 @@ class WSPRMessageSequence(val messages: List<WSPRMessage>) : Symbol {
2626

2727
return WSPRMessageSequence(messages)
2828
}
29+
30+
/**
31+
* Creates a WSPRMessageSequence from WSPR transmission fields.
32+
*
33+
* This reconstructs a message sequence from a list of (callsign, grid, power) tuples
34+
* received from WSPR decodes. The messages must be provided in transmission order
35+
* (least significant first).
36+
*
37+
* @param fields List of triples containing (callsign, gridSquare, powerDbm)
38+
* @return WSPRMessageSequence instance
39+
* @throws IllegalArgumentException if any field is invalid
40+
*/
41+
fun fromWSPRFields(fields: List<Triple<String, String, Int>>): WSPRMessageSequence
42+
{
43+
require(fields.isNotEmpty()) { "Cannot create WSPRMessageSequence from empty list" }
44+
45+
val messages = fields.map { (callsign, grid, power) ->
46+
WSPRMessage.fromWSPRFields(callsign, grid, power)
47+
}
48+
49+
return WSPRMessageSequence(messages)
50+
}
51+
52+
/**
53+
* Creates a WSPRMessageSequence from a list of WSPRMessage objects.
54+
*
55+
* @param messages List of WSPRMessage objects in transmission order
56+
* @return WSPRMessageSequence instance
57+
*/
58+
fun fromMessages(messages: List<WSPRMessage>): WSPRMessageSequence
59+
{
60+
require(messages.isNotEmpty()) { "Cannot create WSPRMessageSequence from empty list" }
61+
return WSPRMessageSequence(messages)
62+
}
2963
}
3064

3165
override fun toString(): String = "WSPRMessageSequence(${messages.size})"
@@ -45,8 +79,21 @@ class WSPRMessageSequence(val messages: List<WSPRMessage>) : Symbol {
4579
return result
4680
}
4781

48-
fun extractValues(): List<Triple<String, String, Int>>
82+
/**
83+
* Decodes the message sequence to a byte array.
84+
*
85+
* This is a convenience method for decoding encrypted message data.
86+
* The BigInteger is converted to a byte array suitable for decryption.
87+
*
88+
* @return ByteArray containing the decoded data
89+
*/
90+
fun decodeToBytes(): ByteArray {
91+
val bigInt = decode()
92+
return bigInt.toByteArray()
93+
}
94+
95+
fun toWSPRFields(): List<Triple<String, String, Int>>
4996
{
50-
return messages.map { it.extractValues() }
97+
return messages.map { it.toWSPRFields() }
5198
}
5299
}

0 commit comments

Comments
 (0)