Accurate position measurement using low-cost cameras and OpenCV

There are many ways to sense the position of an object, and they’re generally either expensive or low-resolution. Laser interferometers are incredibly accurate, but the complex optics & electronics make the price very high. Hand-held laser measures are quite cheap, but they use a time-of-flight measurement method which limits their resolution, as light travels at roughly 1 foot (300 mm) per nanosecond, and making sub-nanosecond measurements isn’t easy. Lidar (light-based radar) is currently quite expensive, and has similar constraints. Ultrasonic methods benefit from the fact that sound waves travel at a much slower speed; they work well in constrained environments, such as measuring the height of liquid in a tank, but multipath reflections are a problem if there is more than one object in view.

Thanks to the smartphone boom, high-resolution camera modules are quite cheap, and I’ve been wondering whether they could be used to sense the position of an object to a reasonable accuracy for everyday measurements (at least 0.5 mm or 0.02 inches).

To test the idea I’ve set up 2 low-cost webcams at right-angles, to sense the X and Y position of an LED. To give a reproducible setup, I’ve engraved a baseboard with 1 cm squares, and laser-cut a LED support, so I can accurately position the LED and see the result.

The webcams are Logitech C270, that can provide an HD video resolution of 720p (i.e. 1280 x 720 pixels). For image analysis I’ll be using Python OpenCV; it has a wide range of sophisticated software tools, that allow you to experiment with some highly advanced methods, but for now I’ll only be using a few basic functions.

The techniques I’m using are equally applicable to single-camera measurements, e.g. tracking the position of the sun in the sky.

Camera input

My camera display application uses PyQt and OpenCV to display camera images, and it is strongly recommended that you start with this, to prove that your cameras will work with the OpenCV drivers. It contains code that can be re-used for this application, so is imported as a module.

Since we’re dealing with multiple cameras and displays, we need a storage class to house the data.

import sys, time, threading, cv2, numpy as np
import cam_display as camdisp

IMG_SIZE    = 1280,720          # 640,480 or 1280,720 or 1920,1080
DISP_SCALE  = 2                 # Scaling factor for display image
DISP_MSEC   = 50                # Delay between display cycles
CAP_API     = cv2.CAP_ANY       # API: CAP_ANY or CAP_DSHOW etc...

# Class to hold capture & display data for a camera
class CamCap(object):
    def __init__(self, cam_num, label, disp):
        self.cam_num, self.label, self.display = cam_num, label, disp
        self.imageq = camdisp.Queue.Queue()
        self.pos = 0
        self.cap = cv2.VideoCapture(self.cam_num-1 + CAP_API)
        self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, IMG_SIZE[0])
        self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, IMG_SIZE[1])

The main window of the GUI is subclassed from cam_display, with the addition of a second display area, and storage for the camera capture data:

# Main window
class MyWindow(camdisp.MyWindow):
    def __init__(self, parent=None):
        camdisp.MyWindow.__init__(self, parent)
        self.label.setFont(LABEL_FONT)
        self.camcaps = []
        self.disp2 = camdisp.ImageWidget(self)
        self.displays.addWidget(self.disp2)
        self.capturing = True

On startup, 2 cameras are added to the window:

if __name__ == '__main__':
    app = camdisp.QApplication(sys.argv)
    win = MyWindow()
    win.camcaps.append(CamCap(2, 'x', win.disp))
    win.camcaps.append(CamCap(1, 'y', win.disp2))
    win.show()
    win.setWindowTitle(VERSION)
    win.start()
    sys.exit(app.exec_())

As with cam_display, a separate thread is used to fetch data from the cameras:

    # Grab camera images (separate thread)
    def grab_images(self):
        while self.capturing:
            for cam in self.camcaps:
                if cam.cap.grab():
                    retval, image = cam.cap.retrieve(0)
                    if image is not None and cam.imageq.qsize() < 2:
                        cam.imageq.put(image)
                    else:
                        time.sleep(DISP_MSEC / 1000.0)
                else:
                    print("Error: can't grab camera image")
                    self.capturing = False
        for cam in self.camcaps:
            cam.cap.release()

Image display

A timer event is used to fetch the image from the queue, convert it to RGB, do the image processing, and display the result.

    # Fetch & display camera images
    def show_images(self):
        for cam in self.camcaps:
            if not cam.imageq.empty():
                image = cam.imageq.get()
                if image is not None and len(image) > 0:
                    img = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
                    cam.pos = colour_detect(img)
                    self.display_image(img, cam.display, DISP_SCALE)
                    self.show_positions()
    
    # Show position values given by cameras
    def show_positions(self, s=""):
        for cam in self.camcaps:
            s += "%s=%-5.1f " % (cam.label, cam.pos)
        self.label.setText(s)

Image processing

We need to measure the horizontal (left-to-right) position of the LED for each camera. If the LED is brighter than the surroundings, this isn’t difficult; first we create a mask that isolates the LED from the background, then extract the ‘contour’ of the object with the background masked off. The contour is a continuous curve that marks the boundary between the object and the background; for the illuminated LED this will approximate to a circle. To find an exact position, the contour is converted to a true circle, which is drawn in yellow, and the horizontal position of the circle centre is returned.

LOWER_DET   = np.array([240,  0,  0])       # Colour limits for detection
UPPER_DET   = np.array([255,200,200])

# Do colour detection on image
def colour_detect(img):
    mask = cv2.inRange(img, LOWER_DET, UPPER_DET)
    ctrs = cv2.findContours(mask, cv2.RETR_TREE,
                            cv2.CHAIN_APPROX_SIMPLE)[-2]
    if len(ctrs) > 0:
        (x,y),radius = cv2.minEnclosingCircle(ctrs[0])
        radius = int(radius)
        cv2.circle(img, (int(x),int(y)), radius, (255,255,0), 2)
        return x
    return 0

This code is remarkably brief, and if you’re thinking that I may have taken a few short-cuts, you’d be right:

Colour detection: I’ve specified the upper and lower RGB values that are acceptable; because this is a red LED, the red value is higher than the rest, being between 240 and 255 (the maximum is 255). I don’t want to trigger on a pure white background so I’ve set the green and blue values between 0 and 200, so a pure white (255,255,255) will be rejected. This approach is a bit too simplistic; if the LED is too bright it can saturate the sensor and appear completely white, and conversely another bright light source can cause the camera’s auto-exposure to automatically reduce the image intensity, such that the LED falls below the required level. The normal defence against this is to use manual camera exposure, which can be adjusted to your specific environment. Also it might be worth changing the RGB colourspace to HSV for image matching; I haven’t yet tried this.

Multiple contours: the findContours function returns a list of contours, and I’m always taking the first of these. In a real application, there may be several contours in the list, and it will be necessary to check them all, to find the most likely – for example, the size of the circle to see if it is within an acceptable range.

However, the measurement method does show some very positive aspects:

Complex background: as you can see from the image at the top of this blog, it works well in a normal office environment – no need for a special plain-colour background.

No focussing: most optical applications require the camera to be focussed, but in this case there is no need. I’ve deliberately chosen a target distance of approximately 4 inches (100 mm) that results in a blurred image, but OpenCV is still able to produce an accurate position indication.

Sub-pixel accuracy: with regard to measurement accuracy, the main rule for the camera is obviously “the more pixels, the better”, but also OpenCV can compute the position to within a fraction of a pixel. My application displays the position (in pixels) to one decimal place; at 4 inches (100 mm) distance, the Logitech cameras’ field of view is about 3.6 inches (90 mm), so if the position can be measured within, say, 0.2 of a pixel, this would be a resolution of 0.0006 inch (0.015 mm).

Of course these figures are purely theoretical, and the resolution will be much reduced in a real-world application, but all the same, it does suggest the technique may be capable of achieving quite good accuracy, at relatively low cost.

Single camera

With minor modifications, the code can be used in a single-camera application, e.g. tracking the position of the sun in the sky.

The code scans all the cameras in the ‘camcaps’ list, so will automatically adapt if there is only one.

The colour_detect function currently returns the horizontal position only; this can be changed to return the vertical as well. The show_positions method can be changed to display both of the returned values from the single camera.

Then you just need a wide-angle lens, and a suitable filter to stop the image sensor being overloaded. Sundial, anyone?

Source code

The ‘campos’ source code is available here, and is compatible with Windows and Linux, Python 2.7 and 3.x, PyQt v4 and v5. It imports my cam_display application, and I strongly recommended that you start by running that on its own, to check compatibility. If it fails, read the Image Capture section of that blog, which contains some pointers that might be of help.

Copyright (c) Jeremy P Bentham 2019. Please credit this blog if you use the information or software in it.

PC / RPi camera display using PyQt and OpenCV

OpenCV is an incredibly powerful image-processing tool, but it can be difficult to know where to start – how do you grab an image from a camera, and display it in a user-friendly GUI? This post describes such an application, that runs unmodified on a PC or Raspberry Pi, Windows or Linux, Python 2.7 or 3.x, and PyQt v4 or v5.

Installation

On Windows, the OpenCV and PyQt5 libraries can be installed using pip:

pip install numpy opencv-python PyQt5

If pip isn’t available, you should be able to run the module from the command line by invoking Python, e.g. for Python 3:

py -3 -m pip install numpy opencv-python PyQt5

Installing on a Raspberry Pi is potentially a lot more complicated; it is generally recommended to install from source, and for opencv-python, this is a bit convoluted. Fortunately there is a simpler option, if you don’t mind using versions that are a few years old, namely to load the binary image from the standard repository, e.g.

sudo apt update
sudo apt install python3-opencv python3-pyqt5 

At the time of writing, the most recent version of Raspbian Linux is ‘buster’, and that has OpenCV 3.2, which is quite usable. The previous ‘stretch’ distribution has python-opencv version 2.4, which is a bit too old: my code isn’t compatible with it.

With regard to cameras, all the USB Webcams I’ve tried have worked fine on Windows without needing to have any extra driver software installed; they also work on the Raspberry Pi, as well as the standard Pi camera with the ribbon-cable interface.

PyQt main window

Being compatible with PyQt version 4 and 5 requires some boilerplate code to handle the way some functions have been moved between libraries:

import sys, time, threading, cv2
try:
    from PyQt5.QtCore import Qt
    pyqt5 = True
except:
    pyqt5 = False
if pyqt5:
    from PyQt5.QtCore import QTimer, QPoint, pyqtSignal
    from PyQt5.QtWidgets import QApplication, QMainWindow, QTextEdit, QLabel
    from PyQt5.QtWidgets import QWidget, QAction, QVBoxLayout, QHBoxLayout
    from PyQt5.QtGui import QFont, QPainter, QImage, QTextCursor
else:
    from PyQt4.QtCore import Qt, pyqtSignal, QTimer, QPoint
    from PyQt4.QtGui import QApplication, QMainWindow, QTextEdit, QLabel
    from PyQt4.QtGui import QWidget, QAction, QVBoxLayout, QHBoxLayout
    from PyQt4.QtGui import QFont, QPainter, QImage, QTextCursor
try:
    import Queue as Queue
except:
    import queue as Queue

The main window is subclassed from PyQt, with a simple arrangement of a menu bar, video image, and text box:

class MyWindow(QMainWindow):
    text_update = pyqtSignal(str)

    # Create main window
    def __init__(self, parent=None):
        QMainWindow.__init__(self, parent)

        self.central = QWidget(self)
        self.textbox = QTextEdit(self.central)
        self.textbox.setFont(TEXT_FONT)
        self.textbox.setMinimumSize(300, 100)
        self.text_update.connect(self.append_text)
        sys.stdout = self
        print("Camera number %u" % camera_num)
        print("Image size %u x %u" % IMG_SIZE)
        if DISP_SCALE > 1:
            print("Display scale %u:1" % DISP_SCALE)

        self.vlayout = QVBoxLayout()        # Window layout
        self.displays = QHBoxLayout()
        self.disp = ImageWidget(self)    
        self.displays.addWidget(self.disp)
        self.vlayout.addLayout(self.displays)
        self.label = QLabel(self)
        self.vlayout.addWidget(self.label)
        self.vlayout.addWidget(self.textbox)
        self.central.setLayout(self.vlayout)
        self.setCentralWidget(self.central)

        self.mainMenu = self.menuBar()      # Menu bar
        exitAction = QAction('&Exit', self)
        exitAction.setShortcut('Ctrl+Q')
        exitAction.triggered.connect(self.close)
        self.fileMenu = self.mainMenu.addMenu('&File')
        self.fileMenu.addAction(exitAction)

There is a horizontal box layout called ‘displays’, that seems to be unnecessary as it only has one display widget in it. This is intentional, since much of my OpenCV experimentation requires additional displays to show the image processing in action; this can easily be done by creating more ImageWidgets, and adding them to the ‘displays’ layout.

Similarly, there is a redundant QLabel below the displays, which isn’t currently used, but is handy for displaying static text below the images.

Text display

It is convenient to redirect the ‘print’ output to the text box, rather than appearing on the Python console. This is done using the ‘text_update’ signal that was defined above:

    # Handle sys.stdout.write: update text display
    def write(self, text):
        self.text_update.emit(str(text))
    def flush(self):
        pass

    # Append to text display
    def append_text(self, text):
        cur = self.textbox.textCursor()     # Move cursor to end of text
        cur.movePosition(QTextCursor.End) 
        s = str(text)
        while s:
            head,sep,s = s.partition("\n")  # Split line at LF
            cur.insertText(head)            # Insert text at cursor
            if sep:                         # New line if LF
                cur.insertBlock()
        self.textbox.setTextCursor(cur)     # Update visible cursor

The use of a signal means that print() calls can be scattered about the code, without having to worry about which thread they’re in.

Image capture

A separate thread is used to capture the camera images, and put them in a queue to be displayed. The camera may produce images faster than they can be displayed, so it is necessary to check how many images are already in the queue; if more than 1, the new image is discarded. This prevents a buildup of unwanted images.

IMG_SIZE    = 1280,720          # 640,480 or 1280,720 or 1920,1080
CAP_API     = cv2.CAP_ANY       # or cv2.CAP_DSHOW, etc...
EXPOSURE    = 0                 # Non-zero for fixed exposure

# Grab images from the camera (separate thread)
def grab_images(cam_num, queue):
    cap = cv2.VideoCapture(cam_num-1 + CAP_API)
    cap.set(cv2.CAP_PROP_FRAME_WIDTH, IMG_SIZE[0])
    cap.set(cv2.CAP_PROP_FRAME_HEIGHT, IMG_SIZE[1])
    if EXPOSURE:
        cap.set(cv2.CAP_PROP_AUTO_EXPOSURE, 0)
        cap.set(cv2.CAP_PROP_EXPOSURE, EXPOSURE)
    else:
        cap.set(cv2.CAP_PROP_AUTO_EXPOSURE, 1)
    while capturing:
        if cap.grab():
            retval, image = cap.retrieve(0)
            if image is not None and queue.qsize() < 2:
                queue.put(image)
            else:
                time.sleep(DISP_MSEC / 1000.0)
        else:
            print("Error: can't grab camera image")
            break
    cap.release()

The choice of image size will depend on the camera used; all cameras support VGA size (640 x 480 pixels), more modern versions the high-definition standards of 720p (1280 x 720) or 1080p (1920 x 1080).

The camera number refers to the position in the list of cameras collected by the operating system; I’ve defined the first camera as number 1, but the OpenCV call defines the first as 0, so the number has to be adjusted.

The same parameter is also used to define the capture API setting; by default this is ‘any’, which usually works well; my Windows 10 system defaults to the MSMF (Microsoft Media Foundation) backend, while the Raspberry Pi defaults to Video for Linux (V4L). Sometimes you may need to force a particular API to be used, for example, I have a Logitech C270 webcam that works fine on Windows 7, but fails on Windows 10 with an ‘MSMF grab error’. Forcing the software to use the DirectShow API (using the cv2.CAP_DSHOW option) fixes the problem.

If you want to check which backend is being used, try:

print("Backend '%s'" % cap.getBackendName())

Unfortunately this only works on the later revisions of OpenCV.

Manual exposure setting can be a bit hit-and-miss, depending on the camera and API you are using; the default is automatic operation, and setting EXPOSURE non-zero (e.g. to a value of -3) generally works, however it can be difficult to set a webcam back to automatic operation: sometimes I’ve had to use another application to do this. So it is suggested that you keep auto-exposure enabled if possible.

[Supplementary note: it seems that these parameter values aren’t standardised across the backends. For example, the CAP_PROP_AUTO_EXPOSURE value in my source code is correct for the MSMF backend; a value of 1 enables automatic exposure, 0 disables it. However, the V4L backend on the Raspberry Pi uses the opposite values: automatic is 0, and manual is 1. So it looks like my code is incorrect for Linux. I haven’t yet found any detailed documentation for this, so had to fall back on reading the source code, namely the OpenCV videoio ‘cap’ files such as cap_msmf.cpp and cap_v4l.cpp.]

Image display

The camera image is displayed in a custom widget:

# Image widget
class ImageWidget(QWidget):
    def __init__(self, parent=None):
        super(ImageWidget, self).__init__(parent)
        self.image = None

    def setImage(self, image):
        self.image = image
        self.setMinimumSize(image.size())
        self.update()

    def paintEvent(self, event):
        qp = QPainter()
        qp.begin(self)
        if self.image:
            qp.drawImage(QPoint(0, 0), self.image)
        qp.end()

A timer event is used to trigger a scan of the image queue. This contains images in the camera format, which must be converted into the PyQt display format:

DISP_SCALE  = 2                 # Scaling factor for display image

    # Start image capture & display
    def start(self):
        self.timer = QTimer(self)           # Timer to trigger display
        self.timer.timeout.connect(lambda: 
                    self.show_image(image_queue, self.disp, DISP_SCALE))
        self.timer.start(DISP_MSEC)         
        self.capture_thread = threading.Thread(target=grab_images, 
                    args=(camera_num, image_queue))
        self.capture_thread.start()         # Thread to grab images

    # Fetch camera image from queue, and display it
    def show_image(self, imageq, display, scale):
        if not imageq.empty():
            image = imageq.get()
            if image is not None and len(image) > 0:
                img = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
                self.display_image(img, display, scale)

    # Display an image, reduce size if required
    def display_image(self, img, display, scale=1):
        disp_size = img.shape[1]//scale, img.shape[0]//scale
        disp_bpl = disp_size[0] * 3
        if scale > 1:
            img = cv2.resize(img, disp_size, 
                             interpolation=cv2.INTER_CUBIC)
        qimg = QImage(img.data, disp_size[0], disp_size[1], 
                      disp_bpl, IMG_FORMAT)
        display.setImage(qimg)

This demonstrates the power of OpenCV; with one function call we convert the image from BGR to RGB format, then another is used to resize the image using cubic interpolation. Finally a PyQt function is used to convert from OpenCV to PyQt format.

Running the application

Make sure you’re using the Python version that has the OpenCV and PyQt installed, e.g. for the Raspberry Pi:

python3 cam_display.py

There is an optional argument that can be used if there are multiple cameras; the default first camera is number 1.

On Linux, some USB Webcams cause a constant stream of JPEG format errors to be printed on the console, complaining about extraneous bytes in the data. There is some discussion online as to the cause of the error, and the cure seems to involve rebuilding the libraries from source; I’m keen to avoid that, so used the simple workaround of suppressing the errors by redirecting STDERR to null:

python3 cam_display.py 2> /dev/null

Fortunately this workaround is only needed with some USB cameras; the standard Raspberry Pi camera with the CSI ribbon-cable interface works fine.

Source code

Full source code is available here.

For a more significant OpenCV application, take a look at this post.

Copyright (c) Jeremy P Bentham 2019. Please credit this blog if you use the information or software in it.

Python WebSocket programming

Real-time display in a Web browser, using data pushed from a server.

winsock

A basic Web interface has a simple request/response format; the browser requests a Web page, and the server responds with that item. The browser’s request may contain parameters to customise the request, but the requests always come from the browser (i.e. ‘client pull’) rather than the server sending data of its own accord (‘server push’).

As browser applications became more sophisticated, there was a need for general-purpose communication channel between server and browser, so if the server has dynamic data (e.g. constantly fluctuating stock price) it can immediately be ‘pushed’ to the client for display. This is achieved by various extensions to the underlying Web transfer protocol (HTTP), and the latest version of the protocol (HTTP/2) has full support for multiple data streams, but I’ll start by creating a minimal application using a simpler HTTP extension that is compatible with all modern browsers, namely Websockets.

What is a socket?

A socket is a logical endpoint for TCP/IP communications, consisting of an IP address and a port number. On servers, the port number implicitly refers to the service you require from that server; for example, an HTTP Web page request is normally sent to to port 80, or port 443 for the secure version HTTPS.

However, there is no law that says HTTP transactions have to be on port 80; if you are running your own local Web server, you may well have set it up to respond on port 8080, since this is easier than using port 80: port numbers below 1024 are generally for use by the operating system, not user-space programs. You can tell the browser to access a specific port on the server by appending a colon and its number to the Web address.

An additional complication is that communications over the Internet have to get past firewalls, most of which are programmed to block communications on unknown port numbers. For the time being I’ll assume that we are using a private local network, so port 8000 will be fine for the Web server, and port 8001 for the Websocket server. In case you wondered, there is no real rationale behind these numbers; anything above 1023 would do.

Websocket

The protocol starts with a normal HTTP request from browser to Websocket server, but it contains an ‘upgrade’ header to change the connection from HTTP to Websocket (WS). If the server agrees to the change, the connection becomes a transparent data link between client & server, without the usual restrictions on HTTP content.

So the elements we need for a simple demonstration are:

  • Web (HTTP) server
  • Websocket (WS) server
  • Web browser
  • Web page with JavaScript code for a Websocket client

This may sound rather complicated, but the reality is really quite easy, as I’ll show below.

Web & Websocket servers

It is tempting to think of combining the Web & Websocket servers into a single entity, but in reality there are two very different requirements; the Web server churns out largely-static pages fetched from disk, while the Websocket server contains application-specific code to organise the flow of non-standard data across the network.

So the solution I’ve adopted is to keep the two servers separate. The simplest possible Web server is included within Python as standard, you just need to run:

# For python 2.7:
  python -m SimpleHTTPServer
# ..or for python3:
  python3 -m http.server

This makes all the files in your current directory visible in the browser, so you can just click on an HTML file to run it. A word of warning: this is can be a major security risk, as an attacker could potentially manipulate the URL to access other information on your system; use with caution.

Next, the Websocket server: there are a few Python libraries containing the protocol negotiation; I’ve chosen SimpleWebSocketServer, which can be installed with ‘pip’ as usual. A minimum of code is needed to make a functioning server (file: websock.py).

# Websocket demo, from iosoft.blog

import signal, sys
from SimpleWebSocketServer import WebSocket, SimpleWebSocketServer

PORTNUM = 8001

# Websocket class to echo received data
class Echo(WebSocket):

    def handleMessage(self):
        print("Echoing '%s'" % self.data)
        self.sendMessage(self.data)

    def handleConnected(self):
        print("Connected")

    def handleClose(self):
        print("Disconnected")

# Handle ctrl-C: close server
def close_server(signal, frame):
    server.close()
    sys.exit()

if __name__ == "__main__":
    print("Websocket server on port %s" % PORTNUM)
    server = SimpleWebSocketServer('', PORTNUM, Echo)
    signal.signal(signal.SIGINT, close_server)
    server.serveforever()

Web page

The browser has a built-in Websocket client, so the Web page just needs to provide:

  • Buttons to open & close the Websocket connection
  • A display of connection status, and Websocket data
  • Some Javascript to link the buttons & display to the Websocket client
  • A data source, that will be echoed back by the Python server

Once the Web page has been received and displayed, the user will click a ‘connect’ button to contact the Websocket server. However, the client needs to know the address of the server in order to make the connection; we could just ask the user to fill in a text box with the value, but it is much nicer for the client to work this out, based on the Web server’s address.

websock_page

Javascript provides a location.host variable that has the current IP address and port number, as shown above.

  // Client for Python SimpleWebsocketServer
  const portnum = 8001;
  var host, server, connected = false;

  // Display the given text
  function display(s)
  {
    document.myform.text.value += s;
    document.myform.text.scrollTop = document.myform.text.scrollHeight;
  }

  // Initialisation
  function init()
  {
    host = location.host ? String(location.host) : "unknown";
    host = host.replace("127.0.0.1", "localhost");
    server = host.replace(/:\d*\b/, ":" + portnum);
    document.myform.text.value = "Host " + host + "\n";
    window.setInterval(timer_tick, 1000);
  }

We use a regular expression to match the Web server port number, and change it to the Websocket server port, on the assumption that the two are hosted at the same IP address. There is also some code to handle the special case of an IP address 127.0.0.1. This address is used by a client, when it is running on the same system as the servers; it should be synonymous with ‘localhost’ but Windows seems to make a distinction between the two, so it is necessary to make a substitution.

Starting and stopping the Websocket connection is relatively straightforward:

  // Open a Websocket connection
  function connect()
  {
    var url = "ws://" + server + "/";
    display("Opening websocket " + url + "\n");
    websock = new WebSocket(url);
    websock.onopen    = function(evt) {sock_open(evt)};
    websock.onclose   = function(evt) {sock_close(evt)};
    websock.onmessage = function(evt) {sock_message(evt)};
    websock.onerror   = function(evt) {sock_error(evt)};
    connected = true;
  }
  // Close a Websocket connection
  function disconnect()
  {
    connected = false;
    websock.close();
  }

Once open, we can send data using a simple function call, and handle incoming data using the callback.

  // Timer tick handler
  function timer_tick()
  {
    if (connected)
      websock.send('*');
  }

  // Display incoming data
  function sock_message(evt)
  {
    display(evt.data);
  }

The resulting display shows the data that has been echoed back by the server:

websock_page2

Web page source

This is the complete source to the Web page (file: websock.html).

<!DOCTYPE html&gt;
<meta charset="utf-8"/&gt;
<title&gt;WebSocket Test</title&gt;
<script language="javascript" type="text/javascript"&gt;

  // Client for Python SimpleWebsocketServer
  const portnum = 8001;
  var host, server, connected = false;

  // Display the given text
  function display(s)
  {
    document.myform.text.value += s;
    document.myform.text.scrollTop = document.myform.text.scrollHeight;
  }

  // Initialisation
  function init()
  {
    host = location.host ? String(location.host) : "unknown";
    host = host.replace("127.0.0.1", "localhost");
    server = host.replace(/:\d*\b/, ":" + portnum);
    document.myform.text.value = "Host " + host + "\n";
    window.setInterval(timer_tick, 1000);
  }

  // Open a Websocket connection
  function connect()
  {
    var url = "ws://" + server + "/";
    display("Opening websocket " + url + "\n");
    websock = new WebSocket(url);
    websock.onopen    = function(evt) {sock_open(evt)};
    websock.onclose   = function(evt) {sock_close(evt)};
    websock.onmessage = function(evt) {sock_message(evt)};
    websock.onerror   = function(evt) {sock_error(evt)};
    connected = true;
  }
  // Close a Websocket connection
  function disconnect()
  {
    connected = false;
    websock.close();
  }

  // Timer tick handler
  function timer_tick()
  {
    if (connected)
      websock.send('*');
  }

  // Display incoming data
  function sock_message(evt)
  {
    display(evt.data);
  }

  // Handlers for other Websocket events
  function sock_open(evt)
  {
    display("Connected\n");
  }
  function sock_close(evt)
  {
    display("\nDisconnected\n");
  }
  function sock_error(evt)
  {
    display("Socket error\n");
    websock.close();
  }

  // Do initialisation when page is loaded
  window.addEventListener("load", init, false);

</script&gt;
<form name="myform"&gt;
  <h2&gt;Websocket test</h2&gt;
  <p&gt;
  <textarea name="text" rows="10" cols="60"&gt;
  </textarea&gt;
  </p&gt;
  <p&gt;
  <input type="button" value="Connect" onClick="connect();"&gt;
  <input type="button" value="Disconnect" onClick="disconnect();"&gt;
  </p&gt;
</form&gt;
</html&gt; 

Running the demonstration

To run the demonstration, open 2 console windows on the server, and change to a suitable working directory containing the HTML and Python files websock.html and websock.py. In the first window, run the Web server of your choice; you can just run the built-in Python server:

# For python 2.7:
  python -m SimpleHTTPServer
# ..or for python3:
  python3 -m http.server

..but this is relatively insecure, so is only suitable for an isolated private network.

In the second console window, run the ‘websock.py’ application; the console should report:

Websocket server on port 8001

Now run a browser on any convenient system, and enter the address of the server, including the Web server port number after a colon, e.g.

10.1.1.220:8000

You should now see the home page of the Web server; if you are using the built-in Python server, there should be a list of files in the current directory. Click on websock.html, then the connect button; an asterisk should appear every second, having been generated by the Javascript client, and echoed back by the Websocket server. To stop the test, click the disconnect button.

In the next post, I will show how this technique can be expanded to provide a graphical real-time display of server data, watch this space…

Copyright (c) Jeremy P Bentham 2019. Please credit this blog if you use the information or software in it.

Web display for OpenOCD

 

ocd_web1

In my reporta project, I used a PyQt program to drive an FTDI adapter, producing a graphical display of the CPU’s internals: a real-time animation showing the I/O states, that doesn’t require any additional programming on the target system.

This post aims to produce a more powerful version, namely:

  • Use a Raspberry Pi as the interface to the CPU (SWD or JTAG)
  • Allow remote diagnosis, by using network communications
  • Use standard web browser graphics in place of PyQt

There are many advantages to using the Web browser as a display tool; most importantly, there is no need to install extra software on the display system; you can even use your smartphone as a display device. Wireless communication between the data acquisition & display can be really useful when working on real-world industrial systems, which are often in cramped and inaccessible locations.

First we have to create the display graphic, and I’m using Scalable Vector Graphics (SVG). Since everything is drawn on-the-fly from 2-dimensional x,y positions, it automatically resizes from large to small screens, which is important for mobile devices.

Scalable Vector Graphics

In a previous post, I created some simple graphics in SVG; now I need to draw something that looks like my demonstration target system, with a pushbutton, seven-segment display and ‘blue pill’ STM32F103 CPU module:

target_sys

My previous PyQt display looked like this:

reporta

..but we can do better than that! My first idea was to create the SVG graphics in Inkscape, then add Javascript code to animate them. The problem with this approach is the very large number of tools & settings in Inkscape; it is easy to create something that looks really good visually, but is extremely difficult (or impossible) to animate. So it is much easier to create the SVG from scratch using the Python ‘svgwrite’ library; the display elements can be structured so as to make animation easy.

Background

The background component is a solderless breadboard, with holes at 0.1 inch pitch. This can be created in SVG using a ‘pattern’:

import svgwrite
PIN_PITCH   = 10
PIN_SIZE    = 2
BB_SIZE     = PIN_PITCH*31, PIN_PITCH*11
TILE_SIZE   = PIN_PITCH, PIN_PITCH
TILE_CENTRE = PIN_PITCH/2.0,PIN_PITCH/2.0

# Create maximised SVG drawing
def create_svg(fname, size):
    return svgwrite.Drawing(fname,
            width="100%", height="100%",
            viewBox=("0 0 %u %u" % size),
            debug=False)

# Add a breadboard background pattern
def add_breadboard(dwg, pos=(XPAD,TPAD), size=BB_SIZE):
    dots = svgwrite.pattern.Pattern(width=TILE_SIZE[0], height=TILE_SIZE[1],
                                    id="dots", patternUnits="userSpaceOnUse")
    dots.add(dwg.rect((0,0), TILE_SIZE, fill="#f0f0f0"))
    dots.add(dwg.circle(TILE_CENTRE, 1, fill="white"))
    dwg.defs.add(dots)
    dwg.add(dwg.rect(pos, size, stroke="darkgray", fill="url(#dots)", filter="url(#shadow)"))

dwg = create_svg(FNAME, DWG_SIZE)
add_breadboard(dwg)

A single light grey tile of 10 x 10 units is defined, with a small white dot in the middle. This is used to fill a full-size rectangle; the SVG interpreter automatically duplicates the tile to fill the area.

You may wonder why I have chosen to make the holes 10 units apart, instead of redefining the SVG coordinate system so they are 0.1 units apart, to match the real-world value. The reason is that I’ve found the 10-unit convention to be much more convenient, as it allows positioning to be done with integer values, and the default line width of 1 unit looks fine, so doesn’t need to be modified.

CPU module

ocd_web_cpu

A blue rectangle is created, with red ‘pins’ that can be animated to show the I/O on/off status. A list is used to define the pins and their I/O functions:

# STM32F103 'blue pill' board pinout, starting top left
BOARD_PINS=("GND", "GND", "3V3", "NRST","PB11","PB10","PB1", "PB0", "PA7", "PA6",
            "PA5", "PA4", "PA3", "PA2", "PA1", "PA0", "PC15","PC14","PC13","VBAT",
            "PB12","PB13","PB14","PB15","PA8", "PA9", "PA10","PA11","PA12","PA15",
            "PB3", "PB4", "PB5", "PB6", "PB7", "PB8", "PB9", "5V",  "GND", "3V3")

CSS styles are used to define the box colour and pin size. The pin text is also defined, using a ‘writing mode’ of top-to-bottom, which produces the vertical labels.

STYLES = """
    .cpu_style     {stroke:darkblue; stroke-width:1; fill:#b0c0e0}
    .pin_style     {stroke:red; stroke-width:1; fill:red}
    .pin_text      {font-size:6px; writing-mode:tb; font-family:Arial}
"""

dwg.defs.add(dwg.style(STYLES))

It is then just a question of iterating across the pins, drawing them and the optional text labels; these are optional so the same code can be used to draw the (unlabeled) seven-segment display pins.

# Add a dual-in-line part
def add_dil_part(dwg, pos, row_pitch, idents, label=False, style="part_style"):
    g = Group(transform="translate"+str(pos), class_="pin_text")
    row_pins = len(idents) / 2
    g.add(dwg.rect((0,0), (row_pins*PIN_PITCH, row_pitch), class_=style))
    for n, ident in enumerate(idents):
        pos = pin_pos(n, row_pins, PIN_PITCH/2,
                      (PIN_PITCH/2, row_pitch-PIN_PITCH/2))
        g.add(dwg.circle(pos, PIN_SIZE/2, class_="pin_style", id=ident))
        if label:
            pos = pin_pos(n, row_pins, PIN_PITCH/2,
                          (PIN_PITCH, row_pitch-PIN_PITCH*2.5))
            g.add(svgwrite.text.Text(ident, pos))
    dwg.add(g)

add_dil_part(dwg, (100,30), PIN_PITCH*7, BOARD_PINS, True, "cpu_style")

A ‘group’ is used to house the complete part, so it can be styled and positioned as a single item.

Seven-segment display

ocd_web_disp

The same dual-in-line code is used to draw the component base; the pins aren’t actually visible in the real part, but have been included as a handy on/off status indication.

The display segments are drawn using a list of points, arranged so that the line drawing is sequential; this is transformed by the ‘zip’ function into a list of start & end points for each line:

# Dimensions of 7-seg display
D7W,D7H,D7L = 20,20,2       # X and Y seg length, and X-direction lean

# Segment endpoints in the order FABCDEG (for continous drawing)
SEG_LINES   = ((D7L, D7H), (D7L*2,0),(D7L*2+D7W,0),(D7L+D7W,D7H),
               (D7W,D7H*2),(0,D7H*2),(D7L,D7H),    (D7L+D7W,D7H))

# Idents for the display pins, starting top left
DISP_PINS = ("PB11","PB10","GND", "PB1", "PB0",
             "PB12","PB13","GND", "PB14","PB15")

# Idents for the segments, in order ABCDEFGH
SEG_PINS =  ("PB1", "PB0", "PB14","PB13",
             "PB12","PB10","PB11","PB15")

STYLES = """
    .seg_stroke    {stroke:#00a000; stroke-width:5; stroke-linecap:round}
"""

# Add 7 display segments
def add_disp_segs(dwg, pos):
    g = Group(transform="translate"+str(pos), class_="seg_stroke")
    lines = zip(SEG_LINES[:-1], SEG_LINES[1:])
    for n, line in enumerate(lines):
        g.add(dwg.line(*line, id=SEG_PINS[n]))
    dwg.add(g)

Pushbutton

A simple square-plus circle gives an approximation to the real button. The square has slightly rounded corners, using the ‘rx’ parameter.

PB_SIZE = 20

# Add a pushbutton
def add_pb(dwg, pos, ident, size=PB_SIZE, fill="darkred"):
    g = Group(transform="translate"+str(pos))
    g.add(dwg.rect((0,0), (size,size), rx=2, fill=fill, opacity=0.8))
    g.add(dwg.circle((size/2,size/2), size/2, fill=fill, id=ident))
    dwg.add(g)

Drop shadow

Adding a drop shadow to a component is a simple way of creating a 3-dimensional effect.

ocd_web2

Confusingly, there are two ways this effect can be achieved; using a CSS definition, or an SVG filter. The CSS method is simpler (since the CSS functionality is a subset of the SVG functionality) but doesn’t work on all browsers, so I’ve used the SVG method instead.

The filter definition consists of a series of steps, with an input and an output; the steps I’ve used are:

  • Get the alpha (i.e. the monochrome) values of the image, and offset by 2 units
  • Add Gaussian blur to the offset image
  • Combine the original image with the offset & blurred image
# Define a shadow filter
def define_shadow(dwg):
    f = dwg.defs.add(dwg.filter(id="shadow", x=0, y=0, width="150%", height="150%"))
    f.feOffset(in_="SourceAlpha", result="AlphaOset", dx="2", dy="2")
    f.feGaussianBlur(in_="AlphaOset", result="AlphaBlur", stdDeviation=2)
    f.feBlend(in_="SourceGraphic", in2="AlphaBlur", mode="normal")

# Add filter to a rectangle, e.g. for the breadboard:
    dwg.add(dwg.rect(pos, size, stroke="darkgray", fill="url(#dots)", filter="url(#shadow)"))

Note the appending of an underscore to ‘in’. This is necessary to avoid a Python syntax error; it is stripped off when the SVG output file is written

Control button, and text display

We need some method of controlling the data connection between the browser and Web server, also displaying the current status. This is achieved by adding an area at the bottom of the graphic.

ocd_web1

The ‘connect’ button is drawn as a group, containing a rounded-corner rectangle, and text.

CTRL_SIZE    = 60,19

STYLES = """
    .ctrl_style    {stroke:black; stroke-width:0.5;
                    font-size:9px; font-family:Arial; text-anchor:middle}
"""

# Add a pushbutton control
def add_ctrl_button(dwg, pos, ident, text, onclick, size=CTRL_SIZE, fill="palegreen"):
    g = Group(transform="translate"+str(pos), onclick=onclick, class_="ctrl_style")
    g.add(dwg.rect((0,0), size, rx=5, fill=fill))
    g.add(svgwrite.text.Text(text, (size[0]/2,12), fill="black", id=ident))
    dwg.add(g)

add_ctrl_button(dwg, (20,133), "button1", "Connect", "click_handler()")

The ‘onclick’ parameter will trigger the given JavaScript function when the button is clicked, e.g.

var connected=0;
function click_handler()
{
    if (connected)
        disconnect();
    else
        connect();
}

The status display consists of 2 lines of text; there is no need for scrolling, so the lines are tagged individually:

TEXTBOX_SIZE = 200,20

STYLES = """
    .textbox_style {stroke-width:1.0; stroke:lightgray; fill:none}
    .text_style    {font-size:8px; font-family:Courier}
"""

# Add a text area
def add_textbox(dwg, pos, size=TEXTBOX_SIZE):
    g = Group(transform="translate"+str(pos))
    g.add(dwg.rect((0,0), size, class_="textbox_style"))
    g.add(svgwrite.text.Text("Line1", (5,8), class_="text_style", id="text1"))
    g.add(svgwrite.text.Text("Line2", (5,17), class_="text_style", id="text2"))
    dwg.add(g)

Updating the text in Javascript just requires the ‘textContent’ to be set, e.g.:

// Connect to host
    function connect()
    {
        text2.textContent = "Connected";
        button1.textContent = "Disconnect";
        connected = 1;
    }

    // Disconnect from host
    function disconnect()
    {
        text2.textContent = "Disconnected";
        button1.textContent = "Connect";
        connected = 0;
    }

 

To be concluded…

The next blog will describe how Raspberry Pi OpenOCD data is used to animate the graphics. It will include a link to the full source code.

Copyright (c) Jeremy P Bentham 2019. Please credit this blog if you use the information or software in it.

3D design with Python and FreeCAD

3D CAD packages can be hard work; there is a lot to learn, which can be a major problem for an infrequent user such as myself.

Most packages support some form of scripting, so why not program my complete design from scratch, without touching the GUI? FreeCAD is a (free) 3D design package, with a comprehensive Python interface, so seems to be ideal…

freecad5

This is simple in theory, but a bit tricky in practice; I’ll spare you the many frustrating false-starts I’ve made, and describe some simple ways of producing 3D objects from scratch in Python. This is very much a work-in-progress, but hopefully will provide some useful pointers if you’re a Python programmer doing occasional 3D design.

The examples here have been tested with FreeCAD v0.16, and the current version 0.18

Running Python code

There are several ways of running a Python script in FreeCAD:

1. Entering commands at the console

In the FreeCAD Python console window, try entering:

FreeCAD.newDocument("Unnamed")
import Part
box = Part.makeBox(4, 3, 2)
Part.show(box)

The result is a bit underwhelming; all you can see is the bottom left-hand corner of a square. If you want to see the box in its full 3-D glory, either use the GUI controls to change the viewpoint, or add the following 2 lines:

FreeCADGui.activeDocument().activeView().viewAxonometric()
FreeCADGui.SendMsgToActiveView("ViewFit")
2. Executing as a Macro

Click on Macro then Macros… and you are given a list of Python macro files that can be executed. They are stored in the default location for scripts; you can alter this to a directory of your choosing, by changing the ‘user macros location’ in the dialog box. If you make that change, it is necessary to exit & re-enter FreeCAD for the change to take effect.

If FreeCAD encounters a problem with your script, it will generally give a sensible error message, however very occasionally a script can corrupt the internals of the program, so it fails to respond in the usual manner. Hence, if you are experiencing problems with previously-good code that suddenly doesn’t work, it is worth restarting FreeCAD in case this fixes the problem.

3. Using the FreeCAD editor

You can load a Python script into FreeCAD using the normal GUI File Open. You are then presented with a nice-looking editor window, into which you can paste one of the examples from this blog. When complete, the file can be run by pressing ctrl-F6. Unfortunately, there are some subtle differences when executing a file in this manner, as opposed to the other methods, see my usage of the recompute() function in the later code examples.

4. Executing a file from the FreeCAD Python console

You can directly execute a file by entering a command at the Python console, e.g.

exec(open("/Projects/FreeCAD/test.py").read())

 

Design methodology

Like many 3D CAD packages, FreeCAD uses the Constructive Solid Geometry (CSG) method, where the final design is built up by adding (fusing) elements together, and subtracting (cutting) one element from another. Simple 3D  objects (cube, cylinder etc.) can be created with a single line of code:

# Simple test of FreeCAD Part scripting, from iosoft.blog

from FreeCAD import Vector
import Part

if FreeCAD.ActiveDocument:
FreeCAD.closeDocument("Unnamed")
doc = FreeCAD.newDocument("Unnamed")
plate = Part.makeBox(50, 30, 2)
verticals = [edge for edge in plate.Edges if edge.BoundBox.ZLength]
plate = plate.makeFillet(5, verticals)
cyl = Part.makeCylinder(10, 2, Vector(20, 15, 0))
plate = plate.cut(cyl)
doc.addObject("Part::Feature", "plate").Shape = plate
FreeCADGui.activeDocument().activeView().viewAxonometric()
FreeCADGui.SendMsgToActiveView("ViewFit")

The result is a plate with rounded corners, and a large off-centre hole:

freecad1

However the code is more than a single line, so some explanations are in order:

Lines 6-8 remove a previous unnamed document, and create a new one. This means that every time you run the script you get a new clean document to display the result. As a safeguard, if you currently have a named document open, the script will error out

Line 9 creates a square plate, size 50 x 30 units, and 2 units thick.

Lines 10 & 11 create the rounded corners (‘fillets’ in FreeCAD terminology), with a radius of 5 units. The makeFillet method requires a list of edges, and we only want to modify the vertical edges, so the list of edges is filtered by checking the z-dimension length is non-zero.

Lines 12 & 13 create a cylinder with 10 units radius and 2 units high, offset from the origin using a ‘Vector’ object, which defines a position in 3-dimensional (x,y,z) space. The plate is then cut with the cylinder, creating a hole 20 units in diameter.

Line 14 adds the resulting object to the current document; without this step, the object won’t be visible.

Lines 15 & 16 set a perspective view, and adjust the zoom level so the part fits in the display space.

The ‘labels and attributes’ on the left-hand side of the screen show there is only one defined object, named ‘plate’; if you click on that you can modify its placement (i.e. its position and orientation) but none of the other design parameters.

Workbenches

FreeCAD has various sets of software tools, divided up into ‘workbenches’. This division isn’t just for the GUI, it also applies to scripting; for example, the above Python code used tools imported from the Part workbench. There is some overlap between workbenches, so there can be other ways of creating the same object, for example starting with 2-dimensional sketches in the Draft workbench:


# Simple test of FreeCAD Draft scripting, from iosoft.blog

from FreeCAD import Vector
import Draft

if FreeCAD.ActiveDocument:
FreeCAD.closeDocument("Unnamed")
doc = FreeCAD.newDocument("Unnamed")

rect = Draft.makeRectangle(50, 30, face=True)
rect.FilletRadius = 5
rect.ViewObject.Visibility = False
plate = doc.addObject("Part::Extrusion", "plate")
plate.Base, plate.Dir = rect, Vector(0, 0, 2)

circ = Draft.makeCircle(10, face=True)
rotation = App.Rotation(Vector(0,0,0), 0)
circ.Placement = App.Placement(Vector(20, 15, 0), rotation)
circ.ViewObject.Visibility = False
cyl = doc.addObject("Part::Extrusion", "cylinder")
cyl.Base, cyl.Dir = circ, Vector(0, 0, 2)

FreeCADGui.activeDocument().activeView().viewAxonometric()
FreeCADGui.SendMsgToActiveView("ViewFit")

cutplate = doc.addObject("Part::Cut", "cutplate")
cutplate.Base, cutplate.Tool = plate, cyl

doc.recompute()

To turn the 2-dimensional x-y sketches into 3-dimensional objects, they are extruded in the z-plane. The Draft workbench has no extrusion capability, so this is done by adding extrusion objects to the document. It is important to set the original 2-dimensional sketches as transparent (set visibility false), otherwise they will form a thin layer that obscures the cutout.

I must explain the last 5 lines of code, where I set the viewing mode, cut the plate with the cylinder, then do a recompute. This is a workaround for some minor issues I found in the current FreeCAD versions, which may well be fixed by now:

  • If the view mode is set after doing the cut, you don’t get the correct perspective view.
  • If the final recompute is omitted, everything works fine when executed as a standalone script, but when the same script is executed from the edit screen, the document is blank; none of the objects are visible.

Apart from these issues, the graphical end result looks exactly the same as with the Part workbench, but note the collection of nested objects in the left-hand window. This is CSG in action; the cut plate is derived from a plate and a cylinder, which in turn are derived from a rectangle and circle.

freecad2

An advantage of this object hierarchy is that the design parameters can be changed within the GUI; for example, try changing the circle radius in the attributes window from 10 to 14 units, hit F5 to recompute, and the hole diameter will increase to 28 units. The primary focus of this blog is to use scripting rather than the GUI, but it can be useful to manually change a few parameters, and instantly see the result.

An interesting quirk of this hierarchy is that the cut plate has effectively taken possession of the circle and cylinder, since they are its sub-objects. This raises the question: what happens if we cut 2 plates with the same cylinder, which of the plates will own the cylinder? Let’s try it out:


# Simple test of FreeCAD CSG hierarchy, from iosoft.blog

from FreeCAD import Vector
import Draft

if FreeCAD.ActiveDocument:
FreeCAD.closeDocument("Unnamed")
doc = FreeCAD.newDocument("Unnamed")

rect = Draft.makeRectangle(50, 30, face=True)
rect.FilletRadius = 5
rect.ViewObject.Visibility = False
plate = doc.addObject("Part::Extrusion", "plate")
plate.Base, plate.Dir = rect, Vector(0, 0, 2)

plate2 = doc.addObject("Part::Extrusion", "plate2")
plate2.Base, plate2.Dir = rect, Vector(0, 0, 2)
rotation = App.Rotation(Vector(0,0,0), 0)
plate2.Placement = App.Placement(Vector(0, 0, 10), rotation)

circ = Draft.makeCircle(10, face=True)
circ.Placement = App.Placement(Vector(20, 15, 0), rotation)
circ.ViewObject.Visibility = False
cyl = doc.addObject("Part::Extrusion", "cylinder")
cyl.Base, cyl.Dir = circ, Vector(0, 0, 12)

FreeCADGui.activeDocument().activeView().viewAxonometric()
FreeCADGui.SendMsgToActiveView("ViewFit")

cutplate = doc.addObject("Part::Cut", "cutplate")
cutplate.Base, cutplate.Tool = plate, cyl

cutplate2 = doc.addObject("Part::Cut", "cutplate2")
cutplate2.Base, cutplate2.Tool = plate2, cyl

doc.recompute()

..and the answer to the question is..

freecad3

FreeCAD v0.18 has duplicated the single cylinder, so it appears twice in the hierarchy. Even though they appear to be separate, these 2 circles & cylinders are actually linked; if you change the radius of one circle, both hole sizes change. The trick is to note that the duplicate objects have the same name; if they were separate items, FreeCAD would have automatically renamed the second one by adding a numeric suffix.

FreeCAD v0.16 does not duplicate the parts, the circle & cylinder only appear once under cutplate2, so caution is needed when working with the older version.

Copyright (c) Jeremy P Bentham 2019. Please credit this blog if you use the information or software in it.

PyQt serial terminal

A simple demonstration of threading in PyQt

I do a lot of work with serial comms; TeraTerm is an excellent serial terminal, but sometimes a customised application is required, for example when dealing with binary data.

The following blog describes a simple Windows & Linux serial terminal, that can be adapted to handle special protocols; it also demonstrates the creation of Python threads, and can serve as a basis for other multi-threaded applications.

pyqt_serialterm1

It won’t win any awards for style, but could come in handy next time you encounter an obscure serial protocol.

Compatibility

My code is compatible with Python 2.7 and 3.x, PyQt v4 and v5, running on Windows or Linux. This necessitates some rather clunky inclusions at the top of the file:

try:
    from PyQt4 import QtGui, QtCore
    from PyQt4.QtGui import QTextEdit, QWidget, QApplication, QVBoxLayout
except:
    from PyQt5 import QtGui, QtCore
    from PyQt5.QtWidgets import QTextEdit, QWidget, QApplication, QVBoxLayout
try:
    import Queue
except:
    import queue as Queue

There is also an issue with the different ways Python 2 and 3 handle serial data; older versions assume that the data type is an ASCII string, while in later versions it is a binary type.

This can lead to all sorts of problems (such as unwanted exceptions) so I’ve included specific functions to ensure that all outgoing data is converted from the internal representation (which could be Unicode) to a string of bytes, and incoming data is converted from the external type (data bytes or string) to a string type:

# Convert a string to bytes (for Python 3)
def str_bytes(s):
    return s.encode('latin-1')

# Convert bytes to string (if Python 3)
def bytes_str(d):
    return d if type(d) is str else "".join([chr(b) for b in d])

You’ll need to install pySerial by the usual methods (e.g. ‘pip install’).

Using threading

The key problem with serial communications is the time it takes; the slower the data rate, the longer the delay before anything happens. Without threading (or any other mitigation scheme) the User Interface (UI) will lock up after each command, waiting for the response. At best, this makes the application appear sluggish and unresponsive; at worst, it can appear to have failed, waiting for a response that never comes.

Threading allows the UI to carry on interacting with the user, while simultaneously keeping the serial link alive. Creating a new thread is really easy in Python; you just subclass QThread, and instantiate it:

class SerialThread(QtCore.QThread):
    def __init__(self, portname, baudrate):
        QtCore.QThread.__init__(self)
        self.portname, self.baudrate = portname, baudrate
...
class MyWidget(QWidget):
    ...
    self.serth = SerialThread(portname, baudrate)

I have chosen to supply the serial parameters (port name & baud rate) when the thread object is instantiated, so they are available when the thread is started; this is done by calling the start() method, which will call a run() method in the QThread class:

# In serial thread:
def run(self):
    print("Opening %s at %u baud" % (self.portname, self.baudrate))
...
# In UI thread:
    self.serth.start()

The run() method needs to keep running in a perpetual loop, but it must be possible to terminate that loop when the program exits – Python won’t do it automatically. I use a global variable, that can be set false to terminate, e.g. in pseudocode:

def run(self):
    [starting: open serial port]
    while self.running:
        [check for incoming characters]
        [check for outgoing characters]
    [finished: close serial port]

When the application is closing, it terminates the thread by setting the boolean variable false, then (very importantly) waits for the thread to finish its execution:

def closeEvent(self, event):
    self.serth.running = False
    self.serth.wait()

Transmitting

The user will be entering keystrokes in the UI thread, and it is tempting to call the serial transmit function from that thread, but this isn’t a good idea; it is better to pass the keystrokes across to the serial thread for transmission, and we need a thread-safe method of doing this. That means we can’t just use a global shared string variable; Python does a lot of behind-the-scenes processing that could lead to an unpredictable result. Instead, we’ll use a First In First Out (FIFO) queue:

# In UI thread..
txq = Queue.Queue()
...
txq.put(s)           # Add string to queue
...
# In serial thread..
if not txq.empty():
    txd = txq.get()  # Get string from queue

So the serial thread polls the transmit queue for any data, outputting it to the serial port.

Receiving

We could use the same technique for received data; the serial thread could add it to a queue that is polled by the UI thread, and somehow trigger a UI redraw when the new data arrives, but I prefer to use a signal; the data is attached to that signal, and is received by a UI function that has registered a connection. The signal has to be in a class definition, and must specify the type of data that will be attached:

class MyWidget(QWidget):
    text_update = QtCore.pyqtSignal(str)

The signal is connected to a function that will process the data:

self.text_update.connect(self.append_text)

So now the serial thread just has to generate a signal when new data is received:

self.text_update.emit(text)

This technique would be quite adequate, but I do like having the output from all my ‘print’ function calls redirected to the same window; it makes for cleaner error reporting when things go wrong, rather than having a separate console with error messages. This is done by redirecting stdout to my widget, and adding write() and flush() handlers:

class MyWidget(QWidget):
    text_update = QtCore.pyqtSignal(str)
    def __init__(self, *args): 
        ...
        self.text_update.connect(self.append_text)
        sys.stdout = self
        ...
    def write(self, text):
        self.text_update.emit(text)  
    def flush(self):
        pass
        ...
    def append_text(self, text):        
        [add text to UI display]

So now, every time I make a print() call, a signal is sent to my append_text function, where the display is updated. The use of a signal means that I can still call print() from any thread, without fear of strange cross-threading problems.

Polling in serial thread

The serial thread is just polling for incoming and outgoing characters, and if there are none, the processor will execute the ‘while’ loop really quickly. In the absence of any delays, it will consume a lot of CPU time just checking for things that don’t exist. This may appear harmless, but it is quite alarming for the user if the CPU fan starts spinning rapidly whenever your application is running, and a laptop user won’t be happy if you needlessly drain their battery by performing pointless tasks. So we need to add some harmless time-wasting to the polling loop, by frequently returning control to the operating system. This can be done by calling the ‘sleep’ function, but we still want the software to be responsive when some serial data actually arrives. A suitable compromise is to use a serial read-function with a timeout, so the software ‘blocks’ (i.e. stalls) until either some characters are received, or there is a timeout:

self.ser = serial.Serial(self.portname, self.baudrate, timeout=SER_TIMEOUT)
...
s = self.ser.read(self.ser.in_waiting or 1)

In case you are unfamiliar with the usage, the ‘or’ function returns the left-hand side if it is true (non-zero), otherwise the right-hand side. So every read attempt is at least 1 character, or more characters if they are available. If none are present, the read function waits until the timeout, so when the serial line is idle, most time will be spent waiting in the operating system.

User Interface

This has been kept as simple as possible, with just a text box as a main widget. One minor complication is that as standard, the text box (which is actually a QTextEdit control) will capture and display all keystrokes, so we need to subclass it to intercept the keys, and call a handler function that adds them to the serial transmit queue. I didn’t want to burden the text box with this functionality, so put the handler in its parent, which is the main widget:

class MyTextBox(QTextEdit):
    def __init__(self, *args): 
        QTextEdit.__init__(self, *args)

    def keyPressEvent(self, event):
        self.parent().keypress_handler(event)

The keystroke handler in the main widget gets the character from the key event, and checks for a ctrl-V ‘paste’ request; I’ve included this feature because I find it useful to cut-and-paste frequently-used serial commands from a document, rather than re-typing them every time.

# In main widget:
def keypress_handler(self, event):
    k = event.key()
    s = RETURN_CHAR if k==QtCore.Qt.Key_Return else event.text()
    if len(s)>0 and s[0]==PASTE_CHAR:
        cb = QApplication.clipboard() 
        self.serth.ser_out(cb.text())
    else:
        self.serth.ser_out(s)

Interestingly, with PyQt5 you can cut-and-paste full 8-bit data (i.e. bytes with the high bit set), but this doesn’t seem to work in PyQt4, which only accepts the usual ASCII character set.

I haven’t included any menu options, you have to specify the COM port on the command-line using the -c option, and baud rate using -b. There is also a -x option to display incoming data in hexadecimal, for example:

Windows:
  python pyqt_serialterm.py -c com2 -b 9600 -x
Linux:
  python pyqt_serialterm.py -c /dev/ttyUSB0 -b 115200

On Linux, if you want non-root access to a serial port, you will generally need to add your username to the ‘dialout’ group:

sudo usermod -a -G dialout $USER

..then log out & back in.

Source code

# Simple PyQT serial terminal v0.09 from iosoft.blog

from PyQt5 import QtGui, QtCore
from PyQt5.QtWidgets import QTextEdit, QWidget, QApplication, QVBoxLayout
try:
    import Queue
except:
    import queue as Queue
import sys, time, serial

WIN_WIDTH, WIN_HEIGHT = 684, 400    # Window size
SER_TIMEOUT = 0.1                   # Timeout for serial Rx
RETURN_CHAR = "\n"                  # Char to be sent when Enter key pressed
PASTE_CHAR  = "\x16"                # Ctrl code for clipboard paste
baudrate    = 115200                # Default baud rate
portname    = "COM1"                # Default port name
hexmode     = False                 # Flag to enable hex display

# Convert a string to bytes
def str_bytes(s):
    return s.encode('latin-1')
    
# Convert bytes to string
def bytes_str(d):
    return d if type(d) is str else "".join([chr(b) for b in d])
    
# Return hexadecimal values of data
def hexdump(data):
    return " ".join(["%02X" % ord(b) for b in data])

# Return a string with high-bit chars replaced by hex values
def textdump(data):
    return "".join(["[%02X]" % ord(b) if b&gt;'\x7e' else b for b in data])
    
# Display incoming serial data
def display(s):
    if not hexmode:
        sys.stdout.write(textdump(str(s)))
    else:
        sys.stdout.write(hexdump(s) + ' ')

# Custom text box, catching keystrokes
class MyTextBox(QTextEdit):
    def __init__(self, *args): 
        QTextEdit.__init__(self, *args)
        
    def keyPressEvent(self, event):     # Send keypress to parent's handler
        self.parent().keypress_handler(event)
            
# Main widget            
class MyWidget(QWidget):
    text_update = QtCore.pyqtSignal(str)
        
    def __init__(self, *args): 
        QWidget.__init__(self, *args)
        self.textbox = MyTextBox()              # Create custom text box
        font = QtGui.QFont()
        font.setFamily("Courier New")           # Monospaced font
        font.setPointSize(10)
        self.textbox.setFont(font)
        layout = QVBoxLayout()
        layout.addWidget(self.textbox)
        self.setLayout(layout)
        self.resize(WIN_WIDTH, WIN_HEIGHT)      # Set window size
        self.text_update.connect(self.append_text)      # Connect text update to handler
        sys.stdout = self                               # Redirect sys.stdout to self
        self.serth = SerialThread(portname, baudrate)   # Start serial thread
        self.serth.start()
        
    def write(self, text):                      # Handle sys.stdout.write: update display
        self.text_update.emit(text)             # Send signal to synchronise call with main thread
        
    def flush(self):                            # Handle sys.stdout.flush: do nothing
        pass

    def append_text(self, text):                # Text display update handler
        cur = self.textbox.textCursor()
        cur.movePosition(QtGui.QTextCursor.End) # Move cursor to end of text
        s = str(text)
        while s:
            head,sep,s = s.partition("\n")      # Split line at LF
            cur.insertText(head)                # Insert text at cursor
            if sep:                             # New line if LF
                cur.insertBlock()
        self.textbox.setTextCursor(cur)         # Update visible cursor

    def keypress_handler(self, event):          # Handle keypress from text box
        k = event.key()
        s = RETURN_CHAR if k==QtCore.Qt.Key_Return else event.text()
        if len(s)&gt;0 and s[0]==PASTE_CHAR:       # Detect ctrl-V paste
            cb = QApplication.clipboard() 
            self.serth.ser_out(cb.text())       # Send paste string to serial driver
        else:
            self.serth.ser_out(s)               # ..or send keystroke
    
    def closeEvent(self, event):                # Window closing
        self.serth.running = False              # Wait until serial thread terminates
        self.serth.wait()
        
# Thread to handle incoming &amp; outgoing serial data
class SerialThread(QtCore.QThread):
    def __init__(self, portname, baudrate): # Initialise with serial port details
        QtCore.QThread.__init__(self)
        self.portname, self.baudrate = portname, baudrate
        self.txq = Queue.Queue()
        self.running = True

    def ser_out(self, s):                   # Write outgoing data to serial port if open
        self.txq.put(s)                     # ..using a queue to sync with reader thread
        
    def ser_in(self, s):                    # Write incoming serial data to screen
        display(s)
        
    def run(self):                          # Run serial reader thread
        print("Opening %s at %u baud %s" % (self.portname, self.baudrate,
              "(hex display)" if hexmode else ""))
        try:
            self.ser = serial.Serial(self.portname, self.baudrate, timeout=SER_TIMEOUT)
            time.sleep(SER_TIMEOUT*1.2)
            self.ser.flushInput()
        except:
            self.ser = None
        if not self.ser:
            print("Can't open port")
            self.running = False
        while self.running:
            s = self.ser.read(self.ser.in_waiting or 1)
            if s:                                       # Get data from serial port
                self.ser_in(bytes_str(s))               # ..and convert to string
            if not self.txq.empty():
                txd = str(self.txq.get())               # If Tx data in queue, write to serial port
                self.ser.write(str_bytes(txd))
        if self.ser:                                    # Close serial port when thread finished
            self.ser.close()
            self.ser = None
            
if __name__ == "__main__":
    app = QApplication(sys.argv) 
    opt = err = None
    for arg in sys.argv[1:]:                # Process command-line options
        if len(arg)==2 and arg[0]=="-":
            opt = arg.lower()
            if opt == '-x':                 # -X: display incoming data in hex
                hexmode = True
                opt = None
        else:
            if opt == '-b':                 # -B num: baud rate, e.g. '9600'
                try:
                    baudrate = int(arg)
                except:
                    err = "Invalid baudrate '%s'" % arg
            elif opt == '-c':               # -C port: serial port name, e.g. 'COM1'
                portname = arg
    if err:
        print(err)
        sys.exit(1)
    w = MyWidget()
    w.setWindowTitle('PyQT Serial Terminal ' + VERSION)
    w.show() 
    sys.exit(app.exec_())
        
# EOF

Copyright (c) Jeremy P Bentham 2019. Please credit this blog if you use the information or software in it.

Programming PSoC: an ARM CPU with programmable hardware

Want to craft your own CPU peripheral? Want to experiment with programmable hardware, but are deterred by the complexity and cost? Read on…

Exif_JPEG_PICTURE
Cypress PSoC kit

The Cypress PSoC family are strange devices; a conventional CPU combined with programmable hardware, that gives unparalleled flexibility to any embedded system.

However, this flexibility means that the ‘PSoC Creator’ development environment is unlike anything else, and can be a bit intimidating for any developer who isn’t used to programmable hardware.

In this blog, I try to de-mystify the development process, hopefully demonstrating that the learning curve isn’t as bad as you might think; you can create a simple demonstration from scratch quicker than on most other platforms.

Why use a PSoC?

  • Combines a conventional CPU with programmable logic
  • Ideal for learning about programmable hardware
  • Completely free development environment – no registration required
  • Low-cost development boards (under US $15)
  • Powerful logic optimisation software
  • Comprehensive analogue capability
  • Vast amount of online documentation and worked examples
  • Wide supply-voltage range (1.71 to 5.5 volts).
  • USB connectivity, simple Bluetooth add-on

So why isn’t everyone using these devices? Well, I must admit that when I first ran their ‘PSoC Creator’ IDE, it was a bit overwhelming – there are so many features it is difficult to know where to start, and I’m too impatient to sit through all the training videos.

So I’ll try to get you up to speed quickly, on the assumption you are familiar with the usual process for developing embedded software. The project I’ve chosen is really trivial – flashing an LED – but this is always the first step I take with any new hardware, and in this case I’ll start by using conventional software techniques, then show how the same application can be implemented purely in hardware, with the CPU doing nothing!

.. and in case you’re wondering whether I’m being paid by Cypress to create an overly-enthusiastic write-up, the answer is no, I have no commercial relationship with them: I just think this is really interesting technology, that deserves a wider audience.

Hardware

Exif_JPEG_PICTURE

For this demonstration, all you need is the Cypress CY8CKIT-059, which is a self-contained evaluation board with a detachable USB interface for programming and debugging. It has a PSoC 5LP device with:

  • 80 MHz ARM Cortex-M3 CPU
  • 256 kbytes flash, 64 kbytes RAM
  • 38 GPIO pins
  • Full-speed USB
  • 24 ‘Programmable Universal Digital Blocks’ (known as UDBs)

The board is in a conventional dual-in-line format, ideal for prototyping on 0.1″ pitch breadboard, and at under US $15, you’re getting a lot of hardware for the money.

The board has 2 snap-apart sections, a ‘kitprog’ programmer on the left, and the actual PSoc on the right. For the time being, I’d suggest you keep the board intact, as you lose some handy features (e.g. a debug serial link to the PC) if the sections are snapped apart and re-joined by an SWD cable.

PSoc Creator

psoc_creator
PSoC Creator

This is a Windows-only application; I’m using version 4.2, and (unlike most programmable logic software) you don’t have to register, or get license keys from the vendor – all the features are available for free, right from the start.

I’ll admit that it can be a bit intimidating to a software developer; the central section in the image above looks like (and is) a circuit schematic, and you don’t normally get that in a software IDE. Once you start using the software, it gets even stranger, as every time you build the application, umpteen C and Verilog files are created; you can get an out-of-control feeling when faced with all the automatically-generated files you didn’t write.

However, fairly quickly you get to appreciate the amount of work that has gone into these pre-prepared (and very well-documented) hardware & software devices – they do make your job a lot easier, and having access to the source files is excellent for experimentation & learning.

Flashing LED example

We’ll start by creating a conventional flashing-LED example in software, then transition to a programmable-hardware version.

When PSOC Creator is launched, you’ll see a Start Page, with the usual option of creating a new Design Project. Click this, and select the Target Device as PSoC 5LP Cy8C5888LTI-LP097. If you get this wrong, no great harm will result, the device programming will fail, with a helpful message as to the device you’ve set, and the device that is actually fitted to the board.

You’ll then be asked whether you want to run a code example or use a template; there are a large number of examples you could try, but for the purposes of this demonstration select an Empty Schematic, and choose workspace & project name you like; I’ve used ‘blink_led’.

Schematic

Now you’re faced with a blank circuit diagram, and may feel completely out of your comfort zone; why is a schematic needed, when all you want to do is blink an LED in software? The answer is that a PSoC, unlike a normal CPU, has really flexible peripherals, with no real pre-programmed function. So until you specify what you want by placing a component in a circuit diagram (the ‘TopDesign schematic’), the functionality doesn’t exist.

psoc_creator2
Blank schematic

To give you a quick guide to this screen, on the left you’ll see various design-wide resources that can be specified, such as I/O pins and the CPU clock. There are tabs for ‘Source’ and ‘Components’; you’ll frequently be switching between these views of your design. On the right side is the Component Catalogue containing various (soft) components that can be dragged onto the schematic.

The lower bar shows the result of compilation & device programming, as with many IDEs. The other area of note is the narrow menu bar to the left of the schematic; this selects drawing tools, the most important is the second one down, which is used to wire up the components.

Select a ‘Digital Output Pin’ from ‘Ports and Pins’ in the catalogue, and drag it onto the schematic. Zoom in so it is visible (I use control-mousewheel to do this). The default name is Pin_1 since all resources are auto-numbered, so double-click it and rename to LED_pin. You’ll see all sorts of other options, hinting at the extent to which everything is programmable; the only change you need to make is to un-check the ‘HW connection’ checkbox.

psoc_creator5
Configuring LED pin

It seems strange to have a hardware pin with no hardware, but what you are actually doing is removing the need for a hardware connection within the programmable logic, since we want the pin to be driven by software only.

psoc_creator3
Schematic with LED pin

You’ll also see a prompt to open the datasheet; try this, and you’ll be pleasantly surprised at the amount of information in the PDF. All components are documented in this way, which is one of the major strengths of the Cypress software tools.

Having defined an output to drive the LED, we need to link that output to a real hardware pin on the device. This is done by double-clicking ‘Pins’ in the Design Wide Resources, which displays a pretty little diagram of the device, with a hint that the LED_pin needs to be assigned.

psoc_creator4
Hardware pin assignment

Clicking the drop-down Port menu gives a long list of possible candidates; according to the kit packaging the LED is on port P2_1, so that is what we’ll select.

Before writing code, we need to define the CPU clock; double-click ‘Clocks’ in the design-wide resources, and you’ll see a spreadsheet of all the clocks we currently have.

psoc_creator6
Clock settings

The BUS_CLK needs to be defined, so double-click it and an even prettier flowchart pops up.

psoc_creator7
Clock flowchart

The IMO is the primary internal oscillator, and in the above settings I’ve used the PLL to boost it from 3 to 50 MHz. In theory the CPU can go up to 80 MHz, but then the timing analyser complains about the clock tolerance; I guess it implies that boosting an internal 3 MHz signal up to the CPU maximum isn’t a good idea, so I’ve backed it off to 50 MHz.

Back to the circuit diagram, right-click the LED pin, and open the datasheet; scroll through this remarkably long document until you get to the API definitions, and you’ll see that the function Pin_Write(uint8 value) is used to drive it. We’ll be using that function in the software.

Software

The source file ‘main.c’ was automatically created with the project; double-click it to open, and you’ll see a minimal framework.

One surprise is that there is a #include of the file ‘project.h’, with an error message saying it doesn’t exist. Don’t worry about it; this file contains the hardware definitions that are needed by the software, and will be generated at build-time.

The simplest way to add a delay is to use the built-in functions, so main.c becomes:

#include "project.h"

int main(void)
{
    CyGlobalIntEnable;
    for(;;)
    {
        LED_pin_Write(1);
        CyDelay(500);
        LED_pin_Write(0);
        CyDelay(500);
    }
}

The datasheet said the API function was Pin_Write, so why is it now called LED_pin_Write? This is an unusual aspect of PSoC programming, caused by the very tight integration between the hardware & software definitions of a device. At build-time, the system creates a custom version of the API to match your device settings in the schematic; the standard prefix ‘Pin’ is replaced with the name we provided, so Pin_Write becomes LED_pin_Write. Quite unconventional, but you soon get to appreciate the convenience of having a single shared namespace for the hardware & software.

In the Build menu, click ‘Build blink_led’ and watch as a myriad of files are generated, hopefully with no errors. If you see an error ‘No input on Instance “LED_pin”‘ then you didn’t disable the ‘HW connection’ on the LED pin (see above), so the software is expecting it to be wired to some internal logic.

Connect the Kitprog USB interface on the left-hand side of the board, and select Program in the Debug menu. If this is the first time you’ve used the board, you may be prompted to select it from a list.

After a brief flurry of programming, the blue LED on the board should start flashing at 1 Hz.

Hardware-only version

We’ll now use the PSoC’s programmable hardware to do the same task, as a jumping-off point for your own experimentation. There are various ways to do this, I’ve chosen one that demonstrates how components are wired up in the schematic.

First you need to double-click the LED pin on the schematic, and re-enable ‘HW connection’ so we can drive it from internal logic.

Now we need to create a suitable clock frequency; we’ll start with a 1 MHz signal, and divide it down to 1 Hz for the LED. Find the ‘Clock’ component in the System section of the Component Catalog, drag it onto the schematic, double click it, and use the following settings.

psoc_creator8
Clock settings

Now find the Frequency Divider in the Utility section of the Component Catalog, and drag it onto the schematic. Double-click, and set a divisor of 1000000.

psoc_creator9
Schematic components

All components follow the convention of having inputs on the left, outputs on the right; this part has Enable, Reset and Clock inputs. These have to be connected to something, you can’t leave inputs unconnected. Since we want the divider to be permanently enabled, and we don’t want it to be reset, we need to force Enable high, and Reset low, so find Logic High and Logic Low in the Digital Logic section of the Component Catalog, and drag one of each onto the schematic.

Now join the component pins using the wiring tool, which is the second one down in the left margin of the schematic; click on a pin, and the wire extends to the cursor; click on another pin, and they are joined. The end result should look something like:

psoc_creator10a
Completed schematic

Once joined, you can drag a component and it will remain connected; to delete a connection, click on a wire section, and press Delete.

All that remains is to get rid of our software, since everything is being done in hardware; change main.c to:

#include "project.h"
int main(void)
{
    for(;;)
    {
    }
}

Now rebuild the project, and program the device. You’ve created your first PSoC hardware project, and hopefully I’ve convinced you to carry on experimenting with these remarkable devices.

Copyright (c) Jeremy P Bentham 2019. Please credit this blog if you are using the information or software in it.