-
Notifications
You must be signed in to change notification settings - Fork 5
/
plate.py
366 lines (328 loc) · 10.8 KB
/
plate.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
"""
Michael duPont - [email protected]
plate.py - Display ICAO METAR weather data with a Raspberry Pi and Adafruit LCD plate
Use plate keypad to select ICAO station/airport ident to display METAR data
Left/Right - Choose position
Up/Down - Choose character A-9
Select - Confirm station ident
LCD panel displays current METAR data
Line1 - IDEN HHMMZ FTRL
Line2 - Rest of METAR report
LCD backlight indicates current Flight Rules
Green - VFR
Blue - MVFR
Red - IFR
Violet - LIFR
While on the main display
Holding select button displays ident selection screen
While on the main and station selection display
Holding left and right buttons gives option to quit or shutdown the Pi
Uses Adafruit RGB Negative 16x2 LCD - https://www.adafruit.com/product/1110
"""
# stdlib
import os
import sys
from time import sleep
from typing import Callable, List, Tuple
# library
import avwx
import Adafruit_CharLCD as LCD
# module
import common
import config as cfg
from common import IDENT_CHARS, logger
# String replacement for Line2 (scrolling data)
replacements = [
["00000KT", "CALM"],
["00000MPS", "CALM"],
["10SM", "UNLM"],
["9999", "UNLM"],
]
FR_COLORS = {
"VFR": (0, 255, 0),
"MVFR": (0, 0, 255),
"IFR": (255, 0, 0),
"LIFR": (255, 0, 255),
}
Coord = Tuple[int, int]
class METARPlate:
"""
Controls LCD plate display and buttons
"""
metar: avwx.Metar
ident: List[str]
lcd: LCD.Adafruit_CharLCDPlate
cols: int = 16
rows: int = 2
def __init__(self, station: str, size: Coord = None):
logger.debug("Running init")
try:
self.metar = avwx.Metar(station)
except avwx.exceptions.BadStation:
self.metar = avwx.Metar("KJFK")
self.ident = common.station_to_ident(station)
if size:
self.cols, self.rows = size
self.lcd = LCD.Adafruit_CharLCDPlate(cols=self.cols, lines=self.rows)
self.clear()
@property
def station(self) -> str:
"""
The current station
"""
return common.ident_to_station(self.ident)
@classmethod
def from_session(cls, session: dict):
"""
Returns a new Screen from a saved session
"""
station = session.get("station", "KJFK")
return cls(station)
def export_session(self, save: bool = True):
"""
Saves or returns a dictionary representing the session's state
"""
session = {"station": self.station}
if save:
common.save_session(session)
return session
@property
def pressed_select(self):
return self.lcd.is_pressed(LCD.SELECT)
@property
def pressed_shutdown(self):
return self.lcd.is_pressed(LCD.LEFT) and self.lcd.is_pressed(LCD.RIGHT)
def clear(self, reset_backlight: bool = True):
"""
Resets the display and backlight color
"""
if reset_backlight:
self.lcd.set_backlight(1)
self.lcd.clear()
self.lcd.set_cursor(0, 0)
def __handle_select(self):
"""
Select METAR station
Use LCD to update 'ident' values
"""
cursorPos = 0
selected = False
self.clear()
self.lcd.message("4-Digit METAR")
# Display default ident
for row in range(4):
self.lcd.set_cursor(row, 1)
self.lcd.message(IDENT_CHARS[self.ident[row]])
self.lcd.set_cursor(0, 1)
self.lcd.show_cursor(True)
# Allow finger to be lifted from select button
sleep(1)
# Selection loop
while not selected:
# Shutdown option
if self.pressed_shutdown:
self.lcd_shutdown()
self.lcd_select()
return
# Previous char
elif self.lcd.is_pressed(LCD.UP):
curNum = self.ident[cursorPos]
if curNum == 0:
curNum = len(IDENT_CHARS)
self.ident[cursorPos] = curNum - 1
self.lcd.message(IDENT_CHARS[self.ident[cursorPos]])
# Next char
elif self.lcd.is_pressed(LCD.DOWN):
newNum = self.ident[cursorPos] + 1
if newNum == len(IDENT_CHARS):
newNum = 0
self.ident[cursorPos] = newNum
self.lcd.message(IDENT_CHARS[self.ident[cursorPos]])
# Move cursor right
elif self.lcd.is_pressed(LCD.RIGHT):
if cursorPos < 3:
cursorPos += 1
# Move cursor left
elif self.lcd.is_pressed(LCD.LEFT):
if cursorPos > 0:
cursorPos -= 1
# Confirm ident
elif self.pressed_select:
selected = True
self.lcd.set_cursor(cursorPos, 1)
sleep(cfg.button_interval)
self.lcd.show_cursor(0)
def lcd_select(self):
"""
Display METAR selection screen on LCD
"""
self.lcd.set_backlight(1)
self.__handle_select()
self.export_session()
self.metar = avwx.Metar(self.station)
self.clear()
self.lcd.message(f"{self.station} selected")
def lcd_timeout(self):
"""
Display timeout message and sleep
"""
logger.warning("Connection Timeout")
self.clear(True)
self.lcd.message("No connection\nCheck back soon")
sleep(cfg.timeout_interval)
def lcd_bad_station(self):
self.clear()
self.lcd.message(f"No Weather Data\nFor {self.station}")
sleep(3)
self.lcd_select()
def lcd_shutdown(self):
"""
Display shutdown options
"""
selection = False
selected = False
self.clear()
msg = "Shutdown the Pi" if cfg.shutdown_on_exit else "Quit the program"
self.lcd.message(f"{msg}?\nY N")
self.lcd.set_cursor(2, 1)
self.lcd.show_cursor(True)
# Allow finger to be lifted from LR buttons
sleep(1)
# Selection loop
while not selected:
# Move cursor right
if self.lcd.is_pressed(LCD.RIGHT) and selection:
self.lcd.set_cursor(2, 1)
selection = False
# Move cursor left
elif self.lcd.is_pressed(LCD.LEFT) and not selection:
self.lcd.set_cursor(0, 1)
selection = True
# Confirm selection
elif self.pressed_select:
selected = True
sleep(cfg.button_interval)
self.lcd.show_cursor(False)
if not selection:
return None
self.clear(False)
self.lcd.set_backlight(0)
if cfg.shutdown_on_exit:
os.system("shutdown -h now")
sys.exit()
def create_display_data(self):
"""
Returns tuple of display data
Line1: IDEN HHMMZ FTRL
Line2: Rest of METAR report
BLInt: Flight rules backlight color
"""
data = self.metar.data
line1 = f"{data.station} {data.time.repr[2:]} {data.flight_rules}"
line2 = data.raw.split(" ", 2)[-1]
if not cfg.include_remarks:
line2 = line2.replace(data.remarks, "").strip()
for src, rep in replacements:
line2 = line2.replace(src, rep).strip()
return line1, line2, FR_COLORS.get(data.flight_rules)
def __scroll_button_check(self) -> bool:
"""
Handles any pressed buttons during main display
"""
# Go to selection screen if select button pressed
if self.pressed_select:
self.lcd_select()
return True
# Go to shutdown screen if left and right buttons pressed
elif self.pressed_shutdown:
self.lcd_shutdown()
return True
return False
@staticmethod
def __sleep_with_input(
up_to: int, handler: Callable, step: float = cfg.button_interval
) -> float:
"""
Sleep for a certain amount while checking an input handler
Returns the elapsed time or None if interrupted
"""
elapsed = 0
for _ in range(int(up_to / step)):
sleep(step)
elapsed += step
if handler():
return
return elapsed
def scroll_line(
self, line: str, handler: Callable, row: int = 1
) -> Tuple[float, bool]:
"""
Scroll a line on the display
Must be given a function to handle button presses
Returns approximate time elapsed and main refresh boolean
"""
elapsed = 0
if len(line) <= self.cols:
self.lcd.set_cursor(0, row)
self.lcd.message(line)
else:
self.lcd.set_cursor(0, row)
self.lcd.message(line[: self.cols])
try:
elapsed += self.__sleep_with_input(2, handler)
except TypeError:
return elapsed, True
for i in range(1, len(line) - (self.cols - 1)):
self.lcd.set_cursor(0, row)
self.lcd.message(line[i : i + self.cols])
sleep(cfg.scroll_interval)
elapsed += cfg.scroll_interval
if handler():
return elapsed, True
try:
elapsed += self.__sleep_with_input(2, handler)
except TypeError:
return elapsed, True
return elapsed, False
def update_metar(self) -> bool:
"""
Update the METAR data and handle any errors
"""
try:
self.metar.update()
except avwx.exceptions.BadStation:
self.lcd_bad_station()
except ConnectionError:
self.lcd_timeout()
except:
logger.exception("Report Update Error")
return False
return True
def lcd_main(self):
"""
Display data until the elapsed time exceeds the update interval
"""
line1, line2, color = self.create_display_data()
logger.info("\t%s\n\t%s", line1, line2)
self.clear()
# Set LCD color to match current flight rules
self.lcd.set_color(*color) if color else self.lcd.set_backlight(1)
elapsed = 0
self.lcd.message(line1)
# Scroll line2 until update interval exceeded
while elapsed < cfg.update_interval:
step, refresh = self.scroll_line(line2, handler=self.__scroll_button_check)
if refresh:
return
elapsed += step
def main() -> int:
logger.debug("Booting")
plate = METARPlate.from_session(common.load_session())
while True:
if not plate.update_metar():
return 1
logger.info(plate.metar.raw)
plate.lcd_main()
return 0
if __name__ == "__main__":
main()