mac70
Published © CC BY-NC-SA

A single-player Holochess table

Project for May the 4th (Star Wars Day). Taking inspiration from a galaxy far, far away : I built my own holochess table

AdvancedFull instructions provided2,928
A single-player Holochess table

Things used in this project

Hardware components

Arduino Giga
×1
TFT LCD, 2.4 "
TFT LCD, 2.4 "
https://www.waveshare.com/wiki/2.4inch_LCD_Module
×6
Servo FS90r
×2
ASKA3D Plate
×1
Arduino Nano R3
Arduino Nano R3
×1

Software apps and online services

Arduino IDE
Arduino IDE

Story

Read more

Custom parts and enclosures

3D table parts

repository for the 3d printed table parts also here: https://www.printables.com/model/1285004-holochess-table

Schematics

Adapter PCB for the displays

Simple prototype adapter PCB for connection of 6 serial displays to Arduino Giga

Code

HoloControls Code

C/C++
Code for Arduino Nano for the control boards
/* 
  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 code base

C/C++
Holochess project for Arduino Giga
/***************************************************
   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 functions

C/C++
USB module for Holochess Giga project
// -- 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");     
  }

}

 

Sound functions for Holochess

C/C++
Sound functions for Arduino Giga
// ---- 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

C/C++
Servo module for Arduino Giga
// ---- 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(); 

}
 

Credits

mac70
7 projects • 43 followers

Comments