Added backlight, reset, triangle and corner commands
[hackover2013-badge-firmware.git] / drivers / lcd / tft / touchscreen.c
1 /**************************************************************************/
2 /*!
3 @file touchscreen.c
4 @author K. Townsend (microBuilder.eu)
5
6 Parts copyright (c) 2001, Carlos E. Vidales. All rights reserved.
7
8 @section LICENSE
9
10 Software License Agreement (BSD License)
11
12 Copyright (c) 2010, microBuilder SARL
13 All rights reserved.
14
15 Redistribution and use in source and binary forms, with or without
16 modification, are permitted provided that the following conditions are met:
17 1. Redistributions of source code must retain the above copyright
18 notice, this list of conditions and the following disclaimer.
19 2. Redistributions in binary form must reproduce the above copyright
20 notice, this list of conditions and the following disclaimer in the
21 documentation and/or other materials provided with the distribution.
22 3. Neither the name of the copyright holders nor the
23 names of its contributors may be used to endorse or promote products
24 derived from this software without specific prior written permission.
25
26 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ''AS IS'' AND ANY
27 EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
28 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
29 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY
30 DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
31 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
32 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
33 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
34 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
35 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
36 */
37 /**************************************************************************/
38 #include "touchscreen.h"
39
40 #include "core/adc/adc.h"
41 #include "core/gpio/gpio.h"
42 #include "core/systick/systick.h"
43 #include "drivers/eeprom/eeprom.h"
44 #include "drivers/lcd/tft/lcd.h"
45 #include "drivers/lcd/tft/drawing.h"
46 #include "drivers/lcd/tft/fonts/dejavusans9.h"
47
48 #define TS_LINE1 "Touch the center of"
49 #define TS_LINE2 "the red circle using"
50 #define TS_LINE3 "a pen or stylus"
51
52 static bool _tsInitialised = FALSE;
53 static uint8_t _tsThreshhold = CFG_TFTLCD_TS_DEFAULTTHRESHOLD;
54 tsPoint_t _tsLCDPoints[3];
55 tsPoint_t _tsTSPoints[3];
56 tsMatrix_t _tsMatrix;
57
58 /**************************************************************************/
59 /* */
60 /* ----------------------- Private Methods ------------------------------ */
61 /* */
62 /**************************************************************************/
63
64 /**************************************************************************/
65 /*!
66 @brief Reads the current Z/pressure level using the ADC
67 */
68 /**************************************************************************/
69 void tsReadZ(uint32_t* z1, uint32_t* z2)
70 {
71 if (!_tsInitialised) tsInit();
72
73 // XP = ADC
74 // XM = GPIO Output Low
75 // YP = GPIO Output High
76 // YM = GPIO Input
77
78 TS_XM_FUNC_GPIO;
79 TS_YP_FUNC_GPIO;
80 TS_YM_FUNC_GPIO;
81
82 gpioSetDir (TS_XM_PORT, TS_XM_PIN, 1);
83 gpioSetDir (TS_YP_PORT, TS_YP_PIN, 1);
84 gpioSetDir (TS_YM_PORT, TS_YM_PIN, 0);
85
86 gpioSetValue(TS_XM_PORT, TS_XM_PIN, 0); // GND
87 gpioSetValue(TS_YP_PORT, TS_YP_PIN, 1); // 3.3V
88
89 TS_XP_FUNC_ADC;
90 *z1 = adcRead(TS_XP_ADC_CHANNEL);
91
92 // XP = GPIO Input
93 // XM = GPIO Output Low
94 // YP = GPIO Output High
95 // YM = ADC
96
97 TS_XP_FUNC_GPIO;
98 gpioSetDir (TS_YM_PORT, TS_YM_PIN, 0);
99
100 TS_YM_FUNC_ADC;
101 *z2 = adcRead(TS_YM_ADC_CHANNEL);
102 }
103
104 /**************************************************************************/
105 /*!
106 @brief Reads the current X position using the ADC
107 */
108 /**************************************************************************/
109 uint32_t tsReadX(void)
110 {
111 if (!_tsInitialised) tsInit();
112
113 // XP = GPIO Output High
114 // XM = GPIO Output Low
115 // YP = ADC
116 // YM = GPIO Input
117
118 TS_XP_FUNC_GPIO;
119 TS_XM_FUNC_GPIO;
120 TS_YM_FUNC_GPIO;
121
122 gpioSetDir (TS_XP_PORT, TS_XP_PIN, 1);
123 gpioSetDir (TS_XM_PORT, TS_XM_PIN, 1);
124 gpioSetDir (TS_YM_PORT, TS_YM_PIN, 0);
125
126 gpioSetValue(TS_XP_PORT, TS_XP_PIN, 1); // 3.3V
127 gpioSetValue(TS_XM_PORT, TS_XM_PIN, 0); // GND
128
129 TS_YP_FUNC_ADC;
130
131 // Return the ADC results
132 return adcRead(TS_YP_ADC_CHANNEL);
133 }
134
135 /**************************************************************************/
136 /*!
137 @brief Reads the current Y position using the ADC
138 */
139 /**************************************************************************/
140 uint32_t tsReadY(void)
141 {
142 if (!_tsInitialised) tsInit();
143
144 // YP = GPIO Output High
145 // YM = GPIO Output Low
146 // XP = GPIO Input
147 // XM = ADC
148
149 TS_YP_FUNC_GPIO;
150 TS_YM_FUNC_GPIO;
151 TS_XP_FUNC_GPIO;
152
153 gpioSetDir (TS_YP_PORT, TS_YP_PIN, 1);
154 gpioSetDir (TS_YM_PORT, TS_YM_PIN, 1);
155 gpioSetDir (TS_XP_PORT, TS_XP_PIN, 0);
156
157 gpioSetValue(TS_YP_PORT, TS_YP_PIN, 1); // 3.3V
158 gpioSetValue(TS_YM_PORT, TS_YM_PIN, 0); // GND
159
160 TS_XM_FUNC_ADC;
161
162 // Return the ADC results
163 return adcRead(TS_XM_ADC_CHANNEL);
164 }
165
166 /**************************************************************************/
167 /*!
168 @brief Centers a line of text horizontally
169 */
170 /**************************************************************************/
171 void tsCalibCenterText(char* text, uint16_t y, uint16_t color)
172 {
173 drawString((lcdGetWidth() - drawGetStringWidth(&dejaVuSans9ptFontInfo, text)) / 2, y, color, &dejaVuSans9ptFontInfo, text);
174 }
175
176 /**************************************************************************/
177 /*!
178 @brief Renders the calibration screen with an appropriately
179 placed test point and waits for a touch event
180 */
181 /**************************************************************************/
182 tsTouchData_t tsRenderCalibrationScreen(uint16_t x, uint16_t y, uint16_t radius)
183 {
184 drawFill(COLOR_WHITE);
185 tsCalibCenterText(TS_LINE1, 50, COLOR_GRAY_50);
186 tsCalibCenterText(TS_LINE2, 65, COLOR_GRAY_50);
187 tsCalibCenterText(TS_LINE3, 80, COLOR_GRAY_50);
188 drawCircle(x, y, radius, COLOR_RED);
189 drawCircle(x, y, radius + 2, COLOR_GRAY_128);
190
191 // Wait for a valid touch events
192 tsTouchData_t data;
193 tsTouchError_t error;
194 bool valid = false;
195 while (!valid)
196 {
197 error = tsRead(&data);
198 if (!error && data.valid)
199 {
200 valid = true;
201 }
202 }
203
204 return data;
205 }
206
207 /**************************************************************************/
208 /*!
209 @brief Calculates the difference between the touch screen and the
210 actual screen co-ordinates, taking into account misalignment
211 and any physical offset of the touch screen.
212
213 @note This is based on the public domain touch screen calibration code
214 written by Carlos E. Vidales (copyright (c) 2001).
215
216 For more inforormation, see the following app notes:
217
218 - AN2173 - Touch Screen Control and Calibration
219 Svyatoslav Paliy, Cypress Microsystems
220 - Calibration in touch-screen systems
221 Wendy Fang and Tony Chang,
222 Analog Applications Journal, 3Q 2007 (Texas Instruments)
223 */
224 /**************************************************************************/
225 int setCalibrationMatrix( tsPoint_t * displayPtr, tsPoint_t * screenPtr, tsMatrix_t * matrixPtr)
226 {
227 int retValue = 0;
228
229 matrixPtr->Divider = ((screenPtr[0].x - screenPtr[2].x) * (screenPtr[1].y - screenPtr[2].y)) -
230 ((screenPtr[1].x - screenPtr[2].x) * (screenPtr[0].y - screenPtr[2].y)) ;
231
232 if( matrixPtr->Divider == 0 )
233 {
234 retValue = -1 ;
235 }
236 else
237 {
238 matrixPtr->An = ((displayPtr[0].x - displayPtr[2].x) * (screenPtr[1].y - screenPtr[2].y)) -
239 ((displayPtr[1].x - displayPtr[2].x) * (screenPtr[0].y - screenPtr[2].y)) ;
240
241 matrixPtr->Bn = ((screenPtr[0].x - screenPtr[2].x) * (displayPtr[1].x - displayPtr[2].x)) -
242 ((displayPtr[0].x - displayPtr[2].x) * (screenPtr[1].x - screenPtr[2].x)) ;
243
244 matrixPtr->Cn = (screenPtr[2].x * displayPtr[1].x - screenPtr[1].x * displayPtr[2].x) * screenPtr[0].y +
245 (screenPtr[0].x * displayPtr[2].x - screenPtr[2].x * displayPtr[0].x) * screenPtr[1].y +
246 (screenPtr[1].x * displayPtr[0].x - screenPtr[0].x * displayPtr[1].x) * screenPtr[2].y ;
247
248 matrixPtr->Dn = ((displayPtr[0].y - displayPtr[2].y) * (screenPtr[1].y - screenPtr[2].y)) -
249 ((displayPtr[1].y - displayPtr[2].y) * (screenPtr[0].y - screenPtr[2].y)) ;
250
251 matrixPtr->En = ((screenPtr[0].x - screenPtr[2].x) * (displayPtr[1].y - displayPtr[2].y)) -
252 ((displayPtr[0].y - displayPtr[2].y) * (screenPtr[1].x - screenPtr[2].x)) ;
253
254 matrixPtr->Fn = (screenPtr[2].x * displayPtr[1].y - screenPtr[1].x * displayPtr[2].y) * screenPtr[0].y +
255 (screenPtr[0].x * displayPtr[2].y - screenPtr[2].x * displayPtr[0].y) * screenPtr[1].y +
256 (screenPtr[1].x * displayPtr[0].y - screenPtr[0].x * displayPtr[1].y) * screenPtr[2].y ;
257
258 // Persist data to EEPROM
259 eepromWriteS32(CFG_EEPROM_TOUCHSCREEN_CAL_AN, matrixPtr->An);
260 eepromWriteS32(CFG_EEPROM_TOUCHSCREEN_CAL_BN, matrixPtr->Bn);
261 eepromWriteS32(CFG_EEPROM_TOUCHSCREEN_CAL_CN, matrixPtr->Cn);
262 eepromWriteS32(CFG_EEPROM_TOUCHSCREEN_CAL_DN, matrixPtr->Dn);
263 eepromWriteS32(CFG_EEPROM_TOUCHSCREEN_CAL_EN, matrixPtr->En);
264 eepromWriteS32(CFG_EEPROM_TOUCHSCREEN_CAL_FN, matrixPtr->Fn);
265 eepromWriteS32(CFG_EEPROM_TOUCHSCREEN_CAL_FN, matrixPtr->Fn);
266 eepromWriteS32(CFG_EEPROM_TOUCHSCREEN_CAL_DIVIDER, matrixPtr->Divider);
267 eepromWriteU8(CFG_EEPROM_TOUCHSCREEN_CALIBRATED, 1);
268 }
269
270 return( retValue ) ;
271 }
272
273 /**************************************************************************/
274 /*!
275 @brief Converts the supplied touch screen location (screenPtr) to
276 a pixel location on the display (displayPtr) using the
277 supplied matrix. The screen orientation is also taken into
278 account when converting the touch screen co-ordinate to
279 a pixel location on the LCD.
280
281 @note This is based on the public domain touch screen calibration code
282 written by Carlos E. Vidales (copyright (c) 2001).
283 */
284 /**************************************************************************/
285 int getDisplayPoint( tsPoint_t * displayPtr, tsPoint_t * screenPtr, tsMatrix_t * matrixPtr )
286 {
287 int retValue = 0 ;
288
289 if( matrixPtr->Divider != 0 )
290 {
291 displayPtr->x = ( (matrixPtr->An * screenPtr->x) +
292 (matrixPtr->Bn * screenPtr->y) +
293 matrixPtr->Cn
294 ) / matrixPtr->Divider ;
295
296 displayPtr->y = ( (matrixPtr->Dn * screenPtr->x) +
297 (matrixPtr->En * screenPtr->y) +
298 matrixPtr->Fn
299 ) / matrixPtr->Divider ;
300 }
301 else
302 {
303 retValue = -1 ;
304 }
305
306 // Adjust value if the screen is in landscape mode
307 lcdOrientation_t orientation;
308 orientation = lcdGetOrientation();
309 if (orientation == LCD_ORIENTATION_LANDSCAPE)
310 {
311 uint32_t oldx, oldy;
312 oldx = displayPtr->x;
313 oldy = displayPtr->y;
314 displayPtr->x = oldy;
315 displayPtr->y = lcdGetHeight() - oldx;
316 }
317
318 return( retValue ) ;
319 }
320
321 /**************************************************************************/
322 /* */
323 /* ----------------------- Public Methods ------------------------------- */
324 /* */
325 /**************************************************************************/
326
327 /**************************************************************************/
328 /*!
329 @brief Initialises the appropriate GPIO pins and ADC for the
330 touchscreen
331 */
332 /**************************************************************************/
333 void tsInit(void)
334 {
335 // Make sure that ADC is initialised
336 adcInit();
337
338 // Set initialisation flag
339 _tsInitialised = TRUE;
340 _tsThreshhold = tsGetThreshhold();
341
342 // Load values from EEPROM if touch screen has already been calibrated
343 if (eepromReadU8(CFG_EEPROM_TOUCHSCREEN_CALIBRATED) == 1)
344 {
345 // Load calibration data
346 _tsMatrix.An = eepromReadS32(CFG_EEPROM_TOUCHSCREEN_CAL_AN);
347 _tsMatrix.Bn = eepromReadS32(CFG_EEPROM_TOUCHSCREEN_CAL_BN);
348 _tsMatrix.Cn = eepromReadS32(CFG_EEPROM_TOUCHSCREEN_CAL_CN);
349 _tsMatrix.Dn = eepromReadS32(CFG_EEPROM_TOUCHSCREEN_CAL_DN);
350 _tsMatrix.En = eepromReadS32(CFG_EEPROM_TOUCHSCREEN_CAL_EN);
351 _tsMatrix.Fn = eepromReadS32(CFG_EEPROM_TOUCHSCREEN_CAL_FN);
352 _tsMatrix.Divider = eepromReadS32(CFG_EEPROM_TOUCHSCREEN_CAL_DIVIDER);
353 }
354 }
355
356 /**************************************************************************/
357 /*!
358 @brief Reads the current X, Y and Z co-ordinates of the touch screen
359 */
360 /**************************************************************************/
361 tsTouchError_t tsRead(tsTouchData_t* data)
362 {
363 uint32_t x1, x2, y1, y2, z1, z2;
364
365 // Assign pressure levels regardless of touch state
366 tsReadZ(&z1, &z2);
367 data->z1 = z1;
368 data->z2 = z2;
369 data->xraw = 0;
370 data->yraw = 0;
371 data->xlcd = 0;
372 data->ylcd = 0;
373
374 // Abort if the screen is not being touched (0 levels reported)
375 if (z1 < _tsThreshhold)
376 {
377 data->valid = false;
378 return TS_ERROR_NONE;
379 }
380
381 // Get two X/Y readings and compare results
382 x1 = tsReadX();
383 x2 = tsReadX();
384 y1 = tsReadY();
385 y2 = tsReadY();
386
387 // Throw an error if both readings aren't identical
388 if (x1 != x2 || y1 != y2)
389 {
390 data->valid = false;
391 data->xraw = x1;
392 data->yraw = y1;
393 return TS_ERROR_XYMISMATCH;
394 }
395
396 // X/Y seems to be valid and reading has been confirmed twice
397 data->xraw = x1;
398 data->yraw = y1;
399
400 // Convert x/y values to pixel location with matrix multiply
401 tsPoint_t location, touch;
402 touch.x = x1;
403 touch.y = y1;
404 getDisplayPoint( &location, &touch, &_tsMatrix) ;
405 data->xlcd = location.x;
406 data->ylcd = location.y;
407 data->valid = true;
408
409 return TS_ERROR_NONE;
410 }
411
412 /**************************************************************************/
413 /*!
414 @brief Starts the screen calibration process. Each corner will be
415 tested, meaning that each boundary (top, left, right and
416 bottom) will be tested twice and the readings averaged.
417 */
418 /**************************************************************************/
419 void tsCalibrate(void)
420 {
421 tsTouchData_t data;
422
423 /* --------------- Welcome Screen --------------- */
424 data = tsRenderCalibrationScreen(lcdGetWidth() / 2, lcdGetHeight() / 2, 5);
425 systickDelay(250);
426
427 /* ----------------- First Dot ------------------ */
428 // 10% over and 10% down
429 data = tsRenderCalibrationScreen(lcdGetWidth() / 10, lcdGetHeight() / 10, 5);
430 _tsLCDPoints[0].x = lcdGetWidth() / 10;
431 _tsLCDPoints[0].y = lcdGetHeight() / 10;
432 _tsTSPoints[0].x = data.xraw;
433 _tsTSPoints[0].y = data.yraw;
434 printf("Point 1 - LCD X:%04d Y:%04d TS X:%04d Y:%04d \r\n",
435 (int)_tsLCDPoints[0].x, (int)_tsLCDPoints[0].y, (int)_tsTSPoints[0].x, (int)_tsTSPoints[0].y);
436 systickDelay(250);
437
438 /* ---------------- Second Dot ------------------ */
439 // 50% over and 90% down
440 data = tsRenderCalibrationScreen(lcdGetWidth() / 2, lcdGetHeight() - lcdGetHeight() / 10, 5);
441 _tsLCDPoints[1].x = lcdGetWidth() / 2;
442 _tsLCDPoints[1].y = lcdGetHeight() - lcdGetHeight() / 10;
443 _tsTSPoints[1].x = data.xraw;
444 _tsTSPoints[1].y = data.yraw;
445 printf("Point 2 - LCD X:%04d Y:%04d TS X:%04d Y:%04d \r\n",
446 (int)_tsLCDPoints[1].x, (int)_tsLCDPoints[1].y, (int)_tsTSPoints[1].x, (int)_tsTSPoints[1].y);
447 systickDelay(250);
448
449 /* ---------------- Third Dot ------------------- */
450 // 90% over and 50% down
451 data = tsRenderCalibrationScreen(lcdGetWidth() - lcdGetWidth() / 10, lcdGetHeight() / 2, 5);
452 _tsLCDPoints[2].x = lcdGetWidth() - lcdGetWidth() / 10;
453 _tsLCDPoints[2].y = lcdGetHeight() / 2;
454 _tsTSPoints[2].x = data.xraw;
455 _tsTSPoints[2].y = data.yraw;
456 printf("Point 3 - LCD X:%04d Y:%04d TS X:%04d Y:%04d \r\n",
457 (int)_tsLCDPoints[2].x, (int)_tsLCDPoints[2].y, (int)_tsTSPoints[2].x, (int)_tsTSPoints[2].y);
458 systickDelay(250);
459
460 // Do matrix calculations for calibration and store to EEPROM
461 setCalibrationMatrix(&_tsLCDPoints[0], &_tsTSPoints[0], &_tsMatrix);
462 }
463
464 /**************************************************************************/
465 /*!
466 @brief Causes a blocking delay until a valid touch event occurs
467
468 @note Thanks to 'rossum' and limor for this nifty little tidbit on
469 debouncing the signals via pressure sensitivity (using Z)
470
471 @section Example
472
473 @code
474 #include "drivers/lcd/tft/touchscreen.h"
475 ...
476 tsTouchData_t data;
477 tsTouchError_t error;
478
479 while (1)
480 {
481 // Cause a blocking delay until a touch event occurs or 5s passes
482 error = tsWaitForEvent(&data, 5000);
483
484 if (error)
485 {
486 switch(error)
487 {
488 case TS_ERROR_TIMEOUT:
489 printf("Timeout occurred %s", CFG_PRINTF_NEWLINE);
490 break;
491 default:
492 break;
493 }
494 }
495 else
496 {
497 // A valid touch event occurred ... display data
498 printf("Touch Event: X = %04u, Y = %04u %s",
499 data.xlcd,
500 data.ylcd,
501 CFG_PRINTF_NEWLINE);
502 }
503 }
504
505 @endcode
506 */
507 /**************************************************************************/
508 tsTouchError_t tsWaitForEvent(tsTouchData_t* data, uint32_t timeoutMS)
509 {
510 if (!_tsInitialised) tsInit();
511
512 tsRead(data);
513
514 // Return the results right away if reading is valid
515 if (data->valid)
516 {
517 return TS_ERROR_NONE;
518 }
519
520 // Handle timeout if delay > 0 milliseconds
521 if (timeoutMS)
522 {
523 uint32_t startTick = systickGetTicks();
524 // Systick rollover may occur while waiting for timeout
525 if (startTick > 0xFFFFFFFF - timeoutMS)
526 {
527 while (data->valid == false)
528 {
529 // Throw alert if timeout delay has been passed
530 if ((systickGetTicks() < startTick) && (systickGetTicks() >= (timeoutMS - (0xFFFFFFFF - startTick))))
531 {
532 return TS_ERROR_TIMEOUT;
533 }
534 tsRead(data);
535 }
536 }
537 // No systick rollover will occur ... calculate timeout the simple way
538 else
539 {
540 // Wait in infinite loop
541 while (data->valid == false)
542 {
543 // Throw timeout if delay has been passed
544 if ((systickGetTicks() - startTick) > timeoutMS)
545 {
546 return TS_ERROR_TIMEOUT;
547 }
548 tsRead(data);
549 }
550 }
551 }
552 // No timeout requested ... wait forever
553 else
554 {
555 while (data->valid == false)
556 {
557 tsRead(data);
558 }
559 }
560
561 // Indicate correct reading
562 return TS_ERROR_NONE;
563 }
564
565 /**************************************************************************/
566 /*!
567 @brief Updates the touch screen threshhold level and saves it
568 to EEPROM
569 */
570 /**************************************************************************/
571 int tsSetThreshhold(uint8_t value)
572 {
573 if ((value < 0) || (value > 254))
574 {
575 return -1;
576 }
577
578 // Update threshhold value
579 _tsThreshhold = value;
580
581 // Persist to EEPROM
582 eepromWriteU8(CFG_EEPROM_TOUCHSCREEN_THRESHHOLD, value);
583
584 return 0;
585 }
586
587 /**************************************************************************/
588 /*!
589 @brief Gets the current touch screen threshhold level from EEPROM
590 (if present) or returns the default value from projectconfig.h
591 */
592 /**************************************************************************/
593 uint8_t tsGetThreshhold(void)
594 {
595 // Check if custom threshold has been set in eeprom
596 uint8_t thold = eepromReadU8(CFG_EEPROM_TOUCHSCREEN_THRESHHOLD);
597 if (thold != 0xFF)
598 {
599 // Use value from EEPROM
600 _tsThreshhold = thold;
601 }
602 else
603 {
604 // Use the default value from projectconfig.h
605 _tsThreshhold = CFG_TFTLCD_TS_DEFAULTTHRESHOLD;
606 }
607
608 return _tsThreshhold;
609 }
This page took 0.087887 seconds and 5 git commands to generate.