Skip to content

Commit 238770f

Browse files
authored
Implement "quick message" feature for pre-canned messages. (#40)
* Implement "quick message" feature for pre-canned messages. * Minor documentation corrections.
1 parent 161ed23 commit 238770f

12 files changed

Lines changed: 802 additions & 113 deletions

File tree

scripts/autokey.py

Lines changed: 127 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
import argparse
1313
from msvcrt import getch
14+
import re
1415

1516
from superkey import *
1617

@@ -66,114 +67,150 @@ def _buffered_mode(port: str = SUPERKEY_DEFAULT_PORT,
6667
"""
6768
with Interface(port=port, baudrate=baudrate, timeout=timeout) as intf:
6869
while True:
70+
try:
6971

70-
# Get next input from user
71-
line = input('> ')
72+
# Get next input from user
73+
line = input('> ')
7274

73-
# Check for special commands
74-
if line == ':q' or line == ':quit' or line == ':exit':
75-
# Exit program
76-
break
75+
# Helper functions
76+
def line_equals(x: str) -> bool:
77+
return line.casefold() == x.casefold()
78+
def line_starts_with(x: str) -> bool:
79+
return line.casefold().startswith(x.casefold())
7780

78-
elif line == ':!' or line == ':panic':
79-
# Panic
80-
intf.panic()
81-
continue
81+
# Check for special commands
82+
if line_equals(':q') or line_equals(':quit') or line_equals(':exit'):
83+
# Exit program
84+
break
8285

83-
elif line == ':wpm':
84-
# Print WPM status
85-
print(f'WPM: {intf.get_wpm():.1f}')
86-
continue
86+
elif line_equals(':!') or line_equals(':panic'):
87+
# Panic
88+
intf.panic()
89+
continue
8790

88-
elif line[:5] == ':wpm ':
89-
# Set WPM
90-
try:
91-
intf.set_wpm(float(line[5:]))
92-
except ValueError:
93-
print('Invalid WPM?')
94-
continue
91+
elif line_equals(':wpm'):
92+
# Print WPM status
93+
print(f'WPM: {intf.get_wpm():.1f}')
94+
continue
9595

96-
elif line == ':buzzer':
97-
# Print buzzer status
98-
print(f'Buzzer: {'On' if intf.get_buzzer_enabled() else 'Off'} ({intf.get_buzzer_frequency()} Hz)')
99-
continue
96+
elif line_starts_with(':wpm '):
97+
# Set WPM
98+
try:
99+
intf.set_wpm(float(line[5:]))
100+
except ValueError:
101+
print('Invalid WPM?')
102+
continue
100103

101-
elif line == ':buzzer off':
102-
# Turn buzzer off
103-
intf.set_buzzer_enabled(False)
104-
continue
104+
elif line_equals(':buzzer'):
105+
# Print buzzer status
106+
print(f'Buzzer: {'On' if intf.get_buzzer_enabled() else 'Off'} ({intf.get_buzzer_frequency()} Hz)')
107+
continue
105108

106-
elif line == ':buzzer on':
107-
# Turn buzzer on
108-
intf.set_buzzer_enabled(True)
109-
continue
109+
elif line_equals(':buzzer off'):
110+
# Turn buzzer off
111+
intf.set_buzzer_enabled(False)
112+
continue
110113

111-
elif line[:18] == ':buzzer frequency ':
112-
# Set buzzer frequency
113-
try:
114-
intf.set_buzzer_frequency(int(line[18:]))
115-
except ValueError:
116-
print('Invalid frequency?')
117-
continue
114+
elif line_equals(':buzzer on'):
115+
# Turn buzzer on
116+
intf.set_buzzer_enabled(True)
117+
continue
118118

119-
elif line == ':paddle':
120-
# Print paddle mode
121-
mode = intf.get_paddle_mode()
122-
if mode == PaddleMode.IAMBIC:
123-
print('Paddle mode: IAMBIC')
124-
elif mode == PaddleMode.ULTIMATIC:
125-
print('Paddle mode: ULTIMATIC')
126-
elif mode == PaddleMode.ULTIMATIC_ALTERNATE:
127-
print('Paddle mode: ULTIMATIC_ALTERNATE')
128-
else:
129-
print('Paddle mode: unknown?')
130-
continue
119+
elif line_starts_with(':buzzer frequency '):
120+
# Set buzzer frequency
121+
try:
122+
intf.set_buzzer_frequency(int(line[18:]))
123+
except ValueError:
124+
print('Invalid frequency?')
125+
continue
131126

132-
elif line == ':paddle iambic':
133-
# Set paddles to iambic mode
134-
intf.set_paddle_mode(PaddleMode.IAMBIC)
135-
continue
127+
elif line_equals(':paddle'):
128+
# Print paddle mode
129+
mode = intf.get_paddle_mode()
130+
if mode == PaddleMode.IAMBIC:
131+
print('Paddle mode: Iambic')
132+
elif mode == PaddleMode.ULTIMATIC:
133+
print('Paddle mode: Ultimatic')
134+
elif mode == PaddleMode.ULTIMATIC_ALTERNATE:
135+
print('Paddle mode: Ultimatic Alternate')
136+
else:
137+
print('Paddle mode: unknown?')
138+
continue
136139

137-
elif line == ':paddle ultimatic':
138-
# Set paddles to ultimatic mode
139-
intf.set_paddle_mode(PaddleMode.ULTIMATIC)
140-
continue
140+
elif line_equals(':paddle iambic'):
141+
# Set paddles to iambic mode
142+
intf.set_paddle_mode(PaddleMode.IAMBIC)
143+
continue
141144

142-
elif line == ':paddle ultimatic_alternate':
143-
# Set paddles to ultimatic alternate mode
144-
intf.set_paddle_mode(PaddleMode.ULTIMATIC_ALTERNATE)
145-
continue
145+
elif line_equals(':paddle ultimatic'):
146+
# Set paddles to ultimatic mode
147+
intf.set_paddle_mode(PaddleMode.ULTIMATIC)
148+
continue
146149

147-
elif line == ':trainer':
148-
# Print trainer mode status
149-
print(f'Trainer mode: {'On' if intf.get_trainer_mode() else 'Off'}')
150-
continue
150+
elif line_equals(':paddle ultimatic_alternate'):
151+
# Set paddles to ultimatic alternate mode
152+
intf.set_paddle_mode(PaddleMode.ULTIMATIC_ALTERNATE)
153+
continue
151154

152-
elif line == ':trainer on':
153-
# Enable trainer mode
154-
intf.set_trainer_mode(True)
155-
continue
155+
elif line_equals(':trainer'):
156+
# Print trainer mode status
157+
print(f'Trainer mode: {'On' if intf.get_trainer_mode() else 'Off'}')
158+
continue
156159

157-
elif line == ':trainer off':
158-
# Disable trainer mode
159-
intf.set_trainer_mode(False)
160-
continue
160+
elif line_equals(':trainer on'):
161+
# Enable trainer mode
162+
intf.set_trainer_mode(True)
163+
continue
161164

162-
elif line[0] == ':':
163-
# Unknown command?
164-
print('Unknown command?')
165-
continue
165+
elif line_equals(':trainer off'):
166+
# Disable trainer mode
167+
intf.set_trainer_mode(False)
168+
continue
169+
170+
elif line_starts_with(':qm'):
171+
# Handle this with regex for easier processing
172+
if match := re.match(r'^:qm get (\d+)$', line, re.IGNORECASE):
173+
print(intf.get_quick_msg(int(match.group(1))))
174+
continue
175+
elif match := re.match(r'^:qm set (\d+) (.+)$', line, re.IGNORECASE):
176+
intf.set_quick_msg(int(match.group(1)), match.group(2))
177+
continue
178+
elif match := re.match(r'^:qm del (\d+)$', line, re.IGNORECASE):
179+
intf.invalidate_quick_msg(int(match.group(1)))
180+
continue
181+
elif match := re.match(r'^:qm (\d+)$', line, re.IGNORECASE):
182+
intf.autokey_quick_msg(int(match.group(1)))
183+
continue
184+
185+
elif line_starts_with(':'):
186+
# Unknown command?
187+
print('Unknown command?')
188+
continue
166189

167-
# Split line into tokens and send in either normal or prosign mode
168-
tokens = line.split('\\')
169-
prosign = False
170-
for token in tokens:
171-
if prosign and len(token) > 1:
172-
intf.autokey(token[:-1], flags=[AutokeyFlag.NO_LETTER_SPACE])
173-
intf.autokey(token[-1])
174-
elif len(token) != 0:
175-
intf.autokey(token)
176-
prosign = not prosign
190+
# Split line into tokens and send in either normal or prosign mode
191+
tokens = line.split('\\')
192+
prosign = False
193+
for token in tokens:
194+
if prosign and len(token) > 1:
195+
intf.autokey(token[:-1], flags=[AutokeyFlag.NO_LETTER_SPACE])
196+
intf.autokey(token[-1])
197+
elif len(token) != 0:
198+
intf.autokey(token)
199+
prosign = not prosign
200+
201+
# Ultra-graceful error handling
202+
except InvalidMessageError:
203+
print('SuperKey responds: invalid message!')
204+
except InvalidSizeError:
205+
print('SuperKey responds: invalid size!')
206+
except InvalidCRCError:
207+
print('SuperKey responds: invalid CRC!')
208+
except InvalidPayloadError:
209+
print('SuperKey responds: invalid payload!')
210+
except InvalidValueError:
211+
print('SuperKey responds: invalid value!')
212+
except InterfaceError:
213+
print('SuperKey responds: unknown error!')
177214

178215

179216
def _immediate_mode(port: str = SUPERKEY_DEFAULT_PORT,

scripts/superkey/interface.py

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
'InvalidSizeError',
2525
'InvalidCRCError',
2626
'InvalidPayloadError',
27+
'InvalidValueError',
2728
'Interface',
2829
'InteractiveInterface',
2930
]
@@ -128,6 +129,13 @@ def autokey(self, string: str, flags: Iterable[AutokeyFlag] = []):
128129
self.__send_packet(MessageID.REQUEST_AUTOKEY_EX, payload)
129130
self.__check_reply_empty()
130131

132+
def autokey_quick_msg(self, index: int):
133+
"""
134+
Sends the `REQUEST_AUTOKEY_QUICK_MSG` command. Keys a quick message.
135+
"""
136+
self.__send_packet(MessageID.REQUEST_AUTOKEY_QUICK_MSG, struct.pack('<B', index))
137+
self.__check_reply_empty()
138+
131139
def get_buzzer_enabled(self) -> bool:
132140
"""
133141
Sends the `REQUEST_GET_BUZZER_ENABLED` command. Returns whether the buzzer is enabled or not.
@@ -191,6 +199,13 @@ def get_paddle_mode(self) -> PaddleMode:
191199
self.__send_packet(MessageID.REQUEST_GET_PADDLE_MODE)
192200
return PaddleMode(self.__check_reply('<B')[0])
193201

202+
def get_quick_msg(self, index: int) -> str:
203+
"""
204+
Sends the `REQUEST_GET_QUICK_MSG` command. Returns the text of the specified quick message.
205+
"""
206+
self.__send_packet(MessageID.REQUEST_GET_QUICK_MSG, struct.pack('<B', index))
207+
return str(self.__check_reply(), encoding='ascii')[:-1]
208+
194209
def get_trainer_mode(self) -> bool:
195210
"""
196211
Sends the `REQUEST_GET_TRAINER_MODE` command. Returns whether trainer mode is enabled or not.
@@ -212,6 +227,13 @@ def get_wpm_scale(self, element: CodeElement) -> float:
212227
self.__send_packet(MessageID.REQUEST_GET_WPM_SCALE, struct.pack('<B', element))
213228
return self.__check_reply('<f')[0]
214229

230+
def invalidate_quick_msg(self, idx: int):
231+
"""
232+
Sends the `REQUEST_INVALIDATE_QUICK_MSG` command. Deletes the specified quick message.
233+
"""
234+
self.__send_packet(MessageID.REQUEST_INVALIDATE_QUICK_MSG, struct.pack('<B', idx))
235+
self.__check_reply_empty()
236+
215237
def panic(self):
216238
"""
217239
Sends the `REQUEST_PANIC` command. Immediately and unconditionally stops keying.
@@ -282,6 +304,19 @@ def set_paddle_mode(self, mode: PaddleMode):
282304
self.__send_packet(MessageID.REQUEST_SET_PADDLE_MODE, struct.pack('<B', mode))
283305
self.__check_reply_empty()
284306

307+
def set_quick_msg(self, index: int, string: str):
308+
"""
309+
Sends the `REQUEST_SET_QUICK_MSG` command. Sets the text of a quick message.
310+
"""
311+
# Assemble payload
312+
payload = (struct.pack('<B', index) + # First byte is index
313+
bytes(string, encoding='ascii') + # Then the string
314+
b'\x00') # Null terminator needs to be added manually
315+
316+
# Send packet and check reply
317+
self.__send_packet(MessageID.REQUEST_SET_QUICK_MSG, payload)
318+
self.__check_reply_empty()
319+
285320
def set_trainer_mode(self, enabled: bool):
286321
"""
287322
Sends the `SET_TRAINER_MODE` command. Enables or disables trainer mode.
@@ -361,7 +396,7 @@ def __receive_header(self):
361396
# Unpack the header and verify the size and CRC are correct
362397
return self.__class__.__unpack_header(reply)
363398

364-
def __check_reply(self, format: str) -> Tuple[any, ...]:
399+
def __check_reply(self, format: Optional[str] = None) -> Tuple[any, ...] | bytes:
365400
"""
366401
Attempts to receive a reply with a payload from the device.
367402
"""
@@ -383,11 +418,20 @@ def __check_reply(self, format: str) -> Tuple[any, ...]:
383418
if payload is not None and self.__class__.__crc16(payload) != crc:
384419
raise InterfaceError('Reply: Invalid CRC?')
385420

386-
# Check payload length
387-
if len(payload) != struct.calcsize(format):
388-
raise InterfaceError('Reply: Invalid payload?')
421+
# Do we have a format string?
422+
if format is not None:
423+
424+
# Check payload length
425+
if len(payload) != struct.calcsize(format):
426+
raise InterfaceError('Reply: Invalid payload?')
427+
428+
# Unpack struct and return
429+
return struct.unpack(format, payload)
430+
431+
else:
389432

390-
return struct.unpack(format, payload)
433+
# Return packed bytes
434+
return payload
391435

392436
def __check_reply_empty(self):
393437
"""

scripts/superkey/types.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@
105105
# Requests
106106
'REQUEST_AUTOKEY',
107107
'REQUEST_AUTOKEY_EX',
108+
'REQUEST_AUTOKEY_QUICK_MSG',
108109
'REQUEST_GET_BUZZER_ENABLED',
109110
'REQUEST_GET_BUZZER_FREQUENCY',
110111
'REQUEST_GET_INVERT_PADDLES',
@@ -114,9 +115,11 @@
114115
'REQUEST_GET_IO_TYPE',
115116
'REQUEST_GET_LED_ENABLED',
116117
'REQUEST_GET_PADDLE_MODE',
118+
'REQUEST_GET_QUICK_MSG',
117119
'REQUEST_GET_TRAINER_MODE',
118120
'REQUEST_GET_WPM',
119121
'REQUEST_GET_WPM_SCALE',
122+
'REQUEST_INVALIDATE_QUICK_MSG',
120123
'REQUEST_PANIC',
121124
'REQUEST_PING',
122125
'REQUEST_RESTORE_DEFAULT_CONFIG',
@@ -127,6 +130,7 @@
127130
'REQUEST_SET_IO_TYPE',
128131
'REQUEST_SET_LED_ENABLED',
129132
'REQUEST_SET_PADDLE_MODE',
133+
'REQUEST_SET_QUICK_MSG',
130134
'REQUEST_SET_TRAINER_MODE',
131135
'REQUEST_SET_WPM',
132136
'REQUEST_SET_WPM_SCALE',

src/main/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ set(EXECUTABLE_SOURCE application/buzzer.c
2525
application/keyer.h
2626
application/led.c
2727
application/led.h
28+
application/quick_msg.c
29+
application/quick_msg.h
2830
application/storage.c
2931
application/storage.h
3032
application/strings.c

0 commit comments

Comments
 (0)