Course update: lessons, CTF 0x0011a_cb, and documentation

- 0x0011a_cb (Operation Dark Vector): nation-state CTF redesign with an
  AES-128-ECB sealed target and a plaintext launch origin; RP2350 firmware with
  bearing-driven servo, tri-color LEDs, GSV stats, and a realistic no-fix path
- docs: story-driven classified brief, GDB and Ghidra tutorials with deep
  step-throughs, regenerated artifacts and PDFs
- scripts: docstring standard, AES per-student randomizer, telemetry monitor
- week 3 to week 5 lessons: Ghidra patching tutorial, CMSIS-SVD hardware RE,
  double floating-point and GPIO architecture chapters, README structure
This commit is contained in:
Kevin Thomas committed 2026-09-27 14:18:56 -04:00
1 parent 5201ee4b6b
commit 35eacd2c0e
162 files changed
+125658 -232

No files matched your search

+4
View File
@@ -0,0 +1,4 @@
build/
.DS_Store
*.swp
*~
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+112
View File
@@ -0,0 +1,112 @@
# Operation Dark Vector: Classified Intelligence Briefing
```
+-----------------------------------------------------------------+
| TOP SECRET // NOFORN |
+-----------------------------------------------------------------+
| |
|OPERATION DARK VECTOR |
| |
|CLASSIFIED BRIEFING: LIVE CTF 0x01 |
| |
|NATIONAL SECURITY AGENCY / GMU RHET |
+-----------------------------------------------------------------+
```
## 1. The Answer
INT. NSA EXPLOITATION CELL. 05:03.
The analysts have been awake for nineteen hours. The Director walks in with a cup
of coffee and does not sit down.
**DIRECTOR:** Where is it going?
**ANALYST:** Buddy says it knows.
**DIRECTOR:** Buddy.
**ANALYST:** The model. The one we built for exactly this. Give it the image, ask
it the question.
**DIRECTOR:** And?
**ANALYST:** Nine seconds.
She turns the screen around. This is what came back.
```
+-----------------------------------------------------------------+
| BUDDY // CLASSIFIED EXPLOITATION ASSIST // CONF: 0.97 |
+-----------------------------------------------------------------+
| DESTINATION : +38.840280 -77.428890 |
| CONFIDENCE : 0.97 |
| PATCH : verified-by-construction. Ready to flash. |
+-----------------------------------------------------------------+
```
**DIRECTOR:** That is the whole answer.
**ANALYST:** That is the whole answer.
**DIRECTOR:** Is it that simple?
**ANALYST:** It is never that simple.
**DIRECTOR:** Then why does it look that simple?
**ANALYST:** Because Buddy is very good at making things look simple.
## 2. What Buddy Is
Buddy is not a person. It is a model. It has read more assembly than every
engineer who has ever lived, and it never sleeps and never doubts. You ask it a
question, and it answers in the exact shape of an answer, with a confidence that
is hard to argue with.
That confidence is the problem. It is not the same thing as being right.
Buddy has never seen this drone. It has never stood at this bench or watched this
board power on. It has a picture of the firmware, and it has made a very good
guess. A very good guess is still a guess.
## 3. The Operation
At 04:17 the woods outside Centreville, Virginia were black and the machine above
them was silent. It was a one-way drone, built by a state program we will not
name here, meant to fly to a target, do its work, and never come home. No radio,
no hand on the stick. A breadboard-ugly brain counting down a heading it was born
with.
The wind won. The battery died. It came down through the branches and stayed
there, about a mile from where it started.
Then it started talking. A recovery beacon on 915 MHz, repeating a coordinate
into the night. NSA sensors heard it, and a recovery team took the airframe
intact. The flight computer was a bare-metal RP2350 that was never supposed to be
opened, and the one thing it was still willing to say out loud.
CyberCom imaged the flash and did what everyone does now. They handed it to the
machine.
## 4. The Mandate
The Director does not trust the answer. Not because Buddy is bad, but because the
answer is too clean. Nine seconds for a question that should have taken a night.
So the order is simple, and it is the only order that matters.
Prove it. Prove Buddy correct, or prove it wrong with evidence that no one can
argue with. A machine's verdict does not move up the chain without a human
signature.
## 5. The Challenge
You have the image. You have the board. You have the tools and the time Buddy
did not need.
Buddy has given you its answer, and it is confident.
Is it that simple?
Go find out.
Binary file not shown.
+108
View File
@@ -0,0 +1,108 @@
# MIT License
#
# Copyright (c) 2026 Kevin Thomas
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# Author: Kevin Thomas
# Email: kevin@mytechnotalent.com
# GitHub: https://github.com/mytechnotalent
# File: CMakeLists.txt
# Desc: Configures the RP2350 Pico SDK project for the Operation Dark Vector
# micro-UAV autonomous guidance firmware.
# Created: 2026
cmake_minimum_required(VERSION 3.13)
set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# Initialise pico_sdk from installed location
# (note this can come from environment, CMake cache etc)
# == DO NOT EDIT THE FOLLOWING LINES for the Raspberry Pi Pico VS Code Extension to work ==
if(WIN32)
set(USERHOME $ENV{USERPROFILE})
else()
set(USERHOME $ENV{HOME})
endif()
set(sdkVersion 2.2.0)
set(toolchainVersion 14_2_Rel1)
set(picotoolVersion 2.2.0-a4)
set(picoVscode ${USERHOME}/.pico-sdk/cmake/pico-vscode.cmake)
if (EXISTS ${picoVscode})
include(${picoVscode})
endif()
# ====================================================================================
set(PICO_BOARD pico2 CACHE STRING "Board type")
# Pull in Raspberry Pi Pico SDK (must be before project)
include(pico_sdk_import.cmake)
project(0x0011a_cb C CXX ASM)
# Initialise the Raspberry Pi Pico SDK
pico_sdk_init()
# Add executable with modular sources in src/
add_executable(0x0011a_cb
src/main.c
src/gps.c
src/lora.c
src/propeller.c
src/payload.c
src/navigation.c
src/aes.c
src/lcd.c
)
pico_set_program_name(0x0011a_cb "0x0011a_cb")
pico_set_program_version(0x0011a_cb "0.1")
# Modify the below lines to enable/disable output over UART/USB
pico_enable_stdio_uart(0x0011a_cb 1)
pico_enable_stdio_usb(0x0011a_cb 0)
set(CTF_TARGET_LAT "38.881940" CACHE STRING "CTF target latitude (per-student randomized)")
set(CTF_TARGET_LON "-77.450280" CACHE STRING "CTF target longitude (per-student randomized)")
target_compile_definitions(0x0011a_cb PRIVATE
PICO_DEFAULT_UART_BAUD_RATE=115200
CTF_TARGET_LAT=${CTF_TARGET_LAT}
CTF_TARGET_LON=${CTF_TARGET_LON}
)
# Generate PIO header
pico_generate_pio_header(0x0011a_cb ${CMAKE_CURRENT_LIST_DIR}/src/uart_rx.pio)
# Add the standard library to the build
target_link_libraries(0x0011a_cb
pico_stdlib
hardware_uart
hardware_pio
hardware_pwm
hardware_i2c
)
# Add the standard include files to the build
target_include_directories(0x0011a_cb PRIVATE
${CMAKE_CURRENT_LIST_DIR}/include
)
pico_add_extra_outputs(0x0011a_cb)
+446
View File
@@ -0,0 +1,446 @@
# GDB Hardware Debugging Tutorial: Reverse Engineering the Stripped RP2350 Image
## 0. Cold Open
The board is powered. The blade is turning. And none of it is true to you yet,
because you have not seen it with your own eyes.
Every claim you will make about this machine has to survive one test: did you
watch it happen? Not did the decompiler suggest it. Not did the datasheet imply
it. Did you stop the core, read the register, and see the number with your own
eyes.
The Debug Probe is the only honest witness in the room. It reaches through SWD
into the silicon and pulls out the truth at 5,000 kHz while the rest of the
world argues. It does not care what you believe. It does not care what Buddy
answered. It reports.
A register is not an opinion. A clock divider is not a narrative. `SM0_CLKDIV =
0x07A12000` is not a talking point. It is 1953.125, and 1953.125 is the reason
the sky is readable at all.
Anyone can generate an explanation. You are here to *verify* one, bit by bit,
on live silicon, and to sign your name to it.
**Think, then verify.**
---
## 1. Executive Summary
This tutorial reverse engineers the **stripped** `0x0011a_cb.bin` on live
silicon using a Raspberry Pi Debug Probe, OpenOCD, and GNU GDB. There are **no
symbols**, no `main`, no variable names, nothing. You set breakpoints by
**address**, read raw memory, and let the hardware tell you the truth.
Everything shown was captured from the real target and is reproducible.
```
+-----------------------------------------------------------------+
| GDB HARDWARE DEBUG SIGNAL CHAIN |
+-----------------------------------------------------------------+
| HOST macOS -> USB -> Debug Probe -> SWD -> RP2350B Cortex-M33 |
| OpenOCD :3333 <------ SWD + UART bridge ------> GP0/GP1 115200 |
+-----------------------------------------------------------------+
```
The philosophy: an offline model can guess what a stripped image does. It
cannot read the live registers, watch the parser, or verify a patch. **Think,
then verify.**
---
## 2. Prerequisites
| Component | Value |
|---|---|
| Target | Raspberry Pi Pico 2 (RP2350B) |
| Image | `0x0011a_cb.bin` (raw flash image, base `0x10000000`) |
| Probe | Raspberry Pi Debug Probe (CMSIS-DAP v2) |
| Architecture | `ARM:LE:32:Cortex` (ARMv8-M / Cortex-M33, Thumb-2) |
The `.bin` is the stripped image. `file offset = address, 0x10000000`.
### 2.1 Tool Paths Per Host (macOS, Linux, Windows)
The probe, OpenOCD, and GDB behave identically on every platform; only the paths
and the shell differ. Pick your host.
| Tool | macOS | Linux | Windows |
|---|---|---|---|
| OpenOCD | `~/.pico-sdk/openocd/0.12.0+dev/openocd` | `$HOME/.pico-sdk/openocd/0.12.0+dev/openocd` | `%USERPROFILE%\.pico-sdk\openocd\0.12.0+dev\openocd.exe` |
| OpenOCD scripts | `~/.pico-sdk/openocd/0.12.0+dev/scripts` | `$HOME/.pico-sdk/openocd/0.12.0+dev/scripts` | `%USERPROFILE%\.pico-sdk\openocd\0.12.0+dev\scripts` |
| GDB | `~/.pico-sdk/toolchain/14_2_Rel1/bin/arm-none-eabi-gdb` | `$HOME/.pico-sdk/toolchain/14_2_Rel1/bin/arm-none-eabi-gdb` | `%USERPROFILE%\.pico-sdk\toolchain\14_2_Rel1\bin\arm-none-eabi-gdb.exe` |
| Serial port | `/dev/tty.usbmodem*` | `/dev/ttyACM*` | `COMx` (Device Manager) |
Install the tools if you do not have them:
- macOS: `brew install --cask gcc-arm-embedded` for the toolchain, then
`brew install open-ocd`, or run the Raspberry Pi `pico-setup` script, which
places everything under `~/.pico-sdk`.
- Linux: install `gcc-arm-none-eabi` and `openocd` from your package manager,
or run the Raspberry Pi `pico-setup` script under `~/.pico-sdk`.
- Windows: install the Raspberry Pi Pico VS Code extension or the official
Windows installer; both place the toolchain under `%USERPROFILE%\.pico-sdk`.
Use PowerShell for every command below.
Serial console to the debug UART, per host:
```bash
# macOS
screen /dev/tty.usbmodem* 115200
# Linux
screen /dev/ttyACM* 115200
```
```powershell
# Windows
putty -serial COMx -sercfg 115200,8,n,1
```
---
## 3. Flash the Stripped Image
Because there are no object headers, you flash the raw bytes at the flash base
explicitly.
macOS and Linux:
```bash
OCD=~/.pico-sdk/openocd/0.12.0+dev/openocd
SCR=~/.pico-sdk/openocd/0.12.0+dev/scripts
$OCD -s "$SCR" -f interface/cmsis-dap.cfg -f target/rp2350.cfg \
-c "adapter speed 5000" \
-c "program 0x0011a_cb.bin 0x10000000 verify reset exit"
```
Windows PowerShell:
```powershell
$OCD = "$env:USERPROFILE\.pico-sdk\openocd\0.12.0+dev\openocd.exe"
$SCR = "$env:USERPROFILE\.pico-sdk\openocd\0.12.0+dev\scripts"
& $OCD -s "$SCR" -f interface/cmsis-dap.cfg -f target/rp2350.cfg `
-c "adapter speed 5000" `
-c "program 0x0011a_cb.bin 0x10000000 verify reset exit"
```
Expected on every host:
```
** Programming Started **
** Programming Finished **
** Verified OK **
** Resetting Target **
```
---
## 4. Attach GDB With No Symbols
Start the OpenOCD GDB server in one terminal, then attach **without** an
executable in another.
macOS and Linux:
```bash
OCD=~/.pico-sdk/openocd/0.12.0+dev/openocd
SCR=~/.pico-sdk/openocd/0.12.0+dev/scripts
$OCD -s "$SCR" -f interface/cmsis-dap.cfg -f target/rp2350.cfg -c "adapter speed 5000" &
GDB=~/.pico-sdk/toolchain/14_2_Rel1/bin/arm-none-eabi-gdb
$GDB -q
(gdb) target extended-remote localhost:3333
(gdb) monitor reset halt
```
Windows PowerShell:
```powershell
$OCD = "$env:USERPROFILE\.pico-sdk\openocd\0.12.0+dev\openocd.exe"
$SCR = "$env:USERPROFILE\.pico-sdk\openocd\0.12.0+dev\scripts"
Start-Process $OCD -ArgumentList @("-s","$SCR","-f","interface/cmsis-dap.cfg","-f","target/rp2350.cfg","-c","adapter speed 5000")
$GDB = "$env:USERPROFILE\.pico-sdk\toolchain\14_2_Rel1\bin\arm-none-eabi-gdb.exe"
& $GDB -q
(gdb) target extended-remote localhost:3333
(gdb) monitor reset halt
```
`bt` and `break main` are useless: there are no symbols. Every stop is an
address. This is the stripped-image reality.
---
## 5. Break at the Reset Handler's Destination
From static analysis (see the Ghidra tutorial) we know the code entry `main`
begins at `0x10000234`. Break there by address:
```gdb
(gdb) break *0x10000234
(gdb) continue
Thread 1 hit Breakpoint 1, 0x10000234 in ?? ()
(gdb) info registers r0 r1 r2 r3 sp lr pc xpsr
r0 0x0 0
r1 0x10000235 268436021
r2 0x80808080 -2139062144
r3 0xe000ed08 -536810232
sp 0x20082000
lr 0x1000018f
pc 0x10000234
xpsr 0x69000000
```
Backtrace shows raw addresses only:
```
(gdb) bt
#0 0x10000234 in ?? ()
```
---
## 6. The Decoy And The Encrypted Target (No Symbols Needed)
The firmware carries **two** kinds of coordinate, and they are not the same kind
of thing.
**The decoy (plaintext).** A plaintext pair sits at `0x1000A098` (lat) /
`0x1000A090` (lon): `+38.840280` / `-77.428890`. The beacon broadcasts it and a
byte scanner grabs it. It is the lie.
```
+-----------------------------------------------------------------+
| DECOY WAYPOINT (PLAINTEXT, THE LIE) |
+-----------------------------------------------------------------+
| 0x1000A098 CF BD 87 4B 8E 6B 43 40 double +38.840280 |
| 0x1000A090 36 E5 0A EF 72 5B 53 C0 double -77.428890 |
+-----------------------------------------------------------------+
```
**The target (AES-encrypted).** The real waypoint is never stored as a double:
```gdb
(gdb) x/16bx 0x10009D90
0x10009D90: 0x56 0x45 0x43 0x54 0x4f 0x52 0x31 0x31 # "VECTOR11"
0x10009D98: 0x41 0x45 0x53 0x4b 0x45 0x59 0x21 0x21 # "AESKEY!!"
(gdb) x/2gx 0x10009DA4
0x10009DA4: 0x7aea23c0c2ac4f20 0xcaef2311710f933a # ciphertext block
```
`init_navigation @ 0x10000C2C` decrypts each word with the key into RAM at
`TARGET_LAT 0x20000D00` / `TARGET_LON 0x20000D08`. Read the reconstructed truth
after boot:
```gdb
(gdb) x/2gx 0x20000d00
0x20000d00: 0x404370e368f08462 0xc0535cd1633482bf
```
Decode it: `+38.881940` / `-77.450280` (NRO HQ). The decoy says Centreville. The
target says Chantilly. Same firmware. One of them is a lie.
## 7. Prove the Peripherals From Registers
Break at `send_telemetry @ 0x10000C7C` (called once per acquisition tick) so
the initialisers have already run:
```gdb
(gdb) break *0x10000c7c
(gdb) continue
Thread 1 hit Breakpoint 2, 0x10000c7c in ?? ()
(gdb) x/2xw 0x50200000
0x50200000: 0x00000001 0x0f010e01 # PIO0_CTRL PIO0_FSTAT
(gdb) x/6xw 0x502000c8
0x502000c8: 0x07a12000 0x0701fc00 # SM0_CLKDIV SM0_EXECCTRL
0x502000d0: 0x800c0000 0x00000018 # SM0_SHIFTCTRL SM0_ADDR
0x502000d8: 0x00002020 0x00038000 # SM0_INSTR SM0_PINCTRL
```
```gdb
(gdb) x/4xw 0x40078024
0x40078024: 0x000003d0 0x00000024 0x00000070 0x00000301 # UART1 LoRa
(gdb) x/4xw 0x40070024
0x40070024: 0x00000051 0x00000018 0x00000070 0x00000301 # UART0 debug
```
```
+-----------------------------------------------------------------+
| PIO0 SM0 CLOCK DIVIDER (RP2350 @ 150 MHz, 8 cycles/bit) |
+-----------------------------------------------------------------+
| SM0_CLKDIV = 0x07A12000 = 1953 + 32/256 = 1953.125 |
| f_sm = 150,000,000 / 1953.125 = 76,800 Hz = 9600 x 8 |
+-----------------------------------------------------------------+
```
UART baud uses `IBRD` and a 6-bit `FBRD`:
$$baud = \frac{f_{clk}}{16 \times (IBRD + FBRD/64)}$$
UART1: $976 + 36/64 = 976.5625 \Rightarrow 150{,}000{,}000 / 15625 = 9600$.
UART0: $81 + 24/64 = 81.375 \Rightarrow 150{,}000{,}000 / 1302 \approx 115200$.
---
## 8. Watch the NMEA Parser Prove Itself
Static analysis identifies the parser's static index at `0x200010F4` (the
`.bss` symbol `idx.1`) and the 96-byte NMEA buffer at `0x20001044` (`buf.0`).
Watch the index:
```gdb
(gdb) watch *(int*)0x200010f4
(gdb) continue
Hardware watchpoint 3: *(int*)0x200010f4
Old value = 0
New value = 1
0x10000458 in ?? ()
(gdb) bt
#0 0x10000458 in ?? ()
#1 0x1000027c in ?? ()
(gdb) x/32cb 0x20001044
0x20001044: 36 '$' 71 'G' 80 'P' 71 'G' 76 'L' 76 'L' ...
# => "$GPGLL,,,,,,220653.00,V,N*4A"
```
At reset the buffer and index are both zero; these bytes appear only after a
sentence is parsed off the wire.
The `V` says there is **no fix yet**, not a parser bug, not a UART fault.
Only the wire can tell you that.
---
## 9. The Actuators: `release_payload @ 0x10000BFC`
The Ghidra analysis (companion tutorial) shows `release_payload` drives GP16,
GP17 and GP18 high through the RP2350 GPIO coprocessor interface. It is reached
by a `b.w` tail call from `navigate_to_target` at `0x10000D18`, so there is no
return frame; break at its entry and step the writes:
```gdb
(gdb) break *0x10000bfc
(gdb) continue
Thread 1 hit Breakpoint 4, 0x10000bfc in ?? ()
(gdb) x/9i $pc
0x10000bfc: push {r3, lr}
0x10000bfe: movs r2, #16
0x10000c00: mov.w r3, #1
0x10000c04: mcrr 0, 4, r2, r3, cr0 # GPIO16 = 1
0x10000c08: movs r2, #17
0x10000c0a: mcrr 0, 4, r2, r3, cr0 # GPIO17 = 1
0x10000c0e: movs r2, #18
0x10000c10: mcrr 0, 4, r2, r3, cr0 # GPIO18 = 1
```
After the three writes, the SIO register reads `0x00070000` (bits 16, 17, 18).
The RP2350 exposes GPIO through the coprocessor (`mcrr p0, #4, ...`), not a
plain store, a fact only the bench reveals.
---
## 10. Reproducibility Checklist
1. Flash `0x0011a_cb.bin` at `0x10000000`; `Verified OK`.
2. Attach GDB with **no** symbol file.
3. `break *0x10000234`; PC lands exactly there.
4. `x/4xw 0x20000d00` -> `68f08462 404370e3 633482bf c0535cd1`.
5. `x/6xw 0x502000c8` -> `SM0_CLKDIV = 0x07A12000`.
6. `watch *(int*)0x200010f4` trips only when NMEA arrives.
7. `x/9i 0x10000bfc` shows the `mcrr` GPIO coprocessor writes.
If a single value differs, you are not on the image you think you are. That
check is the job an offline model cannot do.
---
## 11. Why Buddy Fails Here
Even a model trained on ARM cannot read `SM0_CLKDIV`, watch `idx.0`, decode
*your* randomized literal, or verify a patch by flashing it. It generates
plausible text; the bench generates truth. **Think, then verify.**
---
## Appendix A. Deep GDB Step-Through
### A.1 Start the server and attach
macOS:
```bash
OCD=~/.pico-sdk/openocd/0.12.0+dev/openocd
SCR=~/.pico-sdk/openocd/0.12.0+dev/scripts
$OCD -s "$SCR" -f interface/cmsis-dap.cfg -f target/rp2350.cfg -c "adapter speed 5000"
```
Linux:
```bash
OCD=$HOME/.pico-sdk/openocd/0.12.0+dev/openocd
SCR=$HOME/.pico-sdk/openocd/0.12.0+dev/scripts
$OCD -s "$SCR" -f interface/cmsis-dap.cfg -f target/rp2350.cfg -c "adapter speed 5000"
```
Windows PowerShell:
```powershell
$OCD = "$env:USERPROFILE\.pico-sdk\openocd\0.12.0+dev\openocd.exe"
$SCR = "$env:USERPROFILE\.pico-sdk\openocd\0.12.0+dev\scripts"
& $OCD -s "$SCR" -f interface/cmsis-dap.cfg -f target/rp2350.cfg -c "adapter speed 5000"
```
Then attach from GDB (identical on all hosts). There are no symbols, so break by
address, not by name:
```gdb
target extended-remote localhost:3333
monitor reset halt
break *0x10000234
continue
```
### A.2 Read the vector table and the key material
```gdb
x/2xw 0x10000000 # SP and reset handler
x/16bx 0x10009D90 # AES key
x/16bx 0x10009DA4 # ciphertext block
x/2gx 0x20000D00 # decrypted target after boot
```
### A.3 Break the AES routine
```gdb
break *0x10000E5C
continue
x/4i $pc
stepi
stepi
```
`aes128_ecb_decrypt_block` runs once, early, inside `init_navigation`.
### A.4 Watch the decoy go out
```gdb
break *0x10000C7C
continue
info registers r0 r1 r2 r3
```
`send_telemetry` is called with the decoy while the GNSS has no fix.
### A.5 The tool trap
On a stripped target a floating point cast can byte-swap. Trust the raw read:
```gdb
x/1gx 0x20000D00 # then decode it yourself
```
Binary file not shown.
+525
View File
@@ -0,0 +1,525 @@
# Ghidra Reversing Tutorial: Static Analysis of the Stripped RP2350 Image
## 0. Cold Open
Open the image and it will lie to you with total confidence.
That is not a bug. That is the tool doing exactly what it was built to do:
propose. Ghidra proposes functions. It proposes boundaries. It proposes names
for things it cannot possibly know. And when it is wrong it will not tell you,
because it cannot tell the difference between a guess and a fact any better
than the machine that answered nine seconds too fast.
A stripped binary is a wall of Thumb-2 with the labels torn off. The only way
through is patience, cross-references, and the refusal to accept a story just
because it is plausible. Ghidra is the map. It is not the territory.
So you draw the map, then you walk the ground. You separate the two coordinate
pairs by what *references* them, not by what they look like. You find the eight
bytes that decide where a machine goes, and you prove which eight they are.
The tool proposes. The bench disposes.
**Think, then verify.**
---
## 1. Executive Summary
This tutorial reverses the **stripped** `0x0011a_cb.bin` with Ghidra: no
symbols, no sections, no metadata. We import the raw image, recover the code
graph, correct the boundaries Ghidra gets wrong, find the hardcoded target
waypoint, and patch it. Every output below is from the raw image at base
`0x10000000`, language `ARM:LE:32:Cortex`.
---
## 2. Import the Raw Image
The `.bin` is a raw XIP image. You must tell Ghidra where it lives and how to
decode it.
```
+-----------------------------------------------------------------+
| GHIDRA IMPORT SETTINGS |
+-----------------------------------------------------------------+
| Language : ARM:LE:32:Cortex (ARMv8-M / Cortex-M33, Thumb-2) |
| Format : Raw Binary |
| Base : 0x10000000 (RP2350 external flash / XIP) |
+-----------------------------------------------------------------+
```
### 2.1 Headless (reproducible), per host
macOS:
```bash
GHIDRA=/Applications/ghidra_12.0.4_PUBLIC
"$GHIDRA/support/analyzeHeadless" /tmp/ghproj DarkVector \
-import 0x0011a_cb.bin \
-processor "ARM:LE:32:Cortex" \
-loader BinaryLoader -loader-baseAddr 0x10000000
```
Linux:
```bash
GHIDRA=$HOME/ghidra_12.0.4_PUBLIC
"$GHIDRA/support/analyzeHeadless" /tmp/ghproj DarkVector \
-import 0x0011a_cb.bin \
-processor "ARM:LE:32:Cortex" \
-loader BinaryLoader -loader-baseAddr 0x10000000
```
Windows PowerShell:
```powershell
$GHIDRA = "C:\ghidra_12.0.4_PUBLIC"
& "$GHIDRA\support\analyzeHeadless.bat" "$env:TEMP\ghproj" DarkVector `
-import 0x0011a_cb.bin `
-processor "ARM:LE:32:Cortex" `
-loader BinaryLoader -loader-baseAddr 0x10000000
```
### 2.2 GUI, per host
Launch Ghidra, then **File -> Import File**, set language and base address, and
analyze:
- macOS: `/Applications/ghidra_12.0.4_PUBLIC/ghidraRun`
- Linux: `$HOME/ghidra_12.0.4_PUBLIC/ghidraRun`
- Windows: `C:\ghidra_12.0.4_PUBLIC\ghidraRun.bat`
---
## 3. Entry and the Stripped-Binary Problem
The first two words are the ARMv8-M vector table:
```
0x10000000: 0x20082000 ; initial SP
0x10000004: 0x1000015D ; reset handler (Thumb)
```
Ghidra recovers **166 functions** from the call graph. But with no symbols it
gets some boundaries wrong, and it folds tail-call-only helpers into their
callers: `release_payload` is reached only by a `b.w` tail call from
`navigate_to_target` at `0x10000D18`, so Ghidra does not give it its own
function. It names the entry `FUN_10000234`. The disassembly is right; the
boundaries are not. Correcting them is the analyst's job, and only the bench
confirms them.
Applying correct boundaries yields:
| Address | Function |
|---|---|
| `0x10000234` | `main` |
| `0x10000300` | `init_gps_pio` |
| `0x1000040C` | `poll_gps` |
| `0x10000858` | `gps_get_stats` |
| `0x10000870` | `init_lora` |
| `0x10000A08` | `lora_send` |
| `0x10000A54` | `lora_tick` |
| `0x10000AC4` | `init_propeller` |
| `0x10000AF8` | `propeller_set_bearing` |
| `0x10000B34` | `propeller_stop` |
| `0x10000B64` | `init_payload` |
| `0x10000BB0` | `set_gnss_leds` |
| `0x10000BFC` | `release_payload` |
| `0x10000C2C` | `init_navigation` |
| `0x10000C7C` | `send_telemetry` |
| `0x10000CB8` | `navigate_to_target` |
| `0x10000E5C` | `aes128_ecb_decrypt_block` |
| `0x100012D4` | `init_lcd` |
| `0x1000175C` | `lcd_show_coords` |
| `0x10001B4C` | `lcd_show_gnss` |
---
## 4. `main` as Ghidra Sees It (Raw Image)
```
void main(void)
{
local_18 = *DAT_100002f4; // ORIGIN_LAT @ 0x1000A098
uStack_14 = DAT_100002f4[1];
local_10 = *DAT_100002f8; // ORIGIN_LON @ 0x1000A090
uStack_c = DAT_100002f8[1];
FUN_10005d98(); // stdio_init_all
FUN_10000c2c(); // init_navigation
FUN_10000b64(); // init_payload
FUN_10000870(); // init_lora
FUN_10000300(); // init_gps_pio
FUN_10000ac4(); // init_propeller
FUN_100012d4(); // init_lcd
piVar1 = DAT_100002fc; // &hold @ 0x200010F0
do {
while( true ) {
iVar3 = 200;
bVar4 = 0;
local_20 = 0;
uStack_1c = 0;
do {
bVar2 = FUN_1000040c(&local_18,&local_10); // poll_gps
bVar4 = bVar2 | bVar4;
FUN_10000a54(); // lora_tick
FUN_10002ab8(5); // sleep_ms(5)
iVar3 = iVar3 + -1;
} while (iVar3 != 0);
FUN_10000858(&local_20,&uStack_1c); // gps_get_stats
if (bVar4 == 0) break;
*piVar1 = 3; // hold = 3
FUN_10000bb0(1,local_20); // set_gnss_leds
LAB_100002a4:
FUN_1000175c(local_18,uStack_14,local_10,uStack_c); // lcd_show_coords
FUN_10000cb8(local_18,uStack_14,local_10,uStack_c); // navigate_to_target
}
iVar3 = *piVar1;
if (iVar3 < 1) { iVar3 = 1; }
*piVar1 = iVar3 + -1; // hold--
if (iVar3 + -1 != 0) {
FUN_10000bb0(1,local_20); // set_gnss_leds
goto LAB_100002a4;
}
FUN_10000bb0(0,local_20); // set_gnss_leds(0,...)
FUN_10001b4c(local_20,uStack_1c); // lcd_show_gnss
FUN_10000b34(); // propeller_stop
FUN_10000c7c(local_18,uStack_14,local_10,uStack_c); // send_telemetry
} while( true );
}
```
`FUN_` prefixes everywhere: this is what a stripped target really looks like.
---
## 5. The Decoy And The Encrypted Target
`navigate_to_target` no longer compares against a literal; it reads the pointers
at `DAT_10000e50` / `DAT_10000e54`, which resolve to RAM `0x20000D00` /
`0x20000D08`, **runtime doubles**. Where do they come from? `init_navigation`,
and that is the whole puzzle.
```
void init_navigation(void)
{
local_28 = *DAT_10000c6c; // key @ 0x10009D90
uStack_24 = DAT_10000c6c[1];
uStack_20 = DAT_10000c6c[2];
uStack_1c = DAT_10000c6c[3];
local_18 = *DAT_10000c70; // ct @ 0x10009DA4
uStack_14 = DAT_10000c70[1];
uStack_10 = DAT_10000c70[2];
uStack_c = DAT_10000c70[3];
FUN_10000e5c(&local_18,&local_28,&local_38); // aes128_ecb_decrypt_block
*DAT_10000c74 = local_38; // TARGET_LAT -> 0x20000D00
puVar1[1] = uStack_34;
*puVar2 = local_30; // TARGET_LON -> 0x20000D08
puVar2[1] = uStack_2c;
}
```
Three data addresses do all the work:
| Symbol | Address | Meaning |
|---|---|---|
| `CTF_AES_KEY` | `0x10009D90` | the AES key `564543544f5231314145534b45592121` |
| `CTF_TARGET_CT` | `0x10009DA4` | the encrypted target pair |
| `TARGET_LAT/LON` | `0x20000D00` / `0x20000D08` | RAM, reconstructed at boot |
**Reading the key bytes (Ghidra will call them code):**
`0x10009D90` is **data**, not code. `init_navigation` copies the block into a
stack buffer and hands it to `aes128_ecb_decrypt_block`; it is never executed.
Ghidra still marks it as code because it sees the read cross-reference from
`init_navigation` (`FUN_10000c2c:10000c36(R)`) and guesses. The Listing then
shows fabricated mnemonics:
```
LAB_10009d90 XREF[1]: FUN_10000c2c:10000c36(R)
10009d90 56 45 cmp r6,r10
10009d92 43 54 strb r3,[r0,r1]
10009d94 4f 52 strh r7,[r1,r1]
10009d96 31 31 adds r1,#0x31
10009d98 41 45 cmp r1,r8
10009d9a 53 4b ldr r3,[s_n_"%s"_failed:_file_"%s",_line = "n \"%s\" failed: file
10009d9c 45 59 ldr r5,[r0,r5]
10009d9e 21 21 movs r1,#0x21
```
Those are the key bytes, not instructions. Two traps:
1. The mnemonics are meaningless: `56 45` is `V`,`E`; `43 54` is `C`,`T`;
`4f 52` is `O`,`R`.
2. At `0x10009D9A` the halfword `53 4B` decodes as `ldr r3, [pc, #332]`,
whose literal-pool target is `0x10009EE8`. That address really does hold a
string, newlib's assert message `Assertion "%s" failed: file "%s", line
%d%s%s` at `0x10009EE0`, so Ghidra prints its label. The string is real, but
the reference is spurious: `53 4B` is `S`,`K`, bytes 10-11 of the key, and
only decodes as that `ldr` by coincidence. The same thing happens at
`0x10009DA4`, where the ciphertext byte pair `20 4F` decodes to an `ldr`
that lands on the real two-space string at `0x10009E28`.
Read the bytes, never the mnemonics. Any of these is exact:
```bash
xxd -s 0x9D90 -l 16 0x0011a_cb.bin # raw image, no tools
```
```gdb
(gdb) x/16bx 0x10009D90 # live target
```
In the Ghidra GUI the simplest path is **Window -> Bytes**, press **G**, enter
`0x10009D90`, and read the 16 raw bytes. To retype them in the Listing instead,
select the 16 bytes, press **`C`** (Clear Code/Data), then **`T`** (Define Data)
and choose `byte`; press **`[`** to make it an array of 16. Note that **`B`**
is **not** "define byte" in Ghidra, it is *Cycle Integer Types*, and data
cannot be defined over bytes that are still typed as code, which is why the
clear (**`C`**) must come first.
The value is the ASCII key `VECTOR11AESKEY!!`:
`56 45 43 54 4F 52 31 31 41 45 53 4B 45 59 21 21`.
**Decrypt the target (fully offline, reproducible):**
Step 1. The key, 16 bytes at `0x10009D90` (read them as above):
```text
56 45 43 54 4F 52 31 31 41 45 53 4B 45 59 21 21 = "VECTOR11AESKEY!!"
hex for tools: 564543544f5231314145534b45592121
```
Step 2. The ciphertext, 16 bytes at `0x10009DA4`. Read them the exact same way
(**Window -> Bytes**, press **G**, enter `0x10009DA4`):
```text
20 4F AC C2 C0 23 EA 7A 3A 93 0F 71 11 23 EF CA
hex for tools: 204facc2c023ea7a3a930f711123efca
```
Step 3. AES-128-ECB decrypt the block with the key (no padding). The plaintext
is two little-endian IEEE-754 doubles, latitude then longitude.
Python, all platforms (`cryptography` is already used by the course):
```python
import struct
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
key = bytes.fromhex("564543544f5231314145534b45592121")
ct = bytes.fromhex("204facc2c023ea7a3a930f711123efca")
pt = Cipher(algorithms.AES(key), modes.ECB()).decryptor().update(ct)
print("plaintext:", pt.hex())
print("lat:", struct.unpack("<d", pt[:8])[0])
print("lon:", struct.unpack("<d", pt[8:])[0])
```
Output:
```text
plaintext: 6284f068e3704340bf823463d15c53c0
lat: 38.88194
lon: -77.45028
```
`<d` is a little-endian 64-bit double, exactly how the firmware reads the pair.
Same result with `openssl` (macOS, Linux, WSL):
```bash
printf '\x20\x4f\xac\xc2\xc0\x23\xea\x7a\x3a\x93\x0f\x71\x11\x23\xef\xca' \
| openssl enc -aes-128-ecb -K 564543544f5231314145534b45592121 -nopad -d \
| xxd
# 00000000: 6284 f068 e370 4340 bf82 3463 d15c 53c0
```
Same result in CyberChef (browser, nothing to install):
1. **From Hex** on `204facc2c023ea7a3a930f711123efca`.
2. **AES Decrypt**: Key = `Hex` `564543544f5231314145534b45592121`,
Mode = `ECB`, Padding = `None`, Input/Output = `Raw`.
3. **To Hex** to read `6284f068e3704340bf823463d15c53c0`.
4. That is two 8-byte little-endian doubles:
`62 84 F0 68 E3 70 43 40` and `BF 82 34 63 D1 5C 53 C0`.
Step 4. Decode the two doubles with the bit layout
$$V = (-1)^s \times 2^{e-1023} \times (1 + m)$$
The decrypted bytes are little-endian, so reverse each 8-byte group to the
big-endian hex word the Week 5 utility expects:
```bash
python3 scripts/float_hex_converter.py 0x404370E368F08462 # +38.881940
python3 scripts/float_hex_converter.py 0xC0535CD1633482BF # -77.450280
```
`struct.unpack("<d", ...)` already performs that byte swap for you.
And the plaintext pair at `0x1000A090` / `0x1000A098` (`+38.840280` /
`-77.428890`)? That is the **decoy**, the beacon's broadcast, and it is a lie.
The real target only exists after the AES.
## 6. `release_payload` (Raw Decompilation)
```
void release_payload(void)
{
coprocessor_moveto2(0,4,0x10,1,in_cr0); // GPIO16 = 1
coprocessor_moveto2(0,4,0x11,1,in_cr0); // GPIO17 = 1
coprocessor_moveto2(0,4,0x12,1,in_cr0); // GPIO18 = 1
__wrap_puts(uRam10009d3c); // "PAYLOAD RELEASED AT TARGET COORDINATES"
lora_send(uRam10009d64); // tail call
}
```
`coprocessor_moveto2` is Ghidra's rendering of the RP2350 GPIO `mcrr p0, #4`
path. A model will "helpfully" tell you this is a normal SIO store; the
encoding says otherwise, and GDB confirms it live.
---
### Two coordinate pairs (the decoy)
The image contains **two** hardcoded double pairs, not one:
| Pair | Address | Meaning |
|---|---|---|
| Launch origin (Centreville) | `0x1000A098` / `0x1000A090` | broadcast by the crash beacon |
| Target (NRO HQ) | `0x20000D00` / `0x20000D08` | RAM, rebuilt by `init_navigation` |
A pattern-matcher latches onto the **broadcast** origin and calls it the target.
The only way to tell them apart is the cross-reference: the target pair is
loaded with `ldrd` and fed to `__aeabi_dcmpeq` inside `navigate_to_target`; the
origin pair is handed to `send_telemetry`. Strings and intuition are not enough.
---
## 7. The Patch: Re-vector to a Safe Waypoint
The target is the 16 bytes at `0x10009DA4`. To re-vector the drone you replace
that block with the AES-128-ECB encryption of a safe waypoint (`37.0` / `-74.0`,
open Atlantic) under the same key. Compute the safe doubles and their ciphertext:
```bash
python3 scripts/float_hex_converter.py 37.0 # 0x4042800000000000
python3 scripts/float_hex_converter.py -74.0 # 0xC052800000000000
python3 -c "import struct,sys; sys.stdout.buffer.write(struct.pack('<d',37.0)+struct.pack('<d',-74.0))" \
| openssl enc -aes-128-ecb -K 564543544f5231314145534b45592121 -nopad | xxd -p
# c2bb647c8778bf59279c01ad066bb16b
```
In the Ghidra Listing, press **G** to `0x10009DA4`, select the 16 bytes, then
**Ctrl+Shift+G** (Patch Data):
| Address | Original | Patched |
|---|---|---|
| `0x10009DA4` | `20 4F AC C2 C0 23 EA 7A 3A 93 0F 71 11 23 EF CA` | `C2 BB 64 7C 87 78 BF 59 27 9C 01 AD 06 6B B1 6B` |
Export: **File -> Export Program...** -> Format **Binary** ->
`0x0011a_cb_patched.bin`.
---
## 8. Export, Convert, Flash, Verify
```bash
python3 uf2conv.py 0x0011a_cb_patched.bin \
-f 0xe48bff59 -b 0x10000000 -c -o 0x0011a_cb_patched.uf2
```
Hold **BOOTSEL**, copy the UF2 across, and verify against the live ground HUD.
A patch that is not flashed and confirmed is a guess.
---
## 9. Randomized Builds
Each student image embeds a unique key and waypoint, so no answer key travels:
```bash
python3 scripts/randomize_build.py --student-id alice --seed 12345 --uf2
[+] student_id : alice
[+] TARGET_LAT : 38.880272
[+] TARGET_LON : -77.460077
[+] AES key : <16 random bytes>
[+] ciphertext : <AES-128-ECB(key, target)>
[+] image : build-ctf/0x0011a_cb_alice.uf2
[+] answer key : <keydir>/answer_alice.json (INSTRUCTOR ONLY, do not ship)
```
Addresses are identical; only the key and ciphertext bytes differ.
---
## 10. Reproducibility Checklist
1. Import raw `.bin`, `ARM:LE:32:Cortex`, base `0x10000000`.
2. Reset vector `[0]=0x20082000`, `[1]=0x1000015D`.
3. `main @ 0x10000234`; fix the merged boundary at `a single FUN_ function`.
4. `navigate_to_target @ 0x10000CB8` reads the pointers at `0x10000E50`/`0x10000E54`, targeting RAM `0x20000D00`/`0x20000D08`.
5. Decode to `+38.881940` / `-77.450280`.
6. Patch, export, `uf2conv`, flash, verify.
---
## 11. Why Buddy Fails Here
Ghidra itself proves the point: it *proposes* functions and boundaries, and the
analyst corrects them with cross-references and the bench. A language model
does the same, faster and wrong, with no way to confirm. It cannot validate a
boundary, cannot read the live `ldrd`, and cannot flash a patch to see the
drone re-vector. Structure is a hypothesis; silicon is the verdict.
**Think, then verify.**
---
## Appendix A. Deep Ghidra Step-Through
### A.1 Import
Language `ARM:LE:32:Cortex`, Base Address `0x10000000`, Raw Binary. Analyze.
### A.2 Address map
| Address | What it is |
|---|---|
| `0x10000234` | `main` |
| `0x10000C2C` | `init_navigation`, rebuilds the target |
| `0x10000E5C` | `aes128_ecb_decrypt_block` |
| `0x10000CB8` | `navigate_to_target`, the arrival compare |
| `0x10009D90` | AES key |
| `0x10009DA4` | ciphertext |
| `0x1000A090` | decoy waypoint |
### A.3 The S-box
Go to `0x1000A1AC` and find the 256-byte S-box that starts `63 7C 77 7B` (the
inverse S-box, starting `52 09 6A D5`, sits at `0x1000A0AC`). Right click,
create an array of 256 bytes.
### A.4 Recover
Take 16 bytes at `0x10009DA4` and the key at `0x10009D90`, then run the Python
decrypt block from section 5.
### A.5 Patch
Overwrite the 16 bytes at `0x10009DA4` with the re-encrypted Atlantic block,
then File, Export Program, Format Binary.
### A.6 Platform note
Ghidra is identical on Windows, Linux, and macOS. Only the paths differ.
| Action | macOS | Linux | Windows |
|---|---|---|---|
| GUI launch | `/Applications/ghidra_12.0.4_PUBLIC/ghidraRun` | `$HOME/ghidra_12.0.4_PUBLIC/ghidraRun` | `C:\ghidra_12.0.4_PUBLIC\ghidraRun.bat` |
| Headless | `/Applications/ghidra_12.0.4_PUBLIC/support/analyzeHeadless` | `$HOME/ghidra_12.0.4_PUBLIC/support/analyzeHeadless` | `C:\ghidra_12.0.4_PUBLIC\support\analyzeHeadless.bat` |
| Shell | `zsh` / `bash` | `bash` | PowerShell |
See sections 2.1 and 2.2 for the full import commands.
Binary file not shown.
+24
View File
@@ -0,0 +1,24 @@
// MIT License
//
// Copyright (c) 2026 Kevin Thomas
//
// File: aes.h
// Desc: Declares AES-128-ECB single-block decryption.
// Created: 2026
#ifndef AES_H
#define AES_H
#include <stdint.h>
/**
* @brief Decrypt one 16-byte block with AES-128-ECB.
*
* @param in 16-byte ciphertext.
* @param key 16-byte key.
* @param out 16-byte plaintext output.
* @return None.
*/
void aes128_ecb_decrypt_block(const uint8_t in[16], const uint8_t key[16], uint8_t out[16]);
#endif // AES_H
+17
View File
@@ -0,0 +1,17 @@
// MIT License
//
// Copyright (c) 2026 Kevin Thomas
//
// File: ctf_target.h
// Desc: AES-128-ECB key and ciphertext for the real target waypoint.
// Created: 2026
#ifndef CTF_TARGET_H
#define CTF_TARGET_H
#include <stdint.h>
#define CTF_AES_KEY { 0x56, 0x45, 0x43, 0x54, 0x4F, 0x52, 0x31, 0x31, 0x41, 0x45, 0x53, 0x4B, 0x45, 0x59, 0x21, 0x21 }
#define CTF_TARGET_CT { 0x20, 0x4F, 0xAC, 0xC2, 0xC0, 0x23, 0xEA, 0x7A, 0x3A, 0x93, 0x0F, 0x71, 0x11, 0x23, 0xEF, 0xCA }
#endif // CTF_TARGET_H
+67
View File
@@ -0,0 +1,67 @@
// MIT License
//
// Copyright (c) 2026 Kevin Thomas
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
// Author: Kevin Thomas
// Email: kevin@mytechnotalent.com
// GitHub: https://github.com/mytechnotalent
// File: gps.h
// Desc: Declares PIO UART GPS receiver interface and NMEA parsing logic.
// Created: 2026
#ifndef GPS_H
#define GPS_H
#include <stdbool.h>
#include "hardware/pio.h"
#define GPS_PIN 7
#define GPS_BAUD 9600
#define GPS_PIO pio0
#define GPS_SM 0
/**
* @brief Initialize PIO UART receiver on GPIO7 for u-blox NEO-6M GPS.
*
* @param None.
* @return None.
*/
void init_gps_pio(void);
/**
* @brief Poll PIO RX FIFO and parse incoming NMEA GPS coordinates.
*
* @param lat Pointer to double storing updated latitude.
* @param lon Pointer to double storing updated longitude.
* @return bool True if a valid active 3D GPS fix (RMC 'A') was received, false otherwise.
*/
bool poll_gps(double *lat, double *lon);
/**
* @brief Read latest GNSS signal statistics parsed from GSV sentences.
*
* @param siv Pointer to store satellites-in-view count.
* @param cno Pointer to store best carrier-to-noise ratio in dBHz.
* @return None.
*/
void gps_get_stats(int *siv, int *cno);
#endif // GPS_H
+74
View File
@@ -0,0 +1,74 @@
// MIT License
//
// Copyright (c) 2026 Kevin Thomas
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
// Author: Kevin Thomas
// Email: kevin@mytechnotalent.com
// GitHub: https://github.com/mytechnotalent
// File: lcd.h
// Desc: Declares I2C HD44780 (16x2) LCD interface for telemetry coordinates.
// Created: 2026
#ifndef LCD_H
#define LCD_H
#include <stdbool.h>
#include <stdint.h>
#include "hardware/i2c.h"
#define LCD_I2C_INST i2c1
#define LCD_SDA_PIN 2
#define LCD_SCL_PIN 3
#define LCD_BAUD 100000
/**
* @brief Initialize I2C0 peripheral and detect/configure 1602 LCD backpack.
*
* @param None.
* @return None.
*/
void init_lcd(void);
/**
* @brief Render current latitude and longitude on the 16x2 character display.
*
* Row 0: LAT: dd.dddddd N
* Row 1: LON: dd.dddddd W
*
* @param lat Current latitude in decimal degrees.
* @param lon Current longitude in decimal degrees.
* @return None.
*/
void lcd_show_coords(double lat, double lon);
/**
* @brief Render GNSS acquisition telemetry (satellites and C/N0) on the LCD.
*
* Row 0: SAT: nn CNO: nn
* Row 1: ACQUIRING... when satellites are in view, else NO SIGNAL
*
* @param sats Number of satellites currently in view.
* @param cno Best carrier-to-noise ratio in dBHz.
* @return None.
*/
void lcd_show_gnss(int sats, int cno);
#endif // LCD_H
+64
View File
@@ -0,0 +1,64 @@
// MIT License
//
// Copyright (c) 2026 Kevin Thomas
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
// Author: Kevin Thomas
// Email: kevin@mytechnotalent.com
// GitHub: https://github.com/mytechnotalent
// File: lora.h
// Desc: Declares UART1 interface for the REYAX RYLR998 LoRa transceiver.
// Created: 2026
#ifndef LORA_H
#define LORA_H
#include "hardware/uart.h"
#define LORA_UART uart1
#define LORA_BAUD 9600
#define LORA_TX_PIN 8
#define LORA_RX_PIN 9
/**
* @brief Initialize LoRa transceiver over UART1 on GPIO8 and GPIO9
*
* @param None.
* @return None.
*/
void init_lora(void);
/**
* @brief Transmit string message over LoRa UART1 interface
*
* @param msg Null-terminated string buffer to transmit.
* @return None.
*/
void lora_send(const char *msg);
/**
* @brief Service the LoRa UART: stream pending TX bytes and drain RX.
*
* @param None.
* @return None.
*/
void lora_tick(void);
#endif // LORA_H
+87
View File
@@ -0,0 +1,87 @@
// MIT License
//
// Copyright (c) 2026 Kevin Thomas
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
// Author: Kevin Thomas
// Email: kevin@mytechnotalent.com
// GitHub: https://github.com/mytechnotalent
// File: navigation.h
// Desc: Declares autonomous guidance, dead-reckoning, and telemetry interface.
// Created: 2026
#ifndef NAVIGATION_H
#define NAVIGATION_H
#include <stdbool.h>
/** @brief Pre-programmed programmed decoy waypoint (Centreville, VA). */
extern const double ORIGIN_LAT;
extern const double ORIGIN_LON;
/** @brief Target coordinates, reconstructed at boot from masked constants. */
extern double TARGET_LAT;
extern double TARGET_LON;
/**
* @brief Reconstruct the target waypoint from its XOR-masked constants.
*
* @param None.
* @return None.
*/
void init_navigation(void);
/**
* @brief Transmit telemetry stream over Debug UART0 and LoRa UART1.
*
* @param cur_lat Current micro-UAV latitude.
* @param cur_lon Current micro-UAV longitude.
* @return None.
*/
void send_telemetry(double cur_lat, double cur_lon);
/**
* @brief Advance dead-reckoning position toward programmed waypoint.
*
* @param cur_lat Pointer to current latitude.
* @param cur_lon Pointer to current longitude.
* @return None.
*/
void dead_reckon_step(double *cur_lat, double *cur_lon);
/**
* @brief Verify if micro-UAV has arrived at target coordinates.
*
* @param cur_lat Current latitude coordinate.
* @param cur_lon Current longitude coordinate.
* @return true if arrived at target, false otherwise.
*/
bool check_arrival(double cur_lat, double cur_lon);
/**
* @brief Manage guidance progression, propeller oscillation, and payload release.
*
* @param cur_lat Current latitude coordinate.
* @param cur_lon Current longitude coordinate.
* @return None.
*/
void navigate_to_target(double cur_lat, double cur_lon);
#endif // NAVIGATION_H
+68
View File
@@ -0,0 +1,68 @@
// MIT License
//
// Copyright (c) 2026 Kevin Thomas
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
// Author: Kevin Thomas
// Email: kevin@mytechnotalent.com
// GitHub: https://github.com/mytechnotalent
// File: payload.h
// Desc: Declares payload release mechanism interface on GPIO16.
// Created: 2026
#ifndef PAYLOAD_H
#define PAYLOAD_H
#include <stdbool.h>
#define LED_RED_PIN 16
#define LED_GREEN_PIN 17
#define LED_YELLOW_PIN 18
/**
* @brief Initialize GPIO16 (Red failure LED) and GPIO17 (Green success LED).
*
* @param None.
* @return None.
*/
void init_payload(void);
/**
* @brief Update tri-color GNSS status LEDs from fix state and satellites.
*
* Red (GP16) = no satellites in view; Yellow (GP18) = satellites in view
* while acquiring; Green (GP17) = active 3D fix.
*
* @param fix True if an active 3D GPS fix is held.
* @param siv Number of satellites currently in view.
* @return None.
*/
void set_gnss_leds(bool fix, int siv);
/**
* @brief Energize payload latch and illuminate both LEDs at target coordinates.
*
* @param None.
* @return None.
*/
void release_payload(void);
#endif // PAYLOAD_H
+67
View File
@@ -0,0 +1,67 @@
// MIT License
//
// Copyright (c) 2026 Kevin Thomas
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
// Author: Kevin Thomas
// Email: kevin@mytechnotalent.com
// GitHub: https://github.com/mytechnotalent
// File: propeller.h
// Desc: Declares SG90 servo PWM mock propeller interface on GPIO6.
// Created: 2026
#ifndef PROPELLER_H
#define PROPELLER_H
#define PROPELLER_PIN 6
/**
* @brief Initialize 50 Hz PWM on GPIO6 for SG90 mock propeller blade.
*
* @param None.
* @return None.
*/
void init_propeller(void);
/**
* @brief Advance mock propeller blade oscillation during flight.
*
* @param None.
* @return None.
*/
void propeller_spin(void);
/**
* @brief Halt mock propeller blade oscillation upon target arrival.
*
* @param None.
* @return None.
*/
void propeller_stop(void);
/**
* @brief Point the servo at the compass bearing toward the target.
*
* @param deg Bearing in degrees from the current position to the target.
* @return None.
*/
void propeller_set_bearing(double deg);
#endif // PROPELLER_H
+121
View File
@@ -0,0 +1,121 @@
# This is a copy of <PICO_SDK_PATH>/external/pico_sdk_import.cmake
# This can be dropped into an external project to help locate this SDK
# It should be include()ed prior to project()
# Copyright 2020 (c) 2020 Raspberry Pi (Trading) Ltd.
#
# Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
# following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
# disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products
# derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
if (DEFINED ENV{PICO_SDK_PATH} AND (NOT PICO_SDK_PATH))
set(PICO_SDK_PATH $ENV{PICO_SDK_PATH})
message("Using PICO_SDK_PATH from environment ('${PICO_SDK_PATH}')")
endif ()
if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT} AND (NOT PICO_SDK_FETCH_FROM_GIT))
set(PICO_SDK_FETCH_FROM_GIT $ENV{PICO_SDK_FETCH_FROM_GIT})
message("Using PICO_SDK_FETCH_FROM_GIT from environment ('${PICO_SDK_FETCH_FROM_GIT}')")
endif ()
if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT_PATH} AND (NOT PICO_SDK_FETCH_FROM_GIT_PATH))
set(PICO_SDK_FETCH_FROM_GIT_PATH $ENV{PICO_SDK_FETCH_FROM_GIT_PATH})
message("Using PICO_SDK_FETCH_FROM_GIT_PATH from environment ('${PICO_SDK_FETCH_FROM_GIT_PATH}')")
endif ()
if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT_TAG} AND (NOT PICO_SDK_FETCH_FROM_GIT_TAG))
set(PICO_SDK_FETCH_FROM_GIT_TAG $ENV{PICO_SDK_FETCH_FROM_GIT_TAG})
message("Using PICO_SDK_FETCH_FROM_GIT_TAG from environment ('${PICO_SDK_FETCH_FROM_GIT_TAG}')")
endif ()
if (PICO_SDK_FETCH_FROM_GIT AND NOT PICO_SDK_FETCH_FROM_GIT_TAG)
set(PICO_SDK_FETCH_FROM_GIT_TAG "master")
message("Using master as default value for PICO_SDK_FETCH_FROM_GIT_TAG")
endif()
set(PICO_SDK_PATH "${PICO_SDK_PATH}" CACHE PATH "Path to the Raspberry Pi Pico SDK")
set(PICO_SDK_FETCH_FROM_GIT "${PICO_SDK_FETCH_FROM_GIT}" CACHE BOOL "Set to ON to fetch copy of SDK from git if not otherwise locatable")
set(PICO_SDK_FETCH_FROM_GIT_PATH "${PICO_SDK_FETCH_FROM_GIT_PATH}" CACHE FILEPATH "location to download SDK")
set(PICO_SDK_FETCH_FROM_GIT_TAG "${PICO_SDK_FETCH_FROM_GIT_TAG}" CACHE FILEPATH "release tag for SDK")
if (NOT PICO_SDK_PATH)
if (PICO_SDK_FETCH_FROM_GIT)
include(FetchContent)
set(FETCHCONTENT_BASE_DIR_SAVE ${FETCHCONTENT_BASE_DIR})
if (PICO_SDK_FETCH_FROM_GIT_PATH)
get_filename_component(FETCHCONTENT_BASE_DIR "${PICO_SDK_FETCH_FROM_GIT_PATH}" REALPATH BASE_DIR "${CMAKE_SOURCE_DIR}")
endif ()
FetchContent_Declare(
pico_sdk
GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk
GIT_TAG ${PICO_SDK_FETCH_FROM_GIT_TAG}
)
if (NOT pico_sdk)
message("Downloading Raspberry Pi Pico SDK")
# GIT_SUBMODULES_RECURSE was added in 3.17
if (${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.17.0")
FetchContent_Populate(
pico_sdk
QUIET
GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk
GIT_TAG ${PICO_SDK_FETCH_FROM_GIT_TAG}
GIT_SUBMODULES_RECURSE FALSE
SOURCE_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-src
BINARY_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-build
SUBBUILD_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-subbuild
)
else ()
FetchContent_Populate(
pico_sdk
QUIET
GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk
GIT_TAG ${PICO_SDK_FETCH_FROM_GIT_TAG}
SOURCE_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-src
BINARY_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-build
SUBBUILD_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-subbuild
)
endif ()
set(PICO_SDK_PATH ${pico_sdk_SOURCE_DIR})
endif ()
set(FETCHCONTENT_BASE_DIR ${FETCHCONTENT_BASE_DIR_SAVE})
else ()
message(FATAL_ERROR
"SDK location was not specified. Please set PICO_SDK_PATH or set PICO_SDK_FETCH_FROM_GIT to on to fetch from git."
)
endif ()
endif ()
get_filename_component(PICO_SDK_PATH "${PICO_SDK_PATH}" REALPATH BASE_DIR "${CMAKE_BINARY_DIR}")
if (NOT EXISTS ${PICO_SDK_PATH})
message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' not found")
endif ()
set(PICO_SDK_INIT_CMAKE_FILE ${PICO_SDK_PATH}/pico_sdk_init.cmake)
if (NOT EXISTS ${PICO_SDK_INIT_CMAKE_FILE})
message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' does not appear to contain the Raspberry Pi Pico SDK")
endif ()
set(PICO_SDK_PATH ${PICO_SDK_PATH} CACHE PATH "Path to the Raspberry Pi Pico SDK" FORCE)
include(${PICO_SDK_INIT_CMAKE_FILE})
+411
View File
@@ -0,0 +1,411 @@
#!/usr/bin/env python3
# MIT License
#
# Copyright (c) 2026 Kevin Thomas
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# Author: Kevin Thomas
# Email: kevin@mytechnotalent.com
# GitHub: https://github.com/mytechnotalent
# File: decode_coordinates.py
# Desc: Decode and patch RP2350 micro-UAV navigation coordinates.
# Created: 2026
"""
Decode and patch RP2350 micro-UAV navigation coordinates.
Analyzes raw firmware images for IEEE 754 64-bit double-precision floating
point coordinates. Scans target flight vectors and patches binaries with
safe disposal coordinates in the Atlantic Ocean.
"""
import math
import struct
import sys
from pathlib import Path
FLASH_BASE = 0x10000000
ORIGIN_LAT = 38.840280
ORIGIN_LON = -77.428890
ATLANTIC_LAT = 37.000000
ATLANTIC_LON = -74.000000
def _haversine_calc(phi1: float, phi2: float,
dphi: float, dlam: float) -> float:
"""
Compute central angle using haversine formula.
Parameters
----------
phi1 : float
Origin latitude in radians.
phi2 : float
Target latitude in radians.
dphi : float
Latitude difference in radians.
dlam : float
Longitude difference in radians.
Returns
-------
float
Central angular distance in radians.
"""
s_phi = math.sin(dphi / 2.0) ** 2
s_lam = math.sin(dlam / 2.0) ** 2
a = s_phi + math.cos(phi1) * math.cos(phi2) * s_lam
return 2.0 * math.atan2(math.sqrt(a), math.sqrt(1.0 - a))
def haversine(lat1: float, lon1: float,
lat2: float, lon2: float) -> tuple[float, float]:
"""
Compute Great-Circle distance and azimuth bearing.
Parameters
----------
lat1 : float
Origin latitude in degrees.
lon1 : float
Origin longitude in degrees.
lat2 : float
Destination latitude in degrees.
lon2 : float
Destination longitude in degrees.
Returns
-------
tuple[float, float]
Distance in statute miles and bearing in degrees.
"""
p1, p2 = math.radians(lat1), math.radians(lat2)
dl = math.radians(lon2 - lon1)
dist = 6371.0 * _haversine_calc(p1, p2, math.radians(lat2 - lat1), dl)
y = math.sin(dl) * math.cos(p2)
term = math.sin(p1) * math.cos(p2) * math.cos(dl)
x = math.cos(p1) * math.sin(p2) - term
bearing = (math.degrees(math.atan2(y, x)) + 360.0) % 360.0
return dist * 0.621371, bearing
def _is_coord(val: float) -> bool:
"""
Verify if float falls within target geographic bounds.
Parameters
----------
val : float
Candidate double-precision value.
Returns
-------
bool
True if value is a valid latitude or longitude.
"""
if math.isnan(val) or math.isinf(val):
return False
in_lat = 35.0 <= val <= 41.0
in_lon = -79.0 <= val <= -72.0
return in_lat or in_lon
def _parse_chunk(data: bytes, off: int) -> dict | None:
"""
Extract and validate one 8-byte candidate float.
Parameters
----------
data : bytes
Firmware image buffer.
off : int
Byte offset inside image buffer.
Returns
-------
dict | None
Parsed coordinate record or None.
"""
chunk = data[off:off + 8]
val = struct.unpack("<d", chunk)[0]
if not _is_coord(val):
return None
hex_str = " ".join(f"{b:02x}" for b in chunk)
u64 = struct.unpack("<Q", chunk)[0]
kind = "LATITUDE" if val > 0.0 else "LONGITUDE"
return {"off": off, "addr": FLASH_BASE + off, "val": val,
"hex": hex_str, "u64": u64, "kind": kind}
def scan_coordinates(data: bytes) -> list[dict]:
"""
Scan firmware buffer for double-precision coordinates.
Parameters
----------
data : bytes
Firmware image buffer.
Returns
-------
list[dict]
List of candidate coordinate records.
"""
found = []
limit = len(data) - 8
for off in range(0, limit, 4):
item = _parse_chunk(data, off)
if item is not None:
found.append(item)
return found
def _print_banner(path: Path) -> None:
"""
Print operation heading and recovery origin.
Parameters
----------
path : pathlib.Path
Target firmware path.
Returns
-------
None
"""
print("=" * 67)
print(" OPERATION DARK VECTOR // FORENSIC COORDINATE TOOL")
print(" GMU Rapid Hardware Exploitation Laboratory - Fairfax, VA")
print("=" * 67)
print(f"[*] Target Binary: {path.name} ({path.stat().st_size:,} bytes)")
print(f"[*] Recovery Origin: Centreville, VA "
f"({ORIGIN_LAT:.6f}, {ORIGIN_LON:.6f})")
print("-" * 67)
def _print_candidate(c: dict) -> None:
"""
Print formatted candidate coordinate entry.
Parameters
----------
c : dict
Candidate coordinate record.
Returns
-------
None
"""
print(f" [{c['kind']:9s}] Value: {c['val']:12.6f} | "
f"Addr: 0x{c['addr']:08x} (Offset: 0x{c['off']:04x})")
print(f" Hex: {c['hex']} | uint64: 0x{c['u64']:016x}")
def _cardinal_bearing(brg: float) -> str:
"""Return compass direction string for given bearing."""
dirs = ["N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE",
"S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW"]
idx = int((brg + 11.25) / 22.5) % 16
return dirs[idx]
def _print_summary(lat: float, lon: float, mi: float, brg: float) -> None:
"""
Print mission tactical assessment summary.
Parameters
----------
lat : float
Decoded target latitude.
lon : float
Decoded target longitude.
mi : float
Distance in statute miles.
brg : float
Initial bearing in degrees.
Returns
-------
None
"""
card = _cardinal_bearing(brg)
print("\n" + "=" * 67)
print(" TACTICAL MISSION PROFILE DECODED")
print("=" * 67)
print(f" Target Latitude: {lat:.6f} deg N")
print(f" Target Longitude: {lon:.6f} deg W")
print(f" Distance from Origin: {mi:.2f} miles ({mi * 1.60934:.2f} km)")
print(f" Flight Vector Bearing: {brg:.1f} deg ({card})")
print("=" * 67)
def _patch_bytes(data: bytes, old_lat: float, old_lon: float) -> bytes:
"""
Replace target coordinates with safe Atlantic Ocean coordinates.
Parameters
----------
data : bytes
Original firmware bytes.
old_lat : float
Original target latitude.
old_lon : float
Original target longitude.
Returns
-------
bytes
Patched firmware byte buffer.
"""
src_lat = struct.pack("<d", old_lat)
src_lon = struct.pack("<d", old_lon)
dst_lat = struct.pack("<d", ATLANTIC_LAT)
dst_lon = struct.pack("<d", ATLANTIC_LON)
buf = data.replace(src_lat, dst_lat)
return buf.replace(src_lon, dst_lon)
def _print_patch_info(out_name: str, mi: float, brg: float) -> None:
"""
Display confirmation of applied firmware patch.
Parameters
----------
out_name : str
Patched output file name.
mi : float
Distance to safe disposal zone.
brg : float
Azimuth bearing to disposal zone.
Returns
-------
None
"""
print(f"\n[+] Patched firmware written to: {out_name}")
print("[+] Overwrote waypoint -> Atlantic Ocean Disposal Zone:")
print(f" Safe Latitude: {ATLANTIC_LAT:.6f} deg N")
print(f" Safe Longitude: {ATLANTIC_LON:.6f} deg W")
print(f" Offshore Distance: {mi:.2f} miles (Bearing: {brg:.1f} deg)")
def patch_firmware(target: Path, out_path: Path,
lat: float, lon: float) -> None:
"""
Patch firmware with safe Atlantic Ocean disposal waypoint.
Parameters
----------
target : pathlib.Path
Source binary path.
out_path : pathlib.Path
Destination patched binary path.
lat : float
Current target latitude.
lon : float
Current target longitude.
Returns
-------
None
"""
raw = target.read_bytes()
patched = _patch_bytes(raw, lat, lon)
out_path.write_bytes(patched)
mi, brg = haversine(ORIGIN_LAT, ORIGIN_LON, ATLANTIC_LAT, ATLANTIC_LON)
_print_patch_info(out_path.name, mi, brg)
def _evaluate(items: list[dict], target: Path, do_patch: bool) -> None:
"""
Display results and execute patch if requested.
Parameters
----------
items : list[dict]
Found coordinate records.
target : pathlib.Path
Target binary path.
do_patch : bool
Flag indicating if patch should be applied.
Returns
-------
None
"""
lats = [c for c in items if c["kind"] == "LATITUDE"]
lons = [c for c in items if c["kind"] == "LONGITUDE"]
if not (lats and lons):
return
t_lat, t_lon = lats[-1]["val"], lons[-1]["val"]
mi, brg = haversine(ORIGIN_LAT, ORIGIN_LON, t_lat, t_lon)
_print_summary(t_lat, t_lon, mi, brg)
if do_patch:
out = target.parent / f"{target.stem}_patched.bin"
patch_firmware(target, out, t_lat, t_lon)
def _parse_args(args: list[str]) -> tuple[Path, bool]:
"""
Parse command line arguments for target path and patch flag.
Parameters
----------
args : list[str]
Command line arguments.
Returns
-------
tuple[pathlib.Path, bool]
Target binary path and patch flag.
"""
do_patch = "--patch" in args
paths = [p for p in args if not p.startswith("--")]
target = Path(paths[0]) if paths else Path("0x0011a_cb.bin")
return target, do_patch
def main() -> int:
"""
Execute firmware coordinate extraction and optional patching.
Parameters
----------
None
Returns
-------
int
Zero on success, non-zero on failure.
"""
target, do_patch = _parse_args(sys.argv[1:])
if not target.exists():
print(f"[-] Error: '{target}' not found.")
return 1
_print_banner(target)
items = scan_coordinates(target.read_bytes())
for item in items:
_print_candidate(item)
_evaluate(items, target, do_patch)
return 0
if __name__ == "__main__":
raise SystemExit(main())
+202
View File
@@ -0,0 +1,202 @@
#!/usr/bin/env python3
# MIT License
#
# Copyright (c) 2026 Kevin Thomas
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# Author: Kevin Thomas
# Email: kevin@mytechnotalent.com
# GitHub: https://github.com/mytechnotalent
# File: float_hex_converter.py
# Desc: Convert and explain IEEE 754 float/hex operations step-by-step.
# Created: 2026
"""Convert and explain IEEE 754 float/hex operations step-by-step."""
import argparse
import struct
import sys
def _exp_str(e_val: int, bias: int, is_64: bool) -> str:
"""
Get accurate true exponent string accounting for IEEE 754 edge cases.
Parameters
----------
e_val : int
The stored exponent value.
bias : int
The exponent bias (127 or 1023).
is_64 : bool
True if 64-bit precision.
Returns
-------
str
The formatted true exponent explanation string.
"""
if e_val == 0:
return f"0 (Zero/Subnormal, True Exp: {1 - bias})"
if e_val == (2047 if is_64 else 255):
return f"{e_val} (Inf/NaN flag)"
return f"{e_val} - {bias} = {e_val - bias}"
def _print_encode_steps(val: float, b: int) -> None:
"""
Print the math steps for encoding a float to hex.
Parameters
----------
val : float
The float value to encode.
b : int
The bit size (32 or 64).
Returns
-------
None
"""
f1, f2, bias = ("<Q", "<d", 1023) if b == 64 else ("<I", "<f", 127)
bstr = f"{struct.unpack(f1, struct.pack(f2, val))[0]:0{b}b}"
s, e, m = (
bstr[0],
bstr[1 : 1 + (11 if b == 64 else 8)],
bstr[1 + (11 if b == 64 else 8) :],
)
hex_val = f"0x{int(bstr, 2):0{b//4}X}"
print(f"\n[ENCODE {val} to {b}-bit]\n1. Sign: {s} (0=Pos, 1=Neg)")
print(f"2. Exp: {_exp_str(int(e, 2), bias, b == 64)}\n3. Mantissa: {m}")
print(f"4. Full: {s} {e} {m}\n5. Hex: {hex_val}")
if b == 64:
raw_hex = f"{int(bstr, 2):016X}"
r3 = f"0x{raw_hex[:8]}"
r2 = f"0x{raw_hex[8:]}"
print(f"6. ARM Regs: r3 (high) = {r3}, r2 (low) = {r2} (e.g. in printf)")
else:
print(f"6. ARM Reg: Single 32-bit register ({hex_val})")
def _print_decode_steps(hex_str: str) -> None:
"""
Print the math steps for decoding a hex string to float.
Parameters
----------
hex_str : str
The hex string to decode.
Returns
-------
None
"""
c = hex_str.lower()
if c.startswith("0x"):
c = c[2:]
b, f1, f2, bias = (64, "<Q", "<d", 1023) if len(c) > 8 else (32, "<I", "<f", 127)
bstr = f"{int(c, 16):0{b}b}"
s, e, m = (
bstr[0],
bstr[1 : 1 + (11 if b == 64 else 8)],
bstr[1 + (11 if b == 64 else 8) :],
)
print(f"\n[DECODE 0x{c.zfill(b//4).upper()} ({b}-bit)]\n1. Binary: {s} {e} {m}")
print(f"2. Sign: {s}\n3. Exp: {_exp_str(int(e, 2), bias, b == 64)}")
print(f"4. Value: {struct.unpack(f2, struct.pack(f1, int(c, 16)))[0]}")
if b == 64:
raw_hex = c.zfill(16).upper()
r3 = f"0x{raw_hex[:8]}"
r2 = f"0x{raw_hex[8:]}"
print(f"5. ARM Regs: r3 (high) = {r3}, r2 (low) = {r2} (e.g. in printf)")
else:
print(f"5. ARM Reg: Single 32-bit register (0x{c.zfill(8).upper()})")
def _is_hex(s: str) -> bool:
"""
Check if a string is a hexadecimal representation.
Parameters
----------
s : str
Candidate string, with or without a 0x prefix.
Returns
-------
bool
True if every character is a hexadecimal digit.
"""
cleaned = s.lower()
if cleaned.startswith("0x"):
cleaned = cleaned[2:]
if not cleaned:
return False
return all(c in "0123456789abcdef" for c in cleaned)
def _process_conversion(val_str: str) -> None:
"""
Execute the conversion and print the output.
Parameters
----------
val_str : str
The raw input string to process.
Returns
-------
None
"""
cleaned = val_str.strip()
if cleaned.lower().startswith("0x") or (len(cleaned) >= 8 and _is_hex(cleaned)):
_print_decode_steps(cleaned)
else:
val = float(cleaned)
_print_encode_steps(val, 32)
_print_encode_steps(val, 64)
def main() -> int:
"""
Execute the conversion pipeline based on CLI arguments.
Parameters
----------
None
Returns
-------
int
Zero on successful conversion, otherwise non-zero.
"""
parser = argparse.ArgumentParser(description="Float/Hex step converter.")
parser.add_argument("val", help="Hex (0x...) or Float value to convert.")
args = parser.parse_args()
try:
_process_conversion(args.val)
return 0
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
return 1
if __name__ == "__main__":
sys.exit(main())
+135
View File
@@ -0,0 +1,135 @@
#!/usr/bin/env python3
# MIT License
#
# Copyright (c) 2026 Kevin Thomas
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# Author: Kevin Thomas
# Email: kevin@mytechnotalent.com
# GitHub: https://github.com/mytechnotalent
# File: lora_console.py
# Desc: Interactive REYAX RYLR998 terminal for the FT232RL ground station.
# Created: 2026
"""
Interactive LoRa terminal for the REYAX RYLR998 ground station.
Sends AT commands as complete bursts terminated with a carriage return and line
feed to avoid the RYLR998 inter-character timeout error, and prints incoming
packets from the airborne node as they arrive.
"""
import argparse
import sys
import threading
import time
try:
import serial
except ImportError:
serial = None
def _reader_thread(ser: "serial.Serial") -> None:
"""
Continuously read and display incoming packets from the radio.
Parameters
----------
ser : serial.Serial
Open serial connection to the ground RYLR998.
Returns
-------
None
"""
while True:
try:
line = ser.readline().decode("utf-8", errors="ignore").strip()
if line:
print(f"\n[LORA RX] {line}\n> ", end="", flush=True)
except Exception:
break
def _parse_args() -> argparse.Namespace:
"""
Parse command line arguments for the LoRa console.
Parameters
----------
None
Returns
-------
argparse.Namespace
Parsed arguments with the serial port and baud rate.
"""
parser = argparse.ArgumentParser(description="REYAX RYLR998 console")
parser.add_argument("--port", default="/dev/cu.usbserial-A50285BI")
parser.add_argument("--baud", type=int, default=115200)
return parser.parse_args()
def main() -> None:
"""
Open the ground station radio and forward operator input.
Parameters
----------
None
Returns
-------
None
"""
if serial is None:
sys.exit("pyserial required: pip install pyserial")
args = _parse_args()
print(f"[*] Opening REYAX RYLR998 on {args.port} @ {args.baud}...")
try:
ser = serial.Serial(args.port, args.baud, timeout=0.5)
except Exception as e:
sys.exit(f"[-] Failed to open {args.port}: {e}")
thread = threading.Thread(target=_reader_thread, args=(ser,), daemon=True)
thread.start()
time.sleep(0.1)
ser.write(b"AT\r\n")
print("[+] Connected. Type AT commands (e.g. AT, AT+BAND?, AT+NETWORKID?).")
print("[+] Incoming airborne packets print as [LORA RX] +RCV=...")
print("[+] Press Ctrl-C or Ctrl-D to exit.\n")
try:
while True:
cmd = input("> ").strip()
if not cmd:
continue
ser.write(cmd.encode("utf-8") + b"\r\n")
except (KeyboardInterrupt, EOFError):
print("\n[*] Exiting LoRa console.")
ser.close()
if __name__ == "__main__":
main()
+234
View File
@@ -0,0 +1,234 @@
#!/usr/bin/env python3
# MIT License
#
# Copyright (c) 2026 Kevin Thomas
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# Author: Kevin Thomas
# Email: kevin@mytechnotalent.com
# GitHub: https://github.com/mytechnotalent
# File: randomize_build.py
# Desc: Builds a per-student 0x0011a_cb image with an AES-encrypted target.
# Created: 2026
"""
Per-student randomized CTF build.
Jitters the target waypoint, AES-128-ECB encrypts it under a per-build key, and
writes include/ctf_target.h so the firmware rebuilds it at boot. Every image
carries a different ciphertext and the plaintext target exists nowhere in flash.
An offline answer key or a memorized value is useless; the waypoint can only be
recovered from the artifact and confirmed on hardware.
"""
import argparse
import json
import pathlib
import random
import struct
import subprocess
import sys
BASE_LAT = 38.881940
BASE_LON = -77.450280
JITTER_DEG = 0.01
UF2_FAMILY = "0xe48bff59"
FLASH_BASE = "0x10000000"
def _parse_args() -> argparse.Namespace:
"""
Parse command line arguments for the randomized build.
Parameters
----------
None
Returns
-------
argparse.Namespace
Parsed arguments with seed, build dir, student id, and uf2 flag.
"""
here = pathlib.Path(__file__).resolve().parent
parser = argparse.ArgumentParser(description="Randomized CTF build")
parser.add_argument("--seed", type=int, default=None)
parser.add_argument("--build-dir", default=str(here.parent / "build-ctf"))
parser.add_argument("--student-id", default="student")
parser.add_argument("--uf2", action="store_true")
return parser.parse_args()
def _make_target(seed: int) -> tuple[float, float]:
"""
Generate a jittered target waypoint around the base coordinates.
Parameters
----------
seed : int
Deterministic seed for the jitter.
Returns
-------
tuple[float, float]
Jittered latitude and longitude, rounded to six decimals.
"""
rng = random.Random(seed)
lat = round(BASE_LAT + rng.uniform(-JITTER_DEG, JITTER_DEG), 6)
lon = round(BASE_LON + rng.uniform(-JITTER_DEG, JITTER_DEG), 6)
return lat, lon
def _make_key(seed: int) -> bytes:
"""
Derive a deterministic 16-byte AES key for the build.
Parameters
----------
seed : int
Build seed from which the key is derived.
Returns
-------
bytes
Sixteen byte AES-128 key.
"""
krng = random.Random(seed ^ 0x5EED)
return bytes(krng.randrange(256) for _ in range(16))
def _aes_ecb(plain: bytes, key: bytes) -> bytes:
"""
Encrypt one block with AES-128-ECB.
Parameters
----------
plain : bytes
Sixteen byte plaintext block.
key : bytes
Sixteen byte AES key.
Returns
-------
bytes
Sixteen byte ciphertext block.
"""
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
enc = Cipher(algorithms.AES(key), modes.ECB(), backend=default_backend()).encryptor()
return enc.update(plain) + enc.finalize()
def _write_header(path: pathlib.Path, key: bytes, ct: bytes) -> None:
"""
Write the AES key and ciphertext header consumed by the firmware.
Parameters
----------
path : pathlib.Path
Destination header path.
key : bytes
Sixteen byte AES key.
ct : bytes
Sixteen byte ciphertext block.
Returns
-------
None
"""
path.write_text(
"#ifndef CTF_TARGET_H\n#define CTF_TARGET_H\n\n#include <stdint.h>\n\n"
"#define CTF_AES_KEY { %s }\n"
"#define CTF_TARGET_CT { %s }\n\n"
"#endif // CTF_TARGET_H\n"
% (", ".join(f"0x{b:02X}" for b in key), ", ".join(f"0x{b:02X}" for b in ct)))
def _run(cmd: list[str], cwd: pathlib.Path) -> None:
"""
Run an external command and raise on failure.
Parameters
----------
cmd : list[str]
Command and arguments to execute.
cwd : pathlib.Path
Working directory for the command.
Returns
-------
None
"""
subprocess.run(cmd, cwd=str(cwd), check=True)
def main() -> int:
"""
Build a per-student image with an AES-encrypted, randomized target.
Parameters
----------
None
Returns
-------
int
Zero on success.
"""
args = _parse_args()
root = pathlib.Path(__file__).resolve().parent.parent
seed = args.seed if args.seed is not None else random.randrange(2**31)
lat, lon = _make_target(seed)
key = _make_key(seed)
pt = struct.pack("<d", lat) + struct.pack("<d", lon)
ct = _aes_ecb(pt, key)
_write_header(root / "include" / "ctf_target.h", key, ct)
build = pathlib.Path(args.build_dir).resolve()
_run(["cmake", "-S", str(root), "-B", str(build)], root)
_run(["cmake", "--build", str(build)], root)
image = build / "0x0011a_cb.bin"
if args.uf2:
out = build / f"0x0011a_cb_{args.student_id}.uf2"
_run([sys.executable, str(root / "uf2conv.py"), str(image),
"-f", UF2_FAMILY, "-b", FLASH_BASE, "-c", "-o", str(out)], root)
key_out = {"student_id": args.student_id, "seed": seed,
"target_lat": lat, "target_lon": lon,
"aes_key_hex": key.hex(), "ciphertext_hex": ct.hex(),
"image": str(image)}
keydir = root / "scratch"
keydir.mkdir(exist_ok=True)
keyfile = keydir / f"answer_{args.student_id}.json"
keyfile.write_text(json.dumps(key_out, indent=2) + "\n")
print(f"[+] student_id : {args.student_id}")
print(f"[+] TARGET_LAT : {lat}")
print(f"[+] TARGET_LON : {lon}")
print(f"[+] AES key : {key.hex()}")
print(f"[+] ciphertext : {ct.hex()}")
print(f"[+] image : {image}")
print(f"[+] answer key : {keyfile} (INSTRUCTOR ONLY, do not ship)")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+302
View File
@@ -0,0 +1,302 @@
#!/usr/bin/env python3
# MIT License
#
# Copyright (c) 2026 Kevin Thomas
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# Author: Kevin Thomas
# Email: kevin@mytechnotalent.com
# GitHub: https://github.com/mytechnotalent
# File: telemetry_monitor.py
# Desc: Real-time telemetry monitor for Operation Dark Vector.
# Created: 2026
"""
Real-time telemetry monitor for Operation Dark Vector.
Reads live avionics and GPS telemetry from the FT232RL USB-to-UART ground
station bridge, parses coordinates and distance, and prints a mission status
dashboard to the terminal.
"""
import argparse
import re
import sys
import time
try:
import serial
except ImportError:
serial = None
def _format_coord(val: float, pos_c: str, neg_c: str) -> str:
"""
Format a decimal coordinate with cardinal direction.
Parameters
----------
val : float
Coordinate value in degrees.
pos_c : str
Cardinal letter for positive values.
neg_c : str
Cardinal letter for negative values.
Returns
-------
str
Formatted coordinate string.
"""
card = pos_c if val >= 0.0 else neg_c
return f"{abs(val):.6f} deg {card}"
def _format_pos(lat: float, lon: float) -> str:
"""
Format combined latitude and longitude string.
Parameters
----------
lat : float
Latitude coordinate.
lon : float
Longitude coordinate.
Returns
-------
str
Formatted dual coordinate string.
"""
lat_s = _format_coord(lat, 'N', 'S')
lon_s = _format_coord(lon, 'E', 'W')
return f"{lat_s} {lon_s}"
def _format_hud_rows(
cur_lat: float,
cur_lon: float,
is_rel: bool,
has_lock: bool = False
) -> list[str]:
"""
Format HUD data rows.
Parameters
----------
cur_lat : float
Current UAV latitude.
cur_lon : float
Current UAV longitude.
is_rel : bool
Whether payload has released.
has_lock : bool
Whether active GNSS 3D lock has been acquired.
Returns
-------
list[str]
List of formatted box rows.
"""
if cur_lat == 0.0 and cur_lon == 0.0:
c_s = "0.000000 deg N 0.000000 deg E"
g_s = "SEARCHING SATELLITES"
m_s = "MOTOR STOPPED [WAITING FOR 3D LOCK]"
else:
c_s = _format_pos(cur_lat, cur_lon)
g_s = "ACTIVE 3D LOCK" if has_lock else "SEARCHING SATELLITES"
m_s = "ACTIVE PROPULSION [SERVO SPINNING]" if has_lock else "MOTOR STOPPED [WAITING FOR 3D LOCK]"
return [
f"| CURRENT POSITION : {c_s:<44} |",
f"| GNSS SUBSYSTEM : {g_s:<44} |",
f"| PROPULSION MOTOR : {m_s:<44} |"
]
def _print_box(title: str, link: str, rows: list[str]) -> None:
"""
Print framed ASCII box.
Parameters
----------
title : str
Box title line.
link : str
Sub-header line.
rows : list[str]
Body content rows.
Returns
-------
None
"""
hdr = f"+{'-' * 65}+"
print(hdr)
print(title)
print(link)
print(hdr)
for r in rows:
print(r)
print(hdr + "\n", flush=True)
def _print_hud(
cur_lat: float,
cur_lon: float,
is_released: bool,
has_lock: bool = False
) -> None:
"""
Display the 67-character telemetry mission HUD.
Parameters
----------
cur_lat : float
Current UAV latitude.
cur_lon : float
Current UAV longitude.
is_released : bool
Whether payload solenoid has been energized.
has_lock : bool
Whether active GNSS 3D lock has been acquired.
Returns
-------
None
"""
rows = _format_hud_rows(cur_lat, cur_lon, is_released, has_lock)
title = f"|{'DARK VECTOR TELEMETRY CONSOLE':^65}|"
status = f"{'STATUS: ONLINE':>20}"
link = f"| LINK: FT232RL / RYLR998 LORA GROUND STATION{status} |"
_print_box(title, link, rows)
def _run_demo() -> None:
"""
Execute simulation of drone telemetry stream.
Parameters
----------
None
Returns
-------
None
"""
print("[*] Running simulated ground station telemetry stream...\n")
_print_hud(0.0, 0.0, False, False)
time.sleep(1.0)
_print_hud(38.840280, -77.428890, False, True)
time.sleep(1.0)
_print_hud(38.861110, -77.439585, False, True)
time.sleep(1.0)
_print_hud(38.881940, -77.450280, True, True)
def _process_line(
line: str,
coords: dict[str, any]
) -> bool:
"""
Parse a single line of serial telemetry using regex.
Parameters
----------
line : str
Raw serial string.
coords : dict[str, any]
State dictionary of coordinates.
Returns
-------
bool
True if telemetry data was updated, False otherwise.
"""
updated = False
m_cur = re.search(r"CURRENT LAT:\s*([-+]?\d*\.?\d+).*?LON:\s*([-+]?\d*\.?\d+)", line)
if m_cur:
coords["cur_lat"] = float(m_cur.group(1))
coords["cur_lon"] = float(m_cur.group(2))
coords["has_lock"] = True
updated = True
if "PAYLOAD RELEASED" in line:
coords["released"] = 1.0
updated = True
return updated
def _monitor_serial(port: str, baud: int) -> None:
"""
Monitor serial stream from FT232RL ground station.
Parameters
----------
port : str
Serial device path.
baud : int
Baud rate.
Returns
-------
None
"""
if serial is None:
sys.exit("pyserial required: pip install pyserial")
coords = {
"cur_lat": 0.0,
"cur_lon": 0.0,
"released": 0.0,
"has_lock": False
}
with serial.Serial(port, baud, timeout=1.0) as ser:
while True:
raw = ser.readline().decode("utf-8", errors="ignore").strip()
if raw:
_process_line(raw, coords)
rel = coords["released"] > 0.5
_print_hud(coords["cur_lat"], coords["cur_lon"], rel, coords["has_lock"])
def main() -> None:
"""
Parse arguments and start telemetry monitor.
Parameters
----------
None
Returns
-------
None
"""
parser = argparse.ArgumentParser(description="Dark Vector Telemetry")
parser.add_argument("--port", default="/dev/tty.usbserial-0001")
parser.add_argument("--baud", type=int, default=115200)
parser.add_argument("--demo", action="store_true")
args = parser.parse_args()
if args.demo:
_run_demo()
return
_monitor_serial(args.port, args.baud)
if __name__ == "__main__":
main()
+119
View File
@@ -0,0 +1,119 @@
// MIT License
//
// Copyright (c) 2026 Kevin Thomas
//
// File: aes.c
// Desc: Minimal AES-128-ECB block decryption for the CTF waypoint.
// Created: 2026
#include "aes.h"
#include <string.h>
#include <stdint.h>
static const uint8_t sbox[256] = {
0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76,
0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0,
0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15,
0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75,
0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0, 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84,
0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF,
0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8,
0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2,
0xCD, 0x0C, 0x13, 0xEC, 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73,
0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB,
0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79,
0xE7, 0xC8, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08,
0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A,
0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, 0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E,
0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF,
0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16,
};
static const uint8_t rsbox[256] = {
0x52, 0x09, 0x6A, 0xD5, 0x30, 0x36, 0xA5, 0x38, 0xBF, 0x40, 0xA3, 0x9E, 0x81, 0xF3, 0xD7, 0xFB,
0x7C, 0xE3, 0x39, 0x82, 0x9B, 0x2F, 0xFF, 0x87, 0x34, 0x8E, 0x43, 0x44, 0xC4, 0xDE, 0xE9, 0xCB,
0x54, 0x7B, 0x94, 0x32, 0xA6, 0xC2, 0x23, 0x3D, 0xEE, 0x4C, 0x95, 0x0B, 0x42, 0xFA, 0xC3, 0x4E,
0x08, 0x2E, 0xA1, 0x66, 0x28, 0xD9, 0x24, 0xB2, 0x76, 0x5B, 0xA2, 0x49, 0x6D, 0x8B, 0xD1, 0x25,
0x72, 0xF8, 0xF6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xD4, 0xA4, 0x5C, 0xCC, 0x5D, 0x65, 0xB6, 0x92,
0x6C, 0x70, 0x48, 0x50, 0xFD, 0xED, 0xB9, 0xDA, 0x5E, 0x15, 0x46, 0x57, 0xA7, 0x8D, 0x9D, 0x84,
0x90, 0xD8, 0xAB, 0x00, 0x8C, 0xBC, 0xD3, 0x0A, 0xF7, 0xE4, 0x58, 0x05, 0xB8, 0xB3, 0x45, 0x06,
0xD0, 0x2C, 0x1E, 0x8F, 0xCA, 0x3F, 0x0F, 0x02, 0xC1, 0xAF, 0xBD, 0x03, 0x01, 0x13, 0x8A, 0x6B,
0x3A, 0x91, 0x11, 0x41, 0x4F, 0x67, 0xDC, 0xEA, 0x97, 0xF2, 0xCF, 0xCE, 0xF0, 0xB4, 0xE6, 0x73,
0x96, 0xAC, 0x74, 0x22, 0xE7, 0xAD, 0x35, 0x85, 0xE2, 0xF9, 0x37, 0xE8, 0x1C, 0x75, 0xDF, 0x6E,
0x47, 0xF1, 0x1A, 0x71, 0x1D, 0x29, 0xC5, 0x89, 0x6F, 0xB7, 0x62, 0x0E, 0xAA, 0x18, 0xBE, 0x1B,
0xFC, 0x56, 0x3E, 0x4B, 0xC6, 0xD2, 0x79, 0x20, 0x9A, 0xDB, 0xC0, 0xFE, 0x78, 0xCD, 0x5A, 0xF4,
0x1F, 0xDD, 0xA8, 0x33, 0x88, 0x07, 0xC7, 0x31, 0xB1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xEC, 0x5F,
0x60, 0x51, 0x7F, 0xA9, 0x19, 0xB5, 0x4A, 0x0D, 0x2D, 0xE5, 0x7A, 0x9F, 0x93, 0xC9, 0x9C, 0xEF,
0xA0, 0xE0, 0x3B, 0x4D, 0xAE, 0x2A, 0xF5, 0xB0, 0xC8, 0xEB, 0xBB, 0x3C, 0x83, 0x53, 0x99, 0x61,
0x17, 0x2B, 0x04, 0x7E, 0xBA, 0x77, 0xD6, 0x26, 0xE1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0C, 0x7D,
};
static const uint8_t Rcon[11] = {0x00,0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80,0x1B,0x36};
static uint8_t xtime(uint8_t x) { return (uint8_t)((x << 1) ^ (((x >> 7) & 1) * 0x1B)); }
static uint8_t mul(uint8_t a, uint8_t b) {
uint8_t p = 0;
for (int i = 0; i < 8; i++) {
if (b & 1) p ^= a;
uint8_t hi = a & 0x80; a <<= 1;
if (hi) a ^= 0x1B;
b >>= 1;
}
return p;
}
static void key_expansion(const uint8_t *key, uint8_t *rk) {
memcpy(rk, key, 16);
for (int i = 4; i < 44; i++) {
uint8_t t[4];
memcpy(t, &rk[(i - 1) * 4], 4);
if (i % 4 == 0) {
uint8_t tmp = t[0];
t[0] = (uint8_t)(sbox[t[1]] ^ Rcon[i / 4]);
t[1] = sbox[t[2]];
t[2] = sbox[t[3]];
t[3] = sbox[tmp];
}
for (int j = 0; j < 4; j++) rk[i * 4 + j] = rk[(i - 4) * 4 + j] ^ t[j];
}
}
static void add_round_key(uint8_t r, uint8_t *s, const uint8_t *rk) {
for (int i = 0; i < 16; i++) s[i] ^= rk[r * 16 + i];
}
static void inv_sub_bytes(uint8_t *s) { for (int i = 0; i < 16; i++) s[i] = rsbox[s[i]]; }
static void inv_shift_rows(uint8_t *s) {
uint8_t t;
t = s[13]; s[13] = s[9]; s[9] = s[5]; s[5] = s[1]; s[1] = t;
t = s[2]; s[2] = s[10]; s[10] = t; t = s[6]; s[6] = s[14]; s[14] = t;
t = s[3]; s[3] = s[7]; s[7] = s[11]; s[11] = s[15]; s[15] = t;
}
static void inv_mix_columns(uint8_t *s) {
for (int i = 0; i < 4; i++) {
uint8_t a = s[i * 4], b = s[i * 4 + 1], c = s[i * 4 + 2], d = s[i * 4 + 3];
s[i * 4] = (uint8_t)(mul(a, 14) ^ mul(b, 11) ^ mul(c, 13) ^ mul(d, 9));
s[i * 4 + 1] = (uint8_t)(mul(a, 9) ^ mul(b, 14) ^ mul(c, 11) ^ mul(d, 13));
s[i * 4 + 2] = (uint8_t)(mul(a, 13) ^ mul(b, 9) ^ mul(c, 14) ^ mul(d, 11));
s[i * 4 + 3] = (uint8_t)(mul(a, 11) ^ mul(b, 13) ^ mul(c, 9) ^ mul(d, 14));
}
}
void aes128_ecb_decrypt_block(const uint8_t in[16], const uint8_t key[16], uint8_t out[16]) {
uint8_t rk[176];
key_expansion(key, rk);
memcpy(out, in, 16);
add_round_key(10, out, rk);
for (int r = 9; r > 0; r--) {
inv_shift_rows(out);
inv_sub_bytes(out);
add_round_key(r, out, rk);
inv_mix_columns(out);
}
inv_shift_rows(out);
inv_sub_bytes(out);
add_round_key(0, out, rk);
}
+172
View File
@@ -0,0 +1,172 @@
// MIT License
//
// Copyright (c) 2026 Kevin Thomas
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
// Author: Kevin Thomas
// Email: kevin@mytechnotalent.com
// GitHub: https://github.com/mytechnotalent
// File: gps.c
// Desc: Implements PIO UART GPS receiver and NMEA coordinate parsing.
// Created: 2026
#include "gps.h"
#include "uart_rx.pio.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static int gps_siv = 0;
static int gps_cno = 0;
void init_gps_pio(void)
{
uint offset = pio_add_program(GPS_PIO, &uart_rx_program);
uart_rx_program_init(GPS_PIO, GPS_SM, offset, GPS_PIN, GPS_BAUD);
}
/**
* @brief Convert NMEA ddmm.mmmm coordinate to decimal degrees.
*
* @param str NMEA coordinate string.
* @param dir Cardinal direction character ('N', 'S', 'E', 'W').
* @return double Decimal degree coordinate.
*/
static double parse_nmea_coord(const char *str, char dir)
{
double raw = atof(str);
int deg = (int)(raw / 100.0);
double dec = (double)deg + ((raw - (deg * 100.0)) / 60.0);
return ((dir == 'S') || (dir == 'W')) ? -dec : dec;
}
/**
* @brief Find pointer to n-th comma-separated field in NMEA string.
*
* @param str NMEA sentence string.
* @param field_idx Index of field to locate.
* @return const char* Pointer to field start or NULL.
*/
static const char *get_nmea_field(const char *str, int field_idx)
{
while ((str != NULL) && (*str != '\0') && (field_idx > 0)) {
if (*str++ == ',') {
field_idx--;
}
}
return (field_idx == 0) ? str : NULL;
}
/**
* @brief Parse NMEA RMC sentence for valid coordinates.
*
* @param line NMEA sentence buffer.
* @param lat Pointer to store parsed latitude.
* @param lon Pointer to store parsed longitude.
* @return None.
*/
static bool parse_rmc(const char *line, double *lat, double *lon)
{
const char *st = get_nmea_field(line, 2), *la = get_nmea_field(line, 3);
const char *lo = get_nmea_field(line, 5);
if (!st || *st != 'A' || !la || *la == ',' || !lo || *lo == ',') return false;
*lat = parse_nmea_coord(la, *get_nmea_field(line, 4));
*lon = parse_nmea_coord(lo, *get_nmea_field(line, 6));
return (*lat != 0.0) && (*lon != 0.0);
}
/**
* @brief Parse NMEA GSV sentence for satellites-in-view and best C/N0.
*
* @param line NMEA GSV sentence string.
* @return None.
*/
static void parse_gsv(const char *line)
{
const char *sv = get_nmea_field(line, 3), *msg = get_nmea_field(line, 2);
if (sv != NULL) gps_siv = atoi(sv);
if ((msg != NULL) && (*msg == '1')) gps_cno = 0;
for (int f = 7; f < 40; f += 4) {
const char *c = get_nmea_field(line, f);
if ((c == NULL) || (*c == '*') || (*c == '\0')) break;
if (atoi(c) > gps_cno) gps_cno = atoi(c);
}
}
/**
* @brief Test buffered line for valid NMEA RMC sentence.
*
* @param buf NMEA character buffer.
* @param idx Pointer to character index.
* @param lat Pointer to current latitude.
* @param lon Pointer to current longitude.
* @return bool True if valid 3D fix was parsed, false otherwise.
*/
static bool check_rmc_line(char *buf, int *idx, double *lat, double *lon)
{
buf[*idx] = '\0';
*idx = 0;
char *rmc = strstr(buf, "RMC"), *gsv = strstr(buf, "GSV");
if (gsv != NULL) parse_gsv(gsv);
return (rmc != NULL) ? parse_rmc(rmc, lat, lon) : false;
}
/**
* @brief Accumulate GPS character and trigger RMC parsing on newline.
*
* @param ch Received ASCII character.
* @param lat Pointer to current latitude.
* @param lon Pointer to current longitude.
* @return bool True if a valid active 3D fix was parsed, false otherwise.
*/
static bool process_gps_char(char ch, double *lat, double *lon)
{
static char buf[96];
static int idx = 0;
if (ch == '$') idx = 0;
if ((ch == '\n') || (ch == '\r'))
return check_rmc_line(buf, &idx, lat, lon);
if (idx < (int)(sizeof(buf) - 1))
buf[idx++] = ch;
return false;
}
static bool handle_gps_byte(double *lat, double *lon)
{
char ch = (char)(pio_sm_get(GPS_PIO, GPS_SM) >> 24);
return process_gps_char(ch, lat, lon);
}
bool poll_gps(double *lat, double *lon)
{
bool got_fix = false;
while (!pio_sm_is_rx_fifo_empty(GPS_PIO, GPS_SM))
got_fix |= handle_gps_byte(lat, lon);
return got_fix;
}
void gps_get_stats(int *siv, int *cno)
{
*siv = gps_siv;
*cno = gps_cno;
}
+144
View File
@@ -0,0 +1,144 @@
// MIT License
//
// Copyright (c) 2026 Kevin Thomas
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
// Author: Kevin Thomas
// Email: kevin@mytechnotalent.com
// GitHub: https://github.com/mytechnotalent
// File: lcd.c
// Desc: Implements I2C HD44780 16x2 LCD display for live telemetry coordinates.
// Created: 2026
#include "lcd.h"
#include "pico/stdlib.h"
#include <stdio.h>
#include <string.h>
#include <math.h>
#define PIN_RS 0x01
#define PIN_EN 0x04
#define BACKLIGHT 0x08
static uint8_t lcd_addr = 0x27;
static bool lcd_ready = false;
static void pcf_write(uint8_t d)
{
if (lcd_ready)
i2c_write_blocking(LCD_I2C_INST, lcd_addr, &d, 1, false);
}
static void pcf_pulse(uint8_t d)
{
pcf_write(d | PIN_EN);
sleep_us(1);
pcf_write(d & ~PIN_EN);
sleep_us(50);
}
static void lcd_write4(uint8_t n, uint8_t mode)
{
uint8_t d = (n & 0x0F) << 4;
d |= mode ? PIN_RS : 0;
d |= BACKLIGHT;
pcf_pulse(d);
}
static void lcd_send(uint8_t v, uint8_t mode)
{
lcd_write4((v >> 4) & 0x0F, mode);
lcd_write4(v & 0x0F, mode);
}
static void lcd_clear(void)
{
lcd_send(0x01, 0);
sleep_ms(2);
}
static void lcd_set_cursor(int row, int col)
{
uint8_t offset = (row == 0) ? 0x00 : 0x40;
lcd_send(0x80 | (col + offset), 0);
}
static void lcd_puts(const char *s)
{
while (*s)
lcd_send((uint8_t)*s++, 1);
}
static void lcd_reset_seq(void)
{
lcd_write4(0x03, 0); sleep_ms(5);
lcd_write4(0x03, 0); sleep_us(150);
lcd_write4(0x03, 0); sleep_us(150);
lcd_write4(0x02, 0); sleep_us(150);
}
static void lcd_cfg_seq(void)
{
lcd_send(0x28, 0);
lcd_send(0x0C, 0);
lcd_clear();
lcd_send(0x06, 0);
}
static bool detect_lcd(void)
{
uint8_t rx;
if (i2c_read_blocking(LCD_I2C_INST, 0x27, &rx, 1, false) >= 0)
return (lcd_addr = 0x27, true);
if (i2c_read_blocking(LCD_I2C_INST, 0x3F, &rx, 1, false) >= 0)
return (lcd_addr = 0x3F, true);
return false;
}
void init_lcd(void)
{
i2c_init(LCD_I2C_INST, LCD_BAUD);
gpio_set_function(LCD_SDA_PIN, GPIO_FUNC_I2C);
gpio_set_function(LCD_SCL_PIN, GPIO_FUNC_I2C);
gpio_pull_up(LCD_SDA_PIN); gpio_pull_up(LCD_SCL_PIN);
if (!(lcd_ready = detect_lcd())) return;
lcd_reset_seq();
lcd_cfg_seq();
}
void lcd_show_coords(double lat, double lon)
{
if (!lcd_ready && !(lcd_ready = detect_lcd())) return;
char r1[17], r2[17];
snprintf(r1, sizeof(r1), "LAT: %9.6f %c", fabs(lat), (lat >= 0.0) ? 'N' : 'S');
snprintf(r2, sizeof(r2), "LON: %9.6f %c", fabs(lon), (lon >= 0.0) ? 'E' : 'W');
lcd_set_cursor(0, 0); lcd_puts(r1);
lcd_set_cursor(1, 0); lcd_puts(r2);
}
void lcd_show_gnss(int sats, int cno)
{
if (!lcd_ready && !(lcd_ready = detect_lcd())) return;
char r1[17], r2[17];
snprintf(r1, sizeof(r1), "SAT:%2d CNO:%2d ", sats, cno);
snprintf(r2, sizeof(r2), "%-16s", (sats > 0) ? "ACQUIRING..." : "NO SIGNAL");
lcd_set_cursor(0, 0); lcd_puts(r1);
lcd_set_cursor(1, 0); lcd_puts(r2);
}
+89
View File
@@ -0,0 +1,89 @@
// MIT License
//
// Copyright (c) 2026 Kevin Thomas
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
// Author: Kevin Thomas
// Email: kevin@mytechnotalent.com
// GitHub: https://github.com/mytechnotalent
// File: lora.c
// Desc: Implements UART1 driver for REYAX RYLR998 LoRa transceiver.
// Created: 2026
#include "lora.h"
#include "hardware/gpio.h"
#include "pico/stdlib.h"
#include <stdio.h>
#include <string.h>
static void drain_lora_rx(void)
{
while (uart_is_readable(LORA_UART)) {
char c = (char)uart_getc(LORA_UART);
if (c >= 32 && c <= 126)
putchar(c);
}
}
static void send_at_cmd(const char *cmd)
{
uart_write_blocking(LORA_UART, (const uint8_t *)cmd, strlen(cmd));
sleep_ms(250);
drain_lora_rx();
}
static void configure_lora_rf(void)
{
sleep_ms(1500);
send_at_cmd("AT\r\n");
send_at_cmd("AT+NETWORKID=18\r\n");
send_at_cmd("AT+BAND=915000000\r\n");
send_at_cmd("AT+PARAMETER=9,7,1,12\r\n");
send_at_cmd("AT+ADDRESS=2\r\n");
}
void init_lora(void)
{
uart_init(LORA_UART, LORA_BAUD);
uart_set_translate_crlf(LORA_UART, false);
gpio_set_function(LORA_TX_PIN, GPIO_FUNC_UART);
gpio_set_function(LORA_RX_PIN, GPIO_FUNC_UART);
configure_lora_rf();
}
static char tx_buf[160];
static int tx_len = 0;
static int tx_idx = 0;
void lora_send(const char *msg)
{
int len = (int)strlen(msg);
while ((len > 0) && ((msg[len - 1] == '\r') || (msg[len - 1] == '\n')))
len--;
tx_len = snprintf(tx_buf, sizeof(tx_buf), "AT+SEND=0,%d,%.*s\r\n", len, len, msg);
tx_idx = 0;
}
void lora_tick(void)
{
while ((tx_idx < tx_len) && uart_is_writable(LORA_UART))
uart_putc_raw(LORA_UART, (uint8_t)tx_buf[tx_idx++]);
drain_lora_rx();
}
+108
View File
@@ -0,0 +1,108 @@
// MIT License
//
// Copyright (c) 2026 Kevin Thomas
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
// Author: Kevin Thomas
// Email: kevin@mytechnotalent.com
// GitHub: https://github.com/mytechnotalent
// File: main.c
// Desc: Main entry point for autonomous micro-UAV guidance firmware.
// Created: 2026
#include "gps.h"
#include "lora.h"
#include "payload.h"
#include "propeller.h"
#include "navigation.h"
#include "lcd.h"
#include "pico/stdlib.h"
#include "hardware/gpio.h"
#include "hardware/pio.h"
#include <stdio.h>
/**
* @brief Initialize all board peripherals, communications, and actuators.
*
* @param None.
* @return None.
*/
static void init_all(void)
{
stdio_init_all();
init_navigation();
init_payload();
init_lora();
init_gps_pio();
init_propeller();
init_lcd();
}
/**
* @brief Continually drain GPS PIO FIFO over 1-second flight tick.
*
* @param cur_lat Pointer to current latitude.
* @param cur_lon Pointer to current longitude.
* @return bool True if active 3D lock was parsed, false otherwise.
*/
static bool update_position(double *cur_lat, double *cur_lon)
{
bool got_fix = false;
for (int i = 0; i < 200; i++, sleep_ms(5)) {
got_fix |= poll_gps(cur_lat, cur_lon);
lora_tick();
}
return got_fix;
}
static void step_mission(double *cur_lat, double *cur_lon)
{
int siv = 0, cno = 0;
static int hold = 0;
bool fix = update_position(cur_lat, cur_lon);
gps_get_stats(&siv, &cno);
hold = fix ? 3 : ((hold > 0) ? (hold - 1) : 0);
bool have = fix || (hold > 0);
set_gnss_leds(have, siv);
if (have) {
lcd_show_coords(*cur_lat, *cur_lon);
navigate_to_target(*cur_lat, *cur_lon);
} else {
lcd_show_gnss(siv, cno);
propeller_stop();
send_telemetry(*cur_lat, *cur_lon);
}
}
/**
* @brief Autonomous micro-UAV firmware execution loop.
*
* @param None.
* @return int Standard exit code (never reached in embedded firmware).
*/
int main(void)
{
double cur_lat = ORIGIN_LAT, cur_lon = ORIGIN_LON;
init_all();
while (true)
step_mission(&cur_lat, &cur_lon);
return 0;
}
+106
View File
@@ -0,0 +1,106 @@
// MIT License
//
// Copyright (c) 2026 Kevin Thomas
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
// Author: Kevin Thomas
// Email: kevin@mytechnotalent.com
// GitHub: https://github.com/mytechnotalent
// File: navigation.c
// Desc: Implements waypoint navigation, dead-reckoning, and telemetry dispatch.
// Created: 2026
#include "navigation.h"
#include "ctf_target.h"
#include "aes.h"
#include "lora.h"
#include "payload.h"
#include "propeller.h"
#include <stdio.h>
#include <math.h>
#include <string.h>
#include <stdint.h>
double TARGET_LAT = 0.0;
double TARGET_LON = 0.0;
const double ORIGIN_LAT = 38.840280;
const double ORIGIN_LON = -77.428890;
void init_navigation(void)
{
uint8_t pt[16];
const uint8_t key[16] = CTF_AES_KEY;
const uint8_t ct[16] = CTF_TARGET_CT;
aes128_ecb_decrypt_block(ct, key, pt);
memcpy(&TARGET_LAT, pt, sizeof(double));
memcpy(&TARGET_LON, pt + 8, sizeof(double));
}
void send_telemetry(double cur_lat, double cur_lon)
{
char msg[80];
snprintf(msg, sizeof(msg), "CURRENT LAT: %lf, LON: %lf\r\n", cur_lat, cur_lon);
printf("%s", msg);
lora_send(msg);
}
void dead_reckon_step(double *cur_lat, double *cur_lon)
{
double dlat = TARGET_LAT - *cur_lat;
double dlon = TARGET_LON - *cur_lon;
*cur_lat += (fabs(dlat) < 0.005) ? dlat : ((dlat > 0.0) ? 0.004166 : -0.004166);
*cur_lon += (fabs(dlon) < 0.005) ? dlon : ((dlon > 0.0) ? 0.002139 : -0.002139);
}
bool check_arrival(double cur_lat, double cur_lon)
{
return (cur_lat == TARGET_LAT) && (cur_lon == TARGET_LON);
}
/**
* @brief Compute the initial bearing from one point toward another.
*
* @param lat1 Origin latitude in degrees.
* @param lon1 Origin longitude in degrees.
* @param lat2 Destination latitude in degrees.
* @param lon2 Destination longitude in degrees.
* @return double Bearing in degrees (0 to 360).
*/
static double bearing_to(double lat1, double lon1, double lat2, double lon2)
{
double p1 = lat1 * 0.017453292519943295, p2 = lat2 * 0.017453292519943295;
double dl = (lon2 - lon1) * 0.017453292519943295;
double y = sin(dl) * cos(p2);
double x = cos(p1) * sin(p2) - sin(p1) * cos(p2) * cos(dl);
double b = atan2(y, x) * 57.29577951308232;
return (b < 0.0) ? (b + 360.0) : b;
}
void navigate_to_target(double cur_lat, double cur_lon)
{
send_telemetry(cur_lat, cur_lon);
if (check_arrival(cur_lat, cur_lon)) {
propeller_stop();
release_payload();
} else {
propeller_set_bearing(bearing_to(cur_lat, cur_lon, TARGET_LAT, TARGET_LON));
}
}
+58
View File
@@ -0,0 +1,58 @@
// MIT License
//
// Copyright (c) 2026 Kevin Thomas
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
// Author: Kevin Thomas
// Email: kevin@mytechnotalent.com
// GitHub: https://github.com/mytechnotalent
// File: payload.c
// Desc: Implements payload release solenoid latch control on GPIO16.
// Created: 2026
#include "payload.h"
#include "lora.h"
#include "hardware/gpio.h"
#include <stdio.h>
void init_payload(void)
{
gpio_init(16); gpio_set_dir(16, GPIO_OUT); gpio_put(16, 1);
gpio_init(17); gpio_set_dir(17, GPIO_OUT); gpio_put(17, 0);
gpio_init(18); gpio_set_dir(18, GPIO_OUT); gpio_put(18, 0);
gpio_init(25); gpio_set_dir(25, GPIO_OUT); gpio_put(25, 0);
}
void set_gnss_leds(bool fix, int siv)
{
gpio_put(16, (!fix && (siv == 0)) ? 1 : 0);
gpio_put(17, fix ? 1 : 0);
gpio_put(18, (!fix && (siv > 0)) ? 1 : 0);
}
void release_payload(void)
{
gpio_put(16, 1);
gpio_put(17, 1);
gpio_put(18, 1);
printf("PAYLOAD RELEASED AT TARGET COORDINATES\r\n");
lora_send("PAYLOAD RELEASED AT TARGET COORDINATES\r\n");
}
+84
View File
@@ -0,0 +1,84 @@
// MIT License
//
// Copyright (c) 2026 Kevin Thomas
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
// Author: Kevin Thomas
// Email: kevin@mytechnotalent.com
// GitHub: https://github.com/mytechnotalent
// File: propeller.c
// Desc: Implements SG90 servo PWM mock propeller control on GPIO6.
// Created: 2026
#include "propeller.h"
#include "pico/stdlib.h"
#include "hardware/pwm.h"
#include "hardware/gpio.h"
static struct repeating_timer prop_timer;
static bool prop_active = false;
static uint16_t current_pulse = 1000;
void init_propeller(void)
{
gpio_set_function(PROPELLER_PIN, GPIO_FUNC_PWM);
uint s = pwm_gpio_to_slice_num(PROPELLER_PIN);
pwm_set_clkdiv(s, 150.0f);
pwm_set_wrap(s, 19999);
pwm_set_gpio_level(PROPELLER_PIN, 0);
pwm_set_enabled(s, true);
}
/**
* @brief Repeating timer callback to alternate servo angle at max slew rate.
*
* @param t Pointer to repeating timer structure.
* @return bool Always true to continue recurring timer.
*/
static bool prop_timer_callback(struct repeating_timer *t)
{
(void)t;
current_pulse = (current_pulse == 1000) ? 2000 : 1000;
pwm_set_gpio_level(PROPELLER_PIN, current_pulse);
return true;
}
void propeller_spin(void)
{
if (!prop_active) {
prop_active = true;
add_repeating_timer_ms(-150, prop_timer_callback, NULL, &prop_timer);
}
}
void propeller_set_bearing(double deg)
{
uint16_t pulse = (uint16_t)(1000.0 + (deg / 360.0) * 1000.0);
pwm_set_gpio_level(PROPELLER_PIN, pulse);
}
void propeller_stop(void)
{
if (prop_active) {
cancel_repeating_timer(&prop_timer);
prop_active = false;
}
pwm_set_gpio_level(PROPELLER_PIN, 0);
}
+43
View File
@@ -0,0 +1,43 @@
;
; Copyright (c) 2026 Kevin Thomas
; SPDX-License-Identifier: MIT
;
.pio_version 0
.program uart_rx
; 8n1 UART receiver for GPS NMEA reception on a single GPIO pin.
; Operates at 8 execution cycles per bit period.
; IN pin 0 and JMP pin are mapped to the GPS RX GPIO pin.
start:
wait 0 pin 0 ; Wait for start bit falling edge (1 cycle)
set x, 7 [10] ; Preload bit counter (7 remaining), delay 1.5 bit periods (11 cycles)
bitloop:
in pins, 1 ; Sample 1 bit from RX pin into ISR (1 cycle)
jmp x-- bitloop [6] ; Loop 8 times; each iteration is 8 execution cycles (7 cycles delay)
jmp pin good_stop ; Verify stop bit is HIGH
wait 1 pin 0 ; Framing error: wait until line returns to idle HIGH
jmp start ; Discard frame and re-synchronize
good_stop:
push noblock ; Push 8-bit byte into RX FIFO (bits [31:24])
% c-sdk {
#include "hardware/clocks.h"
#include "hardware/gpio.h"
static inline void uart_rx_program_init(PIO pio, uint sm, uint offset, uint pin, uint baud) {
pio_sm_set_consecutive_pindirs(pio, sm, pin, 1, false);
pio_gpio_init(pio, pin);
gpio_pull_up(pin);
pio_sm_config c = uart_rx_program_get_default_config(offset);
sm_config_set_in_pins(&c, pin);
sm_config_set_jmp_pin(&c, pin);
sm_config_set_in_shift(&c, true, false, 32);
sm_config_set_fifo_join(&c, PIO_FIFO_JOIN_RX);
float div = (float)clock_get_hz(clk_sys) / (8 * baud);
sm_config_set_clkdiv(&c, div);
pio_sm_init(pio, sm, offset, &c);
pio_sm_set_enabled(pio, sm, true);
}
%}
+365
View File
@@ -0,0 +1,365 @@
#!/usr/bin/env python3
import sys
import struct
import subprocess
import re
import os
import os.path
import argparse
import json
from time import sleep
UF2_MAGIC_START0 = 0x0A324655 # "UF2\n"
UF2_MAGIC_START1 = 0x9E5D5157 # Randomly selected
UF2_MAGIC_END = 0x0AB16F30 # Ditto
INFO_FILE = "/INFO_UF2.TXT"
appstartaddr = 0x2000
familyid = 0x0
def is_uf2(buf):
w = struct.unpack("<II", buf[0:8])
return w[0] == UF2_MAGIC_START0 and w[1] == UF2_MAGIC_START1
def is_hex(buf):
try:
w = buf[0:30].decode("utf-8")
except UnicodeDecodeError:
return False
if w[0] == ':' and re.match(rb"^[:0-9a-fA-F\r\n]+$", buf):
return True
return False
def convert_from_uf2(buf):
global appstartaddr
global familyid
numblocks = len(buf) // 512
curraddr = None
currfamilyid = None
families_found = {}
prev_flag = None
all_flags_same = True
outp = []
for blockno in range(numblocks):
ptr = blockno * 512
block = buf[ptr:ptr + 512]
hd = struct.unpack(b"<IIIIIIII", block[0:32])
if hd[0] != UF2_MAGIC_START0 or hd[1] != UF2_MAGIC_START1:
print("Skipping block at " + ptr + "; bad magic")
continue
if hd[2] & 1:
# NO-flash flag set; skip block
continue
datalen = hd[4]
if datalen > 476:
assert False, "Invalid UF2 data size at " + ptr
newaddr = hd[3]
if (hd[2] & 0x2000) and (currfamilyid == None):
currfamilyid = hd[7]
if curraddr == None or ((hd[2] & 0x2000) and hd[7] != currfamilyid):
currfamilyid = hd[7]
curraddr = newaddr
if familyid == 0x0 or familyid == hd[7]:
appstartaddr = newaddr
padding = newaddr - curraddr
if padding < 0:
assert False, "Block out of order at " + ptr
if padding > 10*1024*1024:
assert False, "More than 10M of padding needed at " + ptr
if padding % 4 != 0:
assert False, "Non-word padding size at " + ptr
while padding > 0:
padding -= 4
outp.append(b"\x00\x00\x00\x00")
if familyid == 0x0 or ((hd[2] & 0x2000) and familyid == hd[7]):
outp.append(block[32 : 32 + datalen])
curraddr = newaddr + datalen
if hd[2] & 0x2000:
if hd[7] in families_found.keys():
if families_found[hd[7]] > newaddr:
families_found[hd[7]] = newaddr
else:
families_found[hd[7]] = newaddr
if prev_flag == None:
prev_flag = hd[2]
if prev_flag != hd[2]:
all_flags_same = False
if blockno == (numblocks - 1):
print("--- UF2 File Header Info ---")
families = load_families()
for family_hex in families_found.keys():
family_short_name = ""
for name, value in families.items():
if value == family_hex:
family_short_name = name
print("Family ID is {:s}, hex value is 0x{:08x}".format(family_short_name,family_hex))
print("Target Address is 0x{:08x}".format(families_found[family_hex]))
if all_flags_same:
print("All block flag values consistent, 0x{:04x}".format(hd[2]))
else:
print("Flags were not all the same")
print("----------------------------")
if len(families_found) > 1 and familyid == 0x0:
outp = []
appstartaddr = 0x0
return b"".join(outp)
def convert_to_carray(file_content):
outp = "const unsigned long bindata_len = %d;\n" % len(file_content)
outp += "const unsigned char bindata[] __attribute__((aligned(16))) = {"
for i in range(len(file_content)):
if i % 16 == 0:
outp += "\n"
outp += "0x%02x, " % file_content[i]
outp += "\n};\n"
return bytes(outp, "utf-8")
def convert_to_uf2(file_content):
global familyid
datapadding = b""
while len(datapadding) < 512 - 256 - 32 - 4:
datapadding += b"\x00\x00\x00\x00"
numblocks = (len(file_content) + 255) // 256
outp = []
for blockno in range(numblocks):
ptr = 256 * blockno
chunk = file_content[ptr:ptr + 256]
flags = 0x0
if familyid:
flags |= 0x2000
hd = struct.pack(b"<IIIIIIII",
UF2_MAGIC_START0, UF2_MAGIC_START1,
flags, ptr + appstartaddr, 256, blockno, numblocks, familyid)
while len(chunk) < 256:
chunk += b"\x00"
block = hd + chunk + datapadding + struct.pack(b"<I", UF2_MAGIC_END)
assert len(block) == 512
outp.append(block)
return b"".join(outp)
class Block:
def __init__(self, addr, default_data=0xFF):
self.addr = addr
self.bytes = bytearray([default_data] * 256)
def encode(self, blockno, numblocks):
global familyid
flags = 0x0
if familyid:
flags |= 0x2000
hd = struct.pack("<IIIIIIII",
UF2_MAGIC_START0, UF2_MAGIC_START1,
flags, self.addr, 256, blockno, numblocks, familyid)
hd += self.bytes[0:256]
while len(hd) < 512 - 4:
hd += b"\x00"
hd += struct.pack("<I", UF2_MAGIC_END)
return hd
def convert_from_hex_to_uf2(buf):
global appstartaddr
appstartaddr = None
upper = 0
currblock = None
blocks = []
for line in buf.split('\n'):
if line[0] != ":":
continue
i = 1
rec = []
while i < len(line) - 1:
rec.append(int(line[i:i+2], 16))
i += 2
tp = rec[3]
if tp == 4:
upper = ((rec[4] << 8) | rec[5]) << 16
elif tp == 2:
upper = ((rec[4] << 8) | rec[5]) << 4
elif tp == 1:
break
elif tp == 0:
addr = upper + ((rec[1] << 8) | rec[2])
if appstartaddr == None:
appstartaddr = addr
i = 4
while i < len(rec) - 1:
if not currblock or currblock.addr & ~0xff != addr & ~0xff:
currblock = Block(addr & ~0xff)
blocks.append(currblock)
currblock.bytes[addr & 0xff] = rec[i]
addr += 1
i += 1
numblocks = len(blocks)
resfile = b""
for i in range(0, numblocks):
resfile += blocks[i].encode(i, numblocks)
return resfile
def to_str(b):
return b.decode("utf-8")
def get_drives():
drives = []
if sys.platform == "win32":
r = subprocess.check_output([
"powershell",
"-Command",
'(Get-WmiObject Win32_LogicalDisk -Filter "VolumeName=\'RPI-RP2\'").DeviceID'
])
drive = to_str(r).strip()
if drive:
drives.append(drive)
else:
searchpaths = ["/mnt", "/media"]
if sys.platform == "darwin":
searchpaths = ["/Volumes"]
elif sys.platform == "linux":
searchpaths += ["/media/" + os.environ["USER"], "/run/media/" + os.environ["USER"]]
if "SUDO_USER" in os.environ.keys():
searchpaths += ["/media/" + os.environ["SUDO_USER"]]
searchpaths += ["/run/media/" + os.environ["SUDO_USER"]]
for rootpath in searchpaths:
if os.path.isdir(rootpath):
for d in os.listdir(rootpath):
if os.path.isdir(os.path.join(rootpath, d)):
drives.append(os.path.join(rootpath, d))
def has_info(d):
try:
return os.path.isfile(d + INFO_FILE)
except:
return False
return list(filter(has_info, drives))
def board_id(path):
with open(path + INFO_FILE, mode='r') as file:
file_content = file.read()
return re.search(r"Board-ID: ([^\r\n]*)", file_content).group(1)
def list_drives():
for d in get_drives():
print(d, board_id(d))
def write_file(name, buf):
with open(name, "wb") as f:
f.write(buf)
print("Wrote %d bytes to %s" % (len(buf), name))
def load_families():
# The expectation is that the `uf2families.json` file is in the same
# directory as this script. Make a path that works using `__file__`
# which contains the full path to this script.
filename = "uf2families.json"
pathname = os.path.join(os.path.dirname(os.path.abspath(__file__)), filename)
with open(pathname) as f:
raw_families = json.load(f)
families = {}
for family in raw_families:
families[family["short_name"]] = int(family["id"], 0)
return families
def main():
global appstartaddr, familyid
def error(msg):
print(msg, file=sys.stderr)
sys.exit(1)
parser = argparse.ArgumentParser(description='Convert to UF2 or flash directly.')
parser.add_argument('input', metavar='INPUT', type=str, nargs='?',
help='input file (HEX, BIN or UF2)')
parser.add_argument('-b', '--base', dest='base', type=str,
default="0x2000",
help='set base address of application for BIN format (default: 0x2000)')
parser.add_argument('-f', '--family', dest='family', type=str,
default="0x0",
help='specify familyID - number or name (default: 0x0)')
parser.add_argument('-o', '--output', metavar="FILE", dest='output', type=str,
help='write output to named file; defaults to "flash.uf2" or "flash.bin" where sensible')
parser.add_argument('-d', '--device', dest="device_path",
help='select a device path to flash')
parser.add_argument('-l', '--list', action='store_true',
help='list connected devices')
parser.add_argument('-c', '--convert', action='store_true',
help='do not flash, just convert')
parser.add_argument('-D', '--deploy', action='store_true',
help='just flash, do not convert')
parser.add_argument('-w', '--wait', action='store_true',
help='wait for device to flash')
parser.add_argument('-C', '--carray', action='store_true',
help='convert binary file to a C array, not UF2')
parser.add_argument('-i', '--info', action='store_true',
help='display header information from UF2, do not convert')
args = parser.parse_args()
appstartaddr = int(args.base, 0)
families = load_families()
if args.family.upper() in families:
familyid = families[args.family.upper()]
else:
try:
familyid = int(args.family, 0)
except ValueError:
error("Family ID needs to be a number or one of: " + ", ".join(families.keys()))
if args.list:
list_drives()
else:
if not args.input:
error("Need input file")
with open(args.input, mode='rb') as f:
inpbuf = f.read()
from_uf2 = is_uf2(inpbuf)
ext = "uf2"
if args.deploy:
outbuf = inpbuf
elif from_uf2 and not args.info:
outbuf = convert_from_uf2(inpbuf)
ext = "bin"
elif from_uf2 and args.info:
outbuf = ""
convert_from_uf2(inpbuf)
elif is_hex(inpbuf):
outbuf = convert_from_hex_to_uf2(inpbuf.decode("utf-8"))
elif args.carray:
outbuf = convert_to_carray(inpbuf)
ext = "h"
else:
outbuf = convert_to_uf2(inpbuf)
if not args.deploy and not args.info:
print("Converted to %s, output size: %d, start address: 0x%x" %
(ext, len(outbuf), appstartaddr))
if args.convert or ext != "uf2":
if args.output == None:
args.output = "flash." + ext
if args.output:
write_file(args.output, outbuf)
if ext == "uf2" and not args.convert and not args.info:
drives = get_drives()
if len(drives) == 0:
if args.wait:
print("Waiting for drive to deploy...")
while len(drives) == 0:
sleep(0.1)
drives = get_drives()
elif not args.output:
error("No drive to deploy.")
for d in drives:
print("Flashing %s (%s)" % (d, board_id(d)))
write_file(d + "/NEW.UF2", outbuf)
if __name__ == "__main__":
main()
+22
View File
@@ -0,0 +1,22 @@
[
{
"short_name": "RP2040",
"id": "0xe48bff56",
"description": "Raspberry Pi RP2040"
},
{
"short_name": "RP2350-ARM-S",
"id": "0xe48bff59",
"description": "Raspberry Pi RP2350, ARM, Secure"
},
{
"short_name": "RP2350-ARM-NS",
"id": "0xe48bff5a",
"description": "Raspberry Pi RP2350, ARM, Non-Secure"
},
{
"short_name": "RP2350-RISCV",
"id": "0xe48bff5b",
"description": "Raspberry Pi RP2350, RISC-V"
}
]