mirror of
https://github.com/mytechnotalent/Embedded-Hacking.git
synced 2026-10-20 22:41:53 +02:00
Relocate Classified Briefs to correct course weeks: CB-01 in WEEK06, CB-02 in WEEK07, CB-05 in WEEK11
This commit is contained in:
1 parent
5492b41ead
commit
ee285aa370
91 files changed
+9
-9
No files matched your search
File renamed without changes.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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)
|
||||
Executable
BIN
Binary file not shown.
File diff suppressed because it is too large.
Load diff
Executable
BIN
Binary file not shown.
+2570
-3132
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
Binary file not shown.
File renamed without changes.
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
File renamed without changes.
@@ -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())
|
||||
File renamed without changes.
@@ -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()
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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.
@@ -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.
@@ -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 */
|
||||
@@ -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 */
|
||||
@@ -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 */
|
||||
@@ -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()
|
||||
@@ -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())
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
Reference in new issue
Block a user