-
Notifications
You must be signed in to change notification settings - Fork 6
/
Copy pathLinkMobile.hpp
1919 lines (1685 loc) · 61.5 KB
/
LinkMobile.hpp
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
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#ifndef LINK_MOBILE_H
#define LINK_MOBILE_H
// --------------------------------------------------------------------------
// A high level driver for the Mobile Adapter GB.
// Check out the REON project -> https://github.com/REONTeam
// --------------------------------------------------------------------------
// Usage:
// - 1) Include this header in your main.cpp file and add:
// LinkMobile* linkMobile = new LinkMobile();
// - 2) Add the required interrupt service routines: (*)
// irq_init(NULL);
// irq_add(II_VBLANK, LINK_MOBILE_ISR_VBLANK);
// irq_add(II_SERIAL, LINK_MOBILE_ISR_SERIAL);
// irq_add(II_TIMER3, LINK_MOBILE_ISR_TIMER);
// - 3) Initialize the library with:
// linkMobile->activate();
// // (do something until `linkMobile->isSessionActive()` returns `true`)
// - 4) Call someone:
// linkMobile->call("127000000001");
// // (do something until `linkMobile->isConnectedP2P()` returns `true`)
// - 5) Send/receive data:
// LinkMobile::DataTransfer dataTransfer = { .size = 5 };
// for (u32 i = 0; i < 5; i++)
// dataTransfer.data[i] = ((u8*)"hello")[i];
// linkMobile->transfer(dataTransfer, &dataTransfer);
// // (do something until `dataTransfer.completed` is `true`)
// // (use `dataTransfer` as the received data)
// - 6) Hang up:
// linkMobile->hangUp();
// - 7) Connect to the internet:
// linkMobile->callISP("REON password");
// // (do something until `linkMobile->isConnectedPPP()` returns `true`)
// - 8) Run DNS queries:
// LinkMobile::DNSQuery dnsQuery;
// linkMobile->dnsQuery("something.com", &dnsQuery);
// // (do something until `dnsQuery.completed` is `true`)
// // (use `dnsQuery.success` and `dnsQuery.ipv4`)
// - 9) Open connections:
// auto type = LinkMobile::ConnectionType::TCP;
// LinkMobile::OpenConn openConn;
// linkMobile->openConnection(dnsQuery.ipv4, connType, &openConn);
// // (do something until `openConn.completed` is `true`)
// // (use `openConn.connectionId` as last argument of `transfer(...)`)
// - 10) Close connections:
// LinkMobile::CloseConn closeConn;
// linkMobile->closeConnection(openConn.connectionId, type, &closeConn);
// // (do something until `closeConn.completed` is `true`)
// - 11) Synchronously wait for an action to be completed:
// linkMobile->waitFor(&dnsQuery);
// - 12) Turn off the adapter:
// linkMobile->shutdown();
// --------------------------------------------------------------------------
// (*) libtonc's interrupt handler sometimes ignores interrupts due to a bug.
// That causes packet loss. You REALLY want to use libugba's instead.
// (see examples)
// --------------------------------------------------------------------------
#ifndef LINK_DEVELOPMENT
#pragma GCC system_header
#endif
#include "_link_common.hpp"
#include <cstring>
#include "LinkGPIO.hpp"
#include "LinkSPI.hpp"
#ifndef LINK_MOBILE_QUEUE_SIZE
/**
* @brief Request queue size (how many commands can be queued at the same time).
* The default value is `10`, which seems fine for most games.
* \warning This affects how much memory is allocated. With the default value,
* it's around 3 KB.
*/
#define LINK_MOBILE_QUEUE_SIZE 10
#endif
static volatile char LINK_MOBILE_VERSION[] = "LinkMobile/v7.0.1";
#define LINK_MOBILE_MAX_USER_TRANSFER_LENGTH 254
#define LINK_MOBILE_MAX_COMMAND_TRANSFER_LENGTH 255
#define LINK_MOBILE_MAX_PHONE_NUMBER_LENGTH 32
#define LINK_MOBILE_MAX_LOGIN_ID_LENGTH 32
#define LINK_MOBILE_MAX_PASSWORD_LENGTH 32
#define LINK_MOBILE_MAX_DOMAIN_NAME_LENGTH 253
#define LINK_MOBILE_COMMAND_TRANSFER_BUFFER \
(LINK_MOBILE_MAX_COMMAND_TRANSFER_LENGTH + 4)
#define LINK_MOBILE_DEFAULT_TIMEOUT (60 * 10)
#define LINK_MOBILE_DEFAULT_TIMER_ID 3
#define LINK_MOBILE_BARRIER asm volatile("" ::: "memory")
#if LINK_ENABLE_DEBUG_LOGS != 0
#define _LMLOG_(...) Link::log(__VA_ARGS__)
#else
#define _LMLOG_(...)
#endif
/**
* @brief A high level driver for the Mobile Adapter GB.
*/
class LinkMobile {
private:
using u32 = unsigned int;
using u16 = unsigned short;
using u8 = unsigned char;
static constexpr auto BASE_FREQUENCY = Link::_TM_FREQ_1024;
static constexpr int INIT_WAIT_FRAMES = 7;
static constexpr int INIT_TIMEOUT_FRAMES = 60 * 3;
static constexpr int PING_FREQUENCY_FRAMES = 60;
static constexpr int ADAPTER_WAITING = 0xd2;
static constexpr u32 ADAPTER_WAITING_32BIT = 0xd2d2d2d2;
static constexpr int GBA_WAITING = 0x4b;
static constexpr u32 GBA_WAITING_32BIT = 0x4b4b4b4b;
static constexpr int OR_VALUE = 0x80;
static constexpr int COMMAND_MAGIC_VALUE1 = 0x99;
static constexpr int COMMAND_MAGIC_VALUE2 = 0x66;
static constexpr int DEVICE_GBA = 0x1;
static constexpr int DEVICE_ADAPTER_BLUE = 0x8;
static constexpr int DEVICE_ADAPTER_YELLOW = 0x9;
static constexpr int DEVICE_ADAPTER_GREEN = 0xa;
static constexpr int DEVICE_ADAPTER_RED = 0xb;
static constexpr int ACK_SENDER = 0;
static constexpr int CONFIGURATION_DATA_SIZE = 192;
static constexpr int CONFIGURATION_DATA_CHUNK = CONFIGURATION_DATA_SIZE / 2;
static constexpr const char* FALLBACK_ISP_NUMBER = "#9677";
static constexpr int COMMAND_BEGIN_SESSION = 0x10;
static constexpr int COMMAND_END_SESSION = 0x11;
static constexpr int COMMAND_DIAL_TELEPHONE = 0x12;
static constexpr int COMMAND_HANG_UP_TELEPHONE = 0x13;
static constexpr int COMMAND_WAIT_FOR_TELEPHONE_CALL = 0x14;
static constexpr int COMMAND_TRANSFER_DATA = 0x15;
static constexpr int COMMAND_RESET = 0x16;
static constexpr int COMMAND_TELEPHONE_STATUS = 0x17;
static constexpr int COMMAND_SIO32 = 0x18;
static constexpr int COMMAND_READ_CONFIGURATION_DATA = 0x19;
static constexpr int COMMAND_ISP_LOGIN = 0x21;
static constexpr int COMMAND_ISP_LOGOUT = 0x22;
static constexpr int COMMAND_OPEN_TCP_CONNECTION = 0x23;
static constexpr int COMMAND_CLOSE_TCP_CONNECTION = 0x24;
static constexpr int COMMAND_OPEN_UDP_CONNECTION = 0x25;
static constexpr int COMMAND_CLOSE_UDP_CONNECTION = 0x26;
static constexpr int COMMAND_DNS_QUERY = 0x28;
static constexpr int COMMAND_CONNECTION_CLOSED = 0x1f;
static constexpr int COMMAND_ERROR_STATUS = 0x6e | OR_VALUE;
static constexpr u8 WAIT_TICKS[] = {4, 8};
static constexpr int LOGIN_PARTS_SIZE = 8;
static constexpr u8 LOGIN_PARTS[] = {0x4e, 0x49, 0x4e, 0x54,
0x45, 0x4e, 0x44, 0x4f};
static constexpr int SUPPORTED_DEVICES_SIZE = 4;
static constexpr u8 SUPPORTED_DEVICES[] = {
DEVICE_ADAPTER_BLUE, DEVICE_ADAPTER_YELLOW, DEVICE_ADAPTER_GREEN,
DEVICE_ADAPTER_RED};
static constexpr u8 DIAL_PHONE_FIRST_BYTE[] = {0, 2, 1, 1};
public:
enum State {
NEEDS_RESET = 0,
PINGING = 1,
WAITING_TO_START = 2,
STARTING_SESSION = 3,
ACTIVATING_SIO32 = 4,
WAITING_32BIT_SWITCH = 5,
READING_CONFIGURATION = 6,
SESSION_ACTIVE = 7,
CALL_REQUESTED = 8,
CALLING = 9,
CALL_ESTABLISHED = 10,
ISP_CALL_REQUESTED = 11,
ISP_CALLING = 12,
PPP_LOGIN = 13,
PPP_ACTIVE = 14,
SHUTDOWN_REQUESTED = 15,
ENDING_SESSION = 16,
WAITING_8BIT_SWITCH = 17,
SHUTDOWN = 18
};
enum Role { NO_P2P_CONNECTION, CALLER, RECEIVER };
enum ConnectionType { TCP, UDP };
struct ConfigurationData {
char magic[2];
u8 registrationState;
u8 _unused1_;
u8 primaryDNS[4];
u8 secondaryDNS[4];
char loginId[10];
u8 _unused2_[22];
char email[24];
u8 _unused3_[6];
char smtpServer[20];
char popServer[19];
u8 _unused4_[5];
u8 configurationSlot1[24];
u8 configurationSlot2[24];
u8 configurationSlot3[24];
u8 checksumHigh;
u8 checksumLow;
char _ispNumber1[16 + 1]; // (parsed from `configurationSlot1`)
} __attribute__((packed));
struct AsyncRequest {
volatile bool completed = false;
bool success = false;
bool fail() {
success = false;
completed = true;
return false;
}
};
struct DNSQuery : public AsyncRequest {
u8 ipv4[4] = {};
};
struct OpenConn : public AsyncRequest {
u8 connectionId = 0;
};
struct CloseConn : public AsyncRequest {};
struct DataTransfer : public AsyncRequest {
u8 data[LINK_MOBILE_MAX_USER_TRANSFER_LENGTH] = {};
u8 size = 0;
};
enum CommandResult {
PENDING,
SUCCESS,
INVALID_DEVICE_ID,
INVALID_COMMAND_ACK,
INVALID_MAGIC_BYTES,
WEIRD_DATA_SIZE,
WRONG_CHECKSUM,
ERROR_CODE,
WEIRD_ERROR_CODE
};
struct Error {
enum Type {
NONE,
ADAPTER_NOT_CONNECTED,
PPP_LOGIN_FAILED,
COMMAND_FAILED,
WEIRD_RESPONSE,
TIMEOUT,
WTF
};
Error::Type type = Error::Type::NONE;
State state = State::NEEDS_RESET;
u8 cmdId = 0;
CommandResult cmdResult = CommandResult::PENDING;
u8 cmdErrorCode = 0;
bool cmdIsSending = false;
int reqType = -1;
};
/**
* @brief Constructs a new LinkMobile object.
* @param timeout Number of *frames* without completing a request to reset a
* connection. Defaults to 600 (10 seconds).
* @param timerId GBA Timer to use for waiting.
*/
explicit LinkMobile(u32 timeout = LINK_MOBILE_DEFAULT_TIMEOUT,
u8 timerId = LINK_MOBILE_DEFAULT_TIMER_ID) {
this->config.timeout = timeout;
this->config.timerId = timerId;
}
/**
* @brief Returns whether the library is active or not.
*/
[[nodiscard]] bool isActive() { return isEnabled; }
/**
* @brief Activates the library. After some time, if an adapter is connected,
* the state will be changed to `SESSION_ACTIVE`. If not, the state will be
* `NEEDS_RESET`, and you can retrieve the error with `getError()`.
*/
void activate() {
error = {};
LINK_MOBILE_BARRIER;
isEnabled = false;
LINK_MOBILE_BARRIER;
resetState();
stop();
LINK_MOBILE_BARRIER;
isEnabled = true;
LINK_MOBILE_BARRIER;
start();
}
/**
* @brief Deactivates the library, resetting the serial mode to GPIO.
* \warning Calling `shutdown()` first is recommended, but the adapter will
* put itself in sleep mode after 3 seconds anyway.
*/
void deactivate() {
error = {};
isEnabled = false;
resetState();
stop();
}
/**
* @brief Gracefully shuts down the adapter, closing all connections.
* After some time, the state will be changed to `SHUTDOWN`, and only then
* it's safe to call `deactivate()`.
* \warning Non-blocking. Returns `true` immediately, or `false` if there's no
* active session or available request slots.
*/
bool shutdown() {
if (!canShutdown() || userRequests.isFull())
return false;
pushRequest(UserRequest{.type = UserRequest::Type::SHUTDOWN});
return true;
}
/**
* @brief Initiates a P2P connection with a `phoneNumber`. After some time,
* the state will be `CALL_ESTABLISHED` (or `ACTIVE_SESSION` if the
* connection fails or ends).
* @param phoneNumber The phone number to call. In REON/libmobile this can be
* a number assigned by the relay server, or a 12-digit IPv4 address (for
* example, "127000000001" would be 127.0.0.1).
* \warning Non-blocking. Returns `true` immediately, or `false` if there's no
* active session or available request slots.
*/
bool call(const char* phoneNumber) {
if (state != SESSION_ACTIVE || userRequests.isFull())
return false;
auto request = UserRequest{.type = UserRequest::Type::CALL};
copyString(request.phoneNumber, phoneNumber,
LINK_MOBILE_MAX_PHONE_NUMBER_LENGTH);
pushRequest(request);
return true;
}
/**
* @brief Calls the ISP number registered in the adapter configuration, or a
* default number if the adapter hasn't been configured. Then, performs a
* login operation using the provided REON `password` and `loginId`. After
* some time, the state will be `PPP_ACTIVE`. If `loginId` is empty and the
* adapter has been configured, it will use the one stored in the
* configuration.
* @param password The password, as a null-terminated string (max `32`
* characters).
* @param loginId The login ID, as a null-terminated string (max `32`
* characters). It can be empty if it's already stored in the configuration.
* \warning Non-blocking. Returns `true` immediately, or `false`
* if there's no active session, no available request slots, or no login ID.
*/
bool callISP(const char* password, const char* loginId = "") {
if (state != SESSION_ACTIVE || userRequests.isFull())
return false;
auto request = UserRequest{.type = UserRequest::Type::PPP_LOGIN};
copyString(request.password, password, LINK_MOBILE_MAX_PASSWORD_LENGTH);
if (std::strlen(loginId) > 0)
copyString(request.loginId, loginId, LINK_MOBILE_MAX_LOGIN_ID_LENGTH);
else if (adapterConfiguration.isValid())
copyString(request.loginId, adapterConfiguration.fields._ispNumber1,
LINK_MOBILE_MAX_LOGIN_ID_LENGTH);
else
return false;
pushRequest(request);
return true;
}
/**
* @brief Looks up the IPv4 address for a domain name.
* @param domain A null-terminated string for the domain name (max `253`
* characters). It also accepts a ASCII IPv4 address, converting it into a
* 4-byte address instead of querying the DNS server.
* @param result A pointer to a `LinkMobile::DNSQuery` struct that
* will be filled with the result. When the request is completed, the
* `completed` field will be `true`. If an IP address was found, the `success`
* field will be `true` and the `ipv4` field can be read as a 4-byte address.
* \warning Non-blocking. Returns `true` immediately, or `false` if there's no
* active PPP session or available request slots.
*/
bool dnsQuery(const char* domainName, DNSQuery* result) {
if (state != PPP_ACTIVE || userRequests.isFull())
return result->fail();
result->completed = false;
result->success = false;
u32 size = std::strlen(domainName);
if (size > LINK_MOBILE_MAX_DOMAIN_NAME_LENGTH)
size = LINK_MOBILE_MAX_DOMAIN_NAME_LENGTH;
auto request = UserRequest{.type = UserRequest::Type::DNS_QUERY,
.dns = result,
.send = {.data = {}},
.commandSent = false};
for (u32 i = 0; i < size; i++)
request.send.data[i] = domainName[i];
request.send.size = size;
pushRequest(request);
return true;
}
/**
* @brief Opens a TCP/UDP (`type`) connection at the given `ip` (4-byte
* address) on the given `port`.
* @param ip The 4-byte address.
* @param port The port.
* @param type One of the enum values from `LinkMobile::ConnectionType`.
* @param result A pointer to a `LinkMobile::OpenConn` struct that
* will be filled with the result. When the request is completed, the
* `completed` field will be `true`. If the connection was successful, the
* `success` field will be `true` and the `connectionId` field can be used
* when calling the `transfer(...)` method. If not, you can assume that the
* connection was closed.
* \warning Only `2` connections can be opened at the same time.
* \warning Non-blocking. Returns `true` immediately, or `false` if there's no
* active PPP session, no available request slots.
*/
bool openConnection(const u8* ip,
u16 port,
ConnectionType type,
OpenConn* result) {
if (state != PPP_ACTIVE || userRequests.isFull())
return result->fail();
result->completed = false;
result->success = false;
auto request = UserRequest{.type = UserRequest::Type::OPEN_CONNECTION,
.open = result,
.connectionType = type,
.commandSent = false};
for (u32 i = 0; i < 4; i++)
request.ip[i] = ip[i];
request.port = port;
pushRequest(request);
return true;
}
/**
* @brief Closes an active TCP/UDP (`type`) connection.
* @param connectionId The ID of the connection.
* @param type One of the enum values from `LinkMobile::ConnectionType`.
* @param result A pointer to a `LinkMobile::CloseConn` struct that
* will be filled with the result. When the request is completed, the
* `completed` field will be `true`. If the connection was closed correctly,
* the `success` field will be `true`.
* \warning Non-blocking. Returns `true` immediately, or `false` if there's no
* active PPP session, no available request slots.
*/
bool closeConnection(u8 connectionId,
ConnectionType type,
CloseConn* result) {
if (state != PPP_ACTIVE || userRequests.isFull())
return result->fail();
result->completed = false;
result->success = false;
auto request = UserRequest{.type = UserRequest::Type::CLOSE_CONNECTION,
.close = result,
.connectionType = type,
.commandSent = false};
request.connectionId = connectionId;
pushRequest(request);
return true;
}
/**
* @brief Requests a data transfer and responds the received data. The
* transfer can be done with the other node in a P2P connection, or with any
* open TCP/UDP connection if a PPP session is active. In the case of a
* TCP/UDP connection, the `connectionId` must be provided.
* @param dataToSend The data to send, up to 254 bytes.
* @param result A pointer to a `LinkMobile::DataTransfer` struct that
* will be filled with the received data. It can also point to `dataToSend` to
* reuse the struct. When the transfer is completed, the `completed` field
* will be `true`. If the transfer was successful, the `success` field will be
* `true`.
* \warning Non-blocking. Returns `true` immediately, or `false` if
* there's no active call or available request slots.
*/
bool transfer(DataTransfer dataToSend,
DataTransfer* result,
u8 connectionId = 0xff) {
if ((state != CALL_ESTABLISHED && state != PPP_ACTIVE) ||
userRequests.isFull())
return result->fail();
result->completed = false;
result->success = false;
auto request = UserRequest{.type = UserRequest::Type::TRANSFER,
.connectionId = connectionId,
.send = {.data = {}, .size = dataToSend.size},
.receive = result,
.commandSent = false};
for (u32 i = 0; i < dataToSend.size; i++)
request.send.data[i] = dataToSend.data[i];
pushRequest(request);
return true;
}
/**
* @brief Waits for `asyncRequest` to be completed. Returns `true` if the
* request was completed && successful, and the adapter session is still
* alive. Otherwise, it returns `false`.
* @param asyncRequest A pointer to a `LinkMobile::DNSQuery`,
* `LinkMobile::OpenConn`, `LinkMobile::CloseConn`, or
* `LinkMobile::DataTransfer`.
*/
bool waitFor(AsyncRequest* asyncRequest) {
while (isSessionActive() && !asyncRequest->completed)
Link::_IntrWait(1, Link::_IRQ_SERIAL | Link::_IRQ_VBLANK);
return isSessionActive() && asyncRequest->completed &&
asyncRequest->success;
}
/**
* @brief Hangs up the current P2P or PPP call. Closes all connections.
* \warning Non-blocking. Returns `true` immediately, or `false` if there's no
* active call or available request slots.
*/
bool hangUp() {
if ((state != CALL_ESTABLISHED && state != PPP_ACTIVE) ||
userRequests.isFull())
return false;
pushRequest(UserRequest{.type = UserRequest::Type::HANG_UP});
return true;
}
/**
* @brief Retrieves the adapter configuration.
* @param configurationData A structure that will be filled with the
* configuration data. If the adapter has an active session, the data is
* already loaded, so it's instantaneous.
* \warning Returns `true` if `configurationData` has been filled, or `false`
* if there's no active session.
*/
[[nodiscard]] bool readConfiguration(ConfigurationData& configurationData) {
if (!isSessionActive())
return false;
configurationData = adapterConfiguration.fields;
return true;
}
/**
* @brief Returns the current state.
* @return One of the enum values from `LinkMobile::State`.
*/
[[nodiscard]] State getState() { return state; }
/**
* @brief Returns the current role in the P2P connection.
* @return One of the enum values from `LinkMobile::Role`.
*/
[[nodiscard]] Role getRole() { return role; }
/**
* @brief Returns whether the adapter has been configured or not.
* @return 1 = yes, 0 = no, -1 = unknown (no session active).
*/
[[nodiscard]] int isConfigurationValid() {
if (!isSessionActive())
return -1;
return (int)adapterConfiguration.isValid();
}
/**
* @brief Returns `true` if a P2P call is established (the state is
* `CALL_ESTABLISHED`).
*/
[[nodiscard]] bool isConnectedP2P() { return state == CALL_ESTABLISHED; }
/**
* @brief Returns `true` if a PPP session is active (the state is
* `PPP_ACTIVE`).
*/
[[nodiscard]] bool isConnectedPPP() { return state == PPP_ACTIVE; }
/**
* @brief Returns `true` if the session is active.
*/
[[nodiscard]] bool isSessionActive() {
return state >= SESSION_ACTIVE && state <= SHUTDOWN_REQUESTED;
}
/**
* @brief Returns `true` if there's an active session and there's no previous
* shutdown requests.
*/
[[nodiscard]] bool canShutdown() {
return isSessionActive() && state != SHUTDOWN_REQUESTED;
}
/**
* @brief Returns the current operation mode (`LinkSPI::DataSize`).
*/
[[nodiscard]] LinkSPI::DataSize getDataSize() {
return linkSPI->getDataSize();
}
/**
* @brief Returns details about the last error that caused the connection to
* be aborted.
*/
[[nodiscard]] Error getError() { return error; }
~LinkMobile() { delete linkSPI; }
/**
* @brief This method is called by the VBLANK interrupt handler.
* \warning This is internal API!
*/
void _onVBlank() {
if (!isEnabled)
return;
if (shouldAbortOnStateTimeout()) {
timeoutStateFrames++;
if (timeoutStateFrames >= INIT_TIMEOUT_FRAMES)
return abort(Error::Type::ADAPTER_NOT_CONNECTED);
}
pingFrameCount++;
if (pingFrameCount >= PING_FREQUENCY_FRAMES && isSessionActive() &&
!asyncCommand.isActive) {
pingFrameCount = 0;
cmdTelephoneStatus();
}
processUserRequests();
processNewFrame();
}
/**
* @brief This method is called by the SERIAL interrupt handler.
* \warning This is internal API!
*/
void _onSerial() {
if (!isEnabled)
return;
linkSPI->_onSerial();
u32 newData = linkSPI->getAsyncData();
if (state == NEEDS_RESET)
return;
if (asyncCommand.isActive) {
if (asyncCommand.state == AsyncCommand::State::PENDING) {
if (isSIO32Mode()) {
if (asyncCommand.direction == AsyncCommand::Direction::SENDING)
sendAsyncCommandSIO32(newData);
else
receiveAsyncCommandSIO32(newData);
} else {
if (asyncCommand.direction == AsyncCommand::Direction::SENDING)
sendAsyncCommandSIO8(newData);
else
receiveAsyncCommandSIO8(newData);
}
if (asyncCommand.state == AsyncCommand::State::COMPLETED) {
asyncCommand.isActive = false;
processAsyncCommand();
}
}
} else {
processLoosePacket(newData);
}
}
/**
* @brief This method is called by the TIMER interrupt handler.
* \warning This is internal API!
*/
void _onTimer() {
if (!isEnabled || !hasPendingTransfer)
return;
linkSPI->transferAsync(pendingTransfer);
stopTimer();
hasPendingTransfer = false;
}
struct Config {
u32 timeout;
u32 timerId;
};
/**
* @brief LinkMobile configuration.
* \warning `deactivate()` first, change the config, and `activate()` again!
*/
Config config;
private:
enum AdapterType { BLUE, YELLOW, GREEN, RED, UNKNOWN };
struct UserRequest {
enum Type {
CALL,
PPP_LOGIN,
DNS_QUERY,
OPEN_CONNECTION,
CLOSE_CONNECTION,
TRANSFER,
HANG_UP,
SHUTDOWN
};
Type type;
char phoneNumber[LINK_MOBILE_MAX_PHONE_NUMBER_LENGTH + 1];
char loginId[LINK_MOBILE_MAX_LOGIN_ID_LENGTH + 1];
char password[LINK_MOBILE_MAX_PASSWORD_LENGTH + 1];
DNSQuery* dns;
OpenConn* open;
CloseConn* close;
u8 ip[4];
u16 port;
ConnectionType connectionType;
u8 connectionId;
DataTransfer send;
DataTransfer* receive;
bool commandSent;
u32 timeout;
bool finished;
void cleanup() {
if (finished)
return;
AsyncRequest* metadata = type == DNS_QUERY ? (AsyncRequest*)dns
: type == OPEN_CONNECTION ? (AsyncRequest*)open
: type == CLOSE_CONNECTION ? (AsyncRequest*)close
: type == TRANSFER ? (AsyncRequest*)receive
: nullptr;
if (metadata != nullptr) {
metadata->success = false;
metadata->completed = true;
}
}
};
union AdapterConfiguration {
ConfigurationData fields;
char bytes[CONFIGURATION_DATA_SIZE];
bool isValid() {
return fields.magic[0] == 'M' && fields.magic[1] == 'A' &&
(fields.registrationState & 1) == 1 &&
calculatedChecksum() == reportedChecksum();
}
u16 calculatedChecksum() {
u16 result = 0;
for (u32 i = 0; i < CONFIGURATION_DATA_SIZE - 2; i++)
result += bytes[i];
return result;
}
u16 reportedChecksum() {
return buildU16(fields.checksumHigh, fields.checksumLow);
}
};
struct MagicBytes {
u8 magic1 = COMMAND_MAGIC_VALUE1;
u8 magic2 = COMMAND_MAGIC_VALUE2;
} __attribute__((packed));
struct PacketData {
u8 bytes[LINK_MOBILE_COMMAND_TRANSFER_BUFFER] = {};
} __attribute__((packed));
struct PacketHeader {
u8 commandId = 0;
u8 _unused_ = 0;
u8 _unusedSizeHigh_ = 0;
u8 size = 0;
u16 sum() { return commandId + _unused_ + _unusedSizeHigh_ + size; }
u8 pureCommandId() { return commandId & (~OR_VALUE); }
} __attribute__((packed));
struct PacketChecksum {
u8 high = 0;
u8 low = 0;
} __attribute__((packed));
struct Command {
MagicBytes magicBytes;
PacketHeader header;
PacketData data;
PacketChecksum checksum;
};
struct CommandResponse {
CommandResult result = CommandResult::PENDING;
Command command;
};
struct AsyncCommand {
enum State { PENDING, COMPLETED };
enum Direction { SENDING, RECEIVING };
State state;
CommandResult result;
u32 transferred;
Command cmd;
Direction direction;
u16 expectedChecksum;
u8 errorCommandId;
u8 errorCode;
bool isActive = false;
void reset() {
state = AsyncCommand::State::PENDING;
result = CommandResult::PENDING;
transferred = 0;
cmd = Command{};
direction = AsyncCommand::Direction::SENDING;
expectedChecksum = 0;
errorCommandId = 0;
errorCode = 0;
isActive = false;
}
u8 relatedCommandId() {
return result == CommandResult::ERROR_CODE ? errorCommandId
: cmd.header.pureCommandId();
}
bool respondsTo(u8 commandId) {
return direction == AsyncCommand::Direction::RECEIVING &&
(result == CommandResult::ERROR_CODE
? errorCommandId == commandId
: cmd.header.commandId == (commandId | OR_VALUE));
}
void finish() {
if (cmd.header.commandId == COMMAND_ERROR_STATUS) {
if (cmd.header.size != 2) {
result = CommandResult::WEIRD_ERROR_CODE;
} else {
result = CommandResult::ERROR_CODE;
errorCommandId = cmd.data.bytes[0];
errorCode = cmd.data.bytes[1];
}
} else {
result = CommandResult::SUCCESS;
}
state = AsyncCommand::State::COMPLETED;
}
void fail(CommandResult _result) {
result = _result;
state = AsyncCommand::State::COMPLETED;
}
};
static constexpr u32 PREAMBLE_SIZE =
sizeof(MagicBytes) + sizeof(PacketHeader);
static constexpr u32 CHECKSUM_SIZE = sizeof(PacketChecksum);
using RequestQueue = Link::Queue<UserRequest, LINK_MOBILE_QUEUE_SIZE, false>;
RequestQueue userRequests;
AdapterConfiguration adapterConfiguration;
AsyncCommand asyncCommand;
u32 waitFrames = 0;
u32 timeoutStateFrames = 0;
u32 pingFrameCount = 0;
Role role = Role::NO_P2P_CONNECTION;
LinkSPI* linkSPI = new LinkSPI();
State state = NEEDS_RESET;
PacketData nextCommandData;
u32 nextCommandDataSize = 0;
bool hasPendingTransfer = false;
u32 pendingTransfer = 0;
AdapterType adapterType = AdapterType::UNKNOWN;
Error error = {};
volatile bool isEnabled = false;
void processUserRequests() {
if (!userRequests.canMutate() || userRequests.isEmpty())
return;
if (!isSessionActive()) {
userRequests.clear();
return;
}
if (userRequests.peek().finished)
userRequests.pop();
if (userRequests.isEmpty())
return;
auto request = userRequests.peek();
request.timeout++;
if (shouldAbortOnRequestTimeout() && request.timeout >= config.timeout)
return abort(Error::Type::TIMEOUT);
switch (request.type) {
case UserRequest::Type::CALL: {
if (state != SESSION_ACTIVE && state != CALL_REQUESTED) {
popRequest();
return;
}
if (state != CALL_REQUESTED)
setState(CALL_REQUESTED);
if (!asyncCommand.isActive) {
setState(CALLING);
cmdDialTelephone(request.phoneNumber);
popRequest();
}
break;
}
case UserRequest::Type::PPP_LOGIN: {
if (state != SESSION_ACTIVE && state != ISP_CALL_REQUESTED &&
state != ISP_CALLING) {
popRequest();
return;
}
if (state == SESSION_ACTIVE)
setState(ISP_CALL_REQUESTED);
if (!asyncCommand.isActive && state == ISP_CALL_REQUESTED) {
setState(ISP_CALLING);
cmdDialTelephone(adapterConfiguration.isValid()
? adapterConfiguration.fields._ispNumber1
: FALLBACK_ISP_NUMBER);
}
break;
}
case UserRequest::Type::DNS_QUERY: {
if (state != PPP_ACTIVE) {
popRequest();
return;
}
if (!asyncCommand.isActive && !request.commandSent) {
cmdDNSQuery(request.send.data, request.send.size);
request.commandSent = true;
}
break;
}
case UserRequest::Type::OPEN_CONNECTION: {
if (state != PPP_ACTIVE) {
popRequest();
return;
}
if (!asyncCommand.isActive && !request.commandSent) {
if (request.connectionType == ConnectionType::TCP)
cmdOpenTCPConnection(request.ip, request.port);
else
cmdOpenUDPConnection(request.ip, request.port);
request.commandSent = true;
}
break;
}
case UserRequest::Type::CLOSE_CONNECTION: {
if (state != PPP_ACTIVE) {
popRequest();
return;
}
if (!asyncCommand.isActive && !request.commandSent) {
if (request.connectionType == ConnectionType::TCP)
cmdCloseTCPConnection(request.connectionId);
else
cmdCloseUDPConnection(request.connectionId);
request.commandSent = true;
}
break;
}