diff --git a/_templates/versions.html b/_templates/versions.html
index f755644..1c6607b 100644
--- a/_templates/versions.html
+++ b/_templates/versions.html
@@ -4,15 +4,19 @@
aria-label="Versions">
- v: {{ current_version }}
+ {%- set _cp = current_version.split('-') %}
+ v: {{ _cp[0] }}{% if _cp|length > 1 %} ({{ _cp[1:] | join('-') }}){% endif %}
- Versions
{%- for ver in versions %}
+ {%- set _parts = ver.split('-') %}
+ {%- set _base = _parts[0] %}
+ {%- set _suffix = _parts[1:] | join('-') %}
-
- {{ ver }}{% if ver == latest_version %} (latest){% endif %}
+ {{ _base }}{% if ver == latest_version %} (latest){% elif _suffix %} ({{ _suffix }}){% endif %}
{%- endfor %}
diff --git a/build_versions.sh b/build_versions.sh
index 87ee0a4..eb20148 100644
--- a/build_versions.sh
+++ b/build_versions.sh
@@ -7,6 +7,16 @@
#
# Output lands in _build/html/
/ with a root index.html that
# redirects to the latest (highest semver) version.
+#
+# On the main branch the latest version is always built from the current
+# working tree (HEAD) rather than the tagged commit. This means that a
+# merged PR triggers a correct build immediately without having to move
+# the tag first. The tag still determines the version label (e.g. v1.4.0)
+# shown in the flyout.
+#
+# On any other branch a single-version "preview" build of the current
+# working tree is produced instead. Updating the tag before pushing is
+# fine for those branches since they don't use the working-tree shortcut.
set -euo pipefail
@@ -221,24 +231,40 @@ fi
git -C "$SRCDIR" fetch --tags 2>/dev/null || true
# -------------------------------------------------------------------
-# Collect version tags (sorted descending so index 0 = latest)
+# Collect version tags (sorted descending so highest semver comes first)
# -------------------------------------------------------------------
if [[ $# -gt 0 ]]; then
- TAGS=("$@")
+ mapfile -t ALL_TAGS < <(printf '%s\n' "$@" | sort -rV)
else
- mapfile -t TAGS < <(git -C "$SRCDIR" tag -l 'v[0-9]*' --sort=-version:refname)
+ mapfile -t ALL_TAGS < <(git -C "$SRCDIR" tag -l 'v[0-9]*' --sort=-version:refname)
fi
-if [[ ${#TAGS[@]} -eq 0 ]]; then
- echo "ERROR: No version tags found. Create at least one tag like v1.4.0."
+# Separate into clean release tags (vX.Y.Z) and pre-release tags (vX.Y.Z-suffix).
+# Only release tags are eligible for "latest"; pre-release tags appear in the
+# flyout with their suffix shown as a label, e.g. v1.4.1 (dev).
+RELEASE_TAGS=()
+PRERELEASE_TAGS=()
+for _t in "${ALL_TAGS[@]}"; do
+ if [[ "$_t" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
+ RELEASE_TAGS+=("$_t")
+ else
+ PRERELEASE_TAGS+=("$_t")
+ fi
+done
+
+if [[ ${#RELEASE_TAGS[@]} -eq 0 ]]; then
+ echo "ERROR: No release version tags found. Create at least one vX.Y.Z tag (no suffix)."
exit 1
fi
-LATEST="${TAGS[0]}"
-ALL_JSON=$(printf '%s\n' "${TAGS[@]}" | python3 -c \
- 'import sys,json; print(json.dumps([l.strip() for l in sys.stdin]))')
+# Build order: release versions first (desc), then pre-release (desc).
+LATEST="${RELEASE_TAGS[0]}"
+TAGS=("${RELEASE_TAGS[@]}" "${PRERELEASE_TAGS[@]}")
+ALL_JSON=$(python3 -c 'import sys,json; print(json.dumps(sys.argv[1:]))' "${TAGS[@]}")
echo "=== Building versions: ${TAGS[*]} (latest=$LATEST) ==="
+[[ ${#PRERELEASE_TAGS[@]} -gt 0 ]] && echo " Pre-release: ${PRERELEASE_TAGS[*]}"
+echo " '$LATEST' will be built from main HEAD (not the tag) so merged PRs are live immediately."
rm -rf "$OUTDIR"
mkdir -p "$OUTDIR"
@@ -249,30 +275,40 @@ mkdir -p "$OUTDIR"
for TAG in "${TAGS[@]}"; do
echo ""
echo "--- Building $TAG ---"
- WORKDIR=$(mktemp -d)
- # Export the full tree at that tag into a temp directory
- git -C "$SRCDIR" archive "$TAG" | tar -x -C "$WORKDIR"
- # Copy templates, static, and config from the current branch so the
- # version flyout logic stays consistent across versions.
- # Keep each tag's own index.rst to avoid cross-version content breakage.
- cp -r "$SRCDIR/_templates" "$WORKDIR/_templates"
- cp -r "$SRCDIR/_static" "$WORKDIR/_static"
- cp "$SRCDIR/conf.py" "$WORKDIR/conf.py"
+ if [[ "$TAG" == "$LATEST" ]]; then
+ # Use the current working tree for the latest version so that content
+ # merged to main is published without needing to move the tag first.
+ WORKDIR="$SRCDIR"
+ else
+ WORKDIR=$(mktemp -d)
+ # Export the full tree at that tag into a temp directory
+ git -C "$SRCDIR" archive "$TAG" | tar -x -C "$WORKDIR"
+
+ # Copy templates, static, and config from the current branch so the
+ # version flyout logic stays consistent across versions.
+ cp -r "$SRCDIR/_templates" "$WORKDIR/_templates"
+ cp -r "$SRCDIR/_static" "$WORKDIR/_static"
+ cp "$SRCDIR/conf.py" "$WORKDIR/conf.py"
+ fi
SGW_CURRENT_VERSION="$TAG" \
SGW_ALL_VERSIONS="$ALL_JSON" \
+ SGW_LATEST_VERSION="$LATEST" \
sphinx-build -b html -j auto "$WORKDIR" "$OUTDIR/$TAG"
- # Build a PDF from the same tagged tree so each version has a matching
+ # Build a PDF from the same tree so each version has a matching
# downloadable PDF in //_static/SGWirelessDocs.pdf.
SGW_CURRENT_VERSION="$TAG" \
SGW_ALL_VERSIONS="$ALL_JSON" \
+ SGW_LATEST_VERSION="$LATEST" \
build_pdf_noninteractive "$WORKDIR" "$WORKDIR/_build" "$TAG"
mkdir -p "$OUTDIR/$TAG/_static"
cp "$WORKDIR/_build/latex/SGWirelessDocs.pdf" "$OUTDIR/$TAG/_static/SGWirelessDocs.pdf"
- rm -rf "$WORKDIR"
+ if [[ "$WORKDIR" != "$SRCDIR" ]]; then
+ rm -rf "$WORKDIR"
+ fi
done
# -------------------------------------------------------------------
diff --git a/conf.py b/conf.py
index 223c97f..4854365 100644
--- a/conf.py
+++ b/conf.py
@@ -35,6 +35,9 @@
_current_ver = os.environ.get("SGW_CURRENT_VERSION", f"v{release}")
_all_versions_json = os.environ.get("SGW_ALL_VERSIONS", _json.dumps([_current_ver]))
_all_versions = _json.loads(_all_versions_json)
+_preview_versions_json = os.environ.get("SGW_PREVIEW_VERSIONS", "[]")
+_preview_versions = _json.loads(_preview_versions_json)
+_latest_ver = os.environ.get("SGW_LATEST_VERSION", _all_versions[0] if _all_versions else _current_ver)
templates_path = ["_templates"]
exclude_patterns = ["_build", ".venv", "Thumbs.db", ".DS_Store", "README.md"]
@@ -72,7 +75,8 @@
# Version selector data (populated by build_versions.sh via env vars)
"current_version": _current_ver,
"versions": _all_versions,
- "latest_version": _all_versions[0], # first entry = latest
+ "latest_version": _latest_ver,
+ "preview_versions": _preview_versions,
}
# (sphinx-multiversion settings removed — see build_versions.sh)
diff --git a/programming-references/ctrl-client.rst b/programming-references/ctrl-client.rst
index d21931f..a010e32 100644
--- a/programming-references/ctrl-client.rst
+++ b/programming-references/ctrl-client.rst
@@ -9,14 +9,15 @@ Contents
- Connection
- Miscellaneous
- Examples
+- Logging & Debugging
Fields
------
-.. _ctrlsend_fieldpin_number-value-timestamp0-device_tokennone:
+.. _ctrlsend_fieldpin_number-value-timestamp0:
-ctrl.send_field(pin_number, value, [timestamp=0, device_token=None])
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ctrl.send_field(pin_number, value, [timestamp=0])
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Send a field value to CTRL. Arguments are:
@@ -25,23 +26,19 @@ Send a field value to CTRL. Arguments are:
string, etc.)
- ``timestamp``: Optional. Unix timestamp in seconds. If set to 0 (default),
the server will use the current time
-- ``device_token``: Optional. Device token for sending data to a different
- device
-.. _ctrlsend_field_mapmap-timestamp0-device_tokennone:
+.. _ctrlsend_field_mapmap-timestamp0:
-ctrl.send_field_map(map, [timestamp=0, device_token=None])
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ctrl.send_field_map(map, [timestamp=0])
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-Send multiple field values to CTRL in a single message using a dictionary/map.
-Arguments are:
+Send multiple field values to CTRL in a single message using a list of
+``[pin, value]`` pairs. Arguments are:
-- ``map``: A dictionary where keys are pin/field numbers and values are the
- data to send. Example: ``{1: 25.5, 2: 60.3, 3: "online"}``
+- ``map``: A list of pairs where each pair contains the pin/field number and
+ the value to send. Example: ``[[1, 25.5], [2, 60.3], [3, "online"]]``
- ``timestamp``: Optional. Unix timestamp in seconds. If set to 0 (default),
the server will use the current time
-- ``device_token``: Optional. Device token for sending data to a different
- device
.. _ctrlsend_ping_message:
@@ -51,29 +48,13 @@ ctrl.send_ping_message()
Sends a ping (is-alive) message to CTRL. The platform will answer with a
``pong`` message if connected via WiFi or LTE-M
-.. _ctrlsend_info_message:
+.. _ctrlsend_info_messagedevice_idnone-releasenone:
-ctrl.send_info_message()
-~~~~~~~~~~~~~~~~~~~~~~~~
+ctrl.send_info_message([device_id=None, release=None])
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Send an info message to CTRL containing the device type and firmware version.
-.. _ctrlsend_battery_levelbattery_level:
-
-ctrl.send_battery_level(battery_level)
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Sends the battery level to `Ctrl `__. The argument
-``battery_level`` can be any integer.
-
-You can define ``battery_level`` with a function depending on your hardware.
-
-.. code:: python
-
- def battery_level():
- return 3.7
- ctrl.send_battery_level(battery_level())
-
--------------
Configuration
@@ -121,10 +102,10 @@ shows how it is loaded from the build-in frozen code.
The CTRL API offers several helper functions to work with the configuration:
-.. _ctrlread_configfilenamectrl_configjson-reconnectfalse:
+.. _ctrlread_configfilectrl_configjson-reconnectfalse:
-ctrl.read_config([filename='/ctrl_config.json', reconnect=False])
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ctrl.read_config([file='/ctrl_config.json', reconnect=False])
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Load the CTRL configuration file. By default, this is loaded from
``/ctrl_config.json`` If reconnect=True, ctrl will disconnect and re-connect
@@ -154,10 +135,10 @@ Update a ``key`` and ``value`` of the default configuration file. This will
- ``silent``: set ``silent`` to ``True`` to not print a message to REPL.
- ``reconnect``: calls ``ctrl.reconnect()``
-.. _ctrlset_configkey-valuenone-permanenttrue-silentfalse-reconnectfalse:
+.. _ctrlset_configkeynone-valuenone-permanenttrue-silentfalse-reconnectfalse:
-ctrl.set_config(key, [value=None, permanent=True, silent=False, reconnect=False])
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ctrl.set_config([key=None, value=None, permanent=True, silent=False, reconnect=False])
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Set a ``key`` and ``value`` of the default configuration file. This will
overwrite any existing settings for the specified key.
@@ -219,10 +200,10 @@ need to load a configuration file before calling this. If you are using the
WiFi or LTE-M connection, and it is already available, CTRL will use the
existing connection.
-.. _ctrlenable_ltecarrie-apn-typeip-cid1-bandnone-bandsnone-mode0-fallbackfalse:
+.. _ctrlenable_ltecarrier-apn-typeip-cid1-bandnone-bandsnone-mode0-fallbackfalse:
-ctrl.enable_lte(carrie, apn, [type='IP', cid=1, band=None, bands=None, mode=0, fallback=False])
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ctrl.enable_lte(carrier, apn, [type='IP', cid=1, band=None, bands=None, mode=0, fallback=False])
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Enable connecting via LTE-M connection to CTRL. Enter the paramters you would
normally enter for an LTE connection. If fallback is True, will add LTE-M as
@@ -255,18 +236,26 @@ ctrl.connect_wifi([timeout=120])
Manually connect to CTRL using WiFi and the settings from the configuration
file. The ``timeout`` option is in seconds.
-.. _ctrlconnect_lora_otaatimeout120:
+.. _ctrlconnect_lora_otaatimeout240:
-ctrl.connect_lora_otaa([timeout=120])
+ctrl.connect_lora_otaa([timeout=240])
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Manually connect to CTRL using LoRa OTAA and the settings from the
configuration file. The ``timeout`` option is in seconds.
-.. _ctrldisconnect:
+.. _ctrlconnect_lora_abptimeout240:
+
+ctrl.connect_lora_abp([timeout=240])
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Manually connect to CTRL using LoRa ABP and the settings from the configuration
+file. The ``timeout`` option is in seconds.
+
+.. _ctrldisconnectforcetrue:
-ctrl.disconnect()
-~~~~~~~~~~~~~~~~~
+ctrl.disconnect([force=True])
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Disconnect from CTRL gracefully. Closes the MQTT connection and socket.
@@ -291,10 +280,10 @@ ctrl.ifconfig()
Returns a tuple with IP information when connected over WiFi or LTE-M
-.. _ctrlenable_ssl:
+.. _ctrlenable_sslca_filenone-dump_cafalse:
-ctrl.enable_ssl()
-~~~~~~~~~~~~~~~~~
+ctrl.enable_ssl([ca_file=None, dump_ca=False])
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Enable SSL on the CTRL connection
@@ -304,10 +293,10 @@ Enable SSL on the CTRL connection
**Note that SSL is not currently supported by the CTRL platform**
-.. _ctrldump_cafilecertsgw-capem:
+.. _ctrldump_caca_filecertsgw-capem:
-ctrl.dump_ca([file='/cert/sgw-ca.pem'])
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ctrl.dump_ca([ca_file='/cert/sgw-ca.pem'])
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Write CTRL ROOT CA certificate to file. In order for the firmware to load the
certificate, it needs to be present in the file system. While the firmware has
@@ -349,13 +338,21 @@ ctrl.get_network_type()
Returns the network type currently in use
-.. _ctrldebugnew_level-update_nvstrue:
+.. _ctrldebugnew_levelnone-update_nvstrue:
-ctrl.debug(new_level, [update_nvs=True])
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ctrl.debug([new_level=None, update_nvs=True])
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Sets the debug level at new_level [0-65565] update_nvs will preserve the
-setting after reset
+setting after reset For more details, see CTRL Client Integration.
+
+.. _ctrldbgcomponentnone-levelnone:
+
+ctrl.dbg([component=None, level=None])
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Sets or gets component-level debug settings. For more details, see CTRL Client
+Integration.
.. _ctrlztpnew_statusnone:
@@ -406,15 +403,159 @@ Optionally, you can send a timestamp:
print('sent signal {}'.format(i))
time.sleep(10)
+You can also send multiple fields in one message using ``send_field_map()``:
+
+.. code:: python
+
+ ctrl.send_field_map([[1, 25.5], [2, 60.3], [3, "online"]])
+
+You can define and use a custom logging component:
+
+.. code:: python
+
+ from ctrl_debug import print_debug, register_component
+
+ DEBUG_COMPONENT = "my_module"
+ register_component(DEBUG_COMPONENT, color='yellow')
+
+ print_debug(5, "Something happened", component=DEBUG_COMPONENT)
+
+ ctrl.send_field(255, "An ERROR occured!")
+
+.. _logging--debugging:
+
+Logging & Debugging
+~~~~~~~~~~~~~~~~~~~
+
+The CTRL client library (``ctrl_debug.py``) uses the logging system internally.
+Each CTRL component registers its own logging component under the ``"ctrl"``
+subsystem:
+
+.. list-table::
+ :header-rows: 1
+
+ -
+
+ - Component File
+ - Component Name
+ - Color
+ -
+
+ - ``ctrl_debug.py``
+ - ``main``
+ - green
+ -
+
+ - ``ctrl_config.py``
+ - ``config``
+ - yellow
+ -
+
+ - ``ctrl_connection.py``
+ - ``connection``
+ - blue
+ -
+
+ - ``ctrl_protocol.py``
+ - ``protocol``
+ - white
+ -
+
+ - ``ctrl_library.py``
+ - ``library``
+ - green
+ -
+
+ - ``ctrl_sensors.py``
+ - ``sensors``
+ - cyan
+ -
+
+ - ``ctrl_pyconfig.py``
+ - ``pyconfig``
+ - purple
+
+Enabling Debug Output
+^^^^^^^^^^^^^^^^^^^^^
+
+Use ``ctrl.debug(level)`` to set the global debug level for all components at
+once. This stores the level in NVS (``ctrl_debug``) and updates every
+component's level in ``ctrl.dbg()``:
+
+.. code:: python
+
+ ctrl.debug(100) # Enable all components at level 100
+ ctrl.debug(0) # Disable all components
+ ctrl.debug() # Returns current global debug level
+
+When ``ctrl.debug()`` sets a level, it also populates the per-component dict so
+you can fine-tune individual components afterward with ``ctrl.dbg()``.
+
+.. _per-component-debug-levels-ctrldbg:
+
+Per-Component Debug Levels (``ctrl.dbg()``)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Fine-grained control over individual component output. Levels are stored in NVS
+(``ctrl_dbg``) as a JSON dict and persist across reboots.
+
+.. code:: python
+
+ # Query
+ ctrl.dbg() # Returns full dict, e.g. {'sensors': 0, 'connection': 100, ...}
+ ctrl.dbg("connection") # Returns level for 'connection' (0 if not set)
+
+ # Set individual components
+ ctrl.dbg("connection", 100) # Enable connection at level 100
+ ctrl.dbg("sensors", 0) # Disable sensors (silenced on next boot)
+
+ # Mass update via dict
+ ctrl.dbg({"connection": 10, "protocol": 10, "sensors": 0, "config": 0})
+
+Behavior details:
+
+- Components set to ``0`` are remembered in the dict. On next boot they are
+ registered with ``enabled=False`` and ``silent=True`` with no log output.
+- Components with a level ``> 0`` are registered as enabled.
+- The ``"*"`` wildcard key sets the default level for any component not
+ explicitly listed: ``ctrl.dbg({"*": 100, "sensors": 0})`` enables everything
+ except sensors.
+- ``ctrl.debug(N)`` sets all components to level ``N`` and updates both
+ ``ctrl_debug`` and ``ctrl_dbg`` in NVS.
+
+Typical Workflow
+^^^^^^^^^^^^^^^^
+
+.. code:: python
+
+ # 1. Enable everything to see what's happening
+ ctrl.debug(100)
+
+ # 2. Too noisy - disable sensors and config, keep connection/protocol
+ ctrl.dbg({"sensors": 0, "config": 0, "connection": 10, "protocol": 10})
+
+ # 3. Check current state
+ ctrl.dbg()
+ # {'sensors': 0, 'config': 0, 'library': 100, 'connection': 10, 'protocol': 10, ...}
+
+ # 4. Re-enable a single component
+ ctrl.dbg("sensors", 50)
+
+ # 5. Disable everything
+ ctrl.debug(0)
+
+All settings persist to NVS automatically. After a reboot, each component
+resumes at its saved level.
+
--------------
Deprecated API
--------------
-.. _ctrlsend_signalsignal_number-value:
+.. _ctrlsend_signalargs-kwargs:
-ctrl.send_signal(signal_number, value)
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ctrl.send_signal(\*args, \**kwargs)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
**Deprecated: ``send_signal`` has been removed. Use ``send_field``
instead.**
diff --git a/programming-references/lte-legacy.rst b/programming-references/lte-legacy.rst
index 0da2ea0..a407378 100644
--- a/programming-references/lte-legacy.rst
+++ b/programming-references/lte-legacy.rst
@@ -1,3 +1,6 @@
+LTE Module - Legacy Python Implementation
+=========================================
+
Constructors
------------