7. Building Batch Clients

While the command line (cpi) and GUI (cpigui) clients lend themselves to interactive use, neither is suited for batch use. Thankfully, the logic for communicating with Compound Pi camera servers is split out into its own class (CompoundPiClient) which is used by both clients. The class is relatively simple to use, and also lends itself to construction of batch scripts for controlling Compound Pi camera servers.

The following sections document the API of the class, and provide several examples of batch scripts.

7.1. CompoundPiClient

class compoundpi.client.CompoundPiClient(progress=None)[source]

Implements a network client for Compound Pi servers.

The optional progress parameter provides an object which will be notified of long client operations. When the client begins a long operation it will call the start method of the object with a single parameter indicating the number of expected operations to complete. As the operation progresses, the object’s update method will be called with a parameter indicating the current operation (the update method may be called multiple times with the same number, but it will never decrease within the span of one operation, and it will never exceed the count passed to start). To terminate a long operation prematurely, raise an exception in the update method. Finally, the object’s finish routine will be called with no parameters (if the start method is called, the finish method is guaranteed to be called).

Before controlling any Compound Pi servers, the client must either be told the addresses of the servers, or discover them via broadcast. The servers attribute is the list of available servers. Servers can be defined manually, or discovered by broadcast. See the CompoundPiServerList documentation for further information.

Various methods are provided for configuring and controlling the cameras on the Compound Pi servers (resolution(), framerate(), exposure(), capture(), etc). Each method optionally accepts a set of addresses to operate on. If omitted, the command is applied to all servers that the client knows about (via a broadcast packet).

The one exception to this is the download() method for retrieving captured images. For the sake of efficiency this is expected to operate against one server at a time, so the address parameter is mandatory. The class listens on port 5647 on all available interfaces for download transmissions. If this is incorrect (or if you wish to limit the interfaces that the client listens on), adjust the bind attribute.

When you are finished with the client, you must call the close() method which shuts down the listening socket and server thread. Failure to do so will likely cause your application or script to hang (the server thread is deliberately not marked as a daemon thread, so your script will not terminate while it is still active). For example:

from compoundpi.client import CompoundPiClient

client = CompoundPiClient()
try:
    client.servers.find(10)
    client.capture()
finally:
    client.close()

The client class can be used as a context handler to ensure this happens implicitly:

from compoundpi.client import CompoundPiClient

with CompoundPiClient() as client:
    client.servers.find(10)
    client.capture()
agc(mode, addresses=None)[source]

Called to change the automatic gain control on the servers at the specified addresses (or all defined servers if addresses is omitted). The mode parameter specifies the new exposure mode as a string. Valid values are:

  • 'antishake'
  • 'auto'
  • 'backlight'
  • 'beach'
  • 'fireworks'
  • 'fixedfps'
  • 'night'
  • 'nightpreview'
  • 'off'
  • 'snow'
  • 'sports'
  • 'spotlight'
  • 'verylong'

Note

When mode is set to 'off' the analog and digital gains reported by status() will become fixed. Any other mode causes them to vary according to the selected algorithm. Unfortunately, at present, the camera firmware provides no means for forcing the gains to a particular value (in contrast to AWB and exposure speed).

awb(mode, red=0.0, blue=0.0, addresses=None)[source]

Called to change the white balance on the servers at the specified addresses (or all defined servers if addresses is omitted). The mode parameter specifies the new white balance mode as a string. Valid values are:

  • 'auto'
  • 'cloudy'
  • 'flash'
  • 'fluorescent'
  • 'horizon'
  • 'incandescent'
  • 'off'
  • 'shade'
  • 'sunlight'
  • 'tungsten'

If the special value 'off' is given as the mode, the red and blue parameters specify the red and blue gains of the camera manually as floating point values between 0.0 and 8.0. Reasonable values for red and blue gains can be discovered easily by setting mode to 'auto', waiting a while to let the camera settle, then querying the current gain by calling status(). For example:

from time import sleep
from compoundpi.client import CompoundPiClient

with CompoundPiClient() as client:
    client.servers.network = '192.168.0.0/24'
    client.servers.find(10)
    # Pick an arbitrary camera to determine white balance gains and
    # set it auto white balance
    addr = client.servers[0]
    client.awb('auto', addresses=addr)
    # Wait a few seconds to let the camera measure the scene
    sleep(2)
    # Query the camera's gains and fix all cameras gains accordingly
    status = client.status(addresses=addr)[addr]
    client.awb('off', status.awb_red, status.awb_blue)
brightness(value, addresses=None)[source]

Called to change the brightness level on the servers at the specified addresses (or all defined servers if addresses is omitted). The new level is specified an integer between 0 and 100.

capture(count=1, video_port=False, quality=None, delay=None, addresses=None)[source]

Called to capture images on the servers at the specified addresses (or all defined servers if addresses is omitted). The optional count parameter is an integer value defining how many sequential images to capture, which defaults to 1. The optional video_port parameter defaults to False which indicates that the camera’s slow, but high quality still port should be used for capture. If set to True, the faster, lower quality video port will be used instead. This is particularly useful with count greater than 1 for capturing high motion scenes.

The optional delay parameter defaults to None which indicates that all servers should capture images immediately upon receipt of the CAPTURE message. When using broadcast messages (when addresses is omitted) this typically results in near simultaneous captures, especially with fast, low latency networks like ethernet.

If delay is set to a small floating point value measured in seconds, it indicates that the servers should synchronize their captures to a timestamp (the client calculates the timestamp as now + delay seconds). This functionality assumes that the servers all have accurate clocks which are reasonably in sync with the client’s clock; a typical configuration is to run an NTP server on the client machine, and an NTP client on each of the Compound Pi servers.

Note

Note that this method merely causes the servers to capture images. The captured images are stored in RAM on the servers for later retrieval with the download() method.

clear(addresses=None)[source]

Called to clear captured files from the RAM of the servers at the specified addresses (or all defined servers if addresses is omitted). Currently the protocol for the CLEAR message is fairly crude: it simply clears all captured files on the server; there is no method for specifying a subset of files to wipe.

close()[source]

Closes the client. This must be called once you are finished using the client to ensure that the background thread used for receiving downloaded data is shut down along with its listening socket. You can use the class as a context handler to ensure this happens easily:

import compoundpi.client

with compoundpi.client.CompoundPiClient() as client:
    # When this block terminates, close() will be called
    # implicitly
    client.servers.find(10)
contrast(value, addresses=None)[source]

Called to change the contrast level on the servers at the specified addresses (or all defined servers if addresses is omitted). The new level is specified an integer between -100 and 100.

denoise(value, addresses=None)[source]

Called to change whether the firmware’s denoise algorithm is active on the servers at the specified addresses (or all defined servers if addresses is omitted). The value is a simple boolean, which defaults to True.

download(address, index, output)[source]

Called to download the image with the specified index from the server at address, writing the content to the file-like object provided by the output parameter.

The download() method differs from all other client methods in that it targets a single server at a time (attempting to simultaneously download files from multiple servers would be extremely inefficient). The available image indices can be determined by calling the list() method beforehand. Note that downloading files from servers does not wipe the file from the server’s RAM. Once all files have been successfully retrieved, you should use the clear() method to free up memory on the servers. For example:

import io
from compoundpi.client import CompoundPiClient

with CompoundPiClient() as client:
    client.servers.network = '192.168.0.0/24'
    # Capture an image on all servers
    client.capture()
    # Download all available files from all servers
    for addr, files in client.list().items():
        for f in files:
            print('Downloading image %d from %s (%d bytes)' % (
                f.index,
                addr,
                f.size,
                ))
            with io.open('%s-%d.jpg' % (addr, f.index)) as f:
                client.download(addr, f.index, f)
    # Wipe all files on all servers
    client.clear()
ev(value, addresses=None)[source]

Called to change the exposure compensation (EV) level on the servers at the specified addresses (or all defined servers if addresses is omitted). The new level is specified an integer between -24 and 24 where each increment represents 1/6th of a stop.

exposure(mode, speed=0, addresses=None)[source]

Called to change the exposure on the servers at the specified addresses (or all defined servers if addresses is omitted). The mode parameter specifies the new exposure mode as a string. Valid values are:

  • 'auto'
  • 'off'

The speed parameter specifies the exposure speed manually as a floating point value measured in milliseconds. Reasonable exposure speeds can be discovered easily by setting mode to 'auto', waiting a while to let the camera settle, then querying the current speed by calling status(). For example:

from time import sleep
from compoundpi.client import CompoundPiClient

with CompoundPiClient() as client:
    client.servers.network = '192.168.0.0/24'
    client.servers.find(10)
    # Pick an arbitrary camera to determine exposure speed and set it
    # to auto
    addr = client.servers[0]
    client.exposure('auto', addresses=addr)
    # Wait a few seconds to let the camera measure the scene
    sleep(2)
    # Query the camera's exposure speed and fix all cameras accordingly
    status = client.status(addresses=addr)[addr]
    client.exposure('off', speed=status.exposure_speed)
flip(horizontal, vertical, addresses=None)[source]

Called to change the orientation of the servers at the specified addresses (or all defined servers if addresses is omitted). The horizontal and vertical parameters are boolean values indicating whether to flip the camera’s output along the corresponding axis. The default for both parameters is False.

framerate(rate, addresses=None)[source]

Called to change the camera framerate on the servers at the specified addresses (or all defined servers if addresses is omitted). The rate parameter is the new framerate specified as a numeric value (e.g. int(), float() or Fraction). For example:

from compoundpi.client import CompoundPiClient

with CompoundPiClient() as client:
    client.servers.network = '192.168.0.0/24'
    client.servers.find(10)
    client.framerate(24)
identify(addresses=None)[source]

Called to cause the servers at the specified addresses to physically identify themselves (or all defined servers if addresses is omitted). Currently, the identification takes the form of the server blinking the camera’s LED for 5 seconds.

iso(value, addresses=None)[source]

Called to change the ISO setting on the servers at the specified addresses (or all defined servers if addresses is omitted). The mode parameter specifies the new ISO settings as an integer value. values are 0 (meaning auto), 100, 200, 320, 400, 500, 640, and 800.

list(addresses=None)[source]

Called to list files available for download from the servers at the specified addresses (or all defined servers if addresses is omitted). The method returns a mapping of address to sequences of CompoundPiFile which provide the index, capture timestamp, and size of each image available on the server. For example, to enumerate the total size of all files stored on all servers:

from compoundpi.client import CompoundPiClient

with CompoundPiClient() as client:
    client.servers.network = '192.168.0.0/24'
    client.servers.find(10)
    client.capture()
    size = sum(
        f.size
        for addr, files in client.list().items()
        for f in files
        )
    print('%d bytes available for download' % size)
metering(mode, addresses=None)[source]

Called to change the metering algorithm on the servers at the specified addresses (or all defined servers if addresses is omitted). The mode parameter specifies the new metering mode as a string. Valid values are:

  • 'average'
  • 'backlit'
  • 'matrix'
  • 'spot'
record(length, format=u'h264', quality=None, bitrate=None, intra_period=None, motion_output=False, delay=None, addresses=None)[source]

Called to record video on the servers at the specified addresses (or all defined servers if addresses is omitted). The length parameter specifies the time (in seconds) to record for. This may be a decimal value. The optional format parameter specifies the video codec to use. This defaults to 'h264' but may also be set to 'mjpeg'.

The optional quality parameter specifies the quality that the codec will attempt to maintain. This is an integer value between 1 and 40 for h264 (lower values are better), or an integer value between 1 and 100 for mjpeg (higher values are better). The default provides “good” quality. The optional bitrate parameter specifies the limit of data that the codec is allowed to produce. The default is extremely high to ensure bitrate limiting never occurs by default.

The optional intra_period parameter is only valid with the h264 format and specifies the number of frames in a GOP (group of pictures). As a GOP always starts with a keyframe (I-frame) this effectively dictates how regularly keyframes occurs in the output. The default is 30 frames.

The optional motion_output parameter is only valid with the h264 format and specifies that you wish to capture motion vector estimation data as well as video data. This will be stored in a separate file on the Compound Pi server.

The optional delay parameter defaults to None which indicates that all servers should record video immediately upon receipt of the CAPTURE message. When using broadcast messages (when addresses is omitted) this typically results in near simultaneous recording, especially with fast, low latency networks like ethernet.

Note

Note that this method merely causes the servers to record video. The captured video is stored in RAM on the servers for later retrieval with the download() method.

resolution(width, height, addresses=None)[source]

Called to change the camera resolution on the servers at the specified addresses (or all defined servers if addresses is omitted). The width and height parameters are integers defining the new resolution. For example:

from compoundpi.client import CompoundPiClient

with CompoundPiClient() as client:
    client.servers.network = '192.168.0.0/24'
    client.servers.find(10)
    client.resolution(1280, 720)
saturation(value, addresses=None)[source]

Called to change the saturation level on the servers at the specified addresses (or all defined servers if addresses is omitted). The new level is specified an integer between -100 and 100.

status(addresses=None)[source]

Called to determine the status of servers. The status() method queries all servers at the specified addresses (or all defined servers if addresses is omitted) for their camera configurations. It returns a mapping of address to CompoundPiStatus named tuples. For example:

from compoundpi.client import CompoundPiClient

with CompoundPiClient() as client:
    client.servers.network = '192.168.0.0/24'
    client.servers.find(10)
    print('Configured resolutions:')
    for address, status in client.status().items():
        print('%s: %dx%d' % (
            address,
            status.resolution.width,
            status.resolution.height,
            ))
bind

Defines the port and interfaces the client will listen to for responses.

This attribute defaults to ('0.0.0.0', 5647) meaning that the client defaults to listening on port 5647 on all available network interfaces for responses from Compound Pi servers (the special address 0.0.0.0 means “all available interfaces”). If you wish to change the port, or limit the interfaces the client listens to, assign a tuple of (address, port) to this attribute. For example:

from compoundpi.client import CompoundPiClient

with CompoundPiClient() as client:
    client.bind = ('192.168.0.1', 8000)

Querying this attribute will return a 2-tuple of the current address and port that the client is listening on.

Note

The port of the client’s bound socket doesn’t need to match the server’s port. Both simply default to 5647 for the sake of simplicity.

servers

Stores the list of servers that the client controls.

See CompoundPiServerList for full documentation of the methods of the server list. For most purposes you can treat this as a normal Python list (e.g. append(), remove(), along with item access, length, etc). However, duplicate entries are not permitted, and there are a few extra methods like find() and move().

This property is also writeable; setting it to a list of addresses will cause the server list to insert, remove, and move addresses as necessary to match the specified list. For example, this is a valid way to add a series of addresses to the list:

import compoundpi.client

with compoundpi.client.CompoundPiClient() as client:
    client.servers = [
        '192.168.0.%d' % i for i in range(1, 11)]

7.2. CompoundPiServerList

class compoundpi.client.CompoundPiServerList(progress)[source]

Manages the list of servers under the control of the client.

The server list can be accessed via the CompoundPiClient.servers attribute. The list of defined servers can be manipulated with the familiar append(), remove(), and extend() methods, and individual entries can be replaced by assigning to them or deleted with del in the usual manner. The find() method can be used to discover available servers on the subnet via broadcast.

The list can be iterated over as usual, in reverse order with reversed(), and can be sorted with the sort() method just like a normal list.

Where the server list differs from a typical Python list is firstly that no duplicate addresses are permitted (in this manner, it is akin to a set). Secondly, while addresses can be added in string format, all addresses within the list will be converted to IPv4Address instances (which can be coerced back to strings for display purposes).

Furthermore, a move() method is provided to reposition existing addresses within the list. This is provided because adding new addresses to the list (via append(), extend(), or find() implicitly causes a HELLO message to be transmitted to the new servers to ensure they are alive and understand the correct version of the network protocol), so removing then re-inserting existing entries to move them is inefficient, whilst re-inserting then removing isn’t permitted due to the prevention of duplicates.

You may also assign to the CompoundPiClient.servers attribute to re-order or completely redefine the list. Re-ordering in this case will be done efficiently.

Warning

Upon construction, the assumes the local network is 192.168.0.0/16. Because this class utilizes UDP broadcast packets, it is crucial that the network configuration (including the network mask) is set correctly. If the default network is wrong (which is most likely the case), you must correct it before issuing any commands. This can be done by setting the network attribute.

The class assumes the servers are listening on UDP port 5647 by default. This can be altered via the port attribute.

append(address)[source]

Called to explicitly add a server address to the client’s list. This is equivalent to insertion at the end of the list:

from compoundpi.client import CompoundPiClient

with CompoundPiClient() as client:
    client.servers.network = '192.168.0.0/24'
    client.servers.append('192.168.0.2')
    assert len(client.servers) == 1
    assert '192.168.0.2' in client.servers

Attempting to add an address that is already present in the client’s list will raise a CompoundPiRedefinedServer error.

extend(addresses)[source]

Called to add multiple servers to the client’s list. The addresses parameter must be an iterable of addresses to add.

find(count=0)[source]

Called to discover servers on the client’s network. The find() method broadcasts a HELLO message to the currently configured network. If called with no expected count, the method then waits for the network timeout (default 15 seconds) and adds all servers that replied to the broadcast to the client’s list. If called with an expected count value, the method will terminate as soon as count servers have replied.

Note

If count servers don’t reply, no exception will be raised. Therefore it is important to check the length of the list after calling find().

For example:

from compoundpi.client import CompoundPiClient

with CompoundPiClient() as client:
    client.servers.network = '192.168.2.0/24'
    client.servers.find(10)
    assert len(client.servers) == 10
    print('Found 10 clients:')
    for addr in client.servers:
        print(str(addr))

This method or the append() method are usually the first methods called after construction and configuration of the client instance.

insert(index, address)[source]

Called to explicitly add a server address to the client’s list at the specified index. Before the server is added, the client will send a HELLO to verify that the server is alive. You can query the servers in the client’s list by treating the list as an iterable:

from compoundpi.client import CompoundPiClient

with CompoundPiClient() as client:
    client.servers.network = '192.168.0.0/24'
    client.servers.insert(0, '192.168.0.2')
    assert len(client.servers) == 1
    assert '192.168.0.2' in client.servers

Attempting to add an address that is already present in the client’s list will raise a CompoundPiRedefinedServer error.

move(index, address)[source]

Called to move address (which must already be present within the server list) to index. Positioning is as for insert(); the specified address will be moved so that it occupies index and all later entries will be moved down.

remove(address)[source]

Called to explicitly remove a server address from the client’s list. Nothing is sent to a server that is removed from the list. If the server is still active on the client’s network after removal it will continue to receive broadcast packets but the client will ignore any responses from the server.

Warning

Please note that this may cause unexpected issues. For example, such a server (active but unknown to a client) may capture images in response to a broadcast CAPTURE message. For this reason it is recommended that you shut down any servers that you do not intend to communicate with. Future versions of the protocol may include explicit disconnection messages to mitigate this issue.

Attempting to remove an address that is not present in the client’s list will raise a ValueError.

reverse()[source]

Reverses the order of the servers in the list.

sort(key=None, reverse=False)[source]

Sorts the servers in the list according to the specified key comparison function. If reverse is True, the order of the sort is reversed.

network

Defines the network that all servers belong to.

This attribute defaults to 192.168.0.0/16 meaning that the client assumes all servers belong to the network beginning with 192.168. and accept broadcast packets with the address 192.168.255.255. If this is incorrect (which is likely the case), assign the correct network configuration as a string (in CIDR or network/mask notation) to this attribute. A common configuration is:

from compoundpi.client import CompoundPiClient

with CompoundPiClient() as client:
    client.servers.network = '192.168.0.0/24'

Note that the network mask must be correct for broadcast packets to operate correctly. It is not enough for the network prefix alone to be correct.

Querying this attribute will return a IPv4Network object which can be converted to a string, or enumerated to discover all potential addresses within the defined network.

port

Defines the server port that the client will broadcast to.

This attribute defaults to 5647 meaning that the client will send broadcasts to Compound Pi servers which are assumed to be listening for messages on port 5647. If you have configured cpid differently, simply assign a different value to this attribute. For example:

from compoundpi.client import CompoundPiClient

with CompoundPiClient() as client:
    client.servers.port = 8080

Note

The port of the client’s bound socket (see CompoundPiClient.bind doesn’t need to match the server’s port. Both simply default to 5647 for the sake of simplicity.

timeout

Defines the timeout for responses to commands.

This attribute specifies the length of time that the client will wait for all servers to complete a command and return a response. If all servers have not replied within the specified number of seconds a CompoundPiTransactionFailed error will be raised.

7.3. CompoundPiStatus

class compoundpi.client.CompoundPiStatus(resolution, framerate, awb_mode, ...)[source]

This class is a namedtuple derivative used to store the status of a Compound Pi server. It is recommended you access the information stored by this class by attribute name rather than position (for example: status.resolution rather than status[0]).

resolution

Returns the current resolution of the camera as a Resolution tuple.

framerate

Returns the current framerate of the camera as a Fraction.

awb_mode

Returns the current white balance mode of the camera as a lower case string. See CompoundPiClient.awb() for valid values.

awb_red

Returns the current red gain of the camera’s white balance as a floating point value. If awb_mode is 'off' this is a fixed value. Otherwise, it is the current gain being used by the configured auto white balance mode.

awb_blue

Returns the current blue gain of the camera’s white balance as a floating point value. If awb_mode is 'off' this is a fixed value. Otherwise, it is the current gain being used by the configured auto white balance mode.

agc_mode

Returns the current auto-gain mode of the camera as a lower case string. See CompoundPiClient.agc() for valid values.

agc_analog

Returns the current analog gain applied by the camera. If agc_mode is 'off' this is a fixed (but uneditable) value. Otherwise, it is a value which varies according to the selected AGC algorithm.

agc_digital

Returns the current digital gain used by the camera. If agc_mode is 'off' this is a fixed (but uneditable) value. Otherwise, it is a value which varies according to the selected AGC algorithm.

exposure_mode

Returns the current exposure mode of the camera as a lower case string. See CompoundPiClient.exposure() for valid values.

exposure_speed

Returns the current exposure speed of the camera as a floating point value measured in milliseconds.

iso

Returns the camera’s ISO setting as an integer value. This will be one of 0 (indicating automatic), 100, 200, 320, 400, 500, 640, or 800.

metering_mode

Returns the camera’s metering mode as a lower case string. See CompoundPiClient.metering() for valid values.

brightness

Returns the camera’s brightness level as an integer value between 0 and 100.

contrast

Returns the camera’s contrast level as an integer value between -100 and 100.

saturation

Returns the camera’s saturation level as an integer value between -100 and 100.

ev

Returns the camera’s exposure compensation value as an integer value measured in 1/6ths of a stop. Hence, 24 indicates the camera’s compensation is +4 stops, while -12 indicates -2 stops.

hflip

Returns a boolean value indicating whether the camera’s orientation is horizontally flipped.

vflip

Returns a boolean value indicating whether the camera’s orientation is vertically flipped.

denoise

Returns a boolean value indicating whether the camera’s denoise algorithm is active when capturing.

timestamp

Returns a datetime instance representing the time at which the server received the STATUS message. Due to network latencies there is little point comparing this to the client’s current timestamp. However, if the STATUS message was broadcast to all servers, it can be useful to calculate the maximum difference in the server’s timestamps to determine whether any servers have lost time sync.

files

Returns an integer number indicating the number of files currently stored in the server’s memory.

7.4. CompoundPiFile

class compoundpi.client.CompoundPiFile(filetype, image, timestamp, size)[source]

This class is a namedtuple derivative used to store information about an files stored in the memory of a Compound Pi server. It is recommended you access the information stored by this class by attribute name rather than position (for example: f.size rather than f[3]).

filetype

Specifies what sort of file this is. Can be one of IMAGE, VIDEO, or MOTION.

index

Specifies the index of the file on the server. This is the index that should be passed to CompoundPiClient.download() in order to retrieve this file.

timestamp

Specifies the timestamp on the server at which the file was captured as a datetime instance.

size

Specifies the size of the file as an integer number of bytes.

7.5. Resolution

class compoundpi.client.Resolution(width, height)[source]

Represents an image resolution.

width

The width of the resolution as an integer value.

height

The height of the resolution as an integer value.

7.6. Examples

The following example demonstrates instantiating a client which attempts to find 10 Compound Pi servers on the 192.168.0.0/24 network. It configures all servers to capture images at 720p, captures a single image and then downloads the resulting images to the current directory, naming each image after the IP address of the server that captured it. Finally, the script ensures it clears all images from the servers:

import io
from compoundpi.client import CompoundPiClient

with CompoundPiClient() as client:
    client.servers.network = '192.168.0.0/24'
    client.servers.find(10)
    assert len(client.servers) == 10
    client.resolution(1280, 720)
    client.capture()
    try:
        for addr, files in client.list().items():
            for f in files:
                assert f.filetype == 'IMAGE'
                print('Downloading from %s' % addr)
                with io.open('%s.jpg' % addr, 'wb') as output:
                    client.download(addr, f.index, output)
    finally:
        client.clear()

The following example explicitly defines 5 servers, configures them with a variety of settings, then causes them to capture 5 images in rapid succession from their camera’s video ports. The delay parameter of CompoundPiClient.capture() is used to synchronize the captures to a specific timestamp (it is assumed the servers clocks are synchronized). Finally, all images are downloaded into a series of in-memory streams (it is assumed the client has sufficient RAM to make this efficient):

import io
from compoundpi.client import CompoundPiClient

with CompoundPiClient() as client:
    client.servers.network = '192.168.0.0/24'
    print('Define servers 192.168.0.2-192.168.0.6')
    for i in range(2, 7):
        client.servers.append('192.168.0.%d' % i)
    assert len(client.servers) == 5
    print('Configuring servers')
    client.resolution(1280, 720)
    client.framerate(24)
    client.agc('auto')
    client.awb('off', 1.5, 1.3)
    client.iso(100)
    client.metering('spot')
    client.brightness(50)
    client.contrast(0)
    client.saturation(0)
    client.denoise(False)
    print('Capturing 5 images on all servers after 0.25 second delay')
    client.clear()
    client.capture(5, video_port=True, delay=0.25)
    for addr, status in client.status().items():
        assert status.files == 5
    print('Downloading captures')
    captures = {}
    try:
        for addr, files in client.list().items():
            for f in files:
                assert f.filetype == 'IMAGE'
                print('Downloading capture %d from %s' % (f.index, addr))
                stream = io.BytesIO()
                captures.setdefault(addr, []).append(stream)
                client.download(addr, f.index, stream)
                stream.seek(0)
    finally:
        client.clear()

The following example uses the CompoundPiStatus.timestamp field to determine whether the time on any of the discovered servers deviates from any other server by more than 0.1 seconds:

from compoundpi.client import CompoundPiClient

with CompoundPiClient() as client:
    client.servers.network = '192.168.0.0/24'
    client.servers.find(10)
    assert len(client.servers) == 10
    responses = client.status()
    min_time = min(status.timestamp for status in responses.values())
    for address, status in responses.items():
        if (status.timestamp - min_time).total_seconds() > 0.1:
            print(
                'Warning: time on %s deviates from minimum '
                'by >0.1 seconds' % address)

For more comprehensive examples, you may wish to browse the implementations of the cpi and cpigui applications as these are built using the CompoundPiClient class.