|
| 1 | +### Baudot TTY Message Transmitter with CLUE GUI |
| 2 | +### Pick from four phrases to send from the CLUE screen with buttons |
| 3 | + |
| 4 | +### The 5-bit mode is defined in ANSI TIA/EIA-825 (2000) |
| 5 | +### "A Frequency Shift Keyed Modem for use on the Public Switched Telephone Network" |
| 6 | + |
| 7 | +import time |
| 8 | +import math |
| 9 | +import array |
| 10 | +import board |
| 11 | +from audiocore import RawSample |
| 12 | +import audiopwmio |
| 13 | +import displayio |
| 14 | +from adafruit_display_shapes.circle import Circle |
| 15 | +from adafruit_clue import clue |
| 16 | +from adafruit_display_text import label |
| 17 | +import terminalio |
| 18 | + |
| 19 | +# Enter your messages here no more than 34 characters including spaces per line |
| 20 | +messages = [ |
| 21 | + "HELLO FROM ADAFRUIT INDUSTRIES", |
| 22 | + "12345678910 -$!+='()/:;?", |
| 23 | + "WOULD YOU LIKE TO PLAY A GAME?", |
| 24 | + "WELCOME TO JOHN PARK'S WORKSHOP", |
| 25 | +] |
| 26 | + |
| 27 | + |
| 28 | +clue.display.brightness = 1.0 |
| 29 | +screen = displayio.Group(max_size=5) |
| 30 | + |
| 31 | +VFD_GREEN = 0x00FFD2 |
| 32 | +VFD_BG = 0x000505 |
| 33 | + |
| 34 | +# setup screen |
| 35 | +# BG |
| 36 | +color_bitmap = displayio.Bitmap(240, 240, 1) |
| 37 | +color_palette = displayio.Palette(1) |
| 38 | +color_palette[0] = VFD_BG |
| 39 | +bg_sprite = displayio.TileGrid(color_bitmap, x=0, y=0, pixel_shader=color_palette) |
| 40 | +screen.append(bg_sprite) |
| 41 | + |
| 42 | +# title |
| 43 | +title_label = label.Label( |
| 44 | + terminalio.FONT, text="TTY CLUE", scale=4, color=VFD_GREEN, max_glyphs=11 |
| 45 | +) |
| 46 | +title_label.x = 20 |
| 47 | +title_label.y = 16 |
| 48 | +screen.append(title_label) |
| 49 | + |
| 50 | +# footer |
| 51 | +footer_label = label.Label( |
| 52 | + terminalio.FONT, text="<PICK SEND>", scale=2, color=VFD_GREEN, max_glyphs=40 |
| 53 | +) |
| 54 | +footer_label.x = 4 |
| 55 | +footer_label.y = 220 |
| 56 | +screen.append(footer_label) |
| 57 | + |
| 58 | +# message configs |
| 59 | +messages_config = [ |
| 60 | + (0, messages[0], VFD_GREEN, 2, 60), |
| 61 | + (1, messages[1], VFD_GREEN, 2, 90), |
| 62 | + (2, messages[2], VFD_GREEN, 2, 120), |
| 63 | + (3, messages[3], VFD_GREEN, 2, 150), |
| 64 | +] |
| 65 | + |
| 66 | +messages_labels = {} # dictionary of configured messages_labels |
| 67 | + |
| 68 | +message_group = displayio.Group(max_size=5, scale=1) |
| 69 | + |
| 70 | +for message_config in messages_config: |
| 71 | + (name, textline, color, x, y) = message_config # unpack tuple into five var names |
| 72 | + message_label = label.Label(terminalio.FONT, text=textline, color=color, max_glyphs=50) |
| 73 | + message_label.x = x |
| 74 | + message_label.y = y |
| 75 | + messages_labels[name] = message_label |
| 76 | + message_group.append(message_label) |
| 77 | +screen.append(message_group) |
| 78 | + |
| 79 | +# selection dot |
| 80 | +dot_y = [52, 82, 112, 142] |
| 81 | +dot = Circle(220, 60, 8, outline=VFD_GREEN, fill=VFD_BG) |
| 82 | +screen.append(dot) |
| 83 | + |
| 84 | +clue.display.show(screen) |
| 85 | + |
| 86 | +# constants for sine wave generation |
| 87 | +SIN_LENGTH = 100 # more is less choppy |
| 88 | +SIN_AMPLITUDE = 2 ** 12 # 0 (min) to 32768 (max) 8192 is nice |
| 89 | +SIN_OFFSET = 32767.5 # for 16bit range, (2**16 - 1) / 2 |
| 90 | +DELTA_PI = 2 * math.pi / SIN_LENGTH # happy little constant |
| 91 | + |
| 92 | +sine_wave = [ |
| 93 | + int(SIN_OFFSET + SIN_AMPLITUDE * math.sin(DELTA_PI * i)) for i in range(SIN_LENGTH) |
| 94 | +] |
| 95 | +tones = ( |
| 96 | + RawSample(array.array("H", sine_wave), sample_rate=1800 * SIN_LENGTH), # Bit 0 |
| 97 | + RawSample(array.array("H", sine_wave), sample_rate=1400 * SIN_LENGTH), # Bit 1 |
| 98 | +) |
| 99 | + |
| 100 | +bit_0 = tones[0] |
| 101 | +bit_1 = tones[1] |
| 102 | +carrier = tones[1] |
| 103 | + |
| 104 | + |
| 105 | +char_pause = 0.1 # pause time between chars, set to 0 for fastest rate possible |
| 106 | + |
| 107 | +dac = audiopwmio.PWMAudioOut( |
| 108 | + board.A2 |
| 109 | +) # the CLUE edge connector marked "#0" to STEMMA speaker |
| 110 | +# The CLUE's on-board speaker works OK, not great, just crank amplitude to full before trying. |
| 111 | +# dac = audiopwmio.PWMAudioOut(board.SPEAKER) |
| 112 | + |
| 113 | + |
| 114 | +LTRS = ( |
| 115 | + "\b", |
| 116 | + "E", |
| 117 | + "\n", |
| 118 | + "A", |
| 119 | + " ", |
| 120 | + "S", |
| 121 | + "I", |
| 122 | + "U", |
| 123 | + "\r", |
| 124 | + "D", |
| 125 | + "R", |
| 126 | + "J", |
| 127 | + "N", |
| 128 | + "F", |
| 129 | + "C", |
| 130 | + "K", |
| 131 | + "T", |
| 132 | + "Z", |
| 133 | + "L", |
| 134 | + "W", |
| 135 | + "H", |
| 136 | + "Y", |
| 137 | + "P", |
| 138 | + "Q", |
| 139 | + "O", |
| 140 | + "B", |
| 141 | + "G", |
| 142 | + "FIGS", |
| 143 | + "M", |
| 144 | + "X", |
| 145 | + "V", |
| 146 | + "LTRS", |
| 147 | +) |
| 148 | + |
| 149 | +FIGS = ( |
| 150 | + "\b", |
| 151 | + "3", |
| 152 | + "\n", |
| 153 | + "-", |
| 154 | + " ", |
| 155 | + "-", |
| 156 | + "8", |
| 157 | + "7", |
| 158 | + "\r", |
| 159 | + "$", |
| 160 | + "4", |
| 161 | + "'", |
| 162 | + ",", |
| 163 | + "!", |
| 164 | + ":", |
| 165 | + "(", |
| 166 | + "5", |
| 167 | + '"', |
| 168 | + ")", |
| 169 | + "2", |
| 170 | + "=", |
| 171 | + "6", |
| 172 | + "0", |
| 173 | + "1", |
| 174 | + "9", |
| 175 | + "?", |
| 176 | + "+", |
| 177 | + "FIGS", |
| 178 | + ".", |
| 179 | + "/", |
| 180 | + ";", |
| 181 | + "LTRS", |
| 182 | +) |
| 183 | + |
| 184 | +char_count = 0 |
| 185 | +current_mode = LTRS |
| 186 | + |
| 187 | +# The 5-bit Baudot text telephone (TTY) mode is a Frequency Shift Keyed modem |
| 188 | +# for use on the Public Switched Telephone network. |
| 189 | +# |
| 190 | +# Definitions: |
| 191 | +# Carrier tone is a 1400Hz tone. |
| 192 | +# Binary 0 is an 1800Hz tone. |
| 193 | +# Binary 1 is a 1400Hz tone. |
| 194 | +# Bit duration is 20ms. |
| 195 | + |
| 196 | +# Two modes exist: Letters, aka LTRS, for alphabet characters |
| 197 | +# and Figures aka FIGS for numbers and symbols. These modes are switched by |
| 198 | +# sending the appropriate 5-bit LTRS or FIGS character. |
| 199 | +# |
| 200 | +# Character transmission sequence: |
| 201 | +# Carrier tone transmits for 150ms before each character. |
| 202 | +# Start bit is a binary 0 (sounded for one bit duration of 20ms). |
| 203 | +# 5-bit character code can be a combination of binary 0s and binary 1s. |
| 204 | +# Stop bit is a binary 1 with a minimum duration of 1-1/2 bits (30ms) |
| 205 | +# |
| 206 | +# |
| 207 | + |
| 208 | + |
| 209 | +def baudot_bit(pitch=bit_1, duration=0.022): # spec says 20ms, but adjusted as needed |
| 210 | + dac.play(pitch, loop=True) |
| 211 | + time.sleep(duration) |
| 212 | + # dac.stop() |
| 213 | + |
| 214 | + |
| 215 | +def baudot_carrier(duration=0.15): # Carrier tone is transmitted for 150 ms before the |
| 216 | + # first character is transmitted |
| 217 | + baudot_bit(carrier, duration) |
| 218 | + dac.stop() |
| 219 | + |
| 220 | + |
| 221 | +def baudot_start(): |
| 222 | + baudot_bit(bit_0) |
| 223 | + |
| 224 | + |
| 225 | +def baudot_stop(): |
| 226 | + baudot_bit(bit_1, 0.04) # minimum duration is 30ms |
| 227 | + dac.stop() |
| 228 | + |
| 229 | + |
| 230 | +def send_character(value): |
| 231 | + baudot_carrier() # send carrier tone |
| 232 | + baudot_start() # send start bit tone |
| 233 | + for i in range(5): # send each bit of the character |
| 234 | + bit = (value >> i) & 0x01 # bit shift and bit mask to get value of each bit |
| 235 | + baudot_bit(tones[bit]) # send each bit, either 0 or 1, of a character |
| 236 | + baudot_stop() # send stop bit |
| 237 | + baudot_carrier() # not to spec, but works better to extend carrier |
| 238 | + |
| 239 | + |
| 240 | +def send_message(text): |
| 241 | + global char_count, current_mode # pylint: disable=global-statement |
| 242 | + for char in text: |
| 243 | + if char not in LTRS and char not in FIGS: # just skip unknown characters |
| 244 | + print("Unknown character:", char) |
| 245 | + continue |
| 246 | + |
| 247 | + if char not in current_mode: # switch mode |
| 248 | + if current_mode == LTRS: |
| 249 | + print("Switching mode to FIGS") |
| 250 | + current_mode = FIGS |
| 251 | + send_character(current_mode.index("FIGS")) |
| 252 | + elif current_mode == FIGS: |
| 253 | + print("Switching mode to LTRS") |
| 254 | + current_mode = LTRS |
| 255 | + send_character(current_mode.index("LTRS")) |
| 256 | + # Send char mode at beginning of message and every 72 characters |
| 257 | + if char_count >= 72 or char_count == 0: |
| 258 | + print("Resending mode") |
| 259 | + if current_mode == LTRS: |
| 260 | + send_character(current_mode.index("LTRS")) |
| 261 | + elif current_mode == FIGS: |
| 262 | + send_character(current_mode.index("FIGS")) |
| 263 | + # reset counter |
| 264 | + char_count = 0 |
| 265 | + print(char) |
| 266 | + send_character(current_mode.index(char)) |
| 267 | + time.sleep(char_pause) |
| 268 | + # increment counter |
| 269 | + char_count += 1 |
| 270 | + |
| 271 | + |
| 272 | +message_pick = 0 |
| 273 | + |
| 274 | +while True: |
| 275 | + if clue.button_a: |
| 276 | + message_pick = (message_pick + 1) % 4 # loop through the lines |
| 277 | + dot.y = dot_y[message_pick] |
| 278 | + time.sleep(0.4) # debounce |
| 279 | + |
| 280 | + if clue.button_b: |
| 281 | + dot.fill = VFD_GREEN |
| 282 | + send_message(messages[message_pick]) |
| 283 | + dot.fill = VFD_BG |
0 commit comments