|
From: <fli...@li...> - 2026-05-13 15:56:26
|
unknown user pushed a commit to branch next
in repository flightgear.
The following commit(s) were added to refs/heads/next by this push:
new 496c54857 Expose FGInputDevice to Nasal
496c54857 is described below
SF URL: http://sourceforge.net/p/flightgear/flightgear/ci/496c5485718384fba9918b28607b2804b42389b6/
Commit: 496c5485718384fba9918b28607b2804b42389b6
Author: James Turner
Committer: James Turner
AuthorDate: Wed May 13 14:43:40 2026 +0100
Expose FGInputDevice to Nasal
---
src/Autopilot/route_mgr.cxx | 290 ++++++++++---------
src/Input/FGEventInput.cxx | 11 +-
src/Input/FGEventInput.hxx | 4 +-
src/Input/FGHIDDevice.hxx | 3 +-
src/Input/FGHIDUsage.cxx | 26 +-
src/Input/FGInputDevice.cxx | 124 +++++++-
src/Input/FGInputDevice.hxx | 11 +-
src/Input/FGLinuxEventInput.cxx | 2 +-
src/Input/FGLinuxEventInput.hxx | 3 +-
src/Input/FGReportSetting.cxx | 10 +-
src/Input/FGReportSetting.hxx | 2 +-
src/Scripting/CMakeLists.txt | 2 +
src/Scripting/NasalInput.cxx | 84 ++++++
src/Scripting/NasalInput.hxx | 10 +
src/Scripting/NasalSys.cxx | 25 +-
test_suite/CMakeLists.txt | 5 +-
test_suite/FGTestApi/testGlobals.cxx | 4 +
test_suite/unit_tests/Input/test_inputDevice.cxx | 342 ++++++++++++++++++++++-
test_suite/unit_tests/Input/test_inputDevice.hxx | 8 +
test_suite/unit_tests/Navaids/test_fpNasal.cxx | 2 +
20 files changed, 763 insertions(+), 205 deletions(-)
diff --git a/src/Autopilot/route_mgr.cxx b/src/Autopilot/route_mgr.cxx
index 368fecbde..6516a7f43 100644
--- a/src/Autopilot/route_mgr.cxx
+++ b/src/Autopilot/route_mgr.cxx
@@ -77,7 +77,7 @@ static bool commandActivateFlightPlan(const SGPropertyNode* arg, SGPropertyNode
} else {
self->deactivate();
}
-
+
return true;
}
@@ -95,7 +95,7 @@ static bool commandSetActiveWaypt(const SGPropertyNode* arg, SGPropertyNode *)
if ((index < 0) || (index >= self->numLegs())) {
return false;
}
-
+
self->jumpToIndex(index);
return true;
}
@@ -118,7 +118,7 @@ static bool commandInsertWaypt(const SGPropertyNode* arg, SGPropertyNode *)
pos = SGGeod::fromDeg(arg->getDoubleValue("longitude-deg"),
arg->getDoubleValue("latitude-deg"));
}
-
+
if (arg->hasChild("navaid")) {
if (!pos.isValid()) {
pos = self->flightPlan()->vicinityForInsertIndex(haveIndex ? index : -1 /* append */);
@@ -140,18 +140,17 @@ static bool commandInsertWaypt(const SGPropertyNode* arg, SGPropertyNode *)
SG_LOG( SG_AUTOPILOT, SG_INFO, "Unable to find FGPositioned with ident:" << arg->getStringValue("navaid[1]"));
return false;
}
-
+
double r1 = arg->getDoubleValue("radial"),
r2 = arg->getDoubleValue("radial[1]");
-
+
SGGeod intersection;
bool ok = SGGeodesy::radialIntersection(p->geod(), r1, p2->geod(), r2, intersection);
if (!ok) {
- SG_LOG(SG_AUTOPILOT, SG_INFO, "no valid intersection for:" << p->ident()
- << "," << p2->ident());
- return false;
+ SG_LOG(SG_AUTOPILOT, SG_INFO, "no valid intersection for:" << p->ident() << "," << p2->ident());
+ return false;
}
-
+
std::string name = p->ident() + "-" + p2->ident();
wp = new BasicWaypt(intersection, name, NULL);
} else if (arg->hasChild("offset-nm") && arg->hasChild("radial")) {
@@ -169,13 +168,13 @@ static bool commandInsertWaypt(const SGPropertyNode* arg, SGPropertyNode *)
SG_LOG(SG_AUTOPILOT, SG_INFO, "no such airport" << arg->getStringValue("airport"));
return false;
}
-
+
if (arg->hasChild("runway")) {
if (!apt->hasRunwayWithIdent(arg->getStringValue("runway"))) {
SG_LOG(SG_AUTOPILOT, SG_INFO, "No runway: " << arg->getStringValue("runway") << " at " << apt->ident());
return false;
}
-
+
FGRunway* runway = apt->getRunwayByIdent(arg->getStringValue("runway"));
wp = new RunwayWaypt(runway, NULL);
} else {
@@ -206,11 +205,11 @@ static bool commandInsertWaypt(const SGPropertyNode* arg, SGPropertyNode *)
if (alt >= 0) {
leg->setAltitude(RESTRICT_AT, alt);
}
-
+
if (ias > 0) {
leg->setSpeed(RESTRICT_AT, ias);
}
-
+
return true;
}
@@ -231,11 +230,11 @@ FGRouteMgr::FGRouteMgr() :
listener = new InputListener(this);
input->setStringValue("");
input->addChangeListener(listener);
-
+
SGCommandMgr* cmdMgr = globals->get_commands();
cmdMgr->addCommand("define-user-waypoint", this, &FGRouteMgr::commandDefineUserWaypoint);
cmdMgr->addCommand("delete-user-waypoint", this, &FGRouteMgr::commandDeleteUserWaypoint);
-
+
cmdMgr->addCommand("load-flightplan", commandLoadFlightPlan);
cmdMgr->addCommand("save-flightplan", commandSaveFlightPlan);
cmdMgr->addCommand("activate-flightplan", commandActivateFlightPlan);
@@ -250,7 +249,7 @@ FGRouteMgr::~FGRouteMgr()
{
input->removeChangeListener(listener);
delete listener;
-
+
if (_plan) {
_plan->removeDelegate(this);
}
@@ -270,46 +269,46 @@ FGRouteMgr::~FGRouteMgr()
void FGRouteMgr::init() {
SGPropertyNode_ptr rm(fgGetNode(RM));
-
+
magvar = fgGetNode("/environment/magnetic-variation-deg", true);
-
+
departure = fgGetNode(RM "departure", true);
- departure->tie("airport", SGStringValueMethods<FGRouteMgr>(*this,
- &FGRouteMgr::getDepartureICAO, &FGRouteMgr::setDepartureICAO));
- departure->tie("runway", SGStringValueMethods<FGRouteMgr>(*this,
+ departure->tie("airport", SGStringValueMethods<FGRouteMgr>(*this,
+ &FGRouteMgr::getDepartureICAO, &FGRouteMgr::setDepartureICAO));
+ departure->tie("runway", SGStringValueMethods<FGRouteMgr>(*this,
&FGRouteMgr::getDepartureRunway,
&FGRouteMgr::setDepartureRunway));
- departure->tie("sid", SGStringValueMethods<FGRouteMgr>(*this,
+ departure->tie("sid", SGStringValueMethods<FGRouteMgr>(*this,
&FGRouteMgr::getSID,
&FGRouteMgr::setSID));
-
- departure->tie("name", SGStringValueMethods<FGRouteMgr>(*this,
- &FGRouteMgr::getDepartureName, nullptr));
- departure->tie("field-elevation-ft", SGRawValueMethods<FGRouteMgr, double>(*this,
+
+ departure->tie("name", SGStringValueMethods<FGRouteMgr>(*this,
+ &FGRouteMgr::getDepartureName, nullptr));
+ departure->tie("field-elevation-ft", SGRawValueMethods<FGRouteMgr, double>(*this,
&FGRouteMgr::getDepartureFieldElevation, nullptr));
departure->getChild("etd", 0, true);
departure->getChild("takeoff-time", 0, true);
destination = fgGetNode(RM "destination", true);
destination->getChild("airport", 0, true);
-
- destination->tie("airport", SGStringValueMethods<FGRouteMgr>(*this,
- &FGRouteMgr::getDestinationICAO, &FGRouteMgr::setDestinationICAO));
- destination->tie("runway", SGStringValueMethods<FGRouteMgr>(*this,
- &FGRouteMgr::getDestinationRunway,
- &FGRouteMgr::setDestinationRunway));
- destination->tie("star", SGStringValueMethods<FGRouteMgr>(*this,
+
+ destination->tie("airport", SGStringValueMethods<FGRouteMgr>(*this,
+ &FGRouteMgr::getDestinationICAO, &FGRouteMgr::setDestinationICAO));
+ destination->tie("runway", SGStringValueMethods<FGRouteMgr>(*this,
+ &FGRouteMgr::getDestinationRunway,
+ &FGRouteMgr::setDestinationRunway));
+ destination->tie("star", SGStringValueMethods<FGRouteMgr>(*this,
&FGRouteMgr::getSTAR,
&FGRouteMgr::setSTAR));
- destination->tie("approach", SGStringValueMethods<FGRouteMgr>(*this,
+ destination->tie("approach", SGStringValueMethods<FGRouteMgr>(*this,
&FGRouteMgr::getApproach,
&FGRouteMgr::setApproach));
-
- destination->tie("name", SGStringValueMethods<FGRouteMgr>(*this,
- &FGRouteMgr::getDestinationName, nullptr));
- destination->tie("field-elevation-ft", SGRawValueMethods<FGRouteMgr, double>(*this,
- &FGRouteMgr::getDestinationFieldElevation, nullptr));
-
+
+ destination->tie("name", SGStringValueMethods<FGRouteMgr>(*this,
+ &FGRouteMgr::getDestinationName, nullptr));
+ destination->tie("field-elevation-ft", SGRawValueMethods<FGRouteMgr, double>(*this,
+ &FGRouteMgr::getDestinationFieldElevation, nullptr));
+
destination->getChild("eta", 0, true);
destination->getChild("eta-seconds", 0, true);
destination->getChild("touchdown-time", 0, true);
@@ -339,19 +338,19 @@ void FGRouteMgr::init() {
totalDistance->setDoubleValue(0.0);
distanceToGo = fgGetNode(RM "distance-remaining-nm", true);
distanceToGo->setDoubleValue(0.0);
-
+
ete = fgGetNode(RM "ete", true);
ete->setDoubleValue(0.0);
-
+
elapsedFlightTime = fgGetNode(RM "flight-time", true);
elapsedFlightTime->setDoubleValue(0.0);
-
+
active = fgGetNode(RM "active", true);
active->setBoolValue(false);
-
+
airborne = fgGetNode(RM "airborne", true);
airborne->setBoolValue(false);
-
+
_edited = fgGetNode(RM "signals/edited", true);
_flightplanChanged = fgGetNode(RM "signals/flightplan-changed", true);
_isRoute = fgGetNode(RM "is-route", true);
@@ -360,25 +359,25 @@ void FGRouteMgr::init() {
_currentWpt->setAttribute(SGPropertyNode::LISTENER_SAFE, true);
_currentWpt->tie(SGRawValueMethods<FGRouteMgr, int>
(*this, &FGRouteMgr::currentIndex, &FGRouteMgr::jumpToIndex));
-
+
wp0 = fgGetNode(RM "wp", 0, true);
wp0->getChild("id", 0, true);
wp0->getChild("dist", 0, true);
wp0->getChild("eta", 0, true);
wp0->getChild("eta-seconds", 0, true);
wp0->getChild("bearing-deg", 0, true);
-
+
wp1 = fgGetNode(RM "wp", 1, true);
wp1->getChild("id", 0, true);
wp1->getChild("dist", 0, true);
wp1->getChild("eta", 0, true);
wp1->getChild("eta-seconds", 0, true);
-
+
wpn = fgGetNode(RM "wp-last", 0, true);
wpn->getChild("dist", 0, true);
wpn->getChild("eta", 0, true);
wpn->getChild("eta-seconds", 0, true);
-
+
_pathNode = fgGetNode(RM "file-path", 0, true);
}
@@ -387,13 +386,13 @@ void FGRouteMgr::postinit()
{
setFlightPlan(FlightPlan::create());
_plan->setIdent("default-flightplan");
-
+
SGPath path = SGPath::fromUtf8(_pathNode->getStringValue());
if (!path.isNull()) {
SG_LOG(SG_AUTOPILOT, SG_INFO, "loading flight-plan from: " << path);
loadRoute(path);
}
-
+
_isRoute->setBoolValue(_plan->isRoute());
// this code only matters for the --wp option now - perhaps the option
@@ -409,13 +408,13 @@ void FGRouteMgr::postinit()
SG_LOG(SG_AUTOPILOT, SG_WARN, "Failed to create waypoint from '" << wpStr << "'");
}
}
-
+
update_mirror();
}
weightOnWheels = fgGetNode("/gear/gear[0]/wow", true);
groundSpeed = fgGetNode("/velocities/groundspeed-kt", true);
-
+
// check airbone flag agrees with presets
}
@@ -432,18 +431,17 @@ bool FGRouteMgr::saveRoute(const SGPath& p)
if (!_plan) {
return false;
}
-
+
return _plan->save(p);
}
bool FGRouteMgr::loadRoute(const SGPath& p)
{
- FlightPlan* fp = FlightPlan::create();
- if (!fp->load(p)) {
- delete fp;
- return false;
- }
-
+ FlightPlanRef fp = FlightPlan::create();
+ if (!fp->load(p)) {
+ return false;
+ }
+
setFlightPlan(fp);
return true;
}
@@ -458,22 +456,22 @@ void FGRouteMgr::setFlightPlan(const FlightPlanRef& plan)
if (plan == _plan) {
return;
}
-
+
if (_plan) {
_plan->removeDelegate(this);
if (isRouteActive()) {
_plan->finish();
}
-
+
active->setBoolValue(false);
}
-
+
_plan = plan;
_plan->addDelegate(this);
_isRoute->setBoolValue(_plan->isRoute());
_flightplanChanged->fireValueChanged();
-
+
// fire all the callbacks!
departureChanged();
arrivalChanged();
@@ -505,12 +503,12 @@ void FGRouteMgr::update( double dt )
if (dt <= 0.0) {
return; // paused, nothing to do here
}
-
+
double gs = groundSpeed->getDoubleValue();
if (airborne->getBoolValue()) {
time_t now = globals->get_time_params()->get_cur_time();
elapsedFlightTime->setDoubleValue(difftime(now, _takeoffTime));
-
+
if (weightOnWheels->getBoolValue()) {
// touch down
destination->setIntValue("touchdown-time", now);
@@ -525,7 +523,7 @@ void FGRouteMgr::update( double dt )
departure->setIntValue("takeoff-time", _takeoffTime);
}
}
-
+
if (!active->getBoolValue()) {
return;
}
@@ -537,7 +535,7 @@ void FGRouteMgr::update( double dt )
if (!leg) {
return;
}
-
+
// use RoutePath to compute location of active WP
if (!_routePath) {
_routePath.reset(new RoutePath{_plan});
@@ -553,19 +551,19 @@ void FGRouteMgr::update( double dt )
courseDeg -= magvar->getDoubleValue(); // expose magnetic bearing
wp0->setDoubleValue("bearing-deg", courseDeg);
setETAPropertyFromDistance(wp0, distanceM);
-
+
double totalPathDistanceNm = _plan->totalDistanceNm();
double totalDistanceRemaining = distanceM * SG_METER_TO_NM; // distance to current waypoint
-
+
// total distance to go, is direct distance to wp0, plus the remaining
// path distance from wp0
totalDistanceRemaining += (totalPathDistanceNm - leg->distanceAlongRoute());
-
- wp0->setDoubleValue("distance-along-route-nm",
+
+ wp0->setDoubleValue("distance-along-route-nm",
leg->distanceAlongRoute());
- wp0->setDoubleValue("remaining-distance-nm",
+ wp0->setDoubleValue("remaining-distance-nm",
totalPathDistanceNm - leg->distanceAlongRoute());
-
+
FlightPlan::Leg* nextLeg = _plan->nextLeg();
if (nextLeg) {
wpPos = _routePath->positionForIndex(_plan->currentIndex() + 1);
@@ -575,13 +573,13 @@ void FGRouteMgr::update( double dt )
wp1->setDoubleValue("true-bearing-deg", courseDeg);
courseDeg -= magvar->getDoubleValue(); // expose magnetic bearing
wp1->setDoubleValue("bearing-deg", courseDeg);
- setETAPropertyFromDistance(wp1, distanceM);
- wp1->setDoubleValue("distance-along-route-nm",
+ setETAPropertyFromDistance(wp1, distanceM);
+ wp1->setDoubleValue("distance-along-route-nm",
nextLeg->distanceAlongRoute());
- wp1->setDoubleValue("remaining-distance-nm",
+ wp1->setDoubleValue("remaining-distance-nm",
totalPathDistanceNm - nextLeg->distanceAlongRoute());
}
-
+
distanceToGo->setDoubleValue(totalDistanceRemaining);
wpn->setDoubleValue("dist", totalDistanceRemaining);
ete->setDoubleValue(totalDistanceRemaining / gs * 3600.0);
@@ -601,7 +599,7 @@ Waypt* FGRouteMgr::currentWaypt() const
if (_plan && _plan->currentLeg()) {
return _plan->currentLeg()->waypoint();
}
-
+
return NULL;
}
@@ -610,7 +608,7 @@ int FGRouteMgr::currentIndex() const
if (!_plan) {
return 0;
}
-
+
return _plan->currentIndex();
}
@@ -619,7 +617,7 @@ Waypt* FGRouteMgr::wayptAtIndex(int index) const
if (!_plan) {
throw sg_range_exception("wayptAtindex: no flightplan");
}
-
+
return _plan->legAtIndex(index)->waypoint();
}
@@ -628,7 +626,7 @@ int FGRouteMgr::numLegs() const
if (_plan) {
return _plan->numLegs();
}
-
+
return 0;
}
@@ -643,15 +641,15 @@ void FGRouteMgr::setETAPropertyFromDistance(SGPropertyNode_ptr aProp, double aDi
char eta_str[64];
double eta = aDistance * SG_METER_TO_NM / speed;
aProp->getChild("eta-seconds")->setIntValue( eta * 3600 );
- if ( eta >= 100.0 ) {
+ if (eta >= 100.0) {
eta = 99.999; // clamp
}
-
+
if ( eta < (1.0/6.0) ) {
eta *= 60.0; // within 10 minutes, bump up to min/secs
}
-
- int major = (int)eta,
+
+ int major = (int)eta,
minor = (int)((eta - (int)eta) * 60.0);
snprintf( eta_str, 64, "%d:%02d", major, minor );
aProp->getChild("eta")->setStringValue( eta_str );
@@ -662,10 +660,10 @@ void FGRouteMgr::removeLegAtIndex(int aIndex)
if (!_plan) {
return;
}
-
+
_plan->deleteIndex(aIndex);
}
-
+
void FGRouteMgr::waypointsChanged()
{
update_mirror();
@@ -687,9 +685,9 @@ void FGRouteMgr::update_mirror()
}
return;
}
-
+
int num = _plan->numLegs();
-
+
for (int i = 0; i < num; i++) {
FlightPlan::Leg* leg = _plan->legAtIndex(i);
WayptRef wp = leg->waypoint();
@@ -699,13 +697,13 @@ void FGRouteMgr::update_mirror()
prop->setStringValue("id", wp->ident());
prop->setDoubleValue("longitude-deg", pos.getLongitudeDeg());
prop->setDoubleValue("latitude-deg",pos.getLatitudeDeg());
-
+
// leg course+distance
prop->setDoubleValue("leg-bearing-true-deg", leg->courseDeg());
prop->setDoubleValue("leg-distance-nm", leg->distanceNm());
prop->setDoubleValue("distance-along-route-nm", leg->distanceAlongRoute());
-
+
if (leg->altitudeRestriction() != RESTRICT_NONE) {
double ft = leg->altitudeFt();
prop->setDoubleValue("altitude-m", ft * SG_FEET_TO_METER);
@@ -715,35 +713,35 @@ void FGRouteMgr::update_mirror()
prop->setDoubleValue("altitude-m", -9999.9);
prop->setDoubleValue("altitude-ft", -9999.9);
}
-
+
if (leg->speedRestriction() == SPEED_RESTRICT_MACH) {
prop->setDoubleValue("speed-mach", leg->speedMach());
} else if (leg->speedRestriction() != RESTRICT_NONE) {
prop->setDoubleValue("speed-kts", leg->speedKts());
}
-
+
if (wp->flag(WPT_ARRIVAL)) {
prop->setBoolValue("arrival", true);
}
-
+
if (wp->flag(WPT_DEPARTURE)) {
prop->setBoolValue("departure", true);
}
-
+
if (wp->flag(WPT_MISS)) {
prop->setBoolValue("missed-approach", true);
}
-
+
prop->setBoolValue("generated", wp->flag(WPT_GENERATED));
} // of waypoint iteration
-
+
// set number as listener attachment point
mirror->setIntValue("num", _plan->numLegs());
if (rmDlg) {
rmDlg->updateValues();
}
-
+
totalDistance->setDoubleValue(_plan->totalDistanceNm());
}
@@ -821,12 +819,12 @@ bool FGRouteMgr::activate()
SG_LOG(SG_AUTOPILOT, SG_WARN, "::activate, no flight plan defined");
return false;
}
-
+
if (isRouteActive()) {
SG_LOG(SG_AUTOPILOT, SG_WARN, "duplicate route-activation, no-op");
return false;
}
-
+
_plan->activate();
active->setBoolValue(true);
SG_LOG(SG_AUTOPILOT, SG_INFO, "route-manager, activate route ok");
@@ -838,7 +836,7 @@ void FGRouteMgr::deactivate()
if (!isRouteActive()) {
return;
}
-
+
SG_LOG(SG_AUTOPILOT, SG_INFO, "deactivating flight plan");
active->setBoolValue(false);
}
@@ -856,7 +854,7 @@ void FGRouteMgr::jumpToIndex(int index)
SG_LOG(SG_AUTOPILOT, SG_WARN, "FGRouteMgr::jumpToIndex: ignoring invalid index:" << index);
return;
}
-
+
_plan->setCurrentIndex(index);
}
@@ -867,7 +865,7 @@ void FGRouteMgr::currentWaypointChanged()
wp0->getChild("id")->setStringValue(cur ? cur->ident() : "");
wp1->getChild("id")->setStringValue(next ? next->waypoint()->ident() : "");
-
+
_currentWpt->fireValueChanged();
SG_LOG(SG_AUTOPILOT, SG_INFO, "route manager, current-wp is now " << currentIndex());
}
@@ -877,7 +875,7 @@ std::string FGRouteMgr::getDepartureICAO() const
if (!_plan || !_plan->departureAirport()) {
return "";
}
-
+
return _plan->departureAirport()->ident();
}
@@ -886,7 +884,7 @@ std::string FGRouteMgr::getDepartureName() const
if (!_plan || !_plan->departureAirport()) {
return "";
}
-
+
return _plan->departureAirport()->name();
}
@@ -895,7 +893,7 @@ std::string FGRouteMgr::getDepartureRunway() const
if (_plan && _plan->departureRunway()) {
return _plan->departureRunway()->ident();
}
-
+
return "";
}
@@ -904,7 +902,7 @@ void FGRouteMgr::setDepartureRunway(const std::string& aIdent)
if (!_plan) {
return;
}
-
+
FGAirport* apt = _plan->departureAirport();
if (!apt || aIdent.empty()) {
_plan->setDeparture(apt);
@@ -918,7 +916,7 @@ void FGRouteMgr::setDepartureICAO(const std::string& aIdent)
if (!_plan) {
return;
}
-
+
if (aIdent.length() < 3) {
_plan->setDeparture((FGAirport*) nullptr);
} else {
@@ -931,7 +929,7 @@ std::string FGRouteMgr::getSID() const
if (_plan && _plan->sid()) {
return _plan->sid()->ident();
}
-
+
return "";
}
@@ -947,17 +945,17 @@ flightgear::SID* createDefaultSID(FGRunway* aRunway, double enrouteCourse)
if (!aRunway) {
return NULL;
}
-
+
double runwayElevFt = aRunway->end().getElevationFt();
WayptVec wpts;
std::ostringstream ss;
ss << aRunway->ident() << "-3";
-
+
SGGeod p = aRunway->pointOnCenterline(aRunway->lengthM() + (3.0 * SG_NM_TO_METER));
WayptRef w = new BasicWaypt(p, ss.str(), NULL);
w->setAltitude(runwayElevFt + 3000.0, RESTRICT_AT);
wpts.push_back(w);
-
+
ss.str("");
ss << aRunway->ident() << "-6";
p = aRunway->pointOnCenterline(aRunway->lengthM() + (6.0 * SG_NM_TO_METER));
@@ -989,12 +987,12 @@ flightgear::SID* createDefaultSID(FGRunway* aRunway, double enrouteCourse)
w->setAltitude(runwayElevFt + 9000.0, RESTRICT_AT);
wpts.push_back(w);
}
-
+
for (Waypt* w : wpts) {
w->setFlag(WPT_DEPARTURE);
w->setFlag(WPT_GENERATED);
}
-
+
return flightgear::SID::createTempSID("DEFAULT", aRunway, wpts);
}
@@ -1003,28 +1001,28 @@ void FGRouteMgr::setSID(const std::string& aIdent)
if (!_plan) {
return;
}
-
+
FGAirport* apt = _plan->departureAirport();
if (!apt || aIdent.empty()) {
_plan->setSID((flightgear::SID*) NULL);
return;
- }
-
+ }
+
if (aIdent == "DEFAULT") {
double enrouteCourse = -1.0;
if (_plan->destinationAirport()) {
enrouteCourse = SGGeodesy::courseDeg(apt->geod(), _plan->destinationAirport()->geod());
}
-
+
_plan->setSID(createDefaultSID(_plan->departureRunway(), enrouteCourse));
return;
}
-
+
size_t hyphenPos = aIdent.find('-');
if (hyphenPos != string::npos) {
string sidIdent = aIdent.substr(0, hyphenPos);
string transIdent = aIdent.substr(hyphenPos + 1);
-
+
flightgear::SID* sid = apt->findSIDWithIdent(sidIdent);
Transition* trans = sid ? sid->findTransitionByName(transIdent) : NULL;
_plan->setSID(trans);
@@ -1038,7 +1036,7 @@ std::string FGRouteMgr::getDestinationICAO() const
if (!_plan || !_plan->destinationAirport()) {
return "";
}
-
+
return _plan->destinationAirport()->ident();
}
@@ -1047,7 +1045,7 @@ std::string FGRouteMgr::getDestinationName() const
if (!_plan || !_plan->destinationAirport()) {
return "";
}
-
+
return _plan->destinationAirport()->name();
}
@@ -1056,7 +1054,7 @@ void FGRouteMgr::setDestinationICAO(const std::string& aIdent)
if (!_plan) {
return;
}
-
+
if (aIdent.length() < 3) {
_plan->setDestination((FGAirport*) NULL);
} else {
@@ -1069,7 +1067,7 @@ std::string FGRouteMgr::getDestinationRunway() const
if (_plan && _plan->destinationRunway()) {
return _plan->destinationRunway()->ident();
}
-
+
return "";
}
@@ -1078,7 +1076,7 @@ void FGRouteMgr::setDestinationRunway(const std::string& aIdent)
if (!_plan) {
return;
}
-
+
FGAirport* apt = _plan->destinationAirport();
if (!apt || aIdent.empty()) {
_plan->setDestination(apt);
@@ -1092,7 +1090,7 @@ std::string FGRouteMgr::getApproach() const
if (_plan && _plan->approach()) {
return _plan->approach()->ident();
}
-
+
return "";
}
@@ -1106,7 +1104,7 @@ flightgear::Approach* createDefaultApproach(FGRunway* aRunway, double aEnrouteCo
const double approachHeightFt = 2000.0;
double glideslopeDistanceM = (approachHeightFt * SG_FEET_TO_METER) /
tan(3.0 * SG_DEGREES_TO_RADIANS);
-
+
std::ostringstream ss;
ss << aRunway->ident() << "-12";
WayptVec wpts;
@@ -1116,7 +1114,7 @@ flightgear::Approach* createDefaultApproach(FGRunway* aRunway, double aEnrouteCo
wpts.push_back(w);
// work back form the first point on the centerline
-
+
if (aEnrouteCourse >= 0.0) {
// valid enroute course
int index = 4;
@@ -1133,26 +1131,26 @@ flightgear::Approach* createDefaultApproach(FGRunway* aRunway, double aEnrouteCo
wpts.insert(wpts.begin(), w);
}
}
-
+
p = aRunway->pointOnCenterline(-8.0 * SG_NM_TO_METER);
ss.str("");
ss << aRunway->ident() << "-8";
w = new BasicWaypt(p, ss.str(), NULL);
w->setAltitude(thresholdElevFt + approachHeightFt, RESTRICT_AT);
wpts.push_back(w);
-
- p = aRunway->pointOnCenterline(-glideslopeDistanceM);
+
+ p = aRunway->pointOnCenterline(-glideslopeDistanceM);
ss.str("");
ss << aRunway->ident() << "-GS";
w = new BasicWaypt(p, ss.str(), NULL);
w->setAltitude(thresholdElevFt + approachHeightFt, RESTRICT_AT);
wpts.push_back(w);
-
+
for (Waypt* w : wpts) {
w->setFlag(WPT_APPROACH);
w->setFlag(WPT_GENERATED);
}
-
+
return Approach::createTempApproach("DEFAULT", aRunway, wpts);
}
@@ -1161,18 +1159,18 @@ void FGRouteMgr::setApproach(const std::string& aIdent)
if (!_plan) {
return;
}
-
+
FGAirport* apt = _plan->destinationAirport();
if (aIdent == "DEFAULT") {
double enrouteCourse = -1.0;
if (_plan->departureAirport()) {
enrouteCourse = SGGeodesy::courseDeg(_plan->departureAirport()->geod(), apt->geod());
}
-
+
_plan->setApproach(createDefaultApproach(_plan->destinationRunway(), enrouteCourse));
return;
}
-
+
if (!apt || aIdent.empty()) {
_plan->setApproach(static_cast<Approach*>(nullptr));
} else {
@@ -1185,7 +1183,7 @@ std::string FGRouteMgr::getSTAR() const
if (_plan && _plan->star()) {
return _plan->star()->ident();
}
-
+
return "";
}
@@ -1194,19 +1192,19 @@ void FGRouteMgr::setSTAR(const std::string& aIdent)
if (!_plan) {
return;
}
-
+
FGAirport* apt = _plan->destinationAirport();
if (!apt || aIdent.empty()) {
_plan->setSTAR((STAR*) NULL);
return;
- }
-
+ }
+
string ident(aIdent);
size_t hyphenPos = ident.find('-');
if (hyphenPos != string::npos) {
string starIdent = ident.substr(0, hyphenPos);
string transIdent = ident.substr(hyphenPos + 1);
-
+
STAR* star = apt->findSTARWithIdent(starIdent);
Transition* trans = star ? star->findTransitionByName(transIdent) : NULL;
_plan->setSTAR(trans);
@@ -1225,7 +1223,7 @@ double FGRouteMgr::getDepartureFieldElevation() const
if (!_plan || !_plan->departureAirport()) {
return 0.0;
}
-
+
return _plan->departureAirport()->elevation();
}
@@ -1234,7 +1232,7 @@ double FGRouteMgr::getDestinationFieldElevation() const
if (!_plan || !_plan->destinationAirport()) {
return 0.0;
}
-
+
return _plan->destinationAirport()->elevation();
}
@@ -1332,7 +1330,7 @@ SGPropertyNode_ptr FGRouteMgr::wayptNodeAtIndex(int index) const
if ((index < 0) || (index >= numWaypts())) {
throw sg_range_exception("waypt index out of range", "FGRouteMgr::wayptAtIndex");
}
-
+
return mirror->getChild("wp", index);
}
@@ -1343,7 +1341,7 @@ bool FGRouteMgr::commandDefineUserWaypoint(const SGPropertyNode * arg, SGPropert
SG_LOG(SG_AUTOPILOT, SG_WARN, "missing ident defining user waypoint");
return false;
}
-
+
// check for duplicate idents
FGPositioned::TypeFilter f(FGPositioned::WAYPOINT);
FGPositionedList dups = FGPositioned::findAllWithIdent(ident, &f);
diff --git a/src/Input/FGEventInput.cxx b/src/Input/FGEventInput.cxx
index fe0d27153..7feb9d786 100644
--- a/src/Input/FGEventInput.cxx
+++ b/src/Input/FGEventInput.cxx
@@ -69,7 +69,7 @@ std::string FGEventInput::computeDeviceIndexName(FGInputDevice* dev) const
return os.str();
}
-unsigned FGEventInput::AddDevice(FGInputDevice* inputDevice)
+unsigned FGEventInput::AddDevice(FGInputDevice_ptr inputDevice)
{
SGPropertyNode_ptr baseNode = fgGetNode(propertyRoot, true);
SGPropertyNode_ptr deviceNode = nullptr;
@@ -108,7 +108,6 @@ unsigned FGEventInput::AddDevice(FGInputDevice* inputDevice)
configNode = configMap.configurationForDeviceName(deviceName);
} else {
SG_LOG(SG_INPUT, SG_INFO, "No configuration found for device " << deviceName);
- delete inputDevice;
return INVALID_DEVICE_INDEX;
}
inputDevice->SetUniqueName(nameWithIndex);
@@ -124,7 +123,6 @@ unsigned FGEventInput::AddDevice(FGInputDevice* inputDevice)
if (index == MAX_DEVICES) {
SG_LOG(SG_INPUT, SG_WARN, "To many event devices - ignoring " << inputDevice->GetUniqueName());
- delete inputDevice;
return INVALID_DEVICE_INDEX;
}
@@ -138,12 +136,14 @@ unsigned FGEventInput::AddDevice(FGInputDevice* inputDevice)
bool ok = inputDevice->Open();
if (!ok) {
- delete inputDevice;
return INVALID_DEVICE_INDEX;
}
inputDevices[deviceNode->getIndex()] = inputDevice;
+ // Run Nasal <open> code for the device, now it's open
+ inputDevice->postOpen();
+
SG_LOG(SG_INPUT, SG_INFO, inputDevice->class_id << "::AddDevice '" << inputDevice->GetUniqueName() << "' s/n: " << inputDevice->GetSerialNumber());
return deviceNode->getIndex();
}
@@ -158,9 +158,8 @@ void FGEventInput::RemoveDevice(unsigned index)
FGInputDevice* inputDevice = inputDevices[index];
if (inputDevice) {
SG_LOG(SG_INPUT, SG_DEBUG, "\tremoving (" << index << ") " << inputDevice->GetUniqueName());
- inputDevice->Close();
+ inputDevice->doClose();
inputDevices.erase(index);
- delete inputDevice;
}
deviceNode = baseNode->removeChild("device", index);
}
diff --git a/src/Input/FGEventInput.hxx b/src/Input/FGEventInput.hxx
index 9773e1406..393a38bf6 100644
--- a/src/Input/FGEventInput.hxx
+++ b/src/Input/FGEventInput.hxx
@@ -41,10 +41,10 @@ protected:
const char* filePath;
const char* propertyRoot;
- unsigned AddDevice(FGInputDevice* inputDevice);
+ unsigned AddDevice(FGInputDevice_ptr inputDevice);
void RemoveDevice(unsigned index);
- std::map<int, FGInputDevice*> inputDevices;
+ std::map<int, FGInputDevice_ptr> inputDevices;
FGDeviceConfigurationMap configMap;
SGPropertyNode_ptr nasalClose;
diff --git a/src/Input/FGHIDDevice.hxx b/src/Input/FGHIDDevice.hxx
index 4de86c61c..dafee4dd5 100644
--- a/src/Input/FGHIDDevice.hxx
+++ b/src/Input/FGHIDDevice.hxx
@@ -36,7 +36,6 @@ public:
virtual ~FGHIDDevice();
bool Open() override;
- void Close() override;
void Configure(SGPropertyNode_ptr node) override;
void update(double dt) override;
@@ -84,6 +83,8 @@ public:
uint8_t reportIdForItem(const std::string& name) const;
private:
+ void Close() override;
+
class Report
{
public:
diff --git a/src/Input/FGHIDUsage.cxx b/src/Input/FGHIDUsage.cxx
index 515114884..cc772ab4f 100644
--- a/src/Input/FGHIDUsage.cxx
+++ b/src/Input/FGHIDUsage.cxx
@@ -321,32 +321,32 @@ std::string nameForUsage(uint32_t usagePage, uint32_t usage)
case LED_Remote: return "remote-led";
case LED_Forward: return "forward-led";
case LED_Reverse: return "reverse-led";
- case LED_Stop: return "stop=led";
+ case LED_Stop: return "stop-led";
case LED_Rewind: return "rewind-led";
case LED_FastForward: return "fastforward-led";
case LED_Play: return "play-led";
case LED_Pause: return "pause-led";
case LED_Record: return "record-led";
case LED_Error: return "error-led";
- case LED_UsageSelectedIndicator: return "usageselectedindicator-led";
- case LED_UsageInUseIndicator: return "usageinuseindicator-led";
- case LED_UsageMultiModeIndicator: return "usagemultimodeindicator-led";
- case LED_IndicatorOn: return "indicatoron-led";
- case LED_IndicatorFlash: return "idicatorflash-led";
+ case LED_UsageSelectedIndicator: return "selected-led";
+ case LED_UsageInUseIndicator: return "in-use-led";
+ case LED_UsageMultiModeIndicator: return "mode-led";
+ case LED_IndicatorOn: return "on-led";
+ case LED_IndicatorFlash: return "flash-led";
case LED_IndicatorSlowBlink: return "indicatorslowblink-led";
case LED_IndicatorFastBlink: return "indicatorfastblink-led";
- case LED_IndicatorOff: return "indicatoroff-led";
+ case LED_IndicatorOff: return "off-led";
case LED_FlashOnTime: return "flashontime-led";
case LED_SlowBlinkOnTime: return "slowblinkontime-led";
case LED_SlowBlinkOffTime: return "slowblinkofftime-led";
case LED_FastBlinkOnTime: return "fastblinkontime-led";
case LED_FastBlinkOfftime: return "fastblinkofftime-led";
- case LED_UsageIndicatorColor: return "usageindicatorcolor-led";
- case LED_IndicatorRed: return "usageindicatorred-led";
- case LED_IndicatorGreen: return "usageindicatorgreen-led";
- case LED_IndicatorAmber: return "usageindicatoramber-led";
- case LED_GenericIndicator: return "usagegenericindicator-led";
- case LED_SystemSuspend: return "usagesystemsuspend-led";
+ case LED_UsageIndicatorColor: return "color-led";
+ case LED_IndicatorRed: return "red-led";
+ case LED_IndicatorGreen: return "green-led";
+ case LED_IndicatorAmber: return "amber-led";
+ case LED_GenericIndicator: return "generic-led";
+ case LED_SystemSuspend: return "suspend-led";
case LED_ExternalPowerConnected: return "externalpowerconnected-led";
default:
SG_LOG(SG_INPUT, SG_WARN, "Unhandled HID LED usage:" << usage);
diff --git a/src/Input/FGInputDevice.cxx b/src/Input/FGInputDevice.cxx
index 624736803..4ed5ed4b9 100644
--- a/src/Input/FGInputDevice.cxx
+++ b/src/Input/FGInputDevice.cxx
@@ -7,10 +7,12 @@
#include <map>
#include <string>
+#include <vector>
#include <simgear/debug/ErrorReportingCallback.hxx>
#include <simgear/debug/debug_types.h>
#include <simgear/misc/strutils.hxx>
+#include <simgear/nasal/cppbind/Ghost.hxx>
#include <simgear/structure/exception.hxx>
#include <Main/fg_props.hxx>
@@ -34,6 +36,18 @@ void FGInputDevice::PrivateListener::valueChanged(SGPropertyNode* node)
}
FGInputDevice::~FGInputDevice()
+{
+ if (!deviceNode) {
+ return;
+ }
+
+ auto debug = deviceNode->getNode("debug-events");
+ if (debug) {
+ debug->removeChangeListener(_configListener.get());
+ }
+}
+
+void FGInputDevice::doClose()
{
auto nas = globals->get_subsystem<FGNasalSys>();
if (nas && deviceNode) {
@@ -48,12 +62,19 @@ FGInputDevice::~FGInputDevice()
nas->deleteModule(nasalModule.c_str());
}
- auto debug = deviceNode->getNode("debug-events");
- if (debug) {
- debug->removeChangeListener(_configListener.get());
- }
+ // call our virtual method
+ Close();
}
+static naRef createNasalGhost(FGInputDevice_ptr ref, naContext c)
+{
+ using NasalInputDevice = nasal::Ghost<FGInputDevice_ptr>;
+
+ // We need a non-const shared pointer for the ghost system
+ return NasalInputDevice::makeGhost(c, ref);
+}
+
+
void FGInputDevice::Configure(SGPropertyNode_ptr aDeviceNode)
{
deviceNode = aDeviceNode;
@@ -100,19 +121,56 @@ void FGInputDevice::Configure(SGPropertyNode_ptr aDeviceNode)
lastEventValue = deviceNode->getNode("last-event")->getNode("value", true);
lastEventValue->setDoubleValue(0.0);
+ auto node = deviceNode->getNode("debug-events", true);
+ node->addChangeListener(_configListener.get());
+
SGPropertyNode_ptr nasal = deviceNode->getNode("nasal");
- if (nasal) {
+ auto nas = globals->get_subsystem<FGNasalSys>();
+ const bool haveUpdate = nasal && nasal->hasChild("update");
+ const bool haveOpenOrClose = nasal && (nasal->hasChild("open") || nasal->hasChild("close"));
+ if (nas && (haveUpdate || haveOpenOrClose)) {
+ // pre-create the module hash, so device property is available
+ // immediately, eg during <open> code
+ naContext c = naNewContext();
+ naRef module = nas->getModule(nasalModule, true /*create*/);
+ naRef ghost = createNasalGhost(this, c);
+ nasal::Hash moduleHash(module, c);
+ moduleHash.set("device", ghost);
+ naFreeContext(c);
+ }
+
+ if (nas && haveUpdate) {
+ const auto updateCode = nasal->getChild("update");
+ const auto loc = updateCode->getLocation();
+ _postUpdateCallback = nas->createCode(updateCode->getStringValue(), loc.getPath(), loc.getLine());
+ if (_postUpdateCallback.getErrors().size() > 0) {
+ simgear::reportFailure(simgear::LoadFailure::Misconfigured,
+ simgear::ErrorCode::InputDeviceConfig,
+ "Failed to compile update callback for device"s + _postUpdateCallback.getErrors().front(),
+ sg_location(loc));
+ _postUpdateCallback = {};
+ }
+ }
+}
+
+void FGInputDevice::postOpen()
+{
+ SGPropertyNode_ptr nasal = deviceNode->getNode("nasal");
+ auto nas = globals->get_subsystem<FGNasalSys>();
+
+ if (nasal && nas) {
SGPropertyNode_ptr open = nasal->getNode("open");
if (open) {
const string s = open->getStringValue();
- auto nas = globals->get_subsystem<FGNasalSys>();
- if (nas)
- nas->createModule(nasalModule.c_str(), nasalModule.c_str(), s.c_str(), s.length(), deviceNode);
+ bool ok = nas->createModule(nasalModule.c_str(), nasalModule.c_str(), s.c_str(), s.length(), deviceNode);
+ if (!ok) {
+ simgear::reportFailure(simgear::LoadFailure::Misconfigured,
+ simgear::ErrorCode::InputDeviceConfig,
+ "Failed to load device Nasal",
+ sg_location(open));
+ }
}
}
-
- auto node = deviceNode->getNode("debug-events", true);
- node->addChangeListener(_configListener.get());
}
void FGInputDevice::AddHandledEvent(FGInputEvent_ptr event)
@@ -123,11 +181,25 @@ void FGInputDevice::AddHandledEvent(FGInputEvent_ptr event)
}
}
+naRef FGInputDevice::getModule()
+{
+ auto nas = globals->get_subsystem<FGNasalSys>();
+ if (!nas) {
+ return naRef();
+ }
+
+ return nas->getModule(nasalModule, false /*don't create*/);
+}
+
void FGInputDevice::update(double dt)
{
- for (map<string, FGInputEvent_ptr>::iterator it = handledEvents.begin(); it != handledEvents.end(); it++)
- (*it).second->update(dt);
+ for (auto it : handledEvents) {
+ it.second->update(dt);
+ }
+ naRef module = naNil();
+
+ bool didSend = false;
for (auto r : reportSettings) {
if (r->hasError()) {
continue;
@@ -135,7 +207,11 @@ void FGInputDevice::update(double dt)
try {
if (r->Test()) {
- auto reportData = r->reportBytes(nasalModule);
+ if (naIsNil(module)) {
+ module = getModule();
+ }
+
+ auto reportData = r->reportBytes(module);
if (debugEvents) {
SG_LOG(SG_INPUT, SG_INFO, class_id << " " << GetUniqueName() << ": Sending report " << r->getReportId() << simgear::strutils::encodeHex(reportData));
}
@@ -144,6 +220,8 @@ void FGInputDevice::update(double dt)
} else {
SendOutputReport(r->getReportId(), reportData);
}
+
+ didSend = true;
}
} catch (sg_exception& e) {
r->markAsError();
@@ -153,6 +231,24 @@ void FGInputDevice::update(double dt)
e.getLocation());
}
} // of report setting iteration
+
+ if (didSend && _postUpdateCallback.isValid()) {
+ try {
+ if (naIsNil(module)) {
+ module = getModule();
+ }
+
+ _postUpdateCallback.callWithLocals(module);
+ } catch (sg_exception& e) {
+ simgear::reportFailure(simgear::LoadFailure::Unknown,
+ simgear::ErrorCode::InputDeviceConfig,
+ "Failed to execute post-update callback:"s + e.getMessage(),
+ e.getLocation());
+
+ // FIXME
+ //_postUpdateCallback.reset();
+ }
+ }
}
void FGInputDevice::HandleEvent(FGEventData& eventData)
diff --git a/src/Input/FGInputDevice.hxx b/src/Input/FGInputDevice.hxx
index 4c679ccc9..a6d712ca4 100644
--- a/src/Input/FGInputDevice.hxx
+++ b/src/Input/FGInputDevice.hxx
@@ -13,6 +13,7 @@
#include <string>
#include <simgear/io/lowlevel.hxx>
+#include <simgear/nasal/cppbind/NasalCode.hxx>
#include <simgear/props/props.hxx>
#include <simgear/structure/SGReferenced.hxx>
#include <simgear/structure/SGSharedPtr.hxx>
@@ -29,7 +30,6 @@ public:
virtual ~FGInputDevice();
virtual bool Open() = 0;
- virtual void Close() = 0;
virtual void Send(const char* eventName, double value) = 0;
@@ -81,9 +81,14 @@ public:
void SetDebugEvents(bool debug);
+ void postOpen();
+ void doClose();
+
protected:
FGInputDevice(std::string aName, std::string aSerial = {});
+ virtual void Close() = 0;
+
// A map of events, this device handles
std::map<std::string, FGInputEvent_ptr> handledEvents;
@@ -130,6 +135,10 @@ protected:
};
std::unique_ptr<PrivateListener> _configListener;
+
+ nasal::NasalCode _postUpdateCallback;
+
+ naRef getModule();
};
typedef SGSharedPtr<FGInputDevice> FGInputDevice_ptr;
diff --git a/src/Input/FGLinuxEventInput.cxx b/src/Input/FGLinuxEventInput.cxx
index bbd816632..dbc177e9c 100644
--- a/src/Input/FGLinuxEventInput.cxx
+++ b/src/Input/FGLinuxEventInput.cxx
@@ -515,7 +515,7 @@ void FGLinuxEventInput::update( double dt )
// the pollfd array by filling in the file descriptor
struct pollfd fds[inputDevices.size()];
std::map<int,FGLinuxInputDevice*> devicesByFd;
- std::map<int,FGInputDevice*>::const_iterator it;
+ std::map<int, FGInputDevice_ptr>::const_iterator it;
int i;
for( i=0, it = inputDevices.begin(); it != inputDevices.end(); ++it, i++ ) {
FGInputDevice* p = (*it).second;
diff --git a/src/Input/FGLinuxEventInput.hxx b/src/Input/FGLinuxEventInput.hxx
index 15767ac59..dfa33016c 100644
--- a/src/Input/FGLinuxEventInput.hxx
+++ b/src/Input/FGLinuxEventInput.hxx
@@ -31,7 +31,6 @@ public:
virtual ~FGLinuxInputDevice();
bool Open() override;
- void Close() override;
void Send( const char * eventName, double value ) override;
const char * TranslateEventName( FGEventData & eventData ) override;
@@ -43,6 +42,8 @@ public:
double Normalize( struct input_event & event );
private:
+ void Close() override;
+
std::string devfile;
std::string devpath;
int fd {-1};
diff --git a/src/Input/FGReportSetting.cxx b/src/Input/FGReportSetting.cxx
index 3bb8a5436..030a5ee0c 100644
--- a/src/Input/FGReportSetting.cxx
+++ b/src/Input/FGReportSetting.cxx
@@ -103,15 +103,9 @@ bool FGReportSetting::Test()
return d;
}
-simgear::UInt8Vector FGReportSetting::reportBytes(const std::string& moduleName) const
+simgear::UInt8Vector FGReportSetting::reportBytes(naRef module) const
{
- auto nas = globals->get_subsystem<FGNasalSys>();
- if (!nas) {
- return {};
- }
-
- naRef locals = nas->getModule(moduleName, true /*create*/);
- naRef result = nasalCode.callWithLocals(locals);
+ naRef result = nasalCode.callWithLocals(module);
if (naIsString(result)) {
size_t len = naStr_len(result);
diff --git a/src/Input/FGReportSetting.hxx b/src/Input/FGReportSetting.hxx
index 6adb35f8a..f2099ac29 100644
--- a/src/Input/FGReportSetting.hxx
+++ b/src/Input/FGReportSetting.hxx
@@ -66,7 +66,7 @@ public:
}
bool Test();
- simgear::UInt8Vector reportBytes(const std::string& moduleName) const;
+ simgear::UInt8Vector reportBytes(naRef module) const;
virtual void valueChanged(SGPropertyNode* node);
protected:
diff --git a/src/Scripting/CMakeLists.txt b/src/Scripting/CMakeLists.txt
index 27763457b..4529b7d98 100644
--- a/src/Scripting/CMakeLists.txt
+++ b/src/Scripting/CMakeLists.txt
@@ -19,6 +19,7 @@ set(SOURCES
NasalFlightPlan.cxx
ScriptBinding.cxx
sqlitelib.cxx
+ NasalInput.cxx
# we don't add this here because we need to exclude it in the testSuite
# so it can't go nto fgfsObjects library
# NasalUnitTesting.cxx
@@ -40,6 +41,7 @@ set(HEADERS
NasalTranslations.hxx
NasalFlightPlan.hxx
ScriptBinding.hxx
+ NasalInput.hxx
)
if(WIN32)
diff --git a/src/Scripting/NasalInput.cxx b/src/Scripting/NasalInput.cxx
new file mode 100644
index 000000000..eb5ab62dc
--- /dev/null
+++ b/src/Scripting/NasalInput.cxx
@@ -0,0 +1,84 @@
+// Expose Input module to Nasal
+//
+// SPDX-FileCopyrightText: 2026 James Turner
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "config.h"
+
+#include "NasalInput.hxx"
+
+#include <Input/FGInputDevice.hxx>
+
+#include <simgear/nasal/cppbind/Ghost.hxx>
+#include <simgear/nasal/cppbind/NasalHash.hxx>
+#include <simgear/nasal/cppbind/from_nasal.hxx>
+#include <simgear/nasal/cppbind/to_nasal.hxx>
+
+
+static simgear::UInt8Vector dataFromString(const std::string& str)
+{
+ return simgear::UInt8Vector(
+ reinterpret_cast<const uint8_t*>(str.data()),
+ reinterpret_cast<const uint8_t*>(str.data() + str.size()));
+}
+
+static simgear::UInt8Vector dataFromArg(const nasal::CallContext& ctx, size_t index)
+{
+ if (ctx.isString(index)) {
+ std::string str = ctx.getArg<std::string>(index);
+ return dataFromString(str);
+ } else if (ctx.isVector(index)) {
+ std::vector<int> ints = ctx.getArg<std::vector<int>>(index);
+ simgear::UInt8Vector data;
+ data.reserve(ints.size());
+ for (int v : ints) {
+ if (v < 0 || v > 255) {
+ ctx.runtimeError("data vector contains out-of-range value: %d, (must be 0-255)", v);
+ }
+
+ data.push_back(static_cast<uint8_t>(v));
+ }
+ return data;
+ } else {
+ ctx.runtimeError("data argument must be a string or a vector of integers");
+ return {};
+ }
+}
+
+static naRef f_sendFeatureReport(FGInputDevice& device, const nasal::CallContext& ctx)
+{
+ if (ctx.argc < 2) {
+ ctx.runtimeError("sendFeatureReport(reportId, data) requires 2 arguments");
+ }
+
+ unsigned int reportId = ctx.requireArg<unsigned int>(0);
+ simgear::UInt8Vector data = dataFromArg(ctx, 1);
+ device.SendFeatureReport(reportId, data);
+ return naNil();
+}
+
+static naRef f_sendOutputReport(FGInputDevice& device, const nasal::CallContext& ctx)
+{
+ if (ctx.argc < 2) {
+ ctx.runtimeError("sendOutputReport(reportId, data) requires 2 arguments");
+ }
+
+ unsigned int reportId = ctx.requireArg<unsigned int>(0);
+ simgear::UInt8Vector data = dataFromArg(ctx, 1);
+
+ device.SendOutputReport(reportId, data);
+ return naNil();
+}
+
+//------------------------------------------------------------------------------
+naRef initNasalInput(naRef globals, naContext c)
+{
+ using InputDeviceRef = SGSharedPtr<FGInputDevice>;
+ using NasalInputDevice = nasal::Ghost<InputDeviceRef>;
+
+ NasalInputDevice::init("FGInputDevice")
+ .method("sendFeatureReport", &f_sendFeatureReport)
+ .method("sendOutputReport", &f_sendOutputReport);
+
+ return naNil();
+}
diff --git a/src/Scripting/NasalInput.hxx b/src/Scripting/NasalInput.hxx
new file mode 100644
index 000000000..4fa7f4783
--- /dev/null
+++ b/src/Scripting/NasalInput.hxx
@@ -0,0 +1,10 @@
+//@file Expose Input module to Nasal
+//
+// SPDX-FileCopyrightText: 2026 James Turner
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <simgear/nasal/nasal.h>
+
+naRef initNasalInput(naRef globals, naContext c);
diff --git a/src/Scripting/NasalSys.cxx b/src/Scripting/NasalSys.cxx
index e1e42b0cf..9027e284b 100644
--- a/src/Scripting/NasalSys.cxx
+++ b/src/Scripting/NasalSys.cxx
@@ -49,10 +49,12 @@
#include "NasalCondition.hxx"
#include "NasalFlightPlan.hxx"
#include "NasalHTTP.hxx"
+#include "NasalInput.hxx"
#include "NasalPositioned.hxx"
#include "NasalSGPath.hxx"
#include "NasalString.hxx"
#include "NasalSys.hxx"
+
#include "NasalSys_private.hxx"
#include "NasalTranslations.hxx"
#include "NasalUnitTesting.hxx"
@@ -1127,6 +1129,8 @@ void FGNasalSys::init()
NewGUI::registerNasalBindings(this);
}
+ initNasalInput(d->_globals, d->_context);
+
NasalTimerObj::init("Timer")
.method("start", &TimerObj::start)
.method("stop", &TimerObj::stop)
@@ -1148,6 +1152,10 @@ void FGNasalSys::init()
return;
}
+ if (fgGetString("/sim/nasal-load-priority/file[0]") != "props.nas") {
+ SG_LOG(SG_NASAL, SG_DEV_ALERT, "Nasal loadpriority.xml not included, Nasal loading will fail");
+ }
+
flightgear::initNasalTranslations(d->_globals, d->_context);
flightgear::addons::initAddonClassesForNasal(d->_globals, d->_context);
@@ -1524,7 +1532,6 @@ bool FGNasalSys::createModule(const char* moduleName, const char* fileName,
if (naIsNil(d->_globals))
return false;
- bool didCreateModule = false;
if (!naHash_get(d->_globals, modname, &locals)) {
// if we are re-creating the module for canvas, ensure the C++
// pieces are re-defined first. As far as I can see, Canvas is the only
@@ -1536,7 +1543,6 @@ bool FGNasalSys::createModule(const char* moduleName, const char* fileName,
} else {
locals = naNewHash(ctx);
}
- didCreateModule = true;
}
// store the filename in the module hash, so we could reload it
@@ -1550,10 +1556,16 @@ bool FGNasalSys::createModule(const char* moduleName, const char* fileName,
d->_cmdArg = (SGPropertyNode*)cmdarg;
callWithContext(ctx, code, argc, args, locals);
- if (didCreateModule) {
- hashset(d->_globals, moduleName, locals);
+ if (const char* error = naGetError(ctx)) {
+ int line = naGetLine(ctx, 0);
+ char* file = naStr_data(naGetSourceFile(ctx, 0));
+ SG_LOG(SG_NASAL, SG_ALERT, "Error loading module '" << moduleName << "': " << error << " (at " << file << ":" << line << ")");
+
+ return false;
}
+ hashset(d->_globals, moduleName, locals);
+
naFreeContext(ctx);
return true;
}
@@ -1631,8 +1643,11 @@ naRef FGNasalSys::getModule(const std::string& moduleName, bool create) const
{
naRef mod = naHash_cget(d->_globals, (char*)moduleName.c_str());
if (naIsNil(mod) && create) {
+ naRef modname = naNewString(d->_context);
+ naStr_fromdata(modname, moduleName.data(), moduleName.size());
+
mod = naNewHash(d->_context);
- naHash_cset(d->_globals, (char*)moduleName.c_str(), mod);
+ naHash_set(d->_globals, modname, mod);
}
return mod;
}
diff --git a/test_suite/CMakeLists.txt b/test_suite/CMakeLists.txt
index 5dae5970f..b9e82b44b 100644
--- a/test_suite/CMakeLists.txt
+++ b/test_suite/CMakeLists.txt
@@ -14,7 +14,10 @@ include(SetupFGFSLibraries)
# u unit tests, s system tests, m simgear unit tests
function(fg_add_test_suite SUITE_NAME TYPE)
add_test(NAME "${SUITE_NAME}"
- COMMAND fgfs_test_suite --ctest -${TYPE} "${SUITE_NAME}")
+ COMMAND fgfs_test_suite --ctest -${TYPE} "${SUITE_NAME}" --fg-root ${FG_DATA_DIR}
+ )
+
+
endfunction()
# The test suite output directory.
diff --git a/test_suite/FGTestApi/testGlobals.cxx b/test_suite/FGTestApi/testGlobals.cxx
index baa8675d9..20121a31f 100644
--- a/test_suite/FGTestApi/testGlobals.cxx
+++ b/test_suite/FGTestApi/testGlobals.cxx
@@ -78,6 +78,10 @@ void initTestGlobals(const std::string& testName, const std::string& language,
fgSetDefaults();
+ // load Nasal load priority, which is needed for 'full' NasalSys init
+ auto nasalLoadPriority = globals->get_props()->getNode("/sim/nasal-load-priority", true);
+ readProperties(globals->get_fg_root() / "Nasal/loadpriority.xml", nasalLoadPriority);
+
auto t = globals->get_subsystem_mgr()->add<TimeManager>();
t->bind();
t->init(); // establish mag-var data
diff --git a/test_suite/unit_tests/Input/test_inputDevice.cxx b/test_suite/unit_tests/Input/test_inputDevice.cxx
index 358ed4a5e..68b1f6a82 100644
--- a/test_suite/unit_tests/Input/test_inputDevice.cxx
+++ b/test_suite/unit_tests/Input/test_inputDevice.cxx
@@ -35,7 +35,15 @@ public:
explicit TestInputDevice(const std::string& name) : FGInputDevice(name) {}
bool Open() override { return true; }
- void Close() override {}
+
+ void Close() override
+ {
+ _closeCalled = true;
+ // Record whether the feature report was already populated when Close()
+ // was invoked — used by testNasalClose to verify ordering.
+ _reportSetAtCloseTime = (_lastFeatureReportId != 0);
+ }
+
void Send(const char* /*eventName*/, double /*value*/) override {}
const char* TranslateEventName(FGEventData& /*eventData*/) override
@@ -54,21 +62,55 @@ public:
_lastOutputReportData = data;
}
+ void SendFeatureReport(unsigned int reportId, const simgear::UInt8Vector& data) override
+ {
+ _lastFeatureReportId = reportId;
+ _lastFeatureReportData = data;
+ }
+
void clearReport()
{
_lastOutputReportId = 0;
_lastOutputReportData.clear();
+ _lastFeatureReportId = 0;
+ _lastFeatureReportData.clear();
}
unsigned int getLastOutputReportId() const { return _lastOutputReportId; }
const simgear::UInt8Vector& getLastOutputReportData() const { return _lastOutputReportData; }
+ unsigned int getLastFeatureReportId() const { return _lastFeatureReportId; }
+ const simgear::UInt8Vector& getLastFeatureReportData() const { return _lastFeatureReportData; }
+
+ bool wasCloseCalled() const { return _closeCalled; }
+ bool wasReportSetAtCloseTime() const { return _reportSetAtCloseTime; }
private:
std::string _translatedName;
unsigned int _lastOutputReportId = 0;
simgear::UInt8Vector _lastOutputReportData;
+ unsigned int _lastFeatureReportId = 0;
+ simgear::UInt8Vector _lastFeatureReportData;
+ bool _closeCalled = false;
+ bool _reportSetAtCloseTime = false;
};
+// ---------------------------------------------------------------------------
+// Helper to initialize the full Nasal subsystem for tests that need it.
+// Callers should reset global_nasalMinimalInit in their tearDown if needed.
+// ---------------------------------------------------------------------------
+static void initNasalForTest()
+{
+ fgInitAllowedPaths();
+ globals->get_props()->getNode("nasal", true);
+ globals->get_subsystem_mgr()->add<FGInterpolator>();
+ globals->get_subsystem_mgr()->bind();
+ globals->get_subsystem_mgr()->init();
+
+ global_nasalMinimalInit = false;
+ globals->get_subsystem_mgr()->add<FGNasalSys>();
+ globals->get_subsystem_mgr()->postinit();
+}
+
// ---------------------------------------------------------------------------
// Helpers to build a device property node from an XML snippet and configure
// a TestInputDevice with it. The device node lives in a detached property
@@ -77,14 +119,14 @@ private:
static SGSharedPtr<TestInputDevice> makeDevice(const std::string& name,
const std::string& xmlSnippet)
{
- auto* device = new TestInputDevice(name);
+ SGSharedPtr<TestInputDevice> device = new TestInputDevice(name);
device->SetUniqueName(name);
// Parse the XML snippet into a fresh, standalone property node
SGPropertyNode_ptr node = FGTestApi::propsFromString(xmlSnippet);
device->Configure(node);
- return SGSharedPtr<TestInputDevice>(device);
+ return device;
}
// ---------------------------------------------------------------------------
@@ -988,6 +1030,94 @@ void InputDeviceTests::testRepeatableWithLongPress()
CPPUNIT_ASSERT_EQUAL(0, _longPressCmd.callCount); // still only from scenario 1
}
+// ---------------------------------------------------------------------------
+// testNasalDevice
+//
+// Verify that sendFeatureReport() can be called from inside the <nasal><open>
+// block of a device XML config, and that the call reaches the device's
+// SendFeatureReport override.
+// ---------------------------------------------------------------------------
+void InputDeviceTests::testNasalDevice()
+{
+ initNasalForTest();
+
+ auto device = makeDevice("nasal-test-device", R"(
+ <PropertyList>
+ <nasal>
+ <open>
+ <![CDATA[
+ logprint(LOG_INFO, "In nasal open block");
+ device.sendFeatureReport(42, [10, 20, 30]);
+ logprint(LOG_INFO, "After sendFeatureReport in nasal open block");
+ ]]>
+ </open>
+ </nasal>
+ </PropertyList>
+ )");
+
+ CPPUNIT_ASSERT_EQUAL(0u, device->getLastFeatureReportId());
+ device->Open();
+ device->postOpen();
+
+ CPPUNIT_ASSERT_EQUAL(42u, device->getLastFeatureReportId());
+ auto bytes = device->getLastFeatureReportData();
+ CPPUNIT_ASSERT_EQUAL(size_t(3), bytes.size());
+ CPPUNIT_ASSERT_EQUAL(static_cast<uint8_t>(10), bytes[0]);
+ CPPUNIT_ASSERT_EQUAL(static_cast<uint8_t>(20), bytes[1]);
+ CPPUNIT_ASSERT_EQUAL(static_cast<uint8_t>(30), bytes[2]);
+}
+
+// ---------------------------------------------------------------------------
+// testNasalClose
+//
+// Verify that the <nasal><close> callback fires *before* the virtual Close()
+// method. The nasal block calls device.sendFeatureReport(), which updates
+// the TestInputDevice's internal state. Close() records whether that state
+// was already populated when it ran. If nasal fired first the flag will be
+// true; if Close() ran first it would be false.
+// ---------------------------------------------------------------------------
+void InputDeviceTests::testNasalClose()
+{
+ initNasalForTest();
+
+ auto device = makeDevice("nasal-close-device", R"(
+ <PropertyList>
+ <nasal>
+ <close>
+ <![CDATA[
+ # This executes before the virtual Close() call.
+ # Calling sendFeatureReport() here verifies the device API is
+ # still available (i.e. Close() has not yet severed the link).
+ device.sendFeatureReport(13, [0xAA, 0xBB, 0xCC]);
+ ]]>
+ </close>
+ </nasal>
+ </PropertyList>
+ )");
+
+ // Sanity: no report sent yet, Close() not yet called
+ CPPUNIT_ASSERT_EQUAL(0u, device->getLastFeatureReportId());
+ CPPUNIT_ASSERT(!device->wasCloseCalled());
+
+ device->doClose();
+
+ // The nasal <close> block called sendFeatureReport(13, .....
[truncated message content] |