4. NodeWire Python (nwpython)

nwpython is a variant of python modified for dataflow programming. It is a language developed specifically for NodeWire. In nwpython, every statement is a dataflow assignment and the normal control flow constructs have only limited support.

nwpython is built on these 3 programming constructs:

  • Functions. This is a sequence of assignments. A function can have parameters and can return a value. It is similar to a python function except that it does not support control flow constructs.
  • Rules. A rule is a sequence of assignments with a condition. The assignments are executed whenever the condition is true. The condition is only evaluated when any of the dependent variables of the condition changes.
  • Sequencers. This is used to implement state machines. Each state may have assignments, rules or both. Only the rules in the current state are active at any given time.

4.1. Data Types

nwpython supports standard python data types:

  • Numbers
  • String
  • List
  • Tuple
  • Dictionary/Object

In addition, a special type for representing nodes is also supported. A node is an object whose members are mapped to the node’s ports. Assigning a value to a node’s attribute causes the value to be transmitted to the corresponding port.

Unlike python, nwpython supports using the object syntax for dictionaries:

someone = {
    name: 'Ahmad Sadiq',
    age: 35
}

someone.age = 20

4.2. Expressions and Data flow

Assignments

An assignment results in data flow. The left and right hand side could be nodes that are on two separate devices. They can also be mere variables

k = 7
mynode.port = 0.5
myobj = { num: 4, text: 'val'}
myarray = [1,2,3]

Conditional Expression

k=9
classification='odd' if k%2!=0 else 'even'

List Comprehensions

numbers = [k for k in range(10)]

Dictionary Comprehensions

words = ['one', 'two', 'three']
nums = [1,2,3]
obj = {words[k]:nums[k] for k in range(3)}

4.3. Functions

Function definitions takes exactly the same syntax as python function definitions, except that loop structures are not allowed.

def split(sentence):
    spaces = [-1] + [i for i in range(len(sentence)) if sentence[i]==' '] + [len(sentence)]
    words = [sentence[spaces[i]+1:spaces[i+1]] for i in range(len(spaces)-1)]
    return words

k = split('this is a long sentence')

You can use ‘if’ expressions in a function:

def fn(i):
  if i==9:
    emp = 'nine'
  elif i==8:
    emp = 'eight'
  elif i==7:
    emp = 'seven'
  else:
    return False
  return emp

4.4. Rules

Rules are used to specify expressions that should be evaluated whenever a certain condition is true.

led = 0
delay = time

when time-delay>seconds(1):
  time = delay
  led = 1 if led == 0 else 1

Rules starts with the ‘when’ keyword followed by the condition. Each variable used in the condition is called a signal. And each time any of the signal changes, the condition is evaluated and if true the expressions are then evaluated as well.

When time functions such as seconds, minutes, hours and days are used in the condition, they cause the condition to be evaluated periodically. For example seconds causes the condition to be evaluated every second while hours causes it to be evaluated every hour.

4.5. Sequencers

sequencer blinky:
  init:
    led = 0
    delay = time
    goto toggle
  toggle:
    led = 1 if led == 0 else 1
    goto wait
  wait:
    delay = time
    when time-delay>seconds(1): goto toggle

A sequencer is defined by using the ‘sequencer’ keyword followed by the name of the sequencer instance. Each state of the sequencer is defined by a label and can have assignments or rules under it. Only rules in the current state are active at any point in time. State transition happens by using ‘goto state’ within the sequencer or by assigning the sequencer name to the state from outside the sequencer:

blinky = wait

The initial state is always the first state, ‘init’ in this case.

4.6. Control Flow

Python’s if-elif-else and for statements are supported but only within the above 3 constructs.

when employee:
  if 'userid' in employee:
    emp = employee
  else:
    emp = copy(employee)
    emp.userid = 1

4.7. Built-In Functions

str

with one parameter

Converts number to string

with two parameters

Equivalent to python string’s format function.

str(‘{}-{}-{}’, time.year, time.month, time.mday)

ftime

Time format.

ftime('%X', time) # gets the time component of time
ftime('%x', time) # gets the date component of time

date

converts date to epoch.

epoch = date('2018-04-12', '%Y-%m-%d')

abs

gets the absolute value

len

gets the length of the array.

reduce

reduce([1,2,3], lambda sum, x: sum+x)

sort

sort([1,-2,3], lambda x: abs(x))

node

creates a node object. A node object is a reference to a remote node and can be used to communicated with the node. ports on the node are mapped to attributes of the object.

mynode = node(‘nodename’)

mynode = node(‘nodename’, ‘myinstance’)

int

converts string to integer

float

converts string to float

type

returns the type name of the parameter

copy

makes a shallow copy of the object

deepcopy

makes a deep copy of the object

range

same as python’s range function

app

creates a node from script entered in interactive mode

exec

executes an existing script

kill

terminates a script

4.8. Special Variables

time

Returns the epoch.

You can also use time.second, time.minute, time.hour, time.month, time.year, time.mday, time.wday, amd time.yday to get various compoents of the epoch.

nodes

returns all the nodes currently registered to the instance. You can also monitor when new nodes are connected by using nodes as a signal:

when nodes:
    # new node connected
    no_nodes = len(nodes)

sender

used inside a rule that has an input port as a signal, the node address where the current value of the port came from

inputs = ['led']
when led:
    sender.email = {subject:'led switched', body:'command successfully received'}

signal

returns the name of the signal that triggered the current rule or sequencer state

a = 0
b = 0
c = 0

s = 0

when a or b or c:
    if signal == 'a':
    for i in range(10):
        s = s + i
    else:
        s = a + b + c

me

This variable is used inside a rule to represent the user interface node (usually a browser). It can be used to watch user variables (defined in the layout).

when me.username:
    me.notification = {
        subject:  'Welcome, ' + me.username,
        body: 'You have logged in'
    }

to test this script, you can use the following layout:

<Input title="name" node="me" port="username" />

me.init

This is a signal that is set whenever the ui is loaded on the browser:

inputs

List of input port names

outputs

List of output port names

db

the database object

4.9. Data flow

The Rule and Sequencer constructs are used to implement data flow logic.

inputs = ['data']
data = 0
data2 = 0
items = []

when data:
  data2 = abs(data)

when data2:
  if data2 not in items:
    items =  items + [data2]

Each when construct implement a fixed logic which acts on data as it is passed through the pipeline.

The following script implements a data entry logic where each employee entered is stored in a list. But before then it passes through a 2-stage pipeline where it is checked first for missing userid and an autogenerated value provided if necessary. In the second stage, it is added to the list if an employee with the same userid is not already in the list, otherwise the corresponding list item is updated.

inputs = ['employee']
employee = {}
emp = {}
all_employees = []

when employee:
  if 'userid' in employee:
    emp = employee
  else:
    emp = copy(employee)
    emp.userid = reduce(all_employees, lambda max, x: x if x.userid>max.userid else max).userid+1

when emp:
  ee = [e for e in all_employees if e.userid==emp.userid]
  if len(ee)==0:
    all_employees = all_employees + [emp]
  else:
    index = [i for i in range(len(all_employees)) if all_employees[i].userid==emp.userid][0]
    all_employees[index] = emp

4.10. Accessing Databases and Data-logging

nwpython provides a built-in database that is based on mongoDB. Each instance is assigned an implicit database and it can create and access tables (collections) only within this implicit database

Create a database record

You can save a document to a collection by assigning it to the db object while specifying the collection name as follows:

db[table] = {
   name: 'Ahmad Sadiq',
   age: 35
}

where ‘table’ is the name of the collection. Note that each time you save a document , a uniques _id of 12 bytes hexadecimal number is added to it.

Retrieve a database record

to retrieve a document from a collection, use the mongoDB query syntax:

somepeople = db[table({name:'Ahmad Sadiq'})]

note that the return value is a list.

To return all the documents in a collection use:

allthepeople = db[table({})]

Update a database record

Saving a record while specifying an existing _id will update the record that matches the _id specified:

db[table] = {
   _id: '5abcfec2c6356b5e21df2894',
   name: 'Ahmad Sadiq',
   age: 20
}

4.11. The auto script

This script is automatically executed whenever the cloud service (re)starts. The cloud service could be restarted at anytime for maintenance or update. When this happens all your scripts that were running will be terminated. You can use the auto app to automatically restart all the scripts that you require to be running at all times.

The auto script must be saved under the project name ‘auto’.