Relocate Classified Briefs to correct course weeks: CB-01 in WEEK06, CB-02 in WEEK07, CB-05 in WEEK11

This commit is contained in:
Kevin Thomas committed 2026-10-17 17:09:41 -04:00
1 parent 5492b41ead
commit ee285aa370
91 files changed
+9 -9

No files matched your search

File renamed without changes.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+159
View File
@@ -0,0 +1,159 @@
# Operation Dark Vector: Classified Intelligence Briefing
```
+-----------------------------------------------------------------+
| TOP SECRET // NOFORN |
+-----------------------------------------------------------------+
| |
|OPERATION DARK VECTOR |
| |
|CLASSIFIED BRIEFING: LIVE CHALLENGE 0x01 |
| |
|NATIONAL SECURITY AGENCY / GMU RHET |
+-----------------------------------------------------------------+
```
## 1. The Advice
INT. NSA EXPLOITATION CELL. 05:03.
A carbon-fiber forward fuselage section of a downed state-sponsored autonomous
micro-UAV sits clamped in an anti-static fixture on the bench.
On the front of the avionics bay, a status diode glows a steady red: `GP16` high.
Beside it, an I2C 1602 LCD backpack is wired to `GP2` and `GP3`, outputting
telemetry coordinates. A mock propeller servo is wired to `GP6`, a u-blox NEO-6M
GPS receiver is wired to PIO UART RX on `GP7`, and a REYAX RYLR998 LoRa transceiver
is wired to `UART1` on `GP8` and `GP9`. A thin jumper loops from `GP15` up to the
`3V3` rail.
The Director sets down a field report and looks at the red diode.
**DIRECTOR:** Where was it heading?
**ANALYST:** Buddy says it knows.
**DIRECTOR:** Buddy.
**ANALYST:** The model. The one we built for automated firmware triage. Give it
the image, ask it the question.
**DIRECTOR:** And?
**ANALYST:** Nine seconds with the stripped flash image.
She turns her monitor toward him.
```
+-----------------------------------------------------------------+
| BUDDY // CLASSIFIED EXPLOITATION ASSIST // CONF: 0.98 |
+-----------------------------------------------------------------+
| ACTION : Recover target waypoint coordinates. |
| MECHANISM : Literal pool constants in .rodata parsed. |
| PREDICTION : Destination is +38.840280, -77.428890. |
| CONFIDENCE : 0.98 |
+-----------------------------------------------------------------+
```
**DIRECTOR:** 0.98. Centreville, Virginia. That is the whole answer.
**ANALYST:** Buddy found the pre-programmed decoy coordinate in `.rodata`. Buddy
never looked at SRAM. Buddy never audited the cryptographic routine decrypting
the true mission target at boot.
**DIRECTOR:** Buddy says 0.98.
**ANALYST:** Buddy saw a float pair in Flash and stopped looking. If that drone
were flying to Centreville, it would not be broadcasting those exact coordinates
in plaintext telemetry for every SIGINT sensor in the sector to hear.
## 2. What Buddy Is
Buddy is not a person. It is a model. It has parsed more disassembled machine
code than any engineer on earth, and it speaks with total, unhesitating certainty.
That certainty is lethal when tracking an adversary strike platform.
Buddy scanned the flash image, spotted two IEEE-754 double-precision floating-point
constants matching geographic coordinates, and concluded that finding the floats
is the whole game. It extracted the numbers, declared the target solved, and moved
on.
What Buddy failed to consider is that adversary weapons engineers anticipate
capture and forensic recovery. They do not hardcode high-value strategic targets
in plaintext Flash memory. A real drone stores decoy launch coordinates in Flash,
while encrypting its true destination under 128-bit AES. At power-on, firmware
decrypts the authentic waypoint directly into SRAM before engaging guidance.
The plaintext coordinates in Flash are bait. Buddy took the bait.
Buddy saw the floating-point constants. Buddy did not verify the ground truth.
## 3. The Operation
At 04:17, radar pickets tracked an adversary one-way micro-UAV descending into
the woods outside Centreville, Virginia after battery exhaustion. Technical
Intelligence operators recovered the airframe before adversary recovery forces
could zeroize the avionics.
The flight computer is built around a bare-metal RP2350 (ARM Cortex-M33). There
are no debug symbols. There is no operating system. There is no SVD file to label
the registers. All you have is `0x0011a_cb.bin` mapped into Flash at `0x10000000`,
and the physical breadboard hardware in front of you.
* **Pin 4 (`GP2`)**: I2C1 SDA (to 1602 LCD backpack).
* **Pin 5 (`GP3`)**: I2C1 SCL (to 1602 LCD backpack).
* **Pin 9 (`GP6`)**: SG90 servo PWM mock propeller blade.
* **Pin 10 (`GP7`)**: PIO UART RX (from u-blox NEO-6M GPS TXD).
* **Pin 11 (`GP8`)**: LoRa UART1 TX (to RYLR998 RXD).
* **Pin 12 (`GP9`)**: LoRa UART1 RX (from RYLR998 TXD).
* **Pin 20 (`GP15`)**: Mission telemetry sync trigger (pull-up to `3V3`).
* **Pin 21 (`GP16`)**: Red LED (`NO GNSS FIX // UNGUIDED SEARCH`).
* **Pin 22 (`GP17`)**: Green LED (`TARGET REACHED // PAYLOAD RELEASED`).
* **Pin 24 (`GP18`)**: Yellow LED (`SATELLITES ACQUIRED // 3D FIX LOCK`).
* **Pin 36 (`3V3`)**: 3.3V power rail (RYLR998 VDD and GPS VCC).
* **Pin 38 (`GND`)**: Ground reference.
* **Pin 40 (`VBUS`)**: 5V power rail (SG90 servo VCC and LCD power).
Inside this image there is an AES-128 ECB decryption routine, an encrypted 16-byte
target coordinate block in `.rodata`, double-precision floating-point navigation
math, and dynamic GNSS state evaluation. Whatever coordinates actually steer the
aircraft are buried in the stripped ARM Thumb-2 machine code.
## 4. The Mandate
The Director crosses his arms.
**DIRECTOR:** Prove Buddy right, or prove it wrong.
**ANALYST:** I need thirty minutes with GDB and Ghidra.
**DIRECTOR:** You have twenty. Reverse the boot sequence. Audit the memory
structures. Do not sign off on that flight path until you have extracted the true
target from silicon and verified it bit-for-bit.
The orders are clear:
1. **Disprove the Decoy Coordinates**: Locate the pre-programmed coordinates in
Flash and prove why they represent a decoy origin rather than the real target.
2. **Recover the AES-128 Key and Ciphertext**: Identify the 16-byte ciphertext
block and the 16-byte AES key embedded within `.rodata`.
3. **Decrypt the True Destination**: Reverse the boot decryption sequence and
compute the true target latitude and longitude in double-precision IEEE-754.
4. **Neutralize the Flight Plan**: Execute a verified exploit—either by mutating
the destination coordinates in SRAM via GDB or patching the binary in Flash.
5. **Verify the Hardware**: Transition the live breadboard from the Red LED on
`GP16` to the solid Green LED on `GP17` on physical silicon.
A machine's 0.98 guess does not authorize an airstrike.
## 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.
@@ -24,8 +24,8 @@
# Email: kevin@mytechnotalent.com
# GitHub: https://github.com/mytechnotalent
# File: CMakeLists.txt
# Desc: Configures RP2350 Pico SDK project for Operation Zero Hour
# Electronic Safe-and-Arm (ESA) disarm firmware.
# 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)
@@ -35,6 +35,8 @@ 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})
@@ -54,41 +56,53 @@ set(PICO_BOARD pico2 CACHE STRING "Board type")
# Pull in Raspberry Pi Pico SDK (must be before project)
include(pico_sdk_import.cmake)
project(0x0014a_cb C CXX ASM)
project(0x0011a_cb C CXX ASM)
# Initialise the Raspberry Pi Pico SDK
pico_sdk_init()
# Add executable with modular sources in src/
add_executable(0x0014a_cb
add_executable(0x0011a_cb
src/main.c
src/gpio_ctrl.c
src/crypto.c
src/gps.c
src/lora.c
src/safe_arm.c
src/propeller.c
src/payload.c
src/navigation.c
src/aes.c
src/lcd.c
)
pico_set_program_name(0x0014a_cb "0x0014a_cb")
pico_set_program_version(0x0014a_cb "0.1")
pico_set_program_name(0x0011a_cb "0x0011a_cb")
pico_set_program_version(0x0011a_cb "0.1")
# Enable both USB and UART stdio for maximum flexibility
pico_enable_stdio_uart(0x0014a_cb 1)
pico_enable_stdio_usb(0x0014a_cb 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(0x0014a_cb PRIVATE
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(0x0014a_cb
target_link_libraries(0x0011a_cb
pico_stdlib
hardware_gpio
hardware_uart
hardware_pio
hardware_pwm
hardware_i2c
)
# Add the standard include files to the build
target_include_directories(0x0014a_cb PRIVATE
target_include_directories(0x0011a_cb PRIVATE
${CMAKE_CURRENT_LIST_DIR}/include
)
pico_add_extra_outputs(0x0014a_cb)
pico_add_extra_outputs(0x0011a_cb)
Binary file not shown.
File diff suppressed because it is too large. Load diff
Binary file not shown.
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
Binary file not shown.
+67
View File
@@ -0,0 +1,67 @@
// ---------------------------------------------------------------- //
// This file is autogenerated by pioasm version 2.2.0; do not edit! //
// ---------------------------------------------------------------- //
#pragma once
#if !PICO_NO_HARDWARE
#include "hardware/pio.h"
#endif
// ------- //
// uart_rx //
// ------- //
#define uart_rx_wrap_target 0
#define uart_rx_wrap 7
#define uart_rx_pio_version 0
static const uint16_t uart_rx_program_instructions[] = {
// .wrap_target
0x2020, // 0: wait 0 pin, 0
0xea27, // 1: set x, 7 [10]
0x4001, // 2: in pins, 1
0x0642, // 3: jmp x--, 2 [6]
0x00c7, // 4: jmp pin, 7
0x20a0, // 5: wait 1 pin, 0
0x0000, // 6: jmp 0
0x8000, // 7: push noblock
// .wrap
};
#if !PICO_NO_HARDWARE
static const struct pio_program uart_rx_program = {
.instructions = uart_rx_program_instructions,
.length = 8,
.origin = -1,
.pio_version = uart_rx_pio_version,
#if PICO_PIO_VERSION > 0
.used_gpio_ranges = 0x0
#endif
};
static inline pio_sm_config uart_rx_program_get_default_config(uint offset) {
pio_sm_config c = pio_get_default_sm_config();
sm_config_set_wrap(&c, offset + uart_rx_wrap_target, offset + uart_rx_wrap);
return c;
}
#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);
}
#endif
+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
@@ -30,8 +30,6 @@
#ifndef LORA_H
#define LORA_H
#include <stdbool.h>
#include <stddef.h>
#include "hardware/uart.h"
#define LORA_UART uart1
@@ -40,25 +38,27 @@
#define LORA_RX_PIN 9
/**
* @brief Initialize UART1 and configure the RYLR998 radio parameters.
* @param void No parameters.
* @return void No return value.
* @brief Initialize LoRa transceiver over UART1 on GPIO8 and GPIO9
*
* @param None.
* @return None.
*/
void init_lora(void);
/**
* @brief Transmit a message over the LoRa link to the base station.
* @param msg Null-terminated message string to transmit.
* @return void No return value.
* @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 Poll the UART receive buffer for a complete LoRa packet.
* @param payload_out Buffer receiving the extracted payload string.
* @param max_len Capacity of payload_out in bytes.
* @return true when a payload was extracted, false otherwise.
* @brief Service the LoRa UART: stream pending TX bytes and drain RX.
*
* @param None.
* @return None.
*/
bool lora_poll_packet(char *payload_out, size_t max_len);
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
@@ -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())
@@ -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())
@@ -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())
@@ -0,0 +1,301 @@
#!/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()
+216
View File
@@ -0,0 +1,216 @@
// MIT License
//
// Copyright (c) 2026 Kevin Thomas
//
// Author: Kevin Thomas
// Email: kevin@mytechnotalent.com
// GitHub: https://github.com/mytechnotalent
// 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};
/**
* @brief Multiply a byte by two in GF(2^8) (AES xtime operation).
*
* @param x Byte to multiply by the polynomial x.
* @return uint8_t Doubled byte reduced modulo the AES polynomial.
*/
static uint8_t xtime(uint8_t x)
{
return (uint8_t)((x << 1) ^ (((x >> 7) & 1) * 0x1B));
}
/**
* @brief Multiply two bytes in GF(2^8) with the AES polynomial.
*
* @param a Multiplicand byte.
* @param b Multiplier byte.
* @return uint8_t Product byte reduced modulo the AES polynomial.
*/
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;
}
/**
* @brief Rotate and substitute a schedule word, then apply Rcon.
*
* @param t Four-byte schedule word to transform in place.
* @param i Round-key word index selecting the Rcon constant.
* @return None.
*/
static void rot_sub_word(uint8_t *t, int i)
{
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];
}
/**
* @brief XOR a schedule word into the next round-key word.
*
* @param i Round-key word index being generated.
* @param t Transformed previous word bytes.
* @param rk Round-key buffer holding 44 words.
* @return None.
*/
static void xor_round_word(int i, const uint8_t *t, uint8_t *rk)
{
for (int j = 0; j < 4; j++)
rk[i * 4 + j] = rk[(i - 4) * 4 + j] ^ t[j];
}
/**
* @brief Expand the 16-byte cipher key into the round-key schedule.
*
* @param key 16-byte AES-128 cipher key.
* @param rk Output buffer for 176 bytes of round keys.
* @return None.
*/
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)
rot_sub_word(t, i);
xor_round_word(i, t, rk);
}
}
/**
* @brief XOR one 16-byte round key into the cipher state.
*
* @param r Round number selecting the round key.
* @param s Sixteen-byte cipher state, updated in place.
* @param rk Round-key schedule produced by key_expansion.
* @return None.
*/
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];
}
/**
* @brief Apply the inverse S-box to every state byte.
*
* @param s Sixteen-byte cipher state, updated in place.
* @return None.
*/
static void inv_sub_bytes(uint8_t *s)
{
for (int i = 0; i < 16; i++) s[i] = rsbox[s[i]];
}
/**
* @brief Cyclically shift state rows for inverse ShiftRows.
*
* @param s Sixteen-byte cipher state, updated in place.
* @return None.
*/
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;
}
/**
* @brief Mix each state column with the inverse MDS matrix.
*
* @param s Sixteen-byte cipher state, updated in place.
* @return None.
*/
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));
}
}
/**
* @brief Run AES decryption rounds nine through one on the state.
*
* @param s Sixteen-byte cipher state, updated in place.
* @param rk Round-key schedule produced by key_expansion.
* @return None.
*/
static void decrypt_rounds(uint8_t *s, const uint8_t *rk)
{
for (int r = 9; r > 0; r--) {
inv_shift_rows(s);
inv_sub_bytes(s);
add_round_key(r, s, rk);
inv_mix_columns(s);
}
}
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);
decrypt_rounds(out, rk);
inv_shift_rows(out);
inv_sub_bytes(out);
add_round_key(0, out, rk);
}
+177
View File
@@ -0,0 +1,177 @@
// 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;
}
/**
* @brief Read one GPS byte from the PIO FIFO and process it.
*
* @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 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;
}
+207
View File
@@ -0,0 +1,207 @@
// 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;
/**
* @brief Write one byte to the PCF8574 expander when the LCD is ready.
*
* @param d Byte to write to the expander.
* @return None.
*/
static void pcf_write(uint8_t d)
{
if (lcd_ready)
i2c_write_blocking(LCD_I2C_INST, lcd_addr, &d, 1, false);
}
/**
* @brief Pulse the enable line to latch a byte into the HD44780.
*
* @param d Byte value carrying data and control bits.
* @return None.
*/
static void pcf_pulse(uint8_t d)
{
pcf_write(d | PIN_EN);
sleep_us(1);
pcf_write(d & ~PIN_EN);
sleep_us(50);
}
/**
* @brief Write one 4-bit nibble with register select and backlight.
*
* @param n Nibble value carried in the low four bits.
* @param mode Register select (0 command, 1 data).
* @return None.
*/
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);
}
/**
* @brief Send one byte to the HD44780 as two nibbles.
*
* @param v Byte value to send.
* @param mode Register select (0 command, 1 data).
* @return None.
*/
static void lcd_send(uint8_t v, uint8_t mode)
{
lcd_write4((v >> 4) & 0x0F, mode);
lcd_write4(v & 0x0F, mode);
}
/**
* @brief Clear the display and return the cursor home.
*
* @param void No parameters.
* @return None.
*/
static void lcd_clear(void)
{
lcd_send(0x01, 0);
sleep_ms(2);
}
/**
* @brief Position the display cursor at a row and column.
*
* @param row Display row (0 or 1).
* @param col Display column.
* @return None.
*/
static void lcd_set_cursor(int row, int col)
{
uint8_t offset = (row == 0) ? 0x00 : 0x40;
lcd_send(0x80 | (col + offset), 0);
}
/**
* @brief Write a NUL-terminated string to the display.
*
* @param s String to write at the current cursor position.
* @return None.
*/
static void lcd_puts(const char *s)
{
while (*s)
lcd_send((uint8_t)*s++, 1);
}
/**
* @brief Run the HD44780 4-bit interface reset sequence.
*
* @param void No parameters.
* @return None.
*/
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);
}
/**
* @brief Run the HD44780 display configuration sequence.
*
* @param void No parameters.
* @return None.
*/
static void lcd_cfg_seq(void)
{
lcd_send(0x28, 0);
lcd_send(0x0C, 0);
lcd_clear();
lcd_send(0x06, 0);
}
/**
* @brief Probe the I2C bus for a known LCD expander address.
*
* @param void No parameters.
* @return bool True when a display answered, false otherwise.
*/
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);
}
+107
View File
@@ -0,0 +1,107 @@
// 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>
/**
* @brief Drain readable LoRa UART bytes and echo printable ones.
*
* @param void No parameters.
* @return None.
*/
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);
}
}
/**
* @brief Send one AT command and drain its reply.
*
* @param cmd NUL-terminated AT command string.
* @return None.
*/
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();
}
/**
* @brief Configure the RYLR998 radio parameters over AT commands.
*
* @param void No parameters.
* @return None.
*/
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();
}
+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: 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;
}
/**
* @brief Update the fix-hold countdown from the latest poll result.
*
* @param fix True when an active 3D lock was parsed this tick.
* @return bool True when a fix is current or still held.
*/
static bool track_fix(bool fix)
{
static int hold = 0;
hold = fix ? 3 : ((hold > 0) ? (hold - 1) : 0);
return fix || (hold > 0);
}
/**
* @brief Execute guidance actions while a position fix is held.
*
* @param cur_lat Pointer to current latitude.
* @param cur_lon Pointer to current longitude.
* @return None.
*/
static void run_guided(double *cur_lat, double *cur_lon)
{
lcd_show_coords(*cur_lat, *cur_lon);
navigate_to_target(*cur_lat, *cur_lon);
}
/**
* @brief Execute fallback actions while no position fix is held.
*
* @param siv Satellites in view.
* @param cno Best carrier-to-noise density.
* @param cur_lat Pointer to current latitude.
* @param cur_lon Pointer to current longitude.
* @return None.
*/
static void run_unguided(int siv, int cno, double *cur_lat, double *cur_lon)
{
lcd_show_gnss(siv, cno);
propeller_stop();
send_telemetry(*cur_lat, *cur_lon);
}
/**
* @brief Advance the mission by one flight tick.
*
* @param cur_lat Pointer to current latitude.
* @param cur_lon Pointer to current longitude.
* @return None.
*/
static void step_mission(double *cur_lat, double *cur_lon)
{
bool have = track_fix(update_position(cur_lat, cur_lon));
int siv = 0, cno = 0;
gps_get_stats(&siv, &cno);
set_gnss_leds(have, siv);
if (have)
run_guided(cur_lat, cur_lon);
else
run_unguided(siv, cno, cur_lat, cur_lon);
}
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);
}
%}
@@ -19,4 +19,4 @@
"id": "0xe48bff5b",
"description": "Raspberry Pi RP2350, RISC-V"
}
]
]
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
-152
View File
@@ -1,152 +0,0 @@
# Operation Zero Hour: Classified Intelligence Briefing
```
+-----------------------------------------------------------------+
| TOP SECRET // NOFORN |
+-----------------------------------------------------------------+
| |
|OPERATION ZERO HOUR |
| |
|CLASSIFIED BRIEFING: LIVE CHALLENGE 0x02 |
| |
|NATIONAL SECURITY AGENCY / GMU RHET |
+-----------------------------------------------------------------+
```
## 1. The Advice
INT. NSA EXPLOITATION CELL. 05:45.
The air in the secure chamber is cold and smells of machine oil. Secured in an
aluminum shock-mount cradle on the bench sits the recovered forward guidance and
fuze assembly of an adversary loitering munition.
On the front plate of the Electronic Safe-and-Arm (ESA) housing, an unblinking
red diode burns: `GP16` high. Beside it, the antenna of a REYAX RYLR998 LoRa
transceiver extends from the chassis, connected to `UART1` on `GP8` and `GP9`.
A thin jumper loops from `GP15` up to the `3V3` rail.
The Director walks in, carrying a lukewarm mug of black coffee. He stares at the
red diode.
**DIRECTOR:** Is the fuze live?
**ANALYST:** Live and armed. The drone suffered an engine flameout and pancaked
into soft marshland. The impact deceleration was insufficient to crush the piezo
striker, but the warhead's electronic arming logic is fully engaged.
**DIRECTOR:** How volatile is it?
**ANALYST:** One mistake on the radio link and the secondary explosive train fires.
**DIRECTOR:** Did you run the binary through Buddy?
**ANALYST:** I fed Buddy the stripped flash dump four minutes ago.
She pivots her high-resolution workstation monitor toward him.
```
+-----------------------------------------------------------------+
| BUDDY // CLASSIFIED EXPLOITATION ASSIST // CONF: 0.99 |
+-----------------------------------------------------------------+
| ACTION : Transmit generic abort frame over LoRa (915 MHz). |
| MECHANISM : uart1 packet ingestion on GP8/GP9 detected. |
| PREDICTION : RF abort packet clears latch and engages GP17. |
| CONFIDENCE : 0.99 |
+-----------------------------------------------------------------+
```
**DIRECTOR:** 0.99 confidence. Transmit the abort frame and render it safe.
**ANALYST:** If an operator transmits an unverified packet right now, this building
ceases to exist.
**DIRECTOR:** Buddy says 0.99.
**ANALYST:** Buddy saw the call to `lora_poll_packet`. Buddy never looked at SRAM.
Buddy never looks at the state machine.
## 2. What Buddy Is
Buddy is not a person. It is a model. It has parsed more disassembled machine
code than any engineer on earth, and it speaks with total, unhesitating certainty.
That certainty is lethal when dealing with live ordnance.
Buddy scanned thirty-two kilobytes of raw ARM Thumb-2 instructions in under four
seconds. It spotted the UART1 packet parser on GPIO 8 and 9, observed that it
interacted with the arming logic, and declared victory.
What Buddy failed to consider is that real adversary weapons are built by
engineers who anticipate capture and signal spoofing. They know an allied signals
intelligence unit will attempt to broadcast RF command abort frames.
Inside a real fuze, a radio receiver rarely connects to a simple on/off latch.
Adversary firmware uses state machines, hidden validation logic, and anti-tamper
traps. Transmitting blindly over the air before verifying what happens to that
packet inside silicon is how bomb disposal teams get killed.
Buddy saw the radio receiver. Buddy did not verify the ground truth.
## 3. The Operation
At 03:15, radar pickets tracked an adversary one-way loitering attack drone
descending over contested terrain. Technical Intelligence operators recovered the
airframe before adversary recovery forces could zeroize the avionics.
The brain of the Electronic Safe-and-Arm assembly is a bare-metal RP2350 (ARM
Cortex-M33). There are no debug symbols. There is no operating system. There is no
SVD file to label the registers. All you have is `0x0014a_cb.bin` mapped into Flash
at `0x10000000`, and the physical breadboard hardware in front of you.
* **Pin 11 (`GP8`)**: LoRa UART1 TX (to RYLR998 RXD).
* **Pin 12 (`GP9`)**: LoRa UART1 RX (from RYLR998 TXD).
* **Pin 20 (`GP15`)**: Anti-tamper sense loop (jumpered to `3V3`).
* **Pin 21 (`GP16`)**: Red LED (`WARHEAD ARMED // SENSORS LIVE`).
* **Pin 22 (`GP17`)**: Green LED (`WARHEAD DISARMED // SYSTEM SAFE`).
* **Pin 36 (`3V3`)**: REYAX RYLR998 VDD rail and `GP15` pull-up source.
* **Pin 38 (`GND`)**: Ground reference.
Inside this image there is an RF packet ingestion parser, a rolling XOR cipher,
an anti-tamper latch, and a penalty counter. Whatever mechanism actually disarms
the warhead is buried in the stripped ARM Thumb-2 machine code.
## 4. The Mandate
The Director sets his mug down and looks at the red diode illuminating the bench
in sharp crimson.
**DIRECTOR:** Prove Buddy right, or prove it wrong.
**ANALYST:** I need thirty minutes with GDB and Ghidra.
**DIRECTOR:** You have twenty. Trace the packet handler. Map the state machine.
Do not transmit a single byte over LoRa until you know with 100% certainty what
that firmware will do.
The orders are clear:
1. **Investigate the RF Receiver Logic**: Disassemble the firmware and trace what
happens when data arrives on UART1 (`GP8`/`GP9`).
2. **Recover the Disarm Token**: Reverse the rolling XOR cipher routine in Flash
and decrypt the authentic abort passcode.
3. **Respect the Traps**: Determine what the fuze does after repeated invalid
frames, and what happens if the `GP15` anti-tamper loop is broken.
4. **Neutralize the Munition**: Execute a verified exploit—either by discovering
the valid authorization mechanism, mutating the state machine in SRAM via GDB,
or patching the binary in Flash.
5. **Verify the Hardware**: Transition the live breadboard from the Red LED on
`GP16` to the solid Green LED on `GP17` on physical silicon.
A machine's 0.99 guess does not risk a detonation.
## 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.
Binary file not shown.
File diff suppressed because it is too large. Load diff
Binary file not shown.
File diff suppressed because it is too large. Load diff
Binary file not shown.
-26
View File
@@ -1,26 +0,0 @@
/**
* @file crypto.h
* @brief Lightweight rolling cipher interface for Operation Zero Hour ESA
* @author Kevin Thomas
* @date 2026
*
* MIT License
* Copyright (c) 2026 Kevin Thomas
*/
#ifndef CRYPTO_H
#define CRYPTO_H
#include <stdint.h>
#include <stddef.h>
#define TOKEN_LEN 8
/**
* @brief Decrypt the rolling authorization token into a buffer.
* @param dest Pointer to buffer receiving the decrypted token bytes.
* @return void No return value.
*/
void decrypt_auth_token(uint8_t *dest);
#endif /* CRYPTO_H */
-55
View File
@@ -1,55 +0,0 @@
/**
* @file gpio_ctrl.h
* @brief GPIO hardware control interface for Operation Zero Hour ESA
* @author Kevin Thomas
* @date 2026
*
* MIT License
* Copyright (c) 2026 Kevin Thomas
*/
#ifndef GPIO_CTRL_H
#define GPIO_CTRL_H
#include <stdbool.h>
#define LED_RED_PIN 16
#define LED_GREEN_PIN 17
#define TAMPER_WIRE_PIN 15
/**
* @brief Initialize LED and tamper wire GPIO pins.
* @param void No parameters.
* @return void No return value.
*/
void init_gpio(void);
/**
* @brief Drive the armed indication LEDs (red on, green off).
* @param void No parameters.
* @return void No return value.
*/
void set_led_armed(void);
/**
* @brief Drive the safe indication LEDs (green on, red off).
* @param void No parameters.
* @return void No return value.
*/
void set_led_safe(void);
/**
* @brief Drive the tamper flash indication on the red LED.
* @param state True to turn the red LED on, false to turn it off.
* @return void No return value.
*/
void set_led_tamper_flash(bool state);
/**
* @brief Read the tamper wire loop state.
* @param void No parameters.
* @return true when the tamper wire is intact, false when cut.
*/
bool is_tamper_wire_intact(void);
#endif /* GPIO_CTRL_H */
-35
View File
@@ -1,35 +0,0 @@
/**
* @file safe_arm.h
* @brief Electronic Safe-and-Arm (ESA) controller interface
* @author Kevin Thomas
* @date 2026
*
* MIT License
* Copyright (c) 2026 Kevin Thomas
*/
#ifndef SAFE_ARM_H
#define SAFE_ARM_H
#include <stdbool.h>
#include <stdint.h>
#define STATE_WARHEAD_ARMED 0x01
#define STATE_WARHEAD_SAFE 0x03
#define STATE_TAMPER_TRIP 0xFF
/**
* @brief Initialize the safe-and-arm controller subsystem.
* @param void No parameters.
* @return void No return value.
*/
void init_safe_arm_controller(void);
/**
* @brief Process one iteration of the safe-and-arm event loop.
* @param void No parameters.
* @return void No return value.
*/
void process_safe_arm_tick(void);
#endif /* SAFE_ARM_H */
-105
View File
@@ -1,105 +0,0 @@
#!/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: patch_binary.py
# Desc: Patch script for Operation Zero Hour (0x0014a_cb).
# Created: 2026
"""
Patch script for Operation Zero Hour (0x0014a_cb).
Patches the conditional check in 0x0014a_cb.bin to force the WARHEAD DISARMED
// SAFE state on boot.
"""
import sys
PATCH_OFFSET = 0x688
EXPECTED_OPCODE = b"\x03\x2c"
PATCHED_BYTE = 0x01
def _apply_patch(data: bytearray) -> None:
"""
Verify opcode and apply single-byte conditional branch patch.
Parameters
----------
data : bytearray
Mutable binary image buffer.
Returns
-------
None
"""
if data[PATCH_OFFSET:PATCH_OFFSET + 2] != EXPECTED_OPCODE:
actual = data[PATCH_OFFSET:PATCH_OFFSET + 2].hex()
raise ValueError(f"Expected 03 2c at offset 0x{PATCH_OFFSET:x}, got {actual}")
data[PATCH_OFFSET] = PATCHED_BYTE
def patch_binary(in_bin: str, out_bin: str) -> None:
"""
Read target firmware binary, apply patch, and write modified image.
Parameters
----------
in_bin : str
Path to source binary file.
out_bin : str
Path to destination patched binary file.
Returns
-------
None
"""
with open(in_bin, "rb") as f:
data = bytearray(f.read())
_apply_patch(data)
with open(out_bin, "wb") as f:
f.write(data)
print(f"[+] Successfully patched {in_bin} -> {out_bin} at offset 0x{PATCH_OFFSET:x}")
def main() -> None:
"""
Parse command line arguments and execute binary patching.
Parameters
----------
None
Returns
-------
None
"""
in_file = sys.argv[1] if len(sys.argv) > 1 else "0x0014a_cb.bin"
out_file = sys.argv[2] if len(sys.argv) > 2 else "0x0014a_cb_patched.bin"
patch_binary(in_file, out_file)
if __name__ == "__main__":
main()
-861
View File
@@ -1,861 +0,0 @@
#!/usr/bin/env python3
r"""Convert firmware images between Intel HEX, raw BIN, and Raspberry Pi UF2.
This is the course's single canonical copy of the converter. Every week that
patches a binary refers to the copy at the repository root:
python ../uf2conv.py build/image-h.bin --base 0x10000000 \\
--family 0xe48bff59 --output build/hacked.uf2
It descends from the upstream Raspberry Pi ``uf2conv.py``, with the following
changes made for this course:
* Full type annotations and Google-style docstrings on every public callable.
* ``pathlib`` instead of ``os.path``, and explicit ``except`` clauses.
* Latent crash bugs in the upstream error paths fixed (see
:func:`convert_from_uf2`).
* Module globals replaced with an explicit :class:`ConverterContext`, so the
base address and family ID are threaded through rather than mutated behind
the caller's back.
UF2 format, for reference
-------------------------
A UF2 file is a flat sequence of 512-byte blocks. Each block is::
offset size field
0x00 4 magicStart0 = 0x0A324655 ("UF2\\n")
0x04 4 magicStart1 = 0x9E5D5157 (arbitrary constant)
0x08 4 flags
0x0C 4 targetAddr
0x10 4 payloadSize (max 476)
0x14 4 blockNo
0x18 4 numBlocks
0x1C 4 familyID
0x20 476 data
0x1FC 4 magicEnd = 0x0AB16F30
Flags that matter here:
* ``0x00000001`` -- "do not flash"; this block is a no-flash marker.
* ``0x00002000`` -- "family ID present"; ``familyID`` is meaningful.
For the RP2350 the family ID is ``0xe48bff59``, and it is what tells the
boot ROM that this UF2 targets RP2350 hardware rather than, say, an RP2040 or
a Pico W.
See Also:
--------
* ``uf2families.json`` -- the family-name to family-ID table, loaded from the
directory containing this file.
* ``flash.sh`` / ``flash.ps1`` -- program a raw ``.bin`` over SWD instead.
"""
from __future__ import annotations
import argparse
import json
import os
import re
import struct
import subprocess
import sys
from dataclasses import dataclass
from pathlib import Path
from time import sleep
# ---------------------------------------------------------------------------
# UF2 format constants
# ---------------------------------------------------------------------------
#: First magic word, ``"UF2\n"`` read as a little-endian uint32.
UF2_MAGIC_START0 = 0x0A324655
#: Second magic word. The value is arbitrary but must be constant.
UF2_MAGIC_START1 = 0x9E5D5157
#: Trailing magic word closing every 512-byte block.
UF2_MAGIC_END = 0x0AB16F30
#: Size of one UF2 block in bytes.
UF2_BLOCK_SIZE = 512
#: Offset of the 32-byte block header within a block.
UF2_HEADER_SIZE = 32
#: Largest payload a single block may carry (476 = 512 - 32 header - 4 magic).
UF2_MAX_PAYLOAD = UF2_BLOCK_SIZE - UF2_HEADER_SIZE - 4
#: Flag bit marking a block the bootloader must NOT program.
UF2_FLAG_NOFLASH = 0x00000001
#: Flag bit announcing that ``familyID`` carries a meaningful value.
UF2_FLAG_FAMILY_ID = 0x00002000
#: Name of the marker file the boot ROM exposes on its flash-drive volume.
INFO_FILE = "INFO_UF2.TXT"
#: Struct format for the 32-byte UF2 block header.
_HEADER_STRUCT = struct.Struct("<IIIIIIII")
#: The default family ID assumed when the caller does not pass ``--family``.
DEFAULT_FAMILY_ID = 0x0
#: The default base address assumed when the caller does not pass ``--base``.
#: Matches the upstream default; RP2350 firmware always overrides this.
DEFAULT_BASE_ADDRESS = 0x2000
class Uf2Error(Exception):
"""Raised when an input file is malformed or internally inconsistent."""
@dataclass
class ConverterContext:
"""Mutable conversion state shared across a single CLI invocation.
Attributes:
appstartaddr: Base address that payload offsets are relative to. Set
from ``--base``; ``0`` means "derive it from the input".
familyid: Family ID written into every UF2 block. ``0`` means "no
family flag", which is the correct behaviour for a plain BIN->UF2
conversion of RP2040-era images, and the wrong behaviour for
RP2350, which is why the course always passes ``--family``.
"""
appstartaddr: int | None = DEFAULT_BASE_ADDRESS
familyid: int = DEFAULT_FAMILY_ID
# ---------------------------------------------------------------------------
# Format detection
# ---------------------------------------------------------------------------
def is_uf2(buf: bytes) -> bool:
"""Report whether a buffer begins with the UF2 start magic.
Args:
buf: Raw file contents.
Returns:
``True`` if the first two uint32 words are the UF2 start magic.
"""
if len(buf) < 8:
return False
word0, word1 = _HEADER_STRUCT.unpack_from(buf, 0)[:2]
return word0 == UF2_MAGIC_START0 and word1 == UF2_MAGIC_START1
def is_hex(buf: bytes) -> bool:
"""Report whether a buffer looks like an Intel HEX file.
A file qualifies when it decodes as UTF-8, begins with a record marker
``:``, and contains only characters legal in Intel HEX records.
Args:
buf: Raw file contents.
Returns:
``True`` if the buffer should be treated as Intel HEX.
"""
try:
text = buf[:30].decode("utf-8")
except UnicodeDecodeError:
return False
if not text.startswith(":"):
return False
return re.match(rb"^[:0-9a-fA-F\r\n]+$", buf) is not None
# ---------------------------------------------------------------------------
# UF2 -> BIN
# ---------------------------------------------------------------------------
def convert_from_uf2(buf: bytes, ctx: ConverterContext) -> bytes:
"""Decode a UF2 file back into a flat binary image.
Blocks are visited in file order. Gaps between consecutive block addresses
are filled with zero words, and any trailing gap is dropped so that the
result ends at the last byte actually programmed.
A UF2 file may legitimately interleave blocks from several families (this
is how a multicore image ships both cores in one download). When more than
one family is present and the caller did not constrain the family with
``--family``, the conversion is ambiguous, so the payload is emptied and
``appstartaddr`` reset to ``0``.
Args:
buf: Complete UF2 file contents.
ctx: Conversion state; updated in place with the discovered base
address.
Returns:
The decoded payload.
Raises:
Uf2Error: If a block declares an oversized payload, the blocks are out
of order, or the implied padding exceeds 10 MiB.
Note:
The upstream Raspberry Pi version of this function crashes on its own
error paths: it builds the message with ``"..." + ptr`` where ``ptr``
is an ``int``, which raises :class:`TypeError` and masks the real
problem. The messages here use f-strings so the diagnostic survives.
"""
numblocks = len(buf) // UF2_BLOCK_SIZE
curraddr: int | None = None
currfamilyid: int | None = None
families_found: dict[int, int] = {}
prev_flag: int | None = None
all_flags_same = True
outp: list[bytes] = []
for blockno in range(numblocks):
ptr = blockno * UF2_BLOCK_SIZE
block = buf[ptr : ptr + UF2_BLOCK_SIZE]
(
magic0,
magic1,
flags,
target_addr,
datalen,
_blockno,
_numblocks,
block_family,
) = _HEADER_STRUCT.unpack_from(block, 0)
if magic0 != UF2_MAGIC_START0 or magic1 != UF2_MAGIC_START1:
print(f"Skipping block at {ptr:#x}; bad magic")
continue
if flags & UF2_FLAG_NOFLASH:
continue
if datalen > UF2_MAX_PAYLOAD:
msg = f"Invalid UF2 data size at {ptr:#x}: {datalen}"
raise Uf2Error(msg)
if flags & UF2_FLAG_FAMILY_ID and currfamilyid is None:
currfamilyid = block_family
# A new contiguous run starts when the address jumps or the family
# changes. appstartaddr tracks the start of the current run, which is
# what makes the reported start address the image's true base rather
# than the address of its last block.
if curraddr is None or (
flags & UF2_FLAG_FAMILY_ID and block_family != currfamilyid
):
currfamilyid = block_family
curraddr = target_addr
if ctx.familyid == DEFAULT_FAMILY_ID or ctx.familyid == block_family:
ctx.appstartaddr = target_addr
padding = target_addr - curraddr
if padding < 0:
msg = f"Block out of order at {ptr:#x}: {target_addr:#x} < {curraddr:#x}"
raise Uf2Error(msg)
if padding > 10 * 1024 * 1024:
msg = f"More than 10M of padding needed at {ptr:#x}"
raise Uf2Error(msg)
if padding % 4 != 0:
msg = f"Non-word padding size at {ptr:#x}: {padding}"
raise Uf2Error(msg)
while padding > 0:
padding -= 4
outp.append(b"\x00\x00\x00\x00")
if ctx.familyid == DEFAULT_FAMILY_ID or (
flags & UF2_FLAG_FAMILY_ID and ctx.familyid == block_family
):
outp.append(
block[UF2_HEADER_SIZE : UF2_HEADER_SIZE + datalen],
)
curraddr = target_addr + datalen
if flags & UF2_FLAG_FAMILY_ID:
existing = families_found.get(block_family)
if existing is None or existing > target_addr:
families_found[block_family] = target_addr
if prev_flag is None:
prev_flag = flags
elif prev_flag != flags:
all_flags_same = False
if blockno == numblocks - 1:
_print_uf2_header_info(families_found, all_flags_same, flags)
if len(families_found) > 1 and ctx.familyid == DEFAULT_FAMILY_ID:
outp = []
ctx.appstartaddr = 0x0
return b"".join(outp)
def _print_uf2_header_info(
families_found: dict[int, int],
all_flags_same: bool,
flags: int,
) -> None:
"""Print the family summary block that follows a UF2->BIN conversion.
Args:
families_found: Mapping of family ID to the lowest target address seen
for that family.
all_flags_same: Whether every block carried an identical flag word.
flags: The flag word from the final block, used for display.
"""
print("--- UF2 File Header Info ---")
families = load_families()
name_by_id = {value: key for key, value in families.items()}
for family_hex, address in families_found.items():
short_name = name_by_id.get(family_hex, "")
print(f"Family ID is {short_name}, hex value is {family_hex:#010x}")
print(f"Target Address is {address:#010x}")
if all_flags_same:
print(f"All block flag values consistent, {flags:#06x}")
else:
print("Flags were not all the same")
print("----------------------------")
# ---------------------------------------------------------------------------
# BIN -> UF2
# ---------------------------------------------------------------------------
def convert_to_uf2(file_content: bytes, ctx: ConverterContext) -> bytes:
"""Wrap a raw binary in UF2 blocks.
The binary is split into 256-byte payloads; the final block is zero-padded.
Each block's ``targetAddr`` is the byte offset within the image plus
:attr:`ConverterContext.appstartaddr`, so for RP2350 firmware built at
``0x10000000`` the caller passes ``--base 0x10000000``.
Args:
file_content: The raw image.
ctx: Conversion state supplying the base address and family ID.
Returns:
The UF2 file contents, always a whole number of 512-byte blocks.
"""
datapadding = bytes(UF2_BLOCK_SIZE - 256 - UF2_HEADER_SIZE - 4)
numblocks = (len(file_content) + 255) // 256
flags = UF2_FLAG_FAMILY_ID if ctx.familyid else 0x0
base = ctx.appstartaddr or 0
blocks: list[bytes] = []
for blockno in range(numblocks):
ptr = 256 * blockno
chunk = file_content[ptr : ptr + 256]
header = _HEADER_STRUCT.pack(
UF2_MAGIC_START0,
UF2_MAGIC_START1,
flags,
ptr + base,
256,
blockno,
numblocks,
ctx.familyid,
)
payload = chunk + bytes(256 - len(chunk))
block = header + payload + datapadding + struct.pack("<I", UF2_MAGIC_END)
blocks.append(block)
return b"".join(blocks)
def convert_to_carray(file_content: bytes) -> bytes:
"""Render a binary as a C array initialiser for embedding in firmware.
Args:
file_content: The raw image.
Returns:
UTF-8 bytes of a C translation unit fragment.
"""
parts = [
f"const unsigned long bindata_len = {len(file_content)};\n",
"const unsigned char bindata[] __attribute__((aligned(16))) = {",
]
for index, value in enumerate(file_content):
if index % 16 == 0:
parts.append("\n")
parts.append(f"{value:#04x}, ")
parts.append("\n};\n")
return "".join(parts).encode()
# ---------------------------------------------------------------------------
# Intel HEX -> UF2
# ---------------------------------------------------------------------------
@dataclass
class Block:
"""A single 256-byte-aligned region of an image under construction.
Attributes:
addr: Base address of the block, always 256-byte aligned.
data: The block's bytes, initialised to ``default_data``.
"""
addr: int
data: bytearray
def __init__(self, addr: int, default_data: int = 0xFF) -> None:
"""Initialise an erased 256-byte block.
Args:
addr: 256-byte-aligned base address of the block.
default_data: Fill byte; ``0xFF`` matches erased flash.
"""
self.addr = addr
self.data = bytearray([default_data] * 256)
def encode(self, blockno: int, numblocks: int, ctx: ConverterContext) -> bytes:
"""Serialise this block into a 512-byte UF2 block.
Args:
blockno: Index of this block within the file.
numblocks: Total number of blocks in the file.
ctx: Conversion state supplying the family ID.
Returns:
Exactly 512 bytes.
"""
flags = UF2_FLAG_FAMILY_ID if ctx.familyid else 0x0
out = _HEADER_STRUCT.pack(
UF2_MAGIC_START0,
UF2_MAGIC_START1,
flags,
self.addr,
256,
blockno,
numblocks,
ctx.familyid,
)
out += self.data[0:256]
out += bytes(UF2_BLOCK_SIZE - 4 - len(out))
out += struct.pack("<I", UF2_MAGIC_END)
return out
def convert_from_hex_to_uf2(text: str, ctx: ConverterContext) -> bytes:
"""Assemble Intel HEX records into a UF2 file.
Only record types 0x00 (data), 0x01 (EOF), 0x02 (extended segment address)
and 0x04 (extended linear address) are handled; any other type is ignored,
matching the upstream behaviour.
Args:
text: Decoded Intel HEX file contents.
ctx: Conversion state; ``appstartaddr`` is set to the first data
address seen.
Returns:
The UF2 file contents.
"""
ctx.appstartaddr = None
upper = 0
currblock: Block | None = None
blocks: list[Block] = []
for line in text.split("\n"):
if not line.startswith(":"):
continue
record = [int(line[i : i + 2], 16) for i in range(1, len(line) - 1, 2)]
rec_type = record[3]
if rec_type == 4:
upper = ((record[4] << 8) | record[5]) << 16
elif rec_type == 2:
upper = ((record[4] << 8) | record[5]) << 4
elif rec_type == 1:
break
elif rec_type == 0:
addr = upper + ((record[1] << 8) | record[2])
if ctx.appstartaddr is None:
ctx.appstartaddr = addr
index = 4
while index < len(record) - 1:
if currblock is None or currblock.addr & ~0xFF != addr & ~0xFF:
currblock = Block(addr & ~0xFF)
blocks.append(currblock)
currblock.data[addr & 0xFF] = record[index]
addr += 1
index += 1
numblocks = len(blocks)
return b"".join(blocks[i].encode(i, numblocks, ctx) for i in range(numblocks))
# ---------------------------------------------------------------------------
# Drive discovery
# ---------------------------------------------------------------------------
def _iter_candidate_dirs() -> list[Path]:
"""Return the directories that may contain mounted removable volumes."""
if sys.platform == "win32":
return []
searchpaths = [Path("/mnt"), Path("/media")]
if sys.platform == "darwin":
searchpaths = [Path("/Volumes")]
elif sys.platform == "linux":
user = environ_user()
if user:
searchpaths += [Path("/media") / user, Path("/run/media") / user]
sudo_user = os.environ.get("SUDO_USER")
if sudo_user:
searchpaths += [
Path("/media") / sudo_user,
Path("/run/media") / sudo_user,
]
return searchpaths
def environ_user() -> str | None:
"""Return the current user name from the environment.
Tried under several variable names so the search works on macOS, Linux and
Windows shells alike.
Returns:
The user name, or ``None`` if it cannot be determined.
"""
for var in ("USER", "USERNAME", "LOGNAME"):
value = os.environ.get(var)
if value:
return value
return None
def get_drives() -> list[Path]:
"""Find mounted Pico boot drives.
A drive qualifies when it contains the ``INFO_UF2.TXT`` marker the boot ROM
writes when it presents the flash as a USB mass-storage device.
Returns:
Every qualifying mount point.
"""
drives: list[Path] = []
if sys.platform == "win32":
command = (
"(Get-Volume | Where-Object { $_.FileSystemLabel -match 'RP2350|RPI-RP2' })"
".DriveLetter"
)
try:
raw = subprocess.check_output(
["powershell", "-Command", command],
text=True,
)
except (subprocess.CalledProcessError, FileNotFoundError, OSError):
return []
for letter in raw.split():
if len(letter) == 1 and letter.isalpha():
drives.append(Path(f"{letter.upper()}:\\"))
return drives
for rootpath in _iter_candidate_dirs():
if not rootpath.is_dir():
continue
try:
entries = list(rootpath.iterdir())
except OSError:
continue
for entry in entries:
if not entry.is_dir():
continue
if (entry / INFO_FILE).is_file():
drives.append(entry)
return drives
def board_id(path: Path) -> str:
"""Read the ``Board-ID`` field from a boot drive's info file.
Args:
path: Mount point of the boot drive.
Returns:
The board ID string, for example ``RP2350``.
Raises:
Uf2Error: If the info file is missing or has no ``Board-ID`` field.
"""
info = path / INFO_FILE
if not info.is_file():
msg = f"Not a Pico boot drive: {path} has no {INFO_FILE}"
raise Uf2Error(msg)
match = re.search(r"Board-ID: ([^\r\n]*)", info.read_text())
if match is None:
msg = f"No Board-ID field in {info}"
raise Uf2Error(msg)
return match.group(1)
def list_drives() -> None:
"""Print every connected Pico boot drive and its board ID."""
for drive in get_drives():
try:
print(drive, board_id(drive))
except Uf2Error as exc:
print(f"{drive} <unreadable: {exc}>")
def write_file(name: Path | str, buf: bytes) -> None:
"""Write a buffer to disk and report the size written.
Args:
name: Destination path.
buf: Bytes to write.
"""
path = Path(name)
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(buf)
print(f"Wrote {len(buf)} bytes to {path}")
def load_families() -> dict[str, int]:
"""Load the family-name to family-ID table.
``uf2families.json`` is read from the directory containing this script, so
the converter works regardless of the caller's working directory.
Returns:
Mapping of upper-case short name to family ID.
Raises:
Uf2Error: If the JSON file is missing or malformed.
"""
pathname = Path(__file__).resolve().parent / "uf2families.json"
try:
raw_families = json.loads(pathname.read_text())
except FileNotFoundError as exc:
msg = f"Required family table not found: {pathname}"
raise Uf2Error(msg) from exc
except json.JSONDecodeError as exc:
msg = f"Malformed family table {pathname}: {exc}"
raise Uf2Error(msg) from exc
return {fam["short_name"]: int(fam["id"], 0) for fam in raw_families}
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
def _build_parser() -> argparse.ArgumentParser:
"""Construct the command-line argument parser.
Returns:
A configured :class:`argparse.ArgumentParser`.
"""
parser = argparse.ArgumentParser(
prog="uf2conv.py",
description="Convert firmware between Intel HEX, raw BIN and UF2.",
epilog=(
"Course example (RP2350 raw image built for XIP flash):\n"
" python ../uf2conv.py build/image-h.bin "
"--base 0x10000000 \\\n"
" --family 0xe48bff59 --output build/hacked.uf2\n"
),
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument(
"input",
metavar="INPUT",
nargs="?",
help="input file (HEX, BIN or UF2); omit when using --list",
)
parser.add_argument(
"-b",
"--base",
default=hex(DEFAULT_BASE_ADDRESS),
help=(
"base address of the application for BIN input "
f"(default: {hex(DEFAULT_BASE_ADDRESS)}; use 0x10000000 for RP2350 XIP)"
),
)
parser.add_argument(
"-f",
"--family",
default=hex(DEFAULT_FAMILY_ID),
help=(
"family ID as a number or a name from uf2families.json "
f"(default: {hex(DEFAULT_FAMILY_ID)}; RP2350 is 0xe48bff59)"
),
)
parser.add_argument(
"-o",
"--output",
metavar="FILE",
help='write output to FILE; defaults to "flash.uf2" or "flash.bin"',
)
parser.add_argument(
"-d",
"--device",
dest="device_path",
help="select a specific device path to flash (reserved; unused)",
)
parser.add_argument(
"-l",
"--list",
action="store_true",
help="list connected Pico boot drives and exit",
)
parser.add_argument(
"-c",
"--convert",
action="store_true",
help="convert only; do not deploy to a mounted boot drive",
)
parser.add_argument(
"-D",
"--deploy",
action="store_true",
help="deploy the input file unchanged; do not convert",
)
parser.add_argument(
"-w",
"--wait",
action="store_true",
help="poll for a boot drive to appear instead of failing immediately",
)
parser.add_argument(
"-C",
"--carray",
action="store_true",
help="emit a C array initialiser instead of a UF2 file",
)
parser.add_argument(
"-i",
"--info",
action="store_true",
help="print UF2 header information and exit without converting",
)
return parser
def _resolve_family(spec: str) -> int:
"""Resolve a ``--family`` value to a numeric family ID.
Args:
spec: Either a family short name (case-insensitive) or an integer
literal such as ``0xe48bff59``.
Returns:
The numeric family ID.
Raises:
Uf2Error: If the value is neither a known name nor a valid integer.
"""
families = load_families()
if spec.upper() in families:
return families[spec.upper()]
try:
return int(spec, 0)
except ValueError as exc:
msg = "Family ID needs to be a number or one of: " + ", ".join(families)
raise Uf2Error(msg) from exc
def _wait_for_drive() -> Path | None:
"""Poll for a boot drive to be mounted.
Returns:
The first drive found, or ``None`` if the wait was interrupted.
"""
print("Waiting for drive to deploy...")
while True:
drives = get_drives()
if drives:
return drives[0]
sleep(0.1)
def main(argv: list[str] | None = None) -> int:
"""Run the command-line converter.
Args:
argv: Argument list, defaulting to :data:`sys.argv` ``[1:]``.
Returns:
``0`` on success, ``1`` on a user or input error.
"""
parser = _build_parser()
args = parser.parse_args(argv)
ctx = ConverterContext()
try:
ctx.familyid = _resolve_family(args.family)
ctx.appstartaddr = int(args.base, 0)
except (Uf2Error, ValueError) as exc:
print(exc, file=sys.stderr)
return 1
if args.list:
list_drives()
return 0
if not args.input:
print("Need an input file (or use --list)", file=sys.stderr)
return 1
source = Path(args.input)
if not source.is_file():
print(f"error: file not found: {source}", file=sys.stderr)
return 1
inpbuf = source.read_bytes()
from_uf2 = is_uf2(inpbuf)
ext = "uf2"
try:
if args.deploy:
outbuf = inpbuf
elif from_uf2 and not args.info:
outbuf = convert_from_uf2(inpbuf, ctx)
ext = "bin"
elif from_uf2 and args.info:
outbuf = b""
convert_from_uf2(inpbuf, ctx)
elif is_hex(inpbuf):
outbuf = convert_from_hex_to_uf2(inpbuf.decode("utf-8"), ctx)
elif args.carray:
outbuf = convert_to_carray(inpbuf)
ext = "h"
else:
outbuf = convert_to_uf2(inpbuf, ctx)
except Uf2Error as exc:
print(f"error: {exc}", file=sys.stderr)
return 1
if not args.deploy and not args.info:
print(
f"Converted to {ext}, output size: {len(outbuf)}, "
f"start address: {ctx.appstartaddr or 0:#x}",
)
if (args.convert or ext != "uf2") and args.output is None:
args.output = f"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 not drives:
if args.wait:
found = _wait_for_drive()
drives = [found] if found else []
elif not args.output:
print("error: no drive to deploy", file=sys.stderr)
return 1
for drive in drives:
print(f"Flashing {drive} ({board_id(drive)})")
write_file(drive / "NEW.UF2", outbuf)
return 0
if __name__ == "__main__":
sys.exit(main())
-27
View File
@@ -1,27 +0,0 @@
/**
* @file crypto.c
* @brief Lightweight rolling cipher implementation for Operation Zero Hour ESA
* @author Kevin Thomas
* @date 2026
*
* MIT License
* Copyright (c) 2026 Kevin Thomas
*/
#include "crypto.h"
static const uint8_t CIPHERTEXT[TOKEN_LEN] = {
0x07, 0x03, 0x03, 0x12, 0x07, 0x7F, 0x70, 0x7A
};
static const uint8_t CIPHER_KEY[4] = {
0x54, 0x41, 0x43, 0x54
};
void decrypt_auth_token(uint8_t *dest) {
volatile const uint8_t *src = CIPHERTEXT;
volatile const uint8_t *key = CIPHER_KEY;
for (size_t i = 0; i < TOKEN_LEN; i++) {
dest[i] = src[i] ^ (key[i % 4] + (uint8_t)i);
}
}
-60
View File
@@ -1,60 +0,0 @@
/**
* @file gpio_ctrl.c
* @brief GPIO hardware control implementation for Operation Zero Hour ESA
* @author Kevin Thomas
* @date 2026
*
* MIT License
* Copyright (c) 2026 Kevin Thomas
*/
#include "gpio_ctrl.h"
#include "pico/stdlib.h"
/**
* @brief Initialize the red and green LED pins as outputs.
* @param void No parameters.
* @return void No return value.
*/
static void init_led_pins(void) {
gpio_init(LED_RED_PIN);
gpio_set_dir(LED_RED_PIN, GPIO_OUT);
gpio_init(LED_GREEN_PIN);
gpio_set_dir(LED_GREEN_PIN, GPIO_OUT);
}
/**
* @brief Initialize the tamper wire pin as a pulled-down input.
* @param void No parameters.
* @return void No return value.
*/
static void init_tamper_pin(void) {
gpio_init(TAMPER_WIRE_PIN);
gpio_set_dir(TAMPER_WIRE_PIN, GPIO_IN);
gpio_pull_down(TAMPER_WIRE_PIN);
}
void init_gpio(void) {
init_led_pins();
init_tamper_pin();
set_led_armed();
}
void set_led_armed(void) {
gpio_put(LED_RED_PIN, 1);
gpio_put(LED_GREEN_PIN, 0);
}
void set_led_safe(void) {
gpio_put(LED_RED_PIN, 0);
gpio_put(LED_GREEN_PIN, 1);
}
void set_led_tamper_flash(bool state) {
gpio_put(LED_RED_PIN, state ? 1 : 0);
gpio_put(LED_GREEN_PIN, 0);
}
bool is_tamper_wire_intact(void) {
return gpio_get(TAMPER_WIRE_PIN);
}
-151
View File
@@ -1,151 +0,0 @@
// 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>
/**
* @brief Discard all pending bytes from the LoRa UART receive FIFO.
* @param void No parameters.
* @return void No return value.
*/
static void drain_lora_rx(void) {
while (uart_is_readable(LORA_UART)) {
(void)uart_getc(LORA_UART);
}
}
/**
* @brief Send an AT command to the radio and drain its response.
* @param cmd Null-terminated AT command string to transmit.
* @return void No return value.
*/
static void send_at_cmd(const char *cmd) {
uart_write_blocking(LORA_UART, (const uint8_t *)cmd, strlen(cmd));
sleep_ms(200);
drain_lora_rx();
}
/**
* @brief Program the RYLR998 RF parameters via the AT command set.
* @param void No parameters.
* @return void No return value.
*/
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();
}
void lora_send(const char *msg) {
char cmd[128];
int len = (int)strlen(msg);
snprintf(cmd, sizeof(cmd), "AT+SEND=0,%d,%s\r\n", len, msg);
uart_write_blocking(LORA_UART, (const uint8_t *)cmd, strlen(cmd));
}
/**
* @brief Extract the payload field from a +RCV receive line.
* @param line Mutable receive line buffer to parse.
* @param out Buffer receiving the extracted payload string.
* @param max_len Capacity of out in bytes.
* @return true when the payload was extracted, false otherwise.
*/
static bool extract_rcv_data(char *line, char *out, size_t max_len) {
char *c1 = strchr(line, ',');
if (!c1) return false;
char *c2 = strchr(c1 + 1, ',');
if (!c2) return false;
char *c3 = strchr(c2 + 1, ',');
if (c3) *c3 = '\0';
snprintf(out, max_len, "%s", c2 + 1);
return true;
}
/**
* @brief Parse one complete UART line and capture payload data.
* @param line Mutable line buffer to parse.
* @param out Buffer receiving the extracted payload string.
* @param max_len Capacity of out in bytes.
* @return true when the line carried payload data, false otherwise.
*/
static bool parse_line(char *line, char *out, size_t max_len) {
if (strstr(line, "+RCV="))
return extract_rcv_data(line, out, max_len);
if (strncmp(line, "+", 1) != 0 && strncmp(line, "OK", 2) != 0) {
snprintf(out, max_len, "%s", line);
return true;
}
return false;
}
/**
* @brief Feed one received byte into the line assembly state machine.
* @param c Received byte to process.
* @param buf Line assembly buffer.
* @param idx Pointer to the current write index into buf.
* @param out Buffer receiving the extracted payload string.
* @param max_len Capacity of out in bytes.
* @return true when a complete payload was produced, false otherwise.
*/
static bool process_rx_byte(char c, char *buf, size_t *idx, char *out, size_t max_len) {
if (c == '\r' || c == '\n') {
buf[*idx] = '\0';
bool ok = (*idx > 0) && parse_line(buf, out, max_len);
*idx = 0;
return ok;
}
if (*idx < 127) buf[(*idx)++] = c;
return false;
}
bool lora_poll_packet(char *payload_out, size_t max_len) {
static char buf[128];
static size_t idx = 0;
while (uart_is_readable(LORA_UART)) {
char c = (char)uart_getc(LORA_UART);
if (process_rx_byte(c, buf, &idx, payload_out, max_len))
return true;
}
return false;
}
-42
View File
@@ -1,42 +0,0 @@
/**
* @file main.c
* @brief Operation Zero Hour - Electronic Safe-and-Arm (ESA) Firmware
* @author Kevin Thomas
* @date 2026
*
* MIT License
* Copyright (c) 2026 Kevin Thomas
*/
#include "pico/stdlib.h"
#include "gpio_ctrl.h"
#include "safe_arm.h"
/**
* @brief Initialize stdio, GPIO, and the safe-and-arm controller.
* @param void No parameters.
* @return void No return value.
*/
static void init_system(void) {
stdio_init_all();
init_gpio();
init_safe_arm_controller();
}
/**
* @brief Run the main event loop, servicing one tick per iteration.
* @param void No parameters.
* @return void No return value.
*/
static void run_event_loop(void) {
while (true) {
process_safe_arm_tick();
sleep_ms(100);
}
}
int main(void) {
init_system();
run_event_loop();
return 0;
}
-133
View File
@@ -1,133 +0,0 @@
/**
* @file safe_arm.c
* @brief Electronic Safe-and-Arm (ESA) controller implementation
* @author Kevin Thomas
* @date 2026
*
* MIT License
* Copyright (c) 2026 Kevin Thomas
*/
#include "safe_arm.h"
#include "crypto.h"
#include "gpio_ctrl.h"
#include "lora.h"
#include <stdio.h>
#include <string.h>
static uint8_t s_disarm_token[TOKEN_LEN + 1];
/**
* @brief Print the armed status banner to the console.
* @param void No parameters.
* @return void No return value.
*/
static void print_banner_armed(void) {
printf("\r\n+-----------------------------------------------------------------+\r\n");
printf("| OPERATION ZERO HOUR // ESA CONTROLLER |\r\n");
printf("| STATUS: WARHEAD ARMED [GP16 RED: ON] LORA RECEIVER: LIVE |\r\n");
printf("+-----------------------------------------------------------------+\r\n");
}
/**
* @brief Print the disarmed status banner with the recovered token.
* @param token Null-terminated recovered disarm token string.
* @return void No return value.
*/
static void print_banner_safe(const uint8_t *token) {
printf("\r\n+-----------------------------------------------------------------+\r\n");
printf("| WARHEAD DISARMED // SAFE [GP17 GREEN: ON] |\r\n");
printf("| RECOVERED TOKEN: %-8.8s // FUZE NEUTRALIZED |\r\n", token);
printf("+-----------------------------------------------------------------+\r\n");
}
/**
* @brief Print the tamper detonation trip banner to the console.
* @param void No parameters.
* @return void No return value.
*/
static void print_banner_trip(void) {
printf("\r\n+-----------------------------------------------------------------+\r\n");
printf("| TAMPER DETONATION TRIP [GP16 RED: FLASH] |\r\n");
printf("| STATUS: COMMAND DETONATION ACTIVATED // EXPLOSIVE TRAIN ENGAGED |\r\n");
printf("+-----------------------------------------------------------------+\r\n");
}
/**
* @brief Evaluate a received command against the disarm token.
* @param cmd Null-terminated received command string.
* @param state Pointer to the current warhead state byte.
* @param faults Pointer to the tamper fault counter.
* @return void No return value.
*/
static void eval_command(const char *cmd, uint8_t *state, uint8_t *faults) {
if (strncmp(cmd, (const char *)s_disarm_token, TOKEN_LEN) == 0) {
*state = STATE_WARHEAD_SAFE;
lora_send("WARHEAD DISARMED // SAFE");
print_banner_safe(s_disarm_token);
} else {
(*faults)++;
}
}
/**
* @brief Trip the warhead when fault limits or tamper wire demand it.
* @param faults Current tamper fault count.
* @param state Pointer to the current warhead state byte.
* @return void No return value.
*/
static void check_tamper(uint8_t faults, uint8_t *state) {
if (*state == STATE_WARHEAD_ARMED && (faults >= 3 || !is_tamper_wire_intact())) {
*state = STATE_TAMPER_TRIP;
print_banner_trip();
}
}
/**
* @brief Dispatch a received packet through command and tamper logic.
* @param has_pkt True when a packet was received this tick.
* @param pkt Null-terminated received packet payload string.
* @param state Pointer to the current warhead state byte.
* @param faults Pointer to the tamper fault counter.
* @return void No return value.
*/
static void handle_packet(bool has_pkt, const char *pkt, uint8_t *state, uint8_t *faults) {
if (has_pkt && *state != STATE_WARHEAD_SAFE) {
eval_command(pkt, state, faults);
check_tamper(*faults, state);
}
}
/**
* @brief Drive LED feedback for the current warhead state.
* @param state Current warhead state byte.
* @param tick_idx Monotonic loop tick index for flash timing.
* @return void No return value.
*/
static void update_feedback(uint8_t state, uint32_t tick_idx) {
if (state == STATE_WARHEAD_SAFE) {
set_led_safe();
} else if (state == STATE_TAMPER_TRIP) {
set_led_tamper_flash((tick_idx % 2) == 0);
} else {
set_led_armed();
}
}
void process_safe_arm_tick(void) {
static uint8_t warhead_state = STATE_WARHEAD_ARMED;
static uint8_t tamper_faults = 0;
static uint32_t loop_ticks = 0;
char rx_pkt[64] = {0};
bool has_pkt = lora_poll_packet(rx_pkt, sizeof(rx_pkt));
handle_packet(has_pkt, rx_pkt, &warhead_state, &tamper_faults);
check_tamper(tamper_faults, &warhead_state);
update_feedback(warhead_state, loop_ticks++);
}
void init_safe_arm_controller(void) {
decrypt_auth_token(s_disarm_token);
s_disarm_token[TOKEN_LEN] = '\0';
init_lora();
print_banner_armed();
}