├── KrakenSimmulator ├── .gitignore ├── platform.ini ├── platformio.ini ├── .vscode │ └── extensions.json ├── include │ └── config.h ├── KrakenSim_FSD.md └── src │ └── main.cpp ├── AntennaPositioner ├── .gitignore └── AntennaPositioner-FSD.md ├── .gitignore ├── MorseTestSX1276 └── MorseTestSX1276.ino ├── HornetHunterLoRa_V1 └── HornetHunterLoRa_V1.ino └── README.md /KrakenSimmulator/.gitignore: -------------------------------------------------------------------------------- 1 | .pio 2 | .vscode/.browse.c_cpp.db* 3 | .vscode/c_cpp_properties.json 4 | .vscode/launch.json 5 | .vscode/ipch 6 | -------------------------------------------------------------------------------- /AntennaPositioner/.gitignore: -------------------------------------------------------------------------------- 1 | # Documentation artifacts 2 | /agents.md 3 | /claude.md 4 | /.claude/ 5 | 6 | # Local credentials 7 | /credentials.h 8 | -------------------------------------------------------------------------------- /KrakenSimmulator/platform.ini: -------------------------------------------------------------------------------- 1 | [env:esp32dev] 2 | platform = espressif32 3 | board = esp32dev 4 | framework = arduino 5 | monitor_speed = 115200 6 | build_flags = 7 | -DCORE_DEBUG_LEVEL=0 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore credentials files in any directory 2 | **/credentials.h 3 | credentials.h 4 | 5 | # Ignore Claude Code related files and directories 6 | agents.md 7 | claude.md 8 | .claude/ 9 | -------------------------------------------------------------------------------- /KrakenSimmulator/platformio.ini: -------------------------------------------------------------------------------- 1 | [env:esp32dev] 2 | platform = espressif32 3 | board = esp32dev 4 | framework = arduino 5 | monitor_speed = 115200 6 | build_flags = 7 | -DCORE_DEBUG_LEVEL=0 8 | -------------------------------------------------------------------------------- /KrakenSimmulator/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "platformio.platformio-ide" 6 | ], 7 | "unwantedRecommendations": [ 8 | "ms-vscode.cpptools-extension-pack" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /KrakenSimmulator/include/config.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "credentials.h" 3 | 4 | // ======== HTTP Servers (one port per fake Kraken) ======== 5 | #define HTTP_PORT_A 8081 6 | #define HTTP_PORT_B 8082 7 | 8 | // ======== Station Identity & Position ======== 9 | #define STATION_ID_A "FAKE1" 10 | #define STATION_LAT_A 47.474242 11 | #define STATION_LON_A 7.765962 12 | #define STATION_ALT_A_M 400.0 13 | 14 | #define STATION_ID_B "FAKE2" 15 | // Explicitly set B to ~100 m east of A at lat 47.474242 16 | #define STATION_LAT_B 47.474242 17 | #define STATION_LON_B 7.767291 // computed ≈ A + 0.001329° 18 | #define STATION_ALT_B_M 400.0 19 | // (Auto-place by separation not used in this HTTP build) 20 | 21 | // ======== Moving Object (independent path) ======== 22 | #define OBJ_START_LAT 47.474904 23 | #define OBJ_START_LON 7.766416 24 | #define OBJ_END_LAT 47.473120 25 | #define OBJ_END_LON 7.766545 26 | 27 | // ======== Motion & cadence ======== 28 | #define SPEED_MPS 6.0 // hornet ground speed (m/s) 29 | #define BURST_PERIOD_S 1.0 // 1 Hz updates 30 | #define BURST_JITTER_MS 20.0 // ±ms jitter 31 | 32 | // End-of-path behavior: 0=stop, 1=hold last point, 2=loop to start 33 | #define ON_REACH_END 0 34 | 35 | // ======== Message / Spectrum ======== 36 | #define CENTER_FREQ_HZ 148524000 37 | #define ARRAY_TYPE "ULA" 38 | // Kraken App CSV typically uses 360 unit-circle bins: 39 | #define N_BINS 360 40 | // If your receiver expects 181, change the line above to: // #define N_BINS 181 41 | #define BACKGROUND_LEVEL 0.05f 42 | 43 | // ======== Models (width & RSSI) ======== 44 | // width(rad) = BASE_WIDTH_RAD + K_WIDTH_RAD_PER_M * distance_m 45 | #define BASE_WIDTH_RAD 0.15 46 | #define K_WIDTH_RAD_PER_M 0.004 47 | // RSSI(dBFS) ~ RSSI_REF_DB_AT_1M - 20*log10(distance_m) + noise 48 | #define RSSI_REF_DB_AT_1M -30.0 49 | #define RSSI_NOISE_DB 2.0 50 | #define PEAK_SCALE_DIV 20.0 51 | 52 | // ======== Realism Tweaks ======== 53 | #define POSITION_JITTER_M 0.0 // ±meters per tick 54 | #define TIMESTAMP_JITTER_MS 0.0 // extra ±ms on top of BURST_JITTER_MS 55 | 56 | // ======== Formatting / CSV compatibility ======== 57 | #define BEARING_DECIMALS 0 58 | #define WIDTH_DECIMALS 6 59 | #define RSSI_DECIMALS 2 60 | #define LATLON_DECIMALS 6 61 | #define ALT_DECIMALS 1 62 | #define SPEC_DECIMALS 2 63 | -------------------------------------------------------------------------------- /MorseTestSX1276/MorseTestSX1276.ino: -------------------------------------------------------------------------------- 1 | /* 2 | RadioLib SX127x Morse Receive AM Example 3 | 4 | This example receives Morse code message using 5 | SX1278's FSK modem. The signal is expected to be 6 | modulated as OOK, to be demodulated in AM mode. 7 | 8 | Other modules that can be used for Morse Code 9 | with AFSK modulation: 10 | - SX127x/RFM9x 11 | - RF69 12 | - SX1231 13 | - CC1101 14 | - Si443x/RFM2x 15 | 16 | For default module settings, see the wiki page 17 | https://github.com/jgromes/RadioLib/wiki/Default-configuration 18 | 19 | For full API reference, see the GitHub Pages 20 | https://jgromes.github.io/RadioLib/ 21 | */ 22 | 23 | // include the library 24 | #include 25 | 26 | // ESP32 Pins (your wiring) 27 | #define LORA_NSS 4 // Blue 28 | #define LORA_RESET 3 // White 29 | #define LORA_DIO0 6 // Grey 30 | #define LORA_SCK 5 // Orange 31 | #define LORA_MISO 7 // Green 32 | #define LORA_MOSI 10 // Yellow 33 | 34 | SPIClass* customSPI = new SPIClass(FSPI); 35 | SX1276 radio = new Module( 36 | LORA_NSS, 37 | LORA_DIO0, 38 | LORA_RESET, 39 | RADIOLIB_NC, 40 | *customSPI); 41 | 42 | 43 | // create AFSK client instance using the FSK module 44 | // pin 5 is connected to SX1278 DIO2 45 | AFSKClient audio(&radio, 5); 46 | 47 | // create Morse client instance using the AFSK instance 48 | MorseClient morse(&audio); 49 | 50 | void setup() { 51 | Serial.begin(115200); 52 | delay(1000); 53 | 54 | customSPI->begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_NSS); 55 | 56 | // hardware reset 57 | pinMode(LORA_RESET, OUTPUT); 58 | digitalWrite(LORA_RESET, LOW); 59 | delay(100); 60 | digitalWrite(LORA_RESET, HIGH); 61 | delay(100); 62 | 63 | // initialize SX1278 with default settings 64 | Serial.print(F("[SX1278] Initializing ... ")); 65 | int state = radio.beginFSK(148.577); 66 | if (state == RADIOLIB_ERR_NONE) { 67 | Serial.println(F("success!")); 68 | } else { 69 | Serial.print(F("failed, code ")); 70 | Serial.println(state); 71 | while (true) { delay(10); } 72 | } 73 | 74 | // when using one of the non-LoRa modules for Morse code 75 | // (RF69, CC1101, Si4432 etc.), use the basic begin() method 76 | // int state = radio.begin(); 77 | 78 | // initialize Morse client 79 | Serial.print(F("[Morse] Initializing ... ")); 80 | // AFSK tone frequency: 400 Hz 81 | // speed: 20 words per minute 82 | state = morse.begin(400); 83 | if (state == RADIOLIB_ERR_NONE) { 84 | Serial.println(F("success!")); 85 | } else { 86 | Serial.print(F("failed, code ")); 87 | Serial.println(state); 88 | while (true) { delay(10); } 89 | } 90 | 91 | // after that, set mode to OOK to emulate AM modulation 92 | Serial.print(F("[SX1278] Switching to OOK ... ")); 93 | state = radio.setOOK(true); 94 | if (state == RADIOLIB_ERR_NONE) { 95 | Serial.println(F("success!")); 96 | } else { 97 | Serial.print(F("failed, code ")); 98 | Serial.println(state); 99 | while (true) { delay(10); } 100 | } 101 | 102 | // start direct mode reception 103 | radio.receiveDirect(); 104 | } 105 | 106 | // save symbol and length between loops 107 | byte symbol = 0; 108 | byte len = 0; 109 | 110 | void loop() { 111 | // try to read a new symbol 112 | int state = morse.read(&symbol, &len); 113 | 114 | // check if we have something to decode 115 | if (state != RADIOLIB_MORSE_INTER_SYMBOL) { 116 | // decode and print 117 | Serial.print(MorseClient::decode(symbol, len)); 118 | 119 | // reset the symbol buffer 120 | symbol = 0; 121 | len = 0; 122 | 123 | // check if we have a complete word 124 | if (state == RADIOLIB_MORSE_WORD_COMPLETE) { 125 | // inter-word space, interpret that as a new line 126 | Serial.println(); 127 | } 128 | } 129 | } -------------------------------------------------------------------------------- /HornetHunterLoRa_V1/HornetHunterLoRa_V1.ino: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | // ESP32 Pins (your wiring) 5 | #define LORA_NSS 4 // Blue 6 | #define LORA_RESET 3 // White 7 | #define LORA_DIO0 6 // Grey 8 | #define LORA_SCK 5 // Orange 9 | #define LORA_MISO 7 // Green 10 | #define LORA_MOSI 10 // Yellow 11 | 12 | SPIClass* customSPI = new SPIClass(FSPI); 13 | SX1276 radio = new Module( 14 | LORA_NSS, 15 | LORA_DIO0, 16 | LORA_RESET, 17 | RADIOLIB_NC, 18 | *customSPI); 19 | 20 | // bit rate is fixed here; used to compute max freqDev 21 | const float BIT_RATE_KBPS = 4.8; // [kbps] 22 | // your “nominal” deviation you’d like to use if the constraints allow 23 | const float NOMINAL_DEV_KHZ = 5.0; // [kHz] 24 | 25 | // all allowed FSK rxBw values, descending 26 | const float RXBW_LIST_KHZ[] = { 27 | 250.0, 200.0, 166.7, 125.0, 100.0, 83.3, 62.5, 50.0, 28 | 41.7, 31.3, 25.0, 20.8, 15.6, 12.5, 10.4, 7.8, 29 | 6.3, 5.2, 3.9, 3.1, 2.6 30 | }; 31 | const int NUM_RXBW = sizeof(RXBW_LIST_KHZ) / sizeof(RXBW_LIST_KHZ[0]); 32 | 33 | int bwIndex = 0; 34 | unsigned long lastStepMs = 0; 35 | 36 | void setup() { 37 | Serial.begin(115200); 38 | while (!Serial) 39 | ; 40 | 41 | customSPI->begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_NSS); 42 | 43 | // hardware reset 44 | pinMode(LORA_RESET, OUTPUT); 45 | digitalWrite(LORA_RESET, LOW); 46 | delay(100); 47 | digitalWrite(LORA_RESET, HIGH); 48 | delay(100); 49 | 50 | // initialize FSK at your freq, bit rate, and a safe “starting” deviation 51 | int state = radio.beginFSK( 52 | 868.0, // frequency 53 | 1.2, // bitrate 54 | 1.2, // freq deviation 55 | 5.0, // RX bandwidth 56 | 10, // preamble length 57 | 3, // sync word length 58 | { 0xC1,0x94,0xC1}, // a 3-byte sync word 59 | RADIOLIB_FSK_SHAPING_NONE, 60 | true // CRC on 61 | ); 62 | if (st != RADIOLIB_ERR_NONE) { 63 | Serial.print("FSK init failed, code "); 64 | Serial.println(st); 65 | while (true) 66 | ; 67 | } 68 | Serial.println("FSK ready for CW RSSI"); 69 | 70 | // disable packet engine 71 | //radio.setSyncWord(nullptr, 0); 72 | //radio.setPreambleLength(0); 73 | //radio.setCrcFiltering(false); 74 | 75 | // smoothing 76 | //radio.setRSSIConfig(8, 0); 77 | 78 | // start continuous receive so RSSI register updates 79 | st = radio.startReceive(); 80 | if (st != RADIOLIB_ERR_NONE) { 81 | Serial.print("startReceive failed, code "); 82 | Serial.println(st); 83 | while (true) 84 | ; 85 | } 86 | 87 | // timestamp our first step 88 | lastStepMs = millis(); 89 | bwIndex = 0; 90 | } 91 | 92 | void loop() { 93 | // every 30 s, step to the next smaller rxBw 94 | if (millis() - lastStepMs >= 5000) { 95 | lastStepMs = millis(); 96 | bwIndex = (bwIndex + 1) % NUM_RXBW; 97 | float newBw = RXBW_LIST_KHZ[bwIndex]; 98 | 99 | // apply new Rx bandwidth 100 | //radio.setRxBandwidth(newBw); // sets receiver bandwidth 101 | //radio.setAFCBandwidth(newBw); // keep AFC in sync 102 | 103 | // recompute freqDev so: freqDev ≤ 200 kHz, freqDev + br/2 ≤ 250 kHz 104 | float maxDevByBr = 250.0f - (BIT_RATE_KBPS / 2.0f); 105 | float chosenDev = NOMINAL_DEV_KHZ; 106 | if (chosenDev > maxDevByBr) { chosenDev = maxDevByBr; } 107 | if (chosenDev > 200.0f) { chosenDev = 200.0f; } 108 | if (chosenDev < 0.6f) { chosenDev = 0.6f; } 109 | // radio.setFrequencyDeviation(chosenDev); 110 | // radio.startReceive(); 111 | 112 | Serial.print("→ Switched to rxBw="); 113 | Serial.print(newBw, 1); 114 | Serial.print(" kHz, freqDev="); 115 | Serial.print(chosenDev, 1); 116 | Serial.println(" kHz"); 117 | } 118 | 119 | // just print the live RSSI every loop 120 | float rssi = radio.getRSSI(); 121 | if (rssi > -90) { 122 | Serial.print("CW RSSI: "); 123 | Serial.print(rssi, 1); 124 | Serial.println(" dBm"); 125 | } 126 | 127 | delay(20); 128 | } 129 | -------------------------------------------------------------------------------- /KrakenSimmulator/KrakenSim_FSD.md: -------------------------------------------------------------------------------- 1 | # KrakenSim Functional Specification (FSD) 2 | 3 | ## 0. Purpose & Scope 4 | Provide a deterministic ESP32-based simulator that mimics two KrakenSDR receivers by serving Kraken-style direction-of-arrival (DoA) CSV messages over HTTP. Downstream tooling should be able to swap between these simulated devices and real hardware by only changing the device IP/ports. The moving target path is independent of station placement. 5 | 6 | ## 1. Objectives and Non-Goals 7 | **Objectives** 8 | - Host two independent HTTP listeners (Station A and Station B) that refresh a Kraken CSV payload once per update tick. 9 | - Expose a lightweight JSON status endpoint for each station with the latest bearing and RSSI metadata. 10 | - Keep the simulator deterministic unless jitter knobs are enabled. 11 | 12 | **Non-Goals** 13 | - Streaming over UDP or MQTT. 14 | - Implementing the full Kraken API surface (only `/`, `/DOA_value.html`, `/status.json` are provided). 15 | - Providing UI, TLS termination, or authentication. 16 | 17 | ## 2. High-Level Behavior 18 | - A single moving object travels linearly from `OBJ_START_(LAT/LON)` to `OBJ_END_(LAT/LON)`; the path is sampled every `BURST_PERIOD_S` seconds (default 1 Hz). 19 | - Each tick recomputes the bearing, width, RSSI, and spectrum for both stations and caches the formatted CSV line (`lastCsvA`, `lastCsvB`). 20 | - HTTP handlers read the cached payloads; no per-request recomputation occurs. 21 | - End-of-path handling obeys `ON_REACH_END`: stop, hold, or loop to the start. 22 | 23 | ## 3. External Interfaces 24 | - **Transport:** Two HTTP/1.1 servers bound to `HTTP_PORT_A` and `HTTP_PORT_B` on `WiFi.localIP()`. 25 | - **Routes:** 26 | - `/` -> plaintext help string. 27 | - `/DOA_value.html` -> latest Kraken CSV line (`text/html` response, but body is CSV). 28 | - `/status.json` -> JSON snapshot `{id, lat, lon, bearing, rssi}` for quick diagnostics. 29 | - **CSV field order:** `timestamp_ms`, `bearing_deg`, `confidence_pct`, `rssi_dbfs`, `center_freq_hz`, `array_type`, `latency_ms`, `station_id`, `station_lat`, `station_lon`, `gps_heading_deg`, `compass_heading_deg`, `gps_source`, four reserved zeros, followed by `N_BINS` spectrum amplitudes. 30 | - **Formatting:** bearing is emitted as an integer; confidence uses one decimal place; RSSI uses `RSSI_DECIMALS`; latitude/longitude use `LATLON_DECIMALS`; spectrum bins use `SPEC_DECIMALS`. 31 | - **Client example:** `curl http://192.168.0.175:8081/DOA_value.html` (Station A), `curl http://192.168.0.175:8082/status.json` (Station B). 32 | 33 | ## 4. Configuration & Inputs 34 | - Credentials supplied via `include/credentials.h` (`WIFI_SSID`, `WIFI_PASS`). 35 | - Station macros in `config.h`: `STATION_ID_*`, `STATION_LAT_*`, `STATION_LON_*`, `STATION_ALT_*`, `HTTP_PORT_*`. 36 | - Path and motion: `OBJ_START_*`, `OBJ_END_*`, `SPEED_MPS`, `BURST_PERIOD_S`, `BURST_JITTER_MS`, `ON_REACH_END`. 37 | - Signal/spectrum: `CENTER_FREQ_HZ`, `ARRAY_TYPE`, `N_BINS`, `BACKGROUND_LEVEL`, plus model tuning (`BASE_WIDTH_RAD`, `K_WIDTH_RAD_PER_M`, `RSSI_REF_DB_AT_1M`, `RSSI_NOISE_DB`, `PEAK_SCALE_DIV`). 38 | - Formatting knobs: `BEARING_DECIMALS`, `WIDTH_DECIMALS`, `RSSI_DECIMALS`, `LATLON_DECIMALS`, `SPEC_DECIMALS`. 39 | 40 | ## 5. Algorithms 41 | - Motion via linear interpolation of start/end lat/lon with fraction `u = clamp(elapsed / travelTime)`. 42 | - Distance calculated with the haversine formula; bearing uses the great-circle initial bearing equation. 43 | - Width model: `BASE_WIDTH_RAD + K_WIDTH_RAD_PER_M * distance_m`. 44 | - RSSI model: free-space path loss with bounded noise, clamped to [-120, -10] dBFS. 45 | - Spectrum: `N_BINS` Gaussian bump centered at the compass bearing, mapped to the unit circle (90° offset) with background jitter. 46 | - CSV builder outputs timestamp, integer bearing, confidence, RSSI, frequency, array metadata, headings, channel flags, and spectrum bins with configured precision. 47 | 48 | ## 6. Process & Timing 49 | - `setup()` connects Wi-Fi, computes path metrics, and registers HTTP handlers before starting both servers. 50 | - `loop()` services HTTP clients, then on each scheduled tick updates the cached CSV strings for both stations. Optional jitter perturbs the next tick time. 51 | - When the object reaches its destination, the behavior follows the configured mode (stop/hold/loop). 52 | 53 | ## 7. Logging & Diagnostics 54 | - Serial output reports Wi-Fi connection status, HTTP port bindings, and endpoint URLs. 55 | - `/status.json` can be polled to monitor bearings and RSSI without parsing the full CSV. 56 | -------------------------------------------------------------------------------- /AntennaPositioner/AntennaPositioner-FSD.md: -------------------------------------------------------------------------------- 1 | # Antenna Positioner FSD 2 | 3 | ## 1. Project Overview 4 | The Antenna Positioner is an ESP32-S3–based controller that measures its heading relative to geographic north, tracks its own position, and exposes that data over Wi-Fi. The device must present both real-time telemetry and a simple control/diagnostic surface for companion tools. Firmware is built with PlatformIO using the Arduino framework and must support OTA updates to avoid physical access during field deployments. 5 | 6 | ## 2. Goals & Success Metrics 7 | - Continuously estimate yaw/pitch/roll and absolute heading with ±2° accuracy while reporting GPS-derived position (lat/lon/alt) within standard GNSS tolerances. 8 | - Provide Wi-Fi connectivity that serves JSON REST endpoints and an optional WebSocket stream with ≤500 ms latency between sensor capture and network publication. 9 | - Deliver OTA updates over HTTPS (preferred) or authenticated HTTP, completing within 2 minutes on a standard 802.11n link. 10 | - Achieve ≥95% uptime during 24-hour bench tests without watchdog resets. 11 | 12 | ## 3. Out-of-Scope 13 | - Mechanical actuation (motors, servos) control logic. 14 | - Cloud-side data storage or dashboards. 15 | - Cellular or LoRa backhaul. 16 | 17 | ## 4. Hardware & Interfaces 18 | - **MCU**: ESP32-S3 Super Mini (Wi-Fi + BLE). 19 | - **Orientation sensor**: 9-DoF IMU with magnetometer (e.g., BNO055 or ICM-20948 + QMC5883L) on I²C. 20 | - **Position sensor**: GNSS receiver (UART) capable of NMEA sentences. 21 | - **Status indicators**: Two LEDs (power, Wi-Fi/OTA) and one momentary button for provisioning/reset. 22 | - **Power**: 5V USB-C with optional LiPo + charger module (telemetry only). 23 | 24 | ## 5. Software Architecture 25 | - `src/main.cpp` bootstraps PlatformIO app, starts event loop, and initializes subsystems. 26 | - Submodules: 27 | - `sensors`: drivers, calibration routines, sensor fusion (Madgwick or Mahony filter). 28 | - `network`: Wi-Fi manager, captive portal fallback, MDNS advertisement. 29 | - `services`: REST/WebSocket handlers and OTA endpoints. 30 | - `storage`: preferences (NVS) for Wi-Fi credentials, calibration constants, OTA auth token. 31 | - FreeRTOS tasks: 32 | - Sensor Task (100 Hz) for IMU acquisition and fusion. 33 | - GNSS Task (5 Hz) for parsing NMEA sentences. 34 | - Telemetry Task (5–10 Hz) pushing latest state to queues. 35 | - Web Service Task handling HTTP/WS requests. 36 | 37 | ## 6. Functional Requirements 38 | 1. **Startup & Provisioning** 39 | - On first boot, enter AP mode (`AntennaPositioner-XXXX`) and serve a captive portal at `192.168.4.1` for Wi-Fi credential input. 40 | - Persist credentials securely (NVS) and reboot into station mode. 41 | 2. **Sensor Acquisition** 42 | - Calibrate magnetometer using stored offsets; provide API endpoint to trigger recalibration. 43 | - Fuse accelerometer and gyroscope with magnetometer data to compute heading relative to true north (optionally adjust using GNSS course over ground). 44 | - Validate sensor health and expose self-test status. 45 | 3. **Position Tracking** 46 | - Parse GPS data for latitude, longitude, altitude, speed, and UTC time. 47 | - Maintain last fix timestamp; mark data stale if no fix within 5 seconds. 48 | 4. **Telemetry Service** 49 | - Serve `GET /api/v1/status` returning JSON with heading (deg), orientation quaternion, lat/lon, altitude, velocity, fix age, and firmware metadata. 50 | - Offer `GET /api/v1/stream` WebSocket pushing incremental updates. 51 | - Provide `GET /api/v1/health` with component status and uptime. 52 | 5. **OTA Updates** 53 | - Implement `POST /ota` authenticated via pre-shared token or HTTP basic auth. 54 | - Validate digital signature or checksum before flashing. 55 | - Roll back on failed update; store last known good firmware slot. 56 | 6. **Diagnostics** 57 | - Serial console commands for sensor calibration, Wi-Fi reset, and debug metrics. 58 | - LED patterns signalling Wi-Fi state, OTA in progress, and error conditions. 59 | 60 | ## 7. Configuration & Security 61 | - Store secrets in `include/secrets.h` (ignored by git); template lives in `include/secrets_template.h`. 62 | - Support WPA2 networks, optional fallback to WPA3 where firmware & SDK allow. 63 | - Rate-limit REST requests and require token for mutating actions (calibration, OTA). 64 | 65 | ## 8. Telemetry & Logging 66 | - Use ESP-IDF logging macros wrapped for Arduino to provide leveled logs. 67 | - Buffer last 100 events in RAM for retrieval via `GET /api/v1/logs`. 68 | - Add persistent crash counters and reason codes in NVS for post-mortem analysis. 69 | 70 | ## 9. OTA & Deployment Workflow 71 | - Build via `pio run -e esp32c3-devkit`. 72 | - Upload via USB during development (`pio run -t upload`). OTA uses `pio run -t upload --upload-port http:///ota --upload-port-arg token=`. 73 | - Maintain semantic versioning in `include/version.h`; OTA rejects downgrades unless `force=true`. 74 | 75 | ## 10. Testing & Validation 76 | - Unit tests: sensor fusion math mocked with recorded IMU traces in `test/sensors`. 77 | - HIL tests: bench script verifying heading accuracy via turntable; record actual vs reported heading. 78 | - Network tests: integration harness hitting REST endpoints and validating JSON schema. 79 | - OTA tests: scripted update applying intentionally corrupted image to confirm rollback and error reporting. 80 | 81 | ## 11. Open Questions 82 | - Final sensor selection (exact IMU/GNSS models). 83 | - Whether HTTPS is mandatory (certificate provisioning complexity). 84 | - Need for BLE fallback for provisioning or telemetry. 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🐝 HornetHunter 2 | 3 | > Advanced Radio Direction Finding System for Tracking Invasive Hornets 4 | 5 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) 6 | [![Platform](https://img.shields.io/badge/platform-ESP32--S3-green.svg)](https://www.espressif.com/) 7 | [![Build](https://img.shields.io/badge/build-PlatformIO-orange.svg)](https://platformio.org/) 8 | 9 | ## 📋 Overview 10 | 11 | HornetHunter is a comprehensive radio direction finding (RDF) system designed to track and locate invasive hornets equipped with radio transmitters. The project combines precision antenna positioning, KrakenSDR-based direction-of-arrival (DoA) measurement, and simulation tools to enable effective tracking and eradication of invasive hornet populations. 12 | 13 | The system uses multiple ground stations to triangulate transmitter positions, with each station featuring: 14 | - **Precision orientation tracking** using IMU and GNSS sensors 15 | - **Real-time telemetry** over Wi-Fi networks 16 | - **Direction-of-arrival measurements** from KrakenSDR hardware 17 | - **Simulation capabilities** for testing and development 18 | 19 | --- 20 | 21 | ## 🗂️ Project Structure 22 | 23 | ``` 24 | HornetHunter/ 25 | ├── AntennaPositioner/ # ESP32-S3 antenna orientation controller 26 | ├── KrakenSimmulator/ # Dual KrakenSDR DoA endpoint simulator 27 | └── README.md # This file 28 | ``` 29 | 30 | --- 31 | 32 | ## 📡 AntennaPositioner 33 | 34 | ### Overview 35 | The AntennaPositioner is an ESP32-S3-based controller that continuously measures antenna heading, pitch, roll, and GPS position, exposing real-time telemetry over Wi-Fi for integration with direction-finding systems. 36 | 37 | ### ✨ Key Features 38 | - 🧭 **Precision Orientation**: 9-DoF IMU with magnetometer fusion (±2° accuracy) 39 | - 📍 **GPS Positioning**: Real-time latitude, longitude, altitude tracking 40 | - 🌐 **Wi-Fi Connectivity**: REST API + WebSocket streaming (≤500ms latency) 41 | - 🔄 **OTA Updates**: Secure over-the-air firmware updates 42 | - 🛡️ **High Reliability**: ≥95% uptime with watchdog protection 43 | 44 | ### 🔧 Hardware Requirements 45 | - **MCU**: ESP32-S3 Super Mini (Wi-Fi + BLE) 46 | - **IMU**: 9-DoF sensor (BNO055, ICM-20948 + QMC5883L) 47 | - **GNSS**: UART-based GPS receiver (NMEA) 48 | - **Power**: 5V USB-C with optional LiPo battery 49 | - **Indicators**: Status LEDs and provisioning button 50 | 51 | ### 🚀 Quick Start 52 | ```bash 53 | cd AntennaPositioner 54 | pio run -e esp32s3-devkit 55 | pio run -t upload 56 | ``` 57 | 58 | ### 📊 API Endpoints 59 | - `GET /api/v1/status` - Current heading, orientation, position, and velocity 60 | - `GET /api/v1/stream` - WebSocket live telemetry stream 61 | - `GET /api/v1/health` - System health and uptime 62 | - `POST /ota` - Authenticated OTA firmware update 63 | 64 | ### 🔐 Configuration 65 | 1. Copy `include/secrets_template.h` to `include/secrets.h` 66 | 2. Configure Wi-Fi credentials and OTA token 67 | 3. On first boot, device creates AP `AntennaPositioner-XXXX` for provisioning 68 | 69 | --- 70 | 71 | ## 🛰️ KrakenSimulator 72 | 73 | ### Overview 74 | KrakenSimulator is a deterministic dual-endpoint HTTP server that emulates two independent KrakenSDR direction-of-arrival systems. It simulates a moving target along a configurable path, allowing downstream software to be developed and tested without physical hardware. 75 | 76 | ### ✨ Key Features 77 | - 🎯 **Dual Endpoints**: Two independent KrakenSDR stations on separate ports 78 | - 📈 **Realistic Models**: Bearing, RSSI, width, and spectrum simulation 79 | - ⏱️ **Configurable Motion**: Linear object path with adjustable speed 80 | - 🔄 **Real-time Updates**: 5-10 Hz sample rate with history buffers 81 | - 🧪 **Drop-in Replacement**: JSON format matches real KrakenSDR API 82 | 83 | ### 🔧 Requirements 84 | - **Platform**: ESP32-S3 (or any platform with HTTP server capability) 85 | - **Network**: HTTP server on two configurable ports 86 | - **Memory**: Sufficient for history buffers and spectrum arrays 87 | 88 | ### 🚀 Quick Start 89 | ```bash 90 | cd KrakenSimmulator 91 | pio run -e esp32s3-devkit 92 | pio run -t upload 93 | ``` 94 | 95 | ### 📊 API Endpoints 96 | - `GET /api/v1/doa` - Latest direction-of-arrival measurement 97 | - `GET /api/v1/doa?history=N` - Last N samples 98 | - `GET /api/v1/doa?format=csv` - CSV format output 99 | - `GET /api/v1/metadata` - Station configuration and constants 100 | - `GET /healthz` - Health check endpoint 101 | 102 | ### ⚙️ Configuration 103 | Edit `include/config.h` to set: 104 | - **Network**: `HTTP_PORT_A` (8081), `HTTP_PORT_B` (8082) 105 | - **Stations**: Station IDs, coordinates, altitudes 106 | - **Object Path**: Start/end positions and duration 107 | - **Signal**: Center frequency, array type, update rate 108 | - **Models**: RSSI reference, width parameters, spectrum bins 109 | 110 | ### 📋 Example Response 111 | ```json 112 | { 113 | "timestamp_ms": 1713206400123, 114 | "sequence": 42, 115 | "station_id": "FAKE1", 116 | "station": { 117 | "lat": 47.474242, 118 | "lon": 7.765962, 119 | "alt_m": 400.0 120 | }, 121 | "bearing_deg": 123.45, 122 | "width_rad": 0.213456, 123 | "rssi_dbfs": -47.12, 124 | "center_freq_hz": 148524000, 125 | "array_type": "ULA", 126 | "speed_mps": 6.0, 127 | "spectrum": [0.08, 0.10, 0.12, "..."] 128 | } 129 | ``` 130 | 131 | --- 132 | 133 | ## 🎯 Use Cases 134 | 135 | ### Field Deployment 136 | 1. Deploy multiple AntennaPositioner units at known locations 137 | 2. Connect each to a KrakenSDR direction-finding system 138 | 3. Collect bearing measurements from multiple stations 139 | 4. Triangulate transmitter position using multi-station data 140 | 141 | ### Development & Testing 142 | 1. Run KrakenSimulator to generate synthetic DoA measurements 143 | 2. Test tracking algorithms without physical hardware 144 | 3. Validate triangulation logic with known object paths 145 | 4. Stress-test client software with configurable scenarios 146 | 147 | --- 148 | 149 | ## 🛠️ Development 150 | 151 | ### Prerequisites 152 | - [PlatformIO](https://platformio.org/) installed 153 | - ESP32-S3 development board 154 | - USB cable for programming 155 | 156 | ### Building 157 | ```bash 158 | # Build all projects 159 | pio run 160 | 161 | # Build specific project 162 | cd AntennaPositioner && pio run 163 | cd KrakenSimmulator && pio run 164 | ``` 165 | 166 | ### Testing 167 | Each sub-project includes its own testing framework: 168 | - **Unit tests**: Sensor fusion algorithms and network handlers 169 | - **Integration tests**: Hardware-in-the-loop validation 170 | - **Network tests**: API endpoint schema validation 171 | 172 | --- 173 | 174 | ## 📚 Documentation 175 | 176 | Detailed functional specifications for each component: 177 | - [AntennaPositioner FSD](AntennaPositioner/AntennaPositioner-FSD.md) 178 | - [KrakenSimulator FSD](KrakenSimmulator/KrakenSim_FSD.md) 179 | 180 | --- 181 | 182 | ## 🤝 Contributing 183 | 184 | Contributions are welcome! Please ensure: 185 | - Code follows existing style conventions 186 | - Changes include appropriate tests 187 | - Documentation is updated for new features 188 | - Commit messages are clear and descriptive 189 | 190 | --- 191 | 192 | ## 📄 License 193 | 194 | This project is licensed under the MIT License - see the LICENSE file for details. 195 | 196 | --- 197 | 198 | ## 🔗 Related Projects 199 | 200 | - [KrakenSDR](https://www.krakenrf.com/) - 5-channel coherent radio direction finder 201 | - [PlatformIO](https://platformio.org/) - Cross-platform embedded development 202 | - [ESP-IDF](https://github.com/espressif/esp-idf) - Espressif IoT Development Framework 203 | 204 | --- 205 | 206 | ## 📞 Support 207 | 208 | For issues, questions, or contributions, please open an issue on the GitHub repository. 209 | 210 | --- 211 | 212 | **Built with dedication to protecting ecosystems from invasive species** 🌍🐝 213 | -------------------------------------------------------------------------------- /KrakenSimmulator/src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include "config.h" 6 | 7 | // -------- Utilities -------- 8 | static inline double deg2rad(double d){ return d * M_PI / 180.0; } 9 | static inline double rad2deg(double r){ return r * 180.0 / M_PI; } 10 | 11 | static double haversine_m(double lat1, double lon1, double lat2, double lon2) { 12 | const double R = 6371000.0; 13 | const double dlat = deg2rad(lat2 - lat1); 14 | const double dlon = deg2rad(lon2 - lon1); 15 | const double a = sin(dlat/2)*sin(dlat/2) + 16 | cos(deg2rad(lat1))*cos(deg2rad(lat2)) * sin(dlon/2)*sin(dlon/2); 17 | return R * 2 * atan2(sqrt(a), sqrt(1 - a)); 18 | } 19 | 20 | static double initial_bearing_deg(double lat1, double lon1, double lat2, double lon2) { 21 | double lat1r = deg2rad(lat1), lat2r = deg2rad(lat2); 22 | double dlonr = deg2rad(lon2 - lon1); 23 | double y = sin(dlonr) * cos(lat2r); 24 | double x = cos(lat1r)*sin(lat2r) - sin(lat1r)*cos(lat2r)*cos(dlonr); 25 | double br = rad2deg(atan2(y, x)); 26 | br = fmod((br + 360.0), 360.0); 27 | return br; 28 | } 29 | 30 | static void deg_per_meter(double lat_deg, double °LatPerM, double °LonPerM) { 31 | degLatPerM = 1.0 / 111320.0; 32 | degLonPerM = 1.0 / (111320.0 * cos(deg2rad(lat_deg))); 33 | } 34 | 35 | static void appendCSV(String &s, const String &f){ if(s.length()) s += ","; s += f; } 36 | static void appendCSVf(String &s, double v, uint8_t dp){ 37 | if (s.length()) s += ","; 38 | s += String(v, static_cast(dp)); 39 | } 40 | 41 | // -------- State -------- 42 | WebServer serverA(HTTP_PORT_A); 43 | WebServer serverB(HTTP_PORT_B); 44 | 45 | struct Station { 46 | const char* id; 47 | double lat, lon, alt; 48 | uint32_t seq; 49 | double bearing_deg; 50 | double width_rad; 51 | double rssi_db; 52 | }; 53 | 54 | Station A = { STATION_ID_A, STATION_LAT_A, STATION_LON_A, STATION_ALT_A_M, 1, 0, 0, 0 }; 55 | Station B = { STATION_ID_B, STATION_LAT_B, STATION_LON_B, STATION_ALT_B_M, 1, 0, 0, 0 }; 56 | 57 | // Moving object 58 | const double objStartLat = OBJ_START_LAT; 59 | const double objStartLon = OBJ_START_LON; 60 | const double objEndLat = OBJ_END_LAT; 61 | const double objEndLon = OBJ_END_LON; 62 | 63 | // Timing 64 | unsigned long simStartMs = 0; 65 | double pathLengthM = 0.0; 66 | double travelTimeS = 0.0; 67 | unsigned long nextTickMs = 0; 68 | 69 | // Latest CSV lines (one per 'Kraken') 70 | String lastCsvA; 71 | String lastCsvB; 72 | 73 | // -------- Models -------- 74 | static double widthModel(double distanceM) { 75 | return BASE_WIDTH_RAD + K_WIDTH_RAD_PER_M * distanceM; 76 | } 77 | static double rssiModel(double distanceM) { 78 | if (distanceM < 1.0) distanceM = 1.0; 79 | double noise = ((double)esp_random() / (double)UINT32_MAX) * (2.0*RSSI_NOISE_DB) - RSSI_NOISE_DB; 80 | double rssi = RSSI_REF_DB_AT_1M - 20.0 * log10(distanceM) + noise; 81 | if (rssi < -120.0) rssi = -120.0; 82 | if (rssi > -10.0) rssi = -10.0; 83 | return rssi; 84 | } 85 | static double peakFromRSSI(double rssi) { 86 | double p = -rssi / PEAK_SCALE_DIV; 87 | if (p < 0.1) p = 0.1; 88 | return p; 89 | } 90 | 91 | // 360-bin unit-circle spectrum centered at bearing (compass) mapped to unit-circle 92 | static void makeSpectrum(float *out, int nBins, double bearingCompassDeg, double widthRad, double peak, float bg) { 93 | double unitCenter = fmod(90.0 - bearingCompassDeg + 360.0, 360.0); 94 | double sigmaDeg = (widthRad * 180.0 / M_PI) / 2.0; 95 | if (sigmaDeg < 0.5) sigmaDeg = 0.5; 96 | for (int d = 0; d < nBins; ++d) { 97 | double delta = fabs(d - unitCenter); 98 | if (delta > 180.0) delta = 360.0 - delta; 99 | double gauss = exp(-0.5 * (delta / sigmaDeg) * (delta / sigmaDeg)); 100 | float jitter = 0.9f + 0.2f * (float)esp_random() / (float)UINT32_MAX; 101 | out[d] = (float)(peak * gauss) + bg * jitter; 102 | } 103 | } 104 | 105 | static String buildKrakenCsvLine(const Station& S, double bearingDeg, double widthRad, double rssiDb, 106 | double gpsHeadingDeg, double compassHeadingDeg, 107 | const float* spectrum, int nBins) { 108 | String line; line.reserve(2048); 109 | unsigned long nowMs = millis(); 110 | appendCSV(line, String((uint32_t)(nowMs))); // timestamp ms 111 | appendCSV(line, String((int)round(bearingDeg))); // max DOA compass (0-359) 112 | double conf = 99.0 * exp(-widthRad); if (conf>99.0) conf=99.0; if(conf<0.0) conf=0.0; 113 | appendCSVf(line, conf, 1); // confidence 114 | appendCSVf(line, rssiDb, RSSI_DECIMALS); // RSSI 115 | appendCSV(line, String(CENTER_FREQ_HZ)); // frequency 116 | appendCSV(line, ARRAY_TYPE); // array type 117 | appendCSV(line, "50"); // latency ms (fake) 118 | appendCSV(line, S.id); // station id 119 | appendCSVf(line, S.lat, LATLON_DECIMALS); // lat 120 | appendCSVf(line, S.lon, LATLON_DECIMALS); // lon 121 | appendCSVf(line, gpsHeadingDeg, 1); // GPS heading 122 | appendCSVf(line, compassHeadingDeg, 1); // compass heading 123 | appendCSV(line, "GPS"); // main heading source 124 | // reserved 4 fields 125 | appendCSV(line, "0"); appendCSV(line, "0"); appendCSV(line, "0"); appendCSV(line, "0"); 126 | // spectrum bins 127 | for (int i = 0; i < nBins; ++i) appendCSVf(line, spectrum[i], SPEC_DECIMALS); 128 | return line; 129 | } 130 | 131 | static void computeObject(double& outLat, double& outLon, double& uFraction) { 132 | unsigned long now = millis(); 133 | double elapsedS = (now - simStartMs) / 1000.0; 134 | if (travelTimeS <= 0.0) uFraction = 1.0; 135 | else { uFraction = elapsedS / travelTimeS; if (uFraction>1.0) uFraction=1.0; } 136 | outLat = OBJ_START_LAT + (OBJ_END_LAT - OBJ_START_LAT) * uFraction; 137 | outLon = OBJ_START_LON + (OBJ_END_LON - OBJ_START_LON) * uFraction; 138 | } 139 | 140 | static void updateOneStation(Station& S, double objLat, double objLon, String& outCsv) { 141 | double dist = haversine_m(S.lat, S.lon, objLat, objLon); 142 | double bearing = initial_bearing_deg(S.lat, S.lon, objLat, objLon); 143 | double width = widthModel(dist); 144 | double rssi = rssiModel(dist); 145 | double peak = peakFromRSSI(rssi); 146 | float spectrum[N_BINS]; 147 | makeSpectrum(spectrum, N_BINS, bearing, width, peak, BACKGROUND_LEVEL); 148 | outCsv = buildKrakenCsvLine(S, bearing, width, rssi, bearing, bearing, spectrum, N_BINS); 149 | S.bearing_deg = bearing; 150 | S.width_rad = width; 151 | S.rssi_db = rssi; 152 | S.seq++; 153 | } 154 | 155 | // -------- HTTP Handlers -------- 156 | void handleRootA(){ serverA.send(200, "text/plain", "Kraken A: /DOA_value.html /status.json"); } 157 | void handleRootB(){ serverB.send(200, "text/plain", "Kraken B: /DOA_value.html /status.json"); } 158 | 159 | void handleDOA_A(){ serverA.send(200, "text/html", lastCsvA); } 160 | void handleDOA_B(){ serverB.send(200, "text/html", lastCsvB); } 161 | 162 | void handleStatusA(){ 163 | String json = "{\"id\":\""+String(A.id)+"\",\"lat\":"+String(A.lat,6)+",\"lon\":"+String(A.lon,6)+ 164 | ",\"bearing\":"+String(A.bearing_deg,1)+",\"rssi\":"+String(A.rssi_db,1)+"}"; 165 | serverA.send(200, "application/json", json); 166 | } 167 | void handleStatusB(){ 168 | String json = "{\"id\":\""+String(B.id)+"\",\"lat\":"+String(B.lat,6)+",\"lon\":"+String(B.lon,6)+ 169 | ",\"bearing\":"+String(B.bearing_deg,1)+",\"rssi\":"+String(B.rssi_db,1)+"}"; 170 | serverB.send(200, "application/json", json); 171 | } 172 | 173 | // -------- Setup & Loop -------- 174 | void setup() { 175 | Serial.begin(115200); 176 | delay(200); 177 | WiFi.mode(WIFI_STA); 178 | WiFi.begin(WIFI_SSID, WIFI_PASS); 179 | Serial.print("Connecting WiFi"); 180 | for (int i=0; i<60 && WiFi.status()!=WL_CONNECTED; ++i) { delay(250); Serial.print("."); } 181 | Serial.println(); 182 | if (WiFi.status()!=WL_CONNECTED) { Serial.println("WiFi failed, rebooting"); delay(3000); ESP.restart(); } 183 | Serial.print("WiFi OK. IP: "); Serial.println(WiFi.localIP()); 184 | 185 | // Path metrics 186 | pathLengthM = haversine_m(OBJ_START_LAT, OBJ_START_LON, OBJ_END_LAT, OBJ_END_LON); 187 | travelTimeS = (SPEED_MPS>0.0) ? (pathLengthM / SPEED_MPS) : 0.0; 188 | simStartMs = millis(); 189 | nextTickMs = simStartMs; 190 | 191 | // HTTP A 192 | serverA.on("/", handleRootA); 193 | serverA.on("/DOA_value.html", handleDOA_A); 194 | serverA.on("/status.json", handleStatusA); 195 | serverA.begin(); 196 | Serial.printf("HTTP A started on port %d\n", HTTP_PORT_A); 197 | 198 | // HTTP B 199 | serverB.on("/", handleRootB); 200 | serverB.on("/DOA_value.html", handleDOA_B); 201 | serverB.on("/status.json", handleStatusB); 202 | serverB.begin(); 203 | Serial.printf("HTTP B started on port %d\n", HTTP_PORT_B); 204 | 205 | Serial.printf("Endpoints:\n A: http://%s:%d/DOA_value.html\n B: http://%s:%d/DOA_value.html\n", 206 | WiFi.localIP().toString().c_str(), HTTP_PORT_A, 207 | WiFi.localIP().toString().c_str(), HTTP_PORT_B); 208 | } 209 | 210 | void loop() { 211 | serverA.handleClient(); 212 | serverB.handleClient(); 213 | 214 | unsigned long now = millis(); 215 | if (now < nextTickMs) return; 216 | nextTickMs = now + (unsigned long)(BURST_PERIOD_S * 1000.0); 217 | if (BURST_JITTER_MS>0.0) { 218 | long j = (long)((((double)esp_random()/(double)UINT32_MAX)*2.0 - 1.0) * BURST_JITTER_MS); 219 | long nt = (long)nextTickMs + j; if (nt>0) nextTickMs = (unsigned long)nt; 220 | } 221 | 222 | double objLat, objLon, u; 223 | // compute current object position 224 | { 225 | unsigned long nowMs = millis(); 226 | double elapsedS = (nowMs - simStartMs) / 1000.0; 227 | if (travelTimeS <= 0.0) u = 1.0; 228 | else { u = elapsedS / travelTimeS; if (u>1.0) u=1.0; } 229 | objLat = OBJ_START_LAT + (OBJ_END_LAT - OBJ_START_LAT) * u; 230 | objLon = OBJ_START_LON + (OBJ_END_LON - OBJ_START_LON) * u; 231 | } 232 | 233 | // Update both stations 234 | updateOneStation(A, objLat, objLon, lastCsvA); 235 | updateOneStation(B, objLat, objLon, lastCsvB); 236 | 237 | if (u >= 1.0) { 238 | if (ON_REACH_END == 0) { 239 | // stop updating; keep serving last values 240 | } else if (ON_REACH_END == 2) { 241 | simStartMs = millis(); 242 | A.seq = 1; B.seq = 1; 243 | } else { 244 | // hold: keep emitting same last point 245 | } 246 | } 247 | } 248 | --------------------------------------------------------------------------------