Skip to content

Commit b283a96

Browse files
authored
Add additional commands to Python interface. (#26)
- Add commands to get buzzer enablement and frequency. - Add commands to get and set WPM. - Add commands to get and set WPM element scales. - Add commands to get and set paddle mode and invert settings. - Add command to get device softare version. - Add command to enable / disable LEDs. - Add commands to set type and polarity of I/O pins. - Reduce boilerplate.
1 parent 0bdcd69 commit b283a96

7 files changed

Lines changed: 910 additions & 119 deletions

File tree

scripts/interactive.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313

1414
# ------------------------------------------------------ IMPORTS -------------------------------------------------------
1515

16-
from superkey import Interface, InteractiveInterface
16+
from superkey.interface import Interface, InteractiveInterface
17+
from superkey.types import *
1718

1819
# ----------------------------------------------------- PROCEDURES -----------------------------------------------------
1920

scripts/readme.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,5 @@ function declared on the `Interface` class. No instance is required.
3535
>>> set_buzzer_frequency(800)
3636
>>> autokey('cq cq de n0vig n0vig k')
3737
```
38+
39+
The `dir()` function may be used to get a list of available functions.

scripts/superkey/interface.py

Lines changed: 251 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,13 @@ def __init__(self):
7070
super().__init__("Reply: Invalid payload.")
7171

7272

73+
class InvalidValueError(InterfaceError):
74+
"""Exception indicating that a `REPLY_INVALID_VALUE` reply was received."""
75+
def __init__(self):
76+
"""Initializes a new instance."""
77+
super().__init__('Reply: Invalid value.')
78+
79+
7380
class Interface:
7481
"""
7582
Class encapsulating the serial interface provided by the SuperKey hardware.
@@ -103,79 +110,177 @@ def autokey(self, string: str):
103110
"""
104111
Sends the `REQUEST_AUTOKEY` command. Queues the specified string to be automatically keyed.
105112
"""
106-
# Assemble packet
107-
payload = bytes(string, encoding='ascii') + b'\x00' # null char is not added by default
108-
size = len(payload)
109-
crc = self.__class__.__crc16(payload)
110-
header = self.__class__.__pack_header(MessageID.REQUEST_AUTOKEY, size, crc)
113+
self.__send_packet(MessageID.REQUEST_AUTOKEY, bytes(string, encoding='ascii') + b'\x00')
114+
self.__check_reply_empty()
111115

112-
# Send packet
113-
self.__send(header)
114-
self.__send(payload)
115-
self.__check_empty_reply()
116+
def get_buzzer_enabled(self) -> bool:
117+
"""
118+
Sends the `REQUEST_GET_BUZZER_ENABLED` command. Returns whether the buzzer is enabled or not.
119+
"""
120+
self.__send_packet(MessageID.REQUEST_GET_BUZZER_ENABLED)
121+
return self.__check_reply('<?')[0]
122+
123+
def get_buzzer_frequency(self) -> int:
124+
"""
125+
Sends the `REQUEST_GET_BUZZER_FREQUENCY` command. Returns the current buzzer frequency, in Hz.
126+
"""
127+
self.__send_packet(MessageID.REQUEST_GET_BUZZER_FREQUENCY)
128+
return self.__check_reply('<H')[0]
129+
130+
def get_invert_paddles(self) -> bool:
131+
"""
132+
Sends the `REQUEST_GET_INVERT_PADDLES` command. Returns whether or not the paddles are inverted.
133+
"""
134+
self.__send_packet(MessageID.REQUEST_GET_INVERT_PADDLES)
135+
return self.__check_reply('<?')[0]
136+
137+
def get_io_polarity(self, pin: IOPin) -> IOPolarity:
138+
"""
139+
Sends the `REQUEST_GET_IO_POLARITY` command. Returns the polarity of the specified I/O pin.
140+
"""
141+
self.__send_packet(MessageID.REQUEST_GET_IO_POLARITY, struct.pack('<B', pin))
142+
return IOPolarity(self.__check_reply('<B')[0])
143+
144+
def get_io_state(self, pin: IOPin) -> IOPin:
145+
"""
146+
Sends the `REQUEST_GET_IO_STATE` command. Returns `true` if the specified input / output pin is active.
147+
"""
148+
self.__send_packet(MessageID.REQUEST_GET_IO_STATE, struct.pack('<B', pin))
149+
return self.__check_reply('<?')[0]
150+
151+
def get_io_state_for_type(self, type: IOType) -> IOType:
152+
"""
153+
Sends the `REQUEST_GET_IO_STATE_FOR_TYPE` command. Returns `true` if any I/O pin with the specified type is on.
154+
"""
155+
self.__send_packet(MessageID.REQUEST_GET_IO_STATE_FOR_TYPE, struct.pack('<B', type))
156+
return self.__check_reply('<?')[0]
157+
158+
def get_io_type(self, pin: IOPin) -> IOType:
159+
"""
160+
Sends the `REQUEST_GET_IO_TYPE` command. Returns the type of the specified I/O pin.
161+
"""
162+
self.__send_packet(MessageID.REQUEST_GET_IO_TYPE, struct.pack('<B', pin))
163+
return IOType(self.__check_reply('<B')[0])
164+
165+
def get_led_enabled(self, led: LED) -> bool:
166+
"""
167+
Sends the `REQUEST_GET_LED_ENABLED` command. Returns whether or not the specified LED is enabled.
168+
"""
169+
self.__send_packet(MessageID.REQUEST_GET_LED_ENABLED, struct.pack('<b', led))
170+
return self.__check_reply('<?')[0]
171+
172+
def get_paddle_mode(self) -> PaddleMode:
173+
"""
174+
Sends the `REQUEST_GET_PADDLE_MODE` command. Returns the currently selected paddle mode.
175+
"""
176+
self.__send_packet(MessageID.REQUEST_GET_PADDLE_MODE)
177+
return PaddleMode(self.__check_reply('<B')[0])
178+
179+
def get_wpm(self) -> float:
180+
"""
181+
Sends the `REQUEST_GET_WPM` command. Returns the current WPM setting.
182+
"""
183+
self.__send_packet(MessageID.REQUEST_GET_WPM)
184+
return self.__check_reply('<f')[0]
185+
186+
def get_wpm_scale(self, element: CodeElement) -> float:
187+
"""
188+
Sends the `REQUEST_GET_WPM_SCALE` command. Returns the current WPM scale for the specified code element.
189+
"""
190+
self.__send_packet(MessageID.REQUEST_GET_WPM_SCALE, struct.pack('<B', element))
191+
return self.__check_reply('<f')[0]
116192

117193
def panic(self):
118194
"""
119195
Sends the `REQUEST_PANIC` command. Immediately and unconditionally stops keying.
120196
"""
121-
# Assemble packet
122-
header = self.__class__.__pack_header(MessageID.REQUEST_PANIC)
123-
124-
# Send packet
125-
self.__send(header)
126-
self.__check_empty_reply()
197+
self.__send_packet(MessageID.REQUEST_PANIC)
198+
self.__check_reply_empty()
127199

128200
def ping(self):
129201
"""
130202
Sends the `REQUEST_PING` command. Keys a short test message.
131203
"""
132-
# Assemble packet
133-
header = self.__class__.__pack_header(MessageID.REQUEST_PING)
134-
135-
# Send packet
136-
self.__send(header)
137-
self.__check_empty_reply()
204+
self.__send_packet(MessageID.REQUEST_PING)
205+
self.__check_reply_empty()
138206

139207
def restore_default_config(self):
140208
"""
141209
Sends the `REQUEST_RESTORE_DEFAULT_CONFIG` command. Restores the device to its default configuration.
142210
"""
143-
# Assemble packet
144-
header = self.__class__.__pack_header(MessageID.REQUEST_RESTORE_DEFAULT_CONFIG)
145-
146-
# Send packet
147-
self.__send(header)
148-
self.__check_empty_reply()
211+
self.__send_packet(MessageID.REQUEST_RESTORE_DEFAULT_CONFIG)
212+
self.__check_reply_empty()
149213

150214
def set_buzzer_enabled(self, enabled: bool):
151215
"""
152216
Sends the `REQUEST_SET_BUZZER_ENABLED` command. Enables or disables the device's built-in buzzer.
153217
"""
154-
# Assemble packet
155-
payload = struct.pack('<?', enabled)
156-
size = len(payload)
157-
crc = self.__class__.__crc16(payload)
158-
header = self.__class__.__pack_header(MessageID.REQUEST_SET_BUZZER_ENABLED, size, crc)
159-
160-
# Send packet
161-
self.__send(header)
162-
self.__send(payload)
163-
self.__check_empty_reply()
218+
self.__send_packet(MessageID.REQUEST_SET_BUZZER_ENABLED, struct.pack('<?', enabled))
219+
self.__check_reply_empty()
164220

165221
def set_buzzer_frequency(self, frequency: int):
166222
"""
167223
Sends the `REQUEST_SET_BUZZER_FREQUENCY` command. Sets the frequency (in Hz) of the device's built-in buzzer.
168224
"""
169-
# Assemble packet
170-
payload = struct.pack('<H', frequency)
171-
size = len(payload)
172-
crc = self.__class__.__crc16(payload)
173-
header = self.__class__.__pack_header(MessageID.REQUEST_SET_BUZZER_FREQUENCY, size, crc)
225+
self.__send_packet(MessageID.REQUEST_SET_BUZZER_FREQUENCY, struct.pack('<H', frequency))
226+
self.__check_reply_empty()
227+
228+
def set_invert_paddles(self, inverted: bool):
229+
"""
230+
Sends the `REQUEST_SET_INVERT_PADDLES` command. Sets whether the paddles are inverted.
231+
"""
232+
self.__send_packet(MessageID.REQUEST_SET_INVERT_PADDLES, struct.pack('<?', inverted))
233+
self.__check_reply_empty()
234+
235+
def set_io_polarity(self, pin: IOPin, polarity: IOPolarity):
236+
"""
237+
Sends the `REQUEST_SET_IO_POLARITY` command. Sets the polarity of the specified I/O pin.
238+
"""
239+
self.__send_packet(MessageID.REQUEST_SET_IO_POLARITY, struct.pack('<BB', pin, polarity))
240+
self.__check_reply_empty()
241+
242+
def set_io_type(self, pin: IOPin, type: IOType):
243+
"""
244+
Sends the `REQUEST_SET_IO_TYPE` command. Sets the type of the specified I/O pin.
245+
"""
246+
self.__send_packet(MessageID.REQUEST_SET_IO_TYPE, struct.pack('<BB', pin, type))
247+
self.__check_reply_empty()
248+
249+
def set_led_enabled(self, led: LED, enabled: bool):
250+
"""
251+
Sends the `REQUEST_SET_LED_ENABLED` command. Sets whether the specified LED is enabled or not.
252+
"""
253+
self.__send_packet(MessageID.REQUEST_SET_LED_ENABLED, struct.pack('<B?', led, enabled))
254+
self.__check_reply_empty()
255+
256+
def set_paddle_mode(self, mode: PaddleMode):
257+
"""
258+
Sends the `REQUEST_SET_PADDLE_MODE` command. Sets the current keyer paddle mode.
259+
"""
260+
self.__send_packet(MessageID.REQUEST_SET_PADDLE_MODE, struct.pack('<B', mode))
261+
self.__check_reply_empty()
262+
263+
def set_wpm(self, wpm: float):
264+
"""
265+
Sends the `REQUEST_SET_WPM` command. Sets the keyer's WPM setting.
266+
"""
267+
self.__send_packet(MessageID.REQUEST_SET_WPM, struct.pack('<f', wpm))
268+
self.__check_reply_empty()
269+
270+
def set_wpm_scale(self, element: CodeElement, scale: float):
271+
"""
272+
Sends the `REQUEST_SET_WPM_SCALE` command. Sets the WPM scale for the specified code element.
273+
"""
274+
self.__send_packet(MessageID.REQUEST_SET_WPM_SCALE, struct.pack('<Bf', element, scale))
275+
self.__check_reply_empty()
174276

175-
# Send packet
277+
def version(self) -> str:
278+
"""
279+
Sends the `REQUEST_VERSION` command. Returns the device's version information.
280+
"""
281+
header = self.__class__.__pack_header(MessageID.REQUEST_VERSION)
176282
self.__send(header)
177-
self.__send(payload)
178-
self.__check_empty_reply()
283+
return self.__check_reply_str()
179284

180285
def __validate_serial(self):
181286
"""
@@ -184,21 +289,117 @@ def __validate_serial(self):
184289
if self.serial is None:
185290
raise InterfaceError("The serial port is not open.")
186291

187-
def __check_empty_reply(self) -> bool:
292+
def __send(self, buffer: bytes):
188293
"""
189-
Attempts to receive a generic empty reply from the device.
294+
Transmits the specified buffer.
295+
"""
296+
self.__validate_serial()
297+
self.serial.write(buffer)
298+
299+
def __send_packet(self, message: MessageID, payload: Optional[bytes] = None):
300+
"""
301+
Sends a packet with the specified message ID and payload.
302+
"""
303+
# Get header
304+
size = 0
305+
crc = 0
306+
if payload is not None:
307+
size = len(payload)
308+
crc = self.__class__.__crc16(payload)
309+
header = self.__class__.__pack_header(message, size, crc)
310+
311+
# Send data
312+
self.__send(header)
313+
if payload is not None:
314+
self.__send(payload)
315+
316+
def __receive(self, size: int):
317+
"""
318+
Receives the specified number of bytes.
319+
"""
320+
self.__validate_serial()
321+
return self.serial.read(size=size)
322+
323+
def __receive_header(self):
324+
"""
325+
Attempts to receive a header from the serial port.
190326
"""
191327
# Receive reply and verify we got enough data
192328
reply = self.__receive(HEADER_STRUCT_SIZE)
193329
if len(reply) != HEADER_STRUCT_SIZE:
194330
raise InterfaceError('No reply received.')
195331

196332
# Unpack the header and verify the size and CRC are correct
197-
message, size, crc = self.__class__.__unpack_header(reply)
333+
return self.__class__.__unpack_header(reply)
334+
335+
def __check_reply(self, format: str) -> Tuple[any, ...]:
336+
"""
337+
Attempts to receive a reply with a payload from the device.
338+
"""
339+
# Unpack header
340+
message, size, crc = self.__receive_header()
341+
342+
# Receive payload
343+
if size != 0:
344+
payload = self.__receive(size)
345+
if len(payload) != size:
346+
raise InterfaceError('No payload received.')
347+
else:
348+
payload = None
349+
350+
# Check the message ID
351+
self.__check_reply_message_id(message)
352+
353+
# Check CRC
354+
if payload is not None and self.__class__.__crc16(payload) != crc:
355+
raise InterfaceError('Reply: Invalid CRC?')
356+
357+
# Check payload length
358+
if len(payload) != struct.calcsize(format):
359+
raise InterfaceError('Reply: Invalid payload?')
360+
361+
return struct.unpack(format, payload)
362+
363+
def __check_reply_empty(self):
364+
"""
365+
Attempts to receive a generic empty reply from the device.
366+
"""
367+
# Unpack message
368+
message, size, crc = self.__receive_header()
198369
if size != 0 or crc != 0:
199370
raise InterfaceError('Reply: Invalid size / CRC?')
200371

201372
# Check the message ID
373+
self.__check_reply_message_id(message)
374+
375+
def __check_reply_str(self) -> Optional[str]:
376+
"""
377+
Attempts to receive a reply with a string payload from the device.
378+
"""
379+
# Unpack header
380+
message, size, crc = self.__receive_header()
381+
382+
# Receive payload
383+
if size != 0:
384+
payload = self.__receive(size)
385+
if len(payload) != size:
386+
raise InterfaceError('No payload received.')
387+
else:
388+
return None
389+
390+
# Check the message ID
391+
self.__check_reply_message_id(message)
392+
393+
# Check CRC
394+
if self.__class__.__crc16(payload) != crc:
395+
raise InterfaceError('Reply: Invalid CRC?')
396+
397+
return str(payload, encoding='ascii')
398+
399+
def __check_reply_message_id(self, message: MessageID):
400+
"""
401+
Throws an exception if the specified message ID represent a failure.
402+
"""
202403
if message == MessageID.REPLY_SUCCESS:
203404
return # no error
204405
elif message == MessageID.REPLY_INVALID_MESSAGE:
@@ -209,23 +410,11 @@ def __check_empty_reply(self) -> bool:
209410
raise InvalidCRCError()
210411
elif message == MessageID.REPLY_INVALID_PAYLOAD:
211412
raise InvalidPayloadError()
413+
elif message == MessageID.REPLY_INVALID_VALUE:
414+
raise InvalidValueError()
212415
else:
213416
raise InterfaceError('Reply: Unknown reply?')
214417

215-
def __send(self, buffer: bytes):
216-
"""
217-
Transmits the specified buffer.
218-
"""
219-
self.__validate_serial()
220-
self.serial.write(buffer)
221-
222-
def __receive(self, size: int):
223-
"""
224-
Receives the specified number of bytes.
225-
"""
226-
self.__validate_serial()
227-
return self.serial.read(size=size)
228-
229418
@staticmethod
230419
def __crc16(buffer: bytes, seed: int = 0xFFFF):
231420
"""

0 commit comments

Comments
 (0)