mirror of
https://github.com/mytechnotalent/Embedded-Hacking.git
synced 2026-10-11 18:29:01 +02:00
Add Classified Brief 0x02 and remove hardcoded toolchains
This commit is contained in:
1 parent
ea7fa10403
commit
9d307322d5
44 files changed
+2065
-27
No files matched your search
Executable
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,151 @@
|
||||
# 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`.
|
||||
|
||||
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 sitting in front of you.
|
||||
|
||||
* **Pin 11 (`GP8`)**: LoRa UART1 TX (connected to RYLR998 RXD).
|
||||
* **Pin 12 (`GP9`)**: LoRa UART1 RX (connected to RYLR998 TXD).
|
||||
* **Pin 20 (`GP15`)**: Anti-tamper power sense line (connected 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 power rail and GP15 pull-up source.
|
||||
* **Pin 38 (`GND`)**: Ground reference.
|
||||
|
||||
Adversary engineers do not leave their fuzing mechanisms unguarded. Whatever logic,
|
||||
tokens, or defenses govern this weapon are buried inside thirty-two kilobytes of
|
||||
stripped ARM Cortex-M33 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. **Prove or Disprove Buddy's Prediction**: Determine whether transmitting an
|
||||
abort frame renders the fuze safe, or whether Buddy missed an internal trap.
|
||||
3. **Neutralize the Munition**: Execute a verified exploit—either by discovering
|
||||
the valid authorization mechanism, mutating the state in SRAM via GDB, or
|
||||
patching the binary in Flash.
|
||||
4. **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.
@@ -0,0 +1,94 @@
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2026 Kevin Thomas
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
#
|
||||
# Author: Kevin Thomas
|
||||
# Email: kevin@mytechnotalent.com
|
||||
# GitHub: https://github.com/mytechnotalent
|
||||
# File: CMakeLists.txt
|
||||
# Desc: Configures RP2350 Pico SDK project for Operation Zero Hour
|
||||
# Electronic Safe-and-Arm (ESA) disarm firmware.
|
||||
# Created: 2026
|
||||
|
||||
cmake_minimum_required(VERSION 3.13)
|
||||
|
||||
set(CMAKE_C_STANDARD 11)
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||
|
||||
# Initialise pico_sdk from installed location
|
||||
# == DO NOT EDIT THE FOLLOWING LINES for the Raspberry Pi Pico VS Code Extension to work ==
|
||||
if(WIN32)
|
||||
set(USERHOME $ENV{USERPROFILE})
|
||||
else()
|
||||
set(USERHOME $ENV{HOME})
|
||||
endif()
|
||||
set(sdkVersion 2.2.0)
|
||||
set(toolchainVersion 14_2_Rel1)
|
||||
set(picotoolVersion 2.2.0-a4)
|
||||
set(picoVscode ${USERHOME}/.pico-sdk/cmake/pico-vscode.cmake)
|
||||
if (EXISTS ${picoVscode})
|
||||
include(${picoVscode})
|
||||
endif()
|
||||
# ====================================================================================
|
||||
set(PICO_BOARD pico2 CACHE STRING "Board type")
|
||||
|
||||
# Pull in Raspberry Pi Pico SDK (must be before project)
|
||||
include(pico_sdk_import.cmake)
|
||||
|
||||
project(0x0014a_cb C CXX ASM)
|
||||
|
||||
# Initialise the Raspberry Pi Pico SDK
|
||||
pico_sdk_init()
|
||||
|
||||
# Add executable with modular sources in src/
|
||||
add_executable(0x0014a_cb
|
||||
src/main.c
|
||||
src/gpio_ctrl.c
|
||||
src/crypto.c
|
||||
src/lora.c
|
||||
src/safe_arm.c
|
||||
)
|
||||
|
||||
pico_set_program_name(0x0014a_cb "0x0014a_cb")
|
||||
pico_set_program_version(0x0014a_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)
|
||||
|
||||
target_compile_definitions(0x0014a_cb PRIVATE
|
||||
PICO_DEFAULT_UART_BAUD_RATE=115200
|
||||
)
|
||||
|
||||
# Add the standard library to the build
|
||||
target_link_libraries(0x0014a_cb
|
||||
pico_stdlib
|
||||
hardware_gpio
|
||||
hardware_uart
|
||||
)
|
||||
|
||||
# Add the standard include files to the build
|
||||
target_include_directories(0x0014a_cb PRIVATE
|
||||
${CMAKE_CURRENT_LIST_DIR}/include
|
||||
)
|
||||
|
||||
pico_add_extra_outputs(0x0014a_cb)
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* @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
|
||||
|
||||
void decrypt_auth_token(uint8_t *dest);
|
||||
|
||||
#endif /* CRYPTO_H */
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* @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
|
||||
|
||||
void init_gpio(void);
|
||||
void set_led_armed(void);
|
||||
void set_led_safe(void);
|
||||
void set_led_tamper_flash(bool state);
|
||||
bool is_tamper_wire_intact(void);
|
||||
|
||||
#endif /* GPIO_CTRL_H */
|
||||
@@ -0,0 +1,46 @@
|
||||
// MIT License
|
||||
//
|
||||
// Copyright (c) 2026 Kevin Thomas
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
//
|
||||
// Author: Kevin Thomas
|
||||
// Email: kevin@mytechnotalent.com
|
||||
// GitHub: https://github.com/mytechnotalent
|
||||
// File: lora.h
|
||||
// Desc: Declares UART1 interface for the REYAX RYLR998 LoRa transceiver.
|
||||
// Created: 2026
|
||||
|
||||
#ifndef LORA_H
|
||||
#define LORA_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include "hardware/uart.h"
|
||||
|
||||
#define LORA_UART uart1
|
||||
#define LORA_BAUD 9600
|
||||
#define LORA_TX_PIN 8
|
||||
#define LORA_RX_PIN 9
|
||||
|
||||
void init_lora(void);
|
||||
void lora_send(const char *msg);
|
||||
bool lora_poll_packet(char *payload_out, size_t max_len);
|
||||
|
||||
#endif // LORA_H
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* @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
|
||||
|
||||
void init_safe_arm_controller(void);
|
||||
void process_safe_arm_tick(void);
|
||||
|
||||
#endif /* SAFE_ARM_H */
|
||||
@@ -0,0 +1,121 @@
|
||||
# This is a copy of <PICO_SDK_PATH>/external/pico_sdk_import.cmake
|
||||
|
||||
# This can be dropped into an external project to help locate this SDK
|
||||
# It should be include()ed prior to project()
|
||||
|
||||
# Copyright 2020 (c) 2020 Raspberry Pi (Trading) Ltd.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
|
||||
# following conditions are met:
|
||||
#
|
||||
# 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
|
||||
# disclaimer.
|
||||
#
|
||||
# 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
|
||||
# disclaimer in the documentation and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products
|
||||
# derived from this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
||||
# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
||||
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
if (DEFINED ENV{PICO_SDK_PATH} AND (NOT PICO_SDK_PATH))
|
||||
set(PICO_SDK_PATH $ENV{PICO_SDK_PATH})
|
||||
message("Using PICO_SDK_PATH from environment ('${PICO_SDK_PATH}')")
|
||||
endif ()
|
||||
|
||||
if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT} AND (NOT PICO_SDK_FETCH_FROM_GIT))
|
||||
set(PICO_SDK_FETCH_FROM_GIT $ENV{PICO_SDK_FETCH_FROM_GIT})
|
||||
message("Using PICO_SDK_FETCH_FROM_GIT from environment ('${PICO_SDK_FETCH_FROM_GIT}')")
|
||||
endif ()
|
||||
|
||||
if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT_PATH} AND (NOT PICO_SDK_FETCH_FROM_GIT_PATH))
|
||||
set(PICO_SDK_FETCH_FROM_GIT_PATH $ENV{PICO_SDK_FETCH_FROM_GIT_PATH})
|
||||
message("Using PICO_SDK_FETCH_FROM_GIT_PATH from environment ('${PICO_SDK_FETCH_FROM_GIT_PATH}')")
|
||||
endif ()
|
||||
|
||||
if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT_TAG} AND (NOT PICO_SDK_FETCH_FROM_GIT_TAG))
|
||||
set(PICO_SDK_FETCH_FROM_GIT_TAG $ENV{PICO_SDK_FETCH_FROM_GIT_TAG})
|
||||
message("Using PICO_SDK_FETCH_FROM_GIT_TAG from environment ('${PICO_SDK_FETCH_FROM_GIT_TAG}')")
|
||||
endif ()
|
||||
|
||||
if (PICO_SDK_FETCH_FROM_GIT AND NOT PICO_SDK_FETCH_FROM_GIT_TAG)
|
||||
set(PICO_SDK_FETCH_FROM_GIT_TAG "master")
|
||||
message("Using master as default value for PICO_SDK_FETCH_FROM_GIT_TAG")
|
||||
endif()
|
||||
|
||||
set(PICO_SDK_PATH "${PICO_SDK_PATH}" CACHE PATH "Path to the Raspberry Pi Pico SDK")
|
||||
set(PICO_SDK_FETCH_FROM_GIT "${PICO_SDK_FETCH_FROM_GIT}" CACHE BOOL "Set to ON to fetch copy of SDK from git if not otherwise locatable")
|
||||
set(PICO_SDK_FETCH_FROM_GIT_PATH "${PICO_SDK_FETCH_FROM_GIT_PATH}" CACHE FILEPATH "location to download SDK")
|
||||
set(PICO_SDK_FETCH_FROM_GIT_TAG "${PICO_SDK_FETCH_FROM_GIT_TAG}" CACHE FILEPATH "release tag for SDK")
|
||||
|
||||
if (NOT PICO_SDK_PATH)
|
||||
if (PICO_SDK_FETCH_FROM_GIT)
|
||||
include(FetchContent)
|
||||
set(FETCHCONTENT_BASE_DIR_SAVE ${FETCHCONTENT_BASE_DIR})
|
||||
if (PICO_SDK_FETCH_FROM_GIT_PATH)
|
||||
get_filename_component(FETCHCONTENT_BASE_DIR "${PICO_SDK_FETCH_FROM_GIT_PATH}" REALPATH BASE_DIR "${CMAKE_SOURCE_DIR}")
|
||||
endif ()
|
||||
FetchContent_Declare(
|
||||
pico_sdk
|
||||
GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk
|
||||
GIT_TAG ${PICO_SDK_FETCH_FROM_GIT_TAG}
|
||||
)
|
||||
|
||||
if (NOT pico_sdk)
|
||||
message("Downloading Raspberry Pi Pico SDK")
|
||||
# GIT_SUBMODULES_RECURSE was added in 3.17
|
||||
if (${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.17.0")
|
||||
FetchContent_Populate(
|
||||
pico_sdk
|
||||
QUIET
|
||||
GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk
|
||||
GIT_TAG ${PICO_SDK_FETCH_FROM_GIT_TAG}
|
||||
GIT_SUBMODULES_RECURSE FALSE
|
||||
|
||||
SOURCE_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-src
|
||||
BINARY_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-build
|
||||
SUBBUILD_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-subbuild
|
||||
)
|
||||
else ()
|
||||
FetchContent_Populate(
|
||||
pico_sdk
|
||||
QUIET
|
||||
GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk
|
||||
GIT_TAG ${PICO_SDK_FETCH_FROM_GIT_TAG}
|
||||
|
||||
SOURCE_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-src
|
||||
BINARY_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-build
|
||||
SUBBUILD_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-subbuild
|
||||
)
|
||||
endif ()
|
||||
|
||||
set(PICO_SDK_PATH ${pico_sdk_SOURCE_DIR})
|
||||
endif ()
|
||||
set(FETCHCONTENT_BASE_DIR ${FETCHCONTENT_BASE_DIR_SAVE})
|
||||
else ()
|
||||
message(FATAL_ERROR
|
||||
"SDK location was not specified. Please set PICO_SDK_PATH or set PICO_SDK_FETCH_FROM_GIT to on to fetch from git."
|
||||
)
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
get_filename_component(PICO_SDK_PATH "${PICO_SDK_PATH}" REALPATH BASE_DIR "${CMAKE_BINARY_DIR}")
|
||||
if (NOT EXISTS ${PICO_SDK_PATH})
|
||||
message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' not found")
|
||||
endif ()
|
||||
|
||||
set(PICO_SDK_INIT_CMAKE_FILE ${PICO_SDK_PATH}/pico_sdk_init.cmake)
|
||||
if (NOT EXISTS ${PICO_SDK_INIT_CMAKE_FILE})
|
||||
message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' does not appear to contain the Raspberry Pi Pico SDK")
|
||||
endif ()
|
||||
|
||||
set(PICO_SDK_PATH ${PICO_SDK_PATH} CACHE PATH "Path to the Raspberry Pi Pico SDK" FORCE)
|
||||
|
||||
include(${PICO_SDK_INIT_CMAKE_FILE})
|
||||
Executable
+195
@@ -0,0 +1,195 @@
|
||||
#!/usr/bin/env python3
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2026 Kevin Thomas
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
#
|
||||
# Author: Kevin Thomas
|
||||
# Email: kevin@mytechnotalent.com
|
||||
# GitHub: https://github.com/mytechnotalent
|
||||
# File: lora_console.py
|
||||
# Desc: Interactive REYAX RYLR998 terminal for the FT232RL ground station.
|
||||
# Created: 2026
|
||||
|
||||
"""
|
||||
Interactive LoRa terminal for the REYAX RYLR998 ground station.
|
||||
|
||||
Sends AT commands as complete bursts terminated with a carriage return and line
|
||||
feed to avoid the RYLR998 inter-character timeout error, and prints incoming
|
||||
packets from the airborne node as they arrive.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
|
||||
try:
|
||||
import serial
|
||||
except ImportError:
|
||||
serial = None
|
||||
|
||||
|
||||
def _reader_thread(ser: "serial.Serial") -> None:
|
||||
"""
|
||||
Continuously read and display incoming packets from the radio.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ser : serial.Serial
|
||||
Open serial connection to the ground RYLR998.
|
||||
|
||||
Returns
|
||||
-------
|
||||
None
|
||||
"""
|
||||
while True:
|
||||
try:
|
||||
line = ser.readline().decode("utf-8", errors="ignore").strip()
|
||||
if line:
|
||||
print(f"\n[LORA RX] {line}\n> ", end="", flush=True)
|
||||
except Exception:
|
||||
break
|
||||
|
||||
|
||||
def _parse_args() -> argparse.Namespace:
|
||||
"""
|
||||
Parse command line arguments for the LoRa console.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
None
|
||||
|
||||
Returns
|
||||
-------
|
||||
argparse.Namespace
|
||||
Parsed arguments with the serial port and baud rate.
|
||||
"""
|
||||
parser = argparse.ArgumentParser(description="REYAX RYLR998 console")
|
||||
parser.add_argument("--port", default="/dev/cu.usbserial-A50285BI")
|
||||
parser.add_argument("--baud", type=int, default=115200)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def _open_radio(port: str, baud: int) -> "serial.Serial":
|
||||
"""
|
||||
Open serial connection to the REYAX transceiver.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
port : str
|
||||
Serial device path or COM port identifier.
|
||||
baud : int
|
||||
Baud rate for serial communication.
|
||||
|
||||
Returns
|
||||
-------
|
||||
serial.Serial
|
||||
Open serial interface to the radio.
|
||||
"""
|
||||
print(f"[*] Opening REYAX RYLR998 on {port} @ {baud}...")
|
||||
try:
|
||||
return serial.Serial(port, baud, timeout=0.5)
|
||||
except Exception as e:
|
||||
sys.exit(f"[-] Failed to open {port}: {e}")
|
||||
|
||||
|
||||
def _start_radio(ser: "serial.Serial") -> None:
|
||||
"""
|
||||
Start listener thread and send initial AT ping.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ser : serial.Serial
|
||||
Open serial connection to the ground RYLR998.
|
||||
|
||||
Returns
|
||||
-------
|
||||
None
|
||||
"""
|
||||
thread = threading.Thread(target=_reader_thread, args=(ser,), daemon=True)
|
||||
thread.start()
|
||||
time.sleep(0.1)
|
||||
ser.write(b"AT\r\n")
|
||||
|
||||
|
||||
def _print_banner() -> None:
|
||||
"""
|
||||
Print console connection info and operator command instructions.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
None
|
||||
|
||||
Returns
|
||||
-------
|
||||
None
|
||||
"""
|
||||
print("[+] Connected. Type AT commands (e.g. AT, AT+BAND?, AT+NETWORKID?).")
|
||||
print("[+] Incoming airborne packets print as [LORA RX] +RCV=...")
|
||||
print("[+] Press Ctrl-C or Ctrl-D to exit.\n")
|
||||
|
||||
|
||||
def _console_loop(ser: "serial.Serial") -> None:
|
||||
"""
|
||||
Forward operator input lines to the serial transceiver.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ser : serial.Serial
|
||||
Open serial connection to the ground RYLR998.
|
||||
|
||||
Returns
|
||||
-------
|
||||
None
|
||||
"""
|
||||
try:
|
||||
while True:
|
||||
cmd = input("> ").strip()
|
||||
if cmd:
|
||||
ser.write(cmd.encode("utf-8") + b"\r\n")
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
print("\n[*] Exiting LoRa console.")
|
||||
ser.close()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""
|
||||
Open the ground station radio and forward operator input.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
None
|
||||
|
||||
Returns
|
||||
-------
|
||||
None
|
||||
"""
|
||||
if serial is None:
|
||||
sys.exit("pyserial required: pip install pyserial")
|
||||
args = _parse_args()
|
||||
ser = _open_radio(args.port, args.baud)
|
||||
_start_radio(ser)
|
||||
_print_banner()
|
||||
_console_loop(ser)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+105
@@ -0,0 +1,105 @@
|
||||
#!/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()
|
||||
@@ -0,0 +1,861 @@
|
||||
#!/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())
|
||||
@@ -0,0 +1,22 @@
|
||||
[
|
||||
{
|
||||
"short_name": "RP2040",
|
||||
"id": "0xe48bff56",
|
||||
"description": "Raspberry Pi RP2040"
|
||||
},
|
||||
{
|
||||
"short_name": "RP2350-ARM-S",
|
||||
"id": "0xe48bff59",
|
||||
"description": "Raspberry Pi RP2350, ARM, Secure"
|
||||
},
|
||||
{
|
||||
"short_name": "RP2350-ARM-NS",
|
||||
"id": "0xe48bff5a",
|
||||
"description": "Raspberry Pi RP2350, ARM, Non-Secure"
|
||||
},
|
||||
{
|
||||
"short_name": "RP2350-RISCV",
|
||||
"id": "0xe48bff5b",
|
||||
"description": "Raspberry Pi RP2350, RISC-V"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* @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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* @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"
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
// MIT License
|
||||
//
|
||||
// Copyright (c) 2026 Kevin Thomas
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
//
|
||||
// Author: Kevin Thomas
|
||||
// Email: kevin@mytechnotalent.com
|
||||
// GitHub: https://github.com/mytechnotalent
|
||||
// File: lora.c
|
||||
// Desc: Implements UART1 driver for REYAX RYLR998 LoRa transceiver.
|
||||
// Created: 2026
|
||||
|
||||
#include "lora.h"
|
||||
#include "hardware/gpio.h"
|
||||
#include "pico/stdlib.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
static void drain_lora_rx(void) {
|
||||
while (uart_is_readable(LORA_UART)) {
|
||||
(void)uart_getc(LORA_UART);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* @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"
|
||||
|
||||
static void init_system(void) {
|
||||
stdio_init_all();
|
||||
init_gpio();
|
||||
init_safe_arm_controller();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* @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];
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
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)++;
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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