Category: Projects Related to Palm OS PDAs

Bluetooth Communication with Palm OS (Part 2/2)

Processing and displaying the collected data

Published on: 2020-06-07

In the last part, we configured a PHP server, which is now able to deliver the data that needs to be displayed on a Bluetooth-enabled Palm PDA. A connection is needed to transfer the collected data to the Palm. An ESP32 is perfect for transferring the data from the PHP server to the Palm since it can talk "legacy" Bluetooth and access WiFi simultaneously.

This article is grouped into two parts: the first part is about the setup of the "transfer unit" and the second part is about writing the Palm OS application, which displays the fetched data on a Palm OS device.

Needed Parts

These parts are required for transferring the collected data and displaying it on a Palm:

Setting Up an ESP32

At this point, I will again skip the part of explaining how to set up an ESP32 with the Arduino IDE. There are already good tutorials that explain this very well, like this one: https://randomnerdtutorials.com/installing-the-esp32-board-in-arduino-ide-windows-instructions/

Make sure the drivers for the ESP, as mentioned in the linked article, are installed: https://www.silabs.com/products/development-tools/software/usb-to-uart-bridge-vcp-drivers
Also, these Arduino IDE settings worked fine for me:

("Keine" can be translated as "none".)

Since there is no wiring needed for the ESP32 (except for the power supply), the only important part is the code.

The Code

The only task for the ESP is to receive a request from the Palm, process it, request the needed data from the PHP server, and send it back to the Palm.

Show/hide source code
#include <WiFi.h>
#include <BluetoothSerial.h>
#include <HTTPClient.h>


const char* ssid = "My Wifi SSID";
const char* password =  "secret-password";

BluetoothSerial SerialBT;
char *pin = "1234";

void setup() {

  Serial.begin(9600);
  
  delay(1000); // Power up

  SerialBT.begin("ESP32"); //Bluetooth device name
  SerialBT.setPin(pin);
  Serial.println("The bt-device started, now you can pair it with bluetooth!");

  Serial.println("Starting Wifi...");

  wl_status_t state;

  state = WiFi.status();
      
  while (state != WL_CONNECTED) {
      WiFi.mode(WIFI_STA);
      WiFi.begin(ssid, password);
      Serial.print(".");
      vTaskDelay (2500);
      state = WiFi.status();
  }      
  Serial.println("");
  Serial.print("Connected to ");
  Serial.println(ssid);
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());

}
void loop() {

  while(SerialBT.available()) {

    Serial.println("BT-Data available.");
    
    String sensor = SerialBT.readString();
    sensor.trim();
    Serial.println(sensor);
     
    if ((WiFi.status() == WL_CONNECTED)) {

      Serial.println("Wifi still connected.");
      
      HTTPClient http;
   
      http.begin("http://192.168.1.55/dataProvider.php?sensor="+sensor);
      int httpCode = http.GET();

      Serial.println(httpCode);
   
      if (httpCode > 0) { //Check for the returning code
         
          //http.writeToStream(&SerialBT);
          String payload = http.getString();
          payload.trim();
          Serial.println(payload);
          SerialBT.println(payload);
      } else {
        Serial.println("Invalid data.");
      }
      
      http.end(); //Free the resource
      
      
    } else {
      Serial.println("Need to restart Wifi...");
      WiFi.mode(WIFI_STA);
      WiFi.begin(ssid, password);
      
   }
  }
}

First, we need to include the libraries for WiFi, Bluetooth, and an HTTP client. In lines 5 and 6, the credentials for the WiFi are set. After instantiating the BluetoothSerial class, in line 9, the pin is defined, which is used for authentication. In line 14, the serial port is opened for the debugging output of the serial monitor of the Arduino IDE. We then give the script some time (line 16) to power up completely. With lines 18 and 19, everything is prepared for a Bluetooth connection. After this point, only the WiFi connection gets established.

In the loop function, we wait for the request from the Palm PDA (line 44). After we receive one, the requested data (from the corresponding sensor - line 48) is trimmed to remove any unwanted whitespaces (line 49). After we check that the WiFi connection is still present (line 52) (or restart the connection in lines 79 and 80), we use the HTTP client (line 56) to fetch the needed data from the PHP server (lines 58 - 67). It is important to provide the correct IP address (192.168.1.55 is only a sample IP). Then we simply redirect the data via the Bluetooth connection to our Palm PDA (line 69).

The Palm OS Application

The last and biggest part is about the Palm OS application. We will establish a Bluetooth connection to the ESP32, receive data, process it, and display it on the screen. We will also handle lost connections. The complete project is available here: BluetoothComm source code and can be built with Codewarrior 9.3. (The debug target has some problems, but the release target works fine.)

If you just want to try it out, here is the prebuilt .prc file: bluetoothComm.prc
But keep in mind that at least an ESP32, which sends some mocked data, is needed for a test.

The application is divided into four parts:

BluetoothComm.c

Show/hide source code
/*
 * BluetoothComm.c
 *
 * main file for BluetoothComm
 *
 */
 
#include "BluetoothComm.h"
#include "BluetoothComm_Rsc.h"
#include "BtAppLib.h"
#include "UiAppLib.h"
#include "SrAppLib.h"
 
#include <PalmOS.h>
#include <PalmOSGlue.h>
#include <UI/UIPublic.h>
#include <System/SystemPublic.h>
#include <PalmCompatibility.h>
#include <PalmTypes.h>
#include <PalmOS.h>

// The actual start point of the application,
// which also checks the OS version.
UInt32 PilotMain(UInt16 cmd, MemPtr cmdPBP, UInt16 launchFlags)
{
        UInt32 currentRomVersion;
        UInt32 requiredRomVersion;
        requiredRomVersion = 0x04000000;
        setErrorExit(0);

        FtrGet(sysFtrCreator, sysFtrNumROMVersion, ¤tRomVersion);
        if (currentRomVersion < requiredRomVersion)
        {
                FrmAlert(OsVersionTooLowAlert);
                return(sysErrRomIncompatible);
        }

        if (cmd == sysAppLaunchCmdNormalLaunch)
        {
                Err returnCode;
                returnCode = startApplication();

                if (returnCode != 0)
                {
                        ErrAlert(returnCode);
                        return(returnCode);
                }
                AppEventLoop();

                returnCode = stopApplication();
                
                if (returnCode != 0 && !getErrorExit())
                {
                        ErrAlert(returnCode);
                        return(returnCode);
                }
        }

        return 0;
}

// startApplication also starts the Bluetooth stack as client
Err startApplication()
{
	Err returnCode = 0;

	returnCode = startBluetoothStack();
	if ( returnCode == 0) {
		FrmGotoForm(MainForm);
	}

	return returnCode;
}

// This is the main event loop 
static void AppEventLoop(void)
{
        EventType event;
        Err error;
       
        do
        {
                EvtGetEvent(&event, evtWaitForever);

				
                if (SysHandleEvent(&event)) {
                        continue;
                }


                if (MenuHandleEvent((void *)0, &event, &error)) {
                        continue;
                }


                if (AppHandleEvent(&event)) {
                        continue;
                }


                FrmDispatchEvent(&event);
                
        }
        while (event.eType != appStopEvent && !getErrorExit());
}

// This is the function with the business logic
// This function handles all form events like (software) button presses
static Boolean MainFormHandleEvent(EventType *event)
{

    Boolean handled = false;
    Boolean btRequest = false;
    UInt8 waitCounter = 0;
    UInt8 maxCounter = 10;
    char* rawData = NULL;
    UInt16 tappedButton;
    
  	// The following strings must be identical to the ones in the config.php!
	char* bedroomPtr = "Bedroom"; 
	char* basementPtr =  "Basement";

    if(event->eType == frmOpenEvent) {

            FrmDrawForm(FrmGetActiveForm());
            ClearField(MainForm, DialogField);
			WriteInDialogField(MainForm, DialogField, "Please select a data point.");
            handled = true;
    }

    if(event->eType == ctlSelectEvent) {

            switch (event->data.ctlSelect.controlID)
            {

                case ClearButton:
                        ClearField(MainForm, DialogField);
                        handled = true;
                        break;
                        
                case BedroomBtn:
                		
                        SendDataSerial(bedroomPtr);
                        handled = true;
                        btRequest = true;
                        break;
                        
                case BasementBtn:
                        SendDataSerial(basementPtr);
                        handled = true;
                        btRequest = true;
                        break;
                        
                default:
                  		break;
      		}
    }
    
    if(btRequest){
    
    	ClearField(MainForm, DialogField);
    	WriteInDialogField(MainForm, DialogField, "Fetching data...");
    
	    while(!rawData){
	    
	        waitCounter++;
    		SysTaskDelay(sysTicksPerSecond/4);
	    
			// Read data from the serial port.
		    rawData = ReadSerial();
		    
			if(rawData && StrLen(rawData) > 20){
			
				ProcessSerialData(rawData);
				
			}
			
			if(getErrorExit()){
				break;
			}
			
			if(waitCounter >= maxCounter){
			    ClearField(MainForm, DialogField);
    			WriteInDialogField(MainForm, DialogField, "Fetching data... failed!");
    			
    			tappedButton = handleConnectionLost(BtErrorOccurredAlert3);
    			
    			if(tappedButton == 0 || tappedButton == 1){
    				// exit or reconnect, no more action here required
					break;
				}
    			
				if(tappedButton == 2){
					// retry
					ClearField(MainForm, DialogField);
    				WriteInDialogField(MainForm, DialogField, "Retrying to fetch data...");
					
					waitCounter = 0;
					maxCounter = 20;
				}
				

				
			}
		}
		

		
	}
    
    return handled;
}

// Handle all incoming events
static Boolean AppHandleEvent(EventPtr event)
{
        FormType* formTypePtr;
        UInt16 formId;
        Boolean handled = false;

        if (event->eType == frmLoadEvent)
        {

                formId = event->data.frmLoad.formID;
                formTypePtr = FrmInitForm(formId);
                FrmSetActiveForm(formTypePtr);

                switch (formId)
                {
                case MainForm:
                        FrmSetEventHandler(formTypePtr, MainFormHandleEvent);
                        break;


                default:
                        break;
                }

                return true;
        }

        if (event->eType == menuEvent)
        {
                MenuEraseStatus(NULL);
                switch (event->data.menu.itemID)
                {
                case MainOptionsHelpCmd:
                        FrmAlert(HelpAlert);
                        handled = true;
                        break;

                case MainOptionsAboutCmd:
                        FrmAlert(AboutApplicationAlert);
                        handled = true;
                        break;

                default:
                        break;
                }

                return true;
        }

        return false;
}

// stop the application and close all open ports
Err stopApplication()
{
        Err error;
        UInt16 btPort = getBtPort();
         
        ClearField(MainForm, DialogField);
		WriteInDialogField(MainForm, DialogField, "Shutting down Bluetooth stack...");
       
		error = SrmSendWait(btPort);
		ErrNonFatalDisplayIf(error == serErrBadPort,"SrmClose: bad port");
		if (error == serErrTimeOut){
			FrmAlert(ErrorOccurredAlert);
		}
		SrmClose(btPort);
        FrmCloseAllForms();
        return error;
}

// Triggers an exit if an error occurs (if ErrorExit is not zero)
Err getErrorExit(){

	UInt32 error = 0;

	FtrGet(appFileCreator, 10110, &error);
	
	return error;
}
void setErrorExit(UInt32 error){

	FtrSet(appFileCreator, 10110, error);
}

// This function processes the serial data, which gets received by the ESP32
static void ProcessSerialData(char* rawData){

	UInt8 i;
	UInt8 c = 0;
	UInt8 j = 0;
	UInt8 k = 0;

	char complete[300] = "";
	char time[30] = "";
	char temp[15] = "";
	char hum[15] = "";
	
	// Not beautiful, but it is working.
	// I'm not sure if there is something like "explode(';',rawData);"
	// Since, every data has a fixed length, 
	// we can work with fixed values, too. 
	for(i = 0; i < StrLen(rawData); i++){

		if( ';' == rawData[i]){
			c++;
		}

		if(c == 0 && rawData[i] != ';' && i < 20){
			time[i] = rawData[i];
		}

		if(c == 1 && rawData[i] != ';' && j < 5){
			hum[j] += rawData[i];
			j++;
		}

		if(c == 2 && rawData[i] != ';' && k < 5){
			temp[k] = rawData[i];
			k++;
		}
	}
	  
	StrCat(complete, "Time: ");
	StrCat(complete, time);
	StrCat(complete, "\n");
	StrCat(complete, "Temperature: ");
	StrCat(complete, temp);
	StrCat(complete, "C\n");
	StrCat(complete, "Humidity: ");
	StrCat(complete, hum);
	StrCat(complete, "%\n");
	
	ClearField(MainForm, DialogField);
	WriteInDialogField(MainForm, DialogField, complete);	
}

The first function is the PilotMain. This is the start point of our application. Here we check if Palm OS 4 or higher is available. Since there is also an SDIO Bluetooth card for Palm OS 4 devices with an SDIO slot, the application can also run on these PDAs (e.g., the Palm m125). After the check, startApplication() gets called, where the Bluetooth stack gets started with: startBluetoothStack() This function is defined in the BtAppLib.c. The AppEventLoop simply receives new events and redirects them to the appropriate functions. The most important is: MainFormHandleEvent since AppHandleEvent "only" handles the menu events. (Of course, this could be more important if we wanted it to be.) In the MainFormHandleEvent, we tell the user what to do ("Please select a data point.") after the "frmOpenEvent" is fired and the active (current) form is drawn.
Next, we handle the button press events. There are three buttons:

The clear button simply calls ClearField in UiAppLib.c, which replaces the content of the big text field with an empty string. The bedroom and basement buttons only send a request to the ESP32 via the (serial) Bluetooth interface. SendDataSerial is defined in SrAppLib.c. If the request is sent, we wait 10x 250ms for an answer from the ESP. If we get an answer, we process and display it with ProcessSerialData(rawData). If we do not get an answer within this time, we assume that the connection is lost and display an alert form via handleConnectionLost in UiAppLib.c. The "exit" and "reconnect" functionality is handled inside handleConnectionLost since we need this functionality elsewhere in the code, too. However, we cannot be sure if the ESP32 doesn't take longer than the given time to process the request. So at this point, the user also has the option to retry the request. We simply wait a little longer for an answer from the ESP32. If it takes too long again, we display the same alert form again.
The stopApplication function actually only shuts down the Bluetooth stack. The getErrorExit and setErrorExit functions are used for leaving the application when an error occurs. ProcessSerialData, as mentioned, processes the incoming data from the ESP32 and displays it. Since the data has always a fixed length, splitting the data can be done with fixed positions. It is not nice, but it works.

 

BtAppLib.c

Show/hide source code
#include <BtLib.h>
#include <BtLibTypes.h>
#include <BtCommVdrv.h>

#include "BluetoothComm.h"
#include "BtAppLib.h"

// returns the opened Bluetooth port if needed
UInt16 getBtPort()
{
	UInt32 port = 0;

	FtrGet(appFileCreator, 9811, &port);
	
	return port;
}

// this starts the Bluetooth stack as client
Err startBluetoothStack()
{
	Err returnCode = 0;
	UInt16 port = 0;
	
	SrmOpenConfigType config;
	BtVdOpenParams btParams;
	BtLibSdpUuidType sppUuid;
	
	MemSet( &sppUuid, sizeof(sppUuid), 0 );
	sppUuid.size = btLibUuidSize16;
	sppUuid.UUID[0] = 0x11;
	sppUuid.UUID[1] = 0x01;
	
	MemSet( &btParams, sizeof(btParams), 0 );
	btParams.role = btVdClient;
	btParams.u.client.method = btVdUseUuidList;
	btParams.u.client.u.uuidList.tab = &sppUuid;
	btParams.u.client.u.uuidList.len = 1;
	
	MemSet( &config, sizeof(config), 0 );
	config.function = 0;
	config.drvrDataP = (MemPtr)&btParams;
	config.drvrDataSize = sizeof(btParams);

	returnCode = SrmExtOpen(
			sysFileCVirtRfComm,
			&config,
			sizeof(config),
			&port
			);
			
	FtrSet(appFileCreator, 9811, port);

	return returnCode;
}

This BtAppLib.c library only contains two functions: getBtPort and startBluetoothStack. The first function uses the Palm OS Feature Manager (FtrGet) to provide the Bluetooth port wherever it is needed. The second function (startBluetoothStack) collects all needed parameters to start the Bluetooth stack and also starts it. At the end of this function, the opened Bluetooth port is then stored via the Feature Manager (FtrSet(appFileCreator, 9811, port);).

SrAppLib.c

Show/hide source code
#include "SrAppLib.h"
#include "BtAppLib.h"
#include "BluetoothComm_Rsc.h"
#include "BluetoothComm.h"
#include "BtAppLib.h"
#include "UiAppLib.h"

// This function reads the data, 
// which are received via Bluetooth and returns it
Char* ReadSerial() {

  static Char buffer[100];
  static UInt16 index = 0;
  Err error;
  UInt32 bytes;
  UInt16 btPort;
  
  btPort = getBtPort();
  
  error = SrmReceiveCheck(btPort, & bytes);  
  if(error){
  
  	setErrorExit(error);
  
    ClearField(MainForm, DialogField);
	WriteInDialogField(MainForm, DialogField, "Error while receiving data!");

	handleConnectionLost(BtErrorOccurredAlert2);
	
	return NULL;
  }  
  
  if (bytes + index > sizeof(buffer)) {
    bytes = sizeof(buffer) - index -
      sizeOf7BitChar(\0);
  }

  while (bytes) {
    SrmReceive(btPort, & buffer[index], 1, 0, &error);
    if (error) {
      SrmReceiveFlush(btPort, 1);
      index = 0;
      return NULL;
    }
    switch (buffer[index]) {
    case chrCarriageReturn:
      buffer[index] = chrLineFeed;
    case chrLineFeed:
      buffer[index + 1] = chrNull;
      index = 0;
      return buffer;
      break;
    default:
      index++;
      break;
    }
    bytes--;
  }
  
  return NULL;
  
}


// This function sends text over the serial-(Bluetooth)-interface
void SendDataSerial(void *value){

    Err error;
    UInt16 btPort;

	btPort = getBtPort();

    SrmReceiveFlush(btPort, 0);
    SrmSend(btPort, value, StrLen(value), &error);
    SrmSend(btPort, "\n", 1, &error);
        
    if(error){
    
    	setErrorExit(error);

    	ClearField(MainForm, DialogField);
    	WriteInDialogField(MainForm, DialogField, "Error while sending data!");
    
    	handleConnectionLost(BtErrorOccurredAlert2);
    }   
}

Also, SrAppLib.c only contains two functions, which are related to the serial communication. Bluetooth acts like a serial interface in Palm OS, so fetching and sending data is similar to a real serial interface on Palm OS. ReadSerial is for reading data from the port when the ESP32 sends data to the PDA, and SendDataSerial is for sending data to the ESP32.

UiAppLib.c

Show/hide source code
#include "UiAppLib.h"
#include "BluetoothComm_Rsc.h"
#include "BtAppLib.h"
#include "BluetoothComm.h"

// This function handles a lost connection and offers a reconnect and exit option
UInt16 handleConnectionLost(UInt16 alertID){
	
	UInt16 tappedButton;
	UInt16 btPort;
	
	btPort = getBtPort();
	tappedButton = FrmAlert(alertID);
	
	if(tappedButton == 0){
		//Reconnect
		ClearField(MainForm, DialogField);
		WriteInDialogField(MainForm, DialogField, "Restarting Bluetooth stack...");
		SrmClose(btPort);
		startBluetoothStack();
		ClearField(MainForm, DialogField);
		WriteInDialogField(MainForm, DialogField, "Please select a data point.");
		setErrorExit(0);
	}
	
	if(tappedButton == 1){
		// Exit
		ClearField(MainForm, DialogField);
		WriteInDialogField(MainForm, DialogField, "Shutting down Bluetooth stack...");
		setErrorExit(1);
	}
	
	return tappedButton;
}


// This function clears a field, primarily used to clear the DialogField
Boolean ClearField(UInt16 formID, UInt16 fieldID)
{
	FormPtr 	formPtr;
	UInt16		objectIndex;
	FieldPtr	fieldPtr;
	UInt16 		length;
	Boolean 	handled = false;
	
	
	formPtr = FrmGetFormPtr(formID);
	objectIndex = FrmGetObjectIndex(formPtr, fieldID);
	fieldPtr = (FieldPtr)FrmGetObjectPtr(formPtr, objectIndex);
	length = FldGetTextLength(fieldPtr); 
	
	if(fieldID == DialogField){
		FldSetTextPtr(fieldPtr, "");
		FldDrawField(fieldPtr);
	} else {
		FldDelete(fieldPtr,0,length);
	}
	
    handled = true;
    
	return handled;
}

// This function writes a string into a (Dialog)Field
Boolean WriteInDialogField(UInt16 formID, UInt16 fieldID, MemPtr str)
{
	FormPtr 	formPtr;
	UInt16		objectIndex;
	FieldPtr	fieldPtr;
	
	char* dialogStrPtr;
	char* newDialogStrPtr;
	

	Boolean 	handled = false;
	
	formPtr = FrmGetFormPtr(formID);
	objectIndex = FrmGetObjectIndex(formPtr, fieldID);
	fieldPtr = (FieldPtr)FrmGetObjectPtr(formPtr, objectIndex);
	
	dialogStrPtr = FldGetTextPtr(fieldPtr);
	
	if(dialogStrPtr){
		newDialogStrPtr = MemPtrNew((StrLen(dialogStrPtr)+StrLen(str)+1));
	
		StrCopy(newDialogStrPtr, dialogStrPtr);
    	StrCat(newDialogStrPtr, str);
		FldSetTextPtr(fieldPtr, newDialogStrPtr);
		FldRecalculateField(fieldPtr, true);
	} else {
		FldSetTextPtr(fieldPtr, str);
	}
	
	FldDrawField(fieldPtr);
	
    handled = true;
    
	return handled;
}

UiAppLib.c handles the user interface actions like an alert form and the main display area for the data from the ESP32. The first function is called handleConnectionLost and handles a lost connection. If the connection loss is detected in MainFormHandleEvent (see BluetoothComm.c) or while sending (SendDataSerial) or receiving (ReadSerial) data (see SrAppLib.c), this function is called. It shows an alert form and provides two options: "Reconnect" and "Exit". If "Reconnect" is selected, the Bluetooth connection is shut down and restarted. If "Exit" is selected, "setErrorExit" will be set to 1, which is processed in AppEventLoop and will terminate the application if this value is 1.

Conclusion

The ESP8266 and ESP32 as well as the PHP server are working very well. Only the Palm OS application is far from perfect. But since this is a hobby project and not a professional one, it is good enough. Maybe I will find some time in the future for optimizations, like making the "request buttons" in the application dynamic by replacing them with a dropdown. This would make it possible to fetch all data points first and then show them in the dropdown list. It would also be nice to see in the main field from which room the data came or to have proper error handling when the application is started on a non-Bluetooth-enabled device.
Besides that, the Palm OS application has one known bug: When the device is turned off while the application is running, it can crash or terminate with an alert, which is triggered in the PilotMain function. Also, the ESP8266 may not always return valid values. The latter problem occurs very rarely. So I think both problems are acceptable.

I want to thank the PalmDB community and especially Dmitry Grinberg, who helped me a lot with the implementation of the Palm OS application.