generated from pimoroni/pico-boilerplate
-
Notifications
You must be signed in to change notification settings - Fork 0
/
time.cpp
469 lines (378 loc) · 12.6 KB
/
time.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
/*
* time.cpp - part of UniClock, a Clock for the Galactic Unicorn.
*
* UniClock is an enhance clock / calendar display for the beautiful Galactic
* Unicorn.
*
* All time related things here, initialising and managing the RTC as well as
* all the processing around NTP requests and applying timezones.
*
* Copyright (C) 2023 Pete Favelle <[email protected]>
* Released under the MIT License; see LICENSE for details.
*/
/* System headers. */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "pico/stdlib.h"
#include "pico/cyw43_arch.h"
#include "hardware/rtc.h"
#include "lwip/dns.h"
#include "lwip/pbuf.h"
#include "lwip/udp.h"
/* Local headers. */
#include "uniclock.h"
#include "usbfs.hpp"
/* Module variables. */
static absolute_time_t m_next_ntp_check = nil_time;
static int16_t m_utc_offset = 0;
/* Local / callback functions; not expected to be called from outside. */
/*
* ntp_request - sends an NTP request to the server.
*/
void time_ntp_request( uc_ntpstate_t *p_ntpstate )
{
struct pbuf *l_buffer;
uint8_t *l_payload;
/* Calls into lwIP need to be correctly locked. */
cyw43_arch_lwip_begin();
/* Allocate the packet buffer we'll send. */
l_buffer = pbuf_alloc( PBUF_TRANSPORT, UC_NTP_PACKAGE_LEN, PBUF_RAM );
l_payload = (uint8_t *)l_buffer->payload;
/* Just set the flag in the start of that packet as a V3 client request. */
memset( l_payload, 0, UC_NTP_PACKAGE_LEN );
l_payload[0] = 0x1b;
/* And send it. */
udp_sendto( p_ntpstate->socket, l_buffer, &p_ntpstate->server, UC_NTP_PORT );
/* Lastly free up the buffer. */
pbuf_free( l_buffer );
/* End of lwIP locked calls. */
cyw43_arch_lwip_end();
/* All done. */
return;
}
/*
* add_minutes_to_datetime - adds a set number of minutes to the provided
* datetime.
*/
void time_add_minutes_to_datetime( datetime_t *p_datetime, int16_t p_minutes )
{
time_t l_time;
struct tm l_tmstruct;
struct tm *l_tmptr;
/*
* Unfortunately, the datetime_t structure isn't easy to add arbitrary
* minutes to so we need to convert it into a time_t, do the sums and then
* convert it back.
*/
l_tmstruct.tm_year = p_datetime->year - 1900;
l_tmstruct.tm_mon = p_datetime->month - 1;
l_tmstruct.tm_mday = p_datetime->day;
l_tmstruct.tm_wday = p_datetime->dotw;
l_tmstruct.tm_hour = p_datetime->hour;
l_tmstruct.tm_min = p_datetime->min;
l_tmstruct.tm_sec = p_datetime->sec;
/* Convert all that into a time_t, which is a simple second-since-epoch. */
l_time = mktime( &l_tmstruct );
/* Add in the offset. */
l_time += ( p_minutes * 60 );
/* And convert it back. */
l_tmptr = gmtime( &l_time );
/* Fill in the datetime struct. */
p_datetime->year = l_tmptr->tm_year + 1900;
p_datetime->month = l_tmptr->tm_mon + 1;
p_datetime->day = l_tmptr->tm_mday;
p_datetime->dotw = l_tmptr->tm_wday;
p_datetime->hour = l_tmptr->tm_hour;
p_datetime->min = l_tmptr->tm_min;
p_datetime->sec = l_tmptr->tm_sec;
/* All done. */
return;
}
/*
* set_rtc_by_utc - sets the RTC to the provided time, applying our current
* timezone appropriately.
*/
void time_set_rtc_by_utc( time_t p_utctime )
{
datetime_t l_datetime;
struct tm *l_tmstruct;
/* Add the current UTC offset (in minutes) to that time. */
p_utctime += ( m_utc_offset * 60 );
/* Convert the time_t into a more useful structure. */
l_tmstruct = gmtime( &p_utctime );
/* Fill in the datetime struct. */
l_datetime.year = l_tmstruct->tm_year + 1900;
l_datetime.month = l_tmstruct->tm_mon + 1;
l_datetime.day = l_tmstruct->tm_mday;
l_datetime.dotw = l_tmstruct->tm_wday;
l_datetime.hour = l_tmstruct->tm_hour;
l_datetime.min = l_tmstruct->tm_min;
l_datetime.sec = l_tmstruct->tm_sec;
/* And update the RTC. */
rtc_set_datetime( &l_datetime );
/* All done. */
return;
}
/*
* ntp_response_cb - callback function when an NTP response is received.
*/
void time_ntp_response_cb( void *p_state, struct udp_pcb *p_socket,
struct pbuf *p_buffer, const ip_addr_t *p_addr,
uint16_t p_port )
{
uc_ntpstate_t *l_ntpstate = (uc_ntpstate_t *)p_state;
uint8_t l_mode, l_stratum;
uint8_t l_ntptime[4];
/*
* Called whenever we receive *any* packet; first step is to extract a few
* important flags.
*/
l_mode = pbuf_get_at( p_buffer, 0 ) & 0x07;
l_stratum = pbuf_get_at( p_buffer, 1 );
/*
* And now we try to ensure that this is the data we expected, and not some
* other random UDP packet. As long as it's the right size and port, we'll
* try to make sense of it.
*/
if ( ( p_port == UC_NTP_PORT ) && ( p_buffer->tot_len == UC_NTP_PACKAGE_LEN ) &&
( l_mode == 0x04 ) && ( l_stratum != 0 ) )
{
/* Looks valid; just extract the time value. */
pbuf_copy_partial( p_buffer, l_ntptime, sizeof( l_ntptime ), 40 );
l_ntpstate->ntptime = l_ntptime[0] << 24 | l_ntptime[1] << 16 |
l_ntptime[2] << 8 | l_ntptime[3];
}
/* Need to free the pbuf that we were passed. */
pbuf_free( p_buffer );
/* All done. */
return;
}
/*
* dns_response_cb - callback function when a DNS lookup completes.
*/
void time_dns_response_cb( const char *p_name, const ip_addr_t *p_addr,
void *p_state )
{
uc_ntpstate_t *l_ntpstate = (uc_ntpstate_t *)p_state;
/* If the address is set to a value, we have our answer. */
if ( p_addr != nullptr )
{
/* Save that address. */
memcpy( &l_ntpstate->server, p_addr, sizeof( ip_addr_t ) );
/* And initiate the request. */
time_ntp_request( l_ntpstate );
}
else
{
/* Indicates a failure. All we can really do is set ourselve inactive. */
usb_debug( "Failure in DNS callback" );
l_ntpstate->active_query = false;
}
/* All done. */
return;
}
/* Functions.*/
/*
* init - sets up the time related things.
*/
void time_init( void )
{
datetime_t l_time;
/* Initialise the RTC */
rtc_init();
/* It also needs a valid time setting, before it runs. */
l_time.year = 2023;
l_time.month = 1;
l_time.day = 1;
l_time.dotw = 0;
l_time.hour = l_time.min = l_time.sec = 0;
rtc_set_datetime( &l_time );
/* All done. */
return;
}
/*
* check_sync - if we haven't updated from NTP in some time, initiate it.
* returns true once we have either successfully updated, or
* failed in a fairly firm way.
*/
bool time_check_sync( const uc_config_t *p_config )
{
static bool l_connecting = false;
static uc_ntpstate_t l_ntpstate;
int l_link_status, l_retval;
time_t l_utctime;
/* If we're not due to sync, just say true right away. */
if ( !time_reached( m_next_ntp_check ) )
{
return true;
}
/* So, we're due a check. First off, we need to bring the WiFi online. */
/*
* WiFi on PicoW is relatively painless; first off, we initialise the library,
* switch to station mode and initiate the connection asynchronously.
*/
if ( !l_connecting )
{
/* Initialise the network. */
cyw43_arch_init();
cyw43_arch_enable_sta_mode();
cyw43_arch_wifi_connect_async(
p_config->wifi_ssid, p_config->wifi_password, CYW43_AUTH_WPA2_AES_PSK
);
/* Reset the state object we'll use for our NTP query. */
if ( l_ntpstate.socket != nullptr )
{
udp_remove( l_ntpstate.socket );
l_ntpstate.socket = nullptr;
}
l_ntpstate.ntptime = 0;
l_ntpstate.active_query = false;
/* Set a flag that we're waiting for the connection and return. */
l_connecting = true;
return false;
}
/*
* So, the connecting flag is true so we're just busy waiting for the link
* status to change to 'link up'
*/
l_link_status = cyw43_tcpip_link_status( &cyw43_state, CYW43_ITF_STA );
/*
* If the status is a hard fail, shut it all down and return true - this will
* schedule another attempt at some point in the future, by which time with
* any luck the problem has gone away!
*/
if ( ( l_link_status == CYW43_LINK_FAIL ) || ( l_link_status == CYW43_LINK_BADAUTH ) ||
( l_link_status == CYW43_LINK_NONET ) )
{
usb_debug( "Failed to initialise WiFi (link status %d)", l_link_status );
cyw43_arch_deinit();
l_connecting = false;
return true;
}
/*
* If the link isn't up yet, that's not a failure - just means we need to
* wait a little longer.
*/
if ( l_link_status != CYW43_LINK_UP )
{
return false;
}
/*
* So if we reach here, the WiFi link is up and available. Get the socket
* that we need to work with.
*/
if ( l_ntpstate.socket == nullptr )
{
l_ntpstate.socket = udp_new_ip_type( IPADDR_TYPE_ANY );
if ( l_ntpstate.socket == nullptr )
{
usb_debug( "Failed to create UDP PCB socket" );
return false;
}
/* Set up the callback to handle any packets received on this socket. */
udp_recv( l_ntpstate.socket, time_ntp_response_cb, &l_ntpstate );
}
/*
* If we don't have an active query, it means we need to start one.
*/
if ( !l_ntpstate.active_query )
{
/*
* Step one is to look up the DNS address of our target NTP server. This
* sort of low level operation needs to be properly gated with lwIP.
*/
cyw43_arch_lwip_begin();
l_ntpstate.active_query = true;
l_retval = dns_gethostbyname( p_config->ntp_server, &l_ntpstate.server,
time_dns_response_cb, &l_ntpstate );
cyw43_arch_lwip_end();
/*
* The DNS lookup may return immediately, if we already have a cached answer.
*/
if ( l_retval == ERR_OK )
{
/* Then we can just raise the NTP request directly. */
time_ntp_request( &l_ntpstate );
}
else if ( l_retval != ERR_INPROGRESS )
{
/* A DNS failure; call this a hard fail, and try next cycle. */
usb_debug( "Failed to look up NTP server address (%d)", l_retval );
cyw43_arch_deinit();
l_connecting = false;
return true;
}
/* Nothing more to do; the next cycle may find more work to do. */
return false;
}
/*
* So here, we have an active NTP query. The actual response will be caught
* in callbacks, so here we just wait for NTP time value to appear in the
* state structure. Once we have that, we apply it and close down the WiFi.
*/
if ( l_ntpstate.ntptime > 0 )
{
/* Convert the NTP time into a Unix Epoch style value. */
l_utctime = l_ntpstate.ntptime - UC_NTP_EPOCH_OFFSET;
/* And just update the RTC clock appropriately. */
time_set_rtc_by_utc( l_utctime );
/* Schedule the next NTP sync for the future... */
m_next_ntp_check = make_timeout_time_ms( UC_NTP_REFRESH_MS );
/* That's it, we're all done! */
cyw43_arch_deinit();
l_connecting = false;
return true;
}
/* Not a failure - just means we have more work to do. */
return false;
}
/*
* set_timezone - updates the clock to use the specified timezone; this should
* be one of the standard timezone strings (e.g. 'Europe/London')
* that is recognised by worldtimeapi.org
*/
void time_set_timezone( const char *p_timezone )
{
/* All done. */
return;
}
/*
* set_utc_offset - defines the offset we apply to UTC to determine local time.
* This is applied directly to the RTC, at time of setting and
* whenever we fetch a fresh time via NTP.
* This is considerably easier than applying it to the RTC
* every time we look at the clock!
* Note that this value is given in minutes, to accomodate the
* freaky timezones that shift by fractions of an hour.
*/
void time_set_utc_offset( uc_config_t *p_config, int16_t p_offset )
{
int16_t l_change;
datetime_t l_datetime;
/* Sanity check first; if the requested offset is out of bounds, return. */
if ( ( p_offset < UC_TZ_OFFSET_MIN_MN ) || ( p_offset > UC_TZ_OFFSET_MAX_MN ) )
{
return;
}
/* Work out the change between the current offset and the new one. */
l_change = p_offset - m_utc_offset;
/* Apply this to the current datetime held by the RTC. */
rtc_get_datetime( &l_datetime );
time_add_minutes_to_datetime( &l_datetime, l_change );
rtc_set_datetime( &l_datetime );
/* Update the configuration to reflect this new setting. */
if ( p_config != nullptr )
{
p_config->utc_offset_minutes = p_offset;
config_write( p_config );
}
/* Simples. Now, we save this new offset and we're done. */
m_utc_offset = p_offset;
return;
}
int16_t time_get_utc_offset( void )
{
return m_utc_offset;
}
/* End of file time.cpp */