Find the next step in your career as a Graphisoft Certified BIM Manager!

Archicad C++ API
About Archicad add-on development using the C++ API.
SOLVED!

Receive external data via HTTP GET in a loop

BrunoValads
Contributor

I'm trying to make an automation that allows the user to send data to a server in the web via HTTP POST, and automatically receive data form this server when its available via HTTP GET.

The POST part is solved already, by simply calling it from the menu option clicked by the user, it's a manual action.

The GET part is the problem here, I need it to be constantly requesting the server, i.e in a loop doing GET every second or less, but in a way that Archicad is not frozen by it.

Another idea would be the server sending a command directly to Archicad somehow, making the server acting like the user manually doing the command.

I'm not sure if this is the best approach actually, I'm open to any suggestion.

 

There are a few related things I saw in the forums, like:

1 ACCEPTED SOLUTION

Accepted Solutions
Solution
kovacsv
Booster

I would try this approach:

  1. Make the addon preloaded, so Archicad won't unload it automatically when it's not used.
  2. Start a separate thread that constantly polls your web server (e.g. every 5 seconds or so).
  3. When you get the response from the web server, run the command you need.

The tricky part is step 3. It's not safe to do an operation at any time, since it's possible that Archicad works hard on something else, and your call would mess up things. This is why ACAPI_Command_CallFromEventLoop was invented. It makes sure that your command will be executed when Archicad has nothing important to do. It also runs on the main thread, so you don't have to worry much about multithreading issues.

View solution in original post

8 REPLIES 8
poco2013
Mentor

I believe that, assuming that your polled web site is outside your local network, you would probably need a static address, which is expensive and difficult to get . Lacking that , you might just create a browser in Python to communicate and receive from your remote address.

I believe , the C++ API can do the same ( don't really know how) but would either 'lock up Archicad' or affect performance.

 

The external python script could handle any communication with the remote site and Archicad and would mostly be idle, then ,upon any action, forward any data or commands to a C++AddOn in Archicad via the ExecuteCommand function.There are many instr. videos on YouTube as to how to create a browser in Python.

Gerry

Windows 11 - Visual Studio 2022; ArchiCAD 27
Solution
kovacsv
Booster

I would try this approach:

  1. Make the addon preloaded, so Archicad won't unload it automatically when it's not used.
  2. Start a separate thread that constantly polls your web server (e.g. every 5 seconds or so).
  3. When you get the response from the web server, run the command you need.

The tricky part is step 3. It's not safe to do an operation at any time, since it's possible that Archicad works hard on something else, and your call would mess up things. This is why ACAPI_Command_CallFromEventLoop was invented. It makes sure that your command will be executed when Archicad has nothing important to do. It also runs on the main thread, so you don't have to worry much about multithreading issues.

The external server is a webservice our partner maintains, this add-on should speak directly to it.

 


@poco2013 wrote:

The external python script could handle any communication with the remote site and Archicad and would mostly be idle, then ,upon any action, forward any data or commands to a C++AddOn in Archicad via the ExecuteCommand function.There are many instr. videos on YouTube as to how to create a browser in Python.


By "ExecuteCommand" you mean ExecuteAddOnCommand, right? And can this python script be running externally too?

This approach makes sense in my mind, just the technicalities that are bugging me.

 


@kovacsv wrote:

1. Make the addon preloaded, so Archicad won't unload it automatically when it's not used.


By "preloaded" you mean using ACAPI_KeepInMemory?

 


@kovacsv wrote:
2. Start a separate thread that constantly polls your web server (e.g. every 5 seconds or so).

How exactly I do that? GS::Thread is not documented.

Not sure about "ACAPI_KeepInMemory" -- probably yes. But you do need "return APIAddon_Preload;" in CheckEnvuronment to keep the AddOn from being unloaded. I would definitely run the script externally, Internally run Python scripts in Archicad can be trouble because of focus issues. I run all my scripts externally now. Most from  Stream Deck buttons. I would use the Python script to do the polling and just notify or pass the data to the Archicad AddOn via ExecuteAddOnCommand() function.

Gerry

Windows 11 - Visual Studio 2022; ArchiCAD 27

@BrunoValads wrote:

This approach makes sense in my mind, just the technicalities that are bugging me.

 


@kovacsv wrote:

1. Make the addon preloaded, so Archicad won't unload it automatically when it's not used.


By "preloaded" you mean using ACAPI_KeepInMemory?

 


@kovacsv wrote:
2. Start a separate thread that constantly polls your web server (e.g. every 5 seconds or so).

How exactly I do that? GS::Thread is not documented.


You can return APIAddon_Preload in CheckEnvironment. GS::Thread is not documented, but std::thread is: https://en.cppreference.com/w/cpp/thread/thread. If I understand your use case correctly, you don't need python at all.

Thank a lot @kovacsv and @poco2013 , I achieved exactly the behaviour I wanted. Here's a minimum reproducible example, every spot commented with "Async Test" is directly related to this solution:

 

#include "APIEnvir.h"
#include "ACAPinc.h"

#include "ResourceIds.hpp"
#include "Thread.hpp" // Async Test
#include "InterruptedException.hpp" // Async Test

static void MyCommand(); // Async Test
static void StartCommandInNewThread(void); // Async Test
GSErrCode __ACENV_CALL MyCommand_Handler(GSHandle /*paramHandle*/, GSPtr /*resultData*/, bool /*silentMode*/); // Async Test

static GSErrCode MenuCommandHandler (const API_MenuParams *menuParams)
{
	switch (menuParams->menuItemRef.menuResID)
	{
		case ID_ADDON_MENU:
			switch (menuParams->menuItemRef.itemIndex)
			{
				case 1:
					{
						StartCommandInNewThread(); // Async Test
					}
					break;
			}
			break;
	}

	ACAPI_KeepInMemory(true); // Async Test

	return NoError;
}

API_AddonType __ACDLL_CALL CheckEnvironment (API_EnvirParams* envir)
{
	RSGetIndString (&envir->addOnInfo.name, ID_ADDON_INFO, 1, ACAPI_GetOwnResModule ());
	RSGetIndString (&envir->addOnInfo.description, ID_ADDON_INFO, 2, ACAPI_GetOwnResModule ());

	return APIAddon_Preload; // Async Test
}

GSErrCode __ACDLL_CALL RegisterInterface (void)
{
	ACAPI_Register_SupportedService('TEST', 1); // Async Test

	return ACAPI_Register_Menu (ID_ADDON_MENU, 0, MenuCode_Tools, MenuFlag_Default);
}

GSErrCode __ACENV_CALL Initialize (void)
{
	ACAPI_Install_ModulCommandHandler('TEST', 1, MyCommand_Handler); // Async Test

	return ACAPI_Install_MenuHandler (ID_ADDON_MENU, MenuCommandHandler);
}

GSErrCode __ACENV_CALL FreeData (void)
{
	return NoError;
}

// --- Async Test: My parallel command -----------------------------------------------------------
static void MyCommand()
{
	GSErrCode err = NoError;

	API_ModulID mdid;
	BNZeroMemory(&mdid, sizeof(mdid));
	mdid.developerID = 1; // {1, 1} works for DEMO
	mdid.localID = 1;

	int i = 0;
	while (true)
	{
		DBPrintf("\n%d: HTTP GET ()", i); //DEBUG
		/*
		Pretend there's HTTP GET code here
		*/

		// Hey, GET returned what I want, let's execute some stuff on the project
		if (i % 5 == 0)
		{
			err = ACAPI_Command_CallFromEventLoop(&mdid, 'TEST', 1, nullptr, false, nullptr); // this is tied to the command handler
			if (err != NoError) {
				DBPrintf("\nACAPI_Command_CallFromEventLoop failed: %X\n", err);
			}
		}

		Sleep(1000); // Just to advance loop each second
		++i;
	}
}

// --- Async Test: My command handler, the "escape" from the parallel thread -----------------------------------------------------------
GSErrCode __ACENV_CALL  MyCommand_Handler(GSHandle /*paramHandle*/, GSPtr /*resultData*/, bool /*silentMode*/)
{
	GSErrCode err = NoError;

	DBPrintf("\nMy command!");

	// A simple command to change the active window to 3D view so you can notice the command in the program
	API_WindowInfo windowInfo;
	BNZeroMemory(&windowInfo, sizeof(API_WindowInfo));
	windowInfo.typeID = APIWind_3DModelID;
	ACAPI_Automate(APIDo_ChangeWindowID, &windowInfo);

	return err;
}

// --- Async Test: MonitoredTask class -----------------------------------------------------------
class MonitoredTask : public GS::Runnable {
public:
	explicit MonitoredTask();

	virtual void Run();
};

MonitoredTask::MonitoredTask()
{
}

void MonitoredTask::Run()
{
	try
	{
		MyCommand(); // My function
	}
	catch (GS::InterruptedException&)
	{
		// Empty handler
	}
}

// --- Async Test: Start new thread with the Runnable class -----------------------------------------------------------
static void StartCommandInNewThread(void)
{
	GS::Thread m_worker(new MonitoredTask(), "CommandInNewThread");
	m_worker.Start();
}

 

 

Joel Buehler
Enthusiast

im sorry if im getting this thread all wrong, but isn't the GS::Thread and GS::RunnableTask not exactly for that use case? 

 

JoelBuehler_0-1688395693929.png

 

(Example in Goodies)

Didn't find the answer?

Check other topics in this Forum

Back to Forum

Read the latest accepted solutions!

Accepted Solutions

Start a new conversation!