FlightGear next
heading_indicator_dg.cxx
Go to the documentation of this file.
1/*
2 * SPDX-FileName: heading_indicator_dg.cxx
3 * SPDX-FileComment: a Directional Gyro (DG) compass.
4 * SPDX-License-Identifier: GPL-2.0-or-later
5 * SPDX-FileContributor: Written by Vivian Meazza, started 2005.
6 * SPDX-FileContributor: Enhanced by Benedikt Hallinger, 2023
7 */
8
9#include "config.h"
10
12
13#include <simgear/compiler.h>
14#include <simgear/sg_inlines.h>
15#include <simgear/math/SGMath.hxx>
16#include <iostream>
17#include <string>
18#include <sstream>
19
20#include <Main/fg_props.hxx>
21#include <Main/util.hxx>
22
24#define POW6(x) (x*x*x*x*x*x)
25
26HeadingIndicatorDG::HeadingIndicatorDG(SGPropertyNode* node) : _last_heading_deg(0),
27 _last_indicated_heading_dg(0),
28 _gyro_lag(0)
29{
30 if (!node->getBoolValue("new-default-power-path", false)) {
31 setDefaultPowerSupplyPath("/systems/electrical/outputs/DG");
32 }
33 if (node->hasChild("suction")) {
34 // If vacuum driven, reconfigure default abstract instrument implementation
35 _vacuumDriven = true;
36 _suctionPath = node->getStringValue("suction");
37 SGPropertyNode_ptr _cfg_node = node->getChild("power-supply", 0, true);
38 _cfg_node->setStringValue(_suctionPath);
39
40 _minVacuum = node->getDoubleValue("minimum-vacuum", _minVacuum);
41 }
42
43 _heading_in_nodePath = node->getStringValue("heading-source", "/orientation/heading-deg");
44
45 SGPropertyNode* gyro_cfg = node->getChild("gyro", 0, true);
46 _minSpin = gyro_cfg->getDoubleValue("minimum-spin-norm", 0.9);
47 _gyro_spin_up = gyro_cfg->getDoubleValue("spin-up-sec", 4.0);
48 _gyro_spin_down = gyro_cfg->getDoubleValue("spin-down-sec", 180.0);
49
50 SGPropertyNode* limits_cfg = node->getChild("limits", 0, true);
51 _yaw_rate_nodePath = limits_cfg->getStringValue("yaw-rate-source", "/orientation/yaw-rate-degps");
52 _yaw_error_factor = limits_cfg->getDoubleValue("yaw-error-factor", 0.033);
53 _yaw_limit_rate = limits_cfg->getDoubleValue("yaw-limit-rate", 5.0);
54 _g_error_factor = limits_cfg->getDoubleValue("g-error-factor", 0.033);
55 _gnodePath = limits_cfg->getStringValue("g-node", "/accelerations/pilot-g");
56 _g_filtertime = limits_cfg->getDoubleValue("g-filter-time", 10.0);
57 _g_limit_lower = limits_cfg->getDoubleValue("g-limit-lower", -0.5);
58 _g_limit_upper = limits_cfg->getDoubleValue("g-limit-upper", 1.5);
59 _g_limit_tumble = limits_cfg->getDoubleValue("g-limit-tumble-factor", 1.5);
60
61 readConfig(node, "heading-indicator-dg");
62}
63
67
68void
70{
71 std::string branch = nodePath();
72
73 _heading_in_node = fgGetNode(_heading_in_nodePath, true);
74 _yaw_rate_node = fgGetNode(_yaw_rate_nodePath, true);
75 _g_node = fgGetNode(_gnodePath, true);
76 _we_speed_node = fgGetNode("/velocities/east-relground-fps", true);
77
78 SGPropertyNode *node = fgGetNode(branch, true );
79 _offset_node = node->getChild("offset-deg", 0, true);
80 _heading_bug_error_node = node->getChild("heading-bug-error-deg", 0, true);
81 _error_node = node->getChild("error-deg", 0, true);
82 _nav1_error_node = node->getChild("nav1-course-error-deg", 0, true);
83 _heading_out_node = node->getChild("indicated-heading-deg", 0, true);
84 _drift_ph_out_node = node->getChild("drift-per-hour-deg", 0, true);
85 _lat_nut_node = node->getChild("latitude-nut-setting", 0, true);
86 _transp_wander_out_node = node->getChild("transport-wander-per-hour-deg", 0, true);
87 _caged_node = node->getChild("caged-flag", 0, true);
88 _tumble_node = node->getChild("tumble-norm", 0, true);
89 _tumble_flag_node = node->getChild("tumble-flag", 0, true);
90 _align_node = node->getChild("align-deg", 0, true);
91 _spin_node = node->getChild("spin", 0, true);
92 SGPropertyNode* gyro_node = node->getChild("gyro", 0, true);
93 _minSpin_node = gyro_node->getChild("minimum-spin", 0, true);
94 _gyro_spin_up_node = gyro_node->getChild("spin-up-sec", 0, true);
95 _gyro_spin_down_node = gyro_node->getChild("spin-down-sec", 0, true);
96
97 if (_vacuumDriven) {
98 _suction_node = fgGetNode(_suctionPath, true);
99 _minVacuum_node = node->getChild("minimum-vacuum", 0, true);
100 }
101
102 SGPropertyNode* limits_node = node->getChild("limits", 0, true);
103 _yaw_error_factor_node = limits_node->getChild("yaw-error-factor", 0, true);
104 _yaw_limit_rate_node = limits_node->getChild("yaw-limit-rate", 0, true);
105 _g_filtertime_node = limits_node->getChild("g-filter-time", 0.0, true);
106 _g_error_factor_node = limits_node->getChild("g-error-factor", 0, true);
107 _g_limit_lower_node = limits_node->getChild("g-limit-lower", 0, true);
108 _g_limit_upper_node = limits_node->getChild("g-limit-upper", 0, true);
109 _g_limit_tumble_node = limits_node->getChild("g-limit-tumble-factor", 0, true);
110
112
113 reinit();
114}
115
116void
118{
119 // reset errors/drift values
120 _align_node->setDoubleValue(0.0);
121 _error_node->setDoubleValue(0.0);
122 _offset_node->setDoubleValue(0.0);
123
124 _last_heading_deg = _heading_in_node->getDoubleValue();
125 _last_indicated_heading_dg = _last_heading_deg;
126
127 if (!_minSpin_node->hasValue())
128 _minSpin_node->setDoubleValue(_minSpin);
129 if (_vacuumDriven && !_minVacuum_node->hasValue())
130 _minVacuum_node->setDoubleValue(_minVacuum);
131 if (!_gyro_spin_up_node->hasValue())
132 _gyro_spin_up_node->setDoubleValue(_gyro_spin_up);
133 if (!_gyro_spin_down_node->hasValue())
134 _gyro_spin_down_node->setDoubleValue(_gyro_spin_down);
135
136 _yaw_error_factor_node->setDoubleValue(_yaw_error_factor);
137 _yaw_limit_rate_node->setDoubleValue(_yaw_limit_rate);
138 _g_filtertime_node->setDoubleValue(_g_filtertime);
139 _g_error_factor_node->setDoubleValue(_g_error_factor);
140 _g_limit_lower_node->setDoubleValue(_g_limit_lower);
141 _g_limit_upper_node->setDoubleValue(_g_limit_upper);
142 _g_limit_tumble_node->setDoubleValue(_g_limit_tumble);
143 _last_g = _g_node->getDoubleValue();
144
145 _tumble_flag_node->setBoolValue(0);
146 _tumble_node->setDoubleValue(0.0);
147
148 _gyro.reinit();
149}
150
151void
153{
154 // Get the spin from the gyro
155 if (_vacuumDriven) {
156 // treat the power supply node as vacuum source in inHG
157 _minVacuum = _minVacuum_node->getDoubleValue();
158 _gyro.set_power_norm(isServiceableAndPowered() * _suction_node->getDoubleValue() / _minVacuum);
159 } else {
160 // gyro is operated electrically
161 _gyro.set_power_norm(isServiceableAndPowered());
162 }
163
164 _gyro.set_spin_up(_gyro_spin_up_node->getDoubleValue());
165 _gyro.set_spin_down(_gyro_spin_down_node->getDoubleValue());
166 _gyro.set_spin_norm(_spin_node->getDoubleValue());
167 _gyro.update(dt);
168
169 // read inputs
170 double spin = _gyro.get_spin_norm();
171 double heading = _heading_in_node->getDoubleValue();
172 double offset = _offset_node->getDoubleValue();
173 bool isCaged = _caged_node->getBoolValue();
174
175 _spin_node->setDoubleValue( spin );
176
177 // calculate scaling factor
178 // caged gyro will be forced into position and behave like "stuck".
179 double factor = isCaged ? 0.0 : POW6(spin);
180
181 // calculate time-based precession:
182 // 0° at equator, ~15°/hr (360°/day) at poles (+/- 90°Lat)
183 // (northern hemisphere causes under-read ie. clockwise rotation)
184 // Drift can be corrected by a litude nut setting (wich is a screwed weight on the gimbal)
185 double latPos = globals->get_aircraft_position().getLatitudeRad();
186 double drift_per_hour = -15 * sin(latPos);
187 double lat_nut_setting = _lat_nut_node->getDoubleValue();
188 drift_per_hour += 15 * sin(lat_nut_setting * SG_DEGREES_TO_RADIANS); // correction of latitude
189 drift_per_hour *= factor; // apply spin factor: non spinning/stuck gyro has no drift
190 _drift_ph_out_node->setDoubleValue(drift_per_hour);
191 double drift_per_frame = (drift_per_hour / 60 / 60) * dt; // convert hrs->frame(1/s)
192 offset += drift_per_frame; // apply drift
193
194 // Caluclate transport wander
195 // this is: Degrees-of-longitude-travelled * 1/60tan(lat)
196 // Travelling East->West gives overreading(+) in norhtern hemisphere, underreading(-) south.
197 // Example: flying west at 100 kts at +45° north gives: (100 x Tan 45)/60 = +1.66 degrees per hour.
198 double gnd_speed_kth = (-1 * SG_FPS_TO_KT * _we_speed_node->getDoubleValue()); // convert and flip sign: east speed needs to be negative
199 double transport_wander_p_hour = gnd_speed_kth * (tan(latPos) / 60);
200 transport_wander_p_hour *= factor; // apply spin factor: non spinning/stuck gyro has no drift
201 _transp_wander_out_node->setDoubleValue(transport_wander_p_hour);
202 double transport_wander_per_frame = (transport_wander_p_hour / 60 / 60) * dt; // convert hrs->frame(1/s)
203 offset += transport_wander_per_frame;
204
205 // indication should get more and more stuck at low gyro spins
206 _minSpin = _minSpin_node->getDoubleValue();
207 if (spin < _minSpin || isCaged) {
208 // when gyro spin is low, then any heading change results in
209 // increasing the offset
210 double diff = SGMiscd::normalizePeriodic(-180.0, 180.0, _last_heading_deg - heading);
211 // scaled by 1-factor, so indication is fully stuck at spin==0 (offset compensates
212 // any heading change)
213 offset += diff * (1.0 - factor) * dt;
214
215 if (isCaged && _gyro_lag == 0.0) {
216 // store drift for later, so we can persist the offset once the gyro is back
217 _gyro_lag = heading;
218 }
219 }
220 _last_heading_deg = heading;
221 if (!isCaged && _gyro_lag != 0.0) {
222 // apply stored drift so we avoid the heading jump back to the masked heading when uncaging
223 offset += _gyro_lag - heading;
224 _gyro_lag = 0.0;
225 }
226
227 // normalize offset
228 offset = SGMiscd::normalizePeriodic(-180.0,180.0,offset);
229 _offset_node->setDoubleValue(offset);
230
231 // No magvar - set the alignment manually
232 double align = _align_node->getDoubleValue();
233
234 // Movement-induced error
235 double error = _error_node->getDoubleValue();
236 _yaw_error_factor = _yaw_error_factor_node->getDoubleValue();
237 _yaw_limit_rate = _yaw_limit_rate_node->getDoubleValue();
238 double yaw_rate = _yaw_rate_node->getDoubleValue();
239 if (fabs(yaw_rate) > _yaw_limit_rate) {
240 error += _yaw_error_factor * -yaw_rate * dt * factor;
241 }
242
243 _g_error_factor = _g_error_factor_node->getDoubleValue();
244 _g_limit_lower = _g_limit_lower_node->getDoubleValue();
245 _g_limit_upper = _g_limit_upper_node->getDoubleValue();
246 double g = _g_node->getDoubleValue();
247 _g_filtertime = _g_filtertime_node->getDoubleValue();
248 if (_g_filtertime > 0.0) {
249 g = fgGetLowPass(_last_g, g, dt * _g_filtertime);
250 }
251 _last_g = g;
252 if (g > _g_limit_upper || g < _g_limit_lower) {
253 error += _g_error_factor * g * dt * factor;
254 }
255
256 // Error due to tumbling gyro: calculate the tumble for the next pass.
257 _g_limit_tumble = _g_limit_tumble_node->getDoubleValue();
258 double _g_limit_tumble_lower = _g_limit_lower * _g_limit_tumble;
259 double _g_limit_tumble_upper = _g_limit_upper * _g_limit_tumble;
260 double glimit_tumble_exceed = 0;
261 if (g < _g_limit_tumble_lower)
262 glimit_tumble_exceed = g / _g_limit_tumble_lower;
263 if (g > _g_limit_tumble_upper)
264 glimit_tumble_exceed = g / _g_limit_tumble_upper;
265 if (glimit_tumble_exceed > 0 && !isCaged)
266 _tumble_flag_node->setBoolValue(true);
267
268 if (_tumble_flag_node->getBoolValue()) {
269 double tumble = _tumble_node->getDoubleValue();
270 double tumble_exceed = glimit_tumble_exceed / 2.0;
271 if (tumble_exceed > tumble)
272 tumble = tumble_exceed;
273 if (tumble > 1.0)
274 tumble = 1.0;
275 if (tumble < -1.0)
276 tumble = -1.0;
277
278 // Reerect in 5 minutes or promptly when forced into position
279 double t_reerect = isCaged ? 1.0 : 300.0;
280 double step = dt / t_reerect;
281 if (tumble < -step)
282 tumble += step;
283 else if (tumble > step)
284 tumble -= step;
285 if (fabs(tumble) < 0.01) {
286 tumble = 0.0;
287 _tumble_flag_node->setBoolValue(false);
288 }
289
290 error += tumble * 720.0 * dt / 1.0; // deg/s; where tumble=1.0 -> max rotate speed
291 _tumble_node->setDoubleValue(tumble);
292 }
293
294 error = SGMiscd::normalizePeriodic(-180.0, 180.0, error);
295 _error_node->setDoubleValue(error);
296
297 heading = flightgear::lowPassPeriodicDegreesSigned(_last_indicated_heading_dg, heading, dt * 100 * factor);
298 _last_indicated_heading_dg = heading;
299
300 heading += offset + align + error;
301 heading = SGMiscd::normalizePeriodic(0.0,360.0,heading);
302
303 _heading_out_node->setDoubleValue(heading);
304
305 // calculate the difference between the indicated heading
306 // and the selected heading for use with an autopilot
307 SGPropertyNode* bnode = fgGetNode("/autopilot/settings/heading-bug-deg", false);
308 if ( bnode ) {
309 auto diff = SGMiscd::normalizePeriodic(-180.0, 180.0, bnode->getDoubleValue() - heading);
310 _heading_bug_error_node->setDoubleValue(diff);
311 }
312 // calculate the difference between the indicated heading
313 // and the selected nav1 radial for use with an autopilot
314 SGPropertyNode* nnode = fgGetNode("/instrumentation/nav/radials/selected-deg", false);
315 if ( nnode ) {
316 auto diff = SGMiscd::normalizePeriodic(-180.0, 180.0, nnode->getDoubleValue() - heading);
317 _nav1_error_node->setDoubleValue( diff );
318 }
319}
320
321// end of heading_indicator_dg.cxx
void initServicePowerProperties(SGPropertyNode *node)
void readConfig(SGPropertyNode *config, std::string defaultName)
std::string nodePath() const
bool isServiceableAndPowered() const
void update(double dt) override
FGGlobals * globals
Definition globals.cxx:142
#define POW6(x)
Macro calculating x^6 (faster than super-slow math/pow).
double lowPassPeriodicDegreesSigned(double current, double target, double timeratio)
low-pass filter for bearings, etc in degrees.
Definition util.cxx:95
SGPropertyNode * fgGetNode(const char *path, bool create)
Get a property node.
Definition proptest.cpp:27
double fgGetLowPass(double current, double target, double timeratio)
Move a value towards a target.
Definition util.cxx:46