Hardware components | ||||||
| × | 1 | ||||
![]() |
| × | 6 | |||
| × | 2 | ||||
| × | 1 | ||||
![]() |
| × | 1 | |||
Software apps and online services | ||||||
![]() |
|
Ever wanted to play Holochess like Han and Chewie? I've always been fascinated by the iconic Holochess table from Star Wars, so I decided to create my own version: It's a one-player Holochess table that makes the pieces look like real holograms. Since I've had some experience with my floating display, I've tried out a different way to make aerial images as holograms (check out my "floating display project" for more details).
I recently came across a product from ASKA3D, a Japanese company that makes parts for aerial displays (https://aska3d.com). Their "ASKA3D-Plate" lets me get rid of two key components I used in my recent "floating display" project: namely, a beam splitter and a retro-reflector. You can just mount the plate over a display, and it'll give you a sharp and bright aerial image. It works using the modified corner reflector principle.
I decided to use this plate to build my version of Holochess since I can make game pieces appear even though semitransparent materials. This can make it look like something's popping up on a see-through surface.
But because of how it works, you can only see the image from one side (and the viewing angle is a bit narrow). So, I decided to make a single-player version of Holochess. Here's what my "Hologram generator" looks like: The game pieces (holochess monsters) are created by six small displays. Each display later shows one monster on the round chessboard. The displays are held up by a 3D printed structure and are powered by an Arduino Giga.
I mounted the ASKA 3D plate above the displays, tilting it slightly to boost the light efficiency. Finally, at the top, there's the semi-transparent, round holochess table field, centered in the characteristic round table design, which I also designed from scratch for this project.
GIGA AND 6 DISPLAYSI went with the Arduino Giga for my project because it's got enough processor speed, internal memory, DAC output, and an external 8MB SDRAM that I can use for graphics memory.I'm using six Waveshare 2.4-inch 320x240 TFT modules based on an ILI9341 controller for the displays (also available from other vendors) :https://www.waveshare.com/wiki/2.4inch_LCD_Module.The modules can be supplied with 3.3V and have an onboard display controller (ILI9341) containing a frame buffer. So, only the image updates need to be transferred, which reduces the main CPU's workload. The six displays use 20 IO pins, including the shared SPI pins running at several MHz. I connected them using a break-out board made from a stripe-prototype PCB. This time, I decided against designing my own PCB because it basically just involves distributing the signals and using a few components. All the displays are hooked up in parallel to the "SPI1" bus of the Giga. (D11 -> DIN MOSI SPI1 Transfer Output pin, D13 -> CLK SCK SPI1 Clock). They also use the same reset signal (D53). You can also find a list of all the pin connections in the Arduino code. The individual pins for each display are: Chip Select, DataCommand (0=command, 1=data), and Backlight control.
The two displays in the middle should be able to move to support battle scenes. I attached the center displays to Gantry components, which are commonly used in 3D printing. For the movement, I use FS90R servos with continuous rotation. They're basically PWM-controlled DC motors.
Surprisingly, I had a lot of trouble getting the servos to work right at first, which I hadn't expected. Servo control isn't a big deal for an Arduino, but it was a bit of a challenge with the GIGA at first. The reason is that the PWM outputs of the GIGA run at much higher frequencies (500 Hz), but a typical servo expects 50 Hz. So, I had to use a timer controlled by the mbed API to create the 50Hz PWM that was needed. Ultimately, it's no huge deal if you've got the know-how, but little hiccups like this can definitely slow down a project for a bit!
I designed a holochess table part for the given dimensions and tweaked it to fit my needs as a single-player version, omitting the second control part that's usually there. I designed it from scratch and it's printed in eight parts: six for the frame and two for the front panel. All 3D parts are linked below in case you want to replicate. I put in some extra effort to paint the ring-shaped table part to get a nice metallic finish, just like the original Holochess table. This involves some sanding, priming, followed by a base coat, metallic paint, and clear coat. You can find more details in my video. The cylindrical base was covered with flexible sheet metal around the unit.
The front of the table has the control unit, which has 12 LEDs showing the attack and defense stats of each monster. It also has an IC2 0.91" OLED display to show a tiny menu, an I2C rotary encoder, and two mechanical switches and pots. I used an Arduino Nano to control each element separately, mainly to avoid having a bunch of cables running from the top control unit to the main board. The Giga has plenty of UARTs, so I used a serial connection from the control board. At last, just three cables to the Giga and VCC/GND were needed.
Once the hardware was ready, I created a simple version of single-player Holochess where you can select the game pieces (monsters) using the rotary knob. To start the selection process, just set the game switch on the control board to 0. Then, when you're done selecting, switching back to 1 will start the game play. The computer will randomly pick monsters, and then the battles can begin. The two monsters in the middle fields will fight each other, and the winner (whose attack and defense strength will be compared) will stay on the board. It would've been great to have battle animations for all combinations, but they're really time-consuming. I ended up making just one animation, Monnok vs. Houjix, which is in the video. I might find time later to make a few more animations.
The game pieces are from the amazing work of "Project842" on Etsy, which I bought:https://www.etsy.com/de-en/listing/617756111/3d-printable-chess-pieces-in-spaaaaaaaceThese models are really meant for 3D printing, but you can also move them around in Blender as you like.We can figure out the best camera and light positions for each piece and then save them as bitmaps.Then, the resulting images were converted to binary format and finally saved to a USB flash drive.The Giga will read the images straight from the USB into the frame memory. This makes the loading times faster.
The main unit's code base (based on the Arduino Giga) and the control unit's project (based on the Nano) are included below. It works, but please know it's work in progress...
CONCLUSIONI had a great time building my own Holochess table, which made a galaxy far, far away feel a little closer.
There's definitely room for improvement when it comes to the animations — both idle and battle. Adding more of them will definitely make the overall impression even stronger. But a project like this is never really finished.
I hope you find this work interesting and that it inspires you to create something similar. If you're working on your own version, I'd love to hear your ideas. Please share your thoughts in the comments.
As a maker the Force will be with you. Always.
All the best
Mac
/*
Holo Controls Unit
==================
Arduino Nano
Front Unit to control knobs, displays and LEDs for the Holochess project
!! Important : Before Uploading disconnect UART to GIGA (or power off GIGA) !!
!! Note: RED LED will stay on in case setup hangs to indicate error !!
Features:
- IC2 OLED Display 0.91" 32x128 to show a menu
- Operates 12 LEDs + one always on LED ;
- Serial output to main controller (GIGA)
- Reads and reports status of:
IC2 Rotary Encoder
2 mechanical switches
2 Pods
i2c OLED Display :
https://www.az-delivery.de/en/products/0-91-zoll-i2c-oled-display
i2c Rotary Encoder :
https://learn.adafruit.com/adafruit-i2c-qt-rotary-encoder/overview
Pins:
Encoder and Display are connected to I2C :
Perip Nano
VCC - 3V3
GND - GND
SDA - A4
SDL - A5
LEDs connected to digital IOs : Pins 2..13
Switches connected to A1 and A2 (Switch close to GND)
Pods connected to A6 and A7 (Voltage between VCC and GND)
V1 26.1.2025 initial version
V2 26.3.2025 controls added
V3 05.4.2025 Serial Commands added
V4 02.5.2025 Final version
*/
const String Version = "4"; // Version of this module
#include <Adafruit_SSD1306.h> // OLED display based on 1306
#include <Adafruit_GFX.h> // graphics functions
#include <Adafruit_seesaw.h> // Rotary Encoder
#include <seesaw_neopixel.h> // Indicator on Rotatry Encoder
// 0.91 OLED Display
// display
#define Screen_Width 128
#define Screen_height 32
Adafruit_SSD1306 oled(Screen_Width, Screen_height, &Wire, -1);
// Rotary Encoder
#define SS_SWITCH 24
#define SS_NEOPIX 6
#define SEESAW_ADDR 0x36
Adafruit_seesaw ss;
seesaw_NeoPixel sspixel = seesaw_NeoPixel(1, SS_NEOPIX, NEO_GRB + NEO_KHZ800);
int32_t encoder_position;
// LEDs pin assignments
#define LED1pin 2 // LEDs connected to digital pins D2..D13
#define LED2pin 3
#define LED3pin 4
#define LED4pin 5
#define LED5pin 6
#define LED6pin 7
#define LED7pin 8
#define LED8pin 9
#define LED9pin 10
#define LED10pin 11
#define LED11pin 12
#define LED12pin 13
// LED states (on=high or off=low)
boolean LED1,LED2,LED3,LED4,LED5,LED6,LED7,LED8,LED9,LED10,LED11,LED12 = LOW;
// Switches
#define Switch1pin 16 // Pin A2 D16
#define Switch2pin 15 // Pin A1 D15
int Switch1,Switch2; // indicates switch state (0 or 1 ; undefined=-1)
// Pots
#define Pod1pin A6 // Pin A6 (both only analog inputs) 0..690
#define Pod2pin A7 // Pin A7
int Pod1,Pod2; // indicates values (0..43 ; undefined=-1)
// user input
boolean cmdreceived = false; // true if new command received
String inputString = ""; // contains the received commandline
// variables
const unsigned int menusize=4; // number of defined menu items including 0 (0..size-1)
const String menuitem[] = { "Demo", "LEDs", "May4", "Info"}; // menu items
signed int menu_position=0; // current selected menu-item
const int MENUTIME=120; // timeout preset
unsigned int menu_timeout; // timeout to clear menu when no interaction
unsigned int chaser=0, t=12, dir=0; // LED animation variables
/* ----------------------------------------------------------------------------
Setup after Reset
---------------------------------------------------------------------------- */
void setup()
{
// init UART
Serial.begin(9600);
while (!Serial) delay(10);
// set LED ports to output
pinMode(LED1pin, OUTPUT);
pinMode(LED2pin, OUTPUT);
pinMode(LED3pin, OUTPUT);
pinMode(LED4pin, OUTPUT);
pinMode(LED5pin, OUTPUT);
pinMode(LED6pin, OUTPUT);
pinMode(LED7pin, OUTPUT);
pinMode(LED8pin, OUTPUT);
pinMode(LED9pin, OUTPUT);
pinMode(LED10pin, OUTPUT);
pinMode(LED11pin, OUTPUT);
pinMode(LED12pin, OUTPUT);
allLEDsOff(); // turn all LEDs off...
digitalWrite(LED1pin, HIGH); // ..except the RED LED on the right (would stay on in case setup hangs to indicate error)
// Switches
pinMode(Switch1pin, INPUT_PULLUP);
pinMode(Switch2pin, INPUT_PULLUP);
Switch1=-1; // mark as undefined
Switch2=-1;
// Pots
pinMode(Pod1pin, INPUT);
pinMode(Pod2pin, INPUT);
Pod1 = -1; // mark as undefined / analog readings 0 (right)..690 (left), will be divided by 23, so 0..30
Pod2 = -1;
// Init OLED Display
oled.begin(SSD1306_SWITCHCAPVCC, 0x3c);
delay(10);
oled.clearDisplay(); // clear display
oled.setRotation(1); // 0:Horiz(pins left) 1:Vertical(pins down)
oled.setTextSize(1); // text size
oled.setTextColor(WHITE, BLACK); // text color
oled.setCursor(0, 0); // position to display
oled.println("HOLO");
oled.println("CHESS");
oled.println("V"+Version);
oled.display(); // show on OLED
// Output initial message
Serial.println("\n\nHOLOCHESS CONTROL UNIT");
Serial.println("========================");
Serial.print("Version=");
Serial.println(Version);
// initialize Rotary Encoder module with SEESAW_ADDR
Serial.print("Init rotary encoder...");
if (! ss.begin(SEESAW_ADDR) || ! sspixel.begin(SEESAW_ADDR)) {
Serial.println("Error : Wrong address! Will halt now."); //Couldn't find seesaw on default address
while(1) delay(10);
}
uint32_t version = ((ss.getVersion() >> 16) & 0xFFFF);
if (version != 4991){
Serial.print("Error: wrong firmware! Will halt now. Version=");
Serial.println(version);
while(1) delay(10);
}
Serial.println("ok");
Serial.print("Init other elements...");
// set not so bright!
sspixel.setBrightness(20);
sspixel.show();
// use a pin for the built in encoder switch
ss.pinMode(SS_SWITCH, INPUT_PULLUP);
// get starting position
encoder_position = ss.getEncoderPosition();
// setup completed, now turn off red LED
digitalWrite(LED12pin, LOW);
Serial.println("setup completed.");
// show available commands on serial IF
inputString="help";
getCommand();
inputString="";
Serial.println(" ");
// show main page on OLED
show_main_page();
oled.println("START");
oled.display(); // show on OLED
menu_timeout = 0;
// show LED chaser once
for (int n=0; n<26; n++)
{
LEDchaser();
delay(120);
}
allLEDsOff();
show_main_page();
Serial.println("READY.");
}
/* ----------------------------------------------------------------------------
MAIN LOOP
---------------------------------------------------------------------------- */
void loop()
{
if (Serial.available() > 0) processInput(); // process serial commands
user_input(); // check user buttons
if (chaser) // LED chaser demo active
{
LEDchaser();
delay(analogRead(Pod1pin));
}
}
// advance LED chaser light
void LEDchaser()
{
allLEDsOff();
if (dir==1) // direction
{
switch(t){
case 1: LED1=HIGH; break;
case 2: LED2=HIGH; break;
case 3: LED3=HIGH; break;
case 4: LED4=HIGH; break;
case 5: LED5=HIGH; break;
case 6: LED6=HIGH; break;
case 7: LED7=HIGH; break;
case 8: LED8=HIGH; break;
case 9: LED9=HIGH; break;
case 10: LED10=HIGH; break;
case 11: LED11=HIGH; break;
case 12: LED12=HIGH; break;
}
t++;
if (t==13) dir=0;
}
else
{
switch(t){
case 1: LED1=HIGH; break;
case 2: LED2=HIGH; break;
case 3: LED3=HIGH; break;
case 4: LED4=HIGH; break;
case 5: LED5=HIGH; break;
case 6: LED6=HIGH; break;
case 7: LED7=HIGH; break;
case 8: LED8=HIGH; break;
case 9: LED9=HIGH; break;
case 10: LED10=HIGH; break;
case 11: LED11=HIGH; break;
case 12: LED12=HIGH; break;
}
t--;
if (t==0) dir=1;
}
updateLEDs();
}
// Show main display (menu turned off)
void show_main_page()
{
oled.clearDisplay(); // clear display
oled.setTextColor(WHITE, BLACK); // text color
oled.setCursor(0, 0); // position to display
oled.println("HOLO");
oled.println("CHESS");
oled.println("V"+Version);
oled.display(); // show on OLED
sspixel.setPixelColor(0, 0,0,0); // switch off Neopixel
sspixel.show();
if (chaser) // stop LED demo if was enabled
{
chaser=0;
allLEDsOff();
}
}
// process user interactions
void user_input()
{
int oldSwitch1,oldSwitch2, oldPod1, oldPod2;
// check switches
oldSwitch1=Switch1;
oldSwitch2=Switch2;
Switch1=digitalRead(Switch1pin);
Switch2=digitalRead(Switch2pin);
if (Switch1 != oldSwitch1) // SW1 = Play on/off
{
Serial.print(F("\n->SW1="));
Serial.println(Switch1);
}
if (Switch2 != oldSwitch2) // SW2 = Play or Menu Selector
{
Serial.print(F("\n->SW2="));
Serial.println(Switch2);
if (Switch2 == 0) showmenu();
if (Switch2 == 1) show_main_page();
}
// check Pots
oldPod1=Pod1;
oldPod2=Pod2;
Pod1 = trunc(analogRead(Pod1pin)/20); // div 23 will cut down pod reading to 0..30
Pod2 = trunc(analogRead(Pod2pin)/20);
// handle rotary encoder movements
int32_t new_position = ss.getEncoderPosition(); // get actual encoder position
if (encoder_position != new_position) // encoder moved ?
{
sspixel.setPixelColor(0, 255,255,0); // Neopixel yellow
sspixel.show();
if (Switch2 == 1) // Play Mode
{
if (new_position < encoder_position) // increment menu pos
{
Serial.println("\n->KNOB+");
}
if (new_position > encoder_position) // decrement menu pos
{
Serial.println("\n->KNOB-");
}
}
if (Switch2 == 0) // Menu Mode
{
if (new_position < encoder_position) // increment menu pos
{
menu_position++;
if (menu_position > menusize) menu_position=0;
}
if (new_position > encoder_position) // decrement menu pos
{
menu_position--;
if (menu_position < 0) menu_position=menusize;
}
// show updated menu
showmenu();
}
encoder_position = new_position; // save position
}
// handle encoder button
if (! ss.digitalRead(SS_SWITCH))
{
sspixel.setPixelColor(0, 0,255,255); // Neopixel cyan
sspixel.show();
if (Switch2 == 0) // Menu Mode
{
switch(menu_position){
case 0: // Demo
Serial.println("\n->Menu0");
break;
case 1: // LEDs
Serial.println("\n->Menu1");
chaser=1;
break;
case 2: // Info
Serial.println("\n->Menu2");
break;
case 3: // RST
Serial.println("\n->Menu3");
break;
}
}
else Serial.println("\n->KNOB_PRESSED");
while (! ss.digitalRead(SS_SWITCH)); // wait until button is released
}
}
// show menu as rotating bar on the OLED display
void showmenu()
{
oled.clearDisplay(); // clear display
oled.setTextColor(WHITE, BLACK);
oled.setCursor(0, 0); // position to display
oled.println("HOLO");
oled.println("CHESS");
// Menu Header
oled.setCursor(0, 65);
oled.setTextColor(BLACK,WHITE);
oled.println("MENU");
// show previous menu entry
signed int l=menu_position-1;
if (l<0) l=menusize;
oled.setTextColor(WHITE, BLACK); // text color (normal)
oled.setCursor(0, 80); // position to display
oled.println(menuitem[l]); // text to display
// show currently active menu entry
l++;
if (l>menusize) l=0;
oled.setTextColor(BLACK, WHITE); // text color (highlighted)
oled.setCursor(0, 90); // position to display
oled.println(menuitem[l]); // text to display
// show next menu entry
l++;
if (l>menusize) l=0;
oled.setTextColor(WHITE, BLACK); // text color (normal)
oled.setCursor(0, 100); // position to display
oled.println(menuitem[l]); // text to display
oled.display(); // show on OLED
menu_timeout = MENUTIME; // restart menu time-out timer
}
void updateLEDs(void) // update all 12 LEDs according to LED variable states
{
digitalWrite(LED1pin, LED1);
digitalWrite(LED2pin, LED2);
digitalWrite(LED3pin, LED3);
digitalWrite(LED4pin, LED4);
digitalWrite(LED5pin, LED5);
digitalWrite(LED6pin, LED6);
digitalWrite(LED7pin, LED7);
digitalWrite(LED8pin, LED8);
digitalWrite(LED9pin, LED9);
digitalWrite(LED10pin, LED10);
digitalWrite(LED11pin, LED11);
digitalWrite(LED12pin, LED12);
}
void allLEDsOff(void) // switch all LEDs off
{
LED1 = LOW;
LED2 = LOW;
LED3 = LOW;
LED4 = LOW;
LED5 = LOW;
LED6 = LOW;
LED7 = LOW;
LED8 = LOW;
LED9 = LOW;
LED10 = LOW;
LED11 = LOW;
LED12 = LOW;
updateLEDs();
}
/* -----------------------------------------------------------------------------
Process Input -> Called from main() when serial characters have been received
---------------------------------------------------------------------------- */
void processInput()
{
while (Serial.available()) // process all characters in input buffer received
{
char inChar = (char)(Serial.read()); // read next character
if (inChar == '\n') // Line-Feed received ?
getCommand(); // yes, commandline complete -> process command
else
{
inputString += inChar; // no, add chat to inputString
Serial.print(inChar); // Echo
}
}
}
/* -----------------------------------------------------------------------------
Process Commands - evaluate and execute received commands
----------------------------------------------------------------------------- */
void getCommand()
{
String paramString = ""; // Parameters
int len = inputString.length(); // length of commandline
int t;
// commands :
// --------------------------------------
// helpscreen
if (inputString.startsWith("help"))
{
Serial.println("");
Serial.println(F("available commands :"));
Serial.println(F("DISP <text> ...Display <text> on OLED"));
Serial.println(F("CLEARDISP ...clear Display"));
Serial.println(F("LEDON [N] ...turn on LED 1..12 "));
Serial.println(F("LEDOFF [N] ...turn off LED 1..12, 0=all "));
Serial.println(F("GETSW [N] ...returns state of Switch N "));
Serial.println(F("GETPOD [N] ...returns state of Pod N "));
Serial.println(F("PING_CONTROL ...returns PING_GIGA to main unit "));
}
// --------------------------------------
// Clear Display (clear display and shows homescreen)
if (inputString.startsWith("CLEARDISP"))
{
show_main_page();
Serial.println(F("\n->CLEAR DISPLAY"));
}
// --------------------------------------
// Display clear
if (inputString.startsWith("PING_CONTROL"))
{
Serial.println(F("\nPING_GIGA"));
}
// --------------------------------------
// display message
if (inputString.startsWith("DISP"))
{
paramString = inputString.substring(5,len);
Serial.println(F("\n->DISPLAY MESSAGE "));
oled.println(paramString);
oled.display();
}
// --------------------------------------
// Get Switch
if (inputString.startsWith("GETSW"))
{
paramString = inputString.substring(6,len);
t=paramString.toInt();
if (t > 0)
{
Serial.print(F("\n->SW"));
Serial.print(t);
Serial.print(F("="));
if (t==1) Serial.println(Switch1);
if (t==2) Serial.println(Switch2);
}
}
// --------------------------------------
// Get Pod
if (inputString.startsWith("GETPOD"))
{
paramString = inputString.substring(7,len);
t=paramString.toInt();
if (t > 0)
{
Serial.print(F("\n->POD"));
Serial.print(t);
Serial.print(F("="));
if (t==1) Serial.println(Pod1);
if (t==2) Serial.println(Pod2);
}
}
// --------------------------------------
// LED
if (inputString.startsWith("LEDON"))
{
paramString = inputString.substring(6,len);
t=paramString.toInt();
if (t >=0)
{
Serial.print(F("\n->LED ON "));
Serial.println(t);
switch(t){
case 1: LED1=HIGH; break;
case 2: LED2=HIGH; break;
case 3: LED3=HIGH; break;
case 4: LED4=HIGH; break;
case 5: LED5=HIGH; break;
case 6: LED6=HIGH; break;
case 7: LED7=HIGH; break;
case 8: LED8=HIGH; break;
case 9: LED9=HIGH; break;
case 10: LED10=HIGH; break;
case 11: LED11=HIGH; break;
case 12: LED12=HIGH; break;
}
updateLEDs();
}
}
if (inputString.startsWith("LEDOFF"))
{
paramString = inputString.substring(7,len);
t=paramString.toInt();
if (t >=0)
{
Serial.print(F("\n->LED OFF "));
Serial.println(t);
switch(t){
case 0: allLEDsOff(); break;
case 1: LED1=LOW; break;
case 2: LED2=LOW; break;
case 3: LED3=LOW; break;
case 4: LED4=LOW; break;
case 5: LED5=LOW; break;
case 6: LED6=LOW; break;
case 7: LED7=LOW; break;
case 8: LED8=LOW; break;
case 9: LED9=LOW; break;
case 10: LED10=LOW; break;
case 11: LED11=LOW; break;
case 12: LED12=LOW; break;
}
updateLEDs();
}
}
// --------------------------------------
// ready for next command
Serial.println(F(">"));
inputString = "";
}
/***************************************************
Holochess Project
=================
Arduino GIGA
single-player Holochess-table based on Aerial Display principle.
Hardware:
Arduino GIGA
6x Waveshare 2.4" 320x240 TFT Module based on ILI9341 Controller (https://www.waveshare.com/wiki/2.4inch_LCD_Module)
USB-Drive (FAT32 Format) at USB-A Port with all pictures and sounds
active speaker at line-out for sound output (optional)
Libraries :
Arduino_USBHostMbed5
ILI9341_GIGA_n (https://github.com/KurtE/ILI9341_GIGA_n)
LibPrintF
Arduino_AdvancedAudio
History:
(early experimental versions not listed)
V8 15.02.2025 initial version with Control Board
V9 05.04.2025 Gameplay implemented, Sound added
V10 10.04.2025 LED function for Attack/Defense added, Fight animations
V11 01.05.2025 4th May demo added, final brushup
mac70 May 2025
https://www.hackster.io/mac70/projects
Pins:
TFTs:
common :
VCC - 3.3V magenta
GND - GND white
D11 -> DIN green MOSI (SPI1 Transfer Output pin)
D13 -> CLK orange SCK (SPI1 Clock)
D53 -> RST brown Display Reset
individual pins :
D23..33 -> CS Chip Selects
D22..32 -> DC DataCommands (0=command, 1=data)
D2..7 -> BL gray Backlight (analog PIN PWM brightness control)
Servos :
D8 -> Servo1 PWM input
D9 -> Servo2 PWM input
Control Unit :
Nano -> Giga Serial2
TX -> RX1 (19)
RX -> TX1 (18)
GND -> GND
****************************************************/
const String Version = "11"; // Version of this module
#include <DigitalOut.h>
#include <FATFileSystem.h>
#include <Arduino_USBHostMbed5.h>
#include <Arduino_AdvancedAnalog.h>
#include <SPI.h>
#include <ILI9341_GIGA_n.h>
#include <SDRAM.h>
#include <mbed.h>
// Display Pin assignment
//#define TFT1_MOSI 11 // using &SPI1 will force driver to use these pins
//#define TFT1_MISO 12
//#define TFT1_SCK 13
#define TFT_RST 53 // Reset signal for all displays
#define RST_IGNORE 255 // will handle Reset for all displays, must be ignored in driver =255
#define USE_FRAME_BUFFER 1
// Display 1
#define TFT1_CS 23
#define TFT1_DC 22
#define TFT1_BL 7
ILI9341_GIGA_n tft1(&SPI1, TFT1_CS, TFT1_DC, RST_IGNORE);
// Display 2
#define TFT2_CS 25
#define TFT2_DC 24
#define TFT2_BL 6
ILI9341_GIGA_n tft2(&SPI1, TFT2_CS, TFT2_DC, RST_IGNORE);
// Display 3
#define TFT3_CS 27
#define TFT3_DC 26
#define TFT3_BL 5
ILI9341_GIGA_n tft3(&SPI1, TFT3_CS, TFT3_DC, RST_IGNORE);
// Display 4
#define TFT4_CS 29
#define TFT4_DC 28
#define TFT4_BL 4
ILI9341_GIGA_n tft4(&SPI1, TFT4_CS, TFT4_DC, RST_IGNORE);
// Display 5
#define TFT5_CS 31
#define TFT5_DC 30
#define TFT5_BL 3
ILI9341_GIGA_n tft5(&SPI1, TFT5_CS, TFT5_DC, RST_IGNORE);
// Display 6
#define TFT6_CS 33
#define TFT6_DC 32
#define TFT6_BL 2
ILI9341_GIGA_n tft6(&SPI1, TFT6_CS, TFT6_DC, RST_IGNORE);
// USB device
USBHostMSD msd;
mbed::FATFileSystem usb("usb");
#define CONNECT_TIMEOUT 5000
// Servos
#define SERVO1 D8
#define SERVO2 D9
PinName servo1pin = digitalPinToPinName(SERVO1);
mbed::PwmOut* servo1pwm = new mbed::PwmOut(servo1pin);
PinName servo2pin = digitalPinToPinName(SERVO2);
mbed::PwmOut* servo2pwm = new mbed::PwmOut(servo2pin);
// Endstop switches
#define ENDSTOP1 41
#define ENDSTOP2 43
// DAC for sound output
AdvancedDAC dac0(A12);
// Variables
// Serial control
String inputString = ""; // contains the received commandline from PC
String controlString = ""; // contains the received info from the control-unit
boolean PingReceived; // true if connection was established to Control Unit
boolean logControl = false; // true if all serial2 messages from Nano will be logged
// SDRAM Buffer
uint16_t *sdAnimBuffer1; // Animation Buffer in SDRAM
uint16_t *sdAnimBuffer2; // Animation Buffer in SDRAM
uint16_t *sdBuffer;
// Game control
int mainactive=0; // indicates main game mode entered or not
int sensorVal; // checks a pin value
int xd1,xd2,xd3,xd4,xd5,xd6; // monster x-positions for each display
int yd1,yd2,yd3,yd4,yd5,yd6; // monster y-positions for each display
int Monster[] = {1,1,1,1,1,1}; // current assigned Monster for each field
const String Monstername[] = { "X", "Ghhhk", "Grimtaash", "Houjix", "Strider", "KlorSlug", "Monnok", "Ngok", "Savrip"}; // Monster names 1-8
const int MonsterAttack[] = { 1, 4, 8, 4, 2, 7, 6, 3, 6 };
const int MonsterDefense[] = { 1, 3, 2, 4, 7, 3, 5, 8, 6 };
uint32_t wait, nextcheck; // timeouts
int err;
int Switch1=-1,Switch2=-1; // indicates switch state (0 or 1 ; undefined=-1)
int Monster_Selection_active=0; // indicates that player is selecting monsters
int arrowtoggle=0, arrowon=0; // needed for arrow flashing during monster selection
int playmode=0; // after all monster are selected, indicates active play
// Audio playback
FILE *audiofile = nullptr;
int sample_size = 0;
int samples_count = 0;
int audio_playing = 0;
/* --------------------------------------------------------------------------------------
Setup after Reset
-------------------------------------------------------------------------------------- */
void setup()
{
// init Endstop Switches
pinMode(ENDSTOP1, INPUT_PULLUP);
pinMode(ENDSTOP2, INPUT_PULLUP);
// initialize the external 8MB SDRAM and reserve space for animation buffer
SDRAM.begin();
sdAnimBuffer1 = (uint16_t *)SDRAM.malloc(2* 320* 240 * 45 );
// sdAnimBuffer2 = (uint16_t *)SDRAM.malloc(2* 50* 168 * 154 );
// enable the USB-A port
pinMode(PA_15, OUTPUT);
digitalWrite(PA_15, HIGH);
// random number init
randomSeed(analogRead(0));
// Display Reset
pinMode(TFT_RST, OUTPUT);
delay(150);
digitalWrite(TFT_RST, HIGH);
delay(150);
digitalWrite(TFT_RST, LOW);
// init and wait for Arduino Serial (via USB)
Serial.begin(9600);
while (!Serial && millis() < 1000) ;
// second serial interface for Arduino Nano on Control unit
Serial2.begin(9600); // Serial2 is on pins 18(TX, not used) and 19(RX)
// Display Reset release
delay(450);
digitalWrite(TFT_RST, HIGH);
// initial message on host UART
Serial.println("\n\n\n-------------------------------------------------------------");
Serial.println("\nHOLOCHESS");
Serial.println("=========");
Serial.print("Version =");
Serial.println(Version);
Serial.println("\nuse 'help' to list available commands.");
// turn backlights on
pinMode(TFT1_BL, OUTPUT);
pinMode(TFT2_BL, OUTPUT);
pinMode(TFT3_BL, OUTPUT);
pinMode(TFT4_BL, OUTPUT);
pinMode(TFT5_BL, OUTPUT);
pinMode(TFT6_BL, OUTPUT);
digitalWrite(TFT1_BL,1); // all backlights full on
digitalWrite(TFT2_BL,1);
digitalWrite(TFT3_BL,1);
digitalWrite(TFT4_BL,1);
digitalWrite(TFT5_BL,1);
digitalWrite(TFT6_BL,1);
// init all displays and fill black
delay(250); // first display needs more time after reset
tft1.begin();
delay(150);
tft1.fillScreen(ILI9341_BLACK);
tft1.setRotation(3); // 0:flip ; 1: normal ; 2+3 : 90°
tft2.begin();
tft2.fillScreen(ILI9341_BLACK);
tft2.setRotation(3);
tft3.begin();
tft3.fillScreen(ILI9341_BLACK);
tft3.setRotation(0);
tft4.begin();
tft4.fillScreen(ILI9341_BLACK);
tft4.setRotation(0);
tft5.begin();
tft5.fillScreen(ILI9341_BLACK);
tft5.setRotation(1);
tft6.begin();
tft6.fillScreen(ILI9341_BLACK);
tft6.setRotation(1);
Serial.println("displays initialized.");
// init and home servos
servo1pwm->period_ms(20); // 20ms period=50Hz PWM frequency (standard Servo-control)
servo1pwm->pulsewidth_us(1450); // pulse 1ms..2ms ; middle = off for continous servo FS90r
servo2pwm->period_ms(20);
servo2pwm->pulsewidth_us(1450);
servo1pwm->suspend();
servo2pwm->suspend();
delay(10);
homeservo1();
homeservo2();
delay(10);
// Mount USB stick
Serial.print("mounting USB device... ");
wait = millis();
while(!msd.connect()) {
if((millis() - wait) > CONNECT_TIMEOUT) {
Serial.println("Connect Error ! USB device not attached... will halt now !\n");
while (1);
}
}
err = usb.mount(&msd);
if (err) {
Serial.print("Error mounting USB device! Will halt now ! Error =");
Serial.println(err);
while (1);
}
Serial.println("OK");
// assign game piece bitmap default positions for each display
xd1=80; yd1=80;
xd2=0; yd2=60;
xd3=20; yd3=170;
xd4=10; yd4=90;
xd5=150; yd5=60;
xd6=150; yd6=80;
// initial Monster assignment
Monster[1]=1;
Monster[2]=3;
Monster[3]=2;
Monster[4]=4;
Monster[5]=6;
Monster[6]=8;
// load and show static bitmaps for initial stage
Serial.println("loading initial monster bitmaps");
ShowMonster(1,false);
ShowMonster(2,false);
ShowMonster(3,false);
ShowMonster(4,false);
ShowMonster(5,false);
ShowMonster(6,false);
// load a sequence of images in binary RGB656 format as battle animation (only one implemented so far)
Serial.println("loading battle animation:");
loadUSBanim("/usb/Anim/FightAnim1V2.dat", 40, 240, 320, sdAnimBuffer1);
// Ping Control Board and wait for answer
Serial.println("check if Control board is ready...");
Serial2.println("");
Serial2.println("PING_CONTROL");
PingReceived=0;
while (PingReceived==0)
{
if (Serial2.available() > 0) processControlUnitMessage(); // process serial Control Unit commands (UART2 RX wired to Control board)
}
Serial.println("OK, control board responded.");
// control board is ready, poll current status of Switches
Serial.println("check Switch positions...");
delay(10);
Serial2.println("GETSW 1");
while(Switch1==-1)
{
if (Serial2.available() > 0) processControlUnitMessage(); // process serial Control Unit commands (UART2 RX wired to Control board)
}
delay(10);
Serial2.println("GETSW 2");
while(Switch2==-1)
{
if (Serial2.available() > 0) processControlUnitMessage(); // process serial Control Unit commands (UART2 RX wired to Control board)
}
Serial.print("Switch positions : SW1=");
Serial.print(Switch1);
Serial.print(" , SW2=");
Serial.println(Switch2);
for (int n=1;n<13;n++)
{
Serial2.print("LEDON ");
Serial2.println(n);
delay(10);
}
for (int n=1;n<13;n++)
{
Serial2.print("LEDOFF ");
Serial2.println(n);
delay(10);
}
Serial2.println("CLEARDISP");
Serial2.println("DISP ");
Serial2.println("DISP READY");
delay(80);
if ((Switch1 == 0) && (Switch2 == 1)) // check game mode SW=0 : monster selection; SW=1 : Play
{
// Stop Play mode, Start Monster Selection
Monster_Selection_active=1;
Serial2.println("DISP SELEC");
}
delay(20);
// Play start sound
PlayAudio("/usb/Sounds/startgame.wav");
Serial.println("\nSetup finished - game starts.");
mainactive=1; // game start
nextcheck = millis();
}
/* --------------------------------------------------------------------------------------
MAIN LOOP
-------------------------------------------------------------------------------------- */
void loop()
{
// Check user inputs
if (Serial.available() > 0) processInput(); // process serial PC commands (UART0 connected to USB interface on PC)
if (Serial2.available() > 0) processControlUnitMessage(); // process serial Control Unit commands (UART2 RX wired to Control board)
// Game State Machine Control
if ((Monster_Selection_active >= 1) && (Monster_Selection_active <= 4)) MonsterSelect(); // 1. user monster selection
if (Monster_Selection_active == 5) PickMonstersComputer(); // 2. computer monster selection
if (Monster_Selection_active == 6) BattleSelect(); // 3. battle monster selection
if (Monster_Selection_active == 8) StartBattle(); // 4. start battle and evaluate
// check and keep alive USB drive
checkUSB();
}
// Start battle with current selected monsters on fields 2 and 5
void StartBattle()
{
tft3.fillRect(0, 30, 240, 80, ILI9341_BLACK);
Serial2.println("CLEARDISP");
Serial2.println("DISP ");
Serial2.println("DISP FIGHT");
// User has selected, now Computer selects battle monster
Serial.print("Computer selects battle monster : ");
if (Monster[5]!=6) // if Monnok is on field 5, do not change...
{
// ...else rotate all left until Field5 contains a valid piece
while(Monster[5]==0)
{
Monster[0]=Monster[4];
Monster[4]=Monster[5];
Monster[5]=Monster[6];
Monster[6]=Monster[0];
ShowMonster(4,false);
ShowMonster(5,false);
ShowMonster(6,false);
}
}
Serial.println(Monstername[Monster[5]]);
Serial.println("Battle Starts...");
// jump scroll effect for Monster1
for (int j=0; j<140; j++) {
delay(2);
tft3.setScroll(j);
}
for (int j=140; j>0; j--) {
delay(2);
tft3.setScroll(j);
}
tft3.setScroll(0);
// jump scroll effect for Monster2
for (int j=0; j<140; j++) {
delay(2);
tft4.setScroll(j);
}
for (int j=140; j>0; j--) {
delay(2);
tft4.setScroll(j);
}
tft4.setScroll(0);
// move monsters to the middle
moveservo1(0,170);
moveservo2(0,100);
// and fight...
if ((Monster[2]==3) && (Monster[5]==6)) HoujixMonnokFight(); // if "Houjix vs. Monnok" ...show the only battle animation in this version
// else ... // no animation for other combinations in this version available, perhaps something for future improvements
// evaluate the winner
Serial.println("...Battle over.");
Serial.println("RESULT:");
if (MonsterAttack[Monster[2]]>=MonsterDefense[Monster[5]])
{
Serial.print(Monstername[Monster[2]]);
Serial.print(" is strnger than ");
Serial.println(Monstername[Monster[5]]);
Serial.println("User wins the battle.");
Monster[5]=0; // Computer Monster on field 5 will disappear
}
else if (MonsterAttack[Monster[5]]>=MonsterDefense[Monster[2]])
{
Serial.print(Monstername[Monster[5]]);
Serial.print(" is strnger than ");
Serial.println(Monstername[Monster[2]]);
Serial.println("Computer wins the battle.");
Monster[2]=0; // User Monster on field 2 will disappear
}
else // no winner
{
Serial.print(Monstername[Monster[5]]);
Serial.print(" and ");
Serial.print(Monstername[Monster[2]]);
Serial.println(" are equally strong. No winner. ");
}
// show only remaining monster
tft3.fillScreen(ILI9341_BLACK);
tft4.fillScreen(ILI9341_BLACK);
if (Monster[2] > 0) ShowMonster(2,false);
if (Monster[5] > 0) ShowMonster(5,false);
// move back
moveservo1(1,180);
homeservo1();
moveservo2(1,100);
homeservo2();
// Check for a possible game result
if ((Monster[1] == 0) && (Monster[2] == 0) && (Monster[3] == 0))
{
Serial.println("\n*** User lost the game, Computer won ***\n");
tft4.setTextColor(ILI9341_WHITE);
tft4.setTextSize(3);
tft4.setCursor(40, 100);
tft4.println("GAME OVER");
tft4.setCursor(20, 130);
tft4.println("COMPUTER WINS !");
Serial.println("Reset Unit to re-start... will halt now. Good bye.");
Serial.println("--------------------------------------------------");
Serial2.println("CLEARDISP");
Serial2.println("DISP ");
Serial2.println("DISP GAME");
Serial2.println("DISP OVER");
Serial2.println("DISP YOU");
Serial2.println("DISP LOST");
while(1); // will halt now in this version, reset to re-start
}
if ((Monster[4] == 0) && (Monster[5] == 0) && (Monster[6] == 0))
{
Serial.println("\n*** Computer lost the game, User won - CONGRATULATIONS ***\n");
tft3.setTextColor(ILI9341_WHITE);
tft3.setTextSize(3);
tft3.setCursor(40, 100);
tft3.println("GAME OVER");
tft3.setCursor(40, 130);
tft3.println("USER WINS !");
Serial.println("Reset Unit to re-start... will halt now. Good bye.");
Serial.println("--------------------------------------------------");
Serial2.println("CLEARDISP");
Serial2.println("DISP ");
Serial2.println("DISP GAME");
Serial2.println("DISP OVER");
Serial2.println("DISP YOU");
Serial2.println("DISP WIN");
while(1); // will halt now in this version, reset to re-start
}
Monster_Selection_active = 6; // back to battle selection
}
// Animated fight
void HoujixMonnokFight()
{
int index1=0,index2=0;
tft3.fillScreen(ILI9341_BLACK);
tft4.fillScreen(ILI9341_BLACK);
// battle animation
wait = millis();
while((millis() - wait) < 9000)
{
tft3.writeRect(0, 20, 240, 320, (uint16_t*)sdAnimBuffer1 + index1*320*240);
delay(5);
index1++;
if (index1 == 40)
{
index1=1;
// PlayAudio("/usb/Sounds/Punch2.wav");
delay(10);
}
}
// PlayAudio("/usb/Sounds/Pain.wav");
}
// select 2 Monsters for battle (Note: Changing of actual monsters handled from rotation knob event)
void BattleSelect()
{
if (Monster_Selection_active == 6) // start user battle selection
{
tft3.fillRect(0, 30, 240, 90, ILI9341_BLACK);
tft3.setTextColor(ILI9341_WHITE);
tft3.setTextSize(3);
tft3.setCursor(40, 65);
tft3.println("SELECT");
tft3.setCursor(40, 85);
tft3.println("BATTLE");
Serial.println("Select Battle Player Monster");
Monster_Selection_active = 7;
}
}
// user selects monsters (Note: Changing of actual monsters handled from rotation knob event)
void MonsterSelect()
{
if (Monster_Selection_active == 1) // step 1 : prepare for new game - start completely new selection of monsters
{
ShowMonster(1,false);
ShowMonster(2,false);
ShowMonster(3,false);
ShowMonster(4,false);
ShowMonster(5,false);
ShowMonster(6,false);
tft3.fillRect(0, 30, 240, 90, ILI9341_BLACK);
tft3.setTextColor(ILI9341_WHITE);
tft3.setTextSize(3);
tft3.setCursor(40, 65);
tft3.println("SELECT");
tft3.setCursor(40, 85);
tft3.println("MONSTERS");
Serial.println("Select Player Monsters");
Monster_Selection_active = 2;
Serial.println("Select Monster Field1");
Monster[1]=1;
ShowMonster(1,true);
arrowtoggle=0;
arrowon=0;
}
// toggles the green arrow on/off above monster during selection
arrowtoggle++;
if (arrowtoggle>8000)
{
arrowtoggle=0;
if (arrowon==1) arrowon=0; else arrowon=1;
if(Monster_Selection_active == 2)
{
if (arrowon==1) ShowMonster(1,true); else ShowMonster(1,false);
}
else if(Monster_Selection_active == 3)
{
if (arrowon==1) ShowMonster(2,true); else ShowMonster(2,false);
}
else if(Monster_Selection_active == 4)
{
if (arrowon==1) ShowMonster(3,true); else ShowMonster(3,false);
}
}
}
// afer user monsters are selected, computer will pick 3 random pieces
void PickMonstersComputer()
{
int exists=1;
tft3.fillRect(0, 30, 240, 80, ILI9341_BLACK);
Monster[5]=6; // Monnok set as middle piece to show fight animation
// pick random monsters a few times to illustrate computer selection process
for (int n=0; n<6; n++)
{
exists=1;
while(exists){
Monster[4]= random(1, 9);
if ((Monster[4] != Monster[5]) && (Monster[4] != Monster[6])) exists=0;
}
ShowMonster(4,true);
delay(300);
}
ShowMonster(4,false);
PlayAudio("/usb/Sounds/Monster6.wav");
for (int n=0; n<6; n++)
{
exists=1;
while(exists){
Monster[5]= random(1, 9);
if ((Monster[4] != Monster[5]) && (Monster[5] != Monster[6])) exists=0;
}
ShowMonster(5,true);
delay(300);
}
Monster[5]=6; // Monnok set as middle piece to show fight animation
ShowMonster(5,false);
PlayAudio("/usb/Sounds/Monster5.wav");
for (int n=0; n<6; n++)
{
exists=1;
while(exists){
Monster[6]= random(1, 9);
if ((Monster[6] != Monster[4]) && (Monster[6] != Monster[5])) exists=0;
}
ShowMonster(6,true);
delay(200);
}
ShowMonster(6,false);
PlayAudio("/usb/Sounds/Monster3.wav");
tft3.fillRect(0, 30, 240, 80, ILI9341_BLACK);
tft3.setTextColor(ILI9341_RED);
tft3.setTextSize(4);
tft3.setCursor(40, 90);
tft3.println("FIGHT !");
Serial.println("Selection complete :");
for (int n=1;n<7;n++)
{
Serial.print("Field ");
Serial.print(n);
Serial.print("=");
Serial.print(Monster[n]);
Serial.print(" ");
Serial.println(Monstername[Monster[n]]);
}
Monster_Selection_active = 6;
// playmode=1; // fight can start
}
// show current assigned monster for a specific field (1-6) ; sel=true shows additional selection indicator (green arrow)
void ShowMonster(int field, boolean sel)
{
int num;
char find[2];
char fname[21];
char fex[] = ".dat";
if (Monster[field]==0) // no Monster in this field available (already defeated), show only black display
{
switch(field){
case 1:
tft6.fillScreen(ILI9341_BLACK);
break;
case 2:
tft4.fillScreen(ILI9341_BLACK);
break;
case 3:
tft1.fillScreen(ILI9341_BLACK);
break;
case 4:
tft5.fillScreen(ILI9341_BLACK);
break;
case 5:
tft3.fillScreen(ILI9341_BLACK);
break;
case 6:
tft2.fillScreen(ILI9341_BLACK);
break;
}
}
else // valid Monster, show the assigned bitmap
{
// create file name in the form "Mxf.dat" where x is the monster number (1-8) and field the position (1-6)
num=10*(Monster[field])+field;
itoa(num, find, 10);
strcpy(fname, "/usb/MonsterStills/M"); // path and name beginning
strcat(fname, find); // index number, two digts
strcat(fname, fex); // add ".dat"
// Serial.print("Filename:");
// Serial.println(fname);
// load monster graphic and show on field, if sel is true show green arrow, else erase arrow by black rectangle
switch(field){
case 1:
if (sel) drawUSBbitmap("/usb/GameGraphic/selarrow.dat", 6, xd6, 0, 128, 64); else tft6.fillRect(xd6, 0, 128, 64, ILI9341_BLACK);
drawUSBbitmap(fname, 6, xd6, yd6, 168, 154); // front left
break;
case 2:
if (sel) drawUSBbitmap("/usb/GameGraphic/selarrow.dat", 4, xd4, 30, 128, 64); else tft4.fillRect(xd4, 30, 128, 64, ILI9341_BLACK);
drawUSBbitmap(fname, 4, xd4, yd4, 168, 154); // front middle
break;
case 3:
if (sel) drawUSBbitmap("/usb/GameGraphic/selarrow.dat", 1, xd1, 0, 128, 64); else tft1.fillRect(xd1, 0, 128, 64, ILI9341_BLACK);
drawUSBbitmap(fname, 1, xd1, yd1, 168, 154); // front right
break;
case 4:
if (sel) drawUSBbitmap("/usb/GameGraphic/selarrow2.dat", 5, xd5, 0, 128, 64); else tft5.fillRect(xd5, 0, 128, 64, ILI9341_BLACK);
drawUSBbitmap(fname, 5, xd5, yd5, 168, 154); // back left
break;
case 5:
if (sel) drawUSBbitmap("/usb/GameGraphic/selarrow2.dat", 3, xd3, 50, 128, 64); else tft3.fillRect(xd3, 50, 128, 64, ILI9341_BLACK);
drawUSBbitmap(fname, 3, xd3, yd3, 168, 154); // back middle
break;
case 6:
if (sel) drawUSBbitmap("/usb/GameGraphic/selarrow2.dat", 2, xd2, 0, 128, 64); else tft2.fillRect(xd2, 0, 128, 64, ILI9341_BLACK);
drawUSBbitmap(fname, 2, xd2, yd2, 168, 154); // back right
break;
}
}
}
// Shows Attach and Defense Stats from Monster on field N on LEDs in Control unit
void ShowAttackDefenseLED(int field)
{
int Attack;
int Defense;
int n;
Attack = MonsterAttack[Monster[field]];
Defense = MonsterDefense[Monster[field]];
Serial.print(Monstername[Monster[field]]);
Serial.print(" Attack = ");
Serial.print(Attack);
for (n=1;n<=Attack;n++)
{
Serial2.print("LEDON ");
Serial2.println(n+1);
delay(80);
}
delay(100);
for (n=Attack;n>=1;n--)
{
Serial2.print("LEDOFF ");
Serial2.println(n+1);
delay(80);
}
Serial.print(" Defense = ");
Serial.println(Defense);
for (n=12;n>=(12-Defense);n--)
{
Serial2.print("LEDON ");
Serial2.println(n);
delay(80);
}
delay(100);
for (n=(12-Defense);n<=12; n++)
{
Serial2.print("LEDOFF ");
Serial2.println(n);
delay(80);
}
}
/* --------------------------------------------------------------------------------------
Process Control Unit Message -> Called from main() when serial2 characters have been received
Control Unit must be connected to Serial2 Pins 18,19
-------------------------------------------------------------------------------------- */
void processControlUnitMessage()
{
while (Serial2.available()) // process all characters from control board received
{
char inChar2 = (char)(Serial2.read()); // read next character
if (inChar2 == '\n') // Line-Feed received ?
{
if (logControl) // echo Control info on PC serial (if enabled)
{
Serial.print("[CONTROL]");
Serial.println(controlString);
}
getControlMessage(); // process message from Control Board
}
else
{
controlString += inChar2; // no, add chat to inputString
}
}
}
/* --------------------------------------------------------------------------------------
evaluate and execute received messages from Control board
-------------------------------------------------------------------------------------- */
void getControlMessage()
{
String paramString = ""; // Parameters
int len = controlString.length(); // length of commandline
int sensorVal;
int t,d;
// Switches changed
if (controlString.startsWith("->SW1="))
{
paramString = controlString.substring(6,len);
t=paramString.toInt();
Serial.print("Switch1 : ");
Serial.println(t);
Switch1=t;
if ((mainactive == 1) && (Switch1 == 0) && (Switch2 == 1)) // check for SELECT mode (SW1=0, SW2=1) -> Start new monster selection
{
// Stop Play mode, Start Monster Selection
Monster_Selection_active=1;
playmode=0;
Serial2.println("CLEARDISP");
Serial2.println("DISP ");
Serial2.println("DISP SELEC");
}
else if ((mainactive == 1) && (Switch1 == 1) && (Switch2 == 1)) // check for PLAY mode (SW1=1, SW2=1)
{
// Stop user monster Selection
if(Monster_Selection_active == 2) ShowMonster(1,false);
else if(Monster_Selection_active == 3) ShowMonster(2,false);
else if(Monster_Selection_active == 4) ShowMonster(3,false);
// activate computer randomm onster selection
Monster_Selection_active=5;
Serial2.println("CLEARDISP");
Serial2.println("DISP ");
Serial2.println("DISP PLAY");
}
}
if (controlString.startsWith("->SW2="))
{
paramString = controlString.substring(6,len);
t=paramString.toInt();
Serial.print("Switch2 : ");
Serial.println(t);
Switch2=t;
if ((mainactive == 1) && (Switch1 == 0) && (Switch2 == 1)) Monster_Selection_active=1; else Monster_Selection_active=0; // check for SW1=0 SW2=1 -> monster selection
}
// Rotary knob turned
if (controlString.startsWith("->KNOB-"))
{
Serial.println("Knob turned -");
switch(Monster_Selection_active){ // if Monster select is active, adjust current field's monster
case 2: // select 1st piece
Monster[1]--;
if (Monster[1] == 0) Monster[1] = 8;
// make sure different monsters
while ((Monster[1] == Monster[2]) || (Monster[1] == Monster[3]))
{
Monster[1]--;
if (Monster[1] == 0) Monster[1] = 8;
}
Serial.print("Monster1: "); Serial.println(Monster[1]);
// Serial.print(Monstername[Monster[1]]);
ShowMonster(1,true);
ShowAttackDefenseLED(1);
break;
case 3: // select 2nd piece
Monster[2]--;
if (Monster[2] == 0) Monster[2] = 8;
// make sure different monsters
while ((Monster[2] == Monster[1]) || (Monster[2] == Monster[3]))
{
Monster[2]--;
if (Monster[2] == 0) Monster[2] = 8;
}
Serial.print("Monster2:"); Serial.println(Monster[2]);
ShowMonster(2,true);
// Serial.print(Monstername[Monster[2]]);
ShowAttackDefenseLED(2);
break;
case 4: // select 3rd piece
Monster[3]--;
if (Monster[3] == 0) Monster[3] = 8;
// make sure different monsters
while ((Monster[3] == Monster[1]) || (Monster[3] == Monster[2]))
{
Monster[3]--;
if (Monster[3] == 0) Monster[3] = 8;
}
Serial.print("Monster3:"); Serial.println(Monster[3]);
// Serial.print(Monstername[Monster[3]]);
ShowMonster(3,true);
ShowAttackDefenseLED(3);
break;
case 7: // select battle monster, rotate all left
Monster[0]=Monster[1]; // tmp
Monster[1]=Monster[2]; // 2->1
Monster[2]=Monster[3]; // 3->2
Monster[3]=Monster[0]; // 1->3
ShowMonster(1,false);
ShowMonster(2,false);
ShowMonster(3,false);
break;
}
}
if (controlString.startsWith("->KNOB+"))
{
Serial.println("Knob turned +");
switch(Monster_Selection_active){ // if Monster select is active, adjust current field's monster
case 2: // select 1st piece
Monster[1]++;
if (Monster[1] == 9) Monster[1] = 1;
// make sure different monsters
while ((Monster[1] == Monster[2]) || (Monster[1] == Monster[3]))
{
Monster[1]++;
if (Monster[1] == 9) Monster[1] = 1;
}
Serial.print("Monster1:"); Serial.println(Monster[1]);
// Serial.print(Monstername[Monster[1]]);
ShowMonster(1,true);
ShowAttackDefenseLED(1);
break;
case 3: // select 2nd piece
Monster[2]++;
if (Monster[2] == 9) Monster[2] = 1;
// make sure different monsters
while ((Monster[2] == Monster[1]) || (Monster[2] == Monster[3]))
{
Monster[2]++;
if (Monster[2] == 9) Monster[2] = 1;
}
Serial.print("Monster2:"); Serial.println(Monster[2]);
ShowMonster(2,true);
// Serial.print(Monstername[Monster[2]]);
...
This file has been truncated, please download it to see its full contents.
// -- USB Routines for Holochess project
// read a bitmap in binary 656 format from USB drive toSDRAM buffer and show on screen
void drawUSBbitmap(char* filename, int dispno, int x, int y, int rawWidth, int rawHeight)
{
int err;
sdBuffer = (uint16_t *)SDRAM.malloc(rawWidth * rawHeight * 2 + 32); // reserve image buffer in SDRAM
uint32_t readTime = millis(); // start time measurement
FILE *imagefile = fopen(filename, "r+"); // open for reading
if (imagefile == NULL) Serial.println(" ..File not found !!");
else
{
//Read bitmap from USB in raw rgb565 format and store image in SDRAM var 'sdBuffer'
long freadReturn = fread(sdBuffer, sizeof(uint16_t), rawWidth * rawHeight, imagefile);
//Draw image from sdBuffer on display number 1..6
switch(dispno)
{
case 1:
tft1.writeRect(x, y, rawWidth, rawHeight, (uint16_t*)sdBuffer);
break;
case 2:
tft2.writeRect(x, y, rawWidth, rawHeight, (uint16_t*)sdBuffer);
break;
case 3:
tft3.writeRect(x, y, rawWidth, rawHeight, (uint16_t*)sdBuffer);
break;
case 4:
tft4.writeRect(x, y, rawWidth, rawHeight, (uint16_t*)sdBuffer);
break;
case 5:
tft5.writeRect(x, y, rawWidth, rawHeight, (uint16_t*)sdBuffer);
break;
case 6:
tft6.writeRect(x, y, rawWidth, rawHeight, (uint16_t*)sdBuffer);
break;
}
err = fclose(imagefile);
if (err < 0) {
Serial.print("fclose error:");
Serial.print(strerror(errno));
Serial.print(" (");
Serial.print(-errno);
Serial.print(")");
} else {
}
}
SDRAM.free(sdBuffer);
nextcheck = millis();
}
// read a file from USB in binary format and store in Animation buffer
// the file contains count bitmaps each with same size rawWidth*rawHeight
void loadUSBanim(char* filename, int count, int rawWidth, int rawHeight, uint16_t* targetbuffer)
{
Serial.print("loading animation ");
Serial.print(filename);
uint32_t readTime = millis(); // start time measurement
FILE *imagefile = fopen(filename, "r+"); // open for reading
if (imagefile == NULL) Serial.println(" ..File not found !!");
else
{
Serial.print(" ..");
//Read animation sequence from USB and store image in SDRAM animation buffer
long freadReturn = fread(targetbuffer, sizeof(uint16_t), count * rawWidth * rawHeight, imagefile);
readTime = millis() - readTime;
Serial.print(" time: "); Serial.print(readTime); Serial.println("ms");
fclose(imagefile);
}
}
// checks if the USB is still mounted and creates a periodic dummy access each 10sec to keep USB alive
void checkUSB()
{
if((millis() - nextcheck) > 10000) // some periodic access is needed to keep the USB stick alive !!
{
Serial2.println("LEDON 1"); // LED1 will flash shortly
nextcheck = millis();
FILE *f = fopen("/usb/notexist.txt", "r+"); // dummy read of non-exiting file
int ret = fclose(f);
Serial2.println("LEDOFF 1");
}
// handle USB disconnection and reconnection
if (!msd.connect())
{
msd.connect();
Serial.println("USB re-connect...");
wait = millis();
while(!msd.connect())
{
if((millis() - wait) > CONNECT_TIMEOUT) {
Serial.println("Error ! USB device not found anymore... will halt now !\n");
Serial2.println("LEDON 1");
while (1);
}
}
err = usb.mount(&msd);
if (err) {
Serial.print("Error mounting USB device! Will halt now ! Error =");
Serial.println(err);
Serial2.println("LEDON 1");
Serial2.println("CLEARDISP");
Serial2.println("DISP ");
Serial2.println("DISP USB");
Serial2.println("DISP ERROR");
while (1);
}
Serial.println("OK");
}
}
// ---- Audio Routines for Holochess project
void HandleAudio()
{
while(audio_playing==1)
{
if ( dac0.available() && !feof(audiofile))
{
/* Read data from file. */
uint16_t sample_data[256] = { 0 };
fread(sample_data, sample_size, 256, audiofile);
/* Get a free buffer for writing. */
SampleBuffer buf = dac0.dequeue();
/* Write data to buffer. */
for (size_t i = 0; i < buf.size(); i++)
{
/* Scale down to 12 bit. */
uint16_t const dac_val = ((static_cast<unsigned int>(sample_data[i]) + 32768) >> 4) & 0x0fff;
buf[i] = dac_val;
}
/* Write the buffer to DAC. */
dac0.write(buf);
if(feof(audiofile))
{
fclose(audiofile);
audio_playing=0;
}
}
}
}
void PlayAudio(char* afilename)
{
Serial.print("Reading audio file ");
Serial.print(afilename);
audiofile = fopen(afilename, "rb");
if (audiofile == nullptr)
{
Serial.print(" ..Error opening audio file: ");
Serial.print(strerror(errno));
return;
}
Serial.println();
struct wav_header_t
{
char chunkID[4]; //"RIFF" = 0x46464952
unsigned long chunkSize; //28 [+ sizeof(wExtraFormatBytes) + wExtraFormatBytes] + sum(sizeof(chunk.id) + sizeof(chunk.size) + chunk.size)
char format[4]; //"WAVE" = 0x45564157
char subchunk1ID[4]; //"fmt " = 0x20746D66
unsigned long subchunk1Size; //16 [+ sizeof(wExtraFormatBytes) + wExtraFormatBytes]
unsigned short audioFormat;
unsigned short numChannels;
unsigned long sampleRate;
unsigned long byteRate;
unsigned short blockAlign;
unsigned short bitsPerSample;
};
wav_header_t header;
fread(&header, sizeof(header), 1, audiofile);
char msg[64] = { 0 };
struct chunk_t {
char ID[4];
unsigned long size;
};
chunk_t chunk;
snprintf(msg, sizeof(msg), "id\t"
"size");
// Serial.println(msg);
/* Find data chunk. */
while (true)
{
fread(&chunk, sizeof(chunk), 1, audiofile);
snprintf(msg, sizeof(msg), "%c%c%c%c\t"
"%li",
chunk.ID[0], chunk.ID[1], chunk.ID[2], chunk.ID[3], chunk.size);
// Serial.println(msg);
if (*(unsigned int *)&chunk.ID == 0x61746164)
break;
/* Skip chunk data bytes. */
fseek(audiofile, chunk.size, SEEK_CUR);
}
/* Determine number of samples. */
sample_size = header.bitsPerSample / 8;
samples_count = chunk.size * 8 / header.bitsPerSample;
snprintf(msg, sizeof(msg), "Sample size = %i", sample_size);
// Serial.println(msg);
snprintf(msg, sizeof(msg), "Samples count = %i", samples_count);
// Serial.println(msg);
/* Configure the advanced DAC. */
if (!dac0.begin(AN_RESOLUTION_12, header.sampleRate, 256, 16)) {
// Serial.println("Failed to start DAC !");
// return;
}
audio_playing=1;
HandleAudio();
}
// ---- SERVO FUNCTIONS FOR HOLOCHESS PROJECT
// return Servo1 (Computer-side) to home position (end-stop switch1 will be polled moving backwards)
void homeservo1()
{
Serial.print(" Homing Servo1...");
servo1pwm->resume();
servo1pwm->pulsewidth_us(1530);
sensorVal = digitalRead(ENDSTOP1);
int t=7000;
while((sensorVal==1) && (t>0))
{
t--;
delay(1);
sensorVal = digitalRead(ENDSTOP1);
}
servo1pwm->suspend();
if (t>0) Serial.println("OK"); else Serial.println("Timeout!");
}
// return Servo2 (User-side) to home position (end-stop switch2 will be polled moving backwards)
void homeservo2()
{
Serial.print(" Homing Servo2...");
servo2pwm->resume();
servo2pwm->pulsewidth_us(1320);
sensorVal = digitalRead(ENDSTOP2);
int t=7000;
while((sensorVal==1) && (t>0))
{
t--;
delay(1);
sensorVal = digitalRead(ENDSTOP2);
}
servo2pwm->suspend();
if (t>0) Serial.println("OK"); else Serial.println("Timeout!");
}
// smooth movement of computer side servo (dir=0 -> out to battle ; dir=1 -> back home)
void moveservo1(int dir, int steps)
{
int t=steps;
servo1pwm->pulsewidth_us(1450);
servo1pwm->resume();
if (dir == 0)
{
Serial.print(F(" Moving Servo1 in="));
Serial.print(t);
for (int i=1450; i>(1450-t); i--)
{
servo1pwm->pulsewidth_us(i);
delay(7);
}
for (int i=(1450-t); i<1450; i++)
{
servo1pwm->pulsewidth_us(i);
delay(7);
}
}
else
{
Serial.print(F(" Moving servo1 out="));
Serial.print(t);
for (int i=1450; i<(1450+t); i++)
{
servo1pwm->pulsewidth_us(i);
delay(7);
}
for (int i=(1450+t); i>1450; i--)
{
servo1pwm->pulsewidth_us(i);
delay(7);
sensorVal = digitalRead(ENDSTOP1);
if (sensorVal == 0) i=1450;
}
delay(7);
}
servo1pwm->period_ms(20); // 20ms period=50Hz PWM frequency (standard Servo-control)
servo1pwm->pulsewidth_us(1450); // pulse 1ms..2ms ; middle = off for continous servo FS90r
servo1pwm->suspend();
Serial.println();
}
// smooth movement of player side servo (dir=0 -> out to battle ; dir=1 -> back home)
void moveservo2(int dir, int steps)
{
int t=steps;
servo2pwm->pulsewidth_us(1450);
servo2pwm->resume();
if (dir == 0)
{
Serial.print(F(" Moving Servo2 in="));
Serial.print(t);
for (int i=1450; i<(1450+t); i++)
{
servo2pwm->pulsewidth_us(i);
delay(7);
}
for (int i=(1450+t); i>1450; i--)
{
servo2pwm->pulsewidth_us(i);
delay(7);
}
}
else
{
Serial.print(F(" Moving servo2 out="));
Serial.print(t);
for (int i=1400; i>(1400-t); i--)
{
servo2pwm->pulsewidth_us(i);
delay(7);
}
for (int i=(1400-t); i<1400; i++)
{
servo2pwm->pulsewidth_us(i);
delay(7);
sensorVal = digitalRead(ENDSTOP2);
if (sensorVal == 0) i=1400;
}
delay(7);
}
servo2pwm->period_ms(20);
servo2pwm->pulsewidth_us(1450);
servo2pwm->suspend();
Serial.println();
}
Comments