Commit c1cfa850 authored by Ivan Tyagov's avatar Ivan Tyagov

Slaposify

See merge request nexedi/osie!3
parents d3ae3dba 109a7b84
#!/usr/bin/env python
"""
Pymodbus Synchronous Client Examples
--------------------------------------------------------------------------
The following is an example of how to use the synchronous modbus client
implementation from pymodbus.
It should be noted that the client can also be used with
the guard construct that is available in python 2.5 and up::
with ModbusClient('127.0.0.1') as client:
result = client.read_coils(1,10)
print result
"""
import struct
# --------------------------------------------------------------------------- #
# import the various server implementations
# --------------------------------------------------------------------------- #
from pymodbus.pdu import ModbusRequest, ModbusResponse, ModbusExceptions
from pymodbus.client.sync import ModbusTcpClient as ModbusClient
from pymodbus.bit_read_message import ReadCoilsRequest
from pymodbus.compat import int2byte, byte2int
# --------------------------------------------------------------------------- #
# configure the client logging
# --------------------------------------------------------------------------- #
import logging
logging.basicConfig()
log = logging.getLogger()
log.setLevel(logging.DEBUG)
# --------------------------------------------------------------------------- #
# create your custom message
# --------------------------------------------------------------------------- #
# The following is simply a read coil request that always reads 16 coils.
# Since the function code is already registered with the decoder factory,
# this will be decoded as a read coil response. If you implement a new
# method that is not currently implemented, you must register the request
# and response with a ClientDecoder factory.
# --------------------------------------------------------------------------- #
class CustomModbusResponse(ModbusResponse):
function_code = 55
_rtu_byte_count_pos = 2
def __init__(self, values=None, **kwargs):
ModbusResponse.__init__(self, **kwargs)
self.values = values or []
def encode(self):
""" Encodes response pdu
:returns: The encoded packet message
"""
result = int2byte(len(self.values) * 2)
for register in self.values:
result += struct.pack('>H', register)
return result
def decode(self, data):
""" Decodes response pdu
:param data: The packet data to decode
"""
byte_count = byte2int(data[0])
self.values = []
for i in range(1, byte_count + 1, 2):
self.values.append(struct.unpack('>H', data[i:i + 2])[0])
class CustomModbusRequest(ModbusRequest):
function_code = 55
_rtu_frame_size = 8
def __init__(self, address=None, **kwargs):
ModbusRequest.__init__(self, **kwargs)
self.address = address
self.count = 16
def encode(self):
return struct.pack('>HH', self.address, self.count)
def decode(self, data):
self.address, self.count = struct.unpack('>HH', data)
def execute(self, context):
if not (1 <= self.count <= 0x7d0):
return self.doException(ModbusExceptions.IllegalValue)
if not context.validate(self.function_code, self.address, self.count):
return self.doException(ModbusExceptions.IllegalAddress)
values = context.getValues(self.function_code, self.address,
self.count)
return CustomModbusResponse(values)
......@@ -25,15 +25,16 @@ from pymodbus.datastore import ModbusSlaveContext, ModbusServerContext
from pymodbus.transaction import (ModbusRtuFramer,
ModbusAsciiFramer,
ModbusBinaryFramer)
#from .custom_message import CustomModbusRequest
from pymodbus.pdu import ModbusRequest
from pyA20Lime2 import i2c
import bitarray
from bitarray.util import ba2int
from argparse import ArgumentParser
# XXX: is it needed actually, examine
ModbusRequest.function_code = 55
# name fo device within Lime2
DEFAULT_MOD_IO_DEVICE_NAME = "/dev/i2c-1"
......@@ -128,6 +129,22 @@ log = logging.getLogger()
log.setLevel(logging.DEBUG)
# argument parsing
parser = ArgumentParser()
parser.add_argument("-p", "--port", dest="port",
default = 502,
type = int,
help="The port modbus server will listen to.", metavar="PORT")
parser.add_argument("-i", "--interface", dest="interface",
default = "0.0.0.0",
type = str,
help="The interface modbus server will bind to.", metavar="INTERFACE")
args = parser.parse_args()
PORT = args.port
INTERFACE = args.interface
# main class to communicate with MOD-IO
mod_io = Lime2MODIOI2c()
......@@ -150,7 +167,7 @@ class LimeModbusSlaveContext(ModbusSlaveContext):
# switch relays at MOD-IO
mod_io.setRelayState(address, value)
def run_async_server():
def run_async_server(port = 502, interface = "0.0.0.0"):
store = LimeModbusSlaveContext(
di=ModbusSequentialDataBlock(0, [0]*10),
co=ModbusSequentialDataBlock(0, [0]*10),
......@@ -174,7 +191,7 @@ def run_async_server():
identity.MajorMinorRevision = '0.0.1'
# TCP Server
StartTcpServer(context, identity=identity, address=("0.0.0.0", 502),
StartTcpServer(context, identity=identity, address=(interface, port),
custom_functions=[ModbusRequest])
def main():
......@@ -190,7 +207,7 @@ def main():
mod_io.setRelayStateAllOff()
# run modbus "server"
run_async_server()
run_async_server(interface = INTERFACE, port = PORT)
# switch off all
mod_io.setRelayStateAllOff()
......
# XXX: use later
[instance-profile]
filename = instance.cfg.in
md5sum = b0efa23802f4fa545eaa4e67c83f0b7d
#############################
#
# Deploy OSIE coupler instance
#
#############################
[buildout]
parts =
directory
publish-connection-parameter
# Define egg directories to be the one from Software Release
# (/opt/slapgrid/...)
# Always the same.
eggs-directory = {{ buildout['eggs-directory'] }}
develop-eggs-directory = {{ buildout['develop-eggs-directory'] }}
offline = true
extends = {{ template_monitor }}
[instance-parameter]
# Fetch arbitrary parameters defined by the user in SlapOS Master for his instance.
# We use the slapconfiguration recipe with a few parameters (partition id,
# computer id, certificate, etc).
# It will then authenticate to SlapOS Master and fetch the instance parameters.
# The parameters are accessible from ${instance-parameter:configuration.name-of-parameter}
# Always the same. Just copy/paste.
# See docstring of slapos.cookbook:slapconfiguration for more information.
recipe = slapos.cookbook:slapconfiguration
computer = ${slap-connection:computer-id}
partition = ${slap-connection:partition-id}
url = ${slap-connection:server-url}
key = ${slap-connection:key-file}
cert = ${slap-connection:cert-file}
# Define default parameter(s) that will be used later, in case user didn't
# specify it.
# All possible parameters should have a default.
# In our use case, we are expecting from the user to specify one (optional) parameter: "name". We put the default value here if he doesn't specify it, so that it doesn't crash.
configuration.interface = 0.0.0.0
configuration.port = 1502
# If our use case requires that the user can specify a mail address so that his instance can mail to him (for example), we can do:
# configuration.mail-address =
# If the user doesn't specify it, it won't break and the recipe can handle it (i.e don't send any mail for example).
# Create all needed directories, depending on your needs
[directory]
recipe = slapos.cookbook:mkdirectory
home = ${buildout:directory}
etc = ${:home}/etc
var = ${:home}/var
# Executables put here will be started but not monitored (for startup scripts)
script = ${:etc}/run/
# Executables put here will be started and monitored (for daemons)
service = ${:etc}/service
# Path of the log directory used by our service (see [helloweb])
log = ${:var}/log
# Create a simple web server that says "hello <configuration.name>" to the web.
[helloweb]
# helloworld service is listening on:
# - global IPv6 address, and
# - fixed port
#
# NOTE because every computer partition is allocated its own global IPv6
# address, it is ok to fix the port - different hello-world instances will have
# different IPv6 addresses and they all will be accessible at the same time.
ipv6 = ${instance-parameter:ipv6-random}
# full URL - for convenience
#url = http://[${:ipv6}]:${:port}
# the service will log here
logfile = ${directory:log}/helloweb-${:kind}.log
# Actual script that starts the service:
# This recipe will try to "exec" the command-line after separating parameters.
recipe = slapos.cookbook:wrapper
command-line =
{{ buildout['bin-directory'] }}/pythonwitheggs {{ buildout['parts-directory'] }}/osie/eggs/osie_coupler/osie_modbus.py -p ${instance-parameter:configuration.port}
# Put this shell script in the "etc/service" directory. Each executable of this
# repository will be started and monitored by supervisord. If a service
# exits/crashes, it will trigger a "bang" and cause a re-run of the instance.
wrapper-path = ${directory:service}/helloweb-${:kind}
# promise, that checks that helloweb service is alive
[helloweb-promise]
<= monitor-promise-base
module = check_port_listening
name = helloweb-${:kind}.py
{# macro to instantiate service of `kind` to listen on `port` #}
{% set service_list = [] %}
{% macro hellowebsrv(kind, port) %}
{% do service_list.append(kind) %}
[helloweb-{{ kind }}]
<= helloweb
kind = {{ kind }}
port = {{ port }}
[helloweb-{{ kind }}-promise]
<= helloweb-promise
kind = {{ kind }}
config-hostname= ${instance-parameter:configuration.interface}
config-port = ${instance-parameter:configuration.port}
{% endmacro %}
# services instantiation
{{ hellowebsrv('python', 7777) }}
# register all services/promises to buildout parts
[buildout]
parts +=
{%- for kind in service_list %}
helloweb-{{ kind }}
helloweb-{{ kind }}-promise
{%- endfor %}
# Publish all the parameters needed for the user to connect to the instance.
# It can be anything: URL(s), password(s), or arbitrary parameters.
# Here we'll just echo back the entered name as instance parameter
[publish-connection-parameter]
recipe = slapos.cookbook:publish
port = ${instance-parameter:configuration.port}
interface = ${instance-parameter:configuration.interface}
......@@ -3,6 +3,7 @@
#allow-picked-versions = true
extends =
buildout.hash.cfg
https://lab.nexedi.com/nexedi/slapos/raw/master/stack/monitor/buildout.cfg
https://lab.nexedi.com/nexedi/slapos/raw/master/stack/slapos.cfg
https://lab.nexedi.com/nexedi/slapos/raw/master/component/python3/buildout.cfg
......@@ -10,7 +11,8 @@ parts =
osie-repository
python-interpreter
osie-coupler-egg
# osie-coupler-script
slapos-cookbook
instance-profile
# fix for pypi: https://mail.python.org/pipermail/distutils-sig/2017-October/031712.html
index = https://pypi.python.org/simple/
......@@ -62,12 +64,6 @@ egg = osie-coupler
setup = ${osie-repository:location}/eggs/osie_coupler/
interpreter = ${python-interpreter:interpreter}
[osie-coupler-script]
recipe = zc.recipe.egg:scripts
eggs = ${osie-coupler-egg:egg}
scripts = modbus_server=modbus_server-run
interpreter = ${python-interpreter:interpreter}
[versions]
six = 1.15.0
attrs = 19.2.0
......@@ -88,3 +84,17 @@ m2r = 0.2.1
pyA20Lime2 = 0.2.1
docutils = 0.17.1
mistune = 0.8.4
[instance-profile]
recipe = slapos.recipe.template:jinja2
template = ${:_profile_base_location_}/${:filename}
mode = 0644
rendered = ${buildout:directory}/instance.cfg
extensions = jinja2.ext.do
#filename = instance.cfg.in
context =
section buildout buildout
raw template_monitor ${monitor2-template:rendered}
# md5sum is fetched from buildout.hash.cfg and can be recalculated automatically by
# calling update-hash
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment