being.motors package

All things motors and motor blocks.

Available motor blocks are:
  • DummyMotor

  • LinearMotor

  • RotaryMotor

  • BeltDriveMotor

  • LeadScrewMotor

  • WindupMotor

Submodules

being.motors.blocks module

Motor blocks.

These blocks represent motors in the being block network. The primary input of a motor block accepts target position values and the primary output outputs the actual position values.

Motor block input and output values.

Motor block input and output values.

Additionally each motor block offers some helper methods to change its state during operation (see being.motors.definitions.MotorInterface for more details).

Motor blocks operate with SI values at their in- and outputs.

Available motor blocks are:

class DummyMotor(length: float = 0.04, name: Optional[str] = None)[source]

Bases: being.motors.blocks.MotorBlock

Dummy motor for testing and standalone usage.

Parameters
  • length (optional) – Length of dummy motor in meters.

  • name (optional) – Motor name.

enable(publish: bool = True)[source]

Enable motor (power on).

Parameters

publish (optional) – If to publish motor changes.

disable(publish: bool = True)[source]

Disable motor (no power).

Parameters

publish (optional) – If to publish motor changes.

motor_state()[source]

Return current motor state.

home()[source]

Start homing routine for this motor. Has then to be driven via the update() method.

homing_state()[source]

Return current homing state.

get_length()[source]

What is the length of the motor that will be shown in the UI?.

step(target: float)[source]

Step kinematic simulation one step further towards target position.

Parameters

target – Target position.

update()[source]

Block’s update / run / tick method.

name: str

Block name. Used in user interface to identify block.

inputs: List[InputBase]

Input connections.

outputs: List[OutputBase]

Output connections.

id: int

Ascending block id number. Starting from zero.

class CanMotor(nodeId, motor: Union[str, being.motors.motors.Motor], profiled: bool = False, name: Optional[str] = None, multiplier: float = 1.0, length: Optional[float] = None, node: Optional[being.can.cia_402.CiA402Node] = None, objectDictionary=None, network: Optional[being.backends.CanBackend] = None, settings: Optional[Dict[str, Any]] = None, **controllerKwargs)[source]

Bases: being.motors.blocks.MotorBlock

Motor blocks for steering a CAN motor.

Set-point values are taken from the blocks position value input (cyclic position mode). If profiled=True use profiled position mode instead. The message input targetPosition can than be used to play new PositionProfiles. In any case the actual position values are streamed via the actualPosition value output.

This class initializes all the necessary components for accessing and configuring a CAN motor:

Components needed for each CAN motor.

digraph G {
    node [shape=box];
    subgraph cluster_0 {
        label = "CanMotor";
        network[label="Network"]
        motor[label="Motor"]
        subgraph cluster_1 {
            label = "Controller";
            homing[label="Homing"]
            node_[label="Node"]
        }
    }
}

Important

Since most of the time the network will be created implicitly always use this class within being.resources.manage_resources() context manager.

>>> with manage_resources():
...     motor = CanMotor(nodeId=1, 'DC 22')

Otherwise necessary cleanup can not take place and the network will not get disconnected at the end. Motors would stay enabled and the CAN interface could become unreachable on subsequent starts of the program.

This class relays being.pubsub.PubSub publications from the underlying controller instance to the outside.

>>> def error_callback(errMsg):
...     print('Something went wrong', errMsg)
...
... motor.subscribe(MotorEvent.ERROR, error_callback)
Parameters
  • nodeId – CANopen node id.

  • motor – Motor object or motor name.

  • profiled (optional) – Use profiled position mode instead of cyclic position mode.

  • name (optional) – Block name.

  • multiplier (optional) – Multiplier factor which can be used to scale target position / actual position values (only for cyclic position mode).

  • length (optional) – Motor length which will be shown in the web UI.

  • node (optional) – CAN node of motor driver / controller. If non given create new one (DI).

  • objectDictionary (optional) – Object dictionary for CAN node. If will be tried to identified from known EDS files.

  • network – External CAN network (DI).

  • settings (optional) – Motor settings. Dict of EDS variables -> Raw value to set. EDS variable with path syntax (slash ‘/’ separator) for nested settings. Will be forwarded to the controller initialization function.

  • **controllerKwargs – Arbitrary keyword arguments for controller.

controller: Controller

Motor controller.

enable(publish: bool = False)[source]

Enable motor (power on).

Parameters

publish (optional) – If to publish motor changes.

disable(publish: bool = True)[source]

Disable motor (no power).

Parameters

publish (optional) – If to publish motor changes.

motor_state()[source]

Return current motor state.

home()[source]

Start homing routine for this motor. Has then to be driven via the update() method.

homing_state()[source]

Return current homing state.

get_length()[source]

What is the length of the motor that will be shown in the UI?.

update()[source]

Block’s update / run / tick method.

to_dict()[source]

Convert block to dictionary representation which can be used for dumping as JSON.

Returns

Block’s dictionary representation.

name: str

Block name. Used in user interface to identify block.

inputs: List[InputBase]

Input connections.

outputs: List[OutputBase]

Output connections.

id: int

Ascending block id number. Starting from zero.

class LinearMotor(nodeId, motor='LM 1247', **kwargs)[source]

Bases: being.motors.blocks.CanMotor

Default linear Faulhaber CAN motor.

Parameters
  • nodeId – CANopen node id.

  • motor (optional) – Motor object or motor name.

  • **kwargs – Arbitrary keyword arguments for CanMotor.

name: str

Block name. Used in user interface to identify block.

inputs: List[InputBase]

Input connections.

outputs: List[OutputBase]

Output connections.

id: int

Ascending block id number. Starting from zero.

controller: Controller

Motor controller.

class RotaryMotor(nodeId, motor='DC 22', length=6.283185307179586, **kwargs)[source]

Bases: being.motors.blocks.CanMotor

Default rotary Maxon CAN motor.

Parameters
  • nodeId – CANopen node id.

  • motor (optional) – Motor object or motor name.

  • length (optional) – Length of rotary motor in radian.

  • **kwargs – Arbitrary keyword arguments for CanMotor.

name: str

Block name. Used in user interface to identify block.

inputs: List[InputBase]

Input connections.

outputs: List[OutputBase]

Output connections.

id: int

Ascending block id number. Starting from zero.

controller: Controller

Motor controller.

class BeltDriveMotor(nodeId, length: float, diameter: float, motor='DC 22', **kwargs)[source]

Bases: being.motors.blocks.CanMotor

Default belt drive motor with Maxon controller where the object to be moved is attached on the belt.

Parameters
  • nodeId – CANopen node id.

  • length – Length of belt in meter

  • diameter – Diameter of pinion belt wheel.

  • motor (optional) – Motor object or motor name.

  • **kwargs – Arbitrary keyword arguments for CanMotor.

name: str

Block name. Used in user interface to identify block.

inputs: List[InputBase]

Input connections.

outputs: List[OutputBase]

Output connections.

id: int

Ascending block id number. Starting from zero.

controller: Controller

Motor controller.

class LeadScrewMotor(nodeId, length: float, threadPitch: float, motor='DC 22', **kwargs)[source]

Bases: being.motors.blocks.CanMotor

Default lead screw motor with Maxon controller.

Parameters
  • nodeId – CANopen node id.

  • length – Total length of the lead screw in meter.

  • threadPitch – Pitch on lead screw thread (“heigth” per revolution) in meter.

  • motor (optional) – Motor object or motor name.

  • **kwargs – Arbitrary keyword arguments for CanMotor.

name: str

Block name. Used in user interface to identify block.

inputs: List[InputBase]

Input connections.

outputs: List[OutputBase]

Output connections.

id: int

Ascending block id number. Starting from zero.

controller: Controller

Motor controller.

class WindupMotor(nodeId, diameter: float, length: float, motor: str = 'EC 45', outerDiameter: Optional[float] = None, **kwargs)[source]

Bases: being.motors.blocks.CanMotor

Default windup motor with Maxon controller.

Archimedean spiral is used to map angle onto the arc length of the winch.

Parameters
  • nodeId – CANopen node id.

  • diameter – Inner diameter of the spool / coil. Filament is completely unwind. In meters.

  • length – Length of the filament. Corresponds to the arc length on the coil.

  • motor – Motor object or motor name.

  • outerDiameter (optional) – Outer diameter of the spool / coil. This is the diameter when the filament is completely windup. Can be used to compensate of the windup effect of thicker filament. Default is the same as diameter resulting in a circle.

  • **kwargs – Arbitrary keyword arguments for CanMotor.

update()[source]

Block’s update / run / tick method.

name: str

Block name. Used in user interface to identify block.

inputs: List[InputBase]

Input connections.

outputs: List[OutputBase]

Output connections.

id: int

Ascending block id number. Starting from zero.

controller: Controller

Motor controller.

to_dict()[source]

Convert block to dictionary representation which can be used for dumping as JSON.

Returns

Block’s dictionary representation.

being.motors.controllers module

Motor controllers.

Because of ever so small differences between the different motor controller models there are different subclasses of the main Controller class.

class Controller(node: being.can.cia_402.CiA402Node, motor: being.motors.motors.Motor, length: float, direction: float = 1.0, settings: Optional[dict] = None, operationMode: being.can.cia_402.OperationMode = OperationMode.CYCLIC_SYNCHRONOUS_POSITION, **homingKwargs)[source]

Bases: being.motors.definitions.MotorInterface

Semi abstract controller base class.

Implements general, non-vendor specific, controller functionalities.
  • Configuring and managing of CANopen node.

  • Homing

  • Target position clipping range.

  • Drives node state switch jobs (for asynchronous state changes).

  • SI <-> device units conversion.

  • Relaying EMCY errors.

Parameters
  • node – Connected CanOpen node.

  • motor – Motor definitions / settings.

  • length – Clipping length in SI units.

  • direction – Movement direction.

  • settings – Motor settings.

  • operationMode – Operation mode for node.

  • **homingKwargs – Homing parameters.

EMERGENCY_DESCRIPTIONS: List[tuple]

List of (code (int), mask (int), description (str)) tuples with the error informations.

SUPPORTED_HOMING_METHODS: Set[int]

Set of the supported homing method numbers for the controller.

node: being.can.cia_402.CiA402Node

Connected CiA 402 CANopen node.

motor: being.motors.motors.Motor

Associated hardware motor.

direction

Motor direction.

length

Length of motor.

logger

Instance logger.

position_si_2_device

SI position to device units conversion factor.

velocity_si_2_device

SI velocity to device units conversion factor.

acceleration_si_2_device

SI acceleration to device units conversion factor.

lower

Lower clipping value for target position in device units.

upper

Upper clipping value for target position in device units.

switchJob

Ongoing state switching job.

settings

Final motor settings (which got applied to the drive.

lastState

Last receive state of motor controller.

disable()[source]

Disable motor. Schedule a state switching job. Will start by the next call of Controller.update().

enable()[source]

Enable motor. Schedule a state switching job. Will start by the next call of Controller.update().

motor_state() being.motors.definitions.MotorState[source]

Return current motor state.

capture()[source]

Capture node state before homing.

restore()[source]

Restore captured node state after homing is done.

home()[source]

Start homing for this controller. Will start by the next call of Controller.update().

homing_state() being.motors.homing.HomingState[source]

Return current homing state.

init_homing(**homingKwargs)[source]

Setup homing. Done here and not directly in Controller.__init__() so that child class can overwrite this behavior.

Parameters

**homingKwargs – Arbitrary keyword arguments for Homing.

abstract apply_motor_direction(direction: float)[source]

Configure direction or orientation of controller / motor.

format_emcy(emcy: canopen.emcy.EmcyError) str[source]

Get vendor specific description of EMCY error.

error_history_messages()[source]

Iterate over current error messages in error history register.

set_target_position(targetPosition: float)[source]

Set target position in SI units.

get_actual_position() float[source]

Get actual position in SI units.

play_position_profile(profile: being.motors.definitions.PositionProfile)[source]

Play a position profile PositionProfile.

play_velocity_profile(profile: being.motors.definitions.VelocityProfile)[source]

Play velocity profile VelocityProfile.

state_changed(state: being.can.cia_402.State) bool[source]

Check if node state changed since last call.

publish_errors()[source]

Publish all active EMCY errors. Active error messages get discard afterwards.

update()[source]

Controller tick function. Does the following: - Observe state changes and publish - Observe errors and publish - Drive homing - Drive state switching jobs

class Mclm3002(*args, homingMethod: Optional[int] = None, homingDirection: float = 1.0, operationMode: being.can.cia_402.OperationMode = OperationMode.CYCLIC_SYNCHRONOUS_POSITION, **kwargs)[source]

Bases: being.motors.controllers.Controller

Faulhaber MCLM 3002 controller.

This controller does not support the unofficial max current based hard stop homing methods. We monkey patch a CrudeHoming for these cases, which implements the same behavior.

Parameters
  • node – Connected CanOpen node.

  • motor – Motor definitions / settings.

  • length – Clipping length in SI units.

  • direction – Movement direction.

  • settings – Motor settings.

  • operationMode – Operation mode for node.

  • **homingKwargs – Homing parameters.

EMERGENCY_DESCRIPTIONS: List[tuple]

List of (code (int), mask (int), description (str)) tuples with the error informations.

SUPPORTED_HOMING_METHODS: Set[int]

Set of the supported homing method numbers for the controller.

HARD_STOP_HOMING = {-4, -3, -2, -1}
init_homing(**homingKwargs)[source]

Setup homing. Done here and not directly in Controller.__init__() so that child class can overwrite this behavior.

Parameters

**homingKwargs – Arbitrary keyword arguments for Homing.

apply_motor_direction(direction: float)[source]

Configure direction or orientation of controller / motor.

node: being.can.cia_402.CiA402Node

Connected CiA 402 CANopen node.

motor: being.motors.motors.Motor

Associated hardware motor.

wasEnabled: Optional[bool]
class Epos4(node: being.can.cia_402.CiA402Node, *args, usePositionController: bool = True, recoverRpdoTimeoutError: bool = True, operationMode: being.can.cia_402.OperationMode = OperationMode.CYCLIC_SYNCHRONOUS_POSITION, **kwargs)[source]

Bases: being.motors.controllers.Controller

Maxon EPOS4 controller.

This controllers goes into an error state when RPOD / SYNC messages are not arriving on time -> recoverRpdoTimeoutError which re-enables the motor when the RPOD timeout error occurs.

Also a simple, alternative position controller which sends velocity commands.

Todo

Testing if firmwareVersion < 0x170h?

Parameters
  • usePositionController – If True use position controller on EPOS4 with operation mode CYCLIC_SYNCHRONOUS_POSITION. Otherwise simple custom application side position controller working with the CYCLIC_SYNCHRONOUS_VELOCITY.

  • recoverRpdoTimeoutError – Re-enable drive after a FAULT because of a RPOD timeout error.

node: being.can.cia_402.CiA402Node

Connected CiA 402 CANopen node.

motor: being.motors.motors.Motor

Associated hardware motor.

wasEnabled: Optional[bool]
EMERGENCY_DESCRIPTIONS: List[tuple]

List of (code (int), mask (int), description (str)) tuples with the error informations.

SUPPORTED_HOMING_METHODS: Set[int]

Set of the supported homing method numbers for the controller.

init_homing(**homingKwargs)[source]

Setup homing. Done here and not directly in Controller.__init__() so that child class can overwrite this behavior.

Parameters

**homingKwargs – Arbitrary keyword arguments for Homing.

firmware_version() int[source]

Firmware version of EPOS4 node.

apply_motor_direction(direction: float)[source]

Configure direction or orientation of controller / motor.

static set_all_digital_inputs_to_none(node: canopen.node.remote.RemoteNode)[source]

Set all digital inputs of Epos4 controller to none by default. Reason: Because of settings dictionary it is not possible to have two entries. E.g. unset and then set to HOME_SWITCH.

set_target_position(targetPosition: float)[source]

Set target position in SI units.

publish_errors()[source]

Publish all active EMCY errors. Active error messages get discard afterwards.

update()[source]

Controller tick function. Does the following: - Observe state changes and publish - Observe errors and publish - Drive homing - Drive state switching jobs

being.motors.definitions module

Abstract motor interface.

class MotorEvent(value)[source]

Bases: enum.Enum

Motor / controller events.

STATE_CHANGED = 1

The motor state has changed.

HOMING_CHANGED = 2

The homing state has changed.

ERROR = 3

An error occurred.

class MotorState(value)[source]

Bases: enum.Enum

Simplified motor state.

FAULT = 0

Motor in fault state.

DISABLED = 1

Motor is disabled.

ENABLED = 2

Motor is enabled and working normally.

class PositionProfile(position: float, velocity: Optional[float] = None, acceleration: Optional[float] = None)[source]

Bases: NamedTuple

Position profile segment.

Units are assumed to be SI. Controller has to convert to device units.

Create new instance of PositionProfile(position, velocity, acceleration)

position: float

Profiled target position value.

velocity: Optional[float]

Maximum profile velocity value.

acceleration: Optional[float]

Maximum profile acceleration (and deceleration).

class VelocityProfile(velocity: float, acceleration: Optional[float] = None)[source]

Bases: NamedTuple

Velocity profile segment.

Units are assumed to be SI. Controller has to convert to device units.

Create new instance of VelocityProfile(velocity, acceleration)

velocity: float

Profiled target velocity.

acceleration: Optional[float]

Maximum profile acceleration (and deceleration).

class MotorInterface[source]

Bases: being.pubsub.PubSub, abc.ABC

Base class for motor like things and what they have to provide.

Parameters

events – Supported events.

abstract disable(publish: bool = True)[source]

Disable motor (no power).

Parameters

publish (optional) – If to publish motor changes.

abstract enable(publish: bool = True)[source]

Enable motor (power on).

Parameters

publish (optional) – If to publish motor changes.

abstract motor_state() being.motors.definitions.MotorState[source]

Return current motor state.

abstract home()[source]

Start homing routine for this motor. Has then to be driven via the update() method.

abstract homing_state() being.motors.homing.HomingState[source]

Return current homing state.

being.motors.homing module

Homing procedures and definitions.

Todo

Documentation. Waiting for feature branch merge.

class HomingState(value)[source]

Bases: enum.Enum

Possible homing states.

FAILED = 0
UNHOMED = 1
ONGOING = 2
HOMED = 3
class CiA402Homing(node, timeout=10.0, **kwargs)[source]

Bases: being.motors.homing.HomingBase

CiA 402 by the book.

start_timeout_clock()[source]

Remember when homing needs to end for timeout.

timeout_expired() bool[source]

Check if timeout expired.

change_state(target) Generator[source]

Change to node’s state job.

set_operation_mode(op: being.can.cia_402.OperationMode)[source]

Set operation mode of node. No questions asked…

homing_job()[source]

Standard CiA 402 homing procedure.

class CrudeHoming(node, minWidth, currentLimit, timeout=10.0, **kwargs)[source]

Bases: being.motors.homing.CiA402Homing

Crude hard stop homing for Faulhaber linear motors.

Parameters

speed – Speed for homing in device units.

property width: float

Current homing width in device units.

reset_range()[source]

Reset homing range.

expand_range(pos: float)[source]

Expand homing range.

halt_drive() Generator[source]

Stop drive.

move_drive(velocity: int) Generator[source]

Move motor with constant velocity.

on_the_wall() bool[source]

Check if motor is on the wall.

homing_job(speed: int = 100)[source]

Standard CiA 402 homing procedure.

being.motors.motors module

Effective hardware motors. Represents the effective motor itself. Different default settings.

class DeviceUnits(position: float = 1.0, velocity: float = 1.0, acceleration: float = 1.0)[source]

Bases: NamedTuple

Motor device units factor. Gear factor not included.

Todo

Should this move to to being.motors.controllers.Controller? Is it motor or controller dependent?

Create new instance of DeviceUnits(position, velocity, acceleration)

position: float

Position factor.

velocity: float

Velocity factor.

acceleration: float

Acceleration factor.

class Motor(manufacturer: str, name: str, length: float = inf, units: being.motors.motors.DeviceUnits = DeviceUnits(position=1.0, velocity=1.0, acceleration=1.0), gear: fractions.Fraction = Fraction(1, 1), defaultSettings: dict = {})[source]

Bases: NamedTuple

Hardware motor.

Definitions, settings for different hardware motors.

Create new instance of Motor(manufacturer, name, length, units, gear, defaultSettings)

manufacturer: str

Manufacturer name.

name: str

Motor name.

length: float

Default motor length.

units: being.motors.motors.DeviceUnits

Device units.

gear: fractions.Fraction

Gear ratio.

defaultSettings: dict

Default settings for this motor.

si_2_device_units(which: str) float[source]

Determines the conversion factor from SI units to unit units. The gear ratio is also taken into account.

Parameters

which – Which factor? Either ‘position’, ‘velocity’ or ‘acceleration’.

Returns

Conversion factor.

orify(things: Sequence) str[source]

Comma separate sequence with final ‘or’.

Example

>>> orify(['a', 'b', 'c'])
'a, b or c'
get_motor(name: str) being.motors.motors.Motor[source]

Lookup motor by name.

Parameters

name – Motor name. Can be lowercase and spaces get deleted for easy lookup.

Returns

Motor informations.

Raises

KeyError – If motor could not be found.

Example

>>> motor = get_motor('LM 1247')
... motor.name
'LM 1247'
>>> get_motor('R2D2')
KeyError: 'Unknown motor R2D2! Try one of LM1247, LM0830, LM1483, LM2070, EC45 or DC22'

being.motors.vendor module

Vendor specific definitions / helpers for the different controllers. Error codes, emergency descriptions, supported homing method.

class MaxonMotorType(value)[source]

Bases: enum.IntEnum

Maxon motor types.

PHASE_MODULATED_DC_MOTOR = 1
SINUSOIDAL_PM_BL_MOTOR = 10
TRAPEZOIDAL_PM_BL_MOTOR = 11
class MaxonSensorsConfiguration(sensorType3, sensorType2, sensorType1)[source]

Bases: NamedTuple

Create new instance of MaxonSensorsConfiguration(sensorType3, sensorType2, sensorType1)

sensorType3: int

Alias for field number 0

sensorType2: int

Alias for field number 1

sensorType1: int

Alias for field number 2

to_int() int

Convert named tuples with _field_bits attribute to int.

classmethod from_int(num: int)

Construct named tuple with _field_bits attribute instance.

class MaxonControlStructure(mountingPositionSensor3: int = 0, mountingPositionSensor2: int = 0, mountingPositionSensor1: int = 0, auxiliarySensor: int = 0, mainSensor: int = 1, processValueReference: int = 0, gear: int = 0, positionControlStructure: int = 1, velocityControlStructure: int = 1, currentControlStructure: int = 1)[source]

Bases: NamedTuple

Maxon EPOS4 Axis configuration Control structure.

Create new instance of MaxonControlStructure(mountingPositionSensor3, mountingPositionSensor2, mountingPositionSensor1, auxiliarySensor, mainSensor, processValueReference, gear, positionControlStructure, velocityControlStructure, currentControlStructure)

mountingPositionSensor3: int

Alias for field number 0

mountingPositionSensor2: int

Alias for field number 1

mountingPositionSensor1: int

Alias for field number 2

auxiliarySensor: int

Alias for field number 3

mainSensor: int

Alias for field number 4

processValueReference: int

Alias for field number 5

gear: int

Alias for field number 6

positionControlStructure: int

Alias for field number 7

velocityControlStructure: int

Alias for field number 8

currentControlStructure: int

Alias for field number 9

to_int() int

Convert named tuples with _field_bits attribute to int.

classmethod from_int(num: int)

Construct named tuple with _field_bits attribute instance.

class MaxonDigitalIncrementalEncoderType(method: int = 0, direction: int = 0, index: int = 1)[source]

Bases: NamedTuple

Defines the configuration of the digital incremental encoder 1.

Create new instance of MaxonDigitalIncrementalEncoderType(method, direction, index)

method: int

Alias for field number 0

direction: int

Alias for field number 1

index: int

Alias for field number 2

to_int() int

Convert named tuples with _field_bits attribute to int.

classmethod from_int(num: int)

Construct named tuple with _field_bits attribute instance.

class MaxonDigitalInput(value)[source]

Bases: enum.IntEnum

Values for Configuration of digital inputs.

NONE = 255
QUICK_STOP = 28
DRIVE_ENABLE = 27
POSITIVE_LIMIT_SWITCH_WITHOUT_ERRORS = 25
NEGATIVE_LIMIT_SWITCH_WITHOUT_ERRORS = 24
GENERAL_PURPOSE_H = 23
GENERAL_PURPOSE_G = 22
GENERAL_PURPOSE_F = 21
GENERAL_PURPOSE_E = 20
GENERAL_PURPOSE_D = 19
GENERAL_PURPOSE_C = 18
GENERAL_PURPOSE_B = 17
GENERAL_PURPOSE_A = 16
HOME_SWITCH = 2
POSITIVE_LIMIT_SWITCH = 1
NEGATIVE_LIMIT_SWITCH = 0