From c71424b3adaf55d97b809e3b2972da642cccc51e Mon Sep 17 00:00:00 2001 From: Trent Warlaven Date: Mon, 20 Nov 2023 19:29:12 -0600 Subject: [PATCH] Adafruit Trinkey QT2040 support --- .gitignore | 5 + boot.py | 44 +++++++- code.py | 76 ++++++------- duckyinpython.py | 289 ++++++++++++++++++++++++++++++++--------------- 4 files changed, 275 insertions(+), 139 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..db8c262 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +# VSCode Settings +.vscode/ + +# MacOS Files +.DS_Store diff --git a/boot.py b/boot.py index 7e7d434..4339666 100644 --- a/boot.py +++ b/boot.py @@ -2,16 +2,13 @@ # copyright (c) 2023 Dave Bailey # Author: Dave Bailey (dbisu, @daveisu) # Pico and Pico W board support +# Beta Trinkey QT2040 support -from board import * import board import digitalio import storage noStorage = False -noStoragePin = digitalio.DigitalInOut(GP15) -noStoragePin.switch_to_input(pull=digitalio.Pull.UP) -noStorageStatus = noStoragePin.value # If GP15 is not connected, it will default to being pulled high (True) # If GP is connected to GND, it will be low (False) @@ -24,13 +21,52 @@ # GP15 not connected == USB NOT visible # GP15 connected to GND == USB visible +# Trinkey QT2040: +# Boot button pressed during timeout == USB visible +# Boot button not pressed == USB not visible + if(board.board_id == 'raspberry_pi_pico'): + noStoragePin = digitalio.DigitalInOut(board.GP15) + noStoragePin.switch_to_input(pull=digitalio.Pull.UP) + noStorageStatus = noStoragePin.value # On Pi Pico, default to USB visible noStorage = not noStorageStatus + noStoragePin.deinit() elif(board.board_id == 'raspberry_pi_pico_w'): + noStoragePin = digitalio.DigitalInOut(board.GP15) + noStoragePin.switch_to_input(pull=digitalio.Pull.UP) + noStorageStatus = noStoragePin.value # on Pi Pico W, default to USB hidden by default # so webapp can access storage noStorage = noStorageStatus + noStoragePin.deinit() +elif (board.board_id == 'adafruit_qt2040_trinkey'): + # Import os to read the environment file + import os + # Import supervisor to wait for the timeout + import supervisor + # Listen for the BOOT button being pressed + button = digitalio.DigitalInOut(board.BUTTON) + # There is a hardware pullup on this pin, so no need to set it high + button.switch_to_input() + # Read the timeout from the environment file settings.toml. default to 500ms + wait_timeout = os.getenv('trinkey_storage_timeout', 500) + # Add the timeout to the current time to final timeout time + wait_timeout = supervisor.ticks_ms() + int(wait_timeout) + # Loop for the timeout seeing if the button is pressed + while supervisor.ticks_ms() < wait_timeout: + # By default the BOOT button is pulled high in hardware + # so button.value will be True unless the button is pressed + if not button.value: + print('Not disabling USB drive on Trinkey QT2040') + break + # If the loop completes that means the button was not pressed + else: + # And storage should be disabled + noStorage = True + # Deinit the BOOT button for later use + button.deinit() + if(noStorage == True): # don't show USB drive to host PC diff --git a/code.py b/code.py index 3d47f85..c11be08 100644 --- a/code.py +++ b/code.py @@ -2,23 +2,44 @@ # copyright (c) 2023 Dave Bailey # Author: Dave Bailey (dbisu, @daveisu) # Pico and Pico W board support - +# Beta Trinkey QT2040 support import supervisor - - import time import digitalio -from board import * import board from duckyinpython import * if(board.board_id == 'raspberry_pi_pico_w'): import wifi from webapp import * +# Wait half a second to allow the device to be recognized by the host computer +wait_time = time.monotonic() + .5 + +# turn off automatically reloading when files are written to the pico +#supervisor.disable_autoreload() +supervisor.runtime.autoreload = False + +payload = None +progStatus = False +progStatus = getProgrammingStatus() +print("progStatus", progStatus) +if(progStatus == False): + print("Finding payload") + # not in setup mode, inject the payload + payload = selectPayload() +else: + print("Update your payload") + +# Wait for the half second timeout to expire before running the payload +while time.monotonic() < wait_time: + pass -# sleep at the start to allow the device to be recognized by the host computer -time.sleep(.5) +# If we are not in setup mode, run the payload +if progStatus == False: + print("Running ", payload) + runScript(payload) + print("Done") def startWiFi(): import ipaddress @@ -37,46 +58,19 @@ def startWiFi(): PORT = 80 # Port to listen on print(HOST,PORT) -# turn off automatically reloading when files are written to the pico -#supervisor.disable_autoreload() -supervisor.runtime.autoreload = False - -if(board.board_id == 'raspberry_pi_pico'): - led = pwmio.PWMOut(board.LED, frequency=5000, duty_cycle=0) -elif(board.board_id == 'raspberry_pi_pico_w'): - led = digitalio.DigitalInOut(board.LED) - led.switch_to_output() - - -progStatus = False -progStatus = getProgrammingStatus() -print("progStatus", progStatus) -if(progStatus == False): - print("Finding payload") - # not in setup mode, inject the payload - payload = selectPayload() - print("Running ", payload) - runScript(payload) - - print("Done") -else: - print("Update your payload") - -led_state = False - async def main_loop(): - global led,button1 - - button_task = asyncio.create_task(monitor_buttons(button1)) + tasks = [] + button_task = asyncio.create_task(monitor_buttons()) + tasks.append(button_task) + led_task = asyncio.create_task(blink_led()) + tasks.append(led_task) if(board.board_id == 'raspberry_pi_pico_w'): - pico_led_task = asyncio.create_task(blink_pico_w_led(led)) print("Starting Wifi") startWiFi() print("Starting Web Service") webservice_task = asyncio.create_task(startWebService()) - await asyncio.gather(pico_led_task, button_task, webservice_task) - else: - pico_led_task = asyncio.create_task(blink_pico_led(led)) - await asyncio.gather(pico_led_task, button_task) + tasks.append(webservice_task) + # Pass the task list as *args to wait for them all to complete + await asyncio.gather(*tasks) asyncio.run(main_loop()) diff --git a/duckyinpython.py b/duckyinpython.py index a0a6087..d4e9b80 100644 --- a/duckyinpython.py +++ b/duckyinpython.py @@ -2,18 +2,21 @@ # copyright (c) 2023 Dave Bailey # Author: Dave Bailey (dbisu, @daveisu) - import time import digitalio from digitalio import DigitalInOut, Pull from adafruit_debouncer import Debouncer import board -from board import * +import os import pwmio import asyncio import usb_hid from adafruit_hid.keyboard import Keyboard +if board.board_id == 'adafruit_qt2040_trinkey': + import neopixel_write + NEOPIXEL_MAX = 64 + # comment out these lines for non_US keyboards from adafruit_hid.keyboard_layout_us import KeyboardLayoutUS as KeyboardLayout from adafruit_hid.keycode import Keycode @@ -45,8 +48,57 @@ 'F4': Keycode.F4, 'F5': Keycode.F5, 'F6': Keycode.F6, 'F7': Keycode.F7, 'F8': Keycode.F8, 'F9': Keycode.F9, 'F10': Keycode.F10, 'F11': Keycode.F11, 'F12': Keycode.F12, - } + +if(board.board_id == 'raspberry_pi_pico'): + led = pwmio.PWMOut(board.LED, frequency=5000, duty_cycle=0) +elif(board.board_id == 'raspberry_pi_pico_w'): + led = digitalio.DigitalInOut(board.LED) + led.switch_to_output() +elif(board.board_id == 'adafruit_qt2040_trinkey'): + led = digitalio.DigitalInOut(board.NEOPIXEL) + +def led_on(): + global led + # If the LED is a PWMOut, set the duty cycle to 65535 + # This is for the generic Pico + if isinstance(led, pwmio.PWMOut): + led.duty_cycle = 65535 + # If the LED is a DigitalInOut + elif isinstance(led, digitalio.DigitalInOut): + # If the board has a NEOPIXEL use that as the LED (trinkey) + if hasattr(board, 'NEOPIXEL'): + neopixel_write.neopixel_write(led, bytearray([NEOPIXEL_MAX, NEOPIXEL_MAX, NEOPIXEL_MAX])) + # Otherwise, use the normal LED (pico w) + else: + led.value = True + +def led_off(): + global led + # If the LED is a PWMOut, set the duty cycle to 0 + if isinstance(led, pwmio.PWMOut): + led.duty_cycle = 0 + # If the LED is a DigitalInOut + elif isinstance(led, digitalio.DigitalInOut): + # If the board has a NEOPIXEL use that as the LED (trinkey) + if hasattr(board, 'NEOPIXEL'): + neopixel_write.neopixel_write(led, bytearray([0, 0, 0])) + # Otherwise, use the normal LED (pico w) + else: + led.value = False + +LED_STATE = False + +def toggleLED(): + global LED_STATE + # LED_STATE = True == led on + if LED_STATE: + led_off() + LED_STATE = False + else: + led_on() + LED_STATE = True + def convertLine(line): newline = [] # print(line) @@ -75,8 +127,10 @@ def runScriptLine(line): def sendString(line): layout.write(line) +DEFAULT_DELAY = 0 + def parseLine(line): - global defaultDelay + global DEFAULT_DELAY if(line[0:3] == "REM"): # ignore ducky script comments pass @@ -89,14 +143,11 @@ def parseLine(line): elif(line[0:6] == "IMPORT"): runScript(line[7:]) elif(line[0:13] == "DEFAULT_DELAY"): - defaultDelay = int(line[14:]) * 10 + DEFAULT_DELAY = int(line[14:]) * 10 elif(line[0:12] == "DEFAULTDELAY"): - defaultDelay = int(line[13:]) * 10 + DEFAULT_DELAY = int(line[13:]) * 10 elif(line[0:3] == "LED"): - if(led.value == True): - led.value = False - else: - led.value = True + toggleLED() else: newScriptLine = convertLine(line) runScriptLine(newScriptLine) @@ -104,38 +155,40 @@ def parseLine(line): kbd = Keyboard(usb_hid.devices) layout = KeyboardLayout(kbd) - - - -#init button -button1_pin = DigitalInOut(GP22) # defaults to input -button1_pin.pull = Pull.UP # turn on internal pull-up resistor -button1 = Debouncer(button1_pin) - -#init payload selection switch -payload1Pin = digitalio.DigitalInOut(GP4) -payload1Pin.switch_to_input(pull=digitalio.Pull.UP) -payload2Pin = digitalio.DigitalInOut(GP5) -payload2Pin.switch_to_input(pull=digitalio.Pull.UP) -payload3Pin = digitalio.DigitalInOut(GP10) -payload3Pin.switch_to_input(pull=digitalio.Pull.UP) -payload4Pin = digitalio.DigitalInOut(GP11) -payload4Pin.switch_to_input(pull=digitalio.Pull.UP) - def getProgrammingStatus(): # check GP0 for setup mode # see setup mode for instructions - progStatusPin = digitalio.DigitalInOut(GP0) - progStatusPin.switch_to_input(pull=digitalio.Pull.UP) - progStatus = not progStatusPin.value + # If using the Trinkey, the BOOT button is used instead of GP0 + progStatus = False + if (board.board_id == 'adafruit_qt2040_trinkey'): + # Get the setup timeout from the environment file settings.toml. default to no setup time + # Since this will run rapidly after boot, it could trigger off the storage button press + timeout = os.getenv('one_button_setup_timeout', 500) + if timeout is not None: + print('Waiting for button press to decide if script should run') + import supervisor + button = digitalio.DigitalInOut(board.BUTTON) + button.switch_to_input() + timeout = supervisor.ticks_ms() + int(timeout) + # Loop for the timeout, setting progStatus to True if the button is pressed + while supervisor.ticks_ms() < timeout: + if not button.value: + progStatus = True + break + # After the timeout + else: + progStatus = False + # Deinit the button for later use + button.deinit() + # Non-Trinkey boards + else: + progStatusPin = digitalio.DigitalInOut(board.GP0) + progStatusPin.switch_to_input(pull=digitalio.Pull.UP) + progStatus = not progStatusPin.value return(progStatus) - -defaultDelay = 0 - def runScript(file): - global defaultDelay - + global DEFAULT_DELAY duckyScriptPath = file try: f = open(duckyScriptPath,"r",encoding='utf-8') @@ -146,55 +199,82 @@ def runScript(file): for i in range(int(line[7:])): #repeat the last command parseLine(previousLine) - time.sleep(float(defaultDelay)/1000) + time.sleep(float(DEFAULT_DELAY)/1000) else: parseLine(line) previousLine = line - time.sleep(float(defaultDelay)/1000) + time.sleep(float(DEFAULT_DELAY)/1000) except OSError as e: print("Unable to open file ", file) -def selectPayload(): - global payload1Pin, payload2Pin, payload3Pin, payload4Pin - payload = "payload.dd" - # check switch status - # payload1 = GPIO4 to GND - # payload2 = GPIO5 to GND - # payload3 = GPIO10 to GND - # payload4 = GPIO11 to GND - payload1State = not payload1Pin.value - payload2State = not payload2Pin.value - payload3State = not payload3Pin.value - payload4State = not payload4Pin.value - - if(payload1State == True): - payload = "payload.dd" - - elif(payload2State == True): - payload = "payload2.dd" - - elif(payload3State == True): - payload = "payload3.dd" - - elif(payload4State == True): - payload = "payload4.dd" +# Global list of payloads, and the pin to check for each payload +# Will apply in the order they are added to the list +payloads = [] + +if (board.board_id == 'adafruit_qt2040_trinkey'): + # With only a single button, alternate payloads are not easy + pass +else: + # Other boards need a switch to select the payload + payload1Pin = digitalio.DigitalInOut(board.GP4) + payload1Pin.switch_to_input(pull=digitalio.Pull.UP) + payloads.append(("payload1.dd", payload1Pin)) + + payload2Pin = digitalio.DigitalInOut(board.GP5) + payload2Pin.switch_to_input(pull=digitalio.Pull.UP) + payloads.append(("payload2.dd", payload2Pin)) + + payload3Pin = digitalio.DigitalInOut(board.GP10) + payload3Pin.switch_to_input(pull=digitalio.Pull.UP) + payloads.append(("payload3.dd", payload3Pin)) + + payload4Pin = digitalio.DigitalInOut(board.GP11) + payload4Pin.switch_to_input(pull=digitalio.Pull.UP) + payloads.append(("payload4.dd", payload4Pin)) +def selectPayload(): + global payloads + # Go over all the loaded payload + for payload_name, check_pin in payloads: + # These are all pull-up, so False means the switch is pressed + if not check_pin.value: + # If the switch is pressed, set the payload to the key + payload = payload_name + break else: - # if all pins are high, then no switch is present - # default to payload1 - payload = "payload.dd" + # If we made it through the loop without a break, no switch is pressed + # So try to get the default payload from the environment file + # If it's not there, default to payload.dd + payload = os.getenv('default_payload', "payload.dd") return payload -async def blink_led(led): - print("Blink") - if(board.board_id == 'raspberry_pi_pico'): - blink_pico_led(led) - elif(board.board_id == 'raspberry_pi_pico_w'): - blink_pico_w_led(led) +async def blink_neopixel_led(): + global led + print("starting blink_neopixel_led") + # led_state = True == led off + # led_state = False == led on + led_state = False + while True: + if led_state: + for i in range(100): + # While the LED is off, fade it up + if i < NEOPIXEL_MAX: + neopixel_write.neopixel_write(led, bytearray([i, i, i])) + await asyncio.sleep(0.01) + led_state = False + else: + for i in range(100): + # While the LED is on, fade it down + if i <= NEOPIXEL_MAX: + neopixel_write.neopixel_write(led, bytearray([NEOPIXEL_MAX - i, NEOPIXEL_MAX - i, NEOPIXEL_MAX - i])) + await asyncio.sleep(0.01) + led_state = True + await asyncio.sleep(0.1) -async def blink_pico_led(led): - print("starting blink_pico_led") +async def blink_pwm_led(): + global led + print("starting blink_pwm_led") led_state = False while True: if led_state: @@ -217,8 +297,9 @@ async def blink_pico_led(led): led_state = True await asyncio.sleep(0) -async def blink_pico_w_led(led): - print("starting blink_pico_w_led") +async def blink_binary_led(): + global led + print("starting blink_binary_led") led_state = False while True: if led_state: @@ -233,32 +314,52 @@ async def blink_pico_w_led(led): led_state = True await asyncio.sleep(0.5) -async def monitor_buttons(button1): - global inBlinkeyMode, inMenu, enableRandomBeep, enableSirenMode,pixel - print("starting monitor_buttons") - button1Down = False +async def blink_led(): + global led + # If the LED is a PWMOut, blink it + if isinstance(led, pwmio.PWMOut): + await blink_pwm_led() + # Otherwise decide if it's a neopixel or binary LED + elif isinstance(led, digitalio.DigitalInOut): + if hasattr(board, 'NEOPIXEL'): + await blink_neopixel_led() + else: + await blink_binary_led() + +async def monitor_buttons(): + # Button to re-run the payload + if (board.board_id == 'adafruit_qt2040_trinkey'): + re_run_pin = digitalio.DigitalInOut(board.BUTTON) + re_run_pin.switch_to_input(pull=digitalio.Pull.UP) + else: + re_run_pin = digitalio.DigitalInOut(board.GP22) + re_run_pin.switch_to_input(pull=digitalio.Pull.UP) + re_run_button = Debouncer(re_run_pin) + + print("Starting monitoring of buttons") + re_run_down = False while True: - button1.update() - - button1Pushed = button1.fell - button1Released = button1.rose - button1Held = not button1.value - - if(button1Pushed): - print("Button 1 pushed") - button1Down = True - if(button1Released): - print("Button 1 released") - if(button1Down): + re_run_button.update() + + re_run_pushed = re_run_button.fell + re_run_released = re_run_button.rose + re_run_held = not re_run_button.value + + if(re_run_pushed): + print("Re-run button pushed") + re_run_down = True + if(re_run_released): + print("Re-run button released") + if(re_run_down): print("push and released") - if(button1Released): - if(button1Down): + if(re_run_released): + if(re_run_down): # Run selected payload payload = selectPayload() print("Running ", payload) runScript(payload) print("Done") - button1Down = False + re_run_down = False await asyncio.sleep(0)