Информатички пројекат: Бежична сензорска мрежа у Зигби протоколу
Увод
Пре пар година док сам студирао информатику на франкфуртској високој школи примењених наука (Frankfurt University of Applied Sciences) изабрао сам пројекат професора Краусе као један од понуђених пројеката за предмет Информатички пројекат.
На први поглед, пројекат је по многим карактеристикама мени одговарао јер је имао:
- стварне уређаје на којима је код учитан и тестирање обављено
- сензоре који се могу употребити у практичним ситуацијама.
Све у свему, овај тимски пројекат долази у две варијанте; онај који сам ја изабрао је имао Бошов паметни сензор BNO055 који је садржао у себи сензор за убрзање, жироскоп и магнетометар од којих смо само жироскоп користили да би очитали Ојлерове углове и кватернионе. Касније је био задатак малопре поменуте вредности из сензора визуелно представити у реалном времену уз помоћ Пајтон програмског језика. Иако су ова два задатка била једина и нужна, да би наше колеге из других тимова импресионирао креативношћу и сопственим вештинама у Пајтону и проналажењу креативног решења за примену пројекта, одлучио сам да испрограмирам концепт игру са лавиринтом и куглицом која ће узимати податке са сензора и у реалном времену послужити као контролер за корисника. Концепт игре је био једноставан: играч контролише окретање табле око своје осе и тиме наводи куглицу од почетне тачке до крајње означене тачке у лавиринту. Као шлаг на торти, сваки лавиринт је случајно генерисан и може доћи у чак три величине.
Најпре бих Вам скренуо пажњу на компоненте потребне за овај пројекат.
Компоненте пројекта
Софтвер
- BitCloud Embedded SW Stack од Atmel-а верзија 3.3.0 (C/C++)
- Пајтон верзија 3.8.5
- VPython - Visual Python графичка библиотека верзија 7
Хардвер
- 3 x ZigBee ATMEGA256RFR2 радио модула
- 1 x PAN координатор
- 2 x ZigBee крајња уређаја са BNO055 паметним сензором
- 2 x држача батерија са 2 AA батерије (сваки држач напаја свој крајњи Зигби уређај)
- 2 x Bosch BNO055 9-осни паметни сензори
- 1 x UART мост
- 1 x USB mini до USB 2.0 кабал за конекцију координатора са компјутером
Конекцијски протокол и читање/писање
Циљ пројекта је стварање Personal Area Network (PAN) или на српском личне локалне мреже између Зигби уређаја и комуникација између њих и сензора као и проток тих информација преко координатора (мастера) до компјутера.
Главни комуникацијски протокол између Зигби уређаја и BNO055 9-осних паметних сензора је Inter-Integrated-Circuit (I2C) или под другим називом Two Wire Interface (TWI).
У мрежи је укупно укључено три Зигби уређаја, адресирање тих уређаја не би представљао проблем. Сваки од ових уређаја има 8-битни процесор па је једина опција била коришћење 7-битног адресирања, а постоји и опција 10-битног I2C адресирања за процесоре виших архитектура.
У теорији за 7-битни распон адреса може укупно у овом протоколу комуницирати до 127 (27-1) уређаја/сензора (потребно је најмање један мастер уређај).
Брзина протока података је за овај пројекат изабрана да буде 125 KB/s јер је таман брзина за читање података са сензора.
Нужно напоменути је да је генерално сав код (сем константи) app.c
фајлу који је генерисан у Atmel студију. На крају пројекта је број линија кода износио скоро 1000.
Горе напоменуте вредности конекције је потребно сачувати у I2C HAL_I2cDescriptor_t
дескриптору:
HAL I2C Descriptor
static HAL_I2cDescriptor_t i2cdescriptor={ .tty = TWI_CHANNEL_0, .clockRate = I2C_CLOCK_RATE_125, .id = BNO055_I2C_ADDRESS, }
Током истраживања о сензору у многоме је помогла званична документација. Страна 102 документације објашњава кораке за омогућавање I2C конекције за читање/писање регистара:
- да би писали у регистар сензора потребно је 2 бајта:
- да би селектовали регистар у који ћете касније писати потребно је као први бајт поставити адресу регистра и
- други бајт вредност која ће бити уписана у регистар.
- да би читали регистар потребан је 1 бајт:
- селектовали регистар који ће бити очитан постављањем адресе као вредности првог бајта.
Након што је дескриптор конекције успешно дефинисан, BitCloud API функције за комуникацију могу бити употребљене:
HAL_OpenI2cPacket(HAL_I2cDescriptor_t *descriptor)
,HAL_WriteI2cPacket(HAL_I2cDescriptor_t *descriptor)
,HAL_ReadI2cPacket(HAL_I2cDescriptor_t *descriptor)
andHAL_CloseI2cPacket(HAL_I2cDescriptor_t *descriptor)
.
Ове функције су кључни градивни елементи за функције које сам ја направио за комуникацију са сензором. Погледајмо како изгледа моја фунција писања која користи BitCloud API функције:
Писање регистра сензора
int writeBno055(uint8_t numOfBytes, uint8_t regAddr, uint8_t val){ if(-1 == HAL_OpenI2cPacket(&i2cdescriptor)){ printLineToUSART(ERROR_I2C_OPEN); printLineToUSART("Error in writeBno055!"); appstate=APP_NOTHING; SYS_PostTask(APL_TASK_ID); return -1; }else{ // set isPacketClosed to false since it's open isPacketClosed=false; } if(numOfBytes==1) //register setzen { writeOneByteData[0]=regAddr; i2cdescriptor.f = writeDone; i2cdescriptor.data = writeOneByteData; i2cdescriptor.length = 1; }else if(numOfBytes==2){ // Wert reinschreiben writeTwoBytesData[0]= regAddr; writeTwoBytesData[1]= val; i2cdescriptor.f = writeDone; i2cdescriptor.data = writeTwoBytesData; i2cdescriptor.length = 2; }else{ // invalid numOfBytes argument return -2; } if(-1 == HAL_WriteI2cPacket(&i2cdescriptor)){ printLineToUSART(ERROR_I2C_WRITE); printLineToUSART(" When writing to a register using writeBno055 function!"); appstate = APP_NOTHING; SYS_PostTask(APL_TASK_ID); return -1; } // the packet is closed in the callback function writeDone return 0; }
Проучимо горе приказану функцију. Као прво покушава фунцкија да отвори I2C канал/ресурс позивањем HAL_OpenI2cPacket(&i2cdescriptor)
API функције. Функција тиме покушава за себе да осигура ресурс и ако то није могуће враћа -1 као грешку.
Као први параметар фунције писања uint8_t numOfBytes
могу бити бројеви 1 или 2:
- 1 ако хоћемо само да селектујемо регистар за читање и
- 2 у случају да хоћемо да упишемо вредност у регистар.
- било која друга вредност враћа -2 као грешку и програм се зауставља.
У зависности колики број бајтова шаљемо, вредност i2cdescriptor.length
је или 1 или 2, док i2cdescriptor.data
садржи вредности тих бајт(ова). Отварањем конекције сада можемо да употребимо HAL_WriteI2cPacket(&i2cdescriptor)
јер је дескриптор дефинисан и можемо писати у BNO055 регистре.
Можда сте приметили да не затварамо I2C пакет у горе приказаној функцији. Затварање се у ствари обавља увек након завршетка писања/читања и то у колбек фунцији напоменутој у i2cdescriptor.f
вредности, нпр. writeDone(bool result)
. Тиме нема временских проблема у отварању/затварању пакета и писање, тј. читање може бити течно.
Следи колбек функција за писање:
Колбек фунција за писање
static void writeDone(bool result){ if(result == false){ printLineToUSART(ERROR_I2C_WRITE); appstate = APP_NOTHING; }else{ // succesful write, now close the packet if(-1 == HAL_CloseI2cPacket(&i2cdescriptor)){ printLineToUSART(ERROR_I2C_CLOSE); appstate = APP_NOTHING; }else{ // set isPacketClosed to true isPacketClosed=true; } } SYS_PostTask(APL_TASK_ID); }
Колбек фунција за читање је веома слична само се вредности за USART приказивање у конзоли разликују у првој if
наредби.
Погледајмо сада readBno055
фунцију:
Read sensor registers
int readBno055(uint8_t numOfBytes, uint8_t* readData){ if(-1 == HAL_OpenI2cPacket(&i2cdescriptor)){ printLineToUSART(ERROR_I2C_OPEN); printLineToUSART("Error in readBno055!"); return -1; }else{ // set isPacketClosed to false since it's open isPacketClosed=false; } i2cdescriptor.f = readDone; i2cdescriptor.data = readData; i2cdescriptor.length = numOfBytes; if(-1 == HAL_ReadI2cPacket(&i2cdescriptor)){ printLineToUSART(ERROR_I2C_READ); printLineToUSART("Error in readBno055!"); appstate = APP_NOTHING; SYS_PostTask(APL_TASK_ID); return -1; } return 0; }
Слично фунцији писања и фунција за читање почиње отварањем I2C ресурса као и постављањем одговарајуће колбек фунције, али шта се разликује је бафер (i2cdescriptor.data
) који смо користили за слање података за писање сада садржи вредности које су очитане из сензорских регистара. Као последњи корак функција дефинише број бајтова који ће бити очитани, који за разлику од фунције писања могу бити и преко два.
Програмска стања
Сада када смо коначно поставили темељ за комуникацију са сензорима, погледајмо која су уопште стања заступљена у програму.
APP_NOTHING
- ово стање се користи у случају грешке или потреба мировања програма.APP_INIT
- У овом стању се дефинишу променљиве које ће се користити у програму, међу њима и да ли прочитати Ојлер углове или кватернионе.APP_STARTJOIN_NETWORK
- Нужно Зигби стање у коме координатор (мастер) успоставља PAN мрежу, док се друга два Зигби уређаја каче на мрежу.APP_INIT_ENDPOINT
- Нужно Зигби стање за успостављање крајње тачке.APP_INIT_TRANSMITDATA
- Нужно Зигби стање које дефинише структуру пороке у мрежи. У овом случају структура поруке изгледа овако:
Пејлоад структура
typedef struct _AppMessagePayloadEnddevice_t{ uint8_t moduleName; int32_t eulerX, eulerY, eulerZ; // all values are 1000 times int32_t quatW, quatX, quatY, quatZ; // all values are 1000 times uint32_t voltage; } PACK AppMessagePayload_t;
Као што коментари сугеришу, вредности које се шаљу у ствари су прво помножене са хиљаду. Зарад приказа вредности се касније деле са хиљаду.
APP_SET/READ_ID_REG
су два стања за селектовање и читање првог ИД регистра на сензору, да би прво тестирали I2C конекцију са сензором.APP_SET_PAGEID
је стање које селектује прву страну сензор регистара у коме се налазе сензор подаци.APP_SET_OP_MODE_NDOF
- овде је селектован оперативни мод сензора такозвани фузијски илити NDOF мод. У овом оперативном модусу су доступни подаци у апсолутној оријентацији са сва три подсензора: сензор за убрзање, жироскоп и магнетометер. Велики минус овог модуса је повећана потрошња енергије, а самим тим и батерије, али је једини модус у коме није потребно много сређивања података након читања јер су сви подаци већ у апсолутној оријентацији. Због стандардног оперативног модуса који је аутоматски изабран након почетка напајања -CONFIGMODE
који не укључује саме сензоре - ово стање је нужно да би се добио коректан оперативни модус.APP_SET/READ_CALIBRATION_REG
су стања која активирају додатну калибрацију сензора. Подаци су већ прихватљиви такви какви јесу, али ово стање додатно побољшава податке који долазе са сензора.APP_READ_EULER/QUATERNION
- у овим стањима су подаци очитани, помножени са хиљаду и сачувани каоint32_t
променљиве.APP_READ_BATTERY
као што име сугерише очитава тренутну волтажу батерија повезаних са Зигби крајњим уређајима. Сам координатор је прикачен преко UART моста са компујетером, напајајући се тим начином.APP_TRANSMIT
- пакује сензор податке у мрежне поруке са илиAPP_READ_EULER
илиAPP_READ_QUATERNION
стања и шаље их координатору преко PAN мреже.
Очитавање/писање сензора у пракси
Као што смо већ поменули, да би очитали вредност регистра са сензор подацима, сам регистар мора бити селектован тако што ћемо прво урадити једно I2C писање са адресом регистра. Из овог разлога постоје APP_SET...
стања.
Испод је приказан код којим се селектује регистар са првом вредношћу кватерниона пре даљег очитавања. Овај код није ништа што већ нисмо објаснили. У случају неуспеха програм улази директно у APP_NOTHING
стање.
APP_SET_QUATERNION_REG State
case APP_SET_QUATERNION_REG: if(debugMode){ printLineToUSART("------------------------------"); printLineToUSART("STATE: APP_SET_QUAT_REG"); } if(-1 == writeBno055(1, QUATERNION_DATA_W_LSB_ADDR, 0x00)){ printLineToUSART(ERROR_SEL_REG_QUA); appstate=APP_NOTHING; SYS_PostTask(APL_TASK_ID); } appstate=APP_READ_QUATERNION; SYS_PostTask(APL_TASK_ID); break;
Сада можемо да стварно очитамо и протумачимо кватернион вредности који се чувају у регистрима сензора. За кватернионе је потребно прочитати укупно 8 бајта података (што значи 8 узастопних регистара) и сачувати их у 4 различите променљиве типа int16_t
: w, x, y, z
. Подаци морају да се прво скалирају до одговарајућег опсега и помноже са 1000 да би се сачувале вредности иза децимале (3 места).
APP_READ_QUATERNION State
case APP_READ_QUATERNION: if(debugMode){ printLineToUSART("--------------------------------"); printLineToUSART("STATE: APP_READ_QUATERNION"); } readBno055(8, readDataQ); { int16_t w, x, y, z; w = x = y = z = 0; w = (((uint16_t)readDataQ[1]) << 8 ) | ((uint16_t)readDataQ[0]); x = (((uint16_t)readDataQ[3]) << 8 ) | ((uint16_t)readDataQ[2]); y = (((uint16_t)readDataQ[5]) << 8 ) | ((uint16_t)readDataQ[4]); z = (((uint16_t)readDataQ[7]) << 8 ) | ((uint16_t)readDataQ[6]); // convert the values to appropriate range const double scale = (1.0/(1<<14))*DEC_POINT_PRECISION; // (1/(1<<14))*1000 int32_t wThousand = (w*scale); int32_t xThousand = (x*scale); int32_t yThousand = (y*scale); int32_t zThousand = (z*scale); voltage = (float)adcData * 0.00625*VOLTAGE_RATIO; // 0.00625 = 1.6 (Referenzspannung) / 256 (2 hoch 8 bit Auflösung) uint32_t intVoltage=(uint16_t)(voltage*DEC_POINT_PRECISION); // store these values in network payload message networkMessagePayload=(AppMessagePayload_t){ .moduleName=0x02, .quatW=wThousand, .quatX=xThousand, .quatY=yThousand, .quatZ=zThousand, .voltage=intVoltage }; float floatW=(float)wThousand; float floatX=(float)xThousand; float floatY=(float)yThousand; float floatZ=(float)zThousand; floatW=floatW/DEC_POINT_PRECISION; floatX=floatX/DEC_POINT_PRECISION; floatY=floatY/DEC_POINT_PRECISION; floatZ=floatZ/DEC_POINT_PRECISION; printDoubleToUSART(floatW); printTextToUSART(", "); printDoubleToUSART(floatX); printTextToUSART(", "); printDoubleToUSART(floatY); printTextToUSART(", "); printDoubleToUSART(floatZ); printTextToUSART(", "); printDoubleToUSART(voltage); printLineToUSART(" "); } appstate = APP_READ_BATTERY; SYS_PostTask(APL_TASK_ID); break;
Сада сте у могућности да читате и пишете у регистре BNO055 паметног сензора, као и других који користе овај комуникацијски протокол. Такође можете и да коректно средите податке који долазе из сензора да би били тачни.
То је то што се тиче бекенд дела, завиримо сада у фронтенд део и визуализацију података сензора.
VPython кориснички интерфејс
Пошто је нужни захтев био да се паралелно прикажу два Зигби крајња уређеја на којима је прикачен по један сензор, погледајмо како прво то изгледа.
Приказ на доњој слици је одрађен од стране мог колеге и ја нисам имао утицаја нити доприноса имплементацији овог нужног захтева. Овако је изгледао интерфејс:
Оба приказа приказују тумачене податке сензора у реалном времену.
Овим путем сам дошао на идеју да нађем практичну примену пројекта, а у то време сам био фасцинирам алгоритмима за генерисање лавиритна. Коцкице су се сложиле и концепт игре се полако стварао у глави: генерисати лавиринт на табли који се окреће око своје осе базиран на уносним подацима са сензора.
Погледајмо мој допринос визуализацији овог пројекта.
Кликер-лавиринт игра
Концепт игре је веома једноставан; као играч почињете игру на једном од четири ћошка лавиринта и циљ је да нагињете лавиринт тако да кликер дође у крајњу позицију означену црвеним кругом. Крајња поција се увек налази на дијагоналном ћошку лавиринта од почетне позиције. Због временских потешкоћа при крају пројекта игри је фалило доста тога укључујући и мени, озбиљан систем награде за решен лавиринт, слободан избор величине лавиринта као и многе друге одлике игара.
Табла подржава три величине: малу (3x3 ћелије), средњу (6x6) и велику (9x9). На горњој слици је приказана средња величина табле која је коришћена у демонстрацији пројекта.
Да би програм генерисао лавиринт, имплементиран је рандомизован рекурзиван depth-first search алгоритам.
Закључне речи
Захваљујући разумљивом и спремном да помогне у разумевању, професору Краусу као и његовом веома способном асистенту, пројекат је успео, али и битније од тога - доста се и научило о бежичним мрежама и Зигби стандарду као и о микроконтролерима. Такође ми је пројекат помогао да разумем стратегију успеха у тимском окружењу као и да мотивишем колеге до постизања успеха.