Pantilthat - library


#1

I am getting an error when I use a value from fieldstorage with servo_two(). This is the string containing the value:
stopallv=&lspeedsv=+0+&rspeedsv=+0+&pansv=&tiltsv=3&ledsv=

The error is <type ‘exceptions.ValueError’> Value ntiltv should be between -90 and 90 …

tiltsv=3 being the input variable and value which is between -90 and 90

The python code is:
if form.getfirst(“tiltsv”) != “”:
ntiltsv = int(form.getfirst(“tiltsv”))
PanTilt.servo_two(“ntiltv”)

The error report suggests the error is thrown by pantilthat.py line 137 - max=value_max))

The same error is reported if the value is 0. If i leave out the int() I get a message that the value is not a string or number.
Entering values via the console seems to work correctly but from python the values seem to be incremental rather than to a specific position e.g. if the value 10 were input twice rather than staying at 10 the servo would move to 20.

My difficulties are probably of my own making !! I’d be grateful for some help.


#2

I suspect it’s because your code should be:

if form.getfirst(“tiltsv”) != “”:
    ntiltsv = int(form.getfirst(“tiltsv”))
    PanTilt.servo_two(ntiltv) # <-- note the omission of quotes here

Since wrapping the variable name ntiltv in quotes, turns it into the literal string "ntiltv".


#3

Thanks for the quick response. removing the quotes has stopped the error. I am still having issues with the transfer of values from the web front end to the backend python routine which is running on an nginx/uwsgi server. The front end is sending correct values when the form changes but some are not processed and others seem to trigger the wrong function or in the case of the pantilthat the angle value seems to be added to the previous one rather than setting a specific position. Rather strange as I had used the basic structuure successfully previously. More digging at the weekend.


#4

Interesting- I’ve not done much with Python web backends beyond Flask- do you have any links that document the nginx/uwsgi setup you’re using?

Oh, and as an additional note - I have seen some very weird problems happen when Flask uses Reloader and ends up with multiple instances of a library running simultaneously. Pretty much all of our device libraries will go wrong in elaborate and unpredictable ways if you run more than one instance of them at a time. This could potentially be your problem.

Properly architecting a web back-end for something like Pan Tilt HAT where you might have multiple web-workers serving requests requires you run a singular daemon that handles control of the device, and then have your web-workers push commands into a queue (via whatever protocol/transport you deem most appropriate) and for the daemon to process that queue synchronously.


#5

Thanks for your reply, The basic structure is njinx directs php requests via PHP7 fastcgi socketThis is the main php front end albeit mostly jquery-ui items with a video stream from Rpi_Cam_Web_Interface which contains an mjpeg still stream. The main php page has a hidden form into which values for the two motors, pan and tilt, and lights are put. These are passed to the python backend via UWSGI using post whenever any value changes. All the values are reset to empty except the motor values.
The python backend reads the fieldstorage string and then the values should be applied to the i2c bus via a python driver e.g pantilthat.py (the motor one is thunderborg.py). The frontend is generating the values ok (5 in total) and they are being read and written to a temp file before the functions use them (from fieldstorage).
The Pi3 is not running anything else and there is only one client so I would have expected it to be able to cope with the volume. I am refining the setup to minimise the number of functions in the backend and maximise processing at the client side. Also setting up a switch to stop the video feed to see if the refresh on that is causing the problems. It will take a few days to get it all streamlined and test it but happy to let you see the scripts and nginx/uwsgi config files if you are interested. I have customised them from various sources. Appreciate this isn’t just a pantilthat issue.


#6

In working through streamlining I have run into another weird pantilthat problem. My intention was to create variable “tpos = 0” use get_pan to populate the variable tpos with the current angle and then add a number being the additional angle required. However, the call to pantilthat.get.pan() results in an error: Value 51200 should be between 575 and 2325. The same error comes up if I just enter import pantilthat and then pantilthat.get_hat() in the idle2 shell. The full cgi traceback and return text from the idle shell are in a word document in my OneDrive https://1drv.ms/w/s!AstwrstlVZLcgZQRCwvGvgri5WHl7w.
It seems as though pantilthat is reporting the servo position out of range before any value submitted!! which is bemusing!!


#7

Oooh, this was actually on my TODO list somewhere to look into. Someone was having trouble with embedding this into a camera control project, and was seeing a similar issue.

This looks like a bug in the firmware itself- which is initialising the SERVO1 and SERVO2 positions to 51200 instead of 200 (51200 is 200 << 8). It looks like an endianness issue has crept in.

I’ve pushed a library fix for this: https://github.com/pimoroni/pantilt-hat/commit/0fe6384ba2b9ab8c13ab135c1343317d345af7dd

You can install this version of the library from GitHub by doing the following:

git clone https://github.com/pimoroni/pantilt-hat
cd pantilt-hat/library
sudo python setup.py install

#8

Thanks for the quick reply…and solution. The update gives a sensible answer…and I thought it was me again!!


#9

First the good news, I have streamlined the code and found a couple of bits which were slowing processing. The web page is now passing values back to the server very quickly and uwsgi logs suggest handling times either 18-20 milliseconds or 4-8 milliseconds.
The not so good news is that there seems to be delays in the panhilthat responding to a angle instructions e.g. if I click the web page button for tilt up the console will show the 200 response with the new angle value i.e. get_tilt + 10 but the servo doesn’t run. The initial angle is set to 10 but it may be 30 before there is any movement. Seems odd since the response to get_tilt is near instantaneous. However, it is not consistent. Sometimes the angle will change straight away and step up or down in sequence to limit and back. ther times it can be 3 or 4 clicks before the servo responds.

The main motor control is working consistently without any delay also via a thunderborg i2c board (the pantilthat experience is the same whether the motors are running or not.

The inaction isn’t a major issue as the need to change angle will be infrequent just flagging it in case it causes issues for others.


#10

I wonder if this is related to: https://github.com/pimoroni/pantilt-hat/issues/9

Could your script to control Pan Tilt HAT be exiting and disabling the servos before they get time to move?


#11

I suspect that is probably the case. My web page is delivering 4 variables whenever any one of them changes. The python script if I understand it correctly is triggered whenever a set of values is passed and must run very quickly since a 200 response and replay of the values plus two variables linked to ultrasonic distance sensors are returned to the browser almost instantly. It is working better since I changed the python script to calculate the required angle of pan or tilt (getpan() or gettilt() + increment) instead of the web page. If I make two or three rapid and consecutive clicks on the pan or tilt button it is likely one or more won’t actually move the servo even though the value is reported back to he browser. Possibly the subsequent click is overwriting the previous one before it has been processed. The issue isn’t noticeably consistent, i.e. there have been times when I have made 3/4 rapid clicks and the servo has moved the same number of steps but others where 2 out of three are missed.
I am reasonably sure that the issue is related to the pantilthat python driver or the chip itself since the response to the variables passed via i2c to the Thunderborg motor controller is instantaneous even when they are being changed via a slider resulting in a very fast stream of variables. A difference is that the Thunderborg python module isn’t generally available - it sits in the folder of the script that is calling it. The pantilt and thunderborg modules look similar to my untrained eye suggesting the chip itself is slow processing write instructions.
My current setup is working well enough for my purposes but would be a problem if I were trying to track something. Hope the information helps.


#12

I think you need a separation of concerns to get things working properly- while you can disable the servo auto-off it’s not a good idea to keep the servos loaded.

Your best bet would be to run the Python script as a daemon, so that it is running continuously in the background, and then communicate with that from the web front-end.

There should be no need to tie your page response time to the time it takes to start up that script, initialise the library/driver IC, send the servo command, etc - that will just invite the very strangeness that you’re encountering.

In your case you might be able to use a Python script to arbitrate the servos and run the PanTilt HAT library continuously, and then a separate Python script to communicate with that one, which would save you making any significant changes to your current structure.

What does your complete Python script currently look like?


#13

This is the python script:

#!/usr/bin/python
# -*- coding: Latin -1

 
import sys, cgi, cgitb, os, tempfile, time, picamera, colorsys, math
import pantilthat, UltraBorg
PH = pantilthat.pantilthat
PH.servo_enable(1, True)
PH.servo_enable(2, True)

PH.light_mode( 1 )

UBD = UltraBorg.UltraBorg()
UBD.Init()         # Set the board up (checks the board is connected)

cgitb.enable(display=0, logdir="/var/log/cgi")


# import thunderborg python driver interface
import ThunderBorg
TB = ThunderBorg.ThunderBorg()
TB.i2cAddress = 0x29


# Setup the thunderborg

TB.Init()                          # Set the board up (checks the board is connected)

#set variables
var = 1
EXIT = 0

# setup application and read fieldstorage values to temp file and write a copy to cambot cgi-bin then read values into a form from the begining of the file.
def application(environ, start_response):
    temp = tempfile.NamedTemporaryFile(suffix='_store', prefix='tmp_', dir='/var/www/cambot/cgi-bin/', delete='false')
    temp.write(environ['wsgi.input'].read())
    temp.seek(0)

    form = cgi.FieldStorage(fp=temp, environ=environ, keep_blank_values=True)
    
# Ultrasonic Dist checks
    # Read all four ultrasonic values
    usm1 = UBD.GetDistance1()
    usm2 = UBD.GetDistance2()
    usm3 = UBD.GetDistance3()
    usm4 = UBD.GetDistance4()
    # Convert to the nearest millimeter
    usm1 = int(usm1)
    usm2 = int(usm2)
    usm3 = int(usm3)
    usm4 = int(usm4)
        
    if usm1 != 0 and usm1 <300 :
        status1 = "BLOCKED RL"
        print status1
    else:
        status1 = "CLEAR"
        print status1

    if usm2 != 0 and usm2 <300 :
        status2 = "BLOCKED RR"
        print status2
    else:
        status2= "CLEAR"
        print status2
        
    if usm3 != 0 and usm3 <300 :
        status3 = "BLOCKED FL"
        print status3
    else:
        status3 = "CLEAR"
        print status3

    if usm4 != 0 and usm4 <300 :
        status4 = "BLOCKED RL"
        print  status4
    else:
        status4 = "CLEAR"
        print status4

# is direction clear?
    if (status3 == "CLEAR" and status4 == "CLEAR") :
        ahead = "CLEAR"
        print "Ahead test = " + ahead
    else:
        ahead = "BLOCKED"
        print "Ahead test = " + ahead

    if (status1 == "CLEAR" and status2 == "CLEAR") :
        behind = "CLEAR"
        print "Behind test = " + behind
    else:
        behind = "BLOCKED"
        print "Behind test = " + behind


    #ahead = "CLEAR"
    #behind = "CLEAR"
    

# Get speed settings, determine direction, and whether clear 

    sld1move = float(form.getfirst( "lspeedsv" ))
    print sld1move
    sld2move = float(form.getfirst( "rspeedsv" ))
    print sld2move

# motors stopped
    if (sld1move == 0 and sld2move == 0):
        TB.MotorsOff()
        # set speed left motor
        TB.SetMotor2( float(sld1move) / 100.0)
        # set speed right motor    
        TB.SetMotor1( float(sld2move) / 100.0)

# motors both +    forward
    if (sld1move > 0 and sld2move > 0 ) :
        print "motor values greater than 0 ahead test = " + ahead
        if ahead =="CLEAR" :
            # set speed left motor
            TB.SetMotor2( float(sld1move) / 100.0)
            # set speed right motor    
            TB.SetMotor1( float(sld2move) / 100.0)
# Motors 1 + other 0 turn
    if (sld1move > 0 and sld2move == 0 ) or (sld1move == 0 and sld2move > 0 ) :
        print "motor values greater than 0 ahead test = " + ahead
        if ahead =="CLEAR" :
            # set speed left motor
            TB.SetMotor2( float(sld1move) / 100.0)
            # set speed right motor    
            TB.SetMotor1( float(sld2move) / 100.0)    
# motors one - one + spin
    if (sld1move > 0 and sld2move < 0 ) or (sld1move < 0 and sld2move > 0 ) :
        print "motor values greater than 0 ahead test = " + ahead
        if ahead =="CLEAR" :
            # set speed left motor
            TB.SetMotor2( float(sld1move) / 100.0)
            # set speed right motor    
            TB.SetMotor1( float(sld2move) / 100.0)  
# motors both -  backward          
        
    if (sld1move < 0 and sld2move < 0 ) :
        print "motor values less than 0 behind test = " + behind
        if behind == "CLEAR" :
            # set speed left motor
            TB.SetMotor2( float(sld1move) / 100.0)
            # set speed right motor    
            TB.SetMotor1( float(sld2move) / 100.0)
        

# set camera position
    camerapos = form.getfirst( "cameraposv")
    print camerapos
    global pmove
    global tmove
    pmove = 0
    tmove = 0
    if camerapos == "A" :
        tpos = 0
        movet = 10
        tpos = PH.get_tilt()
        tmove = (tpos - movet)
        if -89 <= tmove <= 89 :
            PH.servo_two( tmove )
    elif camerapos == "B" :
        tpost = 0
        movet = 10
        tpost = PH.get_tilt()
        tmove = (tpost + movet)
        if -89 <= tmove <= 89 :
            PH.servo_two( tmove )
    elif camerapos == "C" :
        ppos = 0
        movep = 10
        ppos = PH.get_pan()
        pmove = (ppos - movep)
        if -89 <= tmove <= 89 :
            PH.servo_one( pmove )
    elif camerapos == "D" :
        ppos = 0
        movep = 10
        ppos = PH.get_pan()
        pmove = (ppos + movep )
        if -89 <= tmove <= 89 :
            PH.servo_one( pmove )
    
# Led control
    global ledv
    ledv = form.getfirst("ledsv")
    if ledv == "led-on":
        PH.set_all(255,255,255) 
        PH.show()
        print "LED On"
        ledv = ""
    elif ledv == "led-off":
        PH.set_all(0,0,0) 
        PH.show()
        print "LED Off"
        ledv = ""
    
      
    start_response( '200 OK', [('Content-Type', 'text/html')])
    return [ "Recieved & Processed Left Speed = " + str( sld1move ) + "Right Speed = " + str( sld2move ) + "Blocked? front = " + ahead + " behind = " + behind + " Pan Pos. " + str( pmove ) + " Tilt Pos. " + str( tmove ) ]

This is the console log for one change showing the format of the fieldstorage string:

success lspeedsv=+0+&rspeedsv=+0+&ledsv=&cameraposv=D
cambot.php:464 create post is working!
cambot.php:475 Recieved & Processed Left Speed = 0.0Right Speed = 0.0Blocked? front = CLEAR behind = BLOCKED Pan Pos. 20 Tilt Pos. 0

The web server is setup to handle the php web page via fastcgi php7.0 fpm.socket whilst the cambot.py python script is run by uwsgi through a separate socket. The effect of the latter is that the python script is running from the start of njinx & uwsgi. I assume that pantilthat, thunderborg, and Ultraborg will be also be ready. The ultraborg is used to handle the four ultrasonic distance sensors as the driver gives a direct reading without messing with gpio pins.

Broadly this is working as expected although the sensor section works only when a change of speed is input but does not stop the cambot as it approaches an obstacle. I need to do some more research to find a way of looping the distance readings to fire’ motorsOff’ when an obstacle is detected in the line of travel.

I am sure this a far from optimal steup but is at the limit of my experience/knowledge!!


#14

You may find that this pattern: http://wsgi.tutorial.codepoint.net/parsing-the-request-post (Which is not a million miles removed from your set up) will fare better.

it looks like, as I’d guessed, your script is instantiated on-demand by the server, runs through the input commands, and then quits again. The startup/shutdown time of this script alone would;

  1. Make for awful performance
  2. Allow enough time for the weirdness you’re encountering to occur with Pan Tilt HAT… in some cases!

You should switch over to a continuously running WSGI server pattern as a first step and see if it works out the kinks.

This wont be your end goal, however, since you would next need to refactor your code slightly to split out the request/response implementation of WSGI from the more continuous heavy-lifting of your application.

For example- your Ultrasonic range checks could potentially be run continuously on a timer, and then your WSGI application need only copy the immediately-available latest values into its response.

Similarly, rather than directly setting a motor speed/direction your WSGI application should just drop the latest posted values into a global variable or state which can be read by the rest of the Python program and acted upon asynchronously.

But let’s not jump the gun! First you should be aiming to build a Python application that runs continuously and that can respond to WSGI posts.


#15

Oooh! I missed this bit. I’m still not 100% convinced this is the case. Judging by the pattern employed in your code- reading a WSGI post from an environment variable- I would expect the Python script to be starting up/exiting for every request processed. That said, this particular setup is new to me so I could be very wrong!

I’m guessing that - unless you’re always posting a valid speed value- if a change of speed is not included - then this line:

sld1move = float(form.getfirst( "lspeedsv" ))

…and ones similar to it, will be crashing with a typeerror, because an empty value or None type cannot be converted to a float:

>>> float("")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: could not convert string to float:


>>> float(None)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: float() argument must be a string or a number, not 'NoneType'

This will cause your whole script to bail


#16

Thanks for your advice. I will have a good look at the link you included in the first message and see if it offers a better approach.
I guess if I print a message ‘cambot.py starting now’ + time it will tell me whether or not the script is running continuously. I think the script probably is running continuously since the uwsgi log reports it starting up (including the initialisation messages from the Thunderborg and Ultraborg. and then Each value/response package is also reported until the size limit for the log is reached.
The front end always sends a lspeedsv value as part of the batch of variables being either 0 or a number or -number so that should be working o.k. I did run into this particular problem with the pantilt part of the script as my first approach was to set a value on the front end but there were times when no value was set and the script crashed hence the move to letters.

Thanks agian for your prompt help. I will let you know whether the current script is running continuously or if I find a cause of the delay problem.


#17

Update.
After further investigation I have established that the python cambot control script is started when uwsgi starts. However, reading the ultrasonic sensors seemed to introduce delays and affect processing. I have modified the setup so that the sensor reading and interpretation is in a separate python module which writes to a csv ffile. The php front end reads the csv file and adds the resulting ahead and behind vales to the html form. This allows them to be used in the cambot control script. A change in any value in the form results in all the values being written to fieldstorage.
To make this setup work I am running uwsgi in emperor mode (allows it to run multiple instances) with one instance running the cambot control as before and a second the dist_mon script. each has its own unix socket. nginx directs requests for the php front end to the php7fpm socket, and for the two uwsgi instance by specific python script location. This has seperated the traffic between front and backends and largely seems to have solved the problem of pantilt changes not being handled.
Using a csv file to hold the sensor values has caused problems as the browser cached the file rather than read it afresh on a set interval. Using Ajax “cache: false,” seems to have solved this.
So the setup is working adequately for now. The next step will be to find a way of getting the php/html front end to read the distmon generated sensor output without it being written to the csv file potentially simplifying the front end page processing (this isn’t too critical as the tablet I use for control has an i7 processor and lots of memory).


#18

Final Update.
At last found a solution. Created a Flask app to read the ultrasonic sensors and create a csv string ahead, value, behind, value, date/time which is the return value. The @approute is the input file for papaparse. the values are used to update the html form which ‘posts’ to the control script. The read is in a setInterval function which is set at 100 milliseconds. Net result is near real-time updating and changes to motor settings. This seems to have removed most of the problems with the pantilthat software. Very occasionally there is no response to a change of pan or tilt value but not frequently enough to cause me a problem.
It will be interesting to see if the 3b+ improves the performance of the setup.

Thanks for your help earlier and I hope the updates may be of use to others.