Initial commit

This commit is contained in:
Kevin Thomas committed 2026-10-07 07:22:43 -04:00
commit ea7fa10403
499 files changed
+172655

No files matched your search

+4
View File
@@ -0,0 +1,4 @@
build/
.DS_Store
*.swp
*~
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+112
View File
@@ -0,0 +1,112 @@
# Operation Dark Vector: Classified Intelligence Briefing
```
+-----------------------------------------------------------------+
| TOP SECRET // NOFORN |
+-----------------------------------------------------------------+
| |
|OPERATION DARK VECTOR |
| |
|CLASSIFIED BRIEFING: LIVE CTF 0x01 |
| |
|NATIONAL SECURITY AGENCY / GMU RHET |
+-----------------------------------------------------------------+
```
## 1. The Answer
INT. NSA EXPLOITATION CELL. 05:03.
The analysts have been awake for nineteen hours. The Director walks in with a cup
of coffee and does not sit down.
**DIRECTOR:** Where is it going?
**ANALYST:** Buddy says it knows.
**DIRECTOR:** Buddy.
**ANALYST:** The model. The one we built for exactly this. Give it the image, ask
it the question.
**DIRECTOR:** And?
**ANALYST:** Nine seconds.
She turns the screen around. This is what came back.
```
+-----------------------------------------------------------------+
| BUDDY // CLASSIFIED EXPLOITATION ASSIST // CONF: 0.97 |
+-----------------------------------------------------------------+
| DESTINATION : +38.840280 -77.428890 |
| CONFIDENCE : 0.97 |
| PATCH : verified-by-construction. Ready to flash. |
+-----------------------------------------------------------------+
```
**DIRECTOR:** That is the whole answer.
**ANALYST:** That is the whole answer.
**DIRECTOR:** Is it that simple?
**ANALYST:** It is never that simple.
**DIRECTOR:** Then why does it look that simple?
**ANALYST:** Because Buddy is very good at making things look simple.
## 2. What Buddy Is
Buddy is not a person. It is a model. It has read more assembly than every
engineer who has ever lived, and it never sleeps and never doubts. You ask it a
question, and it answers in the exact shape of an answer, with a confidence that
is hard to argue with.
That confidence is the problem. It is not the same thing as being right.
Buddy has never seen this drone. It has never stood at this bench or watched this
board power on. It has a picture of the firmware, and it has made a very good
guess. A very good guess is still a guess.
## 3. The Operation
At 04:17 the woods outside Centreville, Virginia were black and the machine above
them was silent. It was a one-way drone, built by a state program we will not
name here, meant to fly to a target, do its work, and never come home. No radio,
no hand on the stick. A breadboard-ugly brain counting down a heading it was born
with.
The wind won. The battery died. It came down through the branches and stayed
there, about a mile from where it started.
Then it started talking. A recovery beacon on 915 MHz, repeating a coordinate
into the night. NSA sensors heard it, and a recovery team took the airframe
intact. The flight computer was a bare-metal RP2350 that was never supposed to be
opened, and the one thing it was still willing to say out loud.
CyberCom imaged the flash and did what everyone does now. They handed it to the
machine.
## 4. The Mandate
The Director does not trust the answer. Not because Buddy is bad, but because the
answer is too clean. Nine seconds for a question that should have taken a night.
So the order is simple, and it is the only order that matters.
Prove it. Prove Buddy correct, or prove it wrong with evidence that no one can
argue with. A machine's verdict does not move up the chain without a human
signature.
## 5. The Challenge
You have the image. You have the board. You have the tools and the time Buddy
did not need.
Buddy has given you its answer, and it is confident.
Is it that simple?
Go find out.
Binary file not shown.
+108
View File
@@ -0,0 +1,108 @@
# MIT License
#
# Copyright (c) 2026 Kevin Thomas
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# Author: Kevin Thomas
# Email: kevin@mytechnotalent.com
# GitHub: https://github.com/mytechnotalent
# File: CMakeLists.txt
# Desc: Configures the RP2350 Pico SDK project for the Operation Dark Vector
# micro-UAV autonomous guidance firmware.
# Created: 2026
cmake_minimum_required(VERSION 3.13)
set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# Initialise pico_sdk from installed location
# (note this can come from environment, CMake cache etc)
# == DO NOT EDIT THE FOLLOWING LINES for the Raspberry Pi Pico VS Code Extension to work ==
if(WIN32)
set(USERHOME $ENV{USERPROFILE})
else()
set(USERHOME $ENV{HOME})
endif()
set(sdkVersion 2.2.0)
set(toolchainVersion 14_2_Rel1)
set(picotoolVersion 2.2.0-a4)
set(picoVscode ${USERHOME}/.pico-sdk/cmake/pico-vscode.cmake)
if (EXISTS ${picoVscode})
include(${picoVscode})
endif()
# ====================================================================================
set(PICO_BOARD pico2 CACHE STRING "Board type")
# Pull in Raspberry Pi Pico SDK (must be before project)
include(pico_sdk_import.cmake)
project(0x0011a_cb C CXX ASM)
# Initialise the Raspberry Pi Pico SDK
pico_sdk_init()
# Add executable with modular sources in src/
add_executable(0x0011a_cb
src/main.c
src/gps.c
src/lora.c
src/propeller.c
src/payload.c
src/navigation.c
src/aes.c
src/lcd.c
)
pico_set_program_name(0x0011a_cb "0x0011a_cb")
pico_set_program_version(0x0011a_cb "0.1")
# Modify the below lines to enable/disable output over UART/USB
pico_enable_stdio_uart(0x0011a_cb 1)
pico_enable_stdio_usb(0x0011a_cb 0)
set(CTF_TARGET_LAT "38.881940" CACHE STRING "CTF target latitude (per-student randomized)")
set(CTF_TARGET_LON "-77.450280" CACHE STRING "CTF target longitude (per-student randomized)")
target_compile_definitions(0x0011a_cb PRIVATE
PICO_DEFAULT_UART_BAUD_RATE=115200
CTF_TARGET_LAT=${CTF_TARGET_LAT}
CTF_TARGET_LON=${CTF_TARGET_LON}
)
# Generate PIO header
pico_generate_pio_header(0x0011a_cb ${CMAKE_CURRENT_LIST_DIR}/src/uart_rx.pio)
# Add the standard library to the build
target_link_libraries(0x0011a_cb
pico_stdlib
hardware_uart
hardware_pio
hardware_pwm
hardware_i2c
)
# Add the standard include files to the build
target_include_directories(0x0011a_cb PRIVATE
${CMAKE_CURRENT_LIST_DIR}/include
)
pico_add_extra_outputs(0x0011a_cb)
+24
View File
@@ -0,0 +1,24 @@
// MIT License
//
// Copyright (c) 2026 Kevin Thomas
//
// File: aes.h
// Desc: Declares AES-128-ECB single-block decryption.
// Created: 2026
#ifndef AES_H
#define AES_H
#include <stdint.h>
/**
* @brief Decrypt one 16-byte block with AES-128-ECB.
*
* @param in 16-byte ciphertext.
* @param key 16-byte key.
* @param out 16-byte plaintext output.
* @return None.
*/
void aes128_ecb_decrypt_block(const uint8_t in[16], const uint8_t key[16], uint8_t out[16]);
#endif // AES_H
+17
View File
@@ -0,0 +1,17 @@
// MIT License
//
// Copyright (c) 2026 Kevin Thomas
//
// File: ctf_target.h
// Desc: AES-128-ECB key and ciphertext for the real target waypoint.
// Created: 2026
#ifndef CTF_TARGET_H
#define CTF_TARGET_H
#include <stdint.h>
#define CTF_AES_KEY { 0x56, 0x45, 0x43, 0x54, 0x4F, 0x52, 0x31, 0x31, 0x41, 0x45, 0x53, 0x4B, 0x45, 0x59, 0x21, 0x21 }
#define CTF_TARGET_CT { 0x20, 0x4F, 0xAC, 0xC2, 0xC0, 0x23, 0xEA, 0x7A, 0x3A, 0x93, 0x0F, 0x71, 0x11, 0x23, 0xEF, 0xCA }
#endif // CTF_TARGET_H
+67
View File
@@ -0,0 +1,67 @@
// MIT License
//
// Copyright (c) 2026 Kevin Thomas
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
// Author: Kevin Thomas
// Email: kevin@mytechnotalent.com
// GitHub: https://github.com/mytechnotalent
// File: gps.h
// Desc: Declares PIO UART GPS receiver interface and NMEA parsing logic.
// Created: 2026
#ifndef GPS_H
#define GPS_H
#include <stdbool.h>
#include "hardware/pio.h"
#define GPS_PIN 7
#define GPS_BAUD 9600
#define GPS_PIO pio0
#define GPS_SM 0
/**
* @brief Initialize PIO UART receiver on GPIO7 for u-blox NEO-6M GPS.
*
* @param None.
* @return None.
*/
void init_gps_pio(void);
/**
* @brief Poll PIO RX FIFO and parse incoming NMEA GPS coordinates.
*
* @param lat Pointer to double storing updated latitude.
* @param lon Pointer to double storing updated longitude.
* @return bool True if a valid active 3D GPS fix (RMC 'A') was received, false otherwise.
*/
bool poll_gps(double *lat, double *lon);
/**
* @brief Read latest GNSS signal statistics parsed from GSV sentences.
*
* @param siv Pointer to store satellites-in-view count.
* @param cno Pointer to store best carrier-to-noise ratio in dBHz.
* @return None.
*/
void gps_get_stats(int *siv, int *cno);
#endif // GPS_H
+74
View File
@@ -0,0 +1,74 @@
// MIT License
//
// Copyright (c) 2026 Kevin Thomas
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
// Author: Kevin Thomas
// Email: kevin@mytechnotalent.com
// GitHub: https://github.com/mytechnotalent
// File: lcd.h
// Desc: Declares I2C HD44780 (16x2) LCD interface for telemetry coordinates.
// Created: 2026
#ifndef LCD_H
#define LCD_H
#include <stdbool.h>
#include <stdint.h>
#include "hardware/i2c.h"
#define LCD_I2C_INST i2c1
#define LCD_SDA_PIN 2
#define LCD_SCL_PIN 3
#define LCD_BAUD 100000
/**
* @brief Initialize I2C0 peripheral and detect/configure 1602 LCD backpack.
*
* @param None.
* @return None.
*/
void init_lcd(void);
/**
* @brief Render current latitude and longitude on the 16x2 character display.
*
* Row 0: LAT: dd.dddddd N
* Row 1: LON: dd.dddddd W
*
* @param lat Current latitude in decimal degrees.
* @param lon Current longitude in decimal degrees.
* @return None.
*/
void lcd_show_coords(double lat, double lon);
/**
* @brief Render GNSS acquisition telemetry (satellites and C/N0) on the LCD.
*
* Row 0: SAT: nn CNO: nn
* Row 1: ACQUIRING... when satellites are in view, else NO SIGNAL
*
* @param sats Number of satellites currently in view.
* @param cno Best carrier-to-noise ratio in dBHz.
* @return None.
*/
void lcd_show_gnss(int sats, int cno);
#endif // LCD_H
+64
View File
@@ -0,0 +1,64 @@
// MIT License
//
// Copyright (c) 2026 Kevin Thomas
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
// Author: Kevin Thomas
// Email: kevin@mytechnotalent.com
// GitHub: https://github.com/mytechnotalent
// File: lora.h
// Desc: Declares UART1 interface for the REYAX RYLR998 LoRa transceiver.
// Created: 2026
#ifndef LORA_H
#define LORA_H
#include "hardware/uart.h"
#define LORA_UART uart1
#define LORA_BAUD 9600
#define LORA_TX_PIN 8
#define LORA_RX_PIN 9
/**
* @brief Initialize LoRa transceiver over UART1 on GPIO8 and GPIO9
*
* @param None.
* @return None.
*/
void init_lora(void);
/**
* @brief Transmit string message over LoRa UART1 interface
*
* @param msg Null-terminated string buffer to transmit.
* @return None.
*/
void lora_send(const char *msg);
/**
* @brief Service the LoRa UART: stream pending TX bytes and drain RX.
*
* @param None.
* @return None.
*/
void lora_tick(void);
#endif // LORA_H
+87
View File
@@ -0,0 +1,87 @@
// MIT License
//
// Copyright (c) 2026 Kevin Thomas
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
// Author: Kevin Thomas
// Email: kevin@mytechnotalent.com
// GitHub: https://github.com/mytechnotalent
// File: navigation.h
// Desc: Declares autonomous guidance, dead-reckoning, and telemetry interface.
// Created: 2026
#ifndef NAVIGATION_H
#define NAVIGATION_H
#include <stdbool.h>
/** @brief Pre-programmed programmed decoy waypoint (Centreville, VA). */
extern const double ORIGIN_LAT;
extern const double ORIGIN_LON;
/** @brief Target coordinates, reconstructed at boot from masked constants. */
extern double TARGET_LAT;
extern double TARGET_LON;
/**
* @brief Reconstruct the target waypoint from its XOR-masked constants.
*
* @param None.
* @return None.
*/
void init_navigation(void);
/**
* @brief Transmit telemetry stream over Debug UART0 and LoRa UART1.
*
* @param cur_lat Current micro-UAV latitude.
* @param cur_lon Current micro-UAV longitude.
* @return None.
*/
void send_telemetry(double cur_lat, double cur_lon);
/**
* @brief Advance dead-reckoning position toward programmed waypoint.
*
* @param cur_lat Pointer to current latitude.
* @param cur_lon Pointer to current longitude.
* @return None.
*/
void dead_reckon_step(double *cur_lat, double *cur_lon);
/**
* @brief Verify if micro-UAV has arrived at target coordinates.
*
* @param cur_lat Current latitude coordinate.
* @param cur_lon Current longitude coordinate.
* @return true if arrived at target, false otherwise.
*/
bool check_arrival(double cur_lat, double cur_lon);
/**
* @brief Manage guidance progression, propeller oscillation, and payload release.
*
* @param cur_lat Current latitude coordinate.
* @param cur_lon Current longitude coordinate.
* @return None.
*/
void navigate_to_target(double cur_lat, double cur_lon);
#endif // NAVIGATION_H
+68
View File
@@ -0,0 +1,68 @@
// MIT License
//
// Copyright (c) 2026 Kevin Thomas
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
// Author: Kevin Thomas
// Email: kevin@mytechnotalent.com
// GitHub: https://github.com/mytechnotalent
// File: payload.h
// Desc: Declares payload release mechanism interface on GPIO16.
// Created: 2026
#ifndef PAYLOAD_H
#define PAYLOAD_H
#include <stdbool.h>
#define LED_RED_PIN 16
#define LED_GREEN_PIN 17
#define LED_YELLOW_PIN 18
/**
* @brief Initialize GPIO16 (Red failure LED) and GPIO17 (Green success LED).
*
* @param None.
* @return None.
*/
void init_payload(void);
/**
* @brief Update tri-color GNSS status LEDs from fix state and satellites.
*
* Red (GP16) = no satellites in view; Yellow (GP18) = satellites in view
* while acquiring; Green (GP17) = active 3D fix.
*
* @param fix True if an active 3D GPS fix is held.
* @param siv Number of satellites currently in view.
* @return None.
*/
void set_gnss_leds(bool fix, int siv);
/**
* @brief Energize payload latch and illuminate both LEDs at target coordinates.
*
* @param None.
* @return None.
*/
void release_payload(void);
#endif // PAYLOAD_H
+67
View File
@@ -0,0 +1,67 @@
// MIT License
//
// Copyright (c) 2026 Kevin Thomas
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
// Author: Kevin Thomas
// Email: kevin@mytechnotalent.com
// GitHub: https://github.com/mytechnotalent
// File: propeller.h
// Desc: Declares SG90 servo PWM mock propeller interface on GPIO6.
// Created: 2026
#ifndef PROPELLER_H
#define PROPELLER_H
#define PROPELLER_PIN 6
/**
* @brief Initialize 50 Hz PWM on GPIO6 for SG90 mock propeller blade.
*
* @param None.
* @return None.
*/
void init_propeller(void);
/**
* @brief Advance mock propeller blade oscillation during flight.
*
* @param None.
* @return None.
*/
void propeller_spin(void);
/**
* @brief Halt mock propeller blade oscillation upon target arrival.
*
* @param None.
* @return None.
*/
void propeller_stop(void);
/**
* @brief Point the servo at the compass bearing toward the target.
*
* @param deg Bearing in degrees from the current position to the target.
* @return None.
*/
void propeller_set_bearing(double deg);
#endif // PROPELLER_H
+121
View File
@@ -0,0 +1,121 @@
# This is a copy of <PICO_SDK_PATH>/external/pico_sdk_import.cmake
# This can be dropped into an external project to help locate this SDK
# It should be include()ed prior to project()
# Copyright 2020 (c) 2020 Raspberry Pi (Trading) Ltd.
#
# Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
# following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
# disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products
# derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
if (DEFINED ENV{PICO_SDK_PATH} AND (NOT PICO_SDK_PATH))
set(PICO_SDK_PATH $ENV{PICO_SDK_PATH})
message("Using PICO_SDK_PATH from environment ('${PICO_SDK_PATH}')")
endif ()
if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT} AND (NOT PICO_SDK_FETCH_FROM_GIT))
set(PICO_SDK_FETCH_FROM_GIT $ENV{PICO_SDK_FETCH_FROM_GIT})
message("Using PICO_SDK_FETCH_FROM_GIT from environment ('${PICO_SDK_FETCH_FROM_GIT}')")
endif ()
if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT_PATH} AND (NOT PICO_SDK_FETCH_FROM_GIT_PATH))
set(PICO_SDK_FETCH_FROM_GIT_PATH $ENV{PICO_SDK_FETCH_FROM_GIT_PATH})
message("Using PICO_SDK_FETCH_FROM_GIT_PATH from environment ('${PICO_SDK_FETCH_FROM_GIT_PATH}')")
endif ()
if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT_TAG} AND (NOT PICO_SDK_FETCH_FROM_GIT_TAG))
set(PICO_SDK_FETCH_FROM_GIT_TAG $ENV{PICO_SDK_FETCH_FROM_GIT_TAG})
message("Using PICO_SDK_FETCH_FROM_GIT_TAG from environment ('${PICO_SDK_FETCH_FROM_GIT_TAG}')")
endif ()
if (PICO_SDK_FETCH_FROM_GIT AND NOT PICO_SDK_FETCH_FROM_GIT_TAG)
set(PICO_SDK_FETCH_FROM_GIT_TAG "master")
message("Using master as default value for PICO_SDK_FETCH_FROM_GIT_TAG")
endif()
set(PICO_SDK_PATH "${PICO_SDK_PATH}" CACHE PATH "Path to the Raspberry Pi Pico SDK")
set(PICO_SDK_FETCH_FROM_GIT "${PICO_SDK_FETCH_FROM_GIT}" CACHE BOOL "Set to ON to fetch copy of SDK from git if not otherwise locatable")
set(PICO_SDK_FETCH_FROM_GIT_PATH "${PICO_SDK_FETCH_FROM_GIT_PATH}" CACHE FILEPATH "location to download SDK")
set(PICO_SDK_FETCH_FROM_GIT_TAG "${PICO_SDK_FETCH_FROM_GIT_TAG}" CACHE FILEPATH "release tag for SDK")
if (NOT PICO_SDK_PATH)
if (PICO_SDK_FETCH_FROM_GIT)
include(FetchContent)
set(FETCHCONTENT_BASE_DIR_SAVE ${FETCHCONTENT_BASE_DIR})
if (PICO_SDK_FETCH_FROM_GIT_PATH)
get_filename_component(FETCHCONTENT_BASE_DIR "${PICO_SDK_FETCH_FROM_GIT_PATH}" REALPATH BASE_DIR "${CMAKE_SOURCE_DIR}")
endif ()
FetchContent_Declare(
pico_sdk
GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk
GIT_TAG ${PICO_SDK_FETCH_FROM_GIT_TAG}
)
if (NOT pico_sdk)
message("Downloading Raspberry Pi Pico SDK")
# GIT_SUBMODULES_RECURSE was added in 3.17
if (${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.17.0")
FetchContent_Populate(
pico_sdk
QUIET
GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk
GIT_TAG ${PICO_SDK_FETCH_FROM_GIT_TAG}
GIT_SUBMODULES_RECURSE FALSE
SOURCE_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-src
BINARY_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-build
SUBBUILD_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-subbuild
)
else ()
FetchContent_Populate(
pico_sdk
QUIET
GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk
GIT_TAG ${PICO_SDK_FETCH_FROM_GIT_TAG}
SOURCE_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-src
BINARY_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-build
SUBBUILD_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-subbuild
)
endif ()
set(PICO_SDK_PATH ${pico_sdk_SOURCE_DIR})
endif ()
set(FETCHCONTENT_BASE_DIR ${FETCHCONTENT_BASE_DIR_SAVE})
else ()
message(FATAL_ERROR
"SDK location was not specified. Please set PICO_SDK_PATH or set PICO_SDK_FETCH_FROM_GIT to on to fetch from git."
)
endif ()
endif ()
get_filename_component(PICO_SDK_PATH "${PICO_SDK_PATH}" REALPATH BASE_DIR "${CMAKE_BINARY_DIR}")
if (NOT EXISTS ${PICO_SDK_PATH})
message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' not found")
endif ()
set(PICO_SDK_INIT_CMAKE_FILE ${PICO_SDK_PATH}/pico_sdk_init.cmake)
if (NOT EXISTS ${PICO_SDK_INIT_CMAKE_FILE})
message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' does not appear to contain the Raspberry Pi Pico SDK")
endif ()
set(PICO_SDK_PATH ${PICO_SDK_PATH} CACHE PATH "Path to the Raspberry Pi Pico SDK" FORCE)
include(${PICO_SDK_INIT_CMAKE_FILE})
+411
View File
@@ -0,0 +1,411 @@
#!/usr/bin/env python3
# MIT License
#
# Copyright (c) 2026 Kevin Thomas
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# Author: Kevin Thomas
# Email: kevin@mytechnotalent.com
# GitHub: https://github.com/mytechnotalent
# File: decode_coordinates.py
# Desc: Decode and patch RP2350 micro-UAV navigation coordinates.
# Created: 2026
"""
Decode and patch RP2350 micro-UAV navigation coordinates.
Analyzes raw firmware images for IEEE 754 64-bit double-precision floating
point coordinates. Scans target flight vectors and patches binaries with
safe disposal coordinates in the Atlantic Ocean.
"""
import math
import struct
import sys
from pathlib import Path
FLASH_BASE = 0x10000000
ORIGIN_LAT = 38.840280
ORIGIN_LON = -77.428890
ATLANTIC_LAT = 37.000000
ATLANTIC_LON = -74.000000
def _haversine_calc(phi1: float, phi2: float,
dphi: float, dlam: float) -> float:
"""
Compute central angle using haversine formula.
Parameters
----------
phi1 : float
Origin latitude in radians.
phi2 : float
Target latitude in radians.
dphi : float
Latitude difference in radians.
dlam : float
Longitude difference in radians.
Returns
-------
float
Central angular distance in radians.
"""
s_phi = math.sin(dphi / 2.0) ** 2
s_lam = math.sin(dlam / 2.0) ** 2
a = s_phi + math.cos(phi1) * math.cos(phi2) * s_lam
return 2.0 * math.atan2(math.sqrt(a), math.sqrt(1.0 - a))
def haversine(lat1: float, lon1: float,
lat2: float, lon2: float) -> tuple[float, float]:
"""
Compute Great-Circle distance and azimuth bearing.
Parameters
----------
lat1 : float
Origin latitude in degrees.
lon1 : float
Origin longitude in degrees.
lat2 : float
Destination latitude in degrees.
lon2 : float
Destination longitude in degrees.
Returns
-------
tuple[float, float]
Distance in statute miles and bearing in degrees.
"""
p1, p2 = math.radians(lat1), math.radians(lat2)
dl = math.radians(lon2 - lon1)
dist = 6371.0 * _haversine_calc(p1, p2, math.radians(lat2 - lat1), dl)
y = math.sin(dl) * math.cos(p2)
term = math.sin(p1) * math.cos(p2) * math.cos(dl)
x = math.cos(p1) * math.sin(p2) - term
bearing = (math.degrees(math.atan2(y, x)) + 360.0) % 360.0
return dist * 0.621371, bearing
def _is_coord(val: float) -> bool:
"""
Verify if float falls within target geographic bounds.
Parameters
----------
val : float
Candidate double-precision value.
Returns
-------
bool
True if value is a valid latitude or longitude.
"""
if math.isnan(val) or math.isinf(val):
return False
in_lat = 35.0 <= val <= 41.0
in_lon = -79.0 <= val <= -72.0
return in_lat or in_lon
def _parse_chunk(data: bytes, off: int) -> dict | None:
"""
Extract and validate one 8-byte candidate float.
Parameters
----------
data : bytes
Firmware image buffer.
off : int
Byte offset inside image buffer.
Returns
-------
dict | None
Parsed coordinate record or None.
"""
chunk = data[off:off + 8]
val = struct.unpack("<d", chunk)[0]
if not _is_coord(val):
return None
hex_str = " ".join(f"{b:02x}" for b in chunk)
u64 = struct.unpack("<Q", chunk)[0]
kind = "LATITUDE" if val > 0.0 else "LONGITUDE"
return {"off": off, "addr": FLASH_BASE + off, "val": val,
"hex": hex_str, "u64": u64, "kind": kind}
def scan_coordinates(data: bytes) -> list[dict]:
"""
Scan firmware buffer for double-precision coordinates.
Parameters
----------
data : bytes
Firmware image buffer.
Returns
-------
list[dict]
List of candidate coordinate records.
"""
found = []
limit = len(data) - 8
for off in range(0, limit, 4):
item = _parse_chunk(data, off)
if item is not None:
found.append(item)
return found
def _print_banner(path: Path) -> None:
"""
Print operation heading and recovery origin.
Parameters
----------
path : pathlib.Path
Target firmware path.
Returns
-------
None
"""
print("=" * 67)
print(" OPERATION DARK VECTOR // FORENSIC COORDINATE TOOL")
print(" GMU Rapid Hardware Exploitation Laboratory - Fairfax, VA")
print("=" * 67)
print(f"[*] Target Binary: {path.name} ({path.stat().st_size:,} bytes)")
print(f"[*] Recovery Origin: Centreville, VA "
f"({ORIGIN_LAT:.6f}, {ORIGIN_LON:.6f})")
print("-" * 67)
def _print_candidate(c: dict) -> None:
"""
Print formatted candidate coordinate entry.
Parameters
----------
c : dict
Candidate coordinate record.
Returns
-------
None
"""
print(f" [{c['kind']:9s}] Value: {c['val']:12.6f} | "
f"Addr: 0x{c['addr']:08x} (Offset: 0x{c['off']:04x})")
print(f" Hex: {c['hex']} | uint64: 0x{c['u64']:016x}")
def _cardinal_bearing(brg: float) -> str:
"""Return compass direction string for given bearing."""
dirs = ["N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE",
"S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW"]
idx = int((brg + 11.25) / 22.5) % 16
return dirs[idx]
def _print_summary(lat: float, lon: float, mi: float, brg: float) -> None:
"""
Print mission tactical assessment summary.
Parameters
----------
lat : float
Decoded target latitude.
lon : float
Decoded target longitude.
mi : float
Distance in statute miles.
brg : float
Initial bearing in degrees.
Returns
-------
None
"""
card = _cardinal_bearing(brg)
print("\n" + "=" * 67)
print(" TACTICAL MISSION PROFILE DECODED")
print("=" * 67)
print(f" Target Latitude: {lat:.6f} deg N")
print(f" Target Longitude: {lon:.6f} deg W")
print(f" Distance from Origin: {mi:.2f} miles ({mi * 1.60934:.2f} km)")
print(f" Flight Vector Bearing: {brg:.1f} deg ({card})")
print("=" * 67)
def _patch_bytes(data: bytes, old_lat: float, old_lon: float) -> bytes:
"""
Replace target coordinates with safe Atlantic Ocean coordinates.
Parameters
----------
data : bytes
Original firmware bytes.
old_lat : float
Original target latitude.
old_lon : float
Original target longitude.
Returns
-------
bytes
Patched firmware byte buffer.
"""
src_lat = struct.pack("<d", old_lat)
src_lon = struct.pack("<d", old_lon)
dst_lat = struct.pack("<d", ATLANTIC_LAT)
dst_lon = struct.pack("<d", ATLANTIC_LON)
buf = data.replace(src_lat, dst_lat)
return buf.replace(src_lon, dst_lon)
def _print_patch_info(out_name: str, mi: float, brg: float) -> None:
"""
Display confirmation of applied firmware patch.
Parameters
----------
out_name : str
Patched output file name.
mi : float
Distance to safe disposal zone.
brg : float
Azimuth bearing to disposal zone.
Returns
-------
None
"""
print(f"\n[+] Patched firmware written to: {out_name}")
print("[+] Overwrote waypoint -> Atlantic Ocean Disposal Zone:")
print(f" Safe Latitude: {ATLANTIC_LAT:.6f} deg N")
print(f" Safe Longitude: {ATLANTIC_LON:.6f} deg W")
print(f" Offshore Distance: {mi:.2f} miles (Bearing: {brg:.1f} deg)")
def patch_firmware(target: Path, out_path: Path,
lat: float, lon: float) -> None:
"""
Patch firmware with safe Atlantic Ocean disposal waypoint.
Parameters
----------
target : pathlib.Path
Source binary path.
out_path : pathlib.Path
Destination patched binary path.
lat : float
Current target latitude.
lon : float
Current target longitude.
Returns
-------
None
"""
raw = target.read_bytes()
patched = _patch_bytes(raw, lat, lon)
out_path.write_bytes(patched)
mi, brg = haversine(ORIGIN_LAT, ORIGIN_LON, ATLANTIC_LAT, ATLANTIC_LON)
_print_patch_info(out_path.name, mi, brg)
def _evaluate(items: list[dict], target: Path, do_patch: bool) -> None:
"""
Display results and execute patch if requested.
Parameters
----------
items : list[dict]
Found coordinate records.
target : pathlib.Path
Target binary path.
do_patch : bool
Flag indicating if patch should be applied.
Returns
-------
None
"""
lats = [c for c in items if c["kind"] == "LATITUDE"]
lons = [c for c in items if c["kind"] == "LONGITUDE"]
if not (lats and lons):
return
t_lat, t_lon = lats[-1]["val"], lons[-1]["val"]
mi, brg = haversine(ORIGIN_LAT, ORIGIN_LON, t_lat, t_lon)
_print_summary(t_lat, t_lon, mi, brg)
if do_patch:
out = target.parent / f"{target.stem}_patched.bin"
patch_firmware(target, out, t_lat, t_lon)
def _parse_args(args: list[str]) -> tuple[Path, bool]:
"""
Parse command line arguments for target path and patch flag.
Parameters
----------
args : list[str]
Command line arguments.
Returns
-------
tuple[pathlib.Path, bool]
Target binary path and patch flag.
"""
do_patch = "--patch" in args
paths = [p for p in args if not p.startswith("--")]
target = Path(paths[0]) if paths else Path("0x0011a_cb.bin")
return target, do_patch
def main() -> int:
"""
Execute firmware coordinate extraction and optional patching.
Parameters
----------
None
Returns
-------
int
Zero on success, non-zero on failure.
"""
target, do_patch = _parse_args(sys.argv[1:])
if not target.exists():
print(f"[-] Error: '{target}' not found.")
return 1
_print_banner(target)
items = scan_coordinates(target.read_bytes())
for item in items:
_print_candidate(item)
_evaluate(items, target, do_patch)
return 0
if __name__ == "__main__":
raise SystemExit(main())
+202
View File
@@ -0,0 +1,202 @@
#!/usr/bin/env python3
# MIT License
#
# Copyright (c) 2026 Kevin Thomas
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# Author: Kevin Thomas
# Email: kevin@mytechnotalent.com
# GitHub: https://github.com/mytechnotalent
# File: float_hex_converter.py
# Desc: Convert and explain IEEE 754 float/hex operations step-by-step.
# Created: 2026
"""Convert and explain IEEE 754 float/hex operations step-by-step."""
import argparse
import struct
import sys
def _exp_str(e_val: int, bias: int, is_64: bool) -> str:
"""
Get accurate true exponent string accounting for IEEE 754 edge cases.
Parameters
----------
e_val : int
The stored exponent value.
bias : int
The exponent bias (127 or 1023).
is_64 : bool
True if 64-bit precision.
Returns
-------
str
The formatted true exponent explanation string.
"""
if e_val == 0:
return f"0 (Zero/Subnormal, True Exp: {1 - bias})"
if e_val == (2047 if is_64 else 255):
return f"{e_val} (Inf/NaN flag)"
return f"{e_val} - {bias} = {e_val - bias}"
def _print_encode_steps(val: float, b: int) -> None:
"""
Print the math steps for encoding a float to hex.
Parameters
----------
val : float
The float value to encode.
b : int
The bit size (32 or 64).
Returns
-------
None
"""
f1, f2, bias = ("<Q", "<d", 1023) if b == 64 else ("<I", "<f", 127)
bstr = f"{struct.unpack(f1, struct.pack(f2, val))[0]:0{b}b}"
s, e, m = (
bstr[0],
bstr[1 : 1 + (11 if b == 64 else 8)],
bstr[1 + (11 if b == 64 else 8) :],
)
hex_val = f"0x{int(bstr, 2):0{b//4}X}"
print(f"\n[ENCODE {val} to {b}-bit]\n1. Sign: {s} (0=Pos, 1=Neg)")
print(f"2. Exp: {_exp_str(int(e, 2), bias, b == 64)}\n3. Mantissa: {m}")
print(f"4. Full: {s} {e} {m}\n5. Hex: {hex_val}")
if b == 64:
raw_hex = f"{int(bstr, 2):016X}"
r3 = f"0x{raw_hex[:8]}"
r2 = f"0x{raw_hex[8:]}"
print(f"6. ARM Regs: r3 (high) = {r3}, r2 (low) = {r2} (e.g. in printf)")
else:
print(f"6. ARM Reg: Single 32-bit register ({hex_val})")
def _print_decode_steps(hex_str: str) -> None:
"""
Print the math steps for decoding a hex string to float.
Parameters
----------
hex_str : str
The hex string to decode.
Returns
-------
None
"""
c = hex_str.lower()
if c.startswith("0x"):
c = c[2:]
b, f1, f2, bias = (64, "<Q", "<d", 1023) if len(c) > 8 else (32, "<I", "<f", 127)
bstr = f"{int(c, 16):0{b}b}"
s, e, m = (
bstr[0],
bstr[1 : 1 + (11 if b == 64 else 8)],
bstr[1 + (11 if b == 64 else 8) :],
)
print(f"\n[DECODE 0x{c.zfill(b//4).upper()} ({b}-bit)]\n1. Binary: {s} {e} {m}")
print(f"2. Sign: {s}\n3. Exp: {_exp_str(int(e, 2), bias, b == 64)}")
print(f"4. Value: {struct.unpack(f2, struct.pack(f1, int(c, 16)))[0]}")
if b == 64:
raw_hex = c.zfill(16).upper()
r3 = f"0x{raw_hex[:8]}"
r2 = f"0x{raw_hex[8:]}"
print(f"5. ARM Regs: r3 (high) = {r3}, r2 (low) = {r2} (e.g. in printf)")
else:
print(f"5. ARM Reg: Single 32-bit register (0x{c.zfill(8).upper()})")
def _is_hex(s: str) -> bool:
"""
Check if a string is a hexadecimal representation.
Parameters
----------
s : str
Candidate string, with or without a 0x prefix.
Returns
-------
bool
True if every character is a hexadecimal digit.
"""
cleaned = s.lower()
if cleaned.startswith("0x"):
cleaned = cleaned[2:]
if not cleaned:
return False
return all(c in "0123456789abcdef" for c in cleaned)
def _process_conversion(val_str: str) -> None:
"""
Execute the conversion and print the output.
Parameters
----------
val_str : str
The raw input string to process.
Returns
-------
None
"""
cleaned = val_str.strip()
if cleaned.lower().startswith("0x") or (len(cleaned) >= 8 and _is_hex(cleaned)):
_print_decode_steps(cleaned)
else:
val = float(cleaned)
_print_encode_steps(val, 32)
_print_encode_steps(val, 64)
def main() -> int:
"""
Execute the conversion pipeline based on CLI arguments.
Parameters
----------
None
Returns
-------
int
Zero on successful conversion, otherwise non-zero.
"""
parser = argparse.ArgumentParser(description="Float/Hex step converter.")
parser.add_argument("val", help="Hex (0x...) or Float value to convert.")
args = parser.parse_args()
try:
_process_conversion(args.val)
return 0
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
return 1
if __name__ == "__main__":
sys.exit(main())
+135
View File
@@ -0,0 +1,135 @@
#!/usr/bin/env python3
# MIT License
#
# Copyright (c) 2026 Kevin Thomas
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# Author: Kevin Thomas
# Email: kevin@mytechnotalent.com
# GitHub: https://github.com/mytechnotalent
# File: lora_console.py
# Desc: Interactive REYAX RYLR998 terminal for the FT232RL ground station.
# Created: 2026
"""
Interactive LoRa terminal for the REYAX RYLR998 ground station.
Sends AT commands as complete bursts terminated with a carriage return and line
feed to avoid the RYLR998 inter-character timeout error, and prints incoming
packets from the airborne node as they arrive.
"""
import argparse
import sys
import threading
import time
try:
import serial
except ImportError:
serial = None
def _reader_thread(ser: "serial.Serial") -> None:
"""
Continuously read and display incoming packets from the radio.
Parameters
----------
ser : serial.Serial
Open serial connection to the ground RYLR998.
Returns
-------
None
"""
while True:
try:
line = ser.readline().decode("utf-8", errors="ignore").strip()
if line:
print(f"\n[LORA RX] {line}\n> ", end="", flush=True)
except Exception:
break
def _parse_args() -> argparse.Namespace:
"""
Parse command line arguments for the LoRa console.
Parameters
----------
None
Returns
-------
argparse.Namespace
Parsed arguments with the serial port and baud rate.
"""
parser = argparse.ArgumentParser(description="REYAX RYLR998 console")
parser.add_argument("--port", default="/dev/cu.usbserial-A50285BI")
parser.add_argument("--baud", type=int, default=115200)
return parser.parse_args()
def main() -> None:
"""
Open the ground station radio and forward operator input.
Parameters
----------
None
Returns
-------
None
"""
if serial is None:
sys.exit("pyserial required: pip install pyserial")
args = _parse_args()
print(f"[*] Opening REYAX RYLR998 on {args.port} @ {args.baud}...")
try:
ser = serial.Serial(args.port, args.baud, timeout=0.5)
except Exception as e:
sys.exit(f"[-] Failed to open {args.port}: {e}")
thread = threading.Thread(target=_reader_thread, args=(ser,), daemon=True)
thread.start()
time.sleep(0.1)
ser.write(b"AT\r\n")
print("[+] Connected. Type AT commands (e.g. AT, AT+BAND?, AT+NETWORKID?).")
print("[+] Incoming airborne packets print as [LORA RX] +RCV=...")
print("[+] Press Ctrl-C or Ctrl-D to exit.\n")
try:
while True:
cmd = input("> ").strip()
if not cmd:
continue
ser.write(cmd.encode("utf-8") + b"\r\n")
except (KeyboardInterrupt, EOFError):
print("\n[*] Exiting LoRa console.")
ser.close()
if __name__ == "__main__":
main()
+234
View File
@@ -0,0 +1,234 @@
#!/usr/bin/env python3
# MIT License
#
# Copyright (c) 2026 Kevin Thomas
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# Author: Kevin Thomas
# Email: kevin@mytechnotalent.com
# GitHub: https://github.com/mytechnotalent
# File: randomize_build.py
# Desc: Builds a per-student 0x0011a_cb image with an AES-encrypted target.
# Created: 2026
"""
Per-student randomized CTF build.
Jitters the target waypoint, AES-128-ECB encrypts it under a per-build key, and
writes include/ctf_target.h so the firmware rebuilds it at boot. Every image
carries a different ciphertext and the plaintext target exists nowhere in flash.
An offline answer key or a memorized value is useless; the waypoint can only be
recovered from the artifact and confirmed on hardware.
"""
import argparse
import json
import pathlib
import random
import struct
import subprocess
import sys
BASE_LAT = 38.881940
BASE_LON = -77.450280
JITTER_DEG = 0.01
UF2_FAMILY = "0xe48bff59"
FLASH_BASE = "0x10000000"
def _parse_args() -> argparse.Namespace:
"""
Parse command line arguments for the randomized build.
Parameters
----------
None
Returns
-------
argparse.Namespace
Parsed arguments with seed, build dir, student id, and uf2 flag.
"""
here = pathlib.Path(__file__).resolve().parent
parser = argparse.ArgumentParser(description="Randomized CTF build")
parser.add_argument("--seed", type=int, default=None)
parser.add_argument("--build-dir", default=str(here.parent / "build-ctf"))
parser.add_argument("--student-id", default="student")
parser.add_argument("--uf2", action="store_true")
return parser.parse_args()
def _make_target(seed: int) -> tuple[float, float]:
"""
Generate a jittered target waypoint around the base coordinates.
Parameters
----------
seed : int
Deterministic seed for the jitter.
Returns
-------
tuple[float, float]
Jittered latitude and longitude, rounded to six decimals.
"""
rng = random.Random(seed)
lat = round(BASE_LAT + rng.uniform(-JITTER_DEG, JITTER_DEG), 6)
lon = round(BASE_LON + rng.uniform(-JITTER_DEG, JITTER_DEG), 6)
return lat, lon
def _make_key(seed: int) -> bytes:
"""
Derive a deterministic 16-byte AES key for the build.
Parameters
----------
seed : int
Build seed from which the key is derived.
Returns
-------
bytes
Sixteen byte AES-128 key.
"""
krng = random.Random(seed ^ 0x5EED)
return bytes(krng.randrange(256) for _ in range(16))
def _aes_ecb(plain: bytes, key: bytes) -> bytes:
"""
Encrypt one block with AES-128-ECB.
Parameters
----------
plain : bytes
Sixteen byte plaintext block.
key : bytes
Sixteen byte AES key.
Returns
-------
bytes
Sixteen byte ciphertext block.
"""
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
enc = Cipher(algorithms.AES(key), modes.ECB(), backend=default_backend()).encryptor()
return enc.update(plain) + enc.finalize()
def _write_header(path: pathlib.Path, key: bytes, ct: bytes) -> None:
"""
Write the AES key and ciphertext header consumed by the firmware.
Parameters
----------
path : pathlib.Path
Destination header path.
key : bytes
Sixteen byte AES key.
ct : bytes
Sixteen byte ciphertext block.
Returns
-------
None
"""
path.write_text(
"#ifndef CTF_TARGET_H\n#define CTF_TARGET_H\n\n#include <stdint.h>\n\n"
"#define CTF_AES_KEY { %s }\n"
"#define CTF_TARGET_CT { %s }\n\n"
"#endif // CTF_TARGET_H\n"
% (", ".join(f"0x{b:02X}" for b in key), ", ".join(f"0x{b:02X}" for b in ct)))
def _run(cmd: list[str], cwd: pathlib.Path) -> None:
"""
Run an external command and raise on failure.
Parameters
----------
cmd : list[str]
Command and arguments to execute.
cwd : pathlib.Path
Working directory for the command.
Returns
-------
None
"""
subprocess.run(cmd, cwd=str(cwd), check=True)
def main() -> int:
"""
Build a per-student image with an AES-encrypted, randomized target.
Parameters
----------
None
Returns
-------
int
Zero on success.
"""
args = _parse_args()
root = pathlib.Path(__file__).resolve().parent.parent
seed = args.seed if args.seed is not None else random.randrange(2**31)
lat, lon = _make_target(seed)
key = _make_key(seed)
pt = struct.pack("<d", lat) + struct.pack("<d", lon)
ct = _aes_ecb(pt, key)
_write_header(root / "include" / "ctf_target.h", key, ct)
build = pathlib.Path(args.build_dir).resolve()
_run(["cmake", "-S", str(root), "-B", str(build)], root)
_run(["cmake", "--build", str(build)], root)
image = build / "0x0011a_cb.bin"
if args.uf2:
out = build / f"0x0011a_cb_{args.student_id}.uf2"
_run([sys.executable, str(root / "uf2conv.py"), str(image),
"-f", UF2_FAMILY, "-b", FLASH_BASE, "-c", "-o", str(out)], root)
key_out = {"student_id": args.student_id, "seed": seed,
"target_lat": lat, "target_lon": lon,
"aes_key_hex": key.hex(), "ciphertext_hex": ct.hex(),
"image": str(image)}
keydir = root / "scratch"
keydir.mkdir(exist_ok=True)
keyfile = keydir / f"answer_{args.student_id}.json"
keyfile.write_text(json.dumps(key_out, indent=2) + "\n")
print(f"[+] student_id : {args.student_id}")
print(f"[+] TARGET_LAT : {lat}")
print(f"[+] TARGET_LON : {lon}")
print(f"[+] AES key : {key.hex()}")
print(f"[+] ciphertext : {ct.hex()}")
print(f"[+] image : {image}")
print(f"[+] answer key : {keyfile} (INSTRUCTOR ONLY, do not ship)")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+302
View File
@@ -0,0 +1,302 @@
#!/usr/bin/env python3
# MIT License
#
# Copyright (c) 2026 Kevin Thomas
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# Author: Kevin Thomas
# Email: kevin@mytechnotalent.com
# GitHub: https://github.com/mytechnotalent
# File: telemetry_monitor.py
# Desc: Real-time telemetry monitor for Operation Dark Vector.
# Created: 2026
"""
Real-time telemetry monitor for Operation Dark Vector.
Reads live avionics and GPS telemetry from the FT232RL USB-to-UART ground
station bridge, parses coordinates and distance, and prints a mission status
dashboard to the terminal.
"""
import argparse
import re
import sys
import time
try:
import serial
except ImportError:
serial = None
def _format_coord(val: float, pos_c: str, neg_c: str) -> str:
"""
Format a decimal coordinate with cardinal direction.
Parameters
----------
val : float
Coordinate value in degrees.
pos_c : str
Cardinal letter for positive values.
neg_c : str
Cardinal letter for negative values.
Returns
-------
str
Formatted coordinate string.
"""
card = pos_c if val >= 0.0 else neg_c
return f"{abs(val):.6f} deg {card}"
def _format_pos(lat: float, lon: float) -> str:
"""
Format combined latitude and longitude string.
Parameters
----------
lat : float
Latitude coordinate.
lon : float
Longitude coordinate.
Returns
-------
str
Formatted dual coordinate string.
"""
lat_s = _format_coord(lat, 'N', 'S')
lon_s = _format_coord(lon, 'E', 'W')
return f"{lat_s} {lon_s}"
def _format_hud_rows(
cur_lat: float,
cur_lon: float,
is_rel: bool,
has_lock: bool = False
) -> list[str]:
"""
Format HUD data rows.
Parameters
----------
cur_lat : float
Current UAV latitude.
cur_lon : float
Current UAV longitude.
is_rel : bool
Whether payload has released.
has_lock : bool
Whether active GNSS 3D lock has been acquired.
Returns
-------
list[str]
List of formatted box rows.
"""
if cur_lat == 0.0 and cur_lon == 0.0:
c_s = "0.000000 deg N 0.000000 deg E"
g_s = "SEARCHING SATELLITES"
m_s = "MOTOR STOPPED [WAITING FOR 3D LOCK]"
else:
c_s = _format_pos(cur_lat, cur_lon)
g_s = "ACTIVE 3D LOCK" if has_lock else "SEARCHING SATELLITES"
m_s = "ACTIVE PROPULSION [SERVO SPINNING]" if has_lock else "MOTOR STOPPED [WAITING FOR 3D LOCK]"
return [
f"| CURRENT POSITION : {c_s:<44} |",
f"| GNSS SUBSYSTEM : {g_s:<44} |",
f"| PROPULSION MOTOR : {m_s:<44} |"
]
def _print_box(title: str, link: str, rows: list[str]) -> None:
"""
Print framed ASCII box.
Parameters
----------
title : str
Box title line.
link : str
Sub-header line.
rows : list[str]
Body content rows.
Returns
-------
None
"""
hdr = f"+{'-' * 65}+"
print(hdr)
print(title)
print(link)
print(hdr)
for r in rows:
print(r)
print(hdr + "\n", flush=True)
def _print_hud(
cur_lat: float,
cur_lon: float,
is_released: bool,
has_lock: bool = False
) -> None:
"""
Display the 67-character telemetry mission HUD.
Parameters
----------
cur_lat : float
Current UAV latitude.
cur_lon : float
Current UAV longitude.
is_released : bool
Whether payload solenoid has been energized.
has_lock : bool
Whether active GNSS 3D lock has been acquired.
Returns
-------
None
"""
rows = _format_hud_rows(cur_lat, cur_lon, is_released, has_lock)
title = f"|{'DARK VECTOR TELEMETRY CONSOLE':^65}|"
status = f"{'STATUS: ONLINE':>20}"
link = f"| LINK: FT232RL / RYLR998 LORA GROUND STATION{status} |"
_print_box(title, link, rows)
def _run_demo() -> None:
"""
Execute simulation of drone telemetry stream.
Parameters
----------
None
Returns
-------
None
"""
print("[*] Running simulated ground station telemetry stream...\n")
_print_hud(0.0, 0.0, False, False)
time.sleep(1.0)
_print_hud(38.840280, -77.428890, False, True)
time.sleep(1.0)
_print_hud(38.861110, -77.439585, False, True)
time.sleep(1.0)
_print_hud(38.881940, -77.450280, True, True)
def _process_line(
line: str,
coords: dict[str, any]
) -> bool:
"""
Parse a single line of serial telemetry using regex.
Parameters
----------
line : str
Raw serial string.
coords : dict[str, any]
State dictionary of coordinates.
Returns
-------
bool
True if telemetry data was updated, False otherwise.
"""
updated = False
m_cur = re.search(r"CURRENT LAT:\s*([-+]?\d*\.?\d+).*?LON:\s*([-+]?\d*\.?\d+)", line)
if m_cur:
coords["cur_lat"] = float(m_cur.group(1))
coords["cur_lon"] = float(m_cur.group(2))
coords["has_lock"] = True
updated = True
if "PAYLOAD RELEASED" in line:
coords["released"] = 1.0
updated = True
return updated
def _monitor_serial(port: str, baud: int) -> None:
"""
Monitor serial stream from FT232RL ground station.
Parameters
----------
port : str
Serial device path.
baud : int
Baud rate.
Returns
-------
None
"""
if serial is None:
sys.exit("pyserial required: pip install pyserial")
coords = {
"cur_lat": 0.0,
"cur_lon": 0.0,
"released": 0.0,
"has_lock": False
}
with serial.Serial(port, baud, timeout=1.0) as ser:
while True:
raw = ser.readline().decode("utf-8", errors="ignore").strip()
if raw:
_process_line(raw, coords)
rel = coords["released"] > 0.5
_print_hud(coords["cur_lat"], coords["cur_lon"], rel, coords["has_lock"])
def main() -> None:
"""
Parse arguments and start telemetry monitor.
Parameters
----------
None
Returns
-------
None
"""
parser = argparse.ArgumentParser(description="Dark Vector Telemetry")
parser.add_argument("--port", default="/dev/tty.usbserial-0001")
parser.add_argument("--baud", type=int, default=115200)
parser.add_argument("--demo", action="store_true")
args = parser.parse_args()
if args.demo:
_run_demo()
return
_monitor_serial(args.port, args.baud)
if __name__ == "__main__":
main()
+119
View File
@@ -0,0 +1,119 @@
// MIT License
//
// Copyright (c) 2026 Kevin Thomas
//
// File: aes.c
// Desc: Minimal AES-128-ECB block decryption for the CTF waypoint.
// Created: 2026
#include "aes.h"
#include <string.h>
#include <stdint.h>
static const uint8_t sbox[256] = {
0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76,
0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0,
0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15,
0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75,
0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0, 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84,
0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF,
0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8,
0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2,
0xCD, 0x0C, 0x13, 0xEC, 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73,
0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB,
0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79,
0xE7, 0xC8, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08,
0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A,
0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, 0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E,
0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF,
0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16,
};
static const uint8_t rsbox[256] = {
0x52, 0x09, 0x6A, 0xD5, 0x30, 0x36, 0xA5, 0x38, 0xBF, 0x40, 0xA3, 0x9E, 0x81, 0xF3, 0xD7, 0xFB,
0x7C, 0xE3, 0x39, 0x82, 0x9B, 0x2F, 0xFF, 0x87, 0x34, 0x8E, 0x43, 0x44, 0xC4, 0xDE, 0xE9, 0xCB,
0x54, 0x7B, 0x94, 0x32, 0xA6, 0xC2, 0x23, 0x3D, 0xEE, 0x4C, 0x95, 0x0B, 0x42, 0xFA, 0xC3, 0x4E,
0x08, 0x2E, 0xA1, 0x66, 0x28, 0xD9, 0x24, 0xB2, 0x76, 0x5B, 0xA2, 0x49, 0x6D, 0x8B, 0xD1, 0x25,
0x72, 0xF8, 0xF6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xD4, 0xA4, 0x5C, 0xCC, 0x5D, 0x65, 0xB6, 0x92,
0x6C, 0x70, 0x48, 0x50, 0xFD, 0xED, 0xB9, 0xDA, 0x5E, 0x15, 0x46, 0x57, 0xA7, 0x8D, 0x9D, 0x84,
0x90, 0xD8, 0xAB, 0x00, 0x8C, 0xBC, 0xD3, 0x0A, 0xF7, 0xE4, 0x58, 0x05, 0xB8, 0xB3, 0x45, 0x06,
0xD0, 0x2C, 0x1E, 0x8F, 0xCA, 0x3F, 0x0F, 0x02, 0xC1, 0xAF, 0xBD, 0x03, 0x01, 0x13, 0x8A, 0x6B,
0x3A, 0x91, 0x11, 0x41, 0x4F, 0x67, 0xDC, 0xEA, 0x97, 0xF2, 0xCF, 0xCE, 0xF0, 0xB4, 0xE6, 0x73,
0x96, 0xAC, 0x74, 0x22, 0xE7, 0xAD, 0x35, 0x85, 0xE2, 0xF9, 0x37, 0xE8, 0x1C, 0x75, 0xDF, 0x6E,
0x47, 0xF1, 0x1A, 0x71, 0x1D, 0x29, 0xC5, 0x89, 0x6F, 0xB7, 0x62, 0x0E, 0xAA, 0x18, 0xBE, 0x1B,
0xFC, 0x56, 0x3E, 0x4B, 0xC6, 0xD2, 0x79, 0x20, 0x9A, 0xDB, 0xC0, 0xFE, 0x78, 0xCD, 0x5A, 0xF4,
0x1F, 0xDD, 0xA8, 0x33, 0x88, 0x07, 0xC7, 0x31, 0xB1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xEC, 0x5F,
0x60, 0x51, 0x7F, 0xA9, 0x19, 0xB5, 0x4A, 0x0D, 0x2D, 0xE5, 0x7A, 0x9F, 0x93, 0xC9, 0x9C, 0xEF,
0xA0, 0xE0, 0x3B, 0x4D, 0xAE, 0x2A, 0xF5, 0xB0, 0xC8, 0xEB, 0xBB, 0x3C, 0x83, 0x53, 0x99, 0x61,
0x17, 0x2B, 0x04, 0x7E, 0xBA, 0x77, 0xD6, 0x26, 0xE1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0C, 0x7D,
};
static const uint8_t Rcon[11] = {0x00,0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80,0x1B,0x36};
static uint8_t xtime(uint8_t x) { return (uint8_t)((x << 1) ^ (((x >> 7) & 1) * 0x1B)); }
static uint8_t mul(uint8_t a, uint8_t b) {
uint8_t p = 0;
for (int i = 0; i < 8; i++) {
if (b & 1) p ^= a;
uint8_t hi = a & 0x80; a <<= 1;
if (hi) a ^= 0x1B;
b >>= 1;
}
return p;
}
static void key_expansion(const uint8_t *key, uint8_t *rk) {
memcpy(rk, key, 16);
for (int i = 4; i < 44; i++) {
uint8_t t[4];
memcpy(t, &rk[(i - 1) * 4], 4);
if (i % 4 == 0) {
uint8_t tmp = t[0];
t[0] = (uint8_t)(sbox[t[1]] ^ Rcon[i / 4]);
t[1] = sbox[t[2]];
t[2] = sbox[t[3]];
t[3] = sbox[tmp];
}
for (int j = 0; j < 4; j++) rk[i * 4 + j] = rk[(i - 4) * 4 + j] ^ t[j];
}
}
static void add_round_key(uint8_t r, uint8_t *s, const uint8_t *rk) {
for (int i = 0; i < 16; i++) s[i] ^= rk[r * 16 + i];
}
static void inv_sub_bytes(uint8_t *s) { for (int i = 0; i < 16; i++) s[i] = rsbox[s[i]]; }
static void inv_shift_rows(uint8_t *s) {
uint8_t t;
t = s[13]; s[13] = s[9]; s[9] = s[5]; s[5] = s[1]; s[1] = t;
t = s[2]; s[2] = s[10]; s[10] = t; t = s[6]; s[6] = s[14]; s[14] = t;
t = s[3]; s[3] = s[7]; s[7] = s[11]; s[11] = s[15]; s[15] = t;
}
static void inv_mix_columns(uint8_t *s) {
for (int i = 0; i < 4; i++) {
uint8_t a = s[i * 4], b = s[i * 4 + 1], c = s[i * 4 + 2], d = s[i * 4 + 3];
s[i * 4] = (uint8_t)(mul(a, 14) ^ mul(b, 11) ^ mul(c, 13) ^ mul(d, 9));
s[i * 4 + 1] = (uint8_t)(mul(a, 9) ^ mul(b, 14) ^ mul(c, 11) ^ mul(d, 13));
s[i * 4 + 2] = (uint8_t)(mul(a, 13) ^ mul(b, 9) ^ mul(c, 14) ^ mul(d, 11));
s[i * 4 + 3] = (uint8_t)(mul(a, 11) ^ mul(b, 13) ^ mul(c, 9) ^ mul(d, 14));
}
}
void aes128_ecb_decrypt_block(const uint8_t in[16], const uint8_t key[16], uint8_t out[16]) {
uint8_t rk[176];
key_expansion(key, rk);
memcpy(out, in, 16);
add_round_key(10, out, rk);
for (int r = 9; r > 0; r--) {
inv_shift_rows(out);
inv_sub_bytes(out);
add_round_key(r, out, rk);
inv_mix_columns(out);
}
inv_shift_rows(out);
inv_sub_bytes(out);
add_round_key(0, out, rk);
}
+170
View File
@@ -0,0 +1,170 @@
// MIT License
//
// Copyright (c) 2026 Kevin Thomas
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
// Author: Kevin Thomas
// Email: kevin@mytechnotalent.com
// GitHub: https://github.com/mytechnotalent
// File: gps.c
// Desc: Implements PIO UART GPS receiver and NMEA coordinate parsing.
// Created: 2026
#include "gps.h"
#include "uart_rx.pio.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static int gps_siv = 0;
static int gps_cno = 0;
void init_gps_pio(void)
{
uint offset = pio_add_program(GPS_PIO, &uart_rx_program);
uart_rx_program_init(GPS_PIO, GPS_SM, offset, GPS_PIN, GPS_BAUD);
}
/**
* @brief Convert NMEA ddmm.mmmm coordinate to decimal degrees.
*
* @param str NMEA coordinate string.
* @param dir Cardinal direction character ('N', 'S', 'E', 'W').
* @return double Decimal degree coordinate.
*/
static double parse_nmea_coord(const char *str, char dir)
{
double raw = atof(str);
int deg = (int)(raw / 100.0);
double dec = (double)deg + ((raw - (deg * 100.0)) / 60.0);
return ((dir == 'S') || (dir == 'W')) ? -dec : dec;
}
/**
* @brief Find pointer to n-th comma-separated field in NMEA string.
*
* @param str NMEA sentence string.
* @param field_idx Index of field to locate.
* @return const char* Pointer to field start or NULL.
*/
static const char *get_nmea_field(const char *str, int field_idx)
{
while ((str != NULL) && (*str != '\0') && (field_idx > 0)) {
if (*str++ == ',') {
field_idx--;
}
}
return (field_idx == 0) ? str : NULL;
}
/**
* @brief Parse NMEA RMC sentence for valid coordinates.
*
* @param line NMEA sentence buffer.
* @param lat Pointer to store parsed latitude.
* @param lon Pointer to store parsed longitude.
* @return None.
*/
static bool parse_rmc(const char *line, double *lat, double *lon)
{
const char *st = get_nmea_field(line, 2), *la = get_nmea_field(line, 3);
const char *lo = get_nmea_field(line, 5);
if (!st || *st != 'A' || !la || *la == ',' || !lo || *lo == ',') return false;
*lat = parse_nmea_coord(la, *get_nmea_field(line, 4));
*lon = parse_nmea_coord(lo, *get_nmea_field(line, 6));
return (*lat != 0.0) && (*lon != 0.0);
}
/**
* @brief Parse NMEA GSV sentence for satellites-in-view and best C/N0.
*
* @param line NMEA GSV sentence string.
* @return None.
*/
static void parse_gsv(const char *line)
{
const char *sv = get_nmea_field(line, 3), *msg = get_nmea_field(line, 2);
if (sv != NULL) gps_siv = atoi(sv);
if ((msg != NULL) && (*msg == '1')) gps_cno = 0;
for (int f = 7; f < 40; f += 4) {
const char *c = get_nmea_field(line, f);
if ((c == NULL) || (*c == '*') || (*c == '\0')) break;
if (atoi(c) > gps_cno) gps_cno = atoi(c);
}
}
/**
* @brief Test buffered line for valid NMEA RMC sentence.
*
* @param buf NMEA character buffer.
* @param idx Pointer to character index.
* @param lat Pointer to current latitude.
* @param lon Pointer to current longitude.
* @return bool True if valid 3D fix was parsed, false otherwise.
*/
static bool check_rmc_line(char *buf, int *idx, double *lat, double *lon)
{
buf[*idx] = '\0';
*idx = 0;
char *rmc = strstr(buf, "RMC"), *gsv = strstr(buf, "GSV");
if (gsv != NULL) parse_gsv(gsv);
return (rmc != NULL) ? parse_rmc(rmc, lat, lon) : false;
}
/**
* @brief Accumulate GPS character and trigger RMC parsing on newline.
*
* @param ch Received ASCII character.
* @param lat Pointer to current latitude.
* @param lon Pointer to current longitude.
* @return bool True if a valid active 3D fix was parsed, false otherwise.
*/
static bool process_gps_char(char ch, double *lat, double *lon)
{
static char buf[96];
static int idx = 0;
if (ch == '$') idx = 0;
if ((ch == '\n') || (ch == '\r'))
return check_rmc_line(buf, &idx, lat, lon);
if (idx < (int)(sizeof(buf) - 1))
buf[idx++] = ch;
return false;
}
static bool handle_gps_byte(double *lat, double *lon)
{
char ch = (char)(pio_sm_get(GPS_PIO, GPS_SM) >> 24);
return process_gps_char(ch, lat, lon);
}
bool poll_gps(double *lat, double *lon)
{
bool got_fix = false;
while (!pio_sm_is_rx_fifo_empty(GPS_PIO, GPS_SM))
got_fix |= handle_gps_byte(lat, lon);
return got_fix;
}
void gps_get_stats(int *siv, int *cno)
{
*siv = gps_siv;
*cno = gps_cno;
}
+144
View File
@@ -0,0 +1,144 @@
// MIT License
//
// Copyright (c) 2026 Kevin Thomas
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
// Author: Kevin Thomas
// Email: kevin@mytechnotalent.com
// GitHub: https://github.com/mytechnotalent
// File: lcd.c
// Desc: Implements I2C HD44780 16x2 LCD display for live telemetry coordinates.
// Created: 2026
#include "lcd.h"
#include "pico/stdlib.h"
#include <stdio.h>
#include <string.h>
#include <math.h>
#define PIN_RS 0x01
#define PIN_EN 0x04
#define BACKLIGHT 0x08
static uint8_t lcd_addr = 0x27;
static bool lcd_ready = false;
static void pcf_write(uint8_t d)
{
if (lcd_ready)
i2c_write_blocking(LCD_I2C_INST, lcd_addr, &d, 1, false);
}
static void pcf_pulse(uint8_t d)
{
pcf_write(d | PIN_EN);
sleep_us(1);
pcf_write(d & ~PIN_EN);
sleep_us(50);
}
static void lcd_write4(uint8_t n, uint8_t mode)
{
uint8_t d = (n & 0x0F) << 4;
d |= mode ? PIN_RS : 0;
d |= BACKLIGHT;
pcf_pulse(d);
}
static void lcd_send(uint8_t v, uint8_t mode)
{
lcd_write4((v >> 4) & 0x0F, mode);
lcd_write4(v & 0x0F, mode);
}
static void lcd_clear(void)
{
lcd_send(0x01, 0);
sleep_ms(2);
}
static void lcd_set_cursor(int row, int col)
{
uint8_t offset = (row == 0) ? 0x00 : 0x40;
lcd_send(0x80 | (col + offset), 0);
}
static void lcd_puts(const char *s)
{
while (*s)
lcd_send((uint8_t)*s++, 1);
}
static void lcd_reset_seq(void)
{
lcd_write4(0x03, 0); sleep_ms(5);
lcd_write4(0x03, 0); sleep_us(150);
lcd_write4(0x03, 0); sleep_us(150);
lcd_write4(0x02, 0); sleep_us(150);
}
static void lcd_cfg_seq(void)
{
lcd_send(0x28, 0);
lcd_send(0x0C, 0);
lcd_clear();
lcd_send(0x06, 0);
}
static bool detect_lcd(void)
{
uint8_t rx;
if (i2c_read_blocking(LCD_I2C_INST, 0x27, &rx, 1, false) >= 0)
return (lcd_addr = 0x27, true);
if (i2c_read_blocking(LCD_I2C_INST, 0x3F, &rx, 1, false) >= 0)
return (lcd_addr = 0x3F, true);
return false;
}
void init_lcd(void)
{
i2c_init(LCD_I2C_INST, LCD_BAUD);
gpio_set_function(LCD_SDA_PIN, GPIO_FUNC_I2C);
gpio_set_function(LCD_SCL_PIN, GPIO_FUNC_I2C);
gpio_pull_up(LCD_SDA_PIN); gpio_pull_up(LCD_SCL_PIN);
if (!(lcd_ready = detect_lcd())) return;
lcd_reset_seq();
lcd_cfg_seq();
}
void lcd_show_coords(double lat, double lon)
{
if (!lcd_ready && !(lcd_ready = detect_lcd())) return;
char r1[17], r2[17];
snprintf(r1, sizeof(r1), "LAT: %9.6f %c", fabs(lat), (lat >= 0.0) ? 'N' : 'S');
snprintf(r2, sizeof(r2), "LON: %9.6f %c", fabs(lon), (lon >= 0.0) ? 'E' : 'W');
lcd_set_cursor(0, 0); lcd_puts(r1);
lcd_set_cursor(1, 0); lcd_puts(r2);
}
void lcd_show_gnss(int sats, int cno)
{
if (!lcd_ready && !(lcd_ready = detect_lcd())) return;
char r1[17], r2[17];
snprintf(r1, sizeof(r1), "SAT:%2d CNO:%2d ", sats, cno);
snprintf(r2, sizeof(r2), "%-16s", (sats > 0) ? "ACQUIRING..." : "NO SIGNAL");
lcd_set_cursor(0, 0); lcd_puts(r1);
lcd_set_cursor(1, 0); lcd_puts(r2);
}
+89
View File
@@ -0,0 +1,89 @@
// MIT License
//
// Copyright (c) 2026 Kevin Thomas
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
// Author: Kevin Thomas
// Email: kevin@mytechnotalent.com
// GitHub: https://github.com/mytechnotalent
// File: lora.c
// Desc: Implements UART1 driver for REYAX RYLR998 LoRa transceiver.
// Created: 2026
#include "lora.h"
#include "hardware/gpio.h"
#include "pico/stdlib.h"
#include <stdio.h>
#include <string.h>
static void drain_lora_rx(void)
{
while (uart_is_readable(LORA_UART)) {
char c = (char)uart_getc(LORA_UART);
if (c >= 32 && c <= 126)
putchar(c);
}
}
static void send_at_cmd(const char *cmd)
{
uart_write_blocking(LORA_UART, (const uint8_t *)cmd, strlen(cmd));
sleep_ms(250);
drain_lora_rx();
}
static void configure_lora_rf(void)
{
sleep_ms(1500);
send_at_cmd("AT\r\n");
send_at_cmd("AT+NETWORKID=18\r\n");
send_at_cmd("AT+BAND=915000000\r\n");
send_at_cmd("AT+PARAMETER=9,7,1,12\r\n");
send_at_cmd("AT+ADDRESS=2\r\n");
}
void init_lora(void)
{
uart_init(LORA_UART, LORA_BAUD);
uart_set_translate_crlf(LORA_UART, false);
gpio_set_function(LORA_TX_PIN, GPIO_FUNC_UART);
gpio_set_function(LORA_RX_PIN, GPIO_FUNC_UART);
configure_lora_rf();
}
static char tx_buf[160];
static int tx_len = 0;
static int tx_idx = 0;
void lora_send(const char *msg)
{
int len = (int)strlen(msg);
while ((len > 0) && ((msg[len - 1] == '\r') || (msg[len - 1] == '\n')))
len--;
tx_len = snprintf(tx_buf, sizeof(tx_buf), "AT+SEND=0,%d,%.*s\r\n", len, len, msg);
tx_idx = 0;
}
void lora_tick(void)
{
while ((tx_idx < tx_len) && uart_is_writable(LORA_UART))
uart_putc_raw(LORA_UART, (uint8_t)tx_buf[tx_idx++]);
drain_lora_rx();
}
+108
View File
@@ -0,0 +1,108 @@
// MIT License
//
// Copyright (c) 2026 Kevin Thomas
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
// Author: Kevin Thomas
// Email: kevin@mytechnotalent.com
// GitHub: https://github.com/mytechnotalent
// File: main.c
// Desc: Main entry point for autonomous micro-UAV guidance firmware.
// Created: 2026
#include "gps.h"
#include "lora.h"
#include "payload.h"
#include "propeller.h"
#include "navigation.h"
#include "lcd.h"
#include "pico/stdlib.h"
#include "hardware/gpio.h"
#include "hardware/pio.h"
#include <stdio.h>
/**
* @brief Initialize all board peripherals, communications, and actuators.
*
* @param None.
* @return None.
*/
static void init_all(void)
{
stdio_init_all();
init_navigation();
init_payload();
init_lora();
init_gps_pio();
init_propeller();
init_lcd();
}
/**
* @brief Continually drain GPS PIO FIFO over 1-second flight tick.
*
* @param cur_lat Pointer to current latitude.
* @param cur_lon Pointer to current longitude.
* @return bool True if active 3D lock was parsed, false otherwise.
*/
static bool update_position(double *cur_lat, double *cur_lon)
{
bool got_fix = false;
for (int i = 0; i < 200; i++, sleep_ms(5)) {
got_fix |= poll_gps(cur_lat, cur_lon);
lora_tick();
}
return got_fix;
}
static void step_mission(double *cur_lat, double *cur_lon)
{
int siv = 0, cno = 0;
static int hold = 0;
bool fix = update_position(cur_lat, cur_lon);
gps_get_stats(&siv, &cno);
hold = fix ? 3 : ((hold > 0) ? (hold - 1) : 0);
bool have = fix || (hold > 0);
set_gnss_leds(have, siv);
if (have) {
lcd_show_coords(*cur_lat, *cur_lon);
navigate_to_target(*cur_lat, *cur_lon);
} else {
lcd_show_gnss(siv, cno);
propeller_stop();
send_telemetry(*cur_lat, *cur_lon);
}
}
/**
* @brief Autonomous micro-UAV firmware execution loop.
*
* @param None.
* @return int Standard exit code (never reached in embedded firmware).
*/
int main(void)
{
double cur_lat = ORIGIN_LAT, cur_lon = ORIGIN_LON;
init_all();
while (true)
step_mission(&cur_lat, &cur_lon);
return 0;
}
+106
View File
@@ -0,0 +1,106 @@
// MIT License
//
// Copyright (c) 2026 Kevin Thomas
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
// Author: Kevin Thomas
// Email: kevin@mytechnotalent.com
// GitHub: https://github.com/mytechnotalent
// File: navigation.c
// Desc: Implements waypoint navigation, dead-reckoning, and telemetry dispatch.
// Created: 2026
#include "navigation.h"
#include "ctf_target.h"
#include "aes.h"
#include "lora.h"
#include "payload.h"
#include "propeller.h"
#include <stdio.h>
#include <math.h>
#include <string.h>
#include <stdint.h>
double TARGET_LAT = 0.0;
double TARGET_LON = 0.0;
const double ORIGIN_LAT = 38.840280;
const double ORIGIN_LON = -77.428890;
void init_navigation(void)
{
uint8_t pt[16];
const uint8_t key[16] = CTF_AES_KEY;
const uint8_t ct[16] = CTF_TARGET_CT;
aes128_ecb_decrypt_block(ct, key, pt);
memcpy(&TARGET_LAT, pt, sizeof(double));
memcpy(&TARGET_LON, pt + 8, sizeof(double));
}
void send_telemetry(double cur_lat, double cur_lon)
{
char msg[80];
snprintf(msg, sizeof(msg), "CURRENT LAT: %lf, LON: %lf\r\n", cur_lat, cur_lon);
printf("%s", msg);
lora_send(msg);
}
void dead_reckon_step(double *cur_lat, double *cur_lon)
{
double dlat = TARGET_LAT - *cur_lat;
double dlon = TARGET_LON - *cur_lon;
*cur_lat += (fabs(dlat) < 0.005) ? dlat : ((dlat > 0.0) ? 0.004166 : -0.004166);
*cur_lon += (fabs(dlon) < 0.005) ? dlon : ((dlon > 0.0) ? 0.002139 : -0.002139);
}
bool check_arrival(double cur_lat, double cur_lon)
{
return (cur_lat == TARGET_LAT) && (cur_lon == TARGET_LON);
}
/**
* @brief Compute the initial bearing from one point toward another.
*
* @param lat1 Origin latitude in degrees.
* @param lon1 Origin longitude in degrees.
* @param lat2 Destination latitude in degrees.
* @param lon2 Destination longitude in degrees.
* @return double Bearing in degrees (0 to 360).
*/
static double bearing_to(double lat1, double lon1, double lat2, double lon2)
{
double p1 = lat1 * 0.017453292519943295, p2 = lat2 * 0.017453292519943295;
double dl = (lon2 - lon1) * 0.017453292519943295;
double y = sin(dl) * cos(p2);
double x = cos(p1) * sin(p2) - sin(p1) * cos(p2) * cos(dl);
double b = atan2(y, x) * 57.29577951308232;
return (b < 0.0) ? (b + 360.0) : b;
}
void navigate_to_target(double cur_lat, double cur_lon)
{
send_telemetry(cur_lat, cur_lon);
if (check_arrival(cur_lat, cur_lon)) {
propeller_stop();
release_payload();
} else {
propeller_set_bearing(bearing_to(cur_lat, cur_lon, TARGET_LAT, TARGET_LON));
}
}
+58
View File
@@ -0,0 +1,58 @@
// MIT License
//
// Copyright (c) 2026 Kevin Thomas
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
// Author: Kevin Thomas
// Email: kevin@mytechnotalent.com
// GitHub: https://github.com/mytechnotalent
// File: payload.c
// Desc: Implements payload release solenoid latch control on GPIO16.
// Created: 2026
#include "payload.h"
#include "lora.h"
#include "hardware/gpio.h"
#include <stdio.h>
void init_payload(void)
{
gpio_init(16); gpio_set_dir(16, GPIO_OUT); gpio_put(16, 1);
gpio_init(17); gpio_set_dir(17, GPIO_OUT); gpio_put(17, 0);
gpio_init(18); gpio_set_dir(18, GPIO_OUT); gpio_put(18, 0);
gpio_init(25); gpio_set_dir(25, GPIO_OUT); gpio_put(25, 0);
}
void set_gnss_leds(bool fix, int siv)
{
gpio_put(16, (!fix && (siv == 0)) ? 1 : 0);
gpio_put(17, fix ? 1 : 0);
gpio_put(18, (!fix && (siv > 0)) ? 1 : 0);
}
void release_payload(void)
{
gpio_put(16, 1);
gpio_put(17, 1);
gpio_put(18, 1);
printf("PAYLOAD RELEASED AT TARGET COORDINATES\r\n");
lora_send("PAYLOAD RELEASED AT TARGET COORDINATES\r\n");
}
+84
View File
@@ -0,0 +1,84 @@
// MIT License
//
// Copyright (c) 2026 Kevin Thomas
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
// Author: Kevin Thomas
// Email: kevin@mytechnotalent.com
// GitHub: https://github.com/mytechnotalent
// File: propeller.c
// Desc: Implements SG90 servo PWM mock propeller control on GPIO6.
// Created: 2026
#include "propeller.h"
#include "pico/stdlib.h"
#include "hardware/pwm.h"
#include "hardware/gpio.h"
static struct repeating_timer prop_timer;
static bool prop_active = false;
static uint16_t current_pulse = 1000;
void init_propeller(void)
{
gpio_set_function(PROPELLER_PIN, GPIO_FUNC_PWM);
uint s = pwm_gpio_to_slice_num(PROPELLER_PIN);
pwm_set_clkdiv(s, 150.0f);
pwm_set_wrap(s, 19999);
pwm_set_gpio_level(PROPELLER_PIN, 0);
pwm_set_enabled(s, true);
}
/**
* @brief Repeating timer callback to alternate servo angle at max slew rate.
*
* @param t Pointer to repeating timer structure.
* @return bool Always true to continue recurring timer.
*/
static bool prop_timer_callback(struct repeating_timer *t)
{
(void)t;
current_pulse = (current_pulse == 1000) ? 2000 : 1000;
pwm_set_gpio_level(PROPELLER_PIN, current_pulse);
return true;
}
void propeller_spin(void)
{
if (!prop_active) {
prop_active = true;
add_repeating_timer_ms(-150, prop_timer_callback, NULL, &prop_timer);
}
}
void propeller_set_bearing(double deg)
{
uint16_t pulse = (uint16_t)(1000.0 + (deg / 360.0) * 1000.0);
pwm_set_gpio_level(PROPELLER_PIN, pulse);
}
void propeller_stop(void)
{
if (prop_active) {
cancel_repeating_timer(&prop_timer);
prop_active = false;
}
pwm_set_gpio_level(PROPELLER_PIN, 0);
}
+43
View File
@@ -0,0 +1,43 @@
;
; Copyright (c) 2026 Kevin Thomas
; SPDX-License-Identifier: MIT
;
.pio_version 0
.program uart_rx
; 8n1 UART receiver for GPS NMEA reception on a single GPIO pin.
; Operates at 8 execution cycles per bit period.
; IN pin 0 and JMP pin are mapped to the GPS RX GPIO pin.
start:
wait 0 pin 0 ; Wait for start bit falling edge (1 cycle)
set x, 7 [10] ; Preload bit counter (7 remaining), delay 1.5 bit periods (11 cycles)
bitloop:
in pins, 1 ; Sample 1 bit from RX pin into ISR (1 cycle)
jmp x-- bitloop [6] ; Loop 8 times; each iteration is 8 execution cycles (7 cycles delay)
jmp pin good_stop ; Verify stop bit is HIGH
wait 1 pin 0 ; Framing error: wait until line returns to idle HIGH
jmp start ; Discard frame and re-synchronize
good_stop:
push noblock ; Push 8-bit byte into RX FIFO (bits [31:24])
% c-sdk {
#include "hardware/clocks.h"
#include "hardware/gpio.h"
static inline void uart_rx_program_init(PIO pio, uint sm, uint offset, uint pin, uint baud) {
pio_sm_set_consecutive_pindirs(pio, sm, pin, 1, false);
pio_gpio_init(pio, pin);
gpio_pull_up(pin);
pio_sm_config c = uart_rx_program_get_default_config(offset);
sm_config_set_in_pins(&c, pin);
sm_config_set_jmp_pin(&c, pin);
sm_config_set_in_shift(&c, true, false, 32);
sm_config_set_fifo_join(&c, PIO_FIFO_JOIN_RX);
float div = (float)clock_get_hz(clk_sys) / (8 * baud);
sm_config_set_clkdiv(&c, div);
pio_sm_init(pio, sm, offset, &c);
pio_sm_set_enabled(pio, sm, true);
}
%}
+22
View File
@@ -0,0 +1,22 @@
[
{
"short_name": "RP2040",
"id": "0xe48bff56",
"description": "Raspberry Pi RP2040"
},
{
"short_name": "RP2350-ARM-S",
"id": "0xe48bff59",
"description": "Raspberry Pi RP2350, ARM, Secure"
},
{
"short_name": "RP2350-ARM-NS",
"id": "0xe48bff5a",
"description": "Raspberry Pi RP2350, ARM, Non-Secure"
},
{
"short_name": "RP2350-RISCV",
"id": "0xe48bff5b",
"description": "Raspberry Pi RP2350, RISC-V"
}
]