Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sending data to the hub with Pybricksdev #181

Closed
laurensvalk opened this issue Dec 23, 2020 · 11 comments
Closed

Sending data to the hub with Pybricksdev #181

laurensvalk opened this issue Dec 23, 2020 · 11 comments
Labels
software: pybricksdev Issues related to the pybrickdev Python package support Request for technical support for a problem that is not a bug or feature request topic: communication Issues related to hub-to-hub/phone/computer communcation

Comments

@laurensvalk
Copy link
Member

laurensvalk commented Dec 23, 2020

Originally posted by @johnscary-ev3 in #154 (comment)

Hi,
I have solved the install problems and I am trying to use the pybricksdev-demo in jupyter-notebook now.
I took the communication demo and made my own program to just take input from the keyboard and send it to the hub.
The "fowarder" program below basically is working, but I always get an "except" branch at the hub,write() statememt.
I am able to read print outs from the hub ok and I can enter a character form the keyboard but can't send anything to the hub.
Hub is doing a getchar().
After the code below is some sample output from the program. There is some normal output from the hub about distance detection and arm (motor) moves, and then I enter a 'c' from the keyboard which is supposed to be a remote command to the robot.
Any clues for me or docs on the hub.write() routine?
The robot code I am using works ok under Pybricks Code getting commands from the keyboard.
Thanks.

async def forwarder(hub):
    
    # Give the hubs some time to start
    await sleep(2)
    print("In forwarder")

    while True:
        if hub.state == hub.RUNNING:
            print("running ", len(hub.output))
            # Check if the remote has printed anything
            if len(hub.output) > 0:
            
                # If so, print it
                print(hub.output)
                hub.output = []
                
            # get a char from input
            a= input()
            print("len=",len(a))
            
            # Try to send it as one byte to the receiving hub
            if len(a) > 0:
                try:
                    print(a)
                    await hub.write(bytes(a))
                except:
                    print("write exception")
        await sleep(0.05)

len= 0
running 3
[bytearray(b'object detected dist = 7.3'), bytearray(b'right_arm rot_angle = -97'), bytearray(b'right_arm rot_angle = 83')]
c
len= 1
c
write exception
running 0

Code fragment on the robot side trying to read the command character
# Read all available characters, keeping only the most recent.
    c = None
    while True:
        read = getchar()
        if read is None:
            break
        c = read

@laurensvalk
Copy link
Member Author

laurensvalk commented Dec 23, 2020

The demo (and most of Pybricksdev) uses asynchronous programming (async/await), which allows a form of "parallel" code execution as long as no functions are blocking execution. I think the input function blocks this flow. This means that the code cannot communicate with the hub at the same time. I suppose you'd need an asynchronous variant of input or another way to read the keyboard state without blocking.

But it may be easier to copy the code to Pybricks Code to try remote control functionality :)

Another thing we can consider, is make it possible again to connect to the hub when a program is running. That way you can start the program as you like, and then connect to Pybricks Code to control it.

I do want to add that both experimental.getchar and pybricksdev are both experimental features that we do not currently support very actively. There's a lot you can do with both, though, so that's why we've released them for the community. I've turned this into a dedicated issue so it's a bit easier for others to join and help as well.

@dlech dlech added software: pybricksdev Issues related to the pybrickdev Python package support Request for technical support for a problem that is not a bug or feature request labels Dec 23, 2020
@johnscary-ev3
Copy link

Hi,
Thanks for the feedback.
I actually got this going sending commands to my robot, but it is using that blocking input() call. See below.
My main problem was getting the hub.write(bytes([ord(command[0])])) call correct to convert the input command string into a single byte to send, using just the first character of what I type in.
Since my PC side program is basically just going to get the program downloaded and then send commands, I think the blocking input() call is ok, and I probably don't really need to use the async calls for that, I guess.
Is there a Python non-blocking call for getting characters from the keyboard?
Searched around some but did not find one.

Going to transfer my jupyter-notebook code over to VSC Python environment next.
Will report back on that.


PC side

async def forwarder(hub):
     
    print("In forwarder")
    command= ''
    
    # Give the hubs some time to start
    while hub.state != hub.RUNNING :
        print('fowarder: Waiting for Hub to Run')
        await sleep(2)
    print('fowarder: Hub Running')
    
    while hub.state == hub.RUNNING :
               
        #Get new command 
        command = input()
              
        # Try to send to the receiving hub
        if len(command) > 0:
            try:
                await hub.write(bytes([ord(command[0])]))
            except:
                pass
            
        #wait for hub  to send any replies
        await sleep(2)
        
        # Check if the remote has printed anything
        #if len(hub.output) > 0:          
            # If so, print it
        #    print(hub.output)
        #   hub.output = []
            
        #wait some   
        await sleep(0.05)
        
    print('forwarder: Hub Not Running')

Robot side

%%file build/robot_test.py

from pybricks.tools import wait
from pybricks.experimental import getchar

print('Hub: started')

while True:
    # listens to characters from the PC.
    char = getchar()
    
    # If we have something, echo it back

    if char is not None:
        print('Hub: char=',char)
        
    wait(50)

@johnscary-ev3
Copy link

Sorry to be back so soon.
I am trying to use the pybricksdev code on a VSC python project to send commands to the robot as above.
I moved the pybricksdev site-packages over to my project and modified the python path to find them.
However on the first from/import statement I get this error below.
It seems to be finding all the module files but on the last one, _openssl , it gives an error. It is a .pyd file and it does exist.
Any suggestions?

Traceback (most recent call last):
File "c:\Users\John\OneDrive\Documents\LEGO Creations\MINDSTORMS Robot Inventor\Pybricks\RemoteControl\RemoteControl_1.py", line 6, in
from pybricksdev.connections import BLEPUPConnection
File ".\site-packages\pybricksdev\connections.py", line 2, in
import asyncssh
File ".\site-packages\asyncssh_init_.py", line 31, in
from .agent import SSHAgentClient, SSHAgentKeyPair, connect_agent
File ".\site-packages\asyncssh\agent.py", line 30, in
from .public_key import SSHKeyPair, load_default_keypairs, load_keypairs
File ".\site-packages\asyncssh\public_key.py", line 45, in
from .encryption import get_encryption_params, get_encryption
File ".\site-packages\asyncssh\encryption.py", line 23, in
from .crypto import BasicCipher, GCMCipher, ChachaCipher, get_cipher_params
File ".\site-packages\asyncssh\crypto_init_.py", line 27, in
from .ec import ECDSAPrivateKey, ECDSAPublicKey, ECDH
File ".\site-packages\asyncssh\crypto\ec.py", line 24, in
from cryptography.hazmat.backends.openssl import backend
File ".\site-packages\cryptography\hazmat\backends\openssl_init_.py", line 7, in
from cryptography.hazmat.backends.openssl.backend import backend
File ".\site-packages\cryptography\hazmat\backends\openssl\backend.py", line 117, in
from cryptography.hazmat.bindings.openssl import binding
File ".\site-packages\cryptography\hazmat\bindings\openssl\binding.py", line 14, in
from cryptography.hazmat.bindings._openssl import ffi, lib
ImportError: DLL load failed while importing _openssl: The parameter is incorrect.
PS C:\Users\John\OneDrive\Documents\LEGO Creations\MINDSTORMS Robot Inventor\Pybricks>

@johnscary-ev3
Copy link

Hi
Figured out I need to use same virtual environment (.venv) as pybricksdev-demo to avoid these "from/import" errors.
Also found out I can use jupyter-notebook extension for VSC to run the notebook demo under VSC as a notebook.
That actually works well. I can also "export" from this notebook mode it to regular Python and that works, but only if I use the "Run Cell" commands embedded in it.
However when I try to run the same code as a regular Python program, not using "Run Cell", I get a lot of errors associated with the use of the "await" statements. Regular Python does not seem to like those.
Looks like the Notebook mode uses IPython which I guess does things differently. See below when I restart the notebook under VSC.
Any inputs?
Is it possible to do this in regular Python?
Maybe I can just use IPython under VSC without the notebook thing?

Restarted 'Python 3.8.6 64-bit ('.venv')' kernel
Python 3.8.6 (tags/v3.8.6:db45529, Sep 23 2020, 15:52:53) [MSC v.1927 64 bit (AMD64)]
Type 'copyright', 'credits' or 'license' for more information
IPython 7.19.0 -- An enhanced Interactive Python. Type '?' for help.

@dlech
Copy link
Member

dlech commented Dec 27, 2020

The notebooks do some magic to allow top level awaiting. In a regular Python program this is not allowed. You have to start your own async event loop instead. So you will need to move all of the await statements into a function and run it like this:

import asyncio

async def my_func():
    # replace this with your code
    print("start")
    await asyncio.sleep(5)
    print("end")

asyncio.run(my_func())

@johnscary-ev3
Copy link

johnscary-ev3 commented Dec 28, 2020

Hi,
Thanks very much for the input on this.
I restructured my program using your suggestion and now it works great under regular Python in VSC.
See below.
So now I can download my pybricks program and then send keyboard commands to the robot using VSC terminal window.
Also learned a lot on this one ;0)
Thanks again.

from pybricksdev.connections import BLEPUPConnection
from pybricksdev.ble import find_device
from asyncio import gather, sleep, run


async def main():
    print('main: Start')
    try:
        hub
    except:
        hub = BLEPUPConnection()   
        # You can search for the address like this:
        address = await find_device('Pybricks Hub', timeout=5)
        await hub.connect(address)
    await gather(
    hub.run('build/robot.py', print_output=True),
    forwarder(hub)
    )
    # Disconnect from the hub
    await hub.disconnect()
    print('main: Stop')


async def forwarder(hub):    
    print("forwarder: Start")
    command= ''   
    # Give the hubs some time to start
    while hub.state != hub.RUNNING :
        print('forwarder: Waiting for Hub to Run')
        await sleep(2)
    print('forwarder: Hub Running')   
    while hub.state == hub.RUNNING :             
        # Get new command 
        command = input()          
        # Try to send to the receiving hub
        if len(command) > 0:
            try:
                await hub.write(bytes([ord(command[0])]))
            except:
                pass          
        # wait for hub  to send any replies
        # 'y' = stop command so wait a bit longer
        if command[0] != 'y':
            await sleep(2)
        else:
            await sleep(4)     
             
    print('forwarder: Hub Not Running')

#start it up
run(main())

@johnscary-ev3
Copy link

johnscary-ev3 commented Dec 30, 2020

Update on this.
Found a way to do non-blocking keyboard input using the msvcrt module for the PC.
First you check if a key has been hit. If so you can read it and optionally echo it.
If no key has been hit the program continues so does not block reading things back from the robot.
Works well.

from pybricksdev.connections import BLEPUPConnection
from pybricksdev.ble import find_device
from asyncio import gather, sleep, run
import msvcrt


async def main():
    print('main: Start')
    try:
        hub
    except:
        hub = BLEPUPConnection()   
        # You can search for the address like this:
        address = await find_device('Pybricks Hub', timeout=5)
        await hub.connect(address)
    await gather(
    hub.run('build/robot.py', print_output=True),
    forwarder(hub)
    )

    # Disconnect from the hub
    await hub.disconnect()
    print('main: Stop')


async def forwarder(hub):    
    print("forwarder: Start")

    # Give the hubs some time to start
    while hub.state != hub.RUNNING :
        print('forwarder: Waiting for Hub to Run')
        await sleep(2)
    print('forwarder: Hub Running')

    # Keyboard command input loop  
    while hub.state == hub.RUNNING :             
        # Non blocking keyboard Input
        if msvcrt.kbhit():
            # get char with or without echo
            command = msvcrt.getch()
            # command = msvcrt.getche()
        else:  
            command= ''
        # Try to send to the receiving hub
        if len(command) > 0:
            try:
                await hub.write(bytes([ord(command)]))
            except:
                pass          
        # wait some    
        await sleep(1)

    # Hub has stopped
    print('forwarder: Hub Not Running')

#start it up
run(main())

@laurensvalk
Copy link
Member Author

Great find, and thanks for sharing!

@laurensvalk
Copy link
Member Author

laurensvalk commented Dec 30, 2020

I think you can simplify this part

    try:
        hub
    except:
        hub = BLEPUPConnection()   
        # You can search for the address like this:
        address = await find_device('Pybricks Hub', timeout=5)
        await hub.connect(address)

to simply

hub = BLEPUPConnection()   
address = await find_device('Pybricks Hub', timeout=5)
await hub.connect(address)

The try/except structure was used in the Jupyter Notebook example so you could run the whole code again without having to connect again. It did that by checking if the hub object already existed.

@johnscary-ev3
Copy link

Yes, I was just thinking the same thing ;0)
The try/except is not really needed there in my regular Python program.
Thanks for the feedback.

@dlech dlech added the topic: communication Issues related to hub-to-hub/phone/computer communcation label Jan 19, 2021
@laurensvalk
Copy link
Member Author

This is resolved, so let's close this issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
software: pybricksdev Issues related to the pybrickdev Python package support Request for technical support for a problem that is not a bug or feature request topic: communication Issues related to hub-to-hub/phone/computer communcation
Projects
None yet
Development

No branches or pull requests

3 participants