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