blob: c2831462a1999dbf9719de84aa139d467fd3defa [file] [log] [blame]
Holger Hans Peter Freyther096f6f92013-11-07 07:21:06 +01001/* Copied from tbf.cpp
2 *
3 * Copyright (C) 2012 Ivan Klyuchnikov
4 * Copyright (C) 2012 Andreas Eversberg <jolly@eversberg.eu>
5 * Copyright (C) 2013 by Holger Hans Peter Freyther
6 *
7 * This program is free software; you can redistribute it and/or
8 * modify it under the terms of the GNU General Public License
9 * as published by the Free Software Foundation; either version 2
10 * of the License, or (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
Holger Hans Peter Freyther096f6f92013-11-07 07:21:06 +010016 */
17
Holger Hans Peter Freyther096f6f92013-11-07 07:21:06 +010018
Holger Hans Peter Freyther857281f2013-11-13 14:56:55 +010019#include <stdio.h>
20
Holger Hans Peter Freyther096f6f92013-11-07 07:21:06 +010021#include <osmocom/core/msgb.h>
Holger Hans Peter Freyther096f6f92013-11-07 07:21:06 +010022
Pau Espin Pedrol4f8384b2022-03-31 19:36:12 +020023#include "bts.h"
Pau Espin Pedrol6b1e9512022-04-04 13:45:56 +020024#include "gprs_ms.h"
Pau Espin Pedrol1de68732020-03-11 14:04:52 +010025#include "pcu_utils.h"
Pau Espin Pedrol4f8384b2022-03-31 19:36:12 +020026#include "llc.h"
27
28void llc_init(struct gprs_llc *llc)
29{
30 llc_reset(llc);
31}
Pau Espin Pedrol1de68732020-03-11 14:04:52 +010032
Holger Hans Peter Freyther096f6f92013-11-07 07:21:06 +010033/* reset LLC frame */
Pau Espin Pedrol4f8384b2022-03-31 19:36:12 +020034void llc_reset(struct gprs_llc *llc)
Holger Hans Peter Freyther096f6f92013-11-07 07:21:06 +010035{
Pau Espin Pedrol4f8384b2022-03-31 19:36:12 +020036 llc->index = 0;
37 llc->length = 0;
Holger Hans Peter Freythera42b2ad2013-11-26 16:39:47 +010038
Pau Espin Pedrol4f8384b2022-03-31 19:36:12 +020039 memset(llc->frame, 0x42, sizeof(llc->frame));
Holger Hans Peter Freyther096f6f92013-11-07 07:21:06 +010040}
41
Pau Espin Pedrol4f8384b2022-03-31 19:36:12 +020042void llc_reset_frame_space(struct gprs_llc *llc)
Holger Hans Peter Freyther096f6f92013-11-07 07:21:06 +010043{
Pau Espin Pedrol4f8384b2022-03-31 19:36:12 +020044 llc->index = 0;
Holger Hans Peter Freyther096f6f92013-11-07 07:21:06 +010045}
46
Jacob Erlbeck612e93e2015-03-20 13:57:27 +010047/* Put an Unconfirmed Information (UI) Dummy command, see GSM 44.064, 6.4.2.2 */
Pau Espin Pedrol4f8384b2022-03-31 19:36:12 +020048void llc_put_dummy_frame(struct gprs_llc *llc, size_t req_len)
Jacob Erlbeck612e93e2015-03-20 13:57:27 +010049{
50 /* The shortest dummy command (the spec requests at least 6 octets) */
51 static const uint8_t llc_dummy_command[] = {
52 0x43, 0xc0, 0x01, 0x2b, 0x2b, 0x2b
53 };
Jacob Erlbeck3db617f2015-07-07 13:43:44 +020054 static const size_t max_dummy_command_len = 79;
Jacob Erlbeck612e93e2015-03-20 13:57:27 +010055
Pau Espin Pedrol4f8384b2022-03-31 19:36:12 +020056 llc_put_frame(llc, llc_dummy_command, sizeof(llc_dummy_command));
Jacob Erlbeck612e93e2015-03-20 13:57:27 +010057
58 if (req_len > max_dummy_command_len)
59 req_len = max_dummy_command_len;
60
61 /* Add further stuffing, if the requested length exceeds the minimum
62 * dummy command length */
Pau Espin Pedrol4f8384b2022-03-31 19:36:12 +020063 if (llc->length < req_len) {
64 memset(&llc->frame[llc->length], 0x2b, req_len - llc->length);
65 llc->length = req_len;
Pau Espin Pedrola70bf722021-03-02 12:28:32 +010066 }
Jacob Erlbeck612e93e2015-03-20 13:57:27 +010067}
68
Pau Espin Pedrol4f8384b2022-03-31 19:36:12 +020069void llc_put_frame(struct gprs_llc *llc, const uint8_t *data, size_t len)
Holger Hans Peter Freyther096f6f92013-11-07 07:21:06 +010070{
Holger Hans Peter Freyther857281f2013-11-13 14:56:55 +010071 /* only put frames when we are empty */
Pau Espin Pedrol4f8384b2022-03-31 19:36:12 +020072 OSMO_ASSERT(llc->index == 0 && llc->length == 0);
73 llc_append_frame(llc, data, len);
Holger Hans Peter Freythere2310262013-11-13 16:56:15 +010074}
Holger Hans Peter Freyther857281f2013-11-13 14:56:55 +010075
Pau Espin Pedrol4f8384b2022-03-31 19:36:12 +020076void llc_append_frame(struct gprs_llc *llc, const uint8_t *data, size_t len)
Holger Hans Peter Freythere2310262013-11-13 16:56:15 +010077{
Holger Hans Peter Freyther857281f2013-11-13 14:56:55 +010078 /* TODO: bounds check */
Pau Espin Pedrol4f8384b2022-03-31 19:36:12 +020079 memcpy(llc->frame + llc->length, data, len);
80 llc->length += len;
Holger Hans Peter Freyther096f6f92013-11-07 07:21:06 +010081}
82
Pau Espin Pedrol0e435fa2023-01-26 14:56:43 +010083static bool llc_is_user_data_frame(const uint8_t *data, size_t len)
Jacob Erlbeck6dbe8222015-05-29 10:37:09 +020084{
85 if (len < 2)
86 return false;
87
88 if ((data[0] & 0x0f) == 1 /* GPRS_SAPI_GMM */)
89 return false;
90
Jacob Erlbeckd0aee852015-06-11 11:47:06 +020091 if ((data[0] & 0xe0) != 0xc0 /* LLC UI */)
92 /* It is not an LLC UI frame, see TS 44.064, 6.3 */
Jacob Erlbeck6dbe8222015-05-29 10:37:09 +020093 return false;
94
95 return true;
96}
97
Pau Espin Pedrol6b1e9512022-04-04 13:45:56 +020098void llc_queue_init(struct gprs_llc_queue *q, struct GprsMs *ms)
Jacob Erlbeck6dbe8222015-05-29 10:37:09 +020099{
Pau Espin Pedrol5c598c72022-03-31 20:22:05 +0200100 unsigned int i;
101
Pau Espin Pedrol6b1e9512022-04-04 13:45:56 +0200102 q->ms = ms;
Pau Espin Pedrol14633832022-03-31 19:08:07 +0200103 q->queue_size = 0;
104 q->queue_octets = 0;
105 q->avg_queue_delay = 0;
Pau Espin Pedrol6b1e9512022-04-04 13:45:56 +0200106 for (i = 0; i < ARRAY_SIZE(q->pq); i++) {
107 INIT_LLIST_HEAD(&q->pq[i].queue);
108 gprs_codel_init(&q->pq[i].codel_state);
109 }
Jacob Erlbeck6dbe8222015-05-29 10:37:09 +0200110}
111
Pau Espin Pedrol6b1e9512022-04-04 13:45:56 +0200112/* interval=0 -> don't use codel in the LLC queue */
113void llc_queue_set_codel_interval(struct gprs_llc_queue *q, unsigned int interval)
114{
115 unsigned int i;
116 if (interval == LLC_CODEL_DISABLE) {
117 q->use_codel = false;
118 return;
119 }
120 q->use_codel = true;
121 for (i = 0; i < ARRAY_SIZE(q->pq); i++)
122 gprs_codel_set_interval(&q->pq[i].codel_state, interval);
123}
Pau Espin Pedrol5fc6e012020-02-26 20:05:33 +0100124
Pau Espin Pedrol5c598c72022-03-31 20:22:05 +0200125static enum gprs_llc_queue_prio llc_sapi2prio(uint8_t sapi)
126{
127 switch (sapi) {
128 case 1:
129 return LLC_QUEUE_PRIO_GMM;
130 case 2:
131 case 7:
132 case 8:
133 return LLC_QUEUE_PRIO_TOM_SMS;
134 default:
135 return LLC_QUEUE_PRIO_OTHER;
136 }
137}
138
Pau Espin Pedrol4f8384b2022-03-31 19:36:12 +0200139void llc_queue_enqueue(struct gprs_llc_queue *q, struct msgb *llc_msg, const struct timespec *expire_time)
Jacob Erlbeck6dbe8222015-05-29 10:37:09 +0200140{
Pau Espin Pedrol4f8384b2022-03-31 19:36:12 +0200141 struct MetaInfo *meta_storage;
Pau Espin Pedrol5c598c72022-03-31 20:22:05 +0200142 struct gprs_llc_hdr *llc_hdr = (struct gprs_llc_hdr *)msgb_data(llc_msg);
143 enum gprs_llc_queue_prio prio;
Jacob Erlbeckb671dbf2015-06-15 14:32:33 +0200144
Pau Espin Pedrol5fc6e012020-02-26 20:05:33 +0100145 osmo_static_assert(sizeof(*meta_storage) <= sizeof(llc_msg->cb), info_does_not_fit);
Jacob Erlbeckb671dbf2015-06-15 14:32:33 +0200146
Pau Espin Pedrol5c598c72022-03-31 20:22:05 +0200147 prio = llc_sapi2prio(llc_hdr->sapi);
148
Pau Espin Pedrol4f8384b2022-03-31 19:36:12 +0200149 q->queue_size += 1;
150 q->queue_octets += msgb_length(llc_msg);
Jacob Erlbeckb671dbf2015-06-15 14:32:33 +0200151
Pau Espin Pedrol4f8384b2022-03-31 19:36:12 +0200152 meta_storage = (struct MetaInfo *)&llc_msg->cb[0];
Pau Espin Pedrol1de68732020-03-11 14:04:52 +0100153 osmo_clock_gettime(CLOCK_MONOTONIC, &meta_storage->recv_time);
Pau Espin Pedrol5fc6e012020-02-26 20:05:33 +0100154 meta_storage->expire_time = *expire_time;
Jacob Erlbeckb671dbf2015-06-15 14:32:33 +0200155
Pau Espin Pedrol6b1e9512022-04-04 13:45:56 +0200156 msgb_enqueue(&q->pq[prio].queue, llc_msg);
Jacob Erlbeck6dbe8222015-05-29 10:37:09 +0200157}
158
Pau Espin Pedrol2182e622021-01-14 16:48:38 +0100159void llc_queue_clear(struct gprs_llc_queue *q, struct gprs_rlcmac_bts *bts)
Holger Hans Peter Freyther096f6f92013-11-07 07:21:06 +0100160{
161 struct msgb *msg;
Pau Espin Pedrol5c598c72022-03-31 20:22:05 +0200162 unsigned int i;
Holger Hans Peter Freyther096f6f92013-11-07 07:21:06 +0100163
Pau Espin Pedrol6b1e9512022-04-04 13:45:56 +0200164 for (i = 0; i < ARRAY_SIZE(q->pq); i++) {
165 while ((msg = msgb_dequeue(&q->pq[i].queue))) {
Pau Espin Pedrol5c598c72022-03-31 20:22:05 +0200166 if (bts)
167 bts_do_rate_ctr_inc(bts, CTR_LLC_FRAME_DROPPED);
168 msgb_free(msg);
169 }
Holger Hans Peter Freyther096f6f92013-11-07 07:21:06 +0100170 }
Holger Hans Peter Freyther550bb882013-12-04 17:10:54 +0100171
Pau Espin Pedrol14633832022-03-31 19:08:07 +0200172 q->queue_size = 0;
173 q->queue_octets = 0;
Holger Hans Peter Freyther096f6f92013-11-07 07:21:06 +0100174}
175
Pau Espin Pedrolda971ee2020-12-16 15:59:45 +0100176void llc_queue_move_and_merge(struct gprs_llc_queue *q, struct gprs_llc_queue *o)
Jacob Erlbeck257b6302015-08-21 18:07:47 +0200177{
178 struct msgb *msg, *msg1 = NULL, *msg2 = NULL;
179 struct llist_head new_queue;
Pau Espin Pedrol5c598c72022-03-31 20:22:05 +0200180 unsigned int i;
Jacob Erlbeck257b6302015-08-21 18:07:47 +0200181 size_t queue_size = 0;
182 size_t queue_octets = 0;
183 INIT_LLIST_HEAD(&new_queue);
184
Pau Espin Pedrol6b1e9512022-04-04 13:45:56 +0200185 for (i = 0; i < ARRAY_SIZE(q->pq); i++) {
Pau Espin Pedrol5c598c72022-03-31 20:22:05 +0200186 while (1) {
187 if (msg1 == NULL)
Pau Espin Pedrol6b1e9512022-04-04 13:45:56 +0200188 msg1 = msgb_dequeue(&q->pq[i].queue);
Jacob Erlbeck257b6302015-08-21 18:07:47 +0200189
Pau Espin Pedrol5c598c72022-03-31 20:22:05 +0200190 if (msg2 == NULL)
Pau Espin Pedrol6b1e9512022-04-04 13:45:56 +0200191 msg2 = msgb_dequeue(&o->pq[i].queue);
Jacob Erlbeck257b6302015-08-21 18:07:47 +0200192
Pau Espin Pedrol5c598c72022-03-31 20:22:05 +0200193 if (msg1 == NULL && msg2 == NULL)
194 break;
Jacob Erlbeck257b6302015-08-21 18:07:47 +0200195
Pau Espin Pedrol5c598c72022-03-31 20:22:05 +0200196 if (msg1 == NULL) {
197 msg = msg2;
198 msg2 = NULL;
199 } else if (msg2 == NULL) {
Jacob Erlbeck257b6302015-08-21 18:07:47 +0200200 msg = msg1;
201 msg1 = NULL;
202 } else {
Pau Espin Pedrol5c598c72022-03-31 20:22:05 +0200203 const struct MetaInfo *mi1 = (struct MetaInfo *)&msg1->cb[0];
204 const struct MetaInfo *mi2 = (struct MetaInfo *)&msg2->cb[0];
205
206 if (timespeccmp(&mi2->recv_time, &mi1->recv_time, >)) {
207 msg = msg1;
208 msg1 = NULL;
209 } else {
210 msg = msg2;
211 msg2 = NULL;
212 }
Jacob Erlbeck257b6302015-08-21 18:07:47 +0200213 }
Pau Espin Pedrol5c598c72022-03-31 20:22:05 +0200214
215 msgb_enqueue(&new_queue, msg);
216 queue_size += 1;
217 queue_octets += msgb_length(msg);
Jacob Erlbeck257b6302015-08-21 18:07:47 +0200218 }
219
Pau Espin Pedrol6b1e9512022-04-04 13:45:56 +0200220 OSMO_ASSERT(llist_empty(&q->pq[i].queue));
221 OSMO_ASSERT(llist_empty(&o->pq[i].queue));
222 llist_splice_init(&new_queue, &q->pq[i].queue);
Jacob Erlbeck257b6302015-08-21 18:07:47 +0200223 }
224
Pau Espin Pedrol14633832022-03-31 19:08:07 +0200225 o->queue_size = 0;
226 o->queue_octets = 0;
Pau Espin Pedrol14633832022-03-31 19:08:07 +0200227 q->queue_size = queue_size;
228 q->queue_octets = queue_octets;
Jacob Erlbeck257b6302015-08-21 18:07:47 +0200229}
230
Daniel Willmann9c623892013-12-04 18:11:47 +0100231#define ALPHA 0.5f
232
Pau Espin Pedrol6b1e9512022-04-04 13:45:56 +0200233static struct msgb *llc_queue_pick_msg(struct gprs_llc_queue *q, enum gprs_llc_queue_prio *prio)
Holger Hans Peter Freyther096f6f92013-11-07 07:21:06 +0100234{
Daniel Willmann9c623892013-12-04 18:11:47 +0100235 struct msgb *msg;
Pau Espin Pedrol1de68732020-03-11 14:04:52 +0100236 struct timespec *tv, tv_now, tv_result;
Daniel Willmann9c623892013-12-04 18:11:47 +0100237 uint32_t lifetime;
Pau Espin Pedrol5c598c72022-03-31 20:22:05 +0200238 unsigned int i;
Pau Espin Pedrol4f8384b2022-03-31 19:36:12 +0200239 const struct MetaInfo *meta_storage;
Daniel Willmann9c623892013-12-04 18:11:47 +0100240
Pau Espin Pedrol6b1e9512022-04-04 13:45:56 +0200241 for (i = 0; i < ARRAY_SIZE(q->pq); i++) {
242 if ((msg = msgb_dequeue(&q->pq[i].queue))) {
243 *prio = (enum gprs_llc_queue_prio)i;
Pau Espin Pedrol5c598c72022-03-31 20:22:05 +0200244 break;
Pau Espin Pedrol6b1e9512022-04-04 13:45:56 +0200245 }
Pau Espin Pedrol5c598c72022-03-31 20:22:05 +0200246 }
Daniel Willmann9c623892013-12-04 18:11:47 +0100247 if (!msg)
248 return NULL;
249
Pau Espin Pedrol4f8384b2022-03-31 19:36:12 +0200250 meta_storage = (struct MetaInfo *)&msg->cb[0];
Jacob Erlbeckb671dbf2015-06-15 14:32:33 +0200251
Pau Espin Pedrol4f8384b2022-03-31 19:36:12 +0200252 q->queue_size -= 1;
253 q->queue_octets -= msgb_length(msg);
Daniel Willmann9c623892013-12-04 18:11:47 +0100254
255 /* take the second time */
Pau Espin Pedrol1de68732020-03-11 14:04:52 +0100256 osmo_clock_gettime(CLOCK_MONOTONIC, &tv_now);
257 tv = (struct timespec *)&msg->data[sizeof(*tv)];
258 timespecsub(&tv_now, &meta_storage->recv_time, &tv_result);
Daniel Willmann9c623892013-12-04 18:11:47 +0100259
Pau Espin Pedrol1de68732020-03-11 14:04:52 +0100260 lifetime = tv_result.tv_sec*1000 + tv_result.tv_nsec/1000000;
Pau Espin Pedrol4f8384b2022-03-31 19:36:12 +0200261 q->avg_queue_delay = q->avg_queue_delay * ALPHA + lifetime * (1-ALPHA);
Daniel Willmann9c623892013-12-04 18:11:47 +0100262
263 return msg;
Holger Hans Peter Freyther096f6f92013-11-07 07:21:06 +0100264}
Holger Hans Peter Freytherfce431c2013-11-13 15:17:12 +0100265
Pau Espin Pedrol6b1e9512022-04-04 13:45:56 +0200266struct msgb *llc_queue_dequeue(struct gprs_llc_queue *q)
267{
268 struct msgb *msg;
269 struct timespec tv_now, tv_now2;
270 uint32_t octets = 0, frames = 0;
271 struct gprs_rlcmac_bts *bts = q->ms->bts;
272 struct gprs_pcu *pcu = bts->pcu;
273 struct timespec hyst_delta = {0, 0};
274 const unsigned keep_small_thresh = 60;
275 enum gprs_llc_queue_prio prio;
276
277 if (pcu->vty.llc_discard_csec)
278 csecs_to_timespec(pcu->vty.llc_discard_csec, &hyst_delta);
279
280 osmo_clock_gettime(CLOCK_MONOTONIC, &tv_now);
281 timespecadd(&tv_now, &hyst_delta, &tv_now2);
282
283 while ((msg = llc_queue_pick_msg(q, &prio))) {
284 const struct MetaInfo *info = (const struct MetaInfo *)&msg->cb[0];
285 const struct timespec *tv_disc = &info->expire_time;
286 const struct timespec *tv_recv = &info->recv_time;
287
288 gprs_bssgp_update_queue_delay(tv_recv, &tv_now);
289
290 if (q->use_codel) {
291 int bytes = llc_queue_octets(q);
292 if (gprs_codel_control(&q->pq[prio].codel_state, tv_recv, &tv_now, bytes))
293 goto drop_frame;
294 }
295
296 /* Is the age below the low water mark? */
297 if (!llc_queue_is_frame_expired(&tv_now2, tv_disc))
298 break;
299
300 /* Is the age below the high water mark */
301 if (!llc_queue_is_frame_expired(&tv_now, tv_disc)) {
302 /* Has the previous message not been dropped? */
303 if (frames == 0)
304 break;
305
306 /* Hysteresis mode, try to discard LLC messages until
307 * the low water mark has been reached */
308
309 /* Check whether to abort the hysteresis mode */
310
311 /* Is the frame small, perhaps only a TCP ACK? */
312 if (msg->len <= keep_small_thresh)
313 break;
314
315 /* Is it a GMM message? */
316 if (!llc_is_user_data_frame(msg->data, msg->len))
317 break;
318 }
319
320 bts_do_rate_ctr_inc(bts, CTR_LLC_FRAME_TIMEDOUT);
321drop_frame:
322 frames++;
323 octets += msg->len;
324 msgb_free(msg);
325 bts_do_rate_ctr_inc(bts, CTR_LLC_FRAME_DROPPED);
326 continue;
327 }
328
329 if (frames) {
330 LOGPMS(q->ms, DTBFDL, LOGL_NOTICE, "Discarding LLC PDU "
331 "because lifetime limit reached, "
332 "count=%u new_queue_size=%zu\n",
333 frames, llc_queue_size(q));
334 if (frames > 0xff)
335 frames = 0xff;
336 if (octets > 0xffffff)
337 octets = 0xffffff;
338 if (pcu->bssgp.bctx)
339 bssgp_tx_llc_discarded(pcu->bssgp.bctx, ms_tlli(q->ms), frames, octets);
340 }
341
342 return msg;
343}
344
Pau Espin Pedrol4f8384b2022-03-31 19:36:12 +0200345void llc_queue_calc_pdu_lifetime(struct gprs_rlcmac_bts *bts, const uint16_t pdu_delay_csec, struct timespec *tv)
Holger Hans Peter Freytherfce431c2013-11-13 15:17:12 +0100346{
347 uint16_t delay_csec;
Pau Espin Pedrolf473ec92021-01-14 14:45:14 +0100348 if (bts->pcu->vty.force_llc_lifetime)
349 delay_csec = bts->pcu->vty.force_llc_lifetime;
Holger Hans Peter Freytherfce431c2013-11-13 15:17:12 +0100350 else
351 delay_csec = pdu_delay_csec;
352
Holger Hans Peter Freyther5a7f6362013-11-21 21:59:32 +0100353 /* keep timestamp at 0 for infinite delay */
Holger Hans Peter Freyther985fd112013-11-13 15:19:39 +0100354 if (delay_csec == 0xffff) {
355 memset(tv, 0, sizeof(*tv));
356 return;
357 }
358
359 /* calculate timestamp of timeout */
Pau Espin Pedrol1de68732020-03-11 14:04:52 +0100360 struct timespec now, csec;
361 osmo_clock_gettime(CLOCK_MONOTONIC, &now);
362 csecs_to_timespec(delay_csec, &csec);
Holger Hans Peter Freyther51e093b2013-11-13 15:35:45 +0100363
Pau Espin Pedrol1de68732020-03-11 14:04:52 +0100364 timespecadd(&now, &csec, tv);
Holger Hans Peter Freytherfce431c2013-11-13 15:17:12 +0100365}
Holger Hans Peter Freytherb1302b02013-11-13 17:15:26 +0100366
Pau Espin Pedrol4f8384b2022-03-31 19:36:12 +0200367bool llc_queue_is_frame_expired(const struct timespec *tv_now, const struct timespec *tv)
Holger Hans Peter Freytherb1302b02013-11-13 17:15:26 +0100368{
369 /* Timeout is infinite */
Pau Espin Pedrol1de68732020-03-11 14:04:52 +0100370 if (tv->tv_sec == 0 && tv->tv_nsec == 0)
Holger Hans Peter Freytherb1302b02013-11-13 17:15:26 +0100371 return false;
372
Pau Espin Pedrol1de68732020-03-11 14:04:52 +0100373 return timespeccmp(tv_now, tv, >);
Holger Hans Peter Freytherb1302b02013-11-13 17:15:26 +0100374}