I've made a python program to convert DuckyScript code to Arduino code for Leonardo boards. Below you can find both the project files and a sample script.
compiler.py
(the program base):
#!/usr/bin/env python3
"""
Usage: {} <input> <output>
<input> should be a DuckieScript file and <output> will be an Arduino sketch
"""
import os
import sys
import fileinput
import textwrap
from data import keycodes
from data import code
delay = 0
cmdtype = 0
commands = ""
includes = ""
defines = ""
loop = ""
def info(type, msg, **kwargs):
lineno = fileinput.lineno()
path = os.path.basename(input_file)
exit = kwargs.get('exit', None)
types = ['error', 'warning', 'info', 'screw up risk']
message = '{}:{}: {}: {}'.format(path, lineno, types[type], msg)
print(message)
if exit:
print('{}: compilation aborted.'.format(path))
sys.exit(exit)
def getkey(keys):
global commands
keys = keys.split()
normalkey = list()
modifiers = 0
for key in keys:
if not key.isupper():
key = key.upper()
if len(key) is not 1:
info(1, 'you should type all the special keys in uppercase')
try:
key = [keycodes.get(code) for code in keycodes if key in code][0]
except IndexError:
info(0, 'unrecognized key: {}'.format(key), exit=127)
if key[1]:
modifiers |= key[0]
else:
normalkey.append(key[0])
if len(normalkey) > 6:
info(0, 'you can\'t press more than 6 keys at the same time', exit=126)
arguments = [0] * 7
arguments[6] = modifiers
for number, argument in enumerate(normalkey):
arguments[number] = argument
arguments = [format(byte, '#04x') for byte in arguments]
commands += 'sendKey({},{},{},{},{},{},{});'.format(*arguments)
commands += '\n'
return True
if len(sys.argv) is not 3:
print(__doc__.format(sys.argv[0]))
sys.exit(1)
elif not os.path.isfile(sys.argv[1]):
print()
print('Error: You must specify a valid input file')
print(__doc__.format(sys.argv[0]))
sys.exit(2)
else:
output_file = sys.argv[2]
input_file = sys.argv[1]
for line in fileinput.input([input_file]):
base = list(map(str.strip, line.split(maxsplit=1)))
if len(base) is 2:
command, options = base
elif len(base) is 1:
command = base[0]
options = None
else:
continue
if not command.isupper() and command not in ('#', '//', ';', '@', '%'):
command = command.upper()
info(1, 'commands should be typed in uppercase')
if command in ('REM', 'COMMENT', '#', '//', ';'):
commands += '// {}'.format(options)
commands += '\n'
needs_delay = False
cmdtype = -1
elif command in ('INCLUDE'):
info(3, 'don\'t use {} unless you really need it'.format(command))
if cmdtype > 0:
info(0, 'you shouldn\'t mix {} in the code'.format(command), exit=5)
includes += '#include {}'.format(options)
includes += '\n'
needs_delay = False
cmdtype = 0
elif command in ('DEFINE'):
info(3, 'don\'t use {} unless you really need it'.format(command))
if cmdtype > 1:
info(0, 'you shouldn\'t mix {} in the code'.format(command), exit=5)
includes += '#define {}'.format(options)
includes += '\n'
needs_delay = False
cmdtype = 1
elif command in ('LOOP', '@'):
info(3, "don't use {} unless you really need it".format(command))
if cmdtype > 2:
info(0, 'you should put {} in the top of the program'.format(command), exit=5)
loop += options
loop += '\n'
needs_delay = False
cmdtype = 2
elif command in ('ARDUINO', 'CODE', '%'):
info(3, "don't use {} unless you really need it".format(command))
commands += options
commands += '\n'
needs_delay = False
cmdtype = 3
elif command in ('DEFAULT_DELAY', 'DEFAULTDELAY'):
if not options.isdigit():
info(0, '{} only accepts integers'.format(command), exit=11)
delay = int(options)
needs_delay = False
cmdtype = -1
elif command in ('SLEEP', 'DELAY', 'WAIT'):
if not options.isdigit():
info(0, '{} only accepts integers'.format(command), exit=11)
commands += 'delay({});'.format(options)
commands += '\n'
needs_delay = False
cmdtype = 3
elif command in ('REPEAT'):
if not options.isdigit():
info(0, '{} only accepts integers'.format(command), exit=11)
last_command = commands.splitlines()[-1]
commands += 'int i=0;'
commands += '\n'
commands += 'for(i; i<={}; i++) {{'.format(options)
commands += '\n'
commands += ' {}'.format(last_command)
commands += '\n'
commands += ' delay({});'.format(delay)
commands += '\n'
commands += '}'
commands += '\n'
needs_delay = False
cmdtype = 3
elif command in ('STRING', 'TEXT', 'PRINT'):
commands += 'Keyboard.print("{}");'.format(options.replace('"', '\\"'))
commands += '\n'
needs_delay = True
cmdtype = 3
else:
cmdtype = 3
if not getkey(line):
info(0, 'unrecognized command: {}'.format(command), exit=10)
if delay > 0 and needs_delay:
commands += 'delay({});'.format(delay)
commands += '\n'
commands = "\n".join([" " + i for i in commands.splitlines()])
loop = "\n".join([" " + i for i in loop.splitlines()])
code = code.format(includes=includes,
defines=defines,
setup=commands,
loop=loop)
with open(output_file, 'w') as output:
output.write(code)
data.py
(the data storage):
#!/usr/bin/env python3
"""
This module contains all the compiler data, including a keycode lookup table
and the arduino program base.
"""
import textwrap
# Here is the base code of the output. Note that the double curl braces escapes
# the format strings. This will avoid later to get confused the format function
code = textwrap.dedent("""
{includes}
{defines}
void sendKey(byte k0, byte k1, byte k2, byte k3,
byte k4, byte k5, byte modifiers)
{{
KeyReport report = {{0}};
report.keys[0] = k0;
report.keys[1] = k1;
report.keys[2] = k2;
report.keys[3] = k3;
report.keys[4] = k4;
report.keys[5] = k5;
report.modifiers = modifiers;
report.reserved = 1;
Keyboard.sendReport(&report);
report = {{0}};
Keyboard.sendReport(&report);
}}
void setup() {{
{setup}
}}
void loop() {{
{loop}
}}
""")
# Dictionary with the keys and their hexadecimal values.
# The structure follows these rules:
#
# ('KEY_ALIAS_1', 'KEY_ALIAS_2', 'KEY_ALIAS_N'): [0xKeyCode, IsModifier]
#
# If the key is a modifier key (CTRL, ALT, SHIFT or GUI), IsModifier will be 1,
# else it will be 0.
#
# The keycodes were extracted from the arduino defines and slightly modified to
# respect the DuckieScript coding standards.
#
# You can get the base of this dictionary piping the arduino defines to these
# commands (only if you're in a *nix system):
#
# sed 's/KEY_//' |
# awk '{ print "(\x27"$2"\x27, \x27KEY_"$2"\x27): ["$3", 0]," }'
keycodes = {
('A', 'KEY_A'): [0x04, 0],
('B', 'KEY_B'): [0x05, 0],
('C', 'KEY_C'): [0x06, 0],
('D', 'KEY_D'): [0x07, 0],
('E', 'KEY_E'): [0x08, 0],
('F', 'KEY_F'): [0x09, 0],
('G', 'KEY_G'): [0x0A, 0],
('H', 'KEY_H'): [0x0B, 0],
('I', 'KEY_I'): [0x0C, 0],
('J', 'KEY_J'): [0x0D, 0],
('K', 'KEY_K'): [0x0E, 0],
('L', 'KEY_L'): [0x0F, 0],
('M', 'KEY_M'): [0x10, 0],
('N', 'KEY_N'): [0x11, 0],
('O', 'KEY_O'): [0x12, 0],
('P', 'KEY_P'): [0x13, 0],
('Q', 'KEY_Q'): [0x14, 0],
('R', 'KEY_R'): [0x15, 0],
('S', 'KEY_S'): [0x16, 0],
('T', 'KEY_T'): [0x17, 0],
('U', 'KEY_U'): [0x18, 0],
('V', 'KEY_V'): [0x19, 0],
('W', 'KEY_W'): [0x1A, 0],
('X', 'KEY_X'): [0x1B, 0],
('Y', 'KEY_Y'): [0x1C, 0],
('Z', 'KEY_Z'): [0x1D, 0],
('1', 'KEY_1'): [0x1E, 0],
('2', 'KEY_2'): [0x1F, 0],
('3', 'KEY_3'): [0x20, 0],
('4', 'KEY_4'): [0x21, 0],
('5', 'KEY_5'): [0x22, 0],
('6', 'KEY_6'): [0x23, 0],
('7', 'KEY_7'): [0x24, 0],
('8', 'KEY_8'): [0x25, 0],
('9', 'KEY_9'): [0x26, 0],
('0', 'KEY_0'): [0x27, 0],
('ENTER', 'NEWLINE', 'KEY_ENTER'): [0x28, 0],
('ESC', 'ESCAPE', 'KEY_ESC'): [0x29, 0],
('BACKSPACE', 'KEY_BACKSPACE'): [0x2A, 0],
('TAB', 'KEY_TAB'): [0x2B, 0],
('SPACE', 'KEY_SPACE'): [0x2C, 0],
('MINUS', 'KEY_MINUS'): [0x2D, 0],
('EQUAL', 'KEY_EQUAL'): [0x2E, 0],
('LEFTBRACE', 'KEY_LEFTBRACE'): [0x2F, 0],
('RIGHTBRACE', 'KEY_RIGHTBRACE'): [0x30, 0],
('BACKSLASH', 'KEY_BACKSLASH'): [0x31, 0],
('NONUSHASH', 'KEY_NONUSHASH'): [0x32, 0],
('SEMICOLON', 'KEY_SEMICOLON'): [0x33, 0],
('APOSTROPHE', 'KEY_APOSTROPHE'): [0x34, 0],
('GRAVE', 'KEY_GRAVE'): [0x35, 0],
('COMMA', 'KEY_COMMA'): [0x36, 0],
('DOT', 'KEY_DOT'): [0x37, 0],
('SLASH', 'KEY_SLASH'): [0x38, 0],
('CAPSLOCK', 'KEY_CAPSLOCK'): [0x39, 0],
('F1', 'KEY_F1'): [0x3A, 0],
('F2', 'KEY_F2'): [0x3B, 0],
('F3', 'KEY_F3'): [0x3C, 0],
('F4', 'KEY_F4'): [0x3D, 0],
('F5', 'KEY_F5'): [0x3E, 0],
('F6', 'KEY_F6'): [0x3F, 0],
('F7', 'KEY_F7'): [0x40, 0],
('F8', 'KEY_F8'): [0x41, 0],
('F9', 'KEY_F9'): [0x42, 0],
('F10', 'KEY_F10'): [0x43, 0],
('F11', 'KEY_F11'): [0x44, 0],
('F12', 'KEY_F12'): [0x45, 0],
('SYSRQ', 'KEY_SYSRQ'): [0x46, 0],
('SCROLLLOCK', 'KEY_SCROLLLOCK'): [0x47, 0],
('PAUSE', 'KEY_PAUSE'): [0x48, 0],
('INSERT', 'KEY_INSERT'): [0x49, 0],
('HOME', 'KEY_HOME'): [0x4A, 0],
('PAGEUP', 'KEY_PAGEUP'): [0x4B, 0],
('DELETE', 'KEY_DELETE'): [0x4C, 0],
('END', 'KEY_END'): [0x4D, 0],
('PAGEDOWN', 'KEY_PAGEDOWN'): [0x4E, 0],
('RIGHT', 'RIGHTARROW', 'KEY_RIGHT'): [0x4F, 0],
('LEFT', 'LEFTARROW', 'KEY_LEFT'): [0x50, 0],
('DOWN', 'DOWNARROW', 'KEY_DOWN'): [0x51, 0],
('UP', 'UPARROW', 'KEY_UP'): [0x52, 0],
('NUMLOCK', 'KEY_NUMLOCK'): [0x53, 0],
('KPSLASH', 'KEY_KPSLASH'): [0x54, 0],
('KPASTERISK', 'KEY_KPASTERISK'): [0x55, 0],
('KPMINUS', 'KEY_KPMINUS'): [0x56, 0],
('KPPLUS', 'KEY_KPPLUS'): [0x57, 0],
('KPENTER', 'KEY_KPENTER'): [0x58, 0],
('KP1', 'KEY_KP1'): [0x59, 0],
('KP2', 'KEY_KP2'): [0x5A, 0],
('KP3', 'KEY_KP3'): [0x5B, 0],
('KP4', 'KEY_KP4'): [0x5C, 0],
('KP5', 'KEY_KP5'): [0x5D, 0],
('KP6', 'KEY_KP6'): [0x5E, 0],
('KP7', 'KEY_KP7'): [0x5F, 0],
('KP8', 'KEY_KP8'): [0x60, 0],
('KP9', 'KEY_KP9'): [0x61, 0],
('KP0', 'KEY_KP0'): [0x62, 0],
('KPDOT', 'KEY_KPDOT'): [0x63, 0],
('102ND', 'KEY_102ND'): [0x64, 0],
('COMPOSE', 'KEY_COMPOSE'): [0x65, 0],
('POWER', 'KEY_POWER'): [0x66, 0],
('KPEQUAL', 'KEY_KPEQUAL'): [0x67, 0],
('F13', 'KEY_F13'): [0x68, 0],
('F14', 'KEY_F14'): [0x69, 0],
('F15', 'KEY_F15'): [0x6A, 0],
('F16', 'KEY_F16'): [0x6B, 0],
('F17', 'KEY_F17'): [0x6C, 0],
('F18', 'KEY_F18'): [0x6D, 0],
('F19', 'KEY_F19'): [0x6E, 0],
('F20', 'KEY_F20'): [0x6F, 0],
('F21', 'KEY_F21'): [0x70, 0],
('F22', 'KEY_F22'): [0x71, 0],
('F23', 'KEY_F23'): [0x72, 0],
('F24', 'KEY_F24'): [0x73, 0],
('OPEN', 'KEY_OPEN'): [0x74, 0],
('HELP', 'KEY_HELP'): [0x75, 0],
('PROPS', 'MENU', 'APP', 'CONTEXTMENU', 'KEY_PROPS'): [0x76, 0],
('FRONT', 'KEY_FRONT'): [0x77, 0],
('STOP', 'KEY_STOP'): [0x78, 0],
('AGAIN', 'KEY_AGAIN'): [0x79, 0],
('UNDO', 'KEY_UNDO'): [0x7A, 0],
('CUT', 'KEY_CUT'): [0x7B, 0],
('COPY', 'KEY_COPY'): [0x7C, 0],
('PASTE', 'KEY_PASTE'): [0x7D, 0],
('FIND', 'KEY_FIND'): [0x7E, 0],
('MUTE', 'KEY_MUTE'): [0x7F, 0],
('VOLUMEUP', 'KEY_VOLUMEUP'): [0x80, 0],
('VOLUMEDOWN', 'KEY_VOLUMEDOWN'): [0x81, 0],
('RETURN', 'KEY_RETURN'): [0x9E, 0],
# ('LEFTCTRL', 'KEY_LEFTCTRL'): [0xE0, 0],
# ('LEFTSHIFT', 'KEY_LEFTSHIFT'): [0xE1, 0],
# ('LEFTALT', KEY_LEFTALT'): [0xE2, 0],
# ('LEFTGUI', 'KEY_LEFTGUI'): [0xE3, 0],
# ('RIGHTCTRL', 'KEY_RIGHTCTRL'): [0xE4, 0],
# ('RIGHTSHIFT', 'KEY_RIGHTSHIFT'): [0xE5, 0],
# ('RIGHTALT', 'KEY_RIGHTALT'): [0xE6, 0],
# ('RIGHTGUI', 'KEY_RIGHTGUI'): [0xE7, 0],
('KEY_MODIFIER_LEFT_CTRL',
'KEY_LEFTCONTROL',
'KEY_LEFTCTRL',
'KEY_CONTROL'
'KEY_CTRL',
'LEFTCONTROL',
'LEFTCTRL',
'CONTROL',
'CTRL',): [0x01, 1],
('KEY_MODIFIER_LEFT_SHIFT',
'KEY_LEFTSHIFT',
'KEY_SHIFT',
'LEFTSHIFT',
'SHIFT'): [0x02, 1],
('KEY_MODIFIER_LEFT_ALT',
'KEY_LEFTOPTION',
'KEY_LEFTALT',
'KEY_OPTION',
'KEY_ALT',
'LEFTOPTION',
'LEFTALT',
'OPTION',
'ALT'): [0x04, 1],
('KEY_MODIFIER_LEFT_GUI',
'KEY_COMMAND',
'KEY_WINDOWS',
'KEY_SUPER',
'KEY_META',
'KEY_GUI',
'KEY_LEFTCOMMAND',
'KEY_LEFTWINDOWS',
'KEY_LEFTSUPER',
'KEY_LEFTMETA',
'KEY_LEFTGUI',
'LEFTCOMMAND',
'LEFTWINDOWS',
'LEFTSUPER',
'LEFTMETA',
'LEFTGUI'
'COMMAND',
'WINDOWS',
'SUPER',
'META',
'GUI'): [0x08, 1],
('KEY_MODIFIER_RIGHT_CTRL',
'KEY_RIGHTCONTROL',
'KEY_RIGHTCTRL',
'RIGHTCONTROL',
'RIGHTCTRL'): [0x10, 1],
('KEY_MODIFIER_RIGHT_SHIFT',
'KEY_RIGHTSHIFT',
'RIGHTSHIFT'): [0x20, 1],
('KEY_MODIFIER_RIGHT_ALT',
'KEY_RIGHTOPTION',
'KEY_RIGHTALT',
'RIGHTOPTION',
'RIGHTALT'): [0x40, 1],
('KEY_MODIFIER_RIGHT_GUI',
'KEY_RIGHTCOMMAND',
'KEY_RIGHTWINDOWS',
'KEY_RIGHTSUPER',
'KEY_RIGHTMETA',
'KEY_RIGHTGUI',
'RIGHTCOMMAND',
'RIGHTWINDOWS',
'RIGHTSUPER',
'RIGHTMETA',
'RIGHTGUI'): [0x80, 1],
('PLAYPAUSE', 'KEY_PLAYPAUSE'): [0xE8, 0],
('STOPCD', 'KEY_STOPCD'): [0xE9, 0],
('PREVIOUSSONG', 'KEY_PREVIOUSSONG'): [0xEA, 0],
('NEXTSONG', 'KEY_NEXTSONG'): [0xEB, 0],
('EJECTCD', 'KEY_EJECTCD'): [0xEC, 0],
('WWW', 'KEY_WWW'): [0xF0, 0],
('BACK', 'KEY_BACK'): [0xF1, 0],
('FORWARD', 'KEY_FORWARD'): [0xF2, 0],
('SCROLLUP', 'KEY_SCROLLUP'): [0xF5, 0],
('SCROLLDOWN', 'KEY_SCROLLDOWN'): [0xF6, 0],
('EDIT', 'KEY_EDIT'): [0xF7, 0]
}
script
(the test script):
REM Script made specially for CodeReview users
REM Only works in Linux machines
DEFAULT_DELAY 500
INCLUDE <SoftwareSerial.h>
DEFINE TEXT "Hello"
LOOP // I only will put a comment on the loop
DELAY 3000
CONTROL ALT T
STRING echo "This is a demo"
ENTER
DELAY 3000
ALT F4
ARDUINO Serial.begin(9600);
ARDUINO Serial.println(TEXT);
REM Program end
I'm so new in Python, so please correct me. I'm sure that I'm missing some tricks. Also I want to make the program the most readable and structured possible.
To all those never heard of DuckyScript:
Ducky Script is the language of the USB Rubber Ducky. Writing scripts for can be done from any common ascii text editor such as Notepad, vi, emacs, nano, gedit, kedit, TextEdit, etc.
Quote from https://github.com/hak5darren/USB-Rubber-Ducky/wiki/Duckyscript