This documentation covers the Chronos 1.4 Coordinator D-Bus API. The internal camera D-Bus API has two major components: The D-Bus-based Coordinator API (formerly the Control API) and the D-Bus-based Video API.
You can use the internal D-Bus APIs to write your own application on the camera. This application will be able to display a UI on the back of the camera as well as respond to remote HTTP requests over MiniUSB and the network.
If you only need to control the camera remotely, you should consider using the HTTP interface instead. The HTTP interface lets you send commands to the camera over the network, and is used by the (planned) remote-control app. It is easier to develop using the HTTP interface than it is using the D-Bus interfaces.
Note: All examples in this document are given in Python, since the reference client was written in it.
The D-Bus Coordinator and Video APIs sit between the hardware and the applications running on the camera.
The APIs provide a way to configure the camera hardware. The APIs also emit updates so that clients can keep themselves up-to-date when the camera is reconfigured. For example, if the HTTP interface receives a request to change the exposure time of the camera, then the user interface on the camera will need to update itself to reflect the new exposure time.
The APIs primarily drive the FPGA of the Chronos, as well as set up the video pipeline to display and save content. While the FPGA image itself is proprietary, the register definitions are available as part of the implementation of the APIs.
The Coordinator D-Bus API deals with configuring the camera, as opposed to the video API which is responsible for playing back and saving footage.
To connect to the Coordinator API, connect to the D-Bus service/path/interface as in the following example:
from PyQt5.QtDBus import QDBusInterface, QDBusConnection
cameraControlAPI = QDBusInterface(
"ca.krontech.chronos.control", #Service
"/ca/krontech/chronos/control", #Path
"", #Interface
QDBusConnection.systemBus() )
To use the mock interface, to avoid misconfiguring the camera during development, connect to the ca.krontech.chronos.control.mock
service instead of ca.krontech.chronos.control
.
All methods result in a reply containing key/value pairs. The map can contain an optional 'value' entry with the data resulting from the function call, or in the case of an error an 'errorName' and 'message' entry. If there is no errorName, then no error should have occured.
Available methods are:
get([string valueName, …]) → {string:value, …}
— Return camera setting values for all strings in the list passed in. See Values for a list of valid keys.set({string valueName: any value, …})
— Update camera values named by string to their new value. See Values for a list of valid keys.availableKeys() → [string valueName, …]
— Get a list of available camera settings keys, for use with get
or set
.availableCalls() → [string callName, …]
— Get a list of available camera settings keys, for use with get
or set
.powerDown()
— Turn off the camera. Useful in conjunction with the "Turn camera on when power connected" checkbox in the Battery & Power screen.framerateForResolution(hRes: int, vRes: int) → float
— Returns the frames per seconds a resolution will record at. Non-valid resolutions will return undefined results.resolutionIsValid(hOffset: int, vOffset: int, hRes: int, vRes: int) → bool
— Returns true if the supplied video resolution and offset is valid for recording. A resolution is not valid if it is offset too far. All parameters are positive integers.calibrate({string testName: boolean run})
— Calibrate the camera. Each calibration for which run is true is ran. Warning - some calibrations may require equipment to run correctly. For example, while black cal is easy to re-run, it is much harder to run white balance since you will need a diffuse, true-white light source. Available calibrations are:
analogCal
: Sensor internal calibration.blackCal
: The normal black calibration routine where you have to cover the lense.zeroTimeBlackCal
: The fast black calibration where exposure time is set to as close to zero as possible.whiteBalance
: Take a white reference and compute the white balance matrix. (API value whiteBalance
.)takeStillReferenceForMotionTriggering()
— When motion triggering is used, the camera may need to be trained to recognise a still scene. For example, if there is some background motion such as trees swaying in the wind, this function will teach the camera to ignore it as background noise.saveCalibrationData(str toFolder)
— Write all sensor calibration data (eg; black cal, white balance) to the folder specified. Usually, one would use the folder '/dev/sda'
, as that represents the first external storage device plugged in on Linux systems such as the camera runs. Calibration data may be restored using the isomorphic loadCalibrationData
.loadCalibrationData(str fromFolder)
— Restores the calibration data saved by saveCalibrationData
, overwriting all active calibration data.applySoftwareUpdate()
— Install another firmware version from an external storage device. See the manual on "updating" for more details.waterfallMotionMap({[id: str name], ['startFrame': int], ['endFrame': int]}) → {startFrame: int, endframe: int, heatmap: 16×n little-endian byte map}
— (unstable)
Generate a waterfall-style heatmap from each of the 16 quadrants of recorded motion data. Returns an 16×n greyscale bitmap, one byte per pixel.
Arguments are a map specifying:
Segment IDs, and their respective startFrame and endFrames, can be read from the recordedSegments value.
saveRegions([{[id: str], [start: int], [end: int], path: str, format: { fps: float, bpp: float, maxBitrate: float }}, …])
— (unstable) Save each region in the input list to file.
Save video clips to disk or network.
Accepts a list of regions, returns a list of statuses.
As with waterfallMotionMap, if no segment id is specified, 'all recorded data' is considered to be the segment, and start/end is relative to all recorded data.
Python example:
api.control('saveRegions', [{
"start": 19104,
"end": 39801,
"id": 'ldPxTT5R', #from api value recordedSegments
"path": '/dev/sda/',
"format": {
'fps': 29.97, #NTSC video
'bpp': .7,
'maxBitrate': 40, #mbps
},
}])
A list of regions available to save and paths available to save to can be retrieved with api.control('get', ['recordedSegments', 'externalStorage'])
.
formatStorage(str path)
— Erase and prepare the device at path for saving video to. A list of paths available to format to can be retrieved with api.control('get', ['externalStorage'])
. Be careful not to erase saved footage!unmount(str path)
— Unmount the storage device mounted at path. A list of paths available to unmount to can be retrieved with api.control('get', ['externalStorage'])
.
To remount a device, either reinsert it or SSH into the camera and run the mount command.
df() → str output
— Returns text written to stdout by the linux utility df
. This is mostly useful for debugging.
Example:
Filesystem 1K-blocks Used Available Use% Mounted on
/dev/root 757856 158420 5683476 22% /
/dev/mmcblk0p1 39497 3961 35537 11% /boot
/dev/sda1 39497 2973 36525 8% /media/sda1
/dev/sda2 757856 167288 5574608 23% /media/sda2
testNetworkStorageCredentials({[networkStorageAddress: str], [networkStorageUsername: str], [networkStoragePassword: str]}) → str error
— Check if a remote file share is available. Returns either a string containing the error message if the share is not connectable-to, or an empty string if the credentials were valid. networkStorageAddress, networkStorageUsername, and networkStoragePassword may be provided to override the API values of the same names.In addition, when a camera setting is changed, a signal named the key is emitted on the D-Bus interface. It can be intercepted with QDBusConnection.systemBus().connect('ca.krontech.chronos.control.mock', '/', '', NAME_OF_KEY, CALLBACK)
.
API Values hold and reflect the state of the camera. They are read and written using the get
and set
method calls listed above. A list of value names can be retrieved by calling the availableKeys
method.
Values can have different levels of mutability:
All strings are UTF-8 encoded. All integers are little-endian. All float values are 64-bit IEEE-754 encoded.
Available values are:
Value Name | Type | Mutability | Description |
---|---|---|---|
HTTPPort |
integer | variable | Specify the port for the web server to run on. This is used for the web app and HTTP API. You can enable the web app and HTTP API by setting localHTTPAccess or remoteHTTPAccess to True . |
SSHPort |
integer | variable | Set the Secure Shell port. By default, port 22 is used, but it is recommended to use a high random port for day-to-day use. It defaults to port 22 because it's easier to connect to, but it is less secure because this is where everyone looks for SSH. |
availableRecording |
list | constant | Get a list of integer gains the image sensor output can be amplified by. Each element of the list is a map with two keys, multiplier and dB . The multiplier values can be used to set previewAnalogGainMultiplier and recordingAnalogGainMultiplier . The decibel values are purely informational. Higher amplification levels will produce a noisier image. Amplification is an analog operation, versus a digital operation like you might get post-processing a video to be brighter. |
batteryCharge |
float | property | On a scale of 0.0 to 1.0, how full is the camera battery? 0 is dead, and 1 is fully charged. |
batteryVoltage |
float | property | A measure of the power the removable battery is putting out, in volts. A happy battery outputs between 12v and 12.5v. This value is graphed on the battery screen on the Chronos. |
cameraApiVersion |
string | constant | The current iteration of this API. Adheres to SemVer. Example: 1.13.0 |
cameraDescription |
string | variable | Does nothing. Can be used to give an easy-to-identify name to a camera when many cameras are being used. Supports emoji. |
cameraFpgaVersion |
string | constant | The firmware iteration the field-programmable gate array inside the Chronos is running. (The FPGA is primarily responsible for recording the high-speed video coming off the sensor to a buffer in RAM.) Example: 1.13.0 |
cameraMemoryGB |
float | constant | Amount of RAM installed in the Chronos in GB (vs GiB). This will correspond to the memory amount printed on the bottom of your camera, if you haven't made any modification. |
cameraModel |
string | constant | The product identifier of the camera hosting the API. This corresponds to the model printed on the bottom of your camera. Example: CR14-1.0 |
cameraSerial |
string | constant | The serial number of the product the API is running on. This corresponds to the SN printed on the bottom of your camera. Example: 00204 |
colorMatrix |
list | variable | A 3x3 matrix controlling colour balance of the recorded video. To disable colour correction, set to the identity matrix:
[[1,0,0],
[0,1,0],
[0,0,1]]
|
commonlySupported |
list | constant |
A list of maps. Each map indicates a preset by specifying hRes (int, px), vRes (int, px), and maximum framerate (float, frames per second). Partial example:
[{
'hRes': 1280,
'vRes': 1024,
'framerate': 1057.362,
}, …]
|
datetime |
string | variable | iso 8601-formatted date, YYYY-MM-DDTHH:MM:SS.mmmmmm . Detailed in the Python datetime docs. |
currentCameraState |
enum | property | Indicates what the camera is doing. One of normal , saving or recording . When saving (to file), parts of the API may be unresponsive. The main difference between normal and recording is whether the red indicator lights on the chronos are red or not. Some actions, like 'stop recording' or 'stop saving', only make sense in certain states. |
currentVideoState |
enum | variable | Indicates what the video display is showing. One of viewfinder or playback .. In viewfinder mode, the video on the back of the camera is showing what the sensor is seeing. In playback mode, the video is showing footage previously recorded in RAM. |
dimScreenWhenNotInUse |
boolean | variable | After a period, dim the screen backlight to conserve battery power. This has a fairly small effect overall. |
disableOverwriting |
boolean | variable | In segmented mode, disable overwriting earlier recorded ring buffer segments. |
externalStorage |
list | property | External storage device partitions.
Returns a list of maps, one map per partition on each external storage device. (Storage devices are things like SD cards, USB thumb sticks, etc.) Maps contain the following keys:
externalStorage example:
[{
"name": "Testdisk",
"device": "mmcblk0p1",
"path": "/dev/sda",
"size": 1294839100, #bytes
"free": 4591,
"interface": "sd",
},{
"name": "Toastdesk",
"device": "sdc1",
"path": "/dev/sdc1",
"size": 2930232316000,
"free": 1418341032982,
"interface": "usb",
}]
|
externallyPowered |
boolean | property | Returns True if the camera is running on mains power. In this mode, the battery will recharge until full. |
focusPeakingColor |
integer | variable |
An integer specifying the color of focus peaking, one of the available focus aids. Like HTML/CSS, this value is composed of four packed 8-bit channels, representing Red, Green, Blue, and Alpha (strength). In hexadecimal, this is formatted as 0xRRGGBBAA. For example, 0xAA00AAFF is maximum-strength purple, because red and blue are both set to AA, and alpha is set to FF. 0xC1681988 is half-strength brown.
To turn focus peaking on or off, set focusPeakingIntensity .
|
focusPeakingIntensity |
string | variable | One of 'off' , 'low' , 'medium' , 'high' . Higher intensity means more color on screen. |
localHTTPAccess |
boolean | variable | Enable web access (HTTP API and web app) over the local network, ie, 192.168.x.x. Note that networkPassword must be set before any network-based control is actually turned on. |
localSSHAccess |
boolean | variable | Enable command-line access over the local network, ie, 192.168.x.x. Note that networkPassword must be set before any network-based control is actually turned on. It is strongly recommended to set up key-based authentication as soon as possible, and disable ssh password login via the command line. |
motionTriggerAdaption |
string | variable | Set how quickly the motion triggering algorithm adapts to non-triggering motion sources. Can be set to high , medium , low , or off to disable learning entirely. Even if adaption is disabled, you can call the takeStillReferenceFor method to retrain the motion trigger. |
motionTriggerHOffset |
integer | variable | The region within the recorded picture which the motion trigger monitors for activity. HRes is the horizontal size of the box, VRes is vertical size of the box, HOffset is the horizontal offset from the upper-left corner of the recorded image, and VOffset is the vertical offset. (The recorded image can be changed with analogous recordingHRes/VRes/HOffset/VOffset, assuming videoState is 'recording' or 'pre-recording' . Otherwise, no motion triggering occurs.)See motionTriggerAdaption and takeStillReferenceForMotionTriggering() for configuring motion within the motion trigger region. |
motionTriggerHRes |
integer | variable | |
motionTriggerVOffset |
integer | variable | |
motionTriggerVRes |
integer | variable | |
recordingMode |
string | variable | Configure how the camera records. 'normal' causes one trigger to record the whole buffer. 'segmented' causes one trigger to record one a segment, the size of which is determined by dividing the total available record length by recordedSegments . gated burst uses the full buffer, but only records when an input signal is high. Signals can be configured by setting triggerConfiguration value. |
recordingSegments |
float | variable | Set how many individual video clips are produced while in segmented record mode. (See recordingMode for details.) For example, if you wanted to record 4 events, you might call control().set({'recordingSegments': 4, 'recordingMode', 'segmented'})) . The minimum is 1, the maximum the of number of frames in the recording buffer. Note that the default on-board UI may not handle viewing extremely large numbers of segments well, since each segment is displayed and saved separately. |
recordedSegments |
list | property | A list of the recorded segments of video. Will always contain one entry unless recordingMode is 'segmented' . Each entry is a map containing the following keys:
|
networkInterfaces |
list | property | A list of connections to the outside world through. Add an element to this list by plugging in an Ethernet cable, the On-The-Go Mini USB cable, or a supported Wi-Fi dongle. (No dongles are officially supported yet, however.) Each element contains the following keys:
Note: If no address is available, then the field will be an empty string ( control().get('networkInterfaces') yields
[{
'id': 'enp0s25',
'name': 'Ethernet',
'localAddress4': '192.168.1.135',
'localAddress6':
'fe80::22c3:8fff:fe3b:966a',
'remoteAddress4': '205.250.126.92',
'remoteAddress6': '',
},{
'id': 'wlp4s0',
'name': 'Mini USB',
'localAddress4': '192.168.12.1',
'localAddress6':
'fe80::f81b:26ff:fee7:24dd',
'remoteAddress4': '',
'remoteAddress6': '',
}]
|
networkPassword |
string | variable | The password which must be supplied for HTTP or SSH access. If no password is set, network access is disabled. Retrieving this field will yield an arbitrary number of • s. |
networkStorageAddress |
string | variable | Network storage allows the camera to save video to a remote device via a network connection. Address: IP address of the network storage device (ie, a Samba share) to connect to when saving. Password and Username provide authentication. The currently set credentials can be tested by calling testNetworkStorageCredentials({}) . If you want to test your credentials before setting them, you can pass them to the function. |
networkStoragePassword |
string | variable | |
networkStorageUsername |
string | variable | |
playbackFrame |
int | variable | The current frame video playback is on. Since this may update very frequently, change events will not fire for this value. |
playbackFramerate |
int | variable | A delta applied to playbackFrame, once every 16ms at most. Above 60fps, frames will be dropped, and below 60fps, the playbackFrame will be updated less frequently. |
powerOnWhen |
boolean | variable | Set to True to have the camera turn on when it is plugged in. The inverse of this, turning off when the charger is disconnected, is achieved by setting the camera to turn off at any battery percentage. For example, to make the camera turn off when it is unpowered and turn on when it is powered again - effectively only using the battery to finish saving - you could make the following call:
api.control('set', {
'powerOnWhenMainsConnected': True,
'saveAndPowerDownWhenLowBattery': True,
'saveAndPowerDownLowBatteryLevel': 100,
})
|
previewAnalog |
integer | variable | One of the availableRecordingAnalogGains . Higher values amplify the light level, but as this is an analog operation this also increases image noise. Only used if in preview mode. See also recordingAnalogGainMultiplier . |
previewHOffset |
integer | variable | Like recordingHOffset /recordingHRes /recordingVOffset /recordingVRes but used for preview mode. If currentCameraState is not set to preview , these properties has no effect. |
previewHRes |
integer | variable | |
previewVOffset |
integer | variable | |
previewVRes |
integer | variable | |
recordedSegments |
list | property | A list of maps, one for each separate segment of video recorded. Each map having the following keys:
recordingSegments . There can only be more than one segment if recordingMode was set to 'segmented' while recording.
|
recordingAnalog |
integer | variable | One of the availableRecordingAnalogGains . Higher values amplify the light level, but as this is an analog operation this also increases image noise. Overridden by previewAnalogGainMultiplier when in videoState is 'preview' . |
recordingExposureNs |
integer | variable | Controls image brightness. Longer exposures lead to a brighter image, but shorter exposures cause less motion blur. The exposure must be 5000ns shorter than the recording period. If set to a value greater than that, it will bump up the recording period appropriately. |
recordingPeriod |
integer | variable | Value, in nanoseconds, indicating the total amount of time to take to record a frame. |
recordingHRes |
|||
recordingHoffset |
|||
recordingVRes |
|||
recordingVoffset |
|||
recordingVStep |
|||
remoteHTTPAccess |
boolean | variable | Enable web access (HTTP API and web app) with no restrictions. The HTTP API and web app will be accessible from any device able to route to it. Note that networkPassword must be set before any network-based control is actually turned on. It is strongly suggested to set networkPassword to a multi-word passphrase in this case, to make it hard to guess. |
remoteSSHAccess |
boolean | variable | Enable command-line access to the camera with no ip-based restrictions. Note that networkPassword must be set before any network-based control is actually turned on. It is strongly recommended to set up key-based authentication over the local network or the OTG cable (which is always enabled), and disable ssh password login via the command line. |
saveAndPowerDown |
|||
saveAndPowerDown |
|||
sensorFramerateMax |
|||
sensorHIncrement |
|||
sensorHMax |
|||
sensorHMin |
|||
sensorMaxExposureNs |
|||
sensorMaxShutterAngle |
|||
sensorMilliframerate |
|||
sensorMinExposureNs |
|||
sensorName |
|||
sensorPixelFormat |
string | constant | Either BYR2 for color cameras or y12 for monochromatic cameras. This corresponds to the Color or Mono information printed on the bottom of the Chronos. |
sensorPixelRate |
|||
sensorQuantizeTimingNs |
|||
sensorRecordsColor |
|||
sensorVIncrement |
|||
sensorVMax |
|||
sensorVMin |
|||
showBlackClipping |
|||
showWhiteClipping |
|||
timingExposureDelayNs |
|||
timingExposureDelayNs |
|||
timingMaxExposureNs |
|||
timingMaxPeriod |
|||
timingMaxShutterAngle |
|||
timingMinExposureNs |
|||
timingMinPeriod |
|||
timingQuantization |
|||
totalAvailableFrames |
|||
totalRecordedFrames |
|||
triggerCapabilities |
|||
triggerConfiguration |
|||
triggerDelay |
in frames | ||
triggerDelayNs |
in nanoseconds | ||
triggerState |
|||
triggers |
|||
whiteBalance |
|||
videoDisplayDevice |
|||
videoDisplayHeight |
|||
videoDisplayWidth |
|||
videoDisplayX |
|||
videoDisplayY |
|||
videoState |
|||
whiteBalance |
Some settings related to the video pipeline take a little while to update, so it's recommended to set them all at once with one call to to set
.
A Python reference client for the control API is available. If you're using Python to script, it may be useful as a library. Otherwise, it may serve as a useful example how to connect to and use the D-Bus API.
It is not required to use the reference client. The client simply sands down some of the rough edges of the D-Bus API. For example, when get
ting or set
ting a single value, the reference client automatically converts the single value into the list the D-Bus API requires. It also emits an update signal immediately on subscription, which eliminates the need for separate initialiser code in many cases.
The reference API has the following available methods:
video(methodName: string, optional args: any)
— Convenience method to invoke one of the D-Bus API Video methods. See the video api documentation for details.control(methodName: string, optional args: any)
— get(string or [strings])
— Convenience method to invoke one of the D-Bus API Video methods. See the control settings keys for a list of values. If a string is passed, this method will automatically wrap it in a list before calling the underlying D-Bus method and unwrap the returned list.set({key: value})
— Wrapper for control('set', {…})
. See the control settings keys for a list of values.observe(valueName: string, callback: Callable[[Any], None])
— Invoke the callback with the new value when the value changes. The value is initially presumed to be unknown, and so is considered to change to the current value is when the subscription is started. valueName is one of the control settings keys.observe_future_only
— Like observe
, but the callback is not called on subscription. This is useful for some of the more complex compound values which can arise around sliders and such.@silenceCallbacks(Qt Widget)
— Function decorator for a callback. Before a decorated callback is called, this turns off events for the supplied Qt Widget. For example, on the main screen of the UI, the callback
@pyqtSlot(int)
@silenceCallbacks('uiExposureSlider')
def updateExposureNs(self, newExposureNs):
self.uiExposureSlider.setValue(newExposureNs)
is used by
api.observe('recordingExposureNs', self.updateExposureNs)
to update the exposure slider when the exposure is changed externally.
The video interface deals with getting the video data from A to B.
The full documentation for the video API is available in the chronos-cli repository.
The user interface, on the screen on the back of the Chronos, uses the D-Bus API to drive the camera and keep itself in sync with the state the camera is in.
I have not written this program yet, but what is documented as follows will follow.
The HTTP interface exposes a web API. It also provides web app which uses the web API. The web app can be used from any phone, tablet, or laptop running a modern web browser.
The HTTP interface is very similar to the D-Bus interface. It mostly exposes the same method calls, and provides the same events. However, it also contains an authentication mechanism so other people can't control your camera without your permission. The D-Bus API does not need an authentication mechanism, since anything capable of talking to it is already running on the camera and can do anything the API can do.
So, you have a specialized application you want the camera to perform? Perhaps you just want to lock down the back-of-camera UI? Luckily for you, it is easy to develop your own application to run on the Chronos.
There are two approaches to developing an app. You can write the code on the camera, or you can write the code on a virtual machine and copy it to the camera later. If you want to set up a VM, refer to ~util/chronos debian setup instructions.txt. The following instructions assume you have a working development environment, either on the camera or in a virtual machine.
Developing directly on your Chronos, via SSH or file upload, is simpler than developing on a virtual machine. If you develop via SSH, you'll usually run commands and edit files in-place on the camera with Vim or Emacs. If you prefer file upload, you'll set up a little watcher-script that will restart your application when you change a file. The advantage of uploading the files is being able to edit them locally, with your preferred text editor, although this method is a little more complex. Either way the files on the camera are changed, the result is the same.
Before making any changes to your camera, it is strongly recommended to make a backup of your system SD card. Pop the SD card out of the bottom of the camera, insert it into your computer, and then make a backup of the card filesystems. (Not the files themselves.) On Linux, this can be done with gzip /dev/sdb --to-stdout --verbose > chronos-fs-$(date +%F).gz
, where /dev/sdb
is the device you just plugged in. You can list plugged-in devices with lsblk
. (You should see BOOT
and ROOTFS
on your microSD card.) After you've taken your backup, which will take a while to complete, test it by restoring it to a different microSD card. If you don't have a spare card to test with, it is advised to say a brief prayer instead.
You can connect via the local network by plugging an Ethernet cable into your camera, or connect directly to your computer with a MicroUSB cable. Either way, the camera will get an IP address which you can SSH to. For example, I have my camera connected to my PC via USB, so I run ssh root@192.168.12.1
to connect to my camera. I can also visit fish://root@192.168.12.1/root/
in Dolphin to browse the files on my camera graphically. When prompted, the password is "chronos", or whatever you set it to.
The camera runs Debian 7 (Wheezy). As such, most common linuxisims will still work, but modern programs must be compiled from scratch. The back-of-camera UI, chronos-gui-2, lives in ~/gui
. For your own app, you may wish to build new screens for chronos-gui-2, or use it as a reference for developing your own Qt app. The D-Bus API resides in ~/chronos-cli
. All software running on the CPU is open source, and can be freely downloaded from GitHub. If you'd like to contribute a bug fix or a new feature to the existing app, please drop us a line on the forum so we can coordinate!
So, basically, make a new my_client_app.py
file on the camera and run it with python3 my_client_app.py
. Refer to above for how to make it do stuff.