Quasar Free tool · Web Serial

Quasar Web Console Control Quasar Basic or Arduino Uno from the browser, over USB or Bluetooth (HC-05)

Connect your board and control it from this page, with nothing to install: switch pins on and off, read buttons and sensors, set PWM and servo angles, plot the analog inputs live and export them as CSV, run sequences and give commands with your voice. It works with Quasar Basic and with any Arduino Uno, over a USB cable or an HC-05 Bluetooth module.

Digital pins

Tap an output to switch it on or off, or choose Input / Pull-up to read a button or a sensor: its state updates by itself. D2 and D3 are used by the HC-05; if an nRF24L01 module is soldered, avoid D9–D13.

PWM

Analog inputs

Tap a value to read it, or turn on the automatic update.

Real-time plotter

Choose the analog inputs and the interval, then press Start: the board sends the readings and the graph draws them live. Export the data as CSV for a spreadsheet or for StarlyLab. Over Bluetooth use 200 ms or more.

Servo motor

Connect the servo signal wire to the chosen pin. Small servos like the SG90 can be powered from the 5V pin; larger ones need an external supply with the GND in common. While a servo is attached, PWM on pins 9 and 10 stops working.

90°

Sequences

Write one command per line and add WAIT followed by the milliseconds to pause. Lines starting with # are comments. The sequence is saved in this browser.

Examples:

Voice control

Press the microphone and say a command: the console turns it into a command for the board. The same idea as ElectroHome, the Android app from 2018 that controlled an Arduino by voice.

Try saying:

  • “turn on pin 13”
  • “turn off pin 13”
  • “toggle pin 7”
  • “set pin 9 to 50 percent”
  • “servo on pin 6 to 90 degrees”
  • “read analog 0”
  • “status”

In Chrome, speech recognition is done by Google's servers: the audio of your command is sent to Google to be converted into text. The connection with the board stays in your browser.

Terminal

How to use it

1

Upload the sketch

Open the Arduino IDE, select Arduino Uno as the board (for Quasar Basic too), paste or download the sketch below and upload it over USB.

2

Choose the connection

For USB you are ready: keep the cable connected. For Bluetooth, wire the HC-05 as in the table below and pair it with your computer or phone (default PIN 1234 or 0000).

3

Open the console

Use Chrome or Edge on a computer, or Chrome on Android. Choose the speed (9600 baud by default) and press Connect.

4

Choose the port

In the browser window select the USB port of the board or the paired HC-05. Then use the pins, the PWM sliders and the terminal.

HC-05 wiring

HC-05Quasar / Arduino UnoNote
VCC5Vthe HC-05 module accepts 5 V on VCC
GNDGND
TXDD2board receives (SoftwareSerial RX)
RXDD3through a voltage divider: 1 kΩ from D3, 2 kΩ to GND

The RXD pin of the HC-05 works at 3.3 V: the voltage divider lowers the 5 V signal of the board. The HC-05 TXD output can go directly to D2.

Commands

PINGchecks the connection, answers PONG QUASAR
ON 13 / OFF 13switches a digital pin from 4 to 13 on or off
TOGGLE 13inverts a digital pin
PWM 9 128PWM value from 0 to 255 on pins 5, 6, 9, 10, 11
MODE 7 INpin mode: OUT, IN or PULLUP; inputs report their changes by themselves
SERVO 6 90servo angle from 0 to 180 (SERVO 6 OFF releases it)
READ A0 / READ D7reads an analog input (0–1023) or a digital pin
STREAM 200 A0 A1sends the chosen analog inputs every 200 ms (20–5000); STREAM OFF stops
STATUSstate of all pins and analog inputs

The sketch

The sketch listens to commands both on USB and on the HC-05, at 9600 baud. It works on Quasar Basic and on Arduino Uno.

/*
  Quasar Console 2.0 - Starlyware
  https://www.starlyware.com/products/quasar/console

  Lets the Quasar web console control the board over USB or over a
  Bluetooth HC-05 module. Works on Quasar Basic and on Arduino Uno
  (any ATmega328P board at 16 MHz). In the Arduino IDE choose "Arduino Uno".

  HC-05 wiring:
    HC-05 VCC -> 5V
    HC-05 GND -> GND
    HC-05 TXD -> D2 (board RX for Bluetooth)
    HC-05 RXD <- D3 through a voltage divider (1 kOhm from D3, 2 kOhm to GND):
                 the HC-05 RX pin works at 3.3 V.

  Commands (one per line, upper or lower case), pins 4-13:
    PING                 -> PONG QUASAR
    HELP                 -> list of commands
    ON 13 / OFF 13       -> digital output HIGH or LOW
    TOGGLE 13            -> invert a digital output
    PWM 9 128            -> PWM 0-255 on pins 5, 6, 9, 10, 11
    MODE 7 IN            -> pin mode: OUT, IN or PULLUP
    SERVO 9 90           -> servo angle 0-180 (SERVO 9 OFF to release it)
    READ A0 / READ D7    -> read an analog input (0-1023) or a digital pin
    STREAM 50 A0 A1      -> send the analog inputs every 50 ms (20-5000)
    STREAM OFF           -> stop the stream
    STATUS               -> state of pins 4-13 and A0-A5

  Answers use KEY=VALUE pairs: D = digital output, P = PWM, I = input,
  U = input with pull-up, S = servo angle, A = analog input, T = time in ms.
  Digital inputs report their changes automatically.

  Note: while a servo is attached, the Servo library uses Timer1 and
  PWM on pins 9 and 10 stops working.

  Serial speed: 9600 baud on both USB and Bluetooth (HC-05 default).
  If your HC-05 is configured at 38400, change BT_BAUD below.
*/

#include <SoftwareSerial.h>
#include <Servo.h>

const uint8_t BT_RX_PIN = 2;   // connected to HC-05 TXD
const uint8_t BT_TX_PIN = 3;   // connected to HC-05 RXD (through the divider)
const long USB_BAUD = 9600;
const long BT_BAUD = 9600;

const uint8_t FIRST_PIN = 4;
const uint8_t LAST_PIN = 13;
const uint8_t LINE_MAX = 40;
const uint8_t MAX_ARGS = 8;

enum QMode : uint8_t { M_OUT, M_PWM, M_IN, M_PULLUP, M_SERVO };

SoftwareSerial bt(BT_RX_PIN, BT_TX_PIN);
Servo servos[LAST_PIN - FIRST_PIN + 1];   // one per pin 4-13 (10 of the 12 available)

uint8_t pinModeOf[LAST_PIN + 1];
int pinValue[LAST_PIN + 1];                // output value, PWM value, input level or servo angle
Stream *lastOut = &Serial;                 // where spontaneous messages go

uint8_t streamMask = 0;                    // bit n = stream analog input An
unsigned long streamInterval = 0;
unsigned long lastStream = 0;
unsigned long lastInputPoll = 0;

char usbLine[LINE_MAX];
uint8_t usbLen = 0;
char btLine[LINE_MAX];
uint8_t btLen = 0;

bool isPwmPin(int p) {
  return p == 5 || p == 6 || p == 9 || p == 10 || p == 11;
}

// Accepts "13" or "D13". Returns -1 if not a usable pin.
int parseDigital(const char *t) {
  if (!t) return -1;
  if (*t == 'D') t++;
  if (*t < '0' || *t > '9') return -1;
  int p = atoi(t);
  return (p >= FIRST_PIN && p <= LAST_PIN) ? p : -1;
}

// Accepts "A0".."A5". Returns -1 if not valid.
int parseAnalog(const char *t) {
  if (!t || t[0] != 'A' || t[1] < '0' || t[1] > '5' || t[2] != 0) return -1;
  return t[1] - '0';
}

char modeLetter(uint8_t m) {
  switch (m) {
    case M_PWM: return 'P';
    case M_IN: return 'I';
    case M_PULLUP: return 'U';
    case M_SERVO: return 'S';
    default: return 'D';
  }
}

void printPin(Stream &out, int p) {
  out.print(modeLetter(pinModeOf[p]));
  out.print(p);
  out.print('=');
  out.print(pinValue[p]);
}

void releaseServo(int p) {
  if (pinModeOf[p] == M_SERVO) servos[p - FIRST_PIN].detach();
}

void makeOutput(int p) {
  releaseServo(p);
  if (pinModeOf[p] == M_IN || pinModeOf[p] == M_PULLUP || pinModeOf[p] == M_SERVO) {
    pinMode(p, OUTPUT);
    digitalWrite(p, LOW);
    pinValue[p] = 0;
  }
  pinModeOf[p] = M_OUT;
}

void printStatus(Stream &out) {
  for (int p = FIRST_PIN; p <= LAST_PIN; p++) {
    printPin(out, p);
    out.print(' ');
  }
  for (int a = 0; a <= 5; a++) {
    out.print('A'); out.print(a); out.print('='); out.print(analogRead(A0 + a));
    if (a < 5) out.print(' ');
  }
  out.println();
}

void handle(char *line, Stream &out) {
  lastOut = &out;
  for (char *c = line; *c; c++) *c = toupper(*c);
  char *arg[MAX_ARGS];
  uint8_t n = 0;
  for (char *t = strtok(line, " "); t && n < MAX_ARGS; t = strtok(NULL, " ")) arg[n++] = t;
  if (n == 0) return;
  const char *cmd = arg[0];

  if (!strcmp(cmd, "PING")) {
    out.println(F("PONG QUASAR"));

  } else if (!strcmp(cmd, "HELP")) {
    out.println(F("OK PING HELP ON OFF TOGGLE PWM MODE SERVO READ STREAM STATUS"));

  } else if (!strcmp(cmd, "STATUS")) {
    out.print(F("STATUS "));
    printStatus(out);

  } else if (!strcmp(cmd, "ON") || !strcmp(cmd, "OFF") || !strcmp(cmd, "TOGGLE")) {
    int p = parseDigital(n > 1 ? arg[1] : NULL);
    if (p < 0) { out.println(F("ERR PIN 4-13")); return; }
    bool wasOn = pinModeOf[p] == M_OUT || pinModeOf[p] == M_PWM ? pinValue[p] > 0 : false;
    makeOutput(p);
    int v = !strcmp(cmd, "ON") ? 1 : !strcmp(cmd, "OFF") ? 0 : (wasOn ? 0 : 1);
    digitalWrite(p, v ? HIGH : LOW);
    pinValue[p] = v;
    out.print(F("OK ")); printPin(out, p); out.println();

  } else if (!strcmp(cmd, "PWM")) {
    int p = parseDigital(n > 1 ? arg[1] : NULL);
    if (p < 0 || !isPwmPin(p)) { out.println(F("ERR PWM PIN 5 6 9 10 11")); return; }
    if (n < 3) { out.println(F("ERR VALUE 0-255")); return; }
    makeOutput(p);
    int v = constrain(atoi(arg[2]), 0, 255);
    analogWrite(p, v);
    pinValue[p] = v;
    pinModeOf[p] = M_PWM;
    out.print(F("OK ")); printPin(out, p); out.println();

  } else if (!strcmp(cmd, "MODE")) {
    int p = parseDigital(n > 1 ? arg[1] : NULL);
    if (p < 0 || n < 3) { out.println(F("ERR MODE PIN OUT|IN|PULLUP")); return; }
    if (!strcmp(arg[2], "OUT")) {
      makeOutput(p);
      digitalWrite(p, LOW);
      pinValue[p] = 0;
    } else if (!strcmp(arg[2], "IN") || !strcmp(arg[2], "PULLUP")) {
      releaseServo(p);
      bool pullup = !strcmp(arg[2], "PULLUP");
      pinMode(p, pullup ? INPUT_PULLUP : INPUT);
      pinModeOf[p] = pullup ? M_PULLUP : M_IN;
      pinValue[p] = digitalRead(p);
    } else {
      out.println(F("ERR MODE OUT|IN|PULLUP")); return;
    }
    out.print(F("OK ")); printPin(out, p); out.println();

  } else if (!strcmp(cmd, "SERVO")) {
    int p = parseDigital(n > 1 ? arg[1] : NULL);
    if (p < 0 || n < 3) { out.println(F("ERR SERVO PIN 0-180|OFF")); return; }
    if (!strcmp(arg[2], "OFF")) {
      makeOutput(p);
      out.print(F("OK ")); printPin(out, p); out.println();
      return;
    }
    int angle = constrain(atoi(arg[2]), 0, 180);
    if (pinModeOf[p] != M_SERVO) {
      servos[p - FIRST_PIN].attach(p);
      pinModeOf[p] = M_SERVO;
    }
    servos[p - FIRST_PIN].write(angle);
    pinValue[p] = angle;
    out.print(F("OK ")); printPin(out, p); out.println();

  } else if (!strcmp(cmd, "READ")) {
    int a = parseAnalog(n > 1 ? arg[1] : NULL);
    if (a >= 0) {
      out.print('A'); out.print(a); out.print('='); out.println(analogRead(A0 + a));
      return;
    }
    int p = parseDigital(n > 1 ? arg[1] : NULL);
    if (p < 0) { out.println(F("ERR READ A0-A5|D4-D13")); return; }
    if (pinModeOf[p] == M_IN || pinModeOf[p] == M_PULLUP) pinValue[p] = digitalRead(p);
    printPin(out, p); out.println();

  } else if (!strcmp(cmd, "STREAM")) {
    if (n > 1 && !strcmp(arg[1], "OFF")) {
      streamMask = 0;
      out.println(F("OK STREAM OFF"));
      return;
    }
    long ms = n > 1 ? atol(arg[1]) : 0;
    uint8_t mask = 0;
    for (uint8_t i = 2; i < n; i++) {
      int a = parseAnalog(arg[i]);
      if (a >= 0) mask |= (1 << a);
    }
    if (ms < 20 || ms > 5000 || mask == 0) { out.println(F("ERR STREAM 20-5000 A0..A5")); return; }
    streamInterval = ms;
    streamMask = mask;
    lastStream = 0;
    out.println(F("OK STREAM ON"));

  } else {
    out.println(F("ERR UNKNOWN COMMAND"));
  }
}

// Collects characters until a newline, then runs the command.
void readLine(Stream &in, char *buf, uint8_t &len) {
  while (in.available()) {
    char c = in.read();
    if (c == '\r') continue;
    if (c == '\n') {
      buf[len] = 0;
      if (len > 0) handle(buf, in);
      len = 0;
    } else if (len < LINE_MAX - 1) {
      buf[len++] = c;
    }
  }
}

// Reports the inputs that changed since the last check.
void pollInputs() {
  for (int p = FIRST_PIN; p <= LAST_PIN; p++) {
    if (pinModeOf[p] != M_IN && pinModeOf[p] != M_PULLUP) continue;
    int v = digitalRead(p);
    if (v != pinValue[p]) {
      pinValue[p] = v;
      printPin(*lastOut, p);
      lastOut->println();
    }
  }
}

void sendStream() {
  lastOut->print(F("STREAM T="));
  lastOut->print(millis());
  for (uint8_t a = 0; a <= 5; a++) {
    if (!(streamMask & (1 << a))) continue;
    lastOut->print(F(" A")); lastOut->print(a); lastOut->print('=');
    lastOut->print(analogRead(A0 + a));
  }
  lastOut->println();
}

void setup() {
  for (int p = FIRST_PIN; p <= LAST_PIN; p++) {
    pinMode(p, OUTPUT);
    digitalWrite(p, LOW);
    pinModeOf[p] = M_OUT;
    pinValue[p] = 0;
  }
  Serial.begin(USB_BAUD);
  bt.begin(BT_BAUD);
  Serial.println(F("READY QUASAR CONSOLE 2"));
  bt.println(F("READY QUASAR CONSOLE 2"));
}

void loop() {
  readLine(Serial, usbLine, usbLen);
  readLine(bt, btLine, btLen);

  unsigned long now = millis();
  if (now - lastInputPoll >= 20) {
    lastInputPoll = now;
    pollInputs();
  }
  if (streamMask && now - lastStream >= streamInterval) {
    lastStream = now;
    sendStream();
  }
}

Frequently asked questions

Chrome and Edge on Windows, macOS, Linux and ChromeOS, and Chrome 137 or later on Android (Bluetooth only). Safari, iPhone, iPad and Firefox do not support Web Serial.

Yes. On a computer, connect the board with the USB cable and choose its port: the sketch answers on USB too. On Android only the Bluetooth connection is available.

Yes. The sketch and the console work with any Arduino Uno or ATmega328P board at 16 MHz, with the same wiring.

Not with this page: the HC-05 uses classic Bluetooth with the serial profile (SPP), which is what Web Serial supports. BLE modules work in a different way.

No. The connection runs entirely in your browser, between the page and your board. Only the voice control uses Google's speech recognition in Chrome, which receives the audio of the command.

The HC-05 uses a software serial port: while the board is sending data, it cannot receive. Over Bluetooth use an interval of 200 ms or more; over USB you can go down to 50 ms.

Back to Quasar Basic