Project Overview


I am working on a “smart watch” with IR and radio control capabilities. The images and videos below depict an Arduino protoype of the device. The project is coded in C and I used PlatformIO (an open source development platform for embedded systems that is accessed through VSCode) for developing and deploying code. The current prototype only has functionality for IR signals which are described below. Some of the documentation is still in progress.

Current Capabilities:

– Copy IR signals from a source

– Save IR signals

– Send IR signals

– Name IR signals

– Delete IR signals

Error loading image

Functions:

Send Signal

The demo above shows the Arduino prototype sending signals to the TV. The signals were saved to the Arduino before the presentation. The IR transmitter only works at short distances. I am assuming this is due to a low current output by the Arduino (the IR transmitter is rated for up to 100mA but an Arduino GPIO pin has a max output of 40mA). This problem will be addressed in the future, but this proves the concept.

Copy and Save Signal

Delete Signal

Rename Signal

Programming

Encoder Commands

The encoder is the input device for the IR controller. Mechanically, it can be rotated or pressed.

alt text

A struct was made for the encoder. The struct holds pointers to global variables that are updated when the encoder is rotated or pressed.

Encoder Struct


typedef struct{
    volatile int* currentPosition;
    int lastPosition;
    volatile bool* CLK_state;
    volatile bool* DT_State;
    volatile bool* ButtonPressed;
}Encoder;

The rotation and button press rely on hardware interrupts. An initalization function is used to make an encoder instance, define the Arduino pins for the encoder, initalize the hardware interrupts, and set the pointers of the encoder instance equal to the addresses of the global variables.

Global Variables Defintions


//true definitions of variables
volatile int encoderPosition = 0;
volatile bool CLK_State = false;    //Output A
// volatile int CLK_lastState;
volatile bool DT_State =false;      //Output B
volatile bool EncoderButtonPressed = false;

Encoder Initialization Function


Encoder encoder_KY040_init(){
  Encoder createdEncoder;
  //define pins
  pinMode(ROTARY_ENCODER_CLK, INPUT_PULLUP);
  pinMode(ROTARY_ENCODER_DT, INPUT_PULLUP);
  pinMode(ROTARY_ENCODER_SW, INPUT_PULLUP);

  //set interrupt for the encoder button
    attachInterrupt(digitalPinToInterrupt(ROTARY_ENCODER_SW), handleEncoderButtonPress, FALLING); // Trigger on rising edge
  // set interrupt for rotate encoder
    attachInterrupt(digitalPinToInterrupt(ROTARY_ENCODER_CLK), handleEncoderRotate, CHANGE);

  // Initialize CLK and DT pin states
  CLK_State = digitalRead(ROTARY_ENCODER_CLK);
  DT_State = digitalRead(ROTARY_ENCODER_DT);

  createdEncoder.ButtonPressed = &EncoderButtonPressed;
  createdEncoder.CLK_state = &CLK_State;
  createdEncoder.DT_State = &DT_State;
  createdEncoder.currentPosition = &encoderPosition;
  createdEncoder.lastPosition = *createdEncoder.currentPosition;
  return createdEncoder;
}

There are two ISR functions, one for the button and one for the rotation of the encoder.

ISR (Interrupt Serivce Routine) Functions

Button ISR


void handleEncoderButtonPress() {
    EncoderButtonPressed = true; // Set flag to indicate button press
}

Rotation ISR


// Interrupt Service Routine (ISR) for Encoder rotate
void handleEncoderRotate() {
  bool aVal = digitalRead(ROTARY_ENCODER_CLK);
  bool bVal = digitalRead(ROTARY_ENCODER_DT);
  
  if (CLK_State && !aVal) {
    if (bVal && !DT_State) {
      encoderPosition++;
    }
    if (!bVal && DT_State) {
      encoderPosition--;
    }
  }

  CLK_State = aVal;
  DT_State = bVal;
}

Check Encoder Rotation Function


uint8_t checkRotation(Encoder encoder){
  if(*encoder.currentPosition != encoder.lastPosition){
    if(*encoder.currentPosition > encoder.lastPosition){
      //clockwise turn
      return 1;
    }else if(*encoder.currentPosition < encoder.lastPosition){
      //counter-clockwise turn
      return 2;
    }
  }
  return 0;
}

Since there is only a single button, I made functions to recognize different patterns to distinguish different inputs. I made a function for a button press, a double click, and a button hold.

Button Press Function


uint8_t buttonPress(){
  if(EncoderButtonPressed){
    EncoderButtonPressed = false;
    return 1;
  }
  return 0;
}

Double Click Function


uint8_t doubleClick() {
    // Check start time
    uint16_t startTime = millis();
    uint16_t diff;
    uint8_t doubleClickDetected = 0;
    if (EncoderButtonPressed == true) {
      doubleClickDetected = 1;
    }

    // Loop for short window to see if double click
    while ((diff = millis() - startTime) < 300) {
        // See if encoder button is pressed
        if (digitalRead(ROTARY_ENCODER_SW) == LOW) {
            // Wait for debounce time
            delay(50);
            // Check if button is still pressed after debounce
            if (digitalRead(ROTARY_ENCODER_SW) == LOW) {
                if (!doubleClickDetected) {
                    doubleClickDetected = 1;
                    // Wait for button to be released
                    while (digitalRead(ROTARY_ENCODER_SW) == LOW);
                    // Debounce release
                    delay(50);
                    // Update start time for second press
                    startTime = millis();
                } else {
                    // Double click detected
                    EncoderButtonPressed = false;
                    return 1;
                }
            }
        }
    }
    return 0;
}

Button Hold Function


uint8_t buttonHold() {
    if(digitalRead(ROTARY_ENCODER_SW) == LOW){
      uint16_t startTime = millis();
      uint16_t diff;
      while (digitalRead(ROTARY_ENCODER_SW) == LOW) {
          diff = millis() - startTime;
          if(digitalRead(ROTARY_ENCODER_SW) == HIGH){
            break;
          }
          if (diff > 500) {
              EncoderButtonPressed = false;
              return 1;
          }
      }
    }
    return 0;
}

I made a buttonCommand function that outputs a 1, 2, or 3 depending on which button function (buttonHold, doubleClick, or buttonPress) is exectuted. This function is easier to call in the main loop because I can call buttonCommand instead of calling each button function individually.

Button Command Function


uint8_t buttonCommand(Encoder encoder){
  while(digitalRead(ROTARY_ENCODER_SW) == HIGH || EncoderButtonPressed == false){
    if(*encoder.currentPosition != encoder.lastPosition){
      return 0;
    }
  }
  if(buttonHold()){
    //EncoderButtonPressed = false;
    return 3;
  }
  else if(doubleClick()){
    EncoderButtonPressed = false;
    return 2;
  }
  else if(buttonPress()){
      return 1;
    }
  EncoderButtonPressed = false;
  return 0;
}

LCD Functions

Documenting in progress

An LCD driver and a basic functions for drawing characters, words, and simple shapes was provided for the LCD screen on the manufacturer website.

I made custom function “centerString” which allowed me specifiy a row on the LCD screen and write a string that was centered on the specified row.



//center string on LCD row
PointCoordinates centerString(const char *str, sFONT* font, int row, uint16_t color_background, uint16_t color_foreground, int clear) {
  
  PointCoordinates startingPixels;                //variable to hold pixel coordinates to where the string will start
  startingPixels.x = 0;
  startingPixels.y = 0;
  
  //check if row selected is outside circle
  if(row < 0 || row > 239){
    return startingPixels;
  }
  
  int stringWidth = font->Width * strlen(str);      //calculate string width
  int centerX = 120;                                //calculate string width
  int startX = centerX - (stringWidth/2);           //starting x coord

  //adjust of starting poistion being less than 0
  if(startX < 0){
    startX = 0;
  }

  int startY = row;   //starting y coord

  //do not clear
  if(clear == 0){
  Paint_DrawString_EN(startX, startY, str, font, color_background, color_foreground);
  }

  //clear
  else if(clear == 1){
    Paint_DrawString_EN(startX, startY, str, font, color_background, color_background);
  }

  startingPixels.x = startX;            // store startng pixelX
  startingPixels.y = startY;            // store starting pixelY
  return startingPixels;
}

The “centerString” function is used when showing the names in the selection menu for the IR signals.

Copy Signals

The device is able to copy signals using an IR reciever and send them back out with an IR transmitter. IR remotes work by sending a sequence of high and low pulses to a receiver. For many remotes high IR pulses signify the start of a new signal and the duration of the following low pulse determines if the sent signal represents a 1 or 0. For example, the specific remote I worked with started a new signal with a 9000us high pulse followed by a 4300us low pulse. After the initial pulses, a high pulse ranging from 570us-605us would be used to seperate low pulses. The remote would send 2 lengths of low pulses, one around 1670us and the other around 520us. One of the durations represents a 1 in binary and the other represents a 0. In this case, I am not sure which is which because decoding the signal is not necessary for this function as the signal only has to be replicated by the device and not understood.

Error Loading Image The above image shows the first 20 and last 20 signals of a captured IR signal from a remote. The signal on the right represents the first 8 pusles of the captured signal.

The copied IR signal is saved to a variable that is of custom type, “IRSignal”. The IRignal stores the durations of each of the copied pulses and the total number of pulses copied for a given IR signal.

Error Loading Image Custom struct made to store results of a copied signal. The “pulse_durations” is an array that stores all the time durations in microseconds of all the pulses captured during the signal copy funciton. The “length” variable is the number of pulses captured for a copied signal.

Copy Signal C Function

The microcontroller begins saving the durations of the pulses after detecting a high pulse on the IR reciever pin. A new pulse duration is saved when it detects a change in the state of the IR reciever pin. It stops saving signals when the receiver pin does not change state for more than 0.3s which signifies the end of an IR signal. The code below shows the C function I made to copy the signal.



int copyIRSignal(IRSignal* sample){

    digitalWrite(IR_TRANS_PIN, LOW);    //disable IR transmitter to prevent interference
    digitalWrite(EN_IR_REC, HIGH);      //enable IR reciever

    //wait for receiver to settle after being activated
    while(digitalRead(IR_REC_PIN) == LOW){
        ;
    }
    EncoderButtonPressed = false;                               // set initial state of encoder button
    bool PinState;                                              // define varaible to track pin state
    signalStarted = LOW;                                        // Initially set detected signal as false
    sample->length = 0;                                         // initalize sample index to 0
    centerString("Scanning", &Font20, 120, BLACK, GREEN, 0);    // notfiy user that device is scanning for signal 
    delay(50);

    //wait fo signal to start
    while(!(signalStarted)){
        // check reciever pin to see if a signal is being recieved
        if(digitalRead(IR_REC_PIN) == LOW){     
            signalStarted = true;               // signal has started
            lastTime = micros();                // initialize lastTime
            currentTime = micros();             // initialize currentTime 
            PinState = LOW;                     // initialize pin state
            break;
        }

        //if button is pressed cancel signal capture function
        if(digitalRead(ROTARY_ENCODER_SW) == LOW){
            break;         
        }
    }
    //Record signal while signal is active and pulse widths are less than 30ms
    while(signalStarted){
        // if pinState changes then new pulse width has started. Save old pulse width
        if(PinState != digitalRead(IR_REC_PIN)){
            currentTime = micros();                                                 // record time when new pulse width starts
            sample->pulse_durations[sample->length] = (currentTime - lastTime);     // calculate old pulse width and save it to sample array 
            sample->length += 1;                                                    // update pulse_durations index
            lastTime = currentTime;                                                 // update lastTime
            PinState = digitalRead(IR_REC_PIN);                                     // update pin state
        }
        // if no pinstate change occurs within 30ms, assume signal has ended
        else if( (micros()-currentTime) > 30000){
            break;
        }
    }
    
    digitalWrite(EN_IR_REC, LOW);       //disable IR reciever
    
    //reset IR macros at the end of transmission
    lastTime = 0;
    currentTime = 0;
    signalStarted = LOW;

    //if signal captured
    if(EncoderButtonPressed == false){
        centerString("Scanning", &Font20, 120, BLACK, GREEN, 1);            // remove "scanning" from LCD
        centerString("Signal Captured!", &Font16, 120, BLACK, GREEN, 0);    // notify user of signal capture
        centerString("Signal Captured!", &Font16, 120, BLACK, GREEN, 1);    // remove "Signal Captured!" from LCD
        return 1;
    }
    // if signal capture canceled
    else if(EncoderButtonPressed == true){
        centerString("Scanning for signal", &Font20, 120, BLACK, GREEN, 1); // remove "scanning for signal" from LCD
        centerString("Scan Canceled", &Font20, 120, BLACK, GREEN, 0);       // notify user of canceld scan
        centerString("Scan Canceled", &Font20, 120, BLACK, GREEN, 1);       // remove "Scan Canceled" from LCD
        EncoderButtonPressed = false;
        return 0;
    }
}

Sending Signal

There is also a send signal function that sends a signal through the IR transmitter. The send signal function works by toggling the transmitter ON and OFF for the durations saved in the “pulse_durations” array inside the “IRSignal” struct (shown above). The send signal function assumes a high pulse to begin with, then toggles when starting the next pulse in the “pulse_durations” array until it reaches the end of the array.

Hardware Connections

Error Loading Image Wiring Schematic for the Arduino

Most of the pins of the Arduino are used and pins that have two names next to them means two signals are sharing the same pin. The CC1101 and NRF24L01 are signals for two different radio chips that haven’t been implemented yet. Hardware modfications will be made in the future to allow for more GPIO pins.

Future Improvements

The IR features here will eventually be integrated into a “smart watch” that also has similar copying capabilites for radio signals. I may try to add spectrum analyzing features for certain frequency bands in the future to identify signals within a short range of the user, but this may be impossible due to deisgn constraints around size and power. The RAM on the Arduino is almost maxed out, so I am planning to switch to different hardware with more RAM. I may switch over to prototyping with a Raspberry Pi, but before making a decision I need to do more research. The GPIO pins are also maxed out, and so I will need hardware to extend the number of available pins, or reduce the number of pins I am using. I may switch to touch screen to reduce the number of pins needed for controls, and it may provide a more friendly user interface. After testing is complete and code has been made, I plan to make a PCB for the watch and 3D print an enclosure.