Compare commits
7 commits
c5510041a3
...
e44ae3524b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e44ae3524b | ||
|
|
f8c2b61fea | ||
|
|
f5f0805802 | ||
|
|
b036cc6252 | ||
|
|
d22c263f6f | ||
|
|
895120fd8c | ||
|
|
989db55caf |
54 changed files with 928 additions and 107 deletions
BIN
Fruit_Jam/Fruit_Jam_IRC_Client/beep.wav
Normal file
BIN
Fruit_Jam/Fruit_Jam_IRC_Client/beep.wav
Normal file
Binary file not shown.
75
Fruit_Jam/Fruit_Jam_IRC_Client/code.py
Normal file
75
Fruit_Jam/Fruit_Jam_IRC_Client/code.py
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
# SPDX-FileCopyrightText: 2025 Tim Cocks for Adafruit Industries
|
||||
# SPDX-License-Identifier: MIT
|
||||
from os import getenv
|
||||
from displayio import Group
|
||||
from terminalio import FONT
|
||||
import supervisor
|
||||
import audiocore
|
||||
import board
|
||||
import busio
|
||||
from digitalio import DigitalInOut
|
||||
from adafruit_esp32spi import adafruit_esp32spi
|
||||
from adafruit_color_terminal import ColorTerminal
|
||||
from adafruit_fruitjam.peripherals import Peripherals
|
||||
|
||||
from curses_irc_client import run_irc_client
|
||||
|
||||
# Configuration - modify these values as needed
|
||||
IRC_CONFIG = {
|
||||
"server": "irc.libera.chat", # Example: irc.libera.chat, irc.freenode.net
|
||||
# "port": 6667, # 6667 - clear text
|
||||
"port": 6697, # 6697 - TLS encrypted
|
||||
"username": "",
|
||||
"channel": "#adafruit-fruit-jam",
|
||||
}
|
||||
|
||||
if IRC_CONFIG["username"] == "":
|
||||
raise ValueError("username must be set in IRC_CONFIG")
|
||||
|
||||
main_group = Group()
|
||||
display = supervisor.runtime.display
|
||||
|
||||
font_bb = FONT.get_bounding_box()
|
||||
screen_size = (display.width // font_bb[0], display.height // font_bb[1])
|
||||
|
||||
terminal = ColorTerminal(FONT, screen_size[0], screen_size[1])
|
||||
main_group.append(terminal.tilegrid)
|
||||
display.root_group = main_group
|
||||
|
||||
# Get WiFi details and Adafruit IO keys, ensure these are setup in settings.toml
|
||||
# (visit io.adafruit.com if you need to create an account, or if you need your Adafruit IO key.)
|
||||
ssid = getenv("CIRCUITPY_WIFI_SSID")
|
||||
password = getenv("CIRCUITPY_WIFI_PASSWORD")
|
||||
|
||||
# If you are using a board with pre-defined ESP32 Pins:
|
||||
esp32_cs = DigitalInOut(board.ESP_CS)
|
||||
esp32_ready = DigitalInOut(board.ESP_BUSY)
|
||||
esp32_reset = DigitalInOut(board.ESP_RESET)
|
||||
|
||||
spi = busio.SPI(board.SCK, board.MOSI, board.MISO)
|
||||
esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset)
|
||||
|
||||
print("Connecting to AP...")
|
||||
while not esp.is_connected:
|
||||
try:
|
||||
esp.connect_AP(ssid, password)
|
||||
except RuntimeError as e:
|
||||
print("could not connect to AP, retrying: ", e)
|
||||
continue
|
||||
|
||||
print("IRC Configuration:")
|
||||
print(f"Server: {IRC_CONFIG['server']}:{IRC_CONFIG['port']}")
|
||||
print(f"Nickname: {IRC_CONFIG['username']}")
|
||||
print(f"Channel: {IRC_CONFIG['channel']}")
|
||||
print("-" * 40)
|
||||
|
||||
fruit_jam_peripherals = Peripherals()
|
||||
beep_wave = audiocore.WaveFile("beep.wav")
|
||||
run_irc_client(
|
||||
esp,
|
||||
IRC_CONFIG,
|
||||
terminal,
|
||||
terminal.tilegrid,
|
||||
audio_interface=fruit_jam_peripherals.audio,
|
||||
beep_wave=beep_wave,
|
||||
)
|
||||
249
Fruit_Jam/Fruit_Jam_IRC_Client/curses_irc_client.py
Normal file
249
Fruit_Jam/Fruit_Jam_IRC_Client/curses_irc_client.py
Normal file
|
|
@ -0,0 +1,249 @@
|
|||
# SPDX-FileCopyrightText: 2025 Tim Cocks for Adafruit Industries
|
||||
# SPDX-License-Identifier: MIT
|
||||
import time
|
||||
import adafruit_dang as curses
|
||||
|
||||
from irc_client import IRCClient
|
||||
|
||||
ANSI_BLACK_ON_GREY = chr(27) + "[30;100m"
|
||||
ANSI_RESET = chr(27) + "[0m"
|
||||
|
||||
|
||||
class Window:
|
||||
"""
|
||||
Terminal Window class that supports basic scrolling.
|
||||
"""
|
||||
|
||||
def __init__(self, n_rows, n_cols, row=0, col=0):
|
||||
self.n_rows = n_rows
|
||||
self.n_cols = n_cols
|
||||
self.row = row
|
||||
self.col = col
|
||||
|
||||
@property
|
||||
def bottom(self):
|
||||
return self.row + self.n_rows - 1
|
||||
|
||||
def up(self, cursor): # pylint: disable=invalid-name
|
||||
if cursor.row == self.row - 1 and self.row > 0:
|
||||
self.row -= 1
|
||||
|
||||
def down(self, buffer, cursor):
|
||||
if cursor.row == self.bottom + 1 and self.bottom < len(buffer) - 1:
|
||||
self.row += 1
|
||||
|
||||
def horizontal_scroll(self, cursor, left_margin=5, right_margin=2):
|
||||
n_pages = cursor.col // (self.n_cols - right_margin)
|
||||
self.col = max(n_pages * self.n_cols - right_margin - left_margin, 0)
|
||||
|
||||
def translate(self, cursor):
|
||||
return cursor.row - self.row, cursor.col - self.col
|
||||
|
||||
|
||||
def irc_client_main(
|
||||
stdscr,
|
||||
radio,
|
||||
irc_config,
|
||||
terminal_tilegrid=None,
|
||||
audio_interface=None,
|
||||
beep_wave=None,
|
||||
):
|
||||
# pylint: disable=too-many-locals, too-many-branches, too-many-statements
|
||||
"""
|
||||
Main curses IRC client application loop.
|
||||
"""
|
||||
irc_client = IRCClient(
|
||||
radio, irc_config, audio_interface=audio_interface, beep_wave=beep_wave
|
||||
)
|
||||
irc_client.connect()
|
||||
# irc_client.join()
|
||||
|
||||
window = Window(terminal_tilegrid.height, terminal_tilegrid.width)
|
||||
stdscr.erase()
|
||||
img = [None] * window.n_rows
|
||||
status_bar = {
|
||||
"user_message": None,
|
||||
"user_message_shown_time": 0,
|
||||
}
|
||||
|
||||
cur_row_index = 0
|
||||
|
||||
user_input = ""
|
||||
|
||||
def show_user_message(message):
|
||||
"""
|
||||
Show a status message to the user
|
||||
"""
|
||||
status_bar["user_message"] = message + (
|
||||
" " * (window.n_cols - 1 - len(message))
|
||||
)
|
||||
status_bar["user_message_shown_time"] = time.monotonic()
|
||||
|
||||
def setline(row, line):
|
||||
"""
|
||||
Set a line of text in the terminal window.
|
||||
"""
|
||||
if img[row] == line:
|
||||
return
|
||||
img[row] = line
|
||||
line += " " * (window.n_cols - len(line) - 1)
|
||||
stdscr.addstr(row, 0, line)
|
||||
|
||||
def get_page(row_index):
|
||||
"""
|
||||
Get a page of messages from the message buffer.
|
||||
"""
|
||||
page_len = window.n_rows - 2
|
||||
|
||||
page_start = max((len(irc_client.message_buffer) + row_index) - page_len, 0)
|
||||
page_end = page_start + page_len
|
||||
|
||||
page = irc_client.message_buffer[page_start:page_end]
|
||||
return page
|
||||
|
||||
# pylint: disable=too-many-nested-blocks
|
||||
try:
|
||||
# main application loop
|
||||
while True:
|
||||
lastrow = 0
|
||||
lines_added = irc_client.update()
|
||||
|
||||
cur_page = get_page(cur_row_index)
|
||||
|
||||
if lines_added > 0 and len(cur_page) < window.n_rows - 2:
|
||||
cur_row_index = max(cur_row_index - lines_added, 0)
|
||||
cur_page = get_page(cur_row_index)
|
||||
|
||||
for row, line in enumerate(cur_page):
|
||||
lastrow = row
|
||||
setline(row, line)
|
||||
|
||||
for row in range(lastrow + 1, window.n_rows - 2):
|
||||
setline(row, "")
|
||||
|
||||
user_input_row = window.n_rows - 2
|
||||
if user_input:
|
||||
setline(user_input_row, user_input)
|
||||
else:
|
||||
setline(user_input_row, " " * (window.n_cols - 1))
|
||||
|
||||
user_message_row = terminal_tilegrid.height - 1
|
||||
if status_bar["user_message"] is None:
|
||||
message = f" {irc_config['username']} | {irc_config['server']} | {irc_config['channel']}" # pylint: disable=line-too-long
|
||||
message += " " * (terminal_tilegrid.width - len(message) - 1)
|
||||
line = f"{ANSI_BLACK_ON_GREY}{message}{ANSI_RESET}"
|
||||
else:
|
||||
line = f"{ANSI_BLACK_ON_GREY}{status_bar['user_message']}{ANSI_RESET}"
|
||||
if status_bar["user_message_shown_time"] + 3.0 < time.monotonic():
|
||||
status_bar["user_message"] = None
|
||||
setline(user_message_row, line)
|
||||
|
||||
# read from the keyboard
|
||||
k = stdscr.getkey()
|
||||
if k is not None:
|
||||
if len(k) == 1 and " " <= k <= "~":
|
||||
user_input += k
|
||||
|
||||
elif k == "\n": # enter key pressed
|
||||
if not user_input.startswith("/"):
|
||||
print(f"sending: {user_input}")
|
||||
irc_client.send_message(user_input)
|
||||
user_input = ""
|
||||
else: # slash commands
|
||||
parts = user_input.split(" ", 1)
|
||||
if parts[0] in {"/j", "/join"}:
|
||||
if len(parts) >= 2 and parts[1] != "":
|
||||
if parts[1] != irc_client.config["channel"]:
|
||||
irc_client.join(parts[1])
|
||||
user_input = ""
|
||||
else:
|
||||
show_user_message("Already in channel")
|
||||
user_input = ""
|
||||
|
||||
else:
|
||||
show_user_message(
|
||||
"Invalid /join arg. Use: /join <channel>"
|
||||
)
|
||||
user_input = ""
|
||||
elif parts[0] == "/msg":
|
||||
to_user, message_to_send = parts[1].split(" ", 1)
|
||||
irc_client.send_dm(to_user, message_to_send)
|
||||
user_input = ""
|
||||
elif parts[0] == "/beep":
|
||||
to_user = parts[1]
|
||||
message_to_send = "*Beep*\x07"
|
||||
irc_client.send_dm(to_user, message_to_send)
|
||||
user_input = ""
|
||||
elif parts[0] == "/op":
|
||||
user_to_op = parts[1]
|
||||
irc_client.op(user_to_op)
|
||||
user_input = ""
|
||||
elif parts[0] == "/deop":
|
||||
user_to_op = parts[1]
|
||||
irc_client.deop(user_to_op)
|
||||
user_input = ""
|
||||
elif parts[0] == "/kick":
|
||||
user_to_kick = parts[1]
|
||||
irc_client.kick(user_to_kick)
|
||||
user_input = ""
|
||||
elif parts[0] == "/ban":
|
||||
user_to_ban = parts[1]
|
||||
irc_client.ban(user_to_ban)
|
||||
user_input = ""
|
||||
elif parts[0] == "/unban":
|
||||
user_to_unban = parts[1]
|
||||
irc_client.unban(user_to_unban)
|
||||
user_input = ""
|
||||
elif parts[0] == "/whois":
|
||||
user_to_check = parts[1]
|
||||
irc_client.whois(user_to_check)
|
||||
user_input = ""
|
||||
|
||||
elif k in ("KEY_BACKSPACE", "\x7f", "\x08"):
|
||||
user_input = user_input[:-1]
|
||||
elif k == "KEY_UP":
|
||||
page_len = window.n_rows - 2
|
||||
if len(irc_client.message_buffer) > page_len:
|
||||
page_start = (
|
||||
len(irc_client.message_buffer) + cur_row_index
|
||||
) - page_len
|
||||
if page_start > 0:
|
||||
cur_row_index -= 1
|
||||
elif k == "KEY_DOWN":
|
||||
if cur_row_index < 0:
|
||||
cur_row_index += 1
|
||||
|
||||
elif k == "KEY_PGUP":
|
||||
page_len = window.n_rows - 2
|
||||
if len(irc_client.message_buffer) > page_len:
|
||||
page_start = (
|
||||
len(irc_client.message_buffer) + cur_row_index
|
||||
) - page_len
|
||||
if page_start > 0:
|
||||
cur_row_index -= 6
|
||||
elif k == "KEY_PGDN":
|
||||
if cur_row_index <= 0:
|
||||
cur_row_index = cur_row_index + 6
|
||||
else:
|
||||
print(f"unknown key: {k}")
|
||||
|
||||
except KeyboardInterrupt as exc:
|
||||
irc_client.disconnect()
|
||||
raise KeyboardInterrupt from exc
|
||||
|
||||
|
||||
def run_irc_client(
|
||||
radio, irc_config, terminal, terminal_tilegrid, audio_interface=None, beep_wave=None
|
||||
):
|
||||
"""
|
||||
Entry point to run the curses IRC client application.
|
||||
"""
|
||||
return curses.custom_terminal_wrapper(
|
||||
terminal,
|
||||
irc_client_main,
|
||||
radio,
|
||||
irc_config,
|
||||
terminal_tilegrid,
|
||||
audio_interface,
|
||||
beep_wave,
|
||||
)
|
||||
BIN
Fruit_Jam/Fruit_Jam_IRC_Client/icon.bmp
Normal file
BIN
Fruit_Jam/Fruit_Jam_IRC_Client/icon.bmp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.2 KiB |
493
Fruit_Jam/Fruit_Jam_IRC_Client/irc_client.py
Normal file
493
Fruit_Jam/Fruit_Jam_IRC_Client/irc_client.py
Normal file
|
|
@ -0,0 +1,493 @@
|
|||
# SPDX-FileCopyrightText: 2025 Tim Cocks for Adafruit Industries
|
||||
# SPDX-License-Identifier: MIT
|
||||
import time
|
||||
|
||||
import adafruit_connection_manager
|
||||
|
||||
ANSI_ESCAPE_CODES = [
|
||||
chr(27) + "[30m",
|
||||
chr(27) + "[31m",
|
||||
chr(27) + "[32m",
|
||||
chr(27) + "[33m",
|
||||
chr(27) + "[34m",
|
||||
chr(27) + "[35m",
|
||||
chr(27) + "[36m",
|
||||
]
|
||||
ANSI_RESET = chr(27) + "[0m"
|
||||
|
||||
|
||||
class IRCClient:
|
||||
"""
|
||||
Handles interaction with IRC Server and makes incoming messages available.
|
||||
|
||||
:param radio: The network radio to connect with.
|
||||
:param dict irc_config: Dictionary containing IRC configration for
|
||||
server, port, username and channel.
|
||||
:param audio_interface: Optional interface to play audio from for beep messages
|
||||
:param beep_wave: Optional wave file to use for beep messages
|
||||
:param int max_line_length: Maximum characters per line to format messages into.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
radio,
|
||||
irc_config,
|
||||
audio_interface=None,
|
||||
beep_wave=None,
|
||||
max_line_length=120,
|
||||
):
|
||||
self.radio = radio
|
||||
self.config = irc_config
|
||||
required = {"username", "server", "channel"}
|
||||
for key in required:
|
||||
if key not in self.config:
|
||||
raise ValueError(
|
||||
f"missing required config key. Required keys are: {required}"
|
||||
)
|
||||
|
||||
if "port" not in self.config:
|
||||
self.config["port"] = 6667
|
||||
if "timeout" not in self.config:
|
||||
self.config["timeout"] = 120
|
||||
|
||||
self.pool = adafruit_connection_manager.get_radio_socketpool(radio)
|
||||
self.connection_manager = adafruit_connection_manager.get_connection_manager(
|
||||
self.pool
|
||||
)
|
||||
|
||||
ssl_context = adafruit_connection_manager.get_radio_ssl_context(radio)
|
||||
print(f"Connecting to {self.config['server']}:{self.config['port']}...")
|
||||
self.socket = self.connection_manager.get_socket(
|
||||
self.config["server"],
|
||||
self.config["port"],
|
||||
"",
|
||||
timeout=0.01,
|
||||
is_ssl=True,
|
||||
ssl_context=ssl_context,
|
||||
)
|
||||
print("Connected")
|
||||
|
||||
# color to use for next unique username
|
||||
self.next_color_index = 4
|
||||
|
||||
# map of unique usernames to color
|
||||
self.user_color_map = {}
|
||||
|
||||
# buffer for incoming data until it's a full line
|
||||
self.line_buffer = ""
|
||||
|
||||
# buffer for full incoming chat messages
|
||||
self.message_buffer = []
|
||||
|
||||
# whether to show whois reply message on screen
|
||||
self.show_whois_reply = False
|
||||
|
||||
self.audio_interface = audio_interface
|
||||
if audio_interface is not None:
|
||||
self.beep_wave = beep_wave
|
||||
|
||||
self.max_line_length = max_line_length
|
||||
|
||||
def connect(self):
|
||||
"""
|
||||
Connect to IRC Server
|
||||
"""
|
||||
# Send nick and user info
|
||||
self.socket.send(f"NICK {self.config['username']}\r\n".encode("utf-8"))
|
||||
self.socket.send(
|
||||
f"USER {self.config['username']} 0 * :{self.config['username']}\r\n".encode(
|
||||
"utf-8"
|
||||
)
|
||||
)
|
||||
|
||||
def disconnect(self):
|
||||
"""
|
||||
Disconnect from IRC Server
|
||||
"""
|
||||
self.socket.send("QUIT :Goodbye\r\n".encode("utf-8"))
|
||||
self.socket.close()
|
||||
|
||||
def readlines(self):
|
||||
"""
|
||||
Read incoming data from the socket and return a list of lines read.
|
||||
"""
|
||||
lines = []
|
||||
# Receive data
|
||||
data = self.socket.recv(4096).decode("utf-8")
|
||||
if not data:
|
||||
raise RuntimeError("Connection closed by server")
|
||||
|
||||
self.line_buffer += data
|
||||
|
||||
# Process complete lines
|
||||
while "\r\n" in self.line_buffer:
|
||||
line, self.line_buffer = self.line_buffer.split("\r\n", 1)
|
||||
|
||||
if line:
|
||||
lines.append(line)
|
||||
|
||||
return lines
|
||||
|
||||
def update(self):
|
||||
"""
|
||||
Check for udpates from the server. Main loop of the program should call this.
|
||||
"""
|
||||
updated_display_lines = 0
|
||||
try:
|
||||
lines = self.readlines()
|
||||
for line in lines:
|
||||
updated_display_lines += self.process_message(line)
|
||||
|
||||
except OSError as e:
|
||||
# no data before timeout
|
||||
# print(e)
|
||||
if "ETIMEDOUT" not in str(e):
|
||||
raise RuntimeError(e) from e
|
||||
# raise RuntimeError("Connection timed out")
|
||||
return updated_display_lines
|
||||
|
||||
def send_message(self, message):
|
||||
"""
|
||||
Send a message to the channel that the user is in.
|
||||
"""
|
||||
irc_command = f"PRIVMSG {self.config['channel']} :{message}\r\n"
|
||||
self.socket.send(irc_command.encode("utf-8"))
|
||||
self.process_message(
|
||||
f":{self.config['username']}!~{self.config['username']}@localhost "
|
||||
+ irc_command[:-2]
|
||||
)
|
||||
|
||||
def send_dm(self, to_user, message):
|
||||
"""
|
||||
Send a direct message to a specified user.
|
||||
"""
|
||||
irc_command = f"PRIVMSG {to_user} :{message}\r\n"
|
||||
self.socket.send(irc_command.encode("utf-8"))
|
||||
color = self.get_color_for_user(to_user)
|
||||
self.message_buffer.append(f"DM out: <{color}{to_user}{ANSI_RESET}> {message}")
|
||||
|
||||
def op(self, user):
|
||||
"""
|
||||
Make specified user an operator in the channel that the user is in.
|
||||
You must already be an operator to grant operator privilege.
|
||||
"""
|
||||
op_cmd = f"MODE {self.config['channel']} +o {user}\r\n"
|
||||
self.socket.send(op_cmd.encode("utf-8"))
|
||||
|
||||
def deop(self, user):
|
||||
"""
|
||||
Remove operator privilege from the specified user for this channel.
|
||||
"""
|
||||
deop_cmd = f"MODE {self.config['channel']} -o {user}\r\n"
|
||||
self.socket.send(deop_cmd.encode("utf-8"))
|
||||
|
||||
def kick(self, user):
|
||||
"""
|
||||
Kick a specified user from the channel.
|
||||
"""
|
||||
kick_cmd = f"KICK {self.config['channel']} {user}\r\n"
|
||||
self.socket.send(kick_cmd.encode("utf-8"))
|
||||
|
||||
def get_technical_name(self, nickname):
|
||||
"""
|
||||
Get the full technical name of a user given a nickname
|
||||
"""
|
||||
start_time = time.monotonic()
|
||||
whois_cmd = f"WHOIS {nickname}\r\n"
|
||||
self.socket.send(whois_cmd.encode("utf-8"))
|
||||
|
||||
whois_resp_lines = None
|
||||
while whois_resp_lines is None and start_time + 3.0 > time.monotonic():
|
||||
try:
|
||||
whois_resp_lines = self.readlines()
|
||||
except OSError as e:
|
||||
if "ETIMEDOUT" in str(e):
|
||||
whois_resp_lines = None
|
||||
else:
|
||||
raise RuntimeError(e) from e
|
||||
|
||||
if whois_resp_lines is None:
|
||||
return None
|
||||
|
||||
for line in whois_resp_lines:
|
||||
line = line.lstrip("\0")
|
||||
parts = line.split(" ", 2)
|
||||
if len(parts) >= 2:
|
||||
command = parts[1]
|
||||
if command != "311":
|
||||
self.process_message(line)
|
||||
continue
|
||||
|
||||
whois_response = parts[2].split(" ", 1)[1]
|
||||
response_parts = whois_response.split(" ")
|
||||
technical_name = f"*!{response_parts[1]}@{response_parts[2]}"
|
||||
return technical_name
|
||||
|
||||
return None
|
||||
|
||||
def ban(self, user):
|
||||
"""
|
||||
Ban the specified user from the channel
|
||||
"""
|
||||
technical_name = self.get_technical_name(user)
|
||||
if technical_name is not None:
|
||||
ban_cmd = f"MODE {self.config['channel']} +b {technical_name}\r\n"
|
||||
self.socket.send(ban_cmd.encode("utf-8"))
|
||||
else:
|
||||
self.message_buffer.append(
|
||||
f"{ANSI_RESET} Error: failed whois lookup for ban"
|
||||
)
|
||||
|
||||
def unban(self, user):
|
||||
"""
|
||||
Unban the specified user from the channel
|
||||
"""
|
||||
technical_name = self.get_technical_name(user)
|
||||
if technical_name is not None:
|
||||
ban_cmd = f"MODE {self.config['channel']} -b {technical_name}\r\n"
|
||||
self.socket.send(ban_cmd.encode("utf-8"))
|
||||
else:
|
||||
self.message_buffer.append(
|
||||
f"{ANSI_RESET} Error: failed whois lookup for unban"
|
||||
)
|
||||
|
||||
def whois(self, user):
|
||||
"""
|
||||
Run a whois query on the specified user
|
||||
"""
|
||||
self.show_whois_reply = True
|
||||
whois_cmd = f"WHOIS {user}\r\n"
|
||||
self.socket.send(whois_cmd.encode("utf-8"))
|
||||
|
||||
def leave_channel(self):
|
||||
"""
|
||||
Leave the channel
|
||||
"""
|
||||
|
||||
self.socket.send(f"PART {self.config['channel']}\r\n".encode("utf-8"))
|
||||
|
||||
def join(self, new_channel=None):
|
||||
"""
|
||||
Join the specified channel. This will leave the prior channel.
|
||||
"""
|
||||
if new_channel is not None and new_channel != self.config["channel"]:
|
||||
self.leave_channel()
|
||||
self.config["channel"] = new_channel
|
||||
|
||||
print(f"Joining channel {self.config['channel']}...")
|
||||
self.socket.send(f"JOIN {self.config['channel']}\r\n".encode("utf-8"))
|
||||
self.message_buffer.append(f"{ANSI_RESET}* Joined {self.config['channel']} *")
|
||||
|
||||
def get_color_for_user(self, username):
|
||||
"""
|
||||
Get the color to use for the specified username
|
||||
"""
|
||||
if username not in self.user_color_map:
|
||||
self.user_color_map[username] = self.next_color_index
|
||||
self.next_color_index += 1
|
||||
if self.next_color_index > 6:
|
||||
self.next_color_index = 1
|
||||
|
||||
return ANSI_ESCAPE_CODES[self.user_color_map[username]]
|
||||
|
||||
@staticmethod
|
||||
def split_string_chunks(s, chunk_size):
|
||||
"""
|
||||
Split a string into chunks of specified size.
|
||||
"""
|
||||
chunks = []
|
||||
for i in range(0, len(s), chunk_size):
|
||||
chunks.append(s[i : i + chunk_size])
|
||||
return chunks
|
||||
|
||||
def process_message(self, message):
|
||||
# pylint: disable=too-many-branches, too-many-statements
|
||||
"""
|
||||
Process an incoming IRC message
|
||||
:param message: The message that came from the IRC server.
|
||||
|
||||
:return lines_added: The number of lines added to the display
|
||||
"""
|
||||
# pylint: disable=too-many-locals
|
||||
lines_added = 0
|
||||
|
||||
message = message.lstrip("\x00")
|
||||
print(f"RAW: {message.encode('utf-8')}")
|
||||
|
||||
# Handle PING messages (keep connection alive)
|
||||
if message.startswith("PING"):
|
||||
pong_response = message.replace("PING", "PONG")
|
||||
self.socket.send(f"{pong_response}\r\n".encode("utf-8"))
|
||||
print("Responded to PING")
|
||||
return 0
|
||||
|
||||
# Parse IRC message format: :prefix COMMAND params
|
||||
parts = message.split(" ", 2)
|
||||
# pylint: disable=too-many-nested-blocks
|
||||
if len(parts) >= 2:
|
||||
command = parts[1]
|
||||
try:
|
||||
command_num = int(command)
|
||||
except ValueError:
|
||||
command_num = None
|
||||
|
||||
# End of MOTD - now we can join the channel
|
||||
if command in {"376", "422"}: # 422 is "no MOTD"
|
||||
# join channel
|
||||
self.join()
|
||||
|
||||
# Welcome messages (001-004 are standard welcome messages)
|
||||
elif command in [
|
||||
"001",
|
||||
"002",
|
||||
"003",
|
||||
"004",
|
||||
"251",
|
||||
"252",
|
||||
"253",
|
||||
"254",
|
||||
"255",
|
||||
"265",
|
||||
"266",
|
||||
"375",
|
||||
"372",
|
||||
]:
|
||||
if len(parts) >= 3:
|
||||
welcome_text = parts[2]
|
||||
if welcome_text.startswith(":"):
|
||||
welcome_text = welcome_text[1:]
|
||||
|
||||
print(
|
||||
f"'{welcome_text[0:11]}' startswith '{self.config['username']}' ? {welcome_text.startswith(self.config['username'])}" # pylint: disable=line-too-long
|
||||
)
|
||||
if welcome_text.startswith(self.config["username"]):
|
||||
welcome_text = welcome_text.replace(
|
||||
self.config["username"], "", 1
|
||||
)
|
||||
# terminal.write(f"WELCOME: {welcome_text}\n")
|
||||
self.message_buffer.append(f"{welcome_text}")
|
||||
lines_added += 1
|
||||
print(f"WELCOME: {welcome_text}")
|
||||
|
||||
# Channel messages
|
||||
elif command == "PRIVMSG":
|
||||
if len(parts) >= 3:
|
||||
# Extract sender nickname
|
||||
sender = parts[0]
|
||||
if sender.startswith(":"):
|
||||
sender = sender[1:]
|
||||
if "!" in sender:
|
||||
sender = sender.split("!")[0]
|
||||
|
||||
# Extract message content
|
||||
message_content = parts[2]
|
||||
|
||||
inc_channel, inc_message = message_content.split(" ", 1)
|
||||
|
||||
message_content = inc_message[1:]
|
||||
|
||||
if "*beep*" in message_content:
|
||||
if (
|
||||
self.audio_interface is not None
|
||||
and not self.audio_interface.playing
|
||||
):
|
||||
print("playing beep")
|
||||
self.audio_interface.play(self.beep_wave)
|
||||
# print(f"is playing: {self.audio_interface.playing}")
|
||||
while self.audio_interface.playing:
|
||||
pass
|
||||
|
||||
print(f"message_content: {message_content.encode('utf-8')}")
|
||||
|
||||
color = self.get_color_for_user(sender)
|
||||
|
||||
if inc_channel == self.config["channel"]:
|
||||
full_line = f"<{color}{sender}{ANSI_RESET}> {message_content}"
|
||||
if len(full_line) < self.max_line_length:
|
||||
self.message_buffer.append(full_line)
|
||||
lines_added += 1
|
||||
else:
|
||||
chunks = self.split_string_chunks(
|
||||
full_line, self.max_line_length
|
||||
)
|
||||
for chunk in chunks:
|
||||
self.message_buffer.append(f"{ANSI_RESET}{chunk}")
|
||||
lines_added += 1
|
||||
elif inc_channel == self.config["username"]:
|
||||
self.message_buffer.append(
|
||||
f"DM in: <{color}{sender}{ANSI_RESET}> {message_content}"
|
||||
)
|
||||
lines_added += 1
|
||||
print(f"<{sender}> {message_content}")
|
||||
|
||||
# Join confirmations
|
||||
elif command == "JOIN":
|
||||
sender = parts[0]
|
||||
if sender.startswith(":"):
|
||||
sender = sender[1:]
|
||||
if "!" in sender:
|
||||
sender = sender.split("!")[0]
|
||||
|
||||
if len(parts) >= 3:
|
||||
joined_channel = parts[2]
|
||||
if joined_channel.startswith(":"):
|
||||
joined_channel = joined_channel[1:]
|
||||
print(f"*** {sender} joined {joined_channel}")
|
||||
|
||||
# error messages
|
||||
elif command_num is not None and 400 <= command_num <= 553:
|
||||
# message codes: https://www.alien.net.au/irc/irc2numerics.html
|
||||
self.message_buffer.append(f"{ANSI_RESET}{command} {parts[2]}")
|
||||
lines_added += 1
|
||||
|
||||
# whois reply
|
||||
elif self.show_whois_reply and command == "311":
|
||||
whois_response = parts[2].split(" ", 1)[1]
|
||||
self.message_buffer.append(f"{ANSI_RESET}{whois_response}")
|
||||
lines_added += 1
|
||||
self.show_whois_reply = False
|
||||
|
||||
# Mode messages
|
||||
elif command == "MODE":
|
||||
action_user = parts[0].split("!", 1)[0][1:]
|
||||
mode_msg_parts = parts[2].split(" ", 2)
|
||||
if len(mode_msg_parts) >= 3:
|
||||
channel, mode, target_user = ( # pylint: disable=unused-variable
|
||||
mode_msg_parts
|
||||
)
|
||||
action_user_color = self.get_color_for_user(action_user)
|
||||
target_user_color = self.get_color_for_user(target_user)
|
||||
self.message_buffer.append(
|
||||
f"{action_user_color}{action_user}{ANSI_RESET} sets mode {mode} on {target_user_color}{target_user}{ANSI_RESET}" # pylint: disable=line-too-long
|
||||
)
|
||||
lines_added += 1
|
||||
|
||||
# Part messages
|
||||
elif command == "PART":
|
||||
sender = parts[0]
|
||||
if sender.startswith(":"):
|
||||
sender = sender[1:]
|
||||
if "!" in sender:
|
||||
sender = sender.split("!")[0]
|
||||
|
||||
if len(parts) >= 3:
|
||||
left_channel = parts[2]
|
||||
print(f"*** {sender} left {left_channel}")
|
||||
|
||||
# Quit messages
|
||||
elif command == "QUIT":
|
||||
sender = parts[0]
|
||||
if sender.startswith(":"):
|
||||
sender = sender[1:]
|
||||
if "!" in sender:
|
||||
sender = sender.split("!")[0]
|
||||
|
||||
quit_message = ""
|
||||
if len(parts) >= 3:
|
||||
quit_message = parts[2]
|
||||
if quit_message.startswith(":"):
|
||||
quit_message = quit_message[1:]
|
||||
|
||||
print(f"*** {sender} quit ({quit_message})")
|
||||
|
||||
return lines_added
|
||||
4
Fruit_Jam/Fruit_Jam_IRC_Client/metadata.json
Normal file
4
Fruit_Jam/Fruit_Jam_IRC_Client/metadata.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"title": "IRC Client",
|
||||
"icon": "icon.bmp"
|
||||
}
|
||||
|
|
@ -56,7 +56,7 @@ icon2 = displayio.TileGrid(bitmap5, pixel_shader=palette5, x = 2, y = 2)
|
|||
group = displayio.Group()
|
||||
# adding start-up bitmap to group
|
||||
group.append(tile_grid)
|
||||
funhouse.splash.append(group)
|
||||
funhouse.graphics.root_group.append(group)
|
||||
# text for fume data
|
||||
fume_text = funhouse.add_text(
|
||||
text=" ",
|
||||
|
|
@ -74,7 +74,7 @@ fan_text = funhouse.add_text(
|
|||
text_font="fonts/Arial-Bold-24.pcf",
|
||||
)
|
||||
# showing graphics
|
||||
funhouse.display.root_group = funhouse.splash
|
||||
funhouse.display.root_group = funhouse.graphics.root_group
|
||||
|
||||
# state machines
|
||||
run = False # state if main code is running
|
||||
|
|
|
|||
|
|
@ -65,10 +65,10 @@ pres_label = funhouse.add_text(
|
|||
)
|
||||
|
||||
# Now display the splash to draw all labels at once
|
||||
funhouse.display.root_group = funhouse.splash
|
||||
funhouse.display.root_group = funhouse.graphics.root_group
|
||||
|
||||
status = Circle(229, 10, 10, fill=0xFF0000, outline=0x880000)
|
||||
funhouse.splash.append(status)
|
||||
funhouse.graphics.root_group.append(status)
|
||||
|
||||
def connected(client, _userdata, _result, _payload):
|
||||
status.fill = 0x00FF00
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ time_label = funhouse.add_text(
|
|||
text=trip_time, text_scale=2, text_position=(30, 25), text_color=0x606060
|
||||
)
|
||||
|
||||
funhouse.display.root_group = funhouse.splash
|
||||
funhouse.display.root_group = funhouse.graphics.root_group
|
||||
|
||||
# Turn on the light
|
||||
print("Turning on light...")
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ mail_label = funhouse.add_text(
|
|||
)
|
||||
reset_label = funhouse.add_text(text="reset", text_position=(3, 70), text_color=GRAY)
|
||||
|
||||
funhouse.display.root_group = funhouse.splash
|
||||
funhouse.display.root_group = funhouse.graphics.root_group
|
||||
|
||||
|
||||
def send_io_data(mail_value):
|
||||
|
|
|
|||
|
|
@ -82,10 +82,10 @@ countdown_label = funhouse.add_text(
|
|||
text_color=0xFFFF00,
|
||||
text_font="fonts/Arial-Bold-24.pcf",
|
||||
)
|
||||
funhouse.display.root_group = funhouse.splash
|
||||
funhouse.display.root_group = funhouse.graphics.root_group
|
||||
|
||||
status = Circle(229, 10, 10, fill=0xFF0000, outline=0x880000)
|
||||
funhouse.splash.append(status)
|
||||
funhouse.graphics.root_group.append(status)
|
||||
|
||||
# Initialize a new MQTT Client object
|
||||
if USE_MQTT:
|
||||
|
|
|
|||
|
|
@ -77,10 +77,10 @@ level_label = funhouse.add_text(
|
|||
text_color=0xFFFF00,
|
||||
text_font="fonts/Arial-Bold-24.pcf",
|
||||
)
|
||||
funhouse.display.root_group = funhouse.splash
|
||||
funhouse.display.root_group = funhouse.graphics.root_group
|
||||
|
||||
status = Circle(229, 10, 10, fill=0xFF0000, outline=0x880000)
|
||||
funhouse.splash.append(status)
|
||||
funhouse.graphics.root_group.append(status)
|
||||
|
||||
# Initialize a new MQTT Client object
|
||||
funhouse.network.init_mqtt(
|
||||
|
|
|
|||
|
|
@ -65,8 +65,8 @@ progress_bar_1 = ProgressBar(
|
|||
BAR_X, 95, BAR_WIDTH, BAR_HEIGHT, 1.0, bar_color=0x999999, outline_color=0x000000
|
||||
)
|
||||
|
||||
magtag.graphics.splash.append(progress_bar)
|
||||
magtag.graphics.splash.append(progress_bar_1)
|
||||
magtag.graphics.root_group.append(progress_bar)
|
||||
magtag.graphics.root_group.append(progress_bar_1)
|
||||
magtag.graphics.set_background("/bmps/background.bmp")
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ for i in range(list_len):
|
|||
|
||||
# Add button labels at the bottom of the screen
|
||||
BUTTON_TEXT_IDX = list_len
|
||||
magtag.graphics.splash.append(Rect(0, magtag.graphics.display.height - 14,
|
||||
magtag.graphics.root_group.append(Rect(0, magtag.graphics.display.height - 14,
|
||||
magtag.graphics.display.width,
|
||||
magtag.graphics.display.height, fill=0x0))
|
||||
magtag.add_text(
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ graphics = Graphics(auto_refresh=False)
|
|||
display = graphics.display
|
||||
|
||||
background = Rect(0, 0, 296, 128, fill=0xFFFFFF)
|
||||
graphics.splash.append(background)
|
||||
graphics.root_group.append(background)
|
||||
|
||||
label_overview_text = Label(
|
||||
font_large,
|
||||
|
|
@ -42,18 +42,18 @@ label_overview_text = Label(
|
|||
color=0x000000,
|
||||
text="Authorize this device with Google:",
|
||||
)
|
||||
graphics.splash.append(label_overview_text)
|
||||
graphics.root_group.append(label_overview_text)
|
||||
|
||||
label_verification_url = Label(font_small, x=0, y=40, line_spacing=0.75, color=0x000000)
|
||||
graphics.splash.append(label_verification_url)
|
||||
graphics.root_group.append(label_verification_url)
|
||||
|
||||
label_user_code = Label(font_small, x=0, y=80, color=0x000000, line_spacing=0.75)
|
||||
graphics.splash.append(label_user_code)
|
||||
graphics.root_group.append(label_user_code)
|
||||
|
||||
label_qr_code = Label(
|
||||
font_small, x=0, y=100, color=0x000000, text="Or scan the QR code:"
|
||||
)
|
||||
graphics.splash.append(label_qr_code)
|
||||
graphics.root_group.append(label_qr_code)
|
||||
|
||||
# Set scope(s) of access required by the API you're using
|
||||
scopes = ["https://www.googleapis.com/auth/calendar.readonly"]
|
||||
|
|
@ -85,7 +85,7 @@ label_verification_url.text = (
|
|||
label_user_code.text = "2. Enter code: %s" % google_auth.user_code
|
||||
|
||||
graphics.qrcode(google_auth.verification_url.encode(), qr_size=2, x=240, y=70)
|
||||
graphics.display.root_group = graphics.splash
|
||||
graphics.display.root_group = graphics.root_group
|
||||
display.refresh()
|
||||
|
||||
# Poll Google's authorization server
|
||||
|
|
@ -98,9 +98,9 @@ print("Add the following lines to your settings.toml file:")
|
|||
print(f'google_access_token="{google_auth.access_token}"')
|
||||
print(f'google_refresh_token="{google_auth.refresh_token}"')
|
||||
|
||||
graphics.splash.pop()
|
||||
graphics.splash.pop()
|
||||
graphics.splash.pop()
|
||||
graphics.root_group.pop()
|
||||
graphics.root_group.pop()
|
||||
graphics.root_group.pop()
|
||||
|
||||
label_overview_text.text = "Successfully Authenticated!"
|
||||
label_verification_url.text = (
|
||||
|
|
|
|||
|
|
@ -205,7 +205,7 @@ magtag.set_background(0xFFFFFF)
|
|||
|
||||
# Add the header
|
||||
line_header = Line(0, 30, 320, 30, color=0x000000)
|
||||
magtag.splash.append(line_header)
|
||||
magtag.graphics.root_group.append(line_header)
|
||||
|
||||
label_header = magtag.add_text(
|
||||
text_font="fonts/Arial-18.pcf",
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ MAGTAG.add_text(
|
|||
|
||||
# Add 14-pixel-tall black bar at bottom of display. It's a distinct layer
|
||||
# (not just background) to appear on top of name list if it runs long.
|
||||
MAGTAG.graphics.splash.append(Rect(0, MAGTAG.graphics.display.height - 14,
|
||||
MAGTAG.graphics.root_group.append(Rect(0, MAGTAG.graphics.display.height - 14,
|
||||
MAGTAG.graphics.display.width,
|
||||
MAGTAG.graphics.display.height, fill=0x0))
|
||||
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ MAGTAG.add_text(
|
|||
|
||||
# Add 14-pixel-tall black bar at bottom of display. It's a distinct layer
|
||||
# (not just background) to appear on top of task list if it runs long.
|
||||
MAGTAG.graphics.splash.append(Rect(0, MAGTAG.graphics.display.height - 14,
|
||||
MAGTAG.graphics.root_group.append(Rect(0, MAGTAG.graphics.display.height - 14,
|
||||
MAGTAG.graphics.display.width,
|
||||
MAGTAG.graphics.display.height, fill=0x0))
|
||||
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ progress_bar = ProgressBar(
|
|||
BAR_X, BAR_Y, BAR_WIDTH, BAR_HEIGHT, 1.0, bar_color=0x999999, outline_color=0x000000
|
||||
)
|
||||
|
||||
magtag.graphics.splash.append(progress_bar)
|
||||
magtag.graphics.root_group.append(progress_bar)
|
||||
|
||||
timestamp = None
|
||||
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ progress_bar = ProgressBar(
|
|||
BAR_X, BAR_Y, BAR_WIDTH, BAR_HEIGHT, 1.0, bar_color=0x999999, outline_color=0x000000
|
||||
)
|
||||
|
||||
magtag.graphics.splash.append(progress_bar)
|
||||
magtag.graphics.root_group.append(progress_bar)
|
||||
|
||||
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ MAGTAG.add_text(
|
|||
|
||||
# Add 14-pixel-tall black bar at bottom of display. It's a distinct layer
|
||||
# (not just background) to appear on top of produce list if it runs long.
|
||||
MAGTAG.graphics.splash.append(Rect(0, MAGTAG.graphics.display.height - 14,
|
||||
MAGTAG.graphics.root_group.append(Rect(0, MAGTAG.graphics.display.height - 14,
|
||||
MAGTAG.graphics.display.width,
|
||||
MAGTAG.graphics.display.height, fill=0x0))
|
||||
|
||||
|
|
|
|||
|
|
@ -130,12 +130,12 @@ station_info.anchored_position = (158, 126)
|
|||
# ----------------------------
|
||||
# Add all the graphic layers
|
||||
# ----------------------------
|
||||
magtag.splash.append(tide_tg)
|
||||
magtag.splash.append(grid_overlay)
|
||||
magtag.splash.append(plot_y_labels)
|
||||
magtag.splash.append(tide_info)
|
||||
magtag.splash.append(date_label)
|
||||
magtag.splash.append(station_info)
|
||||
magtag.graphics.root_group.append(tide_tg)
|
||||
magtag.graphics.root_group.append(grid_overlay)
|
||||
magtag.graphics.root_group.append(plot_y_labels)
|
||||
magtag.graphics.root_group.append(tide_info)
|
||||
magtag.graphics.root_group.append(date_label)
|
||||
magtag.graphics.root_group.append(station_info)
|
||||
|
||||
# /////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
|
|
|||
|
|
@ -344,9 +344,9 @@ future_banners = [
|
|||
make_banner(x=210, y=81),
|
||||
]
|
||||
|
||||
magtag.splash.append(today_banner)
|
||||
magtag.graphics.root_group.append(today_banner)
|
||||
for future_banner in future_banners:
|
||||
magtag.splash.append(future_banner)
|
||||
magtag.graphics.root_group.append(future_banner)
|
||||
|
||||
# ===========
|
||||
# M A I N
|
||||
|
|
|
|||
|
|
@ -283,9 +283,9 @@ future_banners = [
|
|||
make_banner(x=210, y=102),
|
||||
]
|
||||
|
||||
magtag.splash.append(today_banner)
|
||||
magtag.graphics.root_group.append(today_banner)
|
||||
for future_banner in future_banners:
|
||||
magtag.splash.append(future_banner)
|
||||
magtag.graphics.root_group.append(future_banner)
|
||||
|
||||
# ===========
|
||||
# M A I N
|
||||
|
|
|
|||
|
|
@ -280,9 +280,9 @@ future_banners = [
|
|||
make_banner(x=210, y=102),
|
||||
]
|
||||
|
||||
magtag.splash.append(today_banner)
|
||||
magtag.graphics.root_group.append(today_banner)
|
||||
for future_banner in future_banners:
|
||||
magtag.splash.append(future_banner)
|
||||
magtag.graphics.root_group.append(future_banner)
|
||||
|
||||
# ===========
|
||||
# M A I N
|
||||
|
|
|
|||
|
|
@ -136,8 +136,8 @@ def create_text_areas(configs):
|
|||
|
||||
|
||||
def clear_splash():
|
||||
for _ in range(len(pyportal.splash) - 1):
|
||||
pyportal.splash.pop()
|
||||
for _ in range(len(pyportal.root_group) - 1):
|
||||
pyportal.root_group.pop()
|
||||
|
||||
|
||||
def touch_in_button(t, b):
|
||||
|
|
@ -337,8 +337,8 @@ class Time_State(State):
|
|||
def enter(self):
|
||||
self.adjust_backlight_based_on_light(force=True)
|
||||
for ta in self.text_areas:
|
||||
pyportal.splash.append(ta)
|
||||
pyportal.splash.append(self.weather_icon)
|
||||
pyportal.root_group.append(ta)
|
||||
pyportal.root_group.append(self.weather_icon)
|
||||
if snooze_time:
|
||||
# CircuitPython 6 & 7 compatible
|
||||
if self.snooze_file:
|
||||
|
|
@ -348,7 +348,7 @@ class Time_State(State):
|
|||
icon_sprite = displayio.TileGrid(icon, pixel_shader=icon.pixel_shader)
|
||||
|
||||
self.snooze_icon.append(icon_sprite)
|
||||
pyportal.splash.append(self.snooze_icon)
|
||||
pyportal.root_group.append(self.snooze_icon)
|
||||
if alarm_enabled:
|
||||
self.text_areas[1].text = '%2d:%02d' % (alarm_hour, alarm_minute)
|
||||
else:
|
||||
|
|
@ -532,7 +532,7 @@ class Setting_State(State):
|
|||
|
||||
pyportal.set_background(self.background)
|
||||
for ta in self.text_areas:
|
||||
pyportal.splash.append(ta)
|
||||
pyportal.root_group.append(ta)
|
||||
if alarm_enabled:
|
||||
self.text_areas[0].text = '%02d:%02d' % (alarm_hour, alarm_minute) # set time textarea
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -54,6 +54,6 @@ while True:
|
|||
names_textarea.x = names_position[0]
|
||||
names_textarea.y = names_position[1]
|
||||
names_textarea.color = names_color
|
||||
pyportal.splash.append(names_textarea)
|
||||
pyportal.root_group.append(names_textarea)
|
||||
time.sleep(30) # wait 30 seconds to read it
|
||||
pyportal.splash.pop()
|
||||
pyportal.root_group.pop()
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ pyportal = PyPortal(default_bg=BACKGROUND_FILE,
|
|||
text_color=0xFFFFFF)
|
||||
|
||||
circle = Circle(WIDTH - 8, HEIGHT - 7, 5, fill=0)
|
||||
pyportal.splash.append(circle)
|
||||
pyportal.root_group.append(circle)
|
||||
loopcount = 0
|
||||
errorcount = 0
|
||||
while True:
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ for pos in (days_position, hours_position, minutes_position):
|
|||
textarea.x = pos[0]
|
||||
textarea.y = pos[1]
|
||||
textarea.color = text_color
|
||||
pyportal.splash.append(textarea)
|
||||
pyportal.root_group.append(textarea)
|
||||
text_areas.append(textarea)
|
||||
refresh_time = None
|
||||
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ pyportal = PyPortal(url=DATA_SOURCE,
|
|||
default_bg=0x000000)
|
||||
|
||||
|
||||
gfx = electioncal_graphics.Electioncal_Graphics(pyportal.splash, am_pm=True)
|
||||
gfx = electioncal_graphics.Electioncal_Graphics(pyportal.root_group, am_pm=True)
|
||||
display_refresh = None
|
||||
while True:
|
||||
# only query the online time once per hour (and on first run)
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ for pos in (days_position, hours_position, minutes_position):
|
|||
textarea.x = pos[0]
|
||||
textarea.y = pos[1]
|
||||
textarea.color = text_color
|
||||
pyportal.splash.append(textarea)
|
||||
pyportal.root_group.append(textarea)
|
||||
text_areas.append(textarea)
|
||||
refresh_time = None
|
||||
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ for pos in (years_position, days_position, hours_position, minutes_position):
|
|||
textarea.x = pos[0]
|
||||
textarea.y = pos[1]
|
||||
textarea.color = text_color
|
||||
pyportal.splash.append(textarea)
|
||||
pyportal.root_group.append(textarea)
|
||||
text_areas.append(textarea)
|
||||
refresh_time = None
|
||||
|
||||
|
|
|
|||
|
|
@ -26,16 +26,16 @@ font_large.load_glyphs(glyphs)
|
|||
label_overview_text = Label(
|
||||
font_large, x=0, y=45, text="To authorize this device with Google:"
|
||||
)
|
||||
graphics.splash.append(label_overview_text)
|
||||
graphics.root_group.append(label_overview_text)
|
||||
|
||||
label_verification_url = Label(font_small, x=0, y=100, line_spacing=1)
|
||||
graphics.splash.append(label_verification_url)
|
||||
graphics.root_group.append(label_verification_url)
|
||||
|
||||
label_user_code = Label(font_small, x=0, y=150)
|
||||
graphics.splash.append(label_user_code)
|
||||
graphics.root_group.append(label_user_code)
|
||||
|
||||
label_qr_code = Label(font_small, x=0, y=190, text="Or scan the QR code:")
|
||||
graphics.splash.append(label_qr_code)
|
||||
graphics.root_group.append(label_qr_code)
|
||||
|
||||
# Set scope(s) of access required by the API you're using
|
||||
scopes = ["https://www.googleapis.com/auth/calendar.readonly"]
|
||||
|
|
@ -71,7 +71,7 @@ label_user_code.text = "2. Enter code: %s" % google_auth.user_code
|
|||
|
||||
# Create a QR code
|
||||
graphics.qrcode(google_auth.verification_url.encode(), qr_size=2, x=170, y=165)
|
||||
graphics.display.root_group = graphics.splash
|
||||
graphics.display.root_group = graphics.root_group
|
||||
|
||||
# Poll Google's authorization server
|
||||
print("Waiting for browser authorization...")
|
||||
|
|
@ -85,9 +85,9 @@ print("Add the following lines to your settings.toml file:")
|
|||
print(f'GOOGLE_ACCESS_TOKEN = "{google_auth.access_token}"')
|
||||
print(f'GOOGLE_REFRESH_TOKEN = "{google_auth.refresh_token}"')
|
||||
# Remove QR code and code/verification labels
|
||||
graphics.splash.pop()
|
||||
graphics.splash.pop()
|
||||
graphics.splash.pop()
|
||||
graphics.root_group.pop()
|
||||
graphics.root_group.pop()
|
||||
graphics.root_group.pop()
|
||||
|
||||
label_overview_text.text = "Successfully Authenticated!"
|
||||
label_verification_url.text = (
|
||||
|
|
|
|||
|
|
@ -202,7 +202,7 @@ font_events = "fonts/Arial-14.pcf"
|
|||
|
||||
# Add the header
|
||||
line_header = Line(0, 50, 320, 50, color=0x000000)
|
||||
pyportal.splash.append(line_header)
|
||||
pyportal.root_group.append(line_header)
|
||||
|
||||
label_header = pyportal.add_text(
|
||||
text_font="fonts/Arial-18.pcf",
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ for peg in pegs:
|
|||
style=Button.RECT,
|
||||
fill_color=None, outline_color=0x5C3C15,
|
||||
name=peg['label'])
|
||||
pyportal.splash.append(button.group)
|
||||
pyportal.root_group.append(button.group)
|
||||
buttons.append(button)
|
||||
|
||||
note_select = None
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ countdown_text = Label(big_font)
|
|||
countdown_text.x = 25
|
||||
countdown_text.y = 120
|
||||
countdown_text.color = 0x7942a0
|
||||
pyportal.splash.append(countdown_text)
|
||||
pyportal.root_group.append(countdown_text)
|
||||
|
||||
refresh_time = None
|
||||
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ for i, c in enumerate(icons_pal):
|
|||
if c == 0xFFFF00:
|
||||
icons_pal.make_transparent(i)
|
||||
storm_icons = displayio.Group()
|
||||
pyportal.splash.append(storm_icons)
|
||||
pyportal.root_group.append(storm_icons)
|
||||
STORM_CLASS = ("TD", "TS", "HU")
|
||||
|
||||
# setup info label
|
||||
|
|
@ -53,7 +53,7 @@ info_update = Label(
|
|||
)
|
||||
info_update.anchor_point = (0.0, 1.0)
|
||||
info_update.anchored_position = (10, board.DISPLAY.height - 10)
|
||||
pyportal.splash.append(info_update)
|
||||
pyportal.root_group.append(info_update)
|
||||
|
||||
# these are need for lat/lon to screen x/y mapping
|
||||
VIRTUAL_WIDTH = board.DISPLAY.width * 360 / (LON_RANGE[1] - LON_RANGE[0])
|
||||
|
|
|
|||
|
|
@ -43,21 +43,21 @@ pyportal.get_local_time()
|
|||
# Date and time label
|
||||
date_label = Label(FONT, text="0000-00-00", color=DATE_COLOR, x=165, y=223)
|
||||
time_label = Label(FONT, text="00:00:00", color=TIME_COLOR, x=240, y=223)
|
||||
pyportal.splash.append(date_label)
|
||||
pyportal.splash.append(time_label)
|
||||
pyportal.root_group.append(date_label)
|
||||
pyportal.root_group.append(time_label)
|
||||
|
||||
# ISS trail
|
||||
trail_bitmap = displayio.Bitmap(3, 3, 1)
|
||||
trail_palette = displayio.Palette(1)
|
||||
trail_palette[0] = TRAIL_COLOR
|
||||
trail = displayio.Group()
|
||||
pyportal.splash.append(trail)
|
||||
pyportal.root_group.append(trail)
|
||||
|
||||
# ISS location marker
|
||||
marker = displayio.Group()
|
||||
for r in range(MARK_SIZE - MARK_THICKNESS, MARK_SIZE):
|
||||
marker.append(Circle(0, 0, r, outline=MARK_COLOR))
|
||||
pyportal.splash.append(marker)
|
||||
pyportal.root_group.append(marker)
|
||||
|
||||
def get_location(width=WIDTH, height=HEIGHT):
|
||||
"""Fetch current lat/lon, convert to (x, y) tuple scaled to width/height."""
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ display = board.DISPLAY
|
|||
display.rotation = 270
|
||||
|
||||
# instantiate the openweather_graphics class
|
||||
gfx = openweather_graphics.OpenWeather_Graphics(pyportal.splash, am_pm=True, celsius=False)
|
||||
gfx = openweather_graphics.OpenWeather_Graphics(pyportal.root_group, am_pm=True, celsius=False)
|
||||
|
||||
# time keeping for refreshing screen icons and weather information
|
||||
localtile_refresh = None
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ time_textarea = Label(big_font)
|
|||
time_textarea.x = 0
|
||||
time_textarea.y = 130
|
||||
time_textarea.color = 0xFF0000
|
||||
pyportal.splash.append(time_textarea)
|
||||
pyportal.root_group.append(time_textarea)
|
||||
|
||||
# To help us know if we've changed the times, print them out!
|
||||
gremlin_hour, gremlin_min = gremlin_time[3:5]
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ for spot in spots:
|
|||
style=Button.SHADOWROUNDRECT,
|
||||
fill_color=spot['color'], outline_color=0x222222,
|
||||
name=spot['label'])
|
||||
pyportal.splash.append(button)
|
||||
pyportal.root_group.append(button)
|
||||
buttons.append(button)
|
||||
|
||||
mode = 0
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ pyportal = PyPortal(url=DATA_SOURCE,
|
|||
status_neopixel=board.NEOPIXEL,
|
||||
default_bg=0x000000)
|
||||
|
||||
gfx = openweather_graphics.OpenWeather_Graphics(pyportal.splash, am_pm=True, celsius=False)
|
||||
gfx = openweather_graphics.OpenWeather_Graphics(pyportal.root_group, am_pm=True, celsius=False)
|
||||
|
||||
localtile_refresh = None
|
||||
weather_refresh = None
|
||||
|
|
|
|||
|
|
@ -68,13 +68,13 @@ font_small.load_glyphs(b'abcdefghjiklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123
|
|||
label_day = label.Label(font_large, color=LABEL_DAY_COLOR)
|
||||
label_day.x = board.DISPLAY.width // 7
|
||||
label_day.y = 80
|
||||
pyportal.splash.append(label_day)
|
||||
pyportal.root_group.append(label_day)
|
||||
|
||||
# Set up label for the time
|
||||
label_time = label.Label(font_small, color=LABEL_TIME_COLOR)
|
||||
label_time.x = board.DISPLAY.width // 4
|
||||
label_time.y = 150
|
||||
pyportal.splash.append(label_time)
|
||||
pyportal.root_group.append(label_time)
|
||||
|
||||
refresh_time = None
|
||||
while True:
|
||||
|
|
|
|||
|
|
@ -109,13 +109,13 @@ font_small.load_glyphs(b'abcdefghjiklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123
|
|||
label_month = label.Label(font_large, color=LABEL_DAY_COLOR)
|
||||
label_month.x = board.DISPLAY.width // 10
|
||||
label_month.y = 80
|
||||
pyportal.splash.append(label_month)
|
||||
pyportal.root_group.append(label_month)
|
||||
|
||||
# Set up label for the time
|
||||
label_time = label.Label(font_small, color=LABEL_TIME_COLOR)
|
||||
label_time.x = board.DISPLAY.width // 3
|
||||
label_time.y = 150
|
||||
pyportal.splash.append(label_time)
|
||||
pyportal.root_group.append(label_time)
|
||||
|
||||
refresh_time = None
|
||||
while True:
|
||||
|
|
|
|||
|
|
@ -104,11 +104,11 @@ class Switch(object):
|
|||
self.switch.direction = digitalio.Direction.OUTPUT
|
||||
rect = RoundRect(SWITCHX, SWITCHY, 31, 60, 16, outline=SWITCH_COLOR,
|
||||
fill=SWITCH_FILL_COLOR, stroke=3)
|
||||
my_pyportal.splash.append(rect)
|
||||
my_pyportal.root_group.append(rect)
|
||||
self.circle_on = Circle(SWITCHX + 15, SWITCHY + 16, 10, fill=SWITCH_FILL_COLOR)
|
||||
my_pyportal.splash.append(self.circle_on)
|
||||
my_pyportal.root_group.append(self.circle_on)
|
||||
self.circle_off = Circle(SWITCHX + 15, SWITCHY + 42, 10, fill=DISPLAY_COLOR)
|
||||
my_pyportal.splash.append(self.circle_off)
|
||||
my_pyportal.root_group.append(self.circle_off)
|
||||
|
||||
# turn switch on or off
|
||||
def enable(self, enable):
|
||||
|
|
@ -149,7 +149,7 @@ class Clock(object):
|
|||
self.text_areas = create_text_areas(text_area_configs)
|
||||
self.text_areas[2].text = "starting..."
|
||||
for ta in self.text_areas:
|
||||
self.pyportal.splash.append(ta)
|
||||
self.pyportal.root_group.append(ta)
|
||||
|
||||
def adjust_backlight(self, force=False):
|
||||
"""Check light level. Adjust the backlight and background image if it's dark."""
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ DATE_LABEL = Label(date_font, text="0000-00-00 00:00:00", color=DATE_COLOR, x=75
|
|||
|
||||
# Add all the labels to the display
|
||||
for label in HI_LABELS + LO_LABELS + [DATE_LABEL]:
|
||||
pyportal.splash.append(label)
|
||||
pyportal.root_group.append(label)
|
||||
|
||||
def get_tide_info():
|
||||
"""Fetch JSON tide time info and return it."""
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ palette.make_transparent(0)
|
|||
|
||||
# Setup tide plot bitmap
|
||||
tide_plot = displayio.Bitmap(WIDTH, HEIGHT, 3)
|
||||
pyportal.splash.append(displayio.TileGrid(tide_plot, pixel_shader=palette))
|
||||
pyportal.root_group.append(displayio.TileGrid(tide_plot, pixel_shader=palette))
|
||||
|
||||
# Setup font used for date and time
|
||||
date_font = bitmap_font.load_font(cwd+"/fonts/mono-bold-8.bdf")
|
||||
|
|
@ -75,7 +75,7 @@ date_font.load_glyphs(b'1234567890-')
|
|||
|
||||
# Setup date label
|
||||
date_label = Label(date_font, text="0000-00-00", color=DATE_COLOR, x=7, y=14)
|
||||
pyportal.splash.append(date_label)
|
||||
pyportal.root_group.append(date_label)
|
||||
|
||||
if board.board_id == "pyportal_titano":
|
||||
x_pos = 394
|
||||
|
|
@ -84,14 +84,14 @@ else:
|
|||
|
||||
# Setup time label
|
||||
time_label = Label(date_font, text="00:00:00", color=TIME_COLOR, x=x_pos, y=14)
|
||||
pyportal.splash.append(time_label)
|
||||
pyportal.root_group.append(time_label)
|
||||
|
||||
# Setup current time marker
|
||||
time_marker_bitmap = displayio.Bitmap(MARK_SIZE, MARK_SIZE, 3)
|
||||
for pixel in range(MARK_SIZE * MARK_SIZE):
|
||||
time_marker_bitmap[pixel] = 2
|
||||
time_marker = displayio.TileGrid(time_marker_bitmap, pixel_shader=palette, x=-MARK_SIZE, y=-MARK_SIZE)
|
||||
pyportal.splash.append(time_marker)
|
||||
pyportal.root_group.append(time_marker)
|
||||
|
||||
def get_tide_data():
|
||||
"""Fetch JSON tide data and return parsed results in a list."""
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ DATE_LABEL = Label(date_font, text="0000-00-00 00:00:00", color=DATE_COLOR, x=75
|
|||
|
||||
# Add all the labels to the display
|
||||
for label in HI_LABELS + LO_LABELS + [DATE_LABEL]:
|
||||
pyportal.graphics.splash.append(label)
|
||||
pyportal.graphics.root_group.append(label)
|
||||
|
||||
|
||||
def get_tide_info():
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ palette.make_transparent(0)
|
|||
|
||||
# Setup tide plot bitmap
|
||||
tide_plot = displayio.Bitmap(WIDTH, HEIGHT, 3)
|
||||
pyportal.graphics.splash.append(displayio.TileGrid(tide_plot, pixel_shader=palette))
|
||||
pyportal.graphics.root_group.append(displayio.TileGrid(tide_plot, pixel_shader=palette))
|
||||
|
||||
# Setup font used for date and time
|
||||
date_font = bitmap_font.load_font("/fonts/mono-bold-8.bdf")
|
||||
|
|
@ -55,11 +55,11 @@ date_font.load_glyphs(b"1234567890-")
|
|||
|
||||
# Setup date label
|
||||
date_label = Label(date_font, text="0000-00-00", color=DATE_COLOR, x=7, y=14)
|
||||
pyportal.graphics.splash.append(date_label)
|
||||
pyportal.graphics.root_group.append(date_label)
|
||||
|
||||
# Setup time label
|
||||
time_label = Label(date_font, text="00:00:00", color=TIME_COLOR, x=234, y=14)
|
||||
pyportal.graphics.splash.append(time_label)
|
||||
pyportal.graphics.root_group.append(time_label)
|
||||
|
||||
# Setup current time marker
|
||||
time_marker_bitmap = displayio.Bitmap(MARK_SIZE, MARK_SIZE, 3)
|
||||
|
|
@ -67,7 +67,7 @@ time_marker_bitmap.fill(2)
|
|||
time_marker = displayio.TileGrid(
|
||||
time_marker_bitmap, pixel_shader=palette, x=-MARK_SIZE, y=-MARK_SIZE
|
||||
)
|
||||
pyportal.graphics.splash.append(time_marker)
|
||||
pyportal.graphics.root_group.append(time_marker)
|
||||
|
||||
|
||||
def get_tide_data():
|
||||
|
|
|
|||
|
|
@ -133,7 +133,7 @@ alarm_checks = [None, alarms['bed'],alarms['breakfast'],alarms['lunch'],alarms['
|
|||
alarm_gfx = [group_trash, group_bed, group_eat, group_eat, group_eat]
|
||||
|
||||
# allows for the openweather_graphics to show
|
||||
gfx = openweather_graphics.OpenWeather_Graphics(pyportal.splash, am_pm=True, celsius=False)
|
||||
gfx = openweather_graphics.OpenWeather_Graphics(pyportal.root_group, am_pm=True, celsius=False)
|
||||
|
||||
# state machines
|
||||
localtile_refresh = None
|
||||
|
|
@ -250,13 +250,13 @@ while True:
|
|||
print("pressed dismiss button")
|
||||
dismissed = True
|
||||
alarm = False
|
||||
display.root_group = pyportal.splash
|
||||
display.root_group = pyportal.root_group
|
||||
touched = time.monotonic()
|
||||
mode = mode
|
||||
if not switch_snooze.value and not phys_snooze:
|
||||
phys_snooze = True
|
||||
print("pressed snooze button")
|
||||
display.root_group = pyportal.splash
|
||||
display.root_group = pyportal.root_group
|
||||
snoozed = True
|
||||
alarm = False
|
||||
touched = time.monotonic()
|
||||
|
|
@ -271,7 +271,7 @@ while True:
|
|||
if touch:
|
||||
if snooze_buttons[button_mode].contains(touch) and not touch_button_snooze:
|
||||
print("Touched snooze")
|
||||
display.root_group = pyportal.splash
|
||||
display.root_group = pyportal.root_group
|
||||
touch_button_snooze = True
|
||||
snoozed = True
|
||||
alarm = False
|
||||
|
|
@ -281,7 +281,7 @@ while True:
|
|||
print("Touched dismiss")
|
||||
dismissed = True
|
||||
alarm = False
|
||||
display.root_group = pyportal.splash
|
||||
display.root_group = pyportal.root_group
|
||||
touch_button_dismiss = True
|
||||
touched = time.monotonic()
|
||||
mode = mode
|
||||
|
|
|
|||
|
|
@ -121,12 +121,12 @@ pyportal = PyPortal(url=DATA_SOURCE,
|
|||
|
||||
pyportal.preload_font() # speed things up by preloading font
|
||||
|
||||
pyportal.splash.append(loading_text_area) #loading...
|
||||
pyportal.splash.append(q_text_area)
|
||||
pyportal.splash.append(reveal_text_area)
|
||||
pyportal.splash.append(timer_text_area)
|
||||
pyportal.root_group.append(loading_text_area) #loading...
|
||||
pyportal.root_group.append(q_text_area)
|
||||
pyportal.root_group.append(reveal_text_area)
|
||||
pyportal.root_group.append(timer_text_area)
|
||||
for textarea in ans_text_areas:
|
||||
pyportal.splash.append(textarea)
|
||||
pyportal.root_group.append(textarea)
|
||||
|
||||
while True:
|
||||
# Load new question when screen is touched
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ pyportal = PyPortal(url=url,
|
|||
caption_font=CAPTION_FONT_FILE)
|
||||
|
||||
canvas = displayio.Group()
|
||||
pyportal.splash.append(canvas)
|
||||
pyportal.root_group.append(canvas)
|
||||
bar_font = bitmap_font.load_font(BAR_FONT_FILE)
|
||||
|
||||
while True:
|
||||
|
|
|
|||
|
|
@ -84,9 +84,9 @@ light_on_time_position = (15,220)
|
|||
light_on_time_textarea = Label(info_font, color=light_on_time_color,
|
||||
x=light_on_time_position[0], y=light_on_time_position[1])
|
||||
|
||||
pyportal.splash.append(time_textarea)
|
||||
pyportal.splash.append(wakeup_time_textarea)
|
||||
pyportal.splash.append(light_on_time_textarea)
|
||||
pyportal.root_group.append(time_textarea)
|
||||
pyportal.root_group.append(wakeup_time_textarea)
|
||||
pyportal.root_group.append(light_on_time_textarea)
|
||||
|
||||
while True:
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ for pos in (days_position, hours_position, minutes_position):
|
|||
textarea.x = pos[0]
|
||||
textarea.y = pos[1]
|
||||
textarea.color = text_color
|
||||
pyportal.splash.append(textarea)
|
||||
pyportal.root_group.append(textarea)
|
||||
text_areas.append(textarea)
|
||||
refresh_time = None
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue