diff --git a/common.py b/common.py index 10eaa7b2..50a4efc0 100644 --- a/common.py +++ b/common.py @@ -1332,7 +1332,7 @@ def normalize_scale(scale, unit): return tuple(scale), unit_title, unit_symbol -def format_since_firmware(device, packet): +def format_since_firmware(device, packet, nbsp='$nbsp;'): since = packet.get_since_firmware() if since == None or since <= [2, 0, 0]: @@ -1347,7 +1347,7 @@ def format_since_firmware(device, packet): else: assert False - return '\n.. versionadded:: {1}.{2}.{3}$nbsp;({0})\n'.format(suffix, *since) + return '\n.. versionadded:: {2}.{3}.{4}{1}({0})\n'.format(suffix, nbsp, *since) def format_constant_default(prefix, constant_group, constant, value): if prefix.endswith('_'): diff --git a/julia/LICENSE b/julia/LICENSE new file mode 100644 index 00000000..0e259d42 --- /dev/null +++ b/julia/LICENSE @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. diff --git a/julia/Project.toml.template b/julia/Project.toml.template new file mode 100644 index 00000000..a67bb1b2 --- /dev/null +++ b/julia/Project.toml.template @@ -0,0 +1,17 @@ +name = "Tinkerforge" +uuid = "4538fa9c-0d4d-4731-8bd7-3b5bd853821f" +authors = ["Jonas Schumacher "] +version = "<>" + +[deps] +Compat = "34da2185-b29b-5c13-b0c7-acf172513d20" +DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" +DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" +Sockets = "6462fe0b-24de-5631-8697-dd941f90decc" +PyCall = "438e738f-606a-5dbb-bf0a-cddfbfd45ab0" + +[extras] +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + +[targets] +test = ["Test"] \ No newline at end of file diff --git a/julia/Tinkerforge.jl b/julia/Tinkerforge.jl new file mode 100644 index 00000000..8d3daff5 --- /dev/null +++ b/julia/Tinkerforge.jl @@ -0,0 +1,16 @@ +module Tinkerforge + +using Sockets +using PyCall +using DataStructures +using DocStringExtensions + +function __init__() + include("src/ip_connection_base.jl") +end + +include("ip_connection.jl") +include("devices/device_display_names.jl") +include("devices/device_factory.jl") + +end diff --git a/julia/bricklet_lcd_20x4_backup.jl b/julia/bricklet_lcd_20x4_backup.jl new file mode 100644 index 00000000..8b2baecb --- /dev/null +++ b/julia/bricklet_lcd_20x4_backup.jl @@ -0,0 +1,447 @@ + + + +export BrickletLCD20x4Config +struct BrickletLCD20x4Config + cursor::Bool + blinking::Bool +end + +export BrickletLCD20x4Identity +struct BrickletLCD20x4Identity + uid::String + connected_uid::String + position::Char + hardware_version::Vector{Integer} + firmware_version::Vector{Integer} + device_identifier::Integer +end + +export BrickletLCD20x4 +""" +20x4 character alphanumeric display with blue backlight +""" +mutable struct BrickletLCD20x4 <: TinkerforgeDevice + replaced::Bool + uid::Union{Integer, Missing} + uid_string::String + ipcon::IPConnection + device_identifier::Integer + device_display_name::String + device_url_part::String + device_identifier_lock::Base.AbstractLock + device_identifier_check::DeviceIdentifierCheck # protected by device_identifier_lock + wrong_device_display_name::String # protected by device_identifier_lock + api_version::Tuple{Integer, Integer, Integer} + registered_callbacks::Dict{Integer, Function} + expected_response_function_id::Union{Integer, Nothing} # protected by request_lock + expected_response_sequence_number::Union{Integer, Nothing} # protected by request_lock + response_queue::DataStructures.Queue{Symbol} + request_lock::Base.AbstractLock + stream_lock::Base.AbstractLock + + callbacks::Dict{Symbol, Integer} + callback_formats::Dict{Symbol, Tuple{Integer, String}} + high_level_callbacks::Dict{Symbol, Integer} + id_definitions::Dict{Symbol, Integer} + constants::Dict{Symbol, Integer} + response_expected::DefaultDict{Symbol, ResponseExpected} + + """ + Creates an object with the unique device ID *uid* and adds it to + the IP Connection *ipcon*. + """ + function BrickletLCD20x4(uid::String, ipcon::IPConnection) + replaced = false + uid_string = uid + device_identifier = 212 + device_display_name = "LCD 20x4 Bricklet" + device_url_part = "lcd_20x4" # internal + device_identifier_lock = Base.ReentrantLock() + device_identifier_check = DEVICE_IDENTIFIER_CHECK_PENDING # protected by device_identifier_lock + wrong_device_display_name = "?" # protected by device_identifier_lock + api_version = (0, 0, 0) + registered_callbacks = Dict{Integer, Function}() + expected_response_function_id = nothing # protected by request_lock + expected_response_sequence_number = nothing # protected by request_lock + response_queue = DataStructures.Queue{Symbol}() + request_lock = Base.ReentrantLock() + stream_lock = Base.ReentrantLock() + + callbacks = Dict{Symbol, Integer}() + callback_formats = Dict{Symbol, Tuple{Integer, String}}() + high_level_callbacks = Dict{Symbol, Integer}() + id_definitions = Dict{Symbol, Integer}() + constants = Dict{Symbol, Integer}() + response_expected = DefaultDict{Symbol, ResponseExpected}(RESPONSE_EXPECTED_INVALID_FUNCTION_ID) + + device = new( + replaced, + missing, + uid_string, + ipcon, + device_identifier, + device_display_name, + device_url_part, + device_identifier_lock, + device_identifier_check, + wrong_device_display_name, + api_version, + registered_callbacks, + expected_response_function_id, + expected_response_sequence_number, + response_queue, + request_lock, + stream_lock, + callbacks, + callback_formats, + high_level_callbacks, + id_definitions, + constants, + response_expected + ) + _initDevice(device) + + device.api_version = (2, 0, 2) + + device.callbacks[:CALLBACK_BUTTON_PRESSED] = 9 + device.callbacks[:CALLBACK_BUTTON_RELEASED] = 10 + + device.id_definitions[:FUNCTION_WRITE_LINE] = 1 + device.id_definitions[:FUNCTION_CLEAR_DISPLAY] = 2 + device.id_definitions[:FUNCTION_BACKLIGHT_ON] = 3 + device.id_definitions[:FUNCTION_BACKLIGHT_OFF] = 4 + device.id_definitions[:FUNCTION_IS_BACKLIGHT_ON] = 5 + device.id_definitions[:FUNCTION_SET_CONFIG] = 6 + device.id_definitions[:FUNCTION_GET_CONFIG] = 7 + device.id_definitions[:FUNCTION_IS_BUTTON_PRESSED] = 8 + device.id_definitions[:FUNCTION_SET_CUSTOM_CHARACTER] = 11 + device.id_definitions[:FUNCTION_GET_CUSTOM_CHARACTER] = 12 + device.id_definitions[:FUNCTION_SET_DEFAULT_TEXT] = 13 + device.id_definitions[:FUNCTION_GET_DEFAULT_TEXT] = 14 + device.id_definitions[:FUNCTION_SET_DEFAULT_TEXT_COUNTER] = 15 + device.id_definitions[:FUNCTION_GET_DEFAULT_TEXT_COUNTER] = 16 + device.id_definitions[:FUNCTION_GET_IDENTITY] = 255 + + + device.response_expected[:FUNCTION_WRITE_LINE] = RESPONSE_EXPECTED_FALSE + device.response_expected[:FUNCTION_CLEAR_DISPLAY] = RESPONSE_EXPECTED_FALSE + device.response_expected[:FUNCTION_BACKLIGHT_ON] = RESPONSE_EXPECTED_FALSE + device.response_expected[:FUNCTION_BACKLIGHT_OFF] = RESPONSE_EXPECTED_FALSE + device.response_expected[:FUNCTION_IS_BACKLIGHT_ON] = RESPONSE_EXPECTED_ALWAYS_TRUE + device.response_expected[:FUNCTION_SET_CONFIG] = RESPONSE_EXPECTED_FALSE + device.response_expected[:FUNCTION_GET_CONFIG] = RESPONSE_EXPECTED_ALWAYS_TRUE + device.response_expected[:FUNCTION_IS_BUTTON_PRESSED] = RESPONSE_EXPECTED_ALWAYS_TRUE + device.response_expected[:FUNCTION_SET_CUSTOM_CHARACTER] = RESPONSE_EXPECTED_FALSE + device.response_expected[:FUNCTION_GET_CUSTOM_CHARACTER] = RESPONSE_EXPECTED_ALWAYS_TRUE + device.response_expected[:FUNCTION_SET_DEFAULT_TEXT] = RESPONSE_EXPECTED_FALSE + device.response_expected[:FUNCTION_GET_DEFAULT_TEXT] = RESPONSE_EXPECTED_ALWAYS_TRUE + device.response_expected[:FUNCTION_SET_DEFAULT_TEXT_COUNTER] = RESPONSE_EXPECTED_FALSE + device.response_expected[:FUNCTION_GET_DEFAULT_TEXT_COUNTER] = RESPONSE_EXPECTED_ALWAYS_TRUE + device.response_expected[:FUNCTION_GET_IDENTITY] = RESPONSE_EXPECTED_ALWAYS_TRUE + + device.callback_formats[:CALLBACK_BUTTON_PRESSED] = (9, "B") + device.callback_formats[:CALLBACK_BUTTON_RELEASED] = (9, "B") + + add_device(ipcon, device) + + return device + end +end + +export write_line +""" + $(SIGNATURES) + +Writes text to a specific line with a specific position. +The text can have a maximum of 20 characters. + +For example: (0, 7, "Hello") will write *Hello* in the middle of the +first line of the display. + +The display uses a special charset that includes all ASCII characters except +backslash and tilde. The LCD charset also includes several other non-ASCII characters, see +the `charset specification `__ +for details. The Unicode example above shows how to specify non-ASCII characters +and how to translate from Unicode to the LCD charset. +""" +function write_line(brick::BrickletLCD20x4, line, position, text) + check_validity(brick) + + line = Int64(line) + position = Int64(position) + text = create_string(text) + + send_request(brick.ipcon, :FUNCTION_WRITE_LINE, (line, position, text), "B B 20s", 0, "") +end + +export clear_display +""" + $(SIGNATURES) + +Deletes all characters from the display. +""" +function clear_display(brick::BrickletLCD20x4) + check_validity(brick) + + + send_request(brick.ipcon, :FUNCTION_CLEAR_DISPLAY, (), "", 0, "") +end + +export backlight_on +""" + $(SIGNATURES) + +Turns the backlight on. +""" +function backlight_on(brick::BrickletLCD20x4) + check_validity(brick) + + + send_request(brick, :FUNCTION_BACKLIGHT_ON, (), "", 0, "") +end + +export backlight_off +""" + $(SIGNATURES) + +Turns the backlight off. +""" +function backlight_off(brick::BrickletLCD20x4) + check_validity(brick) + + + send_request(brick, :FUNCTION_BACKLIGHT_OFF, (), "", 0, "") +end + +export is_backlight_on +""" + $(SIGNATURES) + +Returns *true* if the backlight is on and *false* otherwise. +""" +function is_backlight_on(brick::BrickletLCD20x4) + check_validity(brick) + + + return send_request(brick.ipcon, :FUNCTION_IS_BACKLIGHT_ON, (), "", 9, "!") +end + +export set_config +""" + $(SIGNATURES) + +Configures if the cursor (shown as "_") should be visible and if it +should be blinking (shown as a blinking block). The cursor position +is one character behind the the last text written with +:func:`Write Line`. +""" +function set_config(brick::BrickletLCD20x4, cursor, blinking) + check_validity(brick) + + cursor = bool(cursor) + blinking = bool(blinking) + + send_request(brick.ipcon, :FUNCTION_SET_CONFIG, (cursor, blinking), "! !", 0, "") +end + +export get_config +""" + $(SIGNATURES) + +Returns the configuration as set by :func:`Set Config`. +""" +function get_config(brick::BrickletLCD20x4) + check_validity(brick) + + + return GetConfig(send_request(brick.ipcon, :FUNCTION_GET_CONFIG, (), "", 10, "! !")) +end + +export is_button_pressed +""" + $(SIGNATURES) + +Returns *true* if the button (0 to 2 or 0 to 3 since hardware version 1.2) +is pressed. + +If you want to react on button presses and releases it is recommended to use +the :cb:`Button Pressed` and :cb:`Button Released` callbacks. +""" +function is_button_pressed(brick::BrickletLCD20x4, button) + check_validity(brick) + + button = int(button) + + return send_request(brick.ipcon, :FUNCTION_IS_BUTTON_PRESSED, (button,), "B", 9, "!") +end + +export set_custom_character +""" + $(SIGNATURES) + +The LCD 20x4 Bricklet can store up to 8 custom characters. The characters +consist of 5x8 pixels and can be addressed with the index 0-7. To describe +the pixels, the first 5 bits of 8 bytes are used. For example, to make +a custom character "H", you should transfer the following: + +* ``character[0] = 0b00010001`` (decimal value 17) +* ``character[1] = 0b00010001`` (decimal value 17) +* ``character[2] = 0b00010001`` (decimal value 17) +* ``character[3] = 0b00011111`` (decimal value 31) +* ``character[4] = 0b00010001`` (decimal value 17) +* ``character[5] = 0b00010001`` (decimal value 17) +* ``character[6] = 0b00010001`` (decimal value 17) +* ``character[7] = 0b00000000`` (decimal value 0) + +The characters can later be written with :func:`Write Line` by using the +characters with the byte representation 8 ("\\\\x08" or "\\\\u0008") to 15 +("\\\\x0F" or "\\\\u000F"). + +You can play around with the custom characters in Brick Viewer version +since 2.0.1. + +Custom characters are stored by the LCD in RAM, so they have to be set +after each startup. + +.. versionadded:: 2.0.1\$nbsp;(Plugin) +""" +function set_custom_character(brick::BrickletLCD20x4, index, character) + check_validity(brick) + + index = int(index) + character = list(map(int, character)) + + send_request(brick.ipcon, :FUNCTION_SET_CUSTOM_CHARACTER, (index, character), "B 8B", 0, "") +end + +export get_custom_character +""" + $(SIGNATURES) + +Returns the custom character for a given index, as set with +:func:`Set Custom Character`. + +.. versionadded:: 2.0.1\$nbsp;(Plugin) +""" +function get_custom_character(brick::BrickletLCD20x4, index) + check_validity(brick) + + index = int(index) + + return send_request(brick.ipcon, :FUNCTION_GET_CUSTOM_CHARACTER, (index,), "B", 16, "8B") +end + +export set_default_text +""" + $(SIGNATURES) + +Sets the default text for lines 0-3. The max number of characters +per line is 20. + +The default text is shown on the LCD, if the default text counter +expires, see :func:`Set Default Text Counter`. + +.. versionadded:: 2.0.2\$nbsp;(Plugin) +""" +function set_default_text(brick::BrickletLCD20x4, line, text) + check_validity(brick) + + line = int(line) + text = create_string(text) + + send_request(brick.ipcon, :FUNCTION_SET_DEFAULT_TEXT, (line, text), "B 20s", 0, "") +end + +export get_default_text +""" + $(SIGNATURES) + +Returns the default text for a given line (0-3) as set by +:func:`Set Default Text`. + +.. versionadded:: 2.0.2\$nbsp;(Plugin) +""" +function get_default_text(brick::BrickletLCD20x4, line) + check_validity(brick) + + line = int(line) + + return send_request(brick.ipcon, :FUNCTION_GET_DEFAULT_TEXT, (line,), "B", 28, "20s") +end + +export set_default_text_counter +""" + $(SIGNATURES) + +Sets the default text counter. This counter is decremented each +ms by the LCD firmware. If the counter reaches 0, the default text +(see :func:`Set Default Text`) is shown on the LCD. + +This functionality can be used to show a default text if the controlling +program crashes or the connection is interrupted. + +A possible approach is to call :func:`Set Default Text Counter` every +minute with the parameter 1000*60*2 (2 minutes). In this case the +default text will be shown no later than 2 minutes after the +controlling program crashes. + +A negative counter turns the default text functionality off. + +.. versionadded:: 2.0.2\$nbsp;(Plugin) +""" +function set_default_text_counter(brick::BrickletLCD20x4, counter) + check_validity(brick) + + counter = int(counter) + + send_request(brick.ipcon, :FUNCTION_SET_DEFAULT_TEXT_COUNTER, (counter,), "i", 0, "") +end + +export get_default_text_counter +""" + $(SIGNATURES) + +Returns the current value of the default text counter. + +.. versionadded:: 2.0.2\$nbsp;(Plugin) +""" +function get_default_text_counter(brick::BrickletLCD20x4) + check_validity(brick) + + + return send_request(brick.ipcon, :FUNCTION_GET_DEFAULT_TEXT_COUNTER, (), "", 12, "i") +end + +export get_identity +""" + $(SIGNATURES) + +Returns the UID, the UID where the Bricklet is connected to, +the position, the hardware and firmware version as well as the +device identifier. + +The position can be 'a', 'b', 'c', 'd', 'e', 'f', 'g' or 'h' (Bricklet Port). +A Bricklet connected to an :ref:`Isolator Bricklet ` is always at +position 'z'. + +The device identifier numbers can be found :ref:`here `. +|device_identifier_constant| +""" +function get_identity(brick::BrickletLCD20x4) + + + return GetIdentity(send_request(brick.ipcon, :FUNCTION_GET_IDENTITY, (), "", 33, "8s 8s c 3B 3B H")) +end + +export register_callback +""" +Registers the given *function* with the given *callback_id*. +""" +function register_callback(device::BrickletLCD20x4, callback_id, function_) + if isnothing(function_) + device.registered_callbacks.pop(callback_id, None) + else + device.registered_callbacks[callback_id] = function_ + end +end diff --git a/julia/changelog.txt b/julia/changelog.txt new file mode 100644 index 00000000..b12a6187 --- /dev/null +++ b/julia/changelog.txt @@ -0,0 +1,337 @@ +2011-11-20: 1.0.0 (a83b9e5) +- Initial version + +2011-12-13: 1.0.1 (6035526) +- Add callback thread to IPConnection (allows to call getters in callbacks) + +2011-12-29: 1.0.2 (c5f4962) +- Add __init__.py to source/tinkerforge/ + +2012-01-02: 1.0.3 (eb369d3) +- Fix thread exception at shutdown + +2012-02-15: 1.0.4 (ffd64f7) +- Add support for IMU Brick, Analog In Bricklet and Analog Out Bricklet + +2012-03-30: 1.0.5 (71aaa0a) +- Remove Python 3.2 bug (no decode function for str in 3.2) + +2012-04-27: 1.0.6 (6f0b9a5) +- Add sync rect support to Stepper Brick bindings + +2012-05-10: 1.0.7 (baa8705) +- Add version information to tinkerforge.egg +- Silently ignore messages from devices with unknown stack ID +- Don't generate register_callback method for devices without callbacks +- Add inline code documentation + +2012-05-15: 1.0.8 (ff4cc5b) +- Fix relative import and str packing problem with Python 3 + +2012-05-18: 1.0.9 (505bc29) +- Ensure that the answering device matches the expected type in + IPConnection.add_device + +2012-05-21: 1.0.10 (3406326) +- Fix device name decoding for add_device handling in Python 3 + +2012-05-22: 1.0.11 (368cd78) +- Don't let a thread join itself + +2012-05-24: 1.0.12 (f837011) +- Treat '-' and ' ' as equal in device name check for backward compatibility + +2012-06-15: 1.0.13 (bc93dc5) +- Fix handling of fragmented packets + +2012-06-28: 1.0.14 (3704047) +- Add RS485 support + +2012-06-29: 1.0.15 (55a3238) +- Add chip temperature and reset functions + +2012-07-01: 1.0.16 (d9ecec6) +- Add monoflop functionality to Dual Relay Bricklet API + +2012-07-03: 1.0.17 (afb45cf) +- Add time base, all-data function/callback and state callback to Stepper + Brick API + +2012-07-13: 1.0.18 (6ac52d1) +- Fix direction of get_all_data_period method in Stepper Brick API +- Make add_device thread-safe +- Ensure correct shutdown order of threads + +2012-08-01: 1.0.19 (f86a5f3) +- Fix race condition in add_device method +- Add monoflop functionality to IO-4 and IO-16 Bricklet API + +2012-09-17: 1.0.20 (dd8498f) +- Add WIFI support + +2012-09-26: 1.0.21 (c8c7862) +- Add getter for WIFI buffer status information +- Change WIFI certificate getter/setter to transfer bytes instead of a string +- Add API for setting of WIFI regulatory domain +- Add reconnect functionality to IPConnection (for WIFI Extension) +- Add API for Industrial Bricklets: Digital In 4, Digital Out 4 and Quad Relay +- Trim NUL characters from strings properly + +2012-09-28: 1.0.22 (69e6ae4) +- Add API for Barometer Bricklet + +2012-10-01: 1.0.23 (4454bda) +- Replace Barometer Bricklet calibrate function with getter/setter for + reference air pressure + +2012-10-12: 1.0.24 (5884dd5) +- Add get_usb_voltage function to Master Brick API +- Add Barometer Bricklet examples +- Handle difference between currentThread and current_thread to support + Python 2.5 +- Changed callback queue from class variable to instance variable + +2012-12-20: 1.0.25 (2b39606) +- Add API for Voltage/Current Bricklet +- Add API for GPS Bricklet + +2013-01-22: 2.0.0 (10c72f9) +- Add compatibility for Protocol 2.0 + +2013-01-25: 2.0.1 (13b1beb) +- Add support for custom characters in LCD Bricklets + +2013-01-31: 2.0.2 (47579a1) +- Fix char list packing in Python 3 + +2013-02-06: 2.0.3 (3db31c0) +- Add get/set_long_wifi_key functions to Master Brick API + +2013-02-19: 2.0.4 (3fd93d3) +- Reduce scope of request and socket lock to improve concurrency +- Improve and unify code for response expected flag handling +- Add get/set_wifi_hostname functions and callbacks for stack/USB voltage and + stack current to Master Brick API + +2013-02-22: 2.0.5 (9d5de14) +- Add get/set_range functions to Analog In Bricklet API +- Fix unlikely race condition in response packet handling +- Fix serialization of Unicode strings + +2013-04-02: 2.0.6 (eeb1f67) +- Add enable/disable functions for POSITION_REACHED and VELOCITY_REACHED + callbacks to Servo Brick API +- Add get/set_i2c_mode (100kHz/400kHz) functions to Temperature Bricklet API +- Add default text functions to LCD 20x4 Bricklet API +- Don't dispatch callbacks after disconnect +- Fix race condition in callback handling that could result in closing the + wrong socket +- Don't ignore socket errors when sending request packets +- Send a request packet at least every 10sec to improve WIFI disconnect + detection + +2013-05-14: 2.0.7 (b847401) +- Add Ethernet Extension support to Master Brick API +- Only send disconnect probe if there was no packet send or received for 5sec +- Fix deserialization of chars in Python 3 +- Add IMU Brick orientation and Barometer Bricklet averaging API + +2013-07-04: 2.0.8 (cdc19b0) +- Add support for PTC Bricklet and Industrial Dual 0-20mA Bricklet + +2013-08-23: 2.0.9 (4b2c2d2) +- Avoid race condition between disconnect probe thread and disconnect function + +2013-08-28: 2.0.10 (2251328) +- Add edge counters to Industrial Digital In 4, IO-4 and IO-16 Bricklet +- Make averaging length configurable for Analog In Bricklet + +2013-09-11: 2.0.11 (405931f) +- Fix signature of edge count functions in IO-16 Bricklet API + +2013-11-27: 2.0.12 (a97b7db) +- Add support for Distance US, Dual Button, Hall Effect, LED Strip, Line, + Moisture, Motion Detector, Multi Touch, Piezo Speaker, Remote Switch, + Rotary Encoder, Segment Display 4x7, Sound Intensity and Tilt Bricklet + +2013-12-19: 2.0.13 (9334f91) +- Add get/set_clock_frequency function to LED Strip Bricklet API +- Fix mixup of get/set_date_time_callback_period and + get/set_motion_callback_period in GPS Bricklet API +- Support addressing types of Intertechno and ELRO Home Easy devices in + Remote Switch Bricklet API + +2014-04-08: 2.1.0 (9124f8e) +- Add authentication support to IPConnection and Master Brick API + +2014-07-03: 2.1.1 (cdb00f1) +- Add support for WS2811 and WS2812 to LED Strip Bricklet API + +2014-08-11: 2.1.2 (a87f5bc) +- Add support for Color, NFC/RFID and Solid State Relay Bricklet +- Get rid of the egg and easy_install, use setuptools directly or pip instead + +2014-12-10: 2.1.3 (2718ddc) +- Handle EINTR error in receive loop + +2014-12-10: 2.1.4 (27725d5) +- Add support for RED Brick + +2015-07-28: 2.1.5 (725ccd3) +- Fix packing of Unicode chars +- Add DEVICE_DISPLAY_NAME constant to all Device classes +- Add functions for all Bricks to turn status LEDs on and off +- Avoid possible connection state race condition on connect +- Add support for IMU Brick 2.0, Accelerometer, Ambient Light 2.0, + Analog In 2.0, Analog Out 2.0, Dust Detector, Industrial Analog Out, + Industrial Dual Analog In, Laser Range Finder, Load Cell and RS232 Bricklet + +2015-11-17: 2.1.6 (158f00f) +- Add missing constant for 19200 baud to RS232 Bricklet API +- Add ERROR callback to RS232 Bricklet API +- Add set_break_condition function to RS232 Bricklet API +- Add unlimited illuminance range constant to Ambient Light Bricklet 2.0 API +- Break API to fix threshold min/max type mismatch in Ambient Light, Analog In + (2.0), Distance IR/US, Humidity, Linear Poti and Voltage Bricklet API +- Break API to fix bool return type mismatch in Servo Brick + (is_position_reached_callback_enabled and is_velocity_reached_callback_enabled + function), Accelerometer Bricklet (is_led_on function) and Load Cell Bricklet + (is_led_on function) API +- Don't decode non-ASCII strings and chars in Python 3 + +2016-01-06: 2.1.7 (3ade121) +- Add support for CO2, OLED 64x48 and 128x64, Thermocouple and UV Light Bricklet + +2016-02-09: 2.1.8 (5552d2c) +- Add support for Real-Time Clock Bricklet +- Break GPS Bricklet API to fix types of altitude and geoidal separation values + (get_altitude function and ALTITUDE callback) + +2016-06-29: 2.1.9 (9db7daa) +- Add support for WIFI Extension 2.0 to Master Brick API +- Add support for CAN Bricklet and RGB LED Bricklet +- Add DATETIME and ALARM callbacks to Real-Time Clock Bricklet API +- Avoid long/unbound connection timeout + +2016-09-08: 2.1.10 (2863e14) +- Add support for RGBW LEDs, channel mapping and SK6812RGBW (NeoPixel RGBW), + LPD8806 and ADA102 (DotStar) chip types to LED Strip Bricklet API + +2017-01-25: 2.1.11 (7aeee37) +- Add support for WIFI Extension 2.0 Mesh mode to Master Brick API +- Add get/set_status_led_config functions to Motion Detector Bricklet API +- Add sensor and fusion mode configuration functions to IMU Brick 2.0 API +- Fix enumerate callback unregistration + +2017-04-21: 2.1.12 (044bd9b) +- Add support for Silent Stepper Brick +- Add get/set_configuration functions to Laser Range Finder Bricklet API to + support Bricklets with LIDAR-Lite sensor hardware version 3 +- Add get_send_timeout_count function to all Brick APIs +- Avoid that the disconnect function can block on Windows for several seconds + +2017-05-11: 2.1.13 (3960b4a) +- Add support for GPS Bricklet 2.0 + +2017-07-26: 2.1.14 (fb903dc) +- Add support for RS485 Bricklet +- Add general streaming support +- Add SPITFP configuration and diagnostics functions to all Brick APIs to + configure and debug the communication between Bricks and Co-MCU Bricklets +- Remove unused get_current_consumption function from Silent Stepper Brick API +- Increase minimum Python version to 2.6 + +2017-11-20: 2.1.15 (f235e3f) +- Add support for DMX, Humidity 2.0, Motorized Linear Poti, RGB LED Button, + RGB LED Matrix and Thermal Imaging Bricklet +- Add get/set_sbas_config functions to GPS Bricklet 2.0 API +- Accept wider range of types for char (str, unicode, bytes, bytearray with + length 1 and int) and list of char / string (str, unicode, bytes, bytearray + and list of char), all type conversion is done with ord / chr + +2018-02-28: 2.1.16 (da741b9) +- Add support for Analog In 3.0, Remote Switch 2.0, Motion Detector 2.0, NFC, + Rotary Encoder 2.0, Solid State 2.0, Temperature IR 2.0 and Outdoor Weather + Bricklet + +2018-06-08: 2.1.17 (8fb62e4) +- Add support for CAN 2.0, Industrial Counter, Industrial Digital In 4 2.0, + Industrial Dual Relay, Industrial Quad Relay 2.0, IO-4 2.0, LED Strip 2.0, + Load Cell 2.0, Particulate Matter, PTC 2.0, Real-Time Clock 2.0, RS232 2.0, + Sound Pressure Level, Thermocouple 2.0 and Voltage/Current 2.0 Bricklet +- Add get/set_maximum_timeout functions to NFC Bricklet API +- Add is_sensor_connected function and SENSOR_CONNECTED callback to PTC Bricklet API +- Break Humidity 2.0, Rotary Encoder 2.0 and Temperature IR 2.0 Bricklet API to + fix types for callback threshold min/max configuration + +2018-09-28: 2.1.18 (f7c65f7) +- Add support for Air Quality, Analog Out 3.0, Barometer 2.0, Distance IR 2.0, + Dual Button 2.0, Industrial Analog Out 2.0, Industrial Digital Out 4 2.0, + Industrial Dual 0-20mA 2.0, Industrial Dual Analog In 2.0, IO-16 2.0, Isolator, + LCD 128x64, OLED 128x64 2.0, One Wire, Temperature 2.0 and UV Light 2.0 Bricklet + +2018-10-05: 2.1.19 (e3c6f36) +- Break API to fix moving-average-length type in Distance IR Bricklet 2.0 API + +2018-11-28: 2.1.20 (0e3b130) +- Add get/set_samples_per_second functions to Humidity Bricklet 2.0 API +- Add button, slider, graph and tab functions to LCD 128x64 Bricklet API + +2019-01-29: 2.1.21 (2617875) +- Add support for Accelerometer 2.0 and Ambient Light 3.0 Bricklet + +2019-05-21: 2.1.22 (a3d0573) +- Add support for CO2 2.0, E-Paper 296x128, Hall Effect 2.0, Joystick 2.0, + Laser Range Finder 2.0, Linear Poti 2.0, Piezo Speaker 2.0, RGB LED 2.0 and + Segment Display 4x7 2.0 Bricklet and HAT and HAT Zero Brick +- Add remove_calibration and get/set_background_calibration_duration functions + to Air Quality Bricklet API +- Properly check UIDs and report invalid UIDs + +2019-08-23: 2.1.23 (59d9363) +- Add support for Color 2.0, Compass, Distance US 2.0, Energy Monitor, + Multi Touch 2.0, Rotary Poti 2.0 and XMC1400 Breakout Bricklet +- Add get/set_filter_configuration functions to Accelerometer Bricklet 2.0 API +- Add CONVERSION_TIME constants to Voltage/Current Bricklet 2.0 API + +2019-11-25: 2.1.24 (b1270ba) +- Add set/get_voltages_callback_configuration functions and VOLTAGES callback + to HAT Brick API +- Add set/get_usb_voltage_callback_configuration functions and USB_VOLTAGE + callback to HAT Zero Brick API +- Add set/get_statistics_callback_configuration functions and STATISTICS + callback to Isolator Bricklet API +- Report error if authentication secret contains non-ASCII chars +- Fix some error format strings in IPConnection class + +2020-04-07: 2.1.25 (3dff30a) +- Properly check device-identifier and report mismatch between used API bindings + device type and actual hardware device type +- Fix race condition between device constructor and callback thread +- Add set/get_flux_linear_parameters functions to Thermal Imaging Bricklet API +- Add set/get_frame_readable_callback_configuration functions and FRAME_READABLE + callback to CAN (2.0), RS232 (2.0) and RS485 Bricklet API +- Add set/get_error_occurred_callback_configuration functions and ERROR_OCCURRED + callback to CAN Bricklet 2.0 API +- Add read_frame function to RS232 Bricklet API +- Add write/read_bricklet_plugin functions to all Brick APIs for internal EEPROM + Bricklet flashing +- Add set_bricklet_xmc_flash_config/data and set/get_bricklets_enabled functions + to Master Brick 3.0 API for internal Co-MCU Bricklet bootloader flashing +- Validate response length before unpacking response +- Properly report replaced device objects as non-functional + +2020-05-19: 2.1.26 (9c76b18) +- Add get_all_voltages and set/get_all_voltages_callback_configuration functions + and ALL_VOLTAGES callback to Industrial Dual Analog In Bricklet 2.0 API +- Add set/get_i2c_mode functions to Barometer Bricklet API + +2020-11-02: 2.1.27 (6399602) +- Add support for IMU Bricklet 3.0 and Industrial Dual AC Relay Bricklet + +2021-01-15: 2.1.28 (797d61e) +- Add support for Performance DC Bricklet and Servo Bricklet 2.0 + +2021-05-06: 2.1.29 (7cd6fa2) +- Add GPIO_STATE callback to Performance DC Bricklet API +- Add support for DC 2.0, Industrial PTC and Silent Stepper Bricklet 2.0 diff --git a/julia/example_authenticate.py b/julia/example_authenticate.py new file mode 100644 index 00000000..13c35304 --- /dev/null +++ b/julia/example_authenticate.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +HOST = "localhost" +PORT = 4223 +SECRET = "My Authentication Secret!" + +from tinkerforge.ip_connection import IPConnection + +# Authenticate each time the connection got (re-)established +def cb_connected(connect_reason): + if connect_reason == IPConnection.CONNECT_REASON_REQUEST: + print("Connected by request") + elif connect_reason == IPConnection.CONNECT_REASON_AUTO_RECONNECT: + print("Auto-Reconnect") + + # Authenticate first... + try: + ipcon.authenticate(SECRET) + print("Authentication succeeded") + except: + print("Could not authenticate") + return + + # ...reenable auto reconnect mechanism, as described below... + ipcon.set_auto_reconnect(True) + + # ...then trigger enumerate + ipcon.enumerate() + +# Print incoming enumeration +def cb_enumerate(uid, connected_uid, position, hardware_version, firmware_version, + device_identifier, enumeration_type): + print("UID: " + uid + ", Enumeration Type: " + str(enumeration_type)) + +if __name__ == "__main__": + # Create IPConnection + ipcon = IPConnection() + + # Disable auto reconnect mechanism, in case we have the wrong secret. + # If the authentication is successful, reenable it. + ipcon.set_auto_reconnect(False) + + # Register Connected Callback + ipcon.register_callback(IPConnection.CALLBACK_CONNECTED, cb_connected) + + # Register Enumerate Callback + ipcon.register_callback(IPConnection.CALLBACK_ENUMERATE, cb_enumerate) + + # Connect to brickd + ipcon.connect(HOST, PORT) + + input("Press key to exit\n") # Use raw_input() in Python 2 + ipcon.disconnect() diff --git a/julia/example_enumerate.py b/julia/example_enumerate.py new file mode 100644 index 00000000..21a5174e --- /dev/null +++ b/julia/example_enumerate.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +HOST = "localhost" +PORT = 4223 + +from tinkerforge.ip_connection import IPConnection + +# Print incoming enumeration +def cb_enumerate(uid, connected_uid, position, hardware_version, firmware_version, + device_identifier, enumeration_type): + print("UID: " + uid) + print("Enumeration Type: " + str(enumeration_type)) + + if enumeration_type == IPConnection.ENUMERATION_TYPE_DISCONNECTED: + print("") + return + + print("Connected UID: " + connected_uid) + print("Position: " + position) + print("Hardware Version: " + str(hardware_version)) + print("Firmware Version: " + str(firmware_version)) + print("Device Identifier: " + str(device_identifier)) + print("") + +if __name__ == "__main__": + # Create connection and connect to brickd + ipcon = IPConnection() + ipcon.connect(HOST, PORT) + + # Register Enumerate Callback + ipcon.register_callback(IPConnection.CALLBACK_ENUMERATE, cb_enumerate) + + # Trigger Enumerate + ipcon.enumerate() + + input("Press key to exit\n") # Use raw_input() in Python 2 + ipcon.disconnect() diff --git a/julia/generate_julia_bindings.py b/julia/generate_julia_bindings.py new file mode 100644 index 00000000..882aab3b --- /dev/null +++ b/julia/generate_julia_bindings.py @@ -0,0 +1,895 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Julia Bindings Generator +Copyright (C) 2020 Jonas Schumacher + +generate_julia_bindings.py: Generator for Julia bindings + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public +License along with this program; if not, write to the +Free Software Foundation, Inc., 59 Temple Place - Suite 330, +Boston, MA 02111-1307, USA. +""" + +import sys + +if sys.hexversion < 0x3040000: + print('Python >= 3.4 required') + sys.exit(1) + +import os +import importlib.util +import importlib.machinery +from pprint import pprint + +def create_generators_module(): + generators_dir = os.path.split(os.path.dirname(os.path.realpath(__file__)))[0] + + if sys.hexversion < 0x3050000: + generators_module = importlib.machinery.SourceFileLoader('generators', os.path.join(generators_dir, '__init__.py')).load_module() + else: + generators_spec = importlib.util.spec_from_file_location('generators', os.path.join(generators_dir, '__init__.py')) + generators_module = importlib.util.module_from_spec(generators_spec) + + generators_spec.loader.exec_module(generators_module) + + sys.modules['generators'] = generators_module + +if 'generators' not in sys.modules: + create_generators_module() + +from generators import common +from generators.julia import julia_common + +class JuliaBindingsDevice(julia_common.JuliaDevice): + def get_julia_import(self): + template = """ + +""" + + if not self.is_released(): + released = '\n#### __DEVICE_IS_NOT_RELEASED__ ####\n' + else: + released = '' + + return template.format(self.get_generator().get_header_comment('hash'), + released) + + def get_julia_namedtuples(self): + tuples = '' + template = """ +export {struct_name}{name_tup} +struct {struct_name}{name_tup} + {params} +end +""" + + for packet in self.get_packets('function'): + if len(packet.get_elements(direction='out')) < 2: + continue + + name = packet.get_name() + + if name.space.startswith('Get '): + name_tup = name.camel[3:] + else: + name_tup = name.camel + + params = [] + + for element in packet.get_elements(direction='out'): + params.append("{0}::{1}".format(element.get_name().under, element.get_julia_type())) + + tuples += template.format(name=name.camel, name_tup=name_tup, params="\n ".join(params), struct_name=self.get_julia_struct_name()) + + for packet in self.get_packets('function'): + if not packet.has_high_level(): + continue + + if len(packet.get_elements(direction='out', high_level=True)) < 2: + continue + + name = packet.get_name(skip=-2) + + if name.space.startswith('Get '): + name_tup = name.camel[3:] + else: + name_tup = name.camel + + params = [] + + for element in packet.get_elements(direction='out', high_level=True): + params.append("{0}::{1}".format(element.get_name().under, element.get_julia_type())) + + tuples += template.format(name=name.camel, name_tup=name_tup, params="\n ".join(params), struct_name=self.get_julia_struct_name()) + + return tuples + + def get_julia_struct(self): + template = """ +export {0} +\"\"\" +{1} +\"\"\" +mutable struct {0} <: TinkerforgeDevice + replaced::Bool + uid::Union{{Integer, Missing}} + uid_string::String + ipcon::IPConnection + device_identifier::Integer + device_display_name::String + device_url_part::String + device_identifier_lock::Base.AbstractLock + device_identifier_check::DeviceIdentifierCheck # protected by device_identifier_lock + wrong_device_display_name::String # protected by device_identifier_lock + api_version::Tuple{{Integer, Integer, Integer}} + registered_callbacks::Dict{{Integer, Function}} + expected_response_function_id::Union{{Symbol, Nothing}} # protected by request_lock + expected_response_sequence_number::Union{{Integer, Nothing}} # protected by request_lock + response_queue::DataStructures.Queue{{Symbol}} + request_lock::Base.AbstractLock + stream_lock::Base.AbstractLock + + callbacks::Dict{{Symbol, Integer}} + callback_formats::Dict{{Symbol, Tuple{{Integer, String}}}} + high_level_callbacks::Dict{{Symbol, Integer}} + id_definitions::Dict{{Symbol, Integer}} + constants::Dict{{Symbol, Union{Integer, String}}} + response_expected::DefaultDict{{Symbol, ResponseExpected}} +""" + + return template.format(self.get_julia_struct_name(), + common.select_lang(self.get_description())) + + def get_julia_callback_id_definitions(self): + callback_ids = '' + template = ' device.callbacks[:CALLBACK_{0}] = {1}\n' + + for packet in self.get_packets('callback'): + callback_ids += template.format(packet.get_name().upper, packet.get_function_id()) + + if self.get_long_display_name() == 'RS232 Bricklet': + callback_ids += ' device.callbacks[:CALLBACK_READ_CALLBACK] = 8 # for backward compatibility\n' + callback_ids += ' device.callbacks[:CALLBACK_ERROR_CALLBACK] = 9 # for backward compatibility\n' + + #callback_ids += '\n' + + for packet in self.get_packets('callback'): + if packet.has_high_level(): + callback_ids += template.format(packet.get_name(skip=-2).upper, -packet.get_function_id()) + + return callback_ids + + def get_julia_function_id_definitions(self): + function_ids = '\n' + template = ' device.id_definitions[:FUNCTION_{0}] = {1}\n' + + for packet in self.get_packets('function'): + function_ids += template.format(packet.get_name().upper, packet.get_function_id()) + + return function_ids + + def get_julia_constants(self): + constant_format = ' device.constants[:{constant_group_name_upper}_{constant_name_upper}] = {constant_value}\n' + + return '\n' + self.get_formatted_constants(constant_format, char_format_func="\"{0}\"".format) + + def get_julia_init_method(self): + template = """ + \"\"\" + Creates an object with the unique device ID *uid* and adds it to + the IP Connection *ipcon*. + \"\"\" + function {0}(uid::String, ipcon::IPConnection) + replaced = false + uid_string = uid + device_identifier = {5} + device_display_name = "{6}" + device_url_part = "{7}" # internal + device_identifier_lock = Base.ReentrantLock() + device_identifier_check = DEVICE_IDENTIFIER_CHECK_PENDING # protected by device_identifier_lock + wrong_device_display_name = "?" # protected by device_identifier_lock + api_version = (0, 0, 0) + registered_callbacks = Dict{{Integer, Function}}() + expected_response_function_id = nothing # protected by request_lock + expected_response_sequence_number = nothing # protected by request_lock + response_queue = DataStructures.Queue{{Symbol}}() + request_lock = Base.ReentrantLock() + stream_lock = Base.ReentrantLock() + + callbacks = Dict{{Symbol, Integer}}() + callback_formats = Dict{{Symbol, Tuple{{Integer, String}}}}() + high_level_callbacks = Dict{{Symbol, Integer}}() + id_definitions = Dict{{Symbol, Integer}}() + constants = Dict{{Symbol, Union{Integer, String}}}() + response_expected = DefaultDict{{Symbol, ResponseExpected}}(RESPONSE_EXPECTED_INVALID_FUNCTION_ID) + + device = new( + replaced, + missing, + uid_string, + ipcon, + device_identifier, + device_display_name, + device_url_part, + device_identifier_lock, + device_identifier_check, + wrong_device_display_name, + api_version, + registered_callbacks, + expected_response_function_id, + expected_response_sequence_number, + response_queue, + request_lock, + stream_lock, + callbacks, + callback_formats, + high_level_callbacks, + id_definitions, + constants, + response_expected + ) + _initDevice(device) + + device.api_version = ({1}, {2}, {3}) + + {4} + return device + end +end +""" + response_expected = '' + + for packet in self.get_packets('function'): + response_expected += ' device.response_expected[:FUNCTION_{1}] = RESPONSE_EXPECTED_{2}\n' \ + .format(self.get_julia_struct_name(), packet.get_name().upper, + packet.get_response_expected().upper()) + + fillins = self.get_julia_callback_id_definitions() + fillins += self.get_julia_function_id_definitions() + fillins += self.get_julia_constants()+'\n' + fillins += common.wrap_non_empty('', response_expected, '\n') + fillins += self.get_julia_callback_formats() + fillins += self.get_julia_high_level_callbacks() + fillins += self.get_julia_add_device() + + return template.format(self.get_julia_struct_name(), + *self.get_api_version(), + fillins, + self.get_device_identifier(), + self.get_long_display_name(), + self.get_name().under) + + def get_julia_callback_formats(self): + callback_formats = '' + template = ' device.callback_formats[:CALLBACK_{1}] = ({2}, "{3}")\n' + + for packet in self.get_packets('callback'): + callback_formats += template.format(self.get_julia_struct_name(), + packet.get_name().upper, + packet.get_response_size(), + packet.get_julia_format_list('out')) + + return callback_formats + '\n' + + def get_julia_high_level_callbacks(self): + high_level_callbacks = '' + template = ' device.high_level_callbacks[:CALLBACK_{1}] = [{4}, Dict("fixed_length" => {2}, "single_chunk" => {3}), nothing]\n' + + for packet in self.get_packets('callback'): + stream = packet.get_high_level('stream_*') + + if stream != None: + roles = [] + + for element in packet.get_elements(direction='out'): + roles.append(element.get_role()) + + high_level_callbacks += template.format(self.get_julia_struct_name(), + packet.get_name(skip=-2).upper, + stream.get_fixed_length(), + stream.has_single_chunk(), + repr(tuple(roles)).replace("'", "\"")) + + return high_level_callbacks + + def get_julia_add_device(self): + return ' add_device(ipcon, device)\n' + + def get_julia_methods(self): + m_tup = """ +export {0} +\"\"\" + $(SIGNATURES) + +{10} +\"\"\" +function {0}(device::{2}{8}{4}) + {11} + {12} + return {1}(send_request(device, :FUNCTION_{3}, ({4}{9}), \"{5}\", {6}, \"{7}\")) +end +""" + m_ret = """ +export {0} +\"\"\" + $(SIGNATURES) + +{9} +\"\"\" +function {0}(device::{1}{7}{3}) + {10} + {11} + return send_request(device, :FUNCTION_{2}, ({3}{8}), \"{4}\", {5}, \"{6}\") +end +""" + m_nor = """ +export {0} +\"\"\" + $(SIGNATURES) + +{7} +\"\"\" +function {0}(device::{1}{5}{3}) + {8} + {9} + send_request(device, :FUNCTION_{2}, ({3}{6}), \"{4}\", 0, \"\") +end +""" + methods = '' + cls = self.get_julia_struct_name() + + # normal and low-level + for packet in self.get_packets('function'): + nb = packet.get_name().camel + ns = packet.get_name().under + nh = ns.upper() + par = packet.get_julia_parameters() + doc = packet.get_julia_formatted_doc() + cp = '' + ct = '' + + if par != '': + cp = ', ' + + if not ',' in par: + ct = ',' + + in_f = packet.get_julia_format_list('in') + out_l = packet.get_response_size() + out_f = packet.get_julia_format_list('out') + + if packet.get_function_id() == 255: # .get_identity + check = '' + else: + check = 'check_validity(device)\n' + + coercions = common.wrap_non_empty('', packet.get_julia_parameter_coercions(), '\n') + out_c = len(packet.get_elements(direction='out')) + + if out_c > 1: + methods += m_tup.format(ns, nb, cls, nh, par, in_f, out_l, out_f, cp, ct, doc, check, coercions) + elif out_c == 1: + methods += m_ret.format(ns, cls, nh, par, in_f, out_l, out_f, cp, ct, doc, check, coercions) + else: + methods += m_nor.format(ns, cls, nh, par, in_f, cp, ct, doc, check, coercions) + + # high-level + template_stream_in = """ +export {function_name} +\"\"\" +{doc} +\"\"\" +function {function_name}(device::{struct_name}{high_level_parameters}) + {coercions} + if length({stream_name_under}) > {stream_max_length} + throw(TinkerforgeInvalidParameterError("{stream_name_space} can be at most {stream_max_length} items long")) + end + + {stream_name_under}_length = length({stream_name_under}) + {stream_name_under}_chunk_offset = 0 + + if {stream_name_under}_length == 0 + {stream_name_under}_chunk_data = [{chunk_padding}] * {chunk_cardinality} + ret = {function_name}_low_level(device, {parameters}) + else + lock(device.stream_lock) do + while {stream_name_under}_chunk_offset < {stream_name_under}_length + {stream_name_under}_chunk_data = create_chunk_data({stream_name_under}, {stream_name_under}_chunk_offset, {chunk_cardinality}, {chunk_padding}) + ret = {function_name}_low_level(device, {parameters}) + {stream_name_under}_chunk_offset += {chunk_cardinality} + end + end + end +{result} +end +""" + template_stream_in_fixed_length = """ +export {function_name} +\"\"\" +{doc} +\"\"\" +function {function_name}(device::{struct_name}{high_level_parameters}) + {coercions} + {stream_name_under}_length = {fixed_length} + {stream_name_under}_chunk_offset = 0 + + if length({stream_name_under}) != {stream_name_under}_length + throw(TinkerforgeInvalidParameterError("{stream_name_space} can be at most ${stream_name_under}_length items long")) + end + + lock(device.stream_lock) do + while {stream_name_under}_chunk_offset < {stream_name_under}_length + {stream_name_under}_chunk_data = create_chunk_data({stream_name_under}, {stream_name_under}_chunk_offset, {chunk_cardinality}, {chunk_padding}) + ret = {function_name}_low_level(device, {parameters}) + {stream_name_under}_chunk_offset += {chunk_cardinality} + end + end +{result} +end +""" + template_stream_in_result = """ + return ret""" + template_stream_in_namedtuple_result = """ + return {result_camel_name}(*ret)""" + template_stream_in_short_write = """ +export {function_name} +\"\"\" +{doc} +\"\"\" +function {function_name}(device::{struct_name}{high_level_parameters}) + {coercions} + if length({stream_name_under}) > {stream_max_length} + throw(TinkerforgeInvalidParameterError("{stream_name_space} can be at most {stream_max_length} items long")) + end + + {stream_name_under}_length = length({stream_name_under}) + {stream_name_under}_chunk_offset = 0 + + if {stream_name_under}_length == 0 + {stream_name_under}_chunk_data = [{chunk_padding}] * {chunk_cardinality} + ret = {function_name}_low_level(device, {parameters}) + {chunk_written_0} + else + {stream_name_under}_written = 0 + + lock(device.stream_lock) do + while {stream_name_under}_chunk_offset < {stream_name_under}_length + {stream_name_under}_chunk_data = create_chunk_data({stream_name_under}, {stream_name_under}_chunk_offset, {chunk_cardinality}, {chunk_padding}) + ret = {function_name}_low_level(device, {parameters}) + {chunk_written_n} + + if {chunk_written_test} < {chunk_cardinality} + break # either last chunk or short write + end + + {stream_name_under}_chunk_offset += {chunk_cardinality} + end + end + end +{result} +end +""" + template_stream_in_short_write_chunk_written = ['{stream_name_under}_written = ret', + '{stream_name_under}_written += ret', + 'ret'] + template_stream_in_short_write_namedtuple_chunk_written = ['{stream_name_under}_written = ret.{stream_name_under}_chunk_written', + '{stream_name_under}_written += ret.{stream_name_under}_chunk_written', + 'ret.{stream_name_under}_chunk_written'] + template_stream_in_short_write_result = """ + return {stream_name_under}_written""" + template_stream_in_short_write_namedtuple_result = """ + return {result_camel_name}({result_fields})""" + template_stream_in_single_chunk = """ +export {function_name} +\"\"\" +{doc} +\"\"\" +function {function_name}(device::{struct_name}{high_level_parameters}) + {coercions} + {stream_name_under}_length = length({stream_name_under}) + {stream_name_under}_data = list({stream_name_under}) # make a copy so we can potentially extend it + + if {stream_name_under}_length > {chunk_cardinality} + throw(TinkerforgeInvalidParameterError("{stream_name_space} can be at most {chunk_cardinality} items long")) + end + + if {stream_name_under}_length < {chunk_cardinality} + {stream_name_under}_data += [{chunk_padding}] * ({chunk_cardinality} - {stream_name_under}_length) + end +{result} +end +""" + template_stream_in_single_chunk_result = """ + return {function_name}_low_level(device, {parameters})""" + template_stream_in_single_chunk_namedtuple_result = """ + return {result_camel_name}({function_name}_low_level(device, {parameters})...)""" + template_stream_out = """ +export {function_name} +\"\"\" +{doc} +\"\"\" +function {function_name}(device::{struct_name}{high_level_parameters}) + {coercions}{fixed_length} + lock(device.stream_lock) do + ret = {function_name}_low_level(device, {parameters}){dynamic_length_3} + {chunk_offset_check}{stream_name_under}_out_of_sync = ret.{stream_name_under}_chunk_offset != 0 + {closing_end} + {chunk_offset_check_indent}{stream_name_under}_data = ret.{stream_name_under}_chunk_data + + while !{stream_name_under}_out_of_sync && length({stream_name_under}_data) < {stream_name_under}_length + ret = {function_name}_low_level(device, {parameters}){dynamic_length_4} + {stream_name_under}_out_of_sync = ret.{stream_name_under}_chunk_offset != length({stream_name_under}_data) + {stream_name_under}_data += ret.{stream_name_under}_chunk_data + end + + if {stream_name_under}_out_of_sync # discard remaining stream to bring it back in-sync + while ret.{stream_name_under}_chunk_offset + {chunk_cardinality} < {stream_name_under}_length + ret = {function_name}_low_level(device, {parameters}){dynamic_length_5} + end + + throw(TinkerforgeStreamOutOfSyncError("{stream_name_space} stream is out-of-sync")) + end + end +{result} +end +""" + template_stream_out_fixed_length = """ + {stream_name_under}_length = {fixed_length} +""" + template_stream_out_dynamic_length = """ +{{indent}}{stream_name_under}_length = ret.{stream_name_under}_length""" + template_stream_out_chunk_offset_check = """ + if ret.{stream_name_under}_chunk_offset == (1 << {shift_size}) - 1 # maximum chunk offset -> stream has no data + {stream_name_under}_length = 0 + {stream_name_under}_out_of_sync = false + {stream_name_under}_data = () + else + """ + template_stream_out_single_chunk = """ +export {function_name} +\"\"\" +{doc} +\"\"\" +function {function_name}(device::{struct_name}{high_level_parameters}) + {coercions} + ret = {function_name}_low_level(device, {parameters}) +{result} +end +""" + template_stream_out_result = """ + return {stream_name_under}_data[:{stream_name_under}_length]""" + template_stream_out_single_chunk_result = """ + return ret.{stream_name_under}_data[:ret.{stream_name_under}_length]""" + template_stream_out_namedtuple_result = """ + return {result_name}({result_fields})""" + + for packet in self.get_packets('function'): + stream_in = packet.get_high_level('stream_in') + stream_out = packet.get_high_level('stream_out') + + if stream_in != None: + if stream_in.get_fixed_length() != None: + template = template_stream_in_fixed_length + elif stream_in.has_short_write() and stream_in.has_single_chunk(): + # the single chunk template also covers short writes + template = template_stream_in_single_chunk + elif stream_in.has_short_write(): + template = template_stream_in_short_write + elif stream_in.has_single_chunk(): + template = template_stream_in_single_chunk + else: + template = template_stream_in + + if stream_in.has_short_write(): + if len(packet.get_elements(direction='out')) < 2: + chunk_written_0 = template_stream_in_short_write_chunk_written[0].format(stream_name_under=stream_in.get_name().under) + chunk_written_n = template_stream_in_short_write_chunk_written[1].format(stream_name_under=stream_in.get_name().under) + chunk_written_test = template_stream_in_short_write_chunk_written[2].format(stream_name_under=stream_in.get_name().under) + else: + chunk_written_0 = template_stream_in_short_write_namedtuple_chunk_written[0].format(stream_name_under=stream_in.get_name().under) + chunk_written_n = template_stream_in_short_write_namedtuple_chunk_written[1].format(stream_name_under=stream_in.get_name().under) + chunk_written_test = template_stream_in_short_write_namedtuple_chunk_written[2].format(stream_name_under=stream_in.get_name().under) + + if len(packet.get_elements(direction='out', high_level=True)) < 2: + if stream_in.has_single_chunk(): + result = template_stream_in_single_chunk_result.format(function_name=packet.get_name(skip=-2).under, + parameters=packet.get_julia_parameters()) + else: + result = template_stream_in_short_write_result.format(stream_name_under=stream_in.get_name().under) + else: + if stream_in.has_single_chunk(): + result = template_stream_in_single_chunk_namedtuple_result.format(function_name=packet.get_name(skip=-2).under, + parameters=packet.get_julia_parameters(), + result_camel_name=packet.get_name(skip=-2).camel) + else: + fields = [] + + for element in packet.get_elements(direction='out', high_level=True): + if element.get_role() == 'stream_written': + fields.append('{0}_written'.format(stream_in.get_name().under)) + else: + fields.append('ret.{0}'.format(element.get_name().under)) + + result = template_stream_in_short_write_namedtuple_result.format(result_camel_name=packet.get_name(skip=-2).camel, + result_fields=', '.join(fields)) + else: + chunk_written_0 = '' + chunk_written_n = '' + chunk_written_test = '' + + if len(packet.get_elements(direction='out', high_level=True)) < 2: + if stream_in.has_single_chunk(): + result = template_stream_in_single_chunk_result.format(function_name=packet.get_name(skip=-2).under, + parameters=packet.get_julia_parameters()) + else: + result = template_stream_in_result + else: + if stream_in.has_single_chunk(): + result = template_stream_in_single_chunk_namedtuple_result.format(function_name=packet.get_name(skip=-2).under, + parameters=packet.get_julia_parameters(), + result_camel_name=packet.get_name(skip=-2).camel) + else: + result = template_stream_in_namedtuple_result.format(result_camel_name=packet.get_name(skip=-2).camel) + + methods += template.format(doc=packet.get_julia_formatted_doc(), + coercions=common.wrap_non_empty('\n ', packet.get_julia_parameter_coercions(high_level=True), '\n'), + function_name=packet.get_name(skip=-2).under, + parameters=packet.get_julia_parameters(), + high_level_parameters=common.wrap_non_empty(', ', packet.get_julia_parameters(high_level=True), ''), + stream_name_space=stream_in.get_name().space, + stream_name_under=stream_in.get_name().under, + stream_max_length=abs(stream_in.get_data_element().get_cardinality()), + fixed_length=stream_in.get_fixed_length(), + chunk_cardinality=stream_in.get_chunk_data_element().get_cardinality(), + chunk_padding=stream_in.get_chunk_data_element().get_julia_default_item_value(), + chunk_written_0=chunk_written_0, + chunk_written_n=chunk_written_n, + chunk_written_test=chunk_written_test, + struct_name=self.get_julia_struct_name(), + #closing_end='end\n' if chunk_offset_check else '', + result=result) + elif stream_out != None: + if stream_out.get_fixed_length() != None: + fixed_length = template_stream_out_fixed_length.format(stream_name_under=stream_out.get_name().under, + fixed_length=stream_out.get_fixed_length()) + dynamic_length = '' + shift_size = int(stream_out.get_chunk_offset_element().get_type().replace('uint', '')) + chunk_offset_check = template_stream_out_chunk_offset_check.format(stream_name_under=stream_out.get_name().under, + shift_size=shift_size) + chunk_offset_check_indent = '' + else: + fixed_length = '' + dynamic_length = template_stream_out_dynamic_length.format(stream_name_under=stream_out.get_name().under) + chunk_offset_check = '' + chunk_offset_check_indent = '' + + if len(packet.get_elements(direction='out', high_level=True)) < 2: + if stream_out.has_single_chunk(): + result = template_stream_out_single_chunk_result.format(stream_name_under=stream_out.get_name().under) + else: + result = template_stream_out_result.format(stream_name_under=stream_out.get_name().under) + else: + fields = [] + + for element in packet.get_elements(direction='out', high_level=True): + if element.get_role() == 'stream_data': + if stream_out.has_single_chunk(): + fields.append('ret.{0}_data[:ret.{0}_length]'.format(stream_out.get_name().under)) + else: + fields.append('{0}_data[:{0}_length]'.format(stream_out.get_name().under)) + else: + fields.append('ret.{0}'.format(element.get_name().under)) + + result = template_stream_out_namedtuple_result.format(result_name=packet.get_name(skip=-2).camel, + result_fields=', '.join(fields)) + + if stream_out.has_single_chunk(): + template = template_stream_out_single_chunk + else: + template = template_stream_out + + methods += template.format(doc=packet.get_julia_formatted_doc(), + coercions=common.wrap_non_empty('\n ', packet.get_julia_parameter_coercions(high_level=True), '\n'), + function_name=packet.get_name(skip=-2).under, + parameters=packet.get_julia_parameters(), + high_level_parameters=common.wrap_non_empty(', ', packet.get_julia_parameters(high_level=True), ''), + stream_name_space=stream_out.get_name().space, + stream_name_under=stream_out.get_name().under, + fixed_length=fixed_length, + dynamic_length_3=dynamic_length.format(indent=' ' * 3), + dynamic_length_4=dynamic_length.format(indent=' ' * 4), + dynamic_length_5=dynamic_length.format(indent=' ' * 5), + chunk_offset_check=chunk_offset_check, + chunk_offset_check_indent=chunk_offset_check_indent, + chunk_cardinality=stream_out.get_chunk_data_element().get_cardinality(), + struct_name=self.get_julia_struct_name(), + closing_end='end\n' if chunk_offset_check != '' else '', + result=result) + + return methods + + def get_julia_register_callback_method(self): + if len(self.get_packets('callback')) == 0: + return '' + + return """ +export register_callback +\"\"\" +Registers the given *function* with the given *callback_id*. +\"\"\" +function register_callback(device::{0}, callback_id, function_) + if isnothing(function_) + device.registered_callbacks.pop(callback_id, None) + else + device.registered_callbacks[callback_id] = function_ + end +end +""".format(self.get_julia_struct_name()) + + def get_julia_old_name(self): + template = """ +{0} = {1} # for backward compatibility +""" + + return ""#template.format(self.get_name().camel, self.get_julia_struct_name()) + + def get_julia_source(self): + source = self.get_julia_import() + source += self.get_julia_namedtuples() + source += self.get_julia_struct() + source += self.get_julia_init_method() + source += self.get_julia_methods() + source += self.get_julia_register_callback_method() + + if self.is_brick() or self.is_bricklet(): + source += self.get_julia_old_name() + + return common.strip_trailing_whitespace(source) + +class JuliaBindingsPacket(julia_common.JuliaPacket): + def get_julia_formatted_doc(self): + text = common.select_lang(self.get_doc_text()) + + def format_parameter(name): + return '``{0}``'.format(name) # FIXME + + text = common.handle_rst_param(text, format_parameter) + text = common.handle_rst_word(text).replace("\\", "\\\\").replace("$", "\\$") + text = common.handle_rst_substitutions(text, self) + text += common.format_since_firmware(self.get_device(), self, nbsp='\\$nbsp;') + + return '\n'.join(text.strip().split('\n')) + + def get_julia_format_list(self, io): + forms = [] + + for element in self.get_elements(direction=io): + forms.append(element.get_julia_struct_format()) + + return ' '.join(forms) + + def get_julia_parameter_coercions(self, high_level=False): + coercions = [] + + for element in self.get_elements(direction='in', high_level=high_level): + name = element.get_name().under + + coercions.append('{0} = {1}'.format(name, element.get_julia_parameter_coercion().format(name))) + + return '\n '.join(coercions) + +class JuliaBindingsGenerator(julia_common.JuliaGeneratorTrait, common.BindingsGenerator): + def get_device_class(self): + return JuliaBindingsDevice + + def get_packet_class(self): + return JuliaBindingsPacket + + def get_element_class(self): + return julia_common.JuliaElement + + def prepare(self): + common.BindingsGenerator.prepare(self) + + self.device_factory_all_classes = [] + self.device_factory_released_classes = [] + self.device_display_names = [] + + def generate(self, device): + filename = '{0}_{1}.jl'.format(device.get_category().under, device.get_name().under) + + with open(os.path.join(self.get_bindings_dir(), filename), 'w') as f: + f.write(device.get_julia_source()) + + self.device_factory_all_classes.append((device.get_julia_import_name(), device.get_julia_struct_name(), device.get_device_identifier())) + + if device.is_released(): + self.device_factory_released_classes.append((device.get_julia_import_name(), device.get_julia_struct_name(), device.get_device_identifier())) + self.device_display_names.append((device.get_device_identifier(), device.get_long_display_name())) + self.released_files.append(filename) + + def finish(self): + template_import = """include("{0}.jl")""" + template = """{0} +{1} + +export get_device_type +function get_device_type(device_identifier::Integer) + device_types = Dict{{Integer, String}}( +{2} + ) + + return device_types[device_identifier] +end + +export create_device +function create_device(device_identifier::Integer, uid::String, ipcon::IPConnection) + return get_device_type(device_identifier)(uid, ipcon) +end +""" + for filename, device_factory_classes in [('device_factory_all.jl', self.device_factory_all_classes), + ('device_factory.jl', self.device_factory_released_classes)]: + imports = [] + classes = [] + + for import_name, class_name, device_identifier in sorted(device_factory_classes): + imports.append(template_import.format(import_name, class_name)) + classes.append(' {0} => "{1}",'.format(device_identifier, class_name)) + + with open(os.path.join(self.get_bindings_dir(), filename), 'w') as f: + f.write(template.format(self.get_header_comment('hash'), + '\n'.join(imports), + '\n'.join(classes))) + + template = """{header} +export get_device_display_name +function get_device_display_name(device_identifier::Integer) + device_display_names = Dict{{Integer, String}}( + {entries} + ) + + try + device_display_name = dict["d"] + catch e + if e isa KeyError + device_display_name = "Unknown Device [{{device_identifier}}]" + end + end + + return device_display_name +end +""" + + entries = [] + + for device_identifier, device_display_name in sorted(self.device_display_names): + entries.append(' {0} => "{1}"'.format(device_identifier, device_display_name)) + + with open(os.path.join(self.get_bindings_dir(), 'device_display_names.jl'), 'w') as f: + f.write(template.format(header=self.get_header_comment('hash'), + entries=',\n '.join(entries))) + + self.released_files.append('device_display_names.jl') + + common.BindingsGenerator.finish(self) + +def generate(root_dir, language, internal): + common.generate(root_dir, language, internal, JuliaBindingsGenerator) + +if __name__ == '__main__': + args = common.dockerize('julia', __file__, add_internal_argument=True) + + generate(os.getcwd(), 'en', args.internal) diff --git a/julia/generate_julia_debian_package.py b/julia/generate_julia_debian_package.py new file mode 100644 index 00000000..c59ae9ff --- /dev/null +++ b/julia/generate_julia_debian_package.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Python Debian Package Generator +Copyright (C) 2020 Matthias Bolte + +generate_python_debian_package.py: Generator for Python Debian Package + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public +License along with this program; if not, write to the +Free Software Foundation, Inc., 59 Temple Place - Suite 330, +Boston, MA 02111-1307, USA. +""" + +import sys + +if sys.hexversion < 0x3040000: + print('Python >= 3.4 required') + sys.exit(1) + +import os +import shutil +import subprocess +import glob +import importlib.util +import importlib.machinery + +def create_generators_module(): + generators_dir = os.path.split(os.path.dirname(os.path.realpath(__file__)))[0] + + if sys.hexversion < 0x3050000: + generators_module = importlib.machinery.SourceFileLoader('generators', os.path.join(generators_dir, '__init__.py')).load_module() + else: + generators_spec = importlib.util.spec_from_file_location('generators', os.path.join(generators_dir, '__init__.py')) + generators_module = importlib.util.module_from_spec(generators_spec) + + generators_spec.loader.exec_module(generators_module) + + sys.modules['generators'] = generators_module + +if 'generators' not in sys.modules: + create_generators_module() + +from generators import common + +def generate(root_dir, language): + version = common.get_changelog_version(root_dir) + debian_dir = os.path.join(root_dir, 'debian') + tmp_dir = os.path.join(root_dir, 'debian_package') + tmp_source_dir = os.path.join(tmp_dir, 'source') + tmp_source_debian_dir = os.path.join(tmp_source_dir, 'debian') + tmp_build_dir = os.path.join(tmp_dir, 'tinkerforge-python-bindings-{0}.{1}.{2}'.format(*version)) + + # Make directories + common.recreate_dir(tmp_dir) + + # Unzip + common.execute(['unzip', + '-q', + os.path.join(root_dir, 'tinkerforge_python_bindings_{0}_{1}_{2}.zip'.format(*version)), + os.path.join('source', '*'), + '-d', + tmp_dir]) + + shutil.copytree(debian_dir, tmp_source_debian_dir) + + common.specialize_template(os.path.join(tmp_source_debian_dir, 'changelog.template'), + os.path.join(tmp_source_debian_dir, 'changelog'), + {'<>': '.'.join(version), + '<>': subprocess.check_output(['date', '-R']).decode('utf-8')}, + remove_template=True) + + # Make package + os.rename(tmp_source_dir, tmp_build_dir) + + with common.ChangedDirectory(tmp_build_dir): + common.execute(['dpkg-buildpackage', + '--no-sign']) + + # Check package + with common.ChangedDirectory(tmp_dir): + common.execute(['lintian', '--pedantic'] + glob.glob('*.deb')) + +if __name__ == '__main__': + common.dockerize('python', __file__) + + generate(os.getcwd(), 'en') diff --git a/julia/generate_julia_doc.py b/julia/generate_julia_doc.py new file mode 100644 index 00000000..13f944eb --- /dev/null +++ b/julia/generate_julia_doc.py @@ -0,0 +1,464 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Python Documentation Generator +Copyright (C) 2012-2015, 2017-2020 Matthias Bolte +Copyright (C) 2011-2013 Olaf Lüke + +generate_python_doc.py: Generator for Python documentation + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public +License along with this program; if not, write to the +Free Software Foundation, Inc., 59 Temple Place - Suite 330, +Boston, MA 02111-1307, USA. +""" + +import sys + +if sys.hexversion < 0x3040000: + print('Python >= 3.4 required') + sys.exit(1) + +import os +import importlib.util +import importlib.machinery + +def create_generators_module(): + generators_dir = os.path.split(os.path.dirname(os.path.realpath(__file__)))[0] + + if sys.hexversion < 0x3050000: + generators_module = importlib.machinery.SourceFileLoader('generators', os.path.join(generators_dir, '__init__.py')).load_module() + else: + generators_spec = importlib.util.spec_from_file_location('generators', os.path.join(generators_dir, '__init__.py')) + generators_module = importlib.util.module_from_spec(generators_spec) + + generators_spec.loader.exec_module(generators_module) + + sys.modules['generators'] = generators_module + +if 'generators' not in sys.modules: + create_generators_module() + +from generators import common +from generators.python import python_common + +class PythonDocDevice(python_common.PythonDevice): + def specialize_python_doc_function_links(self, text): + def specializer(packet, high_level): + if packet.get_type() == 'callback': + return ':py:attr:`CALLBACK_{1} <{0}.CALLBACK_{1}>`'.format(packet.get_device().get_python_class_name(), + packet.get_name(skip=-2 if high_level else 0).upper) + else: + return ':py:func:`{1}() <{0}.{1}>`'.format(packet.get_device().get_python_class_name(), + packet.get_name(skip=-2 if high_level else 0).under) + + return self.specialize_doc_rst_links(text, specializer, prefix='py') + + def get_python_examples(self): + def title_from_filename(filename): + filename = filename.replace('example_', '').replace('.py', '') + return common.under_to_space(filename) + + return common.make_rst_examples(title_from_filename, self) + + def get_python_functions(self, type_): + functions = [] + template = '.. py:function:: {0}.{1}({2})\n\n{3}{4}\n' + cls = self.get_python_class_name() + + for packet in self.get_packets('function'): + if packet.get_doc_type() != type_: + continue + + skip = -2 if packet.has_high_level() else 0 + name = packet.get_name(skip=skip).under + params = packet.get_python_parameters(high_level=True) + meta = packet.get_formatted_element_meta(lambda element, cardinality=None: element.get_python_type(cardinality=cardinality), + lambda element, index=None: element.get_python_name(index=index), + return_object='conditional', + no_out_value={'en': 'None', 'de': 'None'}, + explicit_string_cardinality=True, + explicit_variable_stream_cardinality=True, + explicit_fixed_stream_cardinality=True, + explicit_common_cardinality=True, + high_level=True) + meta_table = common.make_rst_meta_table(meta) + desc = packet.get_python_formatted_doc() + + functions.append(template.format(cls, name, params, meta_table, desc)) + + return ''.join(functions) + + def get_python_callbacks(self): + callbacks = [] + template = '.. py:attribute:: {0}.CALLBACK_{1}\n\n{2}{3}\n' + cls = self.get_python_class_name() + + for packet in self.get_packets('callback'): + skip = -2 if packet.has_high_level() else 0 + meta = packet.get_formatted_element_meta(lambda element, cardinality=None: element.get_python_type(cardinality=cardinality), + lambda element, index=None: element.get_python_name(index=index), + no_out_value={'en': 'no parameters', 'de': 'keine Parameter'}, + explicit_string_cardinality=True, + explicit_variable_stream_cardinality=True, + explicit_fixed_stream_cardinality=True, + explicit_common_cardinality=True, + high_level=True) + meta_table = common.make_rst_meta_table(meta) + desc = packet.get_python_formatted_doc() + + callbacks.append(template.format(cls, packet.get_name(skip=skip).upper, meta_table, desc)) + + return ''.join(callbacks) + + def get_python_api(self): + create_str = { + 'en': """ +.. py:function:: {0}(uid, ipcon) + +{2} + + Creates an object with the unique device ID ``uid``: + + .. code-block:: python + + {1} = {0}("YOUR_DEVICE_UID", ipcon) + + This object can then be used after the IP Connection is connected. +""", + 'de': """ +.. py:function:: {0}(uid, ipcon) + +{2} + + Erzeugt ein Objekt mit der eindeutigen Geräte ID ``uid``: + + .. code-block:: python + + {1} = {0}("YOUR_DEVICE_UID", ipcon) + + Dieses Objekt kann benutzt werden, nachdem die IP Connection verbunden ist. +""" + } + + register_str = { + 'en': """ +.. py:function:: {2}{1}.register_callback(callback_id, function) + +{3} + + Registers the given ``function`` with the given ``callback_id``. + + The available callback IDs with corresponding function signatures are listed + :ref:`below <{0}_python_callbacks>`. +""", + 'de': """ +.. py:function:: {2}{1}.register_callback(callback_id, function) + +{3} + + Registriert die ``function`` für die gegebene ``callback_id``. + + Die verfügbaren Callback IDs mit den zugehörigen Funktionssignaturen sind + :ref:`unten <{0}_python_callbacks>` zu finden. +""" + } + + c_str = { + 'en': """ +.. _{0}_python_callbacks: + +Callbacks +^^^^^^^^^ + +Callbacks can be registered to receive +time critical or recurring data from the device. The registration is done +with the :py:func:`register_callback() <{1}.register_callback>` function of +the device object. The first parameter is the callback ID and the second +parameter the callback function: + +.. code-block:: python + + def my_callback(param): + print(param) + + {2}.register_callback({1}.CALLBACK_EXAMPLE, my_callback) + +The available constants with inherent number and type of parameters are +described below. + +.. note:: + Using callbacks for recurring events is *always* preferred + compared to using getters. It will use less USB bandwidth and the latency + will be a lot better, since there is no round trip time. + +{3} +""", + 'de': """ +.. _{0}_python_callbacks: + +Callbacks +^^^^^^^^^ + +Callbacks können registriert werden um zeitkritische +oder wiederkehrende Daten vom Gerät zu erhalten. Die Registrierung kann +mit der Funktion :py:func:`register_callback() <{1}.register_callback>` des +Geräte Objektes durchgeführt werden. Der erste Parameter ist die Callback ID +und der zweite Parameter die Callback-Funktion: + +.. code-block:: python + + def my_callback(param): + print(param) + + {2}.register_callback({1}.CALLBACK_EXAMPLE, my_callback) + +Die verfügbaren IDs mit der dazugehörigen Parameteranzahl und -typen werden +weiter unten beschrieben. + +.. note:: + Callbacks für wiederkehrende Ereignisse zu verwenden ist + *immer* zu bevorzugen gegenüber der Verwendung von Abfragen. + Es wird weniger USB-Bandbreite benutzt und die Latenz ist + erheblich geringer, da es keine Paketumlaufzeit gibt. + +{3} +""" + } + + api = { + 'en': """ +.. _{0}_python_api: + +API +--- + +Generally, every function of the Python bindings can throw an +``tinkerforge.ip_connection.Error`` exception that has a ``value`` and a +``description`` property. ``value`` can have different values: + +* Error.TIMEOUT = -1 +* Error.NOT_ADDED = -6 (unused since Python bindings version 2.0.0) +* Error.ALREADY_CONNECTED = -7 +* Error.NOT_CONNECTED = -8 +* Error.INVALID_PARAMETER = -9 +* Error.NOT_SUPPORTED = -10 +* Error.UNKNOWN_ERROR_CODE = -11 +* Error.STREAM_OUT_OF_SYNC = -12 +* Error.INVALID_UID = -13 +* Error.NON_ASCII_CHAR_IN_SECRET = -14 +* Error.WRONG_DEVICE_TYPE = -15 +* Error.DEVICE_REPLACED = -16 +* Error.WRONG_RESPONSE_LENGTH = -17 + +All functions listed below are thread-safe. + +{1} + +{2} +""", + 'de': """ +.. _{0}_python_api: + +API +--- + +Prinzipiell kann jede Funktion der Python Bindings +``tinkerforge.ip_connection.Error`` Exception werfen, welche ein ``value`` und +eine ``description`` Property hat. ``value`` kann verschiende Werte haben: + +* Error.TIMEOUT = -1 +* Error.NOT_ADDED = -6 (seit Python Bindings Version 2.0.0 nicht mehr verwendet) +* Error.ALREADY_CONNECTED = -7 +* Error.NOT_CONNECTED = -8 +* Error.INVALID_PARAMETER = -9 +* Error.NOT_SUPPORTED = -10 +* Error.UNKNOWN_ERROR_CODE = -11 +* Error.STREAM_OUT_OF_SYNC = -12 +* Error.INVALID_UID = -13 +* Error.NON_ASCII_CHAR_IN_SECRET = -14 +* Error.WRONG_DEVICE_TYPE = -15 +* Error.DEVICE_REPLACED = -16 +* Error.WRONG_RESPONSE_LENGTH = -17 + +Alle folgend aufgelisteten Funktionen sind Thread-sicher. + +{1} + +{2} +""" + } + + const_str = { + 'en': """ +.. _{0}_python_constants: + +Constants +^^^^^^^^^ + +.. py:attribute:: {1}.DEVICE_IDENTIFIER + + This constant is used to identify a {3}. + + The :py:func:`get_identity() <{1}.get_identity>` function and the + :py:attr:`IPConnection.CALLBACK_ENUMERATE ` + callback of the IP Connection have a ``device_identifier`` parameter to specify + the Brick's or Bricklet's type. + +.. py:attribute:: {1}.DEVICE_DISPLAY_NAME + + This constant represents the human readable name of a {3}. +""", + 'de': """ +.. _{0}_python_constants: + +Konstanten +^^^^^^^^^^ + +.. py:attribute:: {1}.DEVICE_IDENTIFIER + + Diese Konstante wird verwendet um {2} {3} zu identifizieren. + + Die :py:func:`get_identity() <{1}.get_identity>` Funktion und der + :py:attr:`IPConnection.CALLBACK_ENUMERATE ` + Callback der IP Connection haben ein ``device_identifier`` Parameter um den Typ + des Bricks oder Bricklets anzugeben. + +.. py:attribute:: {1}.DEVICE_DISPLAY_NAME + + Diese Konstante stellt den Anzeigenamen eines {3} dar. +""" + } + + create_meta = common.format_simple_element_meta([('uid', 'str', 1, 'in'), + ('ipcon', 'IPConnection', 1, 'in'), + (self.get_name().under, self.get_python_class_name(), 1, 'out')]) + create_meta_table = common.make_rst_meta_table(create_meta) + + cre = common.select_lang(create_str).format(self.get_python_class_name(), + self.get_name().under, + create_meta_table) + + reg_meta = common.format_simple_element_meta([('callback_id', 'int', 1, 'in'), + ('function', 'callable', 1, 'in')], + no_out_value={'en': 'None', 'de': 'None'}) + reg_meta_table = common.make_rst_meta_table(reg_meta) + + reg = common.select_lang(register_str).format(self.get_doc_rst_ref_name(), + self.get_name().camel, + self.get_category().camel, + reg_meta_table) + + bf = self.get_python_functions('bf') + af = self.get_python_functions('af') + ccf = self.get_python_functions('ccf') + c = self.get_python_callbacks() + vf = self.get_python_functions('vf') + if_ = self.get_python_functions('if') + api_str = '' + + if bf: + api_str += common.select_lang(common.bf_str).format(cre, bf) + + if af: + api_str += common.select_lang(common.af_str).format(af) + + if c: + api_str += common.select_lang(common.ccf_str).format(reg, ccf) + api_str += common.select_lang(c_str).format(self.get_doc_rst_ref_name(), + self.get_python_class_name(), + self.get_name().under, + c) + + if vf: + api_str += common.select_lang(common.vf_str).format(vf) + + if if_: + api_str += common.select_lang(common.if_str).format(if_) + + article = 'ein' + + if self.is_brick(): + article = 'einen' + + api_str += common.select_lang(const_str).format(self.get_doc_rst_ref_name(), + self.get_python_class_name(), + article, + self.get_long_display_name()) + + return common.select_lang(api).format(self.get_doc_rst_ref_name(), + self.specialize_python_doc_function_links(common.select_lang(self.get_doc())), + api_str) + + def get_python_doc(self): + doc = common.make_rst_header(self) + doc += common.make_rst_summary(self) + doc += self.get_python_examples() + doc += self.get_python_api() + + return doc + +class PythonDocPacket(python_common.PythonPacket): + def get_python_formatted_doc(self): + text = common.select_lang(self.get_doc_text()) + text = self.get_device().specialize_python_doc_function_links(text) + + def format_parameter(name): + return '``{0}``'.format(name) # FIXME + + text = common.handle_rst_param(text, format_parameter) + text = common.handle_rst_word(text) + text = common.handle_rst_substitutions(text, self) + + prefix = self.get_device().get_python_class_name() + '.' + + def format_element_name(element, index): + if index == None: + return element.get_name().under + + return '{0}[{1}]'.format(element.get_name().under, index) + + text += common.format_constants(prefix, self, format_element_name) + text += common.format_since_firmware(self.get_device(), self) + + return common.shift_right(text, 1) + +class PythonDocGenerator(python_common.PythonGeneratorTrait, common.DocGenerator): + def get_doc_rst_filename_part(self): + return 'Python' + + def get_doc_example_regex(self): + return r'^example_.*\.py$' + + def get_device_class(self): + return PythonDocDevice + + def get_packet_class(self): + return PythonDocPacket + + def get_element_class(self): + return python_common.PythonElement + + def generate(self, device): + with open(device.get_doc_rst_path(), 'w') as f: + f.write(device.get_python_doc()) + +def generate(root_dir, language, internal): + common.generate(root_dir, language, internal, PythonDocGenerator) + +if __name__ == '__main__': + args = common.dockerize('python', __file__, add_internal_argument=True) + + for language in ['en', 'de']: + generate(os.getcwd(), language, args.internal) diff --git a/julia/generate_julia_examples.py b/julia/generate_julia_examples.py new file mode 100644 index 00000000..0d9c8614 --- /dev/null +++ b/julia/generate_julia_examples.py @@ -0,0 +1,667 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Python Examples Generator +Copyright (C) 2015-2019 Matthias Bolte + +generate_python_examples.py: Generator for Python examples + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public +License along with this program; if not, write to the +Free Software Foundation, Inc., 59 Temple Place - Suite 330, +Boston, MA 02111-1307, USA. +""" + +import sys + +if sys.hexversion < 0x3040000: + print('Python >= 3.4 required') + sys.exit(1) + +import os +import importlib.util +import importlib.machinery + +def create_generators_module(): + generators_dir = os.path.split(os.path.dirname(os.path.realpath(__file__)))[0] + + if sys.hexversion < 0x3050000: + generators_module = importlib.machinery.SourceFileLoader('generators', os.path.join(generators_dir, '__init__.py')).load_module() + else: + generators_spec = importlib.util.spec_from_file_location('generators', os.path.join(generators_dir, '__init__.py')) + generators_module = importlib.util.module_from_spec(generators_spec) + + generators_spec.loader.exec_module(generators_module) + + sys.modules['generators'] = generators_module + +if 'generators' not in sys.modules: + create_generators_module() + +from generators import common +from generators.python import python_common + +global_line_prefix = '' + +class PythonConstant(common.Constant): + def get_python_source(self, callback=False): + templateA = '{device_class}.{constant_group_name}_{constant_name}' + templateB = '{device_name}.{constant_group_name}_{constant_name}' + + if callback: + template = templateA + else: + template = templateB + + return template.format(device_class=self.get_device().get_python_class_name(), + device_name=self.get_device().get_initial_name(), + constant_group_name=self.get_constant_group().get_name().upper, + constant_name=self.get_name().upper) + +class PythonExample(common.Example): + def get_python_source(self): + template = r"""#!/usr/bin/env python +# -*- coding: utf-8 -*-{incomplete}{description} + +HOST = "localhost" +PORT = 4223 +UID = "{dummy_uid}" # Change {dummy_uid} to the UID of your {device_name_long_display} +{imports} +from tinkerforge.ip_connection import IPConnection +from tinkerforge.{device_category_under}_{device_name_under} import {device_category_camel}{device_name_camel} +{functions} +if __name__ == "__main__": + ipcon = IPConnection() # Create IP connection + {device_name_initial} = {device_category_camel}{device_name_camel}(UID, ipcon) # Create device object + + ipcon.connect(HOST, PORT) # Connect to brickd + # Don't use device before ipcon is connected +{sources} + input("Press key to exit\n") # Use raw_input() in Python 2{cleanups} + ipcon.disconnect() +""" + + if self.is_incomplete(): + incomplete = '\n\n# FIXME: This example is incomplete' + else: + incomplete = '' + + if self.get_description() != None: + description = '\n\n# {0}'.format(self.get_description().replace('\n', '\n# ')) + else: + description = '' + + imports = [] + functions = [] + sources = [] + cleanups = [] + + for function in self.get_functions(): + imports += function.get_python_imports() + functions.append(function.get_python_function()) + sources.append(function.get_python_source()) + + for cleanup in self.get_cleanups(): + imports += cleanup.get_python_imports() + functions.append(cleanup.get_python_function()) + cleanups.append(cleanup.get_python_source()) + + unique_imports = [] + + for import_ in imports: + if import_ not in unique_imports: + unique_imports.append(import_) + + while None in functions: + functions.remove(None) + + while None in sources: + sources.remove(None) + + if len(sources) == 0: + sources = [' # TODO: Add example code here\n'] + + while None in cleanups: + cleanups.remove(None) + + return template.format(incomplete=incomplete, + description=description, + device_category_camel=self.get_device().get_category().camel, + device_category_under=self.get_device().get_category().under, + device_name_camel=self.get_device().get_name().camel, + device_name_under=self.get_device().get_name().under, + device_name_initial=self.get_device().get_initial_name(), + device_name_long_display=self.get_device().get_long_display_name(), + dummy_uid=self.get_dummy_uid(), + imports=common.wrap_non_empty('\n', ''.join(unique_imports), ''), + functions=common.wrap_non_empty('\n', '\n'.join(functions), ''), + sources='\n' + '\n'.join(sources).replace('\n\r', '').lstrip('\r'), + cleanups=common.wrap_non_empty('\n\n', '\n'.join(cleanups).replace('\n\r', '').lstrip('\r').rstrip('\n'), '\n')) + +class PythonExampleArgument(common.ExampleArgument): + def get_python_source(self): + type_ = self.get_type() + + def helper(value): + if type_ == 'float': + return common.format_float(value) + elif type_ == 'bool': + return str(bool(value)) + elif type_ in ['char', 'string']: + return '"{0}"'.format(value.replace('"', '\\"')) + elif ':bitmask:' in type_: + return common.make_c_like_bitmask(value) + elif type_.endswith(':constant'): + return self.get_value_constant(value).get_python_source() + else: + return str(value) + + value = self.get_value() + + if isinstance(value, list): + return '[{0}]'.format(', '.join([helper(item) for item in value])) + + return helper(value) + +class PythonExampleArgumentsMixin(object): + def get_python_arguments(self): + return [argument.get_python_source() for argument in self.get_arguments()] + +class PythonExampleParameter(common.ExampleParameter): + def get_python_source(self): + return self.get_name().under + + def get_python_prints(self): + if self.get_type().split(':')[-1] == 'constant': + if self.get_label_name() == None: + return [] + + # FIXME: need to handle multiple labels + assert self.get_label_count() == 1 + + template = '{global_line_prefix} {else_}if {name} == {constant_name}:\n{global_line_prefix} print("{label}: {constant_title}"){comment}' + constant_group = self.get_constant_group() + result = [] + + for constant in constant_group.get_constants(): + result.append(template.format(global_line_prefix=global_line_prefix, + else_='el' if len(result) > 0 else '', + name=self.get_name().under, + label=self.get_label_name(), + constant_name=constant.get_python_source(callback=True), + constant_title=constant.get_name().space, + comment=self.get_formatted_comment(' # {0}'))) + + result = ['\r' + '\n'.join(result) + '\r'] + else: + template = '{global_line_prefix} print("{label}: " + {format_prefix}{name}{index}{divisor}{format_suffix}{unit}){comment}' + + if self.get_label_name() == None: + return [] + + if self.get_cardinality() < 0: + return [] # FIXME: streaming + + type_ = self.get_type() + + if ':bitmask:' in type_: + format_prefix = 'format(' + format_suffix = ', "0{0}b")'.format(int(type_.split(':')[2])) + elif type_ in ['char', 'string']: + format_prefix = '' + format_suffix = '' + else: + format_prefix = 'str(' + format_suffix = ')' + + result = [] + + for index in range(self.get_label_count()): + result.append(template.format(global_line_prefix=global_line_prefix, + name=self.get_name().under, + label=self.get_label_name(index=index), + index='[{0}]'.format(index) if self.get_label_count() > 1 else '', + divisor=self.get_formatted_divisor('/{0}'), + unit=self.get_formatted_unit_name(' + " {0}"'), + format_prefix=format_prefix, + format_suffix=format_suffix, + comment=self.get_formatted_comment(' # {0}'))) + + return result + +class PythonExampleResult(common.ExampleResult): + def get_python_variable(self): + name = self.get_name().under + + if name == self.get_device().get_initial_name(): + name += '_' + + return name + + def get_python_prints(self): + if self.get_type().split(':')[-1] == 'constant': + # FIXME: need to handle multiple labels + assert self.get_label_count() == 1 + + template = '{global_line_prefix} {else_}if {name} == {constant_name}:\n{global_line_prefix} print("{label}: {constant_title}"){comment}' + constant_group = self.get_constant_group() + result = [] + + for constant in constant_group.get_constants(): + result.append(template.format(global_line_prefix=global_line_prefix, + else_='el' if len(result) > 0 else '', + name=self.get_name().under, + label=self.get_label_name(), + constant_name=constant.get_python_source(), + constant_title=constant.get_name().space, + comment=self.get_formatted_comment(' # {0}'))) + + result = ['\r' + '\n'.join(result) + '\r'] + else: + template = '{global_line_prefix} print("{label}: " + {format_prefix}{name}{index}{divisor}{format_suffix}{unit}){comment}' + + if self.get_label_name() == None: + return [] + + if self.get_cardinality() < 0: + return [] # FIXME: streaming + + name = self.get_name().under + + if name == self.get_device().get_initial_name(): + name += '_' + + type_ = self.get_type() + + if ':bitmask:' in type_: + format_prefix = 'format(' + format_suffix = ', "0{0}b")'.format(int(type_.split(':')[2])) + elif type_ in ['char', 'string']: + format_prefix = '' + format_suffix = '' + else: + format_prefix = 'str(' + format_suffix = ')' + + result = [] + + for index in range(self.get_label_count()): + result.append(template.format(global_line_prefix=global_line_prefix, + name=name, + label=self.get_label_name(index=index), + index='[{0}]'.format(index) if self.get_label_count() > 1 else '', + divisor=self.get_formatted_divisor('/{0}'), + unit=self.get_formatted_unit_name(' + " {0}"'), + format_prefix=format_prefix, + format_suffix=format_suffix, + comment=self.get_formatted_comment(' # {0}'))) + + return result + +class PythonExampleGetterFunction(common.ExampleGetterFunction, PythonExampleArgumentsMixin): + def get_python_imports(self): + return [] + + def get_python_function(self): + return None + + def get_python_source(self): + template = r"""{global_line_prefix} # Get current {function_name_comment} +{global_line_prefix} {variables} = {device_name}.{function_name_under}({arguments}) +{prints} +""" + variables = [] + prints = [] + + for result in self.get_results(): + variables.append(result.get_python_variable()) + prints += result.get_python_prints() + + while None in prints: + prints.remove(None) + + if len(prints) > 1: + prints.insert(0, '\b') + + result = template.format(global_line_prefix=global_line_prefix, + device_name=self.get_device().get_initial_name(), + function_name_under=self.get_name().under, + function_name_comment=self.get_comment_name(), + variables=','.join(variables), + prints='\n'.join(prints).replace('\b\n\r', '\n').replace('\b', '').replace('\r\n\r', '\n\n').rstrip('\r').replace('\r', '\n'), + arguments=', '.join(self.get_python_arguments())) + + return common.break_string(result, ' ', continuation=' \\', indent_suffix=' ') + +class PythonExampleSetterFunction(common.ExampleSetterFunction, PythonExampleArgumentsMixin): + def get_python_imports(self): + return [] + + def get_python_function(self): + return None + + def get_python_source(self): + template = '{comment1}{global_line_prefix} {device_name}.{function_name}({arguments}){comment2}\n' + + result = template.format(global_line_prefix=global_line_prefix, + device_name=self.get_device().get_initial_name(), + function_name=self.get_name().under, + arguments=','.join(self.get_python_arguments()), + comment1=self.get_formatted_comment1(global_line_prefix + ' # {0}\n', '\r', '\n' + global_line_prefix + ' # '), + comment2=self.get_formatted_comment2(' # {0}', '')) + + return common.break_string(result, '.{0}('.format(self.get_name().under)) + +class PythonExampleCallbackFunction(common.ExampleCallbackFunction): + def get_python_imports(self): + return [] + + def get_python_function(self): + template1A = r"""# Callback function for {function_name_comment} callback +""" + template1B = r"""{override_comment} +""" + template2 = r"""def cb_{function_name_under}({parameters}): +{prints}{extra_message} +""" + override_comment = self.get_formatted_override_comment('# {0}', None, '\n# ') + + if override_comment == None: + template1 = template1A + else: + template1 = template1B + + parameters = [] + prints = [] + + for parameter in self.get_parameters(): + parameters.append(parameter.get_python_source()) + prints += parameter.get_python_prints() + + while None in prints: + prints.remove(None) + + if len(prints) > 1: + prints.append(' print("")') + + extra_message = self.get_formatted_extra_message(' print("{0}")') + + if len(extra_message) > 0 and len(prints) > 0: + extra_message = '\n' + extra_message + + result = template1.format(function_name_comment=self.get_comment_name(), + override_comment=override_comment) + \ + template2.format(function_name_under=self.get_name().under, + parameters=','.join(parameters), + prints='\n'.join(prints).replace('\r\n\r', '\n\n').strip('\r').replace('\r', '\n'), + extra_message=extra_message) + + return common.break_string(result, 'cb_{}('.format(self.get_name().under)) + + def get_python_source(self): + template1 = r""" # Register {function_name_comment}callbacktofunctioncb_{function_name_under} +""" + template2 = r""" {device_name}.register_callback({device_name}.CALLBACK_{function_name_upper},cb_{function_name_under}) +""" + + result1 = template1.format(function_name_under=self.get_name().under, + function_name_comment=self.get_comment_name()) + result2 = template2.format(device_name=self.get_device().get_initial_name(), + function_name_under=self.get_name().under, + function_name_upper=self.get_name().upper) + + return common.break_string(result1, '# ', indent_tail='# ') + \ + common.break_string(result2, 'register_callback(') + +class PythonExampleCallbackPeriodFunction(common.ExampleCallbackPeriodFunction, PythonExampleArgumentsMixin): + def get_python_imports(self): + return [] + + def get_python_function(self): + return None + + def get_python_source(self): + templateA = r""" # Set period for {function_name_comment} callback to {period_sec_short} ({period_msec}ms) + {device_name}.set_{function_name_under}_period({arguments}{period_msec}) +""" + templateB = r""" # Set period for {function_name_comment} callback to {period_sec_short} ({period_msec}ms) + # Note: The {function_name_comment} callback is only called every {period_sec_long} + # if the {function_name_comment} has changed since the last call! + {device_name}.set_{function_name_under}_callback_period({arguments}{period_msec}) +""" + + if self.get_device().get_name().space.startswith('IMU'): + template = templateA # FIXME: special hack for IMU Brick (2.0) callback behavior and name mismatch + else: + template = templateB + + period_msec, period_sec_short, period_sec_long = self.get_formatted_period() + + return template.format(device_name=self.get_device().get_initial_name(), + function_name_under=self.get_name().under, + function_name_comment=self.get_comment_name(), + arguments=common.wrap_non_empty('', ', '.join(self.get_python_arguments()), ', '), + period_msec=period_msec, + period_sec_short=period_sec_short, + period_sec_long=period_sec_long) + +class PythonExampleCallbackThresholdMinimumMaximum(common.ExampleCallbackThresholdMinimumMaximum): + def get_python_source(self): + template = '{minimum}, {maximum}' + + return template.format(minimum=self.get_formatted_minimum(), + maximum=self.get_formatted_maximum()) + +class PythonExampleCallbackThresholdFunction(common.ExampleCallbackThresholdFunction, PythonExampleArgumentsMixin): + def get_python_imports(self): + return [] + + def get_python_function(self): + return None + + def get_python_source(self): + template = r""" # Configure threshold for {function_name_comment} "{option_comment}" + {device_name}.set_{function_name_under}_callback_threshold({arguments}"{option_char}", {minimum_maximums}) +""" + minimum_maximums = [] + + for minimum_maximum in self.get_minimum_maximums(): + minimum_maximums.append(minimum_maximum.get_python_source()) + + return template.format(device_name=self.get_device().get_initial_name(), + function_name_under=self.get_name().under, + function_name_comment=self.get_comment_name(), + arguments=common.wrap_non_empty('', ', '.join(self.get_python_arguments()), ', '), + option_char=self.get_option_char(), + option_comment=self.get_option_comment(), + minimum_maximums=', '.join(minimum_maximums)) + +class PythonExampleCallbackConfigurationFunction(common.ExampleCallbackConfigurationFunction, PythonExampleArgumentsMixin): + def get_python_imports(self): + return [] + + def get_python_function(self): + return None + + def get_python_source(self): + templateA = r""" # Set period for {function_name_comment} callback to {period_sec_short} ({period_msec}ms) + {device_name}.set_{function_name_under}_callback_configuration({arguments}{period_msec}{value_has_to_change}) +""" + templateB = r""" # Set period for {function_name_comment} callback to {period_sec_short} ({period_msec}ms) without a threshold + {device_name}.set_{function_name_under}_callback_configuration({arguments}{period_msec}{value_has_to_change}, "{option_char}", {minimum_maximums}) +""" + templateC = r""" # Configure threshold for {function_name_comment} "{option_comment}" + # with a debounce period of {period_sec_short} ({period_msec}ms) + {device_name}.set_{function_name_under}_callback_configuration({arguments}{period_msec}{value_has_to_change}, "{option_char}", {minimum_maximums}) +""" + + if self.get_option_char() == None: + template = templateA + elif self.get_option_char() == 'x': + template = templateB + else: + template = templateC + + period_msec, period_sec_short, period_sec_long = self.get_formatted_period() + + minimum_maximums = [] + + for minimum_maximum in self.get_minimum_maximums(): + minimum_maximums.append(minimum_maximum.get_python_source()) + + return template.format(device_name=self.get_device().get_initial_name(), + function_name_under=self.get_name().under, + function_name_comment=self.get_comment_name(), + arguments=common.wrap_non_empty('', ', '.join(self.get_python_arguments()), ', '), + period_msec=period_msec, + period_sec_short=period_sec_short, + period_sec_long=period_sec_long, + value_has_to_change=common.wrap_non_empty(', ', self.get_value_has_to_change('True', 'False', ''), ''), + option_char=self.get_option_char(), + option_comment=self.get_option_comment(), + minimum_maximums=', '.join(minimum_maximums)) + +class PythonExampleSpecialFunction(common.ExampleSpecialFunction): + def get_python_imports(self): + if self.get_type() == 'sleep': + return ['import time\n'] + else: + return [] + + def get_python_function(self): + return None + + def get_python_source(self): + global global_line_prefix + + type_ = self.get_type() + + if type_ == 'empty': + return '' + elif type_ == 'debounce_period': + template = r""" # Get threshold callbacks with a debounce time of {period_sec} ({period_msec}ms) + {device_name_initial}.set_debounce_period({period_msec}) +""" + period_msec, period_sec = self.get_formatted_debounce_period() + + return template.format(device_name_initial=self.get_device().get_initial_name(), + period_msec=period_msec, + period_sec=period_sec) + elif type_ == 'sleep': + template = '{comment1}{global_line_prefix} time.sleep({duration}){comment2}\n' + duration = self.get_sleep_duration() + + if duration % 1000 == 0: + duration //= 1000 + else: + duration /= 1000.0 + + return template.format(global_line_prefix=global_line_prefix, + duration=duration, + comment1=self.get_formatted_sleep_comment1(global_line_prefix + ' # {0}\n', '\r', '\n' + global_line_prefix + ' # '), + comment2=self.get_formatted_sleep_comment2(' # {0}', '')) + elif type_ == 'wait': + return None + elif type_ == 'loop_header': + template = '{comment} for i in range({limit}):\n' + global_line_prefix = ' ' + + return template.format(limit=self.get_loop_header_limit(), + comment=self.get_formatted_loop_header_comment(' # {0}\n', '', '\n # ')) + elif type_ == 'loop_footer': + global_line_prefix = '' + + return '\r' + +class PythonExamplesGenerator(python_common.PythonGeneratorTrait, common.ExamplesGenerator): + def get_constant_class(self): + return PythonConstant + + def get_device_class(self): + return python_common.PythonDevice + + def get_example_class(self): + return PythonExample + + def get_example_argument_class(self): + return PythonExampleArgument + + def get_example_parameter_class(self): + return PythonExampleParameter + + def get_example_result_class(self): + return PythonExampleResult + + def get_example_getter_function_class(self): + return PythonExampleGetterFunction + + def get_example_setter_function_class(self): + return PythonExampleSetterFunction + + def get_example_callback_function_class(self): + return PythonExampleCallbackFunction + + def get_example_callback_period_function_class(self): + return PythonExampleCallbackPeriodFunction + + def get_example_callback_threshold_minimum_maximum_class(self): + return PythonExampleCallbackThresholdMinimumMaximum + + def get_example_callback_threshold_function_class(self): + return PythonExampleCallbackThresholdFunction + + def get_example_callback_configuration_function_class(self): + return PythonExampleCallbackConfigurationFunction + + def get_example_special_function_class(self): + return PythonExampleSpecialFunction + + def generate(self, device): + if os.getenv('TINKERFORGE_GENERATE_EXAMPLES_FOR_DEVICE', device.get_name().camel) != device.get_name().camel: + common.print_verbose(' \033[01;31m- skipped\033[0m') + return + + examples_dir = self.get_examples_dir(device) + examples = device.get_examples() + + if len(examples) == 0: + common.print_verbose(' \033[01;31m- no examples\033[0m') + return + + if not os.path.exists(examples_dir): + os.makedirs(examples_dir) + + for example in examples: + filename = 'example_{0}.py'.format(example.get_name().under) + filepath = os.path.join(examples_dir, filename) + + if example.is_incomplete(): + if os.path.exists(filepath) and self.skip_existing_incomplete_example: + common.print_verbose(' - ' + filename + ' \033[01;35m(incomplete, skipped)\033[0m') + continue + else: + common.print_verbose(' - ' + filename + ' \033[01;31m(incomplete)\033[0m') + else: + common.print_verbose(' - ' + filename) + + with open(filepath, 'w') as f: + f.write(example.get_python_source()) + +def generate(root_dir, language, internal): + common.generate(root_dir, language, internal, PythonExamplesGenerator) + +if __name__ == '__main__': + args = common.dockerize('python', __file__, add_internal_argument=True) + + generate(os.getcwd(), 'en', args.internal) diff --git a/julia/generate_julia_zip.py b/julia/generate_julia_zip.py new file mode 100644 index 00000000..64d91c77 --- /dev/null +++ b/julia/generate_julia_zip.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Julia ZIP Generator +Copyright (C) 2012-2015, 2017-2018 Matthias Bolte +Copyright (C) 2011 Olaf Lüke + +generate_julia_zip.py: Generator for Julia ZIP + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public +License along with this program; if not, write to the +Free Software Foundation, Inc., 59 Temple Place - Suite 330, +Boston, MA 02111-1307, USA. +""" + +import sys + +if sys.hexversion < 0x3040000: + print('Python >= 3.4 required') + sys.exit(1) + +import os +import shutil +import importlib.util +import importlib.machinery + +def create_generators_module(): + generators_dir = os.path.split(os.path.dirname(os.path.realpath(__file__)))[0] + + if sys.hexversion < 0x3050000: + generators_module = importlib.machinery.SourceFileLoader('generators', os.path.join(generators_dir, '__init__.py')).load_module() + else: + generators_spec = importlib.util.spec_from_file_location('generators', os.path.join(generators_dir, '__init__.py')) + generators_module = importlib.util.module_from_spec(generators_spec) + + generators_spec.loader.exec_module(generators_module) + + sys.modules['generators'] = generators_module + +if 'generators' not in sys.modules: + create_generators_module() + +from generators import common +from generators.julia import julia_common + +class JuliaZipGenerator(julia_common.JuliaGeneratorTrait, common.ZipGenerator): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.tmp_dir = self.get_zip_dir() + self.tmp_source_dir = os.path.join(self.tmp_dir, 'src') + self.tmp_source_devices_dir = os.path.join(self.tmp_source_dir, 'devices') + self.tmp_examples_dir = os.path.join(self.tmp_dir, 'examples') + + def prepare(self): + super().prepare() + + os.makedirs(self.tmp_source_dir) + os.makedirs(self.tmp_source_devices_dir) + os.makedirs(self.tmp_examples_dir) + + def generate(self, device): + if not device.is_released(): + return + + # Copy device examples + tmp_examples_device = os.path.join(self.tmp_examples_dir, + device.get_category().under, + device.get_name().under) + + if not os.path.exists(tmp_examples_device): + os.makedirs(tmp_examples_device) + + for example in common.find_device_examples(device, r'^example_.*\.jl$'): + shutil.copy(example[1], tmp_examples_device) + + def finish(self): + root_dir = self.get_root_dir() + + # Copy IP Connection examples + if self.get_config_name().space == 'Tinkerforge': + for example in common.find_examples(root_dir, r'^example_.*\.jl$'): + shutil.copy(example[1], self.tmp_examples_dir) + + # Copy bindings and readme + for filename in self.get_released_files() + ['device_factory.jl']: + shutil.copy(os.path.join(self.get_bindings_dir(), filename), self.tmp_source_devices_dir) + + shutil.copy(os.path.join(root_dir, 'Tinkerforge.jl'), self.tmp_source_dir) + shutil.copy(os.path.join(root_dir, 'ip_connection_base.jl'), self.tmp_source_dir) + shutil.copy(os.path.join(root_dir, 'ip_connection.jl'), self.tmp_source_dir) + shutil.copy(os.path.join(root_dir, 'changelog.txt'), self.tmp_dir) + shutil.copy(os.path.join(root_dir, 'readme.txt'), self.tmp_dir) + shutil.copy(os.path.join(root_dir, 'LICENSE'), self.tmp_dir) + + # Make Project.toml + version = self.get_changelog_version() + + common.specialize_template(os.path.join(root_dir, 'Project.toml.template'), + os.path.join(self.tmp_dir, 'Project.toml'), + {'<>': '.'.join(version)}) + + # Make zip + #self.create_zip_file(self.tmp_dir) + +def generate(root_dir, language, internal): + common.generate(root_dir, language, internal, JuliaZipGenerator) + +if __name__ == '__main__': + args = common.dockerize('julia', __file__, add_internal_argument=True) + + generate(os.getcwd(), 'en', args.internal) diff --git a/julia/ip_connection.jl b/julia/ip_connection.jl new file mode 100644 index 00000000..666dcd1e --- /dev/null +++ b/julia/ip_connection.jl @@ -0,0 +1,1485 @@ +export TinkerforgeError +abstract type TinkerforgeError <: Exception end + +export TinkerforgeTimeoutError +struct TinkerforgeTimeoutError <: TinkerforgeError + description::String +end + +export TinkerforgeNotAddedError +struct TinkerforgeNotAddedError <: TinkerforgeError + description::String +end + +export TinkerforgeAlreadyConnectedError +struct TinkerforgeAlreadyConnectedError <: TinkerforgeError + description::String +end + +export TinkerforgeNotConnectedError +struct TinkerforgeNotConnectedError <: TinkerforgeError + description::String +end + +export TinkerforgeInvalidParameterError +struct TinkerforgeInvalidParameterError <: TinkerforgeError + description::String +end + +export TinkerforgeNotSupportedError +struct TinkerforgeNotSupportedError <: TinkerforgeError + description::String +end + +export TinkerforgeUnknownErrorCodeError +struct TinkerforgeUnknownErrorCodeError <: TinkerforgeError + description::String +end + +export TinkerforgeStreamOutOfSyncError +struct TinkerforgeStreamOutOfSyncError <: TinkerforgeError + description::String +end + +export TinkerforgeInvalidUidError +struct TinkerforgeInvalidUidError <: TinkerforgeError + description::String +end + +export TinkerforgeNonASCIICharInSecretError +struct TinkerforgeNonASCIICharInSecretError <: TinkerforgeError + description::String +end + +export TinkerforgeWrongDeviceTypeError +struct TinkerforgeWrongDeviceTypeError <: TinkerforgeError + description::String +end + +export TinkerforgeDeviceReplacedError +struct TinkerforgeDeviceReplacedError <: TinkerforgeError + description::String +end + +export TinkerforgeWrongResponseLengthError +struct TinkerforgeWrongResponseLengthError <: TinkerforgeError + description::String +end + +export ValueError +struct ValueError <: Exception + msg::String +end + +export DeviceIdentifierCheck, DEVICE_IDENTIFIER_CHECK_PENDING, DEVICE_IDENTIFIER_CHECK_MATCH, DEVICE_IDENTIFIER_CHECK_MISMATCH +@enum DeviceIdentifierCheck begin + DEVICE_IDENTIFIER_CHECK_PENDING = 0 + DEVICE_IDENTIFIER_CHECK_MATCH = 1 + DEVICE_IDENTIFIER_CHECK_MISMATCH = 2 +end + +export ResponseExpected, RESPONSE_EXPECTED_INVALID_FUNCTION_ID, RESPONSE_EXPECTED_ALWAYS_TRUE, RESPONSE_EXPECTED_TRUE, RESPONSE_EXPECTED_FALSE +@enum ResponseExpected begin + RESPONSE_EXPECTED_INVALID_FUNCTION_ID = 0 + RESPONSE_EXPECTED_ALWAYS_TRUE = 1 # getter + RESPONSE_EXPECTED_TRUE = 2 # setter + RESPONSE_EXPECTED_FALSE = 3 # setter, default +end + +export TinkerforgeDevice +abstract type TinkerforgeDevice end + +function _initDevice(device::TinkerforgeDevice) + uid = py"base58decode"(device.uid_string) + + if uid > (1 << 64) - 1 + @warn "Code disabled" + #throw(TinkerforgeInvalidUidError("UID '$(device.uid_string)' is too big")) + end + + if uid > ((1 << 32) - 1) + uid_ = uid64_to_uid32(uid) + end + + if uid == 0 + throw(TinkerforgeInvalidUidError("UID '$(device.uid_string)' is empty or maps to zero")) + end + + device.uid = uid + + device.response_expected[:FUNCTION_ADC_CALIBRATE] = RESPONSE_EXPECTED_ALWAYS_TRUE + device.response_expected[:FUNCTION_GET_ADC_CALIBRATION] = RESPONSE_EXPECTED_ALWAYS_TRUE + device.response_expected[:FUNCTION_READ_BRICKLET_UID] = RESPONSE_EXPECTED_ALWAYS_TRUE + device.response_expected[:FUNCTION_WRITE_BRICKLET_UID] = RESPONSE_EXPECTED_ALWAYS_TRUE +end + +function pack_struct(format::String, data) + return py"pack_struct"(format, data) +end + +function unpack_struct(format::String, data) + return py"unpack_struct"(format, data) +end + +export get_api_version +""" +Returns the API version (major, minor, revision) of the bindings for +this device. +""" +function get_api_version(device::TinkerforgeDevice) + return device.api_version +end + +export get_response_expected +""" +Returns the response expected flag for the function specified by the +*function_id* parameter. It is *true* if the function is expected to +send a response, *false* otherwise. + +For getter functions this is enabled by functionault and cannot be disabled, +because those functions will always send a response. For callback +configuration functions it is enabled by functionault too, but can be +disabled via the set_response_expected function. For setter functions +it is disabled by functionault and can be enabled. + +Enabling the response expected flag for a setter function allows to +detect timeouts and other error conditions calls of this setter as +well. The device will then send a response for this purpose. If this +flag is disabled for a setter function then no response is sent and +errors are silently ignored, because they cannot be detected. +""" +function get_response_expected(device::TinkerforgeDevice, function_id::Integer) + if function_id < 0 || function_id >= length(device.response_expected) + throw(ValueError("Function ID $function_id out of range")) + end + + flag = function_id + + if flag == :RESPONSE_EXPECTED_INVALID_FUNCTION_ID + throw(ValueError("Invalid function ID $function_id")) + end + + return flag in [:RESPONSE_EXPECTED_ALWAYS_TRUE, :RESPONSE_EXPECTED_TRUE] +end + +export set_response_expected +""" +Changes the response expected flag of the function specified by the +*function_id* parameter. This flag can only be changed for setter +(functionault value: *false*) and callback configuration functions +(functionault value: *true*). For getter functions it is always enabled. + +Enabling the response expected flag for a setter function allows to +detect timeouts and other error conditions calls of this setter as +well. The device will then send a response for this purpose. If this +flag is disabled for a setter function then no response is sent and +errors are silently ignored, because they cannot be detected. +""" +function set_response_expected(device::TinkerforgeDevice, function_id, response_expected) + if function_id < 0 || function_id >= length(device.response_expected) + throw(ValueError("Function ID {$function_id} out of range")) + end + + flag = device.response_expected[function_id] + + if flag == RESPONSE_EXPECTED_INVALID_FUNCTION_ID + throw(ValueError("Invalid function ID {$function_id}")) + end + + if flag == RESPONSE_EXPECTED_ALWAYS_TRUE + throw(ValueError("Response Expected flag cannot be changed for function ID {$function_id}")) + end + + if bool(response_expected) + device.response_expected[function_id] = RESPONSE_EXPECTED_TRUE + else + device.response_expected[function_id] = RESPONSE_EXPECTED_FALSE + end +end + +export set_response_expected_all +""" +Changes the response expected flag for all setter and callback +configuration functions of this device at once. +""" +function set_response_expected_all(device::TinkerforgeDevice, response_expected) + if bool(response_expected) + flag = RESPONSE_EXPECTED_TRUE + else + flag = RESPONSE_EXPECTED_FALSE + end + + for i in range(length(device.response_expected)) + if device.response_expected[i] in [RESPONSE_EXPECTED_TRUE, RESPONSE_EXPECTED_FALSE] + device.response_expected[i] = flag + end + end +end + +# internal +function check_validity(device::TinkerforgeDevice) + if device.replaced + throw(TinkerforgeDeviceReplacedError("Device has been replaced")) + end + + if device.device_identifier < 0 + return nothing + end + + if device.device_identifier_check == :DEVICE_IDENTIFIER_CHECK_MATCH + return nothing + end + + lock(device.device_identifier_lock) do + if device.device_identifier_check == :DEVICE_IDENTIFIER_CHECK_PENDING + device_identifier = send_request(device.ipcon, 255, (), "", 33, "8s 8s c 3B 3B H")[5+1] # .get_identity + + if device_identifier == device.device_identifier + device.device_identifier_check = :DEVICE_IDENTIFIER_CHECK_MATCH + else + device.device_identifier_check = :DEVICE_IDENTIFIER_CHECK_MISMATCH + device.wrong_device_display_name = get_device_display_name(device_identifier) + end + end + + if device.device_identifier_check == :DEVICE_IDENTIFIER_CHECK_MISMATCH + throw(TinkerforgeWrongDeviceTypeError("UID $(device.uid_string) belongs to a $(device.wrong_device_display_name) instead of the expected $(device.device_display_name)")) + end + end +end + +export IPConnection +Base.@kwdef mutable struct IPConnection + host::Union{IPAddr, Missing} + port::Union{Integer, Missing} + timeout::Real = 2.5 + auto_reconnect::Bool = true + auto_reconnect_allowed::Bool = false + auto_reconnect_pending::Bool = false + auto_reconnect_internal::Bool = false + connect_failure_callback::Union{Function, Nothing} = nothing + sequence_number_lock::Base.AbstractLock = Base.ReentrantLock() + next_sequence_number::Integer = 0 # protected by sequence_number_lock + authentication_lock::Base.AbstractLock = Base.ReentrantLock() + next_authentication_nonce::Integer = 0 # protected by authentication_lock + devices = Dict() + replace_lock::Base.AbstractLock = Base.ReentrantLock() # used to synchronize replacements in the devices dict + registered_callbacks::Dict{Integer, Function} = Dict{Integer, Function}() + socket = nothing # protected by socket_lock + socket_id = 0 # protected by socket_lock + socket_lock::Base.AbstractLock = Base.ReentrantLock() + socket_send_lock::Base.AbstractLock = Base.ReentrantLock() + receive_flag::Bool = false + receive_thread = nothing + callback = nothing + disconnect_probe_flag::Bool = false + disconnect_probe_queue = nothing + disconnect_probe_thread = nothing + waiter::Base.Semaphore = Base.Semaphore(10) + brickd = nothing +end + +export BrickDaemon +Base.@kwdef mutable struct BrickDaemon <: TinkerforgeDevice + replaced::Bool + uid::Union{Integer, Missing} + uid_string::String + ipcon::IPConnection + device_identifier::Integer + device_display_name::String + device_url_part::String + device_identifier_lock::Base.AbstractLock + device_identifier_check::DeviceIdentifierCheck # protected by device_identifier_lock + wrong_device_display_name::String # protected by device_identifier_lock + api_version::Tuple{Integer, Integer, Integer} + registered_callbacks::Dict{Integer, Function} + expected_response_function_id::Union{Integer, Nothing} # protected by request_lock + expected_response_sequence_number::Union{Integer, Nothing} # protected by request_lock + response_queue::DataStructures.Queue{Symbol} + request_lock::Base.AbstractLock + stream_lock::Base.AbstractLock + + callbacks::Dict{Symbol, Integer} + callback_formats::Dict{Symbol, Tuple{Integer, String}} + high_level_callbacks::Dict{Symbol, Integer} + id_definitions::Dict{Symbol, Integer} + constants::Dict{Symbol, Integer} + response_expected::DefaultDict{Symbol, ResponseExpected} + + """ + Creates an object with the unique device ID *uid* and adds it to + the IP Connection *ipcon*. + """ + function BrickDaemon(uid::String, ipcon::IPConnection) + replaced = false + uid_string = uid + device_identifier = 0 + device_display_name = "Brick Daemon" + device_url_part = "brick_daemon" # internal; TODO: Not specified; is this correct? + device_identifier_lock = Base.ReentrantLock() + device_identifier_check = DEVICE_IDENTIFIER_CHECK_PENDING # protected by device_identifier_lock + wrong_device_display_name = "?" # protected by device_identifier_lock + api_version = (0, 0, 0) + registered_callbacks = Dict{Integer, Function}() + expected_response_function_id = nothing # protected by request_lock + expected_response_sequence_number = nothing # protected by request_lock + response_queue = DataStructures.Queue{Symbol}() + request_lock = Base.ReentrantLock() + stream_lock = Base.ReentrantLock() + + callbacks = Dict{Symbol, Integer}() + callback_formats = Dict{Symbol, Tuple{Integer, String}}() + high_level_callbacks = Dict{Symbol, Integer}() + id_definitions = Dict{Symbol, Integer}() + constants = Dict{Symbol, Integer}() + response_expected = DefaultDict{Symbol, ResponseExpected}(RESPONSE_EXPECTED_INVALID_FUNCTION_ID) + + #connected_uid::String + #position::Char + #hardware_version::Vector{Integer} + #firmware_version::Vector{Integer} + + device = new( + replaced, + missing, + uid_string, + ipcon, + device_identifier, + device_display_name, + device_url_part, + device_identifier_lock, + device_identifier_check, + wrong_device_display_name, + api_version, + registered_callbacks, + expected_response_function_id, + expected_response_sequence_number, + response_queue, + request_lock, + stream_lock, + callbacks, + callback_formats, + high_level_callbacks, + id_definitions, + constants, + response_expected + ) + _initDevice(device) + + device.api_version = (2, 0, 0) + + device.id_definitions[:FUNCTION_GET_AUTHENTICATION_NONCE] = 1 + device.id_definitions[:FUNCTION_AUTHENTICATE] = 2 + + device.response_expected[:FUNCTION_GET_AUTHENTICATION_NONCE] = RESPONSE_EXPECTED_ALWAYS_TRUE + device.response_expected[:FUNCTION_AUTHENTICATE] = RESPONSE_EXPECTED_TRUE + + add_device(ipcon, device) + + return device + end +end + +export get_authentication_nonce +function get_authentication_nonce(device::BrickDaemon) + return send_request(device, :FUNCTION_GET_AUTHENTICATION_NONCE, (), "", 12, "4B") +end + +export authenticate +function authenticate(device::BrickDaemon, client_nonce, digest) + send_request(device, :FUNCTION_AUTHENTICATE, (client_nonce, digest), "4B 20B", 0, "") +end + +""" +Creates an IP Connection object that can be used to enumerate the available +devices. It is also required for the constructor of Bricks and Bricklets. +""" +function IPConnection(host::IPAddr, port::Integer) + ipcon = IPConnection(host=host, port=port) + brickd = BrickDaemon("2", ipcon) + ipcon.brickd = brickd + + return ipcon +end +function IPConnection(host::String, port::Integer) + if host == "localhost" + hostIP = ip"127.0.0.1" + else + hostIP = parse(IPAddr, host) + end + + return IPConnection(hostIP, port) +end + +export TinkerforgeIPConFunctions, FUNCTION_ENUMERATE, FUNCTION_ADC_CALIBRATE, + FUNCTION_GET_ADC_CALIBRATION, FUNCTION_READ_BRICKLET_UID, FUNCTION_WRITE_BRICKLET_UID, + FUNCTION_DISCONNECT_PROBE +@enum TinkerforgeIPConFunctions begin + FUNCTION_ENUMERATE = 254 + FUNCTION_ADC_CALIBRATE = 251 + FUNCTION_GET_ADC_CALIBRATION = 250 + FUNCTION_READ_BRICKLET_UID = 249 + FUNCTION_WRITE_BRICKLET_UID = 248 + FUNCTION_DISCONNECT_PROBE = 128 +end + +export TinkerforgeIPConCallbacks, CALLBACK_ENUMERATE, CALLBACK_CONNECTED, CALLBACK_DISCONNECTED +@enum TinkerforgeIPConCallbacks begin + CALLBACK_ENUMERATE = 253 + CALLBACK_CONNECTED = 0 + CALLBACK_DISCONNECTED = 1 +end + +BROADCAST_UID = 0 +DISCONNECT_PROBE_INTERVAL = 5 + +# enumeration_type parameter to the enumerate callback +export TinkerforgeIPConEnumerationType, ENUMERATION_TYPE_AVAILABLE, ENUMERATION_TYPE_CONNECTED, ENUMERATION_TYPE_DISCONNECTED +@enum TinkerforgeIPConEnumerationType begin + ENUMERATION_TYPE_AVAILABLE = 0 + ENUMERATION_TYPE_CONNECTED = 1 + ENUMERATION_TYPE_DISCONNECTED = 2 +end + +# connect_reason parameter to the connected callback +export TinkerforgeIPConConnectReason, CONNECT_REASON_REQUEST, CONNECT_REASON_AUTO_RECONNECT +@enum TinkerforgeIPConConnectReason begin + CONNECT_REASON_REQUEST = 0 + CONNECT_REASON_AUTO_RECONNECT = 1 +end + +# disconnect_reason parameter to the disconnected callback +export TinkerforgeIPConDisconnectReason, DISCONNECT_REASON_REQUEST, DISCONNECT_REASON_ERROR, DISCONNECT_REASON_SHUTDOWN +@enum TinkerforgeIPConDisconnectReason begin + DISCONNECT_REASON_REQUEST = 0 + DISCONNECT_REASON_ERROR = 1 + DISCONNECT_REASON_SHUTDOWN = 2 +end + +# returned by get_connection_state +export TinkerforgeIPConConnectionState, CONNECTION_STATE_DISCONNECTED, CONNECTION_STATE_CONNECTED, CONNECTION_STATE_PENDING +@enum TinkerforgeIPConConnectionState begin + CONNECTION_STATE_DISCONNECTED = 0 + CONNECTION_STATE_CONNECTED = 1 + CONNECTION_STATE_PENDING = 2 # auto-reconnect in process +end + +export TinkerforgeIPConQueueState, QUEUE_EXIT, QUEUE_META, QUEUE_PACKET +@enum TinkerforgeIPConQueueState begin + QUEUE_EXIT = 0 + QUEUE_META = 1 + QUEUE_PACKET = 2 +end + +export CallbackContext +mutable struct CallbackContext + queue::Union{Base.Channel, Nothing} + thread::Union{Base.Task, Nothing} + packet_dispatch_allowed::Bool + lock::Union{Base.AbstractLock, Nothing} + + function CallbackContext() + return new(nothing, nothing, false, nothing) + end +end + +export connect +""" +Creates a TCP/IP connection to the given *host* and *port*. The host +and port can point to a Brick Daemon or to a WIFI/Ethernet Extension. + +Devices can only be controlled when the connection was established +successfully. + +Blocks until the connection is established and throws an exception if +there is no Brick Daemon or WIFI/Ethernet Extension listening at the +given host and port. +""" +function connect(ipcon::IPConnection) + lock(ipcon.socket_lock) do + if !isnothing(ipcon.socket) + throw(TinkerforgeAlreadyConnectedError("Already connected to $(ipcon.host):$(ipcon.port)")) + end + + connect_unlocked(ipcon, false) + end + + return nothing +end +connect(host::IPAddr, port::Integer) = connect(IPConnection(host, port)) +connect(host::String, port::Integer) = connect(IPConnection(host, port)) + +export disconnect +""" +Disconnects the TCP/IP connection from the Brick Daemon or the +WIFI/Ethernet Extension. +""" +function disconnect(ipcon::IPConnection) + lock(ipcon.socket_lock) do + ipcon.auto_reconnect_allowed = false + + if ipcon.auto_reconnect_pending + # abort potentially pending auto reconnect + ipcon.auto_reconnect_pending = false + else + if isnothing(ipcon.socket) + throw(TinkerforgeNotConnectedError("Not connected")) + end + + disconnect_unlocked(ipcon) + end + + # end callback thread + callback = ipcon.callback + ipcon.callback = nothing + end + + # do this outside of socket_lock to allow calling (dis-)connect from + # the callbacks while blocking on the join call here + callback.queue.put((IPConnection.QUEUE_META, + (IPConnection.CALLBACK_DISCONNECTED, + IPConnection.DISCONNECT_REASON_REQUEST, None))) + callback.queue.put((IPConnection.QUEUE_EXIT, None)) + + if threading.current_thread() != callback.thread + callback.thread.join() + end + + return nothing +end + +export authenticate +""" +Performs an authentication handshake with the connected Brick Daemon or +WIFI/Ethernet Extension. If the handshake succeeds the connection switches +from non-authenticated to authenticated state and communication can +continue as normal. If the handshake fails then the connection gets closed. +Authentication can fail if the wrong secret was used or if authentication +is not enabled at all on the Brick Daemon or the WIFI/Ethernet Extension. + +For more information about authentication see +https://www.tinkerforge.com/en/doc/Tutorials/Tutorial_Authentication/Tutorial.html +""" +function authenticate(ipcon::IPConnection, secret) + try + secret_bytes = acsii(secret) + catch e + if e isa ArgumentError + throw(TinkerforgeNonASCIICharInSecretError("Authentication secret contains non-ASCII characters")) + else + rethrow() + end + end + + lock(ipcon.authentication_lock) do + if ipcon.next_authentication_nonce == 0 + try + ipcon.next_authentication_nonce = unpack_struct("> 6) & 0xFFFFFFFF) + subseconds + getpid() + end + end + + server_nonce = get_authentication_nonce(ipcon.brickd) + client_nonce = unpack_struct("<4B", pack_struct(" tmp.status != StatusOpen ? close(tmp) : nothing, timeout) + try + tmp = Sockets.connect(tmp, ipcon.host, ipcon.port) + catch e + error("Could not connect to $(ipcon.host) on port $(ipcon.port) + since the operations was timed out after $(timeout) seconds!") + end + catch e + if ipcon.auto_reconnect_internal + if is_auto_reconnect + return + end + + if !isnothing(ipcon.connect_failure_callback) + connect_failure_callback(ipcon, e) + end + + ipcon.auto_reconnect_allowed = true + + # FIXME: don't misuse disconnected-callback here to trigger an auto-reconnect + # because not actual connection has been established yet + put!(ipcon.callback.queue, (QUEUE_META, (CALLBACK_DISCONNECTED, DISCONNECT_REASON_ERROR, nothing))) + else + # end callback thread + if !is_auto_reconnect + put!(ipcon.callback.queue, (QUEUE_EXIT, nothing)) + + if Threads.threadid() != Threads.threadid(ipcon.callback.thread) + Base.wait(ipcon.callback.thread) + end + + ipcon.callback = nothing + end + end + end + + ipcon.socket = tmp + ipcon.socket_id += 1 + + # create disconnect probe thread + try + ipcon.disconnect_probe_flag = true + ipcon.disconnect_probe_queue = Base.Channel() + ipcon.disconnect_probe_thread = Threads.@spawn disconnect_probe_loop(ipcon) + #self.disconnect_probe_thread.daemon = True + #self.disconnect_probe_thread.start() + catch e + ipcon.disconnect_probe_thread = nothing + + # close socket + close(ipcon.socket) + ipcon.socket = nothing + + # end callback thread + if !is_auto_reconnect + put!(ipcon.callback.queue, (QUEUE_EXIT, nothing)) + + if Threads.threadid() != Threads.threadid(ipcon.callback.thread) + Base.wait(ipcon.callback.thread) + end + + ipcon.callback = nothing + end + + rethrow() + end + + # create receive thread + ipcon.callback.packet_dispatch_allowed = true + + try + ipcon.receive_flag = true + ipcon.receive_thread = Threads.@spawn receive_loop(ipcon, ipcon.socket_id) + #ipcon.receive_thread.daemon = True + #ipcon.receive_thread.start() + catch e + ipcon.receive_thread = nothing + + # close socket + disconnect_unlocked(ipcon) + + # end callback thread + if !is_auto_reconnect + put!(ipcon.callback.queue, (QUEUE_EXIT, nothing)) + + if Threads.threadid() != Threads.threadid(ipcon.callback.thread) + wait(ipcon.callback.thread) + end + + ipcon.callback = nothing + end + + rethrow() + end + + ipcon.auto_reconnect_allowed = false + ipcon.auto_reconnect_pending = false + + if is_auto_reconnect + connect_reason = CONNECT_REASON_AUTO_RECONNECT + else + connect_reason = CONNECT_REASON_REQUEST + end + + put!(ipcon.callback.queue, (QUEUE_META, (CALLBACK_CONNECTED, connect_reason, nothing))) +end + +# internal +function disconnect_unlocked(ipcon::IPConnection) + # NOTE: assumes that socket is not None and socket_lock is locked + + # end disconnect probe thread + put!(ipcon.disconnect_probe_queue, true) + wait(ipcon.disconnect_probe_thread) # FIXME: use a timeout? + ipcon.disconnect_probe_thread = nothing + + # stop dispatching packet callbacks before ending the receive + # thread to avoid timeout exceptions due to callback functions + # trying to call getters + if Threads.threadid() != Threads.threadid(ipcon.callback.thread) + # FIXME: cannot hold callback lock here because this can + # deadlock due to an ordering problem with the socket lock + #with self.callback.lock: + if true + ipcon.callback.packet_dispatch_allowed = false + end + else + ipcon.callback.packet_dispatch_allowed = false + end + + # end receive thread + ipcon.receive_flag = false + + # TODO: Do we need an alternative? + # try + # self.socket.shutdown(socket.SHUT_RDWR) + # catch socket.error + # pass + # end + + if !isnothing(ipcon.receive_thread) + Base.wait(ipcon.receive_thread) # FIXME: use a timeout? + ipcon.receive_thread = nothing + end + + # close socket + close(ipcon.socket) + ipcon.socket = nothing +end + +# internal +function set_auto_reconnect_internal(ipcon::IPConnection, auto_reconnect, connect_failure_callback) + ipcon.auto_reconnect_internal = auto_reconnect + ipcon.connect_failure_callback = connect_failure_callback +end + +# internal +function add_device(ipcon::IPConnection, device) + lock(ipcon.replace_lock) do + replaced_device = get(ipcon.devices, device.uid, nothing) + + if !isnothing(replaced_device) + replaced_device.replaced = true + end + + ipcon.devices[device.uid] = device + end +end + +# internal +function receive_loop(ipcon::IPConnection, socket_id) + # if sys.hexversion < 0x03000000 + # pending_data = '' + # else + # pending_data = bytes() + # end + pending_data = "" + + while ipcon.receive_flag + try + data = read(ipcon.socket, 8192) + catch e + rethrow() # TODO: just for now + #socket.timeout + #continue + + #socket.error + # if self.receive_flag: + # e = sys.exc_info()[1] + # if e.errno == errno.EINTR: + # continue + # end + + # self.handle_disconnect_by_peer(IPConnection.DISCONNECT_REASON_ERROR, socket_id, False) + # end + + # break + end + + if length(data) == 0 + if ipcon.receive_flag + handle_disconnect_by_peer(ipcon, DISCONNECT_REASON_SHUTDOWN, socket_id, false) + end + + break + end + + pending_data *= data + + while ipcon.receive_flag + if length(pending_data) < 8 + # Wait for complete header + break + end + + length = get_length_from_data(pending_data) + + if length(pending_data) < length_ + # Wait for complete packet + break + end + + packet = pending_data[1:length_] + pending_data = pending_data[length_+1:end] + + handle_response(ipcon, packet) + end + end +end + +# internal +function dispatch_meta(ipcon::IPConnection, function_id, parameter, socket_id) + if function_id == CALLBACK_CONNECTED + cb = get(ipcon.registered_callbacks, CALLBACK_CONNECTED, nothing) + + if !isnothing(cb) + cb(parameter) + end + elseif function_id == CALLBACK_DISCONNECTED + if parameter != DISCONNECT_REASON_REQUEST + # need to do this here, the receive_loop is not allowed to + # hold the socket_lock because this could cause a deadlock + # with a concurrent call to the (dis-)connect function + lock(ipcon.socket_lock) do + # don't close the socket if it got disconnected or + # reconnected in the meantime + if !isnothing(ipcon.socket) && ipcon.socket_id == socket_id + # end disconnect probe thread + put!(ipcon.disconnect_probe_queue, true) + wait(ipcon.disconnect_probe_thread) # FIXME: use a timeout? + ipcon.disconnect_probe_thread = nothing + + # close socket + close(ipcon.socket) + ipcon.socket = nothing + end + end + end + + # FIXME: wait a moment here, otherwise the next connect + # attempt will succeed, even if there is no open server + # socket. the first receive will then fail directly + sleep(0.1) + + cb = get(ipcon.registered_callbacks, CALLBACK_DISCONNECTED, nothing) + + if !isnothing(cb) + cb(parameter) + end + + if parameter != DISCONNECT_REASON_REQUEST && ipcon.auto_reconnect && ipcon.auto_reconnect_allowed + ipcon.auto_reconnect_pending = true + retry = true + + # block here until reconnect. this is okay, there is no + # callback to deliver when there is no connection + while retry + retry = false + + lock(ipcon.socket_lock) do + if ipcon.auto_reconnect_allowed && isnothing(ipcon.socket) + try + connect_unlocked(ipcon, true) + catch e + retry = true + end + else + ipcon.auto_reconnect_pending = false + end + end + + if retry + sleep(0.1) + end + end + end + end +end + +# internal +function dispatch_packet(ipcon::IPConnection, packet) + uid = get_uid_from_data(packet) + length = get_length_from_data(packet) + function_id = get_function_id_from_data(packet) + payload = packet[8:end] # TODO: Have a close look with indexing!!! This is still like in python + + if function_id == CALLBACK_ENUMERATE + + cb = ipcon.registered_callbacks[CALLBACK_ENUMERATE] + + if isnothing(cb) + return + end + + if length(packet) != 34 + return # silently ignoring callback with wrong length + end + + uid, connected_uid, position, hardware_version, firmware_version, device_identifier, enumeration_type = unpack_payload(payload, "8s 8s c 3B 3B H B") + + cb(uid, connected_uid, position, hardware_version, + firmware_version, device_identifier, enumeration_type) + + return + end + + device = ipcon.devices[uid] + + if isnothing(device) + return + end + + try + device.check_validity() + catch e + return # silently ignoring callback for invalid device + end + + if -function_id in device.high_level_callbacks + hlcb = device.high_level_callbacks[-function_id] # [roles, options, data] + length, form = device.callback_formats[function_id] # FIXME: currently assuming that low-level callback has more than one element + + if length(packet) != length + return # silently ignoring callback with wrong length + end + + llvalues = unpack_payload(payload, form) + has_data = false + data = nothing + + if !isnothing(hlcb[1]["fixed_length"]) + length = hlcb[1]["fixed_length"] + else + length = llvalues[findfirst("stream_length", hlcb[0])] + end + + if !hlcb[1]["single_chunk"] + chunk_offset = llvalues[findfirst("stream_chunk_offset", hlcb[0])] + else + chunk_offset = 0 + end + + chunk_data = llvalues[findfirst("stream_chunk_data", hlcb[0])] + + if isnothing(hlcb[2]) # no stream in-progress + if chunk_offset == 0 # stream starts + hlcb[2] = chunk_data + + if length(hlcb[2]) >= length_ # stream complete + has_data = true + data = hlcb[2][:length_] + hlcb[2] = nothing + end + else # ignore tail of current stream, wait for next stream start + #pass + end + else # stream in-progress + if chunk_offset != length(hlcb[2]) # stream out-of-sync + has_data = true + data = nothing + hlcb[2] = nothing + else # stream in-sync + hlcb[2] += chunk_data + + if length(hlcb[2]) >= length_ # stream complete + has_data = true + data = hlcb[2][:length] + hlcb[2] = nothing + end + end + end + + cb = device.registered_callbacks[-function_id] + + if has_data && !isnothing(cb) + result = [] + + for (role, llvalue) in zip(hlcb[0], llvalues) + if role == "stream_chunk_data" + append!(result, data) + elseif isnothing(role) + append!(result, llvalue) + end + end + + cb(tuple(result)...) + end + end + + cb = device.registered_callbacks[function_id] + + if !isnothing(cb) + length, form = get(device.callback_formats, function_id, (nothing, nothing)) + + if isnothing(length_) + return # silently ignore registered but unknown callback + end + + if length(packet) != length_ + return # silently ignoring callback with wrong length + end + + if length(form) == 0 + cb() + elseif !(' ' in form) + cb(unpack_payload(payload, form)) + else + cb(unpack_payload(payload, form)...) + end + end +end + +# internal +function callback_loop(ipcon::IPConnection) + callback = ipcon.callback + while true + kind, data = take!(callback.queue) + + # FIXME: cannot hold callback lock here because this can + # deadlock due to an ordering problem with the socket lock + #with callback.lock: + if true + if kind == QUEUE_EXIT + break + elseif kind == QUEUE_META + dispatch_meta(ipcon, data...) + elseif kind == QUEUE_PACKET + # don't dispatch callbacks when the receive thread isn't running + if callback.packet_dispatch_allowed + dispatch_packet(ipcon, data) + end + end + end + end +end + +# internal +# NOTE: the disconnect probe thread is not allowed to hold the socket_lock at any +# time because it is created and joined while the socket_lock is locked +function disconnect_probe_loop(ipcon::IPConnection) + disconnect_probe_queue = ipcon.disconnect_probe_queue + request, _, _ = create_packet_header(ipcon, nothing, 8, FUNCTION_DISCONNECT_PROBE) + + while true + # Here comes a crude way to express the following Pyrhon connected + # try + # disconnect_probe_queue.get(true, DISCONNECT_PROBE_INTERVAL) + # break + # catch queue.Empty + # pass + + wait_disconnect(timeout) = begin + t = @async take!(disconnect_probe_queue) + for i=1:20 + if istaskdone(t) + return true + end + sleep(timeout / N ) + end + return false + end + + if wait_disconnect(DISCONNECT_PROBE_INTERVAL) + break + end + + if ipcon.disconnect_probe_flag + try + lock(self.socket_send_lock) do + while true + try + send(ipcon.socket, request) + break + catch e + #socket.timeout + continue + end + end + end + catch e + #socket.error + handle_disconnect_by_peer(ipcon, DISCONNECT_REASON_ERROR, ipcon.socket_id, false) + break + end + else + ipcon.disconnect_probe_flag = true + end + end +end + +# internal +function send(ipcon::IPConnection, packet) + @warn "locking within send" + lock(ipcon.socket_lock) do + @warn "locked socket" + if isnothing(ipcon.socket) + throw(TinkerforgeNotConnectedError("Not connected")) + end + + try + lock(ipcon.socket_send_lock) do + @warn "locked send" + Base.write(ipcon.socket, packet) + end + catch e + #socket.error + handle_disconnect_by_peer(ipcon, DISCONNECT_REASON_ERROR, nothing, true) + throw(TinkerforgeNotConnectedError("Not connected")) + end + + ipcon.disconnect_probe_flag = false + end +end + +# internal +function send_request(device::TinkerforgeDevice, function_id::Symbol, data, form, length_ret, form_ret) + ipcon = device.ipcon + payload = py"pack_payload"(data, form) + header, response_expected, sequence_number = create_packet_header(ipcon, device, 8 + length(payload), device.id_definitions[function_id]) + request = header * payload + + @warn "going into if" response_expected + + if response_expected + @warn "locking" + lock(device.request_lock) do + @warn "locked" + device.expected_response_function_id = function_id + device.expected_response_sequence_number = sequence_number + + try + @warn "try sending" + send(ipcon, request) + @warn "sending done" + + while true + # Here comes a crude way to express the following Python code + # response = device.response_queue.get(true, self.timeout) + + wait_get(timeout) = begin + t = @async take!(device.response_queue) + for i=1:20 + if istaskdone(t) + return fetch(t) + end + sleep(timeout / N ) + end + return false + end + + @warn "waiting for get" + response = wait_get(ipcon.timeout) + if response == false + throw(TinkerforgeTimeoutError("Timeout occured")) + end + + if function_id == get_function_id_from_data(response) && sequence_number == get_sequence_number_from_data(response) + # ignore old responses that arrived after the timeout expired, but before setting + # expected_response_function_id and expected_response_sequence_number back to None + break + end + end + catch e + #queue.Empty + if e isa TinkerforgeTimeoutError + msg = "Did not receive response for function $function_id in time" + throw(TinkerforgeTimeoutError(msg)) + end + finally + device.expected_response_function_id = nothing + device.expected_response_sequence_number = nothing + end + end + + error_code = get_error_code_from_data(response) + + if error_code == 0 + if length_ret == 0 + length_ret = 8 # setter with response-expected enabled + end + + if length(response) != length_ret + msg = "Expected response of $length_ret byte for function ID $function_id, got $(length(response)) byte instead" + throw(TinkerforgeWrongResponseLengthError(msg)) + end + elseif error_code == 1 + msg = "Got invalid parameter for function $function_id" + throw(TinkerforgeInvalidParameterError(msg)) + elseif error_code == 2 + msg = "Function $function_id is not supported" + throw(TinkerforgeNotSupportedError(msg)) + else + msg = "Function $function_id returned an unknown error" + throw(TinkerforgeUnknownErrorCodeError(msg)) + end + + if length(form_ret) > 0 + return unpack_payload(response[8:end], form_ret) + end + else + @warn "sending without an expected response" + send(ipcon, request) + end +end + +# internal +function get_next_sequence_number(ipcon::IPConnection) + lock(ipcon.sequence_number_lock) do + sequence_number = ipcon.next_sequence_number + 1 + ipcon.next_sequence_number = sequence_number % 15 + return sequence_number + end +end + +# internal +function handle_response(ipcon::IPConnection, packet) + ipcon.disconnect_probe_flag = false + + function_id = get_function_id_from_data(packet) + sequence_number = get_sequence_number_from_data(packet) + + if sequence_number == 0 && function_id == CALLBACK_ENUMERATE + if CALLBACK_ENUMERATE in ipcon.registered_callbacks + enqueue!(ipcon.callback.queue, (QUEUE_PACKET, packet)) + end + + return + end + + uid = get_uid_from_data(packet) + device = ipcon.devices[uid] + + if isnothing(device) + return # Response from an unknown device, ignoring it + end + + if sequence_number == 0 + if function_id in device.registered_callbacks || -function_id in device.high_level_callbacks + enqueue!(ipcon.callback.queue, (QUEUE_PACKET, packet)) + end + + return + end + + if device.expected_response_function_id == function_id && device.expected_response_sequence_number == sequence_number + enqueue!(device.response_queue, packet) + return + end + + # Response seems to be OK, but can't be handled +end + +# internal +function handle_disconnect_by_peer(ipcon::IPConnection, disconnect_reason::String, socket_id::Integer, disconnect_immediately::Bool) + # NOTE: assumes that socket_lock is locked if disconnect_immediately is true + + ipcon.auto_reconnect_allowed = true + + if disconnect_immediately + disconnect_unlocked(ipcon) + end + + enqueue!(ipcon.callback.queue, (QUEUE_META, (CALLBACK_DISCONNECTED, disconnect_reason, socket_id))) +end + +# internal +function create_packet_header(ipcon::IPConnection, device::TinkerforgeDevice, length_::Integer, function_id::Integer) + uid = BROADCAST_UID + sequence_number = get_next_sequence_number(ipcon) + r_bit = 0 + + if !isnothing(device) + uid = device.uid + + if get_response_expected(device, function_id) + r_bit = 1 + end + end + + sequence_number_and_options = (sequence_number << 4) | (r_bit << 3) + + return (pack_struct(" +# Copyright (C) 2011-2012 Olaf Lüke +# +# Redistribution and use in source and binary forms of this file, +# with or without modification, are permitted. See the Creative +# Commons Zero (CC0 1.0) License for more details. + +py""" +import struct +import socket +import sys +import time +import os +import math +import hmac +import hashlib +import errno +import threading + +try: + import queue # Python 3 +except ImportError: + import Queue as queue # Python 2 + +def pack_struct(format, data): + #format = bytearray(format.encode()) + print("bla") + print(data) + print("bla") + return struct.pack(format, *data) + +def unpack_struct(format, data): + #format = bytearray(format.encode()) + return struct.unpack(format, *data) + +def urandom(size): + return os.urandom(size) + +# internal +def get_uid_from_data(data): + return struct.unpack('> 4) & 0x0F + +# internal +def get_error_code_from_data(data): + return (struct.unpack('> 6) & 0x03 + +BASE58 = '123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ' + +# internal +def base58encode(value): + encoded = '' + + while value >= 58: + div, mod = divmod(value, 58) + encoded = BASE58[mod] + encoded + value = div + + return BASE58[value] + encoded + +# internal +def base58decode(encoded): + value = 0 + column_multiplier = 1 + + for c in encoded[::-1]: + try: + column = BASE58.index(c) + except ValueError: + raise Error(Error.INVALID_UID, 'UID "{0}" contains invalid character'.format(encoded), suppress_context=True) + + value += column * column_multiplier + column_multiplier *= 58 + + return value + +# internal +def uid64_to_uid32(uid64): + value1 = uid64 & 0xFFFFFFFF + value2 = (uid64 >> 32) & 0xFFFFFFFF + + uid32 = (value1 & 0x00000FFF) + uid32 |= (value1 & 0x0F000000) >> 12 + uid32 |= (value2 & 0x0000003F) << 16 + uid32 |= (value2 & 0x000F0000) << 6 + uid32 |= (value2 & 0x3F000000) << 2 + + return uid32 + +# internal +def create_chunk_data(data, chunk_offset, chunk_length, chunk_padding): + chunk_data = data[chunk_offset:chunk_offset + chunk_length] + + if len(chunk_data) < chunk_length: + chunk_data += [chunk_padding] * (chunk_length - len(chunk_data)) + + return chunk_data + +if sys.hexversion < 0x03000000: + # internal + def create_char(value): # return str with len() == 1 and ord() <= 255 + if isinstance(value, str) and len(value) == 1: # Python2 str satisfies ord() <= 255 by default + return value + elif isinstance(value, unicode) and len(value) == 1: # pylint: disable=undefined-variable + code_point = ord(value) + + if code_point <= 255: + return chr(code_point) + else: + raise ValueError('Invalid char value: ' + repr(value)) + elif isinstance(value, bytearray) and len(value) == 1: # Python2 bytearray satisfies item <= 255 by default + return chr(value[0]) + elif isinstance(value, int) and value >= 0 and value <= 255: + return chr(value) + else: + raise ValueError('Invalid char value: ' + repr(value)) +else: + # internal + def create_char(value): # return str with len() == 1 and ord() <= 255 + if isinstance(value, str) and len(value) == 1 and ord(value) <= 255: + return value + elif isinstance(value, (bytes, bytearray)) and len(value) == 1: # Python3 bytes/bytearray satisfies item <= 255 by default + return chr(value[0]) + elif isinstance(value, int) and value >= 0 and value <= 255: + return chr(value) + else: + raise ValueError('Invalid char value: ' + repr(value)) + +if sys.hexversion < 0x03000000: + # internal + def create_char_list(value, expected_type='char list'): # return list of str with len() == 1 and ord() <= 255 for all items + if isinstance(value, list): + return map(create_char, value) + elif isinstance(value, str): # Python2 str satisfies ord() <= 255 by default + return list(value) + elif isinstance(value, unicode): # pylint: disable=undefined-variable + chars = [] + + for char in value: + code_point = ord(char) + + if code_point <= 255: + chars.append(chr(code_point)) + else: + raise ValueError('Invalid {0} value: {1}'.format(expected_type, repr(value))) + + return chars + elif isinstance(value, bytearray): # Python2 bytearray satisfies item <= 255 by default + return map(chr, value) + else: + raise ValueError('Invalid {0} value: {1}'.format(expected_type, repr(value))) +else: + # internal + def create_char_list(value, expected_type='char list'): # return list of str with len() == 1 and ord() <= 255 for all items + if isinstance(value, list): + return list(map(create_char, value)) + elif isinstance(value, str): + chars = list(value) + + for char in chars: + if ord(char) > 255: + raise ValueError('Invalid {0} value: {1}'.format(expected_type, repr(value))) + + return chars + elif isinstance(value, (bytes, bytearray)): # Python3 bytes/bytearray satisfies item <= 255 by default + return list(map(chr, value)) + else: + raise ValueError('Invalid {0} value: {1}'.format(expected_type, repr(value))) + +if sys.hexversion < 0x03000000: + # internal + def create_string(value): # return str with ord() <= 255 for all chars + if isinstance(value, str): # Python2 str satisfies ord() <= 255 by default + return value + elif isinstance(value, unicode): # pylint: disable=undefined-variable + chars = [] + + for char in value: + code_point = ord(char) + + if code_point <= 255: + chars.append(chr(code_point)) + else: + raise ValueError('Invalid string value: {0}'.format(repr(value))) + + return ''.join(chars) + elif isinstance(value, bytearray): # Python2 bytearray satisfies item <= 255 by default + chars = [] + + for byte in value: + chars.append(chr(byte)) + + return ''.join(chars) + else: + return ''.join(create_char_list(value, expected_type='string')) +else: + # internal + def create_string(value): # return str with ord() <= 255 for all chars + if isinstance(value, str): + for char in value: + if ord(char) > 255: + raise ValueError('Invalid string value: {0}'.format(repr(value))) + + return value + elif isinstance(value, (bytes, bytearray)): # Python3 bytes/bytearray satisfies item <= 255 by default + chars = [] + + for byte in value: + chars.append(chr(byte)) + + return ''.join(chars) + else: + return ''.join(create_char_list(value, expected_type='string')) + +# internal +def pack_payload(data, form): + if sys.hexversion < 0x03000000: + packed = '' + else: + packed = b'' + + for f, d in zip(form.split(' '), data): + if '!' in f: + if len(f) > 1: + if int(f.replace('!', '')) != len(d): + raise ValueError('Incorrect bool list length') + + p = [0] * int(math.ceil(len(d) / 8.0)) + + for i, b in enumerate(d): + if b: + p[i // 8] |= 1 << (i % 8) + + packed += struct.pack('<{0}B'.format(len(p)), *p) + else: + packed += struct.pack(' 1: + packed += struct.pack('<' + f, *d) + else: + packed += struct.pack('<' + f, d) + else: + if len(f) > 1: + packed += struct.pack('<' + f, *list(map(lambda char: bytes([ord(char)]), d))) + else: + packed += struct.pack('<' + f, bytes([ord(d)])) + elif 's' in f: + if sys.hexversion < 0x03000000: + packed += struct.pack('<' + f, d) + else: + packed += struct.pack('<' + f, bytes(map(ord, d))) + elif len(f) > 1: + packed += struct.pack('<' + f, *d) + else: + packed += struct.pack('<' + f, d) + + return packed + +# Mark start and end of the unpack_payload funtion, so that the +# saleae bindings can extract it +# UNPACK_PAYLOAD_CUT_HERE +# internal +def unpack_payload(data, form): + ret = [] + + for f in form.split(' '): + o = f + + if '!' in f: + if len(f) > 1: + f = '{0}B'.format(int(math.ceil(int(f.replace('!', '')) / 8.0))) + else: + f = 'B' + + f = '<' + f + length = struct.calcsize(f) + x = struct.unpack(f, data[:length]) + + if '!' in o: + y = [] + + if len(o) > 1: + for i in range(int(o.replace('!', ''))): + y.append(x[i // 8] & (1 << (i % 8)) != 0) + else: + y.append(x[0] != 0) + + x = tuple(y) + + if 'c' in f: + if sys.hexversion < 0x03000000: + if len(x) > 1: + ret.append(x) + else: + ret.append(x[0]) + else: + if len(x) > 1: + ret.append(tuple(map(lambda item: chr(ord(item)), x))) + else: + ret.append(chr(ord(x[0]))) + elif 's' in f: + if sys.hexversion < 0x03000000: + s = x[0] + else: + s = ''.join(map(chr, x[0])) + + i = s.find('\x00') + + if i >= 0: + s = s[:i] + + ret.append(s) + elif len(x) > 1: + ret.append(x) + else: + ret.append(x[0]) + + data = data[length:] + + if len(ret) == 1: + return ret[0] + else: + return ret + +""" \ No newline at end of file diff --git a/julia/julia_common.py b/julia/julia_common.py new file mode 100644 index 00000000..0916abfd --- /dev/null +++ b/julia/julia_common.py @@ -0,0 +1,183 @@ +# -*- coding: utf-8 -*- + +""" +Julia Generator +Copyright (C) 2012-2013, 2017, 2019-2020 Matthias Bolte +Copyright (C) 2011-2013 Olaf Lüke +Copyright (C) 2020 Erik Fleckstein + +julia_common.py: Common library for generation of Julia bindings and documentation + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public +License along with this program; if not, write to the +Free Software Foundation, Inc., 59 Temple Place - Suite 330, +Boston, MA 02111-1307, USA. +""" + +from generators import common + +class JuliaDevice(common.Device): + def get_julia_import_name(self): + return self.get_category().under + '_' + self.get_name().under + + def get_julia_struct_name(self): + return self.get_category().camel + self.get_name().camel + +class JuliaPacket(common.Packet): + def get_julia_parameters(self, high_level=False): + parameters = [] + + for element in self.get_elements(direction='in', high_level=high_level): + parameters.append(element.get_name().under) + + return ', '.join(parameters) + +class JuliaElement(common.Element): + julia_types = { + 'int8': 'Integer', + 'uint8': 'Integer', + 'int16': 'Integer', + 'uint16': 'Integer', + 'int32': 'Integer', + 'uint32': 'Integer', + 'int64': 'Integer', + 'uint64': 'Integer', + 'float': 'Real', + 'bool': 'Bool', + 'char': 'Char', + 'string': 'String' + } + + julia_struct_formats = { + 'int8': 'b', + 'uint8': 'B', + 'int16': 'h', + 'uint16': 'H', + 'int32': 'i', + 'uint32': 'I', + 'int64': 'q', + 'uint64': 'Q', + 'float': 'f', + 'bool': '!', + 'char': 'c', + 'string': 's' + } + + julia_default_item_values = { + 'int8': '0', + 'uint8': '0', + 'int16': '0', + 'uint16': '0', + 'int32': '0', + 'uint32': '0', + 'int64': '0', + 'uint64': '0', + 'float': '0.0', + 'bool': 'False', + 'char': "'\\0'", + 'string': "nothing" + } + + julia_parameter_coercions = { + 'int8': ('Int8({0})', 'convert(Vector{{Int8}}, {0})'), + 'uint8': ('UInt8({0})', 'convert(Vector{{UInt8}}, {0})'), + 'int16': ('Int16({0})', 'convert(Vector{{Int16}}, {0})'), + 'uint16': ('UInt16({0})', 'convert(Vector{{UInt16}}, {0})'), + 'int32': ('Int32({0})', 'convert(Vector{{Int32}}, {0})'), + 'uint32': ('UInt32({0})', 'convert(Vector{{UInt32}}, {0})'), + 'int64': ('Int64({0})', 'convert(Vector{{Int64}}, {0})'), + 'uint64': ('UInt64({0})', 'convert(Vector{{UInt64}}, {0})'), + 'float': ('Real({0})', 'convert(Vector{{Float}}, {0})'), + 'bool': ('Bool({0})', 'convert(Vector{{Bool}}, {0})'), + 'char': ('String({0})', 'convert(Vector{{String}}, {0})'), + 'string': ('String({0})', 'convert(Vector{{String}}, {0})') + } + + def format_value(self, value): + if isinstance(value, list): + result = [] + + for subvalue in value: + result.append(self.format_value(subvalue)) + + return '[{0}]'.format(', '.join(result)) + + type_ = self.get_type() + + if type_ == 'float': + return common.format_float(value) + + if type_ == 'bool': + return str(bool(value)) + + if type_ in ['char', 'string']: + return '"{0}"'.format(value.replace('"', '\\"')) + + return str(value) + + def get_julia_name(self, index=None): + return self.get_name(index=index).under + + def get_julia_type(self, cardinality=None): + assert cardinality == None or (isinstance(cardinality, int) and cardinality > 0), cardinality + + julia_type = JuliaElement.julia_types[self.get_type()] + + if cardinality == None: + cardinality = self.get_cardinality() + + if cardinality == 1 or self.get_type() == 'string': + return julia_type + + return 'Vector{{{0}}}'.format(julia_type) + + def get_julia_struct_format(self): + f = JuliaElement.julia_struct_formats[self.get_type()] + cardinality = self.get_cardinality() + + if cardinality > 1: + f = str(cardinality) + f + + return f + + def get_julia_default_item_value(self): + value = JuliaElement.julia_default_item_values[self.get_type()] + + if value == None: + common.GeneratorError('Invalid array item type: ' + self.get_type()) + + return value + + def get_julia_parameter_coercion(self): + coercion = JuliaElement.julia_parameter_coercions[self.get_type()] + + if self.get_cardinality() == 1: + return coercion[0] + else: + return coercion[1] + +class JuliaGeneratorTrait: + def get_bindings_name(self): + return 'julia' + + def get_bindings_display_name(self): + return 'Julia' + + def get_doc_null_value_name(self): + return 'nothing' + + def get_doc_formatted_param(self, element): + return element.get_name().under + + def generates_high_level_callbacks(self): + return True diff --git a/julia/readme.txt b/julia/readme.txt new file mode 100644 index 00000000..b1ac2fad --- /dev/null +++ b/julia/readme.txt @@ -0,0 +1,18 @@ +Tinkerforge Python Bindings +=========================== + +The Python bindings allow you to control Tinkerforge Bricks and Bricklets from +your Python scripts. This ZIP file contains: + + source/ -- source code of the bindings (install with setup.py script) + examples/ -- examples for every Brick and Bricklet + +For more information about the Python bindings (including setup instructions) +go to: + + https://www.tinkerforge.com/en/doc/Software/API_Bindings_Python.html (English) + https://www.tinkerforge.com/de/doc/Software/API_Bindings_Python.html (German) + +The Python bindings are also available from the Python Package Index (PyPI): + + https://pypi.python.org/pypi/tinkerforge diff --git a/julia/test.jl b/julia/test.jl new file mode 100644 index 00000000..867a5de8 --- /dev/null +++ b/julia/test.jl @@ -0,0 +1,5 @@ +using Tinkerforge +ipcon = IPConnection("localhost", 4223) +connect(ipcon) +lcd = BrickletLCD20x4("BL1", ipcon) +backlight_off(lcd) \ No newline at end of file diff --git a/julia/test_ip_connection.py b/julia/test_ip_connection.py new file mode 100644 index 00000000..43287f58 --- /dev/null +++ b/julia/test_ip_connection.py @@ -0,0 +1,161 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import sys +from ip_connection import create_char, create_char_list, create_string, pack_payload, unpack_payload + +def b(value): + if sys.hexversion < 0x03000000: + return value + else: + return bytes(map(ord, value)) + +# +# char +# + +assert(create_char('a') == 'a') # str + +if sys.hexversion < 0x03000000: + assert(create_char(u'a') == 'a') # unicode +else: + assert(create_char(b'a') == 'a') # bytes + +assert(create_char(bytearray([97])) == 'a') # bytearray +assert(create_char(97) == 'a') # int + +for c in range(256): + k = (c + 1) % 256 + + assert(create_char(chr(c)) == chr(c)) # str + assert(create_char(chr(c)) != chr(k)) # str + + if sys.hexversion < 0x03000000: + assert(create_char(unichr(c)) == chr(c)) # unicode + assert(create_char(unichr(c)) != chr(k)) + else: + assert(create_char(bytes([c])) == chr(c)) # bytes + assert(create_char(bytes([c])) != chr(k)) # bytes + + assert(create_char(bytearray([c])) == chr(c)) # bytearray + assert(create_char(bytearray([c])) != chr(k)) # bytearray + assert(create_char(c) == chr(c)) # int + assert(create_char(c) != chr(k)) # int + +try: + create_char('ab') # str + assert(False) +except: + pass + +if sys.hexversion < 0x03000000: + try: + create_char(u'ab') # unicode + assert(False) + except: + pass +else: + try: + create_char(b'ab') # bytes + assert(False) + except: + pass + +try: + create_char(bytearray([42, 17])) # bytearray + assert(False) +except: + pass + +try: + create_char([42, 17]) # int + assert(False) +except: + pass + +try: + create_char(256) # int + assert(False) +except: + pass + +# +# char list +# + +assert(create_char_list('') == []) # str +assert(create_char_list('a') == ['a']) # str +assert(create_char_list('ab') == ['a', 'b']) # str +assert(create_char_list([]) == []) +assert(create_char_list(['a']) == ['a']) # str +assert(create_char_list(['a', 'b']) == ['a', 'b']) # str + +if sys.hexversion < 0x03000000: + assert(create_char_list(u'') == []) # unicode + assert(create_char_list(u'a') == ['a']) # unicode + assert(create_char_list(u'ab') == ['a', 'b']) # unicode + assert(create_char_list([u'a']) == ['a']) # unicode + assert(create_char_list([u'a', u'b']) == ['a', 'b']) # unicode +else: + assert(create_char_list(b'') == []) # bytes + assert(create_char_list(b'a') == ['a']) # bytes + assert(create_char_list(b'ab') == ['a', 'b']) # bytes + assert(create_char_list([b'a']) == ['a']) # bytes + assert(create_char_list([b'a', b'b']) == ['a', 'b']) # bytes + +assert(create_char_list(bytearray([])) == []) # bytearray +assert(create_char_list(bytearray([97])) == ['a']) # bytearray +assert(create_char_list(bytearray([97, 98])) == ['a', 'b']) # bytearray +assert(create_char_list([97]) == ['a']) # int +assert(create_char_list([97, 98]) == ['a', 'b']) # int + +# +# string +# + +assert(create_string('') == '') # str +assert(create_string('a') == 'a') # str +assert(create_string('ab') == 'ab') # str +assert(create_string([]) == '') +assert(create_string(['a']) == 'a') # str +assert(create_string(['a', 'b']) == 'ab') # str + +if sys.hexversion < 0x03000000: + assert(create_string(u'') == '') # unicode + assert(create_string(u'a') == 'a') # unicode + assert(create_string(u'ab') == 'ab') # unicode + assert(create_string([u'a']) == 'a') # unicode + assert(create_string([u'a', u'b']) == 'ab') # unicode +else: + assert(create_string(b'') == '') # bytes + assert(create_string(b'a') == 'a') # bytes + assert(create_string(b'ab') == 'ab') # bytes + assert(create_string([b'a']) == 'a') # bytes + assert(create_string([b'a', b'b']) == 'ab') # bytes + +assert(create_string(bytearray([])) == '') # bytearray +assert(create_string(bytearray([97])) == 'a') # bytearray +assert(create_string(bytearray([97, 98])) == 'ab') # bytearray +assert(create_string([97]) == 'a') # int +assert(create_string([97, 98]) == 'ab') # int + +# +# pack_payload +# + +assert(pack_payload(('a',), 's') == b('a')) +assert(pack_payload(('abc',), '5s') == b('abc\0\0')) +assert(pack_payload(('abc\xff',), '5s') == b('abc\xff\0')) +assert(pack_payload(('a',), 'c') == b('a')) +assert(pack_payload((['a', 'b', 'c'],), '3c') == b('abc')) + +# +# unpack_payload +# + +assert(unpack_payload(b('a'), 's') == 'a') +assert(unpack_payload(b('abc'), '3s') == 'abc') +assert(unpack_payload(b('abc\xff\0'), '5s') == 'abc\xff') +assert(unpack_payload(b('a'), 'c') == 'a') +assert(unpack_payload(b('abc'), '3c') == ('a', 'b', 'c')) +assert(unpack_payload(b('a\xff\0'), '3c') == ('a', '\xff', '\0')) diff --git a/julia/test_python_bindings.py b/julia/test_python_bindings.py new file mode 100644 index 00000000..4f159895 --- /dev/null +++ b/julia/test_python_bindings.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Python Bindings Tester +Copyright (C) 2012-2014, 2017-2018 Matthias Bolte + +test_python_bindings.py: Tests the Python bindings + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public +License along with this program; if not, write to the +Free Software Foundation, Inc., 59 Temple Place - Suite 330, +Boston, MA 02111-1307, USA. +""" + +import sys + +if sys.hexversion < 0x3040000: + print('Python >= 3.4 required') + sys.exit(1) + +import os +import importlib.util +import importlib.machinery + +def create_generators_module(): + generators_dir = os.path.split(os.path.dirname(os.path.realpath(__file__)))[0] + + if sys.hexversion < 0x3050000: + generators_module = importlib.machinery.SourceFileLoader('generators', os.path.join(generators_dir, '__init__.py')).load_module() + else: + generators_spec = importlib.util.spec_from_file_location('generators', os.path.join(generators_dir, '__init__.py')) + generators_module = importlib.util.module_from_spec(generators_spec) + + generators_spec.loader.exec_module(generators_module) + + sys.modules['generators'] = generators_module + +if 'generators' not in sys.modules: + create_generators_module() + +from generators import common + +class PythonTester(common.Tester): + def __init__(self, root_dir, python, extra_paths): + common.Tester.__init__(self, 'python', '.py', root_dir, comment=python, subdirs=['examples', 'source'], extra_paths=extra_paths) + + self.python = python + + def test(self, cookie, tmp_dir, path, extra): + args = [self.python, + '-c', + 'import py_compile; py_compile.compile("{0}", doraise=True)'.format(path)] + + self.execute(cookie, args) + +class PylintTester(common.Tester): + def __init__(self, root_dir, python, comment, extra_paths): + common.Tester.__init__(self, 'python', '.py', root_dir, comment=comment, subdirs=['examples', 'source'], extra_paths=extra_paths) + + self.python = python + + def test(self, cookie, tmp_dir, path, extra): + teardown = None + + if self.python == 'python3': + with open(path, 'r') as f: + code = f.read() + + code = code.replace('raw_input(', 'input(') + path_check = path.replace('.py', '_check.py') + + with open(path_check, 'w') as f: + f.write(code) + + path = path_check + teardown = lambda: [os.remove(path)] + + args = [self.python, + '-c', + 'import sys; sys.path.insert(0, "{0}"); import pylint; pylint.run_pylint()'.format(os.path.join(tmp_dir, 'source')), + '-E', + '--disable=no-name-in-module', + path] + + self.execute(cookie, args, teardown=teardown) + +def test(root_dir): + extra_paths = [os.path.join(root_dir, '../../weather-station/demo/starter_kit_weather_station_demo/main.py'), + os.path.join(root_dir, '../../weather-station/write_to_lcd/python/weather_station.py'), + os.path.join(root_dir, '../../hardware-hacking/remote_switch/python/remote_switch.py'), + os.path.join(root_dir, '../../hardware-hacking/smoke_detector/python/smoke_detector.py'), + os.path.join(root_dir, '../../blinkenlights/demo/starter_kit_blinkenlights_demo/main.py'), + os.path.join(root_dir, '../../blinkenlights/demo/starter_kit_blinkenlights_demo/fire_widget.py'), + os.path.join(root_dir, '../../blinkenlights/demo/starter_kit_blinkenlights_demo/pong_widget.py'), + os.path.join(root_dir, '../../blinkenlights/demo/starter_kit_blinkenlights_demo/tetris_widget.py'), + os.path.join(root_dir, '../../blinkenlights/demo/starter_kit_blinkenlights_demo/images_widget.py'), + os.path.join(root_dir, '../../blinkenlights/demo/starter_kit_blinkenlights_demo/rainbow_widget.py'), + os.path.join(root_dir, '../../blinkenlights/demo/starter_kit_blinkenlights_demo/text_widget.py'), + os.path.join(root_dir, '../../blinkenlights/fire/python/fire.py'), + os.path.join(root_dir, '../../blinkenlights/games/python/keypress.py'), + os.path.join(root_dir, '../../blinkenlights/games/python/pong.py'), + os.path.join(root_dir, '../../blinkenlights/games/python/repeated_timer.py'), + os.path.join(root_dir, '../../blinkenlights/games/python/tetris.py'), + os.path.join(root_dir, '../../blinkenlights/images/python/images.py'), + os.path.join(root_dir, '../../blinkenlights/rainbow/python/rainbow.py'), + os.path.join(root_dir, '../../blinkenlights/text/python/text.py')] + + if not PythonTester(root_dir, 'python', extra_paths).run(): + return False + + # FIXME: doesn't handle PyQt related super false-positves yet + if not PylintTester(root_dir, 'python', 'pylint', []).run():#extra_paths).run(): + return False + + if not PythonTester(root_dir, 'python3', extra_paths).run(): + return False + + # FIXME: doesn't handle PyQt related super false-positves yet + return PylintTester(root_dir, 'python3', 'pylint3', []).run()#extra_paths).run() + +if __name__ == '__main__': + common.dockerize('python', __file__) + + test(os.getcwd()) diff --git a/juliapy/LICENSE b/juliapy/LICENSE new file mode 100644 index 00000000..0e259d42 --- /dev/null +++ b/juliapy/LICENSE @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. diff --git a/juliapy/Project.toml.template b/juliapy/Project.toml.template new file mode 100644 index 00000000..fee1cfc6 --- /dev/null +++ b/juliapy/Project.toml.template @@ -0,0 +1,16 @@ +name = "PyTinkerforge" +uuid = "7f8b0ca4-cb0e-484e-9ab2-c46751bbb33a" +authors = ["Jonas Schumacher "] +version = "<>" + +[deps] +DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" +Sockets = "6462fe0b-24de-5631-8697-dd941f90decc" +PyCall = "438e738f-606a-5dbb-bf0a-cddfbfd45ab0" +Conda = "8f4d0f93-b110-5947-807f-2305c1781a2d" + +[extras] +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + +[targets] +test = ["Test"] \ No newline at end of file diff --git a/juliapy/PyTinkerforge.jl b/juliapy/PyTinkerforge.jl new file mode 100644 index 00000000..48980dfe --- /dev/null +++ b/juliapy/PyTinkerforge.jl @@ -0,0 +1,21 @@ +module PyTinkerforge + +using Sockets +using PyCall +using Conda +using DocStringExtensions + +function __init__() + try + tinkerforge = pyimport("tinkerforge") + catch e + Conda.pip_interop(true) + Conda.pip("install", "Tinkerforge") + end +end + +include("ip_connection.jl") +include("devices/device_display_names.jl") +include("devices/device_factory.jl") + +end diff --git a/juliapy/changelog.txt b/juliapy/changelog.txt new file mode 100644 index 00000000..b12a6187 --- /dev/null +++ b/juliapy/changelog.txt @@ -0,0 +1,337 @@ +2011-11-20: 1.0.0 (a83b9e5) +- Initial version + +2011-12-13: 1.0.1 (6035526) +- Add callback thread to IPConnection (allows to call getters in callbacks) + +2011-12-29: 1.0.2 (c5f4962) +- Add __init__.py to source/tinkerforge/ + +2012-01-02: 1.0.3 (eb369d3) +- Fix thread exception at shutdown + +2012-02-15: 1.0.4 (ffd64f7) +- Add support for IMU Brick, Analog In Bricklet and Analog Out Bricklet + +2012-03-30: 1.0.5 (71aaa0a) +- Remove Python 3.2 bug (no decode function for str in 3.2) + +2012-04-27: 1.0.6 (6f0b9a5) +- Add sync rect support to Stepper Brick bindings + +2012-05-10: 1.0.7 (baa8705) +- Add version information to tinkerforge.egg +- Silently ignore messages from devices with unknown stack ID +- Don't generate register_callback method for devices without callbacks +- Add inline code documentation + +2012-05-15: 1.0.8 (ff4cc5b) +- Fix relative import and str packing problem with Python 3 + +2012-05-18: 1.0.9 (505bc29) +- Ensure that the answering device matches the expected type in + IPConnection.add_device + +2012-05-21: 1.0.10 (3406326) +- Fix device name decoding for add_device handling in Python 3 + +2012-05-22: 1.0.11 (368cd78) +- Don't let a thread join itself + +2012-05-24: 1.0.12 (f837011) +- Treat '-' and ' ' as equal in device name check for backward compatibility + +2012-06-15: 1.0.13 (bc93dc5) +- Fix handling of fragmented packets + +2012-06-28: 1.0.14 (3704047) +- Add RS485 support + +2012-06-29: 1.0.15 (55a3238) +- Add chip temperature and reset functions + +2012-07-01: 1.0.16 (d9ecec6) +- Add monoflop functionality to Dual Relay Bricklet API + +2012-07-03: 1.0.17 (afb45cf) +- Add time base, all-data function/callback and state callback to Stepper + Brick API + +2012-07-13: 1.0.18 (6ac52d1) +- Fix direction of get_all_data_period method in Stepper Brick API +- Make add_device thread-safe +- Ensure correct shutdown order of threads + +2012-08-01: 1.0.19 (f86a5f3) +- Fix race condition in add_device method +- Add monoflop functionality to IO-4 and IO-16 Bricklet API + +2012-09-17: 1.0.20 (dd8498f) +- Add WIFI support + +2012-09-26: 1.0.21 (c8c7862) +- Add getter for WIFI buffer status information +- Change WIFI certificate getter/setter to transfer bytes instead of a string +- Add API for setting of WIFI regulatory domain +- Add reconnect functionality to IPConnection (for WIFI Extension) +- Add API for Industrial Bricklets: Digital In 4, Digital Out 4 and Quad Relay +- Trim NUL characters from strings properly + +2012-09-28: 1.0.22 (69e6ae4) +- Add API for Barometer Bricklet + +2012-10-01: 1.0.23 (4454bda) +- Replace Barometer Bricklet calibrate function with getter/setter for + reference air pressure + +2012-10-12: 1.0.24 (5884dd5) +- Add get_usb_voltage function to Master Brick API +- Add Barometer Bricklet examples +- Handle difference between currentThread and current_thread to support + Python 2.5 +- Changed callback queue from class variable to instance variable + +2012-12-20: 1.0.25 (2b39606) +- Add API for Voltage/Current Bricklet +- Add API for GPS Bricklet + +2013-01-22: 2.0.0 (10c72f9) +- Add compatibility for Protocol 2.0 + +2013-01-25: 2.0.1 (13b1beb) +- Add support for custom characters in LCD Bricklets + +2013-01-31: 2.0.2 (47579a1) +- Fix char list packing in Python 3 + +2013-02-06: 2.0.3 (3db31c0) +- Add get/set_long_wifi_key functions to Master Brick API + +2013-02-19: 2.0.4 (3fd93d3) +- Reduce scope of request and socket lock to improve concurrency +- Improve and unify code for response expected flag handling +- Add get/set_wifi_hostname functions and callbacks for stack/USB voltage and + stack current to Master Brick API + +2013-02-22: 2.0.5 (9d5de14) +- Add get/set_range functions to Analog In Bricklet API +- Fix unlikely race condition in response packet handling +- Fix serialization of Unicode strings + +2013-04-02: 2.0.6 (eeb1f67) +- Add enable/disable functions for POSITION_REACHED and VELOCITY_REACHED + callbacks to Servo Brick API +- Add get/set_i2c_mode (100kHz/400kHz) functions to Temperature Bricklet API +- Add default text functions to LCD 20x4 Bricklet API +- Don't dispatch callbacks after disconnect +- Fix race condition in callback handling that could result in closing the + wrong socket +- Don't ignore socket errors when sending request packets +- Send a request packet at least every 10sec to improve WIFI disconnect + detection + +2013-05-14: 2.0.7 (b847401) +- Add Ethernet Extension support to Master Brick API +- Only send disconnect probe if there was no packet send or received for 5sec +- Fix deserialization of chars in Python 3 +- Add IMU Brick orientation and Barometer Bricklet averaging API + +2013-07-04: 2.0.8 (cdc19b0) +- Add support for PTC Bricklet and Industrial Dual 0-20mA Bricklet + +2013-08-23: 2.0.9 (4b2c2d2) +- Avoid race condition between disconnect probe thread and disconnect function + +2013-08-28: 2.0.10 (2251328) +- Add edge counters to Industrial Digital In 4, IO-4 and IO-16 Bricklet +- Make averaging length configurable for Analog In Bricklet + +2013-09-11: 2.0.11 (405931f) +- Fix signature of edge count functions in IO-16 Bricklet API + +2013-11-27: 2.0.12 (a97b7db) +- Add support for Distance US, Dual Button, Hall Effect, LED Strip, Line, + Moisture, Motion Detector, Multi Touch, Piezo Speaker, Remote Switch, + Rotary Encoder, Segment Display 4x7, Sound Intensity and Tilt Bricklet + +2013-12-19: 2.0.13 (9334f91) +- Add get/set_clock_frequency function to LED Strip Bricklet API +- Fix mixup of get/set_date_time_callback_period and + get/set_motion_callback_period in GPS Bricklet API +- Support addressing types of Intertechno and ELRO Home Easy devices in + Remote Switch Bricklet API + +2014-04-08: 2.1.0 (9124f8e) +- Add authentication support to IPConnection and Master Brick API + +2014-07-03: 2.1.1 (cdb00f1) +- Add support for WS2811 and WS2812 to LED Strip Bricklet API + +2014-08-11: 2.1.2 (a87f5bc) +- Add support for Color, NFC/RFID and Solid State Relay Bricklet +- Get rid of the egg and easy_install, use setuptools directly or pip instead + +2014-12-10: 2.1.3 (2718ddc) +- Handle EINTR error in receive loop + +2014-12-10: 2.1.4 (27725d5) +- Add support for RED Brick + +2015-07-28: 2.1.5 (725ccd3) +- Fix packing of Unicode chars +- Add DEVICE_DISPLAY_NAME constant to all Device classes +- Add functions for all Bricks to turn status LEDs on and off +- Avoid possible connection state race condition on connect +- Add support for IMU Brick 2.0, Accelerometer, Ambient Light 2.0, + Analog In 2.0, Analog Out 2.0, Dust Detector, Industrial Analog Out, + Industrial Dual Analog In, Laser Range Finder, Load Cell and RS232 Bricklet + +2015-11-17: 2.1.6 (158f00f) +- Add missing constant for 19200 baud to RS232 Bricklet API +- Add ERROR callback to RS232 Bricklet API +- Add set_break_condition function to RS232 Bricklet API +- Add unlimited illuminance range constant to Ambient Light Bricklet 2.0 API +- Break API to fix threshold min/max type mismatch in Ambient Light, Analog In + (2.0), Distance IR/US, Humidity, Linear Poti and Voltage Bricklet API +- Break API to fix bool return type mismatch in Servo Brick + (is_position_reached_callback_enabled and is_velocity_reached_callback_enabled + function), Accelerometer Bricklet (is_led_on function) and Load Cell Bricklet + (is_led_on function) API +- Don't decode non-ASCII strings and chars in Python 3 + +2016-01-06: 2.1.7 (3ade121) +- Add support for CO2, OLED 64x48 and 128x64, Thermocouple and UV Light Bricklet + +2016-02-09: 2.1.8 (5552d2c) +- Add support for Real-Time Clock Bricklet +- Break GPS Bricklet API to fix types of altitude and geoidal separation values + (get_altitude function and ALTITUDE callback) + +2016-06-29: 2.1.9 (9db7daa) +- Add support for WIFI Extension 2.0 to Master Brick API +- Add support for CAN Bricklet and RGB LED Bricklet +- Add DATETIME and ALARM callbacks to Real-Time Clock Bricklet API +- Avoid long/unbound connection timeout + +2016-09-08: 2.1.10 (2863e14) +- Add support for RGBW LEDs, channel mapping and SK6812RGBW (NeoPixel RGBW), + LPD8806 and ADA102 (DotStar) chip types to LED Strip Bricklet API + +2017-01-25: 2.1.11 (7aeee37) +- Add support for WIFI Extension 2.0 Mesh mode to Master Brick API +- Add get/set_status_led_config functions to Motion Detector Bricklet API +- Add sensor and fusion mode configuration functions to IMU Brick 2.0 API +- Fix enumerate callback unregistration + +2017-04-21: 2.1.12 (044bd9b) +- Add support for Silent Stepper Brick +- Add get/set_configuration functions to Laser Range Finder Bricklet API to + support Bricklets with LIDAR-Lite sensor hardware version 3 +- Add get_send_timeout_count function to all Brick APIs +- Avoid that the disconnect function can block on Windows for several seconds + +2017-05-11: 2.1.13 (3960b4a) +- Add support for GPS Bricklet 2.0 + +2017-07-26: 2.1.14 (fb903dc) +- Add support for RS485 Bricklet +- Add general streaming support +- Add SPITFP configuration and diagnostics functions to all Brick APIs to + configure and debug the communication between Bricks and Co-MCU Bricklets +- Remove unused get_current_consumption function from Silent Stepper Brick API +- Increase minimum Python version to 2.6 + +2017-11-20: 2.1.15 (f235e3f) +- Add support for DMX, Humidity 2.0, Motorized Linear Poti, RGB LED Button, + RGB LED Matrix and Thermal Imaging Bricklet +- Add get/set_sbas_config functions to GPS Bricklet 2.0 API +- Accept wider range of types for char (str, unicode, bytes, bytearray with + length 1 and int) and list of char / string (str, unicode, bytes, bytearray + and list of char), all type conversion is done with ord / chr + +2018-02-28: 2.1.16 (da741b9) +- Add support for Analog In 3.0, Remote Switch 2.0, Motion Detector 2.0, NFC, + Rotary Encoder 2.0, Solid State 2.0, Temperature IR 2.0 and Outdoor Weather + Bricklet + +2018-06-08: 2.1.17 (8fb62e4) +- Add support for CAN 2.0, Industrial Counter, Industrial Digital In 4 2.0, + Industrial Dual Relay, Industrial Quad Relay 2.0, IO-4 2.0, LED Strip 2.0, + Load Cell 2.0, Particulate Matter, PTC 2.0, Real-Time Clock 2.0, RS232 2.0, + Sound Pressure Level, Thermocouple 2.0 and Voltage/Current 2.0 Bricklet +- Add get/set_maximum_timeout functions to NFC Bricklet API +- Add is_sensor_connected function and SENSOR_CONNECTED callback to PTC Bricklet API +- Break Humidity 2.0, Rotary Encoder 2.0 and Temperature IR 2.0 Bricklet API to + fix types for callback threshold min/max configuration + +2018-09-28: 2.1.18 (f7c65f7) +- Add support for Air Quality, Analog Out 3.0, Barometer 2.0, Distance IR 2.0, + Dual Button 2.0, Industrial Analog Out 2.0, Industrial Digital Out 4 2.0, + Industrial Dual 0-20mA 2.0, Industrial Dual Analog In 2.0, IO-16 2.0, Isolator, + LCD 128x64, OLED 128x64 2.0, One Wire, Temperature 2.0 and UV Light 2.0 Bricklet + +2018-10-05: 2.1.19 (e3c6f36) +- Break API to fix moving-average-length type in Distance IR Bricklet 2.0 API + +2018-11-28: 2.1.20 (0e3b130) +- Add get/set_samples_per_second functions to Humidity Bricklet 2.0 API +- Add button, slider, graph and tab functions to LCD 128x64 Bricklet API + +2019-01-29: 2.1.21 (2617875) +- Add support for Accelerometer 2.0 and Ambient Light 3.0 Bricklet + +2019-05-21: 2.1.22 (a3d0573) +- Add support for CO2 2.0, E-Paper 296x128, Hall Effect 2.0, Joystick 2.0, + Laser Range Finder 2.0, Linear Poti 2.0, Piezo Speaker 2.0, RGB LED 2.0 and + Segment Display 4x7 2.0 Bricklet and HAT and HAT Zero Brick +- Add remove_calibration and get/set_background_calibration_duration functions + to Air Quality Bricklet API +- Properly check UIDs and report invalid UIDs + +2019-08-23: 2.1.23 (59d9363) +- Add support for Color 2.0, Compass, Distance US 2.0, Energy Monitor, + Multi Touch 2.0, Rotary Poti 2.0 and XMC1400 Breakout Bricklet +- Add get/set_filter_configuration functions to Accelerometer Bricklet 2.0 API +- Add CONVERSION_TIME constants to Voltage/Current Bricklet 2.0 API + +2019-11-25: 2.1.24 (b1270ba) +- Add set/get_voltages_callback_configuration functions and VOLTAGES callback + to HAT Brick API +- Add set/get_usb_voltage_callback_configuration functions and USB_VOLTAGE + callback to HAT Zero Brick API +- Add set/get_statistics_callback_configuration functions and STATISTICS + callback to Isolator Bricklet API +- Report error if authentication secret contains non-ASCII chars +- Fix some error format strings in IPConnection class + +2020-04-07: 2.1.25 (3dff30a) +- Properly check device-identifier and report mismatch between used API bindings + device type and actual hardware device type +- Fix race condition between device constructor and callback thread +- Add set/get_flux_linear_parameters functions to Thermal Imaging Bricklet API +- Add set/get_frame_readable_callback_configuration functions and FRAME_READABLE + callback to CAN (2.0), RS232 (2.0) and RS485 Bricklet API +- Add set/get_error_occurred_callback_configuration functions and ERROR_OCCURRED + callback to CAN Bricklet 2.0 API +- Add read_frame function to RS232 Bricklet API +- Add write/read_bricklet_plugin functions to all Brick APIs for internal EEPROM + Bricklet flashing +- Add set_bricklet_xmc_flash_config/data and set/get_bricklets_enabled functions + to Master Brick 3.0 API for internal Co-MCU Bricklet bootloader flashing +- Validate response length before unpacking response +- Properly report replaced device objects as non-functional + +2020-05-19: 2.1.26 (9c76b18) +- Add get_all_voltages and set/get_all_voltages_callback_configuration functions + and ALL_VOLTAGES callback to Industrial Dual Analog In Bricklet 2.0 API +- Add set/get_i2c_mode functions to Barometer Bricklet API + +2020-11-02: 2.1.27 (6399602) +- Add support for IMU Bricklet 3.0 and Industrial Dual AC Relay Bricklet + +2021-01-15: 2.1.28 (797d61e) +- Add support for Performance DC Bricklet and Servo Bricklet 2.0 + +2021-05-06: 2.1.29 (7cd6fa2) +- Add GPIO_STATE callback to Performance DC Bricklet API +- Add support for DC 2.0, Industrial PTC and Silent Stepper Bricklet 2.0 diff --git a/juliapy/example_authenticate.py b/juliapy/example_authenticate.py new file mode 100644 index 00000000..13c35304 --- /dev/null +++ b/juliapy/example_authenticate.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +HOST = "localhost" +PORT = 4223 +SECRET = "My Authentication Secret!" + +from tinkerforge.ip_connection import IPConnection + +# Authenticate each time the connection got (re-)established +def cb_connected(connect_reason): + if connect_reason == IPConnection.CONNECT_REASON_REQUEST: + print("Connected by request") + elif connect_reason == IPConnection.CONNECT_REASON_AUTO_RECONNECT: + print("Auto-Reconnect") + + # Authenticate first... + try: + ipcon.authenticate(SECRET) + print("Authentication succeeded") + except: + print("Could not authenticate") + return + + # ...reenable auto reconnect mechanism, as described below... + ipcon.set_auto_reconnect(True) + + # ...then trigger enumerate + ipcon.enumerate() + +# Print incoming enumeration +def cb_enumerate(uid, connected_uid, position, hardware_version, firmware_version, + device_identifier, enumeration_type): + print("UID: " + uid + ", Enumeration Type: " + str(enumeration_type)) + +if __name__ == "__main__": + # Create IPConnection + ipcon = IPConnection() + + # Disable auto reconnect mechanism, in case we have the wrong secret. + # If the authentication is successful, reenable it. + ipcon.set_auto_reconnect(False) + + # Register Connected Callback + ipcon.register_callback(IPConnection.CALLBACK_CONNECTED, cb_connected) + + # Register Enumerate Callback + ipcon.register_callback(IPConnection.CALLBACK_ENUMERATE, cb_enumerate) + + # Connect to brickd + ipcon.connect(HOST, PORT) + + input("Press key to exit\n") # Use raw_input() in Python 2 + ipcon.disconnect() diff --git a/juliapy/example_enumerate.py b/juliapy/example_enumerate.py new file mode 100644 index 00000000..21a5174e --- /dev/null +++ b/juliapy/example_enumerate.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +HOST = "localhost" +PORT = 4223 + +from tinkerforge.ip_connection import IPConnection + +# Print incoming enumeration +def cb_enumerate(uid, connected_uid, position, hardware_version, firmware_version, + device_identifier, enumeration_type): + print("UID: " + uid) + print("Enumeration Type: " + str(enumeration_type)) + + if enumeration_type == IPConnection.ENUMERATION_TYPE_DISCONNECTED: + print("") + return + + print("Connected UID: " + connected_uid) + print("Position: " + position) + print("Hardware Version: " + str(hardware_version)) + print("Firmware Version: " + str(firmware_version)) + print("Device Identifier: " + str(device_identifier)) + print("") + +if __name__ == "__main__": + # Create connection and connect to brickd + ipcon = IPConnection() + ipcon.connect(HOST, PORT) + + # Register Enumerate Callback + ipcon.register_callback(IPConnection.CALLBACK_ENUMERATE, cb_enumerate) + + # Trigger Enumerate + ipcon.enumerate() + + input("Press key to exit\n") # Use raw_input() in Python 2 + ipcon.disconnect() diff --git a/juliapy/generate_juliapy_bindings.py b/juliapy/generate_juliapy_bindings.py new file mode 100644 index 00000000..32ad67c4 --- /dev/null +++ b/juliapy/generate_juliapy_bindings.py @@ -0,0 +1,816 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +JuliaPy Bindings Generator +Copyright (C) 2020 Jonas Schumacher + +generate_juliapy_bindings.py: Generator for Julia bindings + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public +License along with this program; if not, write to the +Free Software Foundation, Inc., 59 Temple Place - Suite 330, +Boston, MA 02111-1307, USA. +""" + +import sys + +if sys.hexversion < 0x3040000: + print('Python >= 3.4 required') + sys.exit(1) + +import os +import importlib.util +import importlib.machinery +from pprint import pprint + +def create_generators_module(): + generators_dir = os.path.split(os.path.dirname(os.path.realpath(__file__)))[0] + + if sys.hexversion < 0x3050000: + generators_module = importlib.machinery.SourceFileLoader('generators', os.path.join(generators_dir, '__init__.py')).load_module() + else: + generators_spec = importlib.util.spec_from_file_location('generators', os.path.join(generators_dir, '__init__.py')) + generators_module = importlib.util.module_from_spec(generators_spec) + + generators_spec.loader.exec_module(generators_module) + + sys.modules['generators'] = generators_module + +if 'generators' not in sys.modules: + create_generators_module() + +from generators import common +from generators.juliapy import juliapy_common + +class JuliaPyBindingsDevice(juliapy_common.JuliaPyDevice): + def get_juliapy_import(self): + template = """ + +""" + + if not self.is_released(): + released = '\n#### __DEVICE_IS_NOT_RELEASED__ ####\n' + else: + released = '' + + return template.format(self.get_generator().get_header_comment('hash'), + released) + + def get_juliapy_namedtuples(self): + tuples = '' + template = """ +export {struct_name}{name_tup} +struct {struct_name}{name_tup} + {params} +end +""" + + for packet in self.get_packets('function'): + if len(packet.get_elements(direction='out')) < 2: + continue + + name = packet.get_name() + + if name.space.startswith('Get '): + name_tup = name.camel[3:] + else: + name_tup = name.camel + + params = [] + + for element in packet.get_elements(direction='out'): + params.append("{0}::{1}".format(element.get_name().under, element.get_juliapy_type())) + + tuples += template.format(name=name.camel, name_tup=name_tup, params="\n ".join(params), struct_name=self.get_juliapy_struct_name()) + + for packet in self.get_packets('function'): + if not packet.has_high_level(): + continue + + if len(packet.get_elements(direction='out', high_level=True)) < 2: + continue + + name = packet.get_name(skip=-2) + + if name.space.startswith('Get '): + name_tup = name.camel[3:] + else: + name_tup = name.camel + + params = [] + + for element in packet.get_elements(direction='out', high_level=True): + params.append("{0}::{1}".format(element.get_name().under, element.get_juliapy_type())) + + tuples += template.format(name=name.camel, name_tup=name_tup, params="\n ".join(params), struct_name=self.get_juliapy_struct_name()) + + return tuples + + def get_juliapy_struct(self): + template = """ +export {0} +\"\"\" +{1} +\"\"\" +mutable struct {0} <: TinkerforgeDevice + ipcon::IPConnection + deviceInternal::PyObject +""" + + return template.format(self.get_juliapy_struct_name(), + common.select_lang(self.get_description())) + + def get_juliapy_callback_id_definitions(self): + callback_ids = '' + template = ' device.callbacks[:CALLBACK_{0}] = {1}\n' + + for packet in self.get_packets('callback'): + callback_ids += template.format(packet.get_name().upper, packet.get_function_id()) + + if self.get_long_display_name() == 'RS232 Bricklet': + callback_ids += ' device.callbacks[:CALLBACK_READ_CALLBACK] = 8 # for backward compatibility\n' + callback_ids += ' device.callbacks[:CALLBACK_ERROR_CALLBACK] = 9 # for backward compatibility\n' + + #callback_ids += '\n' + + for packet in self.get_packets('callback'): + if packet.has_high_level(): + callback_ids += template.format(packet.get_name(skip=-2).upper, -packet.get_function_id()) + + return callback_ids + + def get_juliapy_function_id_definitions(self): + function_ids = '\n' + template = ' device.id_definitions[:FUNCTION_{0}] = {1}\n' + + for packet in self.get_packets('function'): + function_ids += template.format(packet.get_name().upper, packet.get_function_id()) + + return function_ids + + def get_juliapy_constants(self): + constant_format = ' device.constants[:{constant_group_name_upper}_{constant_name_upper}] = {constant_value}\n' + + return '\n' + self.get_formatted_constants(constant_format, char_format_func="\"{0}\"".format) + + def get_juliapy_init_method(self): + template = """ + \"\"\" + Creates an object with the unique device ID *uid* and adds it to + the IP Connection *ipcon*. + \"\"\" + function {0}(uid::String, ipcon::IPConnection) + package = pyimport("tinkerforge.{1}") + deviceInternal = package.{0}(uid, ipcon.ipconInternal) + + {2} + + return new(ipcon, deviceInternal) + end +end +""" + response_expected = '' + + for packet in self.get_packets('function'): + response_expected += ' device.response_expected[:FUNCTION_{1}] = RESPONSE_EXPECTED_{2}\n' \ + .format(self.get_juliapy_struct_name(), packet.get_name().upper, + packet.get_response_expected().upper()) + + fillins = self.get_juliapy_callback_id_definitions() + #fillins += self.get_juliapy_function_id_definitions() + fillins += self.get_juliapy_constants()+'\n' + #fillins += common.wrap_non_empty('', response_expected, '\n') + #fillins += self.get_juliapy_callback_formats() + #fillins += self.get_juliapy_high_level_callbacks() + #fillins += self.get_juliapy_add_device() + + return template.format(self.get_juliapy_struct_name(), + self.get_juliapy_import_name(), + fillins) + + def get_juliapy_callback_formats(self): + callback_formats = '' + template = ' device.callback_formats[:CALLBACK_{1}] = ({2}, "{3}")\n' + + for packet in self.get_packets('callback'): + callback_formats += template.format(self.get_juliapy_struct_name(), + packet.get_name().upper, + packet.get_response_size(), + packet.get_juliapy_format_list('out')) + + return callback_formats + '\n' + + def get_juliapy_high_level_callbacks(self): + high_level_callbacks = '' + template = ' device.high_level_callbacks[:CALLBACK_{1}] = [{4}, Dict("fixed_length" => {2}, "single_chunk" => {3}), nothing]\n' + + for packet in self.get_packets('callback'): + stream = packet.get_high_level('stream_*') + + if stream != None: + roles = [] + + for element in packet.get_elements(direction='out'): + roles.append(element.get_role()) + + high_level_callbacks += template.format(self.get_juliapy_struct_name(), + packet.get_name(skip=-2).upper, + stream.get_fixed_length(), + stream.has_single_chunk(), + repr(tuple(roles)).replace("'", "\"")) + + return high_level_callbacks + + def get_juliapy_add_device(self): + return ' add_device(ipcon, device)\n' + + def get_juliapy_methods(self): + m_tup = """ +export {0} +\"\"\" + $(SIGNATURES) + +{10} +\"\"\" +function {0}(device::{2}{8}{4}) + return device.deviceInternal.{0}({4}) +end +""" + m_ret = """ +export {0} +\"\"\" + $(SIGNATURES) + +{9} +\"\"\" +function {0}(device::{1}{7}{3}) + return device.deviceInternal.{0}({3}) +end +""" + m_nor = """ +export {0} +\"\"\" + $(SIGNATURES) + +{7} +\"\"\" +function {0}(device::{1}{5}{3}) + device.deviceInternal.{0}({3}) +end +""" + methods = '' + cls = self.get_juliapy_struct_name() + + # normal and low-level + for packet in self.get_packets('function'): + nb = packet.get_name().camel + ns = packet.get_name().under + nh = ns.upper() + par = packet.get_juliapy_parameters() + doc = packet.get_juliapy_formatted_doc() + cp = '' + ct = '' + + if par != '': + cp = ', ' + + if not ',' in par: + ct = ',' + + in_f = packet.get_juliapy_format_list('in') + out_l = packet.get_response_size() + out_f = packet.get_juliapy_format_list('out') + + if packet.get_function_id() == 255: # .get_identity + check = '' + else: + check = 'check_validity(device)\n' + + coercions = common.wrap_non_empty('', packet.get_juliapy_parameter_coercions(), '\n') + out_c = len(packet.get_elements(direction='out')) + + if out_c > 1: + methods += m_tup.format(ns, nb, cls, nh, par, in_f, out_l, out_f, cp, ct, doc, check, coercions) + elif out_c == 1: + methods += m_ret.format(ns, cls, nh, par, in_f, out_l, out_f, cp, ct, doc, check, coercions) + else: + methods += m_nor.format(ns, cls, nh, par, in_f, cp, ct, doc, check, coercions) + + # high-level + template_stream_in = """ +export {function_name} +\"\"\" +{doc} +\"\"\" +function {function_name}(device::{struct_name}{high_level_parameters}) + {coercions} + if length({stream_name_under}) > {stream_max_length} + throw(TinkerforgeInvalidParameterError("{stream_name_space} can be at most {stream_max_length} items long")) + end + + {stream_name_under}_length = length({stream_name_under}) + {stream_name_under}_chunk_offset = 0 + + if {stream_name_under}_length == 0 + {stream_name_under}_chunk_data = [{chunk_padding}] * {chunk_cardinality} + ret = {function_name}_low_level(device, {parameters}) + else + lock(device.stream_lock) do + while {stream_name_under}_chunk_offset < {stream_name_under}_length + {stream_name_under}_chunk_data = create_chunk_data({stream_name_under}, {stream_name_under}_chunk_offset, {chunk_cardinality}, {chunk_padding}) + ret = {function_name}_low_level(device, {parameters}) + {stream_name_under}_chunk_offset += {chunk_cardinality} + end + end + end +{result} +end +""" + template_stream_in_fixed_length = """ +export {function_name} +\"\"\" +{doc} +\"\"\" +function {function_name}(device::{struct_name}{high_level_parameters}) + {coercions} + {stream_name_under}_length = {fixed_length} + {stream_name_under}_chunk_offset = 0 + + if length({stream_name_under}) != {stream_name_under}_length + throw(TinkerforgeInvalidParameterError("{stream_name_space} can be at most ${stream_name_under}_length items long")) + end + + lock(device.stream_lock) do + while {stream_name_under}_chunk_offset < {stream_name_under}_length + {stream_name_under}_chunk_data = create_chunk_data({stream_name_under}, {stream_name_under}_chunk_offset, {chunk_cardinality}, {chunk_padding}) + ret = {function_name}_low_level(device, {parameters}) + {stream_name_under}_chunk_offset += {chunk_cardinality} + end + end +{result} +end +""" + template_stream_in_result = """ + return ret""" + template_stream_in_namedtuple_result = """ + return {result_camel_name}(*ret)""" + template_stream_in_short_write = """ +export {function_name} +\"\"\" +{doc} +\"\"\" +function {function_name}(device::{struct_name}{high_level_parameters}) + {coercions} + if length({stream_name_under}) > {stream_max_length} + throw(TinkerforgeInvalidParameterError("{stream_name_space} can be at most {stream_max_length} items long")) + end + + {stream_name_under}_length = length({stream_name_under}) + {stream_name_under}_chunk_offset = 0 + + if {stream_name_under}_length == 0 + {stream_name_under}_chunk_data = [{chunk_padding}] * {chunk_cardinality} + ret = {function_name}_low_level(device, {parameters}) + {chunk_written_0} + else + {stream_name_under}_written = 0 + + lock(device.stream_lock) do + while {stream_name_under}_chunk_offset < {stream_name_under}_length + {stream_name_under}_chunk_data = create_chunk_data({stream_name_under}, {stream_name_under}_chunk_offset, {chunk_cardinality}, {chunk_padding}) + ret = {function_name}_low_level(device, {parameters}) + {chunk_written_n} + + if {chunk_written_test} < {chunk_cardinality} + break # either last chunk or short write + end + + {stream_name_under}_chunk_offset += {chunk_cardinality} + end + end + end +{result} +end +""" + template_stream_in_short_write_chunk_written = ['{stream_name_under}_written = ret', + '{stream_name_under}_written += ret', + 'ret'] + template_stream_in_short_write_namedtuple_chunk_written = ['{stream_name_under}_written = ret.{stream_name_under}_chunk_written', + '{stream_name_under}_written += ret.{stream_name_under}_chunk_written', + 'ret.{stream_name_under}_chunk_written'] + template_stream_in_short_write_result = """ + return {stream_name_under}_written""" + template_stream_in_short_write_namedtuple_result = """ + return {result_camel_name}({result_fields})""" + template_stream_in_single_chunk = """ +export {function_name} +\"\"\" +{doc} +\"\"\" +function {function_name}(device::{struct_name}{high_level_parameters}) + {coercions} + {stream_name_under}_length = length({stream_name_under}) + {stream_name_under}_data = list({stream_name_under}) # make a copy so we can potentially extend it + + if {stream_name_under}_length > {chunk_cardinality} + throw(TinkerforgeInvalidParameterError("{stream_name_space} can be at most {chunk_cardinality} items long")) + end + + if {stream_name_under}_length < {chunk_cardinality} + {stream_name_under}_data += [{chunk_padding}] * ({chunk_cardinality} - {stream_name_under}_length) + end +{result} +end +""" + template_stream_in_single_chunk_result = """ + return {function_name}_low_level(device, {parameters})""" + template_stream_in_single_chunk_namedtuple_result = """ + return {result_camel_name}({function_name}_low_level(device, {parameters})...)""" + template_stream_out = """ +export {function_name} +\"\"\" +{doc} +\"\"\" +function {function_name}(device::{struct_name}{high_level_parameters}) + {coercions}{fixed_length} + lock(device.stream_lock) do + ret = {function_name}_low_level(device, {parameters}){dynamic_length_3} + {chunk_offset_check}{stream_name_under}_out_of_sync = ret.{stream_name_under}_chunk_offset != 0 + {closing_end} + {chunk_offset_check_indent}{stream_name_under}_data = ret.{stream_name_under}_chunk_data + + while !{stream_name_under}_out_of_sync && length({stream_name_under}_data) < {stream_name_under}_length + ret = {function_name}_low_level(device, {parameters}){dynamic_length_4} + {stream_name_under}_out_of_sync = ret.{stream_name_under}_chunk_offset != length({stream_name_under}_data) + {stream_name_under}_data += ret.{stream_name_under}_chunk_data + end + + if {stream_name_under}_out_of_sync # discard remaining stream to bring it back in-sync + while ret.{stream_name_under}_chunk_offset + {chunk_cardinality} < {stream_name_under}_length + ret = {function_name}_low_level(device, {parameters}){dynamic_length_5} + end + + throw(TinkerforgeStreamOutOfSyncError("{stream_name_space} stream is out-of-sync")) + end + end +{result} +end +""" + template_stream_out_fixed_length = """ + {stream_name_under}_length = {fixed_length} +""" + template_stream_out_dynamic_length = """ +{{indent}}{stream_name_under}_length = ret.{stream_name_under}_length""" + template_stream_out_chunk_offset_check = """ + if ret.{stream_name_under}_chunk_offset == (1 << {shift_size}) - 1 # maximum chunk offset -> stream has no data + {stream_name_under}_length = 0 + {stream_name_under}_out_of_sync = false + {stream_name_under}_data = () + else + """ + template_stream_out_single_chunk = """ +export {function_name} +\"\"\" +{doc} +\"\"\" +function {function_name}(device::{struct_name}{high_level_parameters}) + {coercions} + ret = {function_name}_low_level(device, {parameters}) +{result} +end +""" + template_stream_out_result = """ + return {stream_name_under}_data[:{stream_name_under}_length]""" + template_stream_out_single_chunk_result = """ + return ret.{stream_name_under}_data[:ret.{stream_name_under}_length]""" + template_stream_out_namedtuple_result = """ + return {result_name}({result_fields})""" + + for packet in self.get_packets('function'): + stream_in = packet.get_high_level('stream_in') + stream_out = packet.get_high_level('stream_out') + + if stream_in != None: + if stream_in.get_fixed_length() != None: + template = template_stream_in_fixed_length + elif stream_in.has_short_write() and stream_in.has_single_chunk(): + # the single chunk template also covers short writes + template = template_stream_in_single_chunk + elif stream_in.has_short_write(): + template = template_stream_in_short_write + elif stream_in.has_single_chunk(): + template = template_stream_in_single_chunk + else: + template = template_stream_in + + if stream_in.has_short_write(): + if len(packet.get_elements(direction='out')) < 2: + chunk_written_0 = template_stream_in_short_write_chunk_written[0].format(stream_name_under=stream_in.get_name().under) + chunk_written_n = template_stream_in_short_write_chunk_written[1].format(stream_name_under=stream_in.get_name().under) + chunk_written_test = template_stream_in_short_write_chunk_written[2].format(stream_name_under=stream_in.get_name().under) + else: + chunk_written_0 = template_stream_in_short_write_namedtuple_chunk_written[0].format(stream_name_under=stream_in.get_name().under) + chunk_written_n = template_stream_in_short_write_namedtuple_chunk_written[1].format(stream_name_under=stream_in.get_name().under) + chunk_written_test = template_stream_in_short_write_namedtuple_chunk_written[2].format(stream_name_under=stream_in.get_name().under) + + if len(packet.get_elements(direction='out', high_level=True)) < 2: + if stream_in.has_single_chunk(): + result = template_stream_in_single_chunk_result.format(function_name=packet.get_name(skip=-2).under, + parameters=packet.get_juliapy_parameters()) + else: + result = template_stream_in_short_write_result.format(stream_name_under=stream_in.get_name().under) + else: + if stream_in.has_single_chunk(): + result = template_stream_in_single_chunk_namedtuple_result.format(function_name=packet.get_name(skip=-2).under, + parameters=packet.get_juliapy_parameters(), + result_camel_name=packet.get_name(skip=-2).camel) + else: + fields = [] + + for element in packet.get_elements(direction='out', high_level=True): + if element.get_role() == 'stream_written': + fields.append('{0}_written'.format(stream_in.get_name().under)) + else: + fields.append('ret.{0}'.format(element.get_name().under)) + + result = template_stream_in_short_write_namedtuple_result.format(result_camel_name=packet.get_name(skip=-2).camel, + result_fields=', '.join(fields)) + else: + chunk_written_0 = '' + chunk_written_n = '' + chunk_written_test = '' + + if len(packet.get_elements(direction='out', high_level=True)) < 2: + if stream_in.has_single_chunk(): + result = template_stream_in_single_chunk_result.format(function_name=packet.get_name(skip=-2).under, + parameters=packet.get_juliapy_parameters()) + else: + result = template_stream_in_result + else: + if stream_in.has_single_chunk(): + result = template_stream_in_single_chunk_namedtuple_result.format(function_name=packet.get_name(skip=-2).under, + parameters=packet.get_juliapy_parameters(), + result_camel_name=packet.get_name(skip=-2).camel) + else: + result = template_stream_in_namedtuple_result.format(result_camel_name=packet.get_name(skip=-2).camel) + + methods += template.format(doc=packet.get_juliapy_formatted_doc(), + coercions=common.wrap_non_empty('\n ', packet.get_juliapy_parameter_coercions(high_level=True), '\n'), + function_name=packet.get_name(skip=-2).under, + parameters=packet.get_juliapy_parameters(), + high_level_parameters=common.wrap_non_empty(', ', packet.get_juliapy_parameters(high_level=True), ''), + stream_name_space=stream_in.get_name().space, + stream_name_under=stream_in.get_name().under, + stream_max_length=abs(stream_in.get_data_element().get_cardinality()), + fixed_length=stream_in.get_fixed_length(), + chunk_cardinality=stream_in.get_chunk_data_element().get_cardinality(), + chunk_padding=stream_in.get_chunk_data_element().get_juliapy_default_item_value(), + chunk_written_0=chunk_written_0, + chunk_written_n=chunk_written_n, + chunk_written_test=chunk_written_test, + struct_name=self.get_juliapy_struct_name(), + #closing_end='end\n' if chunk_offset_check else '', + result=result) + elif stream_out != None: + if stream_out.get_fixed_length() != None: + fixed_length = template_stream_out_fixed_length.format(stream_name_under=stream_out.get_name().under, + fixed_length=stream_out.get_fixed_length()) + dynamic_length = '' + shift_size = int(stream_out.get_chunk_offset_element().get_type().replace('uint', '')) + chunk_offset_check = template_stream_out_chunk_offset_check.format(stream_name_under=stream_out.get_name().under, + shift_size=shift_size) + chunk_offset_check_indent = '' + else: + fixed_length = '' + dynamic_length = template_stream_out_dynamic_length.format(stream_name_under=stream_out.get_name().under) + chunk_offset_check = '' + chunk_offset_check_indent = '' + + if len(packet.get_elements(direction='out', high_level=True)) < 2: + if stream_out.has_single_chunk(): + result = template_stream_out_single_chunk_result.format(stream_name_under=stream_out.get_name().under) + else: + result = template_stream_out_result.format(stream_name_under=stream_out.get_name().under) + else: + fields = [] + + for element in packet.get_elements(direction='out', high_level=True): + if element.get_role() == 'stream_data': + if stream_out.has_single_chunk(): + fields.append('ret.{0}_data[:ret.{0}_length]'.format(stream_out.get_name().under)) + else: + fields.append('{0}_data[:{0}_length]'.format(stream_out.get_name().under)) + else: + fields.append('ret.{0}'.format(element.get_name().under)) + + result = template_stream_out_namedtuple_result.format(result_name=packet.get_name(skip=-2).camel, + result_fields=', '.join(fields)) + + if stream_out.has_single_chunk(): + template = template_stream_out_single_chunk + else: + template = template_stream_out + + methods += template.format(doc=packet.get_juliapy_formatted_doc(), + coercions=common.wrap_non_empty('\n ', packet.get_juliapy_parameter_coercions(high_level=True), '\n'), + function_name=packet.get_name(skip=-2).under, + parameters=packet.get_juliapy_parameters(), + high_level_parameters=common.wrap_non_empty(', ', packet.get_juliapy_parameters(high_level=True), ''), + stream_name_space=stream_out.get_name().space, + stream_name_under=stream_out.get_name().under, + fixed_length=fixed_length, + dynamic_length_3=dynamic_length.format(indent=' ' * 3), + dynamic_length_4=dynamic_length.format(indent=' ' * 4), + dynamic_length_5=dynamic_length.format(indent=' ' * 5), + chunk_offset_check=chunk_offset_check, + chunk_offset_check_indent=chunk_offset_check_indent, + chunk_cardinality=stream_out.get_chunk_data_element().get_cardinality(), + struct_name=self.get_juliapy_struct_name(), + closing_end='end\n' if chunk_offset_check != '' else '', + result=result) + + return methods + + def get_juliapy_register_callback_method(self): + if len(self.get_packets('callback')) == 0: + return '' + + return """ +export register_callback +\"\"\" +Registers the given *function* with the given *callback_id*. +\"\"\" +function register_callback(device::{0}, callback_id, function_) + if isnothing(function_) + device.registered_callbacks.pop(callback_id, None) + else + device.registered_callbacks[callback_id] = function_ + end +end +""".format(self.get_juliapy_struct_name()) + + def get_juliapy_old_name(self): + template = """ +{0} = {1} # for backward compatibility +""" + + return ""#template.format(self.get_name().camel, self.get_juliapy_struct_name()) + + def get_juliapy_source(self): + source = self.get_juliapy_import() + source += self.get_juliapy_namedtuples() + source += self.get_juliapy_struct() + source += self.get_juliapy_init_method() + source += self.get_juliapy_methods() + source += self.get_juliapy_register_callback_method() + + if self.is_brick() or self.is_bricklet(): + source += self.get_juliapy_old_name() + + return common.strip_trailing_whitespace(source) + +class JuliaPyBindingsPacket(juliapy_common.JuliaPyPacket): + def get_juliapy_formatted_doc(self): + text = common.select_lang(self.get_doc_text()) + + def format_parameter(name): + return '``{0}``'.format(name) # FIXME + + text = common.handle_rst_param(text, format_parameter) + text = common.handle_rst_word(text).replace("\\", "\\\\").replace("$", "\\$") + text = common.handle_rst_substitutions(text, self) + text += common.format_since_firmware(self.get_device(), self, nbsp='\\$nbsp;') + + return '\n'.join(text.strip().split('\n')) + + def get_juliapy_format_list(self, io): + forms = [] + + for element in self.get_elements(direction=io): + forms.append(element.get_juliapy_struct_format()) + + return ' '.join(forms) + + def get_juliapy_parameter_coercions(self, high_level=False): + coercions = [] + + for element in self.get_elements(direction='in', high_level=high_level): + name = element.get_name().under + + coercions.append('{0} = {1}'.format(name, element.get_juliapy_parameter_coercion().format(name))) + + return '\n '.join(coercions) + +class JuliaPyBindingsGenerator(juliapy_common.JuliaPyGeneratorTrait, common.BindingsGenerator): + def get_device_class(self): + return JuliaPyBindingsDevice + + def get_packet_class(self): + return JuliaPyBindingsPacket + + def get_element_class(self): + return juliapy_common.JuliaPyElement + + def prepare(self): + common.BindingsGenerator.prepare(self) + + self.device_factory_all_classes = [] + self.device_factory_released_classes = [] + self.device_display_names = [] + + def generate(self, device): + filename = '{0}_{1}.jl'.format(device.get_category().under, device.get_name().under) + + with open(os.path.join(self.get_bindings_dir(), filename), 'w') as f: + f.write(device.get_juliapy_source()) + + self.device_factory_all_classes.append((device.get_juliapy_import_name(), device.get_juliapy_struct_name(), device.get_device_identifier())) + + if device.is_released(): + self.device_factory_released_classes.append((device.get_juliapy_import_name(), device.get_juliapy_struct_name(), device.get_device_identifier())) + self.device_display_names.append((device.get_device_identifier(), device.get_long_display_name())) + self.released_files.append(filename) + + def finish(self): + template_import = """include("{0}.jl")""" + template = """{0} +{1} + +export get_device_type +function get_device_type(device_identifier::Integer) + device_types = Dict{{Integer, String}}( +{2} + ) + + return device_types[device_identifier] +end + +export create_device +function create_device(device_identifier::Integer, uid::String, ipcon::IPConnection) + return get_device_type(device_identifier)(uid, ipcon) +end +""" + for filename, device_factory_classes in [('device_factory_all.jl', self.device_factory_all_classes), + ('device_factory.jl', self.device_factory_released_classes)]: + imports = [] + classes = [] + + for import_name, class_name, device_identifier in sorted(device_factory_classes): + imports.append(template_import.format(import_name, class_name)) + classes.append(' {0} => "{1}",'.format(device_identifier, class_name)) + + with open(os.path.join(self.get_bindings_dir(), filename), 'w') as f: + f.write(template.format(self.get_header_comment('hash'), + '\n'.join(imports), + '\n'.join(classes))) + + template = """{header} +export get_device_display_name +function get_device_display_name(device_identifier::Integer) + device_display_names = Dict{{Integer, String}}( + {entries} + ) + + try + device_display_name = dict["d"] + catch e + if e isa KeyError + device_display_name = "Unknown Device [{{device_identifier}}]" + end + end + + return device_display_name +end +""" + + entries = [] + + for device_identifier, device_display_name in sorted(self.device_display_names): + entries.append(' {0} => "{1}"'.format(device_identifier, device_display_name)) + + with open(os.path.join(self.get_bindings_dir(), 'device_display_names.jl'), 'w') as f: + f.write(template.format(header=self.get_header_comment('hash'), + entries=',\n '.join(entries))) + + self.released_files.append('device_display_names.jl') + + common.BindingsGenerator.finish(self) + +def generate(root_dir, language, internal): + common.generate(root_dir, language, internal, JuliaPyBindingsGenerator) + +if __name__ == '__main__': + args = common.dockerize('juliapy', __file__, add_internal_argument=True) + + generate(os.getcwd(), 'en', args.internal) diff --git a/juliapy/generate_juliapy_debian_package.py b/juliapy/generate_juliapy_debian_package.py new file mode 100644 index 00000000..42c585fd --- /dev/null +++ b/juliapy/generate_juliapy_debian_package.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +JuliaPy Debian Package Generator +Copyright (C) 2020 Matthias Bolte + +generate_juliapy_debian_package.py: Generator for JuliaPy Debian Package + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public +License along with this program; if not, write to the +Free Software Foundation, Inc., 59 Temple Place - Suite 330, +Boston, MA 02111-1307, USA. +""" + +import sys + +if sys.hexversion < 0x3040000: + print('Python >= 3.4 required') + sys.exit(1) + +import os +import shutil +import subprocess +import glob +import importlib.util +import importlib.machinery + +def create_generators_module(): + generators_dir = os.path.split(os.path.dirname(os.path.realpath(__file__)))[0] + + if sys.hexversion < 0x3050000: + generators_module = importlib.machinery.SourceFileLoader('generators', os.path.join(generators_dir, '__init__.py')).load_module() + else: + generators_spec = importlib.util.spec_from_file_location('generators', os.path.join(generators_dir, '__init__.py')) + generators_module = importlib.util.module_from_spec(generators_spec) + + generators_spec.loader.exec_module(generators_module) + + sys.modules['generators'] = generators_module + +if 'generators' not in sys.modules: + create_generators_module() + +from generators import common + +def generate(root_dir, language): + version = common.get_changelog_version(root_dir) + debian_dir = os.path.join(root_dir, 'debian') + tmp_dir = os.path.join(root_dir, 'debian_package') + tmp_source_dir = os.path.join(tmp_dir, 'source') + tmp_source_debian_dir = os.path.join(tmp_source_dir, 'debian') + tmp_build_dir = os.path.join(tmp_dir, 'tinkerforge-juliapy-bindings-{0}.{1}.{2}'.format(*version)) + + # Make directories + common.recreate_dir(tmp_dir) + + # Unzip + common.execute(['unzip', + '-q', + os.path.join(root_dir, 'tinkerforge_juliapy_bindings_{0}_{1}_{2}.zip'.format(*version)), + os.path.join('source', '*'), + '-d', + tmp_dir]) + + shutil.copytree(debian_dir, tmp_source_debian_dir) + + common.specialize_template(os.path.join(tmp_source_debian_dir, 'changelog.template'), + os.path.join(tmp_source_debian_dir, 'changelog'), + {'<>': '.'.join(version), + '<>': subprocess.check_output(['date', '-R']).decode('utf-8')}, + remove_template=True) + + # Make package + os.rename(tmp_source_dir, tmp_build_dir) + + with common.ChangedDirectory(tmp_build_dir): + common.execute(['dpkg-buildpackage', + '--no-sign']) + + # Check package + with common.ChangedDirectory(tmp_dir): + common.execute(['lintian', '--pedantic'] + glob.glob('*.deb')) + +if __name__ == '__main__': + common.dockerize('juliapy', __file__) + + generate(os.getcwd(), 'en') diff --git a/juliapy/generate_juliapy_doc.py b/juliapy/generate_juliapy_doc.py new file mode 100644 index 00000000..13f944eb --- /dev/null +++ b/juliapy/generate_juliapy_doc.py @@ -0,0 +1,464 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Python Documentation Generator +Copyright (C) 2012-2015, 2017-2020 Matthias Bolte +Copyright (C) 2011-2013 Olaf Lüke + +generate_python_doc.py: Generator for Python documentation + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public +License along with this program; if not, write to the +Free Software Foundation, Inc., 59 Temple Place - Suite 330, +Boston, MA 02111-1307, USA. +""" + +import sys + +if sys.hexversion < 0x3040000: + print('Python >= 3.4 required') + sys.exit(1) + +import os +import importlib.util +import importlib.machinery + +def create_generators_module(): + generators_dir = os.path.split(os.path.dirname(os.path.realpath(__file__)))[0] + + if sys.hexversion < 0x3050000: + generators_module = importlib.machinery.SourceFileLoader('generators', os.path.join(generators_dir, '__init__.py')).load_module() + else: + generators_spec = importlib.util.spec_from_file_location('generators', os.path.join(generators_dir, '__init__.py')) + generators_module = importlib.util.module_from_spec(generators_spec) + + generators_spec.loader.exec_module(generators_module) + + sys.modules['generators'] = generators_module + +if 'generators' not in sys.modules: + create_generators_module() + +from generators import common +from generators.python import python_common + +class PythonDocDevice(python_common.PythonDevice): + def specialize_python_doc_function_links(self, text): + def specializer(packet, high_level): + if packet.get_type() == 'callback': + return ':py:attr:`CALLBACK_{1} <{0}.CALLBACK_{1}>`'.format(packet.get_device().get_python_class_name(), + packet.get_name(skip=-2 if high_level else 0).upper) + else: + return ':py:func:`{1}() <{0}.{1}>`'.format(packet.get_device().get_python_class_name(), + packet.get_name(skip=-2 if high_level else 0).under) + + return self.specialize_doc_rst_links(text, specializer, prefix='py') + + def get_python_examples(self): + def title_from_filename(filename): + filename = filename.replace('example_', '').replace('.py', '') + return common.under_to_space(filename) + + return common.make_rst_examples(title_from_filename, self) + + def get_python_functions(self, type_): + functions = [] + template = '.. py:function:: {0}.{1}({2})\n\n{3}{4}\n' + cls = self.get_python_class_name() + + for packet in self.get_packets('function'): + if packet.get_doc_type() != type_: + continue + + skip = -2 if packet.has_high_level() else 0 + name = packet.get_name(skip=skip).under + params = packet.get_python_parameters(high_level=True) + meta = packet.get_formatted_element_meta(lambda element, cardinality=None: element.get_python_type(cardinality=cardinality), + lambda element, index=None: element.get_python_name(index=index), + return_object='conditional', + no_out_value={'en': 'None', 'de': 'None'}, + explicit_string_cardinality=True, + explicit_variable_stream_cardinality=True, + explicit_fixed_stream_cardinality=True, + explicit_common_cardinality=True, + high_level=True) + meta_table = common.make_rst_meta_table(meta) + desc = packet.get_python_formatted_doc() + + functions.append(template.format(cls, name, params, meta_table, desc)) + + return ''.join(functions) + + def get_python_callbacks(self): + callbacks = [] + template = '.. py:attribute:: {0}.CALLBACK_{1}\n\n{2}{3}\n' + cls = self.get_python_class_name() + + for packet in self.get_packets('callback'): + skip = -2 if packet.has_high_level() else 0 + meta = packet.get_formatted_element_meta(lambda element, cardinality=None: element.get_python_type(cardinality=cardinality), + lambda element, index=None: element.get_python_name(index=index), + no_out_value={'en': 'no parameters', 'de': 'keine Parameter'}, + explicit_string_cardinality=True, + explicit_variable_stream_cardinality=True, + explicit_fixed_stream_cardinality=True, + explicit_common_cardinality=True, + high_level=True) + meta_table = common.make_rst_meta_table(meta) + desc = packet.get_python_formatted_doc() + + callbacks.append(template.format(cls, packet.get_name(skip=skip).upper, meta_table, desc)) + + return ''.join(callbacks) + + def get_python_api(self): + create_str = { + 'en': """ +.. py:function:: {0}(uid, ipcon) + +{2} + + Creates an object with the unique device ID ``uid``: + + .. code-block:: python + + {1} = {0}("YOUR_DEVICE_UID", ipcon) + + This object can then be used after the IP Connection is connected. +""", + 'de': """ +.. py:function:: {0}(uid, ipcon) + +{2} + + Erzeugt ein Objekt mit der eindeutigen Geräte ID ``uid``: + + .. code-block:: python + + {1} = {0}("YOUR_DEVICE_UID", ipcon) + + Dieses Objekt kann benutzt werden, nachdem die IP Connection verbunden ist. +""" + } + + register_str = { + 'en': """ +.. py:function:: {2}{1}.register_callback(callback_id, function) + +{3} + + Registers the given ``function`` with the given ``callback_id``. + + The available callback IDs with corresponding function signatures are listed + :ref:`below <{0}_python_callbacks>`. +""", + 'de': """ +.. py:function:: {2}{1}.register_callback(callback_id, function) + +{3} + + Registriert die ``function`` für die gegebene ``callback_id``. + + Die verfügbaren Callback IDs mit den zugehörigen Funktionssignaturen sind + :ref:`unten <{0}_python_callbacks>` zu finden. +""" + } + + c_str = { + 'en': """ +.. _{0}_python_callbacks: + +Callbacks +^^^^^^^^^ + +Callbacks can be registered to receive +time critical or recurring data from the device. The registration is done +with the :py:func:`register_callback() <{1}.register_callback>` function of +the device object. The first parameter is the callback ID and the second +parameter the callback function: + +.. code-block:: python + + def my_callback(param): + print(param) + + {2}.register_callback({1}.CALLBACK_EXAMPLE, my_callback) + +The available constants with inherent number and type of parameters are +described below. + +.. note:: + Using callbacks for recurring events is *always* preferred + compared to using getters. It will use less USB bandwidth and the latency + will be a lot better, since there is no round trip time. + +{3} +""", + 'de': """ +.. _{0}_python_callbacks: + +Callbacks +^^^^^^^^^ + +Callbacks können registriert werden um zeitkritische +oder wiederkehrende Daten vom Gerät zu erhalten. Die Registrierung kann +mit der Funktion :py:func:`register_callback() <{1}.register_callback>` des +Geräte Objektes durchgeführt werden. Der erste Parameter ist die Callback ID +und der zweite Parameter die Callback-Funktion: + +.. code-block:: python + + def my_callback(param): + print(param) + + {2}.register_callback({1}.CALLBACK_EXAMPLE, my_callback) + +Die verfügbaren IDs mit der dazugehörigen Parameteranzahl und -typen werden +weiter unten beschrieben. + +.. note:: + Callbacks für wiederkehrende Ereignisse zu verwenden ist + *immer* zu bevorzugen gegenüber der Verwendung von Abfragen. + Es wird weniger USB-Bandbreite benutzt und die Latenz ist + erheblich geringer, da es keine Paketumlaufzeit gibt. + +{3} +""" + } + + api = { + 'en': """ +.. _{0}_python_api: + +API +--- + +Generally, every function of the Python bindings can throw an +``tinkerforge.ip_connection.Error`` exception that has a ``value`` and a +``description`` property. ``value`` can have different values: + +* Error.TIMEOUT = -1 +* Error.NOT_ADDED = -6 (unused since Python bindings version 2.0.0) +* Error.ALREADY_CONNECTED = -7 +* Error.NOT_CONNECTED = -8 +* Error.INVALID_PARAMETER = -9 +* Error.NOT_SUPPORTED = -10 +* Error.UNKNOWN_ERROR_CODE = -11 +* Error.STREAM_OUT_OF_SYNC = -12 +* Error.INVALID_UID = -13 +* Error.NON_ASCII_CHAR_IN_SECRET = -14 +* Error.WRONG_DEVICE_TYPE = -15 +* Error.DEVICE_REPLACED = -16 +* Error.WRONG_RESPONSE_LENGTH = -17 + +All functions listed below are thread-safe. + +{1} + +{2} +""", + 'de': """ +.. _{0}_python_api: + +API +--- + +Prinzipiell kann jede Funktion der Python Bindings +``tinkerforge.ip_connection.Error`` Exception werfen, welche ein ``value`` und +eine ``description`` Property hat. ``value`` kann verschiende Werte haben: + +* Error.TIMEOUT = -1 +* Error.NOT_ADDED = -6 (seit Python Bindings Version 2.0.0 nicht mehr verwendet) +* Error.ALREADY_CONNECTED = -7 +* Error.NOT_CONNECTED = -8 +* Error.INVALID_PARAMETER = -9 +* Error.NOT_SUPPORTED = -10 +* Error.UNKNOWN_ERROR_CODE = -11 +* Error.STREAM_OUT_OF_SYNC = -12 +* Error.INVALID_UID = -13 +* Error.NON_ASCII_CHAR_IN_SECRET = -14 +* Error.WRONG_DEVICE_TYPE = -15 +* Error.DEVICE_REPLACED = -16 +* Error.WRONG_RESPONSE_LENGTH = -17 + +Alle folgend aufgelisteten Funktionen sind Thread-sicher. + +{1} + +{2} +""" + } + + const_str = { + 'en': """ +.. _{0}_python_constants: + +Constants +^^^^^^^^^ + +.. py:attribute:: {1}.DEVICE_IDENTIFIER + + This constant is used to identify a {3}. + + The :py:func:`get_identity() <{1}.get_identity>` function and the + :py:attr:`IPConnection.CALLBACK_ENUMERATE ` + callback of the IP Connection have a ``device_identifier`` parameter to specify + the Brick's or Bricklet's type. + +.. py:attribute:: {1}.DEVICE_DISPLAY_NAME + + This constant represents the human readable name of a {3}. +""", + 'de': """ +.. _{0}_python_constants: + +Konstanten +^^^^^^^^^^ + +.. py:attribute:: {1}.DEVICE_IDENTIFIER + + Diese Konstante wird verwendet um {2} {3} zu identifizieren. + + Die :py:func:`get_identity() <{1}.get_identity>` Funktion und der + :py:attr:`IPConnection.CALLBACK_ENUMERATE ` + Callback der IP Connection haben ein ``device_identifier`` Parameter um den Typ + des Bricks oder Bricklets anzugeben. + +.. py:attribute:: {1}.DEVICE_DISPLAY_NAME + + Diese Konstante stellt den Anzeigenamen eines {3} dar. +""" + } + + create_meta = common.format_simple_element_meta([('uid', 'str', 1, 'in'), + ('ipcon', 'IPConnection', 1, 'in'), + (self.get_name().under, self.get_python_class_name(), 1, 'out')]) + create_meta_table = common.make_rst_meta_table(create_meta) + + cre = common.select_lang(create_str).format(self.get_python_class_name(), + self.get_name().under, + create_meta_table) + + reg_meta = common.format_simple_element_meta([('callback_id', 'int', 1, 'in'), + ('function', 'callable', 1, 'in')], + no_out_value={'en': 'None', 'de': 'None'}) + reg_meta_table = common.make_rst_meta_table(reg_meta) + + reg = common.select_lang(register_str).format(self.get_doc_rst_ref_name(), + self.get_name().camel, + self.get_category().camel, + reg_meta_table) + + bf = self.get_python_functions('bf') + af = self.get_python_functions('af') + ccf = self.get_python_functions('ccf') + c = self.get_python_callbacks() + vf = self.get_python_functions('vf') + if_ = self.get_python_functions('if') + api_str = '' + + if bf: + api_str += common.select_lang(common.bf_str).format(cre, bf) + + if af: + api_str += common.select_lang(common.af_str).format(af) + + if c: + api_str += common.select_lang(common.ccf_str).format(reg, ccf) + api_str += common.select_lang(c_str).format(self.get_doc_rst_ref_name(), + self.get_python_class_name(), + self.get_name().under, + c) + + if vf: + api_str += common.select_lang(common.vf_str).format(vf) + + if if_: + api_str += common.select_lang(common.if_str).format(if_) + + article = 'ein' + + if self.is_brick(): + article = 'einen' + + api_str += common.select_lang(const_str).format(self.get_doc_rst_ref_name(), + self.get_python_class_name(), + article, + self.get_long_display_name()) + + return common.select_lang(api).format(self.get_doc_rst_ref_name(), + self.specialize_python_doc_function_links(common.select_lang(self.get_doc())), + api_str) + + def get_python_doc(self): + doc = common.make_rst_header(self) + doc += common.make_rst_summary(self) + doc += self.get_python_examples() + doc += self.get_python_api() + + return doc + +class PythonDocPacket(python_common.PythonPacket): + def get_python_formatted_doc(self): + text = common.select_lang(self.get_doc_text()) + text = self.get_device().specialize_python_doc_function_links(text) + + def format_parameter(name): + return '``{0}``'.format(name) # FIXME + + text = common.handle_rst_param(text, format_parameter) + text = common.handle_rst_word(text) + text = common.handle_rst_substitutions(text, self) + + prefix = self.get_device().get_python_class_name() + '.' + + def format_element_name(element, index): + if index == None: + return element.get_name().under + + return '{0}[{1}]'.format(element.get_name().under, index) + + text += common.format_constants(prefix, self, format_element_name) + text += common.format_since_firmware(self.get_device(), self) + + return common.shift_right(text, 1) + +class PythonDocGenerator(python_common.PythonGeneratorTrait, common.DocGenerator): + def get_doc_rst_filename_part(self): + return 'Python' + + def get_doc_example_regex(self): + return r'^example_.*\.py$' + + def get_device_class(self): + return PythonDocDevice + + def get_packet_class(self): + return PythonDocPacket + + def get_element_class(self): + return python_common.PythonElement + + def generate(self, device): + with open(device.get_doc_rst_path(), 'w') as f: + f.write(device.get_python_doc()) + +def generate(root_dir, language, internal): + common.generate(root_dir, language, internal, PythonDocGenerator) + +if __name__ == '__main__': + args = common.dockerize('python', __file__, add_internal_argument=True) + + for language in ['en', 'de']: + generate(os.getcwd(), language, args.internal) diff --git a/juliapy/generate_juliapy_examples.py b/juliapy/generate_juliapy_examples.py new file mode 100644 index 00000000..0d9c8614 --- /dev/null +++ b/juliapy/generate_juliapy_examples.py @@ -0,0 +1,667 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Python Examples Generator +Copyright (C) 2015-2019 Matthias Bolte + +generate_python_examples.py: Generator for Python examples + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public +License along with this program; if not, write to the +Free Software Foundation, Inc., 59 Temple Place - Suite 330, +Boston, MA 02111-1307, USA. +""" + +import sys + +if sys.hexversion < 0x3040000: + print('Python >= 3.4 required') + sys.exit(1) + +import os +import importlib.util +import importlib.machinery + +def create_generators_module(): + generators_dir = os.path.split(os.path.dirname(os.path.realpath(__file__)))[0] + + if sys.hexversion < 0x3050000: + generators_module = importlib.machinery.SourceFileLoader('generators', os.path.join(generators_dir, '__init__.py')).load_module() + else: + generators_spec = importlib.util.spec_from_file_location('generators', os.path.join(generators_dir, '__init__.py')) + generators_module = importlib.util.module_from_spec(generators_spec) + + generators_spec.loader.exec_module(generators_module) + + sys.modules['generators'] = generators_module + +if 'generators' not in sys.modules: + create_generators_module() + +from generators import common +from generators.python import python_common + +global_line_prefix = '' + +class PythonConstant(common.Constant): + def get_python_source(self, callback=False): + templateA = '{device_class}.{constant_group_name}_{constant_name}' + templateB = '{device_name}.{constant_group_name}_{constant_name}' + + if callback: + template = templateA + else: + template = templateB + + return template.format(device_class=self.get_device().get_python_class_name(), + device_name=self.get_device().get_initial_name(), + constant_group_name=self.get_constant_group().get_name().upper, + constant_name=self.get_name().upper) + +class PythonExample(common.Example): + def get_python_source(self): + template = r"""#!/usr/bin/env python +# -*- coding: utf-8 -*-{incomplete}{description} + +HOST = "localhost" +PORT = 4223 +UID = "{dummy_uid}" # Change {dummy_uid} to the UID of your {device_name_long_display} +{imports} +from tinkerforge.ip_connection import IPConnection +from tinkerforge.{device_category_under}_{device_name_under} import {device_category_camel}{device_name_camel} +{functions} +if __name__ == "__main__": + ipcon = IPConnection() # Create IP connection + {device_name_initial} = {device_category_camel}{device_name_camel}(UID, ipcon) # Create device object + + ipcon.connect(HOST, PORT) # Connect to brickd + # Don't use device before ipcon is connected +{sources} + input("Press key to exit\n") # Use raw_input() in Python 2{cleanups} + ipcon.disconnect() +""" + + if self.is_incomplete(): + incomplete = '\n\n# FIXME: This example is incomplete' + else: + incomplete = '' + + if self.get_description() != None: + description = '\n\n# {0}'.format(self.get_description().replace('\n', '\n# ')) + else: + description = '' + + imports = [] + functions = [] + sources = [] + cleanups = [] + + for function in self.get_functions(): + imports += function.get_python_imports() + functions.append(function.get_python_function()) + sources.append(function.get_python_source()) + + for cleanup in self.get_cleanups(): + imports += cleanup.get_python_imports() + functions.append(cleanup.get_python_function()) + cleanups.append(cleanup.get_python_source()) + + unique_imports = [] + + for import_ in imports: + if import_ not in unique_imports: + unique_imports.append(import_) + + while None in functions: + functions.remove(None) + + while None in sources: + sources.remove(None) + + if len(sources) == 0: + sources = [' # TODO: Add example code here\n'] + + while None in cleanups: + cleanups.remove(None) + + return template.format(incomplete=incomplete, + description=description, + device_category_camel=self.get_device().get_category().camel, + device_category_under=self.get_device().get_category().under, + device_name_camel=self.get_device().get_name().camel, + device_name_under=self.get_device().get_name().under, + device_name_initial=self.get_device().get_initial_name(), + device_name_long_display=self.get_device().get_long_display_name(), + dummy_uid=self.get_dummy_uid(), + imports=common.wrap_non_empty('\n', ''.join(unique_imports), ''), + functions=common.wrap_non_empty('\n', '\n'.join(functions), ''), + sources='\n' + '\n'.join(sources).replace('\n\r', '').lstrip('\r'), + cleanups=common.wrap_non_empty('\n\n', '\n'.join(cleanups).replace('\n\r', '').lstrip('\r').rstrip('\n'), '\n')) + +class PythonExampleArgument(common.ExampleArgument): + def get_python_source(self): + type_ = self.get_type() + + def helper(value): + if type_ == 'float': + return common.format_float(value) + elif type_ == 'bool': + return str(bool(value)) + elif type_ in ['char', 'string']: + return '"{0}"'.format(value.replace('"', '\\"')) + elif ':bitmask:' in type_: + return common.make_c_like_bitmask(value) + elif type_.endswith(':constant'): + return self.get_value_constant(value).get_python_source() + else: + return str(value) + + value = self.get_value() + + if isinstance(value, list): + return '[{0}]'.format(', '.join([helper(item) for item in value])) + + return helper(value) + +class PythonExampleArgumentsMixin(object): + def get_python_arguments(self): + return [argument.get_python_source() for argument in self.get_arguments()] + +class PythonExampleParameter(common.ExampleParameter): + def get_python_source(self): + return self.get_name().under + + def get_python_prints(self): + if self.get_type().split(':')[-1] == 'constant': + if self.get_label_name() == None: + return [] + + # FIXME: need to handle multiple labels + assert self.get_label_count() == 1 + + template = '{global_line_prefix} {else_}if {name} == {constant_name}:\n{global_line_prefix} print("{label}: {constant_title}"){comment}' + constant_group = self.get_constant_group() + result = [] + + for constant in constant_group.get_constants(): + result.append(template.format(global_line_prefix=global_line_prefix, + else_='el' if len(result) > 0 else '', + name=self.get_name().under, + label=self.get_label_name(), + constant_name=constant.get_python_source(callback=True), + constant_title=constant.get_name().space, + comment=self.get_formatted_comment(' # {0}'))) + + result = ['\r' + '\n'.join(result) + '\r'] + else: + template = '{global_line_prefix} print("{label}: " + {format_prefix}{name}{index}{divisor}{format_suffix}{unit}){comment}' + + if self.get_label_name() == None: + return [] + + if self.get_cardinality() < 0: + return [] # FIXME: streaming + + type_ = self.get_type() + + if ':bitmask:' in type_: + format_prefix = 'format(' + format_suffix = ', "0{0}b")'.format(int(type_.split(':')[2])) + elif type_ in ['char', 'string']: + format_prefix = '' + format_suffix = '' + else: + format_prefix = 'str(' + format_suffix = ')' + + result = [] + + for index in range(self.get_label_count()): + result.append(template.format(global_line_prefix=global_line_prefix, + name=self.get_name().under, + label=self.get_label_name(index=index), + index='[{0}]'.format(index) if self.get_label_count() > 1 else '', + divisor=self.get_formatted_divisor('/{0}'), + unit=self.get_formatted_unit_name(' + " {0}"'), + format_prefix=format_prefix, + format_suffix=format_suffix, + comment=self.get_formatted_comment(' # {0}'))) + + return result + +class PythonExampleResult(common.ExampleResult): + def get_python_variable(self): + name = self.get_name().under + + if name == self.get_device().get_initial_name(): + name += '_' + + return name + + def get_python_prints(self): + if self.get_type().split(':')[-1] == 'constant': + # FIXME: need to handle multiple labels + assert self.get_label_count() == 1 + + template = '{global_line_prefix} {else_}if {name} == {constant_name}:\n{global_line_prefix} print("{label}: {constant_title}"){comment}' + constant_group = self.get_constant_group() + result = [] + + for constant in constant_group.get_constants(): + result.append(template.format(global_line_prefix=global_line_prefix, + else_='el' if len(result) > 0 else '', + name=self.get_name().under, + label=self.get_label_name(), + constant_name=constant.get_python_source(), + constant_title=constant.get_name().space, + comment=self.get_formatted_comment(' # {0}'))) + + result = ['\r' + '\n'.join(result) + '\r'] + else: + template = '{global_line_prefix} print("{label}: " + {format_prefix}{name}{index}{divisor}{format_suffix}{unit}){comment}' + + if self.get_label_name() == None: + return [] + + if self.get_cardinality() < 0: + return [] # FIXME: streaming + + name = self.get_name().under + + if name == self.get_device().get_initial_name(): + name += '_' + + type_ = self.get_type() + + if ':bitmask:' in type_: + format_prefix = 'format(' + format_suffix = ', "0{0}b")'.format(int(type_.split(':')[2])) + elif type_ in ['char', 'string']: + format_prefix = '' + format_suffix = '' + else: + format_prefix = 'str(' + format_suffix = ')' + + result = [] + + for index in range(self.get_label_count()): + result.append(template.format(global_line_prefix=global_line_prefix, + name=name, + label=self.get_label_name(index=index), + index='[{0}]'.format(index) if self.get_label_count() > 1 else '', + divisor=self.get_formatted_divisor('/{0}'), + unit=self.get_formatted_unit_name(' + " {0}"'), + format_prefix=format_prefix, + format_suffix=format_suffix, + comment=self.get_formatted_comment(' # {0}'))) + + return result + +class PythonExampleGetterFunction(common.ExampleGetterFunction, PythonExampleArgumentsMixin): + def get_python_imports(self): + return [] + + def get_python_function(self): + return None + + def get_python_source(self): + template = r"""{global_line_prefix} # Get current {function_name_comment} +{global_line_prefix} {variables} = {device_name}.{function_name_under}({arguments}) +{prints} +""" + variables = [] + prints = [] + + for result in self.get_results(): + variables.append(result.get_python_variable()) + prints += result.get_python_prints() + + while None in prints: + prints.remove(None) + + if len(prints) > 1: + prints.insert(0, '\b') + + result = template.format(global_line_prefix=global_line_prefix, + device_name=self.get_device().get_initial_name(), + function_name_under=self.get_name().under, + function_name_comment=self.get_comment_name(), + variables=','.join(variables), + prints='\n'.join(prints).replace('\b\n\r', '\n').replace('\b', '').replace('\r\n\r', '\n\n').rstrip('\r').replace('\r', '\n'), + arguments=', '.join(self.get_python_arguments())) + + return common.break_string(result, ' ', continuation=' \\', indent_suffix=' ') + +class PythonExampleSetterFunction(common.ExampleSetterFunction, PythonExampleArgumentsMixin): + def get_python_imports(self): + return [] + + def get_python_function(self): + return None + + def get_python_source(self): + template = '{comment1}{global_line_prefix} {device_name}.{function_name}({arguments}){comment2}\n' + + result = template.format(global_line_prefix=global_line_prefix, + device_name=self.get_device().get_initial_name(), + function_name=self.get_name().under, + arguments=','.join(self.get_python_arguments()), + comment1=self.get_formatted_comment1(global_line_prefix + ' # {0}\n', '\r', '\n' + global_line_prefix + ' # '), + comment2=self.get_formatted_comment2(' # {0}', '')) + + return common.break_string(result, '.{0}('.format(self.get_name().under)) + +class PythonExampleCallbackFunction(common.ExampleCallbackFunction): + def get_python_imports(self): + return [] + + def get_python_function(self): + template1A = r"""# Callback function for {function_name_comment} callback +""" + template1B = r"""{override_comment} +""" + template2 = r"""def cb_{function_name_under}({parameters}): +{prints}{extra_message} +""" + override_comment = self.get_formatted_override_comment('# {0}', None, '\n# ') + + if override_comment == None: + template1 = template1A + else: + template1 = template1B + + parameters = [] + prints = [] + + for parameter in self.get_parameters(): + parameters.append(parameter.get_python_source()) + prints += parameter.get_python_prints() + + while None in prints: + prints.remove(None) + + if len(prints) > 1: + prints.append(' print("")') + + extra_message = self.get_formatted_extra_message(' print("{0}")') + + if len(extra_message) > 0 and len(prints) > 0: + extra_message = '\n' + extra_message + + result = template1.format(function_name_comment=self.get_comment_name(), + override_comment=override_comment) + \ + template2.format(function_name_under=self.get_name().under, + parameters=','.join(parameters), + prints='\n'.join(prints).replace('\r\n\r', '\n\n').strip('\r').replace('\r', '\n'), + extra_message=extra_message) + + return common.break_string(result, 'cb_{}('.format(self.get_name().under)) + + def get_python_source(self): + template1 = r""" # Register {function_name_comment}callbacktofunctioncb_{function_name_under} +""" + template2 = r""" {device_name}.register_callback({device_name}.CALLBACK_{function_name_upper},cb_{function_name_under}) +""" + + result1 = template1.format(function_name_under=self.get_name().under, + function_name_comment=self.get_comment_name()) + result2 = template2.format(device_name=self.get_device().get_initial_name(), + function_name_under=self.get_name().under, + function_name_upper=self.get_name().upper) + + return common.break_string(result1, '# ', indent_tail='# ') + \ + common.break_string(result2, 'register_callback(') + +class PythonExampleCallbackPeriodFunction(common.ExampleCallbackPeriodFunction, PythonExampleArgumentsMixin): + def get_python_imports(self): + return [] + + def get_python_function(self): + return None + + def get_python_source(self): + templateA = r""" # Set period for {function_name_comment} callback to {period_sec_short} ({period_msec}ms) + {device_name}.set_{function_name_under}_period({arguments}{period_msec}) +""" + templateB = r""" # Set period for {function_name_comment} callback to {period_sec_short} ({period_msec}ms) + # Note: The {function_name_comment} callback is only called every {period_sec_long} + # if the {function_name_comment} has changed since the last call! + {device_name}.set_{function_name_under}_callback_period({arguments}{period_msec}) +""" + + if self.get_device().get_name().space.startswith('IMU'): + template = templateA # FIXME: special hack for IMU Brick (2.0) callback behavior and name mismatch + else: + template = templateB + + period_msec, period_sec_short, period_sec_long = self.get_formatted_period() + + return template.format(device_name=self.get_device().get_initial_name(), + function_name_under=self.get_name().under, + function_name_comment=self.get_comment_name(), + arguments=common.wrap_non_empty('', ', '.join(self.get_python_arguments()), ', '), + period_msec=period_msec, + period_sec_short=period_sec_short, + period_sec_long=period_sec_long) + +class PythonExampleCallbackThresholdMinimumMaximum(common.ExampleCallbackThresholdMinimumMaximum): + def get_python_source(self): + template = '{minimum}, {maximum}' + + return template.format(minimum=self.get_formatted_minimum(), + maximum=self.get_formatted_maximum()) + +class PythonExampleCallbackThresholdFunction(common.ExampleCallbackThresholdFunction, PythonExampleArgumentsMixin): + def get_python_imports(self): + return [] + + def get_python_function(self): + return None + + def get_python_source(self): + template = r""" # Configure threshold for {function_name_comment} "{option_comment}" + {device_name}.set_{function_name_under}_callback_threshold({arguments}"{option_char}", {minimum_maximums}) +""" + minimum_maximums = [] + + for minimum_maximum in self.get_minimum_maximums(): + minimum_maximums.append(minimum_maximum.get_python_source()) + + return template.format(device_name=self.get_device().get_initial_name(), + function_name_under=self.get_name().under, + function_name_comment=self.get_comment_name(), + arguments=common.wrap_non_empty('', ', '.join(self.get_python_arguments()), ', '), + option_char=self.get_option_char(), + option_comment=self.get_option_comment(), + minimum_maximums=', '.join(minimum_maximums)) + +class PythonExampleCallbackConfigurationFunction(common.ExampleCallbackConfigurationFunction, PythonExampleArgumentsMixin): + def get_python_imports(self): + return [] + + def get_python_function(self): + return None + + def get_python_source(self): + templateA = r""" # Set period for {function_name_comment} callback to {period_sec_short} ({period_msec}ms) + {device_name}.set_{function_name_under}_callback_configuration({arguments}{period_msec}{value_has_to_change}) +""" + templateB = r""" # Set period for {function_name_comment} callback to {period_sec_short} ({period_msec}ms) without a threshold + {device_name}.set_{function_name_under}_callback_configuration({arguments}{period_msec}{value_has_to_change}, "{option_char}", {minimum_maximums}) +""" + templateC = r""" # Configure threshold for {function_name_comment} "{option_comment}" + # with a debounce period of {period_sec_short} ({period_msec}ms) + {device_name}.set_{function_name_under}_callback_configuration({arguments}{period_msec}{value_has_to_change}, "{option_char}", {minimum_maximums}) +""" + + if self.get_option_char() == None: + template = templateA + elif self.get_option_char() == 'x': + template = templateB + else: + template = templateC + + period_msec, period_sec_short, period_sec_long = self.get_formatted_period() + + minimum_maximums = [] + + for minimum_maximum in self.get_minimum_maximums(): + minimum_maximums.append(minimum_maximum.get_python_source()) + + return template.format(device_name=self.get_device().get_initial_name(), + function_name_under=self.get_name().under, + function_name_comment=self.get_comment_name(), + arguments=common.wrap_non_empty('', ', '.join(self.get_python_arguments()), ', '), + period_msec=period_msec, + period_sec_short=period_sec_short, + period_sec_long=period_sec_long, + value_has_to_change=common.wrap_non_empty(', ', self.get_value_has_to_change('True', 'False', ''), ''), + option_char=self.get_option_char(), + option_comment=self.get_option_comment(), + minimum_maximums=', '.join(minimum_maximums)) + +class PythonExampleSpecialFunction(common.ExampleSpecialFunction): + def get_python_imports(self): + if self.get_type() == 'sleep': + return ['import time\n'] + else: + return [] + + def get_python_function(self): + return None + + def get_python_source(self): + global global_line_prefix + + type_ = self.get_type() + + if type_ == 'empty': + return '' + elif type_ == 'debounce_period': + template = r""" # Get threshold callbacks with a debounce time of {period_sec} ({period_msec}ms) + {device_name_initial}.set_debounce_period({period_msec}) +""" + period_msec, period_sec = self.get_formatted_debounce_period() + + return template.format(device_name_initial=self.get_device().get_initial_name(), + period_msec=period_msec, + period_sec=period_sec) + elif type_ == 'sleep': + template = '{comment1}{global_line_prefix} time.sleep({duration}){comment2}\n' + duration = self.get_sleep_duration() + + if duration % 1000 == 0: + duration //= 1000 + else: + duration /= 1000.0 + + return template.format(global_line_prefix=global_line_prefix, + duration=duration, + comment1=self.get_formatted_sleep_comment1(global_line_prefix + ' # {0}\n', '\r', '\n' + global_line_prefix + ' # '), + comment2=self.get_formatted_sleep_comment2(' # {0}', '')) + elif type_ == 'wait': + return None + elif type_ == 'loop_header': + template = '{comment} for i in range({limit}):\n' + global_line_prefix = ' ' + + return template.format(limit=self.get_loop_header_limit(), + comment=self.get_formatted_loop_header_comment(' # {0}\n', '', '\n # ')) + elif type_ == 'loop_footer': + global_line_prefix = '' + + return '\r' + +class PythonExamplesGenerator(python_common.PythonGeneratorTrait, common.ExamplesGenerator): + def get_constant_class(self): + return PythonConstant + + def get_device_class(self): + return python_common.PythonDevice + + def get_example_class(self): + return PythonExample + + def get_example_argument_class(self): + return PythonExampleArgument + + def get_example_parameter_class(self): + return PythonExampleParameter + + def get_example_result_class(self): + return PythonExampleResult + + def get_example_getter_function_class(self): + return PythonExampleGetterFunction + + def get_example_setter_function_class(self): + return PythonExampleSetterFunction + + def get_example_callback_function_class(self): + return PythonExampleCallbackFunction + + def get_example_callback_period_function_class(self): + return PythonExampleCallbackPeriodFunction + + def get_example_callback_threshold_minimum_maximum_class(self): + return PythonExampleCallbackThresholdMinimumMaximum + + def get_example_callback_threshold_function_class(self): + return PythonExampleCallbackThresholdFunction + + def get_example_callback_configuration_function_class(self): + return PythonExampleCallbackConfigurationFunction + + def get_example_special_function_class(self): + return PythonExampleSpecialFunction + + def generate(self, device): + if os.getenv('TINKERFORGE_GENERATE_EXAMPLES_FOR_DEVICE', device.get_name().camel) != device.get_name().camel: + common.print_verbose(' \033[01;31m- skipped\033[0m') + return + + examples_dir = self.get_examples_dir(device) + examples = device.get_examples() + + if len(examples) == 0: + common.print_verbose(' \033[01;31m- no examples\033[0m') + return + + if not os.path.exists(examples_dir): + os.makedirs(examples_dir) + + for example in examples: + filename = 'example_{0}.py'.format(example.get_name().under) + filepath = os.path.join(examples_dir, filename) + + if example.is_incomplete(): + if os.path.exists(filepath) and self.skip_existing_incomplete_example: + common.print_verbose(' - ' + filename + ' \033[01;35m(incomplete, skipped)\033[0m') + continue + else: + common.print_verbose(' - ' + filename + ' \033[01;31m(incomplete)\033[0m') + else: + common.print_verbose(' - ' + filename) + + with open(filepath, 'w') as f: + f.write(example.get_python_source()) + +def generate(root_dir, language, internal): + common.generate(root_dir, language, internal, PythonExamplesGenerator) + +if __name__ == '__main__': + args = common.dockerize('python', __file__, add_internal_argument=True) + + generate(os.getcwd(), 'en', args.internal) diff --git a/juliapy/generate_juliapy_zip.py b/juliapy/generate_juliapy_zip.py new file mode 100644 index 00000000..512fa15a --- /dev/null +++ b/juliapy/generate_juliapy_zip.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +JuliaPy ZIP Generator +Copyright (C) 2012-2015, 2017-2018 Matthias Bolte +Copyright (C) 2011 Olaf Lüke + +generate_juliapy_zip.py: Generator for JuliaPy ZIP + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public +License along with this program; if not, write to the +Free Software Foundation, Inc., 59 Temple Place - Suite 330, +Boston, MA 02111-1307, USA. +""" + +import sys + +if sys.hexversion < 0x3040000: + print('Python >= 3.4 required') + sys.exit(1) + +import os +import shutil +import importlib.util +import importlib.machinery + +def create_generators_module(): + generators_dir = os.path.split(os.path.dirname(os.path.realpath(__file__)))[0] + + if sys.hexversion < 0x3050000: + generators_module = importlib.machinery.SourceFileLoader('generators', os.path.join(generators_dir, '__init__.py')).load_module() + else: + generators_spec = importlib.util.spec_from_file_location('generators', os.path.join(generators_dir, '__init__.py')) + generators_module = importlib.util.module_from_spec(generators_spec) + + generators_spec.loader.exec_module(generators_module) + + sys.modules['generators'] = generators_module + +if 'generators' not in sys.modules: + create_generators_module() + +from generators import common +from generators.juliapy import juliapy_common + +class JuliaPyZipGenerator(juliapy_common.JuliaPyGeneratorTrait, common.ZipGenerator): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.tmp_dir = self.get_zip_dir() + self.tmp_source_dir = os.path.join(self.tmp_dir, 'src') + self.tmp_source_devices_dir = os.path.join(self.tmp_source_dir, 'devices') + self.tmp_examples_dir = os.path.join(self.tmp_dir, 'examples') + + def prepare(self): + super().prepare() + + os.makedirs(self.tmp_source_dir) + os.makedirs(self.tmp_source_devices_dir) + os.makedirs(self.tmp_examples_dir) + + def generate(self, device): + if not device.is_released(): + return + + # Copy device examples + tmp_examples_device = os.path.join(self.tmp_examples_dir, + device.get_category().under, + device.get_name().under) + + if not os.path.exists(tmp_examples_device): + os.makedirs(tmp_examples_device) + + for example in common.find_device_examples(device, r'^example_.*\.jl$'): + shutil.copy(example[1], tmp_examples_device) + + def finish(self): + root_dir = self.get_root_dir() + + # Copy IP Connection examples + if self.get_config_name().space == 'Tinkerforge': + for example in common.find_examples(root_dir, r'^example_.*\.jl$'): + shutil.copy(example[1], self.tmp_examples_dir) + + # Copy bindings and readme + for filename in self.get_released_files() + ['device_factory.jl']: + shutil.copy(os.path.join(self.get_bindings_dir(), filename), self.tmp_source_devices_dir) + + shutil.copy(os.path.join(root_dir, 'PyTinkerforge.jl'), self.tmp_source_dir) + #shutil.copy(os.path.join(root_dir, 'ip_connection_base.jl'), self.tmp_source_dir) + shutil.copy(os.path.join(root_dir, 'ip_connection.jl'), self.tmp_source_dir) + shutil.copy(os.path.join(root_dir, 'changelog.txt'), self.tmp_dir) + shutil.copy(os.path.join(root_dir, 'readme.txt'), self.tmp_dir) + shutil.copy(os.path.join(root_dir, 'LICENSE'), self.tmp_dir) + + # Make Project.toml + version = self.get_changelog_version() + + common.specialize_template(os.path.join(root_dir, 'Project.toml.template'), + os.path.join(self.tmp_dir, 'Project.toml'), + {'<>': '.'.join(version)}) + + # Make zip + #self.create_zip_file(self.tmp_dir) + +def generate(root_dir, language, internal): + common.generate(root_dir, language, internal, JuliaPyZipGenerator) + +if __name__ == '__main__': + args = common.dockerize('juliapy', __file__, add_internal_argument=True) + + generate(os.getcwd(), 'en', args.internal) diff --git a/juliapy/ip_connection.jl b/juliapy/ip_connection.jl new file mode 100644 index 00000000..999afa38 --- /dev/null +++ b/juliapy/ip_connection.jl @@ -0,0 +1,153 @@ +export TinkerforgeDevice +abstract type TinkerforgeDevice end + +# returned by get_connection_state +export TinkerforgeIPConConnectionState, CONNECTION_STATE_DISCONNECTED, CONNECTION_STATE_CONNECTED, CONNECTION_STATE_PENDING +@enum TinkerforgeIPConConnectionState begin + CONNECTION_STATE_DISCONNECTED = 0 + CONNECTION_STATE_CONNECTED = 1 + CONNECTION_STATE_PENDING = 2 # auto-reconnect in process +end + +export IPConnection +mutable struct IPConnection + ipconInternal::PyObject + + function IPConnection() + package = pyimport("tinkerforge.ip_connection") + ipconInternal = package.IPConnection() + + return new(ipconInternal) + end +end + +export connect +""" + $(SIGNATURES) + +Creates a TCP/IP connection to the given *host* and *port*. The host +and port can point to a Brick Daemon or to a WIFI/Ethernet Extension. +Devices can only be controlled when the connection was established +successfully. +Blocks until the connection is established and throws an exception if +there is no Brick Daemon or WIFI/Ethernet Extension listening at the +given host and port. +""" +connect(ipcon::IPConnection, host::IPAddr, port::Integer) = connect(ipcon, string(host), port) +connect(ipcon::IPConnection, host::String, port::Integer) = ipcon.ipconInternal.connect(host, port) +connect(ipcon::IPConnection) = connect(ipcon, "localhost", 4223) + +export disconnect +""" + $(SIGNATURES) + +Disconnects the TCP/IP connection from the Brick Daemon or the +WIFI/Ethernet Extension. +""" +disconnect(ipcon::IPConnection) = ipcon.ipconInternal.disconnect() + +export authenticate +""" + $(SIGNATURES) + +Performs an authentication handshake with the connected Brick Daemon or +WIFI/Ethernet Extension. If the handshake succeeds the connection switches +from non-authenticated to authenticated state and communication can +continue as normal. If the handshake fails then the connection gets closed. +Authentication can fail if the wrong secret was used or if authentication +is not enabled at all on the Brick Daemon or the WIFI/Ethernet Extension. +For more information about authentication see +https://www.tinkerforge.com/en/doc/Tutorials/Tutorial_Authentication/Tutorial.html +""" +authenticate(ipcon::IPConnection, secret::String) = ipcon.ipconInternal.authenticate(secret) + +export get_connection_state +""" + $(SIGNATURES) + +Can return the following states: +- CONNECTION_STATE_DISCONNECTED: No connection is established. +- CONNECTION_STATE_CONNECTED: A connection to the Brick Daemon or + the WIFI/Ethernet Extension is established. +- CONNECTION_STATE_PENDING: IP Connection is currently trying to + connect. +""" +get_connection_state(ipcon::IPConnection) = TinkerforgeIPConConnectionState(ipcon.ipconInternal.get_connection_state()) + +export set_auto_reconnect +""" + $(SIGNATURES) + +Enables or disables auto-reconnect. If auto-reconnect is enabled, +the IP Connection will try to reconnect to the previously given +host and port, if the connection is lost. +Default value is *True*. +""" +set_auto_reconnect(ipcon::IPConnection, auto_reconnect::Bool) = ipcon.ipconInternal.set_auto_reconnect(auto_reconnect) + +export get_auto_reconnect +""" + $(SIGNATURES) + +Returns *true* if auto-reconnect is enabled, *false* otherwise. +""" +get_auto_reconnect(ipcon::IPConnection) = ipcon.ipconInternal.get_auto_reconnect() + + +export set_timeout +""" + $(SIGNATURES) + +Sets the timeout in seconds for getters and for setters for which the +response expected flag is activated. +Default timeout is 2.5. +""" +set_timeout(ipcon::IPConnection, timeout::Real) = ipcon.ipconInternal.set_timeout(timeout) + +export get_timeout +""" + $(SIGNATURES) + +Returns the timeout as set by set_timeout. +""" +get_timeout(ipcon::IPConnection) = ipcon.ipconInternal.get_timeout() + +export enumerate +""" + $(SIGNATURES) + +Broadcasts an enumerate request. All devices will respond with an +enumerate callback. +""" +enumerate(ipcon::IPConnection) = ipcon.ipconInternal.enumerate() + +export wait +""" + $(SIGNATURES) + +Stops the current thread until unwait is called. +This is useful if you rely solely on callbacks for events, if you want +to wait for a specific callback or if the IP Connection was created in +a thread. +Wait and unwait act in the same way as "acquire" and "release" of a +semaphore. +""" +wait(ipcon::IPConnection) = ipcon.ipconInternal.wait() + +export unwait +""" + $(SIGNATURES) + +Unwaits the thread previously stopped by wait. +Wait and unwait act in the same way as "acquire" and "release" of +a semaphore. +""" +unwait(ipcon::IPConnection) = ipcon.ipconInternal.unwait() + +export register_callback +""" + $(SIGNATURES) + +Registers the given *function* with the given *callback_id*. +""" +register_callback(ipcon::IPConnection, callback_id::Integer, func::Function) = ipcon.ipconInternal.register_callback(callback_id, func) diff --git a/juliapy/ip_connection_old.jl b/juliapy/ip_connection_old.jl new file mode 100644 index 00000000..666dcd1e --- /dev/null +++ b/juliapy/ip_connection_old.jl @@ -0,0 +1,1485 @@ +export TinkerforgeError +abstract type TinkerforgeError <: Exception end + +export TinkerforgeTimeoutError +struct TinkerforgeTimeoutError <: TinkerforgeError + description::String +end + +export TinkerforgeNotAddedError +struct TinkerforgeNotAddedError <: TinkerforgeError + description::String +end + +export TinkerforgeAlreadyConnectedError +struct TinkerforgeAlreadyConnectedError <: TinkerforgeError + description::String +end + +export TinkerforgeNotConnectedError +struct TinkerforgeNotConnectedError <: TinkerforgeError + description::String +end + +export TinkerforgeInvalidParameterError +struct TinkerforgeInvalidParameterError <: TinkerforgeError + description::String +end + +export TinkerforgeNotSupportedError +struct TinkerforgeNotSupportedError <: TinkerforgeError + description::String +end + +export TinkerforgeUnknownErrorCodeError +struct TinkerforgeUnknownErrorCodeError <: TinkerforgeError + description::String +end + +export TinkerforgeStreamOutOfSyncError +struct TinkerforgeStreamOutOfSyncError <: TinkerforgeError + description::String +end + +export TinkerforgeInvalidUidError +struct TinkerforgeInvalidUidError <: TinkerforgeError + description::String +end + +export TinkerforgeNonASCIICharInSecretError +struct TinkerforgeNonASCIICharInSecretError <: TinkerforgeError + description::String +end + +export TinkerforgeWrongDeviceTypeError +struct TinkerforgeWrongDeviceTypeError <: TinkerforgeError + description::String +end + +export TinkerforgeDeviceReplacedError +struct TinkerforgeDeviceReplacedError <: TinkerforgeError + description::String +end + +export TinkerforgeWrongResponseLengthError +struct TinkerforgeWrongResponseLengthError <: TinkerforgeError + description::String +end + +export ValueError +struct ValueError <: Exception + msg::String +end + +export DeviceIdentifierCheck, DEVICE_IDENTIFIER_CHECK_PENDING, DEVICE_IDENTIFIER_CHECK_MATCH, DEVICE_IDENTIFIER_CHECK_MISMATCH +@enum DeviceIdentifierCheck begin + DEVICE_IDENTIFIER_CHECK_PENDING = 0 + DEVICE_IDENTIFIER_CHECK_MATCH = 1 + DEVICE_IDENTIFIER_CHECK_MISMATCH = 2 +end + +export ResponseExpected, RESPONSE_EXPECTED_INVALID_FUNCTION_ID, RESPONSE_EXPECTED_ALWAYS_TRUE, RESPONSE_EXPECTED_TRUE, RESPONSE_EXPECTED_FALSE +@enum ResponseExpected begin + RESPONSE_EXPECTED_INVALID_FUNCTION_ID = 0 + RESPONSE_EXPECTED_ALWAYS_TRUE = 1 # getter + RESPONSE_EXPECTED_TRUE = 2 # setter + RESPONSE_EXPECTED_FALSE = 3 # setter, default +end + +export TinkerforgeDevice +abstract type TinkerforgeDevice end + +function _initDevice(device::TinkerforgeDevice) + uid = py"base58decode"(device.uid_string) + + if uid > (1 << 64) - 1 + @warn "Code disabled" + #throw(TinkerforgeInvalidUidError("UID '$(device.uid_string)' is too big")) + end + + if uid > ((1 << 32) - 1) + uid_ = uid64_to_uid32(uid) + end + + if uid == 0 + throw(TinkerforgeInvalidUidError("UID '$(device.uid_string)' is empty or maps to zero")) + end + + device.uid = uid + + device.response_expected[:FUNCTION_ADC_CALIBRATE] = RESPONSE_EXPECTED_ALWAYS_TRUE + device.response_expected[:FUNCTION_GET_ADC_CALIBRATION] = RESPONSE_EXPECTED_ALWAYS_TRUE + device.response_expected[:FUNCTION_READ_BRICKLET_UID] = RESPONSE_EXPECTED_ALWAYS_TRUE + device.response_expected[:FUNCTION_WRITE_BRICKLET_UID] = RESPONSE_EXPECTED_ALWAYS_TRUE +end + +function pack_struct(format::String, data) + return py"pack_struct"(format, data) +end + +function unpack_struct(format::String, data) + return py"unpack_struct"(format, data) +end + +export get_api_version +""" +Returns the API version (major, minor, revision) of the bindings for +this device. +""" +function get_api_version(device::TinkerforgeDevice) + return device.api_version +end + +export get_response_expected +""" +Returns the response expected flag for the function specified by the +*function_id* parameter. It is *true* if the function is expected to +send a response, *false* otherwise. + +For getter functions this is enabled by functionault and cannot be disabled, +because those functions will always send a response. For callback +configuration functions it is enabled by functionault too, but can be +disabled via the set_response_expected function. For setter functions +it is disabled by functionault and can be enabled. + +Enabling the response expected flag for a setter function allows to +detect timeouts and other error conditions calls of this setter as +well. The device will then send a response for this purpose. If this +flag is disabled for a setter function then no response is sent and +errors are silently ignored, because they cannot be detected. +""" +function get_response_expected(device::TinkerforgeDevice, function_id::Integer) + if function_id < 0 || function_id >= length(device.response_expected) + throw(ValueError("Function ID $function_id out of range")) + end + + flag = function_id + + if flag == :RESPONSE_EXPECTED_INVALID_FUNCTION_ID + throw(ValueError("Invalid function ID $function_id")) + end + + return flag in [:RESPONSE_EXPECTED_ALWAYS_TRUE, :RESPONSE_EXPECTED_TRUE] +end + +export set_response_expected +""" +Changes the response expected flag of the function specified by the +*function_id* parameter. This flag can only be changed for setter +(functionault value: *false*) and callback configuration functions +(functionault value: *true*). For getter functions it is always enabled. + +Enabling the response expected flag for a setter function allows to +detect timeouts and other error conditions calls of this setter as +well. The device will then send a response for this purpose. If this +flag is disabled for a setter function then no response is sent and +errors are silently ignored, because they cannot be detected. +""" +function set_response_expected(device::TinkerforgeDevice, function_id, response_expected) + if function_id < 0 || function_id >= length(device.response_expected) + throw(ValueError("Function ID {$function_id} out of range")) + end + + flag = device.response_expected[function_id] + + if flag == RESPONSE_EXPECTED_INVALID_FUNCTION_ID + throw(ValueError("Invalid function ID {$function_id}")) + end + + if flag == RESPONSE_EXPECTED_ALWAYS_TRUE + throw(ValueError("Response Expected flag cannot be changed for function ID {$function_id}")) + end + + if bool(response_expected) + device.response_expected[function_id] = RESPONSE_EXPECTED_TRUE + else + device.response_expected[function_id] = RESPONSE_EXPECTED_FALSE + end +end + +export set_response_expected_all +""" +Changes the response expected flag for all setter and callback +configuration functions of this device at once. +""" +function set_response_expected_all(device::TinkerforgeDevice, response_expected) + if bool(response_expected) + flag = RESPONSE_EXPECTED_TRUE + else + flag = RESPONSE_EXPECTED_FALSE + end + + for i in range(length(device.response_expected)) + if device.response_expected[i] in [RESPONSE_EXPECTED_TRUE, RESPONSE_EXPECTED_FALSE] + device.response_expected[i] = flag + end + end +end + +# internal +function check_validity(device::TinkerforgeDevice) + if device.replaced + throw(TinkerforgeDeviceReplacedError("Device has been replaced")) + end + + if device.device_identifier < 0 + return nothing + end + + if device.device_identifier_check == :DEVICE_IDENTIFIER_CHECK_MATCH + return nothing + end + + lock(device.device_identifier_lock) do + if device.device_identifier_check == :DEVICE_IDENTIFIER_CHECK_PENDING + device_identifier = send_request(device.ipcon, 255, (), "", 33, "8s 8s c 3B 3B H")[5+1] # .get_identity + + if device_identifier == device.device_identifier + device.device_identifier_check = :DEVICE_IDENTIFIER_CHECK_MATCH + else + device.device_identifier_check = :DEVICE_IDENTIFIER_CHECK_MISMATCH + device.wrong_device_display_name = get_device_display_name(device_identifier) + end + end + + if device.device_identifier_check == :DEVICE_IDENTIFIER_CHECK_MISMATCH + throw(TinkerforgeWrongDeviceTypeError("UID $(device.uid_string) belongs to a $(device.wrong_device_display_name) instead of the expected $(device.device_display_name)")) + end + end +end + +export IPConnection +Base.@kwdef mutable struct IPConnection + host::Union{IPAddr, Missing} + port::Union{Integer, Missing} + timeout::Real = 2.5 + auto_reconnect::Bool = true + auto_reconnect_allowed::Bool = false + auto_reconnect_pending::Bool = false + auto_reconnect_internal::Bool = false + connect_failure_callback::Union{Function, Nothing} = nothing + sequence_number_lock::Base.AbstractLock = Base.ReentrantLock() + next_sequence_number::Integer = 0 # protected by sequence_number_lock + authentication_lock::Base.AbstractLock = Base.ReentrantLock() + next_authentication_nonce::Integer = 0 # protected by authentication_lock + devices = Dict() + replace_lock::Base.AbstractLock = Base.ReentrantLock() # used to synchronize replacements in the devices dict + registered_callbacks::Dict{Integer, Function} = Dict{Integer, Function}() + socket = nothing # protected by socket_lock + socket_id = 0 # protected by socket_lock + socket_lock::Base.AbstractLock = Base.ReentrantLock() + socket_send_lock::Base.AbstractLock = Base.ReentrantLock() + receive_flag::Bool = false + receive_thread = nothing + callback = nothing + disconnect_probe_flag::Bool = false + disconnect_probe_queue = nothing + disconnect_probe_thread = nothing + waiter::Base.Semaphore = Base.Semaphore(10) + brickd = nothing +end + +export BrickDaemon +Base.@kwdef mutable struct BrickDaemon <: TinkerforgeDevice + replaced::Bool + uid::Union{Integer, Missing} + uid_string::String + ipcon::IPConnection + device_identifier::Integer + device_display_name::String + device_url_part::String + device_identifier_lock::Base.AbstractLock + device_identifier_check::DeviceIdentifierCheck # protected by device_identifier_lock + wrong_device_display_name::String # protected by device_identifier_lock + api_version::Tuple{Integer, Integer, Integer} + registered_callbacks::Dict{Integer, Function} + expected_response_function_id::Union{Integer, Nothing} # protected by request_lock + expected_response_sequence_number::Union{Integer, Nothing} # protected by request_lock + response_queue::DataStructures.Queue{Symbol} + request_lock::Base.AbstractLock + stream_lock::Base.AbstractLock + + callbacks::Dict{Symbol, Integer} + callback_formats::Dict{Symbol, Tuple{Integer, String}} + high_level_callbacks::Dict{Symbol, Integer} + id_definitions::Dict{Symbol, Integer} + constants::Dict{Symbol, Integer} + response_expected::DefaultDict{Symbol, ResponseExpected} + + """ + Creates an object with the unique device ID *uid* and adds it to + the IP Connection *ipcon*. + """ + function BrickDaemon(uid::String, ipcon::IPConnection) + replaced = false + uid_string = uid + device_identifier = 0 + device_display_name = "Brick Daemon" + device_url_part = "brick_daemon" # internal; TODO: Not specified; is this correct? + device_identifier_lock = Base.ReentrantLock() + device_identifier_check = DEVICE_IDENTIFIER_CHECK_PENDING # protected by device_identifier_lock + wrong_device_display_name = "?" # protected by device_identifier_lock + api_version = (0, 0, 0) + registered_callbacks = Dict{Integer, Function}() + expected_response_function_id = nothing # protected by request_lock + expected_response_sequence_number = nothing # protected by request_lock + response_queue = DataStructures.Queue{Symbol}() + request_lock = Base.ReentrantLock() + stream_lock = Base.ReentrantLock() + + callbacks = Dict{Symbol, Integer}() + callback_formats = Dict{Symbol, Tuple{Integer, String}}() + high_level_callbacks = Dict{Symbol, Integer}() + id_definitions = Dict{Symbol, Integer}() + constants = Dict{Symbol, Integer}() + response_expected = DefaultDict{Symbol, ResponseExpected}(RESPONSE_EXPECTED_INVALID_FUNCTION_ID) + + #connected_uid::String + #position::Char + #hardware_version::Vector{Integer} + #firmware_version::Vector{Integer} + + device = new( + replaced, + missing, + uid_string, + ipcon, + device_identifier, + device_display_name, + device_url_part, + device_identifier_lock, + device_identifier_check, + wrong_device_display_name, + api_version, + registered_callbacks, + expected_response_function_id, + expected_response_sequence_number, + response_queue, + request_lock, + stream_lock, + callbacks, + callback_formats, + high_level_callbacks, + id_definitions, + constants, + response_expected + ) + _initDevice(device) + + device.api_version = (2, 0, 0) + + device.id_definitions[:FUNCTION_GET_AUTHENTICATION_NONCE] = 1 + device.id_definitions[:FUNCTION_AUTHENTICATE] = 2 + + device.response_expected[:FUNCTION_GET_AUTHENTICATION_NONCE] = RESPONSE_EXPECTED_ALWAYS_TRUE + device.response_expected[:FUNCTION_AUTHENTICATE] = RESPONSE_EXPECTED_TRUE + + add_device(ipcon, device) + + return device + end +end + +export get_authentication_nonce +function get_authentication_nonce(device::BrickDaemon) + return send_request(device, :FUNCTION_GET_AUTHENTICATION_NONCE, (), "", 12, "4B") +end + +export authenticate +function authenticate(device::BrickDaemon, client_nonce, digest) + send_request(device, :FUNCTION_AUTHENTICATE, (client_nonce, digest), "4B 20B", 0, "") +end + +""" +Creates an IP Connection object that can be used to enumerate the available +devices. It is also required for the constructor of Bricks and Bricklets. +""" +function IPConnection(host::IPAddr, port::Integer) + ipcon = IPConnection(host=host, port=port) + brickd = BrickDaemon("2", ipcon) + ipcon.brickd = brickd + + return ipcon +end +function IPConnection(host::String, port::Integer) + if host == "localhost" + hostIP = ip"127.0.0.1" + else + hostIP = parse(IPAddr, host) + end + + return IPConnection(hostIP, port) +end + +export TinkerforgeIPConFunctions, FUNCTION_ENUMERATE, FUNCTION_ADC_CALIBRATE, + FUNCTION_GET_ADC_CALIBRATION, FUNCTION_READ_BRICKLET_UID, FUNCTION_WRITE_BRICKLET_UID, + FUNCTION_DISCONNECT_PROBE +@enum TinkerforgeIPConFunctions begin + FUNCTION_ENUMERATE = 254 + FUNCTION_ADC_CALIBRATE = 251 + FUNCTION_GET_ADC_CALIBRATION = 250 + FUNCTION_READ_BRICKLET_UID = 249 + FUNCTION_WRITE_BRICKLET_UID = 248 + FUNCTION_DISCONNECT_PROBE = 128 +end + +export TinkerforgeIPConCallbacks, CALLBACK_ENUMERATE, CALLBACK_CONNECTED, CALLBACK_DISCONNECTED +@enum TinkerforgeIPConCallbacks begin + CALLBACK_ENUMERATE = 253 + CALLBACK_CONNECTED = 0 + CALLBACK_DISCONNECTED = 1 +end + +BROADCAST_UID = 0 +DISCONNECT_PROBE_INTERVAL = 5 + +# enumeration_type parameter to the enumerate callback +export TinkerforgeIPConEnumerationType, ENUMERATION_TYPE_AVAILABLE, ENUMERATION_TYPE_CONNECTED, ENUMERATION_TYPE_DISCONNECTED +@enum TinkerforgeIPConEnumerationType begin + ENUMERATION_TYPE_AVAILABLE = 0 + ENUMERATION_TYPE_CONNECTED = 1 + ENUMERATION_TYPE_DISCONNECTED = 2 +end + +# connect_reason parameter to the connected callback +export TinkerforgeIPConConnectReason, CONNECT_REASON_REQUEST, CONNECT_REASON_AUTO_RECONNECT +@enum TinkerforgeIPConConnectReason begin + CONNECT_REASON_REQUEST = 0 + CONNECT_REASON_AUTO_RECONNECT = 1 +end + +# disconnect_reason parameter to the disconnected callback +export TinkerforgeIPConDisconnectReason, DISCONNECT_REASON_REQUEST, DISCONNECT_REASON_ERROR, DISCONNECT_REASON_SHUTDOWN +@enum TinkerforgeIPConDisconnectReason begin + DISCONNECT_REASON_REQUEST = 0 + DISCONNECT_REASON_ERROR = 1 + DISCONNECT_REASON_SHUTDOWN = 2 +end + +# returned by get_connection_state +export TinkerforgeIPConConnectionState, CONNECTION_STATE_DISCONNECTED, CONNECTION_STATE_CONNECTED, CONNECTION_STATE_PENDING +@enum TinkerforgeIPConConnectionState begin + CONNECTION_STATE_DISCONNECTED = 0 + CONNECTION_STATE_CONNECTED = 1 + CONNECTION_STATE_PENDING = 2 # auto-reconnect in process +end + +export TinkerforgeIPConQueueState, QUEUE_EXIT, QUEUE_META, QUEUE_PACKET +@enum TinkerforgeIPConQueueState begin + QUEUE_EXIT = 0 + QUEUE_META = 1 + QUEUE_PACKET = 2 +end + +export CallbackContext +mutable struct CallbackContext + queue::Union{Base.Channel, Nothing} + thread::Union{Base.Task, Nothing} + packet_dispatch_allowed::Bool + lock::Union{Base.AbstractLock, Nothing} + + function CallbackContext() + return new(nothing, nothing, false, nothing) + end +end + +export connect +""" +Creates a TCP/IP connection to the given *host* and *port*. The host +and port can point to a Brick Daemon or to a WIFI/Ethernet Extension. + +Devices can only be controlled when the connection was established +successfully. + +Blocks until the connection is established and throws an exception if +there is no Brick Daemon or WIFI/Ethernet Extension listening at the +given host and port. +""" +function connect(ipcon::IPConnection) + lock(ipcon.socket_lock) do + if !isnothing(ipcon.socket) + throw(TinkerforgeAlreadyConnectedError("Already connected to $(ipcon.host):$(ipcon.port)")) + end + + connect_unlocked(ipcon, false) + end + + return nothing +end +connect(host::IPAddr, port::Integer) = connect(IPConnection(host, port)) +connect(host::String, port::Integer) = connect(IPConnection(host, port)) + +export disconnect +""" +Disconnects the TCP/IP connection from the Brick Daemon or the +WIFI/Ethernet Extension. +""" +function disconnect(ipcon::IPConnection) + lock(ipcon.socket_lock) do + ipcon.auto_reconnect_allowed = false + + if ipcon.auto_reconnect_pending + # abort potentially pending auto reconnect + ipcon.auto_reconnect_pending = false + else + if isnothing(ipcon.socket) + throw(TinkerforgeNotConnectedError("Not connected")) + end + + disconnect_unlocked(ipcon) + end + + # end callback thread + callback = ipcon.callback + ipcon.callback = nothing + end + + # do this outside of socket_lock to allow calling (dis-)connect from + # the callbacks while blocking on the join call here + callback.queue.put((IPConnection.QUEUE_META, + (IPConnection.CALLBACK_DISCONNECTED, + IPConnection.DISCONNECT_REASON_REQUEST, None))) + callback.queue.put((IPConnection.QUEUE_EXIT, None)) + + if threading.current_thread() != callback.thread + callback.thread.join() + end + + return nothing +end + +export authenticate +""" +Performs an authentication handshake with the connected Brick Daemon or +WIFI/Ethernet Extension. If the handshake succeeds the connection switches +from non-authenticated to authenticated state and communication can +continue as normal. If the handshake fails then the connection gets closed. +Authentication can fail if the wrong secret was used or if authentication +is not enabled at all on the Brick Daemon or the WIFI/Ethernet Extension. + +For more information about authentication see +https://www.tinkerforge.com/en/doc/Tutorials/Tutorial_Authentication/Tutorial.html +""" +function authenticate(ipcon::IPConnection, secret) + try + secret_bytes = acsii(secret) + catch e + if e isa ArgumentError + throw(TinkerforgeNonASCIICharInSecretError("Authentication secret contains non-ASCII characters")) + else + rethrow() + end + end + + lock(ipcon.authentication_lock) do + if ipcon.next_authentication_nonce == 0 + try + ipcon.next_authentication_nonce = unpack_struct("> 6) & 0xFFFFFFFF) + subseconds + getpid() + end + end + + server_nonce = get_authentication_nonce(ipcon.brickd) + client_nonce = unpack_struct("<4B", pack_struct(" tmp.status != StatusOpen ? close(tmp) : nothing, timeout) + try + tmp = Sockets.connect(tmp, ipcon.host, ipcon.port) + catch e + error("Could not connect to $(ipcon.host) on port $(ipcon.port) + since the operations was timed out after $(timeout) seconds!") + end + catch e + if ipcon.auto_reconnect_internal + if is_auto_reconnect + return + end + + if !isnothing(ipcon.connect_failure_callback) + connect_failure_callback(ipcon, e) + end + + ipcon.auto_reconnect_allowed = true + + # FIXME: don't misuse disconnected-callback here to trigger an auto-reconnect + # because not actual connection has been established yet + put!(ipcon.callback.queue, (QUEUE_META, (CALLBACK_DISCONNECTED, DISCONNECT_REASON_ERROR, nothing))) + else + # end callback thread + if !is_auto_reconnect + put!(ipcon.callback.queue, (QUEUE_EXIT, nothing)) + + if Threads.threadid() != Threads.threadid(ipcon.callback.thread) + Base.wait(ipcon.callback.thread) + end + + ipcon.callback = nothing + end + end + end + + ipcon.socket = tmp + ipcon.socket_id += 1 + + # create disconnect probe thread + try + ipcon.disconnect_probe_flag = true + ipcon.disconnect_probe_queue = Base.Channel() + ipcon.disconnect_probe_thread = Threads.@spawn disconnect_probe_loop(ipcon) + #self.disconnect_probe_thread.daemon = True + #self.disconnect_probe_thread.start() + catch e + ipcon.disconnect_probe_thread = nothing + + # close socket + close(ipcon.socket) + ipcon.socket = nothing + + # end callback thread + if !is_auto_reconnect + put!(ipcon.callback.queue, (QUEUE_EXIT, nothing)) + + if Threads.threadid() != Threads.threadid(ipcon.callback.thread) + Base.wait(ipcon.callback.thread) + end + + ipcon.callback = nothing + end + + rethrow() + end + + # create receive thread + ipcon.callback.packet_dispatch_allowed = true + + try + ipcon.receive_flag = true + ipcon.receive_thread = Threads.@spawn receive_loop(ipcon, ipcon.socket_id) + #ipcon.receive_thread.daemon = True + #ipcon.receive_thread.start() + catch e + ipcon.receive_thread = nothing + + # close socket + disconnect_unlocked(ipcon) + + # end callback thread + if !is_auto_reconnect + put!(ipcon.callback.queue, (QUEUE_EXIT, nothing)) + + if Threads.threadid() != Threads.threadid(ipcon.callback.thread) + wait(ipcon.callback.thread) + end + + ipcon.callback = nothing + end + + rethrow() + end + + ipcon.auto_reconnect_allowed = false + ipcon.auto_reconnect_pending = false + + if is_auto_reconnect + connect_reason = CONNECT_REASON_AUTO_RECONNECT + else + connect_reason = CONNECT_REASON_REQUEST + end + + put!(ipcon.callback.queue, (QUEUE_META, (CALLBACK_CONNECTED, connect_reason, nothing))) +end + +# internal +function disconnect_unlocked(ipcon::IPConnection) + # NOTE: assumes that socket is not None and socket_lock is locked + + # end disconnect probe thread + put!(ipcon.disconnect_probe_queue, true) + wait(ipcon.disconnect_probe_thread) # FIXME: use a timeout? + ipcon.disconnect_probe_thread = nothing + + # stop dispatching packet callbacks before ending the receive + # thread to avoid timeout exceptions due to callback functions + # trying to call getters + if Threads.threadid() != Threads.threadid(ipcon.callback.thread) + # FIXME: cannot hold callback lock here because this can + # deadlock due to an ordering problem with the socket lock + #with self.callback.lock: + if true + ipcon.callback.packet_dispatch_allowed = false + end + else + ipcon.callback.packet_dispatch_allowed = false + end + + # end receive thread + ipcon.receive_flag = false + + # TODO: Do we need an alternative? + # try + # self.socket.shutdown(socket.SHUT_RDWR) + # catch socket.error + # pass + # end + + if !isnothing(ipcon.receive_thread) + Base.wait(ipcon.receive_thread) # FIXME: use a timeout? + ipcon.receive_thread = nothing + end + + # close socket + close(ipcon.socket) + ipcon.socket = nothing +end + +# internal +function set_auto_reconnect_internal(ipcon::IPConnection, auto_reconnect, connect_failure_callback) + ipcon.auto_reconnect_internal = auto_reconnect + ipcon.connect_failure_callback = connect_failure_callback +end + +# internal +function add_device(ipcon::IPConnection, device) + lock(ipcon.replace_lock) do + replaced_device = get(ipcon.devices, device.uid, nothing) + + if !isnothing(replaced_device) + replaced_device.replaced = true + end + + ipcon.devices[device.uid] = device + end +end + +# internal +function receive_loop(ipcon::IPConnection, socket_id) + # if sys.hexversion < 0x03000000 + # pending_data = '' + # else + # pending_data = bytes() + # end + pending_data = "" + + while ipcon.receive_flag + try + data = read(ipcon.socket, 8192) + catch e + rethrow() # TODO: just for now + #socket.timeout + #continue + + #socket.error + # if self.receive_flag: + # e = sys.exc_info()[1] + # if e.errno == errno.EINTR: + # continue + # end + + # self.handle_disconnect_by_peer(IPConnection.DISCONNECT_REASON_ERROR, socket_id, False) + # end + + # break + end + + if length(data) == 0 + if ipcon.receive_flag + handle_disconnect_by_peer(ipcon, DISCONNECT_REASON_SHUTDOWN, socket_id, false) + end + + break + end + + pending_data *= data + + while ipcon.receive_flag + if length(pending_data) < 8 + # Wait for complete header + break + end + + length = get_length_from_data(pending_data) + + if length(pending_data) < length_ + # Wait for complete packet + break + end + + packet = pending_data[1:length_] + pending_data = pending_data[length_+1:end] + + handle_response(ipcon, packet) + end + end +end + +# internal +function dispatch_meta(ipcon::IPConnection, function_id, parameter, socket_id) + if function_id == CALLBACK_CONNECTED + cb = get(ipcon.registered_callbacks, CALLBACK_CONNECTED, nothing) + + if !isnothing(cb) + cb(parameter) + end + elseif function_id == CALLBACK_DISCONNECTED + if parameter != DISCONNECT_REASON_REQUEST + # need to do this here, the receive_loop is not allowed to + # hold the socket_lock because this could cause a deadlock + # with a concurrent call to the (dis-)connect function + lock(ipcon.socket_lock) do + # don't close the socket if it got disconnected or + # reconnected in the meantime + if !isnothing(ipcon.socket) && ipcon.socket_id == socket_id + # end disconnect probe thread + put!(ipcon.disconnect_probe_queue, true) + wait(ipcon.disconnect_probe_thread) # FIXME: use a timeout? + ipcon.disconnect_probe_thread = nothing + + # close socket + close(ipcon.socket) + ipcon.socket = nothing + end + end + end + + # FIXME: wait a moment here, otherwise the next connect + # attempt will succeed, even if there is no open server + # socket. the first receive will then fail directly + sleep(0.1) + + cb = get(ipcon.registered_callbacks, CALLBACK_DISCONNECTED, nothing) + + if !isnothing(cb) + cb(parameter) + end + + if parameter != DISCONNECT_REASON_REQUEST && ipcon.auto_reconnect && ipcon.auto_reconnect_allowed + ipcon.auto_reconnect_pending = true + retry = true + + # block here until reconnect. this is okay, there is no + # callback to deliver when there is no connection + while retry + retry = false + + lock(ipcon.socket_lock) do + if ipcon.auto_reconnect_allowed && isnothing(ipcon.socket) + try + connect_unlocked(ipcon, true) + catch e + retry = true + end + else + ipcon.auto_reconnect_pending = false + end + end + + if retry + sleep(0.1) + end + end + end + end +end + +# internal +function dispatch_packet(ipcon::IPConnection, packet) + uid = get_uid_from_data(packet) + length = get_length_from_data(packet) + function_id = get_function_id_from_data(packet) + payload = packet[8:end] # TODO: Have a close look with indexing!!! This is still like in python + + if function_id == CALLBACK_ENUMERATE + + cb = ipcon.registered_callbacks[CALLBACK_ENUMERATE] + + if isnothing(cb) + return + end + + if length(packet) != 34 + return # silently ignoring callback with wrong length + end + + uid, connected_uid, position, hardware_version, firmware_version, device_identifier, enumeration_type = unpack_payload(payload, "8s 8s c 3B 3B H B") + + cb(uid, connected_uid, position, hardware_version, + firmware_version, device_identifier, enumeration_type) + + return + end + + device = ipcon.devices[uid] + + if isnothing(device) + return + end + + try + device.check_validity() + catch e + return # silently ignoring callback for invalid device + end + + if -function_id in device.high_level_callbacks + hlcb = device.high_level_callbacks[-function_id] # [roles, options, data] + length, form = device.callback_formats[function_id] # FIXME: currently assuming that low-level callback has more than one element + + if length(packet) != length + return # silently ignoring callback with wrong length + end + + llvalues = unpack_payload(payload, form) + has_data = false + data = nothing + + if !isnothing(hlcb[1]["fixed_length"]) + length = hlcb[1]["fixed_length"] + else + length = llvalues[findfirst("stream_length", hlcb[0])] + end + + if !hlcb[1]["single_chunk"] + chunk_offset = llvalues[findfirst("stream_chunk_offset", hlcb[0])] + else + chunk_offset = 0 + end + + chunk_data = llvalues[findfirst("stream_chunk_data", hlcb[0])] + + if isnothing(hlcb[2]) # no stream in-progress + if chunk_offset == 0 # stream starts + hlcb[2] = chunk_data + + if length(hlcb[2]) >= length_ # stream complete + has_data = true + data = hlcb[2][:length_] + hlcb[2] = nothing + end + else # ignore tail of current stream, wait for next stream start + #pass + end + else # stream in-progress + if chunk_offset != length(hlcb[2]) # stream out-of-sync + has_data = true + data = nothing + hlcb[2] = nothing + else # stream in-sync + hlcb[2] += chunk_data + + if length(hlcb[2]) >= length_ # stream complete + has_data = true + data = hlcb[2][:length] + hlcb[2] = nothing + end + end + end + + cb = device.registered_callbacks[-function_id] + + if has_data && !isnothing(cb) + result = [] + + for (role, llvalue) in zip(hlcb[0], llvalues) + if role == "stream_chunk_data" + append!(result, data) + elseif isnothing(role) + append!(result, llvalue) + end + end + + cb(tuple(result)...) + end + end + + cb = device.registered_callbacks[function_id] + + if !isnothing(cb) + length, form = get(device.callback_formats, function_id, (nothing, nothing)) + + if isnothing(length_) + return # silently ignore registered but unknown callback + end + + if length(packet) != length_ + return # silently ignoring callback with wrong length + end + + if length(form) == 0 + cb() + elseif !(' ' in form) + cb(unpack_payload(payload, form)) + else + cb(unpack_payload(payload, form)...) + end + end +end + +# internal +function callback_loop(ipcon::IPConnection) + callback = ipcon.callback + while true + kind, data = take!(callback.queue) + + # FIXME: cannot hold callback lock here because this can + # deadlock due to an ordering problem with the socket lock + #with callback.lock: + if true + if kind == QUEUE_EXIT + break + elseif kind == QUEUE_META + dispatch_meta(ipcon, data...) + elseif kind == QUEUE_PACKET + # don't dispatch callbacks when the receive thread isn't running + if callback.packet_dispatch_allowed + dispatch_packet(ipcon, data) + end + end + end + end +end + +# internal +# NOTE: the disconnect probe thread is not allowed to hold the socket_lock at any +# time because it is created and joined while the socket_lock is locked +function disconnect_probe_loop(ipcon::IPConnection) + disconnect_probe_queue = ipcon.disconnect_probe_queue + request, _, _ = create_packet_header(ipcon, nothing, 8, FUNCTION_DISCONNECT_PROBE) + + while true + # Here comes a crude way to express the following Pyrhon connected + # try + # disconnect_probe_queue.get(true, DISCONNECT_PROBE_INTERVAL) + # break + # catch queue.Empty + # pass + + wait_disconnect(timeout) = begin + t = @async take!(disconnect_probe_queue) + for i=1:20 + if istaskdone(t) + return true + end + sleep(timeout / N ) + end + return false + end + + if wait_disconnect(DISCONNECT_PROBE_INTERVAL) + break + end + + if ipcon.disconnect_probe_flag + try + lock(self.socket_send_lock) do + while true + try + send(ipcon.socket, request) + break + catch e + #socket.timeout + continue + end + end + end + catch e + #socket.error + handle_disconnect_by_peer(ipcon, DISCONNECT_REASON_ERROR, ipcon.socket_id, false) + break + end + else + ipcon.disconnect_probe_flag = true + end + end +end + +# internal +function send(ipcon::IPConnection, packet) + @warn "locking within send" + lock(ipcon.socket_lock) do + @warn "locked socket" + if isnothing(ipcon.socket) + throw(TinkerforgeNotConnectedError("Not connected")) + end + + try + lock(ipcon.socket_send_lock) do + @warn "locked send" + Base.write(ipcon.socket, packet) + end + catch e + #socket.error + handle_disconnect_by_peer(ipcon, DISCONNECT_REASON_ERROR, nothing, true) + throw(TinkerforgeNotConnectedError("Not connected")) + end + + ipcon.disconnect_probe_flag = false + end +end + +# internal +function send_request(device::TinkerforgeDevice, function_id::Symbol, data, form, length_ret, form_ret) + ipcon = device.ipcon + payload = py"pack_payload"(data, form) + header, response_expected, sequence_number = create_packet_header(ipcon, device, 8 + length(payload), device.id_definitions[function_id]) + request = header * payload + + @warn "going into if" response_expected + + if response_expected + @warn "locking" + lock(device.request_lock) do + @warn "locked" + device.expected_response_function_id = function_id + device.expected_response_sequence_number = sequence_number + + try + @warn "try sending" + send(ipcon, request) + @warn "sending done" + + while true + # Here comes a crude way to express the following Python code + # response = device.response_queue.get(true, self.timeout) + + wait_get(timeout) = begin + t = @async take!(device.response_queue) + for i=1:20 + if istaskdone(t) + return fetch(t) + end + sleep(timeout / N ) + end + return false + end + + @warn "waiting for get" + response = wait_get(ipcon.timeout) + if response == false + throw(TinkerforgeTimeoutError("Timeout occured")) + end + + if function_id == get_function_id_from_data(response) && sequence_number == get_sequence_number_from_data(response) + # ignore old responses that arrived after the timeout expired, but before setting + # expected_response_function_id and expected_response_sequence_number back to None + break + end + end + catch e + #queue.Empty + if e isa TinkerforgeTimeoutError + msg = "Did not receive response for function $function_id in time" + throw(TinkerforgeTimeoutError(msg)) + end + finally + device.expected_response_function_id = nothing + device.expected_response_sequence_number = nothing + end + end + + error_code = get_error_code_from_data(response) + + if error_code == 0 + if length_ret == 0 + length_ret = 8 # setter with response-expected enabled + end + + if length(response) != length_ret + msg = "Expected response of $length_ret byte for function ID $function_id, got $(length(response)) byte instead" + throw(TinkerforgeWrongResponseLengthError(msg)) + end + elseif error_code == 1 + msg = "Got invalid parameter for function $function_id" + throw(TinkerforgeInvalidParameterError(msg)) + elseif error_code == 2 + msg = "Function $function_id is not supported" + throw(TinkerforgeNotSupportedError(msg)) + else + msg = "Function $function_id returned an unknown error" + throw(TinkerforgeUnknownErrorCodeError(msg)) + end + + if length(form_ret) > 0 + return unpack_payload(response[8:end], form_ret) + end + else + @warn "sending without an expected response" + send(ipcon, request) + end +end + +# internal +function get_next_sequence_number(ipcon::IPConnection) + lock(ipcon.sequence_number_lock) do + sequence_number = ipcon.next_sequence_number + 1 + ipcon.next_sequence_number = sequence_number % 15 + return sequence_number + end +end + +# internal +function handle_response(ipcon::IPConnection, packet) + ipcon.disconnect_probe_flag = false + + function_id = get_function_id_from_data(packet) + sequence_number = get_sequence_number_from_data(packet) + + if sequence_number == 0 && function_id == CALLBACK_ENUMERATE + if CALLBACK_ENUMERATE in ipcon.registered_callbacks + enqueue!(ipcon.callback.queue, (QUEUE_PACKET, packet)) + end + + return + end + + uid = get_uid_from_data(packet) + device = ipcon.devices[uid] + + if isnothing(device) + return # Response from an unknown device, ignoring it + end + + if sequence_number == 0 + if function_id in device.registered_callbacks || -function_id in device.high_level_callbacks + enqueue!(ipcon.callback.queue, (QUEUE_PACKET, packet)) + end + + return + end + + if device.expected_response_function_id == function_id && device.expected_response_sequence_number == sequence_number + enqueue!(device.response_queue, packet) + return + end + + # Response seems to be OK, but can't be handled +end + +# internal +function handle_disconnect_by_peer(ipcon::IPConnection, disconnect_reason::String, socket_id::Integer, disconnect_immediately::Bool) + # NOTE: assumes that socket_lock is locked if disconnect_immediately is true + + ipcon.auto_reconnect_allowed = true + + if disconnect_immediately + disconnect_unlocked(ipcon) + end + + enqueue!(ipcon.callback.queue, (QUEUE_META, (CALLBACK_DISCONNECTED, disconnect_reason, socket_id))) +end + +# internal +function create_packet_header(ipcon::IPConnection, device::TinkerforgeDevice, length_::Integer, function_id::Integer) + uid = BROADCAST_UID + sequence_number = get_next_sequence_number(ipcon) + r_bit = 0 + + if !isnothing(device) + uid = device.uid + + if get_response_expected(device, function_id) + r_bit = 1 + end + end + + sequence_number_and_options = (sequence_number << 4) | (r_bit << 3) + + return (pack_struct(" +Copyright (C) 2011-2013 Olaf Lüke +Copyright (C) 2020 Erik Fleckstein + +juliapy_common.py: Common library for generation of JuliaPy bindings and documentation + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public +License along with this program; if not, write to the +Free Software Foundation, Inc., 59 Temple Place - Suite 330, +Boston, MA 02111-1307, USA. +""" + +from generators import common + +class JuliaPyDevice(common.Device): + def get_juliapy_import_name(self): + return self.get_category().under + '_' + self.get_name().under + + def get_juliapy_struct_name(self): + return self.get_category().camel + self.get_name().camel + +class JuliaPyPacket(common.Packet): + def get_juliapy_parameters(self, high_level=False): + parameters = [] + + for element in self.get_elements(direction='in', high_level=high_level): + parameters.append(element.get_name().under) + + return ', '.join(parameters) + +class JuliaPyElement(common.Element): + juliapy_types = { + 'int8': 'Integer', + 'uint8': 'Integer', + 'int16': 'Integer', + 'uint16': 'Integer', + 'int32': 'Integer', + 'uint32': 'Integer', + 'int64': 'Integer', + 'uint64': 'Integer', + 'float': 'Real', + 'bool': 'Bool', + 'char': 'String', # This is a bad fix for tuple depacking in identities + 'string': 'String' + } + + juliapy_struct_formats = { + 'int8': 'b', + 'uint8': 'B', + 'int16': 'h', + 'uint16': 'H', + 'int32': 'i', + 'uint32': 'I', + 'int64': 'q', + 'uint64': 'Q', + 'float': 'f', + 'bool': '!', + 'char': 'c', + 'string': 's' + } + + juliapy_default_item_values = { + 'int8': '0', + 'uint8': '0', + 'int16': '0', + 'uint16': '0', + 'int32': '0', + 'uint32': '0', + 'int64': '0', + 'uint64': '0', + 'float': '0.0', + 'bool': 'False', + 'char': "'\\0'", + 'string': "nothing" + } + + juliapy_parameter_coercions = { + 'int8': ('Int8({0})', 'convert(Vector{{Int8}}, {0})'), + 'uint8': ('UInt8({0})', 'convert(Vector{{UInt8}}, {0})'), + 'int16': ('Int16({0})', 'convert(Vector{{Int16}}, {0})'), + 'uint16': ('UInt16({0})', 'convert(Vector{{UInt16}}, {0})'), + 'int32': ('Int32({0})', 'convert(Vector{{Int32}}, {0})'), + 'uint32': ('UInt32({0})', 'convert(Vector{{UInt32}}, {0})'), + 'int64': ('Int64({0})', 'convert(Vector{{Int64}}, {0})'), + 'uint64': ('UInt64({0})', 'convert(Vector{{UInt64}}, {0})'), + 'float': ('Real({0})', 'convert(Vector{{Float}}, {0})'), + 'bool': ('Bool({0})', 'convert(Vector{{Bool}}, {0})'), + 'char': ('String({0})', 'convert(Vector{{String}}, {0})'), + 'string': ('String({0})', 'convert(Vector{{String}}, {0})') + } + + def format_value(self, value): + if isinstance(value, list): + result = [] + + for subvalue in value: + result.append(self.format_value(subvalue)) + + return '[{0}]'.format(', '.join(result)) + + type_ = self.get_type() + + if type_ == 'float': + return common.format_float(value) + + if type_ == 'bool': + return str(bool(value)) + + if type_ in ['char', 'string']: + return '"{0}"'.format(value.replace('"', '\\"')) + + return str(value) + + def get_juliapy_name(self, index=None): + return self.get_name(index=index).under + + def get_juliapy_type(self, cardinality=None): + assert cardinality == None or (isinstance(cardinality, int) and cardinality > 0), cardinality + + juliapy_type = JuliaPyElement.juliapy_types[self.get_type()] + + if cardinality == None: + cardinality = self.get_cardinality() + + if cardinality == 1 or self.get_type() == 'string': + return juliapy_type + + return 'Vector{{{0}}}'.format(juliapy_type) + + def get_juliapy_struct_format(self): + f = JuliaPyElement.juliapy_struct_formats[self.get_type()] + cardinality = self.get_cardinality() + + if cardinality > 1: + f = str(cardinality) + f + + return f + + def get_juliapy_default_item_value(self): + value = JuliaPyElement.juliapy_default_item_values[self.get_type()] + + if value == None: + common.GeneratorError('Invalid array item type: ' + self.get_type()) + + return value + + def get_juliapy_parameter_coercion(self): + coercion = JuliaPyElement.juliapy_parameter_coercions[self.get_type()] + + if self.get_cardinality() == 1: + return coercion[0] + else: + return coercion[1] + +class JuliaPyGeneratorTrait: + def get_bindings_name(self): + return 'juliapy' + + def get_bindings_display_name(self): + return 'JuliaPy' + + def get_doc_null_value_name(self): + return 'nothing' + + def get_doc_formatted_param(self, element): + return element.get_name().under + + def generates_high_level_callbacks(self): + return True diff --git a/juliapy/readme.txt b/juliapy/readme.txt new file mode 100644 index 00000000..b1ac2fad --- /dev/null +++ b/juliapy/readme.txt @@ -0,0 +1,18 @@ +Tinkerforge Python Bindings +=========================== + +The Python bindings allow you to control Tinkerforge Bricks and Bricklets from +your Python scripts. This ZIP file contains: + + source/ -- source code of the bindings (install with setup.py script) + examples/ -- examples for every Brick and Bricklet + +For more information about the Python bindings (including setup instructions) +go to: + + https://www.tinkerforge.com/en/doc/Software/API_Bindings_Python.html (English) + https://www.tinkerforge.com/de/doc/Software/API_Bindings_Python.html (German) + +The Python bindings are also available from the Python Package Index (PyPI): + + https://pypi.python.org/pypi/tinkerforge diff --git a/juliapy/test_ip_connection.py b/juliapy/test_ip_connection.py new file mode 100644 index 00000000..43287f58 --- /dev/null +++ b/juliapy/test_ip_connection.py @@ -0,0 +1,161 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import sys +from ip_connection import create_char, create_char_list, create_string, pack_payload, unpack_payload + +def b(value): + if sys.hexversion < 0x03000000: + return value + else: + return bytes(map(ord, value)) + +# +# char +# + +assert(create_char('a') == 'a') # str + +if sys.hexversion < 0x03000000: + assert(create_char(u'a') == 'a') # unicode +else: + assert(create_char(b'a') == 'a') # bytes + +assert(create_char(bytearray([97])) == 'a') # bytearray +assert(create_char(97) == 'a') # int + +for c in range(256): + k = (c + 1) % 256 + + assert(create_char(chr(c)) == chr(c)) # str + assert(create_char(chr(c)) != chr(k)) # str + + if sys.hexversion < 0x03000000: + assert(create_char(unichr(c)) == chr(c)) # unicode + assert(create_char(unichr(c)) != chr(k)) + else: + assert(create_char(bytes([c])) == chr(c)) # bytes + assert(create_char(bytes([c])) != chr(k)) # bytes + + assert(create_char(bytearray([c])) == chr(c)) # bytearray + assert(create_char(bytearray([c])) != chr(k)) # bytearray + assert(create_char(c) == chr(c)) # int + assert(create_char(c) != chr(k)) # int + +try: + create_char('ab') # str + assert(False) +except: + pass + +if sys.hexversion < 0x03000000: + try: + create_char(u'ab') # unicode + assert(False) + except: + pass +else: + try: + create_char(b'ab') # bytes + assert(False) + except: + pass + +try: + create_char(bytearray([42, 17])) # bytearray + assert(False) +except: + pass + +try: + create_char([42, 17]) # int + assert(False) +except: + pass + +try: + create_char(256) # int + assert(False) +except: + pass + +# +# char list +# + +assert(create_char_list('') == []) # str +assert(create_char_list('a') == ['a']) # str +assert(create_char_list('ab') == ['a', 'b']) # str +assert(create_char_list([]) == []) +assert(create_char_list(['a']) == ['a']) # str +assert(create_char_list(['a', 'b']) == ['a', 'b']) # str + +if sys.hexversion < 0x03000000: + assert(create_char_list(u'') == []) # unicode + assert(create_char_list(u'a') == ['a']) # unicode + assert(create_char_list(u'ab') == ['a', 'b']) # unicode + assert(create_char_list([u'a']) == ['a']) # unicode + assert(create_char_list([u'a', u'b']) == ['a', 'b']) # unicode +else: + assert(create_char_list(b'') == []) # bytes + assert(create_char_list(b'a') == ['a']) # bytes + assert(create_char_list(b'ab') == ['a', 'b']) # bytes + assert(create_char_list([b'a']) == ['a']) # bytes + assert(create_char_list([b'a', b'b']) == ['a', 'b']) # bytes + +assert(create_char_list(bytearray([])) == []) # bytearray +assert(create_char_list(bytearray([97])) == ['a']) # bytearray +assert(create_char_list(bytearray([97, 98])) == ['a', 'b']) # bytearray +assert(create_char_list([97]) == ['a']) # int +assert(create_char_list([97, 98]) == ['a', 'b']) # int + +# +# string +# + +assert(create_string('') == '') # str +assert(create_string('a') == 'a') # str +assert(create_string('ab') == 'ab') # str +assert(create_string([]) == '') +assert(create_string(['a']) == 'a') # str +assert(create_string(['a', 'b']) == 'ab') # str + +if sys.hexversion < 0x03000000: + assert(create_string(u'') == '') # unicode + assert(create_string(u'a') == 'a') # unicode + assert(create_string(u'ab') == 'ab') # unicode + assert(create_string([u'a']) == 'a') # unicode + assert(create_string([u'a', u'b']) == 'ab') # unicode +else: + assert(create_string(b'') == '') # bytes + assert(create_string(b'a') == 'a') # bytes + assert(create_string(b'ab') == 'ab') # bytes + assert(create_string([b'a']) == 'a') # bytes + assert(create_string([b'a', b'b']) == 'ab') # bytes + +assert(create_string(bytearray([])) == '') # bytearray +assert(create_string(bytearray([97])) == 'a') # bytearray +assert(create_string(bytearray([97, 98])) == 'ab') # bytearray +assert(create_string([97]) == 'a') # int +assert(create_string([97, 98]) == 'ab') # int + +# +# pack_payload +# + +assert(pack_payload(('a',), 's') == b('a')) +assert(pack_payload(('abc',), '5s') == b('abc\0\0')) +assert(pack_payload(('abc\xff',), '5s') == b('abc\xff\0')) +assert(pack_payload(('a',), 'c') == b('a')) +assert(pack_payload((['a', 'b', 'c'],), '3c') == b('abc')) + +# +# unpack_payload +# + +assert(unpack_payload(b('a'), 's') == 'a') +assert(unpack_payload(b('abc'), '3s') == 'abc') +assert(unpack_payload(b('abc\xff\0'), '5s') == 'abc\xff') +assert(unpack_payload(b('a'), 'c') == 'a') +assert(unpack_payload(b('abc'), '3c') == ('a', 'b', 'c')) +assert(unpack_payload(b('a\xff\0'), '3c') == ('a', '\xff', '\0')) diff --git a/juliapy/test_python_bindings.py b/juliapy/test_python_bindings.py new file mode 100644 index 00000000..4f159895 --- /dev/null +++ b/juliapy/test_python_bindings.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Python Bindings Tester +Copyright (C) 2012-2014, 2017-2018 Matthias Bolte + +test_python_bindings.py: Tests the Python bindings + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public +License along with this program; if not, write to the +Free Software Foundation, Inc., 59 Temple Place - Suite 330, +Boston, MA 02111-1307, USA. +""" + +import sys + +if sys.hexversion < 0x3040000: + print('Python >= 3.4 required') + sys.exit(1) + +import os +import importlib.util +import importlib.machinery + +def create_generators_module(): + generators_dir = os.path.split(os.path.dirname(os.path.realpath(__file__)))[0] + + if sys.hexversion < 0x3050000: + generators_module = importlib.machinery.SourceFileLoader('generators', os.path.join(generators_dir, '__init__.py')).load_module() + else: + generators_spec = importlib.util.spec_from_file_location('generators', os.path.join(generators_dir, '__init__.py')) + generators_module = importlib.util.module_from_spec(generators_spec) + + generators_spec.loader.exec_module(generators_module) + + sys.modules['generators'] = generators_module + +if 'generators' not in sys.modules: + create_generators_module() + +from generators import common + +class PythonTester(common.Tester): + def __init__(self, root_dir, python, extra_paths): + common.Tester.__init__(self, 'python', '.py', root_dir, comment=python, subdirs=['examples', 'source'], extra_paths=extra_paths) + + self.python = python + + def test(self, cookie, tmp_dir, path, extra): + args = [self.python, + '-c', + 'import py_compile; py_compile.compile("{0}", doraise=True)'.format(path)] + + self.execute(cookie, args) + +class PylintTester(common.Tester): + def __init__(self, root_dir, python, comment, extra_paths): + common.Tester.__init__(self, 'python', '.py', root_dir, comment=comment, subdirs=['examples', 'source'], extra_paths=extra_paths) + + self.python = python + + def test(self, cookie, tmp_dir, path, extra): + teardown = None + + if self.python == 'python3': + with open(path, 'r') as f: + code = f.read() + + code = code.replace('raw_input(', 'input(') + path_check = path.replace('.py', '_check.py') + + with open(path_check, 'w') as f: + f.write(code) + + path = path_check + teardown = lambda: [os.remove(path)] + + args = [self.python, + '-c', + 'import sys; sys.path.insert(0, "{0}"); import pylint; pylint.run_pylint()'.format(os.path.join(tmp_dir, 'source')), + '-E', + '--disable=no-name-in-module', + path] + + self.execute(cookie, args, teardown=teardown) + +def test(root_dir): + extra_paths = [os.path.join(root_dir, '../../weather-station/demo/starter_kit_weather_station_demo/main.py'), + os.path.join(root_dir, '../../weather-station/write_to_lcd/python/weather_station.py'), + os.path.join(root_dir, '../../hardware-hacking/remote_switch/python/remote_switch.py'), + os.path.join(root_dir, '../../hardware-hacking/smoke_detector/python/smoke_detector.py'), + os.path.join(root_dir, '../../blinkenlights/demo/starter_kit_blinkenlights_demo/main.py'), + os.path.join(root_dir, '../../blinkenlights/demo/starter_kit_blinkenlights_demo/fire_widget.py'), + os.path.join(root_dir, '../../blinkenlights/demo/starter_kit_blinkenlights_demo/pong_widget.py'), + os.path.join(root_dir, '../../blinkenlights/demo/starter_kit_blinkenlights_demo/tetris_widget.py'), + os.path.join(root_dir, '../../blinkenlights/demo/starter_kit_blinkenlights_demo/images_widget.py'), + os.path.join(root_dir, '../../blinkenlights/demo/starter_kit_blinkenlights_demo/rainbow_widget.py'), + os.path.join(root_dir, '../../blinkenlights/demo/starter_kit_blinkenlights_demo/text_widget.py'), + os.path.join(root_dir, '../../blinkenlights/fire/python/fire.py'), + os.path.join(root_dir, '../../blinkenlights/games/python/keypress.py'), + os.path.join(root_dir, '../../blinkenlights/games/python/pong.py'), + os.path.join(root_dir, '../../blinkenlights/games/python/repeated_timer.py'), + os.path.join(root_dir, '../../blinkenlights/games/python/tetris.py'), + os.path.join(root_dir, '../../blinkenlights/images/python/images.py'), + os.path.join(root_dir, '../../blinkenlights/rainbow/python/rainbow.py'), + os.path.join(root_dir, '../../blinkenlights/text/python/text.py')] + + if not PythonTester(root_dir, 'python', extra_paths).run(): + return False + + # FIXME: doesn't handle PyQt related super false-positves yet + if not PylintTester(root_dir, 'python', 'pylint', []).run():#extra_paths).run(): + return False + + if not PythonTester(root_dir, 'python3', extra_paths).run(): + return False + + # FIXME: doesn't handle PyQt related super false-positves yet + return PylintTester(root_dir, 'python3', 'pylint3', []).run()#extra_paths).run() + +if __name__ == '__main__': + common.dockerize('python', __file__) + + test(os.getcwd())