Skip to content

Commit d7882b6

Browse files
committed
Begin WSPRCodex Implementation
1 parent a4a0311 commit d7882b6

File tree

1 file changed

+146
-0
lines changed
  • Codex/src/main/java/org/operatorfoundation/codex

1 file changed

+146
-0
lines changed
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
package org.operatorfoundation.Codex
2+
3+
import org.operatorfoundation.codex.Decoder
4+
import org.operatorfoundation.codex.Encoder
5+
import org.operatorfoundation.codex.symbols.Number
6+
import org.operatorfoundation.codex.symbols.*
7+
import java.math.BigInteger
8+
9+
/**
10+
* WSPRCodex provides encoding and decoding of arbitrary byte data as WSPR messages.
11+
*
12+
* This class uses the Codex symbol system to encode binary data into the standard
13+
* WSPR message format (callsign, grid square, power level). The encoding is reversible,
14+
* allowing data to be transmitted via WSPR and recovered on the receiving end.
15+
*
16+
* WSPR Message Format:
17+
* - Callsign: 6 characters (letters, numbers, spaces)
18+
* - Grid Square: 4 characters (letters and numbers for Maidenhead locator)
19+
* - Power: 2 characters (power level in dBm: 0, 3, 7, 10, 13... 60)
20+
*
21+
* Example Usage:
22+
* ```kotlin
23+
* val codex = WSPRCodex()
24+
* val plaintext = "Hello WSPR!"
25+
* val encrypted = encryptData(plaintext) // External encryption
26+
*
27+
* // Encode to WSPR message
28+
* val wsprMessage = codex.encode(encrypted)
29+
* println("${wsprMessage.callsign} ${wsprMessage.gridSquare} ${wsprMessage.powerDbm}")
30+
*
31+
* // Decode back to original data
32+
* val decoded = codex.decode(wsprMessage)
33+
* val decrypted = decryptData(decoded) // External decryption
34+
* ```
35+
*/
36+
class WSPRCodex
37+
{
38+
companion object
39+
{
40+
/**
41+
* WSPR symbol configuration matching the standard WSPR message format.
42+
*
43+
* Symbol breakdown:
44+
* - Required('Q'): Fixed prefix (1 byte, size=1, contributes 0 bits)
45+
* - CallLetterNumber (5x): Callsign characters (A-Z, 0-9 = 36 values each)
46+
* - GridLetter (2x): First two grid characters (A-R = 18 values each)
47+
* - Number (2x): Last two grid characters (0-9 = 10 values each)
48+
* - Power: Power level (19 discrete values: 0, 3, 7, 10... 60 dBm)
49+
*/
50+
private val WSPR_SYMBOLS: List<Symbol> = listOf<Symbol>(
51+
Required('Q'.code.toByte()), // Fixed prefix
52+
CallLetterNumber(), // Callsign char 1
53+
CallLetterNumber(), // Callsign char 2
54+
CallLetterNumber(), // Callsign char 3
55+
CallLetterNumber(), // Callsign char 4
56+
CallLetterNumber(), // Callsign char 5
57+
CallLetterNumber(), // Callsign char 6 (often space)
58+
GridLetter(), // Grid char 1
59+
GridLetter(), // Grid char 2
60+
Number(), // Grid char 3
61+
Number(), // Grid char 4
62+
Power() // Power level
63+
)
64+
65+
/**
66+
* Calculates the maximum number of bytes that can be encoded in a single WSPR message.
67+
*
68+
* This is determined by the product of all symbol sizes (excluding size=1 symbols):
69+
* Capacity = log2(36^6 × 18^2 × 10^2 × 19) / 8 bytes
70+
*
71+
* @return Maximum payload size in bytes
72+
*/
73+
fun getMaxPayloadBytes(): Int
74+
{
75+
// Calculate total capacity in bits
76+
val symbolSizes = WSPR_SYMBOLS.map { it.size().toBigInteger() }
77+
val totalCapacity = symbolSizes.fold(BigInteger.ONE) { acc, size -> acc * size }
78+
79+
// Convert to bits (log2 of total capacity)
80+
val capacityBits = totalCapacity.bitLength() - 1
81+
82+
// Convert to bytes (divide by 8)
83+
return capacityBits / 8
84+
}
85+
86+
/**
87+
* Gets the symbol configuration used for WSPR encoding.
88+
* Useful for testing and capacity calculations.
89+
*
90+
* @return List of symbols defining the WSPR message structure
91+
*/
92+
fun getSymbolList(): List<Symbol> = WSPR_SYMBOLS
93+
}
94+
95+
// Encoder and decoder instances configured with WSPR symbols
96+
private val encoder = Encoder(WSPR_SYMBOLS)
97+
private val decoder = Decoder(WSPR_SYMBOLS)
98+
99+
fun encode(data: ByteArray): WSPRDataMessage
100+
{
101+
// TODO: Not implemented
102+
}
103+
104+
fun decode(message: WSPRDataMessage): ByteArray
105+
{
106+
// TODO: Not implemented
107+
}
108+
}
109+
110+
/**
111+
* Data class representing a WSPR message with encoded data.
112+
*
113+
* This structure mirrors the format used by AudioCoder's CJarInterface
114+
* for WSPR encoding and can be directly passed to audio generation methods.
115+
*
116+
* @property callsign Amateur radio callsign (6 characters, letters/numbers/spaces)
117+
* @property gridSquare Maidenhead grid locator (4 characters, e.g., "FN31")
118+
* @property powerDbm Transmitter power in dBm (0, 3, 7, 10, 13... 60)
119+
*/
120+
data class WSPRDataMessage(
121+
val callsign: String,
122+
val gridSquare: String,
123+
val powerDbm: Int
124+
)
125+
{
126+
/**
127+
* Formats the WSPR message in standard display format.
128+
* This is the format typically shown to operators and used in logs.
129+
*
130+
* Example: "K1ABC FN31 23"
131+
*/
132+
override fun toString(): String = "$callsign $gridSquare $powerDbm"
133+
134+
/**
135+
* Validates that the message fields contain valid WSPR data.
136+
*
137+
* @return true if all fields are valid, false otherwise
138+
*/
139+
fun isValid(): Boolean
140+
{
141+
return callsign.length <= 6 &&
142+
callsign.all { it.isLetterOrDigit() || it == ' ' } &&
143+
gridSquare.length == 4 &&
144+
powerDbm in 0..60
145+
}
146+
}

0 commit comments

Comments
 (0)