@@ -96,14 +96,162 @@ class WSPRCodex
9696 private val encoder = Encoder (WSPR_SYMBOLS )
9797 private val decoder = Decoder (WSPR_SYMBOLS )
9898
99+ /* *
100+ * Encodes a byte array as a WSPR message.
101+ *
102+ * The data is converted to a large integer and distributed across the WSPR
103+ * message fields according to the symbol configuration. The encoding is
104+ * deterministic and reversible.
105+ *
106+ * @param data Binary data to encode (e.g., encrypted message bytes)
107+ * @return WSPRDataMessage containing callsign, grid square, and power level
108+ * @throws WSPRCodexException if data exceeds maximum capacity
109+ */
99110 fun encode (data : ByteArray ): WSPRDataMessage
100111 {
101- // TODO: Not implemented
112+ // Validate payload size
113+ if (data.size > getMaxPayloadBytes())
114+ {
115+ throw WSPRCodexException (
116+ " Data size ${data.size} bytes exceeds maximum capacity of ${getMaxPayloadBytes()} bytes. " +
117+ " Consider splitting into multiple messages or reducing payload size."
118+ )
119+ }
120+
121+ // Convert byte array to BigInt for encoding
122+ // Uses big-endian byte order
123+ val dataAsInteger = BigInteger (1 , data) // 1 = positive
124+
125+ // Encode integer across WSPR symbols
126+ val encodedSymbols = encoder.encode(dataAsInteger)
127+
128+ // Parse encoded symbols into WSPR message fields
129+ return parseEncodedSymbolsToMessage(encodedSymbols)
102130 }
103131
132+
133+ /* *
134+ * Decodes a WSPR message back to the original byte array.
135+ *
136+ * Reverses the encoding process by converting the WSPR message fields
137+ * back to symbol representations, then decoding to the original integer
138+ * and finally to the byte array.
139+ *
140+ * @param message WSPR message to decode
141+ * @return Original byte array that was encoded
142+ * @throws WSPRCodexException if message format is invalid
143+ */
104144 fun decode (message : WSPRDataMessage ): ByteArray
105145 {
106- // TODO: Not implemented
146+ // Convert message fields back to encoded symbol format
147+ val encodedSymbols = convertMessageToEncodedSymbols(message)
148+
149+ // Decode symbols back to int
150+ val decodedInteger = decoder.decode(encodedSymbols)
151+
152+ // Convert integer back to byte array
153+ return decodedInteger.toByteArray().let { bytes ->
154+ // BigInteger.toByteArray() may include leading zero byte for sign
155+ // Remove it if present to get original data
156+ if (bytes.isNotEmpty() && bytes[0 ] == 0 .toByte())
157+ {
158+ bytes.copyOfRange(1 , bytes.size)
159+ }
160+ else
161+ {
162+ bytes
163+ }
164+ }
165+ }
166+
167+ // ========== Private Helper Methods ==========
168+
169+ /* *
170+ * Parses encoded symbols into a structured WSPR message.
171+ *
172+ * Symbol mapping:
173+ * - encodedSymbols[0]: Required 'Q' (ignored)
174+ * - encodedSymbols[1-6]: Callsign (6 characters)
175+ * - encodedSymbols[7-10]: Grid square (4 characters)
176+ * - encodedSymbols[11]: Power level (2-character representation)
177+ *
178+ * @param encodedSymbols List of ByteArrays from encoder
179+ * @return Structured WSPR message
180+ */
181+ private fun parseEncodedSymbolsToMessage (encodedSymbols : List <ByteArray >): WSPRDataMessage
182+ {
183+ require(encodedSymbols.size == WSPR_SYMBOLS .size)
184+ {
185+ " Invalid encoded symbol count: ${encodedSymbols.size} , expected: ${WSPR_SYMBOLS .size} "
186+ }
187+
188+ // Extract callsign (symbols 1-6)
189+ val callsign = buildString {
190+ for (i in 1 .. 6 )
191+ {
192+ append(encodedSymbols[i].decodeToString())
193+ }
194+ }
195+
196+ // Extract grid square (symbols 7 - 10)
197+ val gridSquare = buildString {
198+ for (i in 7 .. 10 )
199+ {
200+ append(encodedSymbols[i].decodeToString())
201+ }
202+ }
203+
204+ // Extract power level (Symbol 11)
205+ val powerDbm = encodedSymbols[11 ].decodeToString().toInt()
206+
207+ return WSPRDataMessage (
208+ callsign,
209+ gridSquare,
210+ powerDbm
211+ )
212+ }
213+
214+ /* *
215+ * Converts a WSPR message back to encoded symbol format for decoding.
216+ *
217+ * This reverses the parseEncodedSymbolsToMessage operation, reconstructing
218+ * the ByteArray list that the encoder originally produced.
219+ *
220+ * @param message WSPR message to convert
221+ * @return List of ByteArrays suitable for decoder
222+ */
223+ private fun convertMessageToEncodedSymbols (message : WSPRDataMessage ): List <ByteArray >
224+ {
225+ val symbols = mutableListOf<ByteArray >()
226+
227+ // Add the required prefix
228+ symbols.add(" Q" .toByteArray())
229+
230+ // Add callsign characters (exactly 6 characters)
231+ val paddedCallsign = message.callsign.padEnd(6 , ' ' )
232+ require(paddedCallsign.length == 6 )
233+ {
234+ " Callsign must be 6 characters or less: ${message.callsign} "
235+ }
236+
237+ paddedCallsign.forEach { char ->
238+ symbols.add(char.toString().toByteArray())
239+ }
240+
241+ // Add grid square characters (exactly 4 characters)
242+ require(message.gridSquare.length == 4 )
243+ {
244+ " Grid square must be exactly 4 characters: ${message.gridSquare} "
245+ }
246+
247+ message.gridSquare.forEach { char ->
248+ symbols.add(char.toString().toByteArray())
249+ }
250+
251+ // Add power level
252+ symbols.add(message.powerDbm.toString().toByteArray())
253+
254+ return symbols
107255 }
108256}
109257
@@ -143,4 +291,15 @@ data class WSPRDataMessage(
143291 gridSquare.length == 4 &&
144292 powerDbm in 0 .. 60
145293 }
146- }
294+ }
295+
296+ /* *
297+ * Exception thrown by WSPRCodex for encoding/decoding errors.
298+ *
299+ * @property message Descriptive error message
300+ * @property cause Optional underlying cause of the error
301+ */
302+ class WSPRCodexException (
303+ message : String ,
304+ cause : Throwable ? = null
305+ ) : Exception(message, cause)
0 commit comments