dcs-bios / dcs-bios-arduino-library

Arduino Library to talk to DCS-BIOS
MIT License
61 stars 29 forks source link

Idea: Dynamic Aircraft Support #17

Closed OlHall closed 8 years ago

OlHall commented 9 years ago

I've been wondering about whether it's possible (assuming a suitably sized Arduino) to support multiple aircraft types efficiently in a single Sketch. I realise we could just define all controls for each aircraft, and let the addressing sort itself out, but that will also create a long linked list of controls in the Parser's ExportStreamListener which may not be ideal as it's iterated over in each DcsBiosWrite and again at the end of the frame.

Two questions: Will the aircraft name be updated every frame, or just once at the beginning of the sim session? If it's the former, we'd need to protect against re-initialising the aircraft on every frame...

Secondly, would dynamically creating the controls work?

Something like this...

#include <DcsBios.h>
#include <Servo.h>
#include <LiquidCrystal.h>

/**** Make your changes after this line ****/

// LCD Display
LiquidCrystal lcd(22, 24, 26, 28, 30, 32);

enum AircraftType {
  AcNone,
  AcHuey,
  AcA10c,
  AcKa50
};

DcsBios::StringBuffer<16> AcftNameBuffer(0x0000);
unsigned char AcftType = AcNone;

/**** In most cases, you do not have to change anything below this line ****/

/* Instantiate a ProtocolParser object to parse the DCS-BIOS export stream */
DcsBios::ProtocolParser parser;

void setup() {
  Serial.begin(250000);

  // Initialise the LCD
  lcdInit();
  lcdReset();
}

/*
Your main loop needs to pass data from the DCS-BIOS export
stream to the parser object you instantiated above.

It also needs to call DcsBios::PollingInput::pollInputs()
to detect changes in the state of connected controls and
pass them on to DCS.
*/
void loop() {
  // feed incoming data to the parser
  int data = Serial.read();
  while (data > -1) {
      parser.processChar(data);
      data = Serial.read();
  }

  // poll inputs
  DcsBios::PollingInput::pollInputs();
}

/*
You need to define
void sendDcsBiosMessage(const char* msg, const char* arg)
so that the string msg, followed by a space, the string arg
and a newline gets sent to the DCS-BIOS import stream.

In this example we send it to the serial port, so you need to
run socat to read the data from the serial port and send it
over UDP to DCS-BIOS.

If you are using an Ethernet Shield, you would probably want
to send a UDP packet from this subroutine.
*/
void sendDcsBiosMessage(const char* msg, const char* arg) {
  Serial.write(msg);
  Serial.write(' ');
  Serial.write(arg);
  Serial.write('\n');
}

/*
This subroutine gets called at the end of every update frame
from the export stream (you need to define it even if it
does nothing).

Use this to handle object updates which are not covered by the
DcsBios Arduino library (e.g. displays).
*/
void onDcsBiosFrameSync() {
  // Change of Aircraft
  if( AcftNameBuffer.isDirty() )
  {
    if(( strcmp( AcftNameBuffer.buffer, "UH-1H" ) == 0 ) && ( AcftType != AcHuey ))
    {
      AcftType = AcHuey;
      hueyInit();
      lcdReset();
    }
    else if(( strcmp( AcftNameBuffer.buffer, "A-10C" ) == 0 ) && ( AcftType != AcA10c ))
    {
      AcftType = AcA10c;
      a10cInit();
      lcdReset();
    }
    else if(( strcmp( AcftNameBuffer.buffer, "Ka-50" ) == 0 ) && ( AcftType != AcKa50 ))
    {
      AcftType = AcKa50;
      ka50Init();
      lcdReset();
    }
    else
    {
      lcdReset();
    }
  }

  // Handle aircraft specifics here
  switch( AcftType )
  {
    case AcHuey:
      break;
    case AcA10c:
      break;
    case AcKa50:
      break;
    default:
      break;
  }
}

/*
 * LCD Display
 */
void lcdInit()
{
  lcd.begin(20, 2);
}

/*
 * Huey LCD Display
 *   01234567890123456789
 * 0 HDG:... R:. ALT:....
 * 1 ADF:....MHz SIG:...%
 */

void lcdReset()
{
  switch( AcftType )
  {
    case AcNone:
      lcd.clear();
      lcd.setCursor(4,0);
      lcd.print( "- DCS-BIOS -" );
      lcd.setCursor(1,1);
      lcd.print( "Multiple Aircraft" ); 
      break;
    case AcHuey:
      lcd.clear();
      lcd.setCursor(0,0);
      lcd.print( "HDG:--- R:- ALT:----" );
      lcd.setCursor(0,1);
      lcd.print( "ADF:----kHz SIG:---%" ); 
      break;
    case AcA10c:
      lcd.clear();
      lcd.setCursor(0,0);
      lcd.print( "A-10C Stuff" );
      break;     
    case AcKa50:
      lcd.clear();
      lcd.setCursor(0,0);
      lcd.print( "KA-50" );
      lcd.setCursor(0,1);
      lcd.print( "Ka50.lua To Do!" ); 
      break;
    default:
      lcd.clear();
      lcd.setCursor(0,0);
      lcd.print( AcftNameBuffer.buffer );
      lcd.setCursor(0,1);
      lcd.print( "NOT SUPPORTED" );
  }
}

/*
 * Generic Controls
 */
DcsBios::LED* pWarningMasterCaution;
DcsBios::LED* pWarningRPM;

void acftCleanUp()
{
  // Remove the current inputs & listeners
  DcsBios::PollingInput::firstPollingInput = NULL;
  DcsBios::ExportStreamListener::firstExportStreamListener = NULL;

  // Clean up controls, ready for a new aircraft type
  if( pWarningMasterCaution )
    delete pWarningMasterCaution;

  if( pWarningRPM )
    delete pWarningRPM;
}

/*
 * Huey Controls
 */
void hueyInit()
{
  acftCleanUp();
  pWarningMasterCaution = new DcsBios::LED(0x1416, 0x0100, 41);
  pWarningRPM = new DcsBios::LED(0x1416, 0x0080, 13);
}

/*
 * A10c Controls
 */
void a10cInit()
{
  acftCleanUp();
  pWarningMasterCaution = new DcsBios::LED(0x1012, 0x0800, 41);
}

/*
 * Ka-50 Controls
 */
void ka50Init()
{
  acftCleanUp();
  // Need an aircraft lua first!
}
jboecker commented 9 years ago

Like everything else in DCS-BIOS, the aircraft name is updated whenever it changes. Assigning it to address 0x0000 was a deliberate decision -- that makes sure that it is transmitted first, so when you receive the data for the new aircraft you already know what to look for. The "autosync" mechanism will resend a bit of unchanged data with each update, so the aircraft name will also be retransmitted from time to time, but a properly implemented StringBuffer should not set the dirty flag in this case.

Dynamically creating the controls should work. However, you may want to watch _ACFT_NAME with a specially modified StringBuffer class that calls its callback immediately after a new value has been received to ensure that the rest of the initial update when entering the new aircraft is correctly interpreted.

If dynamically creating the new buffer classes turns out to be too slow, you could also instantiate everything at once and just switch the PollingInput::firstPollingInput and ExportStreamListener::firstExportStreamListener pointers around (assuming there is enough memory to hold everything at once).

I did not consider multi-aircraft panels when designing DCS-BIOS. It is a use case where something like the Helios EOS protocol is clearly superior, because it puts all the configuration on the much more powerful PC.

The rationale behind the DCS-BIOS design is to put all panel-specific information into the panel's firmware.

For a multi-function panel, I would consider using some sort of middleware on the host PC that sits between DCS-BIOS and the panels. You could even talk the DCS-BIOS protocol to the panels, but assign addresses to specific outputs (and have inputs send generic commands, such as "PNL1_S1") and make the middleware translate between the two based on a configuration file and the currently active aircraft.

Think about it -- when a new aircraft module comes out, you don't want to have to re-flash all your panels.

The addresses and input identifiers on the "panel-side" DCS-BIOS protocol don't even have to be generic -- you could build your pit for a specific aircraft and then have the middleware adapt it to different ones.

This may be a job for Helios at some point in the future.

OlHall commented 9 years ago

My motivation for this was because I'm not hardcore enough to want a specific aircraft simpit, but I do like the flexibility of physical switches etc. so my plan was to enable me to remap various encoders, switches, etc. to the most useful functions on each of the aircraft I like to fly (basically only helicopters!). I tend to switch about a bit too, so re-flashing the Arduino each time I do would become a chore.

Even though it's not "authentic" I'd still like to be able to control the radios of the Huey and the Ka-50 from the same set of controls (some redundant in one or the other modes).

I went for DCS-BIOS because it seems to offer that in a very simple package, without the need to any further middleware. I wasn't trying to make DCS-BIOS something it wasn't intended to be, so apologies if that's how it came across.

jboecker commented 9 years ago

Don't worry, I will probably build some kind of multi-function panel myself at some point.

DCS-BIOS is still the way to go in terms of interfacing with DCS, but I think using additional middleware on the host PC to handle multi-aircraft panels makes more sense.