blob: 23b907ac381db1567c1cf8136061690a023a18ad [file] [log] [blame]
Neels Hofmeyr086bd332020-09-18 18:00:50 +02001/* 3GPP TS 23.032 GAD: Universal Geographical Area Description */
2/*
3 * (C) 2020 by sysmocom - s.f.m.c. GmbH <info@sysmocom.de>
4 *
5 * All Rights Reserved
6 *
7 * Author: Neels Hofmeyr <neels@hofmeyr.de>
8 *
9 * This program is free software; you can redistribute it and/or modify
10 * it under the terms of the GNU Affero General Public License as published by
11 * the Free Software Foundation; either version 3 of the License, or
12 * (at your option) any later version.
13 *
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU Affero General Public License for more details.
18 *
19 * You should have received a copy of the GNU Affero General Public License
20 * along with this program. If not, see <http://www.gnu.org/licenses/>.
21 *
22 */
23
24#include <errno.h>
25#include <inttypes.h>
26#include <osmocom/core/msgb.h>
27#include <osmocom/core/utils.h>
28#include <osmocom/gsm/gad.h>
29
30/*! \addtogroup gad
31 * @{
32 * \file gad.c
33 * Message encoding and decoding for 3GPP TS 23.032 GAD: Universal Geographical Area Description.
34 */
35
36const struct value_string osmo_gad_type_names[] = {
37 { GAD_TYPE_ELL_POINT, "Ellipsoid-point" },
38 { GAD_TYPE_ELL_POINT_UNC_CIRCLE, "Ellipsoid-point-with-uncertainty-circle" },
39 { GAD_TYPE_ELL_POINT_UNC_ELLIPSE, "Ellipsoid-point-with-uncertainty-ellipse" },
40 { GAD_TYPE_POLYGON, "Polygon" },
41 { GAD_TYPE_ELL_POINT_ALT, "Ellipsoid-point-with-altitude" },
42 { GAD_TYPE_ELL_POINT_ALT_UNC_ELL, "Ellipsoid-point-with-altitude-and-uncertainty-ellipsoid" },
43 { GAD_TYPE_ELL_ARC, "Ellipsoid-arc" },
44 { GAD_TYPE_HA_ELL_POINT_UNC_ELLIPSE, "High-accuracy-ellipsoid-point-with-uncertainty-ellipse" },
45 { GAD_TYPE_HA_ELL_POINT_ALT_UNC_ELL, "High-accuracy-ellipsoid-point-with-altitude-and-uncertainty-ellipsoid" },
46 {}
47};
48
49/*! Encode a latitude value according to 3GPP TS 23.032.
50 * Useful to clamp a latitude to an actually encodable accuracy:
51 * set_lat = osmo_gad_dec_lat(osmo_gad_enc_lat(orig_lat));
52 * \param[in] deg_1e6 Latitude in micro degrees (degrees * 1e6), -90'000'000 (S) .. 90'000'000 (N).
53 * \returns encoded latitude in host-byte-order (24bit).
54 */
55uint32_t osmo_gad_enc_lat(int32_t deg_1e6)
56{
57 /* N <= ((2**23)/90)*X < N+1
58 * N: encoded latitude
59 * X: latitude in degrees
60 */
61 int32_t sign = 0;
62 int64_t x;
63 deg_1e6 = OSMO_MAX(-90000000, OSMO_MIN(90000000, deg_1e6));
64 if (deg_1e6 < 0) {
65 sign = 1 << 23;
66 deg_1e6 = -deg_1e6;
67 }
68 x = deg_1e6;
69 x <<= 23;
70 x += (1 << 23) - 1;
71 x /= 90 * 1000000;
72 return sign | (x & 0x7fffff);
73}
74
75/*! Decode a latitude value according to 3GPP TS 23.032.
76 * Useful to clamp a latitude to an actually encodable accuracy:
77 * set_lat = osmo_gad_dec_lat(osmo_gad_enc_lat(orig_lat));
78 * \param[in] lat encoded latitude in host-byte-order (24bit).
79 * \returns decoded latitude in micro degrees (degrees * 1e6), -90'000'000 (S) .. 90'000'000 (N).
80 */
81int32_t osmo_gad_dec_lat(uint32_t lat)
82{
83 int64_t sign = 1;
84 int64_t x;
85 if (lat & 0x800000) {
86 sign = -1;
87 lat &= 0x7fffff;
88 }
89 x = lat;
90 x *= 90 * 1000000;
91 x >>= 23;
92 x *= sign;
93 return x;
94}
95
96/*! Encode a longitude value according to 3GPP TS 23.032.
97 * Useful to clamp a longitude to an actually encodable accuracy:
98 * set_lon = osmo_gad_dec_lon(osmo_gad_enc_lon(orig_lon));
99 * \param[in] deg_1e6 Longitude in micro degrees (degrees * 1e6), -180'000'000 (W) .. 180'000'000 (E).
100 * \returns encoded longitude in host-byte-order (24bit).
101 */
102uint32_t osmo_gad_enc_lon(int32_t deg_1e6)
103{
104 /* -180 .. 180 degrees mapped to a signed 24 bit integer.
105 * N <= ((2**24)/360) * X < N+1
106 * N: encoded longitude
107 * X: longitude in degrees
108 */
109 int64_t x;
110 deg_1e6 = OSMO_MAX(-180000000, OSMO_MIN(180000000, deg_1e6));
111 x = deg_1e6;
112 x *= (1 << 24);
113 if (deg_1e6 >= 0)
114 x += (1 << 24) - 1;
115 else
116 x -= (1 << 24) - 1;
117 x /= 360 * 1000000;
118 return (uint32_t)(x & 0xffffff);
119}
120
121/*! Decode a longitude value according to 3GPP TS 23.032.
122 * Normally, encoding and decoding is done via osmo_gad_enc() and osmo_gad_dec() for entire PDUs. But calling this
123 * directly can be useful to clamp a longitude to an actually encodable accuracy:
124 * int32_t set_lon = osmo_gad_dec_lon(osmo_gad_enc_lon(orig_lon));
125 * \param[in] lon Encoded longitude.
126 * \returns Longitude in micro degrees (degrees * 1e6), -180'000'000 (W) .. 180'000'000 (E).
127 */
128int32_t osmo_gad_dec_lon(uint32_t lon)
129{
130 /* -180 .. 180 degrees mapped to a signed 24 bit integer.
131 * N <= ((2**24)/360) * X < N+1
132 * N: encoded longitude
133 * X: longitude in degrees
134 */
135 int32_t slon;
136 int64_t x;
137 if (lon & 0x800000) {
138 /* make the 24bit negative number to a 32bit negative number */
139 slon = lon | 0xff000000;
140 } else {
141 slon = lon;
142 }
143 x = slon;
144 x *= 360 * 1000000;
145 x /= (1 << 24);
146 return x;
147}
148
149/*
150 * r = C((1+x)**K - 1)
151 * C = 10, x = 0.1
152 *
153 * def r(k):
154 * return 10.*(((1+0.1)**k) -1 )
155 * for k in range(128):
156 * print('%d,' % (r(k) * 1000.))
157 */
158static uint32_t table_uncertainty_1e3[128] = {
159 0, 1000, 2100, 3310, 4641, 6105, 7715, 9487, 11435, 13579, 15937, 18531, 21384, 24522, 27974, 31772, 35949,
160 40544, 45599, 51159, 57274, 64002, 71402, 79543, 88497, 98347, 109181, 121099, 134209, 148630, 164494, 181943,
161 201137, 222251, 245476, 271024, 299126, 330039, 364043, 401447, 442592, 487851, 537636, 592400, 652640, 718904,
162 791795, 871974, 960172, 1057189, 1163908, 1281299, 1410429, 1552472, 1708719, 1880591, 2069650, 2277615,
163 2506377, 2758014, 3034816, 3339298, 3674227, 4042650, 4447915, 4893707, 5384077, 5923485, 6516834, 7169517,
164 7887469, 8677216, 9545938, 10501531, 11552685, 12708953, 13980849, 15379933, 16918927, 18611820, 20474002,
165 22522402, 24775642, 27254206, 29980627, 32979690, 36278659, 39907525, 43899277, 48290205, 53120226, 58433248,
166 64277573, 70706330, 77777964, 85556760, 94113436, 103525780, 113879358, 125268293, 137796123, 151576735,
167 166735409, 183409950, 201751945, 221928139, 244121953, 268535149, 295389664, 324929630, 357423593, 393166952,
168 432484648, 475734112, 523308524, 575640376, 633205414, 696526955, 766180651, 842799716, 927080688, 1019789756,
169 1121769732, 1233947705, 1357343476, 1493078824, 1642387706, 1806627477,
170};
171
172/*! Decode an uncertainty circle value according to 3GPP TS 23.032.
173 * Useful to clamp a value to an actually encodable accuracy:
174 * set_unc = osmo_gad_dec_unc(osmo_gad_enc_unc(orig_unc));
175 * \param[in] unc Encoded uncertainty value.
176 * \returns Uncertainty value in millimeters.
177 */
178uint32_t osmo_gad_dec_unc(uint8_t unc)
179{
180 return table_uncertainty_1e3[unc & 0x7f];
181}
182
183/*! Encode an uncertainty circle value according to 3GPP TS 23.032.
184 * Normally, encoding and decoding is done via osmo_gad_enc() and osmo_gad_dec() for entire PDUs. But calling this
185 * directly can be useful to clamp a value to an actually encodable accuracy:
186 * uint32_t set_unc = osmo_gad_dec_unc(osmo_gad_enc_unc(orig_unc));
187 * \param[in] mm Uncertainty value in millimeters.
188 * \returns Encoded uncertainty value.
189 */
190uint8_t osmo_gad_enc_unc(uint32_t mm)
191{
192 uint8_t unc;
193 for (unc = 0; unc < ARRAY_SIZE(table_uncertainty_1e3); unc++) {
194 if (table_uncertainty_1e3[unc] > mm)
195 return unc - 1;
196 }
197 return 127;
198}
199
200/* So far we don't encode a high-accuracy uncertainty anywhere, so these static items would flag as compiler warnings
201 * for unused items. As soon as any HA items get used, remove this ifdef. */
202#ifdef GAD_FUTURE
203
204/*
205 * r = C((1+x)**K - 1)
206 * C = 0.3, x = 0.02
207 *
208 * def r(k):
209 * return 0.3*(((1+0.02)**k) -1 )
210 * for k in range(256):
211 * print('%d,' % (r(k) * 1000.))
212 */
213static uint32_t table_ha_uncertainty_1e3[256] = {
214 0, 6, 12, 18, 24, 31, 37, 44, 51, 58, 65, 73, 80, 88, 95, 103, 111, 120, 128, 137, 145, 154, 163, 173, 182, 192,
215 202, 212, 222, 232, 243, 254, 265, 276, 288, 299, 311, 324, 336, 349, 362, 375, 389, 402, 417, 431, 445, 460,
216 476, 491, 507, 523, 540, 556, 574, 591, 609, 627, 646, 665, 684, 703, 724, 744, 765, 786, 808, 830, 853, 876,
217 899, 923, 948, 973, 998, 1024, 1051, 1078, 1105, 1133, 1162, 1191, 1221, 1252, 1283, 1314, 1347, 1380, 1413,
218 1447, 1482, 1518, 1554, 1592, 1629, 1668, 1707, 1748, 1788, 1830, 1873, 1916, 1961, 2006, 2052, 2099, 2147,
219 2196, 2246, 2297, 2349, 2402, 2456, 2511, 2567, 2625, 2683, 2743, 2804, 2866, 2929, 2994, 3060, 3127, 3195,
220 3265, 3336, 3409, 3483, 3559, 3636, 3715, 3795, 3877, 3961, 4046, 4133, 4222, 4312, 4404, 4498, 4594, 4692,
221 4792, 4894, 4998, 5104, 5212, 5322, 5435, 5549, 5666, 5786, 5907, 6032, 6158, 6287, 6419, 6554, 6691, 6830,
222 6973, 7119, 7267, 7418, 7573, 7730, 7891, 8055, 8222, 8392, 8566, 8743, 8924, 9109, 9297, 9489, 9685, 9884,
223 10088, 10296, 10508, 10724, 10944, 11169, 11399, 11633, 11871, 12115, 12363, 12616, 12875, 13138, 13407, 13681,
224 13961, 14246, 14537, 14834, 15136, 15445, 15760, 16081, 16409, 16743, 17084, 17431, 17786, 18148, 18517, 18893,
225 19277, 19669, 20068, 20475, 20891, 21315, 21747, 22188, 22638, 23096, 23564, 24042, 24529, 25025, 25532, 26048,
226 26575, 27113, 27661, 28220, 28791, 29372, 29966, 30571, 31189, 31818, 32461, 33116, 33784, 34466, 35161, 35871,
227 36594, 37332, 38085, 38852, 39635, 40434, 41249, 42080, 42927, 43792, 44674, 45573, 46491,
228};
229
230static uint32_t osmo_gad_dec_ha_unc(uint8_t unc)
231{
232 return table_uncertainty_1e3[unc];
233}
234
235static uint8_t osmo_gad_enc_ha_unc(uint32_t mm)
236{
237 uint8_t unc;
238 for (unc = 0; unc < ARRAY_SIZE(table_ha_uncertainty_1e3); unc++) {
239 if (table_uncertainty_1e3[unc] > mm)
240 return unc - 1;
241 }
242 return 255;
243}
244
245#endif /* GAD_FUTURE */
246
247/* Return error code, and, if required, allocate and populate struct osmo_gad_err. */
248#define DEC_ERR(RC, TYPE, fmt, args...) do { \
249 if (err) { \
250 *err = talloc_zero(err_ctx, struct osmo_gad_err); \
251 **err = (struct osmo_gad_err){ \
252 .rc = (RC), \
253 .type = (TYPE), \
254 .logmsg = talloc_asprintf(*err, "Error decoding GAD%s%s: " fmt, \
Neels Hofmeyr9faeacd2020-10-12 17:44:09 +0200255 ((int)(TYPE)) >= 0 ? " " : "", \
256 ((int)(TYPE)) >= 0 ? osmo_gad_type_name(TYPE) : "", ##args), \
Neels Hofmeyr086bd332020-09-18 18:00:50 +0200257 }; \
258 } \
259 return RC; \
260 } while(0)
261
262static int osmo_gad_enc_ell_point_unc_circle(struct gad_raw_ell_point_unc_circle *raw, const struct osmo_gad_ell_point_unc_circle *v)
263{
264 if (v->lat < -90000000 || v->lat > 90000000)
265 return -EINVAL;
266 if (v->lon < -180000000 || v->lon > 180000000)
267 return -EINVAL;
268 *raw = (struct gad_raw_ell_point_unc_circle){
269 .h = { .type = GAD_TYPE_ELL_POINT_UNC_CIRCLE },
270 .unc = osmo_gad_enc_unc(v->unc),
271 };
272 osmo_store32be_ext(osmo_gad_enc_lat(v->lat), raw->lat, 3);
273 osmo_store32be_ext(osmo_gad_enc_lon(v->lon), raw->lon, 3);
Neels Hofmeyrb85c87a2020-10-12 17:48:03 +0200274 return sizeof(raw);
Neels Hofmeyr086bd332020-09-18 18:00:50 +0200275}
276
277static int osmo_gad_dec_ell_point_unc_circle(struct osmo_gad_ell_point_unc_circle *v,
278 struct osmo_gad_err **err, void *err_ctx,
279 const struct gad_raw_ell_point_unc_circle *raw)
280{
281 /* Load 24bit big endian */
282 v->lat = osmo_gad_dec_lat(osmo_load32be_ext_2(raw->lat, 3));
283 v->lon = osmo_gad_dec_lon(osmo_load32be_ext_2(raw->lon, 3));
284
285 if (raw->spare2)
286 DEC_ERR(-EINVAL, raw->h.type, "Bit 8 of Uncertainty code should be zero");
287
288 v->unc = osmo_gad_dec_unc(raw->unc);
289 return 0;
290}
291
292static int osmo_gad_raw_len(const union gad_raw *gad_raw)
293{
294 switch (gad_raw->h.type) {
295 case GAD_TYPE_ELL_POINT:
296 return sizeof(gad_raw->ell_point);
297 case GAD_TYPE_ELL_POINT_UNC_CIRCLE:
298 return sizeof(gad_raw->ell_point_unc_circle);
299 case GAD_TYPE_ELL_POINT_UNC_ELLIPSE:
300 return sizeof(gad_raw->ell_point_unc_ellipse);
301 case GAD_TYPE_POLYGON:
302 if (gad_raw->polygon.h.num_points < 3)
303 return -EINVAL;
304 return sizeof(gad_raw->polygon.h)
305 + gad_raw->polygon.h.num_points * sizeof(gad_raw->polygon.point[0]);
306 case GAD_TYPE_ELL_POINT_ALT:
307 return sizeof(gad_raw->ell_point_alt);
308 case GAD_TYPE_ELL_POINT_ALT_UNC_ELL:
309 return sizeof(gad_raw->ell_point_alt_unc_ell);
310 case GAD_TYPE_ELL_ARC:
311 return sizeof(gad_raw->ell_arc);
312 case GAD_TYPE_HA_ELL_POINT_UNC_ELLIPSE:
313 return sizeof(gad_raw->ha_ell_point_unc_ell);
314 case GAD_TYPE_HA_ELL_POINT_ALT_UNC_ELL:
315 return sizeof(gad_raw->ha_ell_point_alt_unc_ell);
316 default:
317 return -ENOTSUP;
318 }
319}
320
321/*! Append a GAD PDU to the msgb.
322 * Write the correct number of bytes depending on the GAD type and possibly on variable length attributes.
323 * \param[out] msg Append to this msgb.
324 * \param[in] gad_raw GAD data to write.
325 * \returns number of bytes appended to msgb, or negative on failure.
326 */
327int osmo_gad_raw_write(struct msgb *msg, const union gad_raw *gad_raw)
328{
329 int len;
330 uint8_t *dst;
331 len = osmo_gad_raw_len(gad_raw);
332 if (len < 0)
333 return len;
334 dst = msgb_put(msg, len);
335 memcpy(dst, (void*)gad_raw, len);
336 return len;
337}
338
339/*! Read a GAD PDU and validate structure.
340 * Memcpy from data to gad_raw struct, and validate correct length depending on the GAD type and possibly on variable
341 * length attributes.
342 * \param[out] gad_raw Copy GAD PDU here.
343 * \param[out] err Returned pointer to error info, dynamically allocated; NULL to not return any.
344 * \param[in] err_ctx Talloc context to allocate err from, if required.
345 * \param[in] data Encoded GAD bytes buffer.
346 * \param[in] len Length of data in bytes.
347 * \returns 0 on success, negative on error. If returning negative and err was non-NULL, *err is guaranteed to point to
348 * an allocated struct osmo_gad_err.
349 */
350int osmo_gad_raw_read(union gad_raw *gad_raw, struct osmo_gad_err **err, void *err_ctx, const uint8_t *data, uint8_t len)
351{
352 int gad_len;
353 const union gad_raw *src;
354 if (err)
355 *err = NULL;
356 if (len < sizeof(src->h))
357 DEC_ERR(-EINVAL, -1, "GAD data too short for header (%u bytes)", len);
358
359 src = (void*)data;
360 gad_len = osmo_gad_raw_len(src);
361 if (gad_len < 0)
362 DEC_ERR(-EINVAL, src->h.type, "GAD data invalid (rc=%d)", gad_len);
363 if (gad_len != len)
364 DEC_ERR(-EINVAL, src->h.type, "GAD data with unexpected length: expected %d bytes, got %u",
365 gad_len, len);
366
367 memcpy((void*)gad_raw, data, gad_len);
368 return 0;
369}
370
371/*! Write GAD values with consistent units to raw GAD PDU representation.
372 * \param[out] gad_raw Write to this buffer.
373 * \param[in] gad GAD values to encode.
374 * \returns number of bytes written, or negative on failure.
375 */
376int osmo_gad_enc(union gad_raw *gad_raw, const struct osmo_gad *gad)
377{
378 switch (gad->type) {
379 case GAD_TYPE_ELL_POINT_UNC_CIRCLE:
380 return osmo_gad_enc_ell_point_unc_circle(&gad_raw->ell_point_unc_circle, &gad->ell_point_unc_circle);
381 default:
382 return -ENOTSUP;
383 }
384}
385
386/*! Decode GAD raw PDU to values with consistent units.
387 * \param[out] gad Decoded GAD values are written here.
388 * \param[out] err Returned pointer to error info, dynamically allocated; NULL to not return any.
389 * \param[in] err_ctx Talloc context to allocate err from, if required.
390 * \param[in] raw Raw GAD data in network-byte-order.
391 * \returns 0 on success, negative on error. If returning negative and err was non-NULL, *err is guaranteed to point to
392 * an allocated struct osmo_gad_err.
393 */
394int osmo_gad_dec(struct osmo_gad *gad, struct osmo_gad_err **err, void *err_ctx, const union gad_raw *raw)
395{
396 *gad = (struct osmo_gad){
397 .type = raw->h.type,
398 };
399 switch (raw->h.type) {
400 case GAD_TYPE_ELL_POINT_UNC_CIRCLE:
401 return osmo_gad_dec_ell_point_unc_circle(&gad->ell_point_unc_circle, err, err_ctx,
402 &raw->ell_point_unc_circle);
403 default:
404 DEC_ERR(-ENOTSUP, raw->h.type, "unsupported GAD type");
405 }
406}
407
408/*! Return a human readable representation of a raw GAD PDU.
409 * Convert to GAD values and feed the result to osmo_gad_to_str_buf().
410 * \param[out] buf Buffer to write string to.
411 * \param[in] buflen sizeof(buf).
412 * \param[in] gad Location data.
413 * \returns number of chars that would be written, like snprintf().
414 */
415int osmo_gad_raw_to_str_buf(char *buf, size_t buflen, const union gad_raw *raw)
416{
417 struct osmo_gad gad;
418 if (osmo_gad_dec(&gad, NULL, NULL, raw)) {
419 struct osmo_strbuf sb = { .buf = buf, .len = buflen };
420 OSMO_STRBUF_PRINTF(sb, "invalid");
421 return sb.chars_needed;
422 }
423 return osmo_gad_to_str_buf(buf, buflen, &gad);
424}
425
426/*! Return a human readable representation of a raw GAD PDU.
427 * Convert to GAD values and feed the result to osmo_gad_to_str_buf().
428 * \param[in] ctx Talloc ctx to allocate string buffer from.
429 * \param[in] raw GAD data in network-byte-order.
430 * \returns resulting string, dynamically allocated.
431 */
432char *osmo_gad_raw_to_str_c(void *ctx, const union gad_raw *raw)
433{
434 OSMO_NAME_C_IMPL(ctx, 128, "ERROR", osmo_gad_raw_to_str_buf, raw)
435}
436
437/*! Return a human readable representation of GAD (location estimate) values.
438 * \param[out] buf Buffer to write string to.
439 * \param[in] buflen sizeof(buf).
440 * \param[in] gad Location data.
441 * \returns number of chars that would be written, like snprintf().
442 */
443int osmo_gad_to_str_buf(char *buf, size_t buflen, const struct osmo_gad *gad)
444{
445 struct osmo_strbuf sb = { .buf = buf, .len = buflen };
446
447 if (!gad) {
448 OSMO_STRBUF_PRINTF(sb, "null");
449 return sb.chars_needed;
450 }
451
452 OSMO_STRBUF_PRINTF(sb, "%s{", osmo_gad_type_name(gad->type));
453
454 switch (gad->type) {
455 case GAD_TYPE_ELL_POINT:
456 OSMO_STRBUF_PRINTF(sb, "lat=");
457 OSMO_STRBUF_APPEND(sb, osmo_int_to_float_str_buf, gad->ell_point.lat, 6);
458 OSMO_STRBUF_PRINTF(sb, ",lon=");
459 OSMO_STRBUF_APPEND(sb, osmo_int_to_float_str_buf, gad->ell_point.lon, 6);
460 break;
461
462 case GAD_TYPE_ELL_POINT_UNC_CIRCLE:
463 OSMO_STRBUF_PRINTF(sb, "lat=");
464 OSMO_STRBUF_APPEND(sb, osmo_int_to_float_str_buf, gad->ell_point_unc_circle.lat, 6);
465 OSMO_STRBUF_PRINTF(sb, ",lon=");
466 OSMO_STRBUF_APPEND(sb, osmo_int_to_float_str_buf, gad->ell_point_unc_circle.lon, 6);
467 OSMO_STRBUF_PRINTF(sb, ",unc=");
468 OSMO_STRBUF_APPEND(sb, osmo_int_to_float_str_buf, gad->ell_point_unc_circle.unc, 3);
469 OSMO_STRBUF_PRINTF(sb, "m");
470 break;
471
472 default:
473 OSMO_STRBUF_PRINTF(sb, "to-str-not-implemented");
474 break;
475 }
476
477 OSMO_STRBUF_PRINTF(sb, "}");
478 return sb.chars_needed;
479}
480
481/*! Return a human readable representation of GAD (location estimate) values.
482 * \param[in] ctx Talloc ctx to allocate string buffer from.
483 * \param[in] val Value to convert to float.
484 * \returns resulting string, dynamically allocated.
485 */
486char *osmo_gad_to_str_c(void *ctx, const struct osmo_gad *gad)
487{
488 OSMO_NAME_C_IMPL(ctx, 128, "ERROR", osmo_gad_to_str_buf, gad)
489}
490
491/*! @} */