Getting started with COM-Server
This page will show you the basics of getting started with COM-Server.
Creating a Connection class
import com_server
conn = com_server.Connection(port="<port>", baud=<baud>)
Each Serial port has a port and a baud rate. The port varies from operating system to operating system. For example:
- On Linux, the port is in the
/devdirectory and begins withttyfollowed by some letters and numbers (e.g./dev/ttyUSB0,/dev/ttyUSB1,/dev/ttyACM0). - On Mac OS, the port is also in the
/devdirectory and begins withcu.then a sequence of letters and numbers. If it is a USB port, then it would becu.usbserialfollowed by a string of letters/numbers (/dev/cu.usbserial-A1252381) - On Windows, the port usually begins with
COM(e.g.COM3).
A good way of determining the port is by disconnecting the serial port, typing in the command below, connecting the port, then entering the command again. The one that shows up is the one that you would use.
> pyserial-ports
If /dev/ttyUSB0 is the one that shows up after the connection, then we would write it like this:
import com_server
conn = com_server.Connection(port="/dev/ttyUSB0", baud=<baud>)
The baud rate is necessary for serial ports because it determines the speed that data should be transferred at, in bits per second. The standard baud rates can be seen here, with the ones that are most commonly used bolded.
Example:
Opening a port at /dev/ttyUSB0 with baud rate 115200:
import com_server
conn = com_server.Connection(port="/dev/ttyUSB0", baud=115200)
115200 and 9600 are the ones most commonly used with Arduino using Serial.begin():
void setup(){
Serial.begin(9600);
// setup code
}
void loop(){
// loop code
}
It is always recommended to use a timeout. The default timeout is 1 second. Below is an example of a port opened at /dev/ttyUSB0 with baud rate 115200 and a timeout of 2 seconds:
import com_server
conn = com_server.Connection(port="/dev/ttyUSB0", baud=115200, timeout=2)
Multiple ports
If you want to try multiple ports, then you can put them as arguments like this:
import com_server
conn = com_server.Connection(115200, "/dev/ttyUSB0", "/dev/ttyUSB1", "/dev/ttyACM0", "/dev/ttyACM1", timeout=1, send_interval=1)
In this example, the program will try to connect to /dev/ttyUSB0 with baud rate 115200. If that fails, then it will try to connect to /dev/ttyUSB1, and so on. It will establish a connection with the first port that succeeds.
Connecting and disconnecting
This is when the object actually connects to the serial port. When this happens, it spawns a thread called the IO thread which handles sending and receiving data to and from the serial port.
When disconnecting the object, it will delete the IO thread and reset its IO variables, including the send queue and the list of received data.
There are two ways of doing this:
- Using the
connectanddisconnectmethods
conn.connect()
# do stuff with the connection object
conn.disconnect()
Important: Remember to always call the disconnect() method if the object will not be used anymore, or there may be stray threads in your program.
- Using a context manager:
with com_server.Connection(port="/dev/ttyUSB0", baud=115200) as conn:
# do stuff with connection object
The context manager will connect the object automatically as it enters and disconnect it automatically when it exits, so you do not have to worry about calling disconnect() before deleting the object.
To check if the connection object is currently connected or not, use the connected property:
print(conn.connected)
Reconnecting
On the event of a disconnect, you can call the reconnect() method to try to reconnect to a port provided in __init__(). However, it will raise a ConnectException if the port is already connected and this is called.
Note that disconnecting the serial device will reset the receive and send queues.
with Connection(...) as conn:
while True:
# It is recommeded to put operations in try-except because
# methods may raise ConnectException if the device disconnects
# while in the middle of the loop.
try:
while conn.connected:
# -------------------------------
# do things with connection here
# -------------------------------
time.sleep(0.01) # recommeded to delay in main thread if in loop
# in case it exits without errors
conn.reconnect(timeout=None)
except ConnectException:
# ConnectException could be thrown if it is disconnected in the middle
# of the loop, right before a send() or receive() method call.
# If timeout is given, then it will try to reconnect within that timeout
# and if it is not reconnected, then it will exit and return False.
#
# If no timeout is given, then it will try to reconnect indefinitely.
conn.reconnect(timeout=None)
Sending
To send data to the serial port, use the send() method. Put the data you want to send as arguments inside the send method.
# will send "a b c 1 2 3\r\n" without the quotations
conn.send("a", "b", "c", 1, 2, 3)
To control what your arguments should be concatenated by, use the concatenate option:
# will send "a;b;c;1;2;3\r\n" without the quotations
conn.send("a", "b", "c", 1, 2, 3, concatenate=';')
By default the concatenate option is a whitespace character.
To control what should be appended at the end before sending, use the ending option:
# will send "a;b;c;1;2;3\n" without the quotations
conn.send("a", "b", "c", 1, 2, 3, ending='\n')
By default the ending option is a carraige return: \r\n.
Note that sending data too quickly will cause this function to return immediately without sending. This is because it takes time to send things through the serial port and sending data too rapidly may lead to data loss. To customize the interval between sending, use the send_interval option in the constructor of this class. The default is 1 second between sending.
If the timeout is reached, the method will return.
Receiving
To get the data that came in from the serial port most recently, use the receive() method:
rcv = conn.receive() # gets bytes object
rcv = conn.receive_str() # gets string object
To get the next most data that came in from the serial port:
rcv = conn.receive(num_before=1) # gets bytes object
rcv = conn.receive_str(num_before=1) # gets string object
Increase the value of num_before to get the next most recent, next next more recent, etc. received data.
If there has not been any received data, then returns None.
To wait for data to be received, use the get() method:
rcv = conn.get(bytes) # gets first bytes after the method is called
rcv = conn.get(str) # gets first string after method is called
If no data gets received before the timeout is reached, then the method will return None.
Full example
This example sends something to the serial port and then prints the first data that comes from the serial port after sending.
import time
from com_server import Connection
conn = Connection(port="/dev/ttyUSB0", baud=115200)
conn.connect()
while conn.connected:
conn.send("Sending something", ending="\n")
received = conn.get(str)
print(received)
time.sleep(1) # wait one second between sending
conn.disconnect()
Creating a ConnectionRoutes class
ConnectionRoutes works similarly to a flask_restful.Api object but with only the resource decorator. It takes in a Connection object and it adds resources that are meant to interact with the Connection objects. What this means is that unlike flask_restful.Api.resource(), ConnectionRoutes.add_resource() gives the class a conn attribute representing the Connection that you can interact with in the HTTP methods. It also checks if the serial connection is currently being used, and if it is, it will respond with a 503 Service Unavailable. Lastly, it checks if the connection is disconnected, and if so, it will respond with a 500 Internal Server Error.
The class that the decorator is wrapping must extend ConnectionResource.
However, the server can be started separately with start_app, and the Flask object is not part of ConnectionRoutes, giving the user more flexibility, unlike the old RestApiHandler.
from com_server import Connection, ConnectionRoutes, ConnectionResource, start_app
from flask import Flask
from flask_restful import Api
app = Flask(__name__)
api = Api(app)
conn = Connection(115200, "/dev/ttyUSB0") # change to your own port
handler = ConnectionRoutes(conn)
# Add your own resources that do not interact with the serial port here with `app` and `api`
@handler.add_resource("/hello_world")
class Hello_World_Resource(ConnectionResource):
"""
This resource will return the most recently received object
on GET and send "Hello world" to the serial device on POST.
The URL to this resource would be localhost:8080/hello_world
"""
def get(self):
return {
"data": self.conn.receive_str()
}
def post(self):
self.conn.send("Hello world", ending="\n")
return {
"message": "sent"
}
# app.run() cannot be used because the serial port is disconnected.
# We have to use start_app(), which automatically connects the serial port
# before running a production server with waitress on http://0.0.0.0:8080
start_app(app, api, handler)
# Note that start_app calls sys.exit(), so nothing should go after start_app
Using the builtin endpoints
COM-Server comes with a list of builtin endpoints, with each one versioned. The latest version is the V1 API, with supports the ConnectionRoutes object. The previous version, V0, uses the old RestApiHandler and I do not recommend using it. However, its usage can be found here.
com_server has an api module that contains all the versions of the builtin endpoints. Note that version V0 is not compatible with version V1 because it uses a completely different handler class.
from com_server import Connection, ConnectionRoutes, start_app
from com_server.api import V1
from flask import Flask
from flask_restful import Api
app = Flask(__name__)
api = Api(app)
conn = Connection(<baud>, "<serport>")
handler = ConnectionRoutes(conn)
# add your own routes and endpoints here
V1(handler)
start_app(app, api, handler)
V1 has an option prefix, indicating the prefix of all the routes in the builtin API. The default is v1, meaning that all version 1 API endpoints are prefixed with /v1/ (e.g. localhost:8080/v1/send).
Note that if you put V1 right before start_app and you have a resource that has the same path as one of the resources in the builtins, then the builtin resource will override your provided resource.
For more information on the builtin endpoints and routes, check out the Version 1 API docs.
Using the CLI
COM-Server provides a command line tool to run the server given serial ports and a baud rate with the builtin V1 API. In your terminal, type in:
> com_server --help
for more information.