# solar_hop v2.2.1 — link-free land / solar-charge / resume for ArduCopter Open reference design. License: CC BY 4.0 (documents), Apache 2.0 (code). No warranty; validated in SITL (Copter-4.5), not flown. ## Files - `solar_hop.lua` — flight-controller state machine (ArduCopter 4.5+ Lua). Every binding it calls was checked against the Copter-4.5 `bindings.desc`, and the full cycle (divert → land → charge → resting-voltage resync → relaunch → resume → NAV_LAND, plus reboot on the perch and reboot after completion) was run in ArduPilot SITL on Copter-4.5. Note: ArduPilot's bundled Lua allows at most 100 local variables per chunk (stock Lua allows 200); tuning lives in one `CFG` table and state in one `V` table for that reason. Do not add top-level locals freely. - `companion_agent.py` — companion computer: graceful shutdown, optional perch selection, data-at-rest encryption - `README.md` — wiring, parameters, mission rules, test sequence, review record ## Wiring | FC output | Function | Hardware | |---|---|---| | AUX1 (RELAY1) | Companion power | Logic-level MOSFET load switch on the 5 V BEC output | | AUX2 (RELAY2) | PV connect | DC solid-state relay in series with the panel positive lead, before the MPPT. ON = charging. `RELAY2_DEFAULT 1` so the pack charges if the script is disabled | | AUX3 (RELAY3) | Shutdown request | Direct to a companion GPIO (BCM 17 by default); FC AUX is 3.3 V logic | | I2C | Pack monitor | INA228 with ≥60 A shunt, bidirectional (`BATT_MONITOR 21`) | | I2C | PV monitor | INA226 on the panel side of the SSR (`BATT2_MONITOR 21`) | | I2C | Cell temperature | MCP9600 + thermocouple on the pack: `TEMP1_TYPE` per sensor, **`TEMP1_SRC 3` (Battery Index), `TEMP1_SRC_ID 1`**. In Copter-4.5 `TEMP1_SRC 1` is ESC, not battery; with the wrong source every thermal protection is silently off | | TELEM2 | Companion MAVLink | 921600 8N1, MAVLink2 | ## Parameters ``` SCR_ENABLE 1 SCR_HEAP_SIZE 150000 FS_GCS_ENABLE 0 FS_THR_ENABLE 0 (testing: FS_THR_ENABLE 2, FS_OPTIONS continue-in-Auto/Guided bits) BATT_CAPACITY BATT_FS_LOW_ACT 0 BATT_LOW_MAH 0 BATT_LOW_VOLT 0 BATT_FS_CRT_ACT 1 BATT_CRT_MAH <15% of capacity> BATT_CRT_VOLT <3.3 V x cells, e.g. 19.8> (Copter-4.5 has no BATT_CRT_PCT; without CRT_MAH/CRT_VOLT there is no flight-controller backstop) BATT2_MONITOR 21 BATT2_FS_LOW_ACT 0 BATT2_FS_CRT_ACT 0 BATT2_LOW_VOLT 0 BATT2_CRT_VOLT 0 TEMP1_TYPE TEMP1_SRC 3 TEMP1_SRC_ID 1 DISARM_DELAY 5 MIS_RESTART 0 WP_YAW_BEHAVIOR 2 RELAY1_FUNCTION 1 RELAY1_PIN RELAY1_DEFAULT 1 RELAY2_FUNCTION 1 RELAY2_PIN RELAY2_DEFAULT 1 RELAY3_FUNCTION 1 RELAY3_PIN RELAY3_DEFAULT 0 FENCE_ENABLE 1 FENCE_TYPE 4 (polygon only) FENCE_ACTION 2 (Always Land) (HOME moves to each perch, so a HOME-centred circle fence and a HOME-relative altitude fence drift; the polygon does not) AVD_ENABLE 1 ADSB_TYPE 1 (uAvionix pingRX, MAVLink) AVD_F_ACTION 2 (climb/descend) AVD_F_RCVRY 1 TERRAIN_ENABLE 1 (preload SRTM tiles for the whole route and all perches; there is no link to fetch them) SCR_USER4 1 operator HOLD: set before any ground test; clear only for the real mission ``` Remote ID: fit and enable for U.S. civil operations (14 CFR Part 89). ## Mission rules (the script audits 1 and 2 at boot and before every launch, and refuses to fly otherwise) 1. **Every NAV item uses an ABSOLUTE (frame 0) or TERRAIN (frame 10) altitude. Never RELATIVE (frame 3).** Copter re-sets HOME on every arm, so after a hop "relative" means relative to the perch; a valley perch would lower the whole remaining route by the elevation difference. 2. The last command is `NAV_LAND` at the recovery coordinates. Never `RTL`. Copter-4.5 does not report mission completion after a NAV_LAND (it disarms and restarts the mission at item 1 while disarmed); the script detects completion from the last nav command it saw while armed, so the recovery landing must be a NAV_LAND item and not a LAND executed some other way. 3. Preplanned perches go in `LAND_PTS` (flat, open sky, agency-controlled ground or roof). The divert is energy-aware: each second in flight the script estimates the energy to reach the nearest perch within `DIVERT_MAX_M` (distance ÷ `WPNAV_SPEED`, plus `DIVERT_RESERVE_S` for the landing, at the smoothed measured flight power, × `DIVERT_MARGIN`) and compares it with the energy above `DIVERT_FLOOR_PCT`. It diverts when that perch is about to become unreachable, or at `LOW_PCT` regardless. A perch the model says it cannot reach is not attempted; the aircraft lands in place (or hands selection to the companion if `COMPANION_SITE_SELECT` is true). Space perches so the nearest one is always well inside what a leg from `LOW_PCT` can cover: on the v2 quad (194 Wh, ~520 W cruise, 12 m/s) that is roughly 1 km. Each perch must also sit far enough along the route that a full charge carries the aircraft past the next waypoint before the model trips again; otherwise it re-perches at the same waypoint, and after three tries the script sets HOLD ("no progress") and waits for the operator. 4. Launches only inside `LAUNCH_WINDOW` (local solar hours from GPS time and longitude; default 08:00–13:00). ## Operator flags (SCR_USER1..4) | Parameter | Meaning | Who sets it | |---|---|---| | SCR_USER1 | Saved waypoint index; `-1` = mission complete (boots into DONE, charge-manages, never flies) | Script. Operator sets `0` before a new mission | | SCR_USER2 | Mission fingerprint; a changed mission clears SCR_USER1 | Script | | SCR_USER3 | Hop count | Script. Operator may zero it | | SCR_USER4 | HOLD: `1` = never launch. The script sets it itself after any human intervention (manual landing, disarm during climb) | Operator clears it deliberately | ## What the script does on the perch - Requests a graceful companion shutdown (GPIO + STATUSTEXT), cuts companion power 30 s later. - Keeps the PV relay closed only while cell temperature is inside the charge window (0–45 °C default; confirm against the cell datasheet). **No temperature reading = no charging and no launch** (`TEMP_REQUIRED = true`). An optional board-temperature fallback with a 5 °C margin exists for operators who accept that risk. - Every 45 min opens the PV relay, waits 3 min for polarisation to relax, and reads resting voltage only if the companion is off and |I| < 0.3 A. The reading resynchronises the flight controller's coulomb counter (`battery:reset_remaining`). A rejected read is retried after 60 s; after five rejections it backs off so charging is not crippled. - After a reboot the SOC is pinned at zero until the first successful resting read. The coulomb counter resets to 100% at boot; v2 trusted it, and would have launched on an empty pack. - Launches when SOC ≥ `RESUME_PCT` and at least one resting read has succeeded on this perch, and: daylight, launch window, cell temperature in the launch window, tilt < 20°, 3D fix, AHRS healthy, pre-arm checks pass, hop count under limit, not stuck on the same waypoint, mission audit passed, HOLD clear. - Before launch: companion on, 60 s boot, PV relay open, HOME set to the perch, GUIDED, arm, takeoff to 30 m above the perch, then AUTO from the saved waypoint. If the launch is refused for any reason the companion is shut down again. - If a human takes the aircraft (any mode the script did not command) it pauses; a manual landing or a disarm during climb sets HOLD and the aircraft stays down until an operator clears it. ## Test sequence (do not skip steps) 1. **SITL.** `sim_vehicle.py -v ArduCopter --console --map --speedup 5`. Copy the script to `scripts/`, set `CFG.SITL_TEST = true` in the file, set the parameters above (SITL relay pins must be < 16, e.g. `RELAYx_PIN 13/14/15`; temperature via `TEMP1_TYPE 5, TEMP1_SRC 3, TEMP1_SRC_ID 1, TEMP1_PIN 0, TEMP1_A0 25`), upload a mission with absolute-frame waypoints (frame 0, ~620 m AMSL at the default CMAC home) ending in NAV_LAND (the audit refuses relative frames). Set `SIM_BATT_VOLTAGE 25.2` (6S), `BATT_CAPACITY 1500` and `WPNAV_SPEED 500` so the simulated draw reaches the divert point within minutes; put one perch in `LAND_PTS` a few hundred metres from the route. Once perched, `param set SIM_BATT_VOLTAGE 25.0`; after the 3-minute settle the resting read lifts the coulomb counter above `RESUME_PCT` and the launch sequence runs. Confirm: divert (energy-aware, before 30% when the perch is far) → land → disarm → perched → OCV read → takeoff → resume at the saved waypoint → NAV_LAND → "DONE: mission complete" and `SCR_USER1 = -1`. Reboot SITL mid-perch and confirm it resumes CHARGING with SOC 0 until the read; reboot after completion and confirm it boots into DONE and never arms. 2. **Bench.** Vehicle strapped down, props off, `SCR_USER4 1`. Confirm relay sequencing with a meter, PV inhibit by cooling the thermocouple (and by unplugging it: charging must stop), the `HOP` dataflash message. 3. **Tether, props on.** `FS_THR_ENABLE 2`, `SCR_USER4 0`, safety pilot. One hop. Confirm climb-against-perch altitude and HOME reset. Take control once in each of DIVERT, LANDING and CLIMB: the script must go PAUSED and never fight the pilot; land manually and confirm the script sets HOLD and does not relaunch, including after a power cycle. 4. **Short mission.** Two perches, three legs, daylight, VLOS, under Part 107 with a safety pilot. Then and only then plan link-free operations under a public-aircraft COA or Part 108 when final. ## Review record v2.1 was then run in ArduPilot SITL (Copter-4.5). Three findings, closed in v2.2: the script exceeded ArduPilot's 100-local limit and never loaded (and, with arming checks on, blocked arming); mission completion was never detected after the final NAV_LAND, so a reboot at the recovery point relaunched to re-fly the last legs; the divert decision ignored distance and ran the pack to the floor 150 m into a 700 m leg. v2.2 then passed in SITL on the unmodified file: clean load; energy-aware divert reaching the perch within 3–6 m on every divert (legs up to 737 m); charge, resting-voltage resync, relaunch and resume at the saved waypoint; mission completion detected after the final NAV_LAND (`SCR_USER1 = -1`); reboot on the perch waited for a resting read before arming; reboot after completion never armed over 14 minutes. The one finding — the no-progress guard did not survive a reboot — is closed in v2.2.1 by setting HOLD when it trips. v2 was reviewed by an independent pass against the Copter-4.5 sources and bindings before release. Ten findings, all closed in v2.1: boot-time resting read taken under charge (false-high SOC); HOME-relative altitudes and fence drifting with the perch; wrong `TEMP1_SRC` leaving thermal protection off; a non-existent `BATT_CRT_PCT` parameter; three autonomous-relaunch paths after human intervention; LANDING overriding a safety pilot; the GUIDED divert target re-sent every second and its return value ignored; companion left powered after a refused launch; divert leg at constant altitude over intervening terrain; integer-format errors on float SOC. What was checked and found correct: mode numbers, 0-based relay indices, the `set_current_cmd` + AUTO resume path, `reset_remaining` semantics, `start_takeoff` preconditions, radian units, logger format, timer wrap behaviour (32-bit differences wrap correctly, so missions longer than 24.8 days need no re-basing). ## Known limits - The OCV table is a generic NMC curve; replace it with a measured table for the actual cells. - No wind check on the ground. Add an anemometer or use a conservative launch window in exposed terrain. - A perch under canopy, on a north slope, or in dust may never reach `RESUME_PCT`; the 6-hour stall warning is only a log entry unless a status beacon is fitted. - The mission audit treats any NAV command in relative frame as a fault; a deliberately relative final NAV_LAND at the recovery point must be written in absolute or terrain frame instead.