-
Notifications
You must be signed in to change notification settings - Fork 13
Expand file tree
/
Copy pathTeleBall.ino
More file actions
2203 lines (1874 loc) · 72.9 KB
/
Copy pathTeleBall.ino
File metadata and controls
2203 lines (1874 loc) · 72.9 KB
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
/* ******************************************************************
TeleBall
Retro Style BreakOut and Tennis Game
made in August 2014 .. January 2015
* idea, code and original circurit design by sy2002
* additional circurit design and board layout by doubleflash
* body housing/case by lamags
License: You are free to share and adapt for any purpose,
even commercially as long as you attribute to sy2002 and
link to http://www.sy2002.de
http://creativecommons.org/licenses/by/4.0/
****************************************************************** */
//#define DUINOKIT //mirror x specifically for the DUINOKIT hardware
#define SHOW_MASTER_SLAVE //show master/slave indicator in question mode
#define STAY_IN_TENNIS //after tennis was entered once, stay in tennis mode
/* ******************************************
Libraries
****************************************** */
//Arduino default EEPROM library for persistent storage
#include <EEPROM.h>
//Arduino default library for accessing the PROGMEM
#include <avr/pgmspace.h>
//MAX7221 LED control library
//information: http://playground.arduino.cc/Main/LedControl
//download: https://github.com/wayoda/LedControl
#include <LedControl.h>
//NRF24L01+ radio driver class
//information & dl: http://tmrh20.github.io/RF24/
#include <SPI.h>
#include <nRF24L01.h>
#include <RF24.h>
/* ******************************************
Factory default values
****************************************** */
enum GameOrientation
{
goRegular,
goPaddleRight,
goUpsideDown,
goPaddleLeft
};
enum GameMode
{
//games
gmBreakOut,
gmTennis,
//standard configuration menu
gmSpeed,
gmBrightness,
//advanced configuration menu
gmPaddleLeft,
gmPaddleRight,
//EEPROM: store current settings or reset to default
gmEEPROM
};
enum GameSounds
{
gsWall, //sound: ball hits the wall
gsPaddle, //sound: ball hits the paddle
gsPlayfield, //sound: ball hits the BreakOut bricks (playfield)
gsLost //sound: ball gets lost
};
//defaults that are set, when "back to factory default" is selected in the extended menu
const GameMode game_mode_default = gmBreakOut; //first game to be started
const unsigned int speed_default = 300; //speed of the game in "milliseconds between moving the ball one pixel"
const byte intensity_default = 0; //brightness of the 8x8 matrix
const unsigned int poti_leftmost_default = 200; //restrict poti range to the left for a more natural feeling
const unsigned int poti_rightmost_default = 823; //... dito to the right
const GameOrientation orientation_default = goRegular; //game orientation
const unsigned long respawn_duration = 1500; //milliseconds to wait after a ball got lost
const byte Balls_max = 3; //maximum amount of balls in BreakOut
const byte Tennis_win = 3; //amount of points needed to win tennis
//button press duration in milliseconds
const unsigned int UniversalButtonPressedShort = 100; //reset game / select menu item
const unsigned int UniversalButtonPressedLong = 750; //enter regular menu
const unsigned int UniversalButtonPressedVeryLong = 3500; //enter advanced menu
//enter multiplayer mode
const unsigned long MultiplayerQuestionMax = 2000; //how long is the question mark shown, before the user can choose
boolean MultiplayerQuestionButton = false; //universal button pressed during rmMaster_init or rmSlave_init
//the speed defines how many milliseconds are between two ball movements
//the movement of the paddle is decoupled from the movement speed of the ball
//the maximum speed heavily depends on the processor type and speed
const unsigned int speed_max = 50; //amount of screen refresh cycles in milliseconds that are wasted...
const unsigned int speed_min = 500; //...until the ball moves on one pixel
//amount of LEDs installed in the device, should be 3
//changing this leads to multiple code parts that need adjustments
const byte LED_count = 3;
//absolute y-coordinate where a paddle hit shall be counted (normally is 1 line away from the line where the paddle resides)
const byte PaddleHit_Top = 1;
const byte PaddleHit_Bottom = 6;
//flags for the adjust speed mode
const unsigned long Flag_Leave = (unsigned long) 1 << 31;
const unsigned long Flag_Leave_Ack = (unsigned long) 1 << 30;
const unsigned long Mask_Speed = 1023;
//EEPROM fingerprint for detecting, if the EEPROM has ever been initialized by TeleBall
const byte EEPROM_Fingerprint_len = 8;
const byte EEPROM_Fingerprint[8] = {'T', 'e', 'l', 'e', 'B', 'a', 'l', 'l'};
/* EEPROM layout
Bytes Type Value
00..07 chars TeleBall device fingerprint
08..09 unsigned int ball speed (variable: Speed)
10 byte display intensity (variable: Intensity)
11..12 unsigned int leftmost poti position (variable: PotiLeftmost)
13..14 unsigned int rightmost poti position (variable: PotiRightmost)
*/
const byte locFingerprint = 0;
const byte locSpeed = 8;
const byte locIntensity = 10;
const byte locPotiLeftmost = 11;
const byte locPotiRightmost = 13;
//needs to be declared here to access the enums (strange compiler behaviour)
void calculateBallMovement(GameMode nowplaying);
void playSound(GameSounds whichsound);
/* ******************************************
Global Game Variables
****************************************** */
GameOrientation Orientation = orientation_default;
//current speed of the game
//unsigned long (i.e. 4 bytes) due to RadioPayloadSize == 4
unsigned long Speed = speed_default;
unsigned long Speed_Old = Speed;
unsigned int Paddle = 0; //current x-position of paddle
unsigned int Paddle_Old = Paddle;
unsigned int Paddle_Remote = 0;
unsigned int Paddle_Remote_Old = Paddle_Remote;
byte Intensity = intensity_default; //brightness of the display
byte Intensity_Old = Intensity;
//poti range restriction for a more natural game feeling
unsigned int PotiLeftmost = poti_leftmost_default;
unsigned int PotiRightmost = poti_rightmost_default;
unsigned int PotiLeftmost_Old = PotiLeftmost;
unsigned int PotiRightmost_Old = PotiRightmost;
//current ball position
char BallX; //a signed variable is used intensionally to...
char BallY; //...cater for off-screen situations
char BallX_Old = BallX;
char BallY_Old = BallY;
//current ball speed in x and y direction
//note: the "char" variable can be negative, so -1 in BallDX means,
//that it moves to the left
char BallDX = 0;
char BallDY = 0;
char BallDX_tbs = 0;
char BallDY_tbs = 0;
//remember old values during configuration
byte rBallX, rBallY, rBallX_Old, rBallY_Old;
char rBallDX, rBallDY, rBallDX_tbs, rBallDY_tbs;
//flag, if a full reset of the game is to be performed
boolean perform_reset = true;
//flag, to distinguish between gameplay and the various configuration modes
GameMode game_mode = game_mode_default;
GameMode game_mode_old = game_mode;
//flag to determin, if the game is currently in a loop state due to won or lost
boolean WonOrLostState = false;
//BreakOut only: amount of balls left and current level
byte Balls;
byte Balls_Old = 0;
byte Level;
//Tennis only: points
char TennisPoints = 0;
char TennisPoints_Old = -1;
char TennisPoints_Remote = 0;
//the timer is used to de-couple the paddle movement from the ball-speed
//i.e. it is the central instance for the overall game speed
unsigned long Timer = 0;
//the respawn_timer is used to wait until the next ball appears after you lose one
unsigned long respawn_timer = 0;
//used for measuring a quick paddle movement right before the ball hits the paddle
//to give the ball an extra spin; this is needed in situations, where you otherwise
//would not be able to clear all "bricks".
unsigned int LastPaddlePos = 0;
unsigned int LastPaddlePos_Remote = 0;
const byte PaddleSpeedThreshold = 1;
//universal button incl. debouncing
unsigned long UniversalButtonPressedStartTime = 0;
boolean UniversalButton_firstcontact = true;
unsigned long MultiplayerQuestionTime = 0;
unsigned long MultiplayerWaitStart = 0;
//breakout: screen memory for storing the current level
byte bricks[8][8];
/* ******************************************
MAX7221 connections to 8x8 LED matrix
DP => ROW1 DG1 => COL1
A => ROW2 DG2 => COL2
B => ROW3 DG3 => COL3
... ...
G => ROW8 DG8 => COL8
Additionally:
1. Connect DIN, CLK, CS as shown below to digital inputs of the Arduino Nano
2. Connect VCC to 5V and GND to GND of the Arduino Nano
****************************************** */
const byte DataIn = 2; //D2 => DIN
const byte CLK = 3; //D3 => CLK
const byte LOAD = 4; //D4 => CS
//create a new object to control the 8x8 LED matrix
LedControl Matrix = LedControl(DataIn, CLK, LOAD, 1);
/* *************************************************
10k potentiometer to A7: paddle
unused analog pin for random seed
PWM enabled digital pin for audio
3 digital pins for remaining ball LEDs
Universal Control Button
************************************************* */
const byte potPaddle = A7;
const byte UnusedAnalog = A6;
const byte PWM_Audio = 9; //D9: Speaker+ ("LS+")
const byte BallDisplay[LED_count] = {5, 6, 7}; //LED #1 at D5, LED #2 at D6, LED #3 at D7
const byte UniversalButton = 8; //D8: "Button+"
/* ******************************************
Levels / Graphics / Patterns / Melodies
****************************************** */
//this is the level pattern of the "bricks"
//modify to create tougher or easier levels
const byte Levels = 3;
const byte bricks_levelheight[Levels] = {3, 4, 4};
const byte bricks_reset[Levels][4][8] PROGMEM =
{
{
{1, 1, 1, 1, 1, 1, 1, 1},
{1, 1, 1, 1, 1, 1, 1, 1},
{1, 1, 1, 1, 1, 1, 1, 1},
{0, 0, 0, 0, 0, 0, 0, 0}
},
{
{1, 0, 1, 1, 1, 1, 0, 1},
{1, 1, 0, 1, 1, 0, 1, 1},
{0, 1, 1, 0, 0, 1, 1, 0},
{0, 0, 1, 1, 1, 1, 0, 0}
},
{
{1, 1, 1, 1, 1, 1, 1, 1},
{1, 1, 1, 1, 1, 1, 1, 1},
{1, 1, 1, 1, 1, 1, 1, 1},
{1, 1, 1, 1, 1, 1, 1, 1}
}
};
// :-) smiley shown, if you win the game
const byte smiley_won[8] PROGMEM =
{
0b00111100,
0b01000010,
0b10100101,
0b10000001,
0b10100101,
0b10011001,
0b01000010,
0b00111100
};
// :-| smiley shown, if you loose the game
const byte smiley_lost[8] PROGMEM =
{
0b00111100,
0b01000010,
0b10100101,
0b10000001,
0b10111101,
0b10000001,
0b01000010,
0b00111100
};
// checkerboard pattern for selecting brightness
const byte select_brightness[8] PROGMEM =
{
0b10101010,
0b01010101,
0b10101010,
0b01010101,
0b10101010,
0b01010101,
0b10101010,
0b01010101
};
//left arrow: select leftmost paddle position
const byte arrow_left[8] PROGMEM =
{
0b00000000,
0b00010000,
0b00100000,
0b01111110,
0b00100000,
0b00010000,
0b00000000,
0b00000000
};
//right arrow: select rightmost paddle position
const byte arrow_right[8] PROGMEM =
{
0b00000000,
0b00001000,
0b00000100,
0b01111110,
0b00000100,
0b00001000,
0b00000000,
0b00000000
};
//EEPROM: store current configuration
const byte eeprom_store[8] PROGMEM =
{
0b11111111,
0b10000001,
0b10111101,
0b10100101,
0b10100101,
0b10111101,
0b10000001,
0b11111111
};
//EEPROM: revert to factory default
const byte eeprom_defaults[8] PROGMEM =
{
0b10000001,
0b01000010,
0b00100100,
0b00011000,
0b00011000,
0b00100100,
0b01000010,
0b10000001,
};
//question mark: enter multiplayer mode
const byte question_multiplayer[8] PROGMEM =
{
0b00011000,
0b00000100,
0b00011000,
0b00100000,
0b00011000,
0b00000000,
0b00001000,
0b00000000
};
const byte yes_multiplayer[8] PROGMEM =
{
0b00011000,
0b00011000,
0b00011000,
0b00011000,
0b00011000,
0b00000000,
0b00011000,
0b00011000
};
//maximum length of any melody
//zero padding of all melodyies needs to be adjusted when changing this
//and hard coded [12] in playMelody function header
const byte melody_maxlen = 12;
//the larger, the quicker the melodies are played
const float melody_speed = 2.5f;
const byte melody_advance_level_len = 7;
const unsigned int melody_advance_level[2][melody_maxlen] PROGMEM =
{
{ 392, 392, 392, 440, 392, 494, 523, 0, 0, 0, 0, 0}, //frequency
{ 500, 250, 250, 500, 1000, 500, 2000, 0, 0, 0, 0, 0} //duration, 1000 = 1 full beat
};
/* *************************************************
NRF24L01+ radio
************************************************* */
//reflect the wiring on the PCB
const byte RadioCE = A0;
const byte RadioCS = A1;
//hardcoded address for sending and listening
//if we'd ever think about un-jam-ing, some other logic needs to be developed including
//a "root" address and then different addresses plus different channel
byte RadioAddress[6] = {"TELEB"};
//need to be 4 bytes long (or, if RadioPayloadSize is other than 4 an appropriate memory buffer)
unsigned long RadioMasterToken = 2309; //initial negotiation: signal that this device is master
unsigned long RadioSlaveToken = 4711; //initial negotiation: signal that this device is slave
unsigned long RadioMasterWaitQ = 2310; //master waiting to start while slave still decides
unsigned long RadioSlaveWaitA = 4712; //slave acknowledging that it is ready to play
unsigned long RadioMasterSCP = 7; //master's poll code when slave is in speed change mode
//small number not interfere with the bitfields during mode changes
unsigned long RadioMasterSCA = 2312; //master's ACK when slave ends speed change mode
//size of all send and receive packets, should be: 4; maximum possible: 32
//changing this from 4 to another size leads to multiple code pieces that need adjustments
const byte RadioPayloadSize = 4;
const byte RadioPipe = 1; //we only need one pipe, hard code it to 1
//milliseconds, until next Radio command, needs to be timed together with RadioWait_Max
const int RadioCycle = 200;
unsigned long Last_RadioCycle = 0;
//wait and listen randomly between RadioWaitMin and
//RadioWaitMax, before
const int RadioWait_Min = 500;
const int RadioWait_Max = 850; //this needs to be carefully timed due to RadioTimeOutVal
unsigned long RadioWait = 0;
//mechanism for handling cases, where the transmission is not working any more,
//e.g. the other player switches-off his device, walks out of range, etc.
unsigned long RadioTimedOut = 0;
const unsigned long RadioTimeOutVal = 3000; //after 3 seconds of "nothing", the radio is considered as timed-out
//save battery: time in milliseconds after which the device powers down
//the radio and ignores further tennis request
const unsigned long RadioPowerSaveTime = 120000;
//avoid jamming the ACK FIFO (TX FIFO) by keeping track of uploaded ACK payloads
boolean RadioACKuploaded = false;
//create a new radio object
RF24 Radio(RadioCE, RadioCS);
//master/slave state machine
enum
{
//some checks are done via "> rmNone", i.e. it is important that the modes are in order
rmIgnore = -2, //do not scan any more, but ignore other TeleBall devices
rmNone = -1, //no other TeleBall device found
//initial find devices and ask the question
rmMaster_init = 0, //about to enter Master mode
rmSlave_init = 1, //about to enter Slave mode
rmMaster_wait = 2, //waiting for Slave to start the game
rmSlave_wait = 3, //waiting for Master to start the game
//standard run mode
rmMaster_run = 4, //running in Master mode
rmSlave_run = 5, //running in Slave mode
//special modes for reset and speed adjustments
rmMaster_reset = 6, //Master device resetting
rmSlave_reset = 7, //Slave device resetting
rmMaster_speedset_by_Master = 8, //Master device in speed set mode
rmMaster_speedset_by_Slave = 9, //Master device listening to Slave's speed set
rmSlave_speedset_by_Master = 10, //Slave device listening to Master's speed set
rmSlave_speedset_by_Slave = 11 //Slave device in speed set mode
} RadioMode = rmNone;
//RadioGameDataFromMaster is the payload sent from master to slave
//IMPORTANT: BITFIELD NEEDS TO BE RadioPayloadSize IN SIZE, so padding is used
struct //use bit fields to squeeze everything in two bytes
{
unsigned int Paddle : 3;
unsigned int BallX : 3;
unsigned int BallY : 3;
unsigned int TennisPoints : 2;
unsigned int WonOrLostState : 1;
unsigned int Reset : 1; //master initiates reset
unsigned int Reset_Ack : 1; //master acknowledges a reset request send by slave
unsigned int SpeedSet : 1; //master initiated a change in the game's speed
unsigned int SpeedSet_Ack : 1; //master acknowledges a speedset request by slave
unsigned int Sound_Wall : 1; //make slave play: ball hits a wall
unsigned int Sound_Paddle : 1; //make slave play: ball hits a paddle
unsigned int Sound_Lost : 1; //make slave play: lost ball
//the padding cannot be arbitrarly done as "padding : <amount>" due to compiler specifics
unsigned int unused_padding1 : 1;
unsigned int unused_padding2 : 1;
unsigned int unused_padding3 : 1;
unsigned int unused_padding4 : 1;
unsigned int unused_padding5 : 1;
unsigned int unused_paddingx : 8;
} RadioGameDataFromMaster;
//RadioGameDataFromSlave is the payload sent from slave to master during ACK
//IMPORTANT: BITFIELD NEEDS TO BE RadioPayloadSize IN SIZE, so padding is used
struct //also uses bit fields
{
unsigned int Paddle : 3;
unsigned int Reset : 1; //slave initiates reset
unsigned int Reset_Ack : 1; //slaves acknowledges a reset initiated by the master
unsigned int SpeedSet : 1; //slave initiates a change in the game's speed
unsigned int SpeedSet_Ack : 1; //slave acknowledges a speedset initiated by the master
//the padding cannot be arbitrarly done as "padding : <amount>" due to compiler specifics
unsigned int unused_padding1 : 1;
unsigned int unused_paddingx : 24;
} RadioGameDataFromSlave;
//sends "sendbuffer" to the receiver: returns true, if sending was successful and the ACK payload
//could be received, else it returns false
//needs preconfigured connections via Radio.openWritingPipe and a receiver, that acknowledges
//the successful receiving via sending a response payload which will be buffered in "ackpayloadbuffer"
//all buffers need to be at least "RadioPayloadSize" in size
boolean radioSend(void* sendbuffer, void* ackpayloadbuffer)
{
//send payload
if (Radio.write(sendbuffer, RadioPayloadSize))
{
//if successfully sent, check if an ACK payload is available
//(since Radio.write is a blocking function that waits for ACK, this should always work)
if (Radio.available())
{
boolean success = false; //be conservative
//use a loop to empty the read FIFO
//just in case there is more than one ACK payload pending: take the newest
byte pipeNo;
while (Radio.available(&pipeNo))
{
success = true;
//retrieve the ACK payload
Radio.read(ackpayloadbuffer, RadioPayloadSize);
}
//success only if the sending worked AND the ACK payload could be retrieved
if (success)
{
RadioTimedOut = millis() + RadioTimeOutVal; //success, so reset the timeout timer
return true;
}
}
}
//something failed, i.e. the sending itself or the reading of the payload
return false;
}
boolean radioReceive(void* receivebuffer, void* ackpayloadbuffer)
{
//RadioAckuploaded mechanism:
//prevent ACK/TX FIFO jam on the receiving device
//(leads to "forgetting" all ACK payload updates after the first one until
//new data is received; presumably the roundtrips are so fast that this
//effect can be neglected
if (!RadioACKuploaded)
{
RadioACKuploaded = true;
//upload ACK payload to TX FIFO, i.e. next read AUTO ACK will use this one
Radio.writeAckPayload(RadioPipe, ackpayloadbuffer, RadioPayloadSize);
}
if (Radio.available())
{
byte pipeNo;
while (Radio.available(&pipeNo))
Radio.read(receivebuffer, RadioPayloadSize);
RadioACKuploaded = false;
RadioTimedOut = millis() + RadioTimeOutVal; //success, so reset the timeout timer
return true;
}
return false;
}
//kind of battery and performance saving scanning and negotiation protocol
//that scans for another player/device and then negotiates: who is master and who is slave
//concept: the first who receives a master token is the slave and sends an ACK with a slave token
//due to the random sending intervals and the fact, that the NRF24L01+ buffers received data in
//a FIFO, this method is working very stable
void radioScanAndDetermineMode()
{
//rmIgnore needs a hard reset of the device to be able to find another device again
if (RadioMode == rmIgnore)
return;
//power save mode for longer battery life
//after RadioPowerSaveTime milliseconds, the radio is powered down
if (RadioMode == rmNone && millis() > RadioPowerSaveTime)
{
RadioMode = rmIgnore;
Radio.powerDown();
}
//do the radio related operations only each RadioCycle milliseconds
unsigned long Millis = millis();
if (!Last_RadioCycle || Millis - Last_RadioCycle > RadioCycle)
{
Last_RadioCycle = Millis;
//scan only, if no other device is detected
if (RadioMode == rmNone)
{
//scan only each RadioWait milliseconds which is in a random interval
if (!RadioWait)
RadioWait = Millis + random(RadioWait_Min, RadioWait_Max);
if (Millis >= RadioWait)
{
RadioWait = 0;
unsigned long payload;
//data received? if yes and if it is the master token, then this device is the slave
//we use ACK payload mechanism to confirm by sending the slave token
if (radioReceive(&payload, &RadioSlaveToken) && payload == RadioMasterToken)
{
RadioMode = rmSlave_init; //enter question mode ("want to play tennis?")
game_mode = gmTennis; //switch to tennis game loop
}
//no data received and random interval is over: send a master token
//if the other side confirms by a slave token, then this device is the master
else
{
//switch to sending mode
Radio.stopListening();
Radio.openWritingPipe(RadioAddress);
if (radioSend(&RadioMasterToken, &payload) && payload == RadioSlaveToken)
{
RadioMode = rmMaster_init; //enter question mode ("want to play tennis?")
game_mode = gmTennis; //switch to tennis game loop
}
//if no ACK or wrong ACK: consider it as no device found and start over by continuing to listen
if (RadioMode == rmNone)
{
//switch back to receiving mode
Radio.openReadingPipe(RadioPipe, RadioAddress);
Radio.startListening();
}
}
}
}
}
}
void radioEmptyReadFIFO()
{
byte pipeNo;
byte NullDevice[RadioPayloadSize];
while (Radio.available(&pipeNo))
Radio.read(&NullDevice, RadioPayloadSize);
}
/* ******************************************
SETUP
****************************************** */
void setup()
{
// Serial.begin(57600); //debug
// eepromReset(); //debug
//EEPROM
//if it has never been writen: fill with default
if (!eepromCheckFingerprint())
eepromWriteFingerprintAndDefaults();
else
eepromReadSettings();
//initialize seed with floating analog number
randomSeed(analogRead(UnusedAnalog));
//The MAX72XX is in power-saving mode on startup, we have to do a wakeup call
Matrix.shutdown(0, false);
//Set the brightness
Matrix.setIntensity(0, Intensity);
//set the digital ports of the Arduino
//that we use for audio output and for
//driving the LEDs to OUTPUT
pinMode(PWM_Audio, OUTPUT);
for (int i = 0; i < LED_count; i++)
pinMode(BallDisplay[i], OUTPUT);
//digital input for the Universal Button
//activating the pullup means and wiring as described
//above means, that the input will read HIGH when
//the switch is open and LOW when the switch is pressed
pinMode(UniversalButton, INPUT_PULLUP);
//setup the nRF23L01+
Radio.begin();
Radio.setAutoAck(1); //Ensure autoACK is enabled
Radio.enableAckPayload(); //Allow optional ack payloads
Radio.setRetries(0, 4); //Smallest time between retries (shall be 0 == 250ms), max no. of retries (shall be 4)
Radio.setPayloadSize(RadioPayloadSize); //standard: 4-byte payload
Radio.setDataRate(RF24_1MBPS); //lower data rate increases the robustness
Radio.setPALevel(RF24_PA_MAX); //high power consumption, high distance
Radio.openReadingPipe(RadioPipe, RadioAddress); //open read pipe on hard coded pipe no and address
Radio.startListening(); //Start listening
Radio.powerUp();
}
/* ******************************************
RESET
****************************************** */
//reset BreakOut means: new game at level 1
void reset_BreakOut()
{
//copy first level pattern to playfield
Level = 0;
breakoutFillLevel_from_PROGMEM();
//reset ball counter
Balls_Old = 0;
Balls = Balls_max;
//random ball start position and direction
BallDX = BallDY = 0;
randomBall();
//first respawn is longer than the other ones
respawn_timer = millis() + (2 * respawn_duration);
}
//reset Tennis means: both devices back to the question mode
void reset_Tennis()
{
//reset score
TennisPoints = 0;
TennisPoints_Old = -1;
TennisPoints_Remote = 0;
tennisRespawn(2 * respawn_duration);
}
void reset()
{
Matrix.clearDisplay(0);
//reset Paddle
handleInput();
LastPaddlePos = Paddle;
//reset housekeepking variables
Timer = 0;
perform_reset = false;
WonOrLostState = false;
MultiplayerWaitStart = 0;
MultiplayerQuestionTime = 0;
MultiplayerQuestionButton = false;
switch (game_mode)
{
//reset game
case gmBreakOut: reset_BreakOut(); break;
//back to question mode: one more round of Tennis or back to BreakOut
case gmTennis:
//reset initiated on the master device
if (RadioMode == rmMaster_run)
RadioMode = rmMaster_reset;
//reset initiated on the slave device
else
RadioMode = rmSlave_reset;
//reset local stats
reset_Tennis();
break ;
}
}
/* ******************************************
ORIENTATION
****************************************** */
void handleOrientation()
{
//support "upside-down" orientations by inverting the Paddle
if (Orientation >= 2)
Paddle = 6 - Paddle;
}
//draws a pixel while respecting the orientation
void putPixel(byte x, byte y, boolean on)
{
switch (Orientation)
{
#ifdef DUINOKIT
case 0: Matrix.setLed(0, x, y, on); break;
#else
case 0: Matrix.setLed(0, 7 - x, y, on); break;
#endif
case 1: Matrix.setLed(0, y, 7 - x, on); break;
case 2: Matrix.setLed(0, 7 - x, 7 - y, on); break;
case 3: Matrix.setLed(0, 7 - y, x, on); break;
}
}
/* ******************************************
CONFIGURATION
****************************************** */
void handleBrightness()
{
//change the brightness of the LEDs
if (Intensity != Intensity_Old)
{
Matrix.setIntensity(0, Intensity);
Intensity_Old = Intensity;
}
}
void adjustSpeed()
{
//to avoid a flickering display: only set the local Speed variable if this device is in single player mode
//or this device is the device that initited the speedset mode (and therefore is managing the Speed variable)
if (RadioMode <= rmNone || RadioMode == rmMaster_speedset_by_Master || RadioMode == rmSlave_speedset_by_Slave)
{
Speed_Old = Speed;
Speed = map(analogRead(potPaddle), 0, 1023, speed_min, speed_max);
Speed = (Speed + Speed_Old) / 2;
Speed = constrain(Speed, speed_max, speed_min); //reversed order, as speed_max is a low number (max means low delay)
}
//in tennis mode: send speed or receive speed from the device that initiated the speedset mode
//tennis is assumed when any radio mode is active
if (RadioMode > rmNone)
if (!tennisHandleAdjustSpeed())
return; //leave the speed set mode and prevent the screen from being scrambled
//flicker-free mechanism of displaying the speed
//as in contrast to Matrix.clearDisplay(0) only the "necessary" pixels are cleared
int ledamount = map(Speed, speed_min, speed_max, 1, 64);
int the_rest = 64 - ledamount;
byte x = 0;
byte y = 0;
while (ledamount--)
{
putPixel(x, y, true);
y++;
if (y == 8)
{
x++;
y = 0;
}
}
while (the_rest--)
{
putPixel(x, y, false);
y++;
if (y == 8)
{
x++;
y = 0;
}
}
}
void adjustBrightness()
{
drawPatternBits_from_PROGMEM(select_brightness, 8);
Intensity = map(analogRead(potPaddle), 0, 1023, 0, 15);
//tennis only: take care that the other device does not time out by sending the speed
if (RadioMode > rmNone)
tennisHandleAdjustSpeed();
}
void adjustPaddle()
{
int poti = analogRead(potPaddle);
if (game_mode == gmPaddleLeft)
{
drawPatternBits_from_PROGMEM(arrow_left, 8);
PotiLeftmost = poti;
}
else
{
drawPatternBits_from_PROGMEM(arrow_right, 8);
PotiRightmost = poti;
}
//tennis only: take care that the other device does not time out by sending the speed
if (RadioMode > rmNone)
tennisHandleAdjustSpeed();
}
/* ******************************************
EEPROM ROUTINES
****************************************** */
void eepromWriteInt(int address, int value)
{
EEPROM.write(address, (byte) value); //write low order byte
EEPROM.write(address + 1, (byte) (value >> 8)); //write high order byte
}
int eepromReadInt(int address)
{
return (EEPROM.read(address + 1) << 8) + EEPROM.read(address);
}
void eepromReset()
{
for (int i = 0; i < 255; i++)
EEPROM.write(i, 255);
}
boolean eepromCheckFingerprint()
{
boolean FingerprintMatch = true;
for (int i = 0; i < EEPROM_Fingerprint_len; i++)
{
// Serial.print("cfp: i = "); Serial.print(i); Serial.print(" value = "); Serial.println(EEPROM.read(locFingerprint + i));
if (EEPROM.read(locFingerprint + i) != EEPROM_Fingerprint[i])
FingerprintMatch = false;
}
return FingerprintMatch;
}
void eepromWriteFingerprintAndDefaults()
{
// Serial.println("EEPROM: Writing fingerprint and defaults.");
//write fingerprint
for (int i = 0; i < EEPROM_Fingerprint_len; i++)
{
// Serial.print("wfp: i = "); Serial.print(i); Serial.print(" value = "); Serial.println(EEPROM_Fingerprint[i]);
EEPROM.write(locFingerprint + i, EEPROM_Fingerprint[i]);
}
//set variables to factory default and write them to EEPROM
Speed = speed_default;
Intensity = intensity_default;
PotiLeftmost = poti_leftmost_default;
PotiRightmost = poti_rightmost_default;
eepromWriteSettings();
}
void eepromReadSettings()
{