Welcome to Arkady’s documentation!¶
Dependencies¶
Arkady uses Python3’s built-in asyncio
, so it supports and
requires use of Python3.5+.
ZeroMQ is employed for socket communication, so pyzmq is required.
What Arkady IS¶
The central problem Arkady seeks to solve is how to set up an interface to an arbitrary “component” and control it from another process. This can be local or remote over a network; it uses ZeroMQ socket communication which is robust and lightweight.
What Arkady IS NOT¶
Though the Arkady library may provide some utilities for talking to Arkady applications. It does not intend to be the central means by which you control said applications. Not because Arkady is lazy, but because Arkady wants to give you freedom. Because ZeroMQ sockets are used for communication, you can communicate with Arkady application interfaces in most major languages: Java, C++, Python, Javascript… all good!
What can I use Arkady to do?¶
You can use Arkady to separate the controller logic of a piece of software from the nitty-gritty of hardware integration. This problem is why I wrote the code that turned into Arkady in the first place: I had an application that needed to simultaneously interact with Arduinos, DMX, video, audio, sensors, and keep track of program control flow. Using Arkady I was able to create a simple interface to all my components in one program, and to write clean logic in another program to leverage this interface.
You can use Arkady to put a network interface on a hardware component and save a lot of wiring. Today you can get a Raspberry Pi Zero W for 5 USD, with a bit more added for peripherals, you can put almost anything with wired control onto the network with Arkady economically.
Creating an Arkady interface¶
Suppose I wish to be able to read the temperature of my Raspberry Pi from
another computer on my network. This command would do the trick from the
command line: /opt/vc/bin/vcgencmd measure_temp
so I want to set up an
Arkady component for it.
from arkady.components import AsyncComponent
import subprocess
class RpiCPUTemp(AsyncComponent):
def handler(self, msg, *args, **kwargs):
if msg == 'get':
# command returns bytestring like b"temp=47.8'C"
temp_out = subprocess.run(
['/opt/vc/bin/vcgencmd',
'measure_temp'],
capture_output=True).stdout.decode('utf-8')
# extract temperature string
temperature = temp_out.split('=')[1].rstrip()
return temperature
else:
return 'Unrecognized msg. Must be "get"'
Now I need to create an Arkady application to make use of this custom “component”.
from arkady import Application
class RpiCPUTempApp(Application):
def config(self):
"""This is called as the last step in setup for the Application"""
# Creates the component and gives it the name 'temp'
self.add_component(RpiCPUTemp, 'temp')
# Creates a router type listener and listens on port 5555
self.add_router(bind_to='tcp://*:5555')
my_app = RpiCPUTempApp()
my_app.run() # blocks until terminated
So now this application will wait for messages. Any message beginning with the word temp will be referred to the RPiCPUTemp component. The message after the name temp will be give to the component method handler as the msg argument. RPiCPUTemp.handler only recognizes the message “get” and will report an error if it gets something else. Otherwise it runs the command and returns the temperature string.
Now, you can send messages via ZeroMQ in whatever language you please. Here’s a simple program in Python that will do so.
import time
import zmq
RPI_URI = 'tcp://localhost:5555' # Same machine
# RPI_URI = 'tcp://192.168.1.111:5555' # remote machine
context = zmq.Context()
socket = context.socket(zmq.REQ) # Request type socket, expects replies
socket.connect(RPI_URI)
while True:
# Send 'temp get'. First word is component name, remainder is message
socket.send_string('temp get')
# Requests (must) receive replies. Print our reply
print(socket.recv_string())
time.sleep(5) # Sleep 5 seconds between temperature checks
Sphinx documentation contents¶
Installing Arkady¶
Arkady is still in early development and I have not issued a release on PyPI, so downloading and installing from source on GitHub is the recommended course of action currently.
https://github.com/SavinaRoja/arkady
Arkady should be installed with Python of version 3.5 or greater, as this is
when asyncio
was introduced. Arkady makes use of asyncio
internally
for concurrent, non-blocking function. You can write Arkady components to take
advantage of asyncio
, but more on that later. All references to python
and pip
in commands are assumed to be for that version or later.
Installation is simple once you have downloaded the source, just navigate to the base directory of the source code and perform:
python setup.py install
or
pip install .
Development Installation¶
If you are looking to experiment with modifying Arkady, you will want to install
it in so-called “development mode” using the -e
flag Creating and working in
a virtual environment is simple and recommended, here it is in bash:
python -m venv venv
source venv/bin/activate
pip install -e .
and for Windows:
python -m venv venv
.\venv\Scripts\activate
pip install -e .
Getting Started with Arkady¶
The first step in working with Arkady is to think about what “action” you wish to make available for networked (websocket from another machine) or simply interprocess control. Let’s start with something simple and accessible, a generic interface to Nanpy, all you need to work along is an Arduino.
Nanpy is a great prototyping tool that lets you interface with an Arduino over a serial connection to control and read pins. Setup and usage will be covered in more detail below.
Choosing a listener¶
A listener is responsible for “listening” for external input, and at present
there are two in Arkady: router
and sub
. The router handles asynchronous
request-reply interaction and is best to use when you need to return information
to a requester, or at least provide acknowledgment of receipt. The sub handles
publish-subscribe type interaction which is a one-way kind of communication.
Because Nanpy permits reading the value of a pin, and I’ll want to be able to send this data back to a requester, I’m going to add a router to my Arkady Application.
# my_application.py
from arkady import Application
class MyApplication(Application):
def config(self):
self.add_router(bind_to='tcp://*:5555')
The method config
gets called during creation of the application and is a
good place to put registrations of listeners and (as we’ll address in a moment)
components. add_router
will set up a router listener for the application,
and we have explicitly passed bind_to='tcp://*:5555'
which instructs the
added router to listen on port 5555 (this is also the default if you don’t
specify).
This application alone won’t do anything until we add at least one component to it.
Creating a component¶
The Nanpy interface has four main functions which we will wish to make accessible
through the application: digitalRead
, analogRead
, digitalWrite
, and
analogWrite
. So let’s create a component that implements those actions.
There are two central base components in Arkady from which to derive:
SerialComponent
and AsyncComponent
. Arkady handles concurrency so it’s
possible for more than one message to come in for a component and run
simultaneously. This can be a problem in some cases, as in this case where we
should only allow one message exchange over the USB to the Arduino at one time.
So we’ll choose SerialComponent
which ensures serial (not concurrent)
execution of jobs.
from arkady.components import SerialComponent
from nanpy import SerialManager, ArduinoApi
class GenericNanpy(SerialComponent):
def __init__(self, port, *args, **kwargs):
super(GenericNanpy, self).__init__(*args, **kwargs)
self._serial_manager = SerialManager(device=port, baudrate=115200)
self.ardu = ArduinoApi(self._serial_manager)
def analog_read(self, pin_number, *_words):
"""Read the pin in analog mode"""
self.ardu.pinMode(pin_number, self.ardu.INPUT)
return self.ardu.analogRead(pin_number)
def digital_read(self, pin_number, *_words):
"""Read the pin in digital mode"""
self.ardu.pinMode(pin_number, self.ardu.INPUT)
return self.ardu.digitalRead(pin_number)
def analog_write(self, pin_number, value, *_words):
"""Write the pin in analog mode to value"""
try:
value = int(value)
except ValueError:
return 'ERROR: Got a value that could not be treated as integer'
self.ardu.pinMode(pin_number, self.ardu.OUTPUT)
self.ardu.analogWrite(pin_number, value)
def digital_write(self, pin_number, value, *_words):
"""Write the pin HIGH if value is 'high' otherwise LOW."""
self.ardu.pinMode(pin_number, self.ardu.OUTPUT)
if value == 'high':
self.ardu.digitalWrite(pin_number, self.ardu.HIGH)
else:
self.ardu.digitalWrite(pin_number, self.ardu.LOW)
In the __init__
method we initialize the Nanpy ArduinoApi
on the
specified port In the methods analog_read
, digital_read
,
analog_write
, and digital_write
we provide Nanpy functionality.
The next step is to write a message handler for the component. The application’s listeners will pass messages received along to this method.
class GenericNanpy(SerialComponent):
...
def handler(self, msg, *args, **kwargs):
"""Handle an inbound message. Returned values go back as reply."""
word_map = {
'dwrite': self.digital_write,
'awrite': self.analog_write,
'dread': self.digital_read,
'aread': self.analog_read,
}
words = msg.split()
if len(words) < 2: # Check for too short message
return 'ERROR: message must contain at least 2 words!'
key_word = words[0]
try:
pin = int(words[1])
except ValueError:
return 'ERROR: got non-int for pin number {}'.format(words[1])
if key_word not in word_map: # Check if we recognize the first word
return 'ERROR: not one of the known functions, {}'.format(word_map.keys())
try:
# Call the corresponding method
ret_val = word_map[key_word](pin, *words[2:])
if ret_val is not None:
ret_val = str(ret_val)
return ret_val
except:
return 'ERROR: "{}" failed, maybe a bad message or connection'.format(msg)
This handler
method does the job of interpreting messages so that action may
be taken, along with some error handling. Care is taken to return the results of
the called methods, as the returned string values will get passed back to a
client of our Arkady application as the body of a reply message, this will be
addressed further below.
Now that our custom component has been implemented, we wish to add it to our
application and register it so that messages may be passed to it. Let’s update
my_application.py
:
# my_application.py
from arkady import Application
ARDUINO_PORT = '/dev/ttyUSB0' # On Windows this is more like "COM3"
class MyApplication(Application):
def config(self):
self.add_component('nanpy', GenericNanpy, ARDUINO_PORT)
self.add_router(bind_to='tcp://*:5555')
This addition to config
tells the application that when the listeners
receive messages, if the first word of the message is “nanpy” then the message
should go to an instance of GenericNanpy
, created once for the application
with GenericNanpy(ARDUINO_PORT)
.
Now the application is completed, and here it is all together:
#!/usr/bin/env python3
"""
Demonstration of a very generic Arkady interface to NanPy.
Direct dependencies are: arkady, nanpy
Indirect depencences are: pyserial, pyzmq
"""
from arkady import Application
from arkady.components import SerialComponent
from nanpy import SerialManager, ArduinoApi
ARDUINO_PORT = '/dev/ttyUSB0' # On Windows this is more like "COM3"
class GenericNanpy(SerialComponent):
def __init__(self, port, *args, **kwargs):
super(GenericNanpy, self).__init__(*args, **kwargs)
self._serial_manager = SerialManager(device=port, baudrate=115200)
self.ardu = ArduinoApi(self._serial_manager)
def analog_read(self, pin_number, *_words):
"""Read the pin in analog mode"""
self.ardu.pinMode(pin_number, self.ardu.INPUT)
return self.ardu.analogRead(pin_number)
def digital_read(self, pin_number, *_words):
"""Read the pin in digital mode"""
self.ardu.pinMode(pin_number, self.ardu.INPUT)
return self.ardu.digitalRead(pin_number)
def analog_write(self, pin_number, value, *_words):
"""Write the pin in analog mode to value"""
try:
value = int(value)
except ValueError:
return 'ERROR: Got a value that could not be treated as integer'
self.ardu.pinMode(pin_number, self.ardu.OUTPUT)
self.ardu.analogWrite(pin_number, value)
def digital_write(self, pin_number, value, *_words):
"""Write the pin HIGH if value is 'high' otherwise LOW."""
self.ardu.pinMode(pin_number, self.ardu.OUTPUT)
if value == 'high':
self.ardu.digitalWrite(pin_number, self.ardu.HIGH)
else:
self.ardu.digitalWrite(pin_number, self.ardu.LOW)
def handler(self, msg, *args, **kwargs):
"""Handle an inbound message. Returned values go back as reply."""
word_map = {
'dwrite': self.digital_write,
'awrite': self.analog_write,
'dread': self.digital_read,
'aread': self.analog_read,
}
words = msg.split()
if len(words) < 2: # Check for too short message
return 'ERROR: message must contain at least 2 words!'
key_word = words[0]
try:
pin = int(words[1])
except ValueError:
return 'ERROR: got non-int for pin number {}'.format(words[1])
if key_word not in word_map: # Check if we recognize the first word
return 'ERROR: not one of the known functions, {}'.format(word_map.keys())
try:
# Call the corresponding method
ret_val = word_map[key_word](pin, *words[2:])
if ret_val is not None:
ret_val = str(ret_val)
return ret_val
except:
return 'ERROR: "{}" failed, maybe a bad message or connection'.format(msg)
class MyApplication(Application):
def config(self):
self.add_component('nanpy', GenericNanpy, ARDUINO_PORT)
self.add_router(bind_to='tcp://*:5555')
MyApplication().run()
A Client for our Arkady Application¶
It should be noted, that because Arkady makes use of ZeroMQ, a client can be written in nearly any language as bindings are widely implemented. The following example is simply an example in Python using PyZMQ. It is interactive so that you might test out sending arbitrary messages to your Arduino (via Nanpy, via Arkady!).
#!/usr/bin/env python3
"""
Demonstration of a program controlling the very generic Arkady interface to
NanPy.
Direct dependencies are: pyzmq
"""
import zmq
NANPY_ADRRESS = 'tcp://localhost:5555' # replace localhost with IP if remote
context = zmq.Context()
sock = context.socket(zmq.REQ)
sock.connect(NANPY_ADRRESS)
while True:
msg = input('Send a message to the Nanpy device: ')
# set first word as "nanpy" so message goes to our registered component
sock.send_string('nanpy ' + msg)
print('Waiting for reply.')
reply = sock.recv_string()
print('Got: ' + reply)
A Quick Guide to Setting up Nanpy¶
Using Nanpy from your computer is as simple as:
pip install nanpy
To put the corresponding firmware on your Arduino, you can get a copy of this firmware with the following command. These instructions are also outlined there.
git clone https://github.com/nanpy/nanpy-firmware.git
Then change directories to the subsequent nanpy-firmware
directory and
execute ./configure.sh
. Then copy the nanpy-firmware/Nanpy
directory
into your Arduino sketchbook directory.
Plug in your Arduino, start the Arduino IDE, configure your boardset and port, open the Nanpy module from the sketchbook, and then Upload. Assuming everything went well, your Arduino should be ready for Nanpy control.
Components¶
A Component represents a fundamental unit of interface and should generally map to a logical unit of control. This could be interaction with an actual physical device or peripheral such as a sensor, a motor, an Arduino, a DMX controller, etc. Or it could be something more virtual such as a set of system calls, internet/intranet queries, a managed subprocess and more.
Two basic device patterns are implemented: SerialComponent and AsyncComponent. Use of SerialComponent is recommended when the underlying work must be strictly serial (meaning non-parallel). AsyncComponent is suitable when multiple executions of the handler can safely run simultaneously.
-
class
arkady.components.
AsyncComponent
(*args, **kwargs)¶ -
requests_runner
()¶ Responsible for taking jobs out of the jobs queue and executing them.
Not implemented in this base class, must be overridden.
Returns:
-
-
class
arkady.components.
Component
(*args, loop=None, **kwargs)¶ The Base Component from which all other devices derive, whether they have synchronous or asynchronous underlying work.
-
requests_runner
()¶ Responsible for taking jobs out of the jobs queue and executing them.
Not implemented in this base class, must be overridden.
Returns:
-
-
class
arkady.components.
DummyAsyncDevice
(*args, **kwargs)¶
-
class
arkady.components.
DummySerialDevice
(*args, **kwargs)¶
Arkady Listeners¶
-
arkady.listeners.
router
(application, bind_to=None)¶ The
router
listener handles asynchronous requests in the request-reply pattern. A request of type zmq.REQ shall be given a reply of type zmq.REPParameters: - application –
- bind_to (string) – Network path on which to listen. Defaults to
'tcp://*:5555'
Returns:
-
arkady.listeners.
sub
(application, connect_to=None, topics=None)¶ The
sub
listener handles asynchronous requests in the pub-sub pattern. A request of type zmq.PUB receives no replyParameters: - application –
- connect_to (string) – A well-known network URI, like ‘tcp://192.168.1.200:5555’
- topics ([string]) – A list of topics as to subscribe to
Returns: