Skip to content

Commit 05dccf3

Browse files
Add "Joy of Arcada" Arduino sketch
1 parent 1b1a288 commit 05dccf3

3 files changed

Lines changed: 7975 additions & 0 deletions

File tree

Joy_of_Arcada/Joy_of_Arcada.ino

Lines changed: 345 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,345 @@
1+
// JOY: Adafruit PyGamer or PyBadge as a friendly USB HID game controller.
2+
// Requires Arcada and Audio libraries.
3+
4+
#include <Adafruit_Arcada.h>
5+
#include <Audio.h>
6+
#include <Keyboard.h>
7+
#include "graphics.h" // Face bitmaps are here
8+
#include "sound.h" // "Pew" sound is here
9+
10+
// Keys corresponding to the various controller buttons.
11+
// If special keycodes are required (shift, arrows, etc.), docs are here:
12+
// https://www.arduino.cc/en/Reference/KeyboardModifiers
13+
#define KEY_A 'z'
14+
#define KEY_B 'x'
15+
#define KEY_START '1'
16+
#define KEY_SELECT '5'
17+
#define KEY_UP KEY_UP_ARROW
18+
#define KEY_DOWN KEY_DOWN_ARROW
19+
#define KEY_LEFT KEY_LEFT_ARROW
20+
#define KEY_RIGHT KEY_RIGHT_ARROW
21+
22+
// MASK_BUTTONS is used to isolate button inputs from direction inputs
23+
// on PyGamer. MASK_PEW is to isolate just the A & B buttons as triggers
24+
// for the occasional random "pew!" noises.
25+
#define MASK_BUTTONS (ARCADA_BUTTONMASK_A | ARCADA_BUTTONMASK_B | ARCADA_BUTTONMASK_START | ARCADA_BUTTONMASK_SELECT)
26+
#define MASK_PEW (ARCADA_BUTTONMASK_A | ARCADA_BUTTONMASK_B)
27+
#define UPPER_LID_SIZE (EYES_HEIGHT * 3 / 4)
28+
#define LOWER_LID_SIZE (EYES_HEIGHT - UPPER_LID_SIZE)
29+
30+
Adafruit_Arcada arcada;
31+
int8_t xState, yState; // Joystick state
32+
float eyeAngle = 0.0;
33+
bool blinking = false;
34+
uint32_t blinkStartTime, blinkDuration;
35+
uint8_t mouthState = 0, priorMouthState = 0, mouthCounter = 0;
36+
uint32_t stickmask = 0;
37+
38+
AudioPlayMemory sound;
39+
AudioOutputAnalogStereo audioOut;
40+
AudioConnection c0(sound, 0, audioOut, 0);
41+
42+
void setup() {
43+
Serial.begin(9600);
44+
//while(!Serial); // Wait for Serial Console before continuing
45+
46+
if(!arcada.begin()) {
47+
Serial.println(F("Arcada failed to init"));
48+
for(;;);
49+
}
50+
arcada.displayBegin();
51+
arcada.setBacklight(255);
52+
arcada.fillScreen(ARCADA_BLACK);
53+
Keyboard.begin();
54+
AudioMemory(10);
55+
56+
// At start, draw the entire blank/neutral face centered on screen
57+
arcada.drawRGBBitmap(
58+
(arcada.width() - FACE_WIDTH ) / 2,
59+
(arcada.height() - FACE_HEIGHT) / 2,
60+
(uint16_t *)face, FACE_WIDTH, FACE_HEIGHT);
61+
62+
// Create an offscreen framebuffer that's just the bounding
63+
// rectangle of the animayed face parts (eyes, mouth).
64+
arcada.createFrameBuffer(EYES_WIDTH, 94);
65+
GFXcanvas16 *canvas = arcada.getCanvas();
66+
uint16_t *buffer = canvas->getBuffer();
67+
// Fill canvas white, then draw mouth in "idle" position.
68+
memset(buffer, 0xFF, canvas->width() * canvas->height() * 2);
69+
// Most of the animation is done using memcpy() and bitmaps that are
70+
// carefully planned to be the same width as the canvas...so we can
71+
// just move entire scanlines this way, simplifies the code.
72+
memcpy(&buffer[canvas->width() * (canvas->height() - MOUTH_HEIGHT)],
73+
mouth_idle, MOUTH_WIDTH * MOUTH_HEIGHT * 2);
74+
75+
// Initialize button state
76+
arcada.readButtons();
77+
uint32_t b = arcada.justPressedButtons();
78+
if(b & ARCADA_BUTTONMASK_A) Keyboard.press(KEY_A);
79+
if(b & ARCADA_BUTTONMASK_B) Keyboard.press(KEY_B);
80+
if(b & ARCADA_BUTTONMASK_START) Keyboard.press(KEY_START);
81+
if(b & ARCADA_BUTTONMASK_SELECT) Keyboard.press(KEY_SELECT);
82+
83+
#if defined(ARCADA_JOYSTICK_X) && defined(ARCADA_JOYSTICK_Y)
84+
// Initialize joystick state. Although the Arcada lib has stuff for
85+
// for analog-stick-to-button-press conversion, this code had some nice
86+
// hysteresis built in, so I'm leaving it here for now, though bulkier.
87+
int pos = arcada.readJoystickX() + 512;
88+
if(pos > (1023 * 4 / 5)) {
89+
Keyboard.press(KEY_RIGHT);
90+
xState = 1;
91+
} else if(pos < (1023 / 5)) {
92+
Keyboard.press(KEY_LEFT);
93+
xState = -1;
94+
} else {
95+
xState = 0;
96+
}
97+
pos = arcada.readJoystickY() + 512;
98+
if(pos > (1023 * 4 / 5)) {
99+
Keyboard.press(KEY_DOWN);
100+
yState = 1;
101+
} else if(pos < (1023 / 5)) {
102+
Keyboard.press(KEY_UP);
103+
yState = -1;
104+
} else {
105+
yState = 0;
106+
}
107+
#else
108+
if(b & ARCADA_BUTTONMASK_RIGHT) Keyboard.press(KEY_RIGHT);
109+
if(b & ARCADA_BUTTONMASK_LEFT) Keyboard.press(KEY_LEFT);
110+
if(b & ARCADA_BUTTONMASK_DOWN) Keyboard.press(KEY_DOWN);
111+
if(b & ARCADA_BUTTONMASK_UP) Keyboard.press(KEY_UP);
112+
stickmask = b & ~MASK_BUTTONS;
113+
#endif
114+
}
115+
116+
void loop() {
117+
int dx, dy, upperLidRows=0, lowerLidRows=0, openRows=EYES_HEIGHT;
118+
float a;
119+
uint32_t b;
120+
GFXcanvas16 *canvas = arcada.getCanvas();
121+
uint16_t *buffer = canvas->getBuffer();
122+
123+
arcada.readButtons();
124+
b = arcada.justPressedButtons();
125+
if(b & ARCADA_BUTTONMASK_A) Keyboard.press(KEY_A);
126+
if(b & ARCADA_BUTTONMASK_B) Keyboard.press(KEY_B);
127+
if(b & ARCADA_BUTTONMASK_START) Keyboard.press(KEY_START);
128+
if(b & ARCADA_BUTTONMASK_SELECT) Keyboard.press(KEY_SELECT);
129+
// If one of the pewing buttons was pressed, and mouth is not currently
130+
// in the "py" position (so, either idle or "oo"ing), there's a random
131+
// chance (1/8) of triggering a new "pew!" sound & animation...
132+
if((b & MASK_PEW) && (mouthState != 1) && !random(8)) {
133+
mouthState = 1; // Set flag, actual pew starts in the mouth code later
134+
}
135+
b = arcada.justReleasedButtons();
136+
if(b & ARCADA_BUTTONMASK_A) Keyboard.release(KEY_A);
137+
if(b & ARCADA_BUTTONMASK_B) Keyboard.release(KEY_B);
138+
if(b & ARCADA_BUTTONMASK_START) Keyboard.release(KEY_START);
139+
if(b & ARCADA_BUTTONMASK_SELECT) Keyboard.release(KEY_SELECT);
140+
141+
#if defined(ARCADA_JOYSTICK_X) && defined(ARCADA_JOYSTICK_Y)
142+
143+
// Analog joystick input with fancy hysteresis
144+
145+
dx = arcada.readJoystickX(), // Joystick position relative to center
146+
dy = arcada.readJoystickY(); // (+/- 512)
147+
148+
// Handle joystick X axis
149+
int pos = dx + 512;
150+
if(xState == 1) { // Stick to right last we checked?
151+
if(pos < (1023 * 3 / 5)) { // Moved left beyond hysteresis threshold?
152+
Keyboard.release(KEY_RIGHT); // Release right arrow key
153+
xState = 0; // and set state to neutral center zone
154+
}
155+
} else if(xState == -1) { // Stick to left last we checked?
156+
if(pos > (1023 * 2 / 5)) { // Moved right beyond hysteresis threshold?
157+
Keyboard.release(KEY_LEFT); // Release left arrow key
158+
xState = 0; // and set state to neutral center zone
159+
}
160+
}
161+
// This is intentionally NOT an 'else' -- state CAN change twice here!
162+
// First change releases left/right keys, second change presses new left/right
163+
if(!xState) { // Stick X previously in neutral center zone?
164+
if(pos > (1023 * 4 / 5)) { // Moved right?
165+
Keyboard.press(KEY_RIGHT); // Press right arrow key
166+
xState = 1; // and set state to right
167+
} else if(pos < (1023 / 5)) { // Else moved left?
168+
Keyboard.press(KEY_LEFT); // Press left arrow key
169+
xState = -1; // and set state to left
170+
}
171+
}
172+
173+
// Handle joystick Y axis
174+
pos = dy + 512;
175+
if(yState == 1) { // Stick down last we checked?
176+
if(pos < (1023 * 3 / 5)) { // Moved up beyond hysteresis threshold?
177+
Keyboard.release(KEY_DOWN); // Release down key
178+
yState = 0; // and set state to neutral center zone
179+
}
180+
} else if(yState == -1) { // Stick up last we checked?
181+
if(pos > (1023 * 2 / 5)) { // Moved down beyond hysteresis threshold?
182+
Keyboard.release(KEY_UP); // Release up key
183+
yState = 0; // and set state to neutral center zone
184+
}
185+
}
186+
// See note above re: not an else
187+
if(!yState) { // Stick Y previously in neutral center zone?
188+
if(pos > (1023 * 4 / 5)) { // Moved down?
189+
Keyboard.press(KEY_DOWN); // Press down key
190+
yState = 1; // and set state to down
191+
} else if(pos < (1023 / 5)) { // Else moved up?
192+
Keyboard.press(KEY_UP); // Press up key
193+
yState = -1; // and set state to up
194+
}
195+
}
196+
197+
// If there's no stick input, have Joy look up, as if watching game.
198+
if((abs(dx) < 30) && (abs(dy) < 30)) {
199+
dx = 0;
200+
dy = -1;
201+
}
202+
203+
#else
204+
205+
// PyBadge directional button input
206+
207+
b = arcada.justPressedButtons() & ~MASK_BUTTONS;
208+
stickmask |= b;
209+
if(b & ARCADA_BUTTONMASK_RIGHT) Keyboard.press(KEY_RIGHT);
210+
if(b & ARCADA_BUTTONMASK_LEFT) Keyboard.press(KEY_LEFT);
211+
if(b & ARCADA_BUTTONMASK_DOWN) Keyboard.press(KEY_DOWN);
212+
if(b & ARCADA_BUTTONMASK_UP) Keyboard.press(KEY_UP);
213+
b = arcada.justReleasedButtons() & ~MASK_BUTTONS;
214+
if(b & ARCADA_BUTTONMASK_RIGHT) Keyboard.release(KEY_RIGHT);
215+
if(b & ARCADA_BUTTONMASK_LEFT) Keyboard.release(KEY_LEFT);
216+
if(b & ARCADA_BUTTONMASK_DOWN) Keyboard.release(KEY_DOWN);
217+
if(b & ARCADA_BUTTONMASK_UP) Keyboard.release(KEY_UP);
218+
stickmask &= ~b;
219+
// If there's no stick input, have Joy look up, as if watching game.
220+
int dir = stickmask ? stickmask : ARCADA_BUTTONMASK_UP;
221+
switch(dir) {
222+
case ARCADA_BUTTONMASK_RIGHT:
223+
dx = 1; dy = 0; break;
224+
case ARCADA_BUTTONMASK_RIGHT | ARCADA_BUTTONMASK_UP:
225+
dx = 1; dy = -1; break;
226+
case ARCADA_BUTTONMASK_UP:
227+
dx = 0; dy = -1; break;
228+
case ARCADA_BUTTONMASK_LEFT | ARCADA_BUTTONMASK_UP:
229+
dx = -1; dy = -1; break;
230+
case ARCADA_BUTTONMASK_LEFT:
231+
dx = -1; dy = 0; break;
232+
case ARCADA_BUTTONMASK_LEFT | ARCADA_BUTTONMASK_DOWN:
233+
dx = -1; dy = 1; break;
234+
case ARCADA_BUTTONMASK_DOWN:
235+
dx = 0; dy = 1; break;
236+
case ARCADA_BUTTONMASK_RIGHT | ARCADA_BUTTONMASK_DOWN:
237+
dx = 1; dy = 1; break;
238+
}
239+
#endif
240+
241+
// Joy's eyes don't directly follow the joystick. They're always
242+
// looking in some direction, pupils are kept around the perimeter
243+
// of the eyes.
244+
245+
a = atan2(dy, dx); // Joystick angle (+/- M_PI)
246+
// Deal with 'seam crossing' at +/- 180 degrees:
247+
if(fabs(a - eyeAngle) > M_PI) {
248+
if(eyeAngle >= 0.0) eyeAngle -= M_PI * 2.0;
249+
else eyeAngle += M_PI * 2.0;
250+
}
251+
eyeAngle = (eyeAngle * 0.9) + (a * 0.1); // Low-pass filter old/new angle
252+
253+
// Determine position of pupils; center +/- 12 pixels
254+
dx = (int)(cos(eyeAngle) * 11.0 + 13.5),
255+
dy = (int)(sin(eyeAngle) * 11.0 + 13.5);
256+
// When eyes are blinking, overwrite sections of offscreen canvas
257+
// with the 'closed' eye image. They converge at the 3/4 mark
258+
// (i.e. upper lid is 3/4 of height, lower lid is 1/4).
259+
if(blinking) { // Currently blinking?
260+
uint32_t t = micros() - blinkStartTime; // Since how long?
261+
if(t > blinkDuration) { // Past end of blink time?
262+
blinking = false; // Turn off blink flag
263+
} else { // Else in mid-blink...
264+
int amount = 900 * t / blinkDuration; // Relative time, 0-900
265+
// First third of blink is fast closing, last 2/3 is slower opening
266+
if(amount > 300) amount = 300 - ((amount - 300) / 2); // 0-300 blinkyness
267+
if(amount > 256) amount = 256; // Clip to 256
268+
upperLidRows = UPPER_LID_SIZE * amount / 256; // How much upper lid, in pixels?
269+
lowerLidRows = LOWER_LID_SIZE * amount / 256; // How much lower lid, in pixels?
270+
openRows = EYES_HEIGHT - upperLidRows - lowerLidRows;
271+
}
272+
} else { // Not blinking
273+
if(!random(250)) { // Each time here, 1/250 chance of new blink
274+
blinking = true;
275+
blinkDuration = random(150000, 250000);
276+
blinkStartTime = micros();
277+
}
278+
}
279+
280+
// Wait for prior DMA transfer to complete; don't modify canvas
281+
// while a screen update is currently in progress. (This is assuming
282+
// SPI DMA is enabled in Adafruit_SPITFT.h. If it is not, that's OK,
283+
// this function call simply compiles to nothing in that case.)
284+
arcada.dmaWait();
285+
286+
if(openRows) {
287+
// Draw the open section of the eyes, then draw the pupils on top
288+
// of this. The pupil-drawing may partly obliterate the eyelid areas,
289+
// so those are drawn last.
290+
memcpy(&buffer[EYES_WIDTH * upperLidRows],
291+
&eyes_open[upperLidRows], EYES_WIDTH * openRows * 2);
292+
canvas->drawRGBBitmap(dx, dy, (uint16_t *)pupil, // Left
293+
(uint8_t *)pupil_mask, PUPIL_WIDTH, PUPIL_HEIGHT);
294+
canvas->drawRGBBitmap(64 + dx, dy, (uint16_t *)pupil, // Right
295+
(uint8_t *)pupil_mask, PUPIL_WIDTH, PUPIL_HEIGHT);
296+
if(upperLidRows) {
297+
memcpy(buffer, eyes_closed, EYES_WIDTH * upperLidRows * 2);
298+
}
299+
if(lowerLidRows) {
300+
memcpy(&buffer[EYES_WIDTH * (EYES_HEIGHT - lowerLidRows)],
301+
&eyes_closed[EYES_HEIGHT - lowerLidRows],
302+
EYES_WIDTH * lowerLidRows * 2);
303+
}
304+
} else {
305+
// Eyes are closed, super simple...
306+
memcpy(buffer, eyes_closed, EYES_WIDTH * EYES_HEIGHT * 2);
307+
}
308+
309+
// Mouth is re-drawn in canvas only when it changes.
310+
if(mouthState != priorMouthState) {
311+
priorMouthState = mouthState;
312+
uint16_t *ptr;
313+
if(mouthState == 0) { // Idle
314+
ptr = (uint16_t *)mouth_idle;
315+
} else if(mouthState == 1) { // "Pew!" just started ("py")
316+
ptr = (uint16_t *)mouth_py;
317+
// New 'pew' sound/animation started...
318+
mouthCounter = 0; // For counting frames
319+
digitalWrite(ARCADA_SPEAKER_ENABLE, HIGH); // Speaker on
320+
sound.play(pew); // Start the sound
321+
} else { // "Pew!" underway ("oo")
322+
ptr = (uint16_t *)mouth_oo;
323+
}
324+
// Copy one of the above three images (from ptr) to bottom of canvas.
325+
memcpy(&buffer[canvas->width() * (canvas->height() - MOUTH_HEIGHT)],
326+
ptr, MOUTH_WIDTH * MOUTH_HEIGHT * 2);
327+
} else if(mouthState) { // Same position as before, but "pew!" playing
328+
if(mouthState == 1) { // "py"
329+
if(++mouthCounter > 12) mouthState++; // Hold mouth pursed for 12 frames
330+
} else { // "oo"
331+
if(!sound.isPlaying()) { // If end of "pew!" sound reached...
332+
mouthState = 0; // Set mouth back to idle
333+
digitalWrite(ARCADA_SPEAKER_ENABLE, LOW); // Speaker off
334+
}
335+
}
336+
}
337+
338+
// Redraw just the animated area of face. A big-endian DMA transfer
339+
// is used...this is fastest as it can continue in the background
340+
// (while we process input on the next frame).
341+
arcada.blitFrameBuffer(
342+
(arcada.width() - FACE_WIDTH ) / 2 + 13,
343+
(arcada.height() - FACE_HEIGHT) / 2 + 25,
344+
false, true); // Non-blocking, big-endian
345+
}

0 commit comments

Comments
 (0)