Space Cowboy
Space Cowboy

Reputation: 1

Socket blocks other functions RPi pico

EDIT: new code at the bottom.

I’m building an art piece. It consists of multiple raspberry pi picos. Coding in micropython.

  1. There is a RPi pico-based remote control that acts like a web server, providing the state of the potentiometer(s).

  2. The art piece has 3 more picos on it that open a socket and get the pot state from the remote. Each pico has motors and cascading WS2812 LEDs to combine color and movement.

  3. There may be other ways to solve the general task of communicating with 3 clients. Bluetooth got a little too complicated… I’m open to ideas. Or if there is a book someone could recommend to help me learn this stuff.

The problem is that while using asyncio, the s.recv code blocks the LEDs moving for a second, and I want the LEDs to keep moving.

I tried to use threads but that didn’t work.

I tried to use s.setblocking(False) also didn’t work. In fact that’s made it pause longer.

Here's the code:

import machine
import network
import time
from secret import ssid, password
import socket
from neopixel import Neopixel
import asyncio

wlan = network.WLAN(network.STA_IF)
wlan.active(True)
wlan.connect(ssid, password)
while not wlan.isconnected() and wlan.status() >= 0:
    print("Waiting to connect:")
    time.sleep(1)
# Should be connected and have an IP address
wlan.status() # 3 == success
wlan.ifconfig()
print(wlan.ifconfig())

async def get_data():
    while True:
        try:
            if wlan.isconnected:
                print("requesting data")
                ai = socket.getaddrinfo("192.168.0.24", 80) # Address of Web Server
                addr = ai[0][-1]
                # Create a socket and make a HTTP request
                s = socket.socket() # Open socket
                s.connect(addr)
                s.send(b"GET Data") # Send request
                ss = str(s.recv(512))  # Attempt to receive data
                s.close()          # Close socket
                await asyncio.sleep_ms(1000)    # wait
            else:
                print("no connection")
                await asyncio.sleep_ms(1000)
        except:
            s.close()
            print('no data, connection closed')
            await asyncio.sleep_ms(1000)
            
async def rainbow(): # This function runs a walking rainbow across the LED strip
    NUM_LEDS = 74
    LED_PIN = 13
    pixels = Neopixel(NUM_LEDS, 0, LED_PIN, "GRB")
    hue_offset = 1028
    brightness = 16
    while True:
      for hue in range(0, 65535, 1000):
        # Set the hues
        for led in range(NUM_LEDS):
          color = pixels.colorHSV(hue +(led * hue_offset), 255, brightness)
          pixels.set_pixel(led, color)
        pixels.show()
        print(f"rgb={color}")
        await asyncio.sleep_ms(100)
 
async def main():
    tasks = []
    tasks = [
        asyncio.create_task(get_data()),
        asyncio.create_task(rainbow()),
    ]
    await asyncio.gather(*tasks)
    
while True:
    asyncio.run(main())

This is the send code on the other picoW

# Webserver to send RGB data
# Tony Goodhew 5 July 2022
import network
import socket
import time
from machine import Pin, ADC
from secret import ssid,password
import random

potA = machine.ADC(26)
potB = machine.ADC(27)
potC = machine.ADC(28)

wlan = network.WLAN(network.STA_IF)
wlan.active(True)
wlan.connect(ssid, password)
       
# Wait for connect or fail
max_wait = 10
while max_wait > 0:
    if wlan.status() < 0 or wlan.status() >= 3:
        break
    max_wait -= 1
    print('waiting for connection...')
    time.sleep(1)

# Handle connection error
if wlan.status() != 3:
    raise RuntimeError('network connection failed')
else:
    print('connected')
    status = wlan.ifconfig()
    print( 'ip = ' + status[0] )

# Open socket
addr = socket.getaddrinfo('0.0.0.0', 80)[0][-1]

s = socket.socket()
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(addr)
s.listen(1)

print('listening on', addr)

# Listen for connections
while True:
    try:
        cl, addr = s.accept()
        print('client connected from', addr)
        request = cl.recv(1024)
        print(request)
        # Do not unpack request
        # We reply to any request the same way
        # Generate 3 values to send back
        print(f"Potentiometer A is: {potA.read_u16()}")
        print(f"Potentiometer B is: {potB.read_u16()}")
        print(f"Potentiometer C is: {potB.read_u16()}")
        r = int((potA.read_u16()))
        g = int((potB.read_u16()))
        b = int((potC.read_u16()))
        # Join to make a simple string with commas as separators
        rgb = str(r) + "," + str(g) + ","+str(b)
        
        response = rgb # This is what we send in reply

        cl.send(response)
        print("Sent:" + rgb)
        cl.close()

    except OSError as e:
        cl.close()
        print('connection closed')

EDIT:

Thanks so much to jupiterbjy for giving me such a great answer!

note: the client was crashing with an ENOMEM so I placed a gc.collect(). I don't know if I'm doing this right. The rainbow resets after about 3-7 seconds so there's more troubleshooting to do.

The new code for the client:


# The Jupiter client

import asyncio
import time
import network
from machine import Pin
from secret import ssid, password
from neopixel import Neopixel
import gc
            

NUM_LEDS = 102
LED_PIN = 17
pixels = Neopixel(NUM_LEDS, 0, LED_PIN, "GRB")
hue_offset = 1028
brightness = 16

async def connect_wlan():
    sta_if = network.WLAN(network.STA_IF)
    sta_if.active(True)
    sta_if.connect(ssid, password)
    
    while not sta_if.isconnected():
        await asyncio.sleep(1)
    
    return sta_if

def log(*args, **kwargs):
    print(f"{time.time():.1}", *args, **kwargs)

async def get_data():
    """Periodic network task"""
    while True:
        reader, writer = await asyncio.open_connection("192.168.0.24", 80)
        log(f"Connected!")

        try:
            writer.write("Big Ring".encode())
            await writer.drain()
            log(f"Request Sent!")
            data = str(await reader.read(1024))
            print(f"Received Data: {data}")
            await asyncio.sleep_ms(200)
            gc.collect()

        except:        
            gc.collect()
            writer.close()
            await writer.wait_closed()
            log(f"Disconnected!")
            await asyncio.sleep(2)

# This function runs a walking rainbow across the LED strip                       
async def rainbow(): 
    while True:
      for hue in range(0, 65535, 1000):
        # Set the hues
        for led in range(NUM_LEDS):
          color = pixels.colorHSV(hue +(led * hue_offset), 255, brightness)
          pixels.set_pixel(led, color)
        pixels.show()
        await asyncio.sleep_ms(100)

async def blinker():
    """Continuous LED blinker task"""    
    # for Pico it's 25, for Pico W/WH it's "LED"
    led = Pin("LED", Pin.OUT)    
    while True:
        led.toggle()
        log("[LED] LED", "ON" if led.value() else "off")
        await asyncio.sleep(1)

async def main():
    sta_if = await connect_wlan()
    tasks = []
    tasks = [
        asyncio.create_task(rainbow()),
        asyncio.create_task(blinker())
        asyncio.create_task(get_data()),
    ]
    await asyncio.gather(*tasks)

while True:
    asyncio.run(main())

The code for the server:

# The Jupiter server

import asyncio
import time
import network
import machine
from machine import Pin
from secret import ssid, password

led = Pin("LED", Pin.OUT) 
potA = machine.ADC(26)
potB = machine.ADC(27)
potC = machine.ADC(28)

async def connect_wlan():
    #Connect to WLAN
    wlan = network.WLAN(network.STA_IF)
    wlan.active(True)
    wlan.connect(ssid, password)

    while wlan.isconnected() == False:
        print('Waiting for connection...')
        await asyncio.sleep(1)
    ip = wlan.ifconfig()[0]
    print(f'Connected on {ip}')
    return ip
    

async def callback(
    reader: asyncio.StreamReader,
    writer: asyncio.StreamWriter,
):
    addr = writer.get_extra_info('peername')
    print(f"Conenction from {addr!r}")
    
    # read from client
    client_data = await reader.read(1024)
    print(f"Client data: {client_data}")
    
    #assess potentiometers and assign to r, g, b
    r = int((potA.read_u16()))
    g = int((potB.read_u16()))
    b = int((potC.read_u16()))

    # Join to make a simple string with commas as separators
    rgb = str(r) + "," + str(g) + ","+str(b)
    
# This is what we send in reply
    response = rgb 
    print(f"sending {response}")

    # send to client
    writer.write(response.encode())
    await writer.drain()
    
    # wait for connection to close
    writer.close()
    await writer.wait_closed()
    
#    print(f"Disconnected {addr!r}")


async def main():
    ip = await connect_wlan()
    server = await asyncio.start_server(callback, ip, 80)

    async with server:
        while True:
            led.toggle()
            await asyncio.sleep(1)


asyncio.run(main())

Upvotes: 0

Views: 231

Answers (1)

jupiterbjy
jupiterbjy

Reputation: 3523

Just tested, indeed it works with asyncio.open_connection & asyncio.start_server.


TL;DR

Below are based on examples that asyncio document page provides. Refer it to better understand the usages of those two async function(or coroutine) is.

Server (Desktop, Python 3.12 Win 64x):

"""
Handles concurrent requests asynchronously.
INTENTIONALLY delays response by 5 seconds to simulate receive delay you suffered at `socket.recv()`.
"""

import asyncio
from datetime import datetime


async def callback(
    reader: asyncio.StreamReader,
    writer: asyncio.StreamWriter,
):
    addr = writer.get_extra_info('peername')
    print(f"Conenction from {addr!r}")
    
    # read from client
    some_request_data = await reader.read(1024)
    
    # intentionally delay response for 5 seconds
    await asyncio.sleep(5)
    
    # send to client
    writer.write("SomeData".encode())
    await writer.drain()
    
    # wait for connection to close
    writer.close()
    await writer.wait_closed()
    
    print(f"Disconnected {addr!r}")


async def main():
    server = await asyncio.start_server(
        callback, "192.168.0.56", "9001"
    )

    async with server:
        await server.serve_forever()


asyncio.run(main())

Client (RPi Pico WH):

"""
Keeps blinking every 2 seconds, while 3 tasks connects to server concurrently & asynchronously, to mimic multiple Pico-s connecting the server.
"""

import asyncio
import time

import network
from machine import Pin

from secret import SSID, PSWD


async def connect_wlan():
    sta_if = network.WLAN(network.STA_IF)
    sta_if.active(True)
    sta_if.connect(SSID, PSWD)
    
    while not sta_if.isconnected():
        await asyncio.sleep(1)
    
    return sta_if


def log(*args, **kwargs):
    print(f"{time.time():.1}", *args, **kwargs)


async def get_data(task_id: int):
    """Periodic network task"""
    
    while True:
        reader, writer = await asyncio.open_connection("192.168.0.56", 9001)
        log(f"[Net {task_id}] Connected!")
            
        writer.write("some request data".encode())
        
        try:
            await writer.drain()
            log(f"[Net {task_id}] Request Sent!")
            
            data = await reader.read(1024)
            log(f"[Net {task_id}] Received Data!")
        finally:        
            writer.close()
            await writer.wait_closed()
            
            log(f"[Net] Disconnected!")
            
            await asyncio.sleep(2)


async def blinker():
    """Continuous LED blinker task"""
    
    # for Pico it's 25, for Pico W/WH it's "LED"
    led = Pin("LED", Pin.OUT)
    
    while True:
        led.toggle()
        log("[LED] LED", "ON" if led.value() else "off")
        await asyncio.sleep(1)


async def main():
    sta_if = await connect_wlan()
    
    blinker_task = asyncio.create_task(blinker())
    network_tasks = [asyncio.create_task(get_data(idx)) for idx in range(3)]
    
    try:
        await asyncio.gather(blinker_task, *network_tasks)
    finally:
        sta_if.active(False)


asyncio.run(main())


Results

From below gif and output - Note how Client's Net 0, 1, 2 tasks maintains 3 connections simultaneously(in high-level concept at least) , and is being held by server delaying each response by 5 seconds.

Meanwhile, LED task is blinking without issue.



Client Output:

MPY: soft reboot
1714092617 [LED] LED off
1714092617 [Net 0] Connected!
1714092617 [Net 1] Connected!
1714092617 [Net 2] Connected!
1714092617 [Net 0] Request Sent!
1714092617 [Net 1] Request Sent!
1714092617 [Net 2] Request Sent!
1714092618 [LED] LED ON
1714092619 [LED] LED off
1714092620 [LED] LED ON
1714092621 [LED] LED off
1714092622 [LED] LED ON
1714092623 [LED] LED off
1714092624 [LED] LED ON
1714092625 [LED] LED off
1714092625 [Net 1] Received Data!
1714092625 [Net] Disconnected!
1714092626 [LED] LED ON
1714092626 [Net 2] Received Data!
1714092626 [Net] Disconnected!
1714092627 [LED] LED off
1714092627 [Net 1] Connected!
1714092627 [Net 1] Request Sent!
1714092628 [Net 0] Received Data!
1714092628 [Net] Disconnected!
1714092628 [LED] LED ON
1714092628 [Net 2] Connected!
1714092628 [Net 2] Request Sent!
1714092629 [LED] LED off
1714092630 [Net 0] Connected!
1714092630 [Net 0] Request Sent!
1714092630 [LED] LED ON
1714092631 [LED] LED off
1714092632 [LED] LED ON
1714092632 [Net 1] Received Data!
1714092632 [Net] Disconnected!
1714092633 [LED] LED off
1714092633 [Net 2] Received Data!
1714092633 [Net] Disconnected!
1714092634 [LED] LED ON
1714092634 [Net 1] Connected!
1714092634 [Net 1] Request Sent!
1714092635 [Net 0] Received Data!
1714092635 [Net] Disconnected!
1714092635 [LED] LED off
1714092635 [Net 2] Connected!
1714092635 [Net 2] Request Sent!
1714092636 [LED] LED ON
1714092637 [Net 0] Connected!
1714092637 [Net 0] Request Sent!
1714092637 [LED] LED off
1714092638 [LED] LED ON
1714092639 [LED] LED off
...


More explanation

From this point, assuming you know what the Asynchronous, Synchronous, Awaitable is.

If not, refer this - I know it might sound harsh but trust me, studying this will be beneficial in long run!

Usually we could even move synchronous operations like socket.send() socket.recv() to thread using asyncio.to_thread() to make it asynchronous but that interface is what Micropython is missing in their Subset of asyncio module.

But worry not, socket is just a low-level networking interface in either CPython (Usual python we see) and Micropython. There's higher-level alternatives such as asyncio.streams - which Python also officially recommends.


To pinpoint the problem of your code, basically what you wrote is like this:

async def some_func():
   time.sleep(1)

While it should've been like this:

async def some_func():
    await asyncio.sleep(1)

Notice the big difference? One has await and one doesn't.

That's what's necessary to do something Asynchronous in python.

Without await and Awaitable it's just a synchronous code, where your thread (either main or not) that's running your function WAIT until that line's execution is done.

def main():
    sync_work_1()
    ...

Here, main() will keep hold the thread until all stuff in it is done.


However, for Awaitable that uses await keywords, are more like a PROMISE to execute eventually and return result. (fun fact: js's equivalent is literally named promise for that reason.

import asyncio
...
async def main():
    await async_work_1()
    ...

Here, main() got promise that async_work_1() will Eventually run, and until then all main() can't do anything since that result isn't here yet so main() also pauses running.

Instead, an infinite loop called EventLoop schedules the execution of async_work_1(). And since main() now can't progress further, EventLoop pause main() and schedule it to be ran After async_work_1() is done. Until then, it pulls out next scheduled Task to do and runs/resumes it.

...

This will be sufficient (yet not 100% accurate) to understand fundamental difference between Awaitable and what's not.

To understand how such structure ever work where everything just decide to stop running - refer this answer about IO & GIL.


So Tl;DR you always need to use Awaitable with await keyword or asyncio.create_task() to achieve concurrency (yet not parallelism).

Which means, if you want any IO operation to be done asynchronously, you almost everytime have to do it thru interfaces that the very asynchronous modules you are using provides. (This case asyncio)

Which is what asyncio.open_connection & asyncio.start_server is.

Upvotes: 1

Related Questions