Team:NAWI Graz/ControlSystem

CONTROL SYSTEM

The integration of the living and inanimate components of the project of course requires a reliable and thought-through architecture of control systems. Based on the Raspberry Pi single board computer, we devised a server / client system that manages actuation, measurement and intra-system interaction. Running on Raspbian Linux and written in Python3, both very well documented approaches for hardware - software interfaces, our system is highly adaptable, scalable and offers flexibility during the experimental development of new functionality. The wide availability and low cost of all the hardware components used in the control system of ties well into our concept of engineering without financial hurdles that could slow down or inhibit developement and innovation.

The Server

Our server was built and programmed as the centerpiece of the control architecture that encompasses the bioreactor and measurement system of and as communication hub between scientists, reactor and robot. The single-board Raspberry Pi hardware platform that is present in all parts of the system is also the base of the server component. Connected to the other hardware components via the GPIO (general purpose input output) interface, and communicating with the robot and client via UDP, the server is the central information hub and controller of the system.

The server code controls the interaction modules, the measurement chambers and the communication with the robot in the arena. It listens for commands that are sent by either the robot or the client software and reacts by executing the following functions.

Directly available -Server functions activated by commands sent from client:

  • heat Starts the heating module for 5 seconds.
  • pump_1 Start all pumps for 11 seconds (the length of one measure cycle) to flush the system and fill it with fresh suspension from the reactor.
  • temp Get the current temperature reading from the themperature sensor in the Temperature Interaction Module.
  • cam Take a testpicture with the master camera and report the measurement result.
  • drive Send out a drive command to the Robot to test range and connection.
  • turn Send out a turn command to the Robot to calibrate the radius.
Tab. 1: List of commands and the corresponding functionality in the servercode.

In the case of a real experiment run in the temperature setup, the robot sends one of the following messages to the server according to its position and orientation in the arena and the sensor input it perceives. The server reacts by initiating the corresponding processes as described below.

Robot - Server Interaction :

  • clear

    The server starts a waiting loop to cool down the potentially still hot heating chamber to prevent tainting the current run by previous runs. After this cool-down period, fresh suspension is pumped into the measurement chamber and a baseline measurement is conducted. To prevent any unknown or outside influences, another waiting loop, identical in duration to the activation heating period during a run with activated temperature interaction, is started now. Finally, the suspension is pumped into the measurement chamber and a measurement is taken. The baseline measurement and actual measurement data is now compared and in case the difference surpasses a given threshold, the server sends the turn command to the robot. Otherwise drive sent to the robot to signal an open path ahead.

  • Tab. 2: List of commands for server interaction.
  • obstacle

    The server starts with a cool-down period to prevent tainted measuring results. A baseline measurement is made with fresh suspension. Fresh suspension is pumped into the temperature interaction module and heated to target temperature. The heated and activated suspension is pumped into the measurement chamber and a measurement is taken and compared to the baseline value. If the baseline and the actual measurement have a difference equal or greater than the given threshold, the server sends the turn command to the robot. Otherwise drive sent to the robot to signal an open path ahead.


The Client

To be able to test all of the control systems functionality (pump control, heating, measurements) without having to actually run the full experiments, a small command line application was written in Python3. Intended to being run on any computer in the same network as the Server and Robot, this application allowed us to conveniently send the above mentioned commands to the server for testing and verification of the componenets of the bioreactor setup and robot. Especially during the calibration of the interaction modules and pump cycle lengths, this little tool proved to be very useful.

The commands are sent and answers are received over UDP, a common data transfer protocol. Although not reliable for critical communication because of missing error checking and guaranteed order of the packets arriving, for the small data packets we are sending here it is sufficient.



The client code for testing the system functionality:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#!/usr/sbin/python3
#  ColiBot Client
#  -
#  A minimal udp messaging client to send (test) messages to the ColiBot server.
#
import socket
import sys

# create datagram udp socket
try:
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
except socket.error:
    print('Failed to create socket')
    sys.exit()

host = '192.168.0.4'
port = 4343

while(1):
    msg = input('Enter message to send : ')
    try:
        # Send string
        sock.sendto(str.encode(msg), (host, port))

        # receive data from client (data, addr)
        d = sock.recvfrom(1024)
        reply = d[0]
        addr = d[1]

        print('Server reply : ' + str(reply))
        if (reply.decode("utf-8") == 'turn'):
            print("Got 'turn' message.")
        if (reply.decode("utf-8") == 'drive'):
            print("Got 'drive' message.")

    except socket.error as e:
        print('Socket Error: {}'.format(e))
        sock.close()
        sys.exit()

    except KeyboardInterrupt:
        sock.close()
        sys.exit()


The server code for controlling the bioreactor and measurement system:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
#! /usr/bin/python3
#    Simple udp socket server

import calendar
import datetime
import logging
import socket
import sys
import time

import RPi.GPIO as GPIO
import serial
from cam.colicam_master import ColiCamMaster
from cam.colicam_slave import ColiCamSlave

from w1thermsensor import W1ThermSensor

# heater pins
HEAT = 40

PORT = 4343  # Arbitrary non-privileged port
HOST = "192.168.43.150"

# Messages
OBSTACLE = 'obstacle'
CLEAR = 'clear'
HEATING_TEST = 'heat'
GET_TEMP = 'temp'
PUMP_1_TEST = 'pump_1'
CAM_TEST = 'cam'
TURN = 'turn'
DRIVE = 'drive'

# heating
HEATING = False
TARGET_TEMP = 43 # degrees
INTERVALL = 8 # seconds
HEATING_PERIOD = 300 # seconds
# pump
PUMP_IVALL = 12

DIFF_THRESHOLD = 12

logger = logging.getLogger('colibot')



def get_temperature():
    """
    read temperature from sensor
    """
    sensor = W1ThermSensor()
    temp_c = sensor.get_temperature()
    return temp_c


def toggle_heating(heat):
    """
    GPIO 20 and 21
    HIGH = heating on
    LOW  = heating off
    """
    if heat is False:
        #logger.info("HEATING OFF")
        HEATING = False
        GPIO.output(HEAT, 0)
    else:
        #logger.info("HEATING ON")
        HEATING = True
        GPIO.output(HEAT, 1)


def start_heating():
    """
    """
    logger.info("*** Entering heating loop ...")
    
    while(1):
        temp = get_temperature()
        logger.info("*** Current temperature: " + str(temp))
        if temp >= (TARGET_TEMP - 2):
            logger.info("*** TARGET_TEMP reached!")
            break;
        toggle_heating(True)
        time.sleep(15)
        toggle_heating(False)
    
    heating_time = HEATING_PERIOD # seconds
    start = calendar.timegm(time.gmtime())
    end = start + heating_time
    logger.info("*** activation period, keeping target temp for 5 mins:")
    remaining_time = heating_time
    
    while(1):
#        logger.info("minutes left: "+str( (end - calendar.timegm(time.gmtime()))/60   ) )
        if calendar.timegm(time.gmtime()) >= end:
            break
        
        temp = get_temperature()
        
#        logger.info("-->")
        logger.info("*** Current temperature: " + str(temp))

        if temp >= (TARGET_TEMP ):
            time.sleep(10)
        elif( temp < TARGET_TEMP ):
 #           logger.info("*** heating ...")
            toggle_heating(True)
            time.sleep(5)
            toggle_heating(False)
        
        #if remaining_time % 60 == 0:
        logger.info("> {} mins remaining.".format( remaining_time / 60 ))
        
        time.sleep(5)
        remaining_time -= 14
        
        
def start_pump(pwms, seconds):
    """
    pump_1:
    pwms[0](11, 180)
    pin 13 
    pin 15 
    
    pump_2:
    pwms[1](19, 180)
    pin 21
    pin 23
    --> ~1ml per second 
    """
    logger.info("*** Starting pumps for {} seconds.".format(str(seconds)))
    pwms[0].start(100) # out
    pwms[1].start(100) # in
    
    time.sleep(seconds)
    logger.info("*** Stopping pumps")
    pwms[0].stop()
    pwms[1].stop()





def take_measure():
    logger.info("*** Measuring ...")
    result = cam.measure()
    return result;
    
def diff_results(bl, m):
    r_diff = abs(bl[0] - m[0])
    g_diff = abs(bl[1] - m[1])
    b_diff = abs(bl[2] - m[2])
    
    diff = r_diff + g_diff + b_diff
    logger.info("*** Diff_results: r_diff={} g_diff={} b_diff={}".format(r_diff, g_diff, b_diff))
    logger.info("*** total: {}".format(diff))
    if(diff >= DIFF_THRESHOLD):
        return True
    else:
        return False
    

def create_socket():
    # Datagram (udp) socket
    try:
        sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        logger.info('Socket created')
    except socket.error as msg:
        logger.error('Failed to create socket.')
        GPIO.cleanup()
        sys.exit()

    # Bind socket to local host and port
    try:
        sock.bind((HOST, PORT))
    except socket.error as msg:
        logger.error('Bind failed. ')
        GPIO.cleanup()
        sys.exit()

    logger.info('Socket bind complete')

    return sock


def start_listening(sock, pwms):
    # now keep talking with the client
    try:
        while 1:
            # receive data from client (data, addr)
            rec = sock.recvfrom(1024)
            data = rec[0]
            addr = rec[1]

            logger.info("***")

            if data.decode("utf-8") == OBSTACLE:  # 'obstacle'
                logger.info(
                    "recieved OBSTACLE: requesting colis and heating them to TARGET_TEMP...")
               
                # wait for some time to cool of heating chamber
                cooldown = 180
                logger.info("Waiting {} to cool chamber.".format(cooldown/60))
                time.sleep(cooldown)
                
                
                # fill chamber for baseline measureing
                start_pump(pwms, PUMP_IVALL) 

                # baseline
                baseline = take_measure()
                logger.info("baseline values: r{}, g{}, b{}".format(baseline[0], baseline[1], baseline[2])) 
                
                start_heating()
                
                # activated to chamber
                start_pump(pwms, 5)

                logger.info("... start analyzing")
                measurement = take_measure()
                   
                if (diff_results(baseline, measurement) is True):                    
                    logger.info(" TURN !!.")
                    reply = 'turn'
                else:
                    logger.info(" DRIVE !!.")
                    reply = 'drive'
                  

            elif data.decode("utf-8") == CLEAR:  # 'clear'
                logger.info("recieved CLEAR: requesting colis and start analyzing.")
                # wait for some time to cool of heating chamber
                cooldown = 180
                logger.info("Waiting {} to cool chamber.".format(cooldown/60))
                time.sleep(cooldown)
                # fill chamber for baseline measureing
                start_pump(pwms, PUMP_IVALL) 
                # baseline
                baseline = take_measure()
                logger.info("baseline values: r{}, g{}, b{}".format(baseline[0], baseline[1], baseline[2])) 
                
                logger.info("waiting loop: {} minutes.".format(HEATING_PERIOD/60))
                time.sleep(HEATING_PERIOD)
                

                start_pump(pwms, 5)

                logger.info("... start analyzing")
                measurement = take_measure()

                if (diff_results(baseline, measurement) is True):
                    logger.info(" TURN !!")
                    reply = 'turn'
                else:
                    reply = 'drive'            
                    
            elif data.decode("utf-8") == HEATING_TEST:  # 'heat'
                logger.info("received HEATING_TEST")
                reply = 'OK: HEATING TEST for 5 seconds ...'
                toggle_heating(True)
                time.sleep(5)
                toggle_heating(False)

            elif data.decode("utf-8") == PUMP_1_TEST:  # 'pump_1'
                logger.info("received PUMP_1_TEST")
                reply = 'OK: PUMPING TEST for 11 seconds (measure cycle)'
                start_pump(pwms, 11)
                             
            elif data.decode("utf-8") == GET_TEMP:  # 'temp'
                logger.info("received GET_TEMP")
                temp = get_temperature()
                reply = 'OK: temperature is ' + \
                    str(temp) + ' degree Celsius ...'
            
            elif data.decode("utf-8") == CAM_TEST:  # 'cam'
                logger.info("received CAM_TEST")
                result = cam.measure()
                reply = 'OK: Took pic, result = ' + str(result)
                cam.exit_clean()
            elif data.decode("utf-8") == DRIVE:  # 'drive'
                logger.info("received DRIVE")
                reply = 'drive'
            elif data.decode("utf-8") == TURN:  # 'turn'
                logger.info("received TURN")
                reply = 'turn'
            
                
            else:
                logger.info("invalid message")
                reply = 'FAIL: ... invalid message: [' + \
                    data.decode("utf-8") + ']'


            sock.sendto(reply.encode("UTF-8"), addr)
            logger.info("Sent message.")
            
            # logger.info('Message[' + addr[0] + ':' +
            #       str(addr[1]) + '] - ' + data.decode("utf-8"))

    except KeyboardInterrupt:
        logger.info("Quitting.")
        if HEATING:
            toggle_heating(False)

        sock.close()
        GPIO.cleanup()
        cam.exit_clean()

def main():

    # format
    FORMAT = "[%(asctime)s %(funcName)18s()]   %(message)s"
    fileHandler = logging.FileHandler('log.txt')
    fileHandler.setFormatter(logging.Formatter(FORMAT))
    logger.addHandler(fileHandler)
    logging.basicConfig(level=20, format=FORMAT, datefmt='%d.%m.%Y %H:%M:%S')
    
    logger.info("Start logging")
    
    # setup GPIO
    GPIO.setmode(GPIO.BOARD)
    GPIO.setup(HEAT, GPIO.OUT)
    GPIO.output(HEAT, 0)

    #pwm1
    GPIO.setup(11, GPIO.OUT)

    GPIO.setup(13, GPIO.OUT)

    GPIO.setup(15, GPIO.OUT)

    
    pwm1 = GPIO.PWM(11, 220)
    GPIO.output(13, 0)
    GPIO.output(15, 1)

    #pwm2
    GPIO.setup(29, GPIO.OUT)
    
    GPIO.setup(31, GPIO.OUT)
    
    GPIO.setup(33, GPIO.OUT)
    
    pwm2 = GPIO.PWM(29, 220)
    GPIO.output(31, 0)
    GPIO.output(33, 1)
    
    pwms = [pwm1, pwm2]
    
    logger.info("###############################")
    logger.info("   ColiBot Thermo udp_server   ")
    logger.info("-------------------------------")
    sock = create_socket()
    logger.info("-------------------------------")
    logger.info("...     start listening     ...")
    start_listening(sock, pwms)

    cam = ColiCamMaster()
    ColiCamSlave()


if __name__ == "__main__":
    main()