edelkrone SDK User Guide
1 Getting Started
The edelkrone SDK controls edelkrone motion control equipment from Python over Bluetooth Low Energy. It supports heads, sliders, jibs, dollies, focus motors and tripods, used on their own or combined in a layout. Release 0.1 covers connecting, reading the device's identity and state, joystick control, stored poses, focus, settings and the tripod.
pip install edelkrone
| Python | 3.11 or newer. |
| Platforms | macOS, Windows and Linux. Bluetooth must be enabled. On macOS, the terminal or application requires Bluetooth permission (System Settings → Privacy & Security → Bluetooth); without it, no units are found. |
| Dependencies | bleak, the Bluetooth package, is installed automatically. |
| Included in the package | This guide, in edelkrone/doc/ inside the installed package, and two example programs: python -m edelkrone.examples.read_device and python -m edelkrone.examples.store_and_return. |
| Version | edelkrone.__version__ |
A first program:
import edelkrone
for unit in edelkrone.discover(): # units in range
print(unit.to_json())
with edelkrone.connect(name="HeadPLUS V3") as device:
print(device.info.to_json()) # identity
print(device.status.to_json()) # current state
device.move.joystick(pan=0.3) # a speed, not a target; repeat while moving
device.move.stop()
device.poses.store(0) # store the current position in slot 0
result = device.poses.move_to(0) # return to it
if not result.is_success:
print(result.reason) # the reason the move did not complete
The SDK follows three principles:
- Calls are synchronous. Each call returns when its operation has completed. No event loop is required. In asynchronous code, run a call in a worker thread, for example
await asyncio.to_thread(device.poses.move_to, 0). - The SDK polls the device. After
connect(), the SDK requests a new reading four times per second, anddevice.statusalways holds the latest one. - Refusals are returned; faults are raised. A request the device declines, such as a move to an empty slot, returns a result that states the reason. A failure of the connection or of a command raises an exception (section 9).
Not included in release 0.1. Targets and point mode, driving to coordinates, creating a dolly path and reading the dolly position, and timelapse. These functions are planned for later releases.
2 Connecting
2.1 edelkrone.connect()
connect() finds the unit, connects to it, reads its identity and a first reading, joins its modules if required, starts polling and returns a Device. If any of these steps fails, it raises an exception that identifies the step (section 9).
device = edelkrone.connect(name="HeadPLUS V3", mac="A4:C1:38:5E:7B:2D", timeout=20.0)
| Argument | Default | Description |
|---|---|---|
name |
None |
The product name printed on the unit. Case and spaces are ignored: "HeadPLUS V3", "headplus v3" and "HEADPLUSV3" are equivalent. |
mac |
None |
The unit's Bluetooth address, in any common notation. The address does not change when the unit is restarted. |
with_modules |
None |
For units that have not been used together before: the addresses of the modules to join to this unit (section 2.3). |
timeout |
20 s | How long to search for the unit before raising not_found. |
transport |
The desktop Bluetooth radio | A Transport held by the program, so that several calls share one radio. A radio opened by connect() is closed by disconnect(). |
config |
Default values | A SessionConfig that sets timeouts and the polling interval. |
allow_narrowing |
False |
Set to True to connect to a unit on its own while it is configured to drive modules. By default this is refused, because poses stored in the reduced layout would not include the modules' axes. |
Specify a name, an address, or both; when both are given, both must match. When neither is given, the SDK connects to the only unit in range. If more than one unit is in range, the call is refused and the message lists the units found.
2.2 Finding Units in Range: edelkrone.discover()
edelkrone.discover(seconds=8.0) scans for the given time and returns every unit found as a list of Seen, ordered by signal strength. It accepts the same transport argument as connect().
for unit in edelkrone.discover():
print(unit.to_json())
device = edelkrone.connect(mac=edelkrone.discover()[0].mac)
{"name": "HeadPLUS V3", "mac": "A4:C1:38:5E:7B:2D", "signal": -51,
"layout": "pan_tilt_and_slide", "modules": ["SliderPLUS V6"], "recognised": true}
| Field | Description |
|---|---|
name, mac |
The product name and the Bluetooth address, as accepted by connect(). |
signal |
The received signal strength in dBm. Values closer to zero indicate a stronger signal. The list is sorted by this field. |
layout, modules |
The layout the unit advertises and the product names of the modules in it. Empty for a unit on its own. |
recognised |
Whether the SDK supports this combination of units. An unrecognised combination is listed but cannot be connected to. |
Connected units are not listed. A unit does not advertise while it is connected to another program or to the edelkrone application on a phone. If a powered unit in range is not listed, close any other connection to it.
2.3 Layouts
Modules mounted on a head, such as a slider, a jib or a dolly, are controlled through the head. The program connects to the head, which relays commands to its modules. device.info.modules lists the modules and device.info.layout names the layout, for example PAN_TILT_AND_SLIDE.
The two three-piece products are also layouts, and in both the program connects to the head:
- Krone X: a HeadPLUS V3 with a JibPLUS and a DollyPLUS (
PAN_TILT_DOLLY_AND_JIB_PLUS). - Krone S: a HeadPLUS V3 with a SliderPLUS and a DollyPLUS (
PAN_TILT_DOLLY_AND_SLIDER).
Units that have been used together before advertise their layout, and connect() requires no further arguments. Units that have not been used together must be joined once. Pass the module addresses in with_modules; the SDK joins them by cable or by radio, as the hardware requires:
device = edelkrone.connect(mac="A4:C1:38:5E:7B:2D", with_modules=["C8:F0:9E:11:22:33"])
A combination of units that the SDK does not support is refused with NotSupported, and the message names the units.
2.4 Ending the Connection
device.disconnect() stops polling and closes the connection. If connect() opened the radio, it closes the radio as well. Calling it more than once has no further effect. A Device is a context manager, so a with block closes the connection in all cases:
with edelkrone.connect(name="SliderPLUS V6") as device:
device.move.joystick(slide=0.2)
time.sleep(1.0)
device.move.stop()
Stop the equipment before disconnecting. Disconnecting does not stop moving equipment. Call
device.move.stop()first, and place both calls in afinallyblock so that the equipment stops if the program fails.
3 The Device
connect() returns a Device. Its info and status describe the unit, and four groups provide the operations: move, poses, focus and settings. The Device itself manages the connection:
| Member | Description |
|---|---|
info |
The unit's identity, read when connecting (section 3.1). |
status |
The latest reading (section 3.2). |
status_updates |
A stream that delivers each new reading; see subscribe() in section 3.2. |
refresh() |
Requests the identity, position and state from the device immediately. |
poll_position(every=…) |
Keeps status.pose up to date. The periodic reading reports the battery and the activity, but not the position; without position polling, status.pose holds the value from the last refresh(). poll_position() requests the position every 250 ms, poll_position(every=1.0) once per second, and poll_position(every=None) stops. Disabled by default, because it doubles the radio traffic. |
is_connected |
Whether the connection is currently open. |
connection_changes |
A stream of True and False values as the connection opens and closes. |
disconnect() |
Closes the connection. A with block calls it on exit. |
3.1 Identity: device.info
An Info object, read when connecting and unchanged until the connection is closed. to_dict() returns it as a dictionary and to_json() as a JSON object; the field names match the Python attributes. All readings and results in the SDK provide the same two methods.
{
"name": "HeadPLUS V3",
"mac": "A4:C1:38:5E:7B:2D",
"firmware": "0.31",
"radio_firmware": "0.34",
"firmware_ok": true,
"layout": "pan_tilt_and_slide",
"modules": ["SliderPLUS V6"],
"axes": ["pan", "slide", "tilt"],
"capabilities": ["focus", "manual_drive", "position", "settings", "stored_pose"]
}
| Field | Description |
|---|---|
name, mac |
The product name printed on the unit and its Bluetooth address. |
firmware, radio_firmware, firmware_ok |
The firmware versions of the device and of its radio module, and whether both meet the minimum versions this SDK supports. If firmware_ok is false, update the firmware with the edelkrone application before continuing. |
layout |
The modules the unit drives and their arrangement. null if the unit reports no layout. |
modules |
The product names of the other units in the layout. |
axes |
The axes the layout can drive or read, as RigAxis values: PAN, TILT, SLIDE, JIB_PAN and JIB_TILT, shown in lower case in JSON. A head on its own has two axes; a head with a slider has three. |
capabilities |
The functions the SDK supports on this model, as Capability values: POSITION, STORED_POSE, MANUAL_DRIVE, FOCUS, SETTINGS and TRIPOD_LEVEL, shown as position, stored_pose, manual_drive, focus, settings and tripod_level in JSON. A function not listed here is refused with NotSupported. To check before calling, use for example Capability.FOCUS in device.info.capabilities. |
3.2 State: device.status
A DeviceStatus object holding the latest reading, in degrees, centimetres, percent and seconds. It is held in memory, so reading it does not communicate with the device. Polling updates it four times per second, and device.refresh() requests a reading immediately. Any field may be null, which always means that the device does not report the value. For example, a slider has no pan angle and a tripod reports no battery level.
{
"model": "HeadPLUS V3",
"at": 1789822822.031,
"activity": "idle",
"pose": {"pan_deg": 89.96, "tilt_deg": 0.0, "slide_cm": 12.4, "focus_cm": null, "jib_pan_deg": null, "jib_tilt_deg": null},
"slide_fraction": 0.31,
"battery_percent": 82.0,
"is_calibrated": true,
"has_focus_motor": false,
"layout": "pan_tilt_and_slide",
"stored_pose_slots": [0, 3],
"modules_responding": true,
"move_progress": null,
"position_is_trusted": true,
"has_status_frame": true,
"tilt_deg": 1.2,
"is_level": true,
"orientation_warning": false
}
| Field | Description |
|---|---|
model, at |
The product name of the unit that sent the reading, and the time of the reading in seconds. |
activity |
An ActivityState: IDLE, MOVING, TRANSITIONING or TRACKING_TARGET, shown as idle, moving, transitioning and tracking_target in JSON. The last two occur only while the edelkrone application is tracking a target with the head. null before the first reading. is_moving provides the same information as a Boolean. |
pose, position_is_trusted |
The position of the equipment as a RigPose: pan and tilt in degrees, the slider position in centimetres and the jib axes in degrees. Axes not present in the layout are null. position_is_trusted is false when the position was taken from a stored slot rather than from a live reading. |
slide_fraction |
The slider or jib position as a fraction of its travel, from 0 to 1. Unlike the value in centimetres, it is included in every reading. null until the slider has been referenced. |
battery_percent |
The battery level. A tripod is powered from the mains and reports none. |
is_calibrated |
For a slider, whether it has been referenced; for a head, whether its level reference is set. null for models that require neither. A slider that is not referenced refuses planned moves and sounds its buzzer; reference it with the edelkrone application. |
has_focus_motor |
Whether a focus motor is fitted, as reported by the device. |
layout |
The layout most recently reported by the device. |
stored_pose_slots |
The slots that contain a stored pose. |
modules_responding |
Whether the modules in the layout respond to the unit. A module that advertises is powered, but not necessarily responding. |
move_progress |
The progress of a planned move, from 0 to 1. |
has_status_frame |
Whether a complete reading has been received. Until then, the other fields are null. The property is_known returns the same value. |
tilt_deg, is_level, orientation_warning |
The unit's inclination from level in degrees, whether it is within 15°, and the device's orientation warning. Reported by PRO heads and by sliders; null on units without an inclination sensor. |
To receive each reading as it arrives, subscribe to status_updates:
for status in device.status_updates.subscribe(max_pending=8):
print(status.pose.pan_deg, status.battery_percent)
A subscription is a queue that the program reads at its own pace. max_pending sets how many readings it holds (256 by default); when it is full, the oldest reading is discarded. Read the subscription on a separate thread and do not send commands from within it: the SDK refuses a command sent from a subscription callback, because the reply would have to be delivered on the thread that is waiting for it.
4 Moving
4.1 Joystick Control: device.move.joystick()
joystick() sets a speed for each axis, from −1 to 1 as a fraction of the maximum speed. It does not set a target position. The command must be repeated approximately every 100 ms for as long as the equipment is to move; when commands stop arriving, the device decelerates and stops after approximately one second. Call stop() to end the movement.
deadline = time.monotonic() + 2.0
try:
while time.monotonic() < deadline:
device.move.joystick(pan=0.3, tilt=-0.1)
time.sleep(0.1)
finally:
device.move.stop()
| Axis | Movement |
|---|---|
pan, tilt |
The head. |
slide |
The slider carriage. In a jib layout, the height of the jib arm. |
jib_pan, jib_tilt |
The rotation and the tilt of the jib arm. |
linear, angular |
A dolly: forward and backward movement, and rotation in place. A dolly on its own moves freely; a dolly in a layout moves only along an existing path, created with the edelkrone application. |
Axes not present in the layout are ignored, so passing zero for them has no effect. Values outside ±1 are limited to ±1. A value that is not a number is refused.
4.2 Stopping
| Member | Description |
|---|---|
stop(settle=2.0) |
Stops all movement: joystick control and a move to a stored pose alike. The stop is sent at once, ahead of any other pending command; every axis is then set to zero, and the call waits up to settle seconds for the device to report that it is stationary. It does not raise exceptions, so it can be used in a finally block: if the connection was lost during the movement, no second exception conceals the first. |
is_moving |
Whether the latest reading reports the device as moving. None before the first reading, which differs from False. |
wait_until_idle(timeout=60.0) |
Waits until the device reports that it is idle; returns False on timeout. poses.move_to() already waits in this way. |
Polling pauses during joystick control. While any joystick axis is non-zero, the SDK pauses polling to keep the joystick responsive. During this time
status_updatesdelivers no readings anddevice.statuskeeps the last one. Polling resumes aftermove.stop()or a joystick command with all axes at zero.
5 Poses
A pose is the position of every axis in the layout, stored in a numbered slot on the device. move_to() returns the equipment to a stored pose; the device plans the motion so that all axes arrive at the same time.
device.poses.store(0)
result = device.poses.move_to(0, style=MoveStyle(speed=0.15, acceleration=0.4))
print(result.to_json()) # {"accepted": true, "completed": true, "duration": 3.4, "reason": null}
| Member | Description |
|---|---|
store(slot) |
Stores the current pose in slot and confirms from the next reading that the device has stored it. Returns False if the device did not store it, for example for a slot the model does not have, or on a dolly, which stores its positions on its path instead. A HeadPLUS V3 provides slots 0 to 11. |
stored |
The slots that contain a pose, as last reported by the device. To test a slot: slot in device.poses.stored. |
clear(slot), clear_all() |
Deletes the pose in one slot, or in all slots. |
move_to(slot, style=None, start=None, timeout=60.0, verify=True) |
Moves to the stored pose and waits until the equipment is stationary. With verify enabled, the SDK compares the start and end positions, and a move in which the equipment did not move is not reported as successful. start names a slot to plan the move from, instead of the current position. A refusal is returned as a result, not raised. |
start_move_to(slot, style=None) |
Sends the move and returns immediately, for programs that control several devices. move.wait_until_idle() waits for the move to finish. |
The result of a move is a MoveOutcome:
| Field | Description |
|---|---|
accepted |
The device accepted the command. |
completed |
The device reported the move as finished and the equipment as stationary. |
duration |
The duration of the move in seconds. |
reason |
If the move did not take place, the reason, for example an empty slot, a slider that is not referenced, or equipment that did not move. None on success. |
is_success |
True when both accepted and completed are true. |
A MoveStyle defines how a move is performed. Its two values range from 0 to 1 as fractions of the device's maximum:
MoveStyle(speed=0.15, acceleration=0.4) # slow, with long acceleration and deceleration
MoveStyle(speed=1.0, acceleration=0.01) # the fastest possible move
MoveStyle() # speed 0.5, acceleration 0.25: the default of move_to()
MoveStyle(speed=0.3, acceleration=0.25, loop=True) # moves back and forth between two poses until move.stop()
speed sets the speed. acceleration sets the share of the move used for acceleration at the start and deceleration at the end. loop repeats the move back and forth.
Short moves. If the equipment is within approximately 3° of the stored pose, the device may not start the planned move. In this case
move_to()returns a result withis_successset toFalseand a reason stating that the equipment did not move. This is a property of the device and not a fault.Axes stored in a pose. A pose stores only the axes of the current layout. If a head is connected without its modules, its poses contain pan and tilt only. Check
device.info.layoutbefore storing poses.
6 Focus
A head with a focus motor, or a focus module on its own, sets focus as a percentage of the lens travel, from 0 at one end to 100 at the other. The motor must first learn the lens: it sets its reference, finds both ends of the travel and measures the distance between them. setup() performs these three steps. It must be called once per connection and turns the focus ring from one end to the other; the duration depends on the lens. Keep the focus ring clear while it runs.
device.focus.setup() # once per connection; turns the focus ring
reached = device.focus.move_to_percent(50)
print(device.focus.percent()) # current position of the focus ring
| Member | Description |
|---|---|
setup(timeout=120.0) |
Prepares the focus motor. It performs any missing steps in order: motor reference, end detection and travel measurement. A second call returns immediately. It must be called once per connection, and again after the unit has been restarted. Refused if no focus motor is fitted. |
move_to_percent(percent, tolerance=1.0) |
Moves to a percentage of the travel and returns the percentage reached. The device performs the movement and stops at the ends found by setup(). |
percent() |
The current position of the focus ring, as a percentage of the travel. |
setup()is required before other operations. On a unit that reports a focus motor, the following calls raiseCommandError·needs_focus_setupuntilsetup()has been called on the current connection: the joystick, all stored-pose operations, the settings and the tripod operations. The following calls are always available: all reads (info,status,refresh(),poses.stored),move.stop()andmove.wait_until_idle(),disconnect()andsetup().The reason is that a stored pose on such a unit includes the focus position, which is measured from the ends found by
setup(). A pose stored or recalled beforesetup()would place the focus ring at an unintended position.When
connect()connects to a unit with a focus motor, it issues aUserWarningstating thatsetup()is required. To suppress it, usewarnings.filterwarnings("ignore", message=".*focus motor.*"). Units without a focus motor are not affected.
Focus in centimetres, that is a distance to the subject, is not supported in this release.
7 Settings
| Member | Description |
|---|---|
buzzer(muted=…) |
Mutes or unmutes the buzzer. |
The device does not acknowledge settings. The call returns once the command has been sent.
8 Tripod
A motorized tripod levels itself and raises or lowers its column. The two operations are reached through device.settings and are refused on units other than tripods.
device.settings.level_tripod() # starts the automatic levelling
end = time.monotonic() + 3.0 # raises the column for 3 seconds
while time.monotonic() < end:
device.settings.drive_tripod(40)
time.sleep(0.2)
device.settings.drive_tripod(0) # stops the column
| Member | Description |
|---|---|
level_tripod() |
Starts the automatic levelling of the tripod. |
drive_tripod(relative_speed) |
Moves the column at a speed from −100 to 100: positive values extend it, negative values retract it, and 0 stops it. Values outside the range are limited to −100 and 100. It must be repeated every 100 to 200 ms for as long as the column is to move, in the same way as the joystick; a single call does not move the column. |
The tripod does not acknowledge these commands. The calls return once the command has been sent.
9 Errors
The SDK distinguishes two kinds of failure. A refusal, such as an empty slot, a slider that is not referenced or a move that did not start, is returned as a result with is_success and a reason. A fault raises one of three exceptions, all subclasses of edelkrone.Error. Each carries a reason code, a message and a cause with technical details for support requests.
try:
device = edelkrone.connect(name="HeadPLUS V3")
except edelkrone.ConnectionError as e:
print(f"{e.reason}: {e.message}") # not_found: HeadPLUS V3 was not seen in 20.0 s. Seen: …
| Exception and reason | Cause | Action |
|---|---|---|
ConnectionError · not_found |
The unit was not found within the timeout. It is switched off, out of range, or connected to another program or phone. | Check the power and close other connections to the unit. discover() lists the units in range. |
ConnectionError · ambiguous |
No name or address was given, and more than one unit is in range. | Specify the unit. The message lists the units found and their addresses. |
ConnectionError · no_radio |
The computer has no Bluetooth adapter, or the Bluetooth package is not installed. | Check the adapter. Reinstall with pip install edelkrone. |
ConnectionError · no_permission |
The operating system has not granted Bluetooth access to the program. | macOS: System Settings → Privacy & Security → Bluetooth. |
ConnectionError · timed_out |
The unit was found but did not respond while connecting (three attempts at three-second intervals), or did not respond to the first identity request. | Restart the unit and reduce the distance. |
ConnectionError · dropped |
The connection was lost. | Reconnect. Place move.stop() and disconnect() in a finally block. |
ConnectionError · wrong_device |
The unit connected but does not provide the services expected for its model, usually because of outdated firmware. | Check info.firmware_ok and update the firmware with the edelkrone application. |
ConnectionError · layout_refused |
The unit did not accept the layout: joining its modules failed, or it is configured with modules and was asked to operate alone. | Check that the modules are switched on. To operate the unit alone, pass allow_narrowing=True. |
CommandError · not_connected |
A command was sent after the connection was lost. | Reconnect. |
CommandError · closed |
A command was sent after disconnect(). |
Connect again. A Device cannot be reused after it is closed. |
CommandError · no_reply |
The device did not respond to a command in time. | Retry once. If the error persists, restart the unit. |
CommandError · bad_reply |
The device sent an unexpected response. | Report the error with info.firmware and cause. |
CommandError · bad_argument |
An argument is invalid, for example not a number, a slot out of range or a speed of nan. |
Correct the argument. |
CommandError · needs_focus_setup |
The unit reports a focus motor and setup() has not been called on this connection. Only calls that change the equipment raise it. |
Call device.focus.setup() once per connection (section 6). |
NotSupported · unsupported |
The model or layout does not provide the function, for example a pose on a tripod or focus on a slider, or the units named in with_modules do not form a supported layout. |
Check device.info.capabilities. |
Signal strength. A unit at the edge of the radio range can be found by
discover()but may fail after connecting, withdropped,not_connectedorno_reply. Layouts with modules are affected first. If connections are unreliable, reduce the distance to the unit.
10 Diagnostic Log
The SDK writes diagnostic messages to EdelkroneLog. The log is disabled by default. To enable it, assign a function to EdelkroneLog.sink and, optionally, set EdelkroneLog.minimum_level to an EdelkroneLogLevel: TRACE, DEBUG, INFO (the default), WARNING or ERROR.
from edelkrone import EdelkroneLog, EdelkroneLogLevel
EdelkroneLog.sink = lambda level, message: print(f"[{level.name}] {message}")
EdelkroneLog.minimum_level = EdelkroneLogLevel.DEBUG
A sink is an EdelkroneLogSink: a function that takes the level and the message. It may be called from several threads, must not raise exceptions and must return quickly. To forward messages to Python's logging module, call a logging logger from the sink.
11 API Summary
The following table lists every name exported by import edelkrone and the section that describes it.
| Name | Description | Section |
|---|---|---|
connect(), discover() |
Connect to a unit; list the units in range. | section 2 |
Device |
A connected unit, with its identity, state and four operation groups. | section 3 |
Info, DeviceStatus, Seen |
The identity, a reading, and a unit found in a scan. Each can be exported as JSON. | section 3.1, section 3.2, section 2.2 |
MoveControl, PoseController, FocusControl, SettingsControl |
The types of device.move, device.poses, device.focus and device.settings, for use in type hints. They are not constructed directly. |
section 4, section 5, section 6, section 7, section 8 |
MoveOutcome, MoveStyle |
The result of a pose move; the speed and acceleration of a move. | section 5 |
ActivityState, Capability |
The activity of a device; a function a model supports. | section 3.2, section 3.1 |
RigPose, RigAxis, EdlSetup, DeviceFamily |
A position, an axis, a layout, and a device family (head, slider, jib, dolly, focus or tripod). | section 3.2, section 3.1, section 2.3 |
SessionConfig, Transport |
The types accepted by connect(config=…) and connect(transport=…). |
section 2.1 |
Error, ConnectionError, CommandError, NotSupported |
The base exception and the three fault types. | section 9 |
EdelkroneLog, EdelkroneLogLevel, EdelkroneLogSink |
The diagnostic log, its levels and the sink type. | section 10 |
__version__ |
The installed version. | section 1 |
This content is subject to change.
If you have any questions about this document, please contact edelkrone.
Copyright © 2026 edelkrone.