Introduction: Quantum Wheelchair Arduino Seat Function Controller

My wife, sitting 24/7 in an electric Quantum iLevel wheelchair, needed a way to control the seat functions of the wheelchair without using her hands. The wheelchair came with a Stealth Head Array, which allows to control the wheelchair by taping with the head to the right and left. However, since my wife can't control hands or head, this function turned out to be useless. We asked the wheelchair vendor, as well as the mobility provider for a simple app running on a computer or tablet, but they couldn't provide a solution for the wheelchair. Attached to the wheelchair is a "Tobii Dynavox TD Pilot", which is an iPad Pro with built-in eye-tracker. So the goal was to control the wheelchair via eye control via a standalone app or website.

Luckily I was able to reengineer the signals used by the Stealth Head Array, which is connected via a Sub-D9 connector, but uses only 3 pins with high/low signals to control the wheelchair seat functions. The pins and signal level are described further below.

So I bought an Arduino ESP8266 (includes WiFi function), connected it to a 4 Channel 5V Relay Module, wrote a little webserver, attached it to my wife's wheelchair, and voila, she was able to control the wheelchair seat functions with her eyes.  

Supplies

Step 1: Assembling/Wiring Arduino, Power Supply, 4 Channel Relay Module

The Arduino and Relay Module operating voltage is 5V. Therefore the 5/3.3V Power Supply Module is required to provide the 5V. Luckily the trigger voltage for the Relay Module is 2.5 to 5V, therefore it works with the 3.3V IO ports of the Arduino. The Relay Module is required to decouple the 12V wheelchair voltage from the circuit.

The male connector, on the wheelchair side which would connect to the head-array, is using the following pins and signals:

  • Pin 8: ground, 0V - we use it to pull the active-low signals down from 12V to 0V
  • Pin 6: 12V (inactive), 0V (active) - active signal for at least 200ms is switching between wheelchair functions, like drive mode, setup, seat functions. This is an optional function and does not need to be used.
  • Pin 3: 12V (inactive), 0V (active) - active signal for at least 200ms is toggling between seat functions, like backrest, footrest, ...
  • Pin 4: 12V (inactive), 0V (active) - active signal for at least 200ms, but less than 500ms is toggling between up and down direction for the previously selected seat-function. If the pin is pulled-down more than 500ms, the seat is actually moving. In the Arduino webserver I provide later on 5 buttons to deal with this pin only - one for toggling between up/down function and 1sec/2sec/4sec/8sec buttons for actually move the seat for this amount of time.

The 1st relay (leftmost) is optional. It serves to activate the alternative control of the seat functions, away from the default joysticks. This function is by default provided by a simple egg-switch; when pressed (low-active) it toggles the control between joystick and alternative control (e.g. head-array or Arduino in our case).

For powering the Arduino and the Relay board, I use a cheap 9V power adapter. Alternatively a 9V battery block works too.


Step 2: Setting Up Arduino IDE

I'm not planning to get into the details in regard to setting up the Arduino IDE. There are hundreds of websites who explain Arduino IDE basics. But, to support ESP8266 Wifi function, I had to install via the BoardManager in the Arduino IDE the following packages/libraries:

  • ESP8266WiFi
  • ESPAsyncWebServer

For programming and debugging the ECP8266 I used the default USB connection between PC/MAC and the ESP8266. After programming the device, the USB cable can be removed.

Step 3: ESP8266 Sketch

The ESP8266 code/sketch is attached. Some explanation to the code:

#include <ESP8266WiFi.h>
#include <ESPAsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include "webpage.h"

... includes all necessary library header files. "webpage.h" contains the HTML/CSS code for the single webpage the ESP8266 webserver provides.

const char *ssid = "your wifi ssid";
const char *password = "your wifi password";

... hard-codes the WiFi SSID (name of your home WiFi) and the password. They ned to be hard-coded, since the project does not include any type of display or input device.

// web server
AsyncWebServer server(80);

... creates the webserver.

const int GPIO_EYE_ENABLE = 12;
const int GPIO_MAIN_FUNCT = 13;
const int GPIO_SEAT_FUNCT = 4;
const int GPIO_SEAT_UPDWN = 5;

... these are the GPIO pins used for controling the relays.

// button ID
enum buttons {
BUTTON_EYE_ENABLE = 1,
BUTTON_MAIN_FUNCT,
BUTTON_SEAT_FUNCT,
BUTTON_SEAT_UPDN1,
BUTTON_SEAT_UPDN2,
BUTTON_SEAT_UPDN3,
BUTTON_SEAT_UPDN4,
BUTTON_SEAT_UPDN5,
};

... defines the used buttons for the website.

//------------------------------------------
void setup()
{
Serial.begin(115200);

// set GPIO output to low
digitalWrite(GPIO_EYE_ENABLE, LOW);
digitalWrite(GPIO_MAIN_FUNCT, LOW);
digitalWrite(GPIO_SEAT_FUNCT, LOW);
digitalWrite(GPIO_SEAT_UPDWN, LOW);

// config GPIOs as output
pinMode(GPIO_EYE_ENABLE, OUTPUT);
pinMode(GPIO_MAIN_FUNCT, OUTPUT);
pinMode(GPIO_SEAT_FUNCT, OUTPUT);
pinMode(GPIO_SEAT_UPDWN, OUTPUT);

... configures the IO pins as output, with default low level (relay = off).

  // Connect to Wi-Fi
Serial.println();
Serial.print("Connecting to ");
Serial.println(ssid);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED)
{
delay(500);
Serial.print(".");
}
// print local IP address and start web server
Serial.println();
Serial.println("WiFi connected.");
Serial.print("IP Address: ");
Serial.println(WiFi.localIP());

... initializes the WiFi function and connects to your home Wifi. At the end of establishing a connection it prints the IP address the webserver has received from your router/DHCP. The IP address can be used later on to connect to your webserver via any browser.

  // Route for root / web page
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
{
request->send_P(200, "text/html", index_html);
});

// Send a GET request to <ESP_IP>/update?output=<val_output>&state=<val_status>
server.on("/update", HTTP_GET, [](AsyncWebServerRequest *request)
{
String val_output = "na";
if (request->hasParam("output"))
{
val_output = request->getParam("output")->value();
Serial.println("GPIO" + String(val_output));

... evaluate what button was pressed on the website.

      switch(val_output.toInt())
{
case BUTTON_EYE_ENABLE:
digitalWrite(GPIO_EYE_ENABLE, HIGH);
delay(250);
digitalWrite(GPIO_EYE_ENABLE, LOW);
break;
case BUTTON_MAIN_FUNCT:
digitalWrite(GPIO_MAIN_FUNCT, HIGH);
delay(250);
digitalWrite(GPIO_MAIN_FUNCT, LOW);
break;
case BUTTON_SEAT_FUNCT:
digitalWrite(GPIO_SEAT_FUNCT, HIGH);
delay(250);
digitalWrite(GPIO_SEAT_FUNCT, LOW);
break;
case BUTTON_SEAT_UPDN1:
digitalWrite(GPIO_SEAT_UPDWN, HIGH);
delay(250);
digitalWrite(GPIO_SEAT_UPDWN, LOW);
break;
case BUTTON_SEAT_UPDN2:
digitalWrite(GPIO_SEAT_UPDWN, HIGH);
delay(1000);
digitalWrite(GPIO_SEAT_UPDWN, LOW);
break;
case BUTTON_SEAT_UPDN3:
digitalWrite(GPIO_SEAT_UPDWN, HIGH);
wdt_disable(); // disable WDT
delay(2000);
wdt_enable(WDTO_2S); // enable WDT with a timeout of 2 seconds
digitalWrite(GPIO_SEAT_UPDWN, LOW);
break;
case BUTTON_SEAT_UPDN4:
digitalWrite(GPIO_SEAT_UPDWN, HIGH);
wdt_disable(); // disable WDT
delay(4000);
wdt_enable(WDTO_2S); // enable WDT with a timeout of 2 seconds
digitalWrite(GPIO_SEAT_UPDWN, LOW);
break;
case BUTTON_SEAT_UPDN5:
digitalWrite(GPIO_SEAT_UPDWN, HIGH);
wdt_disable(); // disable WDT
delay(8000);
wdt_enable(WDTO_2S); // enable WDT with a timeout of 2 seconds
digitalWrite(GPIO_SEAT_UPDWN, LOW);
break;
}

... execute the function based on the button pressed by the user.

    }
request->send(200, "text/plain", "OK");
});

// Start server
server.begin();
}

//------------------------------------------
void loop()
{
}

Step 4: Demo How to Change the Seat Position of a Quantum ILevel Wheelchair