Merge remote-tracking branch 'Sasha_git/master'
# Conflicts: # scenes/bip.tscn # scenes/журнал.tscn # scenes/работа.gd # shaders/bip.gdshader
2
.gitignore
vendored
@@ -25,3 +25,5 @@ mono_crash.*.json
|
||||
|
||||
# Godot 4+ specific ignores
|
||||
.godot/
|
||||
/override.cfg
|
||||
/scripts/commit.gd
|
||||
|
||||
@@ -33,7 +33,7 @@ Log.fatal(text)
|
||||
const CUSTOM = Log.MAX << 1 # Bitwise left shift the MAX value for a custom level.
|
||||
|
||||
func _ready() -> void:
|
||||
Log.add_level(CUSTOM, "Level Name")
|
||||
Log.add_level(CUSTOM, "Level Name")
|
||||
```
|
||||
|
||||
## Calling the custom level:
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
extends EditorPlugin
|
||||
|
||||
|
||||
const AUTOLOAD_NAME = "Log"
|
||||
const AUTOLOAD_NAME = "log"
|
||||
const AUTOLOAD_PATH = "res://addons/godot-logger/scripts/logger.gd"
|
||||
|
||||
const LOGGER_OUTPUT = "LoggerOutput"
|
||||
@@ -13,11 +13,11 @@ const LOGGER_OUTPUT_SCRIPT = "res://addons/godot-logger/scripts/logger_output.gd
|
||||
const LOGGER_OUTPUT_ICON = "res://addons/godot-logger/icons/logger.svg"
|
||||
|
||||
|
||||
func _def_settings(name: String, default: Variant) -> void:
|
||||
if not ProjectSettings.has_setting(name):
|
||||
ProjectSettings.set_setting(name, default)
|
||||
func _def_settings(pname: String, default: Variant) -> void:
|
||||
if not ProjectSettings.has_setting(pname):
|
||||
ProjectSettings.set_setting(pname, default)
|
||||
|
||||
ProjectSettings.set_initial_value(name, default)
|
||||
ProjectSettings.set_initial_value(pname, default)
|
||||
|
||||
|
||||
func _enter_tree() -> void:
|
||||
@@ -37,8 +37,8 @@ func _enter_tree() -> void:
|
||||
_def_settings("plugins/logger/file/file_path", "user://uarep-ctl.log")
|
||||
_def_settings("plugins/logger/file/log_file_write", true)
|
||||
|
||||
_def_settings("plugins/logger/default_output_format", "[{hour}:{minute}:{second}][{level}]{text}")
|
||||
_def_settings("plugins/logger/file_format", "[{hour}:{minute}:{second}][{level}]{text}")
|
||||
_def_settings("plugins/logger/default_output_format", "[{year}-{month}-{day} {hour}:{minute}:{second}][{level}]{text}")
|
||||
_def_settings("plugins/logger/file_format", "[{year}-{month}-{day} {hour}:{minute}:{second}][{level}]{text}")
|
||||
|
||||
add_autoload_singleton(AUTOLOAD_NAME, AUTOLOAD_PATH)
|
||||
add_custom_type(LOGGER_OUTPUT, "RichTextLabel", load(LOGGER_OUTPUT_SCRIPT), load(LOGGER_OUTPUT_ICON))
|
||||
|
||||
@@ -32,12 +32,14 @@ var _format_default_output : String
|
||||
var _format_file : String
|
||||
|
||||
var _names = {
|
||||
INFO: "INFO",
|
||||
DEBUG: "DEBUG",
|
||||
WARNING: "WARNING",
|
||||
ERROR: "ERROR",
|
||||
FATAL: "FATAL"
|
||||
INFO: "ИНФО",
|
||||
DEBUG: "ОТЛАДКА",
|
||||
WARNING: "ВНИМАНИЕ",
|
||||
ERROR: "ОШИБКА",
|
||||
FATAL: "АВАРИЯ"
|
||||
}
|
||||
var _msg_array: Array
|
||||
var _filters: Array
|
||||
|
||||
# Can be overridden to define custom default values.
|
||||
func _enter_tree() -> void:
|
||||
@@ -49,8 +51,8 @@ func _enter_tree() -> void:
|
||||
set_file_path(ProjectSettings.get_setting("plugins/logger/file/file_path", "user://uarep-ctl.log"))
|
||||
set_file_write_enabled(ProjectSettings.get_setting("plugins/logger/file/log_file_write", true))
|
||||
|
||||
_format_default_output = ProjectSettings.get_setting("plugins/logger/default_output_format", "[{hour}:{minute}:{second}][{level}]{text}")
|
||||
_format_file = ProjectSettings.get_setting("plugins/logger/file_format", "[{hour}:{minute}:{second}][{level}]{text}")
|
||||
_format_default_output = ProjectSettings.get_setting("plugins/logger/default_output_format", "[{year}-{month}-{day} {hour}:{minute}:{second}][{level}]{text}")
|
||||
_format_file = ProjectSettings.get_setting("plugins/logger/file_format", "[{year}-{month}-{day} {hour}:{minute}:{second}][{level}]{text}")
|
||||
|
||||
|
||||
func _exit_tree() -> void:
|
||||
@@ -158,6 +160,9 @@ func fatal(text: String) -> void:
|
||||
func format_file(msg: Dictionary) -> String:
|
||||
return _format_file.format(
|
||||
{
|
||||
"year": "%02d" % msg["year"],
|
||||
"month": "%02d" % msg["month"],
|
||||
"day": "%02d" % msg["day"],
|
||||
"hour": "%02d" % msg["hour"],
|
||||
"minute":"%02d" % msg["minute"],
|
||||
"second":"%02d" % msg["second"],
|
||||
@@ -170,6 +175,9 @@ func format_file(msg: Dictionary) -> String:
|
||||
func format_stdout(msg: Dictionary) -> String:
|
||||
return _format_default_output.format(
|
||||
{
|
||||
"year": "%02d" % msg["year"],
|
||||
"month": "%02d" % msg["month"],
|
||||
"day": "%02d" % msg["day"],
|
||||
"hour": "%02d" % msg["hour"],
|
||||
"minute":"%02d" % msg["minute"],
|
||||
"second":"%02d" % msg["second"],
|
||||
@@ -184,15 +192,19 @@ func message(level: int, text: String) -> void:
|
||||
assert(text, "Invalid message text.")
|
||||
|
||||
if _log_enabled and _level & level:
|
||||
var msg : Dictionary = Time.get_time_dict_from_system()
|
||||
var msg : Dictionary = Time.get_datetime_dict_from_system()
|
||||
msg["level"] = level
|
||||
msg["level_name"] = _names[level]
|
||||
msg["text"] = " " + text
|
||||
|
||||
logged.emit(msg)
|
||||
_msg_array.append(msg)
|
||||
if (_flt_msg(msg)):
|
||||
logged.emit(msg)
|
||||
|
||||
|
||||
if _file_write_enabled:
|
||||
_file.store_line(format_file(msg))
|
||||
_file.call_deferred('flush')
|
||||
|
||||
if _default_output_enabled:
|
||||
print(format_stdout(msg))
|
||||
@@ -202,17 +214,53 @@ func _open_file() -> void:
|
||||
if is_instance_valid(_file) and _file.is_open():
|
||||
_file.close()
|
||||
|
||||
_file = FileAccess.open(get_file_path(), FileAccess.WRITE)
|
||||
_file = FileAccess.open(get_file_path(), FileAccess.READ_WRITE)
|
||||
|
||||
var err := FileAccess.get_open_error()
|
||||
if err:
|
||||
_file = FileAccess.open(get_file_path(), FileAccess.WRITE)
|
||||
_file.close()
|
||||
_file = FileAccess.open(get_file_path(), FileAccess.READ_WRITE)
|
||||
return print_debug(error_string(err))
|
||||
|
||||
err = _file.get_error()
|
||||
if err:
|
||||
return print_debug(error_string(err))
|
||||
_file.seek_end(0)
|
||||
|
||||
|
||||
func _close_file() -> void:
|
||||
if is_instance_valid(_file) and _file.is_open():
|
||||
_file = null
|
||||
|
||||
|
||||
func save_file(path):
|
||||
var f = FileAccess.open(path, FileAccess.WRITE)
|
||||
var err := FileAccess.get_open_error()
|
||||
if err:
|
||||
print_debug(error_string(err))
|
||||
var log_text = _file.get_as_text()
|
||||
f.store_line(log_text)
|
||||
f.close()
|
||||
|
||||
|
||||
func set_filtrs(flt_array: Array):
|
||||
_filters = flt_array
|
||||
|
||||
|
||||
func _flt_msg(msg):
|
||||
var flt_arr: Array = []
|
||||
for item in _filters:
|
||||
if item.button_pressed:
|
||||
flt_arr.append(item.get_meta('filter'))
|
||||
if msg['text'].find(item.get_meta('filter')) != -1:
|
||||
return true
|
||||
if msg['level_name'] in flt_arr:
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func add_msgs():
|
||||
for msg in _msg_array:
|
||||
if (_flt_msg(msg)):
|
||||
logged.emit(msg)
|
||||
|
||||
@@ -10,7 +10,7 @@ extends RichTextLabel
|
||||
signal handled()
|
||||
|
||||
|
||||
@export var format_text : String = "[{hour}:{minute}:{second}][{level}]{text}"
|
||||
@export var format_text : String = "[{year}-{month}-{day} {hour}:{minute}:{second}][{level}]{text}"
|
||||
@export var colors : Dictionary = {
|
||||
Logger.INFO: Color.WHITE,
|
||||
Logger.DEBUG: Color.GRAY,
|
||||
@@ -24,7 +24,7 @@ var _logger : Logger
|
||||
|
||||
|
||||
func _enter_tree() -> void:
|
||||
var logger := get_node_or_null("/root/Log") as Logger
|
||||
var logger := get_node_or_null("/root/log") as Logger
|
||||
if is_instance_valid(logger):
|
||||
set_logger(logger)
|
||||
|
||||
@@ -59,6 +59,9 @@ func get_logger() -> Logger:
|
||||
func format(message: Dictionary) -> String:
|
||||
return format_text.format(
|
||||
{
|
||||
"year": "%02d" % message["year"],
|
||||
"month": "%02d" % message["month"],
|
||||
"day": "%02d" % message["day"],
|
||||
"hour": "%02d" % message["hour"],
|
||||
"minute": "%02d" % message["minute"],
|
||||
"second": "%02d" % message["second"],
|
||||
@@ -72,5 +75,4 @@ func handle_message(message: Dictionary) -> void:
|
||||
push_color(get_color(message["level"]))
|
||||
append_text(format(message) + "\n")
|
||||
pop()
|
||||
|
||||
handled.emit()
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
scons target=template_release production=yes use_lto=yes -j4
|
||||
@@ -1,9 +1,20 @@
|
||||
[gd_resource type="FontFile" load_steps=2 format=2]
|
||||
[gd_resource type="FontFile" load_steps=2 format=3 uid="uid://clrxfflc1wrhj"]
|
||||
|
||||
[ext_resource path="res://data/DejaVuSansCondensed.ttf" type="FontFile" id=1]
|
||||
[ext_resource type="FontFile" uid="uid://c7hvei2ks7jp6" path="res://data/DejaVuSansCondensed.ttf" id="1"]
|
||||
|
||||
[resource]
|
||||
size = 20
|
||||
use_mipmaps = true
|
||||
use_filter = true
|
||||
font_data = ExtResource( 1 )
|
||||
fallbacks = Array[Font]([ExtResource("1")])
|
||||
cache/0/16/0/ascent = 0.0
|
||||
cache/0/16/0/descent = 0.0
|
||||
cache/0/16/0/underline_position = 0.0
|
||||
cache/0/16/0/underline_thickness = 0.0
|
||||
cache/0/16/0/scale = 1.0
|
||||
cache/0/16/0/kerning_overrides/16/0 = Vector2(0, 0)
|
||||
cache/0/16/0/kerning_overrides/14/0 = Vector2(0, 0)
|
||||
cache/0/14/0/ascent = 0.0
|
||||
cache/0/14/0/descent = 0.0
|
||||
cache/0/14/0/underline_position = 0.0
|
||||
cache/0/14/0/underline_thickness = 0.0
|
||||
cache/0/14/0/scale = 1.0
|
||||
cache/0/14/0/kerning_overrides/16/0 = Vector2(0, 0)
|
||||
cache/0/14/0/kerning_overrides/14/0 = Vector2(0, 0)
|
||||
|
||||
BIN
data/nine-patch-round.png
Normal file
|
After Width: | Height: | Size: 567 B |
34
data/nine-patch-round.png.import
Normal file
@@ -0,0 +1,34 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://c4mdrmk7h638f"
|
||||
path="res://.godot/imported/nine-patch-round.png-40081195f042d0b4e71ddf4d58b0c134.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://data/nine-patch-round.png"
|
||||
dest_files=["res://.godot/imported/nine-patch-round.png-40081195f042d0b4e71ddf4d58b0c134.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
@@ -4,9 +4,6 @@
|
||||
|
||||
[resource]
|
||||
fallbacks = Array[Font]([ExtResource("1_cxid7")])
|
||||
face_index = null
|
||||
embolden = null
|
||||
transform = null
|
||||
cache/0/17/0/ascent = 0.0
|
||||
cache/0/17/0/descent = 0.0
|
||||
cache/0/17/0/underline_position = 0.0
|
||||
@@ -19,6 +16,7 @@ cache/0/17/0/kerning_overrides/14/0 = Vector2(0, 0)
|
||||
cache/0/17/0/kerning_overrides/8/0 = Vector2(0, 0)
|
||||
cache/0/17/0/kerning_overrides/15/0 = Vector2(0, 0)
|
||||
cache/0/17/0/kerning_overrides/13/0 = Vector2(0, 0)
|
||||
cache/0/17/0/kerning_overrides/20/0 = Vector2(0, 0)
|
||||
cache/0/16/0/ascent = 0.0
|
||||
cache/0/16/0/descent = 0.0
|
||||
cache/0/16/0/underline_position = 0.0
|
||||
@@ -31,6 +29,7 @@ cache/0/16/0/kerning_overrides/14/0 = Vector2(0, 0)
|
||||
cache/0/16/0/kerning_overrides/8/0 = Vector2(0, 0)
|
||||
cache/0/16/0/kerning_overrides/15/0 = Vector2(0, 0)
|
||||
cache/0/16/0/kerning_overrides/13/0 = Vector2(0, 0)
|
||||
cache/0/16/0/kerning_overrides/20/0 = Vector2(0, 0)
|
||||
cache/0/18/0/ascent = 0.0
|
||||
cache/0/18/0/descent = 0.0
|
||||
cache/0/18/0/underline_position = 0.0
|
||||
@@ -43,6 +42,7 @@ cache/0/18/0/kerning_overrides/14/0 = Vector2(0, 0)
|
||||
cache/0/18/0/kerning_overrides/8/0 = Vector2(0, 0)
|
||||
cache/0/18/0/kerning_overrides/15/0 = Vector2(0, 0)
|
||||
cache/0/18/0/kerning_overrides/13/0 = Vector2(0, 0)
|
||||
cache/0/18/0/kerning_overrides/20/0 = Vector2(0, 0)
|
||||
cache/0/14/0/ascent = 0.0
|
||||
cache/0/14/0/descent = 0.0
|
||||
cache/0/14/0/underline_position = 0.0
|
||||
@@ -55,6 +55,7 @@ cache/0/14/0/kerning_overrides/14/0 = Vector2(0, 0)
|
||||
cache/0/14/0/kerning_overrides/8/0 = Vector2(0, 0)
|
||||
cache/0/14/0/kerning_overrides/15/0 = Vector2(0, 0)
|
||||
cache/0/14/0/kerning_overrides/13/0 = Vector2(0, 0)
|
||||
cache/0/14/0/kerning_overrides/20/0 = Vector2(0, 0)
|
||||
cache/0/8/0/ascent = 0.0
|
||||
cache/0/8/0/descent = 0.0
|
||||
cache/0/8/0/underline_position = 0.0
|
||||
@@ -67,6 +68,7 @@ cache/0/8/0/kerning_overrides/14/0 = Vector2(0, 0)
|
||||
cache/0/8/0/kerning_overrides/8/0 = Vector2(0, 0)
|
||||
cache/0/8/0/kerning_overrides/15/0 = Vector2(0, 0)
|
||||
cache/0/8/0/kerning_overrides/13/0 = Vector2(0, 0)
|
||||
cache/0/8/0/kerning_overrides/20/0 = Vector2(0, 0)
|
||||
cache/0/15/0/ascent = 0.0
|
||||
cache/0/15/0/descent = 0.0
|
||||
cache/0/15/0/underline_position = 0.0
|
||||
@@ -79,6 +81,7 @@ cache/0/15/0/kerning_overrides/14/0 = Vector2(0, 0)
|
||||
cache/0/15/0/kerning_overrides/8/0 = Vector2(0, 0)
|
||||
cache/0/15/0/kerning_overrides/15/0 = Vector2(0, 0)
|
||||
cache/0/15/0/kerning_overrides/13/0 = Vector2(0, 0)
|
||||
cache/0/15/0/kerning_overrides/20/0 = Vector2(0, 0)
|
||||
cache/0/13/0/ascent = 0.0
|
||||
cache/0/13/0/descent = 0.0
|
||||
cache/0/13/0/underline_position = 0.0
|
||||
@@ -91,3 +94,17 @@ cache/0/13/0/kerning_overrides/14/0 = Vector2(0, 0)
|
||||
cache/0/13/0/kerning_overrides/8/0 = Vector2(0, 0)
|
||||
cache/0/13/0/kerning_overrides/15/0 = Vector2(0, 0)
|
||||
cache/0/13/0/kerning_overrides/13/0 = Vector2(0, 0)
|
||||
cache/0/13/0/kerning_overrides/20/0 = Vector2(0, 0)
|
||||
cache/0/20/0/ascent = 0.0
|
||||
cache/0/20/0/descent = 0.0
|
||||
cache/0/20/0/underline_position = 0.0
|
||||
cache/0/20/0/underline_thickness = 0.0
|
||||
cache/0/20/0/scale = 1.0
|
||||
cache/0/20/0/kerning_overrides/17/0 = Vector2(0, 0)
|
||||
cache/0/20/0/kerning_overrides/16/0 = Vector2(0, 0)
|
||||
cache/0/20/0/kerning_overrides/18/0 = Vector2(0, 0)
|
||||
cache/0/20/0/kerning_overrides/14/0 = Vector2(0, 0)
|
||||
cache/0/20/0/kerning_overrides/8/0 = Vector2(0, 0)
|
||||
cache/0/20/0/kerning_overrides/15/0 = Vector2(0, 0)
|
||||
cache/0/20/0/kerning_overrides/13/0 = Vector2(0, 0)
|
||||
cache/0/20/0/kerning_overrides/20/0 = Vector2(0, 0)
|
||||
|
||||
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 18 KiB |
34
data/РТР.png.import
Normal file
@@ -0,0 +1,34 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://5dm8b0hcmkc3"
|
||||
path="res://.godot/imported/РТР.png-4dffef42cf4e5844aa84c964b0a8a676.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://data/РТР.png"
|
||||
dest_files=["res://.godot/imported/РТР.png-4dffef42cf4e5844aa84c964b0a8a676.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
BIN
data/СПТ.png
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 1.6 KiB |
BIN
data/УФ.png
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 3.4 KiB |
34
data/Щ3-mnemo.png.import
Normal file
@@ -0,0 +1,34 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://rkvki3gs2kk0"
|
||||
path="res://.godot/imported/Щ3-mnemo.png-7a21825106619ee1b5125d77505fb52b.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://data/Щ3-mnemo.png"
|
||||
dest_files=["res://.godot/imported/Щ3-mnemo.png-7a21825106619ee1b5125d77505fb52b.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
BIN
data/Щ3.png
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 397 B After Width: | Height: | Size: 355 B |
|
Before Width: | Height: | Size: 521 B After Width: | Height: | Size: 477 B |
BIN
data/кнопка-масштаб.png
Normal file
|
After Width: | Height: | Size: 184 B |
34
data/кнопка-масштаб.png.import
Normal file
@@ -0,0 +1,34 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://c1maea6yhd3k7"
|
||||
path="res://.godot/imported/кнопка-масштаб.png-506c26462e59a71e6707c554c495504e.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://data/кнопка-масштаб.png"
|
||||
dest_files=["res://.godot/imported/кнопка-масштаб.png-506c26462e59a71e6707c554c495504e.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
BIN
data/прогресс-заполнение-1.png
Normal file
|
After Width: | Height: | Size: 197 B |
34
data/прогресс-заполнение-1.png.import
Normal file
@@ -0,0 +1,34 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://c1k856fxhrjnh"
|
||||
path="res://.godot/imported/прогресс-заполнение-1.png-8f8393b6735f8a6bd330fcb64a737cde.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://data/прогресс-заполнение-1.png"
|
||||
dest_files=["res://.godot/imported/прогресс-заполнение-1.png-8f8393b6735f8a6bd330fcb64a737cde.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
BIN
data/прогресс-фон-1.png
Normal file
|
After Width: | Height: | Size: 195 B |
34
data/прогресс-фон-1.png.import
Normal file
@@ -0,0 +1,34 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://00871pb6moln"
|
||||
path="res://.godot/imported/прогресс-фон-1.png-64e1c4edb6f9c678c20df2bdb91e5b2b.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://data/прогресс-фон-1.png"
|
||||
dest_files=["res://.godot/imported/прогресс-фон-1.png-64e1c4edb6f9c678c20df2bdb91e5b2b.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
BIN
data/рамка-1.png
Normal file
|
After Width: | Height: | Size: 220 B |
34
data/рамка-1.png.import
Normal file
@@ -0,0 +1,34 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://b0o8jhb5jbrev"
|
||||
path="res://.godot/imported/рамка-1.png-f12c78d1806344f437cab5886e375731.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://data/рамка-1.png"
|
||||
dest_files=["res://.godot/imported/рамка-1.png-f12c78d1806344f437cab5886e375731.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
BIN
data/состояние-исправности-0.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
34
data/состояние-исправности-0.png.import
Normal file
@@ -0,0 +1,34 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://c6nve6f8sfyj2"
|
||||
path="res://.godot/imported/состояние-исправности-0.png-80d296bfe23ad795df15b10d3cbdc6be.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://data/состояние-исправности-0.png"
|
||||
dest_files=["res://.godot/imported/состояние-исправности-0.png-80d296bfe23ad795df15b10d3cbdc6be.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
BIN
data/состояние-исправности-1.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
34
data/состояние-исправности-1.png.import
Normal file
@@ -0,0 +1,34 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://dnreyfh3cd1k2"
|
||||
path="res://.godot/imported/состояние-исправности-1.png-817448af211ad4cecd7fc0b9b688c408.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://data/состояние-исправности-1.png"
|
||||
dest_files=["res://.godot/imported/состояние-исправности-1.png-817448af211ad4cecd7fc0b9b688c408.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
BIN
data/состояние-исправности-2.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
34
data/состояние-исправности-2.png.import
Normal file
@@ -0,0 +1,34 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://c6booa8753u5t"
|
||||
path="res://.godot/imported/состояние-исправности-2.png-980a893fd398fba0dfb382e7c7ae4430.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://data/состояние-исправности-2.png"
|
||||
dest_files=["res://.godot/imported/состояние-исправности-2.png-980a893fd398fba0dfb382e7c7ae4430.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
BIN
data/состояния-исправности.png
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
34
data/состояния-исправности.png.import
Normal file
@@ -0,0 +1,34 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bl4v556yreno1"
|
||||
path="res://.godot/imported/состояния-исправности.png-decaf67c3bc5d297844ef43f946cc651.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://data/состояния-исправности.png"
|
||||
dest_files=["res://.godot/imported/состояния-исправности.png-decaf67c3bc5d297844ef43f946cc651.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
32
data/состояния-исправности.tres
Normal file
@@ -0,0 +1,32 @@
|
||||
[gd_resource type="SpriteFrames" load_steps=5 format=3 uid="uid://jndvpts2tbi3"]
|
||||
|
||||
[ext_resource type="Texture2D" uid="uid://bl4v556yreno1" path="res://data/состояния-исправности.png" id="1_wb1fe"]
|
||||
|
||||
[sub_resource type="AtlasTexture" id="AtlasTexture_n2u5p"]
|
||||
atlas = ExtResource("1_wb1fe")
|
||||
region = Rect2(0, 0, 224, 224)
|
||||
|
||||
[sub_resource type="AtlasTexture" id="AtlasTexture_a4vh8"]
|
||||
atlas = ExtResource("1_wb1fe")
|
||||
region = Rect2(224, 0, 224, 224)
|
||||
|
||||
[sub_resource type="AtlasTexture" id="AtlasTexture_qf05b"]
|
||||
atlas = ExtResource("1_wb1fe")
|
||||
region = Rect2(448, 0, 224, 224)
|
||||
|
||||
[resource]
|
||||
animations = [{
|
||||
"frames": [{
|
||||
"duration": 1.0,
|
||||
"texture": SubResource("AtlasTexture_n2u5p")
|
||||
}, {
|
||||
"duration": 1.0,
|
||||
"texture": SubResource("AtlasTexture_a4vh8")
|
||||
}, {
|
||||
"duration": 1.0,
|
||||
"texture": SubResource("AtlasTexture_qf05b")
|
||||
}],
|
||||
"loop": true,
|
||||
"name": &"default",
|
||||
"speed": 5.0
|
||||
}]
|
||||
4
docs/build.sh
Normal file
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
dot -Tpng "структура.dot" -o "структура.png"
|
||||
pandoc -f markdown_strict --verbose "README.md" > "README.doc"
|
||||
38
docs/струкура-по.dot
Normal file
@@ -0,0 +1,38 @@
|
||||
digraph "Прибор УФ"
|
||||
{
|
||||
graph [nodesep=0.5, overlap = false];
|
||||
node [shape=box, overlap = false];
|
||||
eth0 [label="eth0", shape=cds];
|
||||
eth1 [label="eth1", shape=cds];
|
||||
eth2 [label="eth2", shape=cds];
|
||||
tty0 [label="tty0", shape=cds];
|
||||
tty1 [label="tty1", shape=cds];
|
||||
tty2 [label="tty2", shape=cds];
|
||||
proto_capsrpb [label="Протокол КАПС-РПБ"];
|
||||
proto_5p28 [label="Протокол 5П-28"];
|
||||
uarep_uart [label="UART ModBus"];
|
||||
uarep_ctl [label="ПО УАРЭП"];
|
||||
caps_rpb [label="СПО \"КАПС РПБ\""];
|
||||
p16 [label="П16", shape=plaintext, style=filled];
|
||||
spt25left [label="СПТ25 левый борт", shape=plaintext, style=filled];
|
||||
spt25right [label="СПТ25 правый борт", shape=plaintext, style=filled];
|
||||
|
||||
eth0 -> proto_5p28;
|
||||
eth1 -> proto_5p28;
|
||||
eth0 -> proto_capsrpb;
|
||||
eth1 -> proto_capsrpb;
|
||||
eth0 -> uarep_uart;
|
||||
eth1 -> uarep_uart;
|
||||
eth2 -> caps_rpb;
|
||||
proto_5p28 -> uarep_ctl;
|
||||
uarep_uart -> uarep_ctl;
|
||||
uarep_uart -> tty0;
|
||||
uarep_uart -> tty1;
|
||||
uarep_uart -> tty2;
|
||||
uarep_ctl -> caps_rpb;
|
||||
proto_capsrpb -> caps_rpb;
|
||||
p16 -> eth0 [style=invis]
|
||||
p16 -> eth1 [style=invis]
|
||||
spt25left -> tty0 [style=invis]
|
||||
spt25right -> tty1 [style=invis]
|
||||
}
|
||||
28
git-hook.sh
Normal file
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Описание
|
||||
#
|
||||
# Этот скрипт генерирует модуль на языке GDScript с информацией для
|
||||
# контроля версий в системе git. Использовать для проектов созданных
|
||||
# для GoDot Engine.
|
||||
#
|
||||
# Инструкция по применению
|
||||
#
|
||||
# 1. В папке ./git/hooks создать файлы:
|
||||
# - post-checkout
|
||||
# - post-commit
|
||||
# - post-merge
|
||||
# 2. В каждый файл записать строку:
|
||||
# ./git-hook.sh
|
||||
# 3. Теперь модуль scripts/commit.gd будет содержать данные
|
||||
# о текущей версии, после выполнения следующих операций СКВ:
|
||||
# - переключение ветки;
|
||||
# - выполнение фиксации;
|
||||
# - выполнение слияния.
|
||||
|
||||
|
||||
GDSCRIPT_FILE="scripts/commit.gd"
|
||||
HEAD="$(git rev-parse --abbrev-ref HEAD) $(git rev-parse --short HEAD) $(git log -1 --format=%cd)"
|
||||
echo "extends Node" > ${GDSCRIPT_FILE}
|
||||
echo "# Файл сгенерирован автоматически! Не изменять вручную!" >> ${GDSCRIPT_FILE}
|
||||
echo "const VCS_HEAD = '${HEAD}'" >> ${GDSCRIPT_FILE}
|
||||
105
modbus/client-serial-modbus.py
Normal file
@@ -0,0 +1,105 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""
|
||||
Драйвер клиента для интерфейса Modbus работающего через последовательный порт.
|
||||
(приложение | UDP | JSON) <- (этот драйвер клиента) -> (сервер | Serial | Modbus RTU)
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import argparse
|
||||
import time
|
||||
from socket import *
|
||||
|
||||
import logger
|
||||
from pymodbus.client.sync import ModbusSerialClient
|
||||
|
||||
DEF_SERIAL = 'COM1' if os.name == 'nt' else '/dev/tty0'
|
||||
log_file_dir = os.path.join(os.path.expanduser('~'))
|
||||
log = logger.get_logger(__file__, log_file_dir)
|
||||
EXCEPT_MSGS = {
|
||||
'write': 'ошибка: запись в регистры не выполнена',
|
||||
'read_holding': 'ошибка: чтение регистров хранения не выполнено',
|
||||
'read_input': 'ошибка: чтение регистров входов не выполнено',
|
||||
'<unknown>': 'ошибка: получена неверная команда'
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||
parser.add_argument('-modbus-unit', default=1, type=int, help='Номер устройства Modbus')
|
||||
parser.add_argument('-serial-baud', default=115200, type=int, help='Скорость последовательного порта, бод')
|
||||
parser.add_argument('-serial-port', default=DEF_SERIAL, type=str, help='Последовательный порт для подключения к Modbus серверу')
|
||||
parser.add_argument('-udp-addr', default='localhost', type=str, help='Адрес для отправки сообщений приложению')
|
||||
parser.add_argument('-udp-port-rx', default=44100, type=int, help='Порт для приёма сообщений от приложения')
|
||||
parser.add_argument('-udp-port-tx', default=48000, type=int, help='Порт для отправки сообщений приложению')
|
||||
opts, opts_unk = parser.parse_known_args()
|
||||
print_opts_error(opts_unk)
|
||||
run_sync_client(opts.udp_addr, opts.udp_port_rx, opts.udp_port_tx, opts.serial_port, opts.serial_baud, opts.modbus_unit)
|
||||
|
||||
|
||||
def run_sync_client(udp_addr, udp_port_rx, udp_port_tx, serial_port, serial_baud, unit_num):
|
||||
sock = socket(AF_INET, SOCK_DGRAM)
|
||||
sock.bind((udp_addr, udp_port_rx))
|
||||
log.debug(f'привязан к адресу {udp_addr}:{udp_port_rx}')
|
||||
|
||||
json_decoder = json.JSONDecoder()
|
||||
json_encoder = json.JSONEncoder()
|
||||
|
||||
client = ModbusSerialClient(method='rtu', port=serial_port, baudrate=int(serial_baud))
|
||||
while not client.connect(): # Ждать подключения к серверу
|
||||
time.sleep(3)
|
||||
|
||||
log.debug(f'подключен к серверу через {serial_port} на скорости {serial_baud} бод')
|
||||
|
||||
while True:
|
||||
udp_data = sock.recv(2048) # Ждать команду от приложения
|
||||
udp_data = udp_data.decode('utf-8')
|
||||
message = json_decoder.decode(udp_data) # type: dict
|
||||
|
||||
if 'code' not in message:
|
||||
message['code'] = '<unknown>'
|
||||
|
||||
if 'data' not in message:
|
||||
message['data'] = []
|
||||
|
||||
rc = False
|
||||
if message['code'] == 'write':
|
||||
data = message['data']
|
||||
rq = client.write_registers(data[0], data[1:], unit=unit_num)
|
||||
rc = rq.isError()
|
||||
elif message['code'] == 'read_holding':
|
||||
data = message['data']
|
||||
rq = client.read_holding_registers(data[0], data[1:], unit=unit_num)
|
||||
rc = rq.isError()
|
||||
if not rc:
|
||||
tx_data = json_encoder.encode({'code': 'response', 'data': rq.registers})
|
||||
sock.sendto(bytes(tx_data), (udp_addr, udp_port_tx))
|
||||
elif message['code'] == 'read_input':
|
||||
data = message['data']
|
||||
rq = client.read_input_registers(data[0], data[1:], unit=unit_num)
|
||||
rc = rq.isError()
|
||||
if not rc:
|
||||
tx_data = json_encoder.encode({'code': 'response', 'data': rq.registers})
|
||||
sock.sendto(bytes(tx_data), (udp_addr, udp_port_tx))
|
||||
elif message['code'] == '<unknown>':
|
||||
rc = False
|
||||
|
||||
if rc:
|
||||
code = message['code']
|
||||
data = message['data']
|
||||
rc_msg = EXCEPT_MSGS[code]
|
||||
tx_data = json_encoder.encode({'code': 'exception', 'data': rc_msg})
|
||||
sock.sendto(bytes(tx_data), (udp_addr, udp_port_tx))
|
||||
log.debug(
|
||||
'ошибка при выполнении команды: \"%s\". данные: \"%s\", порт: \"%s\", скорость: %s'
|
||||
% (code, data, serial_port, serial_baud))
|
||||
|
||||
|
||||
def print_opts_error(opts: list) -> None:
|
||||
for opt in opts:
|
||||
log.debug('внимание: неизвестный аргумент: \"%s\"\n' % opt)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
15
modbus/logger.py
Normal file
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
|
||||
def get_logger(source_file, log_dir='logs'):
|
||||
import logging
|
||||
import os
|
||||
source_file = os.path.split(source_file)[-1]
|
||||
log_format = '[%(asctime)s] %(module)s:%(lineno)s %(message)s'
|
||||
logging.basicConfig(format=log_format)
|
||||
log = logging.getLogger()
|
||||
log.setLevel(logging.DEBUG)
|
||||
file_handler = logging.FileHandler(f'{log_dir}/{source_file}.log')
|
||||
file_handler.setFormatter(logging.Formatter(log_format))
|
||||
log.addHandler(file_handler)
|
||||
return log
|
||||
31
modbus/pymodbus/__init__.py
Normal file
@@ -0,0 +1,31 @@
|
||||
'''
|
||||
Pymodbus: Modbus Protocol Implementation
|
||||
-----------------------------------------
|
||||
|
||||
TwistedModbus is built on top of the code developed by:
|
||||
|
||||
Copyright (c) 2001-2005 S.W.A.C. GmbH, Germany.
|
||||
Copyright (c) 2001-2005 S.W.A.C. Bohemia s.r.o., Czech Republic.
|
||||
Hynek Petrak <hynek@swac.cz>
|
||||
|
||||
Released under the the BSD license
|
||||
'''
|
||||
|
||||
import pymodbus.version as __version
|
||||
__version__ = __version.version.short()
|
||||
__author__ = 'Galen Collins'
|
||||
__maintainer__ = 'dhoomakethu'
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Block unhandled logging
|
||||
#---------------------------------------------------------------------------#
|
||||
import logging as __logging
|
||||
try:
|
||||
from logging import NullHandler as __null
|
||||
except ImportError:
|
||||
class __null(__logging.Handler):
|
||||
def emit(self, record):
|
||||
pass
|
||||
|
||||
__logging.getLogger(__name__).addHandler(__null())
|
||||
|
||||
247
modbus/pymodbus/bit_read_message.py
Normal file
@@ -0,0 +1,247 @@
|
||||
"""
|
||||
Bit Reading Request/Response messages
|
||||
--------------------------------------
|
||||
|
||||
"""
|
||||
import struct
|
||||
from pymodbus.pdu import ModbusRequest
|
||||
from pymodbus.pdu import ModbusResponse
|
||||
from pymodbus.pdu import ModbusExceptions as merror
|
||||
from pymodbus.utilities import pack_bitstring, unpack_bitstring
|
||||
from pymodbus.compat import byte2int
|
||||
|
||||
|
||||
class ReadBitsRequestBase(ModbusRequest):
|
||||
''' Base class for Messages Requesting bit values '''
|
||||
|
||||
_rtu_frame_size = 8
|
||||
|
||||
def __init__(self, address, count, **kwargs):
|
||||
''' Initializes the read request data
|
||||
|
||||
:param address: The start address to read from
|
||||
:param count: The number of bits after 'address' to read
|
||||
'''
|
||||
ModbusRequest.__init__(self, **kwargs)
|
||||
self.address = address
|
||||
self.count = count
|
||||
|
||||
def encode(self):
|
||||
''' Encodes a request pdu
|
||||
|
||||
:returns: The encoded pdu
|
||||
'''
|
||||
return struct.pack('>HH', self.address, self.count)
|
||||
|
||||
def decode(self, data):
|
||||
''' Decodes a request pdu
|
||||
|
||||
:param data: The packet data to decode
|
||||
'''
|
||||
self.address, self.count = struct.unpack('>HH', data)
|
||||
|
||||
def get_response_pdu_size(self):
|
||||
"""
|
||||
Func_code (1 byte) + Byte Count(1 byte) + Quantity of Coils (n Bytes)/8,
|
||||
if the remainder is different of 0 then N = N+1
|
||||
:return:
|
||||
"""
|
||||
count = self.count//8
|
||||
if self.count % 8:
|
||||
count += 1
|
||||
|
||||
return 1 + 1 + count
|
||||
|
||||
def __str__(self):
|
||||
''' Returns a string representation of the instance
|
||||
|
||||
:returns: A string representation of the instance
|
||||
'''
|
||||
return "ReadBitRequest(%d,%d)" % (self.address, self.count)
|
||||
|
||||
|
||||
class ReadBitsResponseBase(ModbusResponse):
|
||||
''' Base class for Messages responding to bit-reading values '''
|
||||
|
||||
_rtu_byte_count_pos = 2
|
||||
|
||||
def __init__(self, values, **kwargs):
|
||||
''' Initializes a new instance
|
||||
|
||||
:param values: The requested values to be returned
|
||||
'''
|
||||
ModbusResponse.__init__(self, **kwargs)
|
||||
self.bits = values or []
|
||||
|
||||
def encode(self):
|
||||
''' Encodes response pdu
|
||||
|
||||
:returns: The encoded packet message
|
||||
'''
|
||||
result = pack_bitstring(self.bits)
|
||||
packet = struct.pack(">B", len(result)) + result
|
||||
return packet
|
||||
|
||||
def decode(self, data):
|
||||
''' Decodes response pdu
|
||||
|
||||
:param data: The packet data to decode
|
||||
'''
|
||||
self.byte_count = byte2int(data[0])
|
||||
self.bits = unpack_bitstring(data[1:])
|
||||
|
||||
def setBit(self, address, value=1):
|
||||
''' Helper function to set the specified bit
|
||||
|
||||
:param address: The bit to set
|
||||
:param value: The value to set the bit to
|
||||
'''
|
||||
self.bits[address] = (value != 0)
|
||||
|
||||
def resetBit(self, address):
|
||||
''' Helper function to set the specified bit to 0
|
||||
|
||||
:param address: The bit to reset
|
||||
'''
|
||||
self.setBit(address, 0)
|
||||
|
||||
def getBit(self, address):
|
||||
''' Helper function to get the specified bit's value
|
||||
|
||||
:param address: The bit to query
|
||||
:returns: The value of the requested bit
|
||||
'''
|
||||
return self.bits[address]
|
||||
|
||||
def __str__(self):
|
||||
''' Returns a string representation of the instance
|
||||
|
||||
:returns: A string representation of the instance
|
||||
'''
|
||||
return "%s(%d)" % (self.__class__.__name__, len(self.bits))
|
||||
|
||||
|
||||
class ReadCoilsRequest(ReadBitsRequestBase):
|
||||
'''
|
||||
This function code is used to read from 1 to 2000(0x7d0) contiguous status
|
||||
of coils in a remote device. The Request PDU specifies the starting
|
||||
address, ie the address of the first coil specified, and the number of
|
||||
coils. In the PDU Coils are addressed starting at zero. Therefore coils
|
||||
numbered 1-16 are addressed as 0-15.
|
||||
'''
|
||||
function_code = 1
|
||||
|
||||
def __init__(self, address=None, count=None, **kwargs):
|
||||
''' Initializes a new instance
|
||||
|
||||
:param address: The address to start reading from
|
||||
:param count: The number of bits to read
|
||||
'''
|
||||
ReadBitsRequestBase.__init__(self, address, count, **kwargs)
|
||||
|
||||
def execute(self, context):
|
||||
''' Run a read coils request against a datastore
|
||||
|
||||
Before running the request, we make sure that the request is in
|
||||
the max valid range (0x001-0x7d0). Next we make sure that the
|
||||
request is valid against the current datastore.
|
||||
|
||||
:param context: The datastore to request from
|
||||
:returns: The initializes response message, exception message otherwise
|
||||
'''
|
||||
if not (1 <= self.count <= 0x7d0):
|
||||
return self.doException(merror.IllegalValue)
|
||||
if not context.validate(self.function_code, self.address, self.count):
|
||||
return self.doException(merror.IllegalAddress)
|
||||
values = context.getValues(self.function_code, self.address, self.count)
|
||||
return ReadCoilsResponse(values)
|
||||
|
||||
|
||||
class ReadCoilsResponse(ReadBitsResponseBase):
|
||||
'''
|
||||
The coils in the response message are packed as one coil per bit of
|
||||
the data field. Status is indicated as 1= ON and 0= OFF. The LSB of the
|
||||
first data byte contains the output addressed in the query. The other
|
||||
coils follow toward the high order end of this byte, and from low order
|
||||
to high order in subsequent bytes.
|
||||
|
||||
If the returned output quantity is not a multiple of eight, the
|
||||
remaining bits in the final data byte will be padded with zeros
|
||||
(toward the high order end of the byte). The Byte Count field specifies
|
||||
the quantity of complete bytes of data.
|
||||
'''
|
||||
function_code = 1
|
||||
|
||||
def __init__(self, values=None, **kwargs):
|
||||
''' Intializes a new instance
|
||||
|
||||
:param values: The request values to respond with
|
||||
'''
|
||||
ReadBitsResponseBase.__init__(self, values, **kwargs)
|
||||
|
||||
|
||||
class ReadDiscreteInputsRequest(ReadBitsRequestBase):
|
||||
'''
|
||||
This function code is used to read from 1 to 2000(0x7d0) contiguous status
|
||||
of discrete inputs in a remote device. The Request PDU specifies the
|
||||
starting address, ie the address of the first input specified, and the
|
||||
number of inputs. In the PDU Discrete Inputs are addressed starting at
|
||||
zero. Therefore Discrete inputs numbered 1-16 are addressed as 0-15.
|
||||
'''
|
||||
function_code = 2
|
||||
|
||||
def __init__(self, address=None, count=None, **kwargs):
|
||||
''' Intializes a new instance
|
||||
|
||||
:param address: The address to start reading from
|
||||
:param count: The number of bits to read
|
||||
'''
|
||||
ReadBitsRequestBase.__init__(self, address, count, **kwargs)
|
||||
|
||||
def execute(self, context):
|
||||
''' Run a read discrete input request against a datastore
|
||||
|
||||
Before running the request, we make sure that the request is in
|
||||
the max valid range (0x001-0x7d0). Next we make sure that the
|
||||
request is valid against the current datastore.
|
||||
|
||||
:param context: The datastore to request from
|
||||
:returns: The initializes response message, exception message otherwise
|
||||
'''
|
||||
if not (1 <= self.count <= 0x7d0):
|
||||
return self.doException(merror.IllegalValue)
|
||||
if not context.validate(self.function_code, self.address, self.count):
|
||||
return self.doException(merror.IllegalAddress)
|
||||
values = context.getValues(self.function_code, self.address, self.count)
|
||||
return ReadDiscreteInputsResponse(values)
|
||||
|
||||
|
||||
class ReadDiscreteInputsResponse(ReadBitsResponseBase):
|
||||
'''
|
||||
The discrete inputs in the response message are packed as one input per
|
||||
bit of the data field. Status is indicated as 1= ON; 0= OFF. The LSB of
|
||||
the first data byte contains the input addressed in the query. The other
|
||||
inputs follow toward the high order end of this byte, and from low order
|
||||
to high order in subsequent bytes.
|
||||
|
||||
If the returned input quantity is not a multiple of eight, the
|
||||
remaining bits in the final data byte will be padded with zeros
|
||||
(toward the high order end of the byte). The Byte Count field specifies
|
||||
the quantity of complete bytes of data.
|
||||
'''
|
||||
function_code = 2
|
||||
|
||||
def __init__(self, values=None, **kwargs):
|
||||
''' Intializes a new instance
|
||||
|
||||
:param values: The request values to respond with
|
||||
'''
|
||||
ReadBitsResponseBase.__init__(self, values, **kwargs)
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Exported symbols
|
||||
#---------------------------------------------------------------------------#
|
||||
__all__ = [
|
||||
"ReadCoilsRequest", "ReadCoilsResponse",
|
||||
"ReadDiscreteInputsRequest", "ReadDiscreteInputsResponse",
|
||||
]
|
||||
270
modbus/pymodbus/bit_write_message.py
Normal file
@@ -0,0 +1,270 @@
|
||||
"""
|
||||
Bit Writing Request/Response
|
||||
------------------------------
|
||||
|
||||
TODO write mask request/response
|
||||
"""
|
||||
import struct
|
||||
from pymodbus.constants import ModbusStatus
|
||||
from pymodbus.pdu import ModbusRequest
|
||||
from pymodbus.pdu import ModbusResponse
|
||||
from pymodbus.pdu import ModbusExceptions as merror
|
||||
from pymodbus.utilities import pack_bitstring, unpack_bitstring
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Local Constants
|
||||
#---------------------------------------------------------------------------#
|
||||
# These are defined in the spec to turn a coil on/off
|
||||
#---------------------------------------------------------------------------#
|
||||
_turn_coil_on = struct.pack(">H", ModbusStatus.On)
|
||||
_turn_coil_off = struct.pack(">H", ModbusStatus.Off)
|
||||
|
||||
|
||||
class WriteSingleCoilRequest(ModbusRequest):
|
||||
'''
|
||||
This function code is used to write a single output to either ON or OFF
|
||||
in a remote device.
|
||||
|
||||
The requested ON/OFF state is specified by a constant in the request
|
||||
data field. A value of FF 00 hex requests the output to be ON. A value
|
||||
of 00 00 requests it to be OFF. All other values are illegal and will
|
||||
not affect the output.
|
||||
|
||||
The Request PDU specifies the address of the coil to be forced. Coils
|
||||
are addressed starting at zero. Therefore coil numbered 1 is addressed
|
||||
as 0. The requested ON/OFF state is specified by a constant in the Coil
|
||||
Value field. A value of 0XFF00 requests the coil to be ON. A value of
|
||||
0X0000 requests the coil to be off. All other values are illegal and
|
||||
will not affect the coil.
|
||||
'''
|
||||
function_code = 5
|
||||
_rtu_frame_size = 8
|
||||
|
||||
def __init__(self, address=None, value=None, **kwargs):
|
||||
''' Initializes a new instance
|
||||
|
||||
:param address: The variable address to write
|
||||
:param value: The value to write at address
|
||||
'''
|
||||
ModbusRequest.__init__(self, **kwargs)
|
||||
self.address = address
|
||||
self.value = bool(value)
|
||||
|
||||
def encode(self):
|
||||
''' Encodes write coil request
|
||||
|
||||
:returns: The byte encoded message
|
||||
'''
|
||||
result = struct.pack('>H', self.address)
|
||||
if self.value: result += _turn_coil_on
|
||||
else: result += _turn_coil_off
|
||||
return result
|
||||
|
||||
def decode(self, data):
|
||||
''' Decodes a write coil request
|
||||
|
||||
:param data: The packet data to decode
|
||||
'''
|
||||
self.address, value = struct.unpack('>HH', data)
|
||||
self.value = (value == ModbusStatus.On)
|
||||
|
||||
def execute(self, context):
|
||||
''' Run a write coil request against a datastore
|
||||
|
||||
:param context: The datastore to request from
|
||||
:returns: The populated response or exception message
|
||||
'''
|
||||
#if self.value not in [ModbusStatus.Off, ModbusStatus.On]:
|
||||
# return self.doException(merror.IllegalValue)
|
||||
if not context.validate(self.function_code, self.address, 1):
|
||||
return self.doException(merror.IllegalAddress)
|
||||
|
||||
context.setValues(self.function_code, self.address, [self.value])
|
||||
values = context.getValues(self.function_code, self.address, 1)
|
||||
return WriteSingleCoilResponse(self.address, values[0])
|
||||
|
||||
def get_response_pdu_size(self):
|
||||
"""
|
||||
Func_code (1 byte) + Output Address (2 byte) + Output Value (2 Bytes)
|
||||
:return:
|
||||
"""
|
||||
return 1 + 2 + 2
|
||||
|
||||
def __str__(self):
|
||||
''' Returns a string representation of the instance
|
||||
|
||||
:return: A string representation of the instance
|
||||
'''
|
||||
return "WriteCoilRequest(%d, %s) => " % (self.address, self.value)
|
||||
|
||||
|
||||
class WriteSingleCoilResponse(ModbusResponse):
|
||||
'''
|
||||
The normal response is an echo of the request, returned after the coil
|
||||
state has been written.
|
||||
'''
|
||||
function_code = 5
|
||||
_rtu_frame_size = 8
|
||||
|
||||
def __init__(self, address=None, value=None, **kwargs):
|
||||
''' Initializes a new instance
|
||||
|
||||
:param address: The variable address written to
|
||||
:param value: The value written at address
|
||||
'''
|
||||
ModbusResponse.__init__(self, **kwargs)
|
||||
self.address = address
|
||||
self.value = value
|
||||
|
||||
def encode(self):
|
||||
''' Encodes write coil response
|
||||
|
||||
:return: The byte encoded message
|
||||
'''
|
||||
result = struct.pack('>H', self.address)
|
||||
if self.value: result += _turn_coil_on
|
||||
else: result += _turn_coil_off
|
||||
return result
|
||||
|
||||
def decode(self, data):
|
||||
''' Decodes a write coil response
|
||||
|
||||
:param data: The packet data to decode
|
||||
'''
|
||||
self.address, value = struct.unpack('>HH', data)
|
||||
self.value = (value == ModbusStatus.On)
|
||||
|
||||
def __str__(self):
|
||||
''' Returns a string representation of the instance
|
||||
|
||||
:returns: A string representation of the instance
|
||||
'''
|
||||
return "WriteCoilResponse(%d) => %d" % (self.address, self.value)
|
||||
|
||||
|
||||
class WriteMultipleCoilsRequest(ModbusRequest):
|
||||
'''
|
||||
"This function code is used to force each coil in a sequence of coils to
|
||||
either ON or OFF in a remote device. The Request PDU specifies the coil
|
||||
references to be forced. Coils are addressed starting at zero. Therefore
|
||||
coil numbered 1 is addressed as 0.
|
||||
|
||||
The requested ON/OFF states are specified by contents of the request
|
||||
data field. A logical '1' in a bit position of the field requests the
|
||||
corresponding output to be ON. A logical '0' requests it to be OFF."
|
||||
'''
|
||||
function_code = 15
|
||||
_rtu_byte_count_pos = 6
|
||||
|
||||
def __init__(self, address=None, values=None, **kwargs):
|
||||
''' Initializes a new instance
|
||||
|
||||
:param address: The starting request address
|
||||
:param values: The values to write
|
||||
'''
|
||||
ModbusRequest.__init__(self, **kwargs)
|
||||
self.address = address
|
||||
if not values: values = []
|
||||
elif not hasattr(values, '__iter__'): values = [values]
|
||||
self.values = values
|
||||
self.byte_count = (len(self.values) + 7) // 8
|
||||
|
||||
def encode(self):
|
||||
''' Encodes write coils request
|
||||
|
||||
:returns: The byte encoded message
|
||||
'''
|
||||
count = len(self.values)
|
||||
self.byte_count = (count + 7) // 8
|
||||
packet = struct.pack('>HHB', self.address, count, self.byte_count)
|
||||
packet += pack_bitstring(self.values)
|
||||
return packet
|
||||
|
||||
def decode(self, data):
|
||||
''' Decodes a write coils request
|
||||
|
||||
:param data: The packet data to decode
|
||||
'''
|
||||
self.address, count, self.byte_count = struct.unpack('>HHB', data[0:5])
|
||||
values = unpack_bitstring(data[5:])
|
||||
self.values = values[:count]
|
||||
|
||||
def execute(self, context):
|
||||
''' Run a write coils request against a datastore
|
||||
|
||||
:param context: The datastore to request from
|
||||
:returns: The populated response or exception message
|
||||
'''
|
||||
count = len(self.values)
|
||||
if not (1 <= count <= 0x07b0):
|
||||
return self.doException(merror.IllegalValue)
|
||||
if (self.byte_count != (count + 7) // 8):
|
||||
return self.doException(merror.IllegalValue)
|
||||
if not context.validate(self.function_code, self.address, count):
|
||||
return self.doException(merror.IllegalAddress)
|
||||
|
||||
context.setValues(self.function_code, self.address, self.values)
|
||||
return WriteMultipleCoilsResponse(self.address, count)
|
||||
|
||||
def __str__(self):
|
||||
''' Returns a string representation of the instance
|
||||
|
||||
:returns: A string representation of the instance
|
||||
'''
|
||||
params = (self.address, len(self.values))
|
||||
return "WriteNCoilRequest (%d) => %d " % params
|
||||
|
||||
def get_response_pdu_size(self):
|
||||
"""
|
||||
Func_code (1 byte) + Output Address (2 byte) + Quantity of Outputs (2 Bytes)
|
||||
:return:
|
||||
"""
|
||||
return 1 + 2 + 2
|
||||
|
||||
|
||||
class WriteMultipleCoilsResponse(ModbusResponse):
|
||||
'''
|
||||
The normal response returns the function code, starting address, and
|
||||
quantity of coils forced.
|
||||
'''
|
||||
function_code = 15
|
||||
_rtu_frame_size = 8
|
||||
|
||||
def __init__(self, address=None, count=None, **kwargs):
|
||||
''' Initializes a new instance
|
||||
|
||||
:param address: The starting variable address written to
|
||||
:param count: The number of values written
|
||||
'''
|
||||
ModbusResponse.__init__(self, **kwargs)
|
||||
self.address = address
|
||||
self.count = count
|
||||
|
||||
def encode(self):
|
||||
''' Encodes write coils response
|
||||
|
||||
:returns: The byte encoded message
|
||||
'''
|
||||
return struct.pack('>HH', self.address, self.count)
|
||||
|
||||
def decode(self, data):
|
||||
''' Decodes a write coils response
|
||||
|
||||
:param data: The packet data to decode
|
||||
'''
|
||||
self.address, self.count = struct.unpack('>HH', data)
|
||||
|
||||
def __str__(self):
|
||||
''' Returns a string representation of the instance
|
||||
|
||||
:returns: A string representation of the instance
|
||||
'''
|
||||
return "WriteNCoilResponse(%d, %d)" % (self.address, self.count)
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Exported symbols
|
||||
#---------------------------------------------------------------------------#
|
||||
__all__ = [
|
||||
"WriteSingleCoilRequest", "WriteSingleCoilResponse",
|
||||
"WriteMultipleCoilsRequest", "WriteMultipleCoilsResponse",
|
||||
]
|
||||
43
modbus/pymodbus/client/asynchronous/__init__.py
Normal file
@@ -0,0 +1,43 @@
|
||||
"""
|
||||
Async Modbus Client implementation based on Twisted, tornado and asyncio
|
||||
------------------------------------------------------------------------
|
||||
|
||||
Example run::
|
||||
|
||||
from pymodbus.client.asynchronous import schedulers
|
||||
|
||||
# Import The clients
|
||||
|
||||
from pymodbus.client.asynchronous.tcp import AsyncModbusTCPClient as Client
|
||||
from pymodbus.client.asynchronous.serial import AsyncModbusSerialClient as Client
|
||||
from pymodbus.client.asynchronous.udp import AsyncModbusUDPClient as Client
|
||||
|
||||
# For tornado based asynchronous client use
|
||||
event_loop, future = Client(schedulers.IO_LOOP, port=5020)
|
||||
|
||||
# For twisted based asynchronous client use
|
||||
event_loop, future = Client(schedulers.REACTOR, port=5020)
|
||||
|
||||
# For asyncio based asynchronous client use
|
||||
event_loop, client = Client(schedulers.ASYNC_IO, port=5020)
|
||||
|
||||
# Here event_loop is a thread which would control the backend and future is
|
||||
# a Future/deffered object which would be used to
|
||||
# add call backs to run asynchronously.
|
||||
|
||||
# The Actual client could be accessed with future.result() with Tornado
|
||||
# and future.result when using twisted
|
||||
|
||||
# For asyncio the actual client is returned and event loop is asyncio loop
|
||||
|
||||
"""
|
||||
from pymodbus.compat import is_installed
|
||||
|
||||
installed = is_installed('twisted')
|
||||
if installed:
|
||||
# Import deprecated async client only if twisted is installed #338
|
||||
from pymodbus.client.asynchronous.deprecated.asynchronous import *
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.warning("Importing deprecated clients. "
|
||||
"Dependency Twisted is Installed")
|
||||
890
modbus/pymodbus/client/asynchronous/async_io/__init__.py
Normal file
@@ -0,0 +1,890 @@
|
||||
"""
|
||||
Asynchronous framework adapter for asyncio.
|
||||
"""
|
||||
import socket
|
||||
import asyncio
|
||||
import functools
|
||||
import ssl
|
||||
from pymodbus.exceptions import ConnectionException
|
||||
from pymodbus.client.asynchronous.mixins import AsyncModbusClientMixin
|
||||
from pymodbus.utilities import hexlify_packets
|
||||
from pymodbus.transaction import FifoTransactionManager
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
DGRAM_TYPE = socket.SocketKind.SOCK_DGRAM
|
||||
|
||||
|
||||
class BaseModbusAsyncClientProtocol(AsyncModbusClientMixin):
|
||||
"""
|
||||
Asyncio specific implementation of asynchronous modbus client protocol.
|
||||
"""
|
||||
|
||||
#: Factory that created this instance.
|
||||
factory = None
|
||||
transport = None
|
||||
|
||||
async def execute(self, request=None):
|
||||
"""
|
||||
Executes requests asynchronously
|
||||
:param request:
|
||||
:return:
|
||||
"""
|
||||
req = self._execute(request)
|
||||
resp = await asyncio.wait_for(req, timeout=self._timeout)
|
||||
return resp
|
||||
|
||||
def connection_made(self, transport):
|
||||
"""
|
||||
Called when a connection is made.
|
||||
|
||||
The transport argument is the transport representing the connection.
|
||||
:param transport:
|
||||
:return:
|
||||
"""
|
||||
self.transport = transport
|
||||
self._connectionMade()
|
||||
|
||||
if self.factory:
|
||||
self.factory.protocol_made_connection(self)
|
||||
|
||||
def connection_lost(self, reason):
|
||||
"""
|
||||
Called when the connection is lost or closed.
|
||||
|
||||
The argument is either an exception object or None
|
||||
:param reason:
|
||||
:return:
|
||||
"""
|
||||
self.transport = None
|
||||
self._connectionLost(reason)
|
||||
|
||||
if self.factory:
|
||||
self.factory.protocol_lost_connection(self)
|
||||
|
||||
def data_received(self, data):
|
||||
"""
|
||||
Called when some data is received.
|
||||
data is a non-empty bytes object containing the incoming data.
|
||||
:param data:
|
||||
:return:
|
||||
"""
|
||||
self._dataReceived(data)
|
||||
|
||||
def create_future(self):
|
||||
"""
|
||||
Helper function to create asyncio Future object
|
||||
:return:
|
||||
"""
|
||||
return asyncio.Future()
|
||||
|
||||
def resolve_future(self, f, result):
|
||||
"""
|
||||
Resolves the completed future and sets the result
|
||||
:param f:
|
||||
:param result:
|
||||
:return:
|
||||
"""
|
||||
if not f.done():
|
||||
f.set_result(result)
|
||||
|
||||
def raise_future(self, f, exc):
|
||||
"""
|
||||
Sets exception of a future if not done
|
||||
:param f:
|
||||
:param exc:
|
||||
:return:
|
||||
"""
|
||||
if not f.done():
|
||||
f.set_exception(exc)
|
||||
|
||||
def _connectionMade(self):
|
||||
"""
|
||||
Called upon a successful client connection.
|
||||
"""
|
||||
_logger.debug("Client connected to modbus server")
|
||||
self._connected = True
|
||||
|
||||
def _connectionLost(self, reason):
|
||||
"""
|
||||
Called upon a client disconnect
|
||||
|
||||
:param reason: The reason for the disconnect
|
||||
"""
|
||||
_logger.debug(
|
||||
"Client disconnected from modbus server: %s" % reason)
|
||||
self._connected = False
|
||||
for tid in list(self.transaction):
|
||||
self.raise_future(self.transaction.getTransaction(tid),
|
||||
ConnectionException(
|
||||
'Connection lost during request'))
|
||||
|
||||
@property
|
||||
def connected(self):
|
||||
"""
|
||||
Return connection status.
|
||||
"""
|
||||
return self._connected
|
||||
|
||||
def write_transport(self, packet):
|
||||
return self.transport.write(packet)
|
||||
|
||||
def _execute(self, request, **kwargs):
|
||||
"""
|
||||
Starts the producer to send the next request to
|
||||
consumer.write(Frame(request))
|
||||
"""
|
||||
request.transaction_id = self.transaction.getNextTID()
|
||||
packet = self.framer.buildPacket(request)
|
||||
_logger.debug("send: " + hexlify_packets(packet))
|
||||
self.write_transport(packet)
|
||||
return self._buildResponse(request.transaction_id)
|
||||
|
||||
def _dataReceived(self, data):
|
||||
''' Get response, check for valid message, decode result
|
||||
|
||||
:param data: The data returned from the server
|
||||
'''
|
||||
_logger.debug("recv: " + hexlify_packets(data))
|
||||
unit = self.framer.decode_data(data).get("unit", 0)
|
||||
self.framer.processIncomingPacket(data, self._handleResponse, unit=unit)
|
||||
|
||||
def _handleResponse(self, reply, **kwargs):
|
||||
"""
|
||||
Handle the processed response and link to correct deferred
|
||||
|
||||
:param reply: The reply to process
|
||||
"""
|
||||
if reply is not None:
|
||||
tid = reply.transaction_id
|
||||
handler = self.transaction.getTransaction(tid)
|
||||
if handler:
|
||||
self.resolve_future(handler, reply)
|
||||
else:
|
||||
_logger.debug("Unrequested message: " + str(reply))
|
||||
|
||||
def _buildResponse(self, tid):
|
||||
"""
|
||||
Helper method to return a deferred response
|
||||
for the current request.
|
||||
|
||||
:param tid: The transaction identifier for this response
|
||||
:returns: A defer linked to the latest request
|
||||
"""
|
||||
f = self.create_future()
|
||||
if not self._connected:
|
||||
self.raise_future(f, ConnectionException(
|
||||
'Client is not connected'))
|
||||
else:
|
||||
self.transaction.addTransaction(f, tid)
|
||||
return f
|
||||
|
||||
def close(self):
|
||||
self.transport.close()
|
||||
self._connected = False
|
||||
|
||||
|
||||
class ModbusClientProtocol(BaseModbusAsyncClientProtocol, asyncio.Protocol):
|
||||
"""
|
||||
Asyncio specific implementation of asynchronous modbus client protocol.
|
||||
"""
|
||||
|
||||
#: Factory that created this instance.
|
||||
factory = None
|
||||
transport = None
|
||||
|
||||
def data_received(self, data):
|
||||
"""
|
||||
Called when some data is received.
|
||||
data is a non-empty bytes object containing the incoming data.
|
||||
:param data:
|
||||
:return:
|
||||
"""
|
||||
self._dataReceived(data)
|
||||
|
||||
|
||||
class ModbusUdpClientProtocol(BaseModbusAsyncClientProtocol,
|
||||
asyncio.DatagramProtocol):
|
||||
"""
|
||||
Asyncio specific implementation of asynchronous modbus udp client protocol.
|
||||
"""
|
||||
|
||||
#: Factory that created this instance.
|
||||
factory = None
|
||||
|
||||
def __init__(self, host=None, port=0, **kwargs):
|
||||
self.host = host
|
||||
self.port = port
|
||||
super(self.__class__, self).__init__(**kwargs)
|
||||
|
||||
def datagram_received(self, data, addr):
|
||||
self._dataReceived(data)
|
||||
|
||||
def write_transport(self, packet):
|
||||
return self.transport.sendto(packet)
|
||||
|
||||
|
||||
class ReconnectingAsyncioModbusTcpClient(object):
|
||||
"""
|
||||
Client to connect to modbus device repeatedly over TCP/IP."
|
||||
"""
|
||||
#: Minimum delay in milli seconds before reconnect is attempted.
|
||||
DELAY_MIN_MS = 100
|
||||
#: Maximum delay in milli seconds before reconnect is attempted.
|
||||
DELAY_MAX_MS = 1000 * 60 * 5
|
||||
|
||||
def __init__(self, protocol_class=None, loop=None, **kwargs):
|
||||
"""
|
||||
Initialize ReconnectingAsyncioModbusTcpClient
|
||||
:param protocol_class: Protocol used to talk to modbus device.
|
||||
:param loop: Event loop to use
|
||||
"""
|
||||
#: Protocol used to talk to modbus device.
|
||||
self.protocol_class = protocol_class or ModbusClientProtocol
|
||||
#: Current protocol instance.
|
||||
self.protocol = None
|
||||
#: Event loop to use.
|
||||
self.loop = loop or asyncio.get_event_loop()
|
||||
self.host = None
|
||||
self.port = 0
|
||||
self.connected = False
|
||||
#: Reconnect delay in milli seconds.
|
||||
self.delay_ms = self.DELAY_MIN_MS
|
||||
self._proto_args = kwargs
|
||||
|
||||
def reset_delay(self):
|
||||
"""
|
||||
Resets wait before next reconnect to minimal period.
|
||||
"""
|
||||
self.delay_ms = self.DELAY_MIN_MS
|
||||
|
||||
@asyncio.coroutine
|
||||
def start(self, host, port=502):
|
||||
"""
|
||||
Initiates connection to start client
|
||||
:param host:
|
||||
:param port:
|
||||
:return:
|
||||
"""
|
||||
# force reconnect if required:
|
||||
self.stop()
|
||||
|
||||
_logger.debug('Connecting to %s:%s.' % (host, port))
|
||||
self.host = host
|
||||
self.port = port
|
||||
yield from self._connect()
|
||||
|
||||
def stop(self):
|
||||
"""
|
||||
Stops client
|
||||
:return:
|
||||
"""
|
||||
# prevent reconnect:
|
||||
self.host = None
|
||||
|
||||
if self.connected:
|
||||
if self.protocol:
|
||||
if self.protocol.transport:
|
||||
self.protocol.transport.close()
|
||||
|
||||
def _create_protocol(self):
|
||||
"""
|
||||
Factory function to create initialized protocol instance.
|
||||
"""
|
||||
protocol = self.protocol_class(**self._proto_args)
|
||||
protocol.factory = self
|
||||
return protocol
|
||||
|
||||
@asyncio.coroutine
|
||||
def _connect(self):
|
||||
_logger.debug('Connecting.')
|
||||
try:
|
||||
yield from self.loop.create_connection(self._create_protocol,
|
||||
self.host,
|
||||
self.port)
|
||||
except Exception as ex:
|
||||
_logger.warning('Failed to connect: %s' % ex)
|
||||
asyncio.ensure_future(self._reconnect(), loop=self.loop)
|
||||
else:
|
||||
_logger.info('Connected to %s:%s.' % (self.host, self.port))
|
||||
self.reset_delay()
|
||||
|
||||
def protocol_made_connection(self, protocol):
|
||||
"""
|
||||
Protocol notification of successful connection.
|
||||
"""
|
||||
_logger.info('Protocol made connection.')
|
||||
if not self.connected:
|
||||
self.connected = True
|
||||
self.protocol = protocol
|
||||
else:
|
||||
_logger.error('Factory protocol connect '
|
||||
'callback called while connected.')
|
||||
|
||||
def protocol_lost_connection(self, protocol):
|
||||
"""
|
||||
Protocol notification of lost connection.
|
||||
"""
|
||||
if self.connected:
|
||||
_logger.info('Protocol lost connection.')
|
||||
if protocol is not self.protocol:
|
||||
_logger.error('Factory protocol callback called '
|
||||
'from unexpected protocol instance.')
|
||||
|
||||
self.connected = False
|
||||
self.protocol = None
|
||||
if self.host:
|
||||
asyncio.ensure_future(self._reconnect(), loop=self.loop)
|
||||
else:
|
||||
_logger.error('Factory protocol disconnect callback called while not connected.')
|
||||
|
||||
@asyncio.coroutine
|
||||
def _reconnect(self):
|
||||
_logger.debug('Waiting %d ms before next '
|
||||
'connection attempt.' % self.delay_ms)
|
||||
yield from asyncio.sleep(self.delay_ms / 1000)
|
||||
self.delay_ms = min(2 * self.delay_ms, self.DELAY_MAX_MS)
|
||||
yield from self._connect()
|
||||
|
||||
|
||||
class AsyncioModbusTcpClient(object):
|
||||
"""Client to connect to modbus device over TCP/IP."""
|
||||
|
||||
def __init__(self, host=None, port=502, protocol_class=None, loop=None, **kwargs):
|
||||
"""
|
||||
Initializes Asyncio Modbus Tcp Client
|
||||
:param host: Host IP address
|
||||
:param port: Port to connect
|
||||
:param protocol_class: Protocol used to talk to modbus device.
|
||||
:param loop: Asyncio Event loop
|
||||
"""
|
||||
#: Protocol used to talk to modbus device.
|
||||
self.protocol_class = protocol_class or ModbusClientProtocol
|
||||
#: Current protocol instance.
|
||||
self.protocol = None
|
||||
#: Event loop to use.
|
||||
self.loop = loop or asyncio.get_event_loop()
|
||||
|
||||
self.host = host
|
||||
self.port = port
|
||||
|
||||
self.connected = False
|
||||
self._proto_args = kwargs
|
||||
|
||||
def stop(self):
|
||||
"""
|
||||
Stops the client
|
||||
:return:
|
||||
"""
|
||||
if self.connected:
|
||||
if self.protocol:
|
||||
if self.protocol.transport:
|
||||
self.protocol.transport.close()
|
||||
|
||||
def _create_protocol(self):
|
||||
"""
|
||||
Factory function to create initialized protocol instance.
|
||||
"""
|
||||
protocol = self.protocol_class(**self._proto_args)
|
||||
protocol.factory = self
|
||||
return protocol
|
||||
|
||||
@asyncio.coroutine
|
||||
def connect(self):
|
||||
"""
|
||||
Connect and start Async client
|
||||
:return:
|
||||
"""
|
||||
_logger.debug('Connecting.')
|
||||
try:
|
||||
yield from self.loop.create_connection(self._create_protocol,
|
||||
self.host,
|
||||
self.port)
|
||||
_logger.info('Connected to %s:%s.' % (self.host, self.port))
|
||||
except Exception as ex:
|
||||
_logger.warning('Failed to connect: %s' % ex)
|
||||
# asyncio.asynchronous(self._reconnect(), loop=self.loop)
|
||||
|
||||
def protocol_made_connection(self, protocol):
|
||||
"""
|
||||
Protocol notification of successful connection.
|
||||
"""
|
||||
_logger.info('Protocol made connection.')
|
||||
if not self.connected:
|
||||
self.connected = True
|
||||
self.protocol = protocol
|
||||
else:
|
||||
_logger.error('Factory protocol connect '
|
||||
'callback called while connected.')
|
||||
|
||||
def protocol_lost_connection(self, protocol):
|
||||
"""
|
||||
Protocol notification of lost connection.
|
||||
"""
|
||||
if self.connected:
|
||||
_logger.info('Protocol lost connection.')
|
||||
if protocol is not self.protocol:
|
||||
_logger.error('Factory protocol callback called'
|
||||
' from unexpected protocol instance.')
|
||||
|
||||
self.connected = False
|
||||
self.protocol = None
|
||||
# if self.host:
|
||||
# asyncio.asynchronous(self._reconnect(), loop=self.loop)
|
||||
else:
|
||||
_logger.error('Factory protocol disconnect'
|
||||
' callback called while not connected.')
|
||||
|
||||
|
||||
class ReconnectingAsyncioModbusTlsClient(ReconnectingAsyncioModbusTcpClient):
|
||||
"""
|
||||
Client to connect to modbus device repeatedly over TLS."
|
||||
"""
|
||||
def __init__(self, protocol_class=None, loop=None, framer=None, **kwargs):
|
||||
"""
|
||||
Initialize ReconnectingAsyncioModbusTcpClient
|
||||
:param protocol_class: Protocol used to talk to modbus device.
|
||||
:param loop: Event loop to use
|
||||
"""
|
||||
self.framer = framer
|
||||
ReconnectingAsyncioModbusTcpClient.__init__(self, protocol_class, loop, **kwargs)
|
||||
|
||||
@asyncio.coroutine
|
||||
def start(self, host, port=802, sslctx=None, server_hostname=None):
|
||||
"""
|
||||
Initiates connection to start client
|
||||
:param host:
|
||||
:param port:
|
||||
:param sslctx:
|
||||
:param server_hostname:
|
||||
:return:
|
||||
"""
|
||||
self.sslctx = sslctx
|
||||
if self.sslctx is None:
|
||||
self.sslctx = ssl.create_default_context()
|
||||
# According to MODBUS/TCP Security Protocol Specification, it is
|
||||
# TLSv2 at least
|
||||
self.sslctx.options |= ssl.OP_NO_TLSv1_1
|
||||
self.sslctx.options |= ssl.OP_NO_TLSv1
|
||||
self.sslctx.options |= ssl.OP_NO_SSLv3
|
||||
self.sslctx.options |= ssl.OP_NO_SSLv2
|
||||
self.server_hostname = server_hostname
|
||||
yield from ReconnectingAsyncioModbusTcpClient.start(self, host, port)
|
||||
|
||||
@asyncio.coroutine
|
||||
def _connect(self):
|
||||
_logger.debug('Connecting.')
|
||||
try:
|
||||
yield from self.loop.create_connection(self._create_protocol,
|
||||
self.host,
|
||||
self.port,
|
||||
ssl=self.sslctx,
|
||||
server_hostname=self.server_hostname)
|
||||
except Exception as ex:
|
||||
_logger.warning('Failed to connect: %s' % ex)
|
||||
asyncio.ensure_future(self._reconnect(), loop=self.loop)
|
||||
else:
|
||||
_logger.info('Connected to %s:%s.' % (self.host, self.port))
|
||||
self.reset_delay()
|
||||
|
||||
def _create_protocol(self):
|
||||
"""
|
||||
Factory function to create initialized protocol instance.
|
||||
"""
|
||||
protocol = self.protocol_class(framer=self.framer, **self._proto_args)
|
||||
protocol.transaction = FifoTransactionManager(self)
|
||||
protocol.factory = self
|
||||
return protocol
|
||||
|
||||
class ReconnectingAsyncioModbusUdpClient(object):
|
||||
"""
|
||||
Client to connect to modbus device repeatedly over UDP.
|
||||
"""
|
||||
|
||||
#: Reconnect delay in milli seconds.
|
||||
delay_ms = 0
|
||||
|
||||
#: Maximum delay in milli seconds before reconnect is attempted.
|
||||
DELAY_MAX_MS = 1000 * 60 * 5
|
||||
|
||||
def __init__(self, protocol_class=None, loop=None, **kwargs):
|
||||
"""
|
||||
Initializes ReconnectingAsyncioModbusUdpClient
|
||||
:param protocol_class: Protocol used to talk to modbus device.
|
||||
:param loop: Asyncio Event loop
|
||||
"""
|
||||
#: Protocol used to talk to modbus device.
|
||||
self.protocol_class = protocol_class or ModbusUdpClientProtocol
|
||||
#: Current protocol instance.
|
||||
self.protocol = None
|
||||
#: Event loop to use.
|
||||
self.loop = loop or asyncio.get_event_loop()
|
||||
|
||||
self.host = None
|
||||
self.port = 0
|
||||
|
||||
self.connected = False
|
||||
self._proto_args = kwargs
|
||||
self.reset_delay()
|
||||
|
||||
def reset_delay(self):
|
||||
"""
|
||||
Resets wait before next reconnect to minimal period.
|
||||
"""
|
||||
self.delay_ms = 100
|
||||
|
||||
@asyncio.coroutine
|
||||
def start(self, host, port=502):
|
||||
"""
|
||||
Start reconnecting asynchronous udp client
|
||||
:param host: Host IP to connect
|
||||
:param port: Host port to connect
|
||||
:return:
|
||||
"""
|
||||
# force reconnect if required:
|
||||
self.stop()
|
||||
|
||||
_logger.debug('Connecting to %s:%s.' % (host, port))
|
||||
|
||||
# getaddrinfo returns a list of tuples
|
||||
# - [(family, type, proto, canonname, sockaddr),]
|
||||
# We want sockaddr which is a (ip, port) tuple
|
||||
# udp needs ip addresses, not hostnames
|
||||
addrinfo = yield from self.loop.getaddrinfo(host,
|
||||
port,
|
||||
type=DGRAM_TYPE)
|
||||
self.host, self.port = addrinfo[0][-1]
|
||||
|
||||
yield from self._connect()
|
||||
|
||||
def stop(self):
|
||||
"""
|
||||
Stops connection and prevents reconnect
|
||||
:return:
|
||||
"""
|
||||
# prevent reconnect:
|
||||
self.host = None
|
||||
|
||||
if self.connected:
|
||||
if self.protocol:
|
||||
if self.protocol.transport:
|
||||
self.protocol.transport.close()
|
||||
|
||||
def _create_protocol(self, host=None, port=0):
|
||||
"""
|
||||
Factory function to create initialized protocol instance.
|
||||
"""
|
||||
protocol = self.protocol_class(**self._proto_args)
|
||||
protocol.host = host
|
||||
protocol.port = port
|
||||
protocol.factory = self
|
||||
return protocol
|
||||
|
||||
@asyncio.coroutine
|
||||
def _connect(self):
|
||||
_logger.debug('Connecting.')
|
||||
try:
|
||||
yield from self.loop.create_datagram_endpoint(
|
||||
functools.partial(self._create_protocol,
|
||||
host=self.host,
|
||||
port=self.port),
|
||||
remote_addr=(self.host, self.port)
|
||||
)
|
||||
_logger.info('Connected to %s:%s.' % (self.host, self.port))
|
||||
except Exception as ex:
|
||||
_logger.warning('Failed to connect: %s' % ex)
|
||||
asyncio.ensure_future(self._reconnect(), loop=self.loop)
|
||||
|
||||
def protocol_made_connection(self, protocol):
|
||||
"""
|
||||
Protocol notification of successful connection.
|
||||
"""
|
||||
_logger.info('Protocol made connection.')
|
||||
if not self.connected:
|
||||
self.connected = True
|
||||
self.protocol = protocol
|
||||
else:
|
||||
_logger.error('Factory protocol connect callback '
|
||||
'called while connected.')
|
||||
|
||||
def protocol_lost_connection(self, protocol):
|
||||
"""
|
||||
Protocol notification of lost connection.
|
||||
"""
|
||||
if self.connected:
|
||||
_logger.info('Protocol lost connection.')
|
||||
if protocol is not self.protocol:
|
||||
_logger.error('Factory protocol callback called '
|
||||
'from unexpected protocol instance.')
|
||||
|
||||
self.connected = False
|
||||
self.protocol = None
|
||||
if self.host:
|
||||
asyncio.ensure_future(self._reconnect(), loop=self.loop)
|
||||
else:
|
||||
_logger.error('Factory protocol disconnect '
|
||||
'callback called while not connected.')
|
||||
|
||||
@asyncio.coroutine
|
||||
def _reconnect(self):
|
||||
_logger.debug('Waiting %d ms before next '
|
||||
'connection attempt.' % self.delay_ms)
|
||||
yield from asyncio.sleep(self.delay_ms / 1000)
|
||||
self.delay_ms = min(2 * self.delay_ms, self.DELAY_MAX_MS)
|
||||
yield from self._connect()
|
||||
|
||||
|
||||
class AsyncioModbusUdpClient(object):
|
||||
"""
|
||||
Client to connect to modbus device over UDP.
|
||||
"""
|
||||
|
||||
def __init__(self, host=None, port=502, protocol_class=None, loop=None, **kwargs):
|
||||
"""
|
||||
Initializes Asyncio Modbus UDP Client
|
||||
:param host: Host IP address
|
||||
:param port: Port to connect
|
||||
:param protocol_class: Protocol used to talk to modbus device.
|
||||
:param loop: Asyncio Event loop
|
||||
"""
|
||||
#: Protocol used to talk to modbus device.
|
||||
self.protocol_class = protocol_class or ModbusUdpClientProtocol
|
||||
#: Current protocol instance.
|
||||
self.protocol = None
|
||||
#: Event loop to use.
|
||||
self.loop = loop or asyncio.get_event_loop()
|
||||
|
||||
self.host = host
|
||||
self.port = port
|
||||
|
||||
self.connected = False
|
||||
self._proto_args = kwargs
|
||||
|
||||
def stop(self):
|
||||
"""
|
||||
Stops connection
|
||||
:return:
|
||||
"""
|
||||
# prevent reconnect:
|
||||
# self.host = None
|
||||
|
||||
if self.connected:
|
||||
if self.protocol:
|
||||
if self.protocol.transport:
|
||||
self.protocol.transport.close()
|
||||
|
||||
def _create_protocol(self, host=None, port=0):
|
||||
"""
|
||||
Factory function to create initialized protocol instance.
|
||||
"""
|
||||
protocol = self.protocol_class(**self._proto_args)
|
||||
protocol.host = host
|
||||
protocol.port = port
|
||||
protocol.factory = self
|
||||
return protocol
|
||||
|
||||
@asyncio.coroutine
|
||||
def connect(self):
|
||||
_logger.debug('Connecting.')
|
||||
try:
|
||||
addrinfo = yield from self.loop.getaddrinfo(
|
||||
self.host,
|
||||
self.port,
|
||||
type=DGRAM_TYPE)
|
||||
_host, _port = addrinfo[0][-1]
|
||||
yield from self.loop.create_datagram_endpoint(
|
||||
functools.partial(self._create_protocol,
|
||||
host=_host, port=_port),
|
||||
remote_addr=(self.host, self.port)
|
||||
)
|
||||
_logger.info('Connected to %s:%s.' % (self.host, self.port))
|
||||
except Exception as ex:
|
||||
_logger.warning('Failed to connect: %s' % ex)
|
||||
# asyncio.asynchronous(self._reconnect(), loop=self.loop)
|
||||
|
||||
def protocol_made_connection(self, protocol):
|
||||
"""
|
||||
Protocol notification of successful connection.
|
||||
"""
|
||||
_logger.info('Protocol made connection.')
|
||||
if not self.connected:
|
||||
self.connected = True
|
||||
self.protocol = protocol
|
||||
else:
|
||||
_logger.error('Factory protocol connect '
|
||||
'callback called while connected.')
|
||||
|
||||
def protocol_lost_connection(self, protocol):
|
||||
"""
|
||||
Protocol notification of lost connection.
|
||||
"""
|
||||
if self.connected:
|
||||
_logger.info('Protocol lost connection.')
|
||||
if protocol is not self.protocol:
|
||||
_logger.error('Factory protocol callback '
|
||||
'called from unexpected protocol instance.')
|
||||
|
||||
self.connected = False
|
||||
self.protocol = None
|
||||
# if self.host:
|
||||
# asyncio.asynchronous(self._reconnect(), loop=self.loop)
|
||||
else:
|
||||
_logger.error('Factory protocol disconnect '
|
||||
'callback called while not connected.')
|
||||
|
||||
|
||||
class AsyncioModbusSerialClient(object):
|
||||
"""
|
||||
Client to connect to modbus device over serial.
|
||||
"""
|
||||
transport = None
|
||||
framer = None
|
||||
|
||||
def __init__(self, port, protocol_class=None, framer=None, loop=None,
|
||||
baudrate=9600, bytesize=8, parity='N', stopbits=1, **serial_kwargs):
|
||||
"""
|
||||
Initializes Asyncio Modbus Serial Client
|
||||
:param port: Port to connect
|
||||
:param protocol_class: Protocol used to talk to modbus device.
|
||||
:param framer: Framer to use
|
||||
:param loop: Asyncio Event loop
|
||||
"""
|
||||
#: Protocol used to talk to modbus device.
|
||||
self.protocol_class = protocol_class or ModbusClientProtocol
|
||||
#: Current protocol instance.
|
||||
self.protocol = None
|
||||
#: Event loop to use.
|
||||
self.loop = loop or asyncio.get_event_loop()
|
||||
self.port = port
|
||||
self.baudrate = baudrate
|
||||
self.bytesize = bytesize
|
||||
self.parity = parity
|
||||
self.stopbits = stopbits
|
||||
self.framer = framer
|
||||
self._extra_serial_kwargs = serial_kwargs
|
||||
self._connected_event = asyncio.Event()
|
||||
|
||||
def stop(self):
|
||||
"""
|
||||
Stops connection
|
||||
:return:
|
||||
"""
|
||||
if self._connected:
|
||||
if self.protocol:
|
||||
if self.protocol.transport:
|
||||
self.protocol.transport.close()
|
||||
|
||||
def _create_protocol(self):
|
||||
protocol = self.protocol_class(framer=self.framer)
|
||||
protocol.factory = self
|
||||
return protocol
|
||||
|
||||
@property
|
||||
def _connected(self):
|
||||
return self._connected_event.is_set()
|
||||
|
||||
@asyncio.coroutine
|
||||
def connect(self):
|
||||
"""
|
||||
Connect Async client
|
||||
:return:
|
||||
"""
|
||||
_logger.debug('Connecting.')
|
||||
try:
|
||||
from serial_asyncio import create_serial_connection
|
||||
|
||||
yield from create_serial_connection(
|
||||
self.loop, self._create_protocol, self.port, baudrate=self.baudrate,
|
||||
bytesize=self.bytesize, stopbits=self.stopbits, parity=self.parity, **self._extra_serial_kwargs
|
||||
)
|
||||
yield from self._connected_event.wait()
|
||||
_logger.info('Connected to %s', self.port)
|
||||
except Exception as ex:
|
||||
_logger.warning('Failed to connect: %s', ex)
|
||||
|
||||
def protocol_made_connection(self, protocol):
|
||||
"""
|
||||
Protocol notification of successful connection.
|
||||
"""
|
||||
_logger.info('Protocol made connection.')
|
||||
if not self._connected:
|
||||
self._connected_event.set()
|
||||
self.protocol = protocol
|
||||
else:
|
||||
_logger.error('Factory protocol connect '
|
||||
'callback called while connected.')
|
||||
|
||||
def protocol_lost_connection(self, protocol):
|
||||
"""
|
||||
Protocol notification of lost connection.
|
||||
"""
|
||||
if self._connected:
|
||||
_logger.info('Protocol lost connection.')
|
||||
if protocol is not self.protocol:
|
||||
_logger.error('Factory protocol callback called'
|
||||
' from unexpected protocol instance.')
|
||||
|
||||
self._connected_event.clear()
|
||||
self.protocol = None
|
||||
# if self.host:
|
||||
# asyncio.asynchronous(self._reconnect(), loop=self.loop)
|
||||
else:
|
||||
_logger.error('Factory protocol disconnect callback '
|
||||
'called while not connected.')
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def init_tcp_client(proto_cls, loop, host, port, **kwargs):
|
||||
"""
|
||||
Helper function to initialize tcp client
|
||||
:param proto_cls:
|
||||
:param loop:
|
||||
:param host:
|
||||
:param port:
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
client = ReconnectingAsyncioModbusTcpClient(protocol_class=proto_cls,
|
||||
loop=loop, **kwargs)
|
||||
yield from client.start(host, port)
|
||||
return client
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def init_tls_client(proto_cls, loop, host, port, sslctx=None,
|
||||
server_hostname=None, framer=None, **kwargs):
|
||||
"""
|
||||
Helper function to initialize tcp client
|
||||
:param proto_cls:
|
||||
:param loop:
|
||||
:param host:
|
||||
:param port:
|
||||
:param sslctx:
|
||||
:param server_hostname:
|
||||
:param framer:
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
client = ReconnectingAsyncioModbusTlsClient(protocol_class=proto_cls,
|
||||
loop=loop, framer=framer,
|
||||
**kwargs)
|
||||
yield from client.start(host, port, sslctx, server_hostname)
|
||||
return client
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def init_udp_client(proto_cls, loop, host, port, **kwargs):
|
||||
"""
|
||||
Helper function to initialize UDP client
|
||||
:param proto_cls:
|
||||
:param loop:
|
||||
:param host:
|
||||
:param port:
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
client = ReconnectingAsyncioModbusUdpClient(protocol_class=proto_cls,
|
||||
loop=loop, **kwargs)
|
||||
yield from client.start(host, port)
|
||||
return client
|
||||
47
modbus/pymodbus/client/asynchronous/deprecated/__init__.py
Normal file
@@ -0,0 +1,47 @@
|
||||
import warnings
|
||||
warnings.simplefilter('always', DeprecationWarning)
|
||||
|
||||
WARNING = """
|
||||
Usage of '{}' is deprecated from 2.0.0 and will be removed in future releases.
|
||||
Use the new Async Modbus Client implementation based on Twisted, tornado
|
||||
and asyncio
|
||||
------------------------------------------------------------------------
|
||||
|
||||
Example run::
|
||||
|
||||
from pymodbus.client.asynchronous import schedulers
|
||||
|
||||
# Import The clients
|
||||
|
||||
from pymodbus.client.asynchronous.tcp import AsyncModbusTCPClient as Client
|
||||
from pymodbus.client.asynchronous.serial import AsyncModbusSerialClient as Client
|
||||
from pymodbus.client.asynchronous.udp import AsyncModbusUDPClient as Client
|
||||
|
||||
# For tornado based asynchronous client use
|
||||
event_loop, future = Client(schedulers.IO_LOOP, port=5020)
|
||||
|
||||
# For twisted based asynchronous client use
|
||||
event_loop, deferred = Client(schedulers.REACTOR, port=5020)
|
||||
|
||||
# For asyncio based asynchronous client use
|
||||
event_loop, client = Client(schedulers.ASYNC_IO, port=5020)
|
||||
|
||||
# Here event_loop is a thread which would control the backend and future is
|
||||
# a Future/deffered object which would be used to
|
||||
# add call backs to run asynchronously.
|
||||
|
||||
# The Actual client could be accessed with future.result() with Tornado
|
||||
# and future.result when using twisted
|
||||
|
||||
# For asyncio the actual client is returned and event loop is asyncio loop
|
||||
|
||||
Refer:
|
||||
https://pymodbus.readthedocs.io/en/dev/source/example/async_twisted_client.html
|
||||
https://pymodbus.readthedocs.io/en/dev/source/example/async_tornado_client.html
|
||||
https://pymodbus.readthedocs.io/en/dev/source/example/async_asyncio_client.html
|
||||
|
||||
"""
|
||||
|
||||
|
||||
def deprecated(name): # pragma: no cover
|
||||
warnings.warn(WARNING.format(name), DeprecationWarning)
|
||||
231
modbus/pymodbus/client/asynchronous/deprecated/asynchronous.py
Normal file
@@ -0,0 +1,231 @@
|
||||
"""
|
||||
Implementation of a Modbus Client Using Twisted
|
||||
--------------------------------------------------
|
||||
|
||||
Example run::
|
||||
|
||||
from twisted.internet import reactor, protocol
|
||||
from pymodbus.client.asynchronous import ModbusClientProtocol
|
||||
|
||||
def printResult(result):
|
||||
print "Result: %d" % result.bits[0]
|
||||
|
||||
def process(client):
|
||||
result = client.write_coil(1, True)
|
||||
result.addCallback(printResult)
|
||||
reactor.callLater(1, reactor.stop)
|
||||
|
||||
defer = protocol.ClientCreator(reactor, ModbusClientProtocol
|
||||
).connectTCP("localhost", 502)
|
||||
defer.addCallback(process)
|
||||
|
||||
Another example::
|
||||
|
||||
from twisted.internet import reactor
|
||||
from pymodbus.client.asynchronous import ModbusClientFactory
|
||||
|
||||
def process():
|
||||
factory = reactor.connectTCP("localhost", 502, ModbusClientFactory())
|
||||
reactor.stop()
|
||||
|
||||
if __name__ == "__main__":
|
||||
reactor.callLater(1, process)
|
||||
reactor.run()
|
||||
"""
|
||||
import logging
|
||||
from pymodbus.factory import ClientDecoder
|
||||
from pymodbus.exceptions import ConnectionException
|
||||
from pymodbus.transaction import ModbusSocketFramer
|
||||
from pymodbus.transaction import FifoTransactionManager
|
||||
from pymodbus.transaction import DictTransactionManager
|
||||
from pymodbus.client.common import ModbusClientMixin
|
||||
from pymodbus.client.asynchronous.deprecated import deprecated
|
||||
from twisted.internet import defer, protocol
|
||||
from twisted.python.failure import Failure
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Logging
|
||||
# --------------------------------------------------------------------------- #
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Connected Client Protocols
|
||||
# --------------------------------------------------------------------------- #
|
||||
class ModbusClientProtocol(protocol.Protocol, ModbusClientMixin): # pragma: no cover
|
||||
"""
|
||||
This represents the base modbus client protocol. All the application
|
||||
layer code is deferred to a higher level wrapper.
|
||||
"""
|
||||
|
||||
def __init__(self, framer=None, **kwargs):
|
||||
""" Initializes the framer module
|
||||
|
||||
:param framer: The framer to use for the protocol
|
||||
"""
|
||||
deprecated(self.__class__.__name__)
|
||||
self._connected = False
|
||||
self.framer = framer or ModbusSocketFramer(ClientDecoder())
|
||||
if isinstance(self.framer, type):
|
||||
# Framer class not instance
|
||||
self.framer = self.framer(ClientDecoder(), client=None)
|
||||
if isinstance(self.framer, ModbusSocketFramer):
|
||||
self.transaction = DictTransactionManager(self, **kwargs)
|
||||
else:
|
||||
self.transaction = FifoTransactionManager(self, **kwargs)
|
||||
|
||||
def connectionMade(self):
|
||||
""" Called upon a successful client connection.
|
||||
"""
|
||||
_logger.debug("Client connected to modbus server")
|
||||
self._connected = True
|
||||
|
||||
def connectionLost(self, reason):
|
||||
""" Called upon a client disconnect
|
||||
|
||||
:param reason: The reason for the disconnect
|
||||
"""
|
||||
_logger.debug("Client disconnected from modbus server: %s" % reason)
|
||||
self._connected = False
|
||||
for tid in list(self.transaction):
|
||||
self.transaction.getTransaction(tid).errback(Failure(
|
||||
ConnectionException('Connection lost during request')))
|
||||
|
||||
def dataReceived(self, data):
|
||||
""" Get response, check for valid message, decode result
|
||||
|
||||
:param data: The data returned from the server
|
||||
"""
|
||||
unit = self.framer.decode_data(data).get("uid", 0)
|
||||
self.framer.processIncomingPacket(data, self._handleResponse, unit=unit)
|
||||
|
||||
def execute(self, request):
|
||||
""" Starts the producer to send the next request to
|
||||
consumer.write(Frame(request))
|
||||
"""
|
||||
request.transaction_id = self.transaction.getNextTID()
|
||||
packet = self.framer.buildPacket(request)
|
||||
self.transport.write(packet)
|
||||
return self._buildResponse(request.transaction_id)
|
||||
|
||||
def _handleResponse(self, reply):
|
||||
""" Handle the processed response and link to correct deferred
|
||||
|
||||
:param reply: The reply to process
|
||||
"""
|
||||
if reply is not None:
|
||||
tid = reply.transaction_id
|
||||
handler = self.transaction.getTransaction(tid)
|
||||
if handler:
|
||||
handler.callback(reply)
|
||||
else:
|
||||
_logger.debug("Unrequested message: " + str(reply))
|
||||
|
||||
def _buildResponse(self, tid):
|
||||
""" Helper method to return a deferred response
|
||||
for the current request.
|
||||
|
||||
:param tid: The transaction identifier for this response
|
||||
:returns: A defer linked to the latest request
|
||||
"""
|
||||
if not self._connected:
|
||||
return defer.fail(Failure(
|
||||
ConnectionException('Client is not connected')))
|
||||
|
||||
d = defer.Deferred()
|
||||
self.transaction.addTransaction(d, tid)
|
||||
return d
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# Extra Functions
|
||||
# ---------------------------------------------------------------------- #
|
||||
# if send_failed:
|
||||
# if self.retry > 0:
|
||||
# deferLater(clock, self.delay, send, message)
|
||||
# self.retry -= 1
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Not Connected Client Protocol
|
||||
# --------------------------------------------------------------------------- #
|
||||
class ModbusUdpClientProtocol(protocol.DatagramProtocol, ModbusClientMixin): # pragma: no cover
|
||||
"""
|
||||
This represents the base modbus client protocol. All the application
|
||||
layer code is deferred to a higher level wrapper.
|
||||
"""
|
||||
|
||||
def __init__(self, framer=None, **kwargs):
|
||||
""" Initializes the framer module
|
||||
|
||||
:param framer: The framer to use for the protocol
|
||||
"""
|
||||
deprecated(self.__class__.__name__)
|
||||
self.framer = framer or ModbusSocketFramer(ClientDecoder())
|
||||
if isinstance(self.framer, ModbusSocketFramer):
|
||||
self.transaction = DictTransactionManager(self, **kwargs)
|
||||
else: self.transaction = FifoTransactionManager(self, **kwargs)
|
||||
|
||||
def datagramReceived(self, data, params):
|
||||
""" Get response, check for valid message, decode result
|
||||
|
||||
:param data: The data returned from the server
|
||||
:param params: The host parameters sending the datagram
|
||||
"""
|
||||
_logger.debug("Datagram from: %s:%d" % params)
|
||||
unit = self.framer.decode_data(data).get("uid", 0)
|
||||
self.framer.processIncomingPacket(data, self._handleResponse, unit=unit)
|
||||
|
||||
def execute(self, request):
|
||||
""" Starts the producer to send the next request to
|
||||
consumer.write(Frame(request))
|
||||
"""
|
||||
request.transaction_id = self.transaction.getNextTID()
|
||||
packet = self.framer.buildPacket(request)
|
||||
self.transport.write(packet)
|
||||
return self._buildResponse(request.transaction_id)
|
||||
|
||||
def _handleResponse(self, reply):
|
||||
""" Handle the processed response and link to correct deferred
|
||||
|
||||
:param reply: The reply to process
|
||||
"""
|
||||
if reply is not None:
|
||||
tid = reply.transaction_id
|
||||
handler = self.transaction.getTransaction(tid)
|
||||
if handler:
|
||||
handler.callback(reply)
|
||||
else: _logger.debug("Unrequested message: " + str(reply))
|
||||
|
||||
def _buildResponse(self, tid):
|
||||
""" Helper method to return a deferred response
|
||||
for the current request.
|
||||
|
||||
:param tid: The transaction identifier for this response
|
||||
:returns: A defer linked to the latest request
|
||||
"""
|
||||
d = defer.Deferred()
|
||||
self.transaction.addTransaction(d, tid)
|
||||
return d
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Client Factories
|
||||
# --------------------------------------------------------------------------- #
|
||||
class ModbusClientFactory(protocol.ReconnectingClientFactory): # pragma: no cover
|
||||
""" Simple client protocol factory """
|
||||
|
||||
protocol = ModbusClientProtocol
|
||||
|
||||
def __init__(self):
|
||||
deprecated(self.__class__.__name__)
|
||||
protocol.ReconnectingClientFactory.__init__(self)
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Exported symbols
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
__all__ = [
|
||||
"ModbusClientProtocol", "ModbusUdpClientProtocol", "ModbusClientFactory"
|
||||
]
|
||||
1
modbus/pymodbus/client/asynchronous/factory/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
130
modbus/pymodbus/client/asynchronous/factory/serial.py
Normal file
@@ -0,0 +1,130 @@
|
||||
"""
|
||||
Factory to create asynchronous serial clients based on twisted/tornado/asyncio
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
|
||||
import logging
|
||||
import time
|
||||
from pymodbus.client.asynchronous import schedulers
|
||||
from pymodbus.client.asynchronous.thread import EventLoopThread
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def reactor_factory(port, framer, **kwargs):
|
||||
"""
|
||||
Factory to create twisted serial asynchronous client
|
||||
:param port: Serial port
|
||||
:param framer: Modbus Framer
|
||||
:param kwargs:
|
||||
:return: event_loop_thread and twisted serial client
|
||||
"""
|
||||
from twisted.internet import reactor
|
||||
from twisted.internet.serialport import SerialPort
|
||||
from twisted.internet.protocol import ClientFactory
|
||||
from pymodbus.factory import ClientDecoder
|
||||
|
||||
class SerialClientFactory(ClientFactory):
|
||||
def __init__(self, framer, proto_cls):
|
||||
''' Remember things necessary for building a protocols '''
|
||||
self.proto_cls = proto_cls
|
||||
self.framer = framer
|
||||
|
||||
def buildProtocol(self):
|
||||
''' Create a protocol and start the reading cycle '''
|
||||
proto = self.proto_cls(self.framer)
|
||||
proto.factory = self
|
||||
return proto
|
||||
|
||||
class SerialModbusClient(SerialPort):
|
||||
|
||||
def __init__(self, framer, *args, **kwargs):
|
||||
''' Setup the client and start listening on the serial port
|
||||
|
||||
:param factory: The factory to build clients with
|
||||
'''
|
||||
self.decoder = ClientDecoder()
|
||||
proto_cls = kwargs.pop("proto_cls", None)
|
||||
proto = SerialClientFactory(framer, proto_cls).buildProtocol()
|
||||
SerialPort.__init__(self, proto, *args, **kwargs)
|
||||
|
||||
proto = EventLoopThread("reactor", reactor.run, reactor.stop,
|
||||
installSignalHandlers=0)
|
||||
ser_client = SerialModbusClient(framer, port, reactor, **kwargs)
|
||||
|
||||
return proto, ser_client
|
||||
|
||||
|
||||
def io_loop_factory(port=None, framer=None, **kwargs):
|
||||
"""
|
||||
Factory to create Tornado based asynchronous serial clients
|
||||
:param port: Serial port
|
||||
:param framer: Modbus Framer
|
||||
:param kwargs:
|
||||
:return: event_loop_thread and tornado future
|
||||
"""
|
||||
|
||||
from tornado.ioloop import IOLoop
|
||||
from pymodbus.client.asynchronous.tornado import (AsyncModbusSerialClient as
|
||||
Client)
|
||||
|
||||
ioloop = IOLoop()
|
||||
protocol = EventLoopThread("ioloop", ioloop.start, ioloop.stop)
|
||||
protocol.start()
|
||||
client = Client(port=port, framer=framer, ioloop=ioloop, **kwargs)
|
||||
|
||||
future = client.connect()
|
||||
|
||||
return protocol, future
|
||||
|
||||
|
||||
def async_io_factory(port=None, framer=None, **kwargs):
|
||||
"""
|
||||
Factory to create asyncio based asynchronous serial clients
|
||||
:param port: Serial port
|
||||
:param framer: Modbus Framer
|
||||
:param kwargs: Serial port options
|
||||
:return: asyncio event loop and serial client
|
||||
"""
|
||||
import asyncio
|
||||
from pymodbus.client.asynchronous.async_io import (ModbusClientProtocol,
|
||||
AsyncioModbusSerialClient)
|
||||
loop = kwargs.pop("loop", None) or asyncio.get_event_loop()
|
||||
proto_cls = kwargs.pop("proto_cls", None) or ModbusClientProtocol
|
||||
|
||||
try:
|
||||
from serial_asyncio import create_serial_connection
|
||||
except ImportError:
|
||||
LOGGER.critical("pyserial-asyncio is not installed, "
|
||||
"install with 'pip install pyserial-asyncio")
|
||||
import sys
|
||||
sys.exit(1)
|
||||
|
||||
client = AsyncioModbusSerialClient(port, proto_cls, framer, loop, **kwargs)
|
||||
coro = client.connect()
|
||||
if loop.is_running():
|
||||
future = asyncio.run_coroutine_threadsafe(coro, loop=loop)
|
||||
future.result()
|
||||
else:
|
||||
loop.run_until_complete(coro)
|
||||
return loop, client
|
||||
|
||||
|
||||
def get_factory(scheduler):
|
||||
"""
|
||||
Gets protocol factory based on the backend scheduler being used
|
||||
:param scheduler: REACTOR/IO_LOOP/ASYNC_IO
|
||||
:return:
|
||||
"""
|
||||
if scheduler == schedulers.REACTOR:
|
||||
return reactor_factory
|
||||
elif scheduler == schedulers.IO_LOOP:
|
||||
return io_loop_factory
|
||||
elif scheduler == schedulers.ASYNC_IO:
|
||||
return async_io_factory
|
||||
else:
|
||||
LOGGER.warning("Allowed Schedulers: {}, {}, {}".format(
|
||||
schedulers.REACTOR, schedulers.IO_LOOP, schedulers.ASYNC_IO
|
||||
))
|
||||
raise Exception("Invalid Scheduler '{}'".format(scheduler))
|
||||
123
modbus/pymodbus/client/asynchronous/factory/tcp.py
Normal file
@@ -0,0 +1,123 @@
|
||||
"""
|
||||
Factory to create asynchronous tcp clients based on twisted/tornado/asyncio
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
|
||||
import logging
|
||||
|
||||
from pymodbus.client.asynchronous import schedulers
|
||||
from pymodbus.client.asynchronous.thread import EventLoopThread
|
||||
from pymodbus.constants import Defaults
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def reactor_factory(host="127.0.0.1", port=Defaults.Port, framer=None,
|
||||
source_address=None, timeout=None, **kwargs):
|
||||
"""
|
||||
Factory to create twisted tcp asynchronous client
|
||||
:param host: Host IP address
|
||||
:param port: Port
|
||||
:param framer: Modbus Framer
|
||||
:param source_address: Bind address
|
||||
:param timeout: Timeout in seconds
|
||||
:param kwargs:
|
||||
:return: event_loop_thread and twisted_deferred
|
||||
"""
|
||||
from twisted.internet import reactor, protocol
|
||||
from pymodbus.client.asynchronous.twisted import ModbusTcpClientProtocol
|
||||
|
||||
deferred = protocol.ClientCreator(
|
||||
reactor, ModbusTcpClientProtocol
|
||||
).connectTCP(host, port, timeout=timeout, bindAddress=source_address)
|
||||
|
||||
callback = kwargs.get("callback")
|
||||
errback = kwargs.get("errback")
|
||||
|
||||
if callback:
|
||||
deferred.addCallback(callback)
|
||||
|
||||
if errback:
|
||||
deferred.addErrback(errback)
|
||||
|
||||
protocol = EventLoopThread("reactor", reactor.run, reactor.stop,
|
||||
installSignalHandlers=0)
|
||||
protocol.start()
|
||||
|
||||
return protocol, deferred
|
||||
|
||||
|
||||
def io_loop_factory(host="127.0.0.1", port=Defaults.Port, framer=None,
|
||||
source_address=None, timeout=None, **kwargs):
|
||||
"""
|
||||
Factory to create Tornado based asynchronous tcp clients
|
||||
:param host: Host IP address
|
||||
:param port: Port
|
||||
:param framer: Modbus Framer
|
||||
:param source_address: Bind address
|
||||
:param timeout: Timeout in seconds
|
||||
:param kwargs:
|
||||
:return: event_loop_thread and tornado future
|
||||
"""
|
||||
from tornado.ioloop import IOLoop
|
||||
from pymodbus.client.asynchronous.tornado import AsyncModbusTCPClient as \
|
||||
Client
|
||||
|
||||
ioloop = IOLoop()
|
||||
protocol = EventLoopThread("ioloop", ioloop.start, ioloop.stop)
|
||||
protocol.start()
|
||||
|
||||
client = Client(host=host, port=port, framer=framer,
|
||||
source_address=source_address,
|
||||
timeout=timeout, ioloop=ioloop, **kwargs)
|
||||
|
||||
future = client.connect()
|
||||
|
||||
return protocol, future
|
||||
|
||||
|
||||
def async_io_factory(host="127.0.0.1", port=Defaults.Port, **kwargs):
|
||||
"""
|
||||
Factory to create asyncio based asynchronous tcp clients
|
||||
:param host: Host IP address
|
||||
:param port: Port
|
||||
:param framer: Modbus Framer
|
||||
:param source_address: Bind address
|
||||
:param timeout: Timeout in seconds
|
||||
:param kwargs:
|
||||
:return: asyncio event loop and tcp client
|
||||
"""
|
||||
import asyncio
|
||||
from pymodbus.client.asynchronous.async_io import init_tcp_client
|
||||
loop = kwargs.pop("loop", None) or asyncio.new_event_loop()
|
||||
proto_cls = kwargs.pop("proto_cls", None)
|
||||
if not loop.is_running():
|
||||
asyncio.set_event_loop(loop)
|
||||
cor = init_tcp_client(proto_cls, loop, host, port, **kwargs)
|
||||
client = loop.run_until_complete(asyncio.gather(cor))[0]
|
||||
else:
|
||||
cor = init_tcp_client(proto_cls, loop, host, port, **kwargs)
|
||||
future = asyncio.run_coroutine_threadsafe(cor, loop=loop)
|
||||
client = future.result()
|
||||
|
||||
return loop, client
|
||||
|
||||
|
||||
def get_factory(scheduler):
|
||||
"""
|
||||
Gets protocol factory based on the backend scheduler being used
|
||||
:param scheduler: REACTOR/IO_LOOP/ASYNC_IO
|
||||
:return
|
||||
"""
|
||||
if scheduler == schedulers.REACTOR:
|
||||
return reactor_factory
|
||||
elif scheduler == schedulers.IO_LOOP:
|
||||
return io_loop_factory
|
||||
elif scheduler == schedulers.ASYNC_IO:
|
||||
return async_io_factory
|
||||
else:
|
||||
LOGGER.warning("Allowed Schedulers: {}, {}, {}".format(
|
||||
schedulers.REACTOR, schedulers.IO_LOOP, schedulers.ASYNC_IO
|
||||
))
|
||||
raise Exception("Invalid Scheduler '{}'".format(scheduler))
|
||||
59
modbus/pymodbus/client/asynchronous/factory/tls.py
Normal file
@@ -0,0 +1,59 @@
|
||||
"""
|
||||
Factory to create asynchronous tls clients based on asyncio
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
|
||||
import logging
|
||||
|
||||
from pymodbus.client.asynchronous import schedulers
|
||||
from pymodbus.client.asynchronous.thread import EventLoopThread
|
||||
from pymodbus.constants import Defaults
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
def async_io_factory(host="127.0.0.1", port=Defaults.TLSPort, sslctx=None,
|
||||
server_hostname=None, framer=None, **kwargs):
|
||||
"""
|
||||
Factory to create asyncio based asynchronous tls clients
|
||||
:param host: Host IP address
|
||||
:param port: Port
|
||||
:param sslctx: The SSLContext to use for TLS (default None and auto create)
|
||||
:param server_hostname: Target server's name matched for certificate
|
||||
:param framer: Modbus Framer
|
||||
:param source_address: Bind address
|
||||
:param timeout: Timeout in seconds
|
||||
:param kwargs:
|
||||
:return: asyncio event loop and tcp client
|
||||
"""
|
||||
import asyncio
|
||||
from pymodbus.client.asynchronous.async_io import init_tls_client
|
||||
loop = kwargs.pop("loop", None) or asyncio.new_event_loop()
|
||||
proto_cls = kwargs.pop("proto_cls", None)
|
||||
if not loop.is_running():
|
||||
asyncio.set_event_loop(loop)
|
||||
cor = init_tls_client(proto_cls, loop, host, port, sslctx, server_hostname,
|
||||
framer, **kwargs)
|
||||
client = loop.run_until_complete(asyncio.gather(cor))[0]
|
||||
else:
|
||||
cor = init_tls_client(proto_cls, loop, host, port, sslctx, server_hostname,
|
||||
framer, **kwargs)
|
||||
future = asyncio.run_coroutine_threadsafe(cor, loop=loop)
|
||||
client = future.result()
|
||||
|
||||
return loop, client
|
||||
|
||||
|
||||
def get_factory(scheduler):
|
||||
"""
|
||||
Gets protocol factory based on the backend scheduler being used
|
||||
:param scheduler: ASYNC_IO
|
||||
:return
|
||||
"""
|
||||
if scheduler == schedulers.ASYNC_IO:
|
||||
return async_io_factory
|
||||
else:
|
||||
LOGGER.warning("Allowed Schedulers: {}".format(
|
||||
schedulers.ASYNC_IO
|
||||
))
|
||||
raise Exception("Invalid Scheduler '{}'".format(scheduler))
|
||||
96
modbus/pymodbus/client/asynchronous/factory/udp.py
Normal file
@@ -0,0 +1,96 @@
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
|
||||
import logging
|
||||
|
||||
from pymodbus.client.asynchronous import schedulers
|
||||
from pymodbus.client.asynchronous.thread import EventLoopThread
|
||||
from pymodbus.constants import Defaults
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def reactor_factory(host="127.0.0.1", port=Defaults.Port, framer=None,
|
||||
source_address=None, timeout=None, **kwargs):
|
||||
"""
|
||||
Factory to create twisted udp asynchronous client
|
||||
:param host: Host IP address
|
||||
:param port: Port
|
||||
:param framer: Modbus Framer
|
||||
:param source_address: Bind address
|
||||
:param timeout: Timeout in seconds
|
||||
:param kwargs:
|
||||
:return: event_loop_thread and twisted_deferred
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
def io_loop_factory(host="127.0.0.1", port=Defaults.Port, framer=None,
|
||||
source_address=None, timeout=None, **kwargs):
|
||||
"""
|
||||
Factory to create Tornado based asynchronous udp clients
|
||||
:param host: Host IP address
|
||||
:param port: Port
|
||||
:param framer: Modbus Framer
|
||||
:param source_address: Bind address
|
||||
:param timeout: Timeout in seconds
|
||||
:param kwargs:
|
||||
:return: event_loop_thread and tornado future
|
||||
"""
|
||||
from tornado.ioloop import IOLoop
|
||||
from pymodbus.client.asynchronous.tornado import AsyncModbusUDPClient as \
|
||||
Client
|
||||
|
||||
client = Client(host=host, port=port, framer=framer,
|
||||
source_address=source_address,
|
||||
timeout=timeout, **kwargs)
|
||||
protocol = EventLoopThread("ioloop", IOLoop.current().start,
|
||||
IOLoop.current().stop)
|
||||
protocol.start()
|
||||
future = client.connect()
|
||||
|
||||
return protocol, future
|
||||
|
||||
|
||||
def async_io_factory(host="127.0.0.1", port=Defaults.Port, **kwargs):
|
||||
"""
|
||||
Factory to create asyncio based asynchronous udp clients
|
||||
:param host: Host IP address
|
||||
:param port: Port
|
||||
:param framer: Modbus Framer
|
||||
:param source_address: Bind address
|
||||
:param timeout: Timeout in seconds
|
||||
:param kwargs:
|
||||
:return: asyncio event loop and udp client
|
||||
"""
|
||||
import asyncio
|
||||
from pymodbus.client.asynchronous.async_io import init_udp_client
|
||||
loop = kwargs.pop("loop", None) or asyncio.get_event_loop()
|
||||
proto_cls = kwargs.pop("proto_cls", None)
|
||||
cor = init_udp_client(proto_cls, loop, host, port, **kwargs)
|
||||
if not loop.is_running():
|
||||
client = loop.run_until_complete(asyncio.gather(cor))[0]
|
||||
else:
|
||||
client = asyncio.run_coroutine_threadsafe(cor, loop=loop)
|
||||
client = client.result()
|
||||
return loop, client
|
||||
|
||||
|
||||
def get_factory(scheduler):
|
||||
"""
|
||||
Gets protocol factory based on the backend scheduler being used
|
||||
:param scheduler: REACTOR/IO_LOOP/ASYNC_IO
|
||||
:return
|
||||
"""
|
||||
if scheduler == schedulers.REACTOR:
|
||||
return reactor_factory
|
||||
elif scheduler == schedulers.IO_LOOP:
|
||||
return io_loop_factory
|
||||
elif scheduler == schedulers.ASYNC_IO:
|
||||
return async_io_factory
|
||||
else:
|
||||
LOGGER.warning("Allowed Schedulers: {}, {}, {}".format(
|
||||
schedulers.REACTOR, schedulers.IO_LOOP, schedulers.ASYNC_IO
|
||||
))
|
||||
raise Exception("Invalid Scheduler '{}'".format(scheduler))
|
||||
|
||||
75
modbus/pymodbus/client/asynchronous/mixins.py
Normal file
@@ -0,0 +1,75 @@
|
||||
import logging
|
||||
from pymodbus.client.sync import BaseModbusClient
|
||||
# from pymodbus.bit_read_message import *
|
||||
# from pymodbus.bit_write_message import *
|
||||
# from pymodbus.register_read_message import *
|
||||
# from pymodbus.register_write_message import *
|
||||
# from pymodbus.diag_message import *
|
||||
# from pymodbus.file_message import *
|
||||
# from pymodbus.other_message import *
|
||||
from pymodbus.constants import Defaults
|
||||
|
||||
from pymodbus.factory import ClientDecoder
|
||||
from pymodbus.transaction import ModbusSocketFramer
|
||||
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BaseAsyncModbusClient(BaseModbusClient):
|
||||
"""
|
||||
This represents the base ModbusAsyncClient.
|
||||
"""
|
||||
|
||||
def __init__(self, framer=None, timeout=2, **kwargs):
|
||||
""" Initializes the framer module
|
||||
|
||||
:param framer: The framer to use for the protocol. Default:
|
||||
ModbusSocketFramer
|
||||
:type framer: pymodbus.transaction.ModbusSocketFramer
|
||||
"""
|
||||
self._connected = False
|
||||
self._timeout = timeout
|
||||
|
||||
super(BaseAsyncModbusClient, self).__init__(
|
||||
framer or ModbusSocketFramer(ClientDecoder()), **kwargs
|
||||
)
|
||||
|
||||
|
||||
class AsyncModbusClientMixin(BaseAsyncModbusClient):
|
||||
"""
|
||||
Async Modbus client mixing for UDP and TCP clients
|
||||
"""
|
||||
def __init__(self, host="127.0.0.1", port=Defaults.Port, framer=None,
|
||||
source_address=None, timeout=None, **kwargs):
|
||||
"""
|
||||
Initializes a Modbus TCP/UDP asynchronous client
|
||||
:param host: Host IP address
|
||||
:param port: Port
|
||||
:param framer: Framer to use
|
||||
:param source_address: Specific to underlying client being used
|
||||
:param timeout: Timeout in seconds
|
||||
:param kwargs: Extra arguments
|
||||
"""
|
||||
super(AsyncModbusClientMixin, self).__init__(framer=framer, **kwargs)
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.source_address = source_address or ("", 0)
|
||||
self._timeout = timeout if timeout is not None else Defaults.Timeout
|
||||
|
||||
|
||||
class AsyncModbusSerialClientMixin(BaseAsyncModbusClient):
|
||||
"""
|
||||
Async Modbus Serial Client Mixing
|
||||
"""
|
||||
def __init__(self, framer=None, port=None, **kwargs):
|
||||
"""
|
||||
Initializes a Async Modbus Serial Client
|
||||
:param framer: Modbus Framer
|
||||
:param port: Serial port to use
|
||||
:param kwargs: Extra arguments if any
|
||||
"""
|
||||
super(AsyncModbusSerialClientMixin, self).__init__(framer=framer)
|
||||
self.port = port
|
||||
self.serial_settings = kwargs
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
"""
|
||||
Backend schedulers to use with generic Async clients
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
REACTOR = "reactor"
|
||||
IO_LOOP = "io_loop"
|
||||
ASYNC_IO = "async_io"
|
||||
76
modbus/pymodbus/client/asynchronous/serial.py
Normal file
@@ -0,0 +1,76 @@
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
|
||||
import logging
|
||||
from pymodbus.client.asynchronous.factory.serial import get_factory
|
||||
from pymodbus.transaction import ModbusRtuFramer, ModbusAsciiFramer, ModbusBinaryFramer, ModbusSocketFramer
|
||||
from pymodbus.factory import ClientDecoder
|
||||
from pymodbus.exceptions import ParameterException
|
||||
from pymodbus.compat import IS_PYTHON3, PYTHON_VERSION
|
||||
from pymodbus.client.asynchronous.schedulers import ASYNC_IO
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AsyncModbusSerialClient(object):
|
||||
"""
|
||||
Actual Async Serial Client to be used.
|
||||
|
||||
To use do::
|
||||
|
||||
from pymodbus.client.asynchronous.serial import AsyncModbusSerialClient
|
||||
"""
|
||||
@classmethod
|
||||
def _framer(cls, method):
|
||||
"""
|
||||
Returns the requested framer
|
||||
|
||||
:method: The serial framer to instantiate
|
||||
:returns: The requested serial framer
|
||||
"""
|
||||
method = method.lower()
|
||||
if method == 'ascii':
|
||||
return ModbusAsciiFramer(ClientDecoder())
|
||||
elif method == 'rtu':
|
||||
return ModbusRtuFramer(ClientDecoder())
|
||||
elif method == 'binary':
|
||||
return ModbusBinaryFramer(ClientDecoder())
|
||||
elif method == 'socket':
|
||||
return ModbusSocketFramer(ClientDecoder())
|
||||
|
||||
raise ParameterException("Invalid framer method requested")
|
||||
|
||||
def __new__(cls, scheduler, method, port, **kwargs):
|
||||
"""
|
||||
Scheduler to use:
|
||||
- reactor (Twisted)
|
||||
- io_loop (Tornado)
|
||||
- async_io (asyncio)
|
||||
The methods to connect are::
|
||||
|
||||
- ascii
|
||||
- rtu
|
||||
- binary
|
||||
: param scheduler: Backend to use
|
||||
:param method: The method to use for connection
|
||||
:param port: The serial port to attach to
|
||||
:param stopbits: The number of stop bits to use
|
||||
:param bytesize: The bytesize of the serial messages
|
||||
:param parity: Which kind of parity to use
|
||||
:param baudrate: The baud rate to use for the serial device
|
||||
:param timeout: The timeout between serial requests (default 3s)
|
||||
:param scheduler:
|
||||
:param method:
|
||||
:param port:
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
if (not (IS_PYTHON3 and PYTHON_VERSION >= (3, 4))
|
||||
and scheduler == ASYNC_IO):
|
||||
logger.critical("ASYNCIO is supported only on python3")
|
||||
import sys
|
||||
sys.exit(1)
|
||||
factory_class = get_factory(scheduler)
|
||||
framer = cls._framer(method)
|
||||
yieldable = factory_class(framer=framer, port=port, **kwargs)
|
||||
return yieldable
|
||||
47
modbus/pymodbus/client/asynchronous/tcp.py
Normal file
@@ -0,0 +1,47 @@
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
|
||||
import logging
|
||||
from pymodbus.client.asynchronous.factory.tcp import get_factory
|
||||
from pymodbus.constants import Defaults
|
||||
from pymodbus.compat import IS_PYTHON3, PYTHON_VERSION
|
||||
from pymodbus.client.asynchronous.schedulers import ASYNC_IO
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AsyncModbusTCPClient(object):
|
||||
"""
|
||||
Actual Async Serial Client to be used.
|
||||
|
||||
To use do::
|
||||
|
||||
from pymodbus.client.asynchronous.tcp import AsyncModbusTCPClient
|
||||
"""
|
||||
def __new__(cls, scheduler, host="127.0.0.1", port=Defaults.Port,
|
||||
framer=None, source_address=None, timeout=None, **kwargs):
|
||||
"""
|
||||
Scheduler to use:
|
||||
- reactor (Twisted)
|
||||
- io_loop (Tornado)
|
||||
- async_io (asyncio)
|
||||
:param scheduler: Backend to use
|
||||
:param host: Host IP address
|
||||
:param port: Port
|
||||
:param framer: Modbus Framer to use
|
||||
:param source_address: source address specific to underlying backend
|
||||
:param timeout: Time out in seconds
|
||||
:param kwargs: Other extra args specific to Backend being used
|
||||
:return:
|
||||
"""
|
||||
if (not (IS_PYTHON3 and PYTHON_VERSION >= (3, 4))
|
||||
and scheduler == ASYNC_IO):
|
||||
logger.critical("ASYNCIO is supported only on python3")
|
||||
import sys
|
||||
sys.exit(1)
|
||||
factory_class = get_factory(scheduler)
|
||||
yieldable = factory_class(host=host, port=port, framer=framer,
|
||||
source_address=source_address,
|
||||
timeout=timeout, **kwargs)
|
||||
return yieldable
|
||||
|
||||
54
modbus/pymodbus/client/asynchronous/thread.py
Normal file
@@ -0,0 +1,54 @@
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
|
||||
from threading import Thread
|
||||
|
||||
import logging
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EventLoopThread(object):
|
||||
"""
|
||||
Event loop controlling the backend event loops (io_loop for tornado,
|
||||
reactor for twisted and event_loop for Asyncio)
|
||||
"""
|
||||
def __init__(self, name, start, stop, *args, **kwargs):
|
||||
"""
|
||||
Initialize Event loop thread
|
||||
:param name: Name of the event loop
|
||||
:param start: Start method to start the backend event loop
|
||||
:param stop: Stop method to stop the backend event loop
|
||||
:param args:
|
||||
:param kwargs:
|
||||
"""
|
||||
self._name = name
|
||||
self._start_loop = start
|
||||
self._stop_loop = stop
|
||||
self._args = args
|
||||
self._kwargs = kwargs
|
||||
self._event_loop = Thread(name=self._name, target=self._start)
|
||||
self._event_loop.daemon = True
|
||||
|
||||
def _start(self):
|
||||
"""
|
||||
Starts the backend event loop
|
||||
:return:
|
||||
"""
|
||||
self._start_loop(*self._args, **self._kwargs)
|
||||
|
||||
def start(self):
|
||||
"""
|
||||
Starts the backend event loop
|
||||
:return:
|
||||
"""
|
||||
LOGGER.info("Starting Event Loop: 'PyModbus_{}'".format(self._name))
|
||||
self._event_loop.start()
|
||||
|
||||
def stop(self):
|
||||
"""
|
||||
Stops the backend event loop
|
||||
:return:
|
||||
"""
|
||||
LOGGER.info("Stopping Event Loop: 'PyModbus_{}'".format(self._name))
|
||||
self._stop_loop()
|
||||
52
modbus/pymodbus/client/asynchronous/tls.py
Normal file
@@ -0,0 +1,52 @@
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
|
||||
import logging
|
||||
from pymodbus.client.asynchronous.factory.tls import get_factory
|
||||
from pymodbus.constants import Defaults
|
||||
from pymodbus.compat import IS_PYTHON3, PYTHON_VERSION
|
||||
from pymodbus.client.asynchronous.schedulers import ASYNC_IO
|
||||
from pymodbus.factory import ClientDecoder
|
||||
from pymodbus.transaction import ModbusTlsFramer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AsyncModbusTLSClient(object):
|
||||
"""
|
||||
Actual Async TLS Client to be used.
|
||||
|
||||
To use do::
|
||||
|
||||
from pymodbus.client.asynchronous.tls import AsyncModbusTLSClient
|
||||
"""
|
||||
def __new__(cls, scheduler, host="127.0.0.1", port=Defaults.TLSPort,
|
||||
framer=None, sslctx=None, server_hostname=None,
|
||||
source_address=None, timeout=None, **kwargs):
|
||||
"""
|
||||
Scheduler to use:
|
||||
- async_io (asyncio)
|
||||
:param scheduler: Backend to use
|
||||
:param host: Host IP address
|
||||
:param port: Port
|
||||
:param framer: Modbus Framer to use
|
||||
:param sslctx: The SSLContext to use for TLS (default None and auto create)
|
||||
:param server_hostname: Target server's name matched for certificate
|
||||
:param source_address: source address specific to underlying backend
|
||||
:param timeout: Time out in seconds
|
||||
:param kwargs: Other extra args specific to Backend being used
|
||||
:return:
|
||||
"""
|
||||
if (not (IS_PYTHON3 and PYTHON_VERSION >= (3, 4))
|
||||
and scheduler == ASYNC_IO):
|
||||
logger.critical("ASYNCIO is supported only on python3")
|
||||
import sys
|
||||
sys.exit(1)
|
||||
framer = framer or ModbusTlsFramer(ClientDecoder())
|
||||
factory_class = get_factory(scheduler)
|
||||
yieldable = factory_class(host=host, port=port, sslctx=sslctx,
|
||||
server_hostname=server_hostname,
|
||||
framer=framer, source_address=source_address,
|
||||
timeout=timeout, **kwargs)
|
||||
return yieldable
|
||||
|
||||
498
modbus/pymodbus/client/asynchronous/tornado/__init__.py
Normal file
@@ -0,0 +1,498 @@
|
||||
"""
|
||||
Asynchronous framework adapter for tornado.
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import abc
|
||||
|
||||
import logging
|
||||
|
||||
import time
|
||||
import socket
|
||||
from serial import Serial
|
||||
from tornado import gen
|
||||
from tornado.concurrent import Future
|
||||
from tornado.ioloop import IOLoop
|
||||
from tornado.iostream import IOStream
|
||||
from tornado.iostream import BaseIOStream
|
||||
|
||||
from pymodbus.client.asynchronous.mixins import (AsyncModbusClientMixin,
|
||||
AsyncModbusSerialClientMixin)
|
||||
|
||||
from pymodbus.compat import byte2int
|
||||
from pymodbus.exceptions import (ConnectionException,
|
||||
ModbusIOException,
|
||||
TimeOutException)
|
||||
from pymodbus.utilities import (hexlify_packets,
|
||||
ModbusTransactionState)
|
||||
from pymodbus.constants import Defaults
|
||||
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BaseTornadoClient(AsyncModbusClientMixin):
|
||||
"""
|
||||
Base Tornado client
|
||||
"""
|
||||
stream = None
|
||||
io_loop = None
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""
|
||||
Initializes BaseTornadoClient.
|
||||
ioloop to be passed as part of kwargs ('ioloop')
|
||||
:param args:
|
||||
:param kwargs:
|
||||
"""
|
||||
self.io_loop = kwargs.pop("ioloop", None)
|
||||
super(BaseTornadoClient, self).__init__(*args, **kwargs)
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_socket(self):
|
||||
"""
|
||||
return instance of the socket to connect to
|
||||
"""
|
||||
|
||||
@gen.coroutine
|
||||
def connect(self):
|
||||
"""
|
||||
Connect to the socket identified by host and port
|
||||
|
||||
:returns: Future
|
||||
:rtype: tornado.concurrent.Future
|
||||
"""
|
||||
conn = self.get_socket()
|
||||
self.stream = IOStream(conn, io_loop=self.io_loop or IOLoop.current())
|
||||
self.stream.connect((self.host, self.port))
|
||||
self.stream.read_until_close(None,
|
||||
streaming_callback=self.on_receive)
|
||||
self._connected = True
|
||||
LOGGER.debug("Client connected")
|
||||
|
||||
raise gen.Return(self)
|
||||
|
||||
def on_receive(self, *args):
|
||||
"""
|
||||
On data recieve call back
|
||||
:param args: data received
|
||||
:return:
|
||||
"""
|
||||
data = args[0] if len(args) > 0 else None
|
||||
|
||||
if not data:
|
||||
return
|
||||
LOGGER.debug("recv: " + hexlify_packets(data))
|
||||
unit = self.framer.decode_data(data).get("unit", 0)
|
||||
self.framer.processIncomingPacket(data, self._handle_response, unit=unit)
|
||||
|
||||
def execute(self, request=None):
|
||||
"""
|
||||
Executes a transaction
|
||||
:param request:
|
||||
:return:
|
||||
"""
|
||||
request.transaction_id = self.transaction.getNextTID()
|
||||
packet = self.framer.buildPacket(request)
|
||||
LOGGER.debug("send: " + hexlify_packets(packet))
|
||||
self.stream.write(packet)
|
||||
return self._build_response(request.transaction_id)
|
||||
|
||||
def _handle_response(self, reply, **kwargs):
|
||||
"""
|
||||
Handle response received
|
||||
:param reply:
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
if reply is not None:
|
||||
tid = reply.transaction_id
|
||||
future = self.transaction.getTransaction(tid)
|
||||
if future:
|
||||
future.set_result(reply)
|
||||
else:
|
||||
LOGGER.debug("Unrequested message: {}".format(reply))
|
||||
|
||||
def _build_response(self, tid):
|
||||
"""
|
||||
Builds a future response
|
||||
:param tid:
|
||||
:return:
|
||||
"""
|
||||
f = Future()
|
||||
|
||||
if not self._connected:
|
||||
f.set_exception(ConnectionException("Client is not connected"))
|
||||
return f
|
||||
|
||||
self.transaction.addTransaction(f, tid)
|
||||
return f
|
||||
|
||||
def close(self):
|
||||
"""
|
||||
Closes the underlying IOStream
|
||||
"""
|
||||
LOGGER.debug("Client disconnected")
|
||||
if self.stream:
|
||||
self.stream.close_fd()
|
||||
|
||||
self.stream = None
|
||||
self._connected = False
|
||||
|
||||
|
||||
class BaseTornadoSerialClient(AsyncModbusSerialClientMixin):
|
||||
"""
|
||||
Base Tonado serial client
|
||||
"""
|
||||
stream = None
|
||||
io_loop = None
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""
|
||||
Initializes BaseTornadoSerialClient.
|
||||
ioloop to be passed as part of kwargs ('ioloop')
|
||||
:param args:
|
||||
:param kwargs:
|
||||
"""
|
||||
self.io_loop = kwargs.pop("ioloop", None)
|
||||
super(BaseTornadoSerialClient, self).__init__(*args, **kwargs)
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_socket(self):
|
||||
"""
|
||||
return instance of the socket to connect to
|
||||
"""
|
||||
|
||||
def on_receive(self, *args):
|
||||
# Will be handled ine execute method
|
||||
pass
|
||||
|
||||
def execute(self, request=None):
|
||||
"""
|
||||
Executes a transaction
|
||||
:param request: Request to be written on to the bus
|
||||
:return:
|
||||
"""
|
||||
request.transaction_id = self.transaction.getNextTID()
|
||||
|
||||
def callback(*args):
|
||||
LOGGER.debug("in callback - {}".format(request.transaction_id))
|
||||
while True:
|
||||
waiting = self.stream.connection.in_waiting
|
||||
if waiting:
|
||||
data = self.stream.connection.read(waiting)
|
||||
LOGGER.debug(
|
||||
"recv: " + hexlify_packets(data))
|
||||
unit = self.framer.decode_data(data).get("uid", 0)
|
||||
self.framer.processIncomingPacket(
|
||||
data,
|
||||
self._handle_response,
|
||||
unit,
|
||||
tid=request.transaction_id
|
||||
)
|
||||
break
|
||||
|
||||
packet = self.framer.buildPacket(request)
|
||||
LOGGER.debug("send: " + hexlify_packets(packet))
|
||||
self.stream.write(packet, callback=callback)
|
||||
f = self._build_response(request.transaction_id)
|
||||
return f
|
||||
|
||||
def _handle_response(self, reply, **kwargs):
|
||||
"""
|
||||
Handles a received response and updates a future
|
||||
:param reply: Reply received
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
if reply is not None:
|
||||
tid = reply.transaction_id
|
||||
future = self.transaction.getTransaction(tid)
|
||||
if future:
|
||||
future.set_result(reply)
|
||||
else:
|
||||
LOGGER.debug("Unrequested message: {}".format(reply))
|
||||
|
||||
def _build_response(self, tid):
|
||||
"""
|
||||
Prepare for a response, returns a future
|
||||
:param tid:
|
||||
:return: Future
|
||||
"""
|
||||
f = Future()
|
||||
|
||||
if not self._connected:
|
||||
f.set_exception(ConnectionException("Client is not connected"))
|
||||
return f
|
||||
|
||||
self.transaction.addTransaction(f, tid)
|
||||
return f
|
||||
|
||||
def close(self):
|
||||
"""
|
||||
Closes the underlying IOStream
|
||||
"""
|
||||
LOGGER.debug("Client disconnected")
|
||||
if self.stream:
|
||||
self.stream.close_fd()
|
||||
|
||||
self.stream = None
|
||||
self._connected = False
|
||||
|
||||
|
||||
class SerialIOStream(BaseIOStream):
|
||||
"""
|
||||
Serial IO Stream class to control and handle serial connections
|
||||
over tornado
|
||||
"""
|
||||
def __init__(self, connection, *args, **kwargs):
|
||||
"""
|
||||
Initializes Serial IO Stream
|
||||
:param connection: serial object
|
||||
:param args:
|
||||
:param kwargs:
|
||||
"""
|
||||
self.connection = connection
|
||||
super(SerialIOStream, self).__init__(*args, **kwargs)
|
||||
|
||||
def fileno(self):
|
||||
"""
|
||||
Returns serial fd
|
||||
:return:
|
||||
"""
|
||||
return self.connection.fileno()
|
||||
|
||||
def close_fd(self):
|
||||
"""
|
||||
Closes a serial Fd
|
||||
:return:
|
||||
"""
|
||||
if self.connection:
|
||||
self.connection.close()
|
||||
self.connection = None
|
||||
|
||||
def read_from_fd(self):
|
||||
"""
|
||||
Reads from a fd
|
||||
:return:
|
||||
"""
|
||||
try:
|
||||
chunk = self.connection.readline()
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
return chunk
|
||||
|
||||
def write_to_fd(self, data):
|
||||
"""
|
||||
Writes to a fd
|
||||
:param data:
|
||||
:return:
|
||||
"""
|
||||
try:
|
||||
return self.connection.write(data)
|
||||
except Exception as e:
|
||||
LOGGER.error(e)
|
||||
|
||||
|
||||
class AsyncModbusSerialClient(BaseTornadoSerialClient):
|
||||
"""
|
||||
Tornado based asynchronous serial client
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""
|
||||
Initializes AsyncModbusSerialClient.
|
||||
:param args:
|
||||
:param kwargs:
|
||||
"""
|
||||
self.state = ModbusTransactionState.IDLE
|
||||
self.timeout = kwargs.get('timeout', Defaults.Timeout)
|
||||
self.baudrate = kwargs.get('baudrate', Defaults.Baudrate)
|
||||
if self.baudrate > 19200:
|
||||
self.silent_interval = 1.75 / 1000 # ms
|
||||
else:
|
||||
self._t0 = float((1 + 8 + 2)) / self.baudrate
|
||||
self.silent_interval = 3.5 * self._t0
|
||||
self.silent_interval = round(self.silent_interval, 6)
|
||||
self.last_frame_end = 0.0
|
||||
super(AsyncModbusSerialClient, self).__init__(*args, **kwargs)
|
||||
|
||||
def get_socket(self):
|
||||
"""
|
||||
Creates Pyserial object
|
||||
:return: serial object
|
||||
"""
|
||||
return Serial(port=self.port, **self.serial_settings)
|
||||
|
||||
@gen.coroutine
|
||||
def connect(self):
|
||||
"""Connect to the socket identified by host and port
|
||||
|
||||
:returns: Future
|
||||
:rtype: tornado.concurrent.Future
|
||||
"""
|
||||
conn = self.get_socket()
|
||||
if self.io_loop is None:
|
||||
self.io_loop = IOLoop.current()
|
||||
try:
|
||||
self.stream = SerialIOStream(conn, io_loop=self.io_loop)
|
||||
except Exception as e:
|
||||
LOGGER.exception(e)
|
||||
|
||||
self._connected = True
|
||||
LOGGER.debug("Client connected")
|
||||
|
||||
raise gen.Return(self)
|
||||
|
||||
def execute(self, request):
|
||||
"""
|
||||
Executes a transaction
|
||||
:param request: Request to be written on to the bus
|
||||
:return:
|
||||
"""
|
||||
request.transaction_id = self.transaction.getNextTID()
|
||||
|
||||
def _clear_timer():
|
||||
"""
|
||||
Clear serial waiting timeout
|
||||
"""
|
||||
if self.timeout_handle:
|
||||
self.io_loop.remove_timeout(self.timeout_handle)
|
||||
self.timeout_handle = None
|
||||
|
||||
def _on_timeout():
|
||||
"""
|
||||
Got timeout while waiting data from serial port
|
||||
"""
|
||||
LOGGER.warning("serial receive timeout")
|
||||
_clear_timer()
|
||||
if self.stream:
|
||||
self.io_loop.remove_handler(self.stream.fileno())
|
||||
self.framer.resetFrame()
|
||||
transaction = self.transaction.getTransaction(request.transaction_id)
|
||||
if transaction:
|
||||
transaction.set_exception(TimeOutException())
|
||||
|
||||
def _on_write_done(*args):
|
||||
"""
|
||||
Set up reader part after sucessful write to the serial
|
||||
"""
|
||||
LOGGER.debug("frame sent, waiting for a reply")
|
||||
self.last_frame_end = round(time.time(), 6)
|
||||
self.state = ModbusTransactionState.WAITING_FOR_REPLY
|
||||
self.io_loop.add_handler(self.stream.fileno(), _on_receive, IOLoop.READ)
|
||||
|
||||
def _on_fd_error(fd, *args):
|
||||
_clear_timer()
|
||||
self.io_loop.remove_handler(fd)
|
||||
self.close()
|
||||
self.transaction.getTransaction(request.transaction_id).set_exception(ModbusIOException(*args))
|
||||
|
||||
def _on_receive(fd, events):
|
||||
"""
|
||||
New data in serial buffer to read or serial port closed
|
||||
"""
|
||||
if events & IOLoop.ERROR:
|
||||
_on_fd_error(fd)
|
||||
return
|
||||
|
||||
try:
|
||||
waiting = self.stream.connection.in_waiting
|
||||
if waiting:
|
||||
data = self.stream.connection.read(waiting)
|
||||
LOGGER.debug(
|
||||
"recv: " + hexlify_packets(data))
|
||||
self.last_frame_end = round(time.time(), 6)
|
||||
except OSError as ex:
|
||||
_on_fd_error(fd, ex)
|
||||
return
|
||||
|
||||
self.framer.addToFrame(data)
|
||||
|
||||
# check if we have regular frame or modbus exception
|
||||
fcode = self.framer.decode_data(self.framer.getRawFrame()).get("fcode", 0)
|
||||
if fcode and (
|
||||
(fcode > 0x80 and len(self.framer.getRawFrame()) == exception_response_length)
|
||||
or
|
||||
(len(self.framer.getRawFrame()) == expected_response_length)
|
||||
):
|
||||
_clear_timer()
|
||||
self.io_loop.remove_handler(fd)
|
||||
self.state = ModbusTransactionState.IDLE
|
||||
self.framer.processIncomingPacket(
|
||||
b'', # already sent via addToFrame()
|
||||
self._handle_response,
|
||||
0, # don't care when `single=True`
|
||||
single=True,
|
||||
tid=request.transaction_id
|
||||
)
|
||||
|
||||
packet = self.framer.buildPacket(request)
|
||||
f = self._build_response(request.transaction_id)
|
||||
|
||||
response_pdu_size = request.get_response_pdu_size()
|
||||
expected_response_length = self.transaction._calculate_response_length(response_pdu_size)
|
||||
LOGGER.debug("expected_response_length = %d", expected_response_length)
|
||||
|
||||
exception_response_length = self.transaction._calculate_exception_length() # TODO: calculate once
|
||||
|
||||
if self.timeout:
|
||||
self.timeout_handle = self.io_loop.add_timeout(time.time() + self.timeout, _on_timeout)
|
||||
self._sendPacket(packet, callback=_on_write_done)
|
||||
|
||||
return f
|
||||
|
||||
def _sendPacket(self, message, callback):
|
||||
"""
|
||||
Sends packets on the bus with 3.5char delay between frames
|
||||
:param message: Message to be sent over the bus
|
||||
:return:
|
||||
"""
|
||||
@gen.coroutine
|
||||
def sleep(timeout):
|
||||
yield gen.sleep(timeout)
|
||||
|
||||
try:
|
||||
waiting = self.stream.connection.in_waiting
|
||||
if waiting:
|
||||
result = self.stream.connection.read(waiting)
|
||||
LOGGER.info(
|
||||
"Cleanup recv buffer before send: " + hexlify_packets(result))
|
||||
except OSError as e:
|
||||
self.transaction.getTransaction(
|
||||
message.transaction_id).set_exception(ModbusIOException(e))
|
||||
return
|
||||
|
||||
start = time.time()
|
||||
if self.last_frame_end:
|
||||
waittime = self.last_frame_end + self.silent_interval - start
|
||||
if waittime > 0:
|
||||
LOGGER.debug("Waiting for 3.5 char before next send - %f ms", waittime)
|
||||
sleep(waittime)
|
||||
|
||||
self.state = ModbusTransactionState.SENDING
|
||||
LOGGER.debug("send: " + hexlify_packets(message))
|
||||
self.stream.write(message, callback)
|
||||
|
||||
class AsyncModbusTCPClient(BaseTornadoClient):
|
||||
"""
|
||||
Tornado based Async tcp client
|
||||
"""
|
||||
def get_socket(self):
|
||||
"""
|
||||
Creates socket object
|
||||
:return: socket
|
||||
"""
|
||||
return socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
|
||||
|
||||
|
||||
class AsyncModbusUDPClient(BaseTornadoClient):
|
||||
"""
|
||||
Tornado based Async UDP client
|
||||
"""
|
||||
def get_socket(self):
|
||||
"""
|
||||
Create socket object
|
||||
:return: socket
|
||||
"""
|
||||
return socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM)
|
||||
251
modbus/pymodbus/client/asynchronous/twisted/__init__.py
Normal file
@@ -0,0 +1,251 @@
|
||||
"""
|
||||
Implementation of a Modbus Client Using Twisted
|
||||
--------------------------------------------------
|
||||
|
||||
Example run::
|
||||
|
||||
from twisted.internet import reactor, protocol
|
||||
from pymodbus.client.asynchronous import ModbusClientProtocol
|
||||
|
||||
def printResult(result):
|
||||
print "Result: %d" % result.bits[0]
|
||||
|
||||
def process(client):
|
||||
result = client.write_coil(1, True)
|
||||
result.addCallback(printResult)
|
||||
reactor.callLater(1, reactor.stop)
|
||||
|
||||
defer = protocol.ClientCreator(reactor, ModbusClientProtocol
|
||||
).connectTCP("localhost", 502)
|
||||
defer.addCallback(process)
|
||||
|
||||
Another example::
|
||||
|
||||
from twisted.internet import reactor
|
||||
from pymodbus.client.asynchronous import ModbusClientFactory
|
||||
|
||||
def process():
|
||||
factory = reactor.connectTCP("localhost", 502, ModbusClientFactory())
|
||||
reactor.stop()
|
||||
|
||||
if __name__ == "__main__":
|
||||
reactor.callLater(1, process)
|
||||
reactor.run()
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
from twisted.internet import defer, protocol
|
||||
|
||||
from pymodbus.exceptions import ConnectionException
|
||||
from pymodbus.factory import ClientDecoder
|
||||
from pymodbus.client.asynchronous.mixins import AsyncModbusClientMixin
|
||||
from pymodbus.transaction import FifoTransactionManager, DictTransactionManager
|
||||
from pymodbus.transaction import ModbusSocketFramer, ModbusRtuFramer
|
||||
from pymodbus.utilities import hexlify_packets
|
||||
from twisted.python.failure import Failure
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Logging
|
||||
# --------------------------------------------------------------------------- #
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Connected Client Protocols
|
||||
# --------------------------------------------------------------------------- #
|
||||
class ModbusClientProtocol(protocol.Protocol,
|
||||
AsyncModbusClientMixin):
|
||||
"""
|
||||
This represents the base modbus client protocol. All the application
|
||||
layer code is deferred to a higher level wrapper.
|
||||
"""
|
||||
framer = None
|
||||
|
||||
def __init__(self, framer=None, **kwargs):
|
||||
self._connected = False
|
||||
self.framer = framer or ModbusSocketFramer(ClientDecoder())
|
||||
if isinstance(self.framer, type):
|
||||
# Framer class not instance
|
||||
self.framer = self.framer(ClientDecoder(), client=None)
|
||||
if isinstance(self.framer, ModbusSocketFramer):
|
||||
self.transaction = DictTransactionManager(self, **kwargs)
|
||||
else:
|
||||
self.transaction = FifoTransactionManager(self, **kwargs)
|
||||
|
||||
def connectionMade(self):
|
||||
"""
|
||||
Called upon a successful client connection.
|
||||
"""
|
||||
_logger.debug("Client connected to modbus server")
|
||||
self._connected = True
|
||||
|
||||
def connectionLost(self, reason=None):
|
||||
"""
|
||||
Called upon a client disconnect
|
||||
|
||||
:param reason: The reason for the disconnect
|
||||
"""
|
||||
_logger.debug("Client disconnected from modbus server: %s" % reason)
|
||||
self._connected = False
|
||||
for tid in list(self.transaction):
|
||||
self.transaction.getTransaction(tid).errback(Failure(
|
||||
ConnectionException('Connection lost during request')))
|
||||
|
||||
def dataReceived(self, data):
|
||||
"""
|
||||
Get response, check for valid message, decode result
|
||||
|
||||
:param data: The data returned from the server
|
||||
"""
|
||||
unit = self.framer.decode_data(data).get("unit", 0)
|
||||
self.framer.processIncomingPacket(data, self._handleResponse,
|
||||
unit=unit)
|
||||
|
||||
def execute(self, request):
|
||||
"""
|
||||
Starts the producer to send the next request to
|
||||
consumer.write(Frame(request))
|
||||
"""
|
||||
request.transaction_id = self.transaction.getNextTID()
|
||||
packet = self.framer.buildPacket(request)
|
||||
_logger.debug("send: " + hexlify_packets(packet))
|
||||
self.transport.write(packet)
|
||||
return self._buildResponse(request.transaction_id)
|
||||
|
||||
def _handleResponse(self, reply, **kwargs):
|
||||
"""
|
||||
Handle the processed response and link to correct deferred
|
||||
|
||||
:param reply: The reply to process
|
||||
"""
|
||||
if reply is not None:
|
||||
tid = reply.transaction_id
|
||||
handler = self.transaction.getTransaction(tid)
|
||||
if handler:
|
||||
handler.callback(reply)
|
||||
else:
|
||||
_logger.debug("Unrequested message: " + str(reply))
|
||||
|
||||
def _buildResponse(self, tid):
|
||||
"""
|
||||
Helper method to return a deferred response
|
||||
for the current request.
|
||||
|
||||
:param tid: The transaction identifier for this response
|
||||
:returns: A defer linked to the latest request
|
||||
"""
|
||||
if not self._connected:
|
||||
return defer.fail(Failure(
|
||||
ConnectionException('Client is not connected')))
|
||||
|
||||
d = defer.Deferred()
|
||||
self.transaction.addTransaction(d, tid)
|
||||
return d
|
||||
|
||||
def close(self):
|
||||
"""
|
||||
Closes underlying transport layer ,essentially closing the client
|
||||
:return:
|
||||
"""
|
||||
if self.transport and hasattr(self.transport, "close"):
|
||||
self.transport.close()
|
||||
self._connected = False
|
||||
|
||||
|
||||
class ModbusTcpClientProtocol(ModbusClientProtocol):
|
||||
"""
|
||||
Async TCP Client protocol based on twisted.
|
||||
|
||||
Default framer: ModbusSocketFramer
|
||||
"""
|
||||
framer = ModbusSocketFramer(ClientDecoder())
|
||||
|
||||
|
||||
class ModbusSerClientProtocol(ModbusClientProtocol):
|
||||
"""
|
||||
Async Serial Client protocol based on twisted
|
||||
|
||||
Default framer: ModbusRtuFramer
|
||||
"""
|
||||
def __init__(self, framer=None, **kwargs):
|
||||
framer = framer or ModbusRtuFramer(ClientDecoder())
|
||||
super(ModbusSerClientProtocol, self).__init__(framer, **kwargs)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Not Connected Client Protocol
|
||||
# --------------------------------------------------------------------------- #
|
||||
class ModbusUdpClientProtocol(protocol.DatagramProtocol,
|
||||
AsyncModbusClientMixin):
|
||||
"""
|
||||
This represents the base modbus client protocol. All the application
|
||||
layer code is deferred to a higher level wrapper.
|
||||
"""
|
||||
|
||||
def datagramReceived(self, data, params):
|
||||
"""
|
||||
Get response, check for valid message, decode result
|
||||
|
||||
:param data: The data returned from the server
|
||||
:param params: The host parameters sending the datagram
|
||||
"""
|
||||
_logger.debug("Datagram from: %s:%d" % params)
|
||||
unit = self.framer.decode_data(data).get("uid", 0)
|
||||
self.framer.processIncomingPacket(data, self._handleResponse, unit=unit)
|
||||
|
||||
def execute(self, request):
|
||||
"""
|
||||
Starts the producer to send the next request to
|
||||
consumer.write(Frame(request))
|
||||
"""
|
||||
request.transaction_id = self.transaction.getNextTID()
|
||||
packet = self.framer.buildPacket(request)
|
||||
self.transport.write(packet, (self.host, self.port))
|
||||
return self._buildResponse(request.transaction_id)
|
||||
|
||||
def _handleResponse(self, reply, **kwargs):
|
||||
"""
|
||||
Handle the processed response and link to correct deferred
|
||||
|
||||
:param reply: The reply to process
|
||||
"""
|
||||
if reply is not None:
|
||||
tid = reply.transaction_id
|
||||
handler = self.transaction.getTransaction(tid)
|
||||
if handler:
|
||||
handler.callback(reply)
|
||||
else: _logger.debug("Unrequested message: " + str(reply))
|
||||
|
||||
def _buildResponse(self, tid):
|
||||
"""
|
||||
Helper method to return a deferred response
|
||||
for the current request.
|
||||
|
||||
:param tid: The transaction identifier for this response
|
||||
:returns: A defer linked to the latest request
|
||||
"""
|
||||
d = defer.Deferred()
|
||||
self.transaction.addTransaction(d, tid)
|
||||
return d
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Client Factories
|
||||
# --------------------------------------------------------------------------- #
|
||||
class ModbusClientFactory(protocol.ReconnectingClientFactory):
|
||||
""" Simple client protocol factory """
|
||||
|
||||
protocol = ModbusClientProtocol
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Exported symbols
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
__all__ = [
|
||||
"ModbusClientProtocol",
|
||||
"ModbusUdpClientProtocol",
|
||||
"ModbusClientFactory"
|
||||
]
|
||||
|
||||
47
modbus/pymodbus/client/asynchronous/udp.py
Normal file
@@ -0,0 +1,47 @@
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
|
||||
import logging
|
||||
from pymodbus.constants import Defaults
|
||||
from pymodbus.compat import IS_PYTHON3, PYTHON_VERSION
|
||||
from pymodbus.client.asynchronous.schedulers import ASYNC_IO
|
||||
from pymodbus.client.asynchronous.factory.udp import get_factory
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AsyncModbusUDPClient(object):
|
||||
"""
|
||||
Actual Async UDP Client to be used.
|
||||
|
||||
To use do::
|
||||
|
||||
from pymodbus.client.asynchronous.tcp import AsyncModbusUDPClient
|
||||
"""
|
||||
def __new__(cls, scheduler, host="127.0.0.1", port=Defaults.Port,
|
||||
framer=None, source_address=None, timeout=None, **kwargs):
|
||||
"""
|
||||
Scheduler to use:
|
||||
- reactor (Twisted)
|
||||
- io_loop (Tornado)
|
||||
- async_io (asyncio)
|
||||
:param scheduler: Backend to use
|
||||
:param host: Host IP address
|
||||
:param port: Port
|
||||
:param framer: Modbus Framer to use
|
||||
:param source_address: source address specific to underlying backend
|
||||
:param timeout: Time out in seconds
|
||||
:param kwargs: Other extra args specific to Backend being used
|
||||
:return:
|
||||
"""
|
||||
if (not (IS_PYTHON3 and PYTHON_VERSION >= (3, 4))
|
||||
and scheduler == ASYNC_IO):
|
||||
logger.critical("ASYNCIO is supported only on python3")
|
||||
import sys
|
||||
sys.exit(1)
|
||||
factory_class = get_factory(scheduler)
|
||||
yieldable = factory_class(host=host, port=port, framer=framer,
|
||||
source_address=source_address,
|
||||
timeout=timeout, **kwargs)
|
||||
return yieldable
|
||||
|
||||
155
modbus/pymodbus/client/common.py
Normal file
@@ -0,0 +1,155 @@
|
||||
'''
|
||||
Modbus Client Common
|
||||
----------------------------------
|
||||
|
||||
This is a common client mixin that can be used by
|
||||
both the synchronous and asynchronous clients to
|
||||
simplify the interface.
|
||||
'''
|
||||
from pymodbus.bit_read_message import *
|
||||
from pymodbus.bit_write_message import *
|
||||
from pymodbus.register_read_message import *
|
||||
from pymodbus.register_write_message import *
|
||||
from pymodbus.diag_message import *
|
||||
from pymodbus.file_message import *
|
||||
from pymodbus.other_message import *
|
||||
|
||||
from pymodbus.utilities import ModbusTransactionState
|
||||
|
||||
|
||||
class ModbusClientMixin(object):
|
||||
'''
|
||||
This is a modbus client mixin that provides additional factory
|
||||
methods for all the current modbus methods. This can be used
|
||||
instead of the normal pattern of::
|
||||
|
||||
# instead of this
|
||||
client = ModbusClient(...)
|
||||
request = ReadCoilsRequest(1,10)
|
||||
response = client.execute(request)
|
||||
|
||||
# now like this
|
||||
client = ModbusClient(...)
|
||||
response = client.read_coils(1, 10)
|
||||
'''
|
||||
state = ModbusTransactionState.IDLE
|
||||
last_frame_end = 0
|
||||
silent_interval = 0
|
||||
|
||||
def read_coils(self, address, count=1, **kwargs):
|
||||
'''
|
||||
|
||||
:param address: The starting address to read from
|
||||
:param count: The number of coils to read
|
||||
:param unit: The slave unit this request is targeting
|
||||
:returns: A deferred response handle
|
||||
'''
|
||||
request = ReadCoilsRequest(address, count, **kwargs)
|
||||
return self.execute(request)
|
||||
|
||||
def read_discrete_inputs(self, address, count=1, **kwargs):
|
||||
'''
|
||||
|
||||
:param address: The starting address to read from
|
||||
:param count: The number of discretes to read
|
||||
:param unit: The slave unit this request is targeting
|
||||
:returns: A deferred response handle
|
||||
'''
|
||||
request = ReadDiscreteInputsRequest(address, count, **kwargs)
|
||||
return self.execute(request)
|
||||
|
||||
def write_coil(self, address, value, **kwargs):
|
||||
'''
|
||||
|
||||
:param address: The starting address to write to
|
||||
:param value: The value to write to the specified address
|
||||
:param unit: The slave unit this request is targeting
|
||||
:returns: A deferred response handle
|
||||
'''
|
||||
request = WriteSingleCoilRequest(address, value, **kwargs)
|
||||
return self.execute(request)
|
||||
|
||||
def write_coils(self, address, values, **kwargs):
|
||||
'''
|
||||
|
||||
:param address: The starting address to write to
|
||||
:param values: The values to write to the specified address
|
||||
:param unit: The slave unit this request is targeting
|
||||
:returns: A deferred response handle
|
||||
'''
|
||||
request = WriteMultipleCoilsRequest(address, values, **kwargs)
|
||||
return self.execute(request)
|
||||
|
||||
def write_register(self, address, value, **kwargs):
|
||||
'''
|
||||
|
||||
:param address: The starting address to write to
|
||||
:param value: The value to write to the specified address
|
||||
:param unit: The slave unit this request is targeting
|
||||
:returns: A deferred response handle
|
||||
'''
|
||||
request = WriteSingleRegisterRequest(address, value, **kwargs)
|
||||
return self.execute(request)
|
||||
|
||||
def write_registers(self, address, values, **kwargs):
|
||||
'''
|
||||
|
||||
:param address: The starting address to write to
|
||||
:param values: The values to write to the specified address
|
||||
:param unit: The slave unit this request is targeting
|
||||
:returns: A deferred response handle
|
||||
'''
|
||||
request = WriteMultipleRegistersRequest(address, values, **kwargs)
|
||||
return self.execute(request)
|
||||
|
||||
def read_holding_registers(self, address, count=1, **kwargs):
|
||||
'''
|
||||
|
||||
:param address: The starting address to read from
|
||||
:param count: The number of registers to read
|
||||
:param unit: The slave unit this request is targeting
|
||||
:returns: A deferred response handle
|
||||
'''
|
||||
request = ReadHoldingRegistersRequest(address, count, **kwargs)
|
||||
return self.execute(request)
|
||||
|
||||
def read_input_registers(self, address, count=1, **kwargs):
|
||||
'''
|
||||
|
||||
:param address: The starting address to read from
|
||||
:param count: The number of registers to read
|
||||
:param unit: The slave unit this request is targeting
|
||||
:returns: A deferred response handle
|
||||
'''
|
||||
request = ReadInputRegistersRequest(address, count, **kwargs)
|
||||
return self.execute(request)
|
||||
|
||||
def readwrite_registers(self, *args, **kwargs):
|
||||
'''
|
||||
|
||||
:param read_address: The address to start reading from
|
||||
:param read_count: The number of registers to read from address
|
||||
:param write_address: The address to start writing to
|
||||
:param write_registers: The registers to write to the specified address
|
||||
:param unit: The slave unit this request is targeting
|
||||
:returns: A deferred response handle
|
||||
'''
|
||||
request = ReadWriteMultipleRegistersRequest(*args, **kwargs)
|
||||
return self.execute(request)
|
||||
|
||||
def mask_write_register(self, *args, **kwargs):
|
||||
'''
|
||||
|
||||
:param address: The address of the register to write
|
||||
:param and_mask: The and bitmask to apply to the register address
|
||||
:param or_mask: The or bitmask to apply to the register address
|
||||
:param unit: The slave unit this request is targeting
|
||||
:returns: A deferred response handle
|
||||
'''
|
||||
request = MaskWriteRegisterRequest(*args, **kwargs)
|
||||
return self.execute(request)
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Exported symbols
|
||||
#---------------------------------------------------------------------------#
|
||||
__all__ = [ 'ModbusClientMixin' ]
|
||||
779
modbus/pymodbus/client/sync.py
Normal file
@@ -0,0 +1,779 @@
|
||||
import socket
|
||||
import select
|
||||
import serial
|
||||
import time
|
||||
import ssl
|
||||
import sys
|
||||
from functools import partial
|
||||
from pymodbus.constants import Defaults
|
||||
from pymodbus.utilities import hexlify_packets, ModbusTransactionState
|
||||
from pymodbus.factory import ClientDecoder
|
||||
from pymodbus.exceptions import NotImplementedException, ParameterException
|
||||
from pymodbus.exceptions import ConnectionException
|
||||
from pymodbus.transaction import FifoTransactionManager
|
||||
from pymodbus.transaction import DictTransactionManager
|
||||
from pymodbus.transaction import ModbusSocketFramer, ModbusBinaryFramer
|
||||
from pymodbus.transaction import ModbusAsciiFramer, ModbusRtuFramer
|
||||
from pymodbus.transaction import ModbusTlsFramer
|
||||
from pymodbus.client.common import ModbusClientMixin
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Logging
|
||||
# --------------------------------------------------------------------------- #
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# The Synchronous Clients
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
class BaseModbusClient(ModbusClientMixin):
|
||||
"""
|
||||
Inteface for a modbus synchronous client. Defined here are all the
|
||||
methods for performing the related request methods. Derived classes
|
||||
simply need to implement the transport methods and set the correct
|
||||
framer.
|
||||
"""
|
||||
def __init__(self, framer, **kwargs):
|
||||
""" Initialize a client instance
|
||||
|
||||
:param framer: The modbus framer implementation to use
|
||||
"""
|
||||
self.framer = framer
|
||||
self.transaction = DictTransactionManager(self, **kwargs)
|
||||
self._debug = False
|
||||
self._debugfd = None
|
||||
self.broadcast_enable = kwargs.get('broadcast_enable', Defaults.broadcast_enable)
|
||||
|
||||
# ----------------------------------------------------------------------- #
|
||||
# Client interface
|
||||
# ----------------------------------------------------------------------- #
|
||||
def connect(self):
|
||||
""" Connect to the modbus remote host
|
||||
|
||||
:returns: True if connection succeeded, False otherwise
|
||||
"""
|
||||
raise NotImplementedException("Method not implemented by derived class")
|
||||
|
||||
def close(self):
|
||||
""" Closes the underlying socket connection
|
||||
"""
|
||||
pass
|
||||
|
||||
def is_socket_open(self):
|
||||
"""
|
||||
Check whether the underlying socket/serial is open or not.
|
||||
|
||||
:returns: True if socket/serial is open, False otherwise
|
||||
"""
|
||||
raise NotImplementedException(
|
||||
"is_socket_open() not implemented by {}".format(self.__str__())
|
||||
)
|
||||
|
||||
def send(self, request):
|
||||
if self.state != ModbusTransactionState.RETRYING:
|
||||
_logger.debug("New Transaction state 'SENDING'")
|
||||
self.state = ModbusTransactionState.SENDING
|
||||
return self._send(request)
|
||||
|
||||
def _send(self, request):
|
||||
""" Sends data on the underlying socket
|
||||
|
||||
:param request: The encoded request to send
|
||||
:return: The number of bytes written
|
||||
"""
|
||||
raise NotImplementedException("Method not implemented by derived class")
|
||||
|
||||
def recv(self, size):
|
||||
return self._recv(size)
|
||||
|
||||
def _recv(self, size):
|
||||
""" Reads data from the underlying descriptor
|
||||
|
||||
:param size: The number of bytes to read
|
||||
:return: The bytes read
|
||||
"""
|
||||
raise NotImplementedException("Method not implemented by derived class")
|
||||
|
||||
# ----------------------------------------------------------------------- #
|
||||
# Modbus client methods
|
||||
# ----------------------------------------------------------------------- #
|
||||
def execute(self, request=None):
|
||||
"""
|
||||
:param request: The request to process
|
||||
:returns: The result of the request execution
|
||||
"""
|
||||
if not self.connect():
|
||||
raise ConnectionException("Failed to connect[%s]" % (self.__str__()))
|
||||
return self.transaction.execute(request)
|
||||
|
||||
# ----------------------------------------------------------------------- #
|
||||
# The magic methods
|
||||
# ----------------------------------------------------------------------- #
|
||||
def __enter__(self):
|
||||
""" Implement the client with enter block
|
||||
|
||||
:returns: The current instance of the client
|
||||
"""
|
||||
if not self.connect():
|
||||
raise ConnectionException("Failed to connect[%s]" % (self.__str__()))
|
||||
return self
|
||||
|
||||
def __exit__(self, klass, value, traceback):
|
||||
""" Implement the client with exit block """
|
||||
self.close()
|
||||
|
||||
def idle_time(self):
|
||||
"""
|
||||
Bus Idle Time to initiate next transaction
|
||||
:return: time stamp
|
||||
"""
|
||||
if self.last_frame_end is None or self.silent_interval is None:
|
||||
return 0
|
||||
return self.last_frame_end + self.silent_interval
|
||||
|
||||
def debug_enabled(self):
|
||||
"""
|
||||
Returns a boolean indicating if debug is enabled.
|
||||
"""
|
||||
return self._debug
|
||||
|
||||
def set_debug(self, debug):
|
||||
"""
|
||||
Sets the current debug flag.
|
||||
"""
|
||||
self._debug = debug
|
||||
|
||||
def trace(self, writeable):
|
||||
if writeable:
|
||||
self.set_debug(True)
|
||||
self._debugfd = writeable
|
||||
|
||||
def _dump(self, data, direction):
|
||||
fd = self._debugfd if self._debugfd else sys.stdout
|
||||
try:
|
||||
fd.write(hexlify_packets(data))
|
||||
except Exception as e:
|
||||
_logger.debug(hexlify_packets(data))
|
||||
_logger.exception(e)
|
||||
|
||||
def register(self, function):
|
||||
"""
|
||||
Registers a function and sub function class with the decoder
|
||||
:param function: Custom function class to register
|
||||
:return:
|
||||
"""
|
||||
self.framer.decoder.register(function)
|
||||
|
||||
def __str__(self):
|
||||
""" Builds a string representation of the connection
|
||||
|
||||
:returns: The string representation
|
||||
"""
|
||||
return "Null Transport"
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Modbus TCP Client Transport Implementation
|
||||
# --------------------------------------------------------------------------- #
|
||||
class ModbusTcpClient(BaseModbusClient):
|
||||
""" Implementation of a modbus tcp client
|
||||
"""
|
||||
|
||||
def __init__(self, host='127.0.0.1', port=Defaults.Port,
|
||||
framer=ModbusSocketFramer, **kwargs):
|
||||
""" Initialize a client instance
|
||||
|
||||
:param host: The host to connect to (default 127.0.0.1)
|
||||
:param port: The modbus port to connect to (default 502)
|
||||
:param source_address: The source address tuple to bind to (default ('', 0))
|
||||
:param timeout: The timeout to use for this socket (default Defaults.Timeout)
|
||||
:param framer: The modbus framer to use (default ModbusSocketFramer)
|
||||
|
||||
.. note:: The host argument will accept ipv4 and ipv6 hosts
|
||||
"""
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.source_address = kwargs.get('source_address', ('', 0))
|
||||
self.socket = None
|
||||
self.timeout = kwargs.get('timeout', Defaults.Timeout)
|
||||
BaseModbusClient.__init__(self, framer(ClientDecoder(), self), **kwargs)
|
||||
|
||||
def connect(self):
|
||||
""" Connect to the modbus tcp server
|
||||
|
||||
:returns: True if connection succeeded, False otherwise
|
||||
"""
|
||||
if self.socket:
|
||||
return True
|
||||
try:
|
||||
self.socket = socket.create_connection(
|
||||
(self.host, self.port),
|
||||
timeout=self.timeout,
|
||||
source_address=self.source_address)
|
||||
_logger.debug("Connection to Modbus server established. "
|
||||
"Socket {}".format(self.socket.getsockname()))
|
||||
except socket.error as msg:
|
||||
_logger.error('Connection to (%s, %s) '
|
||||
'failed: %s' % (self.host, self.port, msg))
|
||||
self.close()
|
||||
return self.socket is not None
|
||||
|
||||
def close(self):
|
||||
""" Closes the underlying socket connection
|
||||
"""
|
||||
if self.socket:
|
||||
self.socket.close()
|
||||
self.socket = None
|
||||
|
||||
def _check_read_buffer(self, recv_size=None):
|
||||
time_ = time.time()
|
||||
end = time_ + self.timeout
|
||||
data = None
|
||||
ready = select.select([self.socket], [], [], end - time_)
|
||||
if ready[0]:
|
||||
data = self.socket.recv(1024)
|
||||
return data
|
||||
|
||||
def _send(self, request):
|
||||
""" Sends data on the underlying socket
|
||||
|
||||
:param request: The encoded request to send
|
||||
:return: The number of bytes written
|
||||
"""
|
||||
if not self.socket:
|
||||
raise ConnectionException(self.__str__())
|
||||
if self.state == ModbusTransactionState.RETRYING:
|
||||
data = self._check_read_buffer()
|
||||
if data:
|
||||
return data
|
||||
|
||||
if request:
|
||||
return self.socket.send(request)
|
||||
return 0
|
||||
|
||||
def _recv(self, size):
|
||||
""" Reads data from the underlying descriptor
|
||||
|
||||
:param size: The number of bytes to read
|
||||
:return: The bytes read if the peer sent a response, or a zero-length
|
||||
response if no data packets were received from the client at
|
||||
all.
|
||||
:raises: ConnectionException if the socket is not initialized, or the
|
||||
peer either has closed the connection before this method is
|
||||
invoked or closes it before sending any data before timeout.
|
||||
"""
|
||||
if not self.socket:
|
||||
raise ConnectionException(self.__str__())
|
||||
|
||||
# socket.recv(size) waits until it gets some data from the host but
|
||||
# not necessarily the entire response that can be fragmented in
|
||||
# many packets.
|
||||
# To avoid the splitted responses to be recognized as invalid
|
||||
# messages and to be discarded, loops socket.recv until full data
|
||||
# is received or timeout is expired.
|
||||
# If timeout expires returns the read data, also if its length is
|
||||
# less than the expected size.
|
||||
self.socket.setblocking(0)
|
||||
|
||||
timeout = self.timeout
|
||||
|
||||
# If size isn't specified read up to 4096 bytes at a time.
|
||||
if size is None:
|
||||
recv_size = 4096
|
||||
else:
|
||||
recv_size = size
|
||||
|
||||
data = []
|
||||
data_length = 0
|
||||
time_ = time.time()
|
||||
end = time_ + timeout
|
||||
while recv_size > 0:
|
||||
ready = select.select([self.socket], [], [], end - time_)
|
||||
if ready[0]:
|
||||
recv_data = self.socket.recv(recv_size)
|
||||
if recv_data == b'':
|
||||
return self._handle_abrupt_socket_close(
|
||||
size, data, time.time() - time_)
|
||||
data.append(recv_data)
|
||||
data_length += len(recv_data)
|
||||
time_ = time.time()
|
||||
|
||||
# If size isn't specified continue to read until timeout expires.
|
||||
if size:
|
||||
recv_size = size - data_length
|
||||
|
||||
# Timeout is reduced also if some data has been received in order
|
||||
# to avoid infinite loops when there isn't an expected response
|
||||
# size and the slave sends noisy data continuously.
|
||||
if time_ > end:
|
||||
break
|
||||
|
||||
return b"".join(data)
|
||||
|
||||
def _handle_abrupt_socket_close(self, size, data, duration):
|
||||
""" Handle unexpected socket close by remote end
|
||||
|
||||
Intended to be invoked after determining that the remote end
|
||||
has unexpectedly closed the connection, to clean up and handle
|
||||
the situation appropriately.
|
||||
|
||||
:param size: The number of bytes that was attempted to read
|
||||
:param data: The actual data returned
|
||||
:param duration: Duration from the read was first attempted
|
||||
until it was determined that the remote closed the
|
||||
socket
|
||||
:return: The more than zero bytes read from the remote end
|
||||
:raises: ConnectionException If the remote end didn't send any
|
||||
data at all before closing the connection.
|
||||
"""
|
||||
self.close()
|
||||
readsize = ("read of %s bytes" % size if size
|
||||
else "unbounded read")
|
||||
msg = ("%s: Connection unexpectedly closed "
|
||||
"%.6f seconds into %s" % (self, duration, readsize))
|
||||
if data:
|
||||
result = b"".join(data)
|
||||
msg += " after returning %s bytes" % len(result)
|
||||
_logger.warning(msg)
|
||||
return result
|
||||
msg += " without response from unit before it closed connection"
|
||||
raise ConnectionException(msg)
|
||||
|
||||
def is_socket_open(self):
|
||||
return True if self.socket is not None else False
|
||||
|
||||
def __str__(self):
|
||||
""" Builds a string representation of the connection
|
||||
|
||||
:returns: The string representation
|
||||
"""
|
||||
return "ModbusTcpClient(%s:%s)" % (self.host, self.port)
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
"<{} at {} socket={self.socket}, ipaddr={self.host}, "
|
||||
"port={self.port}, timeout={self.timeout}>"
|
||||
).format(self.__class__.__name__, hex(id(self)), self=self)
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Modbus TLS Client Transport Implementation
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
class ModbusTlsClient(ModbusTcpClient):
|
||||
""" Implementation of a modbus tls client
|
||||
"""
|
||||
|
||||
def __init__(self, host='localhost', port=Defaults.TLSPort, sslctx=None,
|
||||
framer=ModbusTlsFramer, **kwargs):
|
||||
""" Initialize a client instance
|
||||
|
||||
:param host: The host to connect to (default localhost)
|
||||
:param port: The modbus port to connect to (default 802)
|
||||
:param sslctx: The SSLContext to use for TLS (default None and auto create)
|
||||
:param source_address: The source address tuple to bind to (default ('', 0))
|
||||
:param timeout: The timeout to use for this socket (default Defaults.Timeout)
|
||||
:param framer: The modbus framer to use (default ModbusSocketFramer)
|
||||
|
||||
.. note:: The host argument will accept ipv4 and ipv6 hosts
|
||||
"""
|
||||
self.sslctx = sslctx
|
||||
if self.sslctx is None:
|
||||
self.sslctx = ssl.create_default_context()
|
||||
# According to MODBUS/TCP Security Protocol Specification, it is
|
||||
# TLSv2 at least
|
||||
self.sslctx.options |= ssl.OP_NO_TLSv1_1
|
||||
self.sslctx.options |= ssl.OP_NO_TLSv1
|
||||
self.sslctx.options |= ssl.OP_NO_SSLv3
|
||||
self.sslctx.options |= ssl.OP_NO_SSLv2
|
||||
ModbusTcpClient.__init__(self, host, port, framer, **kwargs)
|
||||
|
||||
def connect(self):
|
||||
""" Connect to the modbus tls server
|
||||
|
||||
:returns: True if connection succeeded, False otherwise
|
||||
"""
|
||||
if self.socket: return True
|
||||
try:
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.bind(self.source_address)
|
||||
self.socket = self.sslctx.wrap_socket(sock, server_side=False,
|
||||
server_hostname=self.host)
|
||||
self.socket.settimeout(self.timeout)
|
||||
self.socket.connect((self.host, self.port))
|
||||
except socket.error as msg:
|
||||
_logger.error('Connection to (%s, %s) '
|
||||
'failed: %s' % (self.host, self.port, msg))
|
||||
self.close()
|
||||
return self.socket is not None
|
||||
|
||||
def _recv(self, size):
|
||||
""" Reads data from the underlying descriptor
|
||||
|
||||
:param size: The number of bytes to read
|
||||
:return: The bytes read
|
||||
"""
|
||||
if not self.socket:
|
||||
raise ConnectionException(self.__str__())
|
||||
|
||||
# socket.recv(size) waits until it gets some data from the host but
|
||||
# not necessarily the entire response that can be fragmented in
|
||||
# many packets.
|
||||
# To avoid the splitted responses to be recognized as invalid
|
||||
# messages and to be discarded, loops socket.recv until full data
|
||||
# is received or timeout is expired.
|
||||
# If timeout expires returns the read data, also if its length is
|
||||
# less than the expected size.
|
||||
timeout = self.timeout
|
||||
|
||||
# If size isn't specified read 1 byte at a time.
|
||||
if size is None:
|
||||
recv_size = 1
|
||||
else:
|
||||
recv_size = size
|
||||
|
||||
data = b''
|
||||
time_ = time.time()
|
||||
end = time_ + timeout
|
||||
while recv_size > 0:
|
||||
data += self.socket.recv(recv_size)
|
||||
time_ = time.time()
|
||||
|
||||
# If size isn't specified continue to read until timeout expires.
|
||||
if size:
|
||||
recv_size = size - len(data)
|
||||
|
||||
# Timeout is reduced also if some data has been received in order
|
||||
# to avoid infinite loops when there isn't an expected response
|
||||
# size and the slave sends noisy data continuously.
|
||||
if time_ > end:
|
||||
break
|
||||
|
||||
return data
|
||||
|
||||
def __str__(self):
|
||||
""" Builds a string representation of the connection
|
||||
|
||||
:returns: The string representation
|
||||
"""
|
||||
return "ModbusTlsClient(%s:%s)" % (self.host, self.port)
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
"<{} at {} socket={self.socket}, ipaddr={self.host}, "
|
||||
"port={self.port}, sslctx={self.sslctx}, timeout={self.timeout}>"
|
||||
).format(self.__class__.__name__, hex(id(self)), self=self)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Modbus UDP Client Transport Implementation
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
class ModbusUdpClient(BaseModbusClient):
|
||||
""" Implementation of a modbus udp client
|
||||
"""
|
||||
|
||||
def __init__(self, host='127.0.0.1', port=Defaults.Port,
|
||||
framer=ModbusSocketFramer, **kwargs):
|
||||
""" Initialize a client instance
|
||||
|
||||
:param host: The host to connect to (default 127.0.0.1)
|
||||
:param port: The modbus port to connect to (default 502)
|
||||
:param framer: The modbus framer to use (default ModbusSocketFramer)
|
||||
:param timeout: The timeout to use for this socket (default None)
|
||||
"""
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.socket = None
|
||||
self.timeout = kwargs.get('timeout', None)
|
||||
BaseModbusClient.__init__(self, framer(ClientDecoder(), self), **kwargs)
|
||||
|
||||
@classmethod
|
||||
def _get_address_family(cls, address):
|
||||
""" A helper method to get the correct address family
|
||||
for a given address.
|
||||
|
||||
:param address: The address to get the af for
|
||||
:returns: AF_INET for ipv4 and AF_INET6 for ipv6
|
||||
"""
|
||||
try:
|
||||
_ = socket.inet_pton(socket.AF_INET6, address)
|
||||
except socket.error: # not a valid ipv6 address
|
||||
return socket.AF_INET
|
||||
return socket.AF_INET6
|
||||
|
||||
def connect(self):
|
||||
""" Connect to the modbus tcp server
|
||||
|
||||
:returns: True if connection succeeded, False otherwise
|
||||
"""
|
||||
if self.socket:
|
||||
return True
|
||||
try:
|
||||
family = ModbusUdpClient._get_address_family(self.host)
|
||||
self.socket = socket.socket(family, socket.SOCK_DGRAM)
|
||||
self.socket.settimeout(self.timeout)
|
||||
except socket.error as ex:
|
||||
_logger.error('Unable to create udp socket %s' % ex)
|
||||
self.close()
|
||||
return self.socket is not None
|
||||
|
||||
def close(self):
|
||||
""" Closes the underlying socket connection
|
||||
"""
|
||||
self.socket = None
|
||||
|
||||
def _send(self, request):
|
||||
""" Sends data on the underlying socket
|
||||
|
||||
:param request: The encoded request to send
|
||||
:return: The number of bytes written
|
||||
"""
|
||||
if not self.socket:
|
||||
raise ConnectionException(self.__str__())
|
||||
if request:
|
||||
return self.socket.sendto(request, (self.host, self.port))
|
||||
return 0
|
||||
|
||||
def _recv(self, size):
|
||||
""" Reads data from the underlying descriptor
|
||||
|
||||
:param size: The number of bytes to read
|
||||
:return: The bytes read
|
||||
"""
|
||||
if not self.socket:
|
||||
raise ConnectionException(self.__str__())
|
||||
return self.socket.recvfrom(size)[0]
|
||||
|
||||
def is_socket_open(self):
|
||||
if self.socket:
|
||||
return True
|
||||
return self.connect()
|
||||
|
||||
def __str__(self):
|
||||
""" Builds a string representation of the connection
|
||||
|
||||
:returns: The string representation
|
||||
"""
|
||||
return "ModbusUdpClient(%s:%s)" % (self.host, self.port)
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
"<{} at {} socket={self.socket}, ipaddr={self.host}, "
|
||||
"port={self.port}, timeout={self.timeout}>"
|
||||
).format(self.__class__.__name__, hex(id(self)), self=self)
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Modbus Serial Client Transport Implementation
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
class ModbusSerialClient(BaseModbusClient):
|
||||
""" Implementation of a modbus serial client
|
||||
"""
|
||||
state = ModbusTransactionState.IDLE
|
||||
inter_char_timeout = 0
|
||||
silent_interval = 0
|
||||
|
||||
def __init__(self, method='ascii', **kwargs):
|
||||
""" Initialize a serial client instance
|
||||
|
||||
The methods to connect are::
|
||||
|
||||
- ascii
|
||||
- rtu
|
||||
- binary
|
||||
|
||||
:param method: The method to use for connection
|
||||
:param port: The serial port to attach to
|
||||
:param stopbits: The number of stop bits to use
|
||||
:param bytesize: The bytesize of the serial messages
|
||||
:param parity: Which kind of parity to use
|
||||
:param baudrate: The baud rate to use for the serial device
|
||||
:param timeout: The timeout between serial requests (default 3s)
|
||||
:param strict: Use Inter char timeout for baudrates <= 19200 (adhere
|
||||
to modbus standards)
|
||||
:param handle_local_echo: Handle local echo of the USB-to-RS485 adaptor
|
||||
"""
|
||||
self.method = method
|
||||
self.socket = None
|
||||
BaseModbusClient.__init__(self, self.__implementation(method, self),
|
||||
**kwargs)
|
||||
|
||||
self.port = kwargs.get('port', 0)
|
||||
self.stopbits = kwargs.get('stopbits', Defaults.Stopbits)
|
||||
self.bytesize = kwargs.get('bytesize', Defaults.Bytesize)
|
||||
self.parity = kwargs.get('parity', Defaults.Parity)
|
||||
self.baudrate = kwargs.get('baudrate', Defaults.Baudrate)
|
||||
self.timeout = kwargs.get('timeout', Defaults.Timeout)
|
||||
self._strict = kwargs.get("strict", False)
|
||||
self.last_frame_end = None
|
||||
self.handle_local_echo = kwargs.get("handle_local_echo", False)
|
||||
if self.method == "rtu":
|
||||
if self.baudrate > 19200:
|
||||
self.silent_interval = 1.75 / 1000 # ms
|
||||
else:
|
||||
self._t0 = float((1 + 8 + 2)) / self.baudrate
|
||||
self.inter_char_timeout = 1.5 * self._t0
|
||||
self.silent_interval = 3.5 * self._t0
|
||||
self.silent_interval = round(self.silent_interval, 6)
|
||||
|
||||
@staticmethod
|
||||
def __implementation(method, client):
|
||||
""" Returns the requested framer
|
||||
|
||||
:method: The serial framer to instantiate
|
||||
:returns: The requested serial framer
|
||||
"""
|
||||
method = method.lower()
|
||||
if method == 'ascii':
|
||||
return ModbusAsciiFramer(ClientDecoder(), client)
|
||||
elif method == 'rtu':
|
||||
return ModbusRtuFramer(ClientDecoder(), client)
|
||||
elif method == 'binary':
|
||||
return ModbusBinaryFramer(ClientDecoder(), client)
|
||||
elif method == 'socket':
|
||||
return ModbusSocketFramer(ClientDecoder(), client)
|
||||
raise ParameterException("Invalid framer method requested")
|
||||
|
||||
def connect(self):
|
||||
""" Connect to the modbus serial server
|
||||
|
||||
:returns: True if connection succeeded, False otherwise
|
||||
"""
|
||||
import serial
|
||||
if self.socket:
|
||||
return True
|
||||
try:
|
||||
self.socket = serial.serial_for_url(self.port,
|
||||
timeout=self.timeout,
|
||||
bytesize=self.bytesize,
|
||||
stopbits=self.stopbits,
|
||||
baudrate=self.baudrate,
|
||||
parity=self.parity)
|
||||
if self.method == "rtu":
|
||||
if self._strict:
|
||||
self.socket.interCharTimeout = self.inter_char_timeout
|
||||
self.last_frame_end = None
|
||||
except serial.SerialException as msg:
|
||||
_logger.error(msg)
|
||||
self.close()
|
||||
return self.socket is not None
|
||||
|
||||
def close(self):
|
||||
""" Closes the underlying socket connection
|
||||
"""
|
||||
if self.socket:
|
||||
self.socket.close()
|
||||
self.socket = None
|
||||
|
||||
def _in_waiting(self):
|
||||
in_waiting = ("in_waiting" if hasattr(
|
||||
self.socket, "in_waiting") else "inWaiting")
|
||||
|
||||
if in_waiting == "in_waiting":
|
||||
waitingbytes = getattr(self.socket, in_waiting)
|
||||
else:
|
||||
waitingbytes = getattr(self.socket, in_waiting)()
|
||||
return waitingbytes
|
||||
|
||||
def _send(self, request):
|
||||
""" Sends data on the underlying socket
|
||||
|
||||
If receive buffer still holds some data then flush it.
|
||||
|
||||
Sleep if last send finished less than 3.5 character
|
||||
times ago.
|
||||
|
||||
:param request: The encoded request to send
|
||||
:return: The number of bytes written
|
||||
"""
|
||||
if not self.socket:
|
||||
raise ConnectionException(self.__str__())
|
||||
if request:
|
||||
try:
|
||||
waitingbytes = self._in_waiting()
|
||||
if waitingbytes:
|
||||
result = self.socket.read(waitingbytes)
|
||||
if self.state == ModbusTransactionState.RETRYING:
|
||||
_logger.debug("Sending available data in recv "
|
||||
"buffer {}".format(
|
||||
hexlify_packets(result)))
|
||||
return result
|
||||
if _logger.isEnabledFor(logging.WARNING):
|
||||
_logger.warning("Cleanup recv buffer before "
|
||||
"send: " + hexlify_packets(result))
|
||||
except NotImplementedError:
|
||||
pass
|
||||
if self.state != ModbusTransactionState.SENDING:
|
||||
_logger.debug("New Transaction state 'SENDING'")
|
||||
self.state = ModbusTransactionState.SENDING
|
||||
size = self.socket.write(request)
|
||||
return size
|
||||
return 0
|
||||
|
||||
def _wait_for_data(self):
|
||||
size = 0
|
||||
more_data = False
|
||||
if self.timeout is not None and self.timeout != 0:
|
||||
condition = partial(lambda start, timeout:
|
||||
(time.time() - start) <= timeout,
|
||||
timeout=self.timeout)
|
||||
else:
|
||||
condition = partial(lambda dummy1, dummy2: True, dummy2=None)
|
||||
start = time.time()
|
||||
while condition(start):
|
||||
avaialble = self._in_waiting()
|
||||
if (more_data and not avaialble) or (more_data and avaialble == size):
|
||||
break
|
||||
if avaialble and avaialble != size:
|
||||
more_data = True
|
||||
size = avaialble
|
||||
time.sleep(0.01)
|
||||
return size
|
||||
|
||||
def _recv(self, size):
|
||||
""" Reads data from the underlying descriptor
|
||||
|
||||
:param size: The number of bytes to read
|
||||
:return: The bytes read
|
||||
"""
|
||||
if not self.socket:
|
||||
raise ConnectionException(self.__str__())
|
||||
if size is None:
|
||||
size = self._wait_for_data()
|
||||
result = self.socket.read(size)
|
||||
return result
|
||||
|
||||
def is_socket_open(self):
|
||||
if self.socket:
|
||||
if hasattr(self.socket, "is_open"):
|
||||
return self.socket.is_open
|
||||
else:
|
||||
return self.socket.isOpen()
|
||||
return False
|
||||
|
||||
def __str__(self):
|
||||
""" Builds a string representation of the connection
|
||||
|
||||
:returns: The string representation
|
||||
"""
|
||||
return "ModbusSerialClient(%s baud[%s])" % (self.method, self.baudrate)
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
"<{} at {} socket={self.socket}, method={self.method}, "
|
||||
"timeout={self.timeout}>"
|
||||
).format(self.__class__.__name__, hex(id(self)), self=self)
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Exported symbols
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
__all__ = [
|
||||
"ModbusTcpClient", "ModbusTlsClient", "ModbusUdpClient", "ModbusSerialClient"
|
||||
]
|
||||
167
modbus/pymodbus/client/sync_diag.py
Normal file
@@ -0,0 +1,167 @@
|
||||
import socket
|
||||
import logging
|
||||
import time
|
||||
|
||||
from pymodbus.constants import Defaults
|
||||
from pymodbus.client.sync import ModbusTcpClient
|
||||
from pymodbus.transaction import ModbusSocketFramer
|
||||
from pymodbus.exceptions import ConnectionException
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
LOG_MSGS = {
|
||||
'conn_msg': 'Connecting to modbus device %s',
|
||||
'connfail_msg': 'Connection to (%s, %s) failed: %s',
|
||||
'discon_msg': 'Disconnecting from modbus device %s',
|
||||
'timelimit_read_msg':
|
||||
'Modbus device read took %.4f seconds, '
|
||||
'returned %s bytes in timelimit read',
|
||||
'timeout_msg':
|
||||
'Modbus device timeout after %.4f seconds, '
|
||||
'returned %s bytes %s',
|
||||
'delay_msg':
|
||||
'Modbus device read took %.4f seconds, '
|
||||
'returned %s bytes of %s expected',
|
||||
'read_msg':
|
||||
'Modbus device read took %.4f seconds, '
|
||||
'returned %s bytes of %s expected',
|
||||
'unexpected_dc_msg': '%s %s'}
|
||||
|
||||
|
||||
class ModbusTcpDiagClient(ModbusTcpClient):
|
||||
"""
|
||||
Variant of pymodbus.client.sync.ModbusTcpClient with additional
|
||||
logging to diagnose network issues.
|
||||
|
||||
The following events are logged:
|
||||
|
||||
+---------+-----------------------------------------------------------------+
|
||||
| Level | Events |
|
||||
+=========+=================================================================+
|
||||
| ERROR | Failure to connect to modbus unit; unexpected disconnect by |
|
||||
| | modbus unit |
|
||||
+---------+-----------------------------------------------------------------+
|
||||
| WARNING | Timeout on normal read; read took longer than warn_delay_limit |
|
||||
+---------+-----------------------------------------------------------------+
|
||||
| INFO | Connection attempt to modbus unit; disconnection from modbus |
|
||||
| | unit; each time limited read |
|
||||
+---------+-----------------------------------------------------------------+
|
||||
| DEBUG | Normal read with timing information |
|
||||
+---------+-----------------------------------------------------------------+
|
||||
|
||||
Reads are differentiated between "normal", which reads a specified number of
|
||||
bytes, and "time limited", which reads all data for a duration equal to the
|
||||
timeout period configured for this instance.
|
||||
"""
|
||||
|
||||
# pylint: disable=no-member
|
||||
|
||||
def __init__(self, host='127.0.0.1', port=Defaults.Port,
|
||||
framer=ModbusSocketFramer, **kwargs):
|
||||
""" Initialize a client instance
|
||||
|
||||
The keys of LOG_MSGS can be used in kwargs to customize the messages.
|
||||
|
||||
:param host: The host to connect to (default 127.0.0.1)
|
||||
:param port: The modbus port to connect to (default 502)
|
||||
:param source_address: The source address tuple to bind to (default ('', 0))
|
||||
:param timeout: The timeout to use for this socket (default Defaults.Timeout)
|
||||
:param warn_delay_limit: Log reads that take longer than this as warning.
|
||||
Default True sets it to half of "timeout". None never logs these as
|
||||
warning, 0 logs everything as warning.
|
||||
:param framer: The modbus framer to use (default ModbusSocketFramer)
|
||||
|
||||
.. note:: The host argument will accept ipv4 and ipv6 hosts
|
||||
"""
|
||||
self.warn_delay_limit = kwargs.get('warn_delay_limit', True)
|
||||
super(ModbusTcpDiagClient, self).__init__(host, port, framer, **kwargs)
|
||||
if self.warn_delay_limit is True:
|
||||
self.warn_delay_limit = self.timeout / 2
|
||||
|
||||
# Set logging messages, defaulting to LOG_MSGS
|
||||
for (k, v) in LOG_MSGS.items():
|
||||
self.__dict__[k] = kwargs.get(k, v)
|
||||
|
||||
def connect(self):
|
||||
""" Connect to the modbus tcp server
|
||||
|
||||
:returns: True if connection succeeded, False otherwise
|
||||
"""
|
||||
if self.socket:
|
||||
return True
|
||||
try:
|
||||
_logger.info(self.conn_msg, self)
|
||||
self.socket = socket.create_connection(
|
||||
(self.host, self.port),
|
||||
timeout=self.timeout,
|
||||
source_address=self.source_address)
|
||||
except socket.error as msg:
|
||||
_logger.error(self.connfail_msg, self.host, self.port, msg)
|
||||
self.close()
|
||||
return self.socket is not None
|
||||
|
||||
def close(self):
|
||||
""" Closes the underlying socket connection
|
||||
"""
|
||||
if self.socket:
|
||||
_logger.info(self.discon_msg, self)
|
||||
self.socket.close()
|
||||
self.socket = None
|
||||
|
||||
def _recv(self, size):
|
||||
try:
|
||||
start = time.time()
|
||||
|
||||
result = super(ModbusTcpDiagClient, self)._recv(size)
|
||||
|
||||
delay = time.time() - start
|
||||
if self.warn_delay_limit is not None and delay >= self.warn_delay_limit:
|
||||
self._log_delayed_response(len(result), size, delay)
|
||||
elif not size:
|
||||
_logger.debug(self.timelimit_read_msg, delay, len(result))
|
||||
else:
|
||||
_logger.debug(self.read_msg, delay, len(result), size)
|
||||
|
||||
return result
|
||||
except ConnectionException as ex:
|
||||
# Only log actual network errors, "if not self.socket" then it's a internal code issue
|
||||
if 'Connection unexpectedly closed' in ex.string:
|
||||
_logger.error(self.unexpected_dc_msg, self, ex)
|
||||
raise ex
|
||||
|
||||
def _log_delayed_response(self, result_len, size, delay):
|
||||
if not size and result_len > 0:
|
||||
_logger.info(self.timelimit_read_msg, delay, result_len)
|
||||
elif (result_len == 0 or (size and result_len < size)) and delay >= self.timeout:
|
||||
read_type = ("of %i expected" % size) if size else "in timelimit read"
|
||||
_logger.warning(self.timeout_msg, delay, result_len, read_type)
|
||||
else:
|
||||
_logger.warning(self.delay_msg, delay, result_len, size)
|
||||
|
||||
def __str__(self):
|
||||
""" Builds a string representation of the connection
|
||||
|
||||
:returns: The string representation
|
||||
"""
|
||||
return "ModbusTcpDiagClient(%s:%s)" % (self.host, self.port)
|
||||
|
||||
|
||||
def get_client():
|
||||
""" Returns an appropriate client based on logging level
|
||||
|
||||
This will be ModbusTcpDiagClient by default, or the parent class
|
||||
if the log level is such that the diagnostic client will not log
|
||||
anything.
|
||||
|
||||
:returns: ModbusTcpClient or a child class thereof
|
||||
"""
|
||||
return ModbusTcpDiagClient if _logger.isEnabledFor(logging.ERROR) else ModbusTcpClient
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Exported symbols
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
__all__ = [
|
||||
"ModbusTcpDiagClient", "get_client"
|
||||
]
|
||||
98
modbus/pymodbus/compat.py
Normal file
@@ -0,0 +1,98 @@
|
||||
"""
|
||||
Python 2.x/3.x Compatibility Layer
|
||||
-------------------------------------------------
|
||||
|
||||
This is mostly based on the jinja2 compat code:
|
||||
|
||||
Some py2/py3 compatibility support based on a stripped down
|
||||
version of six so we don't have to depend on a specific version
|
||||
of it.
|
||||
|
||||
:copyright: Copyright 2013 by the Jinja team, see AUTHORS.
|
||||
:license: BSD, see LICENSE for details.
|
||||
"""
|
||||
import sys
|
||||
import six
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# python version checks
|
||||
# --------------------------------------------------------------------------- #
|
||||
PYTHON_VERSION = sys.version_info
|
||||
IS_PYTHON2 = six.PY2
|
||||
IS_PYTHON3 = six.PY3
|
||||
IS_PYPY = hasattr(sys, 'pypy_translation_info')
|
||||
IS_JYTHON = sys.platform.startswith('java')
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# python > 3.3 compatibility layer
|
||||
# --------------------------------------------------------------------------- #
|
||||
# ----------------------------------------------------------------------- #
|
||||
# portable builtins
|
||||
# ----------------------------------------------------------------------- #
|
||||
int2byte = six.int2byte
|
||||
unichr = six.unichr
|
||||
range_type = six.moves.range
|
||||
text_type = six.string_types
|
||||
string_types = six.string_types
|
||||
iterkeys = six.iterkeys
|
||||
itervalues = six.itervalues
|
||||
iteritems = six.iteritems
|
||||
get_next = six.next
|
||||
unicode_string = six.u
|
||||
|
||||
NativeStringIO = six.StringIO
|
||||
ifilter = six.moves.filter
|
||||
imap = six.moves.map
|
||||
izip = six.moves.zip
|
||||
intern = six.moves.intern
|
||||
|
||||
if not IS_PYTHON2:
|
||||
# ----------------------------------------------------------------------- #
|
||||
# module renames
|
||||
# ----------------------------------------------------------------------- #
|
||||
import socketserver
|
||||
# #609 monkey patch for socket server memory leaks
|
||||
# Refer https://bugs.python.org/issue37193
|
||||
socketserver.ThreadingMixIn.daemon_threads = True
|
||||
# ----------------------------------------------------------------------- #
|
||||
# decorators
|
||||
# ----------------------------------------------------------------------- #
|
||||
implements_to_string = lambda x: x
|
||||
|
||||
byte2int = lambda b: b
|
||||
if PYTHON_VERSION >= (3, 4):
|
||||
def is_installed(module):
|
||||
import importlib.util
|
||||
found = importlib.util.find_spec(module)
|
||||
return found
|
||||
else:
|
||||
def is_installed(module):
|
||||
import importlib
|
||||
found = importlib.find_loader(module)
|
||||
return found
|
||||
# --------------------------------------------------------------------------- #
|
||||
# python > 2.5 compatability layer
|
||||
# --------------------------------------------------------------------------- #
|
||||
else:
|
||||
byte2int = six.byte2int
|
||||
# ----------------------------------------------------------------------- #
|
||||
# module renames
|
||||
|
||||
# ----------------------------------------------------------------------- #
|
||||
import SocketServer as socketserver
|
||||
|
||||
# ----------------------------------------------------------------------- #
|
||||
# decorators
|
||||
# ----------------------------------------------------------------------- #
|
||||
def implements_to_string(klass):
|
||||
klass.__unicode__ = klass.__str__
|
||||
klass.__str__ = lambda x: x.__unicode__().encode('utf-8')
|
||||
return klass
|
||||
|
||||
def is_installed(module):
|
||||
import imp
|
||||
try:
|
||||
imp.find_module(module)
|
||||
return True
|
||||
except ImportError:
|
||||
return False
|
||||
268
modbus/pymodbus/constants.py
Normal file
@@ -0,0 +1,268 @@
|
||||
'''
|
||||
Constants For Modbus Server/Client
|
||||
----------------------------------
|
||||
|
||||
This is the single location for storing default
|
||||
values for the servers and clients.
|
||||
'''
|
||||
from pymodbus.interfaces import Singleton
|
||||
|
||||
|
||||
class Defaults(Singleton):
|
||||
''' A collection of modbus default values
|
||||
|
||||
.. attribute:: Port
|
||||
|
||||
The default modbus tcp server port (502)
|
||||
|
||||
.. attribute:: TLSPort
|
||||
|
||||
The default modbus tcp over tls server port (802)
|
||||
|
||||
|
||||
.. attribute:: Backoff
|
||||
|
||||
The default exponential backoff delay (0.3 seconds)
|
||||
|
||||
.. attribute:: Retries
|
||||
|
||||
The default number of times a client should retry the given
|
||||
request before failing (3)
|
||||
|
||||
.. attribute:: RetryOnEmpty
|
||||
|
||||
A flag indicating if a transaction should be retried in the
|
||||
case that an empty response is received. This is useful for
|
||||
slow clients that may need more time to process a request.
|
||||
|
||||
.. attribute:: RetryOnInvalid
|
||||
|
||||
A flag indicating if a transaction should be retried in the
|
||||
case that an invalid response is received.
|
||||
|
||||
.. attribute:: Timeout
|
||||
|
||||
The default amount of time a client should wait for a request
|
||||
to be processed (3 seconds)
|
||||
|
||||
.. attribute:: Reconnects
|
||||
|
||||
The default number of times a client should attempt to reconnect
|
||||
before deciding the server is down (0)
|
||||
|
||||
.. attribute:: TransactionId
|
||||
|
||||
The starting transaction identifier number (0)
|
||||
|
||||
.. attribute:: ProtocolId
|
||||
|
||||
The modbus protocol id. Currently this is set to 0 in all
|
||||
but proprietary implementations.
|
||||
|
||||
.. attribute:: UnitId
|
||||
|
||||
The modbus slave addrss. Currently this is set to 0x00 which
|
||||
means this request should be broadcast to all the slave devices
|
||||
(really means that all the devices should respons).
|
||||
|
||||
.. attribute:: Baudrate
|
||||
|
||||
The speed at which the data is transmitted over the serial line.
|
||||
This defaults to 19200.
|
||||
|
||||
.. attribute:: Parity
|
||||
|
||||
The type of checksum to use to verify data integrity. This can be
|
||||
on of the following::
|
||||
|
||||
- (E)ven - 1 0 1 0 | P(0)
|
||||
- (O)dd - 1 0 1 0 | P(1)
|
||||
- (N)one - 1 0 1 0 | no parity
|
||||
|
||||
This defaults to (N)one.
|
||||
|
||||
.. attribute:: Bytesize
|
||||
|
||||
The number of bits in a byte of serial data. This can be one of
|
||||
5, 6, 7, or 8. This defaults to 8.
|
||||
|
||||
.. attribute:: Stopbits
|
||||
|
||||
The number of bits sent after each character in a message to
|
||||
indicate the end of the byte. This defaults to 1.
|
||||
|
||||
.. attribute:: ZeroMode
|
||||
|
||||
Indicates if the slave datastore should use indexing at 0 or 1.
|
||||
More about this can be read in section 4.4 of the modbus specification.
|
||||
|
||||
.. attribute:: IgnoreMissingSlaves
|
||||
|
||||
In case a request is made to a missing slave, this defines if an error
|
||||
should be returned or simply ignored. This is useful for the case of a
|
||||
serial server emulater where a request to a non-existant slave on a bus
|
||||
will never respond. The client in this case will simply timeout.
|
||||
|
||||
.. attribute:: broadcast_enable
|
||||
|
||||
When False unit_id 0 will be treated as any other unit_id. When True and
|
||||
the unit_id is 0 the server will execute all requests on all server
|
||||
contexts and not respond and the client will skip trying to receive a
|
||||
response. Default value False does not conform to Modbus spec but maintains
|
||||
legacy behavior for existing pymodbus users.
|
||||
|
||||
'''
|
||||
Port = 502
|
||||
TLSPort = 802
|
||||
Backoff = 0.3
|
||||
Retries = 3
|
||||
RetryOnEmpty = False
|
||||
RetryOnInvalid = False
|
||||
Timeout = 3
|
||||
Reconnects = 0
|
||||
TransactionId = 0
|
||||
ProtocolId = 0
|
||||
UnitId = 0x00
|
||||
Baudrate = 19200
|
||||
Parity = 'N'
|
||||
Bytesize = 8
|
||||
Stopbits = 1
|
||||
ZeroMode = False
|
||||
IgnoreMissingSlaves = False
|
||||
ReadSize = 1024
|
||||
broadcast_enable = False
|
||||
|
||||
class ModbusStatus(Singleton):
|
||||
'''
|
||||
These represent various status codes in the modbus
|
||||
protocol.
|
||||
|
||||
.. attribute:: Waiting
|
||||
|
||||
This indicates that a modbus device is currently
|
||||
waiting for a given request to finish some running task.
|
||||
|
||||
.. attribute:: Ready
|
||||
|
||||
This indicates that a modbus device is currently
|
||||
free to perform the next request task.
|
||||
|
||||
.. attribute:: On
|
||||
|
||||
This indicates that the given modbus entity is on
|
||||
|
||||
.. attribute:: Off
|
||||
|
||||
This indicates that the given modbus entity is off
|
||||
|
||||
.. attribute:: SlaveOn
|
||||
|
||||
This indicates that the given modbus slave is running
|
||||
|
||||
.. attribute:: SlaveOff
|
||||
|
||||
This indicates that the given modbus slave is not running
|
||||
'''
|
||||
Waiting = 0xffff
|
||||
Ready = 0x0000
|
||||
On = 0xff00
|
||||
Off = 0x0000
|
||||
SlaveOn = 0xff
|
||||
SlaveOff = 0x00
|
||||
|
||||
|
||||
class Endian(Singleton):
|
||||
''' An enumeration representing the various byte endianess.
|
||||
|
||||
.. attribute:: Auto
|
||||
|
||||
This indicates that the byte order is chosen by the
|
||||
current native environment.
|
||||
|
||||
.. attribute:: Big
|
||||
|
||||
This indicates that the bytes are in little endian format
|
||||
|
||||
.. attribute:: Little
|
||||
|
||||
This indicates that the bytes are in big endian format
|
||||
|
||||
.. note:: I am simply borrowing the format strings from the
|
||||
python struct module for my convenience.
|
||||
'''
|
||||
Auto = '@'
|
||||
Big = '>'
|
||||
Little = '<'
|
||||
|
||||
|
||||
class ModbusPlusOperation(Singleton):
|
||||
''' Represents the type of modbus plus request
|
||||
|
||||
.. attribute:: GetStatistics
|
||||
|
||||
Operation requesting that the current modbus plus statistics
|
||||
be returned in the response.
|
||||
|
||||
.. attribute:: ClearStatistics
|
||||
|
||||
Operation requesting that the current modbus plus statistics
|
||||
be cleared and not returned in the response.
|
||||
'''
|
||||
GetStatistics = 0x0003
|
||||
ClearStatistics = 0x0004
|
||||
|
||||
|
||||
class DeviceInformation(Singleton):
|
||||
''' Represents what type of device information to read
|
||||
|
||||
.. attribute:: Basic
|
||||
|
||||
This is the basic (required) device information to be returned.
|
||||
This includes VendorName, ProductCode, and MajorMinorRevision
|
||||
code.
|
||||
|
||||
.. attribute:: Regular
|
||||
|
||||
In addition to basic data objects, the device provides additional
|
||||
and optional identification and description data objects. All of
|
||||
the objects of this category are defined in the standard but their
|
||||
implementation is optional.
|
||||
|
||||
.. attribute:: Extended
|
||||
|
||||
In addition to regular data objects, the device provides additional
|
||||
and optional identification and description private data about the
|
||||
physical device itself. All of these data are device dependent.
|
||||
|
||||
.. attribute:: Specific
|
||||
|
||||
Request to return a single data object.
|
||||
'''
|
||||
Basic = 0x01
|
||||
Regular = 0x02
|
||||
Extended = 0x03
|
||||
Specific = 0x04
|
||||
|
||||
|
||||
class MoreData(Singleton):
|
||||
''' Represents the more follows condition
|
||||
|
||||
.. attribute:: Nothing
|
||||
|
||||
This indiates that no more objects are going to be returned.
|
||||
|
||||
.. attribute:: KeepReading
|
||||
|
||||
This indicates that there are more objects to be returned.
|
||||
'''
|
||||
Nothing = 0x00
|
||||
KeepReading = 0xFF
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Exported Identifiers
|
||||
#---------------------------------------------------------------------------#
|
||||
__all__ = [
|
||||
"Defaults", "ModbusStatus", "Endian",
|
||||
"ModbusPlusOperation",
|
||||
"DeviceInformation", "MoreData",
|
||||
]
|
||||
12
modbus/pymodbus/datastore/__init__.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from pymodbus.datastore.store import ModbusSequentialDataBlock
|
||||
from pymodbus.datastore.store import ModbusSparseDataBlock
|
||||
from pymodbus.datastore.context import ModbusSlaveContext
|
||||
from pymodbus.datastore.context import ModbusServerContext
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Exported symbols
|
||||
#---------------------------------------------------------------------------#
|
||||
__all__ = [
|
||||
"ModbusSequentialDataBlock", "ModbusSparseDataBlock",
|
||||
"ModbusSlaveContext", "ModbusServerContext",
|
||||
]
|
||||
184
modbus/pymodbus/datastore/context.py
Normal file
@@ -0,0 +1,184 @@
|
||||
from pymodbus.exceptions import ParameterException, NoSuchSlaveException
|
||||
from pymodbus.interfaces import IModbusSlaveContext
|
||||
from pymodbus.datastore.store import ModbusSequentialDataBlock
|
||||
from pymodbus.constants import Defaults
|
||||
from pymodbus.compat import iteritems, itervalues
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Logging
|
||||
#---------------------------------------------------------------------------#
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Slave Contexts
|
||||
#---------------------------------------------------------------------------#
|
||||
class ModbusSlaveContext(IModbusSlaveContext):
|
||||
'''
|
||||
This creates a modbus data model with each data access
|
||||
stored in its own personal block
|
||||
'''
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
''' Initializes the datastores, defaults to fully populated
|
||||
sequential data blocks if none are passed in.
|
||||
|
||||
:param kwargs: Each element is a ModbusDataBlock
|
||||
|
||||
'di' - Discrete Inputs initializer
|
||||
'co' - Coils initializer
|
||||
'hr' - Holding Register initializer
|
||||
'ir' - Input Registers iniatializer
|
||||
'''
|
||||
self.store = dict()
|
||||
self.store['d'] = kwargs.get('di', ModbusSequentialDataBlock.create())
|
||||
self.store['c'] = kwargs.get('co', ModbusSequentialDataBlock.create())
|
||||
self.store['i'] = kwargs.get('ir', ModbusSequentialDataBlock.create())
|
||||
self.store['h'] = kwargs.get('hr', ModbusSequentialDataBlock.create())
|
||||
self.zero_mode = kwargs.get('zero_mode', Defaults.ZeroMode)
|
||||
|
||||
def __str__(self):
|
||||
''' Returns a string representation of the context
|
||||
|
||||
:returns: A string representation of the context
|
||||
'''
|
||||
return "Modbus Slave Context"
|
||||
|
||||
def reset(self):
|
||||
''' Resets all the datastores to their default values '''
|
||||
for datastore in itervalues(self.store):
|
||||
datastore.reset()
|
||||
|
||||
def validate(self, fx, address, count=1):
|
||||
''' Validates the request to make sure it is in range
|
||||
|
||||
:param fx: The function we are working with
|
||||
:param address: The starting address
|
||||
:param count: The number of values to test
|
||||
:returns: True if the request in within range, False otherwise
|
||||
'''
|
||||
if not self.zero_mode:
|
||||
address = address + 1
|
||||
_logger.debug("validate: fc-[%d] address-%d: count-%d" % (fx, address,
|
||||
count))
|
||||
return self.store[self.decode(fx)].validate(address, count)
|
||||
|
||||
def getValues(self, fx, address, count=1):
|
||||
''' Get `count` values from datastore
|
||||
|
||||
:param fx: The function we are working with
|
||||
:param address: The starting address
|
||||
:param count: The number of values to retrieve
|
||||
:returns: The requested values from a:a+c
|
||||
'''
|
||||
if not self.zero_mode:
|
||||
address = address + 1
|
||||
_logger.debug("getValues fc-[%d] address-%d: count-%d" % (fx, address,
|
||||
count))
|
||||
return self.store[self.decode(fx)].getValues(address, count)
|
||||
|
||||
def setValues(self, fx, address, values):
|
||||
''' Sets the datastore with the supplied values
|
||||
|
||||
:param fx: The function we are working with
|
||||
:param address: The starting address
|
||||
:param values: The new values to be set
|
||||
'''
|
||||
if not self.zero_mode:
|
||||
address = address + 1
|
||||
_logger.debug("setValues[%d] %d:%d" % (fx, address, len(values)))
|
||||
self.store[self.decode(fx)].setValues(address, values)
|
||||
|
||||
def register(self, fc, fx, datablock=None):
|
||||
"""
|
||||
Registers a datablock with the slave context
|
||||
:param fc: function code (int)
|
||||
:param fx: string representation of function code (e.g 'cf' )
|
||||
:param datablock: datablock to associate with this function code
|
||||
:return:
|
||||
"""
|
||||
self.store[fx] = datablock or ModbusSequentialDataBlock.create()
|
||||
self._IModbusSlaveContext__fx_mapper[fc] = fx
|
||||
|
||||
|
||||
class ModbusServerContext(object):
|
||||
''' This represents a master collection of slave contexts.
|
||||
If single is set to true, it will be treated as a single
|
||||
context so every unit-id returns the same context. If single
|
||||
is set to false, it will be interpreted as a collection of
|
||||
slave contexts.
|
||||
'''
|
||||
|
||||
def __init__(self, slaves=None, single=True):
|
||||
''' Initializes a new instance of a modbus server context.
|
||||
|
||||
:param slaves: A dictionary of client contexts
|
||||
:param single: Set to true to treat this as a single context
|
||||
'''
|
||||
self.single = single
|
||||
self._slaves = slaves or {}
|
||||
if self.single:
|
||||
self._slaves = {Defaults.UnitId: self._slaves}
|
||||
|
||||
def __iter__(self):
|
||||
''' Iterater over the current collection of slave
|
||||
contexts.
|
||||
|
||||
:returns: An iterator over the slave contexts
|
||||
'''
|
||||
return iteritems(self._slaves)
|
||||
|
||||
def __contains__(self, slave):
|
||||
''' Check if the given slave is in this list
|
||||
|
||||
:param slave: slave The slave to check for existence
|
||||
:returns: True if the slave exists, False otherwise
|
||||
'''
|
||||
if self.single and self._slaves:
|
||||
return True
|
||||
else:
|
||||
return slave in self._slaves
|
||||
|
||||
def __setitem__(self, slave, context):
|
||||
''' Used to set a new slave context
|
||||
|
||||
:param slave: The slave context to set
|
||||
:param context: The new context to set for this slave
|
||||
'''
|
||||
if self.single:
|
||||
slave = Defaults.UnitId
|
||||
if 0xf7 >= slave >= 0x00:
|
||||
self._slaves[slave] = context
|
||||
else:
|
||||
raise NoSuchSlaveException('slave index :{} '
|
||||
'out of range'.format(slave))
|
||||
|
||||
def __delitem__(self, slave):
|
||||
''' Wrapper used to access the slave context
|
||||
|
||||
:param slave: The slave context to remove
|
||||
'''
|
||||
if not self.single and (0xf7 >= slave >= 0x00):
|
||||
del self._slaves[slave]
|
||||
else:
|
||||
raise NoSuchSlaveException('slave index: {} '
|
||||
'out of range'.format(slave))
|
||||
|
||||
def __getitem__(self, slave):
|
||||
''' Used to get access to a slave context
|
||||
|
||||
:param slave: The slave context to get
|
||||
:returns: The requested slave context
|
||||
'''
|
||||
if self.single:
|
||||
slave = Defaults.UnitId
|
||||
if slave in self._slaves:
|
||||
return self._slaves.get(slave)
|
||||
else:
|
||||
raise NoSuchSlaveException("slave - {} does not exist, "
|
||||
"or is out of range".format(slave))
|
||||
|
||||
def slaves(self):
|
||||
# Python3 now returns keys() as iterable
|
||||
return list(self._slaves.keys())
|
||||
7
modbus/pymodbus/datastore/database/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from pymodbus.datastore.database.sql_datastore import SqlSlaveContext
|
||||
from pymodbus.datastore.database.redis_datastore import RedisSlaveContext
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Exported symbols
|
||||
#---------------------------------------------------------------------------#
|
||||
__all__ = ["SqlSlaveContext", "RedisSlaveContext"]
|
||||
243
modbus/pymodbus/datastore/database/redis_datastore.py
Normal file
@@ -0,0 +1,243 @@
|
||||
import redis
|
||||
from pymodbus.interfaces import IModbusSlaveContext
|
||||
from pymodbus.utilities import pack_bitstring, unpack_bitstring
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Logging
|
||||
#---------------------------------------------------------------------------#
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Context
|
||||
#---------------------------------------------------------------------------#
|
||||
class RedisSlaveContext(IModbusSlaveContext):
|
||||
'''
|
||||
This is a modbus slave context using redis as a backing
|
||||
store.
|
||||
'''
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
''' Initializes the datastores
|
||||
|
||||
:param host: The host to connect to
|
||||
:param port: The port to connect to
|
||||
:param prefix: A prefix for the keys
|
||||
'''
|
||||
host = kwargs.get('host', 'localhost')
|
||||
port = kwargs.get('port', 6379)
|
||||
self.prefix = kwargs.get('prefix', 'pymodbus')
|
||||
self.client = kwargs.get('client', redis.Redis(host=host, port=port))
|
||||
self._build_mapping()
|
||||
|
||||
def __str__(self):
|
||||
''' Returns a string representation of the context
|
||||
|
||||
:returns: A string representation of the context
|
||||
'''
|
||||
return "Redis Slave Context %s" % self.client
|
||||
|
||||
def reset(self):
|
||||
''' Resets all the datastores to their default values '''
|
||||
self.client.flushall()
|
||||
|
||||
def validate(self, fx, address, count=1):
|
||||
''' Validates the request to make sure it is in range
|
||||
|
||||
:param fx: The function we are working with
|
||||
:param address: The starting address
|
||||
:param count: The number of values to test
|
||||
:returns: True if the request in within range, False otherwise
|
||||
'''
|
||||
address = address + 1 # section 4.4 of specification
|
||||
_logger.debug("validate[%d] %d:%d" % (fx, address, count))
|
||||
return self._val_callbacks[self.decode(fx)](address, count)
|
||||
|
||||
def getValues(self, fx, address, count=1):
|
||||
''' Get `count` values from datastore
|
||||
|
||||
:param fx: The function we are working with
|
||||
:param address: The starting address
|
||||
:param count: The number of values to retrieve
|
||||
:returns: The requested values from a:a+c
|
||||
'''
|
||||
address = address + 1 # section 4.4 of specification
|
||||
_logger.debug("getValues[%d] %d:%d" % (fx, address, count))
|
||||
return self._get_callbacks[self.decode(fx)](address, count)
|
||||
|
||||
def setValues(self, fx, address, values):
|
||||
''' Sets the datastore with the supplied values
|
||||
|
||||
:param fx: The function we are working with
|
||||
:param address: The starting address
|
||||
:param values: The new values to be set
|
||||
'''
|
||||
address = address + 1 # section 4.4 of specification
|
||||
_logger.debug("setValues[%d] %d:%d" % (fx, address, len(values)))
|
||||
self._set_callbacks[self.decode(fx)](address, values)
|
||||
|
||||
#--------------------------------------------------------------------------#
|
||||
# Redis Helper Methods
|
||||
#--------------------------------------------------------------------------#
|
||||
def _get_prefix(self, key):
|
||||
''' This is a helper to abstract getting bit values
|
||||
|
||||
:param key: The key prefix to use
|
||||
:returns: The key prefix to redis
|
||||
'''
|
||||
return "%s:%s" % (self.prefix, key)
|
||||
|
||||
def _build_mapping(self):
|
||||
'''
|
||||
A quick helper method to build the function
|
||||
code mapper.
|
||||
'''
|
||||
self._val_callbacks = {
|
||||
'd': lambda o, c: self._val_bit('d', o, c),
|
||||
'c': lambda o, c: self._val_bit('c', o, c),
|
||||
'h': lambda o, c: self._val_reg('h', o, c),
|
||||
'i': lambda o, c: self._val_reg('i', o, c),
|
||||
}
|
||||
self._get_callbacks = {
|
||||
'd': lambda o, c: self._get_bit('d', o, c),
|
||||
'c': lambda o, c: self._get_bit('c', o, c),
|
||||
'h': lambda o, c: self._get_reg('h', o, c),
|
||||
'i': lambda o, c: self._get_reg('i', o, c),
|
||||
}
|
||||
self._set_callbacks = {
|
||||
'd': lambda o, v: self._set_bit('d', o, v),
|
||||
'c': lambda o, v: self._set_bit('c', o, v),
|
||||
'h': lambda o, v: self._set_reg('h', o, v),
|
||||
'i': lambda o, v: self._set_reg('i', o, v),
|
||||
}
|
||||
|
||||
#--------------------------------------------------------------------------#
|
||||
# Redis discrete implementation
|
||||
#--------------------------------------------------------------------------#
|
||||
_bit_size = 16
|
||||
_bit_default = '\x00' * (_bit_size % 8)
|
||||
|
||||
def _get_bit_values(self, key, offset, count):
|
||||
''' This is a helper to abstract getting bit values
|
||||
|
||||
:param key: The key prefix to use
|
||||
:param offset: The address offset to start at
|
||||
:param count: The number of bits to read
|
||||
'''
|
||||
key = self._get_prefix(key)
|
||||
s = divmod(offset, self._bit_size)[0]
|
||||
e = divmod(offset + count, self._bit_size)[0]
|
||||
|
||||
request = ('%s:%s' % (key, v) for v in range(s, e + 1))
|
||||
response = self.client.mget(request)
|
||||
return response
|
||||
|
||||
def _val_bit(self, key, offset, count):
|
||||
''' Validates that the given range is currently set in redis.
|
||||
If any of the keys return None, then it is invalid.
|
||||
|
||||
:param key: The key prefix to use
|
||||
:param offset: The address offset to start at
|
||||
:param count: The number of bits to read
|
||||
'''
|
||||
response = self._get_bit_values(key, offset, count)
|
||||
return True if None not in response else False
|
||||
|
||||
def _get_bit(self, key, offset, count):
|
||||
'''
|
||||
|
||||
:param key: The key prefix to use
|
||||
:param offset: The address offset to start at
|
||||
:param count: The number of bits to read
|
||||
'''
|
||||
response = self._get_bit_values(key, offset, count)
|
||||
response = (r or self._bit_default for r in response)
|
||||
result = ''.join(response)
|
||||
result = unpack_bitstring(result)
|
||||
return result[offset:offset + count]
|
||||
|
||||
def _set_bit(self, key, offset, values):
|
||||
'''
|
||||
|
||||
:param key: The key prefix to use
|
||||
:param offset: The address offset to start at
|
||||
:param values: The values to set
|
||||
'''
|
||||
count = len(values)
|
||||
s = divmod(offset, self._bit_size)[0]
|
||||
e = divmod(offset + count, self._bit_size)[0]
|
||||
value = pack_bitstring(values)
|
||||
|
||||
current = self._get_bit_values(key, offset, count)
|
||||
current = (r or self._bit_default for r in current)
|
||||
current = ''.join(current)
|
||||
current = current[0:offset] + value.decode('utf-8') + current[offset + count:]
|
||||
final = (current[s:s + self._bit_size] for s in range(0, count, self._bit_size))
|
||||
|
||||
key = self._get_prefix(key)
|
||||
request = ('%s:%s' % (key, v) for v in range(s, e + 1))
|
||||
request = dict(zip(request, final))
|
||||
self.client.mset(request)
|
||||
|
||||
#--------------------------------------------------------------------------#
|
||||
# Redis register implementation
|
||||
#--------------------------------------------------------------------------#
|
||||
_reg_size = 16
|
||||
_reg_default = '\x00' * (_reg_size % 8)
|
||||
|
||||
def _get_reg_values(self, key, offset, count):
|
||||
''' This is a helper to abstract getting register values
|
||||
|
||||
:param key: The key prefix to use
|
||||
:param offset: The address offset to start at
|
||||
:param count: The number of bits to read
|
||||
'''
|
||||
key = self._get_prefix(key)
|
||||
#s = divmod(offset, self.__reg_size)[0]
|
||||
#e = divmod(offset+count, self.__reg_size)[0]
|
||||
|
||||
#request = ('%s:%s' % (key, v) for v in range(s, e + 1))
|
||||
request = ('%s:%s' % (key, v) for v in range(offset, count + 1))
|
||||
response = self.client.mget(request)
|
||||
return response
|
||||
|
||||
def _val_reg(self, key, offset, count):
|
||||
''' Validates that the given range is currently set in redis.
|
||||
If any of the keys return None, then it is invalid.
|
||||
|
||||
:param key: The key prefix to use
|
||||
:param offset: The address offset to start at
|
||||
:param count: The number of bits to read
|
||||
'''
|
||||
response = self._get_reg_values(key, offset, count)
|
||||
return None not in response
|
||||
|
||||
def _get_reg(self, key, offset, count):
|
||||
'''
|
||||
|
||||
:param key: The key prefix to use
|
||||
:param offset: The address offset to start at
|
||||
:param count: The number of bits to read
|
||||
'''
|
||||
response = self._get_reg_values(key, offset, count)
|
||||
response = [r or self._reg_default for r in response]
|
||||
return response[offset:offset + count]
|
||||
|
||||
def _set_reg(self, key, offset, values):
|
||||
'''
|
||||
|
||||
:param key: The key prefix to use
|
||||
:param offset: The address offset to start at
|
||||
:param values: The values to set
|
||||
'''
|
||||
count = len(values)
|
||||
#s = divmod(offset, self.__reg_size)
|
||||
#e = divmod(offset+count, self.__reg_size)
|
||||
|
||||
#current = self.__get_reg_values(key, offset, count)
|
||||
|
||||
key = self._get_prefix(key)
|
||||
request = ('%s:%s' % (key, v) for v in range(offset, count + 1))
|
||||
request = dict(zip(request, values))
|
||||
self.client.mset(request)
|
||||
184
modbus/pymodbus/datastore/database/sql_datastore.py
Normal file
@@ -0,0 +1,184 @@
|
||||
import sqlalchemy
|
||||
import sqlalchemy.types as sqltypes
|
||||
from sqlalchemy.sql import and_
|
||||
from sqlalchemy.schema import UniqueConstraint
|
||||
from sqlalchemy.sql.expression import bindparam
|
||||
|
||||
from pymodbus.exceptions import NotImplementedException
|
||||
from pymodbus.interfaces import IModbusSlaveContext
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Logging
|
||||
# --------------------------------------------------------------------------- #
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Context
|
||||
# --------------------------------------------------------------------------- #
|
||||
class SqlSlaveContext(IModbusSlaveContext):
|
||||
"""
|
||||
This creates a modbus data model with each data access
|
||||
stored in its own personal block
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
""" Initializes the datastores
|
||||
|
||||
:param kwargs: Each element is a ModbusDataBlock
|
||||
"""
|
||||
self.table = kwargs.get('table', 'pymodbus')
|
||||
self.database = kwargs.get('database', 'sqlite:///pymodbus.db')
|
||||
self._db_create(self.table, self.database)
|
||||
|
||||
def __str__(self):
|
||||
""" Returns a string representation of the context
|
||||
|
||||
:returns: A string representation of the context
|
||||
"""
|
||||
return "Modbus Slave Context"
|
||||
|
||||
def reset(self):
|
||||
""" Resets all the datastores to their default values """
|
||||
self._metadata.drop_all()
|
||||
self._db_create(self.table, self.database)
|
||||
|
||||
def validate(self, fx, address, count=1):
|
||||
""" Validates the request to make sure it is in range
|
||||
|
||||
:param fx: The function we are working with
|
||||
:param address: The starting address
|
||||
:param count: The number of values to test
|
||||
:returns: True if the request in within range, False otherwise
|
||||
"""
|
||||
address = address + 1 # section 4.4 of specification
|
||||
_logger.debug("validate[%d] %d:%d" % (fx, address, count))
|
||||
return self._validate(self.decode(fx), address, count)
|
||||
|
||||
def getValues(self, fx, address, count=1):
|
||||
""" Get `count` values from datastore
|
||||
|
||||
:param fx: The function we are working with
|
||||
:param address: The starting address
|
||||
:param count: The number of values to retrieve
|
||||
:returns: The requested values from a:a+c
|
||||
"""
|
||||
address = address + 1 # section 4.4 of specification
|
||||
_logger.debug("get-values[%d] %d:%d" % (fx, address, count))
|
||||
return self._get(self.decode(fx), address, count)
|
||||
|
||||
def setValues(self, fx, address, values, update=True):
|
||||
""" Sets the datastore with the supplied values
|
||||
|
||||
:param fx: The function we are working with
|
||||
:param address: The starting address
|
||||
:param values: The new values to be set
|
||||
:param update: Update existing register in the db
|
||||
"""
|
||||
address = address + 1 # section 4.4 of specification
|
||||
_logger.debug("set-values[%d] %d:%d" % (fx, address, len(values)))
|
||||
if update:
|
||||
self._update(self.decode(fx), address, values)
|
||||
else:
|
||||
self._set(self.decode(fx), address, values)
|
||||
|
||||
# ----------------------------------------------------------------------- #
|
||||
# Sqlite Helper Methods
|
||||
# ----------------------------------------------------------------------- #
|
||||
def _db_create(self, table, database):
|
||||
""" A helper method to initialize the database and handles
|
||||
|
||||
:param table: The table name to create
|
||||
:param database: The database uri to use
|
||||
"""
|
||||
self._engine = sqlalchemy.create_engine(database, echo=False)
|
||||
self._metadata = sqlalchemy.MetaData(self._engine)
|
||||
self._table = sqlalchemy.Table(table, self._metadata,
|
||||
sqlalchemy.Column('type', sqltypes.String(1)),
|
||||
sqlalchemy.Column('index', sqltypes.Integer),
|
||||
sqlalchemy.Column('value', sqltypes.Integer),
|
||||
UniqueConstraint('type', 'index', name='key'))
|
||||
self._table.create(checkfirst=True)
|
||||
self._connection = self._engine.connect()
|
||||
|
||||
def _get(self, type, offset, count):
|
||||
"""
|
||||
:param type: The key prefix to use
|
||||
:param offset: The address offset to start at
|
||||
:param count: The number of bits to read
|
||||
:returns: The resulting values
|
||||
"""
|
||||
query = self._table.select(and_(
|
||||
self._table.c.type == type,
|
||||
self._table.c.index >= offset,
|
||||
self._table.c.index <= offset + count - 1)
|
||||
)
|
||||
query = query.order_by(self._table.c.index.asc())
|
||||
result = self._connection.execute(query).fetchall()
|
||||
return [row.value for row in result]
|
||||
|
||||
def _build_set(self, type, offset, values, prefix=''):
|
||||
""" A helper method to generate the sql update context
|
||||
|
||||
:param type: The key prefix to use
|
||||
:param offset: The address offset to start at
|
||||
:param values: The values to set
|
||||
:param prefix: Prefix fields index and type, defaults to empty string
|
||||
"""
|
||||
result = []
|
||||
for index, value in enumerate(values):
|
||||
result.append({
|
||||
prefix + 'type': type,
|
||||
prefix + 'index': offset + index,
|
||||
'value': value
|
||||
})
|
||||
return result
|
||||
|
||||
def _check(self, type, offset, values):
|
||||
result = self._get(type, offset, count=1)
|
||||
return False if len(result) > 0 else True
|
||||
|
||||
def _set(self, type, offset, values):
|
||||
"""
|
||||
|
||||
:param key: The type prefix to use
|
||||
:param offset: The address offset to start at
|
||||
:param values: The values to set
|
||||
"""
|
||||
if self._check(type, offset, values):
|
||||
context = self._build_set(type, offset, values)
|
||||
query = self._table.insert()
|
||||
result = self._connection.execute(query, context)
|
||||
return result.rowcount == len(values)
|
||||
else:
|
||||
return False
|
||||
|
||||
def _update(self, type, offset, values):
|
||||
"""
|
||||
|
||||
:param type: The type prefix to use
|
||||
:param offset: The address offset to start at
|
||||
:param values: The values to set
|
||||
"""
|
||||
context = self._build_set(type, offset, values, prefix='x_')
|
||||
query = self._table.update().values(value='value')
|
||||
query = query.where(and_(
|
||||
self._table.c.type == bindparam('x_type'),
|
||||
self._table.c.index == bindparam('x_index')))
|
||||
result = self._connection.execute(query, context)
|
||||
return result.rowcount == len(values)
|
||||
|
||||
def _validate(self, type, offset, count):
|
||||
"""
|
||||
:param key: The key prefix to use
|
||||
:param offset: The address offset to start at
|
||||
:param count: The number of bits to read
|
||||
:returns: The result of the validation
|
||||
"""
|
||||
query = self._table.select(and_(
|
||||
self._table.c.type == type,
|
||||
self._table.c.index >= offset,
|
||||
self._table.c.index <= offset + count - 1))
|
||||
result = self._connection.execute(query).fetchall()
|
||||
return len(result) == count
|
||||
108
modbus/pymodbus/datastore/remote.py
Normal file
@@ -0,0 +1,108 @@
|
||||
from pymodbus.exceptions import NotImplementedException
|
||||
from pymodbus.interfaces import IModbusSlaveContext
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Logging
|
||||
#---------------------------------------------------------------------------#
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Context
|
||||
#---------------------------------------------------------------------------#
|
||||
class RemoteSlaveContext(IModbusSlaveContext):
|
||||
''' TODO
|
||||
This creates a modbus data model that connects to
|
||||
a remote device (depending on the client used)
|
||||
'''
|
||||
|
||||
def __init__(self, client, unit=None):
|
||||
''' Initializes the datastores
|
||||
|
||||
:param client: The client to retrieve values with
|
||||
:param unit: Unit ID of the remote slave
|
||||
'''
|
||||
self._client = client
|
||||
self.unit = unit
|
||||
self.__build_mapping()
|
||||
|
||||
def reset(self):
|
||||
''' Resets all the datastores to their default values '''
|
||||
raise NotImplementedException()
|
||||
|
||||
def validate(self, fx, address, count=1):
|
||||
''' Validates the request to make sure it is in range
|
||||
|
||||
:param fx: The function we are working with
|
||||
:param address: The starting address
|
||||
:param count: The number of values to test
|
||||
:returns: True if the request in within range, False otherwise
|
||||
'''
|
||||
_logger.debug("validate[%d] %d:%d" % (fx, address, count))
|
||||
result = self.__get_callbacks[self.decode(fx)](address, count)
|
||||
return not result.isError()
|
||||
|
||||
def getValues(self, fx, address, count=1):
|
||||
''' Get `count` values from datastore
|
||||
|
||||
:param fx: The function we are working with
|
||||
:param address: The starting address
|
||||
:param count: The number of values to retrieve
|
||||
:returns: The requested values from a:a+c
|
||||
'''
|
||||
# TODO deal with deferreds
|
||||
_logger.debug("get values[%d] %d:%d" % (fx, address, count))
|
||||
result = self.__get_callbacks[self.decode(fx)](address, count)
|
||||
return self.__extract_result(self.decode(fx), result)
|
||||
|
||||
def setValues(self, fx, address, values):
|
||||
''' Sets the datastore with the supplied values
|
||||
|
||||
:param fx: The function we are working with
|
||||
:param address: The starting address
|
||||
:param values: The new values to be set
|
||||
'''
|
||||
# TODO deal with deferreds
|
||||
_logger.debug("set values[%d] %d:%d" % (fx, address, len(values)))
|
||||
self.__set_callbacks[self.decode(fx)](address, values)
|
||||
|
||||
def __str__(self):
|
||||
''' Returns a string representation of the context
|
||||
|
||||
:returns: A string representation of the context
|
||||
'''
|
||||
return "Remote Slave Context(%s)" % self._client
|
||||
|
||||
def __build_mapping(self):
|
||||
'''
|
||||
A quick helper method to build the function
|
||||
code mapper.
|
||||
'''
|
||||
kwargs = {}
|
||||
if self.unit:
|
||||
kwargs["unit"] = self.unit
|
||||
self.__get_callbacks = {
|
||||
'd': lambda a, c: self._client.read_discrete_inputs(a, c, **kwargs),
|
||||
'c': lambda a, c: self._client.read_coils(a, c, **kwargs),
|
||||
'h': lambda a, c: self._client.read_holding_registers(a, c, **kwargs),
|
||||
'i': lambda a, c: self._client.read_input_registers(a, c, **kwargs),
|
||||
}
|
||||
self.__set_callbacks = {
|
||||
'd': lambda a, v: self._client.write_coils(a, v, **kwargs),
|
||||
'c': lambda a, v: self._client.write_coils(a, v, **kwargs),
|
||||
'h': lambda a, v: self._client.write_registers(a, v, **kwargs),
|
||||
'i': lambda a, v: self._client.write_registers(a, v, **kwargs),
|
||||
}
|
||||
|
||||
def __extract_result(self, fx, result):
|
||||
''' A helper method to extract the values out of
|
||||
a response. TODO make this consistent (values?)
|
||||
'''
|
||||
if not result.isError():
|
||||
if fx in ['d', 'c']:
|
||||
return result.bits
|
||||
if fx in ['h', 'i']:
|
||||
return result.registers
|
||||
else:
|
||||
return result
|
||||
313
modbus/pymodbus/datastore/store.py
Normal file
@@ -0,0 +1,313 @@
|
||||
"""
|
||||
Modbus Server Datastore
|
||||
-------------------------
|
||||
|
||||
For each server, you will create a ModbusServerContext and pass
|
||||
in the default address space for each data access. The class
|
||||
will create and manage the data.
|
||||
|
||||
Further modification of said data accesses should be performed
|
||||
with [get,set][access]Values(address, count)
|
||||
|
||||
Datastore Implementation
|
||||
-------------------------
|
||||
|
||||
There are two ways that the server datastore can be implemented.
|
||||
The first is a complete range from 'address' start to 'count'
|
||||
number of indecies. This can be thought of as a straight array::
|
||||
|
||||
data = range(1, 1 + count)
|
||||
[1,2,3,...,count]
|
||||
|
||||
The other way that the datastore can be implemented (and how
|
||||
many devices implement it) is a associate-array::
|
||||
|
||||
data = {1:'1', 3:'3', ..., count:'count'}
|
||||
[1,3,...,count]
|
||||
|
||||
The difference between the two is that the latter will allow
|
||||
arbitrary gaps in its datastore while the former will not.
|
||||
This is seen quite commonly in some modbus implementations.
|
||||
What follows is a clear example from the field:
|
||||
|
||||
Say a company makes two devices to monitor power usage on a rack.
|
||||
One works with three-phase and the other with a single phase. The
|
||||
company will dictate a modbus data mapping such that registers::
|
||||
|
||||
n: phase 1 power
|
||||
n+1: phase 2 power
|
||||
n+2: phase 3 power
|
||||
|
||||
Using this, layout, the first device will implement n, n+1, and n+2,
|
||||
however, the second device may set the latter two values to 0 or
|
||||
will simply not implmented the registers thus causing a single read
|
||||
or a range read to fail.
|
||||
|
||||
I have both methods implemented, and leave it up to the user to change
|
||||
based on their preference.
|
||||
"""
|
||||
from pymodbus.exceptions import NotImplementedException, ParameterException
|
||||
from pymodbus.compat import iteritems, iterkeys, itervalues, get_next
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Logging
|
||||
#---------------------------------------------------------------------------#
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Datablock Storage
|
||||
#---------------------------------------------------------------------------#
|
||||
class BaseModbusDataBlock(object):
|
||||
'''
|
||||
Base class for a modbus datastore
|
||||
|
||||
Derived classes must create the following fields:
|
||||
@address The starting address point
|
||||
@defult_value The default value of the datastore
|
||||
@values The actual datastore values
|
||||
|
||||
Derived classes must implemented the following methods:
|
||||
validate(self, address, count=1)
|
||||
getValues(self, address, count=1)
|
||||
setValues(self, address, values)
|
||||
'''
|
||||
|
||||
def default(self, count, value=False):
|
||||
''' Used to initialize a store to one value
|
||||
|
||||
:param count: The number of fields to set
|
||||
:param value: The default value to set to the fields
|
||||
'''
|
||||
self.default_value = value
|
||||
self.values = [self.default_value] * count
|
||||
self.address = 0x00
|
||||
|
||||
def reset(self):
|
||||
''' Resets the datastore to the initialized default value '''
|
||||
self.values = [self.default_value] * len(self.values)
|
||||
|
||||
def validate(self, address, count=1):
|
||||
''' Checks to see if the request is in range
|
||||
|
||||
:param address: The starting address
|
||||
:param count: The number of values to test for
|
||||
:returns: True if the request in within range, False otherwise
|
||||
'''
|
||||
raise NotImplementedException("Datastore Address Check")
|
||||
|
||||
def getValues(self, address, count=1):
|
||||
''' Returns the requested values from the datastore
|
||||
|
||||
:param address: The starting address
|
||||
:param count: The number of values to retrieve
|
||||
:returns: The requested values from a:a+c
|
||||
'''
|
||||
raise NotImplementedException("Datastore Value Retrieve")
|
||||
|
||||
def setValues(self, address, values):
|
||||
''' Returns the requested values from the datastore
|
||||
|
||||
:param address: The starting address
|
||||
:param values: The values to store
|
||||
'''
|
||||
raise NotImplementedException("Datastore Value Retrieve")
|
||||
|
||||
def __str__(self):
|
||||
''' Build a representation of the datastore
|
||||
|
||||
:returns: A string representation of the datastore
|
||||
'''
|
||||
return "DataStore(%d, %d)" % (len(self.values), self.default_value)
|
||||
|
||||
def __iter__(self):
|
||||
''' Iterater over the data block data
|
||||
|
||||
:returns: An iterator of the data block data
|
||||
'''
|
||||
if isinstance(self.values, dict):
|
||||
return iteritems(self.values)
|
||||
return enumerate(self.values, self.address)
|
||||
|
||||
|
||||
class ModbusSequentialDataBlock(BaseModbusDataBlock):
|
||||
''' Creates a sequential modbus datastore '''
|
||||
|
||||
def __init__(self, address, values):
|
||||
''' Initializes the datastore
|
||||
|
||||
:param address: The starting address of the datastore
|
||||
:param values: Either a list or a dictionary of values
|
||||
'''
|
||||
self.address = address
|
||||
if hasattr(values, '__iter__'):
|
||||
self.values = list(values)
|
||||
else:
|
||||
self.values = [values]
|
||||
self.default_value = self.values[0].__class__()
|
||||
|
||||
@classmethod
|
||||
def create(klass):
|
||||
''' Factory method to create a datastore with the
|
||||
full address space initialized to 0x00
|
||||
|
||||
:returns: An initialized datastore
|
||||
'''
|
||||
return klass(0x00, [0x00] * 65536)
|
||||
|
||||
def validate(self, address, count=1):
|
||||
''' Checks to see if the request is in range
|
||||
|
||||
:param address: The starting address
|
||||
:param count: The number of values to test for
|
||||
:returns: True if the request in within range, False otherwise
|
||||
'''
|
||||
result = (self.address <= address)
|
||||
result &= ((self.address + len(self.values)) >= (address + count))
|
||||
return result
|
||||
|
||||
def getValues(self, address, count=1):
|
||||
''' Returns the requested values of the datastore
|
||||
|
||||
:param address: The starting address
|
||||
:param count: The number of values to retrieve
|
||||
:returns: The requested values from a:a+c
|
||||
'''
|
||||
start = address - self.address
|
||||
return self.values[start:start + count]
|
||||
|
||||
def setValues(self, address, values):
|
||||
''' Sets the requested values of the datastore
|
||||
|
||||
:param address: The starting address
|
||||
:param values: The new values to be set
|
||||
'''
|
||||
if not isinstance(values, list):
|
||||
values = [values]
|
||||
start = address - self.address
|
||||
self.values[start:start + len(values)] = values
|
||||
|
||||
|
||||
class ModbusSparseDataBlock(BaseModbusDataBlock):
|
||||
"""
|
||||
Creates a sparse modbus datastore
|
||||
|
||||
E.g Usage.
|
||||
sparse = ModbusSparseDataBlock({10: [3, 5, 6, 8], 30: 1, 40: [0]*20})
|
||||
|
||||
This would create a datablock with 3 blocks starting at
|
||||
offset 10 with length 4 , 30 with length 1 and 40 with length 20
|
||||
|
||||
sparse = ModbusSparseDataBlock([10]*100)
|
||||
Creates a sparse datablock of length 100 starting at offset 0 and default value of 10
|
||||
|
||||
sparse = ModbusSparseDataBlock() --> Create Empty datablock
|
||||
sparse.setValues(0, [10]*10) --> Add block 1 at offset 0 with length 10 (default value 10)
|
||||
sparse.setValues(30, [20]*5) --> Add block 2 at offset 30 with length 5 (default value 20)
|
||||
|
||||
if mutable is set to True during initialization, the datablock can not be altered with
|
||||
setValues (new datablocks can not be added)
|
||||
"""
|
||||
|
||||
def __init__(self, values=None, mutable=True):
|
||||
"""
|
||||
Initializes a sparse datastore. Will only answer to addresses
|
||||
registered, either initially here, or later via setValues()
|
||||
|
||||
:param values: Either a list or a dictionary of values
|
||||
:param mutable: The data-block can be altered later with setValues(i.e add more blocks)
|
||||
|
||||
If values are list , This is as good as sequential datablock.
|
||||
Values as dictionary should be in {offset: <values>} format, if values
|
||||
is a list, a sparse datablock is created starting at offset with the length of values.
|
||||
If values is a integer, then the value is set for the corresponding offset.
|
||||
|
||||
"""
|
||||
self.values = {}
|
||||
self._process_values(values)
|
||||
self.mutable = mutable
|
||||
self.default_value = self.values.copy()
|
||||
self.address = get_next(iterkeys(self.values), None)
|
||||
|
||||
@classmethod
|
||||
def create(klass, values=None):
|
||||
''' Factory method to create sparse datastore.
|
||||
Use setValues to initialize registers.
|
||||
|
||||
:param values: Either a list or a dictionary of values
|
||||
:returns: An initialized datastore
|
||||
'''
|
||||
return klass(values)
|
||||
|
||||
def reset(self):
|
||||
''' Reset the store to the initially provided defaults'''
|
||||
self.values = self.default_value.copy()
|
||||
|
||||
def validate(self, address, count=1):
|
||||
''' Checks to see if the request is in range
|
||||
|
||||
:param address: The starting address
|
||||
:param count: The number of values to test for
|
||||
:returns: True if the request in within range, False otherwise
|
||||
'''
|
||||
if count == 0:
|
||||
return False
|
||||
handle = set(range(address, address + count))
|
||||
return handle.issubset(set(iterkeys(self.values)))
|
||||
|
||||
def getValues(self, address, count=1):
|
||||
''' Returns the requested values of the datastore
|
||||
|
||||
:param address: The starting address
|
||||
:param count: The number of values to retrieve
|
||||
:returns: The requested values from a:a+c
|
||||
'''
|
||||
return [self.values[i] for i in range(address, address + count)]
|
||||
|
||||
def _process_values(self, values):
|
||||
def _process_as_dict(values):
|
||||
for idx, val in iteritems(values):
|
||||
if isinstance(val, (list, tuple)):
|
||||
for i, v in enumerate(val):
|
||||
self.values[idx + i] = v
|
||||
else:
|
||||
self.values[idx] = int(val)
|
||||
if isinstance(values, dict):
|
||||
_process_as_dict(values)
|
||||
return
|
||||
if hasattr(values, '__iter__'):
|
||||
values = dict(enumerate(values))
|
||||
elif values is None:
|
||||
values = {} # Must make a new dict here per instance
|
||||
else:
|
||||
raise ParameterException("Values for datastore must "
|
||||
"be a list or dictionary")
|
||||
_process_as_dict(values)
|
||||
|
||||
def setValues(self, address, values, use_as_default=False):
|
||||
''' Sets the requested values of the datastore
|
||||
|
||||
:param address: The starting address
|
||||
:param values: The new values to be set
|
||||
:param use_as_default: Use the values as default
|
||||
'''
|
||||
if isinstance(values, dict):
|
||||
new_offsets = list(set(list(values.keys())) - set(list(self.values.keys())))
|
||||
if new_offsets and not self.mutable:
|
||||
raise ParameterException("Offsets {} not "
|
||||
"in range".format(new_offsets))
|
||||
self._process_values(values)
|
||||
else:
|
||||
if not isinstance(values, list):
|
||||
values = [values]
|
||||
for idx, val in enumerate(values):
|
||||
if address+idx not in self.values and not self.mutable:
|
||||
raise ParameterException("Offset {} not "
|
||||
"in range".format(address+idx))
|
||||
self.values[address + idx] = val
|
||||
if not self.address:
|
||||
self.address = get_next(iterkeys(self.values), None)
|
||||
if use_as_default:
|
||||
for idx, val in iteritems(self.values):
|
||||
self.default_value[idx] = val
|
||||
626
modbus/pymodbus/device.py
Normal file
@@ -0,0 +1,626 @@
|
||||
"""
|
||||
Modbus Device Controller
|
||||
-------------------------
|
||||
|
||||
These are the device management handlers. They should be
|
||||
maintained in the server context and the various methods
|
||||
should be inserted in the correct locations.
|
||||
"""
|
||||
from pymodbus.constants import DeviceInformation
|
||||
from pymodbus.interfaces import Singleton
|
||||
from pymodbus.utilities import dict_property
|
||||
from pymodbus.compat import iteritems, itervalues, izip, int2byte
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Network Access Control
|
||||
#---------------------------------------------------------------------------#
|
||||
class ModbusAccessControl(Singleton):
|
||||
'''
|
||||
This is a simple implementation of a Network Management System table.
|
||||
Its purpose is to control access to the server (if it is used).
|
||||
We assume that if an entry is in the table, it is allowed accesses to
|
||||
resources. However, if the host does not appear in the table (all
|
||||
unknown hosts) its connection will simply be closed.
|
||||
|
||||
Since it is a singleton, only one version can possible exist and all
|
||||
instances pull from here.
|
||||
'''
|
||||
__nmstable = [
|
||||
"127.0.0.1",
|
||||
]
|
||||
|
||||
def __iter__(self):
|
||||
''' Iterater over the network access table
|
||||
|
||||
:returns: An iterator of the network access table
|
||||
'''
|
||||
return self.__nmstable.__iter__()
|
||||
|
||||
def __contains__(self, host):
|
||||
''' Check if a host is allowed to access resources
|
||||
|
||||
:param host: The host to check
|
||||
'''
|
||||
return host in self.__nmstable
|
||||
|
||||
def add(self, host):
|
||||
''' Add allowed host(s) from the NMS table
|
||||
|
||||
:param host: The host to add
|
||||
'''
|
||||
if not isinstance(host, list):
|
||||
host = [host]
|
||||
for entry in host:
|
||||
if entry not in self.__nmstable:
|
||||
self.__nmstable.append(entry)
|
||||
|
||||
def remove(self, host):
|
||||
''' Remove allowed host(s) from the NMS table
|
||||
|
||||
:param host: The host to remove
|
||||
'''
|
||||
if not isinstance(host, list):
|
||||
host = [host]
|
||||
for entry in host:
|
||||
if entry in self.__nmstable:
|
||||
self.__nmstable.remove(entry)
|
||||
|
||||
def check(self, host):
|
||||
''' Check if a host is allowed to access resources
|
||||
|
||||
:param host: The host to check
|
||||
'''
|
||||
return host in self.__nmstable
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Modbus Plus Statistics
|
||||
#---------------------------------------------------------------------------#
|
||||
class ModbusPlusStatistics(object):
|
||||
'''
|
||||
This is used to maintain the current modbus plus statistics count. As of
|
||||
right now this is simply a stub to complete the modbus implementation.
|
||||
For more information, see the modbus implementation guide page 87.
|
||||
'''
|
||||
|
||||
__data = OrderedDict({
|
||||
'node_type_id' : [0x00] * 2, # 00
|
||||
'software_version_number' : [0x00] * 2, # 01
|
||||
'network_address' : [0x00] * 2, # 02
|
||||
'mac_state_variable' : [0x00] * 2, # 03
|
||||
'peer_status_code' : [0x00] * 2, # 04
|
||||
'token_pass_counter' : [0x00] * 2, # 05
|
||||
'token_rotation_time' : [0x00] * 2, # 06
|
||||
|
||||
'program_master_token_failed' : [0x00], # 07 hi
|
||||
'data_master_token_failed' : [0x00], # 07 lo
|
||||
'program_master_token_owner' : [0x00], # 08 hi
|
||||
'data_master_token_owner' : [0x00], # 08 lo
|
||||
'program_slave_token_owner' : [0x00], # 09 hi
|
||||
'data_slave_token_owner' : [0x00], # 09 lo
|
||||
'data_slave_command_transfer' : [0x00], # 10 hi
|
||||
'__unused_10_lowbit' : [0x00], # 10 lo
|
||||
|
||||
'program_slave_command_transfer' : [0x00], # 11 hi
|
||||
'program_master_rsp_transfer' : [0x00], # 11 lo
|
||||
'program_slave_auto_logout' : [0x00], # 12 hi
|
||||
'program_master_connect_status' : [0x00], # 12 lo
|
||||
'receive_buffer_dma_overrun' : [0x00], # 13 hi
|
||||
'pretransmit_deferral_error' : [0x00], # 13 lo
|
||||
'frame_size_error' : [0x00], # 14 hi
|
||||
'repeated_command_received' : [0x00], # 14 lo
|
||||
'receiver_alignment_error' : [0x00], # 15 hi
|
||||
'receiver_collision_abort_error' : [0x00], # 15 lo
|
||||
'bad_packet_length_error' : [0x00], # 16 hi
|
||||
'receiver_crc_error' : [0x00], # 16 lo
|
||||
'transmit_buffer_dma_underrun' : [0x00], # 17 hi
|
||||
'bad_link_address_error' : [0x00], # 17 lo
|
||||
|
||||
'bad_mac_function_code_error' : [0x00], # 18 hi
|
||||
'internal_packet_length_error' : [0x00], # 18 lo
|
||||
'communication_failed_error' : [0x00], # 19 hi
|
||||
'communication_retries' : [0x00], # 19 lo
|
||||
'no_response_error' : [0x00], # 20 hi
|
||||
'good_receive_packet' : [0x00], # 20 lo
|
||||
'unexpected_path_error' : [0x00], # 21 hi
|
||||
'exception_response_error' : [0x00], # 21 lo
|
||||
'forgotten_transaction_error' : [0x00], # 22 hi
|
||||
'unexpected_response_error' : [0x00], # 22 lo
|
||||
|
||||
'active_station_bit_map' : [0x00] * 8, # 23-26
|
||||
'token_station_bit_map' : [0x00] * 8, # 27-30
|
||||
'global_data_bit_map' : [0x00] * 8, # 31-34
|
||||
'receive_buffer_use_bit_map' : [0x00] * 8, # 35-37
|
||||
'data_master_output_path' : [0x00] * 8, # 38-41
|
||||
'data_slave_input_path' : [0x00] * 8, # 42-45
|
||||
'program_master_outptu_path' : [0x00] * 8, # 46-49
|
||||
'program_slave_input_path' : [0x00] * 8, # 50-53
|
||||
})
|
||||
|
||||
def __init__(self):
|
||||
'''
|
||||
Initialize the modbus plus statistics with the default
|
||||
information.
|
||||
'''
|
||||
self.reset()
|
||||
|
||||
def __iter__(self):
|
||||
''' Iterater over the statistics
|
||||
|
||||
:returns: An iterator of the modbus plus statistics
|
||||
'''
|
||||
return iteritems(self.__data)
|
||||
|
||||
def reset(self):
|
||||
''' This clears all of the modbus plus statistics
|
||||
'''
|
||||
for key in self.__data:
|
||||
self.__data[key] = [0x00] * len(self.__data[key])
|
||||
|
||||
def summary(self):
|
||||
''' Returns a summary of the modbus plus statistics
|
||||
|
||||
:returns: 54 16-bit words representing the status
|
||||
'''
|
||||
return itervalues(self.__data)
|
||||
|
||||
def encode(self):
|
||||
''' Returns a summary of the modbus plus statistics
|
||||
|
||||
:returns: 54 16-bit words representing the status
|
||||
'''
|
||||
total, values = [], sum(self.__data.values(), [])
|
||||
for c in range(0, len(values), 2):
|
||||
total.append((values[c] << 8) | values[c+1])
|
||||
return total
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Device Information Control
|
||||
#---------------------------------------------------------------------------#
|
||||
class ModbusDeviceIdentification(object):
|
||||
'''
|
||||
This is used to supply the device identification
|
||||
for the readDeviceIdentification function
|
||||
|
||||
For more information read section 6.21 of the modbus
|
||||
application protocol.
|
||||
'''
|
||||
__data = {
|
||||
0x00: '', # VendorName
|
||||
0x01: '', # ProductCode
|
||||
0x02: '', # MajorMinorRevision
|
||||
0x03: '', # VendorUrl
|
||||
0x04: '', # ProductName
|
||||
0x05: '', # ModelName
|
||||
0x06: '', # UserApplicationName
|
||||
0x07: '', # reserved
|
||||
0x08: '', # reserved
|
||||
# 0x80 -> 0xFF are private
|
||||
}
|
||||
|
||||
__names = [
|
||||
'VendorName',
|
||||
'ProductCode',
|
||||
'MajorMinorRevision',
|
||||
'VendorUrl',
|
||||
'ProductName',
|
||||
'ModelName',
|
||||
'UserApplicationName',
|
||||
]
|
||||
|
||||
def __init__(self, info=None):
|
||||
'''
|
||||
Initialize the datastore with the elements you need.
|
||||
(note acceptable range is [0x00-0x06,0x80-0xFF] inclusive)
|
||||
|
||||
:param information: A dictionary of {int:string} of values
|
||||
'''
|
||||
if isinstance(info, dict):
|
||||
for key in info:
|
||||
if (0x06 >= key >= 0x00) or (0xFF >= key >= 0x80):
|
||||
self.__data[key] = info[key]
|
||||
|
||||
def __iter__(self):
|
||||
''' Iterater over the device information
|
||||
|
||||
:returns: An iterator of the device information
|
||||
'''
|
||||
return iteritems(self.__data)
|
||||
|
||||
def summary(self):
|
||||
''' Return a summary of the main items
|
||||
|
||||
:returns: An dictionary of the main items
|
||||
'''
|
||||
return dict(zip(self.__names, itervalues(self.__data)))
|
||||
|
||||
def update(self, value):
|
||||
''' Update the values of this identity
|
||||
using another identify as the value
|
||||
|
||||
:param value: The value to copy values from
|
||||
'''
|
||||
self.__data.update(value)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
''' Wrapper used to access the device information
|
||||
|
||||
:param key: The register to set
|
||||
:param value: The new value for referenced register
|
||||
'''
|
||||
if key not in [0x07, 0x08]:
|
||||
self.__data[key] = value
|
||||
|
||||
def __getitem__(self, key):
|
||||
''' Wrapper used to access the device information
|
||||
|
||||
:param key: The register to read
|
||||
'''
|
||||
return self.__data.setdefault(key, '')
|
||||
|
||||
def __str__(self):
|
||||
''' Build a representation of the device
|
||||
|
||||
:returns: A string representation of the device
|
||||
'''
|
||||
return "DeviceIdentity"
|
||||
|
||||
#-------------------------------------------------------------------------#
|
||||
# Properties
|
||||
#-------------------------------------------------------------------------#
|
||||
VendorName = dict_property(lambda s: s.__data, 0)
|
||||
ProductCode = dict_property(lambda s: s.__data, 1)
|
||||
MajorMinorRevision = dict_property(lambda s: s.__data, 2)
|
||||
VendorUrl = dict_property(lambda s: s.__data, 3)
|
||||
ProductName = dict_property(lambda s: s.__data, 4)
|
||||
ModelName = dict_property(lambda s: s.__data, 5)
|
||||
UserApplicationName = dict_property(lambda s: s.__data, 6)
|
||||
|
||||
|
||||
class DeviceInformationFactory(Singleton):
|
||||
''' This is a helper factory that really just hides
|
||||
some of the complexity of processing the device information
|
||||
requests (function code 0x2b 0x0e).
|
||||
'''
|
||||
|
||||
__lookup = {
|
||||
DeviceInformation.Basic: lambda c, r, i: c.__gets(r, list(range(i, 0x03))),
|
||||
DeviceInformation.Regular: lambda c, r, i: c.__gets(r, list(range(i, 0x07))
|
||||
if c.__get(r, i)[i] else list(range(0, 0x07))),
|
||||
DeviceInformation.Extended: lambda c, r, i: c.__gets(r,
|
||||
[x for x in range(i, 0x100) if x not in range(0x07, 0x80)]
|
||||
if c.__get(r, i)[i] else
|
||||
[x for x in range(0, 0x100) if x not in range(0x07, 0x80)]),
|
||||
DeviceInformation.Specific: lambda c, r, i: c.__get(r, i),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get(cls, control, read_code=DeviceInformation.Basic, object_id=0x00):
|
||||
''' Get the requested device data from the system
|
||||
|
||||
:param control: The control block to pull data from
|
||||
:param read_code: The read code to process
|
||||
:param object_id: The specific object_id to read
|
||||
:returns: The requested data (id, length, value)
|
||||
'''
|
||||
identity = control.Identity
|
||||
return cls.__lookup[read_code](cls, identity, object_id)
|
||||
|
||||
@classmethod
|
||||
def __get(cls, identity, object_id):
|
||||
''' Read a single object_id from the device information
|
||||
|
||||
:param identity: The identity block to pull data from
|
||||
:param object_id: The specific object id to read
|
||||
:returns: The requested data (id, length, value)
|
||||
'''
|
||||
return { object_id:identity[object_id] }
|
||||
|
||||
@classmethod
|
||||
def __gets(cls, identity, object_ids):
|
||||
''' Read multiple object_ids from the device information
|
||||
|
||||
:param identity: The identity block to pull data from
|
||||
:param object_ids: The specific object ids to read
|
||||
:returns: The requested data (id, length, value)
|
||||
'''
|
||||
return dict((oid, identity[oid]) for oid in object_ids if identity[oid])
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Counters Handler
|
||||
#---------------------------------------------------------------------------#
|
||||
class ModbusCountersHandler(object):
|
||||
'''
|
||||
This is a helper class to simplify the properties for the counters::
|
||||
|
||||
0x0B 1 Return Bus Message Count
|
||||
|
||||
Quantity of messages that the remote
|
||||
device has detected on the communications system since its
|
||||
last restart, clear counters operation, or power-up. Messages
|
||||
with bad CRC are not taken into account.
|
||||
|
||||
0x0C 2 Return Bus Communication Error Count
|
||||
|
||||
Quantity of CRC errors encountered by the remote device since its
|
||||
last restart, clear counters operation, or power-up. In case of
|
||||
an error detected on the character level, (overrun, parity error),
|
||||
or in case of a message length < 3 bytes, the receiving device is
|
||||
not able to calculate the CRC. In such cases, this counter is
|
||||
also incremented.
|
||||
|
||||
0x0D 3 Return Slave Exception Error Count
|
||||
|
||||
Quantity of MODBUS exception error detected by the remote device
|
||||
since its last restart, clear counters operation, or power-up. It
|
||||
comprises also the error detected in broadcast messages even if an
|
||||
exception message is not returned in this case.
|
||||
Exception errors are described and listed in "MODBUS Application
|
||||
Protocol Specification" document.
|
||||
|
||||
0xOE 4 Return Slave Message Count
|
||||
|
||||
Quantity of messages addressed to the remote device, including
|
||||
broadcast messages, that the remote device has processed since its
|
||||
last restart, clear counters operation, or power-up.
|
||||
|
||||
0x0F 5 Return Slave No Response Count
|
||||
|
||||
Quantity of messages received by the remote device for which it
|
||||
returned no response (neither a normal response nor an exception
|
||||
response), since its last restart, clear counters operation, or
|
||||
power-up. Then, this counter counts the number of broadcast
|
||||
messages it has received.
|
||||
|
||||
0x10 6 Return Slave NAK Count
|
||||
|
||||
Quantity of messages addressed to the remote device for which it
|
||||
returned a Negative Acknowledge (NAK) exception response, since
|
||||
its last restart, clear counters operation, or power-up. Exception
|
||||
responses are described and listed in "MODBUS Application Protocol
|
||||
Specification" document.
|
||||
|
||||
0x11 7 Return Slave Busy Count
|
||||
|
||||
Quantity of messages addressed to the remote device for which it
|
||||
returned a Slave Device Busy exception response, since its last
|
||||
restart, clear counters operation, or power-up. Exception
|
||||
responses are described and listed in "MODBUS Application
|
||||
Protocol Specification" document.
|
||||
|
||||
0x12 8 Return Bus Character Overrun Count
|
||||
|
||||
Quantity of messages addressed to the remote device that it could
|
||||
not handle due to a character overrun condition, since its last
|
||||
restart, clear counters operation, or power-up. A character
|
||||
overrun is caused by data characters arriving at the port faster
|
||||
than they can.
|
||||
|
||||
.. note:: I threw the event counter in here for convinience
|
||||
'''
|
||||
__data = dict([(i, 0x0000) for i in range(9)])
|
||||
__names = [
|
||||
'BusMessage',
|
||||
'BusCommunicationError',
|
||||
'SlaveExceptionError',
|
||||
'SlaveMessage',
|
||||
'SlaveNoResponse',
|
||||
'SlaveNAK',
|
||||
'SlaveBusy',
|
||||
'BusCharacterOverrun'
|
||||
'Event '
|
||||
]
|
||||
|
||||
def __iter__(self):
|
||||
''' Iterater over the device counters
|
||||
|
||||
:returns: An iterator of the device counters
|
||||
'''
|
||||
return izip(self.__names, itervalues(self.__data))
|
||||
|
||||
def update(self, values):
|
||||
''' Update the values of this identity
|
||||
using another identify as the value
|
||||
|
||||
:param values: The value to copy values from
|
||||
'''
|
||||
for k, v in iteritems(values):
|
||||
v += self.__getattribute__(k)
|
||||
self.__setattr__(k, v)
|
||||
|
||||
def reset(self):
|
||||
''' This clears all of the system counters
|
||||
'''
|
||||
self.__data = dict([(i, 0x0000) for i in range(9)])
|
||||
|
||||
def summary(self):
|
||||
''' Returns a summary of the counters current status
|
||||
|
||||
:returns: A byte with each bit representing each counter
|
||||
'''
|
||||
count, result = 0x01, 0x00
|
||||
for i in itervalues(self.__data):
|
||||
if i != 0x00: result |= count
|
||||
count <<= 1
|
||||
return result
|
||||
|
||||
#-------------------------------------------------------------------------#
|
||||
# Properties
|
||||
#-------------------------------------------------------------------------#
|
||||
BusMessage = dict_property(lambda s: s.__data, 0)
|
||||
BusCommunicationError = dict_property(lambda s: s.__data, 1)
|
||||
BusExceptionError = dict_property(lambda s: s.__data, 2)
|
||||
SlaveMessage = dict_property(lambda s: s.__data, 3)
|
||||
SlaveNoResponse = dict_property(lambda s: s.__data, 4)
|
||||
SlaveNAK = dict_property(lambda s: s.__data, 5)
|
||||
SlaveBusy = dict_property(lambda s: s.__data, 6)
|
||||
BusCharacterOverrun = dict_property(lambda s: s.__data, 7)
|
||||
Event = dict_property(lambda s: s.__data, 8)
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Main server control block
|
||||
#---------------------------------------------------------------------------#
|
||||
class ModbusControlBlock(Singleton):
|
||||
'''
|
||||
This is a global singleton that controls all system information
|
||||
|
||||
All activity should be logged here and all diagnostic requests
|
||||
should come from here.
|
||||
'''
|
||||
|
||||
__mode = 'ASCII'
|
||||
__diagnostic = [False] * 16
|
||||
__instance = None
|
||||
__listen_only = False
|
||||
__delimiter = '\r'
|
||||
__counters = ModbusCountersHandler()
|
||||
__identity = ModbusDeviceIdentification()
|
||||
__plus = ModbusPlusStatistics()
|
||||
__events = []
|
||||
|
||||
#-------------------------------------------------------------------------#
|
||||
# Magic
|
||||
#-------------------------------------------------------------------------#
|
||||
def __str__(self):
|
||||
''' Build a representation of the control block
|
||||
|
||||
:returns: A string representation of the control block
|
||||
'''
|
||||
return "ModbusControl"
|
||||
|
||||
def __iter__(self):
|
||||
''' Iterater over the device counters
|
||||
|
||||
:returns: An iterator of the device counters
|
||||
'''
|
||||
return self.__counters.__iter__()
|
||||
|
||||
#-------------------------------------------------------------------------#
|
||||
# Events
|
||||
#-------------------------------------------------------------------------#
|
||||
def addEvent(self, event):
|
||||
''' Adds a new event to the event log
|
||||
|
||||
:param event: A new event to add to the log
|
||||
'''
|
||||
self.__events.insert(0, event)
|
||||
self.__events = self.__events[0:64] # chomp to 64 entries
|
||||
self.Counter.Event += 1
|
||||
|
||||
def getEvents(self):
|
||||
''' Returns an encoded collection of the event log.
|
||||
|
||||
:returns: The encoded events packet
|
||||
'''
|
||||
events = [event.encode() for event in self.__events]
|
||||
return b''.join(events)
|
||||
|
||||
def clearEvents(self):
|
||||
''' Clears the current list of events
|
||||
'''
|
||||
self.__events = []
|
||||
|
||||
#-------------------------------------------------------------------------#
|
||||
# Other Properties
|
||||
#-------------------------------------------------------------------------#
|
||||
Identity = property(lambda s: s.__identity)
|
||||
Counter = property(lambda s: s.__counters)
|
||||
Events = property(lambda s: s.__events)
|
||||
Plus = property(lambda s: s.__plus)
|
||||
|
||||
def reset(self):
|
||||
''' This clears all of the system counters and the
|
||||
diagnostic register
|
||||
'''
|
||||
self.__events = []
|
||||
self.__counters.reset()
|
||||
self.__diagnostic = [False] * 16
|
||||
|
||||
#-------------------------------------------------------------------------#
|
||||
# Listen Properties
|
||||
#-------------------------------------------------------------------------#
|
||||
def _setListenOnly(self, value):
|
||||
''' This toggles the listen only status
|
||||
|
||||
:param value: The value to set the listen status to
|
||||
'''
|
||||
self.__listen_only = bool(value)
|
||||
|
||||
ListenOnly = property(lambda s: s.__listen_only, _setListenOnly)
|
||||
|
||||
#-------------------------------------------------------------------------#
|
||||
# Mode Properties
|
||||
#-------------------------------------------------------------------------#
|
||||
def _setMode(self, mode):
|
||||
''' This toggles the current serial mode
|
||||
|
||||
:param mode: The data transfer method in (RTU, ASCII)
|
||||
'''
|
||||
if mode in ['ASCII', 'RTU']:
|
||||
self.__mode = mode
|
||||
|
||||
Mode = property(lambda s: s.__mode, _setMode)
|
||||
|
||||
#-------------------------------------------------------------------------#
|
||||
# Delimiter Properties
|
||||
#-------------------------------------------------------------------------#
|
||||
def _setDelimiter(self, char):
|
||||
''' This changes the serial delimiter character
|
||||
|
||||
:param char: The new serial delimiter character
|
||||
'''
|
||||
if isinstance(char, str):
|
||||
self.__delimiter = char.encode()
|
||||
if isinstance(char, bytes):
|
||||
self.__delimiter = char
|
||||
elif isinstance(char, int):
|
||||
self.__delimiter = int2byte(char)
|
||||
|
||||
Delimiter = property(lambda s: s.__delimiter, _setDelimiter)
|
||||
|
||||
#-------------------------------------------------------------------------#
|
||||
# Diagnostic Properties
|
||||
#-------------------------------------------------------------------------#
|
||||
def setDiagnostic(self, mapping):
|
||||
''' This sets the value in the diagnostic register
|
||||
|
||||
:param mapping: Dictionary of key:value pairs to set
|
||||
'''
|
||||
for entry in iteritems(mapping):
|
||||
if entry[0] >= 0 and entry[0] < len(self.__diagnostic):
|
||||
self.__diagnostic[entry[0]] = (entry[1] != 0)
|
||||
|
||||
def getDiagnostic(self, bit):
|
||||
''' This gets the value in the diagnostic register
|
||||
|
||||
:param bit: The bit to get
|
||||
:returns: The current value of the requested bit
|
||||
'''
|
||||
try:
|
||||
if bit and bit >= 0 and bit < len(self.__diagnostic):
|
||||
return self.__diagnostic[bit]
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def getDiagnosticRegister(self):
|
||||
''' This gets the entire diagnostic register
|
||||
|
||||
:returns: The diagnostic register collection
|
||||
'''
|
||||
return self.__diagnostic
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Exported Identifiers
|
||||
#---------------------------------------------------------------------------#
|
||||
__all__ = [
|
||||
"ModbusAccessControl",
|
||||
"ModbusPlusStatistics",
|
||||
"ModbusDeviceIdentification",
|
||||
"DeviceInformationFactory",
|
||||
"ModbusControlBlock"
|
||||
]
|
||||
790
modbus/pymodbus/diag_message.py
Normal file
@@ -0,0 +1,790 @@
|
||||
'''
|
||||
Diagnostic Record Read/Write
|
||||
------------------------------
|
||||
|
||||
These need to be tied into a the current server context
|
||||
or linked to the appropriate data
|
||||
'''
|
||||
import struct
|
||||
|
||||
from pymodbus.constants import ModbusStatus, ModbusPlusOperation
|
||||
from pymodbus.pdu import ModbusRequest
|
||||
from pymodbus.pdu import ModbusResponse
|
||||
from pymodbus.device import ModbusControlBlock
|
||||
from pymodbus.exceptions import NotImplementedException
|
||||
from pymodbus.utilities import pack_bitstring
|
||||
|
||||
_MCB = ModbusControlBlock()
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Diagnostic Function Codes Base Classes
|
||||
# diagnostic 08, 00-18,20
|
||||
#---------------------------------------------------------------------------#
|
||||
# TODO Make sure all the data is decoded from the response
|
||||
#---------------------------------------------------------------------------#
|
||||
class DiagnosticStatusRequest(ModbusRequest):
|
||||
'''
|
||||
This is a base class for all of the diagnostic request functions
|
||||
'''
|
||||
function_code = 0x08
|
||||
_rtu_frame_size = 8
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
'''
|
||||
Base initializer for a diagnostic request
|
||||
'''
|
||||
ModbusRequest.__init__(self, **kwargs)
|
||||
self.message = None
|
||||
|
||||
def encode(self):
|
||||
'''
|
||||
Base encoder for a diagnostic response
|
||||
we encode the data set in self.message
|
||||
|
||||
:returns: The encoded packet
|
||||
'''
|
||||
packet = struct.pack('>H', self.sub_function_code)
|
||||
if self.message is not None:
|
||||
if isinstance(self.message, str):
|
||||
packet += self.message.encode()
|
||||
elif isinstance(self.message, bytes):
|
||||
packet += self.message
|
||||
elif isinstance(self.message, list):
|
||||
for piece in self.message:
|
||||
packet += struct.pack('>H', piece)
|
||||
elif isinstance(self.message, int):
|
||||
packet += struct.pack('>H', self.message)
|
||||
return packet
|
||||
|
||||
def decode(self, data):
|
||||
''' Base decoder for a diagnostic request
|
||||
|
||||
:param data: The data to decode into the function code
|
||||
'''
|
||||
self.sub_function_code, self.message = struct.unpack('>HH', data)
|
||||
|
||||
def get_response_pdu_size(self):
|
||||
"""
|
||||
Func_code (1 byte) + Sub function code (2 byte) + Data (2 * N bytes)
|
||||
:return:
|
||||
"""
|
||||
if not isinstance(self.message,list):
|
||||
self.message = [self.message]
|
||||
return 1 + 2 + 2 * len(self.message)
|
||||
|
||||
|
||||
class DiagnosticStatusResponse(ModbusResponse):
|
||||
'''
|
||||
This is a base class for all of the diagnostic response functions
|
||||
|
||||
It works by performing all of the encoding and decoding of variable
|
||||
data and lets the higher classes define what extra data to append
|
||||
and how to execute a request
|
||||
'''
|
||||
function_code = 0x08
|
||||
_rtu_frame_size = 8
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
'''
|
||||
Base initializer for a diagnostic response
|
||||
'''
|
||||
ModbusResponse.__init__(self, **kwargs)
|
||||
self.message = None
|
||||
|
||||
def encode(self):
|
||||
'''
|
||||
Base encoder for a diagnostic response
|
||||
we encode the data set in self.message
|
||||
|
||||
:returns: The encoded packet
|
||||
'''
|
||||
packet = struct.pack('>H', self.sub_function_code)
|
||||
if self.message is not None:
|
||||
if isinstance(self.message, str):
|
||||
packet += self.message.encode()
|
||||
elif isinstance(self.message, bytes):
|
||||
packet += self.message
|
||||
elif isinstance(self.message, list):
|
||||
for piece in self.message:
|
||||
packet += struct.pack('>H', piece)
|
||||
elif isinstance(self.message, int):
|
||||
packet += struct.pack('>H', self.message)
|
||||
return packet
|
||||
|
||||
def decode(self, data):
|
||||
''' Base decoder for a diagnostic response
|
||||
|
||||
:param data: The data to decode into the function code
|
||||
'''
|
||||
word_len = len(data)//2
|
||||
if len(data) % 2:
|
||||
word_len += 1
|
||||
data = data + b'0'
|
||||
data = struct.unpack('>' + 'H'*word_len, data)
|
||||
self.sub_function_code, self.message = data[0], data[1:]
|
||||
|
||||
|
||||
class DiagnosticStatusSimpleRequest(DiagnosticStatusRequest):
|
||||
'''
|
||||
A large majority of the diagnostic functions are simple
|
||||
status request functions. They work by sending 0x0000
|
||||
as data and their function code and they are returned
|
||||
2 bytes of data.
|
||||
|
||||
If a function inherits this, they only need to implement
|
||||
the execute method
|
||||
'''
|
||||
|
||||
def __init__(self, data=0x0000, **kwargs):
|
||||
'''
|
||||
General initializer for a simple diagnostic request
|
||||
|
||||
The data defaults to 0x0000 if not provided as over half
|
||||
of the functions require it.
|
||||
|
||||
:param data: The data to send along with the request
|
||||
'''
|
||||
DiagnosticStatusRequest.__init__(self, **kwargs)
|
||||
self.message = data
|
||||
|
||||
def execute(self, *args):
|
||||
''' Base function to raise if not implemented '''
|
||||
raise NotImplementedException("Diagnostic Message Has No Execute Method")
|
||||
|
||||
|
||||
class DiagnosticStatusSimpleResponse(DiagnosticStatusResponse):
|
||||
'''
|
||||
A large majority of the diagnostic functions are simple
|
||||
status request functions. They work by sending 0x0000
|
||||
as data and their function code and they are returned
|
||||
2 bytes of data.
|
||||
'''
|
||||
|
||||
def __init__(self, data=0x0000, **kwargs):
|
||||
''' General initializer for a simple diagnostic response
|
||||
|
||||
:param data: The resulting data to return to the client
|
||||
'''
|
||||
DiagnosticStatusResponse.__init__(self, **kwargs)
|
||||
self.message = data
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Diagnostic Sub Code 00
|
||||
#---------------------------------------------------------------------------#
|
||||
class ReturnQueryDataRequest(DiagnosticStatusRequest):
|
||||
'''
|
||||
The data passed in the request data field is to be returned (looped back)
|
||||
in the response. The entire response message should be identical to the
|
||||
request.
|
||||
'''
|
||||
sub_function_code = 0x0000
|
||||
|
||||
def __init__(self, message=0x0000, **kwargs):
|
||||
''' Initializes a new instance of the request
|
||||
|
||||
:param message: The message to send to loopback
|
||||
'''
|
||||
DiagnosticStatusRequest.__init__(self, **kwargs)
|
||||
if isinstance(message, list):
|
||||
self.message = message
|
||||
else:
|
||||
self.message = [message]
|
||||
|
||||
def execute(self, *args):
|
||||
''' Executes the loopback request (builds the response)
|
||||
|
||||
:returns: The populated loopback response message
|
||||
'''
|
||||
return ReturnQueryDataResponse(self.message)
|
||||
|
||||
|
||||
class ReturnQueryDataResponse(DiagnosticStatusResponse):
|
||||
'''
|
||||
The data passed in the request data field is to be returned (looped back)
|
||||
in the response. The entire response message should be identical to the
|
||||
request.
|
||||
'''
|
||||
sub_function_code = 0x0000
|
||||
|
||||
def __init__(self, message=0x0000, **kwargs):
|
||||
''' Initializes a new instance of the response
|
||||
|
||||
:param message: The message to loopback
|
||||
'''
|
||||
DiagnosticStatusResponse.__init__(self, **kwargs)
|
||||
if isinstance(message, list):
|
||||
self.message = message
|
||||
else: self.message = [message]
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Diagnostic Sub Code 01
|
||||
#---------------------------------------------------------------------------#
|
||||
class RestartCommunicationsOptionRequest(DiagnosticStatusRequest):
|
||||
'''
|
||||
The remote device serial line port must be initialized and restarted, and
|
||||
all of its communications event counters are cleared. If the port is
|
||||
currently in Listen Only Mode, no response is returned. This function is
|
||||
the only one that brings the port out of Listen Only Mode. If the port is
|
||||
not currently in Listen Only Mode, a normal response is returned. This
|
||||
occurs before the restart is executed.
|
||||
'''
|
||||
sub_function_code = 0x0001
|
||||
|
||||
def __init__(self, toggle=False, **kwargs):
|
||||
''' Initializes a new request
|
||||
|
||||
:param toggle: Set to True to toggle, False otherwise
|
||||
'''
|
||||
DiagnosticStatusRequest.__init__(self, **kwargs)
|
||||
if toggle:
|
||||
self.message = [ModbusStatus.On]
|
||||
else: self.message = [ModbusStatus.Off]
|
||||
|
||||
def execute(self, *args):
|
||||
''' Clear event log and restart
|
||||
|
||||
:returns: The initialized response message
|
||||
'''
|
||||
#if _MCB.ListenOnly:
|
||||
return RestartCommunicationsOptionResponse(self.message)
|
||||
|
||||
class RestartCommunicationsOptionResponse(DiagnosticStatusResponse):
|
||||
'''
|
||||
The remote device serial line port must be initialized and restarted, and
|
||||
all of its communications event counters are cleared. If the port is
|
||||
currently in Listen Only Mode, no response is returned. This function is
|
||||
the only one that brings the port out of Listen Only Mode. If the port is
|
||||
not currently in Listen Only Mode, a normal response is returned. This
|
||||
occurs before the restart is executed.
|
||||
'''
|
||||
sub_function_code = 0x0001
|
||||
|
||||
def __init__(self, toggle=False, **kwargs):
|
||||
''' Initializes a new response
|
||||
|
||||
:param toggle: Set to True if we toggled, False otherwise
|
||||
'''
|
||||
DiagnosticStatusResponse.__init__(self, **kwargs)
|
||||
if toggle:
|
||||
self.message = [ModbusStatus.On]
|
||||
else: self.message = [ModbusStatus.Off]
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Diagnostic Sub Code 02
|
||||
#---------------------------------------------------------------------------#
|
||||
class ReturnDiagnosticRegisterRequest(DiagnosticStatusSimpleRequest):
|
||||
'''
|
||||
The contents of the remote device's 16-bit diagnostic register are
|
||||
returned in the response
|
||||
'''
|
||||
sub_function_code = 0x0002
|
||||
|
||||
def execute(self, *args):
|
||||
''' Execute the diagnostic request on the given device
|
||||
|
||||
:returns: The initialized response message
|
||||
'''
|
||||
#if _MCB.isListenOnly():
|
||||
register = pack_bitstring(_MCB.getDiagnosticRegister())
|
||||
return ReturnDiagnosticRegisterResponse(register)
|
||||
|
||||
|
||||
class ReturnDiagnosticRegisterResponse(DiagnosticStatusSimpleResponse):
|
||||
'''
|
||||
The contents of the remote device's 16-bit diagnostic register are
|
||||
returned in the response
|
||||
'''
|
||||
sub_function_code = 0x0002
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Diagnostic Sub Code 03
|
||||
#---------------------------------------------------------------------------#
|
||||
class ChangeAsciiInputDelimiterRequest(DiagnosticStatusSimpleRequest):
|
||||
'''
|
||||
The character 'CHAR' passed in the request data field becomes the end of
|
||||
message delimiter for future messages (replacing the default LF
|
||||
character). This function is useful in cases of a Line Feed is not
|
||||
required at the end of ASCII messages.
|
||||
'''
|
||||
sub_function_code = 0x0003
|
||||
|
||||
def execute(self, *args):
|
||||
''' Execute the diagnostic request on the given device
|
||||
|
||||
:returns: The initialized response message
|
||||
'''
|
||||
char = (self.message & 0xff00) >> 8
|
||||
_MCB.Delimiter = char
|
||||
return ChangeAsciiInputDelimiterResponse(self.message)
|
||||
|
||||
|
||||
class ChangeAsciiInputDelimiterResponse(DiagnosticStatusSimpleResponse):
|
||||
'''
|
||||
The character 'CHAR' passed in the request data field becomes the end of
|
||||
message delimiter for future messages (replacing the default LF
|
||||
character). This function is useful in cases of a Line Feed is not
|
||||
required at the end of ASCII messages.
|
||||
'''
|
||||
sub_function_code = 0x0003
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Diagnostic Sub Code 04
|
||||
#---------------------------------------------------------------------------#
|
||||
class ForceListenOnlyModeRequest(DiagnosticStatusSimpleRequest):
|
||||
'''
|
||||
Forces the addressed remote device to its Listen Only Mode for MODBUS
|
||||
communications. This isolates it from the other devices on the network,
|
||||
allowing them to continue communicating without interruption from the
|
||||
addressed remote device. No response is returned.
|
||||
'''
|
||||
sub_function_code = 0x0004
|
||||
|
||||
def execute(self, *args):
|
||||
''' Execute the diagnostic request on the given device
|
||||
|
||||
:returns: The initialized response message
|
||||
'''
|
||||
_MCB.ListenOnly = True
|
||||
return ForceListenOnlyModeResponse()
|
||||
|
||||
|
||||
class ForceListenOnlyModeResponse(DiagnosticStatusResponse):
|
||||
'''
|
||||
Forces the addressed remote device to its Listen Only Mode for MODBUS
|
||||
communications. This isolates it from the other devices on the network,
|
||||
allowing them to continue communicating without interruption from the
|
||||
addressed remote device. No response is returned.
|
||||
|
||||
This does not send a response
|
||||
'''
|
||||
sub_function_code = 0x0004
|
||||
should_respond = False
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
''' Initializer to block a return response
|
||||
'''
|
||||
DiagnosticStatusResponse.__init__(self, **kwargs)
|
||||
self.message = []
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Diagnostic Sub Code 10
|
||||
#---------------------------------------------------------------------------#
|
||||
class ClearCountersRequest(DiagnosticStatusSimpleRequest):
|
||||
'''
|
||||
The goal is to clear ll counters and the diagnostic register.
|
||||
Also, counters are cleared upon power-up
|
||||
'''
|
||||
sub_function_code = 0x000A
|
||||
|
||||
def execute(self, *args):
|
||||
''' Execute the diagnostic request on the given device
|
||||
|
||||
:returns: The initialized response message
|
||||
'''
|
||||
_MCB.reset()
|
||||
return ClearCountersResponse(self.message)
|
||||
|
||||
|
||||
class ClearCountersResponse(DiagnosticStatusSimpleResponse):
|
||||
'''
|
||||
The goal is to clear ll counters and the diagnostic register.
|
||||
Also, counters are cleared upon power-up
|
||||
'''
|
||||
sub_function_code = 0x000A
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Diagnostic Sub Code 11
|
||||
#---------------------------------------------------------------------------#
|
||||
class ReturnBusMessageCountRequest(DiagnosticStatusSimpleRequest):
|
||||
'''
|
||||
The response data field returns the quantity of messages that the
|
||||
remote device has detected on the communications systems since its last
|
||||
restart, clear counters operation, or power-up
|
||||
'''
|
||||
sub_function_code = 0x000B
|
||||
|
||||
def execute(self, *args):
|
||||
''' Execute the diagnostic request on the given device
|
||||
|
||||
:returns: The initialized response message
|
||||
'''
|
||||
count = _MCB.Counter.BusMessage
|
||||
return ReturnBusMessageCountResponse(count)
|
||||
|
||||
|
||||
class ReturnBusMessageCountResponse(DiagnosticStatusSimpleResponse):
|
||||
'''
|
||||
The response data field returns the quantity of messages that the
|
||||
remote device has detected on the communications systems since its last
|
||||
restart, clear counters operation, or power-up
|
||||
'''
|
||||
sub_function_code = 0x000B
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Diagnostic Sub Code 12
|
||||
#---------------------------------------------------------------------------#
|
||||
class ReturnBusCommunicationErrorCountRequest(DiagnosticStatusSimpleRequest):
|
||||
'''
|
||||
The response data field returns the quantity of CRC errors encountered
|
||||
by the remote device since its last restart, clear counter operation, or
|
||||
power-up
|
||||
'''
|
||||
sub_function_code = 0x000C
|
||||
|
||||
def execute(self, *args):
|
||||
''' Execute the diagnostic request on the given device
|
||||
|
||||
:returns: The initialized response message
|
||||
'''
|
||||
count = _MCB.Counter.BusCommunicationError
|
||||
return ReturnBusCommunicationErrorCountResponse(count)
|
||||
|
||||
|
||||
class ReturnBusCommunicationErrorCountResponse(DiagnosticStatusSimpleResponse):
|
||||
'''
|
||||
The response data field returns the quantity of CRC errors encountered
|
||||
by the remote device since its last restart, clear counter operation, or
|
||||
power-up
|
||||
'''
|
||||
sub_function_code = 0x000C
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Diagnostic Sub Code 13
|
||||
#---------------------------------------------------------------------------#
|
||||
class ReturnBusExceptionErrorCountRequest(DiagnosticStatusSimpleRequest):
|
||||
'''
|
||||
The response data field returns the quantity of modbus exception
|
||||
responses returned by the remote device since its last restart,
|
||||
clear counters operation, or power-up
|
||||
'''
|
||||
sub_function_code = 0x000D
|
||||
|
||||
def execute(self, *args):
|
||||
''' Execute the diagnostic request on the given device
|
||||
|
||||
:returns: The initialized response message
|
||||
'''
|
||||
count = _MCB.Counter.BusExceptionError
|
||||
return ReturnBusExceptionErrorCountResponse(count)
|
||||
|
||||
|
||||
class ReturnBusExceptionErrorCountResponse(DiagnosticStatusSimpleResponse):
|
||||
'''
|
||||
The response data field returns the quantity of modbus exception
|
||||
responses returned by the remote device since its last restart,
|
||||
clear counters operation, or power-up
|
||||
'''
|
||||
sub_function_code = 0x000D
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Diagnostic Sub Code 14
|
||||
#---------------------------------------------------------------------------#
|
||||
class ReturnSlaveMessageCountRequest(DiagnosticStatusSimpleRequest):
|
||||
'''
|
||||
The response data field returns the quantity of messages addressed to the
|
||||
remote device, or broadcast, that the remote device has processed since
|
||||
its last restart, clear counters operation, or power-up
|
||||
'''
|
||||
sub_function_code = 0x000E
|
||||
|
||||
def execute(self, *args):
|
||||
''' Execute the diagnostic request on the given device
|
||||
|
||||
:returns: The initialized response message
|
||||
'''
|
||||
count = _MCB.Counter.SlaveMessage
|
||||
return ReturnSlaveMessageCountResponse(count)
|
||||
|
||||
|
||||
class ReturnSlaveMessageCountResponse(DiagnosticStatusSimpleResponse):
|
||||
'''
|
||||
The response data field returns the quantity of messages addressed to the
|
||||
remote device, or broadcast, that the remote device has processed since
|
||||
its last restart, clear counters operation, or power-up
|
||||
'''
|
||||
sub_function_code = 0x000E
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Diagnostic Sub Code 15
|
||||
#---------------------------------------------------------------------------#
|
||||
class ReturnSlaveNoResponseCountRequest(DiagnosticStatusSimpleRequest):
|
||||
'''
|
||||
The response data field returns the quantity of messages addressed to the
|
||||
remote device, or broadcast, that the remote device has processed since
|
||||
its last restart, clear counters operation, or power-up
|
||||
'''
|
||||
sub_function_code = 0x000F
|
||||
|
||||
def execute(self, *args):
|
||||
''' Execute the diagnostic request on the given device
|
||||
|
||||
:returns: The initialized response message
|
||||
'''
|
||||
count = _MCB.Counter.SlaveNoResponse
|
||||
return ReturnSlaveNoReponseCountResponse(count)
|
||||
|
||||
|
||||
class ReturnSlaveNoReponseCountResponse(DiagnosticStatusSimpleResponse):
|
||||
'''
|
||||
The response data field returns the quantity of messages addressed to the
|
||||
remote device, or broadcast, that the remote device has processed since
|
||||
its last restart, clear counters operation, or power-up
|
||||
'''
|
||||
sub_function_code = 0x000F
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Diagnostic Sub Code 16
|
||||
#---------------------------------------------------------------------------#
|
||||
class ReturnSlaveNAKCountRequest(DiagnosticStatusSimpleRequest):
|
||||
'''
|
||||
The response data field returns the quantity of messages addressed to the
|
||||
remote device for which it returned a Negative Acknowledge (NAK) exception
|
||||
response, since its last restart, clear counters operation, or power-up.
|
||||
Exception responses are described and listed in section 7 .
|
||||
'''
|
||||
sub_function_code = 0x0010
|
||||
|
||||
def execute(self, *args):
|
||||
''' Execute the diagnostic request on the given device
|
||||
|
||||
:returns: The initialized response message
|
||||
'''
|
||||
count = _MCB.Counter.SlaveNAK
|
||||
return ReturnSlaveNAKCountResponse(count)
|
||||
|
||||
|
||||
class ReturnSlaveNAKCountResponse(DiagnosticStatusSimpleResponse):
|
||||
'''
|
||||
The response data field returns the quantity of messages addressed to the
|
||||
remote device for which it returned a Negative Acknowledge (NAK) exception
|
||||
response, since its last restart, clear counters operation, or power-up.
|
||||
Exception responses are described and listed in section 7.
|
||||
'''
|
||||
sub_function_code = 0x0010
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Diagnostic Sub Code 17
|
||||
#---------------------------------------------------------------------------#
|
||||
class ReturnSlaveBusyCountRequest(DiagnosticStatusSimpleRequest):
|
||||
'''
|
||||
The response data field returns the quantity of messages addressed to the
|
||||
remote device for which it returned a Slave Device Busy exception response,
|
||||
since its last restart, clear counters operation, or power-up.
|
||||
'''
|
||||
sub_function_code = 0x0011
|
||||
|
||||
def execute(self, *args):
|
||||
''' Execute the diagnostic request on the given device
|
||||
|
||||
:returns: The initialized response message
|
||||
'''
|
||||
count = _MCB.Counter.SlaveBusy
|
||||
return ReturnSlaveBusyCountResponse(count)
|
||||
|
||||
|
||||
class ReturnSlaveBusyCountResponse(DiagnosticStatusSimpleResponse):
|
||||
'''
|
||||
The response data field returns the quantity of messages addressed to the
|
||||
remote device for which it returned a Slave Device Busy exception response,
|
||||
since its last restart, clear counters operation, or power-up.
|
||||
'''
|
||||
sub_function_code = 0x0011
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Diagnostic Sub Code 18
|
||||
#---------------------------------------------------------------------------#
|
||||
class ReturnSlaveBusCharacterOverrunCountRequest(DiagnosticStatusSimpleRequest):
|
||||
'''
|
||||
The response data field returns the quantity of messages addressed to the
|
||||
remote device that it could not handle due to a character overrun condition,
|
||||
since its last restart, clear counters operation, or power-up. A character
|
||||
overrun is caused by data characters arriving at the port faster than they
|
||||
can be stored, or by the loss of a character due to a hardware malfunction.
|
||||
'''
|
||||
sub_function_code = 0x0012
|
||||
|
||||
def execute(self, *args):
|
||||
''' Execute the diagnostic request on the given device
|
||||
|
||||
:returns: The initialized response message
|
||||
'''
|
||||
count = _MCB.Counter.BusCharacterOverrun
|
||||
return ReturnSlaveBusCharacterOverrunCountResponse(count)
|
||||
|
||||
|
||||
class ReturnSlaveBusCharacterOverrunCountResponse(DiagnosticStatusSimpleResponse):
|
||||
'''
|
||||
The response data field returns the quantity of messages addressed to the
|
||||
remote device that it could not handle due to a character overrun condition,
|
||||
since its last restart, clear counters operation, or power-up. A character
|
||||
overrun is caused by data characters arriving at the port faster than they
|
||||
can be stored, or by the loss of a character due to a hardware malfunction.
|
||||
'''
|
||||
sub_function_code = 0x0012
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Diagnostic Sub Code 19
|
||||
#---------------------------------------------------------------------------#
|
||||
class ReturnIopOverrunCountRequest(DiagnosticStatusSimpleRequest):
|
||||
'''
|
||||
An IOP overrun is caused by data characters arriving at the port
|
||||
faster than they can be stored, or by the loss of a character due
|
||||
to a hardware malfunction. This function is specific to the 884.
|
||||
'''
|
||||
sub_function_code = 0x0013
|
||||
|
||||
def execute(self, *args):
|
||||
''' Execute the diagnostic request on the given device
|
||||
|
||||
:returns: The initialized response message
|
||||
'''
|
||||
count = _MCB.Counter.BusCharacterOverrun
|
||||
return ReturnIopOverrunCountResponse(count)
|
||||
|
||||
|
||||
class ReturnIopOverrunCountResponse(DiagnosticStatusSimpleResponse):
|
||||
'''
|
||||
The response data field returns the quantity of messages
|
||||
addressed to the slave that it could not handle due to an 884
|
||||
IOP overrun condition, since its last restart, clear counters
|
||||
operation, or power-up.
|
||||
'''
|
||||
sub_function_code = 0x0013
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Diagnostic Sub Code 20
|
||||
#---------------------------------------------------------------------------#
|
||||
class ClearOverrunCountRequest(DiagnosticStatusSimpleRequest):
|
||||
'''
|
||||
Clears the overrun error counter and reset the error flag
|
||||
|
||||
An error flag should be cleared, but nothing else in the
|
||||
specification mentions is, so it is ignored.
|
||||
'''
|
||||
sub_function_code = 0x0014
|
||||
|
||||
def execute(self, *args):
|
||||
''' Execute the diagnostic request on the given device
|
||||
|
||||
:returns: The initialized response message
|
||||
'''
|
||||
_MCB.Counter.BusCharacterOverrun = 0x0000
|
||||
return ClearOverrunCountResponse(self.message)
|
||||
|
||||
|
||||
class ClearOverrunCountResponse(DiagnosticStatusSimpleResponse):
|
||||
'''
|
||||
Clears the overrun error counter and reset the error flag
|
||||
'''
|
||||
sub_function_code = 0x0014
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Diagnostic Sub Code 21
|
||||
#---------------------------------------------------------------------------#
|
||||
class GetClearModbusPlusRequest(DiagnosticStatusSimpleRequest):
|
||||
'''
|
||||
In addition to the Function code (08) and Subfunction code
|
||||
(00 15 hex) in the query, a two-byte Operation field is used
|
||||
to specify either a 'Get Statistics' or a 'Clear Statistics'
|
||||
operation. The two operations are exclusive - the 'Get'
|
||||
operation cannot clear the statistics, and the 'Clear'
|
||||
operation does not return statistics prior to clearing
|
||||
them. Statistics are also cleared on power-up of the slave
|
||||
device.
|
||||
'''
|
||||
sub_function_code = 0x0015
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(GetClearModbusPlusRequest, self).__init__(**kwargs)
|
||||
|
||||
def get_response_pdu_size(self):
|
||||
"""
|
||||
Returns a series of 54 16-bit words (108 bytes) in the data field of the response
|
||||
(this function differs from the usual two-byte length of the data field). The data
|
||||
contains the statistics for the Modbus Plus peer processor in the slave device.
|
||||
Func_code (1 byte) + Sub function code (2 byte) + Operation (2 byte) + Data (108 bytes)
|
||||
:return:
|
||||
"""
|
||||
if self.message == ModbusPlusOperation.GetStatistics:
|
||||
data = 2 + 108 # byte count(2) + data (54*2)
|
||||
else:
|
||||
data = 0
|
||||
return 1 + 2 + 2 + 2+ data
|
||||
|
||||
def execute(self, *args):
|
||||
''' Execute the diagnostic request on the given device
|
||||
|
||||
:returns: The initialized response message
|
||||
'''
|
||||
message = None # the clear operation does not return info
|
||||
if self.message == ModbusPlusOperation.ClearStatistics:
|
||||
_MCB.Plus.reset()
|
||||
message = self.message
|
||||
else:
|
||||
message = [self.message]
|
||||
message += _MCB.Plus.encode()
|
||||
return GetClearModbusPlusResponse(message)
|
||||
|
||||
def encode(self):
|
||||
'''
|
||||
Base encoder for a diagnostic response
|
||||
we encode the data set in self.message
|
||||
|
||||
:returns: The encoded packet
|
||||
'''
|
||||
packet = struct.pack('>H', self.sub_function_code)
|
||||
packet += struct.pack('>H', self.message)
|
||||
return packet
|
||||
|
||||
|
||||
class GetClearModbusPlusResponse(DiagnosticStatusSimpleResponse):
|
||||
'''
|
||||
Returns a series of 54 16-bit words (108 bytes) in the data field
|
||||
of the response (this function differs from the usual two-byte
|
||||
length of the data field). The data contains the statistics for
|
||||
the Modbus Plus peer processor in the slave device.
|
||||
'''
|
||||
sub_function_code = 0x0015
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Exported symbols
|
||||
#---------------------------------------------------------------------------#
|
||||
__all__ = [
|
||||
"DiagnosticStatusRequest", "DiagnosticStatusResponse",
|
||||
"ReturnQueryDataRequest", "ReturnQueryDataResponse",
|
||||
"RestartCommunicationsOptionRequest", "RestartCommunicationsOptionResponse",
|
||||
"ReturnDiagnosticRegisterRequest", "ReturnDiagnosticRegisterResponse",
|
||||
"ChangeAsciiInputDelimiterRequest", "ChangeAsciiInputDelimiterResponse",
|
||||
"ForceListenOnlyModeRequest", "ForceListenOnlyModeResponse",
|
||||
"ClearCountersRequest", "ClearCountersResponse",
|
||||
"ReturnBusMessageCountRequest", "ReturnBusMessageCountResponse",
|
||||
"ReturnBusCommunicationErrorCountRequest", "ReturnBusCommunicationErrorCountResponse",
|
||||
"ReturnBusExceptionErrorCountRequest", "ReturnBusExceptionErrorCountResponse",
|
||||
"ReturnSlaveMessageCountRequest", "ReturnSlaveMessageCountResponse",
|
||||
"ReturnSlaveNoResponseCountRequest", "ReturnSlaveNoReponseCountResponse",
|
||||
"ReturnSlaveNAKCountRequest", "ReturnSlaveNAKCountResponse",
|
||||
"ReturnSlaveBusyCountRequest", "ReturnSlaveBusyCountResponse",
|
||||
"ReturnSlaveBusCharacterOverrunCountRequest", "ReturnSlaveBusCharacterOverrunCountResponse",
|
||||
"ReturnIopOverrunCountRequest", "ReturnIopOverrunCountResponse",
|
||||
"ClearOverrunCountRequest", "ClearOverrunCountResponse",
|
||||
"GetClearModbusPlusRequest", "GetClearModbusPlusResponse",
|
||||
]
|
||||
197
modbus/pymodbus/events.py
Normal file
@@ -0,0 +1,197 @@
|
||||
'''
|
||||
Modbus Remote Events
|
||||
------------------------------------------------------------
|
||||
|
||||
An event byte returned by the Get Communications Event Log function
|
||||
can be any one of four types. The type is defined by bit 7
|
||||
(the high-order bit) in each byte. It may be further defined by bit 6.
|
||||
'''
|
||||
from pymodbus.exceptions import NotImplementedException
|
||||
from pymodbus.exceptions import ParameterException
|
||||
from pymodbus.utilities import pack_bitstring, unpack_bitstring
|
||||
|
||||
|
||||
class ModbusEvent(object):
|
||||
|
||||
def encode(self):
|
||||
''' Encodes the status bits to an event message
|
||||
|
||||
:returns: The encoded event message
|
||||
'''
|
||||
raise NotImplementedException()
|
||||
|
||||
def decode(self, event):
|
||||
''' Decodes the event message to its status bits
|
||||
|
||||
:param event: The event to decode
|
||||
'''
|
||||
raise NotImplementedException()
|
||||
|
||||
|
||||
class RemoteReceiveEvent(ModbusEvent):
|
||||
''' Remote device MODBUS Receive Event
|
||||
|
||||
The remote device stores this type of event byte when a query message
|
||||
is received. It is stored before the remote device processes the message.
|
||||
This event is defined by bit 7 set to logic '1'. The other bits will be
|
||||
set to a logic '1' if the corresponding condition is TRUE. The bit layout
|
||||
is::
|
||||
|
||||
Bit Contents
|
||||
----------------------------------
|
||||
0 Not Used
|
||||
2 Not Used
|
||||
3 Not Used
|
||||
4 Character Overrun
|
||||
5 Currently in Listen Only Mode
|
||||
6 Broadcast Receive
|
||||
7 1
|
||||
'''
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
''' Initialize a new event instance
|
||||
'''
|
||||
self.overrun = kwargs.get('overrun', False)
|
||||
self.listen = kwargs.get('listen', False)
|
||||
self.broadcast = kwargs.get('broadcast', False)
|
||||
|
||||
def encode(self):
|
||||
''' Encodes the status bits to an event message
|
||||
|
||||
:returns: The encoded event message
|
||||
'''
|
||||
bits = [False] * 3
|
||||
bits += [self.overrun, self.listen, self.broadcast, True]
|
||||
packet = pack_bitstring(bits)
|
||||
return packet
|
||||
|
||||
def decode(self, event):
|
||||
''' Decodes the event message to its status bits
|
||||
|
||||
:param event: The event to decode
|
||||
'''
|
||||
bits = unpack_bitstring(event)
|
||||
self.overrun = bits[4]
|
||||
self.listen = bits[5]
|
||||
self.broadcast = bits[6]
|
||||
|
||||
|
||||
class RemoteSendEvent(ModbusEvent):
|
||||
''' Remote device MODBUS Send Event
|
||||
|
||||
The remote device stores this type of event byte when it finishes
|
||||
processing a request message. It is stored if the remote device
|
||||
returned a normal or exception response, or no response.
|
||||
|
||||
This event is defined by bit 7 set to a logic '0', with bit 6 set to a '1'.
|
||||
The other bits will be set to a logic '1' if the corresponding
|
||||
condition is TRUE. The bit layout is::
|
||||
|
||||
Bit Contents
|
||||
-----------------------------------------------------------
|
||||
0 Read Exception Sent (Exception Codes 1-3)
|
||||
1 Slave Abort Exception Sent (Exception Code 4)
|
||||
2 Slave Busy Exception Sent (Exception Codes 5-6)
|
||||
3 Slave Program NAK Exception Sent (Exception Code 7)
|
||||
4 Write Timeout Error Occurred
|
||||
5 Currently in Listen Only Mode
|
||||
6 1
|
||||
7 0
|
||||
'''
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
''' Initialize a new event instance
|
||||
'''
|
||||
self.read = kwargs.get('read', False)
|
||||
self.slave_abort = kwargs.get('slave_abort', False)
|
||||
self.slave_busy = kwargs.get('slave_busy', False)
|
||||
self.slave_nak = kwargs.get('slave_nak', False)
|
||||
self.write_timeout = kwargs.get('write_timeout', False)
|
||||
self.listen = kwargs.get('listen', False)
|
||||
|
||||
def encode(self):
|
||||
''' Encodes the status bits to an event message
|
||||
|
||||
:returns: The encoded event message
|
||||
'''
|
||||
bits = [self.read, self.slave_abort, self.slave_busy,
|
||||
self.slave_nak, self.write_timeout, self.listen]
|
||||
bits += [True, False]
|
||||
packet = pack_bitstring(bits)
|
||||
return packet
|
||||
|
||||
def decode(self, event):
|
||||
''' Decodes the event message to its status bits
|
||||
|
||||
:param event: The event to decode
|
||||
'''
|
||||
# todo fix the start byte count
|
||||
bits = unpack_bitstring(event)
|
||||
self.read = bits[0]
|
||||
self.slave_abort = bits[1]
|
||||
self.slave_busy = bits[2]
|
||||
self.slave_nak = bits[3]
|
||||
self.write_timeout = bits[4]
|
||||
self.listen = bits[5]
|
||||
|
||||
|
||||
class EnteredListenModeEvent(ModbusEvent):
|
||||
''' Remote device Entered Listen Only Mode
|
||||
|
||||
The remote device stores this type of event byte when it enters
|
||||
the Listen Only Mode. The event is defined by a content of 04 hex.
|
||||
'''
|
||||
|
||||
value = 0x04
|
||||
__encoded = b'\x04'
|
||||
|
||||
def encode(self):
|
||||
''' Encodes the status bits to an event message
|
||||
|
||||
:returns: The encoded event message
|
||||
'''
|
||||
return self.__encoded
|
||||
|
||||
def decode(self, event):
|
||||
''' Decodes the event message to its status bits
|
||||
|
||||
:param event: The event to decode
|
||||
'''
|
||||
if event != self.__encoded:
|
||||
raise ParameterException('Invalid decoded value')
|
||||
|
||||
|
||||
class CommunicationRestartEvent(ModbusEvent):
|
||||
''' Remote device Initiated Communication Restart
|
||||
|
||||
The remote device stores this type of event byte when its communications
|
||||
port is restarted. The remote device can be restarted by the Diagnostics
|
||||
function (code 08), with sub-function Restart Communications Option
|
||||
(code 00 01).
|
||||
|
||||
That function also places the remote device into a 'Continue on Error'
|
||||
or 'Stop on Error' mode. If the remote device is placed into 'Continue on
|
||||
Error' mode, the event byte is added to the existing event log. If the
|
||||
remote device is placed into 'Stop on Error' mode, the byte is added to
|
||||
the log and the rest of the log is cleared to zeros.
|
||||
|
||||
The event is defined by a content of zero.
|
||||
'''
|
||||
|
||||
value = 0x00
|
||||
__encoded = b'\x00'
|
||||
|
||||
def encode(self):
|
||||
''' Encodes the status bits to an event message
|
||||
|
||||
:returns: The encoded event message
|
||||
'''
|
||||
return self.__encoded
|
||||
|
||||
def decode(self, event):
|
||||
''' Decodes the event message to its status bits
|
||||
|
||||
:param event: The event to decode
|
||||
'''
|
||||
if event != self.__encoded:
|
||||
raise ParameterException('Invalid decoded value')
|
||||
130
modbus/pymodbus/exceptions.py
Normal file
@@ -0,0 +1,130 @@
|
||||
"""
|
||||
Pymodbus Exceptions
|
||||
--------------------
|
||||
|
||||
Custom exceptions to be used in the Modbus code.
|
||||
"""
|
||||
|
||||
|
||||
class ModbusException(Exception):
|
||||
""" Base modbus exception """
|
||||
|
||||
def __init__(self, string):
|
||||
""" Initialize the exception
|
||||
:param string: The message to append to the error
|
||||
"""
|
||||
self.string = string
|
||||
|
||||
def __str__(self):
|
||||
return 'Modbus Error: %s' % self.string
|
||||
|
||||
def isError(self):
|
||||
"""Error"""
|
||||
return True
|
||||
|
||||
|
||||
class ModbusIOException(ModbusException):
|
||||
""" Error resulting from data i/o """
|
||||
|
||||
def __init__(self, string="", function_code=None):
|
||||
""" Initialize the exception
|
||||
:param string: The message to append to the error
|
||||
"""
|
||||
self.fcode = function_code
|
||||
self.message = "[Input/Output] %s" % string
|
||||
ModbusException.__init__(self, self.message)
|
||||
|
||||
|
||||
class ParameterException(ModbusException):
|
||||
""" Error resulting from invalid parameter """
|
||||
|
||||
def __init__(self, string=""):
|
||||
""" Initialize the exception
|
||||
|
||||
:param string: The message to append to the error
|
||||
"""
|
||||
message = "[Invalid Parameter] %s" % string
|
||||
ModbusException.__init__(self, message)
|
||||
|
||||
|
||||
class NoSuchSlaveException(ModbusException):
|
||||
""" Error resulting from making a request to a slave
|
||||
that does not exist """
|
||||
|
||||
def __init__(self, string=""):
|
||||
""" Initialize the exception
|
||||
|
||||
:param string: The message to append to the error
|
||||
"""
|
||||
message = "[No Such Slave] %s" % string
|
||||
ModbusException.__init__(self, message)
|
||||
|
||||
|
||||
class NotImplementedException(ModbusException):
|
||||
""" Error resulting from not implemented function """
|
||||
|
||||
def __init__(self, string=""):
|
||||
""" Initialize the exception
|
||||
:param string: The message to append to the error
|
||||
"""
|
||||
message = "[Not Implemented] %s" % string
|
||||
ModbusException.__init__(self, message)
|
||||
|
||||
|
||||
class ConnectionException(ModbusException):
|
||||
""" Error resulting from a bad connection """
|
||||
|
||||
def __init__(self, string=""):
|
||||
""" Initialize the exception
|
||||
|
||||
:param string: The message to append to the error
|
||||
"""
|
||||
message = "[Connection] %s" % string
|
||||
ModbusException.__init__(self, message)
|
||||
|
||||
|
||||
class InvalidMessageReceivedException(ModbusException):
|
||||
"""
|
||||
Error resulting from invalid response received or decoded
|
||||
"""
|
||||
|
||||
def __init__(self, string=""):
|
||||
""" Initialize the exception
|
||||
|
||||
:param string: The message to append to the error
|
||||
"""
|
||||
message = "[Invalid Message] %s" % string
|
||||
ModbusException.__init__(self, message)
|
||||
|
||||
|
||||
class MessageRegisterException(ModbusException):
|
||||
"""
|
||||
Error resulting from failing to register a custom message request/response
|
||||
"""
|
||||
def __init__(self, string=""):
|
||||
message = '[Error registering message] %s' % string
|
||||
ModbusException.__init__(self, message)
|
||||
|
||||
|
||||
class TimeOutException(ModbusException):
|
||||
""" Error resulting from modbus response timeout """
|
||||
|
||||
def __init__(self, string=""):
|
||||
""" Initialize the exception
|
||||
|
||||
:param string: The message to append to the error
|
||||
"""
|
||||
message = "[Timeout] %s" % string
|
||||
ModbusException.__init__(self, message)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Exported symbols
|
||||
# --------------------------------------------------------------------------- #
|
||||
__all__ = [
|
||||
"ModbusException", "ModbusIOException",
|
||||
"ParameterException", "NotImplementedException",
|
||||
"ConnectionException", "NoSuchSlaveException",
|
||||
"InvalidMessageReceivedException",
|
||||
"MessageRegisterException", "TimeOutException"
|
||||
]
|
||||
312
modbus/pymodbus/factory.py
Normal file
@@ -0,0 +1,312 @@
|
||||
"""
|
||||
Modbus Request/Response Decoder Factories
|
||||
-------------------------------------------
|
||||
|
||||
The following factories make it easy to decode request/response messages.
|
||||
To add a new request/response pair to be decodeable by the library, simply
|
||||
add them to the respective function lookup table (order doesn't matter, but
|
||||
it does help keep things organized).
|
||||
|
||||
Regardless of how many functions are added to the lookup, O(1) behavior is
|
||||
kept as a result of a pre-computed lookup dictionary.
|
||||
"""
|
||||
|
||||
from pymodbus.pdu import IllegalFunctionRequest
|
||||
from pymodbus.pdu import ExceptionResponse
|
||||
from pymodbus.pdu import ModbusRequest, ModbusResponse
|
||||
from pymodbus.pdu import ModbusExceptions as ecode
|
||||
from pymodbus.interfaces import IModbusDecoder
|
||||
from pymodbus.exceptions import ModbusException, MessageRegisterException
|
||||
from pymodbus.bit_read_message import *
|
||||
from pymodbus.bit_write_message import *
|
||||
from pymodbus.diag_message import *
|
||||
from pymodbus.file_message import *
|
||||
from pymodbus.other_message import *
|
||||
from pymodbus.mei_message import *
|
||||
from pymodbus.register_read_message import *
|
||||
from pymodbus.register_write_message import *
|
||||
from pymodbus.compat import byte2int
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Logging
|
||||
# --------------------------------------------------------------------------- #
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Server Decoder
|
||||
# --------------------------------------------------------------------------- #
|
||||
class ServerDecoder(IModbusDecoder):
|
||||
""" Request Message Factory (Server)
|
||||
|
||||
To add more implemented functions, simply add them to the list
|
||||
"""
|
||||
__function_table = [
|
||||
ReadHoldingRegistersRequest,
|
||||
ReadDiscreteInputsRequest,
|
||||
ReadInputRegistersRequest,
|
||||
ReadCoilsRequest,
|
||||
WriteMultipleCoilsRequest,
|
||||
WriteMultipleRegistersRequest,
|
||||
WriteSingleRegisterRequest,
|
||||
WriteSingleCoilRequest,
|
||||
ReadWriteMultipleRegistersRequest,
|
||||
DiagnosticStatusRequest,
|
||||
ReadExceptionStatusRequest,
|
||||
GetCommEventCounterRequest,
|
||||
GetCommEventLogRequest,
|
||||
ReportSlaveIdRequest,
|
||||
ReadFileRecordRequest,
|
||||
WriteFileRecordRequest,
|
||||
MaskWriteRegisterRequest,
|
||||
ReadFifoQueueRequest,
|
||||
ReadDeviceInformationRequest,
|
||||
]
|
||||
__sub_function_table = [
|
||||
ReturnQueryDataRequest,
|
||||
RestartCommunicationsOptionRequest,
|
||||
ReturnDiagnosticRegisterRequest,
|
||||
ChangeAsciiInputDelimiterRequest,
|
||||
ForceListenOnlyModeRequest,
|
||||
ClearCountersRequest,
|
||||
ReturnBusMessageCountRequest,
|
||||
ReturnBusCommunicationErrorCountRequest,
|
||||
ReturnBusExceptionErrorCountRequest,
|
||||
ReturnSlaveMessageCountRequest,
|
||||
ReturnSlaveNoResponseCountRequest,
|
||||
ReturnSlaveNAKCountRequest,
|
||||
ReturnSlaveBusyCountRequest,
|
||||
ReturnSlaveBusCharacterOverrunCountRequest,
|
||||
ReturnIopOverrunCountRequest,
|
||||
ClearOverrunCountRequest,
|
||||
GetClearModbusPlusRequest,
|
||||
ReadDeviceInformationRequest,
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
""" Initializes the client lookup tables
|
||||
"""
|
||||
functions = set(f.function_code for f in self.__function_table)
|
||||
self.__lookup = dict([(f.function_code, f) for f in self.__function_table])
|
||||
self.__sub_lookup = dict((f, {}) for f in functions)
|
||||
for f in self.__sub_function_table:
|
||||
self.__sub_lookup[f.function_code][f.sub_function_code] = f
|
||||
|
||||
def decode(self, message):
|
||||
""" Wrapper to decode a request packet
|
||||
|
||||
:param message: The raw modbus request packet
|
||||
:return: The decoded modbus message or None if error
|
||||
"""
|
||||
try:
|
||||
return self._helper(message)
|
||||
except ModbusException as er:
|
||||
_logger.warning("Unable to decode request %s" % er)
|
||||
return None
|
||||
|
||||
def lookupPduClass(self, function_code):
|
||||
""" Use `function_code` to determine the class of the PDU.
|
||||
|
||||
:param function_code: The function code specified in a frame.
|
||||
:returns: The class of the PDU that has a matching `function_code`.
|
||||
"""
|
||||
return self.__lookup.get(function_code, ExceptionResponse)
|
||||
|
||||
def _helper(self, data):
|
||||
"""
|
||||
This factory is used to generate the correct request object
|
||||
from a valid request packet. This decodes from a list of the
|
||||
currently implemented request types.
|
||||
|
||||
:param data: The request packet to decode
|
||||
:returns: The decoded request or illegal function request object
|
||||
"""
|
||||
function_code = byte2int(data[0])
|
||||
request = self.__lookup.get(function_code, lambda: None)()
|
||||
if not request:
|
||||
_logger.debug("Factory Request[%d]" % function_code)
|
||||
request = IllegalFunctionRequest(function_code)
|
||||
else:
|
||||
fc_string = "%s: %s" % (
|
||||
str(self.__lookup[function_code]).split('.')[-1].rstrip(
|
||||
"'>"),
|
||||
function_code
|
||||
)
|
||||
_logger.debug("Factory Request[%s]" % fc_string)
|
||||
request.decode(data[1:])
|
||||
|
||||
if hasattr(request, 'sub_function_code'):
|
||||
lookup = self.__sub_lookup.get(request.function_code, {})
|
||||
subtype = lookup.get(request.sub_function_code, None)
|
||||
if subtype: request.__class__ = subtype
|
||||
|
||||
return request
|
||||
|
||||
def register(self, function=None):
|
||||
"""
|
||||
Registers a function and sub function class with the decoder
|
||||
:param function: Custom function class to register
|
||||
:return:
|
||||
"""
|
||||
if function and not issubclass(function, ModbusRequest):
|
||||
raise MessageRegisterException("'{}' is Not a valid Modbus Message"
|
||||
". Class needs to be derived from "
|
||||
"`pymodbus.pdu.ModbusRequest` "
|
||||
"".format(
|
||||
function.__class__.__name__
|
||||
))
|
||||
self.__lookup[function.function_code] = function
|
||||
if hasattr(function, "sub_function_code"):
|
||||
if function.function_code not in self.__sub_lookup:
|
||||
self.__sub_lookup[function.function_code] = dict()
|
||||
self.__sub_lookup[function.function_code][
|
||||
function.sub_function_code] = function
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Client Decoder
|
||||
# --------------------------------------------------------------------------- #
|
||||
class ClientDecoder(IModbusDecoder):
|
||||
""" Response Message Factory (Client)
|
||||
|
||||
To add more implemented functions, simply add them to the list
|
||||
"""
|
||||
__function_table = [
|
||||
ReadHoldingRegistersResponse,
|
||||
ReadDiscreteInputsResponse,
|
||||
ReadInputRegistersResponse,
|
||||
ReadCoilsResponse,
|
||||
WriteMultipleCoilsResponse,
|
||||
WriteMultipleRegistersResponse,
|
||||
WriteSingleRegisterResponse,
|
||||
WriteSingleCoilResponse,
|
||||
ReadWriteMultipleRegistersResponse,
|
||||
DiagnosticStatusResponse,
|
||||
ReadExceptionStatusResponse,
|
||||
GetCommEventCounterResponse,
|
||||
GetCommEventLogResponse,
|
||||
ReportSlaveIdResponse,
|
||||
ReadFileRecordResponse,
|
||||
WriteFileRecordResponse,
|
||||
MaskWriteRegisterResponse,
|
||||
ReadFifoQueueResponse,
|
||||
ReadDeviceInformationResponse,
|
||||
]
|
||||
__sub_function_table = [
|
||||
ReturnQueryDataResponse,
|
||||
RestartCommunicationsOptionResponse,
|
||||
ReturnDiagnosticRegisterResponse,
|
||||
ChangeAsciiInputDelimiterResponse,
|
||||
ForceListenOnlyModeResponse,
|
||||
ClearCountersResponse,
|
||||
ReturnBusMessageCountResponse,
|
||||
ReturnBusCommunicationErrorCountResponse,
|
||||
ReturnBusExceptionErrorCountResponse,
|
||||
ReturnSlaveMessageCountResponse,
|
||||
ReturnSlaveNoReponseCountResponse,
|
||||
ReturnSlaveNAKCountResponse,
|
||||
ReturnSlaveBusyCountResponse,
|
||||
ReturnSlaveBusCharacterOverrunCountResponse,
|
||||
ReturnIopOverrunCountResponse,
|
||||
ClearOverrunCountResponse,
|
||||
GetClearModbusPlusResponse,
|
||||
ReadDeviceInformationResponse,
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
""" Initializes the client lookup tables
|
||||
"""
|
||||
functions = set(f.function_code for f in self.__function_table)
|
||||
self.__lookup = dict([(f.function_code, f)
|
||||
for f in self.__function_table])
|
||||
self.__sub_lookup = dict((f, {}) for f in functions)
|
||||
for f in self.__sub_function_table:
|
||||
self.__sub_lookup[f.function_code][f.sub_function_code] = f
|
||||
|
||||
def lookupPduClass(self, function_code):
|
||||
""" Use `function_code` to determine the class of the PDU.
|
||||
|
||||
:param function_code: The function code specified in a frame.
|
||||
:returns: The class of the PDU that has a matching `function_code`.
|
||||
"""
|
||||
return self.__lookup.get(function_code, ExceptionResponse)
|
||||
|
||||
def decode(self, message):
|
||||
""" Wrapper to decode a response packet
|
||||
|
||||
:param message: The raw packet to decode
|
||||
:return: The decoded modbus message or None if error
|
||||
"""
|
||||
try:
|
||||
return self._helper(message)
|
||||
except ModbusException as er:
|
||||
_logger.error("Unable to decode response %s" % er)
|
||||
|
||||
except Exception as ex:
|
||||
_logger.error(ex)
|
||||
return None
|
||||
|
||||
def _helper(self, data):
|
||||
"""
|
||||
This factory is used to generate the correct response object
|
||||
from a valid response packet. This decodes from a list of the
|
||||
currently implemented request types.
|
||||
|
||||
:param data: The response packet to decode
|
||||
:returns: The decoded request or an exception response object
|
||||
"""
|
||||
fc_string = function_code = byte2int(data[0])
|
||||
if function_code in self.__lookup:
|
||||
fc_string = "%s: %s" % (
|
||||
str(self.__lookup[function_code]).split('.')[-1].rstrip("'>"),
|
||||
function_code
|
||||
)
|
||||
_logger.debug("Factory Response[%s]" % fc_string)
|
||||
response = self.__lookup.get(function_code, lambda: None)()
|
||||
if function_code > 0x80:
|
||||
code = function_code & 0x7f # strip error portion
|
||||
response = ExceptionResponse(code, ecode.IllegalFunction)
|
||||
if not response:
|
||||
raise ModbusException("Unknown response %d" % function_code)
|
||||
response.decode(data[1:])
|
||||
|
||||
if hasattr(response, 'sub_function_code'):
|
||||
lookup = self.__sub_lookup.get(response.function_code, {})
|
||||
subtype = lookup.get(response.sub_function_code, None)
|
||||
if subtype: response.__class__ = subtype
|
||||
|
||||
return response
|
||||
|
||||
def register(self, function=None, sub_function=None, force=False):
|
||||
"""
|
||||
Registers a function and sub function class with the decoder
|
||||
:param function: Custom function class to register
|
||||
:param sub_function: Custom sub function class to register
|
||||
:param force: Force update the existing class
|
||||
:return:
|
||||
"""
|
||||
if function and not issubclass(function, ModbusResponse):
|
||||
raise MessageRegisterException("'{}' is Not a valid Modbus Message"
|
||||
". Class needs to be derived from "
|
||||
"`pymodbus.pdu.ModbusResponse` "
|
||||
"".format(
|
||||
function.__class__.__name__
|
||||
))
|
||||
self.__lookup[function.function_code] = function
|
||||
if hasattr(function, "sub_function_code"):
|
||||
if function.function_code not in self.__sub_lookup:
|
||||
self.__sub_lookup[function.function_code] = dict()
|
||||
self.__sub_lookup[function.function_code][
|
||||
function.sub_function_code] = function
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Exported symbols
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
__all__ = ['ServerDecoder', 'ClientDecoder']
|
||||
|
||||
|
||||
395
modbus/pymodbus/file_message.py
Normal file
@@ -0,0 +1,395 @@
|
||||
'''
|
||||
File Record Read/Write Messages
|
||||
-------------------------------
|
||||
|
||||
Currently none of these messages are implemented
|
||||
'''
|
||||
import struct
|
||||
from pymodbus.pdu import ModbusRequest
|
||||
from pymodbus.pdu import ModbusResponse
|
||||
from pymodbus.pdu import ModbusExceptions as merror
|
||||
from pymodbus.compat import byte2int
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# File Record Types
|
||||
#---------------------------------------------------------------------------#
|
||||
class FileRecord(object):
|
||||
''' Represents a file record and its relevant data.
|
||||
'''
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
''' Initializes a new instance
|
||||
|
||||
:params reference_type: Defaults to 0x06 (must be)
|
||||
:params file_number: Indicates which file number we are reading
|
||||
:params record_number: Indicates which record in the file
|
||||
:params record_data: The actual data of the record
|
||||
:params record_length: The length in registers of the record
|
||||
:params response_length: The length in bytes of the record
|
||||
'''
|
||||
self.reference_type = kwargs.get('reference_type', 0x06)
|
||||
self.file_number = kwargs.get('file_number', 0x00)
|
||||
self.record_number = kwargs.get('record_number', 0x00)
|
||||
self.record_data = kwargs.get('record_data', '')
|
||||
|
||||
self.record_length = kwargs.get('record_length', len(self.record_data) // 2)
|
||||
self.response_length = kwargs.get('response_length', len(self.record_data) + 1)
|
||||
|
||||
def __eq__(self, relf):
|
||||
''' Compares the left object to the right
|
||||
'''
|
||||
return self.reference_type == relf.reference_type \
|
||||
and self.file_number == relf.file_number \
|
||||
and self.record_number == relf.record_number \
|
||||
and self.record_length == relf.record_length \
|
||||
and self.record_data == relf.record_data
|
||||
|
||||
def __ne__(self, relf):
|
||||
''' Compares the left object to the right
|
||||
'''
|
||||
return not self.__eq__(relf)
|
||||
|
||||
def __repr__(self):
|
||||
''' Gives a representation of the file record
|
||||
'''
|
||||
params = (self.file_number, self.record_number, self.record_length)
|
||||
return 'FileRecord(file=%d, record=%d, length=%d)' % params
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# File Requests/Responses
|
||||
#---------------------------------------------------------------------------#
|
||||
class ReadFileRecordRequest(ModbusRequest):
|
||||
'''
|
||||
This function code is used to perform a file record read. All request
|
||||
data lengths are provided in terms of number of bytes and all record
|
||||
lengths are provided in terms of registers.
|
||||
|
||||
A file is an organization of records. Each file contains 10000 records,
|
||||
addressed 0000 to 9999 decimal or 0x0000 to 0x270f. For example, record
|
||||
12 is addressed as 12. The function can read multiple groups of
|
||||
references. The groups can be separating (non-contiguous), but the
|
||||
references within each group must be sequential. Each group is defined
|
||||
in a seperate 'sub-request' field that contains seven bytes::
|
||||
|
||||
The reference type: 1 byte (must be 0x06)
|
||||
The file number: 2 bytes
|
||||
The starting record number within the file: 2 bytes
|
||||
The length of the record to be read: 2 bytes
|
||||
|
||||
The quantity of registers to be read, combined with all other fields
|
||||
in the expected response, must not exceed the allowable length of the
|
||||
MODBUS PDU: 235 bytes.
|
||||
'''
|
||||
function_code = 0x14
|
||||
_rtu_byte_count_pos = 2
|
||||
|
||||
def __init__(self, records=None, **kwargs):
|
||||
''' Initializes a new instance
|
||||
|
||||
:param records: The file record requests to be read
|
||||
'''
|
||||
ModbusRequest.__init__(self, **kwargs)
|
||||
self.records = records or []
|
||||
|
||||
def encode(self):
|
||||
''' Encodes the request packet
|
||||
|
||||
:returns: The byte encoded packet
|
||||
'''
|
||||
packet = struct.pack('B', len(self.records) * 7)
|
||||
for record in self.records:
|
||||
packet += struct.pack('>BHHH', 0x06, record.file_number,
|
||||
record.record_number, record.record_length)
|
||||
return packet
|
||||
|
||||
def decode(self, data):
|
||||
''' Decodes the incoming request
|
||||
|
||||
:param data: The data to decode into the address
|
||||
'''
|
||||
self.records = []
|
||||
byte_count = byte2int(data[0])
|
||||
for count in range(1, byte_count, 7):
|
||||
decoded = struct.unpack('>BHHH', data[count:count+7])
|
||||
record = FileRecord(file_number=decoded[1],
|
||||
record_number=decoded[2], record_length=decoded[3])
|
||||
if decoded[0] == 0x06: self.records.append(record)
|
||||
|
||||
def execute(self, context):
|
||||
''' Run a read exeception status request against the store
|
||||
|
||||
:param context: The datastore to request from
|
||||
:returns: The populated response
|
||||
'''
|
||||
# TODO do some new context operation here
|
||||
# if file number, record number, or address + length
|
||||
# is too big, return an error.
|
||||
files = []
|
||||
return ReadFileRecordResponse(files)
|
||||
|
||||
|
||||
class ReadFileRecordResponse(ModbusResponse):
|
||||
'''
|
||||
The normal response is a series of 'sub-responses,' one for each
|
||||
'sub-request.' The byte count field is the total combined count of
|
||||
bytes in all 'sub-responses.' In addition, each 'sub-response'
|
||||
contains a field that shows its own byte count.
|
||||
'''
|
||||
function_code = 0x14
|
||||
_rtu_byte_count_pos = 2
|
||||
|
||||
def __init__(self, records=None, **kwargs):
|
||||
''' Initializes a new instance
|
||||
|
||||
:param records: The requested file records
|
||||
'''
|
||||
ModbusResponse.__init__(self, **kwargs)
|
||||
self.records = records or []
|
||||
|
||||
def encode(self):
|
||||
''' Encodes the response
|
||||
|
||||
:returns: The byte encoded message
|
||||
'''
|
||||
total = sum(record.response_length + 1 for record in self.records)
|
||||
packet = struct.pack('B', total)
|
||||
for record in self.records:
|
||||
packet += struct.pack('>BB', 0x06, record.record_length)
|
||||
packet += record.record_data
|
||||
return packet
|
||||
|
||||
def decode(self, data):
|
||||
''' Decodes a the response
|
||||
|
||||
:param data: The packet data to decode
|
||||
'''
|
||||
count, self.records = 1, []
|
||||
byte_count = byte2int(data[0])
|
||||
while count < byte_count:
|
||||
response_length, reference_type = struct.unpack('>BB', data[count:count+2])
|
||||
count += response_length + 1 # the count is not included
|
||||
record = FileRecord(response_length=response_length,
|
||||
record_data=data[count - response_length + 1:count])
|
||||
if reference_type == 0x06: self.records.append(record)
|
||||
|
||||
|
||||
class WriteFileRecordRequest(ModbusRequest):
|
||||
'''
|
||||
This function code is used to perform a file record write. All
|
||||
request data lengths are provided in terms of number of bytes
|
||||
and all record lengths are provided in terms of the number of 16
|
||||
bit words.
|
||||
'''
|
||||
function_code = 0x15
|
||||
_rtu_byte_count_pos = 2
|
||||
|
||||
def __init__(self, records=None, **kwargs):
|
||||
''' Initializes a new instance
|
||||
|
||||
:param records: The file record requests to be read
|
||||
'''
|
||||
ModbusRequest.__init__(self, **kwargs)
|
||||
self.records = records or []
|
||||
|
||||
def encode(self):
|
||||
''' Encodes the request packet
|
||||
|
||||
:returns: The byte encoded packet
|
||||
'''
|
||||
total_length = sum((record.record_length * 2) + 7 for record in self.records)
|
||||
packet = struct.pack('B', total_length)
|
||||
for record in self.records:
|
||||
packet += struct.pack('>BHHH', 0x06, record.file_number,
|
||||
record.record_number, record.record_length)
|
||||
packet += record.record_data
|
||||
return packet
|
||||
|
||||
def decode(self, data):
|
||||
''' Decodes the incoming request
|
||||
|
||||
:param data: The data to decode into the address
|
||||
'''
|
||||
count, self.records = 1, []
|
||||
byte_count = byte2int(data[0])
|
||||
while count < byte_count:
|
||||
decoded = struct.unpack('>BHHH', data[count:count+7])
|
||||
response_length = decoded[3] * 2
|
||||
count += response_length + 7
|
||||
record = FileRecord(record_length=decoded[3],
|
||||
file_number=decoded[1], record_number=decoded[2],
|
||||
record_data=data[count - response_length:count])
|
||||
if decoded[0] == 0x06: self.records.append(record)
|
||||
|
||||
def execute(self, context):
|
||||
''' Run the write file record request against the context
|
||||
|
||||
:param context: The datastore to request from
|
||||
:returns: The populated response
|
||||
'''
|
||||
# TODO do some new context operation here
|
||||
# if file number, record number, or address + length
|
||||
# is too big, return an error.
|
||||
return WriteFileRecordResponse(self.records)
|
||||
|
||||
|
||||
class WriteFileRecordResponse(ModbusResponse):
|
||||
'''
|
||||
The normal response is an echo of the request.
|
||||
'''
|
||||
function_code = 0x15
|
||||
_rtu_byte_count_pos = 2
|
||||
|
||||
def __init__(self, records=None, **kwargs):
|
||||
''' Initializes a new instance
|
||||
|
||||
:param records: The file record requests to be read
|
||||
'''
|
||||
ModbusResponse.__init__(self, **kwargs)
|
||||
self.records = records or []
|
||||
|
||||
def encode(self):
|
||||
''' Encodes the response
|
||||
|
||||
:returns: The byte encoded message
|
||||
'''
|
||||
total_length = sum((record.record_length * 2) + 7 for record in self.records)
|
||||
packet = struct.pack('B', total_length)
|
||||
for record in self.records:
|
||||
packet += struct.pack('>BHHH', 0x06, record.file_number,
|
||||
record.record_number, record.record_length)
|
||||
packet += record.record_data
|
||||
return packet
|
||||
|
||||
def decode(self, data):
|
||||
''' Decodes the incoming request
|
||||
|
||||
:param data: The data to decode into the address
|
||||
'''
|
||||
count, self.records = 1, []
|
||||
byte_count = byte2int(data[0])
|
||||
while count < byte_count:
|
||||
decoded = struct.unpack('>BHHH', data[count:count+7])
|
||||
response_length = decoded[3] * 2
|
||||
count += response_length + 7
|
||||
record = FileRecord(record_length=decoded[3],
|
||||
file_number=decoded[1], record_number=decoded[2],
|
||||
record_data=data[count - response_length:count])
|
||||
if decoded[0] == 0x06: self.records.append(record)
|
||||
|
||||
|
||||
class ReadFifoQueueRequest(ModbusRequest):
|
||||
'''
|
||||
This function code allows to read the contents of a First-In-First-Out
|
||||
(FIFO) queue of register in a remote device. The function returns a
|
||||
count of the registers in the queue, followed by the queued data.
|
||||
Up to 32 registers can be read: the count, plus up to 31 queued data
|
||||
registers.
|
||||
|
||||
The queue count register is returned first, followed by the queued data
|
||||
registers. The function reads the queue contents, but does not clear
|
||||
them.
|
||||
'''
|
||||
function_code = 0x18
|
||||
_rtu_frame_size = 6
|
||||
|
||||
def __init__(self, address=0x0000, **kwargs):
|
||||
''' Initializes a new instance
|
||||
|
||||
:param address: The fifo pointer address (0x0000 to 0xffff)
|
||||
'''
|
||||
ModbusRequest.__init__(self, **kwargs)
|
||||
self.address = address
|
||||
self.values = [] # this should be added to the context
|
||||
|
||||
def encode(self):
|
||||
''' Encodes the request packet
|
||||
|
||||
:returns: The byte encoded packet
|
||||
'''
|
||||
return struct.pack('>H', self.address)
|
||||
|
||||
def decode(self, data):
|
||||
''' Decodes the incoming request
|
||||
|
||||
:param data: The data to decode into the address
|
||||
'''
|
||||
self.address = struct.unpack('>H', data)[0]
|
||||
|
||||
def execute(self, context):
|
||||
''' Run a read exeception status request against the store
|
||||
|
||||
:param context: The datastore to request from
|
||||
:returns: The populated response
|
||||
'''
|
||||
if not (0x0000 <= self.address <= 0xffff):
|
||||
return self.doException(merror.IllegalValue)
|
||||
if len(self.values) > 31:
|
||||
return self.doException(merror.IllegalValue)
|
||||
# TODO pull the values from some context
|
||||
return ReadFifoQueueResponse(self.values)
|
||||
|
||||
|
||||
class ReadFifoQueueResponse(ModbusResponse):
|
||||
'''
|
||||
In a normal response, the byte count shows the quantity of bytes to
|
||||
follow, including the queue count bytes and value register bytes
|
||||
(but not including the error check field). The queue count is the
|
||||
quantity of data registers in the queue (not including the count register).
|
||||
|
||||
If the queue count exceeds 31, an exception response is returned with an
|
||||
error code of 03 (Illegal Data Value).
|
||||
'''
|
||||
function_code = 0x18
|
||||
|
||||
@classmethod
|
||||
def calculateRtuFrameSize(cls, buffer):
|
||||
''' Calculates the size of the message
|
||||
|
||||
:param buffer: A buffer containing the data that have been received.
|
||||
:returns: The number of bytes in the response.
|
||||
'''
|
||||
hi_byte = byte2int(buffer[2])
|
||||
lo_byte = byte2int(buffer[3])
|
||||
return (hi_byte << 16) + lo_byte + 6
|
||||
|
||||
def __init__(self, values=None, **kwargs):
|
||||
''' Initializes a new instance
|
||||
|
||||
:param values: The list of values of the fifo to return
|
||||
'''
|
||||
ModbusResponse.__init__(self, **kwargs)
|
||||
self.values = values or []
|
||||
|
||||
def encode(self):
|
||||
''' Encodes the response
|
||||
|
||||
:returns: The byte encoded message
|
||||
'''
|
||||
length = len(self.values) * 2
|
||||
packet = struct.pack('>HH', 2 + length, length)
|
||||
for value in self.values:
|
||||
packet += struct.pack('>H', value)
|
||||
return packet
|
||||
|
||||
def decode(self, data):
|
||||
''' Decodes a the response
|
||||
|
||||
:param data: The packet data to decode
|
||||
'''
|
||||
self.values = []
|
||||
_, count = struct.unpack('>HH', data[0:4])
|
||||
for index in range(0, count - 4):
|
||||
idx = 4 + index * 2
|
||||
self.values.append(struct.unpack('>H', data[idx:idx + 2])[0])
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Exported symbols
|
||||
#---------------------------------------------------------------------------#
|
||||
__all__ = [
|
||||
"FileRecord",
|
||||
"ReadFileRecordRequest", "ReadFileRecordResponse",
|
||||
"WriteFileRecordRequest", "WriteFileRecordResponse",
|
||||
"ReadFifoQueueRequest", "ReadFifoQueueResponse",
|
||||
]
|
||||
49
modbus/pymodbus/framer/__init__.py
Normal file
@@ -0,0 +1,49 @@
|
||||
from pymodbus.interfaces import IModbusFramer
|
||||
import struct
|
||||
|
||||
# Unit ID, Function Code
|
||||
BYTE_ORDER = '>'
|
||||
FRAME_HEADER = 'BB'
|
||||
|
||||
# Transaction Id, Protocol ID, Length, Unit ID, Function Code
|
||||
SOCKET_FRAME_HEADER = BYTE_ORDER + 'HHH' + FRAME_HEADER
|
||||
|
||||
# Function Code
|
||||
TLS_FRAME_HEADER = BYTE_ORDER + 'B'
|
||||
|
||||
class ModbusFramer(IModbusFramer):
|
||||
"""
|
||||
Base Framer class
|
||||
"""
|
||||
|
||||
def _validate_unit_id(self, units, single):
|
||||
"""
|
||||
Validates if the received data is valid for the client
|
||||
:param units: list of unit id for which the transaction is valid
|
||||
:param single: Set to true to treat this as a single context
|
||||
:return: """
|
||||
|
||||
if single:
|
||||
return True
|
||||
else:
|
||||
if 0 in units or 0xFF in units:
|
||||
# Handle Modbus TCP unit identifier (0x00 0r 0xFF)
|
||||
# in asynchronous requests
|
||||
return True
|
||||
return self._header['uid'] in units
|
||||
|
||||
def sendPacket(self, message):
|
||||
"""
|
||||
Sends packets on the bus with 3.5char delay between frames
|
||||
:param message: Message to be sent over the bus
|
||||
:return:
|
||||
"""
|
||||
return self.client.send(message)
|
||||
|
||||
def recvPacket(self, size):
|
||||
"""
|
||||
Receives packet from the bus with specified len
|
||||
:param size: Number of bytes to read
|
||||
:return:
|
||||
"""
|
||||
return self.client.recv(size)
|
||||
208
modbus/pymodbus/framer/ascii_framer.py
Normal file
@@ -0,0 +1,208 @@
|
||||
import struct
|
||||
from binascii import b2a_hex, a2b_hex
|
||||
|
||||
from pymodbus.exceptions import ModbusIOException
|
||||
from pymodbus.utilities import checkLRC, computeLRC
|
||||
from pymodbus.framer import ModbusFramer, FRAME_HEADER, BYTE_ORDER
|
||||
|
||||
|
||||
ASCII_FRAME_HEADER = BYTE_ORDER + FRAME_HEADER
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Logging
|
||||
# --------------------------------------------------------------------------- #
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Modbus ASCII Message
|
||||
# --------------------------------------------------------------------------- #
|
||||
class ModbusAsciiFramer(ModbusFramer):
|
||||
"""
|
||||
Modbus ASCII Frame Controller::
|
||||
|
||||
[ Start ][Address ][ Function ][ Data ][ LRC ][ End ]
|
||||
1c 2c 2c Nc 2c 2c
|
||||
|
||||
* data can be 0 - 2x252 chars
|
||||
* end is '\\r\\n' (Carriage return line feed), however the line feed
|
||||
character can be changed via a special command
|
||||
* start is ':'
|
||||
|
||||
This framer is used for serial transmission. Unlike the RTU protocol,
|
||||
the data in this framer is transferred in plain text ascii.
|
||||
"""
|
||||
|
||||
def __init__(self, decoder, client=None):
|
||||
""" Initializes a new instance of the framer
|
||||
|
||||
:param decoder: The decoder implementation to use
|
||||
"""
|
||||
self._buffer = b''
|
||||
self._header = {'lrc': '0000', 'len': 0, 'uid': 0x00}
|
||||
self._hsize = 0x02
|
||||
self._start = b':'
|
||||
self._end = b"\r\n"
|
||||
self.decoder = decoder
|
||||
self.client = client
|
||||
|
||||
# ----------------------------------------------------------------------- #
|
||||
# Private Helper Functions
|
||||
# ----------------------------------------------------------------------- #
|
||||
def decode_data(self, data):
|
||||
if len(data) > 1:
|
||||
uid = int(data[1:3], 16)
|
||||
fcode = int(data[3:5], 16)
|
||||
return dict(unit=uid, fcode=fcode)
|
||||
return dict()
|
||||
|
||||
def checkFrame(self):
|
||||
""" Check and decode the next frame
|
||||
|
||||
:returns: True if we successful, False otherwise
|
||||
"""
|
||||
start = self._buffer.find(self._start)
|
||||
if start == -1:
|
||||
return False
|
||||
if start > 0: # go ahead and skip old bad data
|
||||
self._buffer = self._buffer[start:]
|
||||
start = 0
|
||||
|
||||
end = self._buffer.find(self._end)
|
||||
if end != -1:
|
||||
self._header['len'] = end
|
||||
self._header['uid'] = int(self._buffer[1:3], 16)
|
||||
self._header['lrc'] = int(self._buffer[end - 2:end], 16)
|
||||
data = a2b_hex(self._buffer[start + 1:end - 2])
|
||||
return checkLRC(data, self._header['lrc'])
|
||||
return False
|
||||
|
||||
def advanceFrame(self):
|
||||
""" Skip over the current framed message
|
||||
This allows us to skip over the current message after we have processed
|
||||
it or determined that it contains an error. It also has to reset the
|
||||
current frame header handle
|
||||
"""
|
||||
self._buffer = self._buffer[self._header['len'] + 2:]
|
||||
self._header = {'lrc': '0000', 'len': 0, 'uid': 0x00}
|
||||
|
||||
def isFrameReady(self):
|
||||
""" Check if we should continue decode logic
|
||||
This is meant to be used in a while loop in the decoding phase to let
|
||||
the decoder know that there is still data in the buffer.
|
||||
|
||||
:returns: True if ready, False otherwise
|
||||
"""
|
||||
return len(self._buffer) > 1
|
||||
|
||||
def addToFrame(self, message):
|
||||
""" Add the next message to the frame buffer
|
||||
This should be used before the decoding while loop to add the received
|
||||
data to the buffer handle.
|
||||
|
||||
:param message: The most recent packet
|
||||
"""
|
||||
self._buffer += message
|
||||
|
||||
def getFrame(self):
|
||||
""" Get the next frame from the buffer
|
||||
|
||||
:returns: The frame data or ''
|
||||
"""
|
||||
start = self._hsize + 1
|
||||
end = self._header['len'] - 2
|
||||
buffer = self._buffer[start:end]
|
||||
if end > 0:
|
||||
return a2b_hex(buffer)
|
||||
return b''
|
||||
|
||||
def resetFrame(self):
|
||||
""" Reset the entire message frame.
|
||||
This allows us to skip ovver errors that may be in the stream.
|
||||
It is hard to know if we are simply out of sync or if there is
|
||||
an error in the stream as we have no way to check the start or
|
||||
end of the message (python just doesn't have the resolution to
|
||||
check for millisecond delays).
|
||||
"""
|
||||
self._buffer = b''
|
||||
self._header = {'lrc': '0000', 'len': 0, 'uid': 0x00}
|
||||
|
||||
def populateResult(self, result):
|
||||
""" Populates the modbus result header
|
||||
|
||||
The serial packets do not have any header information
|
||||
that is copied.
|
||||
|
||||
:param result: The response packet
|
||||
"""
|
||||
result.unit_id = self._header['uid']
|
||||
|
||||
# ----------------------------------------------------------------------- #
|
||||
# Public Member Functions
|
||||
# ----------------------------------------------------------------------- #
|
||||
def processIncomingPacket(self, data, callback, unit, **kwargs):
|
||||
"""
|
||||
The new packet processing pattern
|
||||
|
||||
This takes in a new request packet, adds it to the current
|
||||
packet stream, and performs framing on it. That is, checks
|
||||
for complete messages, and once found, will process all that
|
||||
exist. This handles the case when we read N + 1 or 1 // N
|
||||
messages at a time instead of 1.
|
||||
|
||||
The processed and decoded messages are pushed to the callback
|
||||
function to process and send.
|
||||
|
||||
:param data: The new packet data
|
||||
:param callback: The function to send results to
|
||||
:param unit: Process if unit id matches, ignore otherwise (could be a
|
||||
list of unit ids (server) or single unit id(client/server))
|
||||
:param single: True or False (If True, ignore unit address validation)
|
||||
|
||||
"""
|
||||
if not isinstance(unit, (list, tuple)):
|
||||
unit = [unit]
|
||||
single = kwargs.get('single', False)
|
||||
self.addToFrame(data)
|
||||
while self.isFrameReady():
|
||||
if self.checkFrame():
|
||||
if self._validate_unit_id(unit, single):
|
||||
frame = self.getFrame()
|
||||
result = self.decoder.decode(frame)
|
||||
if result is None:
|
||||
raise ModbusIOException("Unable to decode response")
|
||||
self.populateResult(result)
|
||||
self.advanceFrame()
|
||||
callback(result) # defer this
|
||||
else:
|
||||
_logger.error("Not a valid unit id - {}, "
|
||||
"ignoring!!".format(self._header['uid']))
|
||||
self.resetFrame()
|
||||
else:
|
||||
break
|
||||
|
||||
def buildPacket(self, message):
|
||||
""" Creates a ready to send modbus packet
|
||||
Built off of a modbus request/response
|
||||
|
||||
:param message: The request/response to send
|
||||
:return: The encoded packet
|
||||
"""
|
||||
encoded = message.encode()
|
||||
buffer = struct.pack(ASCII_FRAME_HEADER, message.unit_id,
|
||||
message.function_code)
|
||||
checksum = computeLRC(encoded + buffer)
|
||||
|
||||
packet = bytearray()
|
||||
params = (message.unit_id, message.function_code)
|
||||
packet.extend(self._start)
|
||||
packet.extend(('%02x%02x' % params).encode())
|
||||
packet.extend(b2a_hex(encoded))
|
||||
packet.extend(('%02x' % checksum).encode())
|
||||
packet.extend(self._end)
|
||||
return bytes(packet).upper()
|
||||
|
||||
|
||||
# __END__
|
||||
|
||||
227
modbus/pymodbus/framer/binary_framer.py
Normal file
@@ -0,0 +1,227 @@
|
||||
import struct
|
||||
from pymodbus.exceptions import ModbusIOException
|
||||
from pymodbus.utilities import checkCRC, computeCRC
|
||||
from pymodbus.framer import ModbusFramer, FRAME_HEADER, BYTE_ORDER
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Logging
|
||||
# --------------------------------------------------------------------------- #
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
BINARY_FRAME_HEADER = BYTE_ORDER + FRAME_HEADER
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Modbus Binary Message
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
class ModbusBinaryFramer(ModbusFramer):
|
||||
"""
|
||||
Modbus Binary Frame Controller::
|
||||
|
||||
[ Start ][Address ][ Function ][ Data ][ CRC ][ End ]
|
||||
1b 1b 1b Nb 2b 1b
|
||||
|
||||
* data can be 0 - 2x252 chars
|
||||
* end is '}'
|
||||
* start is '{'
|
||||
|
||||
The idea here is that we implement the RTU protocol, however,
|
||||
instead of using timing for message delimiting, we use start
|
||||
and end of message characters (in this case { and }). Basically,
|
||||
this is a binary framer.
|
||||
|
||||
The only case we have to watch out for is when a message contains
|
||||
the { or } characters. If we encounter these characters, we
|
||||
simply duplicate them. Hopefully we will not encounter those
|
||||
characters that often and will save a little bit of bandwitch
|
||||
without a real-time system.
|
||||
|
||||
Protocol defined by jamod.sourceforge.net.
|
||||
"""
|
||||
|
||||
def __init__(self, decoder, client=None):
|
||||
""" Initializes a new instance of the framer
|
||||
|
||||
:param decoder: The decoder implementation to use
|
||||
"""
|
||||
self._buffer = b''
|
||||
self._header = {'crc': 0x0000, 'len': 0, 'uid': 0x00}
|
||||
self._hsize = 0x01
|
||||
self._start = b'\x7b' # {
|
||||
self._end = b'\x7d' # }
|
||||
self._repeat = [b'}'[0], b'{'[0]] # python3 hack
|
||||
self.decoder = decoder
|
||||
self.client = client
|
||||
|
||||
# ----------------------------------------------------------------------- #
|
||||
# Private Helper Functions
|
||||
# ----------------------------------------------------------------------- #
|
||||
def decode_data(self, data):
|
||||
if len(data) > self._hsize:
|
||||
uid = struct.unpack('>B', data[1:2])[0]
|
||||
fcode = struct.unpack('>B', data[2:3])[0]
|
||||
return dict(unit=uid, fcode=fcode)
|
||||
return dict()
|
||||
|
||||
def checkFrame(self):
|
||||
""" Check and decode the next frame
|
||||
|
||||
:returns: True if we are successful, False otherwise
|
||||
"""
|
||||
start = self._buffer.find(self._start)
|
||||
if start == -1:
|
||||
return False
|
||||
if start > 0: # go ahead and skip old bad data
|
||||
self._buffer = self._buffer[start:]
|
||||
|
||||
end = self._buffer.find(self._end)
|
||||
if end != -1:
|
||||
self._header['len'] = end
|
||||
self._header['uid'] = struct.unpack('>B', self._buffer[1:2])[0]
|
||||
self._header['crc'] = struct.unpack('>H', self._buffer[end - 2:end])[0]
|
||||
data = self._buffer[start + 1:end - 2]
|
||||
return checkCRC(data, self._header['crc'])
|
||||
return False
|
||||
|
||||
def advanceFrame(self):
|
||||
""" Skip over the current framed message
|
||||
This allows us to skip over the current message after we have processed
|
||||
it or determined that it contains an error. It also has to reset the
|
||||
current frame header handle
|
||||
"""
|
||||
self._buffer = self._buffer[self._header['len'] + 2:]
|
||||
self._header = {'crc':0x0000, 'len':0, 'uid':0x00}
|
||||
|
||||
def isFrameReady(self):
|
||||
""" Check if we should continue decode logic
|
||||
This is meant to be used in a while loop in the decoding phase to let
|
||||
the decoder know that there is still data in the buffer.
|
||||
|
||||
:returns: True if ready, False otherwise
|
||||
"""
|
||||
return len(self._buffer) > 1
|
||||
|
||||
def addToFrame(self, message):
|
||||
""" Add the next message to the frame buffer
|
||||
This should be used before the decoding while loop to add the received
|
||||
data to the buffer handle.
|
||||
|
||||
:param message: The most recent packet
|
||||
"""
|
||||
self._buffer += message
|
||||
|
||||
def getFrame(self):
|
||||
""" Get the next frame from the buffer
|
||||
|
||||
:returns: The frame data or ''
|
||||
"""
|
||||
start = self._hsize + 1
|
||||
end = self._header['len'] - 2
|
||||
buffer = self._buffer[start:end]
|
||||
if end > 0:
|
||||
return buffer
|
||||
return b''
|
||||
|
||||
def populateResult(self, result):
|
||||
""" Populates the modbus result header
|
||||
|
||||
The serial packets do not have any header information
|
||||
that is copied.
|
||||
|
||||
:param result: The response packet
|
||||
"""
|
||||
result.unit_id = self._header['uid']
|
||||
|
||||
# ----------------------------------------------------------------------- #
|
||||
# Public Member Functions
|
||||
# ----------------------------------------------------------------------- #
|
||||
def processIncomingPacket(self, data, callback, unit, **kwargs):
|
||||
"""
|
||||
The new packet processing pattern
|
||||
|
||||
This takes in a new request packet, adds it to the current
|
||||
packet stream, and performs framing on it. That is, checks
|
||||
for complete messages, and once found, will process all that
|
||||
exist. This handles the case when we read N + 1 or 1 // N
|
||||
messages at a time instead of 1.
|
||||
|
||||
The processed and decoded messages are pushed to the callback
|
||||
function to process and send.
|
||||
|
||||
:param data: The new packet data
|
||||
:param callback: The function to send results to
|
||||
:param unit: Process if unit id matches, ignore otherwise (could be a
|
||||
list of unit ids (server) or single unit id(client/server)
|
||||
:param single: True or False (If True, ignore unit address validation)
|
||||
|
||||
"""
|
||||
self.addToFrame(data)
|
||||
if not isinstance(unit, (list, tuple)):
|
||||
unit = [unit]
|
||||
single = kwargs.get('single', False)
|
||||
while self.isFrameReady():
|
||||
if self.checkFrame():
|
||||
if self._validate_unit_id(unit, single):
|
||||
result = self.decoder.decode(self.getFrame())
|
||||
if result is None:
|
||||
raise ModbusIOException("Unable to decode response")
|
||||
self.populateResult(result)
|
||||
self.advanceFrame()
|
||||
callback(result) # defer or push to a thread?
|
||||
else:
|
||||
_logger.debug("Not a valid unit id - {}, "
|
||||
"ignoring!!".format(self._header['uid']))
|
||||
self.resetFrame()
|
||||
break
|
||||
|
||||
else:
|
||||
_logger.debug("Frame check failed, ignoring!!")
|
||||
self.resetFrame()
|
||||
break
|
||||
|
||||
def buildPacket(self, message):
|
||||
""" Creates a ready to send modbus packet
|
||||
|
||||
:param message: The request/response to send
|
||||
:returns: The encoded packet
|
||||
"""
|
||||
data = self._preflight(message.encode())
|
||||
packet = struct.pack(BINARY_FRAME_HEADER,
|
||||
message.unit_id,
|
||||
message.function_code) + data
|
||||
packet += struct.pack(">H", computeCRC(packet))
|
||||
packet = self._start + packet + self._end
|
||||
return packet
|
||||
|
||||
def _preflight(self, data):
|
||||
"""
|
||||
Preflight buffer test
|
||||
|
||||
This basically scans the buffer for start and end
|
||||
tags and if found, escapes them.
|
||||
|
||||
:param data: The message to escape
|
||||
:returns: the escaped packet
|
||||
"""
|
||||
array = bytearray()
|
||||
for d in data:
|
||||
if d in self._repeat:
|
||||
array.append(d)
|
||||
array.append(d)
|
||||
return bytes(array)
|
||||
|
||||
def resetFrame(self):
|
||||
""" Reset the entire message frame.
|
||||
This allows us to skip ovver errors that may be in the stream.
|
||||
It is hard to know if we are simply out of sync or if there is
|
||||
an error in the stream as we have no way to check the start or
|
||||
end of the message (python just doesn't have the resolution to
|
||||
check for millisecond delays).
|
||||
"""
|
||||
self._buffer = b''
|
||||
self._header = {'crc': 0x0000, 'len': 0, 'uid': 0x00}
|
||||
|
||||
|
||||
# __END__
|
||||
334
modbus/pymodbus/framer/rtu_framer.py
Normal file
@@ -0,0 +1,334 @@
|
||||
import struct
|
||||
import time
|
||||
|
||||
from pymodbus.exceptions import ModbusIOException
|
||||
from pymodbus.exceptions import InvalidMessageReceivedException
|
||||
from pymodbus.utilities import checkCRC, computeCRC
|
||||
from pymodbus.utilities import hexlify_packets, ModbusTransactionState
|
||||
from pymodbus.compat import byte2int
|
||||
from pymodbus.framer import ModbusFramer, FRAME_HEADER, BYTE_ORDER
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Logging
|
||||
# --------------------------------------------------------------------------- #
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
RTU_FRAME_HEADER = BYTE_ORDER + FRAME_HEADER
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Modbus RTU Message
|
||||
# --------------------------------------------------------------------------- #
|
||||
class ModbusRtuFramer(ModbusFramer):
|
||||
"""
|
||||
Modbus RTU Frame controller::
|
||||
|
||||
[ Start Wait ] [Address ][ Function Code] [ Data ][ CRC ][ End Wait ]
|
||||
3.5 chars 1b 1b Nb 2b 3.5 chars
|
||||
|
||||
Wait refers to the amount of time required to transmit at least x many
|
||||
characters. In this case it is 3.5 characters. Also, if we receive a
|
||||
wait of 1.5 characters at any point, we must trigger an error message.
|
||||
Also, it appears as though this message is little endian. The logic is
|
||||
simplified as the following::
|
||||
|
||||
block-on-read:
|
||||
read until 3.5 delay
|
||||
check for errors
|
||||
decode
|
||||
|
||||
The following table is a listing of the baud wait times for the specified
|
||||
baud rates::
|
||||
|
||||
------------------------------------------------------------------
|
||||
Baud 1.5c (18 bits) 3.5c (38 bits)
|
||||
------------------------------------------------------------------
|
||||
1200 13333.3 us 31666.7 us
|
||||
4800 3333.3 us 7916.7 us
|
||||
9600 1666.7 us 3958.3 us
|
||||
19200 833.3 us 1979.2 us
|
||||
38400 416.7 us 989.6 us
|
||||
------------------------------------------------------------------
|
||||
1 Byte = start + 8 bits + parity + stop = 11 bits
|
||||
(1/Baud)(bits) = delay seconds
|
||||
"""
|
||||
|
||||
def __init__(self, decoder, client=None):
|
||||
""" Initializes a new instance of the framer
|
||||
|
||||
:param decoder: The decoder factory implementation to use
|
||||
"""
|
||||
self._buffer = b''
|
||||
self._header = {'uid': 0x00, 'len': 0, 'crc': b'\x00\x00'}
|
||||
self._hsize = 0x01
|
||||
self._end = b'\x0d\x0a'
|
||||
self._min_frame_size = 4
|
||||
self.decoder = decoder
|
||||
self.client = client
|
||||
|
||||
# ----------------------------------------------------------------------- #
|
||||
# Private Helper Functions
|
||||
# ----------------------------------------------------------------------- #
|
||||
def decode_data(self, data):
|
||||
if len(data) > self._hsize:
|
||||
uid = byte2int(data[0])
|
||||
fcode = byte2int(data[1])
|
||||
return dict(unit=uid, fcode=fcode)
|
||||
return dict()
|
||||
|
||||
def checkFrame(self):
|
||||
"""
|
||||
Check if the next frame is available.
|
||||
Return True if we were successful.
|
||||
|
||||
1. Populate header
|
||||
2. Discard frame if UID does not match
|
||||
"""
|
||||
try:
|
||||
self.populateHeader()
|
||||
frame_size = self._header['len']
|
||||
data = self._buffer[:frame_size - 2]
|
||||
crc = self._header['crc']
|
||||
crc_val = (byte2int(crc[0]) << 8) + byte2int(crc[1])
|
||||
return checkCRC(data, crc_val)
|
||||
except (IndexError, KeyError, struct.error):
|
||||
return False
|
||||
|
||||
def advanceFrame(self):
|
||||
"""
|
||||
Skip over the current framed message
|
||||
This allows us to skip over the current message after we have processed
|
||||
it or determined that it contains an error. It also has to reset the
|
||||
current frame header handle
|
||||
"""
|
||||
|
||||
self._buffer = self._buffer[self._header['len']:]
|
||||
_logger.debug("Frame advanced, resetting header!!")
|
||||
self._header = {'uid': 0x00, 'len': 0, 'crc': b'\x00\x00'}
|
||||
|
||||
def resetFrame(self):
|
||||
"""
|
||||
Reset the entire message frame.
|
||||
This allows us to skip over errors that may be in the stream.
|
||||
It is hard to know if we are simply out of sync or if there is
|
||||
an error in the stream as we have no way to check the start or
|
||||
end of the message (python just doesn't have the resolution to
|
||||
check for millisecond delays).
|
||||
"""
|
||||
_logger.debug("Resetting frame - Current Frame in "
|
||||
"buffer - {}".format(hexlify_packets(self._buffer)))
|
||||
self._buffer = b''
|
||||
self._header = {'uid': 0x00, 'len': 0, 'crc': b'\x00\x00'}
|
||||
|
||||
def isFrameReady(self):
|
||||
"""
|
||||
Check if we should continue decode logic
|
||||
This is meant to be used in a while loop in the decoding phase to let
|
||||
the decoder know that there is still data in the buffer.
|
||||
|
||||
:returns: True if ready, False otherwise
|
||||
"""
|
||||
if len(self._buffer) <= self._hsize:
|
||||
return False
|
||||
|
||||
try:
|
||||
# Frame is ready only if populateHeader() successfully populates crc field which finishes RTU frame
|
||||
# Otherwise, if buffer is not yet long enough, populateHeader() raises IndexError
|
||||
self.populateHeader()
|
||||
except IndexError:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def populateHeader(self, data=None):
|
||||
"""
|
||||
Try to set the headers `uid`, `len` and `crc`.
|
||||
|
||||
This method examines `self._buffer` and writes meta
|
||||
information into `self._header`.
|
||||
|
||||
Beware that this method will raise an IndexError if
|
||||
`self._buffer` is not yet long enough.
|
||||
"""
|
||||
data = data if data is not None else self._buffer
|
||||
self._header['uid'] = byte2int(data[0])
|
||||
func_code = byte2int(data[1])
|
||||
pdu_class = self.decoder.lookupPduClass(func_code)
|
||||
size = pdu_class.calculateRtuFrameSize(data)
|
||||
self._header['len'] = size
|
||||
|
||||
if len(data) < size:
|
||||
# crc yet not available
|
||||
raise IndexError
|
||||
self._header['crc'] = data[size - 2:size]
|
||||
|
||||
def addToFrame(self, message):
|
||||
"""
|
||||
This should be used before the decoding while loop to add the received
|
||||
data to the buffer handle.
|
||||
|
||||
:param message: The most recent packet
|
||||
"""
|
||||
self._buffer += message
|
||||
|
||||
def getFrame(self):
|
||||
"""
|
||||
Get the next frame from the buffer
|
||||
|
||||
:returns: The frame data or ''
|
||||
"""
|
||||
start = self._hsize
|
||||
end = self._header['len'] - 2
|
||||
buffer = self._buffer[start:end]
|
||||
if end > 0:
|
||||
_logger.debug("Getting Frame - {}".format(hexlify_packets(buffer)))
|
||||
return buffer
|
||||
return b''
|
||||
|
||||
def populateResult(self, result):
|
||||
"""
|
||||
Populates the modbus result header
|
||||
|
||||
The serial packets do not have any header information
|
||||
that is copied.
|
||||
|
||||
:param result: The response packet
|
||||
"""
|
||||
result.unit_id = self._header['uid']
|
||||
result.transaction_id = self._header['uid']
|
||||
|
||||
# ----------------------------------------------------------------------- #
|
||||
# Public Member Functions
|
||||
# ----------------------------------------------------------------------- #
|
||||
def processIncomingPacket(self, data, callback, unit, **kwargs):
|
||||
"""
|
||||
The new packet processing pattern
|
||||
|
||||
This takes in a new request packet, adds it to the current
|
||||
packet stream, and performs framing on it. That is, checks
|
||||
for complete messages, and once found, will process all that
|
||||
exist. This handles the case when we read N + 1 or 1 // N
|
||||
messages at a time instead of 1.
|
||||
|
||||
The processed and decoded messages are pushed to the callback
|
||||
function to process and send.
|
||||
|
||||
:param data: The new packet data
|
||||
:param callback: The function to send results to
|
||||
:param unit: Process if unit id matches, ignore otherwise (could be a
|
||||
list of unit ids (server) or single unit id(client/server)
|
||||
:param single: True or False (If True, ignore unit address validation)
|
||||
|
||||
"""
|
||||
if not isinstance(unit, (list, tuple)):
|
||||
unit = [unit]
|
||||
self.addToFrame(data)
|
||||
single = kwargs.get("single", False)
|
||||
if self.isFrameReady():
|
||||
if self.checkFrame():
|
||||
if self._validate_unit_id(unit, single):
|
||||
self._process(callback)
|
||||
else:
|
||||
_logger.debug("Not a valid unit id - {}, "
|
||||
"ignoring!!".format(self._header['uid']))
|
||||
self.resetFrame()
|
||||
else:
|
||||
_logger.debug("Frame check failed, ignoring!!")
|
||||
self.resetFrame()
|
||||
else:
|
||||
_logger.debug("Frame - [{}] not ready".format(data))
|
||||
|
||||
def buildPacket(self, message):
|
||||
"""
|
||||
Creates a ready to send modbus packet
|
||||
|
||||
:param message: The populated request/response to send
|
||||
"""
|
||||
data = message.encode()
|
||||
packet = struct.pack(RTU_FRAME_HEADER,
|
||||
message.unit_id,
|
||||
message.function_code) + data
|
||||
packet += struct.pack(">H", computeCRC(packet))
|
||||
message.transaction_id = message.unit_id # Ensure that transaction is actually the unit id for serial comms
|
||||
return packet
|
||||
|
||||
def sendPacket(self, message):
|
||||
"""
|
||||
Sends packets on the bus with 3.5char delay between frames
|
||||
:param message: Message to be sent over the bus
|
||||
:return:
|
||||
"""
|
||||
start = time.time()
|
||||
timeout = start + self.client.timeout
|
||||
while self.client.state != ModbusTransactionState.IDLE:
|
||||
if self.client.state == ModbusTransactionState.TRANSACTION_COMPLETE:
|
||||
ts = round(time.time(), 6)
|
||||
_logger.debug("Changing state to IDLE - Last Frame End - {}, "
|
||||
"Current Time stamp - {}".format(
|
||||
self.client.last_frame_end, ts)
|
||||
)
|
||||
|
||||
if self.client.last_frame_end:
|
||||
idle_time = self.client.idle_time()
|
||||
if round(ts - idle_time, 6) <= self.client.silent_interval:
|
||||
_logger.debug("Waiting for 3.5 char before next "
|
||||
"send - {} ms".format(
|
||||
self.client.silent_interval * 1000)
|
||||
)
|
||||
time.sleep(self.client.silent_interval)
|
||||
else:
|
||||
# Recovering from last error ??
|
||||
time.sleep(self.client.silent_interval)
|
||||
self.client.state = ModbusTransactionState.IDLE
|
||||
elif self.client.state == ModbusTransactionState.RETRYING:
|
||||
# Simple lets settle down!!!
|
||||
# To check for higher baudrates
|
||||
time.sleep(self.client.timeout)
|
||||
break
|
||||
else:
|
||||
if time.time() > timeout:
|
||||
_logger.debug("Spent more time than the read time out, "
|
||||
"resetting the transaction to IDLE")
|
||||
self.client.state = ModbusTransactionState.IDLE
|
||||
else:
|
||||
_logger.debug("Sleeping")
|
||||
time.sleep(self.client.silent_interval)
|
||||
size = self.client.send(message)
|
||||
self.client.last_frame_end = round(time.time(), 6)
|
||||
return size
|
||||
|
||||
def recvPacket(self, size):
|
||||
"""
|
||||
Receives packet from the bus with specified len
|
||||
:param size: Number of bytes to read
|
||||
:return:
|
||||
"""
|
||||
result = self.client.recv(size)
|
||||
self.client.last_frame_end = round(time.time(), 6)
|
||||
return result
|
||||
|
||||
def _process(self, callback, error=False):
|
||||
"""
|
||||
Process incoming packets irrespective error condition
|
||||
"""
|
||||
data = self.getRawFrame() if error else self.getFrame()
|
||||
result = self.decoder.decode(data)
|
||||
if result is None:
|
||||
raise ModbusIOException("Unable to decode request")
|
||||
elif error and result.function_code < 0x80:
|
||||
raise InvalidMessageReceivedException(result)
|
||||
else:
|
||||
self.populateResult(result)
|
||||
self.advanceFrame()
|
||||
callback(result) # defer or push to a thread?
|
||||
|
||||
def getRawFrame(self):
|
||||
"""
|
||||
Returns the complete buffer
|
||||
"""
|
||||
_logger.debug("Getting Raw Frame - "
|
||||
"{}".format(hexlify_packets(self._buffer)))
|
||||
return self._buffer
|
||||
|
||||
# __END__
|
||||
217
modbus/pymodbus/framer/socket_framer.py
Normal file
@@ -0,0 +1,217 @@
|
||||
import struct
|
||||
from pymodbus.exceptions import ModbusIOException
|
||||
from pymodbus.exceptions import InvalidMessageReceivedException
|
||||
from pymodbus.utilities import hexlify_packets
|
||||
from pymodbus.framer import ModbusFramer, SOCKET_FRAME_HEADER
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Logging
|
||||
# --------------------------------------------------------------------------- #
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Modbus TCP Message
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
class ModbusSocketFramer(ModbusFramer):
|
||||
""" Modbus Socket Frame controller
|
||||
|
||||
Before each modbus TCP message is an MBAP header which is used as a
|
||||
message frame. It allows us to easily separate messages as follows::
|
||||
|
||||
[ MBAP Header ] [ Function Code] [ Data ] \
|
||||
[ tid ][ pid ][ length ][ uid ]
|
||||
2b 2b 2b 1b 1b Nb
|
||||
|
||||
while len(message) > 0:
|
||||
tid, pid, length`, uid = struct.unpack(">HHHB", message)
|
||||
request = message[0:7 + length - 1`]
|
||||
message = [7 + length - 1:]
|
||||
|
||||
* length = uid + function code + data
|
||||
* The -1 is to account for the uid byte
|
||||
"""
|
||||
|
||||
def __init__(self, decoder, client=None):
|
||||
""" Initializes a new instance of the framer
|
||||
|
||||
:param decoder: The decoder factory implementation to use
|
||||
"""
|
||||
self._buffer = b''
|
||||
self._header = {'tid': 0, 'pid': 0, 'len': 0, 'uid': 0}
|
||||
self._hsize = 0x07
|
||||
self.decoder = decoder
|
||||
self.client = client
|
||||
|
||||
# ----------------------------------------------------------------------- #
|
||||
# Private Helper Functions
|
||||
# ----------------------------------------------------------------------- #
|
||||
def checkFrame(self):
|
||||
"""
|
||||
Check and decode the next frame Return true if we were successful
|
||||
"""
|
||||
if self.isFrameReady():
|
||||
(self._header['tid'], self._header['pid'],
|
||||
self._header['len'], self._header['uid']) = struct.unpack(
|
||||
'>HHHB', self._buffer[0:self._hsize])
|
||||
|
||||
# someone sent us an error? ignore it
|
||||
if self._header['len'] < 2:
|
||||
self.advanceFrame()
|
||||
# we have at least a complete message, continue
|
||||
elif len(self._buffer) - self._hsize + 1 >= self._header['len']:
|
||||
return True
|
||||
# we don't have enough of a message yet, wait
|
||||
return False
|
||||
|
||||
def advanceFrame(self):
|
||||
""" Skip over the current framed message
|
||||
This allows us to skip over the current message after we have processed
|
||||
it or determined that it contains an error. It also has to reset the
|
||||
current frame header handle
|
||||
"""
|
||||
length = self._hsize + self._header['len'] - 1
|
||||
self._buffer = self._buffer[length:]
|
||||
self._header = {'tid': 0, 'pid': 0, 'len': 0, 'uid': 0}
|
||||
|
||||
def isFrameReady(self):
|
||||
""" Check if we should continue decode logic
|
||||
This is meant to be used in a while loop in the decoding phase to let
|
||||
the decoder factory know that there is still data in the buffer.
|
||||
|
||||
:returns: True if ready, False otherwise
|
||||
"""
|
||||
return len(self._buffer) > self._hsize
|
||||
|
||||
def addToFrame(self, message):
|
||||
""" Adds new packet data to the current frame buffer
|
||||
|
||||
:param message: The most recent packet
|
||||
"""
|
||||
self._buffer += message
|
||||
|
||||
def getFrame(self):
|
||||
""" Return the next frame from the buffered data
|
||||
|
||||
:returns: The next full frame buffer
|
||||
"""
|
||||
length = self._hsize + self._header['len'] - 1
|
||||
return self._buffer[self._hsize:length]
|
||||
|
||||
def populateResult(self, result):
|
||||
"""
|
||||
Populates the modbus result with the transport specific header
|
||||
information (pid, tid, uid, checksum, etc)
|
||||
|
||||
:param result: The response packet
|
||||
"""
|
||||
result.transaction_id = self._header['tid']
|
||||
result.protocol_id = self._header['pid']
|
||||
result.unit_id = self._header['uid']
|
||||
|
||||
# ----------------------------------------------------------------------- #
|
||||
# Public Member Functions
|
||||
# ----------------------------------------------------------------------- #
|
||||
def decode_data(self, data):
|
||||
if len(data) > self._hsize:
|
||||
tid, pid, length, uid, fcode = struct.unpack(SOCKET_FRAME_HEADER,
|
||||
data[0:self._hsize+1])
|
||||
return dict(tid=tid, pid=pid, length=length, unit=uid, fcode=fcode)
|
||||
return dict()
|
||||
|
||||
def processIncomingPacket(self, data, callback, unit, **kwargs):
|
||||
"""
|
||||
The new packet processing pattern
|
||||
|
||||
This takes in a new request packet, adds it to the current
|
||||
packet stream, and performs framing on it. That is, checks
|
||||
for complete messages, and once found, will process all that
|
||||
exist. This handles the case when we read N + 1 or 1 // N
|
||||
messages at a time instead of 1.
|
||||
|
||||
The processed and decoded messages are pushed to the callback
|
||||
function to process and send.
|
||||
|
||||
:param data: The new packet data
|
||||
:param callback: The function to send results to
|
||||
:param unit: Process if unit id matches, ignore otherwise (could be a
|
||||
list of unit ids (server) or single unit id(client/server)
|
||||
:param single: True or False (If True, ignore unit address validation)
|
||||
:return:
|
||||
"""
|
||||
if not isinstance(unit, (list, tuple)):
|
||||
unit = [unit]
|
||||
single = kwargs.get("single", False)
|
||||
_logger.debug("Processing: " + hexlify_packets(data))
|
||||
self.addToFrame(data)
|
||||
while True:
|
||||
if self.isFrameReady():
|
||||
if self.checkFrame():
|
||||
if self._validate_unit_id(unit, single):
|
||||
self._process(callback)
|
||||
else:
|
||||
_logger.debug("Not a valid unit id - {}, "
|
||||
"ignoring!!".format(self._header['uid']))
|
||||
self.resetFrame()
|
||||
else:
|
||||
_logger.debug("Frame check failed, ignoring!!")
|
||||
self.resetFrame()
|
||||
else:
|
||||
if len(self._buffer):
|
||||
# Possible error ???
|
||||
if self._header['len'] < 2:
|
||||
self._process(callback, error=True)
|
||||
break
|
||||
|
||||
def _process(self, callback, error=False):
|
||||
"""
|
||||
Process incoming packets irrespective error condition
|
||||
"""
|
||||
data = self.getRawFrame() if error else self.getFrame()
|
||||
result = self.decoder.decode(data)
|
||||
if result is None:
|
||||
raise ModbusIOException("Unable to decode request")
|
||||
elif error and result.function_code < 0x80:
|
||||
raise InvalidMessageReceivedException(result)
|
||||
else:
|
||||
self.populateResult(result)
|
||||
self.advanceFrame()
|
||||
callback(result) # defer or push to a thread?
|
||||
|
||||
def resetFrame(self):
|
||||
"""
|
||||
Reset the entire message frame.
|
||||
This allows us to skip ovver errors that may be in the stream.
|
||||
It is hard to know if we are simply out of sync or if there is
|
||||
an error in the stream as we have no way to check the start or
|
||||
end of the message (python just doesn't have the resolution to
|
||||
check for millisecond delays).
|
||||
"""
|
||||
self._buffer = b''
|
||||
self._header = {'tid': 0, 'pid': 0, 'len': 0, 'uid': 0}
|
||||
|
||||
def getRawFrame(self):
|
||||
"""
|
||||
Returns the complete buffer
|
||||
"""
|
||||
return self._buffer
|
||||
|
||||
def buildPacket(self, message):
|
||||
""" Creates a ready to send modbus packet
|
||||
|
||||
:param message: The populated request/response to send
|
||||
"""
|
||||
data = message.encode()
|
||||
packet = struct.pack(SOCKET_FRAME_HEADER,
|
||||
message.transaction_id,
|
||||
message.protocol_id,
|
||||
len(data) + 2,
|
||||
message.unit_id,
|
||||
message.function_code)
|
||||
packet += data
|
||||
return packet
|
||||
|
||||
|
||||
# __END__
|
||||
185
modbus/pymodbus/framer/tls_framer.py
Normal file
@@ -0,0 +1,185 @@
|
||||
import struct
|
||||
from pymodbus.exceptions import ModbusIOException
|
||||
from pymodbus.exceptions import InvalidMessageReceivedException
|
||||
from pymodbus.utilities import hexlify_packets
|
||||
from pymodbus.framer import ModbusFramer, TLS_FRAME_HEADER
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Logging
|
||||
# --------------------------------------------------------------------------- #
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Modbus TLS Message
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
class ModbusTlsFramer(ModbusFramer):
|
||||
""" Modbus TLS Frame controller
|
||||
|
||||
No prefix MBAP header before decrypted PDU is used as a message frame for
|
||||
Modbus Security Application Protocol. It allows us to easily separate
|
||||
decrypted messages which is PDU as follows:
|
||||
|
||||
[ Function Code] [ Data ]
|
||||
1b Nb
|
||||
"""
|
||||
|
||||
def __init__(self, decoder, client=None):
|
||||
""" Initializes a new instance of the framer
|
||||
|
||||
:param decoder: The decoder factory implementation to use
|
||||
"""
|
||||
self._buffer = b''
|
||||
self._header = {}
|
||||
self._hsize = 0x0
|
||||
self.decoder = decoder
|
||||
self.client = client
|
||||
|
||||
# ----------------------------------------------------------------------- #
|
||||
# Private Helper Functions
|
||||
# ----------------------------------------------------------------------- #
|
||||
def checkFrame(self):
|
||||
"""
|
||||
Check and decode the next frame Return true if we were successful
|
||||
"""
|
||||
if self.isFrameReady():
|
||||
# we have at least a complete message, continue
|
||||
if len(self._buffer) - self._hsize >= 1:
|
||||
return True
|
||||
# we don't have enough of a message yet, wait
|
||||
return False
|
||||
|
||||
def advanceFrame(self):
|
||||
""" Skip over the current framed message
|
||||
This allows us to skip over the current message after we have processed
|
||||
it or determined that it contains an error. It also has to reset the
|
||||
current frame header handle
|
||||
"""
|
||||
self._buffer = b''
|
||||
self._header = {}
|
||||
|
||||
def isFrameReady(self):
|
||||
""" Check if we should continue decode logic
|
||||
This is meant to be used in a while loop in the decoding phase to let
|
||||
the decoder factory know that there is still data in the buffer.
|
||||
|
||||
:returns: True if ready, False otherwise
|
||||
"""
|
||||
return len(self._buffer) > self._hsize
|
||||
|
||||
def addToFrame(self, message):
|
||||
""" Adds new packet data to the current frame buffer
|
||||
|
||||
:param message: The most recent packet
|
||||
"""
|
||||
self._buffer += message
|
||||
|
||||
def getFrame(self):
|
||||
""" Return the next frame from the buffered data
|
||||
|
||||
:returns: The next full frame buffer
|
||||
"""
|
||||
return self._buffer[self._hsize:]
|
||||
|
||||
def populateResult(self, result):
|
||||
"""
|
||||
Populates the modbus result with the transport specific header
|
||||
information (no header before PDU in decrypted message)
|
||||
|
||||
:param result: The response packet
|
||||
"""
|
||||
return
|
||||
|
||||
# ----------------------------------------------------------------------- #
|
||||
# Public Member Functions
|
||||
# ----------------------------------------------------------------------- #
|
||||
def decode_data(self, data):
|
||||
if len(data) > self._hsize:
|
||||
(fcode,) = struct.unpack(TLS_FRAME_HEADER, data[0:self._hsize+1])
|
||||
return dict(fcode=fcode)
|
||||
return dict()
|
||||
|
||||
def processIncomingPacket(self, data, callback, unit, **kwargs):
|
||||
"""
|
||||
The new packet processing pattern
|
||||
|
||||
This takes in a new request packet, adds it to the current
|
||||
packet stream, and performs framing on it. That is, checks
|
||||
for complete messages, and once found, will process all that
|
||||
exist. This handles the case when we read N + 1 or 1 // N
|
||||
messages at a time instead of 1.
|
||||
|
||||
The processed and decoded messages are pushed to the callback
|
||||
function to process and send.
|
||||
|
||||
:param data: The new packet data
|
||||
:param callback: The function to send results to
|
||||
:param unit: Process if unit id matches, ignore otherwise (could be a
|
||||
list of unit ids (server) or single unit id(client/server)
|
||||
:param single: True or False (If True, ignore unit address validation)
|
||||
:return:
|
||||
"""
|
||||
if not isinstance(unit, (list, tuple)):
|
||||
unit = [unit]
|
||||
# no unit id for Modbus Security Application Protocol
|
||||
single = kwargs.get("single", True)
|
||||
_logger.debug("Processing: " + hexlify_packets(data))
|
||||
self.addToFrame(data)
|
||||
|
||||
if self.isFrameReady():
|
||||
if self.checkFrame():
|
||||
if self._validate_unit_id(unit, single):
|
||||
self._process(callback)
|
||||
else:
|
||||
_logger.debug("Not in valid unit id - {}, "
|
||||
"ignoring!!".format(unit))
|
||||
self.resetFrame()
|
||||
else:
|
||||
_logger.debug("Frame check failed, ignoring!!")
|
||||
self.resetFrame()
|
||||
|
||||
def _process(self, callback, error=False):
|
||||
"""
|
||||
Process incoming packets irrespective error condition
|
||||
"""
|
||||
data = self.getRawFrame() if error else self.getFrame()
|
||||
result = self.decoder.decode(data)
|
||||
if result is None:
|
||||
raise ModbusIOException("Unable to decode request")
|
||||
elif error and result.function_code < 0x80:
|
||||
raise InvalidMessageReceivedException(result)
|
||||
else:
|
||||
self.populateResult(result)
|
||||
self.advanceFrame()
|
||||
callback(result) # defer or push to a thread?
|
||||
|
||||
def resetFrame(self):
|
||||
"""
|
||||
Reset the entire message frame.
|
||||
This allows us to skip ovver errors that may be in the stream.
|
||||
It is hard to know if we are simply out of sync or if there is
|
||||
an error in the stream as we have no way to check the start or
|
||||
end of the message (python just doesn't have the resolution to
|
||||
check for millisecond delays).
|
||||
"""
|
||||
self._buffer = b''
|
||||
|
||||
def getRawFrame(self):
|
||||
"""
|
||||
Returns the complete buffer
|
||||
"""
|
||||
return self._buffer
|
||||
|
||||
def buildPacket(self, message):
|
||||
""" Creates a ready to send modbus packet
|
||||
|
||||
:param message: The populated request/response to send
|
||||
"""
|
||||
data = message.encode()
|
||||
packet = struct.pack(TLS_FRAME_HEADER, message.function_code)
|
||||
packet += data
|
||||
return packet
|
||||
|
||||
# __END__
|
||||
248
modbus/pymodbus/interfaces.py
Normal file
@@ -0,0 +1,248 @@
|
||||
"""
|
||||
Pymodbus Interfaces
|
||||
---------------------
|
||||
|
||||
A collection of base classes that are used throughout
|
||||
the pymodbus library.
|
||||
"""
|
||||
from pymodbus.exceptions import (NotImplementedException,
|
||||
MessageRegisterException)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Generic
|
||||
# --------------------------------------------------------------------------- #
|
||||
class Singleton(object):
|
||||
"""
|
||||
Singleton base class
|
||||
http://mail.python.org/pipermail/python-list/2007-July/450681.html
|
||||
"""
|
||||
def __new__(cls, *args, **kwargs):
|
||||
""" Create a new instance
|
||||
"""
|
||||
if '_inst' not in vars(cls):
|
||||
cls._inst = object.__new__(cls)
|
||||
return cls._inst
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Project Specific
|
||||
# --------------------------------------------------------------------------- #
|
||||
class IModbusDecoder(object):
|
||||
""" Modbus Decoder Base Class
|
||||
|
||||
This interface must be implemented by a modbus message
|
||||
decoder factory. These factories are responsible for
|
||||
abstracting away converting a raw packet into a request / response
|
||||
message object.
|
||||
"""
|
||||
|
||||
def decode(self, message):
|
||||
""" Wrapper to decode a given packet
|
||||
|
||||
:param message: The raw modbus request packet
|
||||
:return: The decoded modbus message or None if error
|
||||
"""
|
||||
raise NotImplementedException(
|
||||
"Method not implemented by derived class")
|
||||
|
||||
def lookupPduClass(self, function_code):
|
||||
""" Use `function_code` to determine the class of the PDU.
|
||||
|
||||
:param function_code: The function code specified in a frame.
|
||||
:returns: The class of the PDU that has a matching `function_code`.
|
||||
"""
|
||||
raise NotImplementedException(
|
||||
"Method not implemented by derived class")
|
||||
|
||||
def register(self, function=None):
|
||||
"""
|
||||
Registers a function and sub function class with the decoder
|
||||
:param function: Custom function class to register
|
||||
:return:
|
||||
"""
|
||||
raise NotImplementedException(
|
||||
"Method not implemented by derived class")
|
||||
|
||||
|
||||
class IModbusFramer(object):
|
||||
"""
|
||||
A framer strategy interface. The idea is that we abstract away all the
|
||||
detail about how to detect if a current message frame exists, decoding
|
||||
it, sending it, etc so that we can plug in a new Framer object (tcp,
|
||||
rtu, ascii).
|
||||
"""
|
||||
|
||||
def checkFrame(self):
|
||||
""" Check and decode the next frame
|
||||
|
||||
:returns: True if we successful, False otherwise
|
||||
"""
|
||||
raise NotImplementedException(
|
||||
"Method not implemented by derived class")
|
||||
|
||||
def advanceFrame(self):
|
||||
""" Skip over the current framed message
|
||||
This allows us to skip over the current message after we have processed
|
||||
it or determined that it contains an error. It also has to reset the
|
||||
current frame header handle
|
||||
"""
|
||||
raise NotImplementedException(
|
||||
"Method not implemented by derived class")
|
||||
|
||||
def addToFrame(self, message):
|
||||
""" Add the next message to the frame buffer
|
||||
|
||||
This should be used before the decoding while loop to add the received
|
||||
data to the buffer handle.
|
||||
|
||||
:param message: The most recent packet
|
||||
"""
|
||||
raise NotImplementedException(
|
||||
"Method not implemented by derived class")
|
||||
|
||||
def isFrameReady(self):
|
||||
""" Check if we should continue decode logic
|
||||
|
||||
This is meant to be used in a while loop in the decoding phase to let
|
||||
the decoder know that there is still data in the buffer.
|
||||
|
||||
:returns: True if ready, False otherwise
|
||||
"""
|
||||
raise NotImplementedException(
|
||||
"Method not implemented by derived class")
|
||||
|
||||
def getFrame(self):
|
||||
""" Get the next frame from the buffer
|
||||
|
||||
:returns: The frame data or ''
|
||||
"""
|
||||
raise NotImplementedException(
|
||||
"Method not implemented by derived class")
|
||||
|
||||
def populateResult(self, result):
|
||||
""" Populates the modbus result with current frame header
|
||||
|
||||
We basically copy the data back over from the current header
|
||||
to the result header. This may not be needed for serial messages.
|
||||
|
||||
:param result: The response packet
|
||||
"""
|
||||
raise NotImplementedException(
|
||||
"Method not implemented by derived class")
|
||||
|
||||
def processIncomingPacket(self, data, callback):
|
||||
""" The new packet processing pattern
|
||||
|
||||
This takes in a new request packet, adds it to the current
|
||||
packet stream, and performs framing on it. That is, checks
|
||||
for complete messages, and once found, will process all that
|
||||
exist. This handles the case when we read N + 1 or 1 / N
|
||||
messages at a time instead of 1.
|
||||
|
||||
The processed and decoded messages are pushed to the callback
|
||||
function to process and send.
|
||||
|
||||
:param data: The new packet data
|
||||
:param callback: The function to send results to
|
||||
"""
|
||||
raise NotImplementedException(
|
||||
"Method not implemented by derived class")
|
||||
|
||||
def buildPacket(self, message):
|
||||
""" Creates a ready to send modbus packet
|
||||
|
||||
The raw packet is built off of a fully populated modbus
|
||||
request / response message.
|
||||
|
||||
:param message: The request/response to send
|
||||
:returns: The built packet
|
||||
"""
|
||||
raise NotImplementedException(
|
||||
"Method not implemented by derived class")
|
||||
|
||||
|
||||
class IModbusSlaveContext(object):
|
||||
"""
|
||||
Interface for a modbus slave data context
|
||||
|
||||
Derived classes must implemented the following methods:
|
||||
reset(self)
|
||||
validate(self, fx, address, count=1)
|
||||
getValues(self, fx, address, count=1)
|
||||
setValues(self, fx, address, values)
|
||||
"""
|
||||
__fx_mapper = {2: 'd', 4: 'i'}
|
||||
__fx_mapper.update([(i, 'h') for i in [3, 6, 16, 22, 23]])
|
||||
__fx_mapper.update([(i, 'c') for i in [1, 5, 15]])
|
||||
|
||||
def decode(self, fx):
|
||||
""" Converts the function code to the datastore to
|
||||
|
||||
:param fx: The function we are working with
|
||||
:returns: one of [d(iscretes),i(nputs),h(olding),c(oils)
|
||||
"""
|
||||
return self.__fx_mapper[fx]
|
||||
|
||||
def reset(self):
|
||||
""" Resets all the datastores to their default values
|
||||
"""
|
||||
raise NotImplementedException("Context Reset")
|
||||
|
||||
def validate(self, fx, address, count=1):
|
||||
""" Validates the request to make sure it is in range
|
||||
|
||||
:param fx: The function we are working with
|
||||
:param address: The starting address
|
||||
:param count: The number of values to test
|
||||
:returns: True if the request in within range, False otherwise
|
||||
"""
|
||||
raise NotImplementedException("validate context values")
|
||||
|
||||
def getValues(self, fx, address, count=1):
|
||||
""" Get `count` values from datastore
|
||||
|
||||
:param fx: The function we are working with
|
||||
:param address: The starting address
|
||||
:param count: The number of values to retrieve
|
||||
:returns: The requested values from a:a+c
|
||||
"""
|
||||
raise NotImplementedException("get context values")
|
||||
|
||||
def setValues(self, fx, address, values):
|
||||
""" Sets the datastore with the supplied values
|
||||
|
||||
:param fx: The function we are working with
|
||||
:param address: The starting address
|
||||
:param values: The new values to be set
|
||||
"""
|
||||
raise NotImplementedException("set context values")
|
||||
|
||||
|
||||
class IPayloadBuilder(object):
|
||||
"""
|
||||
This is an interface to a class that can build a payload
|
||||
for a modbus register write command. It should abstract
|
||||
the codec for encoding data to the required format
|
||||
(bcd, binary, char, etc).
|
||||
"""
|
||||
|
||||
def build(self):
|
||||
""" Return the payload buffer as a list
|
||||
|
||||
This list is two bytes per element and can
|
||||
thus be treated as a list of registers.
|
||||
|
||||
:returns: The payload buffer as a list
|
||||
"""
|
||||
raise NotImplementedException("set context values")
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Exported symbols
|
||||
# --------------------------------------------------------------------------- #
|
||||
__all__ = [
|
||||
'Singleton',
|
||||
'IModbusDecoder', 'IModbusFramer', 'IModbusSlaveContext',
|
||||
'IPayloadBuilder',
|
||||
]
|
||||
0
modbus/pymodbus/internal/__init__.py
Normal file
42
modbus/pymodbus/internal/ptwisted.py
Normal file
@@ -0,0 +1,42 @@
|
||||
'''
|
||||
A collection of twisted utility code
|
||||
'''
|
||||
from pymodbus.compat import IS_PYTHON2, IS_PYTHON3
|
||||
if IS_PYTHON2:
|
||||
from twisted.cred import portal, checkers
|
||||
from twisted.conch import manhole, manhole_ssh
|
||||
from twisted.conch.insults import insults
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Logging
|
||||
#---------------------------------------------------------------------------#
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Twisted Helper Methods
|
||||
#---------------------------------------------------------------------------#
|
||||
def InstallManagementConsole(namespace, users={'admin': 'admin'}, port=503):
|
||||
''' Helper method to start an ssh management console
|
||||
for the modbus server.
|
||||
|
||||
:param namespace: The data to constrain the server to
|
||||
:param users: The users to login with
|
||||
:param port: The port to host the server on
|
||||
'''
|
||||
if IS_PYTHON3:
|
||||
raise NotImplemented("This code currently doesn't work on python3")
|
||||
from twisted.internet import reactor
|
||||
|
||||
def build_protocol():
|
||||
p = insults.ServerProtocol(manhole.ColoredManhole, namespace)
|
||||
return p
|
||||
|
||||
r = manhole_ssh.TerminalRealm()
|
||||
r.chainedProtocolFactory = build_protocol
|
||||
c = checkers.InMemoryUsernamePasswordDatabaseDontUse(**users)
|
||||
p = portal.Portal(r, [c])
|
||||
factory = manhole_ssh.ConchFactory(p)
|
||||
reactor.listenTCP(port, factory)
|
||||
|
||||
213
modbus/pymodbus/mei_message.py
Normal file
@@ -0,0 +1,213 @@
|
||||
'''
|
||||
Encapsulated Interface (MEI) Transport Messages
|
||||
-----------------------------------------------
|
||||
|
||||
'''
|
||||
import struct
|
||||
from pymodbus.constants import DeviceInformation, MoreData
|
||||
from pymodbus.pdu import ModbusRequest
|
||||
from pymodbus.pdu import ModbusResponse
|
||||
from pymodbus.device import ModbusControlBlock
|
||||
from pymodbus.device import DeviceInformationFactory
|
||||
from pymodbus.pdu import ModbusExceptions as merror
|
||||
from pymodbus.compat import iteritems, byte2int, IS_PYTHON3
|
||||
|
||||
_MCB = ModbusControlBlock()
|
||||
|
||||
|
||||
class _OutOfSpaceException(Exception):
|
||||
# This exception exists here as a simple, local way to manage response
|
||||
# length control for the only MODBUS command which requires it under
|
||||
# standard, non-error conditions. It and the structures associated with
|
||||
# it should ideally be refactored and applied to all responses, however,
|
||||
# since a Client can make requests which result in disallowed conditions,
|
||||
# such as, for instance, requesting a register read of more registers
|
||||
# than will fit in a single PDU. As per the specification, the PDU is
|
||||
# restricted to 253 bytes, irrespective of the transport used.
|
||||
#
|
||||
# See Page 5/50 of MODBUS Application Protocol Specification V1.1b3.
|
||||
def __init__(self, oid):
|
||||
self.oid = oid
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Read Device Information
|
||||
#---------------------------------------------------------------------------#
|
||||
class ReadDeviceInformationRequest(ModbusRequest):
|
||||
'''
|
||||
This function code allows reading the identification and additional
|
||||
information relative to the physical and functional description of a
|
||||
remote device, only.
|
||||
|
||||
The Read Device Identification interface is modeled as an address space
|
||||
composed of a set of addressable data elements. The data elements are
|
||||
called objects and an object Id identifies them.
|
||||
'''
|
||||
function_code = 0x2b
|
||||
sub_function_code = 0x0e
|
||||
_rtu_frame_size = 7
|
||||
|
||||
def __init__(self, read_code=None, object_id=0x00, **kwargs):
|
||||
''' Initializes a new instance
|
||||
|
||||
:param read_code: The device information read code
|
||||
:param object_id: The object to read from
|
||||
'''
|
||||
ModbusRequest.__init__(self, **kwargs)
|
||||
self.read_code = read_code or DeviceInformation.Basic
|
||||
self.object_id = object_id
|
||||
|
||||
def encode(self):
|
||||
''' Encodes the request packet
|
||||
|
||||
:returns: The byte encoded packet
|
||||
'''
|
||||
packet = struct.pack('>BBB', self.sub_function_code,
|
||||
self.read_code, self.object_id)
|
||||
return packet
|
||||
|
||||
def decode(self, data):
|
||||
''' Decodes data part of the message.
|
||||
|
||||
:param data: The incoming data
|
||||
'''
|
||||
params = struct.unpack('>BBB', data)
|
||||
self.sub_function_code, self.read_code, self.object_id = params
|
||||
|
||||
def execute(self, context):
|
||||
''' Run a read exeception status request against the store
|
||||
|
||||
:param context: The datastore to request from
|
||||
:returns: The populated response
|
||||
'''
|
||||
if not (0x00 <= self.object_id <= 0xff):
|
||||
return self.doException(merror.IllegalValue)
|
||||
if not (0x00 <= self.read_code <= 0x04):
|
||||
return self.doException(merror.IllegalValue)
|
||||
|
||||
information = DeviceInformationFactory.get(_MCB,
|
||||
self.read_code, self.object_id)
|
||||
return ReadDeviceInformationResponse(self.read_code, information)
|
||||
|
||||
def __str__(self):
|
||||
''' Builds a representation of the request
|
||||
|
||||
:returns: The string representation of the request
|
||||
'''
|
||||
params = (self.read_code, self.object_id)
|
||||
return "ReadDeviceInformationRequest(%d,%d)" % params
|
||||
|
||||
|
||||
class ReadDeviceInformationResponse(ModbusResponse):
|
||||
'''
|
||||
'''
|
||||
function_code = 0x2b
|
||||
sub_function_code = 0x0e
|
||||
|
||||
@classmethod
|
||||
def calculateRtuFrameSize(cls, buffer):
|
||||
''' Calculates the size of the message
|
||||
|
||||
:param buffer: A buffer containing the data that have been received.
|
||||
:returns: The number of bytes in the response.
|
||||
'''
|
||||
size = 8 # skip the header information
|
||||
count = byte2int(buffer[7])
|
||||
|
||||
while count > 0:
|
||||
_, object_length = struct.unpack('>BB', buffer[size:size+2])
|
||||
size += object_length + 2
|
||||
count -= 1
|
||||
return size + 2
|
||||
|
||||
def __init__(self, read_code=None, information=None, **kwargs):
|
||||
''' Initializes a new instance
|
||||
|
||||
:param read_code: The device information read code
|
||||
:param information: The requested information request
|
||||
'''
|
||||
ModbusResponse.__init__(self, **kwargs)
|
||||
self.read_code = read_code or DeviceInformation.Basic
|
||||
self.information = information or {}
|
||||
self.number_of_objects = 0
|
||||
self.conformity = 0x83 # I support everything right now
|
||||
self.next_object_id = 0x00
|
||||
self.more_follows = MoreData.Nothing
|
||||
self.space_left = None
|
||||
|
||||
def _encode_object(self, object_id, data):
|
||||
self.space_left -= (2 + len(data))
|
||||
if self.space_left <= 0:
|
||||
raise _OutOfSpaceException(object_id)
|
||||
encoded_obj = struct.pack('>BB', object_id, len(data))
|
||||
if IS_PYTHON3:
|
||||
if isinstance(data, bytes):
|
||||
encoded_obj += data
|
||||
else:
|
||||
encoded_obj += data.encode()
|
||||
else:
|
||||
encoded_obj += data.encode()
|
||||
self.number_of_objects += 1
|
||||
return encoded_obj
|
||||
|
||||
def encode(self):
|
||||
''' Encodes the response
|
||||
|
||||
:returns: The byte encoded message
|
||||
'''
|
||||
packet = struct.pack('>BBB', self.sub_function_code,
|
||||
self.read_code, self.conformity)
|
||||
self.space_left = 253 - 6
|
||||
objects = b''
|
||||
try:
|
||||
for (object_id, data) in iteritems(self.information):
|
||||
if isinstance(data, list):
|
||||
for item in data:
|
||||
objects += self._encode_object(object_id, item)
|
||||
else:
|
||||
objects += self._encode_object(object_id, data)
|
||||
except _OutOfSpaceException as e:
|
||||
self.next_object_id = e.oid
|
||||
self.more_follows = MoreData.KeepReading
|
||||
|
||||
packet += struct.pack('>BBB', self.more_follows, self.next_object_id,
|
||||
self.number_of_objects)
|
||||
packet += objects
|
||||
return packet
|
||||
|
||||
def decode(self, data):
|
||||
''' Decodes a the response
|
||||
|
||||
:param data: The packet data to decode
|
||||
'''
|
||||
params = struct.unpack('>BBBBBB', data[0:6])
|
||||
self.sub_function_code, self.read_code = params[0:2]
|
||||
self.conformity, self.more_follows = params[2:4]
|
||||
self.next_object_id, self.number_of_objects = params[4:6]
|
||||
self.information, count = {}, 6 # skip the header information
|
||||
|
||||
while count < len(data):
|
||||
object_id, object_length = struct.unpack('>BB', data[count:count+2])
|
||||
count += object_length + 2
|
||||
if object_id not in self.information.keys():
|
||||
self.information[object_id] = data[count-object_length:count]
|
||||
else:
|
||||
if isinstance(self.information[object_id], list):
|
||||
self.information[object_id].append(data[count-object_length:count])
|
||||
else:
|
||||
self.information[object_id] = [self.information[object_id],
|
||||
data[count - object_length:count]]
|
||||
|
||||
def __str__(self):
|
||||
''' Builds a representation of the response
|
||||
|
||||
:returns: The string representation of the response
|
||||
'''
|
||||
return "ReadDeviceInformationResponse(%d)" % self.read_code
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Exported symbols
|
||||
#---------------------------------------------------------------------------#
|
||||
__all__ = [
|
||||
"ReadDeviceInformationRequest", "ReadDeviceInformationResponse",
|
||||
]
|
||||
453
modbus/pymodbus/other_message.py
Normal file
@@ -0,0 +1,453 @@
|
||||
'''
|
||||
Diagnostic record read/write
|
||||
|
||||
Currently not all implemented
|
||||
'''
|
||||
import struct
|
||||
from pymodbus.constants import ModbusStatus
|
||||
from pymodbus.pdu import ModbusRequest
|
||||
from pymodbus.pdu import ModbusResponse
|
||||
from pymodbus.device import ModbusControlBlock, DeviceInformationFactory
|
||||
from pymodbus.compat import byte2int, int2byte
|
||||
|
||||
_MCB = ModbusControlBlock()
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# TODO Make these only work on serial
|
||||
#---------------------------------------------------------------------------#
|
||||
class ReadExceptionStatusRequest(ModbusRequest):
|
||||
'''
|
||||
This function code is used to read the contents of eight Exception Status
|
||||
outputs in a remote device. The function provides a simple method for
|
||||
accessing this information, because the Exception Output references are
|
||||
known (no output reference is needed in the function).
|
||||
'''
|
||||
function_code = 0x07
|
||||
_rtu_frame_size = 4
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
''' Initializes a new instance
|
||||
'''
|
||||
ModbusRequest.__init__(self, **kwargs)
|
||||
|
||||
def encode(self):
|
||||
''' Encodes the message
|
||||
'''
|
||||
return b''
|
||||
|
||||
def decode(self, data):
|
||||
''' Decodes data part of the message.
|
||||
|
||||
:param data: The incoming data
|
||||
'''
|
||||
pass
|
||||
|
||||
def execute(self, context=None):
|
||||
''' Run a read exeception status request against the store
|
||||
|
||||
:returns: The populated response
|
||||
'''
|
||||
status = _MCB.Counter.summary()
|
||||
return ReadExceptionStatusResponse(status)
|
||||
|
||||
def __str__(self):
|
||||
''' Builds a representation of the request
|
||||
|
||||
:returns: The string representation of the request
|
||||
'''
|
||||
return "ReadExceptionStatusRequest(%d)" % (self.function_code)
|
||||
|
||||
|
||||
class ReadExceptionStatusResponse(ModbusResponse):
|
||||
'''
|
||||
The normal response contains the status of the eight Exception Status
|
||||
outputs. The outputs are packed into one data byte, with one bit
|
||||
per output. The status of the lowest output reference is contained
|
||||
in the least significant bit of the byte. The contents of the eight
|
||||
Exception Status outputs are device specific.
|
||||
'''
|
||||
function_code = 0x07
|
||||
_rtu_frame_size = 5
|
||||
|
||||
def __init__(self, status=0x00, **kwargs):
|
||||
''' Initializes a new instance
|
||||
|
||||
:param status: The status response to report
|
||||
'''
|
||||
ModbusResponse.__init__(self, **kwargs)
|
||||
self.status = status
|
||||
|
||||
def encode(self):
|
||||
''' Encodes the response
|
||||
|
||||
:returns: The byte encoded message
|
||||
'''
|
||||
return struct.pack('>B', self.status)
|
||||
|
||||
def decode(self, data):
|
||||
''' Decodes a the response
|
||||
|
||||
:param data: The packet data to decode
|
||||
'''
|
||||
self.status = byte2int(data[0])
|
||||
|
||||
def __str__(self):
|
||||
''' Builds a representation of the response
|
||||
|
||||
:returns: The string representation of the response
|
||||
'''
|
||||
arguments = (self.function_code, self.status)
|
||||
return "ReadExceptionStatusResponse(%d, %s)" % arguments
|
||||
|
||||
# Encapsulate interface transport 43, 14
|
||||
# CANopen general reference 43, 13
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# TODO Make these only work on serial
|
||||
#---------------------------------------------------------------------------#
|
||||
class GetCommEventCounterRequest(ModbusRequest):
|
||||
'''
|
||||
This function code is used to get a status word and an event count from
|
||||
the remote device's communication event counter.
|
||||
|
||||
By fetching the current count before and after a series of messages, a
|
||||
client can determine whether the messages were handled normally by the
|
||||
remote device.
|
||||
|
||||
The device's event counter is incremented once for each successful
|
||||
message completion. It is not incremented for exception responses,
|
||||
poll commands, or fetch event counter commands.
|
||||
|
||||
The event counter can be reset by means of the Diagnostics function
|
||||
(code 08), with a subfunction of Restart Communications Option
|
||||
(code 00 01) or Clear Counters and Diagnostic Register (code 00 0A).
|
||||
'''
|
||||
function_code = 0x0b
|
||||
_rtu_frame_size = 4
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
''' Initializes a new instance
|
||||
'''
|
||||
ModbusRequest.__init__(self, **kwargs)
|
||||
|
||||
def encode(self):
|
||||
''' Encodes the message
|
||||
'''
|
||||
return b''
|
||||
|
||||
def decode(self, data):
|
||||
''' Decodes data part of the message.
|
||||
|
||||
:param data: The incoming data
|
||||
'''
|
||||
pass
|
||||
|
||||
def execute(self, context=None):
|
||||
''' Run a read exeception status request against the store
|
||||
|
||||
:returns: The populated response
|
||||
'''
|
||||
status = _MCB.Counter.Event
|
||||
return GetCommEventCounterResponse(status)
|
||||
|
||||
def __str__(self):
|
||||
''' Builds a representation of the request
|
||||
|
||||
:returns: The string representation of the request
|
||||
'''
|
||||
return "GetCommEventCounterRequest(%d)" % (self.function_code)
|
||||
|
||||
|
||||
class GetCommEventCounterResponse(ModbusResponse):
|
||||
'''
|
||||
The normal response contains a two-byte status word, and a two-byte
|
||||
event count. The status word will be all ones (FF FF hex) if a
|
||||
previously-issued program command is still being processed by the
|
||||
remote device (a busy condition exists). Otherwise, the status word
|
||||
will be all zeros.
|
||||
'''
|
||||
function_code = 0x0b
|
||||
_rtu_frame_size = 8
|
||||
|
||||
def __init__(self, count=0x0000, **kwargs):
|
||||
''' Initializes a new instance
|
||||
|
||||
:param count: The current event counter value
|
||||
'''
|
||||
ModbusResponse.__init__(self, **kwargs)
|
||||
self.count = count
|
||||
self.status = True # this means we are ready, not waiting
|
||||
|
||||
def encode(self):
|
||||
''' Encodes the response
|
||||
|
||||
:returns: The byte encoded message
|
||||
'''
|
||||
if self.status: ready = ModbusStatus.Ready
|
||||
else: ready = ModbusStatus.Waiting
|
||||
return struct.pack('>HH', ready, self.count)
|
||||
|
||||
def decode(self, data):
|
||||
''' Decodes a the response
|
||||
|
||||
:param data: The packet data to decode
|
||||
'''
|
||||
ready, self.count = struct.unpack('>HH', data)
|
||||
self.status = (ready == ModbusStatus.Ready)
|
||||
|
||||
def __str__(self):
|
||||
''' Builds a representation of the response
|
||||
|
||||
:returns: The string representation of the response
|
||||
'''
|
||||
arguments = (self.function_code, self.count, self.status)
|
||||
return "GetCommEventCounterResponse(%d, %d, %d)" % arguments
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# TODO Make these only work on serial
|
||||
#---------------------------------------------------------------------------#
|
||||
class GetCommEventLogRequest(ModbusRequest):
|
||||
'''
|
||||
This function code is used to get a status word, event count, message
|
||||
count, and a field of event bytes from the remote device.
|
||||
|
||||
The status word and event counts are identical to that returned by
|
||||
the Get Communications Event Counter function (11, 0B hex).
|
||||
|
||||
The message counter contains the quantity of messages processed by the
|
||||
remote device since its last restart, clear counters operation, or
|
||||
power-up. This count is identical to that returned by the Diagnostic
|
||||
function (code 08), sub-function Return Bus Message Count (code 11,
|
||||
0B hex).
|
||||
|
||||
The event bytes field contains 0-64 bytes, with each byte corresponding
|
||||
to the status of one MODBUS send or receive operation for the remote
|
||||
device. The remote device enters the events into the field in
|
||||
chronological order. Byte 0 is the most recent event. Each new byte
|
||||
flushes the oldest byte from the field.
|
||||
'''
|
||||
function_code = 0x0c
|
||||
_rtu_frame_size = 4
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
''' Initializes a new instance
|
||||
'''
|
||||
ModbusRequest.__init__(self, **kwargs)
|
||||
|
||||
def encode(self):
|
||||
''' Encodes the message
|
||||
'''
|
||||
return b''
|
||||
|
||||
def decode(self, data):
|
||||
''' Decodes data part of the message.
|
||||
|
||||
:param data: The incoming data
|
||||
'''
|
||||
pass
|
||||
|
||||
def execute(self, context=None):
|
||||
''' Run a read exeception status request against the store
|
||||
|
||||
:returns: The populated response
|
||||
'''
|
||||
results = {
|
||||
'status' : True,
|
||||
'message_count' : _MCB.Counter.BusMessage,
|
||||
'event_count' : _MCB.Counter.Event,
|
||||
'events' : _MCB.getEvents(),
|
||||
}
|
||||
return GetCommEventLogResponse(**results)
|
||||
|
||||
def __str__(self):
|
||||
''' Builds a representation of the request
|
||||
|
||||
:returns: The string representation of the request
|
||||
'''
|
||||
return "GetCommEventLogRequest(%d)" % self.function_code
|
||||
|
||||
|
||||
class GetCommEventLogResponse(ModbusResponse):
|
||||
'''
|
||||
The normal response contains a two-byte status word field,
|
||||
a two-byte event count field, a two-byte message count field,
|
||||
and a field containing 0-64 bytes of events. A byte count
|
||||
field defines the total length of the data in these four field
|
||||
'''
|
||||
function_code = 0x0c
|
||||
_rtu_byte_count_pos = 2
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
''' Initializes a new instance
|
||||
|
||||
:param status: The status response to report
|
||||
:param message_count: The current message count
|
||||
:param event_count: The current event count
|
||||
:param events: The collection of events to send
|
||||
'''
|
||||
ModbusResponse.__init__(self, **kwargs)
|
||||
self.status = kwargs.get('status', True)
|
||||
self.message_count = kwargs.get('message_count', 0)
|
||||
self.event_count = kwargs.get('event_count', 0)
|
||||
self.events = kwargs.get('events', [])
|
||||
|
||||
def encode(self):
|
||||
''' Encodes the response
|
||||
|
||||
:returns: The byte encoded message
|
||||
'''
|
||||
if self.status: ready = ModbusStatus.Ready
|
||||
else: ready = ModbusStatus.Waiting
|
||||
packet = struct.pack('>B', 6 + len(self.events))
|
||||
packet += struct.pack('>H', ready)
|
||||
packet += struct.pack('>HH', self.event_count, self.message_count)
|
||||
packet += b''.join(struct.pack('>B', e) for e in self.events)
|
||||
return packet
|
||||
|
||||
def decode(self, data):
|
||||
''' Decodes a the response
|
||||
|
||||
:param data: The packet data to decode
|
||||
'''
|
||||
length = byte2int(data[0])
|
||||
status = struct.unpack('>H', data[1:3])[0]
|
||||
self.status = (status == ModbusStatus.Ready)
|
||||
self.event_count = struct.unpack('>H', data[3:5])[0]
|
||||
self.message_count = struct.unpack('>H', data[5:7])[0]
|
||||
|
||||
self.events = []
|
||||
for e in range(7, length + 1):
|
||||
self.events.append(byte2int(data[e]))
|
||||
|
||||
def __str__(self):
|
||||
''' Builds a representation of the response
|
||||
|
||||
:returns: The string representation of the response
|
||||
'''
|
||||
arguments = (self.function_code, self.status, self.message_count, self.event_count)
|
||||
return "GetCommEventLogResponse(%d, %d, %d, %d)" % arguments
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# TODO Make these only work on serial
|
||||
#---------------------------------------------------------------------------#
|
||||
class ReportSlaveIdRequest(ModbusRequest):
|
||||
'''
|
||||
This function code is used to read the description of the type, the
|
||||
current status, and other information specific to a remote device.
|
||||
'''
|
||||
function_code = 0x11
|
||||
_rtu_frame_size = 4
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
''' Initializes a new instance
|
||||
'''
|
||||
ModbusRequest.__init__(self, **kwargs)
|
||||
|
||||
def encode(self):
|
||||
''' Encodes the message
|
||||
'''
|
||||
return b''
|
||||
|
||||
def decode(self, data):
|
||||
''' Decodes data part of the message.
|
||||
|
||||
:param data: The incoming data
|
||||
'''
|
||||
pass
|
||||
|
||||
def execute(self, context=None):
|
||||
''' Run a report slave id request against the store
|
||||
|
||||
:returns: The populated response
|
||||
'''
|
||||
reportSlaveIdData = None
|
||||
if context:
|
||||
reportSlaveIdData = getattr(context, 'reportSlaveIdData', None)
|
||||
if not reportSlaveIdData:
|
||||
information = DeviceInformationFactory.get(_MCB)
|
||||
identifier = "-".join(information.values()).encode()
|
||||
identifier = identifier or b'Pymodbus'
|
||||
reportSlaveIdData = identifier
|
||||
return ReportSlaveIdResponse(reportSlaveIdData)
|
||||
|
||||
def __str__(self):
|
||||
''' Builds a representation of the request
|
||||
|
||||
:returns: The string representation of the request
|
||||
'''
|
||||
return "ReportSlaveIdRequest(%d)" % self.function_code
|
||||
|
||||
|
||||
class ReportSlaveIdResponse(ModbusResponse):
|
||||
'''
|
||||
The format of a normal response is shown in the following example.
|
||||
The data contents are specific to each type of device.
|
||||
'''
|
||||
function_code = 0x11
|
||||
_rtu_byte_count_pos = 2
|
||||
|
||||
def __init__(self, identifier=b'\x00', status=True, **kwargs):
|
||||
''' Initializes a new instance
|
||||
|
||||
:param identifier: The identifier of the slave
|
||||
:param status: The status response to report
|
||||
'''
|
||||
ModbusResponse.__init__(self, **kwargs)
|
||||
self.identifier = identifier
|
||||
self.status = status
|
||||
self.byte_count = None
|
||||
|
||||
def encode(self):
|
||||
''' Encodes the response
|
||||
|
||||
:returns: The byte encoded message
|
||||
'''
|
||||
if self.status:
|
||||
status = ModbusStatus.SlaveOn
|
||||
else:
|
||||
status = ModbusStatus.SlaveOff
|
||||
length = len(self.identifier) + 1
|
||||
packet = int2byte(length)
|
||||
packet += self.identifier # we assume it is already encoded
|
||||
packet += int2byte(status)
|
||||
return packet
|
||||
|
||||
def decode(self, data):
|
||||
''' Decodes a the response
|
||||
|
||||
Since the identifier is device dependent, we just return the
|
||||
raw value that a user can decode to whatever it should be.
|
||||
|
||||
:param data: The packet data to decode
|
||||
'''
|
||||
self.byte_count = byte2int(data[0])
|
||||
self.identifier = data[1:self.byte_count + 1]
|
||||
status = byte2int(data[-1])
|
||||
self.status = status == ModbusStatus.SlaveOn
|
||||
|
||||
def __str__(self):
|
||||
''' Builds a representation of the response
|
||||
|
||||
:returns: The string representation of the response
|
||||
'''
|
||||
arguments = (self.function_code, self.identifier, self.status)
|
||||
return "ReportSlaveIdResponse(%s, %s, %s)" % arguments
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# TODO Make these only work on serial
|
||||
#---------------------------------------------------------------------------#
|
||||
# report device identification 43, 14
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Exported symbols
|
||||
#---------------------------------------------------------------------------#
|
||||
__all__ = [
|
||||
"ReadExceptionStatusRequest", "ReadExceptionStatusResponse",
|
||||
"GetCommEventCounterRequest", "GetCommEventCounterResponse",
|
||||
"GetCommEventLogRequest", "GetCommEventLogResponse",
|
||||
"ReportSlaveIdRequest", "ReportSlaveIdResponse",
|
||||
]
|
||||
509
modbus/pymodbus/payload.py
Normal file
@@ -0,0 +1,509 @@
|
||||
"""
|
||||
Modbus Payload Builders
|
||||
------------------------
|
||||
|
||||
A collection of utilities for building and decoding
|
||||
modbus messages payloads.
|
||||
|
||||
|
||||
"""
|
||||
from struct import pack, unpack
|
||||
from pymodbus.interfaces import IPayloadBuilder
|
||||
from pymodbus.constants import Endian
|
||||
from pymodbus.utilities import pack_bitstring
|
||||
from pymodbus.utilities import unpack_bitstring
|
||||
from pymodbus.utilities import make_byte_string
|
||||
from pymodbus.exceptions import ParameterException
|
||||
from pymodbus.compat import unicode_string, IS_PYTHON3, PYTHON_VERSION
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Logging
|
||||
# --------------------------------------------------------------------------- #
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
WC = {
|
||||
"b": 1,
|
||||
"h": 2,
|
||||
"e": 2,
|
||||
"i": 4,
|
||||
"l": 4,
|
||||
"q": 8,
|
||||
"f": 4,
|
||||
"d": 8
|
||||
}
|
||||
|
||||
|
||||
class BinaryPayloadBuilder(IPayloadBuilder):
|
||||
"""
|
||||
A utility that helps build payload messages to be
|
||||
written with the various modbus messages. It really is just
|
||||
a simple wrapper around the struct module, however it saves
|
||||
time looking up the format strings. What follows is a simple
|
||||
example::
|
||||
|
||||
builder = BinaryPayloadBuilder(byteorder=Endian.Little)
|
||||
builder.add_8bit_uint(1)
|
||||
builder.add_16bit_uint(2)
|
||||
payload = builder.build()
|
||||
"""
|
||||
|
||||
def __init__(self, payload=None, byteorder=Endian.Little,
|
||||
wordorder=Endian.Big, repack=False):
|
||||
""" Initialize a new instance of the payload builder
|
||||
|
||||
:param payload: Raw binary payload data to initialize with
|
||||
:param byteorder: The endianess of the bytes in the words
|
||||
:param wordorder: The endianess of the word (when wordcount is >= 2)
|
||||
:param repack: Repack the provided payload based on BO
|
||||
"""
|
||||
self._payload = payload or []
|
||||
self._byteorder = byteorder
|
||||
self._wordorder = wordorder
|
||||
self._repack = repack
|
||||
|
||||
def _pack_words(self, fstring, value):
|
||||
"""
|
||||
Packs Words based on the word order and byte order
|
||||
|
||||
# ---------------------------------------------- #
|
||||
# pack in to network ordered value #
|
||||
# unpack in to network ordered unsigned integer #
|
||||
# Change Word order if little endian word order #
|
||||
# Pack values back based on correct byte order #
|
||||
# ---------------------------------------------- #
|
||||
|
||||
:param value: Value to be packed
|
||||
:return:
|
||||
"""
|
||||
value = pack("!{}".format(fstring), value)
|
||||
wc = WC.get(fstring.lower())//2
|
||||
up = "!{}H".format(wc)
|
||||
payload = unpack(up, value)
|
||||
|
||||
if self._wordorder == Endian.Little:
|
||||
payload = list(reversed(payload))
|
||||
|
||||
fstring = self._byteorder + "H"
|
||||
payload = [pack(fstring, word) for word in payload]
|
||||
payload = b''.join(payload)
|
||||
|
||||
return payload
|
||||
|
||||
def to_string(self):
|
||||
""" Return the payload buffer as a string
|
||||
|
||||
:returns: The payload buffer as a string
|
||||
"""
|
||||
return b''.join(self._payload)
|
||||
|
||||
def __str__(self):
|
||||
""" Return the payload buffer as a string
|
||||
|
||||
:returns: The payload buffer as a string
|
||||
"""
|
||||
return self.to_string().decode('utf-8')
|
||||
|
||||
def reset(self):
|
||||
""" Reset the payload buffer
|
||||
"""
|
||||
self._payload = []
|
||||
|
||||
def to_registers(self):
|
||||
""" Convert the payload buffer into a register
|
||||
layout that can be used as a context block.
|
||||
|
||||
:returns: The register layout to use as a block
|
||||
"""
|
||||
# fstring = self._byteorder+'H'
|
||||
fstring = '!H'
|
||||
payload = self.build()
|
||||
if self._repack:
|
||||
payload = [unpack(self._byteorder+"H", value)[0] for value in payload]
|
||||
else:
|
||||
payload = [unpack(fstring, value)[0] for value in payload]
|
||||
_logger.debug(payload)
|
||||
return payload
|
||||
|
||||
def to_coils(self):
|
||||
"""Convert the payload buffer into a coil
|
||||
layout that can be used as a context block.
|
||||
|
||||
:returns: The coil layout to use as a block
|
||||
"""
|
||||
payload = self.to_registers()
|
||||
coils = [bool(int(bit)) for reg
|
||||
in payload for bit in format(reg, '016b')]
|
||||
return coils
|
||||
|
||||
def build(self):
|
||||
""" Return the payload buffer as a list
|
||||
|
||||
This list is two bytes per element and can
|
||||
thus be treated as a list of registers.
|
||||
|
||||
:returns: The payload buffer as a list
|
||||
"""
|
||||
string = self.to_string()
|
||||
length = len(string)
|
||||
string = string + (b'\x00' * (length % 2))
|
||||
return [string[i:i+2] for i in range(0, length, 2)]
|
||||
|
||||
def add_bits(self, values):
|
||||
""" Adds a collection of bits to be encoded
|
||||
|
||||
If these are less than a multiple of eight,
|
||||
they will be left padded with 0 bits to make
|
||||
it so.
|
||||
|
||||
:param value: The value to add to the buffer
|
||||
"""
|
||||
value = pack_bitstring(values)
|
||||
self._payload.append(value)
|
||||
|
||||
def add_8bit_uint(self, value):
|
||||
""" Adds a 8 bit unsigned int to the buffer
|
||||
|
||||
:param value: The value to add to the buffer
|
||||
"""
|
||||
fstring = self._byteorder + 'B'
|
||||
self._payload.append(pack(fstring, value))
|
||||
|
||||
def add_16bit_uint(self, value):
|
||||
""" Adds a 16 bit unsigned int to the buffer
|
||||
|
||||
:param value: The value to add to the buffer
|
||||
"""
|
||||
fstring = self._byteorder + 'H'
|
||||
self._payload.append(pack(fstring, value))
|
||||
|
||||
def add_32bit_uint(self, value):
|
||||
""" Adds a 32 bit unsigned int to the buffer
|
||||
|
||||
:param value: The value to add to the buffer
|
||||
"""
|
||||
fstring = 'I'
|
||||
# fstring = self._byteorder + 'I'
|
||||
p_string = self._pack_words(fstring, value)
|
||||
self._payload.append(p_string)
|
||||
|
||||
def add_64bit_uint(self, value):
|
||||
""" Adds a 64 bit unsigned int to the buffer
|
||||
|
||||
:param value: The value to add to the buffer
|
||||
"""
|
||||
fstring = 'Q'
|
||||
p_string = self._pack_words(fstring, value)
|
||||
self._payload.append(p_string)
|
||||
|
||||
def add_8bit_int(self, value):
|
||||
""" Adds a 8 bit signed int to the buffer
|
||||
|
||||
:param value: The value to add to the buffer
|
||||
"""
|
||||
fstring = self._byteorder + 'b'
|
||||
self._payload.append(pack(fstring, value))
|
||||
|
||||
def add_16bit_int(self, value):
|
||||
""" Adds a 16 bit signed int to the buffer
|
||||
|
||||
:param value: The value to add to the buffer
|
||||
"""
|
||||
fstring = self._byteorder + 'h'
|
||||
self._payload.append(pack(fstring, value))
|
||||
|
||||
def add_32bit_int(self, value):
|
||||
""" Adds a 32 bit signed int to the buffer
|
||||
|
||||
:param value: The value to add to the buffer
|
||||
"""
|
||||
fstring = 'i'
|
||||
p_string = self._pack_words(fstring, value)
|
||||
self._payload.append(p_string)
|
||||
|
||||
def add_64bit_int(self, value):
|
||||
""" Adds a 64 bit signed int to the buffer
|
||||
|
||||
:param value: The value to add to the buffer
|
||||
"""
|
||||
fstring = 'q'
|
||||
p_string = self._pack_words(fstring, value)
|
||||
self._payload.append(p_string)
|
||||
|
||||
def add_16bit_float(self, value):
|
||||
""" Adds a 16 bit float to the buffer
|
||||
|
||||
:param value: The value to add to the buffer
|
||||
"""
|
||||
if IS_PYTHON3 and PYTHON_VERSION.minor >= 6:
|
||||
fstring = 'e'
|
||||
p_string = self._pack_words(fstring, value)
|
||||
self._payload.append(p_string)
|
||||
else:
|
||||
_logger.warning("float16 only supported on python3.6 and above!!!")
|
||||
|
||||
def add_32bit_float(self, value):
|
||||
""" Adds a 32 bit float to the buffer
|
||||
|
||||
:param value: The value to add to the buffer
|
||||
"""
|
||||
fstring = 'f'
|
||||
p_string = self._pack_words(fstring, value)
|
||||
self._payload.append(p_string)
|
||||
|
||||
def add_64bit_float(self, value):
|
||||
""" Adds a 64 bit float(double) to the buffer
|
||||
|
||||
:param value: The value to add to the buffer
|
||||
"""
|
||||
fstring = 'd'
|
||||
p_string = self._pack_words(fstring, value)
|
||||
self._payload.append(p_string)
|
||||
|
||||
def add_string(self, value):
|
||||
""" Adds a string to the buffer
|
||||
|
||||
:param value: The value to add to the buffer
|
||||
"""
|
||||
value = make_byte_string(value)
|
||||
fstring = self._byteorder + str(len(value)) + 's'
|
||||
self._payload.append(pack(fstring, value))
|
||||
|
||||
|
||||
class BinaryPayloadDecoder(object):
|
||||
"""
|
||||
A utility that helps decode payload messages from a modbus
|
||||
response message. It really is just a simple wrapper around
|
||||
the struct module, however it saves time looking up the format
|
||||
strings. What follows is a simple example::
|
||||
|
||||
decoder = BinaryPayloadDecoder(payload)
|
||||
first = decoder.decode_8bit_uint()
|
||||
second = decoder.decode_16bit_uint()
|
||||
"""
|
||||
|
||||
def __init__(self, payload, byteorder=Endian.Little, wordorder=Endian.Big):
|
||||
""" Initialize a new payload decoder
|
||||
|
||||
:param payload: The payload to decode with
|
||||
:param byteorder: The endianess of the payload
|
||||
:param wordorder: The endianess of the word (when wordcount is >= 2)
|
||||
"""
|
||||
self._payload = payload
|
||||
self._pointer = 0x00
|
||||
self._byteorder = byteorder
|
||||
self._wordorder = wordorder
|
||||
|
||||
@classmethod
|
||||
def fromRegisters(klass, registers, byteorder=Endian.Little,
|
||||
wordorder=Endian.Big):
|
||||
""" Initialize a payload decoder with the result of
|
||||
reading a collection of registers from a modbus device.
|
||||
|
||||
The registers are treated as a list of 2 byte values.
|
||||
We have to do this because of how the data has already
|
||||
been decoded by the rest of the library.
|
||||
|
||||
:param registers: The register results to initialize with
|
||||
:param byteorder: The Byte order of each word
|
||||
:param wordorder: The endianess of the word (when wordcount is >= 2)
|
||||
:returns: An initialized PayloadDecoder
|
||||
"""
|
||||
_logger.debug(registers)
|
||||
if isinstance(registers, list): # repack into flat binary
|
||||
payload = b''.join(pack('!H', x) for x in registers)
|
||||
return klass(payload, byteorder, wordorder)
|
||||
raise ParameterException('Invalid collection of registers supplied')
|
||||
|
||||
@classmethod
|
||||
def bit_chunks(cls, coils, size=8):
|
||||
chunks = [coils[i: i + size] for i in range(0, len(coils), size)]
|
||||
return chunks
|
||||
|
||||
@classmethod
|
||||
def fromCoils(klass, coils, byteorder=Endian.Little, wordorder=Endian.Big):
|
||||
""" Initialize a payload decoder with the result of
|
||||
reading a collection of coils from a modbus device.
|
||||
|
||||
The coils are treated as a list of bit(boolean) values.
|
||||
|
||||
:param coils: The coil results to initialize with
|
||||
:param byteorder: The endianess of the payload
|
||||
:returns: An initialized PayloadDecoder
|
||||
"""
|
||||
if isinstance(coils, list):
|
||||
payload = b''
|
||||
padding = len(coils) % 8
|
||||
if padding: # Pad zero's
|
||||
extra = [False] * padding
|
||||
coils = extra + coils
|
||||
chunks = klass.bit_chunks(coils)
|
||||
for chunk in chunks:
|
||||
payload += pack_bitstring(chunk[::-1])
|
||||
return klass(payload, byteorder)
|
||||
raise ParameterException('Invalid collection of coils supplied')
|
||||
|
||||
def _unpack_words(self, fstring, handle):
|
||||
"""
|
||||
Un Packs Words based on the word order and byte order
|
||||
|
||||
# ---------------------------------------------- #
|
||||
# Unpack in to network ordered unsigned integer #
|
||||
# Change Word order if little endian word order #
|
||||
# Pack values back based on correct byte order #
|
||||
# ---------------------------------------------- #
|
||||
:param handle: Value to be unpacked
|
||||
:return:
|
||||
"""
|
||||
handle = make_byte_string(handle)
|
||||
wc = WC.get(fstring.lower()) // 2
|
||||
up = "!{}H".format(wc)
|
||||
handle = unpack(up, handle)
|
||||
if self._wordorder == Endian.Little:
|
||||
handle = list(reversed(handle))
|
||||
|
||||
# Repack as unsigned Integer
|
||||
pk = self._byteorder + 'H'
|
||||
handle = [pack(pk, p) for p in handle]
|
||||
_logger.debug(handle)
|
||||
handle = b''.join(handle)
|
||||
return handle
|
||||
|
||||
def reset(self):
|
||||
""" Reset the decoder pointer back to the start
|
||||
"""
|
||||
self._pointer = 0x00
|
||||
|
||||
def decode_8bit_uint(self):
|
||||
""" Decodes a 8 bit unsigned int from the buffer
|
||||
"""
|
||||
self._pointer += 1
|
||||
fstring = self._byteorder + 'B'
|
||||
handle = self._payload[self._pointer - 1:self._pointer]
|
||||
handle = make_byte_string(handle)
|
||||
return unpack(fstring, handle)[0]
|
||||
|
||||
def decode_bits(self):
|
||||
""" Decodes a byte worth of bits from the buffer
|
||||
"""
|
||||
self._pointer += 1
|
||||
# fstring = self._endian + 'B'
|
||||
handle = self._payload[self._pointer - 1:self._pointer]
|
||||
handle = make_byte_string(handle)
|
||||
return unpack_bitstring(handle)
|
||||
|
||||
def decode_16bit_uint(self):
|
||||
""" Decodes a 16 bit unsigned int from the buffer
|
||||
"""
|
||||
self._pointer += 2
|
||||
fstring = self._byteorder + 'H'
|
||||
handle = self._payload[self._pointer - 2:self._pointer]
|
||||
handle = make_byte_string(handle)
|
||||
return unpack(fstring, handle)[0]
|
||||
|
||||
def decode_32bit_uint(self):
|
||||
""" Decodes a 32 bit unsigned int from the buffer
|
||||
"""
|
||||
self._pointer += 4
|
||||
fstring = 'I'
|
||||
# fstring = 'I'
|
||||
handle = self._payload[self._pointer - 4:self._pointer]
|
||||
handle = self._unpack_words(fstring, handle)
|
||||
return unpack("!"+fstring, handle)[0]
|
||||
|
||||
def decode_64bit_uint(self):
|
||||
""" Decodes a 64 bit unsigned int from the buffer
|
||||
"""
|
||||
self._pointer += 8
|
||||
fstring = 'Q'
|
||||
handle = self._payload[self._pointer - 8:self._pointer]
|
||||
handle = self._unpack_words(fstring, handle)
|
||||
return unpack("!"+fstring, handle)[0]
|
||||
|
||||
def decode_8bit_int(self):
|
||||
""" Decodes a 8 bit signed int from the buffer
|
||||
"""
|
||||
self._pointer += 1
|
||||
fstring = self._byteorder + 'b'
|
||||
handle = self._payload[self._pointer - 1:self._pointer]
|
||||
handle = make_byte_string(handle)
|
||||
return unpack(fstring, handle)[0]
|
||||
|
||||
def decode_16bit_int(self):
|
||||
""" Decodes a 16 bit signed int from the buffer
|
||||
"""
|
||||
self._pointer += 2
|
||||
fstring = self._byteorder + 'h'
|
||||
handle = self._payload[self._pointer - 2:self._pointer]
|
||||
handle = make_byte_string(handle)
|
||||
return unpack(fstring, handle)[0]
|
||||
|
||||
def decode_32bit_int(self):
|
||||
""" Decodes a 32 bit signed int from the buffer
|
||||
"""
|
||||
self._pointer += 4
|
||||
fstring = 'i'
|
||||
handle = self._payload[self._pointer - 4:self._pointer]
|
||||
handle = self._unpack_words(fstring, handle)
|
||||
return unpack("!"+fstring, handle)[0]
|
||||
|
||||
def decode_64bit_int(self):
|
||||
""" Decodes a 64 bit signed int from the buffer
|
||||
"""
|
||||
self._pointer += 8
|
||||
fstring = 'q'
|
||||
handle = self._payload[self._pointer - 8:self._pointer]
|
||||
handle = self._unpack_words(fstring, handle)
|
||||
return unpack("!"+fstring, handle)[0]
|
||||
|
||||
def decode_16bit_float(self):
|
||||
""" Decodes a 16 bit float from the buffer
|
||||
"""
|
||||
if IS_PYTHON3 and PYTHON_VERSION.minor >= 6:
|
||||
self._pointer += 2
|
||||
fstring = 'e'
|
||||
handle = self._payload[self._pointer - 2:self._pointer]
|
||||
handle = self._unpack_words(fstring, handle)
|
||||
return unpack("!"+fstring, handle)[0]
|
||||
else:
|
||||
_logger.warning("float16 only supported on python3.6 and above!!!")
|
||||
|
||||
def decode_32bit_float(self):
|
||||
""" Decodes a 32 bit float from the buffer
|
||||
"""
|
||||
self._pointer += 4
|
||||
fstring = 'f'
|
||||
handle = self._payload[self._pointer - 4:self._pointer]
|
||||
handle = self._unpack_words(fstring, handle)
|
||||
return unpack("!"+fstring, handle)[0]
|
||||
|
||||
def decode_64bit_float(self):
|
||||
""" Decodes a 64 bit float(double) from the buffer
|
||||
"""
|
||||
self._pointer += 8
|
||||
fstring = 'd'
|
||||
handle = self._payload[self._pointer - 8:self._pointer]
|
||||
handle = self._unpack_words(fstring, handle)
|
||||
return unpack("!"+fstring, handle)[0]
|
||||
|
||||
def decode_string(self, size=1):
|
||||
""" Decodes a string from the buffer
|
||||
|
||||
:param size: The size of the string to decode
|
||||
"""
|
||||
self._pointer += size
|
||||
s = self._payload[self._pointer - size:self._pointer]
|
||||
return s
|
||||
|
||||
def skip_bytes(self, nbytes):
|
||||
""" Skip n bytes in the buffer
|
||||
|
||||
:param nbytes: The number of bytes to skip
|
||||
"""
|
||||
self._pointer += nbytes
|
||||
return None
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Exported Identifiers
|
||||
#---------------------------------------------------------------------------#
|
||||
__all__ = ["BinaryPayloadBuilder", "BinaryPayloadDecoder"]
|
||||
247
modbus/pymodbus/pdu.py
Normal file
@@ -0,0 +1,247 @@
|
||||
"""
|
||||
Contains base classes for modbus request/response/error packets
|
||||
"""
|
||||
from pymodbus.interfaces import Singleton
|
||||
from pymodbus.exceptions import NotImplementedException
|
||||
from pymodbus.constants import Defaults
|
||||
from pymodbus.utilities import rtuFrameSize
|
||||
from pymodbus.compat import iteritems, int2byte, byte2int
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Logging
|
||||
# --------------------------------------------------------------------------- #
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Base PDU's
|
||||
# --------------------------------------------------------------------------- #
|
||||
class ModbusPDU(object):
|
||||
"""
|
||||
Base class for all Modbus messages
|
||||
|
||||
.. attribute:: transaction_id
|
||||
|
||||
This value is used to uniquely identify a request
|
||||
response pair. It can be implemented as a simple counter
|
||||
|
||||
.. attribute:: protocol_id
|
||||
|
||||
This is a constant set at 0 to indicate Modbus. It is
|
||||
put here for ease of expansion.
|
||||
|
||||
.. attribute:: unit_id
|
||||
|
||||
This is used to route the request to the correct child. In
|
||||
the TCP modbus, it is used for routing (or not used at all. However,
|
||||
for the serial versions, it is used to specify which child to perform
|
||||
the requests against. The value 0x00 represents the broadcast address
|
||||
(also 0xff).
|
||||
|
||||
.. attribute:: check
|
||||
|
||||
This is used for LRC/CRC in the serial modbus protocols
|
||||
|
||||
.. attribute:: skip_encode
|
||||
|
||||
This is used when the message payload has already been encoded.
|
||||
Generally this will occur when the PayloadBuilder is being used
|
||||
to create a complicated message. By setting this to True, the
|
||||
request will pass the currently encoded message through instead
|
||||
of encoding it again.
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
""" Initializes the base data for a modbus request """
|
||||
self.transaction_id = kwargs.get('transaction', Defaults.TransactionId)
|
||||
self.protocol_id = kwargs.get('protocol', Defaults.ProtocolId)
|
||||
self.unit_id = kwargs.get('unit', Defaults.UnitId)
|
||||
self.skip_encode = kwargs.get('skip_encode', False)
|
||||
self.check = 0x0000
|
||||
|
||||
def encode(self):
|
||||
""" Encodes the message
|
||||
|
||||
:raises: A not implemented exception
|
||||
"""
|
||||
raise NotImplementedException()
|
||||
|
||||
def decode(self, data):
|
||||
""" Decodes data part of the message.
|
||||
|
||||
:param data: is a string object
|
||||
:raises: A not implemented exception
|
||||
"""
|
||||
raise NotImplementedException()
|
||||
|
||||
@classmethod
|
||||
def calculateRtuFrameSize(cls, buffer):
|
||||
""" Calculates the size of a PDU.
|
||||
|
||||
:param buffer: A buffer containing the data that have been received.
|
||||
:returns: The number of bytes in the PDU.
|
||||
"""
|
||||
if hasattr(cls, '_rtu_frame_size'):
|
||||
return cls._rtu_frame_size
|
||||
elif hasattr(cls, '_rtu_byte_count_pos'):
|
||||
return rtuFrameSize(buffer, cls._rtu_byte_count_pos)
|
||||
else: raise NotImplementedException(
|
||||
"Cannot determine RTU frame size for %s" % cls.__name__)
|
||||
|
||||
|
||||
class ModbusRequest(ModbusPDU):
|
||||
""" Base class for a modbus request PDU """
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
""" Proxy to the lower level initializer """
|
||||
ModbusPDU.__init__(self, **kwargs)
|
||||
|
||||
def doException(self, exception):
|
||||
""" Builds an error response based on the function
|
||||
|
||||
:param exception: The exception to return
|
||||
:raises: An exception response
|
||||
"""
|
||||
exc = ExceptionResponse(self.function_code, exception)
|
||||
_logger.error(exc)
|
||||
return exc
|
||||
|
||||
|
||||
class ModbusResponse(ModbusPDU):
|
||||
""" Base class for a modbus response PDU
|
||||
|
||||
.. attribute:: should_respond
|
||||
|
||||
A flag that indicates if this response returns a result back
|
||||
to the client issuing the request
|
||||
|
||||
.. attribute:: _rtu_frame_size
|
||||
|
||||
Indicates the size of the modbus rtu response used for
|
||||
calculating how much to read.
|
||||
"""
|
||||
|
||||
should_respond = True
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
""" Proxy to the lower level initializer """
|
||||
ModbusPDU.__init__(self, **kwargs)
|
||||
|
||||
def isError(self):
|
||||
"""Checks if the error is a success or failure"""
|
||||
return self.function_code > 0x80
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Exception PDU's
|
||||
# --------------------------------------------------------------------------- #
|
||||
class ModbusExceptions(Singleton):
|
||||
"""
|
||||
An enumeration of the valid modbus exceptions
|
||||
"""
|
||||
IllegalFunction = 0x01
|
||||
IllegalAddress = 0x02
|
||||
IllegalValue = 0x03
|
||||
SlaveFailure = 0x04
|
||||
Acknowledge = 0x05
|
||||
SlaveBusy = 0x06
|
||||
MemoryParityError = 0x08
|
||||
GatewayPathUnavailable = 0x0A
|
||||
GatewayNoResponse = 0x0B
|
||||
|
||||
@classmethod
|
||||
def decode(cls, code):
|
||||
""" Given an error code, translate it to a
|
||||
string error name.
|
||||
|
||||
:param code: The code number to translate
|
||||
"""
|
||||
values = dict((v, k) for k, v in iteritems(cls.__dict__)
|
||||
if not k.startswith('__') and not callable(v))
|
||||
return values.get(code, None)
|
||||
|
||||
|
||||
class ExceptionResponse(ModbusResponse):
|
||||
""" Base class for a modbus exception PDU """
|
||||
ExceptionOffset = 0x80
|
||||
_rtu_frame_size = 5
|
||||
|
||||
def __init__(self, function_code, exception_code=None, **kwargs):
|
||||
""" Initializes the modbus exception response
|
||||
|
||||
:param function_code: The function to build an exception response for
|
||||
:param exception_code: The specific modbus exception to return
|
||||
"""
|
||||
ModbusResponse.__init__(self, **kwargs)
|
||||
self.original_code = function_code
|
||||
self.function_code = function_code | self.ExceptionOffset
|
||||
self.exception_code = exception_code
|
||||
|
||||
def encode(self):
|
||||
""" Encodes a modbus exception response
|
||||
|
||||
:returns: The encoded exception packet
|
||||
"""
|
||||
return int2byte(self.exception_code)
|
||||
|
||||
def decode(self, data):
|
||||
""" Decodes a modbus exception response
|
||||
|
||||
:param data: The packet data to decode
|
||||
"""
|
||||
self.exception_code = byte2int(data[0])
|
||||
|
||||
def __str__(self):
|
||||
""" Builds a representation of an exception response
|
||||
|
||||
:returns: The string representation of an exception response
|
||||
"""
|
||||
message = ModbusExceptions.decode(self.exception_code)
|
||||
parameters = (self.function_code, self.original_code, message)
|
||||
return "Exception Response(%d, %d, %s)" % parameters
|
||||
|
||||
|
||||
class IllegalFunctionRequest(ModbusRequest):
|
||||
"""
|
||||
Defines the Modbus slave exception type 'Illegal Function'
|
||||
This exception code is returned if the slave::
|
||||
|
||||
- does not implement the function code **or**
|
||||
- is not in a state that allows it to process the function
|
||||
"""
|
||||
ErrorCode = 1
|
||||
|
||||
def __init__(self, function_code, **kwargs):
|
||||
""" Initializes a IllegalFunctionRequest
|
||||
|
||||
:param function_code: The function we are erroring on
|
||||
"""
|
||||
ModbusRequest.__init__(self, **kwargs)
|
||||
self.function_code = function_code
|
||||
|
||||
def decode(self, data):
|
||||
""" This is here so this failure will run correctly
|
||||
|
||||
:param data: Not used
|
||||
"""
|
||||
pass
|
||||
|
||||
def execute(self, context):
|
||||
""" Builds an illegal function request error response
|
||||
|
||||
:param context: The current context for the message
|
||||
:returns: The error response packet
|
||||
"""
|
||||
return ExceptionResponse(self.function_code, self.ErrorCode)
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Exported symbols
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
__all__ = [
|
||||
'ModbusRequest', 'ModbusResponse', 'ModbusExceptions',
|
||||
'ExceptionResponse', 'IllegalFunctionRequest',
|
||||
]
|
||||
|
||||
359
modbus/pymodbus/register_read_message.py
Normal file
@@ -0,0 +1,359 @@
|
||||
'''
|
||||
Register Reading Request/Response
|
||||
---------------------------------
|
||||
'''
|
||||
import struct
|
||||
from pymodbus.pdu import ModbusRequest
|
||||
from pymodbus.pdu import ModbusResponse
|
||||
from pymodbus.pdu import ModbusExceptions as merror
|
||||
from pymodbus.compat import int2byte, byte2int
|
||||
|
||||
|
||||
class ReadRegistersRequestBase(ModbusRequest):
|
||||
'''
|
||||
Base class for reading a modbus register
|
||||
'''
|
||||
_rtu_frame_size = 8
|
||||
|
||||
def __init__(self, address, count, **kwargs):
|
||||
''' Initializes a new instance
|
||||
|
||||
:param address: The address to start the read from
|
||||
:param count: The number of registers to read
|
||||
'''
|
||||
ModbusRequest.__init__(self, **kwargs)
|
||||
self.address = address
|
||||
self.count = count
|
||||
|
||||
def encode(self):
|
||||
''' Encodes the request packet
|
||||
|
||||
:return: The encoded packet
|
||||
'''
|
||||
return struct.pack('>HH', self.address, self.count)
|
||||
|
||||
def decode(self, data):
|
||||
''' Decode a register request packet
|
||||
|
||||
:param data: The request to decode
|
||||
'''
|
||||
self.address, self.count = struct.unpack('>HH', data)
|
||||
|
||||
def get_response_pdu_size(self):
|
||||
"""
|
||||
Func_code (1 byte) + Byte Count(1 byte) + 2 * Quantity of Coils (n Bytes)
|
||||
:return:
|
||||
"""
|
||||
return 1 + 1 + 2 * self.count
|
||||
|
||||
def __str__(self):
|
||||
''' Returns a string representation of the instance
|
||||
|
||||
:returns: A string representation of the instance
|
||||
'''
|
||||
return "ReadRegisterRequest (%d,%d)" % (self.address, self.count)
|
||||
|
||||
|
||||
class ReadRegistersResponseBase(ModbusResponse):
|
||||
'''
|
||||
Base class for responsing to a modbus register read
|
||||
'''
|
||||
|
||||
_rtu_byte_count_pos = 2
|
||||
|
||||
def __init__(self, values, **kwargs):
|
||||
''' Initializes a new instance
|
||||
|
||||
:param values: The values to write to
|
||||
'''
|
||||
ModbusResponse.__init__(self, **kwargs)
|
||||
self.registers = values or []
|
||||
|
||||
def encode(self):
|
||||
''' Encodes the response packet
|
||||
|
||||
:returns: The encoded packet
|
||||
'''
|
||||
result = int2byte(len(self.registers) * 2)
|
||||
for register in self.registers:
|
||||
result += struct.pack('>H', register)
|
||||
return result
|
||||
|
||||
def decode(self, data):
|
||||
''' Decode a register response packet
|
||||
|
||||
:param data: The request to decode
|
||||
'''
|
||||
byte_count = byte2int(data[0])
|
||||
self.registers = []
|
||||
for i in range(1, byte_count + 1, 2):
|
||||
self.registers.append(struct.unpack('>H', data[i:i + 2])[0])
|
||||
|
||||
def getRegister(self, index):
|
||||
''' Get the requested register
|
||||
|
||||
:param index: The indexed register to retrieve
|
||||
:returns: The request register
|
||||
'''
|
||||
return self.registers[index]
|
||||
|
||||
def __str__(self):
|
||||
''' Returns a string representation of the instance
|
||||
|
||||
:returns: A string representation of the instance
|
||||
'''
|
||||
return "%s (%d)" % (self.__class__.__name__, len(self.registers))
|
||||
|
||||
|
||||
class ReadHoldingRegistersRequest(ReadRegistersRequestBase):
|
||||
'''
|
||||
This function code is used to read the contents of a contiguous block
|
||||
of holding registers in a remote device. The Request PDU specifies the
|
||||
starting register address and the number of registers. In the PDU
|
||||
Registers are addressed starting at zero. Therefore registers numbered
|
||||
1-16 are addressed as 0-15.
|
||||
'''
|
||||
function_code = 3
|
||||
|
||||
def __init__(self, address=None, count=None, **kwargs):
|
||||
''' Initializes a new instance of the request
|
||||
|
||||
:param address: The starting address to read from
|
||||
:param count: The number of registers to read from address
|
||||
'''
|
||||
ReadRegistersRequestBase.__init__(self, address, count, **kwargs)
|
||||
|
||||
def execute(self, context):
|
||||
''' Run a read holding request against a datastore
|
||||
|
||||
:param context: The datastore to request from
|
||||
:returns: An initialized response, exception message otherwise
|
||||
'''
|
||||
if not (1 <= self.count <= 0x7d):
|
||||
return self.doException(merror.IllegalValue)
|
||||
if not context.validate(self.function_code, self.address, self.count):
|
||||
return self.doException(merror.IllegalAddress)
|
||||
values = context.getValues(self.function_code, self.address, self.count)
|
||||
return ReadHoldingRegistersResponse(values)
|
||||
|
||||
|
||||
class ReadHoldingRegistersResponse(ReadRegistersResponseBase):
|
||||
'''
|
||||
This function code is used to read the contents of a contiguous block
|
||||
of holding registers in a remote device. The Request PDU specifies the
|
||||
starting register address and the number of registers. In the PDU
|
||||
Registers are addressed starting at zero. Therefore registers numbered
|
||||
1-16 are addressed as 0-15.
|
||||
'''
|
||||
function_code = 3
|
||||
|
||||
def __init__(self, values=None, **kwargs):
|
||||
''' Initializes a new response instance
|
||||
|
||||
:param values: The resulting register values
|
||||
'''
|
||||
ReadRegistersResponseBase.__init__(self, values, **kwargs)
|
||||
|
||||
|
||||
class ReadInputRegistersRequest(ReadRegistersRequestBase):
|
||||
'''
|
||||
This function code is used to read from 1 to approx. 125 contiguous
|
||||
input registers in a remote device. The Request PDU specifies the
|
||||
starting register address and the number of registers. In the PDU
|
||||
Registers are addressed starting at zero. Therefore input registers
|
||||
numbered 1-16 are addressed as 0-15.
|
||||
'''
|
||||
function_code = 4
|
||||
|
||||
def __init__(self, address=None, count=None, **kwargs):
|
||||
''' Initializes a new instance of the request
|
||||
|
||||
:param address: The starting address to read from
|
||||
:param count: The number of registers to read from address
|
||||
'''
|
||||
ReadRegistersRequestBase.__init__(self, address, count, **kwargs)
|
||||
|
||||
def execute(self, context):
|
||||
''' Run a read input request against a datastore
|
||||
|
||||
:param context: The datastore to request from
|
||||
:returns: An initialized response, exception message otherwise
|
||||
'''
|
||||
if not (1 <= self.count <= 0x7d):
|
||||
return self.doException(merror.IllegalValue)
|
||||
if not context.validate(self.function_code, self.address, self.count):
|
||||
return self.doException(merror.IllegalAddress)
|
||||
values = context.getValues(self.function_code, self.address, self.count)
|
||||
return ReadInputRegistersResponse(values)
|
||||
|
||||
|
||||
class ReadInputRegistersResponse(ReadRegistersResponseBase):
|
||||
'''
|
||||
This function code is used to read from 1 to approx. 125 contiguous
|
||||
input registers in a remote device. The Request PDU specifies the
|
||||
starting register address and the number of registers. In the PDU
|
||||
Registers are addressed starting at zero. Therefore input registers
|
||||
numbered 1-16 are addressed as 0-15.
|
||||
'''
|
||||
function_code = 4
|
||||
|
||||
def __init__(self, values=None, **kwargs):
|
||||
''' Initializes a new response instance
|
||||
|
||||
:param values: The resulting register values
|
||||
'''
|
||||
ReadRegistersResponseBase.__init__(self, values, **kwargs)
|
||||
|
||||
|
||||
class ReadWriteMultipleRegistersRequest(ModbusRequest):
|
||||
'''
|
||||
This function code performs a combination of one read operation and one
|
||||
write operation in a single MODBUS transaction. The write
|
||||
operation is performed before the read.
|
||||
|
||||
Holding registers are addressed starting at zero. Therefore holding
|
||||
registers 1-16 are addressed in the PDU as 0-15.
|
||||
|
||||
The request specifies the starting address and number of holding
|
||||
registers to be read as well as the starting address, number of holding
|
||||
registers, and the data to be written. The byte count specifies the
|
||||
number of bytes to follow in the write data field."
|
||||
'''
|
||||
function_code = 23
|
||||
_rtu_byte_count_pos = 10
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
''' Initializes a new request message
|
||||
|
||||
:param read_address: The address to start reading from
|
||||
:param read_count: The number of registers to read from address
|
||||
:param write_address: The address to start writing to
|
||||
:param write_registers: The registers to write to the specified address
|
||||
'''
|
||||
ModbusRequest.__init__(self, **kwargs)
|
||||
self.read_address = kwargs.get('read_address', 0x00)
|
||||
self.read_count = kwargs.get('read_count', 0)
|
||||
self.write_address = kwargs.get('write_address', 0x00)
|
||||
self.write_registers = kwargs.get('write_registers', None)
|
||||
if not hasattr(self.write_registers, '__iter__'):
|
||||
self.write_registers = [self.write_registers]
|
||||
self.write_count = len(self.write_registers)
|
||||
self.write_byte_count = self.write_count * 2
|
||||
|
||||
def encode(self):
|
||||
''' Encodes the request packet
|
||||
|
||||
:returns: The encoded packet
|
||||
'''
|
||||
result = struct.pack('>HHHHB',
|
||||
self.read_address, self.read_count, \
|
||||
self.write_address, self.write_count, self.write_byte_count)
|
||||
for register in self.write_registers:
|
||||
result += struct.pack('>H', register)
|
||||
return result
|
||||
|
||||
def decode(self, data):
|
||||
''' Decode the register request packet
|
||||
|
||||
:param data: The request to decode
|
||||
'''
|
||||
self.read_address, self.read_count, \
|
||||
self.write_address, self.write_count, \
|
||||
self.write_byte_count = struct.unpack('>HHHHB', data[:9])
|
||||
self.write_registers = []
|
||||
for i in range(9, self.write_byte_count + 9, 2):
|
||||
register = struct.unpack('>H', data[i:i + 2])[0]
|
||||
self.write_registers.append(register)
|
||||
|
||||
def execute(self, context):
|
||||
''' Run a write single register request against a datastore
|
||||
|
||||
:param context: The datastore to request from
|
||||
:returns: An initialized response, exception message otherwise
|
||||
'''
|
||||
if not (1 <= self.read_count <= 0x07d):
|
||||
return self.doException(merror.IllegalValue)
|
||||
if not (1 <= self.write_count <= 0x079):
|
||||
return self.doException(merror.IllegalValue)
|
||||
if (self.write_byte_count != self.write_count * 2):
|
||||
return self.doException(merror.IllegalValue)
|
||||
if not context.validate(self.function_code, self.write_address,
|
||||
self.write_count):
|
||||
return self.doException(merror.IllegalAddress)
|
||||
if not context.validate(self.function_code, self.read_address,
|
||||
self.read_count):
|
||||
return self.doException(merror.IllegalAddress)
|
||||
context.setValues(self.function_code, self.write_address,
|
||||
self.write_registers)
|
||||
registers = context.getValues(self.function_code, self.read_address,
|
||||
self.read_count)
|
||||
return ReadWriteMultipleRegistersResponse(registers)
|
||||
|
||||
def get_response_pdu_size(self):
|
||||
"""
|
||||
Func_code (1 byte) + Byte Count(1 byte) + 2 * Quantity of Coils (n Bytes)
|
||||
:return:
|
||||
"""
|
||||
return 1 + 1 + 2 * self.read_count
|
||||
|
||||
def __str__(self):
|
||||
''' Returns a string representation of the instance
|
||||
|
||||
:returns: A string representation of the instance
|
||||
'''
|
||||
params = (self.read_address, self.read_count, self.write_address,
|
||||
self.write_count)
|
||||
return "ReadWriteNRegisterRequest R(%d,%d) W(%d,%d)" % params
|
||||
|
||||
|
||||
class ReadWriteMultipleRegistersResponse(ModbusResponse):
|
||||
'''
|
||||
The normal response contains the data from the group of registers that
|
||||
were read. The byte count field specifies the quantity of bytes to
|
||||
follow in the read data field.
|
||||
'''
|
||||
function_code = 23
|
||||
_rtu_byte_count_pos = 2
|
||||
|
||||
def __init__(self, values=None, **kwargs):
|
||||
''' Initializes a new instance
|
||||
|
||||
:param values: The register values to write
|
||||
'''
|
||||
ModbusResponse.__init__(self, **kwargs)
|
||||
self.registers = values or []
|
||||
|
||||
def encode(self):
|
||||
''' Encodes the response packet
|
||||
|
||||
:returns: The encoded packet
|
||||
'''
|
||||
result = int2byte(len(self.registers) * 2)
|
||||
for register in self.registers:
|
||||
result += struct.pack('>H', register)
|
||||
return result
|
||||
|
||||
def decode(self, data):
|
||||
''' Decode the register response packet
|
||||
|
||||
:param data: The response to decode
|
||||
'''
|
||||
bytecount = byte2int(data[0])
|
||||
for i in range(1, bytecount, 2):
|
||||
self.registers.append(struct.unpack('>H', data[i:i + 2])[0])
|
||||
|
||||
def __str__(self):
|
||||
''' Returns a string representation of the instance
|
||||
|
||||
:returns: A string representation of the instance
|
||||
'''
|
||||
return "ReadWriteNRegisterResponse (%d)" % len(self.registers)
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Exported symbols
|
||||
#---------------------------------------------------------------------------#
|
||||
__all__ = [
|
||||
"ReadHoldingRegistersRequest", "ReadHoldingRegistersResponse",
|
||||
"ReadInputRegistersRequest", "ReadInputRegistersResponse",
|
||||
"ReadWriteMultipleRegistersRequest", "ReadWriteMultipleRegistersResponse",
|
||||
]
|
||||
354
modbus/pymodbus/register_write_message.py
Normal file
@@ -0,0 +1,354 @@
|
||||
'''
|
||||
Register Writing Request/Response Messages
|
||||
-------------------------------------------
|
||||
'''
|
||||
import struct
|
||||
from pymodbus.pdu import ModbusRequest
|
||||
from pymodbus.pdu import ModbusResponse
|
||||
from pymodbus.pdu import ModbusExceptions as merror
|
||||
|
||||
|
||||
class WriteSingleRegisterRequest(ModbusRequest):
|
||||
'''
|
||||
This function code is used to write a single holding register in a
|
||||
remote device.
|
||||
|
||||
The Request PDU specifies the address of the register to
|
||||
be written. Registers are addressed starting at zero. Therefore register
|
||||
numbered 1 is addressed as 0.
|
||||
'''
|
||||
function_code = 6
|
||||
_rtu_frame_size = 8
|
||||
|
||||
def __init__(self, address=None, value=None, **kwargs):
|
||||
''' Initializes a new instance
|
||||
|
||||
:param address: The address to start writing add
|
||||
:param value: The values to write
|
||||
'''
|
||||
ModbusRequest.__init__(self, **kwargs)
|
||||
self.address = address
|
||||
self.value = value
|
||||
|
||||
def encode(self):
|
||||
''' Encode a write single register packet packet request
|
||||
|
||||
:returns: The encoded packet
|
||||
'''
|
||||
packet = struct.pack('>H', self.address)
|
||||
if self.skip_encode:
|
||||
packet += self.value
|
||||
else:
|
||||
packet += struct.pack('>H', self.value)
|
||||
return packet
|
||||
|
||||
def decode(self, data):
|
||||
''' Decode a write single register packet packet request
|
||||
|
||||
:param data: The request to decode
|
||||
'''
|
||||
self.address, self.value = struct.unpack('>HH', data)
|
||||
|
||||
def execute(self, context):
|
||||
''' Run a write single register request against a datastore
|
||||
|
||||
:param context: The datastore to request from
|
||||
:returns: An initialized response, exception message otherwise
|
||||
'''
|
||||
if not (0 <= self.value <= 0xffff):
|
||||
return self.doException(merror.IllegalValue)
|
||||
if not context.validate(self.function_code, self.address, 1):
|
||||
return self.doException(merror.IllegalAddress)
|
||||
|
||||
context.setValues(self.function_code, self.address, [self.value])
|
||||
values = context.getValues(self.function_code, self.address, 1)
|
||||
return WriteSingleRegisterResponse(self.address, values[0])
|
||||
|
||||
def get_response_pdu_size(self):
|
||||
"""
|
||||
Func_code (1 byte) + Register Address(2 byte) + Register Value (2 bytes)
|
||||
:return:
|
||||
"""
|
||||
return 1 + 2 + 2
|
||||
|
||||
def __str__(self):
|
||||
''' Returns a string representation of the instance
|
||||
|
||||
:returns: A string representation of the instance
|
||||
'''
|
||||
return "WriteRegisterRequest %d" % self.address
|
||||
|
||||
|
||||
class WriteSingleRegisterResponse(ModbusResponse):
|
||||
'''
|
||||
The normal response is an echo of the request, returned after the
|
||||
register contents have been written.
|
||||
'''
|
||||
function_code = 6
|
||||
_rtu_frame_size = 8
|
||||
|
||||
def __init__(self, address=None, value=None, **kwargs):
|
||||
''' Initializes a new instance
|
||||
|
||||
:param address: The address to start writing add
|
||||
:param value: The values to write
|
||||
'''
|
||||
ModbusResponse.__init__(self, **kwargs)
|
||||
self.address = address
|
||||
self.value = value
|
||||
|
||||
def encode(self):
|
||||
''' Encode a write single register packet packet request
|
||||
|
||||
:returns: The encoded packet
|
||||
'''
|
||||
return struct.pack('>HH', self.address, self.value)
|
||||
|
||||
def decode(self, data):
|
||||
''' Decode a write single register packet packet request
|
||||
|
||||
:param data: The request to decode
|
||||
'''
|
||||
self.address, self.value = struct.unpack('>HH', data)
|
||||
|
||||
def __str__(self):
|
||||
''' Returns a string representation of the instance
|
||||
|
||||
:returns: A string representation of the instance
|
||||
'''
|
||||
params = (self.address, self.value)
|
||||
return "WriteRegisterResponse %d => %d" % params
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Write Multiple Registers
|
||||
#---------------------------------------------------------------------------#
|
||||
class WriteMultipleRegistersRequest(ModbusRequest):
|
||||
'''
|
||||
This function code is used to write a block of contiguous registers (1
|
||||
to approx. 120 registers) in a remote device.
|
||||
|
||||
The requested written values are specified in the request data field.
|
||||
Data is packed as two bytes per register.
|
||||
'''
|
||||
function_code = 16
|
||||
_rtu_byte_count_pos = 6
|
||||
_pdu_length = 5 #func + adress1 + adress2 + outputQuant1 + outputQuant2
|
||||
|
||||
def __init__(self, address=None, values=None, **kwargs):
|
||||
''' Initializes a new instance
|
||||
|
||||
:param address: The address to start writing to
|
||||
:param values: The values to write
|
||||
'''
|
||||
ModbusRequest.__init__(self, **kwargs)
|
||||
self.address = address
|
||||
if values is None:
|
||||
values = []
|
||||
elif not hasattr(values, '__iter__'):
|
||||
values = [values]
|
||||
self.values = values
|
||||
self.count = len(self.values)
|
||||
self.byte_count = self.count * 2
|
||||
|
||||
def encode(self):
|
||||
''' Encode a write single register packet packet request
|
||||
|
||||
:returns: The encoded packet
|
||||
'''
|
||||
packet = struct.pack('>HHB', self.address, self.count, self.byte_count)
|
||||
if self.skip_encode:
|
||||
return packet + b''.join(self.values)
|
||||
|
||||
for value in self.values:
|
||||
packet += struct.pack('>H', value)
|
||||
|
||||
return packet
|
||||
|
||||
def decode(self, data):
|
||||
''' Decode a write single register packet packet request
|
||||
|
||||
:param data: The request to decode
|
||||
'''
|
||||
self.address, self.count, \
|
||||
self.byte_count = struct.unpack('>HHB', data[:5])
|
||||
self.values = [] # reset
|
||||
for idx in range(5, (self.count * 2) + 5, 2):
|
||||
self.values.append(struct.unpack('>H', data[idx:idx + 2])[0])
|
||||
|
||||
def execute(self, context):
|
||||
''' Run a write single register request against a datastore
|
||||
|
||||
:param context: The datastore to request from
|
||||
:returns: An initialized response, exception message otherwise
|
||||
'''
|
||||
if not (1 <= self.count <= 0x07b):
|
||||
return self.doException(merror.IllegalValue)
|
||||
if (self.byte_count != self.count * 2):
|
||||
return self.doException(merror.IllegalValue)
|
||||
if not context.validate(self.function_code, self.address, self.count):
|
||||
return self.doException(merror.IllegalAddress)
|
||||
|
||||
context.setValues(self.function_code, self.address, self.values)
|
||||
return WriteMultipleRegistersResponse(self.address, self.count)
|
||||
|
||||
def get_response_pdu_size(self):
|
||||
"""
|
||||
Func_code (1 byte) + Starting Address (2 byte) + Quantity of Reggisters (2 Bytes)
|
||||
:return:
|
||||
"""
|
||||
return 1 + 2 + 2
|
||||
|
||||
def __str__(self):
|
||||
''' Returns a string representation of the instance
|
||||
|
||||
:returns: A string representation of the instance
|
||||
'''
|
||||
params = (self.address, self.count)
|
||||
return "WriteMultipleRegisterRequest %d => %d" % params
|
||||
|
||||
|
||||
class WriteMultipleRegistersResponse(ModbusResponse):
|
||||
'''
|
||||
"The normal response returns the function code, starting address, and
|
||||
quantity of registers written.
|
||||
'''
|
||||
function_code = 16
|
||||
_rtu_frame_size = 8
|
||||
|
||||
def __init__(self, address=None, count=None, **kwargs):
|
||||
''' Initializes a new instance
|
||||
|
||||
:param address: The address to start writing to
|
||||
:param count: The number of registers to write to
|
||||
'''
|
||||
ModbusResponse.__init__(self, **kwargs)
|
||||
self.address = address
|
||||
self.count = count
|
||||
|
||||
def encode(self):
|
||||
''' Encode a write single register packet packet request
|
||||
|
||||
:returns: The encoded packet
|
||||
'''
|
||||
return struct.pack('>HH', self.address, self.count)
|
||||
|
||||
def decode(self, data):
|
||||
''' Decode a write single register packet packet request
|
||||
|
||||
:param data: The request to decode
|
||||
'''
|
||||
self.address, self.count = struct.unpack('>HH', data)
|
||||
|
||||
def __str__(self):
|
||||
''' Returns a string representation of the instance
|
||||
|
||||
:returns: A string representation of the instance
|
||||
'''
|
||||
params = (self.address, self.count)
|
||||
return "WriteMultipleRegisterResponse (%d,%d)" % params
|
||||
|
||||
class MaskWriteRegisterRequest(ModbusRequest):
|
||||
'''
|
||||
This function code is used to modify the contents of a specified holding
|
||||
register using a combination of an AND mask, an OR mask, and the
|
||||
register's current contents. The function can be used to set or clear
|
||||
individual bits in the register.
|
||||
'''
|
||||
function_code = 0x16
|
||||
_rtu_frame_size = 10
|
||||
|
||||
def __init__(self, address=0x0000, and_mask=0xffff, or_mask=0x0000,
|
||||
**kwargs):
|
||||
''' Initializes a new instance
|
||||
|
||||
:param address: The mask pointer address (0x0000 to 0xffff)
|
||||
:param and_mask: The and bitmask to apply to the register address
|
||||
:param or_mask: The or bitmask to apply to the register address
|
||||
'''
|
||||
ModbusRequest.__init__(self, **kwargs)
|
||||
self.address = address
|
||||
self.and_mask = and_mask
|
||||
self.or_mask = or_mask
|
||||
|
||||
def encode(self):
|
||||
''' Encodes the request packet
|
||||
|
||||
:returns: The byte encoded packet
|
||||
'''
|
||||
return struct.pack('>HHH', self.address, self.and_mask,
|
||||
self.or_mask)
|
||||
|
||||
def decode(self, data):
|
||||
''' Decodes the incoming request
|
||||
|
||||
:param data: The data to decode into the address
|
||||
'''
|
||||
self.address, self.and_mask, self.or_mask = struct.unpack('>HHH',
|
||||
data)
|
||||
|
||||
def execute(self, context):
|
||||
''' Run a mask write register request against the store
|
||||
|
||||
:param context: The datastore to request from
|
||||
:returns: The populated response
|
||||
'''
|
||||
if not (0x0000 <= self.and_mask <= 0xffff):
|
||||
return self.doException(merror.IllegalValue)
|
||||
if not (0x0000 <= self.or_mask <= 0xffff):
|
||||
return self.doException(merror.IllegalValue)
|
||||
if not context.validate(self.function_code, self.address, 1):
|
||||
return self.doException(merror.IllegalAddress)
|
||||
values = context.getValues(self.function_code, self.address, 1)[0]
|
||||
values = ((values & self.and_mask) | self.or_mask)
|
||||
context.setValues(self.function_code, self.address, [values])
|
||||
return MaskWriteRegisterResponse(self.address, self.and_mask,
|
||||
self.or_mask)
|
||||
|
||||
|
||||
class MaskWriteRegisterResponse(ModbusResponse):
|
||||
'''
|
||||
The normal response is an echo of the request. The response is returned
|
||||
after the register has been written.
|
||||
'''
|
||||
function_code = 0x16
|
||||
_rtu_frame_size = 10
|
||||
|
||||
def __init__(self, address=0x0000, and_mask=0xffff, or_mask=0x0000,
|
||||
**kwargs):
|
||||
''' Initializes a new instance
|
||||
|
||||
:param address: The mask pointer address (0x0000 to 0xffff)
|
||||
:param and_mask: The and bitmask applied to the register address
|
||||
:param or_mask: The or bitmask applied to the register address
|
||||
'''
|
||||
ModbusResponse.__init__(self, **kwargs)
|
||||
self.address = address
|
||||
self.and_mask = and_mask
|
||||
self.or_mask = or_mask
|
||||
|
||||
def encode(self):
|
||||
''' Encodes the response
|
||||
|
||||
:returns: The byte encoded message
|
||||
'''
|
||||
return struct.pack('>HHH', self.address, self.and_mask,
|
||||
self.or_mask)
|
||||
|
||||
def decode(self, data):
|
||||
''' Decodes a the response
|
||||
|
||||
:param data: The packet data to decode
|
||||
'''
|
||||
self.address, self.and_mask, self.or_mask = struct.unpack('>HHH',
|
||||
data)
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------#
|
||||
# Exported symbols
|
||||
#---------------------------------------------------------------------------#
|
||||
__all__ = [
|
||||
"WriteSingleRegisterRequest", "WriteSingleRegisterResponse",
|
||||
"WriteMultipleRegistersRequest", "WriteMultipleRegistersResponse",
|
||||
"MaskWriteRegisterRequest", "MaskWriteRegisterResponse"
|
||||
]
|
||||
315
modbus/pymodbus/repl/README.md
Normal file
@@ -0,0 +1,315 @@
|
||||
# Pymodbus REPL
|
||||
|
||||
## Dependencies
|
||||
|
||||
Depends on [prompt_toolkit](https://python-prompt-toolkit.readthedocs.io/en/stable/index.html) and [click](http://click.pocoo.org/6/quickstart/)
|
||||
|
||||
Install dependencies
|
||||
```
|
||||
$ pip install click prompt_toolkit --upgrade
|
||||
```
|
||||
|
||||
Or
|
||||
Install pymodbus with repl support
|
||||
```
|
||||
$ pip install pymodbus[repl] --upgrade
|
||||
```
|
||||
|
||||
## Usage Instructions
|
||||
RTU and TCP are supported as of now
|
||||
|
||||
```
|
||||
✗ pymodbus.console --help
|
||||
Usage: pymodbus.console [OPTIONS] COMMAND [ARGS]...
|
||||
|
||||
Options:
|
||||
--version Show the version and exit.
|
||||
--verbose Verbose logs
|
||||
--broadcast-support Support broadcast messages
|
||||
--help Show this message and exit.
|
||||
|
||||
Commands:
|
||||
serial
|
||||
tcp
|
||||
|
||||
|
||||
```
|
||||
TCP Options
|
||||
|
||||
```
|
||||
✗ pymodbus.console tcp --help
|
||||
Usage: pymodbus.console tcp [OPTIONS]
|
||||
|
||||
Options:
|
||||
--host TEXT Modbus TCP IP
|
||||
--port INTEGER Modbus TCP port
|
||||
--framer TEXT Override the default packet framer tcp|rtu
|
||||
--help Show this message and exit.
|
||||
|
||||
```
|
||||
|
||||
SERIAL Options
|
||||
```
|
||||
✗ pymodbus.console serial --help
|
||||
Usage: pymodbus.console serial [OPTIONS]
|
||||
|
||||
Options:
|
||||
--method TEXT Modbus Serial Mode (rtu/ascii)
|
||||
--port TEXT Modbus RTU port
|
||||
--baudrate INTEGER Modbus RTU serial baudrate to use. Defaults to 9600
|
||||
--bytesize [5|6|7|8] Modbus RTU serial Number of data bits. Possible
|
||||
values: FIVEBITS, SIXBITS, SEVENBITS, EIGHTBITS.
|
||||
Defaults to 8
|
||||
|
||||
--parity [N|E|O|M|S] Modbus RTU serial parity. Enable parity checking.
|
||||
Possible values: PARITY_NONE, PARITY_EVEN, PARITY_ODD
|
||||
PARITY_MARK, PARITY_SPACE. Default to 'N'
|
||||
|
||||
--stopbits [1|1.5|2] Modbus RTU serial stop bits. Number of stop bits.
|
||||
Possible values: STOPBITS_ONE,
|
||||
STOPBITS_ONE_POINT_FIVE, STOPBITS_TWO. Default to '1'
|
||||
|
||||
--xonxoff INTEGER Modbus RTU serial xonxoff. Enable software flow
|
||||
control.Defaults to 0
|
||||
|
||||
--rtscts INTEGER Modbus RTU serial rtscts. Enable hardware (RTS/CTS)
|
||||
flow control. Defaults to 0
|
||||
|
||||
--dsrdtr INTEGER Modbus RTU serial dsrdtr. Enable hardware (DSR/DTR)
|
||||
flow control. Defaults to 0
|
||||
|
||||
--timeout FLOAT Modbus RTU serial read timeout. Defaults to 0.025 sec
|
||||
--write-timeout FLOAT Modbus RTU serial write timeout. Defaults to 2 sec
|
||||
--help Show this message and exit.
|
||||
```
|
||||
|
||||
To view all available commands type `help`
|
||||
|
||||
TCP
|
||||
```
|
||||
$ pymodbus.console tcp --host 192.168.128.126 --port 5020
|
||||
|
||||
> help
|
||||
Available commands:
|
||||
client.change_ascii_input_delimiter Diagnostic sub command, Change message delimiter for future requests.
|
||||
client.clear_counters Diagnostic sub command, Clear all counters and diag registers.
|
||||
client.clear_overrun_count Diagnostic sub command, Clear over run counter.
|
||||
client.close Closes the underlying socket connection
|
||||
client.connect Connect to the modbus tcp server
|
||||
client.debug_enabled Returns a boolean indicating if debug is enabled.
|
||||
client.force_listen_only_mode Diagnostic sub command, Forces the addressed remote device to its Listen Only Mode.
|
||||
client.get_clear_modbus_plus Diagnostic sub command, Get or clear stats of remote modbus plus device.
|
||||
client.get_com_event_counter Read status word and an event count from the remote device's communication event counter.
|
||||
client.get_com_event_log Read status word, event count, message count, and a field of event bytes from the remote device.
|
||||
client.host Read Only!
|
||||
client.idle_time Bus Idle Time to initiate next transaction
|
||||
client.is_socket_open Check whether the underlying socket/serial is open or not.
|
||||
client.last_frame_end Read Only!
|
||||
client.mask_write_register Mask content of holding register at `address` with `and_mask` and `or_mask`.
|
||||
client.port Read Only!
|
||||
client.read_coils Reads `count` coils from a given slave starting at `address`.
|
||||
client.read_device_information Read the identification and additional information of remote slave.
|
||||
client.read_discrete_inputs Reads `count` number of discrete inputs starting at offset `address`.
|
||||
client.read_exception_status Read the contents of eight Exception Status outputs in a remote device.
|
||||
client.read_holding_registers Read `count` number of holding registers starting at `address`.
|
||||
client.read_input_registers Read `count` number of input registers starting at `address`.
|
||||
client.readwrite_registers Read `read_count` number of holding registers starting at `read_address` and write `write_registers` starting at `write_address`.
|
||||
client.report_slave_id Report information about remote slave ID.
|
||||
client.restart_comm_option Diagnostic sub command, initialize and restart remote devices serial interface and clear all of its communications event counters .
|
||||
client.return_bus_com_error_count Diagnostic sub command, Return count of CRC errors received by remote slave.
|
||||
client.return_bus_exception_error_count Diagnostic sub command, Return count of Modbus exceptions returned by remote slave.
|
||||
client.return_bus_message_count Diagnostic sub command, Return count of message detected on bus by remote slave.
|
||||
client.return_diagnostic_register Diagnostic sub command, Read 16-bit diagnostic register.
|
||||
client.return_iop_overrun_count Diagnostic sub command, Return count of iop overrun errors by remote slave.
|
||||
client.return_query_data Diagnostic sub command , Loop back data sent in response.
|
||||
client.return_slave_bus_char_overrun_count Diagnostic sub command, Return count of messages not handled by remote slave due to character overrun condition.
|
||||
client.return_slave_busy_count Diagnostic sub command, Return count of server busy exceptions sent by remote slave.
|
||||
client.return_slave_message_count Diagnostic sub command, Return count of messages addressed to remote slave.
|
||||
client.return_slave_no_ack_count Diagnostic sub command, Return count of NO ACK exceptions sent by remote slave.
|
||||
client.return_slave_no_response_count Diagnostic sub command, Return count of No responses by remote slave.
|
||||
client.silent_interval Read Only!
|
||||
client.state Read Only!
|
||||
client.timeout Read Only!
|
||||
client.write_coil Write `value` to coil at `address`.
|
||||
client.write_coils Write `value` to coil at `address`.
|
||||
client.write_register Write `value` to register at `address`.
|
||||
client.write_registers Write list of `values` to registers starting at `address`.
|
||||
```
|
||||
|
||||
SERIAL
|
||||
```
|
||||
$ pymodbus.console serial --port /dev/ttyUSB0 --baudrate 19200 --timeout 2
|
||||
> help
|
||||
Available commands:
|
||||
client.baudrate Read Only!
|
||||
client.bytesize Read Only!
|
||||
client.change_ascii_input_delimiter Diagnostic sub command, Change message delimiter for future requests.
|
||||
client.clear_counters Diagnostic sub command, Clear all counters and diag registers.
|
||||
client.clear_overrun_count Diagnostic sub command, Clear over run counter.
|
||||
client.close Closes the underlying socket connection
|
||||
client.connect Connect to the modbus serial server
|
||||
client.debug_enabled Returns a boolean indicating if debug is enabled.
|
||||
client.force_listen_only_mode Diagnostic sub command, Forces the addressed remote device to its Listen Only Mode.
|
||||
client.get_baudrate Serial Port baudrate.
|
||||
client.get_bytesize Number of data bits.
|
||||
client.get_clear_modbus_plus Diagnostic sub command, Get or clear stats of remote modbus plus device.
|
||||
client.get_com_event_counter Read status word and an event count from the remote device's communication event counter.
|
||||
client.get_com_event_log Read status word, event count, message count, and a field of event bytes from the remote device.
|
||||
client.get_parity Enable Parity Checking.
|
||||
client.get_port Serial Port.
|
||||
client.get_serial_settings Gets Current Serial port settings.
|
||||
client.get_stopbits Number of stop bits.
|
||||
client.get_timeout Serial Port Read timeout.
|
||||
client.idle_time Bus Idle Time to initiate next transaction
|
||||
client.inter_char_timeout Read Only!
|
||||
client.is_socket_open c l i e n t . i s s o c k e t o p e n
|
||||
client.mask_write_register Mask content of holding register at `address` with `and_mask` and `or_mask`.
|
||||
client.method Read Only!
|
||||
client.parity Read Only!
|
||||
client.port Read Only!
|
||||
client.read_coils Reads `count` coils from a given slave starting at `address`.
|
||||
client.read_device_information Read the identification and additional information of remote slave.
|
||||
client.read_discrete_inputs Reads `count` number of discrete inputs starting at offset `address`.
|
||||
client.read_exception_status Read the contents of eight Exception Status outputs in a remote device.
|
||||
client.read_holding_registers Read `count` number of holding registers starting at `address`.
|
||||
client.read_input_registers Read `count` number of input registers starting at `address`.
|
||||
client.readwrite_registers Read `read_count` number of holding registers starting at `read_address` and write `write_registers` starting at `write_address`.
|
||||
client.report_slave_id Report information about remote slave ID.
|
||||
client.restart_comm_option Diagnostic sub command, initialize and restart remote devices serial interface and clear all of its communications event counters .
|
||||
client.return_bus_com_error_count Diagnostic sub command, Return count of CRC errors received by remote slave.
|
||||
client.return_bus_exception_error_count Diagnostic sub command, Return count of Modbus exceptions returned by remote slave.
|
||||
client.return_bus_message_count Diagnostic sub command, Return count of message detected on bus by remote slave.
|
||||
client.return_diagnostic_register Diagnostic sub command, Read 16-bit diagnostic register.
|
||||
client.return_iop_overrun_count Diagnostic sub command, Return count of iop overrun errors by remote slave.
|
||||
client.return_query_data Diagnostic sub command , Loop back data sent in response.
|
||||
client.return_slave_bus_char_overrun_count Diagnostic sub command, Return count of messages not handled by remote slave due to character overrun condition.
|
||||
client.return_slave_busy_count Diagnostic sub command, Return count of server busy exceptions sent by remote slave.
|
||||
client.return_slave_message_count Diagnostic sub command, Return count of messages addressed to remote slave.
|
||||
client.return_slave_no_ack_count Diagnostic sub command, Return count of NO ACK exceptions sent by remote slave.
|
||||
client.return_slave_no_response_count Diagnostic sub command, Return count of No responses by remote slave.
|
||||
client.set_baudrate Baudrate setter.
|
||||
client.set_bytesize Byte size setter.
|
||||
client.set_parity Parity Setter.
|
||||
client.set_port Serial Port setter.
|
||||
client.set_stopbits Stop bit setter.
|
||||
client.set_timeout Read timeout setter.
|
||||
client.silent_interval Read Only!
|
||||
client.state Read Only!
|
||||
client.stopbits Read Only!
|
||||
client.timeout Read Only!
|
||||
client.write_coil Write `value` to coil at `address`.
|
||||
client.write_coils Write `value` to coil at `address`.
|
||||
client.write_register Write `value` to register at `address`.
|
||||
client.write_registers Write list of `values` to registers starting at `address`.
|
||||
result.decode Decode the register response to known formatters.
|
||||
result.raw Return raw result dict.
|
||||
|
||||
```
|
||||
|
||||
Every command has auto suggetion on the arguments supported , supply arg and value are to be supplied in `arg=val` format.
|
||||
```
|
||||
|
||||
> client.read_holding_registers count=4 address=9 unit=1
|
||||
{
|
||||
"registers": [
|
||||
60497,
|
||||
47134,
|
||||
34091,
|
||||
15424
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The last result could be accessed with `result.raw` command
|
||||
```
|
||||
> result.raw
|
||||
{
|
||||
"registers": [
|
||||
15626,
|
||||
55203,
|
||||
28733,
|
||||
18368
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
For Holding and Input register reads, the decoded value could be viewed with `result.decode`
|
||||
```
|
||||
> result.decode word_order=little byte_order=little formatters=float64
|
||||
28.17
|
||||
|
||||
>
|
||||
```
|
||||
|
||||
Client settings could be retrieved and altered as well.
|
||||
```
|
||||
> # For serial settings
|
||||
|
||||
> # Check the serial mode
|
||||
> client.method
|
||||
"rtu"
|
||||
|
||||
> client.get_serial_settings
|
||||
{
|
||||
"t1.5": 0.00171875,
|
||||
"baudrate": 9600,
|
||||
"read timeout": 0.5,
|
||||
"port": "/dev/ptyp0",
|
||||
"t3.5": 0.00401,
|
||||
"bytesize": 8,
|
||||
"parity": "N",
|
||||
"stopbits": 1.0
|
||||
}
|
||||
> client.set_timeout value=1
|
||||
null
|
||||
|
||||
> client.get_timeout
|
||||
1.0
|
||||
|
||||
> client.get_serial_settings
|
||||
{
|
||||
"t1.5": 0.00171875,
|
||||
"baudrate": 9600,
|
||||
"read timeout": 1.0,
|
||||
"port": "/dev/ptyp0",
|
||||
"t3.5": 0.00401,
|
||||
"bytesize": 8,
|
||||
"parity": "N",
|
||||
"stopbits": 1.0
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
To Send broadcast requests, use `--broadcast-support` and send requests with unit id as `0`.
|
||||
`write_coil`, `write_coils`, `write_register`, `write_registers` are supported.
|
||||
|
||||
```
|
||||
✗ pymodbus.console --broadcast-support tcp --host 192.168.1.8 --port 5020
|
||||
|
||||
----------------------------------------------------------------------------
|
||||
__________ _____ .___ __________ .__
|
||||
\______ \___.__. / \ ____ __| _/ \______ \ ____ ______ | |
|
||||
| ___< | |/ \ / \ / _ \ / __ | | _// __ \\____ \| |
|
||||
| | \___ / Y ( <_> ) /_/ | | | \ ___/| |_> > |__
|
||||
|____| / ____\____|__ /\____/\____ | /\ |____|_ /\___ > __/|____/
|
||||
\/ \/ \/ \/ \/ \/|__|
|
||||
v1.2.0 - [pymodbus, version 2.4.0]
|
||||
----------------------------------------------------------------------------
|
||||
|
||||
> client.write_registers address=0 values=10,20,30,40 unit=0
|
||||
{
|
||||
"broadcasted": true
|
||||
}
|
||||
|
||||
> client.write_registers address=0 values=10,20,30,40 unit=1
|
||||
{
|
||||
"address": 0,
|
||||
"count": 4
|
||||
}
|
||||
```
|
||||
|
||||
## DEMO
|
||||
|
||||
[](https://asciinema.org/a/y1xOk7lm59U1bRBE2N1pDIj2o)
|
||||
[](https://asciinema.org/a/edUqZN77fdjxL2toisiilJNwI)
|
||||
|
||||
7
modbus/pymodbus/repl/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
"""
|
||||
Pymodbus REPL Module.
|
||||
|
||||
Copyright (c) 2018 Riptide IO, Inc. All Rights Reserved.
|
||||
|
||||
"""
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
4
modbus/pymodbus/repl/client/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
"""
|
||||
Copyright (c) 2020 by RiptideIO
|
||||
All rights reserved.
|
||||
"""
|
||||
156
modbus/pymodbus/repl/client/completer.py
Normal file
@@ -0,0 +1,156 @@
|
||||
"""
|
||||
Command Completion for pymodbus REPL.
|
||||
|
||||
Copyright (c) 2018 Riptide IO, Inc. All Rights Reserved.
|
||||
|
||||
"""
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
from prompt_toolkit.completion import Completer, Completion
|
||||
from prompt_toolkit.styles import Style
|
||||
from prompt_toolkit.filters import Condition
|
||||
from prompt_toolkit.application.current import get_app
|
||||
from pymodbus.repl.client.helper import get_commands
|
||||
from pymodbus.compat import string_types
|
||||
|
||||
|
||||
@Condition
|
||||
def has_selected_completion():
|
||||
complete_state = get_app().current_buffer.complete_state
|
||||
return (complete_state is not None and
|
||||
complete_state.current_completion is not None)
|
||||
|
||||
|
||||
style = Style.from_dict({
|
||||
'completion-menu.completion': 'bg:#008888 #ffffff',
|
||||
'completion-menu.completion.current': 'bg:#00aaaa #000000',
|
||||
'scrollbar.background': 'bg:#88aaaa',
|
||||
'scrollbar.button': 'bg:#222222',
|
||||
})
|
||||
|
||||
|
||||
class CmdCompleter(Completer):
|
||||
"""
|
||||
Completer for Pymodbus REPL.
|
||||
"""
|
||||
|
||||
def __init__(self, client=None, commands=None, ignore_case=True):
|
||||
"""
|
||||
|
||||
:param client: Modbus Client
|
||||
:param commands: Commands to be added for Completion (list)
|
||||
:param ignore_case: Ignore Case while looking up for commands
|
||||
"""
|
||||
self._commands = commands or get_commands(client)
|
||||
self._commands['help'] = ""
|
||||
self._command_names = self._commands.keys()
|
||||
self.ignore_case = ignore_case
|
||||
|
||||
@property
|
||||
def commands(self):
|
||||
return self._commands
|
||||
|
||||
@property
|
||||
def command_names(self):
|
||||
return self._commands.keys()
|
||||
|
||||
def completing_command(self, words, word_before_cursor):
|
||||
"""
|
||||
Determine if we are dealing with supported command.
|
||||
|
||||
:param words: Input text broken in to word tokens.
|
||||
:param word_before_cursor: The current word before the cursor, \
|
||||
which might be one or more blank spaces.
|
||||
:return:
|
||||
"""
|
||||
if len(words) == 1 and word_before_cursor != '':
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def completing_arg(self, words, word_before_cursor):
|
||||
"""
|
||||
Determine if we are currently completing an argument.
|
||||
|
||||
:param words: The input text broken into word tokens.
|
||||
:param word_before_cursor: The current word before the cursor, \
|
||||
which might be one or more blank spaces.
|
||||
:return: Specifies whether we are currently completing an arg.
|
||||
"""
|
||||
if len(words) > 1 and word_before_cursor != '':
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def arg_completions(self, words, word_before_cursor):
|
||||
"""
|
||||
Generates arguments completions based on the input.
|
||||
|
||||
:param words: The input text broken into word tokens.
|
||||
:param word_before_cursor: The current word before the cursor, \
|
||||
which might be one or more blank spaces.
|
||||
:return: A list of completions.
|
||||
"""
|
||||
cmd = words[0].strip()
|
||||
cmd = self._commands.get(cmd, None)
|
||||
if cmd:
|
||||
return cmd
|
||||
|
||||
def _get_completions(self, word, word_before_cursor):
|
||||
if self.ignore_case:
|
||||
word_before_cursor = word_before_cursor.lower()
|
||||
return self.word_matches(word, word_before_cursor)
|
||||
|
||||
def word_matches(self, word, word_before_cursor):
|
||||
"""
|
||||
Match the word and word before cursor
|
||||
|
||||
:param words: The input text broken into word tokens.
|
||||
:param word_before_cursor: The current word before the cursor, \
|
||||
which might be one or more blank spaces.
|
||||
:return: True if matched.
|
||||
|
||||
"""
|
||||
if self.ignore_case:
|
||||
word = word.lower()
|
||||
return word.startswith(word_before_cursor)
|
||||
|
||||
def get_completions(self, document, complete_event):
|
||||
"""
|
||||
Get completions for the current scope.
|
||||
|
||||
:param document: An instance of `prompt_toolkit.Document`.
|
||||
:param complete_event: (Unused).
|
||||
:return: Yields an instance of `prompt_toolkit.completion.Completion`.
|
||||
"""
|
||||
word_before_cursor = document.get_word_before_cursor(WORD=True)
|
||||
text = document.text_before_cursor.lstrip()
|
||||
words = document.text.strip().split()
|
||||
meta = None
|
||||
commands = []
|
||||
if len(words) == 0:
|
||||
# yield commands
|
||||
pass
|
||||
if self.completing_command(words, word_before_cursor):
|
||||
commands = self._command_names
|
||||
c_meta = {
|
||||
k: v.help_text
|
||||
if not isinstance(v, string_types)
|
||||
else v for k, v in self._commands.items()
|
||||
}
|
||||
meta = lambda x: (x, c_meta.get(x, ''))
|
||||
else:
|
||||
if not list(filter(lambda cmd: any(x == cmd for x in words),
|
||||
self._command_names)):
|
||||
# yield commands
|
||||
pass
|
||||
|
||||
if ' ' in text:
|
||||
command = self.arg_completions(words, word_before_cursor)
|
||||
commands = list(command.get_completion())
|
||||
commands = list(filter(lambda cmd: not(any(cmd in x for x in words)), commands))
|
||||
meta = command.get_meta
|
||||
for a in commands:
|
||||
if self._get_completions(a, word_before_cursor):
|
||||
cmd, display_meta = meta(a) if meta else ('', '')
|
||||
yield Completion(a, -len(word_before_cursor),
|
||||
display_meta=display_meta)
|
||||
327
modbus/pymodbus/repl/client/helper.py
Normal file
@@ -0,0 +1,327 @@
|
||||
"""
|
||||
Helper Module for REPL actions.
|
||||
|
||||
Copyright (c) 2018 Riptide IO, Inc. All Rights Reserved.
|
||||
|
||||
"""
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
import json
|
||||
import pygments
|
||||
import inspect
|
||||
from collections import OrderedDict
|
||||
from pygments.lexers.data import JsonLexer
|
||||
from prompt_toolkit.formatted_text import PygmentsTokens, HTML
|
||||
from prompt_toolkit import print_formatted_text
|
||||
|
||||
from pymodbus.payload import BinaryPayloadDecoder, Endian
|
||||
from pymodbus.compat import PYTHON_VERSION, IS_PYTHON2, string_types, izip
|
||||
|
||||
predicate = inspect.ismethod
|
||||
if IS_PYTHON2 or PYTHON_VERSION < (3, 3):
|
||||
argspec = inspect.getargspec
|
||||
else:
|
||||
predicate = inspect.isfunction
|
||||
argspec = inspect.signature
|
||||
|
||||
|
||||
FORMATTERS = {
|
||||
'int8': 'decode_8bit_int',
|
||||
'int16': 'decode_16bit_int',
|
||||
'int32': 'decode_32bit_int',
|
||||
'int64': 'decode_64bit_int',
|
||||
'uint8': 'decode_8bit_uint',
|
||||
'uint16': 'decode_16bit_uint',
|
||||
'uint32': 'decode_32bit_uint',
|
||||
'uint64': 'decode_64bit_int',
|
||||
'float16': 'decode_16bit_float',
|
||||
'float32': 'decode_32bit_float',
|
||||
'float64': 'decode_64bit_float',
|
||||
}
|
||||
|
||||
|
||||
DEFAULT_KWARGS = {
|
||||
'unit': 'Slave address'
|
||||
}
|
||||
|
||||
OTHER_COMMANDS = {
|
||||
"result.raw": "Show RAW Result",
|
||||
"result.decode": "Decode register response to known formats",
|
||||
}
|
||||
EXCLUDE = ['execute', 'recv', 'send', 'trace', 'set_debug']
|
||||
CLIENT_METHODS = [
|
||||
'connect', 'close', 'idle_time', 'is_socket_open', 'get_port', 'set_port',
|
||||
'get_stopbits', 'set_stopbits', 'get_bytesize', 'set_bytesize',
|
||||
'get_parity', 'set_parity', 'get_baudrate', 'set_baudrate', 'get_timeout',
|
||||
'set_timeout', 'get_serial_settings'
|
||||
|
||||
]
|
||||
CLIENT_ATTRIBUTES = []
|
||||
|
||||
|
||||
class Command(object):
|
||||
"""
|
||||
Class representing Commands to be consumed by Completer.
|
||||
"""
|
||||
def __init__(self, name, signature, doc, unit=False):
|
||||
"""
|
||||
|
||||
:param name: Name of the command
|
||||
:param signature: inspect object
|
||||
:param doc: Doc string for the command
|
||||
:param unit: Use unit as additional argument in the command .
|
||||
"""
|
||||
self.name = name
|
||||
self.doc = doc.split("\n") if doc else " ".join(name.split("_"))
|
||||
self.help_text = self._create_help()
|
||||
self.param_help = self._create_arg_help()
|
||||
if signature:
|
||||
if IS_PYTHON2:
|
||||
self._params = signature
|
||||
else:
|
||||
self._params = signature.parameters
|
||||
self.args = self.create_completion()
|
||||
else:
|
||||
self._params = ''
|
||||
|
||||
if self.name.startswith("client.") and unit:
|
||||
self.args.update(**DEFAULT_KWARGS)
|
||||
|
||||
def _create_help(self):
|
||||
doc = filter(lambda d: d, self.doc)
|
||||
cmd_help = list(filter(
|
||||
lambda x: not x.startswith(":param") and not x.startswith(
|
||||
":return"), doc))
|
||||
return " ".join(cmd_help).strip()
|
||||
|
||||
def _create_arg_help(self):
|
||||
param_dict = {}
|
||||
params = list(filter(lambda d: d.strip().startswith(":param"),
|
||||
self.doc))
|
||||
for param in params:
|
||||
param, help = param.split(":param")[1].strip().split(":")
|
||||
param_dict[param] = help
|
||||
return param_dict
|
||||
|
||||
def create_completion(self):
|
||||
"""
|
||||
Create command completion meta data.
|
||||
|
||||
:return:
|
||||
"""
|
||||
words = {}
|
||||
|
||||
def _create(entry, default):
|
||||
if entry not in ['self', 'kwargs']:
|
||||
if isinstance(default, (int, string_types)):
|
||||
entry += "={}".format(default)
|
||||
return entry
|
||||
|
||||
if IS_PYTHON2:
|
||||
if not self._params.defaults:
|
||||
defaults = [None]*len(self._params.args)
|
||||
else:
|
||||
defaults = list(self._params.defaults)
|
||||
missing = len(self._params.args) - len(defaults)
|
||||
if missing > 1:
|
||||
defaults.extend([None]*missing)
|
||||
defaults.insert(0, None)
|
||||
for arg, default in izip(self._params.args, defaults):
|
||||
entry = _create(arg, default)
|
||||
if entry:
|
||||
entry, meta = self.get_meta(entry)
|
||||
words[entry] = help
|
||||
else:
|
||||
for arg in self._params.values():
|
||||
entry = _create(arg.name, arg.default)
|
||||
if entry:
|
||||
entry, meta = self.get_meta(entry)
|
||||
words[entry] = meta
|
||||
|
||||
return words
|
||||
|
||||
def get_completion(self):
|
||||
"""
|
||||
Gets a list of completions.
|
||||
|
||||
:return:
|
||||
"""
|
||||
return self.args.keys()
|
||||
|
||||
def get_meta(self, cmd):
|
||||
"""
|
||||
Get Meta info of a given command.
|
||||
|
||||
:param cmd: Name of command.
|
||||
:return: Dict containing meta info.
|
||||
"""
|
||||
cmd = cmd.strip()
|
||||
cmd = cmd.split("=")[0].strip()
|
||||
return cmd, self.param_help.get(cmd, '')
|
||||
|
||||
def __str__(self):
|
||||
if self.doc:
|
||||
return "Command {:>50}{:<20}".format(self.name, self.doc)
|
||||
return "Command {}".format(self.name)
|
||||
|
||||
|
||||
def _get_requests(members):
|
||||
commands = list(filter(lambda x: (x[0] not in EXCLUDE
|
||||
and x[0] not in CLIENT_METHODS
|
||||
and callable(x[1])),
|
||||
members))
|
||||
commands = {
|
||||
"client.{}".format(c[0]):
|
||||
Command("client.{}".format(c[0]),
|
||||
argspec(c[1]), inspect.getdoc(c[1]), unit=True)
|
||||
for c in commands if not c[0].startswith("_")
|
||||
}
|
||||
return commands
|
||||
|
||||
|
||||
def _get_client_methods(members):
|
||||
commands = list(filter(lambda x: (x[0] not in EXCLUDE
|
||||
and x[0] in CLIENT_METHODS),
|
||||
members))
|
||||
commands = {
|
||||
"client.{}".format(c[0]):
|
||||
Command("client.{}".format(c[0]),
|
||||
argspec(c[1]), inspect.getdoc(c[1]), unit=False)
|
||||
for c in commands if not c[0].startswith("_")
|
||||
}
|
||||
return commands
|
||||
|
||||
|
||||
def _get_client_properties(members):
|
||||
global CLIENT_ATTRIBUTES
|
||||
commands = list(filter(lambda x: not callable(x[1]), members))
|
||||
commands = {
|
||||
"client.{}".format(c[0]):
|
||||
Command("client.{}".format(c[0]), None, "Read Only!", unit=False)
|
||||
for c in commands if (not c[0].startswith("_")
|
||||
and isinstance(c[1], (string_types, int, float)))
|
||||
}
|
||||
CLIENT_ATTRIBUTES.extend(list(commands.keys()))
|
||||
return commands
|
||||
|
||||
|
||||
def get_commands(client):
|
||||
"""
|
||||
Helper method to retrieve all required methods and attributes of a client \
|
||||
object and convert it to commands.
|
||||
|
||||
:param client: Modbus Client object.
|
||||
:return:
|
||||
"""
|
||||
commands = dict()
|
||||
members = inspect.getmembers(client)
|
||||
requests = _get_requests(members)
|
||||
client_methods = _get_client_methods(members)
|
||||
client_attr = _get_client_properties(members)
|
||||
|
||||
result_commands = inspect.getmembers(Result, predicate=predicate)
|
||||
result_commands = {
|
||||
"result.{}".format(c[0]):
|
||||
Command("result.{}".format(c[0]), argspec(c[1]),
|
||||
inspect.getdoc(c[1]))
|
||||
for c in result_commands if (not c[0].startswith("_")
|
||||
and c[0] != "print_result")
|
||||
}
|
||||
commands.update(requests)
|
||||
commands.update(client_methods)
|
||||
commands.update(client_attr)
|
||||
commands.update(result_commands)
|
||||
return commands
|
||||
|
||||
|
||||
class Result(object):
|
||||
"""
|
||||
Represent result command.
|
||||
"""
|
||||
function_code = None
|
||||
data = None
|
||||
|
||||
def __init__(self, result):
|
||||
"""
|
||||
:param result: Response of a modbus command.
|
||||
"""
|
||||
if isinstance(result, dict): # Modbus response
|
||||
self.function_code = result.pop('function_code', None)
|
||||
self.data = dict(result)
|
||||
else:
|
||||
self.data = result
|
||||
|
||||
def decode(self, formatters, byte_order='big', word_order='big'):
|
||||
"""
|
||||
Decode the register response to known formatters.
|
||||
|
||||
:param formatters: int8/16/32/64, uint8/16/32/64, float32/64
|
||||
:param byte_order: little/big
|
||||
:param word_order: little/big
|
||||
:return: Decoded Value
|
||||
"""
|
||||
# Read Holding Registers (3)
|
||||
# Read Input Registers (4)
|
||||
# Read Write Registers (23)
|
||||
if not isinstance(formatters, (list, tuple)):
|
||||
formatters = [formatters]
|
||||
|
||||
if self.function_code not in [3, 4, 23]:
|
||||
print_formatted_text(
|
||||
HTML("<red>Decoder works only for registers!!</red>"))
|
||||
return
|
||||
byte_order = (Endian.Little if byte_order.strip().lower() == "little"
|
||||
else Endian.Big)
|
||||
word_order = (Endian.Little if word_order.strip().lower() == "little"
|
||||
else Endian.Big)
|
||||
decoder = BinaryPayloadDecoder.fromRegisters(self.data.get('registers'),
|
||||
byteorder=byte_order,
|
||||
wordorder=word_order)
|
||||
for formatter in formatters:
|
||||
formatter = FORMATTERS.get(formatter)
|
||||
if not formatter:
|
||||
print_formatted_text(
|
||||
HTML("<red>Invalid Formatter - {}"
|
||||
"!!</red>".format(formatter)))
|
||||
return
|
||||
decoded = getattr(decoder, formatter)()
|
||||
self.print_result(decoded)
|
||||
|
||||
def raw(self):
|
||||
"""
|
||||
Return raw result dict.
|
||||
|
||||
:return:
|
||||
"""
|
||||
self.print_result()
|
||||
|
||||
def _process_dict(self, d):
|
||||
new_dict = OrderedDict()
|
||||
for k, v in d.items():
|
||||
if isinstance(v, bytes):
|
||||
v = v.decode('utf-8')
|
||||
elif isinstance(v, dict):
|
||||
v = self._process_dict(v)
|
||||
elif isinstance(v, (list, tuple)):
|
||||
v = [v1.decode('utf-8') if isinstance(v1, bytes) else v1
|
||||
for v1 in v ]
|
||||
new_dict[k] = v
|
||||
return new_dict
|
||||
|
||||
def print_result(self, data=None):
|
||||
"""
|
||||
Prettu print result object.
|
||||
|
||||
:param data: Data to be printed.
|
||||
:return:
|
||||
"""
|
||||
data = data or self.data
|
||||
if isinstance(data, dict):
|
||||
data = self._process_dict(data)
|
||||
elif isinstance(data, (list, tuple)):
|
||||
data = [v.decode('utf-8') if isinstance(v, bytes) else v
|
||||
for v in data]
|
||||
elif isinstance(data, bytes):
|
||||
data = data.decode('utf-8')
|
||||
tokens = list(pygments.lex(json.dumps(data, indent=4),
|
||||
lexer=JsonLexer()))
|
||||
print_formatted_text(PygmentsTokens(tokens))
|
||||