Audit-clean C standard across all 5 cb challenges: fix 3 functions over 8 exec lines; rebuild bins; update CB-0x03 and CB-0x05 guides; verify all guides against new bins

This commit is contained in:
Kevin Thomas committed 2026-10-11 17:43:45 -04:00
1 parent 4ade66738f
commit 23b9f628ef
51 files changed
+3744 -7904

No files matched your search

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+138 -6
View File
@@ -7,16 +7,148 @@
| |
|OPERATION IRON NET |
| |
|CLASSIFIED BRIEFING: LIVE CTF 0x03 |
|CLASSIFIED BRIEFING: LIVE CHALLENGE 0x03 |
| |
|NATIONAL SECURITY AGENCY / GMU RHET |
+-----------------------------------------------------------------+
```
## 1. The Intercept
At 0200 hours, Forward Operating Base (FOB) Alpha detected a series of covert environmental sensor drops. The adversary is using micro-UAVs to deploy static ground sensors that measure atmospheric conditions (temperature and humidity) via a single-wire protocol. If the conditions match a highly specific, hardcoded threshold, the sensor node transmits a tactical strike authorization over LoRa.
## 1. The Advice
## 2. The Mandate
CyberCom intercepted one of the sensor nodes. The hardware runs a RP2350. The firmware calculates a complex bitwise checksum (`current_humidity ^ 0xAF & 0x3C`) to validate the environment before authorizing the strike.
INT. NSA EXPLOITATION CELL. 04:10.
Buddy, our AI exploitation model, has failed to crack the checksum logic. You must attach GDB, analyze the bitwise operators in the memory registers, and either manually inject the correct bitmask to force a false-positive strike authorization, or patch the firmware to bypass the bitwise conditional entirely.
A grey field junction box sits on the bench, its lid peeled back. Inside,
secured in potting compound, is a covert environmental sensor node recovered
from a contested waterway.
On the top of the enclosure, a status diode glows a steady red. Beside it, a
REYAX RYLR998 LoRa transceiver is wired to `UART1` on `GP8` and `GP9`. A
three-wire ribbon terminates in a DHT11 temperature and humidity canister. 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:** What is it?
**ANALYST:** A remote environmental trigger. The adversary seeds these along a
river corridor. The DHT11 samples the air. When the conditions hit a preset
window, the node arms a payload channel and broadcasts a strike authorization
over LoRa.
**DIRECTOR:** Is it live?
**ANALYST:** Armed and listening. The payload relay on `GP18` is wired. One
authenticated strike frame and the cell downstream fires.
**DIRECTOR:** Run it through Buddy.
**ANALYST:** Already did. Four minutes with the stripped flash image.
She turns her monitor toward him.
```
+-----------------------------------------------------------------+
| BUDDY // CLASSIFIED EXPLOITATION ASSIST // CONF: 0.98 |
+-----------------------------------------------------------------+
| ACTION : Replay the environmental checksum window. |
| MECHANISM : DHT11 humidity byte XOR 0xAF, masked by 0x3C. |
| PREDICTION : Force (humidity ^ 0xAF) & 0x3C == 0x18, node arm. |
| CONFIDENCE : 0.98 |
+-----------------------------------------------------------------+
```
**DIRECTOR:** 0.98. Forge the reading, arm the node, learn its payload.
**ANALYST:** Buddy found the bitwise window. Buddy did not find the door behind
it. There is an authorization token, and there is a trap.
**DIRECTOR:** Buddy says 0.98.
**ANALYST:** Buddy disassembled `evaluate_environment`. Buddy never followed the
SRAM the state machine lives in. Buddy never read the fault counter.
## 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 the target is a live payload channel.
Buddy scanned the flash image and found a tidy bitwise test. It concluded that
satisfying the arithmetic is the whole game. It published the mask and the window
and declared the node owned.
What Buddy failed to model is that adversary tripwires are engineered for exactly
this attack. A real node does not trade a payload on a single sensor byte. It
runs a state machine, validates an encrypted token carried over the air, and
counts its own faults so that a spoofer who fumbles the protocol three times
burns the node instead of arming it.
There is also a physical seal. Cut the sense loop and the node latches shut. The
correct env window is necessary. It is nowhere near sufficient.
Buddy saw the bitwise test. Buddy did not verify the ground truth.
## 3. The Operation
At 01:55, a Riverine Technical Intelligence team recovered an intact node before
adversary recovery could zeroize it. The enclosure is a single-board design 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 `0x001aa_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 19 (`GP14`)**: DHT11 single-wire data line.
* **Pin 20 (`GP15`)**: Anti-tamper sense loop (jumpered to `3V3`).
* **Pin 21 (`GP16`)**: Red LED (`NODE ARMED // LISTENING`).
* **Pin 22 (`GP17`)**: Green LED (`STRIKE AUTHORIZED`).
* **Pin 24 (`GP18`)**: Payload trigger relay.
* **Pin 36 (`3V3`)**: RYLR998 VDD rail and `GP15` pull-up source.
* **Pin 38 (`GND`)**: Ground reference.
Inside this image there is an environment gate, an over-the-air authorization
token, a fault counter, and a tamper latch. Whatever mechanism actually authorizes
a strike is 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. Trace the environment routine. Map the state
machine. Do not forge a reading over LoRa until you know with certainty what that
firmware will do with the next frame, and what it will do to you if the frame is
wrong.
The orders are clear:
1. **Recover the Environment Gate**: Locate the bitwise validation routine and
work out which DHT11 humidity value satisfies it.
2. **Recover the Authorization Token**: Find the custom cipher that is applied at
boot and decrypt the plaintext strike token it protects.
3. **Respect the Traps**: Determine what the node does after repeated faults, and
what happens if the `GP15` sense loop is broken.
4. **Neutralize the Node**: Execute a verified exploit, either by recovering the
token and satisfying the gate, by mutating the state machine in SRAM with GDB,
or by 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 a strike.
## 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.
+3 -1
View File
@@ -55,7 +55,10 @@ pico_sdk_init()
add_executable(0x001aa_cb
src/main.c
src/gpio_ctrl.c
src/dht11.c
src/crypto.c
src/lora.c
src/logic.c
)
@@ -73,7 +76,6 @@ target_link_libraries(0x001aa_cb
pico_stdlib
hardware_gpio
hardware_uart
hardware_pwm
)
target_include_directories(0x001aa_cb PRIVATE
+26
View File
@@ -0,0 +1,26 @@
/**
* @file crypto.h
* @brief Lightweight rolling cipher interface for Operation Iron Net
* @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 strike 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 */
+20 -1
View File
@@ -10,14 +10,33 @@
#ifndef DHT11_H
#define DHT11_H
#include <stdint.h>
#include <stdbool.h>
#define DHT11_PIN 14
typedef struct {
uint8_t humidity_int;
uint8_t humidity_dec;
uint8_t temp_int;
uint8_t temp_dec;
uint8_t checksum;
bool valid;
} dht11_data_t;
/**
* @brief Initialize the DHT11 data pin and idle the sensor high.
* @param void No parameters.
* @return void No return value.
*/
void dht11_init(void);
/**
* @brief Perform a single-wire DHT11 read into the reading cache.
* @param void No parameters.
* @return dht11_data_t* Pointer to the current sensor reading.
*/
dht11_data_t* dht11_read(void);
#endif
#endif // DHT11_H
+63
View File
@@ -0,0 +1,63 @@
/**
* @file gpio_ctrl.h
* @brief Operation Iron Net Firmware
* @author Kevin Thomas
* @date 2026
*
* MIT License
* Copyright (c) 2026 Kevin Thomas
*/
#ifndef GPIO_CTRL_H
#define GPIO_CTRL_H
#include <stdbool.h>
#define PAYLOAD_PIN 18
#define LED_RED_PIN 16
#define LED_GREEN_PIN 17
#define TAMPER_PIN 15
/**
* @brief Initialize the status LEDs, payload, and tamper 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 authorized indication LEDs (green on, red off).
* @param void No parameters.
* @return void No return value.
*/
void set_led_authorized(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 enclosure tamper loop state.
* @param void No parameters.
* @return true when the tamper wire is intact, false when cut.
*/
bool is_tamper_wire_intact(void);
/**
* @brief Pulse the payload marker output to deploy the marker.
* @param void No parameters.
* @return void No return value.
*/
void payload_deploy(void);
#endif // GPIO_CTRL_H
+22 -1
View File
@@ -10,5 +10,26 @@
#ifndef LOGIC_H
#define LOGIC_H
#include <stdbool.h>
#include <stdint.h>
#define STATE_ENV_ARMED 0x01
#define STATE_ENV_AUTHORIZED 0x03
#define STATE_ENV_LOCKOUT 0xFF
/**
* @brief Initialize the strike logic, token, radio, and feedback.
* @param void No parameters.
* @return void No return value.
*/
void init_strike_controller(void);
/**
* @brief Evaluate the environment and fire the payload when authorized.
* @param void No parameters.
* @return void No return value.
*/
void evaluate_environment(void);
#endif
#endif // LOGIC_H
+64
View File
@@ -0,0 +1,64 @@
// MIT License
//
// Copyright (c) 2026 Kevin Thomas
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
// Author: Kevin Thomas
// Email: kevin@mytechnotalent.com
// GitHub: https://github.com/mytechnotalent
// File: lora.h
// Desc: Declares UART1 interface for the REYAX RYLR998 LoRa transceiver.
// Created: 2026
#ifndef LORA_H
#define LORA_H
#include <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
/**
* @brief Initialize UART1 and configure the RYLR998 radio parameters.
* @param void No parameters.
* @return void No return value.
*/
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.
*/
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.
*/
bool lora_poll_packet(char *payload_out, size_t max_len);
#endif // LORA_H
+195
View File
@@ -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()
+109
View File
@@ -0,0 +1,109 @@
#!/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 Iron Net (0x001aa_cb).
# Created: 2026
"""
Patch script for Operation Iron Net (0x001aa_cb).
Forces the environment gate and the authorization-token gate to true so the
node deploys the payload on boot without a valid sensor reading or token.
"""
import sys
PATCHES = [
(0x7BC, b"\x58\xd1", b"\x00\xbf", "nop (force authorization accept)"),
(0x802, b"\x1b\xb1", b"\x00\xbf", "nop (ignore the environment gate)"),
(0x7D2, b"\x02\x2b", b"\xff\x2b", "cmp r3, #0xff (raise fault threshold)"),
]
def _apply_patch(data: bytearray) -> None:
"""
Verify each opcode and apply its replacement bytes in place.
Parameters
----------
data : bytearray
Mutable binary image buffer.
Returns
-------
None
"""
for offset, expected, patched, note in PATCHES:
if data[offset:offset + len(expected)] != expected:
actual = data[offset:offset + len(expected)].hex()
raise ValueError(f"Expected {expected.hex()} at 0x{offset:x}, got {actual}")
data[offset:offset + len(patched)] = patched
print(f"[+] Patched 0x{offset:x}: {note}")
def patch_binary(in_bin: str, out_bin: str) -> None:
"""
Read target firmware binary, apply patches, 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}")
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 "0x001aa_cb.bin"
out_file = sys.argv[2] if len(sys.argv) > 2 else "0x001aa_cb_patched.bin"
patch_binary(in_file, out_file)
if __name__ == "__main__":
main()
+861
View File
@@ -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())
+22
View File
@@ -0,0 +1,22 @@
[
{
"short_name": "RP2040",
"id": "0xe48bff56",
"description": "Raspberry Pi RP2040"
},
{
"short_name": "RP2350-ARM-S",
"id": "0xe48bff59",
"description": "Raspberry Pi RP2350, ARM, Secure"
},
{
"short_name": "RP2350-ARM-NS",
"id": "0xe48bff5a",
"description": "Raspberry Pi RP2350, ARM, Non-Secure"
},
{
"short_name": "RP2350-RISCV",
"id": "0xe48bff5b",
"description": "Raspberry Pi RP2350, RISC-V"
}
]
+27
View File
@@ -0,0 +1,27 @@
/**
* @file crypto.c
* @brief Lightweight rolling cipher implementation for Operation Iron Net
* @author Kevin Thomas
* @date 2026
*
* MIT License
* Copyright (c) 2026 Kevin Thomas
*/
#include "crypto.h"
static const uint8_t CIPHERTEXT[TOKEN_LEN] = {
0x1A, 0x07, 0x03, 0x18, 0x06, 0x12, 0x65, 0x64
};
static const uint8_t CIPHER_KEY[4] = {
0x49, 0x52, 0x4F, 0x4E
};
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);
}
}
+130 -9
View File
@@ -9,19 +9,140 @@
*/
#include "dht11.h"
#include <stdio.h>
#include "hardware/gpio.h"
#include "pico/stdlib.h"
#include "pico/time.h"
dht11_data_t current_reading;
static dht11_data_t g_current_reading;
/**
* @brief Block until the data pin reaches a level or a timeout expires.
* @param level Logic level to wait for (true high, false low).
* @param timeout_us Maximum time to wait in microseconds.
* @return true when the level was observed, false on timeout.
*/
static bool dht11_wait_for(bool level, uint32_t timeout_us) {
uint64_t start = time_us_64();
while (gpio_get(DHT11_PIN) != level) {
if ((uint32_t)(time_us_64() - start) > timeout_us) {
return false;
}
}
return true;
}
/**
* @brief Drive the 18 ms start pulse and wait for the sensor response.
* @param void No parameters.
* @return true when the response handshake was seen, false otherwise.
*/
static bool dht11_start_signal(void) {
gpio_set_dir(DHT11_PIN, GPIO_OUT);
gpio_put(DHT11_PIN, 0);
sleep_ms(20);
gpio_set_dir(DHT11_PIN, GPIO_IN);
return dht11_wait_for(false, 100)
&& dht11_wait_for(true, 100)
&& dht11_wait_for(false, 100);
}
/**
* @brief Measure a single high pulse and return its decoded bit value.
* @param void No parameters.
* @return 1 for a long pulse, 0 for a short pulse, or -1 on timeout.
*/
static int dht11_read_bit(void) {
if (!dht11_wait_for(true, 100)) {
return -1;
}
uint64_t high_start = time_us_64();
if (!dht11_wait_for(false, 100)) {
return -1;
}
return ((uint32_t)(time_us_64() - high_start) > 40) ? 1 : 0;
}
/**
* @brief Sample the 40 data bits by measuring each high pulse width.
* @param bytes Output buffer receiving the five decoded bytes.
* @return true when all forty bits were captured, false on timeout.
*/
static bool dht11_capture_bits(uint8_t *bytes) {
for (int i = 0; i < 40; i++) {
int bit = dht11_read_bit();
if (bit < 0) {
return false;
}
bytes[i / 8] = (uint8_t)((bytes[i / 8] << 1) | bit);
}
return true;
}
/**
* @brief Verify the frame checksum equals the low byte of the data sum.
* @param bytes Pointer to the five decoded frame bytes.
* @return true when the checksum matches, false otherwise.
*/
static bool dht11_checksum_ok(const uint8_t *bytes) {
uint8_t sum = bytes[0] + bytes[1] + bytes[2] + bytes[3];
return sum == bytes[4];
}
/**
* @brief Copy the validated frame bytes into the reading structure.
* @param out Pointer to the reading structure to populate.
* @param bytes Pointer to the five decoded frame bytes.
* @return void No return value.
*/
static void dht11_store_bytes(dht11_data_t *out, const uint8_t *bytes) {
out->humidity_int = bytes[0];
out->humidity_dec = bytes[1];
out->temp_int = bytes[2];
out->temp_dec = bytes[3];
out->checksum = bytes[4];
out->valid = true;
}
/**
* @brief Run the start handshake and capture the five frame bytes.
* @param bytes Output buffer receiving the five decoded bytes.
* @return true when the handshake and capture both succeeded.
*/
static bool dht11_collect_frame(uint8_t *bytes) {
if (!dht11_start_signal()) {
return false;
}
return dht11_capture_bits(bytes);
}
/**
* @brief Issue the start pulse and reconstruct the five sensor bytes.
* @param out Pointer to the reading structure to populate.
* @return true when a valid checksum frame was decoded, false otherwise.
*/
static bool dht11_read_frame(dht11_data_t *out) {
uint8_t bytes[5] = {0};
if (!dht11_collect_frame(bytes)) {
return false;
}
if (dht11_checksum_ok(bytes)) {
dht11_store_bytes(out, bytes);
return true;
}
return false;
}
void dht11_init(void) {
current_reading.humidity_int = 45;
current_reading.humidity_dec = 0;
current_reading.temp_int = 22;
current_reading.temp_dec = 0;
current_reading.checksum = 0;
gpio_init(DHT11_PIN);
gpio_set_dir(DHT11_PIN, GPIO_OUT);
gpio_put(DHT11_PIN, 1);
g_current_reading.valid = false;
}
dht11_data_t* dht11_read(void) {
// Simulated read
return &current_reading;
g_current_reading.valid = false;
if (!dht11_read_frame(&g_current_reading)) {
g_current_reading.valid = false;
}
return &g_current_reading;
}
+78
View File
@@ -0,0 +1,78 @@
/**
* @file gpio_ctrl.c
* @brief GPIO hardware control implementation for Operation Iron Net
* @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 status 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 payload marker pin as a low output.
* @param void No parameters.
* @return void No return value.
*/
static void init_payload_pin(void) {
gpio_init(PAYLOAD_PIN);
gpio_set_dir(PAYLOAD_PIN, GPIO_OUT);
gpio_put(PAYLOAD_PIN, 0);
}
/**
* @brief Initialize the tamper loop pin as a pulled-down input.
* @param void No parameters.
* @return void No return value.
*/
static void init_tamper_pin(void) {
gpio_init(TAMPER_PIN);
gpio_set_dir(TAMPER_PIN, GPIO_IN);
gpio_pull_down(TAMPER_PIN);
}
void init_gpio(void) {
init_led_pins();
init_payload_pin();
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_authorized(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_PIN);
}
void payload_deploy(void) {
gpio_put(PAYLOAD_PIN, 1);
sleep_ms(250);
gpio_put(PAYLOAD_PIN, 0);
}
+175 -17
View File
@@ -10,27 +10,185 @@
#include "logic.h"
#include "dht11.h"
#include "crypto.h"
#include "gpio_ctrl.h"
#include "lora.h"
#include <stdio.h>
#include "pico/stdlib.h"
#include "hardware/gpio.h"
#include <string.h>
#define PAYLOAD_GPIO 15
static volatile const uint8_t s_env_mask = 0xAF;
static volatile const uint8_t s_env_pattern = 0x18;
static uint8_t s_auth_token[TOKEN_LEN + 1];
static uint8_t g_strike_state = STATE_ENV_ARMED;
static uint8_t g_tamper_faults = 0;
static bool g_env_ok = false;
static bool g_authorized = false;
void __noinline evaluate_environment(void) {
dht11_data_t* reading = dht11_read();
// Complex bitwise checksum logic for students to reverse
uint8_t calculated = (reading->humidity_int ^ 0xAF) & 0x3C;
if (calculated == 0x18) {
printf("[*] STRIKE AUTHORIZED. DEPLOYING PAYLOAD.\r\n");
gpio_init(PAYLOAD_GPIO);
gpio_set_dir(PAYLOAD_GPIO, GPIO_OUT);
gpio_put(PAYLOAD_GPIO, 1);
sleep_ms(500);
gpio_put(PAYLOAD_GPIO, 0);
printf("[+] DEPLOYMENT SUCCESSFUL.\r\n");
/**
* @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 IRON NET // SENSOR NODE |\r\n");
printf("| STATUS: ARMED [GP16 RED: ON] LORA RECEIVER: LIVE |\r\n");
printf("+-----------------------------------------------------------------+\r\n");
}
/**
* @brief Print the payload-authorized banner to the console.
* @param void No parameters.
* @return void No return value.
*/
static void print_banner_authorized(void) {
printf("\r\n+-----------------------------------------------------------------+\r\n");
printf("| STRIKE AUTHORIZED // MARKER DEPLOYED [GP17 GREEN: ON] |\r\n");
printf("+-----------------------------------------------------------------+\r\n");
}
/**
* @brief Print the tamper lockout banner to the console.
* @param void No parameters.
* @return void No return value.
*/
static void print_banner_lockout(void) {
printf("\r\n+-----------------------------------------------------------------+\r\n");
printf("| NODE LOCKOUT [GP16 RED: FLASH] |\r\n");
printf("| STATUS: TAMPER INTEGRITY FAULT // REBOOT REQUIRED |\r\n");
printf("+-----------------------------------------------------------------+\r\n");
}
/**
* @brief Copy the low byte of the DHT11 frame into a stored sample.
* @param reading Pointer to the latest validated sensor reading.
* @return true when the bitwise environment gate is satisfied.
*/
static bool env_matches(const dht11_data_t *reading) {
uint8_t calculated = (reading->humidity_int ^ s_env_mask) & 0x3C;
return calculated == s_env_pattern;
}
/**
* @brief Print the sensor frame invalid message and increment the fault counter.
* @param faults Pointer to the tamper fault counter.
* @return void No return value.
*/
static void log_invalid_frame(uint8_t *faults) {
printf("[-] SENSOR FRAME INVALID. FAULT LOGGED.\r\n");
(*faults)++;
}
/**
* @brief Print the sensor reading data to the console.
* @param reading Pointer to the validated sensor reading.
* @return void No return value.
*/
static void log_reading(const dht11_data_t *reading) {
printf("[*] ENV RH %u.%u%% T %u.%uC\r\n", reading->humidity_int,
reading->humidity_dec, reading->temp_int, reading->temp_dec);
}
/**
* @brief Read the sensor and update the environment gate result.
* @param faults Pointer to the tamper fault counter.
* @return void No return value.
*/
static void evaluate_sensor(uint8_t *faults) {
dht11_data_t *reading = dht11_read();
if (!reading->valid) {
log_invalid_frame(faults);
return;
}
log_reading(reading);
if (!g_env_ok) {
g_env_ok = env_matches(reading);
}
}
/**
* @brief Poll the radio and set the authorization flag on a token match.
* @param faults Pointer to the tamper fault counter.
* @return void No return value.
*/
static void evaluate_uplink(uint8_t *faults) {
char pkt[64] = {0};
if (!lora_poll_packet(pkt, sizeof(pkt))) {
return;
}
if (strncmp(pkt, (const char *)s_auth_token, TOKEN_LEN) == 0) {
g_authorized = true;
printf("[+] AUTHORIZATION TOKEN ACCEPTED.\r\n");
} else {
printf("[-] INVALID AUTHORIZATION TOKEN.\r\n");
(*faults)++;
}
}
/**
* @brief Trip the node when fault limits or the tamper loop demand it.
* @param faults Current tamper fault count.
* @return void No return value.
*/
static void check_trap(uint8_t faults) {
if (g_strike_state == STATE_ENV_LOCKOUT) {
return;
}
if (faults >= 3 || !is_tamper_wire_intact()) {
g_strike_state = STATE_ENV_LOCKOUT;
print_banner_lockout();
}
}
/**
* @brief Deploy the payload when the environment and token both pass.
* @param void No parameters.
* @return void No return value.
*/
static void try_deploy(void) {
if (g_strike_state == STATE_ENV_AUTHORIZED) {
return;
}
if (g_env_ok && g_authorized) {
g_strike_state = STATE_ENV_AUTHORIZED;
payload_deploy();
lora_send("STRIKE AUTHORIZED // MARKER DEPLOYED");
print_banner_authorized();
} else {
printf("[-] ENVIRONMENT SUB-OPTIMAL. HOLDING.\r\n");
}
}
/**
* @brief Drive LED feedback for the current strike state.
* @param state Current strike 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_ENV_AUTHORIZED) {
set_led_authorized();
} else if (state == STATE_ENV_LOCKOUT) {
set_led_tamper_flash((tick_idx % 2) == 0);
} else {
set_led_armed();
}
}
void evaluate_environment(void) {
static uint32_t ticks = 0;
if (g_strike_state != STATE_ENV_LOCKOUT) {
evaluate_sensor(&g_tamper_faults);
evaluate_uplink(&g_tamper_faults);
check_trap(g_tamper_faults);
try_deploy();
}
update_feedback(g_strike_state, ticks++);
}
void init_strike_controller(void) {
decrypt_auth_token(s_auth_token);
s_auth_token[TOKEN_LEN] = '\0';
init_lora();
print_banner_armed();
}
+151
View File
@@ -0,0 +1,151 @@
// 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=3\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;
}
+22 -5
View File
@@ -10,19 +10,36 @@
#include <stdio.h>
#include "pico/stdlib.h"
#include "gpio_ctrl.h"
#include "dht11.h"
#include "logic.h"
int main(void) {
/**
* @brief Initialize stdio, GPIO, the sensor, and the strike controller.
* @param void No parameters.
* @return void No return value.
*/
static void init_system(void) {
stdio_init_all();
init_gpio();
dht11_init();
sleep_ms(2000);
printf("SYSTEM BOOT: IRON NET SENSOR NODE ONLINE.\r\n");
init_strike_controller();
}
/**
* @brief Run the main event loop, servicing one sensor tick per iteration.
* @param void No parameters.
* @return void No return value.
*/
static void run_event_loop(void) {
while (true) {
evaluate_environment();
sleep_ms(1500);
}
}
int main(void) {
init_system();
run_event_loop();
return 0;
}