adafruit / circuitpython

CircuitPython - a Python implementation for teaching coding with microcontrollers
https://circuitpython.org
Other
4.07k stars 1.2k forks source link

CPY core module socketpool request add attributes to be able to receive udp multicast packets #7556

Closed PaulskPt closed 8 months ago

PaulskPt commented 1 year ago

Some years ago I wrote and used successfully a Python script for a Raspberry Pi to receive, unpack and display UDP multicast packets, broadcasted from an X-Plane flight simulator app running on a MS Windows PC. In this moment I am trying to port this script to CircuitPython V8.0.0.-rc.2 on an Adafruit Feather ESP32-S2 with TFT, however I am not able to proceed because, TMHO, the core module 'socketpool' lacks attributes I need for my project.

Ref: readthedocs_latest_socketpool socketpool has (among some others) the following attributes: 'AF_INET', 'AF_INET6', 'EAI_NONAME', 'IPPROTO_TCP', 'SOCK_DGRAM', 'SOCK_RAW', 'SOCK_STREAM', 'TCP_NODELAY', 'gaierror', 'getaddrinfo', 'socket'

My 'wishlist' of attributes to add: IPPROTO_UDP SO_REUSEADDR SOL_SOCKET IPPROTO_IP IP_ADD_MEMBERSHIP INADDR_ANY inet_aton()

Below an image of BECN packets captured using WireShark: multicast_udp_packets_captured

The following is a test script to receive, unpack and print to REPL received multicast udp packets. It fails because of socketpool.SocketPool is lacking the attributes listed above in my 'wishlist':

# code.py -- receive UDP multicast Beacon & Data packets sent from a desktop PC (MS Windows 11 Pro) 
# running X-Plane 12 flight simulator which broadcast UDP multicast BECN & Data packets.
# This is a test script for CircuitPython V8.0.0-rc.2 to receive, unpack and display these packets.
# Platform: Adafruit Feather ESP32-S2 with TFT
# 2023-02-08 @PaulskPt Github
#type:ignore

import time, wifi, socketpool, os, struct, binascii
my_debug = True

ip = wifi.radio.ipv4_address

if ip:
    s_ip = str(ip)
    le_s_ip = len(s_ip)

if s_ip is not None and len(s_ip) > 0 and s_ip != '0.0.0.0':
    print("\nWiFi already connected")
else:
    wifi.radio.connect(ssid=os.getenv("CIRCUITPY_WIFI_SSID"), password=os.getenv("CIRCUITPY_WIFI_PASSWORD"))

print("IP addr:", wifi.radio.ipv4_address)

try:
    pool = socketpool.SocketPool(wifi.radio)
    #udp_host = str(wifi.radio.ipv4_address) # LAN IP as a string
    MCAST_GRP = "239.255.1.1"
    MCAST_PORT = 49707  # a number of your choosing, should be 1024-65000
    udp_buffer = bytearray(61)  # stores our incoming packet

    sock = pool.socket(pool.AF_INET, pool.SOCK_DGRAM, pool.IPPROTO_UDP) # UDP socket
    sock.setsockopt(pool.SO_REUSEADDR, 1)
    sock.settimeout(5)
    sock.bind((MCAST_GRP, MCAST_PORT))
    mreq = struct.pack("4sl", sock.inet_aton(MCAST_GRP), pool.INADDR_ANY)
    sock.setsockopt(pool.IPPROTO_IP, pool.IP_ADD_MEMBERSHIP, mreq)

    print(f"waiting for UDP packets from remote host to multicast group: \'{MCAST_GRP}\', port: {MCAST_PORT}")
except Exception as e:
    raise 

BeaconData = {}
data = {}

while not BeaconData:
    try:
        size, sender = sock.recvfrom_into(udp_buffer)
        print(f"Received udp packet from {sender[0]}:", udp_buffer)
        header = udp_buffer[0:4]
        if header == b'BECN':
            print("Received a BECN packet")
        time.sleep(2)
        # * Data
        data = udp_buffer[5:21]
        # struct becn_struct
        # {
        #   uchar beacon_major_version;     // 1 at the time of X-Plane 10.40
        #   uchar beacon_minor_version;     // 1 at the time of X-Plane 10.40
        #   xint application_host_id;       // 1 for X-Plane, 2 for PlaneMaker
        #   xint version_number;            // 104014 for X-Plane 10.40b14 - 113201 for X-Plane 11.32
        #   uint role;                      // 1 for master, 2 for extern visual, 3 for iOS
        #   ushort port;                    // port number X-Plane is listening on
        #   xchr    computer_name[strDIM];  // the hostname of the computer
        # };
        beacon_major_version = 0
        beacon_minor_version = 0
        application_host_id = 0
        xplane_version_number = 0
        role = 0
        port = 0
        (
          beacon_major_version,  # 1 at the time of X-Plane 10.40
          beacon_minor_version,  # 1 at the time of X-Plane 10.40, 2 at the time of X-Plane 11
          application_host_id,   # 1 for X-Plane, 2 for PlaneMaker
          xplane_version_number, # 104014 for X-Plane 10.40b14 - 113201 for X-Plane 11.32
          role,                  # 1 for master, 2 for extern visual, 3 for IOS
          port,                  # port number X-Plane is listening on
        ) = struct.unpack("<BBiiIH", data)

        if my_debug:
            print('beacon_major_version = ', beacon_major_version)
            print('beacon_minor_version = ', beacon_minor_version)
            print('application_host_id = ', application_host_id)

        # Originally beacon_minor_version was checked for a value of 1 but investigation by Paulsk revealed that X-Plane 11 returns a value of  2
        computer_name = udp_buffer[21:-1]
        if beacon_major_version == 1 \
           and beacon_minor_version == 2 \
           and application_host_id == 1:
            BeaconData["IP"] = sender[0]
            BeaconData["Port"] = port
            BeaconData["hostname"] = computer_name.decode()
            BeaconData["XPlaneVersion"] = xplane_version_number
            BeaconData["role"] = role

            if my_debug:
                print('\nBeacon UDP packet received:')
                print('Host IP         =', BeaconData["IP"])
                print('Port            =', BeaconData["Port"])
                print('Hostname        =', BeaconData["hostname"])
                print('X-Plane version =', BeaconData["XPlaneVersion"])
                print('Role            =', BeaconData["role"])
        else:
            print('Unknown packet from '+sender[0])
            print(str(len(udp_buffer)) + ' bytes')
            print(udp_buffer)
            print(binascii.hexlify(udp_buffer))

    except OSError as e:
        if e.errno == 116:  # ETIMEDOUT
            raise

print("Done!")
guidobonerz commented 8 months ago

Any news about this topic?

dhalbert commented 8 months ago

We have https://github.com/adafruit/circuitpython/pull/8752 and https://github.com/adafruit/circuitpython/pull/8306. Do these help? You can test the 9.0.0-alpha releases.

PaulskPt commented 8 months ago

Thank you Dan. I am going to test it.

PaulskPt commented 8 months ago

Monday, January 22, 2024. 1h38 PM GMT

Board: Adafruit Feather ESP32-S2 TFT Flashed with: Adafruit CircuitPython 9.0.0-alpha.6-29-g5f318c37bb on 2024-01-19; Adafruit Feather ESP32-S2 TFT with ESP32S2

IDE: Mu 1.2.0

The REPL output below shows that the board, connected to the in house WiFi, receives the UPD multicast data packets that X-Plane 12 was set for to transmit.

a) REPL output of manual test to see which attributes socketpool.SocketPool has now (with the latest version of CircuitPython):

Adafruit CircuitPython 9.0.0-alpha.6-29-g5f318c37bb on 2024-01-19; Adafruit Feather ESP32-S2 TFT with ESP32S2

>>>
>>> import wifi
>>> import socketpool
>>> pl = socketpool.SocketPool(wifi.radio)
>>> dir(pl)
['__class__', 'AF_INET', 'AF_INET6', 'EAI_NONAME', 'IPPROTO_ICMP', 'IPPROTO_IP', 'IPPROTO_IPV6', 'IPPROTO_RAW', 'IPPROTO_TCP', 'IPPROTO_UDP', 'IP_MULTICAST_TTL', 'SOCK_DGRAM', 'SOCK_RAW', 'SOCK_STREAM', 'TCP_NODELAY', 'gaierror', 'getaddrinfo', 'socket']
>>> 

b) REPL output (cpy script running):

soft reboot

Auto-reload is on. Simply save files over USB to run them or enter REPL to disable.
code.py output:

This script is running on an 'adafruit_feather_esp32s2_tft'
main():                  Date time sync interval set to: 10 minutes
dg.__init__():           Entering...
dr.__init__():           self.use_udp_host= True
dr.__init__():           self.MCAST_GRP= 235.255.1.1
dr.__init__():           self.MCAST_PORT= 49707
setup():                 WiFi already connected to: 'xxxxxx'  (Note: SSID masked)
create_groups():         loading image 'bmp/avatar.bmp'
create_groups():         loading image 'bmp/blinka.bmp'
main():                  We are running Python version: 3.4.0 
main():                  The following values will be used:
main():                  <IP-address> of this device: 192.168.1.xxx  (Note: IP masked)
main():                  and <Multicast Port>: 49707
-----------------------------------------------------------------------------------------
wifi_connect():          Connecting to 'xxxxx'  (Note: SSID masked)
wifi_connect():          WiFi already connected and IP is wanted IP: 192.168.1.xxx   (Note: IP masked)
wifi_connect():          connected to: 'xxxxx'  (Note: SSID masked)
wifi_connect():          Resolved google address: '216.58.209.78'
wifi_connect():          Ping google.com [216.58.209.78]:18 ms
main():                  We are going to run a dataref test...
main():                  type(dr)= <class 'XPlaneDatarefRx'>
main():                  contents dr= <XPlaneDatarefRx object at 0x3f804670>
dr.OpenDatarefSocket:    We are going to open a socket for Dataref request and answers
dr.OpenDatarefSocket:    type(pool)= <class 'SocketPool'>
dr.FindIp():             type(self.BeaconData)= <class 'dict'>
dr.FindIp():             self.BeaconData= {}
dr.FindIp():             packet_size= 71
dr.FindIp():             waiting for beacon packets, udp_host 192.168.1.xxx, port 49707   (Note: IP masked)
dr.FindIp():             header= DATA
dr.FindIp():             nr bytes received= 71 from 192.168.1.xxx   (Note: IP of host running XPlane12 masked)
dr.dataref_test():       beacon: {}
main():                  call to dr.dataref_test() sucdessful
dg.OpenUDPSocket():      pool= <SocketPool>
dg.OpenUDPSocket():      waiting for packets on host 192.168.1.xxx, port 49707 (Note: IP masked)

dg.msgs_unpack():        using unpack string: 'self.udp_unpack_str_3', size= 36 bytes, value= '<iffffifff'
dg.msgs_unpack():        us= (3, 101.651, 101.951, 104.995, 104.995, -998653952, 116.978, 120.826, 120.826)

dg.msgs_unpack():        self.values_struct_3.items() = dict_items([('vtrue_ktgs', 104.995), ('vind_mph', 116.978), ('vtrue_mphas', 120.826), ('vtrue_mphgs', 120.826), ('ID', 3), ('vind_keas', 101.951), ('vtrue_ktas', 104.995), ('vind_kias', 101.651), ('nothing', -998653952)])

dg.msgs_unpack():        using unpack string: 'self.udp_unpack_str_17', size= 36 bytes, value= '<ifffiffif'
dg.msgs_unpack():        us= (17, 1.7446, 1.91166, 273.543, -998653952, 273.255, -0.288664, -998653952, 276.984)

dg.msgs_unpack():        self.values_struct_17.items() = dict_items([('mag_comp', 276.984), ('hding_mag', 273.255), ('mavar_deg', -0.288664), ('hding_true', 273.543), ('ID', 17), ('nothing1', -998653952), ('pitch_deg', 1.7446), ('roll_deg', 1.91166), ('nothing2', -998653952)])

dg.msgs_unpack():        using unpack string: 'self.udp_unpack_str_20', size= 36 bytes, value= '<iffffffff'
dg.msgs_unpack():        us= (20, 51.5768, -1.61574, 1997.41, 1648.58, 344.963, 1997.41, 51.5, -1.0)

dg.msgs_unpack():        self.values_struct_20.items() = dict_items([('terrn_ftmsl', 344.963), ('lat_orign', 51.5), ('lon_deg', -1.61574), ('lon_orign', -1.0), ('ID', 20), ('p-alt_ftmsl', 1997.41), ('CG_ftmsl', 1997.41), ('lat_deg', 51.5768), ('gear_ftagl', 1648.58)])

dg.msgs_unpack():        using unpack string: 'self.udp_unpack_str_102', size= 36 bytes, value= '<iffffffii'
dg.msgs_unpack():        us= (102, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0, 1177464832)

dg.msgs_unpack():        self.values_struct_102.items() = dict_items([('dme_speed', 0.0), ('dme_mode', 0.0), ('dme_time', 0.0), ('dme_found', 0.0), ('ID', 102), ('dme_n-typ', 0), ('dme_nav01', 0.0), ('dme-3_freq', 1177464832), ('dme_dist', 0.0)])

dg.disp_hdg_alt():       xp_lst= [(3, 101.651, 101.951, 104.995, 104.995, -998653952, 116.978, 120.826, 120.826), (17, 1.7446, 1.91166, 273.543, -998653952, 273.255, -0.288664, -998653952, 276.984), (20, 51.5768, -1.61574, 1997.41, 1648.58, 344.963, 1997.41, 51.5, -1.0), (102, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0, 1177464832)]
dg.disp_hdg_alt():       self.hdg_alt_lst= [273.255, 1997.41]

dg.disp_hdg_alt():       showing page: main
main():                  call to dg.datagram_test() successful

[...]  etcetera...

Conclusion: problem solved for me within this context. To everyone that contributed to add the requested attributes my sincere "Thank you!".

PaulskPt commented 8 months ago

Closing this issue

zenz commented 8 months ago

Monday, January 22, 2024. 1h38 PM GMT

Board: Adafruit Feather ESP32-S2 TFT Flashed with: Adafruit CircuitPython 9.0.0-alpha.6-29-g5f318c37bb on 2024-01-19; Adafruit Feather ESP32-S2 TFT with ESP32S2

IDE: Mu 1.2.0

The REPL output below shows that the board, connected to the in house WiFi, receives the UPD multicast data packets that X-Plane 12 was set for to transmit.

a) REPL output of manual test to see which attributes socketpool.SocketPool has now (with the latest version of CircuitPython):

Adafruit CircuitPython 9.0.0-alpha.6-29-g5f318c37bb on 2024-01-19; Adafruit Feather ESP32-S2 TFT with ESP32S2

>>>
>>> import wifi
>>> import socketpool
>>> pl = socketpool.SocketPool(wifi.radio)
>>> dir(pl)
['__class__', 'AF_INET', 'AF_INET6', 'EAI_NONAME', 'IPPROTO_ICMP', 'IPPROTO_IP', 'IPPROTO_IPV6', 'IPPROTO_RAW', 'IPPROTO_TCP', 'IPPROTO_UDP', 'IP_MULTICAST_TTL', 'SOCK_DGRAM', 'SOCK_RAW', 'SOCK_STREAM', 'TCP_NODELAY', 'gaierror', 'getaddrinfo', 'socket']
>>> 

b) REPL output (cpy script running):

soft reboot

Auto-reload is on. Simply save files over USB to run them or enter REPL to disable.
code.py output:

This script is running on an 'adafruit_feather_esp32s2_tft'
main():                  Date time sync interval set to: 10 minutes
dg.__init__():           Entering...
dr.__init__():           self.use_udp_host= True
dr.__init__():           self.MCAST_GRP= 235.255.1.1
dr.__init__():           self.MCAST_PORT= 49707
setup():                 WiFi already connected to: 'xxxxxx'  (Note: SSID masked)
create_groups():         loading image 'bmp/avatar.bmp'
create_groups():         loading image 'bmp/blinka.bmp'
main():                  We are running Python version: 3.4.0 
main():                  The following values will be used:
main():                  <IP-address> of this device: 192.168.1.xxx  (Note: IP masked)
main():                  and <Multicast Port>: 49707
-----------------------------------------------------------------------------------------
wifi_connect():          Connecting to 'xxxxx'  (Note: SSID masked)
wifi_connect():          WiFi already connected and IP is wanted IP: 192.168.1.xxx   (Note: IP masked)
wifi_connect():          connected to: 'xxxxx'  (Note: SSID masked)
wifi_connect():          Resolved google address: '216.58.209.78'
wifi_connect():          Ping google.com [216.58.209.78]:18 ms
main():                  We are going to run a dataref test...
main():                  type(dr)= <class 'XPlaneDatarefRx'>
main():                  contents dr= <XPlaneDatarefRx object at 0x3f804670>
dr.OpenDatarefSocket:    We are going to open a socket for Dataref request and answers
dr.OpenDatarefSocket:    type(pool)= <class 'SocketPool'>
dr.FindIp():             type(self.BeaconData)= <class 'dict'>
dr.FindIp():             self.BeaconData= {}
dr.FindIp():             packet_size= 71
dr.FindIp():             waiting for beacon packets, udp_host 192.168.1.xxx, port 49707   (Note: IP masked)
dr.FindIp():             header= DATA
dr.FindIp():             nr bytes received= 71 from 192.168.1.xxx   (Note: IP of host running XPlane12 masked)
dr.dataref_test():       beacon: {}
main():                  call to dr.dataref_test() sucdessful
dg.OpenUDPSocket():      pool= <SocketPool>
dg.OpenUDPSocket():      waiting for packets on host 192.168.1.xxx, port 49707 (Note: IP masked)

dg.msgs_unpack():        using unpack string: 'self.udp_unpack_str_3', size= 36 bytes, value= '<iffffifff'
dg.msgs_unpack():        us= (3, 101.651, 101.951, 104.995, 104.995, -998653952, 116.978, 120.826, 120.826)

dg.msgs_unpack():        self.values_struct_3.items() = dict_items([('vtrue_ktgs', 104.995), ('vind_mph', 116.978), ('vtrue_mphas', 120.826), ('vtrue_mphgs', 120.826), ('ID', 3), ('vind_keas', 101.951), ('vtrue_ktas', 104.995), ('vind_kias', 101.651), ('nothing', -998653952)])

dg.msgs_unpack():        using unpack string: 'self.udp_unpack_str_17', size= 36 bytes, value= '<ifffiffif'
dg.msgs_unpack():        us= (17, 1.7446, 1.91166, 273.543, -998653952, 273.255, -0.288664, -998653952, 276.984)

dg.msgs_unpack():        self.values_struct_17.items() = dict_items([('mag_comp', 276.984), ('hding_mag', 273.255), ('mavar_deg', -0.288664), ('hding_true', 273.543), ('ID', 17), ('nothing1', -998653952), ('pitch_deg', 1.7446), ('roll_deg', 1.91166), ('nothing2', -998653952)])

dg.msgs_unpack():        using unpack string: 'self.udp_unpack_str_20', size= 36 bytes, value= '<iffffffff'
dg.msgs_unpack():        us= (20, 51.5768, -1.61574, 1997.41, 1648.58, 344.963, 1997.41, 51.5, -1.0)

dg.msgs_unpack():        self.values_struct_20.items() = dict_items([('terrn_ftmsl', 344.963), ('lat_orign', 51.5), ('lon_deg', -1.61574), ('lon_orign', -1.0), ('ID', 20), ('p-alt_ftmsl', 1997.41), ('CG_ftmsl', 1997.41), ('lat_deg', 51.5768), ('gear_ftagl', 1648.58)])

dg.msgs_unpack():        using unpack string: 'self.udp_unpack_str_102', size= 36 bytes, value= '<iffffffii'
dg.msgs_unpack():        us= (102, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0, 1177464832)

dg.msgs_unpack():        self.values_struct_102.items() = dict_items([('dme_speed', 0.0), ('dme_mode', 0.0), ('dme_time', 0.0), ('dme_found', 0.0), ('ID', 102), ('dme_n-typ', 0), ('dme_nav01', 0.0), ('dme-3_freq', 1177464832), ('dme_dist', 0.0)])

dg.disp_hdg_alt():       xp_lst= [(3, 101.651, 101.951, 104.995, 104.995, -998653952, 116.978, 120.826, 120.826), (17, 1.7446, 1.91166, 273.543, -998653952, 273.255, -0.288664, -998653952, 276.984), (20, 51.5768, -1.61574, 1997.41, 1648.58, 344.963, 1997.41, 51.5, -1.0), (102, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0, 1177464832)]
dg.disp_hdg_alt():       self.hdg_alt_lst= [273.255, 1997.41]

dg.disp_hdg_alt():       showing page: main
main():                  call to dg.datagram_test() successful

[...]  etcetera...

Conclusion: problem solved for me within this context. To everyone that contributed to add the requested attributes my sincere "Thank you!".

Hi @PaulskPt , Would you mind to provide part of code to show how to receive multicast udp msg? I'm have problem too to receive multicast udp message. and cannot figure out how to make it work in circuitpython. I'm using 9.0.0 beta0

PaulskPt commented 8 months ago

Hi @zenz , thank you for your request. I published/renewed my repo on Github. Here is the link: repo . Let me know if you need more assistance. Don't forget to fill in the blanks in file ´settings.toml.

zenz commented 8 months ago

repo

Thank you @PaulskPt , But the repo doesn't exist?

PaulskPt commented 8 months ago

I had an issue with the repo. I deleted it and created a new one. I pasted the new link in my first answer to you above. I tested the link. It works. Try again please.

zenz commented 8 months ago

I had an issue with the repo. I deleted it and created a new one. I pasted the new link in my first answer to you above. I tested the link. It works. Try again please.

Still no luck. perhaps you'd make it private? I can't see it with 404 error.

PaulskPt commented 8 months ago

I am so sorry, yes the repo was labled private. I don't understand it because I use Github for Desktop (MS Windows 11) PC, created the repo and clicked to publicize it. Try again please

zenz commented 8 months ago

I am so sorry, yes the repo was labled private. I don't understand it because I use Github for Desktop (MS Windows 11) PC, created the repo and clicked to publicize it. Try again please

Thank you, I can see it now. but from your code, I think you actually use unicast, not multicast?

anecdata commented 8 months ago

I'm trying this out:

code.py ```py import os import asyncio import traceback import wifi import socketpool import time import traceback # choose your own multicast group adventure GROUPS = (("239.255.255.250", 1900), ("224.0.0.251", 5353), ("239.255.1.1", 138)) MAXBUF = 256 TIMEOUT = 0 # non-blocking async def udpserver(group): with pool.socket(pool.AF_INET, pool.SOCK_DGRAM, pool.IP_MULTICAST_TTL) as s: s.bind((group[0], group[1])) s.settimeout(TIMEOUT) buf = bytearray(MAXBUF) print(f'{time.monotonic_ns()} Listening on {group[0]}:{group[1]}') while True: try: size, sender = s.recvfrom_into(buf) print(f'{time.monotonic_ns()} Received UDP packet from {sender[0]}:{sender[1]} on {group[0]}:{group[1]}', buf[0:size]) except OSError as ex: # if ex.errno == 11: # EAGAIN pass else: print(f'{group[0]}:{group[1]} {repr(ex)}') traceback.print_exception(ex, ex, ex.__traceback__) await asyncio.sleep(0) time.sleep(3) # wait for serial pool = socketpool.SocketPool(wifi.radio) # choose your own connect adventure wifi.radio.connect(os.getenv("WIFI_SSID"), os.getenv("WIFI_PASSWORD")) print(wifi.radio.ipv4_address) async def main(): tasks = [] for taskno in range(len(GROUPS)): tasks.append(asyncio.create_task(udpserver(GROUPS[taskno]))) await asyncio.gather(*tasks) asyncio.run(main()) ```

So far, one of my printers sends packets to 239.255.1.1:138. Suggestions welcome if I have some of the protocol snippets wrong.

PaulskPt commented 8 months ago

Years ago when I started the project, it used MULTICAST_GROUP and MULTICAST PORT (in this moment these values are still in the file settings.toml and read in at start). In the past I used SBC (Raspberry Pi or similar equipment) and later microcontrollers. These were connected to a desktop PC running X-Plane (10 it was) via USB-cable and a USB-to-Serial converter, receiving serial data via UART. The current state of the app, using data from XPlane-12 which the flightsimulator transmits to a device with the chosen IP-address. I use this method now so my ESP32-S2 TFT now will be able to run this Circuitpython application using WiFi and only be powered from a battery.

zenz commented 8 months ago

I'm trying this out:

code.py

import os
import asyncio
import traceback
import wifi
import socketpool
import time
import traceback

# choose your own multicast group adventure
GROUPS = (("239.255.255.250", 1900), ("224.0.0.251", 5353), ("239.255.1.1", 138))
MAXBUF = 256
TIMEOUT = 0  # non-blocking

async def udpserver(group):
    with pool.socket(pool.AF_INET, pool.SOCK_DGRAM, pool.IP_MULTICAST_TTL) as s:
        s.bind((group[0], group[1]))
        s.settimeout(TIMEOUT)
        buf = bytearray(MAXBUF)
        print(f'{time.monotonic_ns()} Listening on {group[0]}:{group[1]}')
        while True:
            try:
                size, sender = s.recvfrom_into(buf)
                print(f'{time.monotonic_ns()} Received UDP packet from {sender[0]}:{sender[1]} on {group[0]}:{group[1]}', buf[0:size])
            except OSError as ex:  # 
                if ex.errno == 11:  # EAGAIN
                    pass
                else:
                    print(f'{group[0]}:{group[1]} {repr(ex)}')
                    traceback.print_exception(ex, ex, ex.__traceback__)
            await asyncio.sleep(0)

time.sleep(3)  # wait for serial
pool = socketpool.SocketPool(wifi.radio)
# choose your own connect adventure
wifi.radio.connect(os.getenv("WIFI_SSID"), os.getenv("WIFI_PASSWORD"))
print(wifi.radio.ipv4_address)

async def main():
    tasks = []
    for taskno in range(len(GROUPS)):
        tasks.append(asyncio.create_task(udpserver(GROUPS[taskno])))
    await asyncio.gather(*tasks)

asyncio.run(main())

So far, one of my printers sends packets to 239.255.1.1:138. Suggestions welcome if I have some of the protocol snippets wrong.

I have similar code, it can run, but can receive nothing(my code written with arduino works). so I think multicast doesn't work.