/* * Copyright 2015-18 Hillcrest Laboratories, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License and * any applicable agreements you may have with Hillcrest Laboratories, Inc. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /* * Hillcrest Sensor Hub Transport Protocol (SHTP) API */ #include "shtp.h" #include "sh2_err.h" #include "sh2_util.h" #include // ------------------------------------------------------------------------ // Private types #define SH2_MAX_APPS (5) #define SHTP_APP_NAME_LEN (32) #define SH2_MAX_CHANS (8) #define SHTP_CHAN_NAME_LEN (32) // Defined Globally Unique Identifiers #define GUID_SHTP (0) // Command Channel commands and parameters #define SHTP_CHAN_COMMAND (0) #define CMD_ADVERTISE (0) #define CMD_ADVERTISE_SHTP (0) #define CMD_ADVERTISE_ALL (1) #define RESP_ADVERTISE (0) #define SHTP_HDR_LEN (4) #define TAG_SHTP_VERSION 0x80 typedef struct shtp_App_s { uint32_t guid; char appName[SHTP_APP_NAME_LEN]; } shtp_App_t; typedef struct shtp_AppListener_s { uint16_t guid; shtp_AdvertCallback_t *callback; void *cookie; } shtp_AppListener_t; typedef struct shtp_ChanListener_s { uint16_t guid; char chanName[SHTP_CHAN_NAME_LEN]; shtp_Callback_t *callback; void *cookie; } shtp_ChanListener_t; typedef struct shtp_Channel_s { uint8_t nextOutSeq; uint8_t nextInSeq; uint32_t guid; // app id char chanName[SHTP_CHAN_NAME_LEN]; bool wake; shtp_Callback_t *callback; void *cookie; } shtp_Channel_t; typedef enum { ADVERT_NEEDED, ADVERT_REQUESTED, ADVERT_IDLE, } advert_phase_t; // Per-instance data for SHTP typedef struct shtp_s { // Associated SHTP HAL // If 0, this indicates the SHTP instance is available for new opens sh2_Hal_t *pHal; // Asynchronous Event callback and it's cookie shtp_EventCallback_t *eventCallback; void * eventCookie; // Data from adverts char shtpVersion[8]; uint16_t outMaxPayload; uint16_t outMaxTransfer; // Transmit support uint8_t outTransfer[SH2_HAL_MAX_TRANSFER_OUT]; // Receive support uint16_t inMaxTransfer; uint16_t inRemaining; uint8_t inChan; uint8_t inPayload[SH2_HAL_MAX_PAYLOAD_IN]; uint16_t inCursor; uint32_t inTimestamp; uint8_t inTransfer[SH2_HAL_MAX_TRANSFER_IN]; // What stage of advertisement processing are we in. advert_phase_t advertPhase; // Applications shtp_App_t app[SH2_MAX_APPS]; uint8_t nextApp; // Advert registrations uint8_t nextAppListener; shtp_AppListener_t appListener[SH2_MAX_APPS]; // SHTP Channels shtp_Channel_t chan[SH2_MAX_CHANS]; // Channel listeners shtp_ChanListener_t chanListener[SH2_MAX_CHANS]; uint8_t nextChanListener; // Stats uint32_t txDiscards; uint32_t shortFragments; uint32_t tooLargePayloads; uint32_t badRxChan; uint32_t badTxChan; } shtp_t; // ------------------------------------------------------------------------ // Private data // Advertisement request static const uint8_t advertise[] = { CMD_ADVERTISE, CMD_ADVERTISE_ALL }; #define MAX_INSTANCES (1) static shtp_t instances[MAX_INSTANCES]; static bool shtp_initialized = false; // ------------------------------------------------------------------------ // Private functions static void shtp_init(void) { // clear instance memory. // In particular, this clears the pHal pointers which are used // to determine if an instance is open and in-use. memset(instances, 0, sizeof(instances)); // Set the initialized flag so this doesn't happen again. shtp_initialized = true; } static shtp_t *getInstance(void) { for (int n = 0; n < MAX_INSTANCES; n++) { if (instances[n].pHal == 0) { // This instance is free return &instances[n]; } } // Can't give an instance, none are free return 0; } // Register a listener for an app (advertisement listener) static void addAdvertListener(shtp_t *pShtp, uint16_t guid, shtp_AdvertCallback_t *callback, void * cookie) { shtp_AppListener_t *pAppListener = 0; // Bail out if no space for more apps if (pShtp->nextAppListener >= SH2_MAX_APPS) return; // Register this app pAppListener = &pShtp->appListener[pShtp->nextAppListener]; pShtp->nextAppListener++; pAppListener->guid = guid; pAppListener->callback = callback; pAppListener->cookie = cookie; } // Try to match registered listeners with their channels. // This is performed every time the underlying Channel, App, Listener data structures are updated. // As a result, channel number to callback association is fast when receiving packets static void updateCallbacks(shtp_t *pShtp) { // Figure out which callback is associated with each channel. // Channel -> (GUID, Chan name). // GUID -> App name. // (App name, Chan name) -> Callback uint32_t guid; const char * chanName = 0; for (int chanNo = 0; chanNo < SH2_MAX_CHANS; chanNo++) { // Reset callback for this channel until we find the right one. pShtp->chan[chanNo].callback = 0; if (pShtp->chan[chanNo].guid == 0xFFFFFFFF) { // This channel entry not used. continue; } // Get GUID and Channel Name for this channel guid = pShtp->chan[chanNo].guid; chanName = pShtp->chan[chanNo].chanName; // Look for a listener registered with this guid, channel name for (int listenerNo = 0; listenerNo < SH2_MAX_CHANS; listenerNo++) { if ((pShtp->chanListener[listenerNo].callback != 0) && (pShtp->chanListener[listenerNo].guid == guid) && (strcmp(chanName, pShtp->chanListener[listenerNo].chanName) == 0)) { // This listener is the one for this channel pShtp->chan[chanNo].callback = pShtp->chanListener[listenerNo].callback; pShtp->chan[chanNo].cookie = pShtp->chanListener[listenerNo].cookie; break; } } } } // Register a new channel listener static int addChanListener(shtp_t *pShtp, uint16_t guid, const char * chanName, shtp_Callback_t *callback, void *cookie) { shtp_ChanListener_t *pListener = 0; // Bail out if there are too many listeners registered if (pShtp->nextChanListener >= SH2_MAX_CHANS) return SH2_ERR; // Register channel listener pListener = &pShtp->chanListener[pShtp->nextChanListener]; pShtp->nextChanListener++; pListener->guid = guid; strcpy(pListener->chanName, chanName); pListener->callback = callback; pListener->cookie = cookie; // re-evaluate channel callbacks updateCallbacks(pShtp); return SH2_OK; } static inline uint16_t min_u16(uint16_t a, uint16_t b) { if (a < b) { return a; } else { return b; } } // Send a cargo as a sequence of transports static int txProcess(shtp_t *pShtp, uint8_t chan, const uint8_t* pData, uint32_t len) { int status = SH2_OK; bool continuation = false; uint16_t cursor = 0; uint16_t remaining; uint16_t transferLen; // length of transfer, minus the header uint16_t lenField; cursor = 0; remaining = len; while (remaining > 0) { // How much data (not header) can we send in next transfer transferLen = min_u16(remaining, pShtp->outMaxTransfer-SHTP_HDR_LEN); // Length field will be transferLen + SHTP_HDR_LEN lenField = transferLen + SHTP_HDR_LEN; // Put the header in the out buffer pShtp->outTransfer[0] = lenField & 0xFF; pShtp->outTransfer[1] = (lenField >> 8) & 0xFF; if (continuation) { pShtp->outTransfer[1] |= 0x80; } pShtp->outTransfer[2] = chan; pShtp->outTransfer[3] = pShtp->chan[chan].nextOutSeq++; // Stage one tranfer in the out buffer memcpy(pShtp->outTransfer+SHTP_HDR_LEN, pData+cursor, transferLen); remaining -= transferLen; cursor += transferLen; // Transmit (try repeatedly while HAL write returns 0) status = pShtp->pHal->write(pShtp->pHal, pShtp->outTransfer, lenField); while (status == 0) { shtp_service(pShtp); status = pShtp->pHal->write(pShtp->pHal, pShtp->outTransfer, lenField); } if (status < 0) { // Error, throw away this cargo pShtp->txDiscards++; return status; } // For the rest of this transmission, packets are continuations. continuation = true; } return SH2_OK; } // Callback for SHTP app-specific advertisement tags static void shtpAdvertHdlr(void *cookie, uint8_t tag, uint8_t len, uint8_t *val) { shtp_t *pShtp = (shtp_t *)cookie; switch (tag) { case TAG_SHTP_VERSION: if (strlen((const char *)val) < sizeof(pShtp->shtpVersion)) { strcpy(pShtp->shtpVersion, (const char *)val); } break; default: break; } } // Add one to the set of known Apps static void addApp(shtp_t *pShtp, uint32_t guid) { shtp_App_t *pApp = 0; // Bail out if this GUID is already registered for (int n = 0; n < pShtp->nextApp; n++) { if (pShtp->app[n].guid == guid) return; } // Bail out if no space for more apps if (pShtp->nextApp >= SH2_MAX_APPS) return; // Register this app pApp = &pShtp->app[pShtp->nextApp]; pShtp->nextApp++; pApp->guid = guid; strcpy(pApp->appName, ""); // Re-evaluate channel callbacks updateCallbacks(pShtp); } static void setAppName(shtp_t *pShtp, uint32_t guid, const char * appName) { shtp_App_t *pApp = 0; // Find the app entry with this GUID for (unsigned n = 0; n < pShtp->nextApp; n++) { if (pShtp->app[n].guid == guid) { pApp = &pShtp->app[n]; strcpy(pApp->appName, appName); return; } } } // Add one to the set of known channels static void addChannel(shtp_t *pShtp, uint8_t chanNo, uint32_t guid, const char * chanName, bool wake) { if (chanNo >= SH2_MAX_CHANS) return; shtp_Channel_t * pChan = &pShtp->chan[chanNo]; // Store channel definition pChan->guid = guid; strcpy(pChan->chanName, chanName); pChan->wake = wake; // Init channel-associated data pChan->nextOutSeq = 0; pChan->nextInSeq = 0; pChan->callback = 0; pChan->cookie = 0; // Re-evaluate channel callbacks updateCallbacks(pShtp); } static void callAdvertHandler(shtp_t *pShtp, uint32_t guid, uint8_t tag, uint8_t len, uint8_t *val) { // Find listener for this app for (int n = 0; n < SH2_MAX_APPS; n++) { if (pShtp->appListener[n].guid == guid) { // Found matching App entry if (pShtp->appListener[n].callback != 0) { pShtp->appListener[n].callback(pShtp->appListener[n].cookie, tag, len, val); return; } } } } static void processAdvertisement(shtp_t *pShtp, uint8_t *payload, uint16_t payloadLen) { uint16_t x; uint8_t tag; uint8_t len; uint8_t *val; uint16_t cursor = 1; uint32_t guid = 0; char appName[SHTP_APP_NAME_LEN]; char chanName[SHTP_CHAN_NAME_LEN]; uint8_t chanNo = 0; bool wake = false; strcpy(appName, ""); strcpy(chanName, ""); pShtp->advertPhase = ADVERT_IDLE; while (cursor < payloadLen) { tag = payload[cursor++]; len = payload[cursor++]; val = payload+cursor; cursor += len; // Process tag switch (tag) { case TAG_NULL: // Reserved value, not a valid tag. break; case TAG_GUID: // A new GUID is being established so terminate advertisement process with earlier app, if any. callAdvertHandler(pShtp, guid, TAG_NULL, 0, 0); guid = readu32(val); addApp(pShtp, guid); strcpy(appName, ""); strcpy(chanName, ""); break; case TAG_MAX_CARGO_PLUS_HEADER_WRITE: x = readu16(val) - SHTP_HDR_LEN; if (x < SH2_HAL_MAX_PAYLOAD_OUT) { pShtp->outMaxPayload = x; } break; case TAG_MAX_CARGO_PLUS_HEADER_READ: x = readu16(val) - SHTP_HDR_LEN; // No need to store this! break; case TAG_MAX_TRANSFER_WRITE: x = readu16(val) - SHTP_HDR_LEN; if (x < SH2_HAL_MAX_TRANSFER_OUT) { pShtp->outMaxTransfer = x; } else { pShtp->outMaxTransfer = SH2_HAL_MAX_TRANSFER_OUT; } break; case TAG_MAX_TRANSFER_READ: x = readu16(val) - SHTP_HDR_LEN; if (x < SH2_HAL_MAX_TRANSFER_IN) { pShtp->inMaxTransfer = x; } break; case TAG_NORMAL_CHANNEL: chanNo = readu8(val); wake = false; break; case TAG_WAKE_CHANNEL: chanNo = readu8(val); wake = true; break; case TAG_APP_NAME: strcpy(appName, (const char *)val); setAppName(pShtp, guid, appName); break; case TAG_CHANNEL_NAME: strcpy(chanName, (const char *)val); addChannel(pShtp, chanNo, guid, (const char *)val, wake); // Store channel metadata if (chanNo < SH2_MAX_CHANS) { pShtp->chan[chanNo].guid = guid; strcpy(pShtp->chan[chanNo].chanName, chanName); pShtp->chan[chanNo].wake = wake; } break; case TAG_ADV_COUNT: // Not yet supported. break; default: // Nothing special needs to be done with this tag. break; } // Deliver a TLV entry to the app's handler callAdvertHandler(pShtp, guid, tag, len, val); } // terminate advertisement process with last app callAdvertHandler(pShtp, guid, TAG_NULL, 0, 0); } // Callback for SHTP command channel static void shtpCmdListener(void *cookie, uint8_t *payload, uint16_t len, uint32_t timestamp) { shtp_t *pShtp = (shtp_t *)cookie; if ((payload == 0) || (len == 0)) return; uint8_t response = payload[0]; switch (response) { case RESP_ADVERTISE: processAdvertisement(pShtp, payload, len); break; default: // unknown response break; } } static void rxAssemble(shtp_t *pShtp, uint8_t *in, uint16_t len, uint32_t t_us) { uint16_t payloadLen; bool continuation; uint8_t chan = 0; uint8_t seq = 0; // discard invalid short fragments if (len < SHTP_HDR_LEN) { pShtp->shortFragments++; return; } // Interpret header fields payloadLen = (in[0] + (in[1] << 8)) & (~0x8000); continuation = ((in[1] & 0x80) != 0); chan = in[2]; seq = in[3]; if (payloadLen < SHTP_HDR_LEN) { pShtp->shortFragments++; if (pShtp->eventCallback) { pShtp->eventCallback(pShtp->eventCookie, SHTP_SHORT_FRAGMENT); } return; } if ((chan >= SH2_MAX_CHANS) || (chan >= pShtp->nextChanListener)) { // Invalid channel id. pShtp->badRxChan++; if (pShtp->eventCallback) { pShtp->eventCallback(pShtp->eventCookie, SHTP_BAD_RX_CHAN); } return; } // Discard earlier assembly in progress if the received data doesn't match it. if (pShtp->inRemaining) { // Check this against previously received data. if (!continuation || (chan != pShtp->inChan) || (seq != pShtp->chan[chan].nextInSeq)) { // This fragment doesn't fit with previous one, discard earlier data pShtp->inRemaining = 0; } } if (pShtp->inRemaining == 0) { if (payloadLen > sizeof(pShtp->inPayload)) { // Error: This payload won't fit! Discard it. pShtp->tooLargePayloads++; if (pShtp->eventCallback) { pShtp->eventCallback(pShtp->eventCookie, SHTP_TOO_LARGE_PAYLOADS); } return; } // This represents a new payload // Store timestamp pShtp->inTimestamp = t_us; // Start a new assembly. pShtp->inCursor = 0; pShtp->inChan = chan; } // Append the new fragment to the payload under construction. if (len > payloadLen) { // Only use the valid portion of the transfer len = payloadLen; } memcpy(pShtp->inPayload + pShtp->inCursor, in+SHTP_HDR_LEN, len-SHTP_HDR_LEN); pShtp->inCursor += len-SHTP_HDR_LEN; pShtp->inRemaining = payloadLen - len; // If whole payload received, deliver it to channel listener. if (pShtp->inRemaining == 0) { // Call callback if there is one. if (pShtp->chan[chan].callback != 0) { pShtp->chan[chan].callback(pShtp->chan[chan].cookie, pShtp->inPayload, pShtp->inCursor, pShtp->inTimestamp); } } // Remember next sequence number we expect for this channel. pShtp->chan[chan].nextInSeq = seq + 1; } // ------------------------------------------------------------------------ // Public functions // Takes HAL pointer, returns shtp ID for use in future calls. // HAL will be opened by this call. void *shtp_open(sh2_Hal_t *pHal) { if (!shtp_initialized) { // Perform one-time module initialization shtp_init(); } // Validate params if (pHal == 0) { // Error return 0; } // Find an available instance for this open shtp_t *pShtp = getInstance(); if (pShtp == 0) { // No instances available, return error return 0; } // Clear the SHTP instance as a shortcut to initializing all fields memset(pShtp, 0, sizeof(shtp_t)); // Store reference to the HAL pShtp->pHal = pHal; // Clear the asynchronous event callback point pShtp->eventCallback = 0; pShtp->eventCookie = 0; // Initialize state vars (be prepared for adverts) pShtp->outMaxPayload = SH2_HAL_MAX_PAYLOAD_OUT; pShtp->outMaxTransfer = SH2_HAL_MAX_TRANSFER_OUT; // Establish SHTP App and command channel a priori addApp(pShtp, GUID_SHTP); addChannel(pShtp, 0, GUID_SHTP, "command", false); // Register SHTP advert listener and command channel listener shtp_listenAdvert(pShtp, GUID_SHTP, shtpAdvertHdlr, pShtp); shtp_listenChan(pShtp, GUID_SHTP, "command", shtpCmdListener, pShtp); // When we open the HAL, it resets the device and adverts are sent automatically. // So we go to ADVERT_REQUESTED state. They are on the way. pShtp->advertPhase = ADVERT_REQUESTED; // Open HAL pHal->open(pHal); return pShtp; } // Releases resources associated with this SHTP instance. // HAL will not be closed. void shtp_close(void *pInstance) { shtp_t *pShtp = (shtp_t *)pInstance; pShtp->pHal->close(pShtp->pHal); // Clear pShtp // (Resetting pShtp->pHal to 0, returns this instance to the free pool) memset(pShtp, 0, sizeof(shtp_t)); } // Register the pointer of the callback function for reporting asynchronous events void shtp_setEventCallback(void *pInstance, shtp_EventCallback_t * eventCallback, void *eventCookie) { shtp_t *pShtp = (shtp_t *)pInstance; pShtp->eventCallback = eventCallback; pShtp->eventCookie = eventCookie; } // Register a listener for an SHTP channel int shtp_listenChan(void *pInstance, uint16_t guid, const char * chan, shtp_Callback_t *callback, void * cookie) { shtp_t *pShtp = (shtp_t *)pInstance; // Balk if channel name isn't valid if ((chan == 0) || (strlen(chan) == 0)) return SH2_ERR_BAD_PARAM; return addChanListener(pShtp, guid, chan, callback, cookie); } // Register a listener for SHTP advertisements int shtp_listenAdvert(void *pInstance, uint16_t guid, shtp_AdvertCallback_t *advertCallback, void * cookie) { shtp_t *pShtp = (shtp_t *)pInstance; // Register the advert listener addAdvertListener(pShtp, guid, advertCallback, cookie); // Arrange for a new set of advertisements, for this listener if (pShtp->advertPhase == ADVERT_IDLE) { pShtp->advertPhase = ADVERT_NEEDED; } return SH2_OK; } // Look up the channel number for a particular app, channel. uint8_t shtp_chanNo(void *pInstance, const char * appName, const char * chanName) { shtp_t *pShtp = (shtp_t *)pInstance; int chan = 0; uint32_t guid = 0xFFFFFFFF; // Determine GUID for this appname for (int n = 0; n < SH2_MAX_APPS; n++) { if (strcmp(pShtp->app[n].appName, appName) == 0) { guid = pShtp->app[n].guid; break; } } if (guid == 0xFFFFFFFF) return -1; for (chan = 0; chan < SH2_MAX_CHANS; chan++) { if ((strcmp(pShtp->chan[chan].chanName, chanName) == 0) && pShtp->chan[chan].guid == guid) { // Found match return chan; } } // Not found return 0xFF; } // Send an SHTP payload on a particular channel int shtp_send(void *pInstance, uint8_t channel, const uint8_t *payload, uint16_t len) { shtp_t *pShtp = (shtp_t *)pInstance; int ret = SH2_OK; if (len > pShtp->outMaxPayload) { return SH2_ERR_BAD_PARAM; } if (channel >= SH2_MAX_CHANS) { pShtp->badTxChan++; return SH2_ERR_BAD_PARAM; } ret = txProcess(pShtp, channel, payload, len); return ret; } // Check for received data and process it. void shtp_service(void *pInstance) { shtp_t *pShtp = (shtp_t *)pInstance; uint32_t t_us = 0; if (pShtp->advertPhase == ADVERT_NEEDED) { pShtp->advertPhase = ADVERT_REQUESTED; // do this before send, to avoid recursion. int status = shtp_send(pShtp, SHTP_CHAN_COMMAND, advertise, sizeof(advertise)); if (status != SH2_OK) { // Oops, advert request failed. Go back to needing one. pShtp->advertPhase = ADVERT_NEEDED; } } int len = pShtp->pHal->read(pShtp->pHal, pShtp->inTransfer, sizeof(pShtp->inTransfer), &t_us); if (len) { rxAssemble(pShtp, pShtp->inTransfer, len, t_us); } }