From 594f4857f909605a48525938a240cd58da4d32fb Mon Sep 17 00:00:00 2001 From: Rob Meades Date: Tue, 8 Aug 2023 16:28:30 +0100 Subject: [PATCH] Feature, cellular, SARA-R5: add support for CellTime. (#955) CellTime, the feature through which accurate hardware timing may be achieved through the cellular network and/or GNSS, is now supported: see u_cell_time.h and the uCellTime API. This feature is supported on SARA-R5 cellular modules only. --- cell/README.md | 3 +- cell/api/u_cell_cfg.h | 4 +- cell/api/u_cell_info.h | 4 +- cell/api/u_cell_net.h | 101 ++- cell/api/u_cell_time.h | 453 +++++++++++ cell/src/u_cell.c | 2 + cell/src/u_cell_cfg.c | 5 +- cell/src/u_cell_info.c | 44 +- cell/src/u_cell_loc.c | 23 +- cell/src/u_cell_net.c | 211 ++++- cell/src/u_cell_private.c | 112 +++ cell/src/u_cell_private.h | 53 ++ cell/src/u_cell_time.c | 843 ++++++++++++++++++++ cell/src/u_cell_time_private.h | 62 ++ cell/test/u_cell_net_test.c | 3 + cell/test/u_cell_time_test.c | 729 +++++++++++++++++ port/platform/arduino/source.txt | 1 + port/platform/arduino/source_test.txt | 1 + port/platform/common/automation/DATABASE.md | 6 +- ubxlib.h | 1 + 20 files changed, 2544 insertions(+), 117 deletions(-) create mode 100644 cell/api/u_cell_time.h create mode 100644 cell/src/u_cell_time.c create mode 100644 cell/src/u_cell_time_private.h create mode 100644 cell/test/u_cell_time_test.c diff --git a/cell/README.md b/cell/README.md index cc1bd823..c53bdb2c 100644 --- a/cell/README.md +++ b/cell/README.md @@ -13,7 +13,8 @@ The cellular APIs are split into the following groups: - `sock`: sockets, for exchanging data (but see the [common/sock](/common/sock) component for the best way to do this). - `mqtt`: MQTT client (but see the [common/mqtt_client](/common/mqtt_client) component for the best way to do this). - `http`: HTTP client (but see the [common/http_client](/common/http_client) component for the best way to do this). -- `loc`: getting a location fix anywhere using the Cell Locate service (but see the [common/location](/common/location) component for the best way to do this) and using the Assist Now service to improve the time to first fix when a GNSS module is included inside or connected-via the cellular module; you will need an authentication token from the [Location Services section](https://portal.thingstream.io/app/location-services) of your [Thingstream portal](https://portal.thingstream.io/app/dashboard). If you have a GNSS chip inside or connected via a cellular module and want to control it directly from your MCU see the [gnss](/gnss) API but note that the `loc` API here will make use of a such a GNSS chip in any case. +- `loc`: getting a location fix anywhere using the Cell Locate service (but see the [common/location](/common/location) component for the best way to do this) and using the Assist Now service to improve the time to - `time`: support for CellTime, a feature through which accurate HW timing may be achieved using cellular and/or GNSS (SARA-R5 only). +first fix when a GNSS module is included inside or connected-via the cellular module; you will need an authentication token from the [Location Services section](https://portal.thingstream.io/app/location-services) of your [Thingstream portal](https://portal.thingstream.io/app/dashboard). If you have a GNSS chip inside or connected via a cellular module and want to control it directly from your MCU see the [gnss](/gnss) API but note that the `loc` API here will make use of a such a GNSS chip in any case. - `gpio`: configure and set the state of GPIO lines that are on the cellular module. - `file`: access to file storage on the cellular module. - `fota`: access to information about the state of FOTA in the cellular module. diff --git a/cell/api/u_cell_cfg.h b/cell/api/u_cell_cfg.h index 1186d492..e4d75c37 100755 --- a/cell/api/u_cell_cfg.h +++ b/cell/api/u_cell_cfg.h @@ -399,7 +399,7 @@ int32_t uCellCfgSetGreetingCallback(uDeviceHandle_t cellHandle, * @param size the number of bytes available at pStr, * including room for a null terminator. * @return on success, the number of characters copied into - * pStr NOT including the terminator (i.e. as + * pStr NOT including the terminator (as * strlen() would return), on failure negative * error code. If there is no greeting message * zero will be returned. @@ -486,7 +486,7 @@ int32_t uCellCfgGetGnssProfile(uDeviceHandle_t cellHandle, char *pServerName, * * @param cellHandle the handle of the cellular instance. * @param timeLocal the local time in seconds since midnight on - * 1st Jan 1970, (i.e. Unix time, but local rather + * 1st Jan 1970, (Unix time, but local rather * than UTC). * @param timeZoneSeconds the time-zone offset of timeLocal in seconds; for * example, if you are one hour ahead of UTC diff --git a/cell/api/u_cell_info.h b/cell/api/u_cell_info.h index 4973f321..82925d03 100755 --- a/cell/api/u_cell_info.h +++ b/cell/api/u_cell_info.h @@ -326,8 +326,8 @@ int32_t uCellInfoGetTimeUtcStr(uDeviceHandle_t cellHandle, * offset in seconds; may be NULL. * @return on success the local time in * seconds since midnight on 1st - * Jan 1970 (i.e. Unix time but - * local instead of UTC) else + * Jan 1970 (Unix time but local + * instead of UTC) else * negative error code. */ int64_t uCellInfoGetTime(uDeviceHandle_t cellHandle, diff --git a/cell/api/u_cell_net.h b/cell/api/u_cell_net.h index 3b1fcd14..d276451c 100644 --- a/cell/api/u_cell_net.h +++ b/cell/api/u_cell_net.h @@ -136,6 +136,18 @@ extern "C" { ((status) == U_CELL_NET_STATUS_REGISTERED_NO_CSFB_HOME) || \ ((status) == U_CELL_NET_STATUS_REGISTERED_NO_CSFB_ROAMING)) +#ifndef U_CELL_NET_DEEP_SCAN_RETRIES +/** The number of times to retry a deep scan on error. + */ +# define U_CELL_NET_DEEP_SCAN_RETRIES 2 +#endif + +#ifndef U_CELL_NET_DEEP_SCAN_TIME_SECONDS +/** A guard time-out value for uCellNetDeepScan(). + */ +# define U_CELL_NET_DEEP_SCAN_TIME_SECONDS 240 +#endif + /* ---------------------------------------------------------------- * TYPES * -------------------------------------------------------------- */ @@ -238,6 +250,21 @@ typedef enum { U_CELL_NET_REG_DOMAIN_MAX_NUM } uCellNetRegDomain_t; +/** Information on a cell, passed to the callback of uCellNetDeepScan(), + * could be used in a call to uCellTimeSyncCellEnable(). + */ +typedef struct { + int32_t mcc; /**< mobile country code. */ + int32_t mnc; /**< mobile network code. */ + int32_t tac; /**< tracking area code. */ + int32_t earfcnDownlink; /**< downlink E-UTRAN absolute radio frequency channel number. */ + int32_t earfcnUplink; /**< uplink E-UTRAN absolute radio frequency channel number. */ + int32_t cellIdLogical; /**< logical cell ID. */ + int32_t cellIdPhysical; /**< physical cell ID. */ + int32_t rsrpDbm; /**< current reference signal received power in dBm. */ + int32_t rsrqDb; /**< current reference signal received quality in dB. */ +} uCellNetCellInfo_t; + /* ---------------------------------------------------------------- * FUNCTIONS * -------------------------------------------------------------- */ @@ -284,11 +311,9 @@ typedef enum { * convenience. This function may also be * used to feed any watchdog timer that * might be running during longer cat-M1/NB1 - * network search periods. The single - * int32_t parameter is the cell handle. - * May be NULL, in which case the connection - * attempt will eventually time out on - * failure. + * network search periods. May be NULL, in + * which case the connection attempt will + * eventually time out on failure. * @return zero on success or negative error code on * failure. */ @@ -320,10 +345,9 @@ int32_t uCellNetConnect(uDeviceHandle_t cellHandle, * This function may also be used to feed * any watchdog timer that might be running * during longer cat-M1/NB1 network search - * periods. The single int32_t parameter - * is the cell handle. May be NULL, in which - * case the registration attempt will - * eventually time-out on failure. + * periods. May be NULL, in which case the + * registration attempt will eventually + * time out on failure. * @return zero on success or negative error code on * failure. */ @@ -392,8 +416,7 @@ int32_t uCellNetActivate(uDeviceHandle_t cellHandle, * continue while it returns true. This * allows the caller to terminate * activation at their convenience. - * May be NULL. The single int32_t - * parameter is the cell handle. + * May be NULL. * @return zero on success or negative error code * on failure. */ @@ -413,8 +436,7 @@ int32_t uCellNetDeactivate(uDeviceHandle_t cellHandle, * continue while it returns true. This * allows the caller to terminate * registration at their convenience. - * May be NULL. The single int32_t - * parameter is the cell handle. + * May be NULL. * @return zero on success or negative error code on * failure. */ @@ -463,8 +485,7 @@ int32_t uCellNetDisconnect(uDeviceHandle_t cellHandle, * watch-dog function to be called if required; * may be NULL. The function should return * true; if it returns false the network - * scan will be aborted. The single - * int32_t parameter is the cell handle. + * scan will be aborted. * @return the number of networks found or negative * error code. If * #U_CELL_ERROR_TEMPORARY_FAILURE is returned @@ -523,10 +544,60 @@ int32_t uCellNetScanGetNext(uDeviceHandle_t cellHandle, */ void uCellNetScanGetLast(uDeviceHandle_t cellHandle); +/** Do an extended network search, AT+COPS=5; only supported on SARA-R5. + * The detected cells may be used with uCellTimeSyncCellEnable(), + * supported on SARA-R5xx-01B and later modules. + * + * @param cellHandle the handle of the cellular instance. + * @param[in] pCallback pointer to a function to process each + * result, as they are returned, where the + * first parameter is the handle of the + * cellular device, the second parameter + * is a pointer to the cell information + * WHICH MAY BE NULL if pCallback is just + * being called as a periodic "keep going" + * check (the contents of a (non-NULL) pointer + * MUST be copied by the callback as it + * will no longer be valid once the + * callback has returned) and the third + * parameter is the value of pCallbackParameter; + * the function should return true to + * continue the scan or it may return false + * to abort the scan (e.g. if it has been + * informed of a good enough cell). + * May be NULL (useful for debugging only). + * A scan will be aborted if more than + * #U_CELL_NET_DEEP_SCAN_TIME_SECONDS pass. + * IMPORTANT: the callback function should + * not call back into this API (which will + * be locked): it must return false to allow + * uCellNetDeepScan() to exit and only then + * should it call, for instance, + * uCellTimeSyncCellEnable(). + * @param[in,out] pCallbackParameter a pointer to be passed to pCallback + * as its third parameter; may be NULL. + * @return on success the number of cells that were + * detected else negative error code; note that + * this is the number of cells in a complete + * and successful scan. If the scan had to + * be repeated because the module indicated + * a failure part way through then the + * callback may end up being called more + * times than the return value might suggest. + * A value of zero will be returned if the + * scan succeeds but returns no cells. + */ +int32_t uCellNetDeepScan(uDeviceHandle_t cellHandle, + bool (*pCallback) (uDeviceHandle_t, + uCellNetCellInfo_t *, + void *), + void *pCallbackParameter); + /** Enable or disable the registration status call-back. This * call-back allows the application to know the various * states of the network scanning, registration and rejections * from the networks. + * * You may use the #U_CELL_NET_STATUS_MEANS_REGISTERED macro * with the second parameter passed to the callback to * determine if the status value means that the module is diff --git a/cell/api/u_cell_time.h b/cell/api/u_cell_time.h new file mode 100644 index 00000000..e3ae626f --- /dev/null +++ b/cell/api/u_cell_time.h @@ -0,0 +1,453 @@ +/* + * Copyright 2019-2023 u-blox + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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. + */ + +#ifndef _U_CELL_TIME_H_ +#define _U_CELL_TIME_H_ + +/* Only header files representing a direct and unavoidable + * dependency between the API of this module and the API + * of another module should be included here; otherwise + * please keep #includes to your .c files. */ + +#include "u_device.h" + +/** \addtogroup _cell + * @{ + */ + +/** @file + * @brief THIS API IS NOT THE WAY TO GET/SET THE CLOCK/CALENDER TIME! + * For that, see uCellInfoGetTimeUtc(), uCellInfoGetTimeUtcStr() + * and uCellInfoGetTime() in u_cell_info.h or uCellCfgSetTime() + * in u_cell_cfg.h. But, since you found this file, aliases + * for those functions are also provided here. + * + * This header file defines the CellTime APIs that can be used + * to employ the highly accurate timing of the cellular network + * to toggle a GPIO on the cellular module with high accuracy or, + * conversely, to measure the time that a GPIO was toggled with + * high accuracy. In other words, the functions are about + * timING, using an arbitrary time-base, and NOT about absolute + * clock/calender time. This API is only currently supported by + * SARA-R5 modules. + * + * These functions are thread-safe with the proviso that a cellular + * instance should not be accessed before it has been added or after + * it has been removed. + */ + +#ifdef __cplusplus +extern "C" { +#endif + +/* ---------------------------------------------------------------- + * COMPILE-TIME MACROS + * -------------------------------------------------------------- */ + +#ifndef U_CELL_TIME_PULSE_PERIOD_SECONDS +/** The period of a time pulse from the cellular module, range 0 + * (which equates to 0.5 seconds) to 4 seconds, only used + * for #U_CELL_TIME_MODE_PULSE. + */ +# define U_CELL_TIME_PULSE_PERIOD_SECONDS 1 +#endif + +#ifndef U_CELL_TIME_PULSE_WIDTH_MILLISECONDS +/** The width of a time pulse from the cellular module, range 0 to + * 490 milliseconds if #U_CELL_TIME_PULSE_PERIOD_SECONDS is 0, else + * range 0 to 990 milliseconds; only used for #U_CELL_TIME_MODE_PULSE. + */ +# define U_CELL_TIME_PULSE_WIDTH_MILLISECONDS 100 +#endif + +/** The number of seconds between 1st Jan 1970 and 1st Jan 2018; + * add this to the timeNanoseconds of #uCellTime_t when cellTime + * is true to convert the arbitrary CellTime time-base to Unix time. + */ +#define U_CELL_TIME_CONVERT_TO_UNIX_SECONDS 1514764800ULL + +#ifndef U_CELL_TIME_SYNC_MODE +/** The sync mode used by uCellTimeSyncCellEnable(): 1 includes sending + * a RACH, 2 does not. + */ +# define U_CELL_TIME_SYNC_MODE 1 +#endif + +#ifndef U_CELL_TIME_SYNC_TIME_SECONDS +/** A guard time-out value for uCellTimeSyncCellEnable(). + */ +# define U_CELL_TIME_SYNC_TIME_SECONDS 30 +#endif + +/* ---------------------------------------------------------------- + * TYPES + * -------------------------------------------------------------- */ + +/** The possible modes that CellTime can operate in. + */ +typedef enum { + U_CELL_TIME_MODE_OFF = 0, /**< this mode cannot be set, it is + a _result_ of calling + uCellTimeDisable(). */ + U_CELL_TIME_MODE_PULSE = 1, /**< time pulses will be emitted + on a pin of the cellular module. */ + U_CELL_TIME_MODE_ONE_SHOT = 2, /**< time synchronisation is a one-shot + pulse on a pin of the cellular + module, plus a timestamp URC + will also be emitted. */ + U_CELL_TIME_MODE_EXT_INT_TIMESTAMP = 3, /**< a timestamp URC will be emitted + when the EXT_INT pin of the cellular + module is asserted (see + uCellTimeSetCallback()). */ + U_CELL_TIME_MODE_BEST_EFFORT = 4 /**< best effort GNSS/RTC/cellular time; + this mode cannot be set, it is a + _result_ of calling uCellTimeEnable() + with cellTimeOnly set to false. */ +} uCellTimeMode_t; + +/** A structure to contain the time as returned by the URC +UUTIME, + * used by the callback of uCellTimeSetCallback(). + */ +typedef struct { + bool cellTime; /**< true if timeNanoseconds is a high + accuracy relative time, derived from + the timing of the cellular network, + else timeNanoseconds is derived + from GNSS/RTC (and can be treated as UTC). */ + int64_t timeNanoseconds; /**< the time in nanoseconds. If cellTime + is true the value is relative, including + any offset passed to uCellTimeEnable(), + else it is UTC Unix time i.e. since + midnight on 1st Jan 1970. If you wish + to convert the relative CellTime to the + Unix time-base, you can do so by adding to it + #U_CELL_TIME_CONVERT_TO_UNIX_SECONDS * 1000000000; + of course this does NOT make it UTC, just + a Unix time near the start of 2018. */ + int64_t accuracyNanoseconds; /**< the accuracy of timeNanoseconds in + nanoseconds. */ +} uCellTime_t; + +/** The possible sources of time synchronisation for CellTime. + */ +typedef enum { + U_CELL_TIME_SOURCE_INIT = 0, /**< just starting up, no source yet. */ + U_CELL_TIME_SOURCE_GNSS = 1, /**< synchronisation achieved using GNSS, + time will be UTC. */ + U_CELL_TIME_SOURCE_CELL = 2, /**< synchronisation achieved using the + cellular network, time will be + much more accurate but will be of + an arbitrary base, not UTC. */ + U_CELL_TIME_SOURCE_RTC = 3 /**< synchronisation achieved using the + RTC, time will be UTC. */ +} uCellTimeSource_t; + +/** The possible results of a CellTime operation. + */ +typedef enum { + U_CELL_TIME_RESULT_SUCCESS = 0, /**< all done, no error. */ + U_CELL_TIME_RESULT_UTC_ALIGNMENT = 1, /**< UTC alignment has + been achieved, the + offsetNanoseconds element + of #uCellTimeEvent_t will + contain the timing + discontinuity that + resulted. */ + U_CELL_TIME_RESULT_OFFSET_DETECTED = 2, /**< an offset has been + detected in cellular + timing, the + offsetNanoseconds + element of + #uCellTimeEvent_t will + contain the offset. */ + U_CELL_TIME_RESULT_TIMEOUT = 3, + U_CELL_TIME_RESULT_GPIO_ERROR = 4, + U_CELL_TIME_RESULT_SYNC_LOST = 5 /** synchronisation with + the cellular network + has been lost, time + is no longer valid. */ +} uCellTimeResult_t; + +/** A structure to contain a CellTime event, mostly the contents of the + * URC +UUTIMEIND, used by the callback of uCellTimeEnable(). + */ +typedef struct { + bool synchronised; /**< true if synchronisation has been + achieved. */ + uCellTimeResult_t result; /**< the, possibly intermediate, result of + a CellTime operation. */ + uCellTimeMode_t mode; /**< the mode that CellTime is currently + operating in. */ + uCellTimeSource_t source; /**< the source currently used for timing. */ + int32_t cellIdPhysical; /**< the physical cell ID of the serving + cell; only populated if source is + #U_CELL_TIME_SOURCE_CELL, -1 otherwise. */ + bool cellTime; /**< true if high-accuracy timing, derived + from that of the cellular network, + has been achieved, else the timing is + best-effort, derived from GNSS/RTC. */ + int64_t offsetNanoseconds;/**< may be populated when the result field + indicates that a discontinuity in + cellular timing has been detected + (#U_CELL_TIME_RESULT_UTC_ALIGNMENT or + #U_CELL_TIME_RESULT_OFFSET_DETECTED); a + value of LLONG_MIN is used to indicate + "not present". */ +} uCellTimeEvent_t; + +/* ---------------------------------------------------------------- + * FUNCTIONS: CELLTIME + * -------------------------------------------------------------- */ + +/** Enable CellTime, only supported on SARA-R5. CellTime is about + * using the highly accurate cellular network for timing of hardware, + * using an arbitrary time-base, it is NOT about absolute clock/calender + * time (UTC or local). + * + * If this function returns success it doesn't necessarily mean that + * the requested CellTime operation has succeeded, since the operation + * may take a while to complete: please monitor pCallback for progress + * and for the final outcome; look for the synchronised field of + * #uCellTimeEvent_t being set to true to indicate "done". + * + * You may call this function repeatedly provided the mode doesn't + * change; if you want to change mode please call uCellTimeDisable() + * first. + * + * Any time pulse will appear on pin number/GPIO ID 19, pin name + * "GPIO6" of the cellular module. If the mode is + * #U_CELL_TIME_MODE_PULSE then for SARA-R5xx-00B the pulse width + * is fixed at 3 ms and the period 1 second while for SARA-R5xx-01B and + * later the pulse will be of duration #U_CELL_TIME_PULSE_WIDTH_MILLISECONDS + * and period #U_CELL_TIME_PULSE_PERIOD_SECONDS. If the mode is + * #U_CELL_TIME_MODE_EXT_INT_TIMESTAMP then the input pin is + * the "EXT_INT" pin of the cellular module, pin number/GPIO ID 33. + * + * If the GNSS device is external to the cellular module, two additional + * pins must be connected: pin number/GPIO ID 46, pin name "SDIO_CMD", + * must be connected to the GNSS device TIMEPULSE output and pin + * number/GPIO ID 25, pin name "GPIO4", must be conncted to the + * GNSS device EXTINT output. + * + * If uCellGpioConfig() had previously been called to use the pins + * in question as user-controllable pins, this will override that + * setting. + * + * If the GNSS device available to the cellular module is already in + * use for something else (e.g. used by the GNSS API or by Cell Locate) + * then it cannot also be used for CellTime and hence an error may be + * returned if cellTimeOnly is set to false. + * + * @param cellHandle the handle of the cellular instance. + * @param mode the mode that CellTime should operate in, + * must be one of #U_CELL_TIME_MODE_PULSE, + * #U_CELL_TIME_MODE_ONE_SHOT or + * #U_CELL_TIME_MODE_EXT_INT_TIMESTAMP. + * @param cellTimeOnly if true then only the cellular network is + * used to provide highly accurate CellTime, + * else GNSS/RTC may also be used to provide + * a best-effort result. + * @param offsetNanoseconds an offset to be added to the timing of + * the time pulse in nanoseconds when it is + * a high accuracy, but relative, CellTime, + * derived from the cellular network; not + * used when cellTime is reported as false. + * To align CellTime with Unix time, set this to + * #U_CELL_TIME_CONVERT_TO_UNIX_SECONDS * 1000000000. + * @param[in] pCallback pointer to the function to monitor the + * outcome of the CellTime operation, where + * the first parameter is the handle of the + * cellular device, the second parameter + * is a pointer to the latest result (the + * contents of which MUST be copied by the + * callback as it will no longer be valid + * once the callback has returned) and the + * third parameter is the value of + * pCallbackParameter; may be NULL. NOTE + * that this callback may be called both + * BEFORE and AFTER uCellTimeEnable() has + * returned and hence both pCallback and + * pCallbackParameter must remain valid at + * least until uCellTimeDisable() is called. + * @param[in,out] pCallbackParameter a pointer to be passed to pCallback + * as its third parameter; may be NULL, + * must remain valid at least until + * uCellTimeDisable() is called. + * @return zero on success else negative error code. + */ +int32_t uCellTimeEnable(uDeviceHandle_t cellHandle, + uCellTimeMode_t mode, bool cellTimeOnly, + int64_t offsetNanoseconds, + void (*pCallback) (uDeviceHandle_t, + uCellTimeEvent_t *, + void *), + void *pCallbackParameter); + +/** Disable CellTime. After this function has returned, the callbacks + * passed to uCellTimeEnable() and uCellTimeSetCallback() will no longer + * be called, i.e. you must call uCellTimeSetCallback() again if you + * decide to re-enable CellTime. + * + * Note that this does not change the pin configurations that + * may have been set by uCellTimeEnable(); should you wish to use + * any of them as user-controllable pins once more, you must call + * uCellGpioConfig() to do so. + * + * @param cellHandle the handle of the cellular instance. + * @return zero on success else negative error code. + */ +int32_t uCellTimeDisable(uDeviceHandle_t cellHandle); + +/** Set a callback which will be called when time has been received + * in a +UUTIME URC. Only relevant when operating in modes + * #U_CELL_TIME_MODE_ONE_SHOT or #U_CELL_TIME_MODE_EXT_INT_TIMESTAMP. + * + * @param cellHandle the handle of the cellular instance. + * @param[in] pCallback pointer to the function to handle any + * time-keeping status changes, where the + * first parameter is the handle of the + * cellular device, the second parameter + * is a pointer to the new time information + * (the contents of which MUST be copied by + * the callback as it will no longer be valid + * once the callback has returned) and the + * third parameter is the value of + * pCallbackParameter. Use NULL to cancel + * an existing callback. + * @param[in,out] pCallbackParameter a pointer to be passed to pCallback + * as its third parameter; may be NULL. + * @return zero on success else negative error code. + */ +int32_t uCellTimeSetCallback(uDeviceHandle_t cellHandle, + void (*pCallback) (uDeviceHandle_t, + uCellTime_t *, + void *), + void *pCallbackParameter); + +/** Force the cellular module to synchronize to a specific cell + * of a specific MNO for CellTime purposes, for example using one + * of the cells returned by uCellNetDeepScan(); supported on + * SARA-R5xx-01B and later. When this has returned successfully + * uCellTimeEnable() may be called to perform HW timing based on + * that cell. + * + * By default a RACH is sent as part of the synchronisation; see + * #U_CELL_TIME_SYNC_MODE if you wish to change this. A guard + * timer of #U_CELL_TIME_SYNC_TIME_SECONDS is applied. + * + * No SIM is required/used, since there is no network connection, + * and hence there is no limitation as to network operator choice. + * + * Note that calling this function will de-register/disconnect the + * cellular module from the network WHATEVER the result, e.g. even + * if synchronisation fails; it is up to you to re-connect to + * the network by calling uCellNetConnect() afterwards. + * + * @param cellHandle the handle of the cellular instance. + * @param[in] pCell a pointer to the cell to synchronize + * with, cannot be NULL. + * @param[in,out] pTimingAdvance on entry this should point to the + * timing advance to use with the cell, + * -1 or a NULL pointer if the timing + * advance is not known. On return, if + * this pointer it not NULL it will be + * set to the timing advance that was + * employed with the cell, or -1 if the + * timing advance cannot be established, + * e.g. due to RACH failure. + * @return zero on success else negative error code. + */ +int32_t uCellTimeSyncCellEnable(uDeviceHandle_t cellHandle, + uCellNetCellInfo_t *pCell, + int32_t *pTimingAdvance); + +/** Disable synchronisation to a specific cell. This does NOT + * restore the module to a connected/registered state: please call + * uCellNetConnect() to do that. + * + * @param cellHandle the handle of the cellular instance. + * @return zero on success else negative error code. + */ +int32_t uCellTimeSyncCellDisable(uDeviceHandle_t cellHandle); + +/* ---------------------------------------------------------------- + * FUNCTIONS: ALIASES OF THE TIME-RELATED FUNCTIONS OF CFG AND INFO + * -------------------------------------------------------------- */ + +/** An alias of uCellInfoGetTimeUtc(); get the clock/calender UTC time. + * + * @param cellHandle the handle of the cellular instance. + * @return on success the Unix UTC time, else negative + * error code. + */ +int64_t uCellTimeGetUtc(uDeviceHandle_t cellHandle); + +/** An alias of uCellInfoGetTimeUtcStr(); get the clock/calender + * UTC time as a string. + * + * @param cellHandle the handle of the cellular instance. + * @param[out] pStr a pointer to size bytes of storage into which + * the UTC time string will be copied. + * Room should be allowed for a null terminator, + * which will be added to terminate the string. + * This pointer cannot be NULL. + * @param size the number of bytes available at pStr, including + * room for a null terminator. Must be greater or equal + * to 32 bytes. + * @return on success, the number of characters copied into + * pStr NOT include the terminator (as strlen() would + * return), on failure negative error code. + */ +int32_t uCellTimeGetUtcStr(uDeviceHandle_t cellHandle, + char *pStr, size_t size); + +/** An alias of uCellInfoGetTime(); get the clock/calender local time. + * + * @param cellHandle the handle of the cellular + * instance. + * @param[in] pTimeZoneSeconds a place to put the time-zone + * offset in seconds; may be NULL. + * @return on success the local time in + * seconds since midnight on 1st + * Jan 1970 (Unix time but local + * instead of UTC) else negative + * error code. + */ +int64_t uCellTimeGet(uDeviceHandle_t cellHandle, int32_t *pTimeZoneSeconds); + +/** An alias of uCellCfgSetTime(); set the clock/calender local time. + * + * @param cellHandle the handle of the cellular instance. + * @param timeLocal the local time in seconds since midnight on + * 1st Jan 1970, (Unix time, but local rather + * than UTC). + * @param timeZoneSeconds the time-zone offset of timeLocal in seconds; for + * example, if you are one hour ahead of UTC + * timeZoneSeconds would be 3600. + * @return zero on success or negative error code on failure. + */ +int64_t uCellTimeSet(uDeviceHandle_t cellHandle, int64_t timeLocal, + int32_t timeZoneSeconds); + +#ifdef __cplusplus +} +#endif + +/** @}*/ + +#endif // _U_CELL_TIME_H_ + +// End of file diff --git a/cell/src/u_cell.c b/cell/src/u_cell.c index 409d6d35..f2a7bd74 100644 --- a/cell/src/u_cell.c +++ b/cell/src/u_cell.c @@ -140,6 +140,8 @@ static void removeCellInstance(uCellPrivateInstance_t *pInstance) uCellPrivateHttpRemoveContext(pInstance); // Free any CMUX context uCellMuxPrivateRemoveContext(pInstance); + // Free any CellTime context + uCellPrivateCellTimeRemoveContext(pInstance); uDeviceDestroyInstance(U_DEVICE_INSTANCE(pInstance->cellHandle)); uPortFree(pInstance); pCurrent = NULL; diff --git a/cell/src/u_cell_cfg.c b/cell/src/u_cell_cfg.c index 8b44778b..d0aea266 100755 --- a/cell/src/u_cell_cfg.c +++ b/cell/src/u_cell_cfg.c @@ -903,7 +903,10 @@ static void GREETING_urc(uAtClientHandle_t atHandle, void *pParameter) pGreeting->cellHandle = pInstance->cellHandle; pGreeting->pCallback = pInstance->pGreetingCallback; pGreeting->pCallbackParameter = pInstance->pGreetingCallbackParameter; - uAtClientCallback(atHandle, greetingCallback, pGreeting); + if (uAtClientCallback(atHandle, greetingCallback, pGreeting) != 0) { + // Clean up on error + uPortFree(pGreeting); + } } } } diff --git a/cell/src/u_cell_info.c b/cell/src/u_cell_info.c index ca85bac1..1d2cf12c 100755 --- a/cell/src/u_cell_info.c +++ b/cell/src/u_cell_info.c @@ -75,46 +75,6 @@ * STATIC FUNCTIONS * -------------------------------------------------------------- */ -// Convert RSRP in 3GPP TS 36.133 format to dBm. -// Returns 0 if the number is not known. -// 0: -141 dBm or less, -// 1..96: from -140 dBm to -45 dBm with 1 dBm steps, -// 97: -44 dBm or greater, -// 255: not known or not detectable. -static int32_t rsrpToDbm(int32_t rsrp) -{ - int32_t rsrpDbm = 0; - - if ((rsrp >= 0) && (rsrp <= 97)) { - rsrpDbm = rsrp - (97 + 44); - if (rsrpDbm < -141) { - rsrpDbm = -141; - } - } - - return rsrpDbm; -} - -// Convert RSRQ in 3GPP TS 36.133 format to dB. -// Returns 0x7FFFFFFF if the number is not known. -// -30: less than -34 dB -// -29..46: from -34 dB to 2.5 dB with 0.5 dB steps -// where 0 is -19.5 dB -// 255: not known or not detectable. -static int32_t rsrqToDb(int32_t rsrq) -{ - int32_t rsrqDb = 0x7FFFFFFF; - - if ((rsrq >= -30) && (rsrq <= 46)) { - rsrqDb = (rsrq - 39) / 2; - if (rsrqDb < -34) { - rsrqDb = -34; - } - } - - return rsrqDb; -} - // Convert the UTRAN RSSI number in 3GPP TS 25.133 format to dBm. // Returns 0x7FFFFFFF if the number is not known. // 0: less than -100 dBm @@ -281,7 +241,7 @@ static int32_t getRadioParamsUcged2SaraR5(uAtClientHandle_t atHandle, // Skip , and uAtClientSkipParameters(atHandle, 3); // RSRP is element 11, coded as specified in TS 36.133 - pRadioParameters->rsrpDbm = rsrpToDbm(uAtClientReadInt(atHandle)); + pRadioParameters->rsrpDbm = uCellPrivateRsrpToDbm(uAtClientReadInt(atHandle)); // RSRQ is element 12, coded as specified in TS 36.133. x = uAtClientReadInt(atHandle); if (uAtClientErrorGet(atHandle) == 0) { @@ -289,7 +249,7 @@ static int32_t getRadioParamsUcged2SaraR5(uAtClientHandle_t atHandle, // we check for errors here so as not to mix up // what might be a negative error code with a // negative return value. - pRadioParameters->rsrqDb = rsrqToDb(x); + pRadioParameters->rsrqDb = uCellPrivateRsrqToDb(x); } // SINR is element 13, directly in dB, a decimal number // with a mantissa, 255 if unknown. diff --git a/cell/src/u_cell_loc.c b/cell/src/u_cell_loc.c index 8f3fd593..8b7d8f64 100644 --- a/cell/src/u_cell_loc.c +++ b/cell/src/u_cell_loc.c @@ -1163,9 +1163,6 @@ bool uCellLocGnssInsideCell(uDeviceHandle_t cellHandle) { uCellPrivateInstance_t *pInstance; bool isInside = false; - uAtClientHandle_t atHandle; - int32_t bytesRead; - char buffer[64]; // Enough for the ATI response if (gUCellPrivateMutex != NULL) { @@ -1173,25 +1170,7 @@ bool uCellLocGnssInsideCell(uDeviceHandle_t cellHandle) pInstance = pUCellPrivateGetInstance(cellHandle); if (pInstance != NULL) { - atHandle = pInstance->atHandle; - // Simplest way to check is to send ATI and see if - // it includes an "M8" or an "M10" - uAtClientLock(atHandle); - uAtClientCommandStart(atHandle, "ATI"); - uAtClientCommandStop(atHandle); - uAtClientResponseStart(atHandle, NULL); - bytesRead = uAtClientReadBytes(atHandle, buffer, - sizeof(buffer) - 1, false); - uAtClientResponseStop(atHandle); - if ((uAtClientUnlock(atHandle) == 0) && (bytesRead > 0)) { - // Add a terminator - buffer[bytesRead] = 0; - if (strstr(buffer, "M8") != NULL) { - isInside = true; - } else if (strstr(buffer, "M10") != NULL) { - isInside = true; - } - } + isInside = uCellPrivateGnssInsideCell(pInstance); } U_PORT_MUTEX_UNLOCK(gUCellPrivateMutex); diff --git a/cell/src/u_cell_net.c b/cell/src/u_cell_net.c index b0174bc5..02cefb68 100644 --- a/cell/src/u_cell_net.c +++ b/cell/src/u_cell_net.c @@ -35,6 +35,7 @@ #include "stdbool.h" #include "string.h" // memcpy(), memcmp(), strlen() #include "stdio.h" // snprintf() +#include "ctype.h" // isblank() #include "u_cfg_sw.h" #include "u_cfg_os_platform_specific.h" // For #define U_CFG_OS_CLIB_LEAKS @@ -737,31 +738,6 @@ static int32_t radioOff(uCellPrivateInstance_t *pInstance) return errorCode; } -// Perform an abort of an AT command. -static void abortCommand(const uCellPrivateInstance_t *pInstance) -{ - uAtClientHandle_t atHandle = pInstance->atHandle; - uAtClientDeviceError_t deviceError; - bool success = false; - - // Abort is done by sending anything, we use - // here just a space, after an AT command has - // been sent and before the response comes - // back. It is, however, possible for - // an abort to be ignored so we test for that - // and try a few times - for (size_t x = 3; (x > 0) && !success; x--) { - uAtClientLock(atHandle); - uAtClientCommandStart(atHandle, " "); - uAtClientCommandStopReadResponse(atHandle); - uAtClientDeviceErrorGet(atHandle, &deviceError); - // Check that a device error has been signalled, - // we've not simply timed out - success = (deviceError.type != U_AT_CLIENT_DEVICE_ERROR_TYPE_NO_ERROR); - uAtClientUnlock(atHandle); - } -} - // Prepare for connection with the network. static int32_t prepareConnect(uCellPrivateInstance_t *pInstance) { @@ -850,7 +826,7 @@ static int32_t setAutomaticMode(const uCellPrivateInstance_t *pInstance) U_AT_CLIENT_DEVICE_ERROR_TYPE_NO_ERROR)) { // If we never got an answer, abort the // command and check the status - abortCommand(pInstance); + uCellPrivateAbortAtCommand(pInstance); uAtClientLock(atHandle); uAtClientCommandStart(atHandle, "AT+COPS?"); uAtClientCommandStop(atHandle); @@ -1087,7 +1063,7 @@ static int32_t registerNetwork(uCellPrivateInstance_t *pInstance, // we timed out waiting for an answer: need to // abort the command for the module to start // listening to us again - abortCommand(pInstance); + uCellPrivateAbortAtCommand(pInstance); } // Let the registration outcome be decided // by the code block below, driven by the URCs @@ -1612,7 +1588,7 @@ static int32_t activateContextUpsd(const uCellPrivateInstance_t *pInstance, (deviceError.type == U_AT_CLIENT_DEVICE_ERROR_TYPE_NO_ERROR)) { // If we never got an answer, abort the // UPSDA command first. - abortCommand(pInstance); + uCellPrivateAbortAtCommand(pInstance); } } @@ -2015,6 +1991,69 @@ static int32_t getDnsStrUpsd(const uCellPrivateInstance_t *pInstance, return errorCode; } +// Move a pointer on if it is pointing to a blank. +static void stripBlanks(char **ppStr) +{ + while (isblank((int32_t) **ppStr)) { + (*ppStr)++; + } +} + +// Parse a line returned by AT+COPS=5. +// Returns 1 if a line is found, U_ERROR_COMMON_TIMEOUT otherwise. +static int32_t parseDeepScanLine(uAtClientHandle_t atHandle, + uCellNetCellInfo_t *pCell) +{ + int32_t errorCodeOrNumber = (int32_t) U_ERROR_COMMON_TIMEOUT; + int32_t numParameters = 0; + char buffer[32]; + char *pStr; + + // The line should contain something like + // MCC:222, MNC:88, TAC:562c, CI:57367043, DLF: 1325, ULF:19325, PCI:163, RSRP LEV:25, RSRQ LEV:1 + memset(pCell, 0, sizeof(*pCell)); + while ((errorCodeOrNumber == (int32_t) U_ERROR_COMMON_TIMEOUT) && + (uAtClientReadString(atHandle, buffer, sizeof(buffer), false) > 0)) { + pStr = buffer; + stripBlanks(&pStr); + if (strstr(pStr, "MCC:") == pStr) { + pCell->mcc = strtol(pStr + 4, NULL, 10); + numParameters++; + } else if (strstr(pStr, "MNC:") == pStr) { + pCell->mnc = strtol(pStr + 4, NULL, 10); + numParameters++; + } else if (strstr(pStr, "TAC:") == pStr) { + pCell->tac = strtol(pStr + 4, NULL, 16); + numParameters++; + } else if (strstr(pStr, "CI:") == pStr) { + pCell->cellIdLogical = strtol(pStr + 3, NULL, 10); + numParameters++; + } else if (strstr(pStr, "DLF:") == pStr) { + pCell->earfcnDownlink = strtol(pStr + 4, NULL, 10); + numParameters++; + } else if (strstr(pStr, "ULF:") == pStr) { + pCell->earfcnUplink = strtol(pStr + 4, NULL, 10); + numParameters++; + } else if (strstr(pStr, "PCI:") == pStr) { + pCell->cellIdPhysical = strtol(pStr + 4, NULL, 10); + numParameters++; + } else if (strstr(pStr, "RSRP LEV:") == pStr) { + pCell->rsrpDbm = uCellPrivateRsrpToDbm(strtol(pStr + 9, NULL, 10)); + numParameters++; + } else if (strstr(pStr, "RSRQ LEV:") == pStr) { + pCell->rsrqDb = uCellPrivateRsrqToDb(strtol(pStr + 9, NULL, 10)); + numParameters++; + } else if (strstr(pStr, "OK\r\n") != NULL) { + errorCodeOrNumber = (int32_t) U_ERROR_COMMON_SUCCESS; + } + if (numParameters == 9) { + errorCodeOrNumber = 1; + } + } + + return errorCodeOrNumber; +} + /* ---------------------------------------------------------------- * PUBLIC FUNCTIONS * -------------------------------------------------------------- */ @@ -2651,7 +2690,7 @@ int32_t uCellNetScanGetFirst(uDeviceHandle_t cellHandle, if (!gotAnswer) { // If we never got an answer, abort the // command first. - abortCommand(pInstance); + uCellPrivateAbortAtCommand(pInstance); } } @@ -2726,6 +2765,120 @@ void uCellNetScanGetLast(uDeviceHandle_t cellHandle) } } +// Do an extended network search. +int32_t uCellNetDeepScan(uDeviceHandle_t cellHandle, + bool (*pCallback) (uDeviceHandle_t, + uCellNetCellInfo_t *, + void *), + void *pCallbackParameter) +{ + int32_t errorCodeOrNumber = (int32_t) U_ERROR_COMMON_NOT_INITIALISED; + uCellPrivateInstance_t *pInstance; + uCellNetCellInfo_t cell; + uAtClientHandle_t atHandle; + uAtClientDeviceError_t deviceError; + bool keepGoing = true; + int32_t number; + int32_t cFunMode; + int32_t startTimeMs; + + if (gUCellPrivateMutex != NULL) { + + U_PORT_MUTEX_LOCK(gUCellPrivateMutex); + + errorCodeOrNumber = (int32_t) U_ERROR_COMMON_INVALID_PARAMETER; + pInstance = pUCellPrivateGetInstance(cellHandle); + if (pInstance != NULL) { + errorCodeOrNumber = (int32_t) U_ERROR_COMMON_NOT_SUPPORTED; + if (pInstance->pModule->moduleType == U_CELL_MODULE_TYPE_SARA_R5) { + // Make sure the radio is on for this + cFunMode = uCellPrivateCFunOne(pInstance); + atHandle = pInstance->atHandle; + // Do this three times: if the module + // is busy doing its own search when we ask it + // to do a network search, as it might be if + // we've just come out of airplane mode, + // it may return "Temporary Failure" + startTimeMs = uPortGetTickTimeMs(); + for (size_t x = U_CELL_NET_DEEP_SCAN_RETRIES + 1; + (x > 0) && (errorCodeOrNumber < 0) && keepGoing && + (uPortGetTickTimeMs() - startTimeMs < U_CELL_NET_DEEP_SCAN_TIME_SECONDS * 1000); + x--) { + number = 0; + errorCodeOrNumber = (int32_t) U_ERROR_COMMON_TIMEOUT; + uAtClientLock(atHandle); + // Set the timeout to a second so that we + // can spin around the loop and check + // pKeepGoingCallback while waiting + uAtClientTimeoutSet(atHandle, 1000); + uAtClientCommandStart(atHandle, "AT+COPS=5"); + uAtClientCommandStop(atHandle); + // Will get back a set of lines of the form: + // MCC:222, MNC:88, TAC:562c, CI:57367043, DLF: 1325, ULF:19325, PCI:163, RSRP LEV:25, RSRQ LEV:1 + // These lines are "dribbled" out, one by one, and we + // want to be able to stop the command part way through, + // hence the AT handling code below is more complex than usual. + while ((errorCodeOrNumber == (int32_t) U_ERROR_COMMON_TIMEOUT) && keepGoing && + (uPortGetTickTimeMs() - startTimeMs < U_CELL_NET_DEEP_SCAN_TIME_SECONDS * 1000)) { + if (uAtClientResponseStart(atHandle, NULL) == 0) { + // See if we have a line + errorCodeOrNumber = parseDeepScanLine(atHandle, &cell); + if (errorCodeOrNumber > 0) { + // Got something, call the callback + number += errorCodeOrNumber; + if (pCallback != NULL) { + keepGoing = pCallback(cellHandle, &cell, pCallbackParameter); + } + // Wait for more + errorCodeOrNumber = (int32_t) U_ERROR_COMMON_TIMEOUT; + uPortTaskBlock(1000); + } + } else { + // Either there was nothing (a timeout) or there was a "+CME ERROR" + // message or there was an "OK" + errorCodeOrNumber = uAtClientErrorGet(atHandle); + if (errorCodeOrNumber != (int32_t) U_ERROR_COMMON_SUCCESS) { + // It was either a "CME ERROR" or a timeout; determine which + uAtClientDeviceErrorGet(atHandle, &deviceError); + if (deviceError.type == U_AT_CLIENT_DEVICE_ERROR_TYPE_NO_ERROR) { + // Not a "+CME ERROR", must have been a timeout, call the + // callback with NULL to see if we should continue + if (pCallback != NULL) { + keepGoing = pCallback(cellHandle, NULL, pCallbackParameter); + } + errorCodeOrNumber = (int32_t) U_ERROR_COMMON_TIMEOUT; + uAtClientClearError(atHandle); + if (keepGoing) { + uPortTaskBlock(1000); + } + } + } + } + } + uAtClientResponseStop(atHandle); + uAtClientUnlock(atHandle); + if (errorCodeOrNumber == (int32_t) U_ERROR_COMMON_SUCCESS) { + // If we got a complete response, accept the answer + errorCodeOrNumber = number; + } else if (errorCodeOrNumber == (int32_t) U_ERROR_COMMON_TIMEOUT) { + // Abort the command first to avoid it being caught + // up in any future command sequence + uCellPrivateAbortAtCommand(pInstance); + } + } + // Put the AT+CFUN back if it was not already 1 + if ((cFunMode >= 0) && (cFunMode != 1)) { + uCellPrivateCFunMode(pInstance, cFunMode); + } + } + } + + U_PORT_MUTEX_UNLOCK(gUCellPrivateMutex); + } + + return errorCodeOrNumber; +} + // Enable or disable the registration status call-back. int32_t uCellNetSetRegistrationStatusCallback(uDeviceHandle_t cellHandle, void (*pCallback) (uCellNetRegDomain_t, diff --git a/cell/src/u_cell_private.c b/cell/src/u_cell_private.c index 2fd21820..10494b00 100644 --- a/cell/src/u_cell_private.c +++ b/cell/src/u_cell_private.c @@ -59,8 +59,10 @@ #include "u_cell_pwr.h" #include "u_cell_cfg.h" #include "u_cell_http.h" +#include "u_cell_time.h" #include "u_cell_http_private.h" #include "u_cell_pwr_private.h" +#include "u_cell_time_private.h" /* ---------------------------------------------------------------- * COMPILE-TIME MACROS @@ -606,6 +608,36 @@ uCellPrivateInstance_t *pUCellPrivateGetInstance(uDeviceHandle_t cellHandle) return pInstance; } +// Convert RSRP in 3GPP TS 36.133 format to dBm. +int32_t uCellPrivateRsrpToDbm(int32_t rsrp) +{ + int32_t rsrpDbm = 0; + + if ((rsrp >= 0) && (rsrp <= 97)) { + rsrpDbm = rsrp - (97 + 44); + if (rsrpDbm < -141) { + rsrpDbm = -141; + } + } + + return rsrpDbm; +} + +// Convert RSRQ in 3GPP TS 36.133 format to dB. +int32_t uCellPrivateRsrqToDb(int32_t rsrq) +{ + int32_t rsrqDb = 0x7FFFFFFF; + + if ((rsrq >= -30) && (rsrq <= 46)) { + rsrqDb = (rsrq - 39) / 2; + if (rsrqDb < -34) { + rsrqDb = -34; + } + } + + return rsrqDb; +} + // Set the radio parameters back to defaults. void uCellPrivateClearRadioParameters(uCellPrivateRadioParameters_t *pParameters) { @@ -1416,4 +1448,84 @@ int32_t uCellPrivateGetGnssProfile(const uCellPrivateInstance_t *pInstance, return errorCodeOrBitMap; } +// Check whether there is a GNSS chip on-board the cellular module. +bool uCellPrivateGnssInsideCell(const uCellPrivateInstance_t *pInstance) +{ + bool isInside = false; + uAtClientHandle_t atHandle; + int32_t bytesRead; + char buffer[64]; // Enough for the ATI response + + if (pInstance != NULL) { + atHandle = pInstance->atHandle; + // Simplest way to check is to send ATI and see if + // it includes an "M8" or an "M10" + uAtClientLock(atHandle); + uAtClientCommandStart(atHandle, "ATI"); + uAtClientCommandStop(atHandle); + uAtClientResponseStart(atHandle, NULL); + bytesRead = uAtClientReadBytes(atHandle, buffer, + sizeof(buffer) - 1, false); + uAtClientResponseStop(atHandle); + if ((uAtClientUnlock(atHandle) == 0) && (bytesRead > 0)) { + // Add a terminator + buffer[bytesRead] = 0; + if (strstr(buffer, "M8") != NULL) { + isInside = true; + } else if (strstr(buffer, "M10") != NULL) { + isInside = true; + } + } + } + + return isInside; +} + +// Remove the CellTime context for the given instance. +void uCellPrivateCellTimeRemoveContext(uCellPrivateInstance_t *pInstance) +{ + uCellTimePrivateContext_t *pContext; + + if (pInstance != NULL) { + pContext = (uCellTimePrivateContext_t *) pInstance->pCellTimeContext; + if (pContext != NULL) { + if (pContext->pCallbackEvent != NULL) { + uAtClientRemoveUrcHandler(pInstance->atHandle, "+UUTIMEIND:"); + } + if (pContext->pCallbackTime != NULL) { + uAtClientRemoveUrcHandler(pInstance->atHandle, "+UTIME:"); + } + uPortFree(pInstance->pCellTimeContext); + pInstance->pCellTimeContext = NULL; + } + uPortFree(pInstance->pCellTimeCellSyncContext); + pInstance->pCellTimeCellSyncContext = NULL; + } +} + +// Perform an abort of an AT command. +void uCellPrivateAbortAtCommand(const uCellPrivateInstance_t *pInstance) +{ + uAtClientHandle_t atHandle = pInstance->atHandle; + uAtClientDeviceError_t deviceError; + bool success = false; + + // Abort is done by sending anything, we use + // here just a space, after an AT command has + // been sent and before the response comes + // back. It is, however, possible for + // an abort to be ignored so we test for that + // and try a few times + for (size_t x = 3; (x > 0) && !success; x--) { + uAtClientLock(atHandle); + uAtClientCommandStart(atHandle, " "); + uAtClientCommandStopReadResponse(atHandle); + uAtClientDeviceErrorGet(atHandle, &deviceError); + // Check that a device error has been signalled, + // we've not simply timed out + success = (deviceError.type != U_AT_CLIENT_DEVICE_ERROR_TYPE_NO_ERROR); + uAtClientUnlock(atHandle); + } +} + // End of file diff --git a/cell/src/u_cell_private.h b/cell/src/u_cell_private.h index 1e8b366b..c205946b 100644 --- a/cell/src/u_cell_private.h +++ b/cell/src/u_cell_private.h @@ -452,6 +452,8 @@ typedef struct uCellPrivateInstance_t { void *pHttpContext; /**< Hook for a HTTP context. */ void *pMuxContext; /**< CMUX context, lodged here as a void * to avoid spreading its types all over. */ + void *pCellTimeContext; /**< Hook for CellTime context. */ + void *pCellTimeCellSyncContext; /**< Hook for CellTime cell synchronisation context. */ struct uCellPrivateInstance_t *pNext; } uCellPrivateInstance_t; @@ -480,6 +482,15 @@ extern uPortMutexHandle_t gUCellPrivateMutex; * FUNCTIONS * -------------------------------------------------------------- */ +/** Abort an AT command; only works if the AT command is actually + * an abortable one according to the AT manual. + * + * Note: gUCellPrivateMutex should be locked before this is called. + * + * @param pInstance a pointer to the instance. + */ +void uCellPrivateAbortAtCommand(const uCellPrivateInstance_t *pInstance); + /** Return true if the given buffer contains only numeric * characters (0 to 9). * @@ -499,6 +510,32 @@ bool uCellPrivateIsNumeric(const char *pBuffer, size_t bufferSize); */ uCellPrivateInstance_t *pUCellPrivateGetInstance(uDeviceHandle_t cellHandle); +/** Convert RSRP in 3GPP TS 36.133 format to dBm. + * + * Returns 0 if the number is not known. + * 0: -141 dBm or less, + * 1..96: from -140 dBm to -45 dBm with 1 dBm steps, + * 97: -44 dBm or greater, + * 255: not known or not detectable. + * + * @param rsrp the RSRP in 3GPP TS 36.133 units. + * @return the RSRP in dBm. + */ +int32_t uCellPrivateRsrpToDbm(int32_t rsrp); + +/** Convert RSRQ in 3GPP TS 36.133 format to dB. + * + * Returns 0x7FFFFFFF if the number is not known. + * -30: less than -34 dB + * -29..46: from -34 dB to 2.5 dB with 0.5 dB steps + * where 0 is -19.5 dB + * 255: not known or not detectable. + * + * @param rsrq the RSRP in 3GPP TS 36.133 units. + * @return the RSRP in dBm. + */ +int32_t uCellPrivateRsrqToDb(int32_t rsrq); + /** Set the radio parameters back to defaults. * * @param pParameters pointer to a radio parameters structure. @@ -877,6 +914,22 @@ int32_t uCellPrivateSetGnssProfile(const uCellPrivateInstance_t *pInstance, int32_t uCellPrivateGetGnssProfile(const uCellPrivateInstance_t *pInstance, char *pServerName, size_t sizeBytes); +/** Check whether there is a GNSS chip on-board the cellular module. + * + * @param pInstance a pointer to the cellular instance. + * @return true if there is a GNSS chip inside the cellular + * module, else false. + */ +bool uCellPrivateGnssInsideCell(const uCellPrivateInstance_t *pInstance); + +/** Remove the CellTime context for the given instance. + * + * Note: gUCellPrivateMutex should be locked before this is called. + * + * @param pInstance a pointer to the cellular instance. + */ +void uCellPrivateCellTimeRemoveContext(uCellPrivateInstance_t *pInstance); + #ifdef __cplusplus } #endif diff --git a/cell/src/u_cell_time.c b/cell/src/u_cell_time.c new file mode 100644 index 00000000..de7bd13e --- /dev/null +++ b/cell/src/u_cell_time.c @@ -0,0 +1,843 @@ +/* + * Copyright 2019-2023 u-blox + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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. + */ + +/* Only #includes of u_* and the C standard library are allowed here, + * no platform stuff and no OS stuff. Anything required from + * the platform/OS must be brought in through u_port* to maintain + * portability. + */ + +/** @file + * @brief Implementation of the CellTime API for cellular. + */ + +#ifdef U_CFG_OVERRIDE +# include "u_cfg_override.h" // For a customer's configuration override +#endif + +#include "limits.h" // INT_MIN +#include "stdlib.h" // strtol() +#include "stddef.h" // NULL, size_t etc. +#include "stdint.h" // int32_t etc. +#include "stdio.h" // snprintf() +#include "stdbool.h" +#include "string.h" // memcpy(), strstr() +#include "ctype.h" // isdigit() + +#include "u_cfg_sw.h" +#include "u_error_common.h" + +#include "u_port_clib_platform_specific.h" /* snprintf(), must be included + before the other port files. */ +#include "u_port.h" +#include "u_port_debug.h" +#include "u_port_os.h" +#include "u_port_heap.h" +#include "u_port_uart.h" + +#include "u_time.h" + +#include "u_at_client.h" + +#include "u_device_shared.h" + +#include "u_cell_module_type.h" +#include "u_cell_file.h" +#include "u_cell.h" // Order is +#include "u_cell_net.h" // important here +#include "u_cell_private.h" // don't change it +#include "u_cell_info.h" +#include "u_cell_cfg.h" +#include "u_cell_time.h" +#include "u_cell_time_private.h" + +/* ---------------------------------------------------------------- + * COMPILE-TIME MACROS + * -------------------------------------------------------------- */ + +/* ---------------------------------------------------------------- + * TYPES + * -------------------------------------------------------------- */ + +/** All the parameters for the event callback. + */ +typedef struct { + uDeviceHandle_t cellHandle; + void (*pCallback) (uDeviceHandle_t, uCellTimeEvent_t *, void *); + void *pCallbackParameter; + uCellTimeEvent_t event; + int32_t cellIdPhysicalFromCellSync; +} uCellTimeEventData_t; + +/** All the parameters for the time callback. + */ +typedef struct { + uDeviceHandle_t cellHandle; + void (*pCallback) (uDeviceHandle_t, uCellTime_t *, void *); + void *pCallbackParameter; + uCellTime_t time; +} uCellTimeTimeData_t; + +/* ---------------------------------------------------------------- + * VARIABLES + * -------------------------------------------------------------- */ + +/** Map the result code received in a +UUTIMECELLSELECT URC to one + * of our error codes. + */ +static const int32_t gSyncResultToErrorCode[] = { + (int32_t) U_ERROR_COMMON_CANCELLED, // 0: synchronisation disabled, cell released + (int32_t) U_ERROR_COMMON_SUCCESS, // 1: synchronization enabled and successful, camped on the requested cell, TA is available + (int32_t) U_ERROR_COMMON_NOT_FOUND, // 2: synchronization enabled and unsuccessful, the requested cell was not found + (int32_t) U_CELL_ERROR_CONNECTED, // 3: cellular functionality not switched off, the synchronization cannot be enabled or disabled + (int32_t) U_ERROR_COMMON_SUCCESS, // 4: RACH failure: synchronization enabled and successful, camped on the requested cell but TA is not available + (int32_t) U_ERROR_COMMON_UNKNOWN // 5: generic error (e.g. release configuration failure) +}; + +/* ---------------------------------------------------------------- + * STATIC FUNCTIONS + * -------------------------------------------------------------- */ + +// Set a cellular module GPIO pin to a given function. +static int32_t gpioConfig(uAtClientHandle_t atHandle, int32_t gpioId, + int32_t function) +{ + uAtClientLock(atHandle); + uAtClientCommandStart(atHandle, "AT+UGPIOC="); + uAtClientWriteInt(atHandle, gpioId); + uAtClientWriteInt(atHandle, function); + uAtClientCommandStopReadResponse(atHandle); + return uAtClientUnlock(atHandle); +} + +// Convert a string of the form "0123456789.0123456789", representing +// a number with N fractional digits, into a number times 1,000,000,000 +static int64_t numberX1e9(const char *pNumber, size_t fractionalDigits) +{ + int64_t x1e9 = 0; + int32_t x = 0; + int64_t y; + int32_t z = 9; + + // Find out how many digits there are before the first + // non-digit thing (which might be a decimal point). + while (isdigit((int32_t) *(pNumber + x))) { // *NOPAD* stop AStyle making * look like a multiply + x++; + } + + // Now read those digits and accumulate them into x1e9 + while (x > 0) { + y = *pNumber - '0'; + for (int32_t z = 1; z < x; z++) { + y *= 10; + } + x1e9 += y; + x--; + pNumber++; + } + + x1e9 *= 1000000000; + + if (*pNumber == '.') { + // If we're now at a decimal point, skip over it and + // deal with the fractional part of up to fractionalDigits + pNumber++; + x = fractionalDigits; + while (isdigit((int32_t) *pNumber) && (x > 0)) { // *NOPAD* + y = *pNumber - '0'; + for (int32_t w = 1; w < z; w++) { + y *= 10; + } + x1e9 += y; + x--; + z--; + pNumber++; + } + } + + return x1e9; +} + +// Callback via which the user's event callback is called. +// This must be called through the uAtClientCallback() mechanism +// in order to prevent customer code blocking the AT client. +static void eventCallback(uAtClientHandle_t atHandle, void *pParameter) +{ + uCellTimeEventData_t *pEventData = (uCellTimeEventData_t *) pParameter; + + if (pEventData != NULL) { + if (pEventData->pCallback != NULL) { + if (pEventData->event.source == U_CELL_TIME_SOURCE_CELL) { + // Need to populate the cellIdPhysical field, first try using + // AT+CELLINFO, which goes as follows: + // +UCELLINFO: ,,,,,,,,,,,,,,, + uAtClientLock(atHandle); + uAtClientCommandStart(atHandle, "AT+UCELLINFO?"); + uAtClientCommandStop(atHandle); + uAtClientResponseStart(atHandle, "+UCELLINFO:"); + // Skip , , , and + uAtClientSkipParameters(atHandle, 5); + // Read + pEventData->event.cellIdPhysical = uAtClientReadInt(atHandle); + if (pEventData->event.cellIdPhysical == 0xFFFF) { + // The physical cell ID is not known, use one we might + // have saved from forcing sync + pEventData->event.cellIdPhysical = pEventData->cellIdPhysicalFromCellSync; + } + uAtClientResponseStop(atHandle); + uAtClientUnlock(atHandle); + } + pEventData->pCallback(pEventData->cellHandle, + &(pEventData->event), + pEventData->pCallbackParameter); + } + uPortFree(pEventData); + } +} + +// URC handler for +UUTIMEIND. +static void UUTIMEIND_urc(uAtClientHandle_t atHandle, void *pParam) +{ + uCellPrivateInstance_t *pInstance = (uCellPrivateInstance_t *) pParam; + uCellTimePrivateContext_t *pCellTimeContext; + uCellTimeCellSyncPrivateContext_t *pCellTimeCellSyncContext; + uCellTimeEventData_t *pEventData; + uCellTimeEvent_t event = {0}; + int32_t x; + + if (pInstance != NULL) { + pCellTimeContext = (uCellTimePrivateContext_t *) pInstance->pCellTimeContext; + if (pCellTimeContext != NULL) { + event.cellIdPhysical = -1; // This is populated later + // Format is +UUTIMEIND: ,,,[,,] + // Read it all into a local structure + event.mode = uAtClientReadInt(atHandle); + event.source = uAtClientReadInt(atHandle); + x = uAtClientReadInt(atHandle); + if (x == 0) { + event.cellTime = true; + } + event.result = uAtClientReadInt(atHandle); + if ((event.result == U_CELL_TIME_RESULT_UTC_ALIGNMENT) || + (event.result == U_CELL_TIME_RESULT_OFFSET_DETECTED)) { + event.offsetNanoseconds = uAtClientReadInt(atHandle); + if (event.offsetNanoseconds >= 0) { + x = uAtClientReadInt(atHandle); + if (x > 0) { + event.offsetNanoseconds += ((int64_t) x) * 1000000000; + } + } + } + if ((event.source != U_CELL_TIME_SOURCE_INIT) && + ((event.result == U_CELL_TIME_RESULT_SUCCESS) || + (event.result == U_CELL_TIME_RESULT_UTC_ALIGNMENT) || + (event.result == U_CELL_TIME_RESULT_OFFSET_DETECTED))) { + // If we not initialisting and the result is not an error case, + // we are synchronised + event.synchronised = true; + } + if ((event.mode >= 0) && (event.source >= 0) && (event.result >= 0) && + (pCellTimeContext->pCallbackEvent != NULL)) { + // Put the data for the callback into a struct to our + // local callback via the AT client's callback mechanism + // to decouple it from any URC handler. + // Note: it is up to eventCallback() to free the allocated memory. + pEventData = (uCellTimeEventData_t *) pUPortMalloc(sizeof(uCellTimeEventData_t)); + if (pEventData != NULL) { + memcpy(&(pEventData->event), &event, sizeof(pEventData->event)); + // We can't always get the physical cell ID of the cell + // we are syncrhonised to by asking the module so pass on the + // physical cell ID that we might have forced synchronisation + // to for it to use in that case + pEventData->cellIdPhysicalFromCellSync = -1; + pCellTimeCellSyncContext = (uCellTimeCellSyncPrivateContext_t *) + pInstance->pCellTimeCellSyncContext; + if (pCellTimeCellSyncContext != NULL) { + pEventData->cellIdPhysicalFromCellSync = pCellTimeCellSyncContext->cellIdPhysical; + } + pEventData->cellHandle = pInstance->cellHandle; + pEventData->pCallback = pCellTimeContext->pCallbackEvent; + pEventData->pCallbackParameter = pCellTimeContext->pCallbackEventParam; + if (uAtClientCallback(atHandle, eventCallback, pEventData) != 0) { + // Clean up on error + uPortFree(pEventData); + } + } + } + } + } +} + +// Callback via which the user's time callback is called. +// This must be called through the uAtClientCallback() mechanism +// in order to prevent customer code blocking the AT client. +static void timeCallback(uAtClientHandle_t atHandle, void *pParameter) +{ + uCellTimeTimeData_t *pTimeData = (uCellTimeTimeData_t *) pParameter; + + (void) atHandle; + + if (pTimeData != NULL) { + if (pTimeData->pCallback != NULL) { + pTimeData->pCallback(pTimeData->cellHandle, + &(pTimeData->time), + pTimeData->pCallbackParameter); + } + uPortFree(pTimeData); + } +} + +// URC handler for +UUTIME. +static void UUTIME_urc(uAtClientHandle_t atHandle, void *pParam) +{ + uCellPrivateInstance_t *pInstance = (uCellPrivateInstance_t *) pParam; + uCellTimePrivateContext_t *pContext; + uCellTimeTimeData_t *pTimeData; + uCellTime_t time = {0}; + char buffer[25]; // Enough room for "012345678.012345678" ir 22/08/2020" or "11:22:33", plus a terminator + int32_t length; + size_t offset; + int32_t numParameters = 0; + int32_t months; + int32_t x; + int64_t timeSeconds = 0; + + if (pInstance != NULL) { + pContext = (uCellTimePrivateContext_t *) pInstance->pCellTimeContext; + if (pContext != NULL) { + // Format is +UUTIME: ,