Skip to content

Commit a004376

Browse files
committed
1 Message WSPRCodex with Tests
1 parent c49458e commit a004376

File tree

5 files changed

+109
-52
lines changed

5 files changed

+109
-52
lines changed

Codex/src/main/java/org/operatorfoundation/codex/Decoder.kt

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -55,14 +55,10 @@ class Decoder(private val symbols: List<Symbol>)
5555
if (symbol.size() == 1)
5656
{
5757
// Symbols with size 1 don't contribute to the numeric value
58-
println("decode_step(encoded value: ${encodedValue.decodeToString()}, symbol: $symbol, index: $index)")
59-
6058
return 0.toBigInteger()
6159
}
6260
else
6361
{
64-
println("decode_step(encoded value: ${encodedValue.decodeToString()}, symbol: $symbol, index: $index)")
65-
6662
if (index == symbols.size - 1)
6763
{
6864
// Last symbol: just return its decoded value
@@ -73,13 +69,11 @@ class Decoder(private val symbols: List<Symbol>)
7369
// Calculate product of remaining symbol sizes
7470
val remainingSymbols = symbols.subList(index + 1, symbols.size)
7571
val remainingSymbolSizes = remainingSymbols.map { it.size() }
76-
val positionMultiplier = remainingSymbolSizes.fold(1) { acc, size -> acc * size }
77-
78-
println("history:/n remaining symbol sizes - $remainingSymbolSizes, position multiplier: $positionMultiplier")
72+
// Use BigInteger to avoid integer overflow when multiplying symbol sizes
73+
val positionMultiplier = remainingSymbolSizes.fold(BigInteger.ONE) { acc, size -> acc * size.toBigInteger() }
7974

8075
// Multiply decoded value by position weight
81-
val result = symbol.decode(encodedValue) * positionMultiplier.toBigInteger()
82-
println("result: $result")
76+
val result = symbol.decode(encodedValue) * positionMultiplier
8377

8478
return result
8579
}

Codex/src/main/java/org/operatorfoundation/codex/Encoder.kt

Lines changed: 3 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,6 @@ class Encoder(private val symbols: List<Symbol>)
6060
if (symbol.size() == 1)
6161
{
6262
// (size = 1) don't consume any of the value
63-
println("encode_step( current value: $currentValue, symbol: $symbol, index: $index)")
64-
6563
// For debugging: show symbol capacity up to this point
6664
val symbolsUpToHere = if (index == 0)
6765
{
@@ -71,56 +69,40 @@ class Encoder(private val symbols: List<Symbol>)
7169
{
7270
symbols.subList(0, symbols.size - index)
7371
}
74-
75-
val symbolSizes = symbolsUpToHere.map { it.size() }
76-
val totalCapacity = symbolSizes.fold(1) { acc, size -> acc * size }
77-
78-
println("history (symbol sizes): $symbolSizes, total capacity: $totalCapacity")
7972

8073
// Symbol with size 1 encodes its fixed value and passes through the current value
8174
val encodedBytes = symbol.encode(currentValue)
8275
val result = Pair(encodedBytes, currentValue)
8376

84-
println("result: (${encodedBytes.decodeToString()}, $currentValue)")
85-
8677
return result
8778
}
8879
else
8980
{
90-
println("encode_step(currrent value: $currentValue, symbol: $symbol, index: $index)")
91-
9281
if (index == symbols.size - 1)
9382
{
9483
// Last symbol: encode all remaining value
9584
val encodedBytes = symbol.encode(currentValue)
9685
val result = Pair(encodedBytes, 0.toBigInteger())
9786

98-
println("result: (${encodedBytes.decodeToString()}, 0)")
99-
10087
return result
10188
}
10289
else
10390
{
10491
// Calculate capacity of remaining symbols
10592
val remainingSymbols = symbols.subList(index + 1, symbols.size)
10693
val remainingSymbolSizes = remainingSymbols.map { it.size() }
107-
val remainingCapacity = remainingSymbolSizes.fold(1) { acc, size -> acc * size }
108-
109-
println("history:\n remaining symbol sizes $remainingSymbolSizes, remaining capacity: $remainingCapacity")
94+
val remainingCapacity = remainingSymbolSizes.fold(BigInteger.ONE) { acc, size -> acc * size.toBigInteger() }
11095

11196
// Determine value for this symbol position
11297
// Division gives us how many "chunks" of remaining capacity we have
113-
val symbolValue = (currentValue / remainingCapacity.toBigInteger()).min(symbol.size().toBigInteger() - 1.toBigInteger())
114-
println("Symbol value: $symbolValue")
98+
val symbolValue = (currentValue / remainingCapacity).min(symbol.size().toBigInteger() - BigInteger.ONE)
11599

116100
// Modulo gives us what's left for the remaining symbols
117-
val leftoverValue = currentValue % remainingCapacity.toBigInteger()
118-
println("Leftover value: $leftoverValue")
101+
val leftoverValue = currentValue % remainingCapacity
119102

120103
// Encode this symbol's portion
121104
val encodedBytes = symbol.encode(symbolValue)
122105
val result = Pair(encodedBytes, leftoverValue)
123-
println("result: (${encodedBytes.decodeToString()}, $leftoverValue)")
124106

125107
return result
126108
}

Codex/src/main/java/org/operatorfoundation/codex/WSPRCodex.kt

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -70,30 +70,35 @@ class WSPRCodex
7070
*/
7171
fun getMaxPayloadBytes(): Int
7272
{
73-
// DEBUG: Print out what symbols we have and their sizes
74-
println("=== Symbol Configuration Debug ===")
75-
WSPR_SYMBOLS.forEachIndexed { index, symbol ->
76-
println("Symbol $index: $symbol, size: ${symbol.size()}")
77-
}
73+
// The actual capacity depends on how the encoder distributes data across symbols
74+
// We need to calculate the maximum value that can flow through the encoding process
75+
// without overflow at any symbol position
7876

79-
// Calculate the product of all symbol sizes
80-
// e.g. if we have symbols with sizes [36, 36, 18, 10, 19]
81-
// total capacity = 36 x 36 x 18 x 10 x 19 (total number of unique calculations)
77+
// Start from the end and work backwards to find capacity at each position
78+
// Symbol with size=1 (Required) doesn't contribute to capacity
79+
val effectiveSymbols = WSPR_SYMBOLS.filter { it.size() > 1 }
8280

83-
// Convert list of symbols to list of their sizes
84-
val symbolSizes = WSPR_SYMBOLS.map { it.size().toBigInteger() }
85-
// Multiply all sizes together
86-
val totalCapacity = symbolSizes.fold(BigInteger.ONE) { acc, size -> acc * size}
81+
if (effectiveSymbols.isEmpty())
82+
{
83+
return 0
84+
}
85+
86+
// Calculate the product of all effective symbol sizes (the total number of unique values we can encode)
87+
var totalCapacity = BigInteger.ONE
88+
effectiveSymbols.forEach { symbol ->
89+
totalCapacity *= symbol.size().toBigInteger()
90+
}
8791

88-
// The max integer we can encode is (totalCapacity - 1)
92+
// The maximum value we can encode is (totalCapacity - 1)
93+
// because we count from 0
8994
val maxEncodableValue = totalCapacity - BigInteger.ONE
9095

91-
// Convert maxEncodableValue to bytes so we know how many bytes we need (how many bytes it takes to store this number)
96+
// Convert to byte array to see how many bytes this value requires
9297
val byteArray = maxEncodableValue.toByteArray()
9398

94-
// BigInteger sometimes adds an extra leading zero byte for the sign
95-
// Remove the extra byte if it exists
96-
return if (byteArray.isNotEmpty() && byteArray[0] == 0.toByte())
99+
// BigInteger.toByteArray() may include a leading zero byte for the sign bit
100+
// Remove it if present
101+
val actualBytes = if (byteArray.isNotEmpty() && byteArray[0] == 0.toByte())
97102
{
98103
byteArray.size - 1
99104
}
@@ -102,6 +107,7 @@ class WSPRCodex
102107
byteArray.size
103108
}
104109

110+
return actualBytes
105111
}
106112

107113
/**

Codex/src/test/java/org/operatorfoundation/codex/EncoderDecoderTest.kt

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,4 +175,34 @@ class EncoderDecoderTest
175175
}
176176
}
177177

178+
179+
// @Test
180+
// fun testRequiredSymbolValidation()
181+
// {
182+
// // Test that Required symbol validates correctly
183+
// val decoder = Decoder(listOf(Required('X'.code.toByte()), Number()))
184+
//
185+
// // Should decode successfully with 'X'
186+
// val validInput = listOf("X".toByteArray(), "5".toByteArray())
187+
// val result = decoder.decode(validInput)
188+
// assertEquals(BigInteger.valueOf(5), result)
189+
//
190+
// // Should throw with wrong required character
191+
// val invalidInput = listOf("Y".toByteArray(), "5".toByteArray())
192+
// assertThrows(IllegalArgumentException::class.java) {
193+
// decoder.decode(invalidInput)
194+
// }
195+
// }
196+
//
197+
// @Test
198+
// fun testEncoderOverflow()
199+
// {
200+
// // Test that encoder throws when value is too large
201+
// val encoder = Encoder(listOf(Binary(), Binary())) // Max value = 3
202+
//
203+
// assertThrows(Exception::class.java) {
204+
// encoder.encode(BigInteger.valueOf(4))
205+
// }
206+
// }
207+
178208
}

Codex/src/test/java/org/operatorfoundation/codex/WSPRCodexTest.kt

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,47 @@ class WSPRCodexTest
1616
codex = WSPRCodex()
1717
}
1818

19+
@Test
20+
fun testCompleteWorkflow()
21+
{
22+
println("\n=== Complete WSPR Encoding Workflow ===")
23+
24+
// Step 1: Create a short message
25+
val plaintext = "Hello"
26+
println("1. Plaintext: '$plaintext' (${plaintext.length} chars)")
27+
28+
// Step 2: Simulate encryption
29+
val plaintextBytes = plaintext.toByteArray()
30+
println("2. Plaintext bytes: ${plaintextBytes.size} bytes")
31+
32+
// Check capacity
33+
val maxCapacity = WSPRCodex.getMaxPayloadBytes()
34+
println("3. WSPR capacity: $maxCapacity bytes")
35+
assertTrue(plaintextBytes.size <= maxCapacity, "Message fits in capacity")
36+
37+
// Step 3: Encode to WSPR message
38+
val wsprMessage = codex.encode(plaintextBytes)
39+
println("4. WSPR Message: $wsprMessage")
40+
println(" - Callsign: '${wsprMessage.callsign}'")
41+
println(" - Grid: '${wsprMessage.gridSquare}'")
42+
println(" - Power: ${wsprMessage.powerDbm} dBm")
43+
44+
// Step 4: Verify message is valid
45+
assertTrue(wsprMessage.isValid(), "WSPR message should be valid")
46+
47+
// Step 5: Decode back to bytes
48+
val decodedBytes = codex.decode(wsprMessage)
49+
println("5. Decoded bytes: ${decodedBytes.size} bytes")
50+
51+
// Step 6: Convert back to text
52+
val decodedText = String(decodedBytes)
53+
println("6. Decoded text: '$decodedText'")
54+
55+
// Verify round-trip
56+
assertEquals(plaintext, decodedText, "Round-trip should preserve original text")
57+
println("✓ Round-trip successful!\n")
58+
}
59+
1960
@Test
2061
fun testMaxPayload()
2162
{
@@ -24,22 +65,26 @@ class WSPRCodexTest
2465

2566
println("Maximum WSPR payload capacity: $maxBytes bytes")
2667

27-
// Should be around 28 bytes based on symbol sizes
28-
assertTrue(maxBytes in 20..35, "Expected capacity around 28 bytes, got $maxBytes")
68+
// WSPR symbol configuration provides 7 bytes of capacity
69+
assertEquals(7, maxBytes, "Expected exactly 7 bytes capacity")
2970
}
3071

3172
@Test
3273
fun testActualCapacity() {
3374
// Try encoding increasingly large byte arrays until we find the limit
3475
var workingSize = 0
3576

36-
for (size in 1..50) {
77+
for (size in 1..50)
78+
{
3779
val testData = ByteArray(size) { 0xFF.toByte() }
38-
try {
80+
try
81+
{
3982
codex.encode(testData)
4083
workingSize = size
4184
println("Size $size: SUCCESS")
42-
} catch (e: Exception) {
85+
}
86+
catch (e: Exception)
87+
{
4388
println("Size $size: FAILED - ${e.message}")
4489
break
4590
}

0 commit comments

Comments
 (0)