In this project we will look into a way to construct a user-input-device for ESP32 and other MCU's that are capable of reading capacitive touch values from at least 3 available GPIOs. It is very easy to make and costs literally nothing.
The hardest part will be to reliably attach 4 wires to your MCU pins. That said, due to the nature of capacitive touch interfaces, makers will have to experiment with isolation, shapes and sizes to optimize their results.
However I tried to specify things to a level that allows for out-of-the-workbench-operation. This device is best compared to a trackball, mousepad, joystick or similar, yet it is none of the above. Its extreme simplicity comes with some moderate catches that require the user to get familiar with it. However, then it is fully capable of doing the job - while admittedly still being fairly uncomfortable, compared to a real mouse, and in the context of the onscreen keyboard that is used with this device, a real pain compared to a mechanical full-size keyboard - of course. Yet, one can enter a short text, and in the case of my XPLORA software-project, send it over radio-waves using the TTGO's LoRa chip.
Materials used:
1 Sheetmetal-bottom of potato-chips-roll
1 broad stickytape thin cargo brown
4 wires
Tin and soldering iron
Hard to believe but that's really all. Of course an MCU, ideally a TTGO, is needed, the code should work for TTGO oled 0.96 LoRa V2-1.6.1. and the other TTGO oled versions, however version 1.0 has one GPIO swapped and the source need to be edited accordingly. Generally the specific code sections posted here should work on any ESP32, provided pin-assignments are adjusted.
Construction guide
Tin/wires can be soldered to these pads easily (very unlike on aluminum). The pads need rounded corners and have at least about the size of a fingertip or a good 1/4 square inch. The smaller, the less sensitive they are, so they need a certain minimum size, operation may fail when too small.
One pad is a simple touch button, like a left mouse-click. The other 3 pads facilitate the mouse control device.
Consider the following pin-out and design guide:
The way this works is as follows:
Pad A (GPIO 14) only checks whether the finger touches the pad. Pads t1 and t2 (GPIOs 12 and 04 ) are constantly measuring the capacitive coupling and do detect proximity few millimeters before touching. Whenever the state of pad A is "1" (touched), the values of pad t1 and t2 will be compared to the respective values measured 10ms in the past, and a ramp-up or ramp-down difference can be detected and used to determine finger x / y trajectory and speed.
It's important to note that with smaller pads, the tendency of pad-spillover rises (touching one button affects the other buttons), esp. with too small gaps between them. Also the wires from the pads may impact one-another, as well as the pads if significant lengths of wire are running along them. With capacitive sensors it's all about surface area and distance. A flat ribbon cable that has some space between the wires may be best.
Importance of the correct gaps
Distances between pad A vs pads t1/t2: 3 mm, between t1 and t2: 1.5 to 2mm (more if pads are small). Some eperimentation may be required to get the optimal distances. If these distances are excessively wrong, the device may fail to work.
People may experiment with "zigzag" shaped seams between t1 and t2 to get a broader and smoother gradient when both pads are touched for diagonal motion. It is advised to experiment with the distances while having the measured values of all sensors on the screen. When the desired design was found and the prototype works, the code can be further optimized.
Diverting from the prototype design later on may require the code to be adjusted, or if designed badly, may not work sufficiently at all. Hence I advise you to design this device including the wiring carefully, so it's working and can be reproduced identically.
Once an ideal shape is found, people may just etch it on a PCB. (covered with spray paint) Pads should have no sharp corners or edges. Such must be cut slightly round. Precise Flattening and sanding of the edges is advised.
Pads must be fixed firmly to a rigid surface and should not wobble or bend when pressure is varied.
Operating or putting the device on the Ground may have undesired side-effects due to capacitive earth coupling. The same is true when holding it in the palm of ones hand, it may affect the buttons already. It is best used when laying on a table or similar elevated object.
The metal pads are then covered by stickytape or a spray-paint layer, and mounted inside a suitable case. Currently I am using a part of an old USB cable for connection, but it's so sturdy and rebellious - better use a cable that doesn't have its own agenda next time.
The Software
Here I am using C++ in Arduno IDE, but I would think this works with other IDEs too.The touch-capable pins of the ESP32 will deliver a certain base-level value when untouched, that is typically around 600. It is slightly noisy, fluctuates by +-2. When a pad is touched by a finger then the value goes down to about 400. However the value starts sinking even before we actually touch the pad. This window of proximity sensing we are going to use. But first things first. Due to fluctuations and noise, right after booting we first measure the baseline for a while and calculate the approximate base-level.
But some globals must first be declared before setup():
int touch_baselevel1=0; // x distance sensor // touchbase level when untouched
int touch_baselevel2=0; // y distance sensor
int touch_baselevel3=0; // touchpad physical contact sensor
int touch_baselevel4=0; // "leftclick" sensor
int approx1=0;// actual touch pin reads (x ramp)
int approx2=0; //y ramp
int approx3=0; // pad contact sensor for ramp algo
int approx4=0; // for leftclick separate button
int lastapprox1=0;// to remember previous touch pin reads (for ramp detection)
int lastapprox2=0;
int mytouchpin1=4; // user input touchpad GPIO use...
int mytouchpin2=12;
int mytouchpin3=14; // these 2 we share with the sd-card, may have to set again as input after card access.
int mytouchpin4=15; //14=touch_sd_sclk, 15=touch_sd_mosi, btw: 25=adc_led, 34,35=adc;
int mousex=64; // calculated mouse coords, also sets initial mouse coords
int mousey=52;
int realmousex=0; // smoothed mouse coords (rubberband-follower)
int realmousey=0;
Having declared the touchpins as globals and assigned the gpio pins before setup(), inside setup() we will then...
pinMode(mytouchpin1 , INPUT); // used as touch sensor x-motion gpio 04
pinMode(mytouchpin2 , INPUT); // used as touch sensor y-motion gpio 12
pinMode(mytouchpin3 , INPUT); // used as touch sensor finger contact gpio 14
pinMode(mytouchpin4 , INPUT); // used as touch sensor separate "secure" leftclick button gpio 15
// touchpin calibration / get average base level
for(int i=0;i<10;i++){
touch_baselevel1+=touchRead(mytouchpin1);
touch_baselevel2+=touchRead(mytouchpin2);
touch_baselevel3+=touchRead(mytouchpin3);
touch_baselevel4+=touchRead(mytouchpin4);
// todo: find way to auto-calibrate frequently (aka how to know the user doesn't touch anything)
delay(4);
}
touch_baselevel1/=10;
touch_baselevel2/=10;
touch_baselevel3/=10;
touch_baselevel4/=10;So from now on we only have to ask whether a certain pin reads for example "less than touch_baselevel4 - 150" to detect a button touch. We also ignore the noise by setting a threshold window. We furthermore account for the fact that with this way of gradient measurement, simply put, our mouse moves up slower than down, so we boost it slightly when moved upwards, and on the X-axis we do the same.
Notice variables mousex, mousey, realmousex, realmousey etc. are global ints, declared before setup(). That way you can access them from any function at any time. The following myUpdateMouse() function should be called in a loop about every 10 to 20 ms.
void myUpdateMouse()
{
// user input, touchpin 12, 14, 4 ,15, see definitions in globals section
lastapprox1=approx1;
lastapprox2=approx2;
approx1=touchRead(mytouchpin1); // these variable names seem a bit misleadig, but anyway
approx2=touchRead(mytouchpin2);
approx3=touchRead(mytouchpin3);
approx4=touchRead(mytouchpin4);
int mi=1; // window for gradient detection / noise exclusion
int ma=150;
// mouse navigation...
if(approx3 <(touch_baselevel3-35)) // finger contact? only compute motion if finger is touching
{
// any ramp up or down of distance x sensor?
if ( (approx1 < (touch_baselevel1-10)) && (abs(approx1-lastapprox1) >mi) && (abs(approx1-lastapprox1) <ma) )
{
int tmpx=(approx1-lastapprox1)/4; // this divisor affects mouse sensitivity...
if(tmpx>0){tmpx*=1.4;} // boost if positive
mousex-=tmpx;
if(mousex>129) mousex=129;
if(mousex<-2) mousex=-2;
}
// any ramp up or down of distance y sensor?
if ( (approx2 < (touch_baselevel2)-10) && (abs(approx2-lastapprox2) >mi) && (abs(approx2-lastapprox2) <ma) )
{
int tmpy=(approx2-lastapprox2)/6; // this divisor affects mouse sensitivity...
if(tmpy>0){tmpy*=1.4;} // boost if positive
mousey-=tmpy;
if(mousey>65) mousey=65;
if(mousey<-2) mousey=-2;
}
}
else // no finger contact
{
}
// smoothing mouse movement
realmousex-=((realmousex-mousex)/5); // this divisor affects the speed at which the smoothed mouse catches up with the real mouse coords
realmousey-=((realmousey-mousey)/5); // (so the name may be confusing, but anyway :-) )
// a mouse pointer may then be drawn at position realmousex,realmousey..
}Additionally, each of these pads can be used as a normal button, by asking:
if(approx2 < (touch_baselevel2-150)) {do_stuff();} // button 2 pressed? (not just accidentally touched)A value of 150 works well here, but users may have to adjust this value if their button fires too easily or too hard aka it fires before even touching, or too much finger pressure is required. Lowering the value makes the button more sensitive.
With that I hope I have said everything that is of technical relevance. For more details find my XPLORA project on github, where after some code cleanup all the source-code will be released in the near future.
Why am I doing this?
Well, as much as I love Meshtastic(tm), I always felt there is a need for user input on those many LoRa MCUs that can run a Meshtastic(tm) node. They all require pairing over bluetooth with a smartphone to enter and send a message, although they do display received messages, albeit rudimentarily.
Exclusions are the few devices that support MUI, like the T-Deck or Crowpanel. However even MUI doesn't allow you to do all settings, it just adds a text input feature and a decent Touchscreen GUI.
I am currently working on my LoRa communication project "XPLORA", which at this moment contains a simple LoRa public chat, as well as a "yell" option that instructs receiving XPLORA stations to re-broadcast a message once, resulting in public broadcasts with maximum / unlimited reach.
Other than the implemented features, I am also developing a P2P data packet routing solution for LoRa, and I realize there is no need for a hops-limit (like Meshtastic(tm) has its 7 hops limit), if the routing is smart, such limits make no sense.
Concluding
However I don't think that my project will be the next big thing, successor of Meshtastic(tm) or anything like that, but I do hope that some people get the memo, and feel inspired to implement some of my ideas and solutions, namely eg. a way to enter text even on the simplest Meshtastic(tm )LoRa MCU (with a display). And of course, this is mostly just for fun. Yes, we want to have a LoRa radio ready for when hell breaks lose. But other than the prepper-ambition, this is also just really great fun.







Comments