Программирование для Palm OS: что же там самое сложное?

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.
Приветствую всех!
Думаю, многие из нас хоть раз слышали о КПК на базе Palm OS. А некоторым даже довелось иметь такой в использовании.
И так уж вышло, что пальмы стали одной из самых противоречивых платформ в плане разработки под них, в чём легко можно убедиться, почитав комментарии к любой статье про программирование для этих устройств.



Итак, в сегодняшней статье поговорим о самом специфическом аспекте разработки под такие КПК. Разберёмся, что же было предметом дискуссий о целесообразности программирования для пальм, узнаем, насколько всё сложно. Традиционно будет много интересного.

Суть такова


Давным-давно я уже писал про введение в разработку под пальмы. Пост этот был весьма успешным, но там была скромно опущена одна из важнейших составляющих программирования, в общем-то, для любой платформы — хранение данных. На одних формах и тексте далеко не уехать, поэтому я решил написать продолжение данного поста, где и рассказать о том, как с этим работать.

В чём сложность?


Те, кому в своё время довелось пользоваться пальмами, знают, о чём я. Дело в том, что в этих КПК, в отличие, например, от устройств на Windows Mobile, нет никакой файловой системы, а хранилище представляет собой этакую базу данных. Таким образом, в отличие от стандартных операций над файлами необходимо следить за объёмом используемых данных, чтобы уложиться в размер одной записи (которая представляет собой вместилище для последовательности байт фиксированной длины) и при необходимости сохранить их в соседнюю ячейку, возможно, предусмотреть обмен данными с компьютером (ведь нельзя просто так взять и записать данные на карту памяти, так как во многих пальмах их попросту не было) или открыть и просмотреть запись извне. В этом плане КПК на базе Pocket PC были куда удобнее, так как предоставляли полноценную ФС со всеми её возможностями. Это и вызывало ожесточённые споры насчёт того, сложно ли писать под пальмы. Безусловно, платформа обладала и другими свойственными только ей особенностями, но работу с файлами (которых в этой ОС по сути и не было) упоминали практически все.

И вот я сам решил попробовать разобраться с использованием этой БД, на себе испытал всю боль разработчиков для пальмы, так что теперь можно наконец-то поведать об этом миру.

Обзор оборудования


Как и в прошлой статье, тестировать все приложения будем на ТСД Symbol SPT-1550.

image

Это мой любимый КПК на базе Palm OS. Не самая древняя версия системы, работа от батареек, красивый дизайн, совместимость с аксессуарами от третьей пальмы — всё это мне в нём очень нравится. На борту процессор MC68000 на 33 МГц и восемь мегабайт ОЗУ (а по совместительству и RAM-диска).

Как устроено хранилище данных в пальме


Итак, каждое приложение может создать на RAM-диске одну или несколько баз данных. Сама БД представляет собой заголовок и совокупность записей (каждая из которых тоже имеет свои атрибуты), куда можно писать какие-то данные. Максимальный размер одной записи — 64 КБ, что связано с размером блока памяти. В одной базе может храниться до 32768 записей, таким образом, БД может весить до двух ГБ. В реальности, конечно, это число гораздо меньше, так как объём RAM-диска у типичных пальм исчисляется единицами мегабайт.
Формат этих баз данных называется PDB (Palm Database), по своей структуре он очень схож с PRC (Palm Resource Code), использующемся для хранения приложений. Разница заключается лишь в том, что в PDB в записях хранятся пользовательские данные, а в PRC — исполняемый код и ресурсы.

Ресурсы


На удивление, в интернете нашлось не так уж и много информации по тому, как работать с базами данных на пальме. Конечно, есть какие-то исходники приложений, есть описание функций, но вот простого рабочего примера я так и не нашёл. Ну а раз так, самое время восполнить это упущение.
Итак, создаём новый проект в CodeWarrior. Именно его мы и возьмём за основу.



Открываем редактор ресурсов и добавляем кнопки для загрузки и выгрузки данных из БД. Также создаём несколько новых уведомлений — об отсутствии данных, об успешном или неудачном сохранении, о создании БД.



Теперь можно запустить приложение и убедиться, что пока что всё работает.
Итак, ресурсные файлы получились следующие:

Rsc.h
#define NoDataAlert 1014
#define SuccessAlert 1013
#define ErrorAlert 1012
#define DbInitAlert 1011
#define GetButton 1010
#define AddButton 1009
#define MainClearTextButton 1008
#define MainDescriptionField 1007
#define MainForm 1006
#define RomIncompatibleAlert 1005
#define EditOnlyMenuBar 1003
#define MainMenuBar 1001


Rsc.rcp
// test2_Rsc.rcp
//
// PilRC-format resources for test2
//
// Generated by the CodeWarrior for Palm OS V9 application wizard

GENERATEHEADER "test2_Rsc.h"
RESETAUTOID 1000

MENU ID MainMenuBar
BEGIN
    PULLDOWN "Edit"
    BEGIN
		MENUITEM "Undo" ID 10000 "U"
		MENUITEM "Cut" ID 10001 "X"
		MENUITEM "Copy"ID 10002 "C"
		MENUITEM "Paste" ID 10003 "P"
		MENUITEM "Select All" ID 10004 "S"
		MENUITEM SEPARATOR ID 10005
		MENUITEM "Keyboard" ID 10006 "K"
		MENUITEM "Graffiti Help" ID 10007 "G"
    END


END	

MENU ID EditOnlyMenuBar
BEGIN
    PULLDOWN "Edit"
    BEGIN
		MENUITEM "Undo" ID 10000 "U"
		MENUITEM "Cut" ID 10001 "X"
		MENUITEM "Copy"ID 10002 "C"
		MENUITEM "Paste" ID 10003 "P"
		MENUITEM "Select All" ID 10004 "S"
		MENUITEM SEPARATOR ID 10005
		MENUITEM "Keyboard" ID 10006 "K"
		MENUITEM "Graffiti Help" ID 10007 "G"
    END
END

ALERT ID RomIncompatibleAlert
    DEFAULTBUTTON 0
    ERROR
BEGIN
    TITLE "System Incompatible"
    MESSAGE "System Version 3.0 or greater is required to run this application."
    BUTTONS "OK"
END

FORM ID MainForm AT (0 0 160 160)
	NOSAVEBEHIND NOFRAME
	MENUID MainMenuBar
BEGIN
	TITLE "test2"
    GRAFFITISTATEINDICATOR AT (149 148)
	FIELD ID MainDescriptionField AT (0 16 160 126)
		MULTIPLELINES
		EDITABLE
		UNDERLINED
		MAXCHARS 1024
	BUTTON "Clear Text" ID MainClearTextButton AT (1 147 AUTO 12)
	BUTTON "Add" ID AddButton  AT (73 144 32 12)
	BUTTON "Get" ID GetButton   AT (113 144 32 12)
END

ICONFAMILYEX
BEGIN
    BITMAP "icon-lg-1.bmp" BPP 1 
    BITMAP "icon-lg-2.bmp" BPP 2 
    BITMAP "icon-lg-8.bmp" BPP 8 TRANSPARENTINDEX 210 COMPRESS
    BITMAP "icon-lg-1-d144.bmp" BPP 1 DENSITY 2
    BITMAP "icon-lg-2-d144.bmp" BPP 2 DENSITY 2
    BITMAP "icon-lg-8-d144.bmp" BPP 8 TRANSPARENTINDEX 210 COMPRESS DENSITY 2
END

SMALLICONFAMILYEX
BEGIN
    BITMAP "icon-sm-1.bmp"  BPP 1
    BITMAP "icon-sm-2.bmp"  BPP 2
    BITMAP "icon-sm-8.bmp"  BPP 8 TRANSPARENTINDEX 210 COMPRESS
    BITMAP "icon-sm-1-d144.bmp" BPP 1 DENSITY 2
    BITMAP "icon-sm-2-d144.bmp" BPP 2 DENSITY 2
    BITMAP "icon-sm-8-d144.bmp" BPP 8 TRANSPARENTINDEX 210 COMPRESS DENSITY 2 
END


ALERT ID DbInitAlert 
WARNING
BEGIN
     TITLE "DB init"
     MESSAGE "DB does not exist... creating"
     BUTTONS "OK" 
END

ALERT ID ErrorAlert 
ERROR
BEGIN
     TITLE "Error"
     MESSAGE "something went wrong..."
     BUTTONS "OK" "Cancel"
END

ALERT ID SuccessAlert 
ERROR
BEGIN
     TITLE "DB write"
     MESSAGE "Succesfully saved"
     BUTTONS "OK" 
END

ALERT ID NoDataAlert 
ERROR
BEGIN
     TITLE "Empty database"
     MESSAGE "No data"
     BUTTONS "OK" 
END


Именно тут содержатся все графические элементы программы, точнее, их описание.

Создаём базу данных


Теперь очередь самого хранилища. И, понятное дело, базу данных надо для начала создать.
Для этого существует функция DmCreateDatabase, которая используется примерно так:

const char * myDBName = "myTestDB";
error = DmCreateDatabase(0, myDBName, 'TEST', 'DATA', false);
if (error) return error;

Первый параметр этой функции — номер карты памяти, где хранить эту самую базу (у КПК без съёмных носителей всегда 0), название базы (обычная строка), Creator ID (четырёхбайтный код создателя базы, теперь, когда коммерческие приложения под пальму не выпускаются, можно указать любой, главное, чтобы он не конфликтовал с каким-либо другим приложением), тип базы ('DATA') и флаг ресурсной базы (в данном случае выключенный).
После вызова этой функции будет создана пустая БД с такими атрибутами.
Но делать это надо только единожды, если попробовать создать одну и ту же БД несколько раз, произойдёт Fatal Exception. Поэтому правильным вариантом будет пытаться открыть нужную базу, а если она не открывается, то пробовать её создать.

Делается это так:

static Err checkAndCreateDatabase(void) {
	DmOpenRef myDBPointer = 0;
	Err error;
	myDBPointer = DmOpenDatabaseByTypeCreator('DATA', 'TEST', dmModeReadWrite);
	if (!myDBPointer) {
		FrmAlert(DbInitAlert);
		error = DmCreateDatabase(0, myDBName, 'TEST', 'DATA', false);
		if (error) return error;
		myDBPointer = DmOpenDatabaseByTypeCreator('DATA', 'TEST', dmModeReadWrite);
		if (!myDBPointer) {
			return DmGetLastErr();
			}
	}
	DmCloseDatabase(myDBPointer);
	return error;
}

Для начала мы просто пытаемся открыть нашу базу. Функция DmOperDatabaseByTypeCreator возвращает указатель на открытую базу. Если он нулевой, значит, база не открылась. В этом случае пытаемся её создать. Если же она не создаётся, то функция возвращает ошибку.



После того, как база была создана (или же мы просто убедились в её существовании), необходимо завершить соединение.

Записываем данные


Ну что же, время попробовать что-то записать.
Для начала разберёмся, откуда будем что-то считывать. В примере есть текстовое поле, так что будем брать этот самый текст из него. Делается это так:

char * tstdata = "";
FieldType * field = (FieldType*)GetObjectPtr(MainDescriptionField);
if (field)	{
	tstdata = FldGetTextPtr(field);
}

Итак, для начала надо получить указатель на это поле по его ID (указывается в конструкторе ресурсов или прописывается в *_Rsc.h-файле). Далее остаётся только получить указатель на текст.
Функции GetObjectPtr в стандартной библиотеке нет, но она создаётся по умолчанию в файле с кодом у нового проекта. Выглядит она так:

static void * GetObjectPtr(UInt16 objectID)
{
	FormType * frmP;

	frmP = FrmGetActiveForm();
	return FrmGetObjectPtr(frmP, FrmGetObjectIndex(frmP, objectID));
}

Дело в том, что нельзя просто так взять и обратиться к какому-то элементу, небходимо ещё и иметь указатель на форму, на которой он расположен. Эта же функция получает нужный указатель просто по ID объекта.

С данными разобрались. Теперь очередь сохранения.

В данном примере я не буду использовать несколько записей, ограничимся всего лишь одной. Впрочем, для нескольких этот принцип отличается не особо.

Итак, для начала необходимо создать новую запись. Делается это функцией DmNewRecord:

UInt16 recIndex = dmMaxRecordIndex;
MemHandle recH;
recH = DmNewRecord(myDBPointer, &recIndex, StrLen(data));

Первый параметр этой функции — указатель на открытую базу данных, второй — индекс (номер этой записи в базе, лежащий, как нетрудно догадаться, в диапазоне от 0 до 32767), третий — размер этой самой записи. dmMaxRecordIndex — особая константа, служащая для указания, что писать надо в конец базы. В данном примере записывать будем текст, поэтому в качестве размера указываем длину строки. Если же предполагается писать какие-то другие данные, то проще всего будет запихнуть их в структуру, а затем сохранять уже её.
Далее надо заблокировать этот участок памяти и записать свои данные, после чего разблокировать его обратно:

if (recH) {
	recP = MemHandleLock(recH);
	DmWrite(recP, 0, data, StrLen(data));
	error = DmReleaseRecord(myDBPointer, recIndex, true);
	MemHandleUnlock(recH);
	}

Так как это единственная запись в базе, сохранять будем по нулевому индексу.



Нужно обязательно проверять, действительно ли запись открылась, в противном случае попытка что-либо туда записать приведёт к появлению Fatal Exception.
Но, разумеется, если запись уже есть, то создавать её не надо, так что нужно добавить проверку и тут:

if(DmNumRecords(myDBPointer) == 0) {
	recH = DmNewRecord(myDBPointer, &recIndex, StrLen(data));
	if (recH) {
			recP = MemHandleLock(recH);
			DmWrite(recP, 0, data, StrLen(data));
			error = DmReleaseRecord(myDBPointer, recIndex, true);
			MemHandleUnlock(recH);
		}
	}
	else {
		UInt16 newSize = StrLen(data);
		recH = DmResizeRecord(myDBPointer, 0, newSize);
		if (recH) {
			recP = MemHandleLock(recH);
			DmWrite(recP, 0, data, StrLen(data));
			DmReleaseRecord(myDBPointer, 0, true);
			MemHandleUnlock(recH);
			}
	}

Для начала получаем число записей в базе. Если там ничего нет, значит, надо создать новую и закинуть данные туда. Если же запись уже есть, то подгоняем её под размер новых данных и сохраняем их туда.

Вся функция для записи в итоге получилась вот такая:

static Err putIntoDatabase(char * data) {
	DmOpenRef myDBPointer = 0;
	Err error = errNone;
	UInt16 recIndex = dmMaxRecordIndex;
	MemHandle recH;
	MemPtr recP;
	
	myDBPointer = DmOpenDatabaseByTypeCreator('DATA', 'TEST', dmModeReadWrite);
	if(DmNumRecords(myDBPointer) == 0) {
	recH = DmNewRecord(myDBPointer, &recIndex, StrLen(data));
	if (recH) {
			recP = MemHandleLock(recH);
			DmWrite(recP, 0, data, StrLen(data));
			error = DmReleaseRecord(myDBPointer, recIndex, true);
			MemHandleUnlock(recH);
		}
	}
	else {
		UInt16 newSize = StrLen(data);
		recH = DmResizeRecord(myDBPointer, 0, newSize);
		if (recH) {
			recP = MemHandleLock(recH);
			DmWrite(recP, 0, data, StrLen(data));
			DmReleaseRecord(myDBPointer, 0, true);
			MemHandleUnlock(recH);
			}
	}
	error = DmCloseDatabase(myDBPointer);
	return error;
}

А вот так она вызывается в обработчике событий от главной формы:

if (eventP->data.ctlSelect.controlID == AddButton) {
	FieldType * field = (FieldType*)GetObjectPtr(MainDescriptionField);
	if (field)
	{
	tstdata = FldGetTextPtr(field);
	error = putIntoDatabase(tstdata);
	if(error != errNone) {
	FrmAlert(ErrorAlert);
	FrmCloseAllForms();
		}
	FrmAlert(SuccessAlert);
	}
}

Тут мы отправляем полученный текст для записи в базу и выдаём соответствующий alert при успешном сохранении.



В работе это выглядит примерно так.

Чтение из базы


С записью разобрались. Теперь пробуем прочитать наши данные.



Для начала убедимся, что они там вообще есть:

 if(DmNumRecords(myDBPointer) == 0) {
 	FrmAlert(NoDataAlert);
	DmCloseDatabase(myDBPointer);
 	return errNone;
 	}

Вот тут стоит упомянуть о том, как устроена каждая запись. Помимо самих данных она содержит в себе заголовок из четырёх байт, три байта из которых — ID записи (это не индекс), а ещё один — атрибуты. Четыре бита в нём — номер категории, который тоже можно задавать, а ещё четыре — статусные биты:

  1. Secret bit — защищённая запись. Если этот бит установлен, запись не показывается до ввода пароля пользователя.
  2. Busy bit — запись занята. Устанавливается при обращении к записи и сбрасывается при вызове функции DmReleaseRecord. Если этот бит установлен, другие функции не могут получить доступ к записи.
  3. Dirty bit — признак изменённой записи. Устанавливается или обнуляется в третьем аргументе функции DmReleaseRecord.
  4. Delete bit — признак удалённой записи. Само содержимое при этом сохраняется и живёт вплоть до следующей синхронизации с компьютером. При этом функция DmDeleteRecord лишь взводит этот бит, а DmRemoveRecord сносит всю запись.

Так вот, для обращения к записи существуют сразу две функции — DmGetRecord и DmQueryRecord. Разница между ними в том, первая может использоваться как для чтения, так и для записи, а вторая не устанавливает busy bit (но при этом запись открывается только для чтения). Её и будем использовать:

recH = DmQueryRecord(myDBPointer, 0);
if (recH) {
 	recP = MemHandleLock(recH);
 	updateText(recP);
     	MemHandleUnlock(recH);
}

Итак, после чтения записи мы получаем указатель на то, что содержится в ней. Далее надо заблокировать эту область памяти, обновить наш текст и разблокировать её обратно.
Таким образом, всё чтение из памяти выглядит вот так:

static Err getFromDatabase() {
	Err error = errNone;
	DmOpenRef myDBPointer = 0;
 	MemHandle recH;
 	char * recP;
 	myDBPointer = DmOpenDatabaseByTypeCreator('DATA', 'TEST', dmModeReadWrite);
 	if(DmNumRecords(myDBPointer) == 0) {
 	FrmAlert(NoDataAlert);
	DmCloseDatabase(myDBPointer);
 	return errNone;
 	}
 	recH = DmQueryRecord(myDBPointer, 0);
 	if (recH) {
 		   	recP = MemHandleLock(recH);
 		   	updateText(recP);
     		MemHandleUnlock(recH);
     		}
   	error = DmCloseDatabase(myDBPointer);
 	return error;
}

Теперь очередь самой формы. С выводом в неё текста всё очень просто:

static void updateText(char * data) {
	FieldType * field = (FieldType*)GetObjectPtr(MainDescriptionField);
	if (field)
	{
		FldDelete(field, 0, 0xFFFF);					
		FldDrawField(field);
		FldInsert(field, data, StrLen(data));
		}
}

Для начала получаем указатель на форму, затем очищаем её и выводим новый текст.



Вообще, перед началом работы следует убедиться, что база действительно открылась, так как попытка обращения к несуществующей БД вызовет критическую ошибку. Впрочем, в данном случае отсутствие такой проверки не слишком критично, так как наличие рабочей базы проверяется при запуске, а все функции перед выходом завершают подключение.

Тесты на железе


Ну что же, время протестировать эту прогу на реальном КПК. Тут уже стоят некоторые приложения, как раз можно убедиться, что их базы не конфликтуют с нашей.



И оно даже успешно работает! Если напечатать что-то в текстовом поле и попробовать сохранить, то всё проходит успешно, приложение не вылетает и не отправляет КПК в перезагрузку.

Редактирование БД на ПК


Понятное дело, существует софт для того, чтобы просматривать или изменять базу (например, слитую в ходе HotSync) на компьютере.



Вот одна из таких программ, PDB Editor. По сути в ней можно делать всё то же самое, что выполнялось нами на КПК.

Вот как-то так


Как можно видеть, работа с данными в таких КПК действительно куда сложнее таковой на других платформах. И нет, это особенность именно пальмы, на других платформах всё могло быть совершенно иначе. Вот, к примеру, чтение из файла на Psion EPOC16 (он, к слову, старше Palm OS лет так на семь) которое выполняется примерно так:

INT ret;
TEXT some_byte;
ret=p_open(&f, FileName, P_FSTREAM_TEXT|P_FSHARE|P_FRANDOM);
ret = p_read(f, &some_byte, sizeof(TEXT));

На пальме же необходимо создавать базу данных и запись в ней вообще для любого хранения, даже если необходимо записать какое-то единственное значение типа максимального числа очков в игре. И если все эти действия всё равно кажутся простыми, то на более серьёзных проектах, где писать надо много и часто, работа с БД с учётом её ограничений становится уже куда сложнее.

Такие дела.

Ссылки


  • Описание API Palm OS для работы с базами данных. Практически всё, что понадобилось для создания этого примера, было взято там.
  • Редактор PDB
  • Ещё один пример работы с БД. У меня он, увы, не заработал.




Возможно, захочется почитать и это:

  • ➤ Как подключить магнитный считыватель к микроконтроллеру
  • ➤ Программирование для Palm OS: ставим CodeWarrior и оживляем ТСД
  • ➤ Alan Wake: 13 лет спустя
  • ➤ 96 лет со дня рождения Джона Маккарти
  • ➤ Нюансы разработки парсера для своего языка программирования

Источник: https://habr.com/ru/companies/timeweb/articles/757726/


Интересные статьи

Интересные статьи

Мы продолжаем создавать клон Uniswap V1! Наша реализация почти готова: мы реализовали все основные механики смарт-контракта Биржи, включая функции ценообразования, обмена, выпуска LP-токенов и сбора к...
Привет. Меня зовут Вадим Бараненко. С украинским офисом EPAM я сотрудничаю в роли архитектора решений. И в этом материале я хотел бы поделиться своими взглядами и опытом ...
Предлагаем вашему вниманию подборку материалов от python.org о том, с чего начать первые шаги в программировании. Если Вы никогда не занимались программированием раньше, эти ...
В этой статье я хочу продемонстрировать использование DispmanX API одноплатных компьютеров Raspberry. DispmanX API предоставляет возможность создавать на десктопе Raspberry новые отображаемые с...
Поздравляем Хабр с выходом R2DBC версии Arabba-RELEASE! Это самый первый стабильный релиз проекта. R2DBC (Reactive Relational Database Connectivity) — открытый проект, посвященный реактивному ...