I’ve been toying with a project idea for some time and have finally decided to build it.
AFK, which stands for “Away From Keyboard,” is a term predominantly used in the gaming world. When a player needs to leave the game temporarily, they might say, “I’m AFK” to inform their buddies.
There’s another use for AFK, often involving third-party software that performs a series of keyboard strokes based on a script. For example, this can be used for extended tasks in games like mining in Minecraft.
However, I envisioned a different purpose for this concept.
When you step away from your desk for a few minutes, your computer often switches to an idle state, particularly noticeable in instant messaging (IM) applications. This change in status lets others know you’re AFK.
Meet the AFK Buddy.
This device uses a Time-of-Flight (TOF) VL53L1X sensor to detect a person’s presence in front of the monitor. If it senses no one there, it acts like a mouse, moving the cursor in circles to keep the computer from appearing idle.
The device is built based on ESP32 S3 Dev Kit board with VL53L1X TOF sensor and 1 Addressable Led (FASTLED).
I have chosen S3 and not C3, Because C3 do not support HID interface that allows us to emulate mouse movements.
I have used also 32AWG PTFE wires for easy wiring and soldering.
The device case was designed using on shape and you can download the STL from thingiverse.
It was designed to be attached to a Lenovo 27″ monitor with a frame thinckness of 17 mm.
To make sure that the device only runs during my working hours and days, it needs to connect to the Wi-Fi and synchronize its time with an NTP server.
Once it has the correct time and day of the week, it will start measuring whether someone is present in front of the monitor.
You can easily determine its status by looking at the RGB LED: when it starts up, it will shine green, but after connecting to Wi-Fi and syncing with NTP, it will turn off.
Then, if it’s outside my working hours, the LED will be red. If it’s during my working hours and someone is in front of the monitor, the LED will remain off. Once the device senses that there’s no one in front of it, it will blink yellow every 3 seconds for 30 seconds and then turn white. The mouse cursor will than start to move in a small circle until it detects someone in front of the display again.
Keep in mind: in order for the ESP to act as a mouse, you must connect it to the USB connector on the ESP and not the COM.
#include "USB.h"
#include "USBHIDMouse.h"
#include "USBHIDKeyboard.h"
#include
#include
#include "FastLED.h"
#include
#include
#include
#define NUM_LEDS 1
#define DELAY 30
#define BLINK_DELAY 3000
#define START_TIME 8 // Modify this for staring work time
#define END_TIME 17 // Modify this for ending work time
const char* ssid = "NETWORK SSID";
const char* password = "NETWORK PASSWORD";
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP);
int dayOfWeek = -1;
String currentTime;
int currentHour;
int dow_range[5] = {0, 1, 2, 3, 4}; // Modify this array as needed where 0 is sunday and 6 is saturday
CRGB leds[NUM_LEDS];
VL53L1X sensor;
const int fastledPin = 4;
USBHIDMouse Mouse;
USBHIDKeyboard Keyboard;
int afk_trigger = 1500;
int trigger_millis = 0;
bool blink = true;
int blink_millis = 0;
void setup_wifi() {
unsigned long startAttemptTime = millis();
Serial.println("Connecting to WiFi...");
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
if (millis() - startAttemptTime > 60000) { // 60 seconds
Serial.println("Failed to connect to WiFi. Rebooting...");
ESP.restart(); // Reboot the ESP32
}
delay(500);
Serial.print(".");
}
Serial.println("WiFi connected");
Serial.println("IP address: ");
Serial.println(WiFi.localIP());
timeClient.begin();
// Set offset time in seconds to adjust for your timezone, for example:
// GMT +1 = 3600
// GMT +8 = 28800
// GMT -1 = -3600
// GMT 0 = 0
timeClient.setTimeOffset(2*3600);
}
void move_mouse_in_circle(int radius, int steps) {
float theta = 0;
float dTheta = 2 * PI / steps;
int lastX = radius;
int lastY = 0;
for (int i = 0; i < steps; i++) {
theta += dTheta;
int x = radius * cos(theta);
int y = radius * sin(theta);
Mouse.move(x - lastX, y - lastY);
lastX = x;
lastY = y;
delay(100); // Delay for visibility of movement
}
}
void init_tof_sensor(){
Wire.begin();
sensor.setTimeout(150);
while (!sensor.init())
{
Serial.println("Failed to detect and initialize sensor!");
sensor.setTimeout(100);
}
sensor.setDistanceMode(VL53L1X::Long);
sensor.setMeasurementTimingBudget(50000);
sensor.startContinuous(50);
}
void setup() {
Serial.begin(115200);
FastLED.addLeds(leds, NUM_LEDS);
leds[0] = CRGB::Blue;
FastLED.setBrightness(40);
FastLED.show();
setup_wifi();
leds[0] = CRGB::Green;
FastLED.setBrightness(40);
FastLED.show();
while (!Serial) { delay(10); }
Serial.println("VL53L1X test");
init_tof_sensor();
Serial.println("Test Passed.");
Mouse.begin();
Keyboard.begin();
USB.begin();
}
void get_day_and_time(){
timeClient.update();
unsigned long epochTime = timeClient.getEpochTime();
struct tm *ptm = gmtime((time_t *)&epochTime);
dayOfWeek = ptm->tm_wday;
Serial.print("Day of the week: ");
Serial.println(dayOfWeek);
Serial.println(timeClient.getFormattedTime());
currentTime = timeClient.getFormattedTime();
currentHour = timeClient.getHours();
}
boolean in_dow(){
int rangeLength = sizeof(dow_range) / sizeof(dow_range[0]);
for (int i = 0; i < rangeLength; i++) {
if (dayOfWeek == dow_range[i]) {
return true;
}
}
return false;
}
void loop() {
get_day_and_time();
if (currentHour >= START_TIME && currentHour < END_TIME && in_dow()){
int distance = sensor.readRangeContinuousMillimeters();
Serial.print("TOF Distance value : ");
Serial.println(distance);
if (distance > afk_trigger){
if (trigger_millis == 0){
trigger_millis = millis();
}
if (millis() > (trigger_millis + (DELAY*1000))){
leds[0] = CRGB::White;
FastLED.show();
move_mouse_in_circle(10, 5);
} else {
if ((blink_millis + BLINK_DELAY) < millis()){
blink = !blink;
if(!blink){
blink_millis = millis();
}
}
if (blink){
leds[0] = CRGB::Orange;
}else {
leds[0] = CRGB::Black;
}
FastLED.show();
}
} else{
trigger_millis = 0;
leds[0] = CRGB::Black;
FastLED.show();
}
} else {
leds[0] = CRGB::Red;
FastLED.show();
}
}