; Fire Gimbal Control - example configuration. ; ; Copy this file to "config.ini" and edit for your deployment: ; cp config/config.example.ini config.ini ; ; WARNING: do not edit config.ini while the program is running. `gimbal heading ; offset`, `gimbal heading setup` and `gimbal pitch setup` rewrite the [Motor] ; block in place, so concurrent edits would be lost. Stop the program first. ; ; The real config.ini is gitignored because it contains credentials. ; MQTT credentials may instead be supplied via the environment variables ; FGC_MQTT_USER and FGC_MQTT_PW, which take precedence over the values here. [General] ; Tower identity, substituted into all MQTT topics and CamEvent payloads. tower_name = ExampleTower ; Seconds between captures. image_interval = 5 ; 1 = print motor telemetry each loop tick, 0 = quiet. debug = 0 [Network] ; MQTT broker address (the ground-station / ZKMS server). zkms_server_ip = 127.0.0.1 ; Broker port. 1883 is the MQTT default (RabbitMQ's MQTT plugin included); 8883 for ; TLS. Ignored if zkms_server_ip already carries a port ("host:1884") or is a full ; URI ("tcp://host:1884") - in both cases that value wins. mqtt_port = 1883 ; Prefer the FGC_MQTT_USER / FGC_MQTT_PW environment variables over these. mqtt_user = CHANGE_ME mqtt_pw = CHANGE_ME [Camera] ; Defaults inherited by every camera block below; each may override them. ; JPEG XL encoding. distance: 0 = lossless, ~0.8 = near-lossless; effort 1..9. jxl_distance = 0.8 jxl_effort = 4 ; --- One [Camera.] block per camera ----------------------------------- ; Nothing is implied by position: the block names the camera, `label` names its ; output folder and its CamEvent "cam" field, and `driver` picks the backend. ; ; The camera's own settings are NOT here. They live in a settings XML exported ; from the VimbaX Viewer - the Viewer validates combinations as you configure, ; which a text file cannot - and the program hands that XML to the SDK. A ; profile named X is /X.xml. Beside it the program writes X.txt, a ; generated human-readable record of the same settings; it configures nothing. [Camera.rgb] enabled = true driver = vimba ; only vimba is implemented id = DEV_0000000000 ; USB DEV_ id, or a GigE IP label = RGB ; output subdir + CamEvent "cam" profile_dir = config/camera_profiles/rgb profile = standard ; /standard.xml ; Clockwise rotation applied to saved images: 0, 90, 180 or 270. rotation_deg = 0 ; Per-profile timing. Not camera settings - how long the program waits around ; them. After the gimbal reports standstill it holds still this long before ; keeping a frame, so the image is exposed at rest with the auto-exposure ; settled; frame_timeout_ms then abandons the waypoint rather than parking on it. ; With several cameras the scan uses the LONGEST wait across them. [Camera.rgb.standard] settle_wait_ms = 1000 frame_timeout_ms = 2000 [Camera.rgb.night] settle_wait_ms = 2500 frame_timeout_ms = 5000 [Serial] ; Motor-controller serial device and baud rate. device = /dev/ttyACM0 baud = 115200 [IMU] ; Xsens MTi orientation/inertial sensor. Wired to the LattePanda's RS-232 header ; (an onboard hardware UART), so this is a stable /dev/ttyS* node - NOT a USB ; device and NOT /dev/ttyUSB0-3 (those are the Quectel modem). Find it with ; ls /dev/ttyS* ; dmesg | grep -iE 'ttyS|LPSS|HSUART' (often /dev/ttyS4) ; Enable by declaring it in hardware.ini as [Hardware] imu = ; the legacy ; [Features] enable_imu applies only on a tower with no hardware.ini. The host ; reconfigures the MTi to a Euler + calibrated 100 Hz stream at startup. device = baud = 115200 ; The host puts the MTi into a fixed output configuration at every startup ; (Euler + calibrated at 100 Hz) and then READS IT BACK to check the device ; actually took it. There is deliberately no setting for that configuration: it ; is a wire-format contract the parser depends on, not a preference. A mismatch ; is logged as an error and shown as a red MISCONFIGURED row in the Sensors ; view (press i) - the program keeps running so you can diagnose it. ; ; The MTi measures orientation against its OWN earth frame, and the classic ; unit's is NWU (X North, Y West, Z UP) - which reports pitch FALLING as the nose ; rises. The host re-references it to NED. That correction is not settable here: ; it is a fact about the device, stated once in kMtiReference (MtiProtocol.h), ; and shown as the Reference row in the Sensors view (press i). ; ; It is also NOT the same thing as the mount below, and one cannot substitute for ; the other: the earth reference multiplies from the left, the mount from the ; right. No x/y/z value can correct an inverted pitch. ; ; --- Mounting orientation -------------------------------------------------- ; How the IMU is bolted to the gimbal. Both the gimbal and the IMU use a NED ; body frame: ; ; X = forward Y = right Z = down ; roll about X pitch about Y yaw about Z ; (positive follows the right-hand rule about that axis) ; ; x, y and z are DEGREES, and they are the rotation you apply TO THE IMU to ; bring it into alignment with the gimbal - the turns needed to get from the ; IMU's orientation to the gimbal's. So an IMU that physically sits turned 90 ; deg to the right is corrected with z = -90. ; ; Applied in Z-X-Y order, each turn about the axes as they stand after the ; previous one: first z about down, then x about the new forward, then y about ; the new right. ; ; Worked examples: ; dead level, facing the same way as the gimbal ..... x=0 y=0 z=0 ; upside down, still facing forward ................. x=180 y=0 z=0 ; turned 90 deg clockwise seen from above ........... x=0 y=0 z=-90 ; lying on its right side, facing forward ........... x=-90 y=0 z=0 ; upside down AND turned to face backwards .......... x=180 y=0 z=180 ; ; Every reading the program produces - orientation, acceleration, rate of turn ; and magnetic field - is rotated into the gimbal frame by this. Leaving all ; three at 0 changes nothing. ; ; Check it on the rig: press `i` for the Sensors view. Every reading there is ; already corrected, and IMU CONFIG shows the mount being applied. Level the ; gimbal - roll and pitch should read ~0. Tip the nose up - pitch goes ; positive. Roll to the right - roll goes positive. Slew right - yaw increases. ; ; NOTE: `gimbal pitch setup` fits IMU pitch against encoder counts, so a ; calibration made before this was set has the mount's error baked in. Re-run it ; after changing these. Yaw is unaffected - `gimbal heading setup` sights ; landmarks and never consults the IMU. x = 0 y = 0 z = 0 [Env] ; Ambient temperature/humidity sensor (Adafruit SHT41), wired to the ; LattePanda's OWN native I2C bus - NOT the Arduino/motor serial link. Bus ; number varies by board; confirm with `i2cdetect -y N` (SHT41 shows up at ; 0x44). Enable by declaring it in hardware.ini as [Hardware] env = ; the ; legacy [Features] enable_env applies only on a tower with no hardware.ini (or ; run with --mock-env, which implies it). i2c_addr takes decimal or 0x-hex. i2c_device = /dev/i2c-1 i2c_addr = 0x44 period_ms = 2000 [Motor] ; Degrees<->encoder-counts maps, anchored on the mechanical centre found by ; homing. The firmware speaks only in absolute encoder counts; these map them to ; the heading/elevation degrees used by MQTT (target_HDG) and CamEvent. ; ; Homing finds both endstops, moves to the middle of that range and DEFINES the ; midpoint as _center_count. Anchoring on the midpoint rather than one ; endstop averages the error of both, and a large positive value keeps the whole ; working range positive (the driver misbehaves on negative positions). ; ; Set the heading with `gimbal heading setup` (sight two known landmarks) or ; `gimbal heading offset ` to nudge it. Pitch uses `gimbal pitch setup`, ; which needs the IMU. ; ; Travel limits are NOT configured here - they are derived from the firmware's ; homed soft limits after every home, the only place they can be correct. ; ; Mechanical travel between the hard endstops now belongs in hardware.ini as ; [Motor.] travel_deg, where homing uses it to derive counts_per_deg below. ; Mechanical travel of this gimbal (between hard endstops, found at homing): ; Yaw ~320 deg (NOT a full revolution: there is an unreachable arc) ; Pitch 40 deg ; ; Yaw starting point: firmware steps_per_rev=177000 over ~180 deg => ~983.33. yaw_counts_per_deg = 983.33 yaw_center_count = 500000 ; True bearing (0 = north) the gimbal points at when parked at the centre. yaw_heading_at_center = 0.0 ; Clearance kept clear of each hard endstop, in counts. Sent to the firmware, ; which enforces it. Counts, not degrees: it must apply at homing time, before ; any degree map is trusted. yaw_soft_margin_counts = 5000 ; Pitch (physical endstops). CALIBRATE with `gimbal pitch setup`. pitch_counts_per_deg = 8333.33 pitch_center_count = 500000 ; Elevation above level (+ = up) at the centre. This is NOT 0: the mechanical ; centre is the middle of pitch travel, which sits well above the horizon. pitch_angle_at_center = 10.0 pitch_soft_margin_counts = 5000 ; Live encoder-error WARN threshold in counts (0 = off). enc_error_warn_counts = 400 ; How long a full two-end homing may take before the host calls it failed. A ; property of the drivetrain: a high gear ratio searching at a gentle speed takes ; minutes, and too tight a value reports a healthy axis as broken. homing_timeout_s = 65 [Scan] ; Capture scan grid (the (yaw,pitch) waypoints auto-sweep steps through). ; If grid_file is set, that CSV is used verbatim (one "yaw_deg,pitch_deg" per ; line, '#' comments allowed) - edit it to define exact scan coordinates. ; If grid_file is blank, a grid is generated: yaw_intervals positions across ; [yaw_min_deg, yaw_max_deg] at each pitch level. ; The sweep always runs front to back, then returns to the first waypoint and ; repeats, so every pass captures the waypoints in the same order. The return ; leg is one long slew (the yaw arc cannot wrap), so order the grid with that ; in mind. grid_file = yaw_intervals = 56 yaw_min_deg = -90 yaw_max_deg = 90 pitch_levels = 10,30,50 [Paths] ; Directory for saved .jxl images. Supports leading ~ and $ENV expansion. ; If blank, defaults to $XDG_DATA_HOME/fire_gimbal_control/images ; (i.e. ~/.local/share/fire_gimbal_control/images). output_dir = [Features] ; Enable/disable subsystems and select mock (simulated) implementations. ; CLI flags (e.g. --no-mqtt, --mock-camera, --mock-serial) override these. enable_mqtt = true ; false: open no camera at all - a mock source stands in, no Vimba X needed and ; no profile XMLs required. For bringing a tower up one subsystem at a time. enable_camera = true enable_serial = true ; LEGACY. On a tower with a hardware.ini these are derived from [Hardware] imu ; and [Hardware] env - declaring a sensor there is what enables it, and a flag ; here that contradicts the declaration is warned about and ignored. They remain ; only for towers that have not been migrated. enable_imu = false enable_env = false ; The mocks are NOT legacy: substituting a simulated device is a choice about ; this run, not a statement about what is bolted to the tower, so it stays here. mock_camera = false mock_serial = false mock_imu = false mock_env = false [Test] ; Hardware self-test command (`test`). `profile` is the default profile used when ; none is named on the command line. `baseline_file` is where `test baseline` pins ; a golden report (empty => /test_baseline.txt). Each [TestProfile.*] is a ; named bundle of repetition counts, sampling windows and pass thresholds; modules ; read the keys they need (unknown keys are ignored, so new tests can add knobs ; here without code changes). profile = standard baseline_file = ; `test hardware camera` USB preconditions. Both are facts about the machine, not ; tuning. usbfs_memory_mb reverts to 16 on every reboot unless it is on the kernel ; cmdline, and frame delivery stalls below ~1000 (known-issues #19). usbfs_min_mb = 1000 ; The camera port's usb3_lpm_permit node. The path differs per board, so it cannot ; be guessed: left empty, the LPM check reports "not checked" rather than passing. ; Find it with the procedure in known-issues.md, e.g. ; usb3_lpm_path = /sys/devices/.../usb2/2-0:1.0/usb2-port3/usb3_lpm_permit usb3_lpm_path = [TestProfile.standard] reps = 5 ; repetitions for homing / friction / balance drift_warn_pct = 15 ; flag a metric drifting more than this vs baseline drift_gates_pass = false ; true => a drift flag also fails the metric home_spread_max_counts = 80 ; max endstop-count spread across homings home_ms_min = 4000 ; expected homing-time band (ms) home_ms_max = 20000 encoder_diag_reps = 2 imu_sample_window_ms = 3000 ; static window for IMU health imu_drift_window_ms = 60000 ; long static window for the yaw-drift fit imu_yaw_noise_max_deg = 0.5 imu_yaw_drift_max_deg_min = 1.0 imu_expected_hz = 100 ; imu_expected_profile = VRU_general ; assert the active XKF profile, if set backlash_step_counts = 200,500,1000 ; small steps probing reversal dead-band balance_steps = 10 ; pitch up/down current-profile resolution balance_asym_max = 4 ; max |up-down| current (CS) before "imbalanced" cpu_temp_max_c = 85 disk_free_min_gb = 5 ram_avail_min_mb = 256 [TestProfile.quick] reps = 3 imu_sample_window_ms = 1000 imu_drift_window_ms = 10000 [TestProfile.strict] reps = 10 drift_warn_pct = 8 drift_gates_pass = true [UI] ; Full-screen terminal dashboard (sectioned, colored, live status + log pane). ; false => headless line console (default; stdin commands, stdout logs). ; CLI --tui / --no-tui override this. Requires a build with WITH_TUI=ON. enable_tui = false [Logging] ; level: trace|debug|info|warn|error|off (default info). --log-level overrides. level = info ; Verbatim per-category wire tracing, independent of the level above. Comma list ; of: serial, mqtt, camera, control, all, none. OFF by default. --trace overrides. ; Lines are tagged [SERIAL]/[MQTT]/[CAMERA]/[CONTROL] with TX/RX direction, e.g. ; [SERIAL] TX kd180 / [SERIAL] RX $;1234;0;... trace =