Created
August 26, 2026 12:44
-
-
Save jxsl13/78e5fb191453d5634222b2d1a7a39773 to your computer and use it in GitHub Desktop.
g-hub.lua
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| -- ===================================================================== | |
| -- Logitech G HUB - Human Circle Mouse Jiggler | |
| -- ===================================================================== | |
| -- | |
| -- QUICK START | |
| -- | |
| -- Normally you only need to configure: | |
| -- | |
| -- TOGGLE_BUTTON | |
| -- PERSON_SEED | |
| -- | |
| -- For the LED indicator, create the two G HUB macros described below. | |
| -- | |
| -- | |
| -- ===================================================================== | |
| -- PRO 2 LIGHTSPEED - PROGRAMMABLE BUTTONS | |
| -- ===================================================================== | |
| -- | |
| -- Logitech specifies 8 programmable physical buttons on the | |
| -- PRO 2 LIGHTSPEED: | |
| -- | |
| -- Physical button Device designation | |
| -- --------------------------------------------------------------- | |
| -- Left mouse button Button 1 | |
| -- Right mouse button Button 2 | |
| -- Wheel / middle click Button 3 | |
| -- Side Back G4 | |
| -- Side Forward G5 | |
| -- Opposite-side Back G6 | |
| -- Opposite-side Forward G7 | |
| -- Bottom DPI cycle button Button 8 | |
| -- | |
| -- The magnetic side buttons allow right-handed, left-handed, | |
| -- no-side-button, and full-side-button configurations. | |
| -- | |
| -- | |
| -- ===================================================================== | |
| -- VALID TOGGLE_BUTTON VALUES FOR THIS SCRIPT | |
| -- ===================================================================== | |
| -- | |
| -- IMPORTANT: | |
| -- | |
| -- Logitech's physical button numbering and the Lua mouse-button | |
| -- numbering are NOT identical for the right and middle buttons. | |
| -- | |
| -- Use these LUA values for TOGGLE_BUTTON: | |
| -- | |
| -- TOGGLE_BUTTON = 1 | |
| -- Left mouse button | |
| -- Physical PRO 2 button: Button 1 | |
| -- Lua: Left Mouse Button | |
| -- | |
| -- Supported by this script. | |
| -- G HUB does not report the primary button by default, therefore | |
| -- the script automatically enables primary-button events when | |
| -- TOGGLE_BUTTON is set to 1. | |
| -- | |
| -- | |
| -- TOGGLE_BUTTON = 2 | |
| -- Wheel / middle click | |
| -- Physical PRO 2 button: Button 3 | |
| -- Lua: Middle Mouse Button | |
| -- | |
| -- Supported by this script. | |
| -- | |
| -- | |
| -- TOGGLE_BUTTON = 3 | |
| -- Right mouse button | |
| -- Physical PRO 2 button: Button 2 | |
| -- Lua: Right Mouse Button | |
| -- | |
| -- Supported by this script. | |
| -- | |
| -- | |
| -- TOGGLE_BUTTON = 4 | |
| -- XButton1 / Back | |
| -- Normally corresponds to G4 in the standard right-handed setup. | |
| -- | |
| -- Supported by this script. | |
| -- RECOMMENDED. | |
| -- | |
| -- | |
| -- TOGGLE_BUTTON = 5 | |
| -- XButton2 / Forward | |
| -- Normally corresponds to G5 in the standard right-handed setup. | |
| -- | |
| -- Supported by this script. | |
| -- RECOMMENDED. | |
| -- | |
| -- | |
| -- --------------------------------------------------------------------- | |
| -- WHY G6, G7 AND BUTTON 8 ARE NOT VALID TOGGLE_BUTTON VALUES HERE | |
| -- --------------------------------------------------------------------- | |
| -- | |
| -- G6, G7 and the bottom Button 8 are programmable in G HUB. | |
| -- | |
| -- G HUB can generate mouse-button events for additional physical | |
| -- buttons on supported mice, but Logitech's IsMouseButtonPressed() | |
| -- polling API only defines the following buttons: | |
| -- | |
| -- 1 = Left | |
| -- 2 = Middle | |
| -- 3 = Right | |
| -- 4 = X1 | |
| -- 5 = X2 | |
| -- | |
| -- This script must poll the toggle button WHILE: | |
| -- | |
| -- - drawing a circle | |
| -- - sleeping for up to 180 seconds | |
| -- - waiting inside the active Lua event handler | |
| -- | |
| -- Therefore this implementation intentionally restricts TOGGLE_BUTTON | |
| -- to Lua mouse buttons 1 through 5. | |
| -- | |
| -- Using G6, G7 or Button 8 would make immediate interruption of the | |
| -- active loop unreliable because IsMouseButtonPressed() cannot poll | |
| -- those buttons. | |
| -- | |
| -- For the simplest and least intrusive configuration use: | |
| -- | |
| -- 4 = G4 / Back | |
| -- 5 = G5 / Forward | |
| -- | |
| -- ===================================================================== | |
| -- ===================================================================== | |
| -- USER CONFIGURATION | |
| -- ===================================================================== | |
| local TOGGLE_BUTTON = 5 | |
| -- Persistent virtual person. | |
| -- | |
| -- Change this number to create a different person. | |
| -- Keep it unchanged to always use the same person. | |
| -- | |
| -- You may enter ANY integer. | |
| -- | |
| -- Native internal PRNG state range: | |
| -- | |
| -- 1 .. 2147483646 | |
| -- | |
| -- You do NOT need to manually stay inside this range. | |
| -- The script normalizes the configured value automatically. | |
| -- | |
| -- Examples: | |
| -- | |
| -- 731942 | |
| -- 42 | |
| -- 123456789 | |
| -- 0 | |
| -- -12345 | |
| -- 9999999999 | |
| -- | |
| -- For consistent behavior, use a whole number and keep it unchanged. | |
| -- | |
| local PERSON_SEED = 731942 | |
| -- ===================================================================== | |
| -- DPI LED INDICATOR - ONE-TIME G HUB SETUP | |
| -- ===================================================================== | |
| -- | |
| -- G HUB Lua cannot directly set the current DPI table index. | |
| -- | |
| -- The script therefore uses two ordinary G HUB macros. | |
| -- | |
| -- Create TWO "No Repeat" macros: | |
| -- | |
| -- Macro name: DPI_UP | |
| -- Action: System -> Mouse -> DPI Up | |
| -- | |
| -- Macro name: DPI_DOWN | |
| -- Action: System -> Mouse -> DPI Down | |
| -- | |
| -- | |
| -- Keep at least three DPI stages configured. | |
| -- | |
| -- The script uses: | |
| -- | |
| -- DPI/Profile 1 -> one LED | |
| -- DPI/Profile 3 -> three LEDs | |
| -- | |
| -- | |
| -- ENABLE INDICATION: | |
| -- | |
| -- one LED | |
| -- -> | |
| -- three LEDs | |
| -- -> | |
| -- one LED | |
| -- -> | |
| -- three LEDs | |
| -- -> | |
| -- one LED | |
| -- -> | |
| -- three LEDs | |
| -- -> | |
| -- one LED | |
| -- | |
| -- | |
| -- DISABLE INDICATION: | |
| -- | |
| -- one LED | |
| -- | |
| -- | |
| -- DPI/Profile 1 should therefore contain your normal working DPI. | |
| -- | |
| -- Example: | |
| -- | |
| -- DPI 1 = 800 DPI <- normal working DPI | |
| -- DPI 2 = 850 DPI | |
| -- DPI 3 = 900 DPI | |
| -- | |
| -- The temporary higher DPI stages are selected only while displaying | |
| -- the indicator. Automated cursor movement starts only after DPI 1 has | |
| -- been restored. | |
| -- | |
| -- ===================================================================== | |
| local DPI_UP_MACRO = "DPI_UP" | |
| local DPI_DOWN_MACRO = "DPI_DOWN" | |
| local DPI_MAX_PROFILE_COUNT = 5 | |
| local DPI_STEPS_TO_THREE_LEDS = 2 | |
| local DPI_COMMAND_DELAY_MS = 65 | |
| local LED_ON_PULSE_COUNT = 3 | |
| local LED_ON_HOLD_MIN_MS = 140 | |
| local LED_ON_HOLD_MAX_MS = 210 | |
| local LED_ON_GAP_MIN_MS = 100 | |
| local LED_ON_GAP_MAX_MS = 160 | |
| local LED_OFF_HOLD_MIN_MS = 180 | |
| local LED_OFF_HOLD_MAX_MS = 260 | |
| -- ===================================================================== | |
| -- PAUSE BETWEEN CIRCLES | |
| -- ===================================================================== | |
| -- | |
| -- A completely new pause is calculated after EVERY circle. | |
| -- | |
| -- Center-weighted randomization is used: | |
| -- | |
| -- minimum = 10 seconds | |
| -- maximum = 180 seconds | |
| -- | |
| -- Middle values are more common than extreme values. | |
| -- | |
| -- ===================================================================== | |
| local PAUSE_MIN_MS = 10000 | |
| local PAUSE_MAX_MS = 180000 | |
| -- ===================================================================== | |
| -- CIRCLE RADIUS | |
| -- ===================================================================== | |
| local RADIUS_MIN_PX = 21.0 | |
| local RADIUS_MAX_PX = 27.0 | |
| -- ===================================================================== | |
| -- HUMAN MOVEMENT SPEED | |
| -- ===================================================================== | |
| -- | |
| -- Speed is calculated from circle geometry: | |
| -- | |
| -- circumference = 2 * pi * radius | |
| -- | |
| -- speed = circumference * frequency | |
| -- | |
| -- speed = 2 * pi * radius * frequency | |
| -- | |
| -- | |
| -- Frequency range: | |
| -- | |
| -- 0.65 .. 1.10 Hz | |
| -- | |
| -- With radius: | |
| -- | |
| -- 21 .. 27 px | |
| -- | |
| -- the nominal average tangential range is approximately: | |
| -- | |
| -- 85.8 .. 186.6 px/s | |
| -- | |
| -- Additional correlated variation is applied during the circle. | |
| -- | |
| -- ===================================================================== | |
| local MOVEMENT_FREQUENCY_MIN_HZ = 0.65 | |
| local MOVEMENT_FREQUENCY_MAX_HZ = 1.10 | |
| local FREQUENCY_CIRCLE_VARIATION_MIN_HZ = -0.085 | |
| local FREQUENCY_CIRCLE_VARIATION_MAX_HZ = 0.085 | |
| local HUMAN_SPEED_MIN_PX_PER_SEC = 75.0 | |
| local HUMAN_SPEED_MAX_PX_PER_SEC = 210.0 | |
| -- ===================================================================== | |
| -- INTERNAL SAMPLE TIMING | |
| -- ===================================================================== | |
| local SAMPLE_MIN_MS = 7 | |
| local SAMPLE_MAX_MS = 11 | |
| -- ===================================================================== | |
| -- ELLIPTICITY | |
| -- ===================================================================== | |
| local ELLIPTICITY_VARIATION_MIN = -0.018 | |
| local ELLIPTICITY_VARIATION_MAX = 0.018 | |
| local ELLIPTICITY_DRIFT_SD_MIN = 0.004 | |
| local ELLIPTICITY_DRIFT_SD_MAX = 0.012 | |
| local ELLIPTICITY_TAU_MIN = 4.0 | |
| local ELLIPTICITY_TAU_MAX = 7.0 | |
| -- ===================================================================== | |
| -- ELLIPSE ORIENTATION | |
| -- ===================================================================== | |
| local ROTATION_VARIATION_MIN = -0.08 | |
| local ROTATION_VARIATION_MAX = 0.08 | |
| local ROTATION_DRIFT_SD_MIN = 0.006 | |
| local ROTATION_DRIFT_SD_MAX = 0.020 | |
| local ROTATION_TAU_MIN = 5.0 | |
| local ROTATION_TAU_MAX = 9.0 | |
| -- ===================================================================== | |
| -- SLOW RADIUS VARIATION | |
| -- ===================================================================== | |
| local RADIUS_DRIFT_SD_MIN = 0.20 | |
| local RADIUS_DRIFT_SD_MAX = 0.55 | |
| local RADIUS_DRIFT_TAU_MIN = 2.0 | |
| local RADIUS_DRIFT_TAU_MAX = 4.0 | |
| -- ===================================================================== | |
| -- SPEED VARIATION DURING A CIRCLE | |
| -- ===================================================================== | |
| local TEMPO_SD_MIN = 0.018 | |
| local TEMPO_SD_MAX = 0.045 | |
| local TEMPO_TAU_MIN = 0.55 | |
| local TEMPO_TAU_MAX = 1.10 | |
| local INTRA_CIRCLE_SPEED_MIN_FACTOR = 0.88 | |
| local INTRA_CIRCLE_SPEED_MAX_FACTOR = 1.12 | |
| -- ===================================================================== | |
| -- MOTOR NOISE | |
| -- ===================================================================== | |
| local MOTOR_NOISE_SD_MIN = 0.16 | |
| local MOTOR_NOISE_SD_MAX = 0.36 | |
| local MOTOR_NOISE_TAU_MIN = 0.055 | |
| local MOTOR_NOISE_TAU_MAX = 0.120 | |
| -- ===================================================================== | |
| -- PHYSIOLOGICAL TREMOR | |
| -- ===================================================================== | |
| local TREMOR_FREQ_MIN = 8.0 | |
| local TREMOR_FREQ_MAX = 12.0 | |
| local TREMOR_FREQ_CIRCLE_VARIATION = 0.25 | |
| local TREMOR_AMP_X_MIN = 0.18 | |
| local TREMOR_AMP_X_MAX = 0.38 | |
| local TREMOR_AMP_Y_MIN = 0.15 | |
| local TREMOR_AMP_Y_MAX = 0.34 | |
| local TREMOR_FREQ_DRIFT_SD_MIN = 0.12 | |
| local TREMOR_FREQ_DRIFT_SD_MAX = 0.32 | |
| local TREMOR_FREQ_DRIFT_TAU_MIN = 1.5 | |
| local TREMOR_FREQ_DRIFT_TAU_MAX = 2.8 | |
| local TREMOR_AMP_DRIFT_SD_MIN = 0.025 | |
| local TREMOR_AMP_DRIFT_SD_MAX = 0.060 | |
| local TREMOR_AMP_DRIFT_TAU_MIN = 1.0 | |
| local TREMOR_AMP_DRIFT_TAU_MAX = 2.0 | |
| -- ===================================================================== | |
| -- BUTTON POLLING | |
| -- ===================================================================== | |
| local BUTTON_POLL_MS = 15 | |
| local TOGGLE_DEBOUNCE_MS = 180 | |
| -- ===================================================================== | |
| -- INTERNAL CONSTANTS AND STATE | |
| -- ===================================================================== | |
| local TWO_PI = math.pi * 2.0 | |
| local PERSON_PRNG_MODULUS = 2147483647 | |
| local enabled = false | |
| local running = false | |
| local configurationValid = true | |
| local ignoreToggleUntil = 0 | |
| local gaussianHasSpare = false | |
| local gaussianSpare = 0 | |
| -- ===================================================================== | |
| -- BASIC HELPERS | |
| -- ===================================================================== | |
| local function Clamp(value, minimum, maximum) | |
| if value < minimum then | |
| return minimum | |
| end | |
| if value > maximum then | |
| return maximum | |
| end | |
| return value | |
| end | |
| local function RandomFloat(minimum, maximum) | |
| return minimum + math.random() * (maximum - minimum) | |
| end | |
| -- Center-weighted random value. | |
| local function HumanRandom(minimum, maximum) | |
| local randomValue = | |
| ( | |
| math.random() + | |
| math.random() + | |
| math.random() | |
| ) / 3.0 | |
| return minimum + randomValue * (maximum - minimum) | |
| end | |
| local function RoundSigned(value) | |
| if value >= 0 then | |
| return math.floor(value + 0.5) | |
| end | |
| return math.ceil(value - 0.5) | |
| end | |
| -- ===================================================================== | |
| -- TOGGLE CONFIGURATION VALIDATION | |
| -- ===================================================================== | |
| local function IsToggleButtonSupported() | |
| return | |
| TOGGLE_BUTTON >= 1 and | |
| TOGGLE_BUTTON <= 5 | |
| end | |
| -- ===================================================================== | |
| -- PERSON SEED NORMALIZATION | |
| -- ===================================================================== | |
| local function NormalizePersonSeed(seed) | |
| seed = tonumber(seed) or 1 | |
| seed = math.floor(seed) | |
| seed = seed % PERSON_PRNG_MODULUS | |
| if seed <= 0 then | |
| seed = 1 | |
| end | |
| return seed | |
| end | |
| local NORMALIZED_PERSON_SEED = | |
| NormalizePersonSeed(PERSON_SEED) | |
| -- ===================================================================== | |
| -- GAUSSIAN RANDOM GENERATOR | |
| -- ===================================================================== | |
| local function Gaussian() | |
| if gaussianHasSpare then | |
| gaussianHasSpare = false | |
| return gaussianSpare | |
| end | |
| local u1 = 0.0 | |
| repeat | |
| u1 = math.random() | |
| until u1 > 0.000001 | |
| local u2 = math.random() | |
| local magnitude = | |
| math.sqrt(-2.0 * math.log(u1)) | |
| local phase = | |
| TWO_PI * u2 | |
| gaussianSpare = | |
| magnitude * math.sin(phase) | |
| gaussianHasSpare = true | |
| return | |
| magnitude * math.cos(phase) | |
| end | |
| -- ===================================================================== | |
| -- TEMPORALLY CORRELATED NOISE | |
| -- ===================================================================== | |
| local function CorrelatedNoise( | |
| value, | |
| tau, | |
| standardDeviation, | |
| deltaTime | |
| ) | |
| local decay = | |
| math.exp(-deltaTime / tau) | |
| local randomScale = | |
| standardDeviation * | |
| math.sqrt(1.0 - decay * decay) | |
| return | |
| value * decay + | |
| randomScale * Gaussian() | |
| end | |
| -- ===================================================================== | |
| -- DEDICATED PERSON PRNG | |
| -- ===================================================================== | |
| local personState = | |
| NORMALIZED_PERSON_SEED | |
| local function PersonRandom01() | |
| personState = | |
| ( | |
| personState * 16807 | |
| ) % PERSON_PRNG_MODULUS | |
| return | |
| personState / PERSON_PRNG_MODULUS | |
| end | |
| local function PersonRandom(minimum, maximum) | |
| return | |
| minimum + | |
| PersonRandom01() * | |
| (maximum - minimum) | |
| end | |
| local function PersonHumanRandom(minimum, maximum) | |
| local randomValue = | |
| ( | |
| PersonRandom01() + | |
| PersonRandom01() + | |
| PersonRandom01() | |
| ) / 3.0 | |
| return | |
| minimum + | |
| randomValue * | |
| (maximum - minimum) | |
| end | |
| -- ===================================================================== | |
| -- PERSISTENT PERSON PROFILE | |
| -- ===================================================================== | |
| local PERSON = {} | |
| local function CreatePersonProfile() | |
| personState = | |
| NORMALIZED_PERSON_SEED | |
| PERSON.radiusBias = | |
| PersonHumanRandom( | |
| 0.97, | |
| 1.03 | |
| ) | |
| PERSON.frequencyPreference = | |
| PersonHumanRandom( | |
| 0.30, | |
| 0.70 | |
| ) | |
| PERSON.ellipticity = | |
| PersonHumanRandom( | |
| -0.028, | |
| 0.028 | |
| ) | |
| PERSON.rotation = | |
| PersonHumanRandom( | |
| -0.18, | |
| 0.18 | |
| ) | |
| PERSON.noiseBias = | |
| PersonHumanRandom( | |
| 0.90, | |
| 1.10 | |
| ) | |
| PERSON.tremorBias = | |
| PersonHumanRandom( | |
| 0.90, | |
| 1.10 | |
| ) | |
| PERSON.xScale = | |
| PersonHumanRandom( | |
| 0.985, | |
| 1.025 | |
| ) | |
| PERSON.yScale = | |
| PersonHumanRandom( | |
| 0.975, | |
| 1.015 | |
| ) | |
| PERSON.tremorFreqX = | |
| PersonHumanRandom( | |
| 8.5, | |
| 10.8 | |
| ) | |
| PERSON.tremorFreqY = | |
| PersonHumanRandom( | |
| 8.7, | |
| 11.2 | |
| ) | |
| PERSON.harmonic2 = | |
| PersonHumanRandom( | |
| 0.006, | |
| 0.018 | |
| ) | |
| PERSON.harmonic3 = | |
| PersonHumanRandom( | |
| 0.0025, | |
| 0.009 | |
| ) | |
| PERSON.harmonic2Phase = | |
| PersonRandom( | |
| 0.0, | |
| TWO_PI | |
| ) | |
| PERSON.harmonic3Phase = | |
| PersonRandom( | |
| 0.0, | |
| TWO_PI | |
| ) | |
| if PersonRandom01() < 0.5 then | |
| PERSON.direction = -1 | |
| else | |
| PERSON.direction = 1 | |
| end | |
| end | |
| -- ===================================================================== | |
| -- DPI LED INDICATOR HELPERS | |
| -- ===================================================================== | |
| local function PlayDpiUp() | |
| PlayMacro(DPI_UP_MACRO) | |
| Sleep(DPI_COMMAND_DELAY_MS) | |
| end | |
| local function PlayDpiDown() | |
| PlayMacro(DPI_DOWN_MACRO) | |
| Sleep(DPI_COMMAND_DELAY_MS) | |
| end | |
| local function ForceDpiProfile1() | |
| -- Four downward commands guarantee reaching DPI/Profile 1 even | |
| -- when starting from DPI/Profile 5. | |
| for _ = 1, DPI_MAX_PROFILE_COUNT - 1 do | |
| PlayDpiDown() | |
| end | |
| end | |
| local function MoveProfile1ToProfile3() | |
| for _ = 1, DPI_STEPS_TO_THREE_LEDS do | |
| PlayDpiUp() | |
| end | |
| end | |
| local function MoveProfile3ToProfile1() | |
| for _ = 1, DPI_STEPS_TO_THREE_LEDS do | |
| PlayDpiDown() | |
| end | |
| end | |
| -- ===================================================================== | |
| -- ENABLED LED INDICATION | |
| -- ===================================================================== | |
| -- | |
| -- Blink all three LEDs three times. | |
| -- | |
| -- The mouse always returns to DPI/Profile 1 before cursor automation | |
| -- starts. | |
| -- | |
| -- ===================================================================== | |
| local function ShowEnabledIndicator() | |
| ForceDpiProfile1() | |
| for pulse = 1, LED_ON_PULSE_COUNT do | |
| MoveProfile1ToProfile3() | |
| Sleep( | |
| math.random( | |
| LED_ON_HOLD_MIN_MS, | |
| LED_ON_HOLD_MAX_MS | |
| ) | |
| ) | |
| MoveProfile3ToProfile1() | |
| if pulse < LED_ON_PULSE_COUNT then | |
| Sleep( | |
| math.random( | |
| LED_ON_GAP_MIN_MS, | |
| LED_ON_GAP_MAX_MS | |
| ) | |
| ) | |
| end | |
| end | |
| OutputLogMessage( | |
| "[HumanJiggler] LED indicator: ON\n" | |
| ) | |
| end | |
| -- ===================================================================== | |
| -- DISABLED LED INDICATION | |
| -- ===================================================================== | |
| -- | |
| -- Force DPI/Profile 1 so the one-LED pattern is displayed. | |
| -- | |
| -- ===================================================================== | |
| local function ShowDisabledIndicator() | |
| ForceDpiProfile1() | |
| Sleep( | |
| math.random( | |
| LED_OFF_HOLD_MIN_MS, | |
| LED_OFF_HOLD_MAX_MS | |
| ) | |
| ) | |
| OutputLogMessage( | |
| "[HumanJiggler] LED indicator: OFF\n" | |
| ) | |
| end | |
| -- ===================================================================== | |
| -- TOGGLE BUTTON HELPERS | |
| -- ===================================================================== | |
| local function WaitForToggleRelease() | |
| while IsMouseButtonPressed(TOGGLE_BUTTON) do | |
| Sleep(5) | |
| end | |
| end | |
| local function CheckToggleOff() | |
| if not IsMouseButtonPressed(TOGGLE_BUTTON) then | |
| return false | |
| end | |
| enabled = false | |
| WaitForToggleRelease() | |
| ignoreToggleUntil = | |
| GetRunningTime() + | |
| TOGGLE_DEBOUNCE_MS | |
| ShowDisabledIndicator() | |
| return true | |
| end | |
| -- ===================================================================== | |
| -- INTERRUPTIBLE SLEEP | |
| -- ========================================= |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment