A CircuitPython IRC bot for Metro ESP32-S3 to show status notifications on a small dedicated screen.
This uses a 2.8" TFT display shield to show the topic of an IRC channel in large text. Combined with a local Raspberry Pi hosted IRC server, you can use this to make a wireless status notification system for environmental sensors or whatever. Once you put the wifi and IRC server details in your settings.toml, the display will connect automatically. Then you can use any IRC bot or client you like to set the channel topic to whatever should be shown on the display. For example, I built this to work with my serial-sensor-hub project which forwards USB serial sensor reports from my lora-greenhouse-monitor sensors.
Set Up Raspberry Pi OS with IRC Server
If you want to host your own IRC server on Raspberry Pi, this explains how. If you don't care about that, you can skip down to the Updating CircuitPython section.
First you need to set up a Raspberry Pi with Raspberry Pi OS. I prefer to use the Raspberry Pi OS Lite version and to enable the SSH server so I can access it remotely, but it doesn't really matter. You do you. Any vaguely recent model of Raspberry Pi should work fine as ngircd will run on a potato. I'm using a Raspberry Pi 3 Model B.
Once you have the Raspberry Pi up and running, you'll need to install and configure the ircd IRC server. Don't worry. It's easy to set up.
Official Pi OS Install Method
- Follow the Raspberry Pi Foundation Getting Started instructions at https://www.raspberrypi.com/documentation/computers/getting-started.html to prepare a bootable disk image for Raspberry Pi OS
- Continue following the instructions to set up a user account and network connection.
- Enable SSH remote terminal access: https://www.raspberrypi.com/documentation/computers/remote-access.html#ssh
Old School CLI Raspberry Pi OS Install Method
If like me you prefer to use macOS commandline tools (dd ...) rather than the
Raspberry Pi Imager app, you might find it useful to know that the macOS Finder
can expand xz archives. Just open the archive file in the Finder.
Verify the SHA256 hash:
shasum -a 256 ~/Downloads/2025-10-01-raspios-trixie-arm64-lite.img.xz
Check for the correct disk file number for your sd card (be careful!):
diskutil list
...
Unmount the drive for your SD card (e.g. assume you found it was /dev/disk5):
diskutil unmountDisk /dev/disk5
Flash the disk image with dd (DANGER! double check drive number!):
sudo dd if=2025-10-01-raspios-trixie-arm64-lite.img of=/dev/rdisk5 bs=1M; sync
If you do things according to the official instructions you can just use the
GUI config stuff. But, if you prefer to use the Lite OS version and CLI tools,
You'll need to run sudo raspi-config to configure your Pi.
sudo raspi-config
In raspi-config:
- System Options > Wireless LAN > set wifi country, SSID, and password
- Interface Options > SSH > enable ssh
- Localisation Options > Locales > select your locale, but also select
en_US.UTF-8to avoid trouble with software that assumesen_USis present - Localisation Options > Timezone > set your timezone
- Advanced Options > Expand Filesystem
- Advanced Options > Logging > select Volatile (to reduce SD card wear)
- Advanced Options > WLAN Power Save > select No (improve wifi reliability)
Running a local IRC server on your home network will work better if your
Raspberry Pi has an IP address that doesn't change. For typical home wifi
routers that assign IP addresses with DHCP, it's usually possible to log into
the router admin console and configure IP address assignments. For example, my
router's DHCP assigns my computer a 192.168.0.x IP address with the gateway
192.168.0.1 and the router admin console is at http://192.168.0.1.
CAUTION: If you're unfamiliar with configuring routers, tread carefully. It's possible to get yourself into trouble. You might want to seek help from an experienced friend.
Wifi routers usually have a feature for assigning a static IP address to a connected device. The name of the feature and the means of enabling it may vary by router brand and model. For example, my router has a page for Connected Devices, and each device has an "EDIT" button. If you click EDIT, it gives you several choices including an option to pick between "DHCP" and "Reserved IP". For me, picking the "Reserved IP" option for a Raspberry Pi device causes the router to always assign it the same IP.
You might need to know the MAC address of your Raspberry Pi's Wifi interface. You can check that from a terminal on the Raspberry Pi with the command:
ip address
So far, the instructions have been for a Pi with a keyboard and HDMI display. To log into the Pi remotely by SSH, or to connect to its IRC server (once you set one up), you will need to know the Pi's IP address. To check the IP address, do:
ip address
Look for something like 191.168.0.100. Probably the fourth number will be
something other than 100, but the first three might be the same.
It's easier to log in remotely by SSH if you generate a key pair on your normal computer and install a copy of the public key from that pair on the Pi.
For example:
-
On macOS, generate a keypair (or skip the first bit to use existing keys) and print a copy of the public key (
.ssh/id_ed25519.pub):ssh-keygen -C macbook -t ed25519 cat ~/.ssh/id_ed25519.pub
If you select the option to protect your private key with a password, which is a good idea to do, you might want to configure SSH to store that password in your macOS login keychain. You can do that by editing
~/.ssh/configon the mac to add the following lines:Host * UseKeychain yes
Once you do that, you will probably be prompted for the private key's password once, when you first try to use it. After that first time, the login keychain should unlock the private key for you automatically.
-
On macOS, log into your Pi by SSH using password authentication using the IP address and username for your Pi. For example:
ssh [email protected]
-
Once you're in a Pi terminal using SSH, create a
.sshdirectory on the PI, then edit.ssh/authorized_keysand paste in your public key(s):mkdir -p ~/.ssh chmod 700 ~/.ssh nano ~/.ssh/authorized_keys # paste your mac's public key, then exit and save the file
-
On macOS, start a new Terminal window and try connecting to the Pi by SSH using your SSH keypair:
ssh [email protected]
This may ask you for the password to unlock your private key the first time out. But, after that, assuming you set up
UseKeychain yes, the key should unlock automatically. [Optional] to make it even easier, you can add a line to your
/etc/hostsfile on the mac (e.g.192.168.0.100 pi3b), then you could do something likessh pi@pi3b. CAUTION: Tread carefully withsudo nano /etc/hostsas editing mistakes there can cause trouble.
This all assumes you're logged into the Pi over SSH or at its keyboard. Some of these steps are optional aesthetic tweaks to modify default behaviors that I personally find irritating. Maybe you don't care. In that case, feel free to skip those steps.
-
Get updates and install useful packages including irssi and ngircd. When you install ngircd, you will have a working IRC server with default settings, bound to all your IP addresses (localhost and wifi):
sudo apt update sudo apt upgrade sudo apt install figlet tio screen git vim irssi ngircd
-
(Optional) Replace annoying default SSH and ngircd motd login messages with a figlet hostname banner
figlet -f small $(hostname) | sudo tee /etc/motd sudo sed -i 's/^uname/#uname/' /etc/update-motd.d/10-uname figlet -f small "Welcome to $(hostname)" | sudo tee /etc/ngircd/ngircd.motd sudo systemctl restart ngircd
-
(Optional) Configure the ngircd server to have a persistent
#sensorschannel, to have an operator user, and to disable some unnecessary default features.I've found these changes to be convenient when working on bots and bringing up a new sensor network installation with irc-display-bot, serial-sensor-hub, and a regular
irssichat window. Without the config changes, channel op status can get weird depending on which clients connect and disconnect in what order. The options stuff is just to make the protocol less chatty so my terminal doesn't fill up with useless log messages while working on bots.First, begin editing the ngircd config file with:
sudo nano /etc/ngircd/ngircd.conf
Scroll down to the end of the file, paste this stuff at the bottom, edit the operator name and password to whatever you like, then save the file and exit nano:
[Channel] Name = #sensors Modes = n [Operator] Name = admin Password = notMyRealPassword [Options] DNS = no Ident = no MorePrivacy = yes ScrubCTCP = yes
Check that you didn't make any typos by running:
ngircd --configtest
If configtest didn't complain about anything, then restart ngircd:
sudo systemctl restart ngircd
-
(Optional) Make systemd services to turn off the activity and power LEDs
You can turn off the Raspberry Pi LEDs by echoing "none" into
/sys/class/leds/ACT/triggerand/sys/class/leds/PWR/trigger, but the effect doesn't persist across a reboot. To make the change permanent, you can make a simple systemd service to run a shell command at boot. I like this so I don't get distracted by things blinking at me in my peripheral vision. Maybe you like the blinkenlights though. Your choice.First, create a new systemd service file:
sudo nano /etc/systemd/system/disable-leds.service
Paste this stuff into the file, then save it:
[Unit] Description=Disable ACT LED After=network.target [Service] Type=oneshot ExecStart=/bin/sh -c 'echo none > /sys/class/leds/ACT/trigger; \ echo none > /sys/class/leds/PWR/trigger' [Install] WantedBy=multi-user.targetFinally, enable the new service:
sudo systemctl enable disable-leds.service
Updating CircuitPython
As I write this (November 14, 2025), CircuitPython 10.0.3 is the most recent build. To keep your board up to date, you can use the "DOWNLOAD .BIN NOW" button on the download page of circuitpython.org:
- Metro ESP32-S3 page
To install the .BIN File:
- Connect your board to a computer with a USB data cable (charge-only cables won't work! )
- Press and hold the board's boot button
- Press and release the reset button
- Release the boot button
- Follow the instructions in the Web Serial ESPTool section of the "CircuitPython on ESP32 Quick Start" learn guide to update your board with CircuitPython (I've had the best luck with using Adafruit WebSerial ESPTool to erase the whole flash then program the .BIN file)
CircuitPython Code
You can view the code at the samblenny/irc-display-bot GitHub repository. To download a zip archive project bundle with the code and all the necessary libraries, use the "Download Project Bundle" button:
Install & Setup
You will need to:
- Update your Metro ESP32-S3 to CircuitPython 10.0.3 *BEFORE* attaching the TFT display so that you can reach the Boot button
- Attach the TFT display
- Get the project bundle from the release page and copy its code and library files from the 10.x directory to your board's `CIRCUITPY` drive
- MOST IMPORTANTLY: You need to put your wifi credentials and IRC server details in settings.toml. Without filling in settings, it won't work!
Example settings.toml
Your settings.toml file should have the following environment variables, but you'll need to edit their values to match your wifi network and IRC server. The connection code is built for easy setup on a local Raspberry Pi over private wifi. So, it doesn't do TLS, and it doesn't do passwords. The port is hardcoded to 6667, but you can edit the code to change it. If you want to use this with my serial-sensor-bot code, you can pick the channel. Just make sure they're both configured to use the same one.
WIFI_SSID = "your-wifi-ssid"
WIFI_PASSWORD = "your-wifi-password"
IRC_SERVER = "192.168.0.200"
IRC_NICK = "tftbot"
IRC_CHAN = "#sensors"
Install Project Bundle
To copy the project bundle files to your CIRCUITPY drive:
- Download the project bundle .zip file using the "Download Project Bundle" button above.
- Expand the zip file by opening it, or use unzip in a Terminal. The zip archive should expand to a folder. When you open the folder, it should contain a README.txt file and a CircuitPython 10.x folder.
- Open the CircuitPython 10.x folder and copy all of its contents to your CIRCUITPY drive.
To learn more about copying libraries to your CIRCUITPY drive, check out the CircuitPython Libraries section of the Welcome to CircuitPython! learn guide.
# SPDX-License-Identifier: MIT
# SPDX-FileCopyrightText: Copyright 2025 Sam Blenny
#
# See NOTES.md for documentation links and pinout info.
#
import board
from microcontroller import cpu
from micropython import const
import os
import time
import wifi
from sb_chardisplay import CharDisplay
from sb_ircbot import IRCBot
# ---------------------------------------------------------------------------
# Options for settings.toml
# - `WIFI_SSID = "your ssid"` set the SSID to use for your wifi
# - `WIFI_PASSWORD = "the password"` set password for your wifi
# - `IRC_SERVER = "<some IP address>"` set IP address for your IRC server
# - `IRC_NICK = "<nickname>"` set nickname to use for your IRC server
#
WIFI_SSID = None
if (val := os.getenv("WIFI_SSID")) is not None:
WIFI_SSID = str(val)
WIFI_PASSWORD = None
if (val := os.getenv("WIFI_PASSWORD")) is not None:
WIFI_PASSWORD = str(val)
IRC_SERVER = None
if (val := os.getenv("IRC_SERVER")) is not None:
IRC_SERVER = str(val)
IRC_NICK = None
if (val := os.getenv("IRC_NICK")) is not None:
IRC_NICK = str(val)
IRC_CHAN = None
if (val := os.getenv("IRC_CHAN")) is not None:
IRC_CHAN = str(val)
# ---------------------------------------------------------------------------
def print_settings_banner():
# Print startup banner showing if settings.toml is missing something
heading = '# === IRC Dashboard Gadget Settings ==='
m = 'missing'
print()
print(heading)
print('# WIFI_SSID: [%s]' % (m if WIFI_SSID is None else 'ok'))
print('# WIFI_PASSWORD: [%s]' % (m if WIFI_PASSWORD is None else 'ok'))
print('# IRC_SERVER: [%s]' % (m if IRC_SERVER is None else 'ok'))
print('# IRC_NICK: [%s]' % (m if IRC_NICK is None else 'ok'))
print('# IRC_CHAN: [%s]' % (m if IRC_CHAN is None else 'ok'))
print('# ' + ('=' * (len(heading)-2)))
def wifi_connect():
# Try to connect to wifi
if WIFI_SSID and WIFI_PASSWORD:
try:
wifi.radio.connect(ssid=WIFI_SSID, password=WIFI_PASSWORD)
ip = wifi.radio.ipv4_address
return ip
except ConnectionError as e:
print(e)
return None
def run():
# This has the main program logic. Putting these things inside a function
# helps force me to avoid spaghetti code. When lots of stuff lives in the
# global namespace, it's too easy to write functions that unintentionally
# reference global variables.
print_settings_banner()
cd = CharDisplay()
# Reduce CPU frequency so the board runs cooler. The default ESP32-S3
# default frequency is 240 MHz. To avoid messing up time.monotonic(), don't
# attempt to set this below 80 MHz.
if 'esp32s3' in board.board_id:
cpu.frequency = 80_000_000
# Wrap the radio stuff in a big retry loop
RETRY_S = const(5)
RETRY_S_MAX = const(180)
WIFI_RETRY = const(20)
READ_TIME = const(0.8) # give me time to read the screen
irc_retry = RETRY_S
while True:
# Ensure Wifi is up
if not wifi.radio.connected:
while True:
cd.set_status("WiFi Connect...")
time.sleep(READ_TIME)
ip = wifi_connect()
if ip:
cd.set_status('WiFi IP is %s' % ip)
time.sleep(READ_TIME)
break
else:
# Wifi problem: sleep for a bit then try again
cd.set_status('Wifi Down')
time.sleep(WIFI_RETRY)
continue
else:
cd.set_status("WiFi already connected")
time.sleep(READ_TIME)
# Ensure IRC is up, then start responding to IRC messages
cd.set_status("IRC Connect...")
time.sleep(READ_TIME)
radio = wifi.radio
with IRCBot(IRC_NICK, IRC_CHAN, IRC_SERVER, port=6667) as irc:
while radio.connected and not irc.connected:
# If initial connect failed, retry with exponential backoff
time.sleep(irc_retry)
irc_retry = min(RETRY_S_MAX, irc_retry * 2)
cd.set_status('IRC Connect Retry...')
time.sleep(READ_TIME)
irc.connect()
irc.register()
# Main IRC bot loop
last_rx = time.monotonic()
PING_TIMEOUT = const(200)
while radio.connected and irc.connected:
line = irc.recv_line()
if line is None:
if time.monotonic() - last_rx > PING_TIMEOUT:
# Something's wrong. It's too quiet. Close & reconnect
cd.set_status('IRC: connection timeout')
irc.close()
time.sleep(irc_retry)
irc_retry = min(RETRY_S_MAX, irc_retry * 2)
break
time.sleep(0.010)
continue
last_rx = time.monotonic()
(prefix, cmd, params) = line
print(cmd, params)
if cmd == '433':
# - 433 * <nick> :Nickname already in use
cd.set_status('IRC: Nick in use')
irc.close()
time.sleep(irc_retry)
irc_retry = min(RETRY_S_MAX, irc_retry * 2)
break
elif cmd == 'PING':
irc.pong(params)
elif cmd == 'JOIN':
my_nick = ':%s!' % IRC_NICK
if prefix and prefix.find(my_nick) == 0:
# Set status line to channel name when I join. Ignore
# join notifications about other users.
cd.set_status(params[1:]) # slice off leading ':'
# SUCCESS: Joined OK, so reset the retry interval
irc_retry = RETRY_S
elif cmd == '332':
# Channel topic notification for JOIN
# Typical cmd+params format: `332 tftbot #sensors :!pre /`
nickchan = '%s %s :' % (IRC_NICK, IRC_CHAN)
pre = '!pre '
if not params.startswith(nickchan):
continue
text = params[len(nickchan):]
if text.startswith(pre):
# bot mode for displaying preformatted text with
# dynamically slectable line delimeters: first char of
# first word after the `!pre` is the delimeter that
# gets replaced with line breaks
text = text[len(pre):]
if len(text) >= 1:
delim = text[0]
text = text[1:].replace(delim, '\n')
cd.set_topic(text, wrap='pre')
else:
# Default to hard wrapping lines
cd.set_topic(text, wrap='hard')
elif cmd == 'TOPIC':
# Channel topic notifiction after JOIN
# Typical cmd+param format: `TOPIC #sensors :!pre /...`
chan = '%s :' % IRC_CHAN
pre = '!pre '
if not params.startswith(chan):
continue
text = params[len(chan):]
if text.startswith(pre):
# bot mode for displaying preformatted text with
# dynamically slectable line delimeters: first char of
# first word after the `!pre` is the delimeter that
# gets replaced with line breaks
text = text[len(pre):]
if len(text) >= 1:
delim = text[0]
text = text[1:].replace(delim, '\n')
cd.set_topic(text, wrap='pre')
else:
# Default to hard wrapping lines
cd.set_topic(text, wrap='hard')
# ---
# Main entry point
# ---
run()
import displayio
import supervisor
import usb_hid
# Turn of features that can slow things down and present surprising outputs
displayio.release_displays()
usb_hid.disable()
# Disable status bar to stop the default USB serial escape sequence noise
try:
supervisor.status_bar.console = False
supervisor.status_bar.display = False
except Exception:
pass
# SPDX-License-Identifier: MIT
# SPDX-FileCopyrightText: Copyright 2025 Sam Blenny
#
# See NOTES.md for documentation links and pinout info.
#
import atexit
import board
import displayio
from fourwire import FourWire
import terminalio
from adafruit_display_text import label
from adafruit_ili9341 import ILI9341
class CharDisplay:
def __init__(self, width=16):
# Try to initialize 2.8" TFT display shield with ILI9341 chip
displayio.release_displays()
spi = board.SPI()
tft_cs = board.D10
tft_dc = board.D9
display_bus = FourWire(spi, command=tft_dc, chip_select=tft_cs)
display = ILI9341(display_bus, width=320, height=240,
rotation=180, auto_refresh=False)
group = displayio.Group()
display.root_group = group
display.refresh()
# System status textbox: scale=1, one line, very top of display
status = label.Label(font=terminalio.FONT, scale=1, color=0x80ef00)
status.anchor_point = (1, 0)
status.anchored_position = (316, 4)
status.line_spacing = 1.0 # default is 1.25
group.append(status)
# IRC topic textbox: scale=3, 6 lines, this is for IRC notifications
topic = label.Label(font=terminalio.FONT, scale=3, color=0xefef00)
topic.anchor_point = (0, 0)
topic.anchored_position = (8, 16)
topic.line_spacing = 1.0 # default is 1.25
group.append(topic)
# Set an atexit handler to release the display once code.py
# ends. This is an aesthetic filter to prevent CircuitPython's
# supervisor from hijacking the display to show its own stuff.
def atexit_shutdown_display():
try:
status.text = 'OFFLINE'
display.refresh()
displayio.release_displays()
except AttributeError:
pass
atexit.register(atexit_shutdown_display)
self.display = display
self.status = status
self.topic = topic
self.width = width
def hard_wrap(self, text):
w = self.width
return '\n'.join([text[i:i+w] for i in range(0, len(text), w)])
def word_wrap(self, text):
# Format a string to fit on a narrow screen by word-wrapping at spaces
w = self.width
lines = []
start = 0
prev_space = 0
for _ in range(len(text)):
space = text.find(' ', start)
if len(text) - start <= w:
# All the remaining text fits in one line without breaking
lines.append(text[start:])
start = len(text)
break
elif space == -1 or space - start > w:
# No space in the first width characters, so hard wrap
lines.append(text[start:start+w])
start += w
else:
# There's at least one space suitable for a line break
for _ in range(len(text)):
next_space = text.find(' ', space + 1)
if next_space == -1:
break
elif next_space - start <= w + 1:
space = next_space
else:
break
lines.append(text[start:space])
start = space + 1
if start < len(text):
lines.append(text[start:])
return '\n'.join(lines)
def set_status(self, txt):
# Show the message text the top status line
print("status =", txt)
self.status.text = txt
self.display.refresh()
def set_topic(self, txt, wrap=None):
# Show the message text in the 6x16 character display IRC topic area
print("topic =", txt)
if wrap == 'hard':
wrapped = self.hard_wrap(txt)
elif wrap == 'word' or wrap is None:
wrapped = self.word_wrap(txt)
elif wrap == 'pre':
wrapped = txt
self.topic.text = wrapped
self.display.refresh()
# SPDX-License-Identifier: MIT
# SPDX-FileCopyrightText: Copyright 2025 Sam Blenny
#
# See NOTES.md for documentation links and pinout info.
#
import re
import socketpool
import wifi
class IRCBot:
def __init__(self, nick, chan, server, port=6667):
self.rx_buf = bytearray(512)
self.line_buf = bytearray(512)
self.pool = socketpool.SocketPool(wifi.radio)
self.sock = self.pool.socket() # defaults to IP + TCP
self.nick = nick
self.chan = chan
self.server = server
self.port = port
self.connected = False
self.registered = False
self.connect_timeout = 10
self._recv_line_iterator = None
self.irc_re = re.compile(
br'^((:\S+)\s+)?' # optional prefix (match group 2)
br'(\S+)\s*' # command or number (match group 3)
br'(.*)' # params (match group 4)
)
def close(self):
self.sock.close()
self.connected = False
self.registered = False
def connect(self):
try:
self.sock.settimeout(self.connect_timeout)
self.sock.connect((self.server, self.port))
self._recv_line_iterator = self._recv_line_gen()
self.connected = True
except OSError as e:
# Exceptions I've seen trigger this (bad port, bad IP, etc):
# - OSError: [Errno 104] ECONNRESET
# - OSError: [Errno 116] ETIMEDOUT
# - OSError: [Errno 118] EHOSTUNREACH
# - OSError: [Errno 120] EALREADY
# - OSError: [Errno 128] ENOTCONN
print('ERR connect: "%s", errno=%d', e, e.errno)
self.connected = False
def register(self, timeout=30):
if not self.connected:
return False
self.sock.settimeout(timeout)
try:
msg = (
'NICK {0}\r\n'
'USER {0} 0 * :{0}\r\n'
'JOIN {1}\r\n'
).format(self.nick, self.chan)
self.sock.sendall(msg.encode())
# This might fail, but for now assume it worked
self.registered = True
except OSError as e:
print('ERR register: "%s", errno=%d', e, e.errno)
return False
return True
def _recv_line_gen(self, timeout=1):
# This generator wraps Socket.recv_into() to provide line buffering
if not self.connected:
return None
self.sock.settimeout(timeout)
buf = self.rx_buf
line = self.line_buf
bufmv = memoryview(buf)
linemv = memoryview(line)
max_line = len(line)
buf_start = 0
line_end = 0
while True:
# Try to get some TCP bytes (maybe a full line, maybe less)
try:
size = self.sock.recv_into(buf)
except OSError as e:
# Exceptions I've seen trigger this:
# - OSError: [Errno 116] ETIMEDOUT
if e.errno == 116:
pass # timeout is fine
else:
print('ERR recv_into: "%s", errno=%d', e, e.errno)
# ---
yield None # no data yet -> yield nothing
# ---
continue
# BEGIN full line parsing
# Assume there may be multiple CRLF terminated lines in rx buffer
# and that line buffer may already have some accumulated data...
while (crlf := buf.find(b'\r\n', buf_start, size)) > -1:
line_buf_space = max_line - line_end
line_len = crlf - buf_start
# Got a CRLF line ending
if line_len < line_buf_space:
# Got newline and line will fit in the line buffer
line[line_end:line_end+line_len] = bufmv[buf_start:crlf]
line_end += line_len
else:
# Got newline but line is too long, so truncate it
limit = min(0, max_line - line_end)
line[line_end:max_line] = bufmv[buf_start:buf_start+limit]
line_end = max_line
# ---
yield bytes(linemv[:line_end]).decode() # Yield line
# ---
line_end = 0 # Clear line buffer
buf_start = crlf + 2 # Adjust rx buffer pointer
if buf_start >= size:
# RX buffer is empty, so start over with new packet
buf_start = 0
continue
# END full line parsing
# BEGIN partial line parsing
line_buf_space = max_line - line_end
part_len = size - buf_start
if part_len < line_buf_space:
# Partial line fits in line buffer -> copy it
line[line_end:line_end+part_len] = bufmv[buf_start:size]
line_end += part_len
else:
# Partial line is too long -> truncate it
limit = min(0, max_line - line_end)
line[line_end:max_line] = bufmv[buf_start:buf_start+limit]
line_end += limit
# Clear the RX buffer
buf_start = 0
# END partial line parsing
def recv_line(self):
# Return a line from the TCP stream buffer (includes auto-PONG)
#
# CAUTION!!! group(n) returns matches as strings, not type bytes.
# If you want to use them as bytes, you'll need to do .encode().
#
it = self._recv_line_iterator
if it and (line := next(it)):
# Got a line, so parse it with regex match
m = self.irc_re.match(line)
prefix = m.group(2)
command = m.group(3)
params = m.group(4)
return (prefix, command, params)
return None
def pong(self, params):
# Send a PONG to keep the connection alive (params type is string)
try:
msg = 'PONG {}\r\n'.format(params)
self.sock.sendall(msg.encode())
except OSError as e:
print('ERR pong: "%s", errno=%d', e, e.errno)
# Context handlers
def __enter__(self):
self.connect()
return self
def __exit__(self, exc_type, exc_value, traceback):
self.close()
return False # don't suppress exceptions
This page (IRC Display Bot) was last updated on December 07, 2025.
Text editor powered by tinymce.