51st-Vfw / JAFDTC

JAFDTC DTC tool for DCS
GNU General Public License v3.0
0 stars 1 forks source link

[Proposal] Add a data gathering step before the avionics setup #15

Closed fizzle77 closed 6 months ago

fizzle77 commented 7 months ago

Following up on https://github.com/51st-Vfw/JAFDTC/pull/12#issuecomment-2094008993, where I mentioned a desire to make decisions based on data scraped from cockpit displays. I have a better understanding now why that's not at all how the system is designed today and how difficult that would be.

What I'm thinking about as an alternative is adding an initial "button mashing" step, the purpose of which is to discover things about the jet's current configuration.

My specific use case is for weapons loadout. I want to get all the info I need and write those details to JSON, then read that JSON (or whatever) as I build the actual setup. Without this, the only path is to have JAFDTC DSMS configs that correspond 1:1 with a weapons loadout. With 11 pylons and lots of possibilities in the Hawg, that really limits its utility. It would be way better to have weapon-specific settings that are applied if and where they're loaded. E.g. if there are GBU-54s anywhere on the jet, here's how I want them configured. If I parse the DSMS INV to know what's loaded where, this is doable.

It feels like this would be useful in other scenarios, even the really simple one I initially mentioned, knowing where the COMM page is. This would let JAFDTC be more resilient to atypical starting states, though it would take some effort jet by jet and that's not my main intent.

Roughly, I expect this would be an additional method on IUploadAgent and UploadAgentBase that would (for an airframe that implemented it) execute exploratory commands built via the BuilderBase family. The contract would be that the executed lua functions would leave behind a (probably) JSON file. BuilderBase or inheritors would read the JSON file, clean it up, and provide that data for use in the existing build and load steps.

What do you think? Zero expectation for quick feedback, just getting it down while it's in my head.

ilominar commented 6 months ago

See ilominar_preflight_builds, for an initial partially complete mock up that should work within the existing framework.

On the DCS side, you'd define

JAFDTC_A10C_Func_MyStateQuery(param1, param2)
  local response = "Hello World!"
  return response
end

response could be JSON, though it is going to be encapsulated as a string value in the JSON the telemetry provides back to JAFDTC, so you'd need to escape the crap out of it. Probably easier to adopt something like "KEY1=VAL1;KEY2=VAL2" more string-y format.

On the JAFDTC side, you'd define an IBuilder and supply it through IUploadAgent::PreflightBuilder, build method would look something like this:

public void Build()
{
  // this is not defined yet, but looks a lot like AddRunFunction() in BuilderBase
  AddQuery("MyStateQuery", new() { "param1", "param2" });
}

IUploadAgent::Load moves to async as first does a query/response path (if there is a IUploadAgent::PreflightBuilder defined) before invoking the current Build sequence. Probably need to change the signature of Build to be something like this

public void Build(string preflightResponse = null);

The way this works behind the scenes, is the handler for AddQuery on the DCS side (something like existing JAFDTC_Cmd_RunFunc) will capture the return value from the invoked function and send it back in the Response field of the regular telemetry stream (see TelemDataRx). This is a one-message-only thing. On the JAFDTC end, Load subscribes to an event that is tripped on message ingress from DCS if there is a response valid (this event is one-shot). Load code now sends the AddQuery and waits for the response to trip the JAFDTC-side event, gathers the response, and proceeds with the normal build.

Format and processing of the response is left up to the airframe. Only constraint is it has to be encodable as a string value in the telemetry stream coming back from DCS (see LuaExportAfterNextFrame in JAFDTC.lua).

Note that this is going to be a one-time thing, not an arbitrary back-and-forth between DCS and JAFDTC (doing that, I think, would require refactoring/redesigning the networking part, and honestly, not sure the complexity is worth the additional flexibility).

Would that work?

fizzle77 commented 6 months ago

Would that work?

Yep this looks great. I'll experiment with it this week.

ilominar commented 6 months ago

Ok. Will finish it up then.

As a head’s up, I’m going to refactor the upload / Lua stack to clean up a bunch of things that have been annoying me for a while. Figure it’s finally time since the preflight stuff is going to require some interfaces to change and that’s going to touch a lot. May as well clean up while I’m at it.

Also, now thinking back and forth will be possible within some constraints without much additional effort. Plan to plumb the upload agent to support that.

fizzle77 commented 6 months ago

FWIW I think the single-pass state gathering you described is just as effective for what I had in mind, so don't add the back-and-forth on my account. If you have a use case for it or want it for some other reason, knock yourself out.

ilominar commented 6 months ago

It's in and working, supporting multiple passes. Pushed to branch. Turns out going that direction made things less invasive than expected.

I'm going to do some more checks, then will merge. There will then be another clean up pass on all of the Lua and some of the low-level builder stuff for a clean up.

ilominar commented 6 months ago

Merged, see 249abff.

fizzle77 commented 6 months ago

So it looks like there's no way to build up and run a set of cockpit actions before your query. Is that right or am I missing something? I thought this might resemble CoreSetupBuilder except that it would culminate with (or perhaps allow interspersed calls to) a synchronous query function returning a string. This would let you get cockpit displays in a state to be parsed and returned.

You sealed CoreQueryBuilder so overriding its Build or otherwise messing with it to accomplish this does not look appropriate. I could perhaps perform cockpit actions from the _Fn_ query lua function? Probably with calls to JAFDTC_Core_PerformAction()? But that seems like hard-mode and also unlikely to be your intent.

A nifty shape for this would be for each system builder to have its own (optional, if implemented) state-collecting routine, executed before Build(). In it the implementer would build up actions like we do now, but every time we hit a Query call we'd synchronously execute the actions to that point, call the query lua, and give the implementer the opportunity to parse the return and save the cockpit state info as appropriate to the system. Then the Build() routine could later make use of that data and configure accordingly.

I'd be happy to work on this myself if you're amenable. Or let you play around with it if you'd prefer. Or figure out some other alternative. Or to just do the thing you intended here if I've missed it. 😄

ilominar commented 6 months ago

I think the current design is fine and capable of doing what you're after? But I may be missing something. It may not be intuitive though to others not in my head.

You are correct in that you cannot, at command processing time on the Lua end, interleave queries and commands. As far as I can tell, I don't think that's a big deal or limiting. UploadAgentBase:Load gives the game away:

        public async Task<bool> Load()
        {
            StringBuilder sb = new();
            SetupBuilder(sb).Build();
            await Task.Run(() => BuildSystems(sb));
            if (sb.Length > 0)
            {
                TeardownBuilder(sb).Build();
            }
            return SendCommandsToDCS(sb);
        }

What we have is a two-phase operation (let's ignore queries for the moment):

  1. Figure out what commands to send to the jet and accumulate them into a StringBuilder
  2. Send the contents of the StringBuilder to DCS to "execute"

The Build methods are not incrementally sending the command stream to DCS, they are assembling a sequence that will be sent in a single pass to DCS after the entire sequence is fully assembled. I don't really see a huge value in making this incremental, given complexity TBH (and it would break some UI things like DCS-side progress indications that rely on knowing how much work is to be done up front).

Now, throwing queries into the mix, they operate outside of (1) and (2) above (there are a bunch of reasons I believe they need to do this, some boiling down to the way DCS and JAFDTC communicate along with limitations on how the DCS side works, some boiling down to not adding complexity that isn't super useful).

I have assumed here that a use case of "put the display into a state to read it out" prior to building the sequence doesn't make sense. That is, why not just read the state of the display at the get-go and then build the right sequence? This does also assume that things are deterministic. That is, you know that Y is the next state from X if you provide input A. That seemed reasonable---even if A is context sensitive, there must be some state that tips off what A is going to do

Queries, then, would direct assembly in (1). But they do so at compile time (so-to-speak), not run-time. There are already run-time mechanisms available to direct command streams (e.g., If/While/Exec) and queries in that run-time context make no sense (since by the time you did a query at run-time, JAFDTC will have already assembled the command sequence, uploaded it, and moved on---the upload is fire-and-forget from JAFDTC's perspective). With queries, the sequence becomes this,

  1. Send a query and wait for a response
  2. Use the response to that query to direct figuring out what commands to send to the jet and accumulate them into a StringBuilder
  3. Send the contents of the StringBuilder to DCS to "execute"

As it turns out, you can lather/rinse/repeat steps 1 and 2 multiple times. Though the only value of that lies in breaking the state delivery up into smaller chunks that maybe align with systems. Again, dynamically influencing what actions Lua takes at execution time can already be handled without state queries. Given that, it doesn't really make sense to have anything other than a single query command in a query (but this command can call different queries based on a unique name). Thus CoreQueryBuilder is sealed (it certainly could be unsealed and we could provide a virtual function to vend the right object like SetupBuilder, but I've yet to see a point to doing so) as you really only need what the base implementation does. FWIW, I am going to wrap the Task.Run stuff in a nicer API to clean things up, though.

So, what models does this allow. Let's say you have a jet X with system A and B. We could do something like this in X's specialization of UploadAgentBase:

public override void BuildSystems(StringBuilder sb)
{
    string stateRaw = Query("GetStateOfX", new() { "all", "the", "state" });
    object state = ParseStateForXSysA(stateRaw);
    new ASystemBuilder(_cfg, _dcsCmds, sb, state).Build();
    new BSystemBuilder(_cfg, _dcsCmds, sb, state).Build();
}

Here, Query is the cleaned up version of the Task.Run sequence. In this case, you grab some state and the builders for the systems use that state to direct what they come up with (like, "if CMDS is off, add action to turn it on"). The system builders now need the state as a parameter (I considered passing it as a parameter to Build but just went with a constructor argument since you're going to end up parking that in a property anyway). Or you could do something like this in the system builders,

public override void Build()
{
    if (!_cfg.IsDefault)
    {
        string stateRaw = MyUploadAgent.Query("GetStateOfX_SysA", new() { "just", "for", "A" });
        object state = ParseStateForXSysA(stateRaw);
        if ((state.MaximumEffect == "On") && (_cfg.A.Knob12 == "1"))
        {
            AddAction(Panel, Button);
        }
    }
}

In this case, we have to push the UploadAgent instance down into the builder (where it lands in the MyUploadAgent property). Both of these solutions could also be done without queries at all by placing the C# logic that assembles the commands in Lua on the DCS side. In the previous case, the build code would look the same, but the query and parsing would already be done.

You could have the builder base class automagically do the query for you. I left that out because I figured the specific format of the reply and what you're parsing into are best left to the derived class and the reused code ends up being a line or two (once query gets cleaned up) but requires a bunch of scaffolding to generalize it to a base class (like, you now need a virtual way to get the query name and parameters).

fizzle77 commented 6 months ago

I think I'm following. I was aware of what you're calling compile-time vs. run-time here: that we're really just building a big string of commands that get executed all at once with only minimal run-time branching or looping. I understand that it doesn't allow for interleaved queries and commands.

The problem, which I think is still there, is that you can't get any cockpit state that's behind a button press or two. The state I need for the A-10 DSMS, the jet's loadout, is behind precisely two button clicks. One to bring up the DSMS and one to get to its INV page. I can't get what I need from the MFD until I've called up the inventory. The same would be true for the HMCS profile, which I'm hoping to do later.

I understand that I can repeatedly query then add compile-time steps, but that isn't helpful if the state is locked behind actions I need to take at run time. It that's my only option, I'd have to execute those clicks in a lua function, potentially the query function itself. I can do that if that was your intent. I'm still not sure that was your intent. :)

My original proposal, which I still think is viable, was to break the upload into two steps: one for gathering state and the second to perform setup. Both of these phases would use the existing compile-then-run process.

In the first, state gathering phase, you could make any changes in the cockpit you want, using all the nifty click tooling you've already built in C#. When you've added the steps to get to the needed MFD page, for example, you would specify executing a query command whose purpose is to return the relevant parts of that MFD's state to JAFDTC. You're still in compile time, we're not interleaving. You've merely added a query command. You could use the existing AddIf and AddWhile runtime branching/looping to assist as necessary. This phase executes its runtime, clicking. around and parsing display tables. The strings are returned and stashed away in JAFDTC. They could just be saved as string properties to be parsed in phase two, or we could pass delegates to nicely parse them into appropriate data structures.

Now you're in phase two, the setup phase, which is exactly what we have now. But the state gathering phase has provided everything we need to know about the state of the jet. We do all the same kinds of setup building commands we're doing today, but we know precisely what's where and can build exactly the right setup for that jet state. Here in C# at "compile time" we know precisely the correct steps to take without any branching at "run time."

I thought this was a relatively simple way to enable virtually all the functionality of fully interleaved queries and commands, without taking on all that work and complexity. Am I making sense?

fizzle77 commented 6 months ago

I have assumed here that a use case of "put the display into a state to read it out" prior to building the sequence doesn't make sense. That is, why not just read the state of the display at the get-go and then build the right sequence? This does also assume that things are deterministic. That is, you know that Y is the next state from X if you provide input A. That seemed reasonable---even if A is context sensitive, there must be some state that tips off what A is going to do

I should address this more specifically. This might be possible but it would be pretty gross. What I'm trying to do is allow building DSMS profiles in JAFDTC that don't map 1:1 with specific loadouts. Rather than building a configuration for a precise loadout across all 11 pylons, you can just define settings for each munition type, APKWS, GBU-54, Mk-82, etc. and JAFDTC will configure them that way wherever they're found on the jet.

Doing that level of branching with the existing AddIf would be tremendously complex. For each pylon, figure out which of the 10-20 possible munitions is there? Then branch off into the 5-10 clicks to configure that type? Possibly doable but really, really gross. It would be straightforward if I could gather the inventory first, then do all that logic in C#.

EDIT: I’ll give this a try tomorrow, just using AddWhen and AddIf to pull this off. Maybe it’s not quite as bad as I think!

ilominar commented 6 months ago

Ok, now fully understand what you're trying to do. Example crystalized it (shoulda asked for that first... :) ). Didn't clue in that you might have to mash buttons to make state discoverable via Lua.

Chewing on this, it might be straight-forward to support. Unseal CoreQueryBuilder, require derived Build implementations to call base.Build() prior to returning, put a CoreQueryBuilder on the interface to Query.

public override void BuildSystems(StringBuilder sb)
{
    // build query "preamble" command stream to put in front of a query
    StringBuilder sbQuery = new();
    IBuilder queryBuilder = new MyQueryBuilder(_cfg, _dcsCmds, sbQuery).Build();
    // appends query command on stream built into queryBuilder, passing null => just do query
    string response = Query(queryBuilder, "MyQueryFn", new() { "arg0" });

    // generate configuration commands as usual
    new SystemABuilder(_cfg, _dcsCmds, sb, response).Build();
    new SystemBBuilder(_cfg, _dcsCmds, sb, response).Build();
    new SystemCBuilder(_cfg, _dcsCmds, sb, response).Build();
}
fizzle77 commented 6 months ago

Quick and dirty proof of concept for ☝️ here.

It works! It's a little janky getting the query data into the system builder where you need it. And the system builder fundamentally knows whether or not a query should be run depending on e.g. non-default settings. Eventually pushing this down into the system builders feels like a win.

ilominar commented 6 months ago

Cool. I implemented the changes late last night, but haven't pushed the branch yet.

ilominar commented 6 months ago

Push down is roughed-in. There is a QueryBuilderBase IBuilder inheriting from BuilderBase that all query builders must derive from. This version of the builder adds a Query() method that submits the query and waits for the response. So, something like this,

public override void BuildSystems(StringBuilder sb)
{
    // build query command stream
    StringBuilder sbQuery = new();
    MyQueryBuilder queryBuilder = new (_cfg, _dcsCmds, sbQuery).Build();
    string response = queryBuilder.Query();

    // generate configuration commands as usual
    new SystemABuilder(_cfg, _dcsCmds, sb, response).Build();
    new SystemBBuilder(_cfg, _dcsCmds, sb, response).Build();
    new SystemCBuilder(_cfg, _dcsCmds, sb, response).Build();
}

You could also have MyQueryBuilder buit down in the System?Builders if you wanted.

fizzle77 commented 6 months ago

Great! Pretty psyched I got my roughed-in version working end to end today. Once your official method is in and I can migrate, it should be ready to merge.

ilominar commented 6 months ago

Merged. https://github.com/51st-Vfw/JAFDTC/commit/2228006fadc7bb522eb7a350655771b9adbeb8ea