|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Pebbles Software Architecture
From the Pittsburgh Pebbles PDA Project
| |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
DLL PluginTo compile and install the DLL plugin:
EXE PluginTo compile and install the EXE plugin:
Palm ClientTo compile and install the Palm client:
Windows CE ClientTo compile and install the Windows CE client:
|
Once you have at least one client and one plugin installed correctly, you should be able to connect your PDA to the PC and run Ping on your PDA. Tapping on the "Ping" button should make the PC beep, followed a short time later by a beep from your PDA.
The communication protocol used by Pebbles applications is defined by the header file Pebbles.h (also available in the Ping ZIP archive as Ping\Pebbles.h). The following discussion essentially duplicates the comments in this header file.
A Pebbles application consists of two parts: a program running on a PDA (the "client"), and a program running on a desktop computer (the "plugin"). The client and the plugin communicate through a central mediator called PebblesPC, a process which runs on the desktop computer and multiplexes clients with plugins. The basic architecture looks like this:
+------------> DLL plugin
| Windows
| messages
|
PDA client <-------------> PebblesPC <-----+
serial port |
or | network
network socket | socket
+------------> remote plugin process
PDA clients communicate with PebblesPC through some kind of byte stream (serial ports and network sockets are supported in the current implementation). PebblesPC communicates with plugins using either Windows event messages (for DLL plugins, which are loaded into PebblesPC's address space) or a network socket (for plugins running in a different address space or across the network).
This document defines the protocols for all three interfaces: the client protocol for the interface between clients and PebblesPC, the DLL plugin protocol for the interface between PebblesPC and DLL plugins, and the socket plugin protocol for the interface between PebblesPC and remote plugins.
PalmPilot developers should be aware of the distinction between a Pebbles plugin and a HotSync conduit. The Pebbles architecture is intended to support applications that need a live connection between the PDA and the PC. Examples of such applications include chat and emulating the keyboard and mouse. A Pebbles plugin is ready to run at all times -- it responds immediately when a user connects a PDA to the PC and starts up the associated client on the PDA. HotSync conduits, on the other hand, are intended to synchronize data on the PDA with data on the PC, and are invoked only when the user initiates a HotSync.
As indicated in the previous section, the PDA client communicates with PebblesPC through a byte stream. PebblesPC currently supports two byte stream interfaces:
The message protocol is the same regardless of whether the client connects through a serial port or a network socket. Messages are passed back and forth on the byte stream using a simple format. Short messages (less than 64KB) look like this:
command 1 byte Command code (see below) length 2 bytes Length of data field in bytes, LSB first data <length> bytes Data or arguments for command
Pebbles.h defines several macros which are useful for parsing and assembling messages:
Parsing: PEBBLES_CMD(msg) Returns the command code of a message PEBBLES_LEN(msg) Returns the length of a message's data block PEBBLES_DATA(msg) Returns a pointer to a message's data Assembling: PEBBLES_HEADER_SIZE # of bytes in a Pebbles short message header i.e., sizeof(command and length fields) PEBBLES_SET_CMD(msg, cmd) Sets the command code of a message PEBBLES_SET_LEN(msg, len) Sets the length of a message PEBBLES_DATA(msg) Returns a pointer to message data to be filled in
command 1 byte Command code (see below) flag 2 bytes Always 0xFFFF to indicate long message length 4 bytes Length of data field in bytes, little-endian data <length> bytes Data or arguments for command
Several macros are provided for handling long messages:
PEBBLES_LEN(msg) == PEBBLES_LONG_MESSAGE_SIZE
Tests whether a message is short or long format
Parsing:
PEBBLES_CMD(msg) Returns the command code of a long message
PEBBLES_LONG_LEN(msg) Returns the length of a long message's data block
PEBBLES_LONG_DATA(msg) Returns a pointer to a long message's data
Assembling:
PEBBLES_LONG_HEADER_SIZE # of bytes in a Pebbles long message header
i.e., sizeof(command, flag, and length fields)
PEBBLES_SET_CMD(msg, cmd) Sets the command code of a long message
PEBBLES_SET_LEN(msg, PEBBLES_LONG_MESSAGE_SIZE)
Sets the flag field to indicate a long message
PEBBLES_SET_LONG_LEN(msg, len) Sets the length of a long message
PEBBLES_LONG_DATA(msg) Returns a pointer to message data to be filled in
The following command codes defined in Pebbles.h are reserved by PebblesPC for system commands:
#define CMD_PEBBLES_NO_MESSAGE 0
// Reserved as a return value for PebblesReceiveHeader()
#define CMD_PEBBLES_STARTUP 1
// Data: none
//
// Sent by PebblesPC to client when PebblesPC opens a serial port,
// just in case an client is listening at the other end.
// Client responds by sending CMD_PEBBLES_CHANGE_PLUGIN.
#define CMD_PEBBLES_CHANGE_PLUGIN 2
// Data:
// either no data
// or three fields: plugin name (null-terminated string, case doesn't matter)
// user name (null-terminated string)
// hardware flow control, 1 byte (0 to disable, !0 to enable)
//
// Sent by client to switch to a plugin. If no data is sent with the message,
// the client is disconnected from its plugin. Client should send a no-data
// CMD_PEBBLES_CHANGE_PLUGIN when it exits, to notify plugin that it's
// gone.
#define CMD_PEBBLES_ACK_CHANGE_PLUGIN 3
// Data: none
//
// Sent by PebblesPC to acknowledge that it has found and
// connected to the plugin requested by CMD_PEBBLES_CHANGE_PLUGIN.
// Client should not send any user-defined commands
// until it sees this acknowledgement.
#define CMD_PEBBLES_NO_ACK_CHANGE_PLUGIN 4
// Data: none
//
// Sent by PebblesPC to inform the PDA that the requested
// plugin could not be found.
#define CMD_PEBBLES_KEEPALIVE 5
// Data: none
//
// Sent to ensure that connection between PDA and PC
// is still active
#define CMD_PEBBLES_FLOW_CONTROL 6
// Data: 1-byte flag to turn on or off flow control (1 means on, 0 means off)
//
// Sent by an app to change PebblesPC's output hardware flow control.
// Flow control needs to be turned off for raw IR (which doesn't support it)
// and turned on for regular serial ports.
//
// Hardware flow control is always re-enabled after every CHANGE_PLUGIN,
// so the app must disable it after every CHANGE_PLUGIN if desired.
#define CMD_PEBBLES_CHANGE_USERNAME 7
// Data: user name
//
// Sent by app to PC and contains user name
#define CMD_PEBBLES_RESERVED1 8
// Reserved because of a bug -- PalmPilot sometimes sends this byte
// when it opens its serial port.
#define CMD_PEBBLES_RESERVED2 127
// Reserved because of a bug -- PalmPilot sometimes sends this byte
// when it closes its serial port.
#define CMD_PEBBLES_HOTSYNC -128
// Reserved because HotSync uses this byte to indicate the start of
// a Hotsync session.
To avoid conflicting with these and future system commands, Pebbles applications should limit their commands to the following range:
#define CMD_PEBBLES_BASE 32 // smallest application-specific command #define CMD_PEBBLES_MAX 126 // largest application-specific command
Under the current protocol, only one Pebbles application can use the serial connection at a time, so application developers need not worry that their application-defined command codes will conflict with those of other applications.
A DLL plugin is dynamically loaded into PebblesPC. When the plugin is loaded, it spawns a Win32 thread in PebblesPC's address space and returns the thread handle to PebblesPC. All subsequent communication between PebblesPC and the plugin occurs by passing Windows events back and forth, using the Win32 system call PostThreadMessage.
A plugin should be compiled as a DLL which exports the following startup function for PebblesPC to call:
extern "C" BOOL WINAPI PebblesMain (HWND hwnd, PebblesPlugin *pPlugin);
PebblesMain should perform initialization and create a thread for the plugin. The parameters are as follows:
typedef struct _PebblesPlugin {
unsigned long thread; // Plugin thread ID.
// Plugin should always set this field!
int hasAbout; // true if plugin has an About dialog box
// and will respond to WM_PEBBLES_ABOUT event.
// Defaults to false.
int hasPrefs; // true if plugin has a configuration dialog box
// and will respond to WM_PEBBLES_PREFS event.
// Defaults to false.
void * hThread; // Plugin thread handle (the return value of CreateThread).
// Plugin should always set this field!
} PebblesPlugin;
PebblesMain should return true if the plugin started successfully.
PebblesMain should start a running thread to serve as the plugin, and return the thread handle in pPlugin->thread. You can create a thread in a variety of ways under Win32: CreateThread(), _beginthread(), _beginthreadex(), or (using MFC) the class CWinThread are all possibilities.
When PebblesPC starts up, it attempts to load all DLLs located in the same directory as PebblesPC.exe and calls PebblesMain on each one. Plugins which were successfully found and loaded are displayed in the "Plugins..." dialog box.
DLL plugins run in the same address space as PebblesPC. As a result, DLL plugin developers should observe a few cautions:
After a plugin has been started, PebblesPC communicates with it by sending events to its Windows event queue. A Windows event has three fields: MSG (an event code), WPARAM (a 32-bit argument), and LPARAM (another 32-bit argument). The MSG codes used by PebblesPC are defined below:
#define WM_PEBBLES_BASE (WM_USER + 4242)
#define WM_PEBBLES_NEW_USER (WM_PEBBLES_BASE + 0)
// Sent by PebblesPC to plugin when a new user connects to the plugin.
//
// wParam (PebblesUser*) points to PebblesUser struct.
// Pointer is valid as long as user remains
// connected; plugin should not free it.
//
// lParam 0
#define WM_PEBBLES_DONE_USER (WM_PEBBLES_BASE + 1)
// Sent by PebblesPC to plugin when a user disconnects from the plugin.
//
// wParam (PebblesUser*) points to PebblesUser struct.
// Pointer is valid as long as user remains
// connected; plugin should not free it.
//
// lParam 0
#define WM_PEBBLES_RECEIVED (WM_PEBBLES_BASE + 2)
// Sent by PebblesPC to the plugin when a user-defined command is
// received from a connected app.
//
// wParam (PebblesUser*) points to PebblesUser struct.
// Pointer is valid as long as user remains
// connected; plugin should not free it.
//
// lParam (char*) points to the message,
// which starts with a CMD_ code and a
// length field, in the format described above.
// Plugin must free this block with GlobalFree.
#define WM_PEBBLES_SEND (WM_PEBBLES_BASE + 3)
// Sent by plugin to PebblesPC to send a message back to the app.
// The plugin should send this message with PostThreadMessage()
// using the thread handle in the PebblesUser structure.
//
// wParam (PebblesUser*) points to PebblesUser struct.
// Pointer is valid as long as user remains
// connected; plugin should not free it.
//
// lParam (char*) points to the message,
// which starts with a CMD_ code and a
// length field, in the format described above.
// Must be allocated with GlobalAlloc.
// PebblesPC will free this block with GlobalFree.
#define WM_PEBBLES_ABOUT (WM_PEBBLES_BASE + 6)
// Sent by PebblesPC to plugin to display an About box describing
// the plugin. Plugins with no About box can beep or do nothing.
//
// wParam (HWND) PebblesPC window handle
//
// lParam 0
#define WM_PEBBLES_PREFS (WM_PEBBLES_BASE + 7)
// Sent by PebblesPC to plugin to display an optional configuration
// dialog for the plugin. Plugins with no configuration can beep
// or do nothing.
//
// wParam (HWND) PebblesPC window handle
//
// lParam 0
#define WM_PEBBLES_SOCKET_PLUGIN_NAME (WM_PEBBLES_BASE + 8)
// Sent by PluginData to notify Pebbles that it has received a name
// and is ready to be added to the plugin list
//
// wParam (CPebblesPluginData *) Pointer to Plugin Data class
//
// lParam 0
Several of these events include a pointer to PebblesUser, which is a structure describing a Pebbles user. The most important field of PebblesUser is thread, which is the handle of a PebblesPC thread that may be used to send messages back to the PDA corresponding to this user.
typedef struct _PebblesUser {
int id; // Small integer corresponding to user's serial
// connection (numbered from 0).
char *name; // User name (null-terminated; may be zero
// length, but never NULL).
unsigned long thread; // User thread ID; outgoing
// WM_PEBBLES_SEND events should be posted
// here.
} PebblesUser;
PebblesPC also supports plugins running in a remote address space, either on the same machine or across the network, communicating by a network socket. If you have an existing executable program that you want to act as a Pebbles plugin, then you want to use this protocol.
A simple implementation of the socket plugin protocol is provided with the Ping sample application (Ping\PC-exe\PebblesSocket.cpp and Ping\PC-exe\PebblesSocket.h).
To connect to PebblesPC, call PebblesSocketConnect(). This section explains how the connection process works.
The remote plugin originates the connection to PebblesPC. If PebblesPC is allowing network connections (controlled by a checkbox on the main PebblesPC dialog), it listens for plugin connections on port 4242 (PEBBLES_PLUGIN_PORTNUM in Pebbles.h).
After opening a connection to PebblesPC, the plugin sends a CMD_SOCKET_PLUGIN_NAME message to provide its name to PebblesPC.
Messages in the socket plugin protocol are similar to the client protocol. Socket plugin messages have one additional field after the header:
Short messages (less than 64KB): command 1 byte Command code (see below) length 2 bytes Length of data field in bytes, LSB first userid 1 byte User ID data <length> bytes Data or arguments for command Long messages: command 1 byte Command code (see below) flag 2 bytes Always 0xFFFF to indicate long message length 4 bytes Length of data field in bytes, little-endian userid 1 byte User ID data <length> bytes Data or arguments for command
Two special values are defined for the userid field:
#define PEBBLES_USERID_ALL_USERS 0 #define PEBBLES_USERID_NO_USER 255
NO_USER should be used for control messages intended for PebblesPC, such as CMD_SOCKET_PLUGIN_NAME. ALL_USERS can be used to broadcast a message to all users connected to this plugin.
Messages can be read and written using PebblesSocketRead() and PebblesSocketWrite(). These functions read and write full messages.
For parsing and assembling socket plugin messages, Pebbles.h defines several macros. For the command and length fields, see the macros for the client protocol.
PEBBLES_GET_USERID(msg) Returns the userid field of any kind of message PEBBLES_SET_USERID(msg, userid) Sets the userid field of any kind of message PEBBLES_SOCKET_MESSAGE(msg) Returns a pointer to a socket plugin message's data
The following command codes defined in Pebbles.h are used for the socket plugin protocol.
// sent to socket plugin when a new user arrives #define CMD_SOCKET_NEW_USER 9 // Data: user name (no null terminator) // sent to socket plugin when a user leaves #define CMD_SOCKET_DONE_USER 10 // sent by socket plugin to announce its name to PebblesPC #define CMD_SOCKET_PLUGIN_NAME 12 // Data: plugin name (no null terminator)
In this section, we illustrate the Pebbles protocol with a complete example using Ping. This example shows how PebblesPC starts up, how it finds and loads a DLL plugin, how it forwards messages between a client and the plugin, and how everything shuts down. The flowchart below depicts the three software entities (client, PebblesPC, and plugin) and the messages that pass between them.
|
Ping Client |
|
PebblesPC |
|
Ping Plugin (DLL on PC) |
1. User executes PebblesPC.exe on the PC.
| | \|/ |
||||
| 2. PebblesPC searches for DLLs in its executable's directory and tries to call PebblesMain on each one. |
PebblesMain() -----------------> |
3. Plugin creates a thread, performs any needed initialization, and
returns the thread handle to PebblesPC.
| | \|/ |
||
| 5. No Pebbles client is currently running on the PDA, so the message is ignored. (If a Pebbles client were running, it would try to connect to its plugin as shown in the next step.) |
CMD_STARTUP <----------------- |
4. PebblesPC opens its serial port(s) and sends out a startup message, in case a PDA is already connected and waiting on the other end. |
<----------------- |
|
|
|
||||
6. User switches to Ping client on the PDA.
| | \|/ |
||||
| 7. Ping client opens the PDA serial port and requests a connection to its plugin. |
CMD_CHANGE_PLUGIN Data: "Ping" -----------------> |
8. PebblesPC finds "Ping" in its list of loaded DLLs and sets up a
virtual connection between the PDA and the plugin.
| | \|/ |
|
|
| 10. Ping client records that it has successfully connected, and beeps to inform the user as well. |
CMD_ACK_CHANGE_PLUGIN <----------------- |
9. PebblesPC notifies the client that it has successfully connected to
a plugin.
| | \|/ |
|
|
| 11. PebblesPC notifies the plugin that a new user has connected to it. |
WM_PEBBLES_NEW_USER -----------------> |
12. Ping plugin does nothing with this message. (Other plugins might respond by recording the user in a list, or sending some data to the client.) | ||
|
|
||||
|
13. User taps on "Ping" button. | | \|/ |
||||
| 14. Ping client sends an application-defined message to its plugin. |
CMD_PING -----------------> |
15. Since the command code is application-defined, PebblesPC simply forwards the whole message to the plugin using a Win32 event. |
WM_PEBBLES_RECEIVED -----------------> |
16. Ping plugin unpacks the client's message, and looks at the command
code.
| | \|/ |
| 19. Ping client responds to CMD_PONG message by beeping. |
CMD_PONG <----------------- |
18. PebblesPC forwards the message to the client. |
WM_PEBBLES_SEND <----------------- |
17. Ping plugin responds to CMD_PING by causing the PC to beep and constructing a CMD_PONG reply message. The reply message is passed back to PebblesPC as a Win32 event sent to the thread handle in PebblesUser. |
|
|
||||
20. User switches away from the Ping application.
| | \|/ |
||||
| 21. Ping disconnects from its plugin by sending an empty change-plugin message, then closes its serial port. |
CMD_CHANGE_PLUGIN Data: "" -----------------> |
22. PebblesPC notifies the plugin that the user has disconnected. |
WM_PEBBLES_DONE_USER -----------------> |
23. Ping plugin does nothing with this message. (Other plugins might respond by removing the user from a list of current users.) |
|
|
||||
24. User exits PebblesPC.
| | \|/ |
||||
| 25. Before shutting down, PebblesPC tells each loaded plugin to clean up and exit using a standard Windows message. |
WM_QUIT -----------------> |
26. Ping plugin does any necessary cleanup.
| | \|/ |
||
| 28. After detecting that all loaded plugins have exited, PebblesPC exits. |
<----------------- |
27. Ping plugin exits. | ||