Developing a New App
By default, MAE resides under /usr/mae/src (PC, Linux) or /opt/mae/src (MacOS), so this documentation references that path. This is also known as MAESRCDIR.
Pick a location for your app. If it is outside of MAE, you'll want to link MAE in (extend MAE). For example, if you are developing under /usr/abc/src, then you'll want to link key MAE source directories into your environment. For example,
cd /usr/abc/src
ln -s /usr/mae/src/maeapi .
ln -s /usr/mae/src/utillib .
ln -s /usr/mae/src/libdb .
ln -s /usr/mae/src/tool .
ln -s /usr/mae/src/[[commhub]] .
ln -s /usr/mae/src/[[dbbroker]] .
ln -s /usr/mae/src/[[msgbroker]] .
ln -s /usr/mae/src/[[rpcbroker]] .
ln -s /usr/mae/src/lib .
ln -s /usr/mae/src/[[usergw]] .
ln -s /usr/mae/src/[[noticegw]] .
ln -s /usr/mae/src/[[imager]] .
ln -s /usr/mae/src/[[guibroker]] .
For MAE to work, you'll need to integrate it into your environmment.
Edit /usr/mae/conf/[[commhub#Commhub.ini|commhub.ini]], look under the dbbroker sectoin, and update the settings for your server, user, password, dbname, and dbport as needed. If commhub is already running, restart it. Alternatively, you can use tset to set those parameters and not restart commhub.
(Re)Start dbbroker to connect to the data store.
By default, MAE is only configured with its basic daemon services - dbbroker, msgbroker, and rpcbroker. You can enable additional MAE daemon services if present. Under /usr/mae/conf, you may see files such as mgmttaskdb_*.csv. Those are the registration files for additional daemons if you need them. Register them using:
tadd mgmttaskdb_*.csv
Review the file /usr/mae/src/utillib/maedefs.h. Set TDIR to your target run-time root directory if not /usr/mae. Add any other global defines for your environment here.
MAE organizes its documentation as wiki text files under /usr/mae/html/doc. The program wiki2doc is used to convert it to HTML. Once usergw is running, you can visit http://sitename/doc/pagename to view it.
MAE binaries live in /usr/mae/bin. You may want to add this to your PATH.
The commhub program is the MAE communication hub; it is central to MAE's operation. If it is not running, MAE is not running. Choose the user id for running MAE. Make sure /usr/mae/conf and /usr/mae/log are writable by that user. If you use multiple users to run MAE, make sure they all can write to /usr/mae/log. The user that starts commhub is the id that runs all daemons and jobs launched by commhub.
Start commhub simply with
/usr/mae/bin/commhub
It runs in the background. Use tstatus, tstart, tstop, and related control utilities to run MAE tasks and check on them.
Create an app name that will also be used for the messenger channel name.
Create /usr/mae/src/newappname.
Create these files
Filename |
Contents |
Makefile |
Standard targets required: all clean online and deps. Note that the deps target runs the genmakedeps tool to automatically generate include file dependencies. |
newappname.cpp |
This file establishes communication with commhub via a messenger channel, registers RPC services available, and turns control over to MAETask. |
newappname.h |
You may wish to put all your commonly used .h needs in here, including newappnameApp.h. |
cache.cpp |
This file manages all your data needed for the user activity. The data is typically drawn from and possibly stored back to the datastore. |
newappnameApp.h |
The main class that interacts with user activity. It contains the user state of that interaction and the methods to respond to requests, whether triggered by the user or other apps. |
newappnameApp.cpp |
The logic that supports the app's purpose and handles user activity. |
channel.mreg |
Your app's MAE registration file, procesed by genmae. Here, all messages coming to/from commhub are registered. It routes data from messages to the appropriate NewappnameApp methods as you specify. The channel is the name of the communication channel that your app uses in the MAE environmemt (often, this is just the app's name). Some design is required here. You may want to plan ahead for what messages will be accepted by your app. Each message is a keyword that triggers an action. Some messages have paramters, whether they are callback properties or properties to influence the action. If using usergw, menu items, keystrokes, and UI clicks are registered to their appropriate messages. If your app provides a MAE API for other apps to call, define those MAE API calls here. |
msgNewappname.cpp |
This file will be generated from channel.mreg using genmae. This file handles all messages coming from commhub. It calls the appropriate NewappnameApp methods. Some design is required here. You may want to plan ahead for what messages will be accepted by your app. Each message is a keyword that triggers an action. Some messages have paramters, whether they are callback properties or properties to influence the action. |
mgmtNewappname.cpp |
This file handles all app management calls that come from supervisor, which monitors and controls daemons and apps. The management calls typically have to do with the cached data identified in cache.cpp. |
Most apps use data structures from the /usr/mae/src/lib directory to load and save block of data, such as Places, PCs, Monsters, etc. If your app needs any new data structures, create them in /usr/mae/src/lib.
When creating a new data structure, create the core data block and use the genmae utility to generate a lot of the basic source code needed (load, save, GETs, SETs, etc.). For example, a core data block may be:
// CORE DATA BEGIN
DbRecNum id;
string name;
int age;
HashArray properties;
// CORE DATA END
The above may be in a file People.h or just People. Create the database table for it using
genmae People.h -sql
By convention, store the table creating SQL in a file name table_tablename.sql. This will make it easier to create a number of tables at the same time.
Commhub is responsible for starting and stopping all daemons and apps. Register your app with it.
Create a management task registration file, /usr/mae/conf/mgmttaskdb_app.csv that contains the header line from /usr/mae/conf/mgmttaskdb.csv and an appropriate data line for your app below it. For assistance, see commhub. Next, register your new daemon with
tadd /usr/mae/conf/mgmttaskdb_app.csv
(A past bug required you to stop commhub, edit the /usr/mae/conf/task.csv to change 'nada' to the appname, but that should be fixed by now.)
In C++, every program must have a main() routine. MAE defines this for your app. Instead, your app needs to define certain points in program execution.
The basic shell of your app is:
#include <MAEApp.h >
MAEApp maeapp("newappname");
void MAE::init(int argc, char ** argv)
/** Initialize the app.
* At this point, messages can be sent and the database is available, but
* the app is not considered ready.
* @param argc - # of command line arguments
* @param argv - the command line arguments
*/
{
...
}
void MAE::main()
/** Everything is initialized. The app swings into action -
* which may just be a matter of waiting for events or
* messages
*/
{
/* wait for events */
// task.process();
}
void MAE::quit (int rc, const string & msg)
/** This app is going down, whether initiated by the OS or MAE environment.
* @param rc - the program's exit code (0 means normal exit, 1+ means err exit)
* @param msg - an explanation why the app is shutting down
*/
{
...
}
Each app needs a MAE registration file (.mreg) where messaging, UI response handling, and other interfacing is defined. The genmae program takes the .mreg file and generates code that integrates with your MAE app.
The GuiBroker daemon is responsible for redering advanced UI features and callbacks from user activity. UserGW provides the bridge/gateway via HTTP between MAE and the user's browser.
If usergw is used to deliver displayable content and interact with the user, then a base webpage must be setup. It should be /usr/mae/html/newappname.html. It declares a number of UI regions and other page setup. Often, records (or subpages) are written to named regions where those records contain many more named regions. This is how the interface is built up.
To interact properly with MAE, these named regions must be present:
Further, these JavaScript files must be included:
And these CSS files in the head block:
Here's an example (empty) HTML page:
<! -- newappname Start Page -- >
<head >
<link rel="stylesheet" type="text/css" href="mae.css" / >
<link rel="stylesheet" type="text/css" href="newappname.css" / >
<style >
</style >
</head >
<body >
<script src="/prototype.js" language="JavaScript" type="text/javascript" > </script >
<script src="/updater.js" language="JavaScript" type="text/javascript" > </script >
<!-- ---------- MENUS ----------- -- >
<div id="menubar" > </div >
<div id="submenubar" > </div >
<!-- ---------- PROMPTS ----------- -- >
<div id="promptbox" > </div >
<!-- ---------- ATTENTION ----------- -- >
<div id="attention" > </div >
<!-- ---------- MASTHEAD ----------- -- >
<table cellpadding="0" cellspacing="0" id="masthead" >
<tr >
<td id="title" > </td >
<td id="ruleset" align="right" > </td >
</tr >
</table >
<!-- ---------- WINDOWS ----------- -- >
<div id="windowtabs" > </div >
<div id="windowset" > </div >
<!-- ---------- RE-LOGIN ----------- -- >
<div id="loginbox" style="visibility: hidden;" > </div >
</body >
</html >
Register user menu selections in your channel.mreg file. For example:
#menu "Story" "Create"
#msg "Story.create" storyButtonCreate();
#menu "Story" "Download"
#msg "Story.Download" storyButtonDownload();
#menu "Story" "Guide"
#msg "Story.Guide" storyButtonGuide();
Register each menu, menu option, message, and newappnameApp class method that handles the menu option.
Register user keystrokes in the channel.mreg file as. For example:
#key "ctrl-s"
#msg "storyedit.save" userSave();
Register each keystroke, message, and newappnameApp class method that handles the keystroke.
If your app responds to a user clicking on a specifically named region, register that mouse click in the channel.mreg file as. For example:
#hotspot "UserAccount"
#msg "account.settings" accountSettings();
Register each hotspot region, message, and newappnameApp class method that handles the click.
If your app responds to specific (x,y) mouse click inside a specifically named region, register that in the channel.mreg file as. For example:
#mouse "landmap"
#msg "map.landmap" mapPointSelect();
Register each mouse movement region, message, and newappnameApp class method that handles the click. The app will receive an event for each mouse down and mouse up event. Future plans include events for movement as well.
If other apps call your apps with requests or updates, register that in the channel.mreg file as. For example:
#api setfield(const UserDevice & device, const string & table, const string & field, const string & value)
/** Update/Replace the table and field entry with the value provided
* @param device - the end-user's device
* @param table - the area with a collection of fields in it
* @param field - the specific field within that area
* @param value - the string to display
* @return true if sent successfully
*/
#param device=device
#param table=table
#param field=field
#param value=value
#msg "field" void cmdField(const string & table, const string & field, const string & value);
/** Update/Replace the table and field entry with the value provided
* @param table - the area with a collection of fields in it
* @param field - the specific field within that area
* @param value - the string to display
*/
#param table=table
The API call that the other app makes follows #api. Inside your app, the method from the #msg line is called to handle the API call. The #param statement map message variables to/from the method calls.
Once your have created your channel.mreg file, convert it into code as msgChannel.cpp using
genmae channel.mreg -msg msgChannel.cpp
which creates code that integrates with the MAE environment. The code registers your application, accepts messages, and routes them appropriately. The code is readable and useful to reference. When compiling it, you may get errors reproted in msgChannel.cpp or channel.mreg.
If you support an API, you'll need to also generate your ChannelAPI.h and ChannelAPI.cpp file using
genmae channel.mreg -apicpp -apih
After creating all those #msg statements referring to methods to call, you can create method prototypes for your .h file using:
genmae channel.mreg -cbh
and you can generate dummy method code using
genmae channel.mreg -cbcpp
If UserMan is your user's login landing page, then you'll want to register your app with it so the user can navigate to your app's UI.
The user's home UI page is provided by UserMan. If the app is not registered there, the user will not be able to navigate to it.
In file /usr/mae/conf/[[commhub#Commhub.ini|commhub.ini]], update domain.apps (where domain is the value of domain.default in commhub.ini): add the new app's name. This requires a restart of commhub unless you use tpset:
tpset userman $(tpget userman domain.apps),newappname
But if you mess that up, you'll need to know what the previous value of domain.apps was.
You need to create /usr/mae/html/record/tilenewappname.html where newappname matches identically the newappname for the domain.apps property.
Example HTML file:
<!-- UserMan - Tile for HexEdit -- >
<figure id=tilemapedit class="tile" >
<img id=tilemapedit_image class="tile-image" src=images/tile_mapedit.jpg title="Edit indoor place maps" >
<figcaption id=tilemapedit_label class="tile-label" >Place Maps </figcaption >
</figure >
Inside the tilenewappname.html file, you specified an image file such as tile_mapedit.jpg (in above example). This file needs to be created and added to /usr/mae/html/images/.
Your app is not required to use this interface to access a datastore, but it is available as a abstracted datastore for your app. MAE's base daemons all use this datastore service.
Before running DbBroker (the datastore daemon), you must configure commhub.ini, so DbBroker knows where to connect. For example:
dbtypes=mysql,csv
mysql.tables=*
mysql.server=db
mysql.user=maeuser
mysql.passwd=maepasswd
mysql.dbname=newappname
mysql.dbport=3306
csv.tables=TestCSV
csv.TestCSV.filename=/usr/mae/worlddat/Item.csv
ini.tables=n/a
postgresql.tables=n/a
where
Once configured, start DbBroker to verify connectivity:
tstart dbbroker
tstatus dbbroker
Once the database has tables, you can view those table names using
tdbcmd -tables
And the schema for a specific table name can be viewed using
tdbcmd User -schema
MAE uses a number of tables for its basic daemons; those daemons won't work without those tables.
MAE datatbase tables can be configured using these commands:
cd /usr/mae/src
cat */table_*.sql | mysql -h db -u maeuser - p newappname
You will need to create database tables for your app if your app needs them. We recommend you put the SQL CREATE TABLE statement inside a table_newappname.sql file in your source code directory.
Note that each database table has an Id field, which is an integer and automatically incremented when records are appended.
Your app may access the datastore via DbBroker; use the Datastore class to do this, included using:
#include <Datastore.h >
Create a class to handle records for each database table. In that class, define a static variable for the handle to the database:
static Datastore db;
which is then declared using
Datastore newappname::db;
and initialize it using open():
if (!db.isOpen()) {
db.open(newappname);
if (!db.statusOK()) {
return false; // db is a no go
}
}
return true;
There are a number of methods for appending, deleting, setting, getting, and querying data.
A database table record is a series of key/value pairs, which is conveniently and dynamically stored in a HashArray. Create a new record by passing a HashArray, like this:
HashArray values;
values.set("Name", name);
DbRecNum id= db.addRecord(values);
Upon success, addRecord() returns the non-zero record id.
Given a record id, deleting that table record is straightforward:
db.deleteRecord(id);
Using DbBroker, the interface handles these field types:
To set a specific field value in the open table, use setValue(), like this
db.setValue(recno, fieldname, value);
When querying a field value, speicify the type as well:
db.getFieldLogical(recno, fieldname);
db.getFieldInt(recno, fieldname);
db.getFieldFloat(recno, fieldname);
db.getFieldString(recno, fieldname);
Note that a date if fetched as a string.
To query a list of record ids that match ANDed criteria, using the queryRecords() method. For example:
HashArray criteria;
criteria.set("User_id", user_id);
criteria.set("Setting_id", setting_id);
DbRecList result= db.queryRecords(criteria);
Creating a class for each database table can be tedious since the same basic methods need to be setup for the same operations. To get you started, you can use the genmae utility to generate your .h interface file and .cpp implementation file.
If you have already created your database table, you can type:
genmae db2h User
genmae db2cpp User
Used this way, it will connect to the database (it may prompt you for a password), grab the table schema, and generate the files in the current directory. For example, User.h and User.cpp in the above example. When pulling from the database, you lose the nuance of data structures, so the .h and .cpp will require more modification.
Instead, you can create a basic .h file with your record fields, bounded by spcial comments, such as
// CORE DATA BEGIN
DbRecNum id;
string Name;
string Password;
int LoginCount;
HashArray properties;
// CORE DATA END
and then run these commands
genmae incl temp.h > User.h
genmae cpp temp.h > User.cpp
That will generate enough code to provide you a solid starting point for your class.
There are two ways to send a message. Sending a message without expecting a synchronized answer is the most common. Sending a message and expecting a response is actually an RPC (Remote Procedure Call) message.
The point of sending a message is for another application to recieve it and respond to it. Both sides (sending, receiving) must be operating correctly for the message to be meaningful.
Coding an asynchonous is straightforward, but has required parts. Ensure the right include file:
#include <MAE.h >
which includes the Messenger class.
A message has these essential parts:
During program initialization, the app's channel must be declared to MAE. The msgbroker daemon receives the registration request. It is responsible for routing the request to its destination. If no app is listening on the destination channel, the message is not delivered. In your app's main program file, decalre the channel to be the same as the name of the program using
MAEApp maeapp("newappname");
or declare the channel differently using a longer form:
MAEApp maeapp("newappname", channel);
where channel is one or more channel names, separated by commas.
Before sending a message, the app must composing the message. The payload is stored in the XMLData structure, which may simply be a flat collection of properties or a hierarchical organization of properties. The property names should match the .mreg file that receives. If the request keyword is fixed, the properities can be documented in the sending app's .mreg file (in genmae, see #send).
Once all required parameters are ready, sending the message is a matter of calling Messenger::send(). For example:
static Messenger msg;
string destination= "other";
string request= "doit";
XMLData params;
params.set("color", "blue");
bool sent= msg.send(destination, request, params);
send() returns false if there is no active connection to commhub, otherwise, the message is sent to msgbroker for processing.
Registering a message channel, registering a message request, and routing those requests to your code is all handled inside the .mreg file, which genmae processes.
At the top of your newappname.mreg file, declare the channel like this
#channel=display
#appclass=UserApp
#appcache=UserCache
#cacheid=ui // hence UserCache[ui]
#instance=instance
where channel declares the channel name (which should match the MAEApp declaration in the main program), appclass is the class that manages each user's instance/state, appcache is the name of the vector of appclass instances, cacheid is the variable used for indexing into the vector (used for code generation), and instance is the name of the variable to hold the specific appcache instance for the current request being handled.
The next section is the code to find the index of the relevant instance. For example
#idcode
// process the parameters - we require a valid user
int user_id= device.getUserId();
if (user_id == 0)
user_id= param.getInt("uid");
if (user_id <= 0) {
// err out
string msg= (string)"Unknown user id(" + user_id + ") for " + request;
task.log (msg);
MAE::mgmt.incrementInputErrorCount();
return;
}
int ui= UserCache.findPos(user_id, false);
if (ui < 0) {
// We have a new user
UserApp newUserApp(user_id);
ui= UserCache.add(user_id, newUserApp);
}
if (ui < 0) {
// we were given an invalid user (we must have a valid user)
task.log ((string)"UserSettings(" + user_id + ") not found/created");
mgmt.incrementInboundErrorCount();
return;
}
UserApp & instance= UserCache[ui];
instance.setDevice(device);
Finally, declare the message request keyword, its expected parameters, and your program method to handle it. For example,
#msg "tileclick" doTile(const string & tile);
#param tile1=@device.getRegion()
#param tile2=@string(tile1,4)
#param tile=@string(tile2,0,tile2.size()-6)
which is trickier than most, but demonstrates severaal things. The #msg line declares the keyword tileclick and the app method to handle it, e.g. UserCache[ui].doTile(tile). The #param lines are typically assigned to the inbound parameters from the payload, but here the value of tile is calculated from the inbound parameter device which MAE pre-assigns. Often, a #param statement simply maps an inbound variable name to a program variable, but if the inbound parameter begins with @, then the text following it is taken as literal program code. Note that tile1 and tile2 are not inbound parameters or previously declared program variables; genmae knows this and defaults them to string variables; it knows that tile is a string because of the doTile() declaration.
Synchronous messages send a request and receive a response. App program execution is paused while waiting for the response. For a synchronous message to be successful, both sides (sending and receiving) of the message must operate correctly.
Sending a synchronous message is very similar to sending an asynchonous message, but it uses the RemoteService class instead of the Messenger Class and it does not need to know the destination channel. Here's an example:
XMLData param;
param.set("setting", setting.toHashArray().toString());
param.set("wid", setting.getId());
param.set("name", name);
XMLData result= RemoteService::call("melee.addTeam", param);
where you'll observe that parameters of structures are serialized to strings for transmission. The response comes back as an XMLData structure with these properties:
Here's an example of handling the response:
if (!errmsg.empty()) {
string logmsg= string("Error: RPC addTeam.") + setting.getId() + "." + name + " failed: " + errmsg;
task.log(logmsg);
return -1;
}
int team_no= result.getInt("team_no");
The app that recieves the synchronous request must register the service. This is done in the main program file in the MAE::init() method. Each service must have a unique keyword. When registering, a static callback method must be specified; it will be called each time a request is received. For example,
RemoteService serviceAddTeam("melee.addTeam", MeleeApp::rpcAddTeam);
where melee.addTeam is the keyword and MeleeApp::rpcAddTeam is the callback.
Next, create the callback to handle the synchronous message. This method receives:
The callback routine processes the inbound parameters, performs its function, and then constructs its result to be transmitted back. The result is ultimately returned by this method. Here's an example:
XMLData MeleeApp::rpcAddTeam(string service, XMLData & params, string appname, string user, void * data)
{
DbRecNum setting_id= params.getInt("wid");
string team_name= params["name"];
XMLData result;
int si= findSetting(setting_id);
if (si < 0) {
// we were given an invalid setting
task.log ((string)"MeleeApp::rpcAddTeam: Error from " + appname + ": invalid setting (id="+setting_id+")");
RemoteService::setError(result, string("melee.rpcAddTeam: Invalid setting_id ") + setting_id);
}
else {
// add the team; return the team's id
int team_no= MeleeCache[si].addTeam(team_name);
result.set("team_no", team_no);
}
return result;
}
If an error is encountered during procesing of the request, the app can call RemoteService::setError() to register the error in the response.
If your daemon/task/app needs to report a log message of interest to MAE or users maintaining your software, log the message using MAE logging facility. For example:
task.log((string)"User "+user_id+" has enabled feature 23");
Your message will be routed to the root commhub and saved with a timestamp and your app's name to /usr/mae/log/commhub.log.
Aside from viewing the file directly, you can view the file using the tviewlog tool. This tool allows you to view the last few messages specific to an app, for example:
tviewlog -20 appname
will show you the last 20 messages logged concerting appname.
In addition to your usual debuggers or debugging tools, MAE provides a debug logging facility. To toggle debugging on/off, the user types
tdebug appname
When debug is toggled on, the file /usr/mae/log/appnamedbg.log appears. MAE writes to this file, letting you know what is happening inside MAE. You may also write to this file; just use the FILE * dbgf like this:
if (debug_on) fprintf (dbgf, "Debug checkpoint\n");
When debug is toggled off, debug logging stops. You can delete appnamedbg.log and toggle debugging back if, if desired.
Although the debug output file contains MAE messages going to/from your app, you may be specifically interested in just those messages. If so, you can toggle a message tap on/off using:
ttap appname
When the tap begins, it will append a timestamp and message to the file /usr/mae/log/appname.tap. All MAE messages are logged: DbBroker, MsgBroker messages, RpcBroker, and CommHub control messages messages.
Update /usr/mae/src/Makefile so it visits the directory for newappname for targets: daemons, deps and clean.
The supervisor app queries the state of apps periodically and provides an interface for the application operator to tweak some settings. This is enabled by handling management messages in mgmtNewappname.cpp.