|
SMLQuickStartGuide
Getting started with SML integration.
SML Quick Start GuideOverviewSML (Soar Markup Language) provides an interface into Soar based around sending and receiving commands packaged as XML packets. The interface is designed to support connecting environments to Soar (where input and output data structures are sent back and forth) and to support debuggers (where commands to print out specific productions or working memory elements are sent back and forth). We refer to these environments and debuggers as "clients". The details and motivation behind the development of the SML language are described in the "Soar XML Interface Specification" which goes into a lot more depth on the details of the XML dialect. However, for users this guide may be largely sufficient. Client SMLA client can choose to send and receive the XML commands directly by sending them to a socket maintained by Soar (port 12121 by default). The format used is 4-byte length followed by stream of characters representing the XML message. However, we donÂ't expect many clients will have to go down to this level. Instead, we provide a series of classes that together hide the details of the XML messaging system while allowing the client full control over Soar. This guide will provide a quick introduction to using those classes which we call the ClientSML module. Required LibrariesClientSML is currently available in C++, Java and Tcl (courtesy of SWIG). For the current C++ implementation the libraries required to build a C++ client (on Windows) are:
Also if youÂ're working in Java you need Java_sml_ClientInterface.dll, in Tcl youÂ'd need Tcl_sml_ClientInterface.dll. You get the idea. The required include files are all found in ClientSML/include. Configuring Visual Studio to use these libraries can be a bit confusing if youÂ're not used to it. HereÂ's some specific steps: // Doug: These were my changes from a vanilla MFC app created by Visual Studio in order to get SML going for a project named "Test": // 1) Added header sml_Client.h // 2) Set include path (C/C++ | General in properties) to include path to sml_Client.h // 3) Set runtime library to multi-threaded (C/C++ | Code Generation). Multi-threaded debug for debug build, multi-threaded for release. // 4) Added references to ElementXML, ConnectionSML and ClientSML projects (in Solution Explorer under References node in tree) // (I made this easy by adding this Test project to the SML.sln solution inside SoarIO so all projects were available to use). // Could just as easily have added .lib's under Linker | Input in properties and set the library include path. // They're equivalent but using references is less error prone (it copies the .lib information from the referenced project). // 5) Added menu item and code to create kernel, create agent, print s1 and show results in message box. // 6) Copied Test.exe to soar-library/bin folder in 8.6.2 release so it would find all required DLLs (SoarKernelSML.dll and ElementXML.dll). // Could also put them on the path, copy them to where this executable is etc. Simple SML exampleThis example is something of a "hello world" example of how to use the major elements of ClientSML. Once you understand this example, you'll be pretty much ready to dive in. // Generally only need this one header file
#include "sml_Client.h"
using namespace sml ;
using namespace std ;
void main() {
// Create an instance of the Soar kernel in our process
Kernel* pKernel = Kernel::CreateKernelInNewThread() ;
// Check that nothing went wrong. We will always get back a kernel object
// even if something went wrong and we have to abort.
if (pKernel->HadError())
{
cout << pKernel->GetLastErrorDescription() << endl ;
return ;
}
// Create a Soar agent named "test"
// NOTE: We don't delete the agent pointer. It's owned by the kernel
sml::Agent* pAgent = pKernel->CreateAgent("test") ;
// Check that nothing went wrong
// NOTE: No agent gets created if thereÂ's a problem, so we have to check for
// errors through the kernel object.
if (pKernel->HadError())
{
cout << pKernel->GetLastErrorDescription() << endl ;
return ;
}
// Load some productions
pAgent->LoadProductions("testsml.soar") ;
if (pAgent->HadError())
{
cout << pAgent->GetLastErrorDescription() << endl ;
return ;
}
Identifier* pInputLink = pAgent->GetInputLink() ;
// Create (I3 ^plane P1) (P1 ^type Boeing747 ^speed 200 ^direction 50.5) on
// the input link. (We donÂ't own any of the returned objects).
Identifier* pID = pAgent->CreateIdWME(pInputLink, "plane") ;
StringElement* pWME1 = pAgent->CreateStringWME(pID, "type", "Boeing747") ;
IntElement* pWME2 = pAgent->CreateIntWME(pID, "speed", 200) ;
FloatElement* pWME3 = pAgent->CreateFloatWME(pID, "direction", 50.5) ;
// Send the changes to working memory to Soar
// With 8.6.2 this call is optional as changes are sent automatically.
pAgent->Commit() ;
// Run Soar for 2 decisions
pAgent->RunSelf(2) ;
// Change (P1 ^speed) to 300 and send that change to Soar
pAgent->Update(pWME2, 300) ;
pAgent->Commit() ;
// Run Soar until it generates output or 15 decision cycles have passed
// (More normal case is to just run for a decision rather than until output).
pAgent->RunSelfTilOutput() ;
// Go through all the commands we've received (if any) since we last ran Soar.
int numberCommands = pAgent->GetNumberCommands() ;
for (int i = 0 ; i < numberCommands ; i++)
{
Identifier* pCommand = pAgent->GetCommand(i) ;
std::string name = pCommand->GetCommandName() ;
std::string speed = pCommand->GetParameterValue("speed") ;
// Update environment here to reflect agent's command
// Then mark the command as completed
pCommand->AddStatusComplete() ;
// Or could do the same manually like this:
// pAgent->CreateStringWME(pCommand, "status", "complete") ;
}
// See if anyone (e.g. a debugger) has sent commands to Soar
// Without calling this method periodically, remote connections will be ignored if
// we choose the "CreateKernelInCurrentThread" method.
pKernel->CheckForIncomingCommands() ;
// Create an example Soar command line
std::string cmd = "excise --all" ;
// Execute the command
char const* pResult = pKernel->ExecuteCommandLine(cmd.c_str(),pAgent->GetAgentName()) ;
// Shutdown and clean up
pKernel->Shutdown() ; // Deletes all agents (unless using a remote connection)
delete pKernel ; // Deletes the kernel itself
} // end mainSimple example explainedCreating the kernel// Create an instance of the Soar kernel in our process
Kernel* pKernel = Kernel::CreateKernelInNewThread("SoarKernelSML") ;The client can either create a local Soar kernel (i.e. load Soar as a DLL into its process) or a remote connection to an existing Soar kernel (where commands are sent over a socket to a separate process on the same or a different machine). The local kernel can either be created in the same thread as the caller or in a new thread. Using the same thread will generally be a bit faster, but it requires the client to periodically call pKernel->CheckForIncomingCommands() so that the kernel has a chance to check for commands coming in from other remote processes (e.g. from a debugger). So if you want maximum speed choose the current thread option, but your code will be a bit more complicated. If speed is not as critical then choose the new thread option. The speed difference isnÂ't that large, perhaps a factor of two on the cost of making a call -- which will often be dwarfed by the cost of matching productions or updating the environment. As you are just reading the "SML Quick Start Guide" I would strongly recommend you use CreateKernelInNewThread until you are somewhat familiar with the system. If you find you need higher performance later, switching over to the CurrentThread model later only requires changing this one line of code. Creating input link wmeÂ's// Create (I3 ^plane P1) (P1 ^type Boeing747 ^speed 200 ^direction 50.5) on // the input link. (We donÂ't own any of the returned objects). Identifier* pID = pAgent->CreateIdWME(pInputLink, "plane") ; StringElement* pWME1 = pAgent->CreateStringWME(pID, "type", "Boeing747") ; The client can build up an arbitrarily complex graph of working memory elements (WMEs) attached to the input-link. Each WME consists of a triplet: (identifier ^attribute value). The first identifier comes from "getInputLink" and then new identifiers are created by CreateIdWME() and new simple WMEs are created through CreateStringWME/CreateIntWME/CreateFloatWME. A WMEÂ's value can be updated through the pAgent->Update() method and it can be removed through pAgent->DestroyWME() which also makes the working memory object invalid. A graph (rather than a simple tree) can be created through pAgent->CreateSharedIdWME(). This creates a new identifier WME with the same value as an existing identifier. (E.g. given pOrig = (P7 ^object O3) then CreateSharedIdWME(pInputLink, "same", pOrig) would create (I1 ^same O3)). Committing changes// Send the changes to working memory to Soar pAgent->Commit() ; The client must explicitly request that changes to working memory be sent over to Soar. This explicit command allows the communication layer to be more efficient, by collecting all changes together and sending them as a single command. With 8.6.2 changes are sent over immediately they are made so Commit() is unnecessary. This produces slightly worse performance (as changes are not collected together into a single packet) so to get maximum performance call SetAutoCommit(false) and write the manual commit calls. Running Soar// Run Soar until it generates output or 15 decision cycles have passed pAgent->RunSelfTilOutput() ; // Run Soar for 2 decisions pAgent->RunSelf(2) ; In most real environments, Soar should be run with pKernel->RunAllAgentsForever() and then use a call to pKernel->StopAllAgents() to interrupt. This allows a user to run the environment from a debugger as well as from the environment. ThereÂ's more discussion of this in Section 2 of this document. Retrieving Output// Go through all the commands we've received (if any) since we last ran Soar.
int numberCommands = pAgent->GetNumberCommands() ;
for (int i = 0 ; i < numberCommands ; i++)
{
Identifier* pCommand = pAgent->GetCommand(i) ;
std::string name = pCommand->GetCommandName() ;
std::string speed = pCommand->GetParameterValue("speed") ;
// Update environment here to reflect agent's command
// Then mark the command as completed
pCommand->AddStatusComplete() ;
// Or could do the same manually like this:
// pAgent->CreateStringWME(pCommand, "status", "complete") ;
}There is direct support provided for an output model where "commands" are represented as distinction identifierÂ's on the output-link. For example, if the output-link identifier is O1 then the agent might add a move command with (O1 ^move M1) (M1 ^speed 20). If you choose to adopt this model for the agentÂ's actions then it is possible to query the agent for the number of commands added since the last time Soar was run and to retrieve each Command in turn, its name and parameter values. In this example, pCommand would point to M1, the name would be move and the parameter value for speed would be 20. If you wish to adopt a different model for agent actions, that is also supported, but the support is less direct. First, notice that GetCommand() returns a standard Identifier WME, so this can be manipulated in the same manner as any other WME. In particular IdentifierÂ's offer GetNumberChildren and GetChild methods, so using these it is possible to start from the output link and examine all of working memory beneath the output link. There are also other methods (such as FindByAttribute) that can make this search more efficient. Secondly, you can use IsJustAdded() and AreChildrenModified() on WMEs on the output-link (and below) to determine what has changed since the last time Soar was run. Third, you can call AddOutputHandler() to register a function that is called whenever a specific attribute is added to the output link. Finally, if that is not sufficient, it is possible to call GetNumberOutputLinkChanges() and GetOutputLinkChange() to get a list of all of the WMEs to have been added or removed since the last time Soar was run. From this collection of methods it should be possible to support just about any manner of output model you wish to adopt, but we would recommend the "Command" model shown above. The Command Line// Create an example Soar command line std::string cmd = "excise --all" ; // Execute the command char const* pResult = pKernel->ExecuteCommandLine(cmd.c_str(),pAgent->GetAgentName()) ; To this point the discussion has been purely about environments and supporting I/O. However, the ExecuteCommandLine methods allow a client to send any command to Soar that can be typed at the Soar command prompt. Using this method, productions can be loaded, excised, printed out etc. ExecuteCommandLineXML() is also available which returns the output as a structured XML message, making it easier and safer for a client to parse values from the output. See the online documentation (currently at http://soar.googlecode.com/) for details the format of that XML output for each command. Capturing print outputTo capture output during a run you need to register for the smlEVENT_PRINT event which will be called periodically during the course of the run. To do this you need to define a handler function which will be called during the run. HereÂ's a simple example: void MyPrintEventHandler(smlPrintEventId id, void* pUserData, Agent* pAgent, char const* pMessage)
{
// In this case the user data is a string we're building up
std::string* pTrace = (std::string*)pUserData ;
(*pTrace) += pMessage ;
}The method includes a piece of "userData" which is defined by you when you register for the event. In this case we would have: std::string trace ; int callbackp = pAgent->RegisterForPrintEvent(smlEVENT_PRINT, MyPrintEventHandler, &trace) ; Note how the string "trace" is passed into the registration function. This object is then passed back to the handler, which uses it to build up a complete trace. After this handler has been registered calling: result = pAgent->Run(4) ; would run Soar for 4 decisions and the trace output would be collected in the string "trace". There is now also another way to get this output by registering for smlEVENT_XML_TRACE_OUTPUT. This event sends XML objects rather than text strings. Displaying these to the user requires more work by the client, but if the client wishes to parse the text working with XML is much easier. This is the approach taken in the Java debugger. EventsThere are a lot of events you can register for and the list given here will surely grow over time. Here are the types of the handlers in C++ and Java (the Java ones are a little different from the C++ and are more error prone as theyÂ're checked at runtime not at compile time): C++ event handlers (if youÂ're not sure how to convert these types into functions look at the example of the print handler in the previous section): // Handler for Run events. // Passed back the event ID, the agent and the phase together with whatever user data we registered with the client typedef void (*RunEventHandler)(smlRunEventId id, void* pUserData, Agent* pAgent, smlPhase phase); // Handler for Agent events (such as creation/destruction etc.). typedef void (*AgentEventHandler)(smlAgentEventId id, void* pUserData, Agent* pAgent) ; // Handler for Print events. typedef void (*PrintEventHandler)(smlPrintEventId id, void* pUserData, Agent* pAgent, char const* pMessage) ; // Handler for Production manager events. typedef void (*ProductionEventHandler)(smlProductionEventId id, void* pUserData, Agent* pAgent, char const* pProdName, char const* pInstantion) ; // Handler for System events. typedef void (*SystemEventHandler)(smlSystemEventId id, void* pUserData, Kernel* pKernel) ; // Handler for XML events. The data for the event is passed back in pXML. // NOTE: To keep a copy of the ClientXML* you are passed use ClientXML* pMyXML = new ClientXML(pXML) to create // a copy of the object. This is very efficient and just adds a reference to the underlying XML message object. // You need to delete ClientXML objects you create and you should not delete the pXML object you are passed. typedef void (*XMLEventHandler)(smlXMLEventId id, void* pUserData, Agent* pAgent, ClientXML* pXML) ; // Handler for RHS (right hand side) function firings // pFunctionName and pArgument define the RHS function being called (the client may parse pArgument to extract other values) // The return value is a string which allows the RHS function to create a symbol: e.g. ^att (exec plus 2 2) producting ^att 4 // NOTE: This is the one place in clientSML where we use a std::string in an interface. If you wish to compile with a pure "C" interface // this can be replaced by a handler that is passed a buffer and a length. The length is passed within the framework already (from the kernel to here) // so this is an easy transition. typedef std::string (*RhsEventHandler)(smlRhsEventId id, void* pUserData, Agent* pAgent, char const* pFunctionName, char const* pArgument) ; Java event handlers are based on implementing an interface within an object: From Kernel: public interface SystemEventInterface {
public void systemEventHandler(int eventID, Object data, Kernel kernel) ;
}
public interface UpdateEventInterface {
public void updateEventHandler(int eventID, Object data, Kernel kernel, int runFlags) ;
}
public interface StringEventInterface {
public void stringEventHandler(int eventID, Object userData, Kernel kernel, String callbackData) ;
}
public interface AgentEventInterface {
public void agentEventHandler(int eventID, Object data, String agentName) ;
}
public interface RhsFunctionInterface {
public String rhsFunctionHandler(int eventID, Object data, String agentName, String functionName, String argument) ;
}
public interface ClientMessageInterface {
public String clientMessageHandler(int eventID, Object data, String agentName, String functionName, String argument) ;From Agent: public interface RunEventInterface {
public void runEventHandler(int eventID, Object data, Agent agent, int phase) ;
}
public interface ProductionEventInterface {
public void productionEventHandler(int eventID, Object data, Agent agent, String prodName, String instantiation) ;
}
public interface PrintEventInterface {
public void printEventHandler(int eventID, Object data, Agent agent, String message) ;
}
public interface xmlEventInterface {
public void xmlEventHandler(int eventID, Object data, Agent agent, ClientXML xml) ;
}
public interface OutputEventInterface {
public void outputEventHandler(Object data, String agentName, String attributeName, WMElement pWmeAdded) ;
}
public interface OutputNotificationInterface {
public void outputNotificationHandler(Object data, Agent agent) ;
}Examples of implementations: public void runEventHandler(int eventID, Object data, Agent agent, int phase)
{
System.out.println("Received run event in Java") ;
}
// We pass back the agent's name because the Java Agent object may not yet
// exist for this agent yet. The underlying C++ object *will* exist by the
// time this method is called. So instead we look up the Agent object
// from the kernel with GetAgent().
public void agentEventHandler(int eventID, Object data, String agentName)
{
System.out.println("Received agent event in Java") ;
}
public void productionEventHandler(int eventID, Object data, Agent agent, String prodName, String instantiation)
{
System.out.println("Received production event in Java") ;
}
public void systemEventHandler(int eventID, Object data, Kernel kernel)
{
System.out.println("Received system event in Java") ;
}
public void printEventHandler(int eventID, Object data, Agent agent, String message)
{
System.out.println("Received print event in Java: " + message) ;
}
public void xmlEventHandler(int eventID, Object data, Agent agent, ClientXML xml)
{
String xmlText = xml.GenerateXMLString(true) ;
System.out.println("Received xml trace event in Java: " + xmlText) ;
String allChildren = "" ;
if (xml.GetNumberChildren() > 0)
{
ClientXML child = new ClientXML() ;
xml.GetChild(child, 0) ;
String childText = child.GenerateXMLString(true) ;
allChildren += childText ;
child.delete() ;
}
}
public String testRhsHandler(int eventID, Object data, String agentName, String functionName, String argument)
{
System.out.println("Received rhs function event in Java for function: " + functionName + "(" + argument + ")") ;
return "My rhs result " + argument ;
}To see more about events look at the sml_ClientEvents.h header file or the TestSMLEvents test program (currently located in the Tools folder). Events in TclTcl requires that callbacks to a Tcl interpreter happen in the same thread as the interpreter is executing in. By default, this will not always happen for SML events. This is because there is an event thread started up by SML (running in the client) which is used to check for incoming events and make the necessary callback calls. Therefore for Tcl we recommend shutting down this event thread and polling explicitly for incoming events. This can be done in a few lines of code like this: # We want to make sure to handle events in the Tcl thread
# so we turn off the event thread and poll for events instead.
$_kernel StopEventThread
checkForEvents $_kernel
# Explicitly check for incoming commands and events every n milliseconds
proc checkForEvents {k} {
$k CheckForIncomingCommands
after 10 checkForEvents $k
}This assumes $_kernel is a variable set to the SML kernel object. Note, the after 10 triggers another call to the checkForEvents() method after a 10ms delay. Making this value larger will make Tcl less responsive to events. Making it smaller will consume more CPU time checking for events. Building an environmentIf you are converting an existing SGIO environment you should also read the "Moving from SGIO to SML" document as well as this section. The Java implementation of Towers of Hanoi (TOH) is a useful reference, showing a simple environment environment. IÂ'll use that to as an example here, so the code snippets will be in Java, but a very similar approach can be taken in other languages (currently Tcl or C++). InitializationThe first step is to create an instance of the kernel and then create an agent. The name passed to CreateKernelInNewThread is the name of the library to load (DLL on Windows). This is optional in 8.6.2 and defaults to SoarKernelSML. It is important to check for an error using the kernel.HadError() method. CreateKernel will not return an empty kernel object even if initialization fails. This is deliberate design to ensure that meaningful errors can be reported to the user. Initialization Code Sample // create our Soar kernel
kernel = Kernel.CreateKernelInNewThread();
if (kernel.HadError())
{
// Better to use a “Message Box” to display this if your platform/toolkit allows.
System.out.println("Error creating kernel: " + kernel.GetLastErrorDescription()) ;
System.exit(1);
}
agent = kernel.CreateAgent(AGENT_NAME);
boolean load = agent.LoadProductions("towers-of-hanoi-SML.soar");
if (!load || agent.HadError()) {
throw new IllegalStateException("Error loading productions: "
+ agent.GetLastErrorDescription());
}InputMapping from the environmentÂ's state to input values involves calls to create, update and destroy working memory elements using calls like these: agent.Create<type>WME (e.g. agent.CreateIntWME) agent.Update() agent.DestroyWME Working memory elements are linked together to form a tree (actually a graph) with the input link as the root of the tree. To get the input link identifier (the top of the tree) call agent.GetInputLink(). A key step after making multiple calls to modify the input link is to call Commit(). All changes to working memory are buffered (in the environment) until the moment Commit is called, at which point they are all sent over as a single large message to the kernel. This greatly improves performance, but you must remember to call Commit before running the agent or your changes wonÂ't actually appear on the agentÂ's input link. This was changed in 8.6.2 so the default now is that Commit() is called automatically each time a wme is modified on the input link. To enable the faster version which then requires manual calls to Commit() call SetAutoCommit(false). Calling Commit multiple times during a single input cycle will not cause problems. OutputThe most common way to examine output from an agent is to use the "Commands" model. With this method the agent places each output command on the output-link using the format: (X ^output-link I3) (I3 ^command-name C1) (C1 ^param1 value ^param2 value) Thus the name of the command appears directly under the output-link and all parameters are added to the command identifier and can only be one level deep. (If you wish to use an alternative model see below). If the agent adopts this format for output commands they can be easily retrieved by the environment using code like this: Update World Code Sample -- get Output, Change-World-State, then send Input private void updateWorld()
// See if any commands were generated on the output link
// (In general we might want to update the world when the agent
// takes no action in which case some code would be outside this if statement
// but for this environment that's not necessary).
if (agent.Commands())
{
// perform the command on the output link
Identifier command = agent.GetCommand(0);
if (!command.GetCommandName().equals(MOVE_DISK)) {
throw new IllegalStateException("Unknown Command: "
+ command.GetCommandName());
}
if (command.GetParameterValue(SOURCE_PEG) == null ||
command.GetParameterValue(DESTINATION_PEG) == null) {
throw new IllegalStateException("Parameter(s) missing for Command "
+ MOVE_DISK);
}
int srcPeg = command.GetParameterValue("source-peg").charAt(0) - 'A';
int dstPeg = command.GetParameterValue("destination-peg").charAt(0) - 'A';
// Change the state of the world and generate new input
moveDisk(srcPeg, dstPeg);
// Tell the agent that this command has executed in the environment.
command.AddStatusComplete();
// Send the new input link changes to the agent
agent.Commit();
// "agent.GetCommand(n)" is based on watching for changes to the output link
// so before we do a run we clear the last set of changes.
// (You can always read the output link directly and not use the
// commands model to determine what has changed between runs).
agent.ClearOutputLinkChanges() ;
if (isAtGoalState())
fireAtGoalState();
}
}The method GetCommandName and GetParameterValue extract the appropriate attributes and values from the output link and return them to the environment. The method AddStatusComplete adds (C1 ^status complete) to the command structure, indicating to the agent that this command has executed and can now be removed by the agent. (Note that this is simple form of input that in this case is sent to the output link rather than the input link. This is technically a bit of "back door" input, but is simpler than passing a large structure to the input link and having the agent match up the two. This addition is made during the agentÂ's next input cycle as with all input). In order for the Commands method to work, the system has to keep track of changes to working memory as only newly added commands are reported through the GetCommand method. For this process to work, the environment must call ClearOutputLinkChanges() before running the agent again. While this Command model should be sufficient for almost all environments, if there is a need to process other structures, the environment can always choose to ignore this model partially or completely. First, the method GetCommand returns an Identifier object. The environment can use the methods GetNumberChildren and GetChild to walk this Identifier and locate any arbitrary working memory element beneath an Identifier or the environment can use the FindByAttribute method to retrieve the values of working memory elements that have a particular attribute. In fact a environment can abandon the Command model completely and simply call GetOutputLink() to get the Identifier object at the top of the output link and proceed to examine the tree (graph) from there. Running the agent(s)There are two main methods for running an agent:
The trick here is that the code for running the agents should be separate from the code for updating the world (collecting output; updating world state; sending input). By separating the two we can either issue the run from the environment or from a debugger (or other client) and everything works correctly. LetÂ's assume we have the updateWorld() method from above, which should have the form: void updateWorld()
{
check-for-output() ;
update-world-state() ;
send-new-input() ;
}Then the code samples below show how to connect up the system so that updateWorld() is only called once all agents have completed the output phase (i.e. at the end of a decision cycle). Run Sample Code public void run()
{
m_StopNow = false ;
// Start a run
// (since this is single agent could use agent.RunSelfForever() instead, but this shows how to run multi-agent environments)
kernel.RunAllAgentsForever() ;
}
public void step()
{
// Run one decision
kernel.RunAllAgents(1) ;
}
public void stop()
{
// We'd like to call StopSoar() directly from here but we're in a different
// thread and right now this waits patiently for the runForever call to finish
// before it executes...not really the right behavior. So instead we use a flag and
// issue StopSoar() in a callback.
m_StopNow = true ;
}Event-based method for updating the world public void registerForUpdateWorldEvent()
{
int updateCallback = kernel.RegisterForUpdateEvent(sml.smlUpdateEventId.smlEVENT_AFTER_ALL_OUTPUT_PHASES, this, "updateEventHandler", null) ;
}
public void updateEventHandler(int eventID, Object data, Kernel kernel, int runFlags)
{
// Might not call updateWorld() depending on runFlags in a fuller environment.
// See the section below for more on this.
updateWorld() ;
// We have a problem at the moment with calling Stop() from arbitrary threads
// so for now we'll make sure to call it within an event callback.
// Do this test after calling updateWorld() so that method can set m_StopNow if it
// wishes and trigger an immediate stop.
if (m_StopNow)
{
m_StopNow = false ;
kernel.StopAllAgents() ;
}
}The Run code is pretty simple. The only issues to be aware of are:
The way updateWorld() is called is after the smlEVENT_AFTER_ALL_OUTPUT_PHASES event fires. Why bother to do this rather than writing run as: while (!stopped)
{
run(1) ;
update-world() ;
}There are two basic reasons. First, there are no guarantees that run(1) will run for a decision. If we include breakpoints on productions or phase transitions this call may run for less time, possibly confusing the environment (as the agents have not progressed as far as expected). Second, by making the call to updateWorld() event based, we can issue arbitrary run commands from a debugger and the environment will function correctly. For more on this see the "Event-Driven Environments Proposal.doc" file. Also in Tcl please be sure to read about how to poll for events in Tcl (see Section 1.6.1). Supporting running without updating the environmentFor some environments it may make sense to allow the user to run an agent without starting the environment. This can be helpful when debugging one agent or having reached a particular situation in the world, stopping and wanting to step slowly to observe the reasoning. To support this the environment should check the runFlags parameter passed to the updateEventHandler. This is a bit field which currently consists of the values in this enum: Run Flags typedef enum
{
sml_NONE = 0, // No special flags set
sml_RUN_SELF = 1 << 0, // User included --self flag when running agent
sml_RUN_ALL = 1 << 1, // User ran all agents
sml_UPDATE_WORLD = 1 << 2, // User explicitly requested world to update
sml_DONT_UPDATE_WORLD = 1 << 3, // User explicitly requested world to not update
} smlRunFlags ;Based on these flags, the environment should decide whether to update or not. We leave this entirely to the discretion of the designer because different combinations may make sense for different situations but this might be a typical set (for a multi-agent environment): if sml_RUN_SELF set and SML_UPDATE_WORLD not set or sml_DONT_UPDATE_WORLD set then donÂ't call updateWorld(). The meaning of this would be that:
You are free to make other choices if thereÂ's a compelling reason. Integrating with other clients (esp. the debugger)One final issue is updating the controls in the environment correctly if the user runs Soar from the debugger and not the environment (e.g. disabling the run button and enabling stop during a run). There are two approaches to this. First, ignore the problem and donÂ't enable/disable controls. Instead, just handle errors if the user tries to issue a second run while a run is already going ahead. The second option is to register for the smlEVENT_SYSTEM_START and smlEVENT_SYSTEM_STOP events and use these to enable/disable the UI in the environment in appropriate ways. HereÂ's an example for implementing this support: Registering and Handling Start/Stop Sample Code public void registerForStartStopEvents()
{
if (kernel != null)
{
int startCallback = kernel.RegisterForSystemEvent(smlSystemEventId.smlEVENT_SYSTEM_START, this, “systemEventHandler”, null) ;
int stopCallback = kernel.RegisterForSystemEvent(smlSystemEventId.smlEVENT_SYSTEM_STOP, this, “systemEventHandler”, null) ;
}
}
public void systemEventHandler(int eventID, Object data, Kernel kernel)
{
if (eventID == sml.smlSystemEventId.smlEVENT_SYSTEM_START.swigValue())
{
// The callback comes in on Soar's thread and we have to update the buttons
// on the UI thread, so switch threads.
dpy.asyncExec(new Runnable() { public void run() { updateButtons(true) ; } } ) ;
}
if (eventID == sml.smlSystemEventId.smlEVENT_SYSTEM_STOP.swigValue())
{
// The callback comes in on Soar's thread and we have to update the buttons
// on the UI thread, so switch threads.
dpy.asyncExec(new Runnable() { public void run() { updateButtons(false) ; } } ) ;
}
}
public void updateButtons(boolean running)
{
boolean done = game.isAtGoalState() ;
startButton.setEnabled(!running && !done) ;
stopButton.setEnabled(running) ;
resetButton.setEnabled(!running && !done) ;
stepButton.setEnabled(!running && !done) ;
}
Further detailsTo learn more about ClientSML and SML in general, the best documentation is the header files for the methods in ClientSML. In particular, sml_ClientKernel.h, sml_ClientAgent.h and sml_ClientIdentifier.h contain a lot of useful methods and explanations. Beyond that check the Soar XML Interface Spec and any other documentation in its vicinity. Using other languagesThe Tcl and Java interfaces were generated by SWIG (http://www.swig.org). We have provided some custom code to help SWIG out in a few places, mostly with callbacks. If youÂ're interested in creating interfaces for other languages that SWIG supports, check out the SWIG documentation and try to follow our existing solutions. A Tcl package, called Tcl_sml_ClientInterface is available. On Windows it is located in the soar-library directory. This can be used by including the following line in your Tcl code: package require tcl_sml_clientinterface Note that the directory where the package resides must be added to TclÂ's auto_path variable. The available functions are direct translations of their C++ counterparts. See TOHtest.tcl in soar-library for examples showing how to use the Tcl interface. The Tcl package includes Tcl_sml_ClientInterface.dll (or the equivalent for other platforms), which is required. The Java interface is available via a collection of .java files which directly mirror their C++ counterparts. See SoarIO/examples/TestJavaSML for an example showing the usage. Java must have access to the Java_sml_ClientInterface.dll (or the equivalent for other platforms). Helpful TipsRequired Visual Studio project settingsIf you are creating a new Visual Studio project, be sure to use the following settings (locations are from VS.NET 2003):
Memory ManagementMemory management is actually really easy. Generally, the only objects you should explicitly delete are the kernel object and any objects you directly allocated through a call to new. In Java and Tcl, this generally means you can just let things go out of scope when youÂ're done with them. There are a couple special cases you should be aware of, though:
Boosting PerformanceIt is often desirable to maximize the performance of your SML application. This section assumes that you just want to make things as fast as possible after you have finished debugging your application. Debugging is an inherently slow process, so these tips will be less helpful while youÂ're still debugging.
| |