spiderwire 0.1.0a1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,37 @@
1
+ # Byte-compiled / cache
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # Distribution / packaging
7
+ build/
8
+ dist/
9
+ *.egg-info/
10
+ *.egg
11
+ wheels/
12
+
13
+ # Virtual envs
14
+ .venv/
15
+ venv/
16
+ env/
17
+
18
+ # Tooling caches
19
+ .ruff_cache/
20
+ .pytest_cache/
21
+ .mypy_cache/
22
+ .tox/
23
+ .coverage
24
+ htmlcov/
25
+
26
+ # uv
27
+ uv.lock
28
+ uv.lock.local
29
+
30
+ # Local secrets / env
31
+ .env
32
+ .env.local
33
+
34
+ # IDE / OS
35
+ .idea/
36
+ .vscode/
37
+ .DS_Store
@@ -0,0 +1,27 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0a1] - 2026-05-08
11
+
12
+ ### Added
13
+
14
+ - First pre-release on PyPI / TestPyPI for the publishing trial run.
15
+ Pre-release versions are skipped by default `pip install spiderwire`;
16
+ use `pip install --pre spiderwire` to opt in.
17
+ - `spiderwire` library: Modbus RTU protocol (`protocol`), RS-485 transport
18
+ (`transport`), per-device register map and decoders (`registers`), and
19
+ tiered bus master with setpoint heartbeat (`bus`).
20
+ - `gss-ctrl` CLI: `scan`, `poll`, `read`, `write`, `fan`, `blower`,
21
+ `light` commands. Acts as a stand-in master for the OEM SpiderFarmer
22
+ GSS hub on a USB-RS485 adapter.
23
+ - Reference docs: `docs/protocol-analysis.md`, `docs/device-map.md`,
24
+ `docs/hw-notes.md`.
25
+
26
+ [Unreleased]: https://github.com/1am/spiderwire/compare/v0.1.0a1...HEAD
27
+ [0.1.0a1]: https://github.com/1am/spiderwire/releases/tag/v0.1.0a1
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 1AM
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,169 @@
1
+ Metadata-Version: 2.4
2
+ Name: spiderwire
3
+ Version: 0.1.0a1
4
+ Summary: SpiderWire — SpiderFarmer GSS Modbus protocol library + gss-ctrl CLI
5
+ Project-URL: Homepage, https://github.com/1am/spiderwire
6
+ Project-URL: Source, https://github.com/1am/spiderwire
7
+ Project-URL: Issues, https://github.com/1am/spiderwire/issues
8
+ Project-URL: Changelog, https://github.com/1am/spiderwire/blob/main/CHANGELOG.md
9
+ Author: 1AM
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: grow,gss,home-assistant,horticulture,modbus,modbus-rtu,rs485,spiderfarmer
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: End Users/Desktop
16
+ Classifier: Operating System :: MacOS
17
+ Classifier: Operating System :: POSIX :: Linux
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3 :: Only
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Topic :: Home Automation
25
+ Classifier: Topic :: System :: Hardware :: Hardware Drivers
26
+ Classifier: Topic :: Terminals :: Serial
27
+ Classifier: Typing :: Typed
28
+ Requires-Python: >=3.10
29
+ Requires-Dist: pyserial>=3.5
30
+ Description-Content-Type: text/markdown
31
+
32
+ # SpiderWire
33
+
34
+ [![PyPI](https://img.shields.io/pypi/v/spiderwire.svg)](https://pypi.org/project/spiderwire/)
35
+ [![Python](https://img.shields.io/pypi/pyversions/spiderwire.svg)](https://pypi.org/project/spiderwire/)
36
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
37
+ [![CI](https://github.com/1am/spiderwire/actions/workflows/ci.yml/badge.svg)](https://github.com/1am/spiderwire/actions/workflows/ci.yml)
38
+
39
+ Open Modbus RTU library and `gss-ctrl` CLI for the **SpiderFarmer GSS**
40
+ peripheral bus (inline fans, CO₂ sensor, sensor hub, light driver). The
41
+ OEM GSS hub is a Modbus RTU master over a proprietary RJ12 wire layout;
42
+ SpiderWire replaces that hub so you can drive the bus from any host —
43
+ locally, no cloud account.
44
+
45
+ > **Unofficial and experimental.** This is an independent project with no
46
+ > affiliation, endorsement, or relationship with SpiderFarmer. It works
47
+ > on my hardware, but the GSS ecosystem ships in many hardware and
48
+ > firmware revisions — yours may behave differently or not work at all.
49
+ > Expect rough edges and verify behavior on your own bus before relying
50
+ > on it.
51
+
52
+ Two surfaces, one library:
53
+
54
+ | Surface | Role | What ships with it |
55
+ | --------------------------- | ------------------------------------------ | ------------------------------------------------------------ |
56
+ | `spiderwire` Python package | Transport, register map, tiered bus master | `bus.py`, `protocol.py`, `registers.py`, `transport.py` |
57
+ | `gss-ctrl` CLI | Test / ops / manual control over USB-RS485 | `gss-ctrl scan / poll / read / write / fan / blower / light` |
58
+
59
+ A companion Home Assistant integration is planned — it will depend on
60
+ the published `spiderwire` wheel and expose the bus as HA entities.
61
+
62
+ - [PCB preview](https://www.youtube.com/watch?v=0Yn37gflFO0)
63
+
64
+ ## Install
65
+
66
+ From PyPI (recommended):
67
+
68
+ ```bash
69
+ pip install spiderwire
70
+ ```
71
+
72
+ From source (for development):
73
+
74
+ ```bash
75
+ git clone https://github.com/1am/spiderwire.git
76
+ cd spiderwire
77
+ pip install -e ".[dev]" # or: uv sync --extra dev
78
+ ```
79
+
80
+ Requires Python 3.10+ and a USB-RS485 adapter (`pyserial` is the only
81
+ runtime dependency).
82
+
83
+ ## CLI quickstart
84
+
85
+ ```bash
86
+ make scan PORT=/dev/ttyUSB0 # discover devices on the bus
87
+ make poll PORT=/dev/ttyUSB0 # master mode: tiered poll + heartbeat
88
+ make read PORT=... ADDR=0x0A QTY=28
89
+ make fan PORT=... ADDR=0x04 SPEED=15
90
+ make light PORT=... PCT=50
91
+ make blower PORT=... PCT=40
92
+ ```
93
+
94
+ Without the OEM GSS hub on the bus, **`gss-ctrl poll`** takes over
95
+ master duties: tiered polling (~1 s fast, ~2.5 s actuators, ~7 s scan)
96
+ plus the setpoint heartbeat broadcast (~3.5 s), matching the OEM
97
+ cadence peripherals expect.
98
+
99
+ Full CLI: `gss-ctrl --help`. Commands: `scan`, `poll`, `read`, `write`,
100
+ `fan`, `blower`, `light`.
101
+
102
+ ## Library use
103
+
104
+ ```python
105
+ from spiderwire import BusMaster, RS485Transport
106
+
107
+ with RS485Transport("/dev/ttyUSB0", baudrate=115200) as tx:
108
+ bus = BusMaster(tx)
109
+ bus.poll_loop(interval=1.0, callback=print)
110
+ ```
111
+
112
+ See `src/spiderwire/bus.py` for the tiered scheduler,
113
+ `src/spiderwire/registers.py` for the per-device register map, and
114
+ `src/spiderwire/transport.py` for the RS-485 framer.
115
+
116
+ ## Layout
117
+
118
+ ```
119
+ spiderwire/
120
+ ├── src/
121
+ │ ├── spiderwire/ Python package (lib)
122
+ │ │ ├── bus.py tiered master + heartbeat scheduler
123
+ │ │ ├── protocol.py Modbus RTU framing + CRC
124
+ │ │ ├── registers.py per-device register map + decoders
125
+ │ │ └── transport.py RS-485 framer over pyserial
126
+ │ └── gss_ctrl_pc/ gss-ctrl CLI (stand-in for the OEM master)
127
+ ├── tests/ pytest suite (no hardware required)
128
+ ├── docs/ protocol + device map reference
129
+ ├── pyproject.toml builds the `spiderwire` wheel + `gss-ctrl` script
130
+ └── Makefile dev shortcuts
131
+ ```
132
+
133
+ ## Docs
134
+
135
+ - [`docs/device-map.md`](docs/device-map.md) — per-device register map
136
+ for every address observed on the bus.
137
+ - [`docs/protocol-analysis.md`](docs/protocol-analysis.md) — protocol
138
+ reference: physical layer, function codes, tiered polling, heartbeat.
139
+ - [`docs/hw-notes.md`](docs/hw-notes.md) — OEM hub board notes and RJ12
140
+ pinout.
141
+
142
+ ## License
143
+
144
+ Copyright (c) 2026 [1AM](https://1am.pl)
145
+
146
+ Released under the [MIT License](LICENSE) — free to use, modify, and
147
+ distribute, including in commercial and closed-source products. The
148
+ only requirement is that the copyright notice and license text are
149
+ preserved in copies or substantial portions of the software.
150
+
151
+ ## Disclaimer
152
+
153
+ This software is provided "as is", without warranty of any kind, express
154
+ or implied, including but not limited to the warranties of
155
+ merchantability, fitness for a particular purpose, and non-infringement.
156
+
157
+ This project interacts with mains-powered grow equipment over an RS-485
158
+ bus. Incorrect wiring, miswired connectors, unsupported devices, or
159
+ misuse of the protocol can damage hardware, void manufacturer warranties,
160
+ cause fire, or result in personal injury. You are solely responsible for
161
+ verifying the correctness of your wiring, your device configuration, and
162
+ the commands you send.
163
+
164
+ In no event shall the author or contributors be liable for any direct,
165
+ indirect, incidental, special, exemplary, or consequential damages —
166
+ including but not limited to damage to equipment, crops, property, or
167
+ persons — arising from the use of, or inability to use, this software.
168
+
169
+ Use at your own risk.
@@ -0,0 +1,138 @@
1
+ # SpiderWire
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/spiderwire.svg)](https://pypi.org/project/spiderwire/)
4
+ [![Python](https://img.shields.io/pypi/pyversions/spiderwire.svg)](https://pypi.org/project/spiderwire/)
5
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
6
+ [![CI](https://github.com/1am/spiderwire/actions/workflows/ci.yml/badge.svg)](https://github.com/1am/spiderwire/actions/workflows/ci.yml)
7
+
8
+ Open Modbus RTU library and `gss-ctrl` CLI for the **SpiderFarmer GSS**
9
+ peripheral bus (inline fans, CO₂ sensor, sensor hub, light driver). The
10
+ OEM GSS hub is a Modbus RTU master over a proprietary RJ12 wire layout;
11
+ SpiderWire replaces that hub so you can drive the bus from any host —
12
+ locally, no cloud account.
13
+
14
+ > **Unofficial and experimental.** This is an independent project with no
15
+ > affiliation, endorsement, or relationship with SpiderFarmer. It works
16
+ > on my hardware, but the GSS ecosystem ships in many hardware and
17
+ > firmware revisions — yours may behave differently or not work at all.
18
+ > Expect rough edges and verify behavior on your own bus before relying
19
+ > on it.
20
+
21
+ Two surfaces, one library:
22
+
23
+ | Surface | Role | What ships with it |
24
+ | --------------------------- | ------------------------------------------ | ------------------------------------------------------------ |
25
+ | `spiderwire` Python package | Transport, register map, tiered bus master | `bus.py`, `protocol.py`, `registers.py`, `transport.py` |
26
+ | `gss-ctrl` CLI | Test / ops / manual control over USB-RS485 | `gss-ctrl scan / poll / read / write / fan / blower / light` |
27
+
28
+ A companion Home Assistant integration is planned — it will depend on
29
+ the published `spiderwire` wheel and expose the bus as HA entities.
30
+
31
+ - [PCB preview](https://www.youtube.com/watch?v=0Yn37gflFO0)
32
+
33
+ ## Install
34
+
35
+ From PyPI (recommended):
36
+
37
+ ```bash
38
+ pip install spiderwire
39
+ ```
40
+
41
+ From source (for development):
42
+
43
+ ```bash
44
+ git clone https://github.com/1am/spiderwire.git
45
+ cd spiderwire
46
+ pip install -e ".[dev]" # or: uv sync --extra dev
47
+ ```
48
+
49
+ Requires Python 3.10+ and a USB-RS485 adapter (`pyserial` is the only
50
+ runtime dependency).
51
+
52
+ ## CLI quickstart
53
+
54
+ ```bash
55
+ make scan PORT=/dev/ttyUSB0 # discover devices on the bus
56
+ make poll PORT=/dev/ttyUSB0 # master mode: tiered poll + heartbeat
57
+ make read PORT=... ADDR=0x0A QTY=28
58
+ make fan PORT=... ADDR=0x04 SPEED=15
59
+ make light PORT=... PCT=50
60
+ make blower PORT=... PCT=40
61
+ ```
62
+
63
+ Without the OEM GSS hub on the bus, **`gss-ctrl poll`** takes over
64
+ master duties: tiered polling (~1 s fast, ~2.5 s actuators, ~7 s scan)
65
+ plus the setpoint heartbeat broadcast (~3.5 s), matching the OEM
66
+ cadence peripherals expect.
67
+
68
+ Full CLI: `gss-ctrl --help`. Commands: `scan`, `poll`, `read`, `write`,
69
+ `fan`, `blower`, `light`.
70
+
71
+ ## Library use
72
+
73
+ ```python
74
+ from spiderwire import BusMaster, RS485Transport
75
+
76
+ with RS485Transport("/dev/ttyUSB0", baudrate=115200) as tx:
77
+ bus = BusMaster(tx)
78
+ bus.poll_loop(interval=1.0, callback=print)
79
+ ```
80
+
81
+ See `src/spiderwire/bus.py` for the tiered scheduler,
82
+ `src/spiderwire/registers.py` for the per-device register map, and
83
+ `src/spiderwire/transport.py` for the RS-485 framer.
84
+
85
+ ## Layout
86
+
87
+ ```
88
+ spiderwire/
89
+ ├── src/
90
+ │ ├── spiderwire/ Python package (lib)
91
+ │ │ ├── bus.py tiered master + heartbeat scheduler
92
+ │ │ ├── protocol.py Modbus RTU framing + CRC
93
+ │ │ ├── registers.py per-device register map + decoders
94
+ │ │ └── transport.py RS-485 framer over pyserial
95
+ │ └── gss_ctrl_pc/ gss-ctrl CLI (stand-in for the OEM master)
96
+ ├── tests/ pytest suite (no hardware required)
97
+ ├── docs/ protocol + device map reference
98
+ ├── pyproject.toml builds the `spiderwire` wheel + `gss-ctrl` script
99
+ └── Makefile dev shortcuts
100
+ ```
101
+
102
+ ## Docs
103
+
104
+ - [`docs/device-map.md`](docs/device-map.md) — per-device register map
105
+ for every address observed on the bus.
106
+ - [`docs/protocol-analysis.md`](docs/protocol-analysis.md) — protocol
107
+ reference: physical layer, function codes, tiered polling, heartbeat.
108
+ - [`docs/hw-notes.md`](docs/hw-notes.md) — OEM hub board notes and RJ12
109
+ pinout.
110
+
111
+ ## License
112
+
113
+ Copyright (c) 2026 [1AM](https://1am.pl)
114
+
115
+ Released under the [MIT License](LICENSE) — free to use, modify, and
116
+ distribute, including in commercial and closed-source products. The
117
+ only requirement is that the copyright notice and license text are
118
+ preserved in copies or substantial portions of the software.
119
+
120
+ ## Disclaimer
121
+
122
+ This software is provided "as is", without warranty of any kind, express
123
+ or implied, including but not limited to the warranties of
124
+ merchantability, fitness for a particular purpose, and non-infringement.
125
+
126
+ This project interacts with mains-powered grow equipment over an RS-485
127
+ bus. Incorrect wiring, miswired connectors, unsupported devices, or
128
+ misuse of the protocol can damage hardware, void manufacturer warranties,
129
+ cause fire, or result in personal injury. You are solely responsible for
130
+ verifying the correctness of your wiring, your device configuration, and
131
+ the commands you send.
132
+
133
+ In no event shall the author or contributors be liable for any direct,
134
+ indirect, incidental, special, exemplary, or consequential damages —
135
+ including but not limited to damage to equipment, crops, property, or
136
+ persons — arising from the use of, or inability to use, this software.
137
+
138
+ Use at your own risk.
@@ -0,0 +1,212 @@
1
+ # Device Map — Current Bus State
2
+
3
+ Snapshot of what actually lives on the RS-485 bus as captured by the
4
+ `gss-ctrl` CLI (`gss-ctrl <serial> scan` / `read`, or `make scan` /
5
+ `make read` from the repo root). Example port:
6
+ `/dev/cu.usbserial-130` (macOS).
7
+ See [`protocol-analysis.md`](./protocol-analysis.md) for the full
8
+ protocol write-up and historical observations; this file is a concise,
9
+ per-device reference grouped by role.
10
+
11
+ **Code:** parsing, tier lists, and `DEFAULT_QTY` live in
12
+ [`registers.py`](../registers.py);
13
+ orchestration in
14
+ [`bus.py`](../bus.py).
15
+ The Home Assistant custom component
16
+ [`custom_components/spiderfarmer/`](https://github.com/1am/spiderfarmer-ha/tree/main/custom_components/spiderfarmer/)
17
+ drives the same `BusMaster` + `RS485Transport` stack (see below).
18
+
19
+ ## Summary
20
+
21
+ | Addr | Role | Device | Header type (reg 6) | Model (reg 4) | FW (reg 2–3) | HW (reg 7) | Reliable register qty |
22
+ |------|------|--------|---------------------|---------------|--------------|------------|-----------------------|
23
+ | 0x03 | **Sensor** | CO₂ sensor | `0x0308` | `0x0330` | `"05.12"` (ASCII) | `0x0202` | 13 |
24
+ | 0x0A | **Sensor** + actuator | Sensor hub (air T/RH, soil T, PPFD); `reg 19` is the *reported* light value, not a setpoint | `0x0400` | `0x0330` | `"05.12"` (ASCII) | `0x0202` | 28 |
25
+ | 0x04 | **Actuator** | **Light dimmer (primary)** — accepts FC06 → reg 10 in **percent (0-100)**, never echoes writes | `0x0301` | `0x0426` | `0xF784C96D` (binary) | `0x0105` | 24 — silent to FC03 in current rig, but acts on FC06 writes |
26
+ | 0x06 | **Actuator** | **Blower / ventilation** (OEM UI calls it *"Light 2"*) — FC06 → **reg 14** in percent (0-100), *does* echo | `0x0204` | `0x043D` | `0xF784C90F` (binary) | `0x0102` | 16 |
27
+
28
+ `DeviceHeader.type_name` in `registers.py` (`LIGHT=0x02`, `FAN=0x03`,
29
+ `SENSOR_HUB=0x04`) maps **only the high byte** of register 6. That is
30
+ why the CLI prints `fan` for the CO₂ sensor at `0x03` (header type high
31
+ byte is `0x03`). The subtype in the low byte is what actually
32
+ distinguishes CO₂ (`0x08`) from the duct fan (`0x01`).
33
+
34
+ ## Tier placement (OEM orchestration)
35
+
36
+ `BusMaster.tick()` (see `bus.py`) interleaves the same four schedules the
37
+ OEM GSS hub uses (see `protocol-analysis.md` §"Bus Timing"). On each call,
38
+ actuators run **when their ~2.5 s deadline has elapsed** (before the fast
39
+ pair), the fast sensor pair **always** runs, then heartbeat / silent scan
40
+ fire on their own timers:
41
+
42
+ | Tier | Cadence | Addresses / action |
43
+ |------|---------|---------------------|
44
+ | B — actuators | ~2.5 s | `0x04`, `0x06` (FC03 read when due) |
45
+ | A — fast sensors | every tick (~1.0 s outer loop) | `0x03`, `0x0A` |
46
+ | D — heartbeat broadcast | ~3.5 s | FC 0x10 → 0x00, 26 regs @ 1001 |
47
+ | C — silent-slot scan | ~7.0 s | `0x10, 0x02, 0x01, 0x0C, 0x05, 0x07, 0x0B, 0x0D, 0x0E, 0x08, 0x0F, 0x13` |
48
+
49
+ Moving a device between tiers is editing `FAST_ADDRS`,
50
+ `ACTUATOR_ADDRS`, or `SCAN_ADDRS` in `spiderwire/registers.py` (or
51
+ mutating `bus.fast_addrs` / `actuator_addrs` / `scan_addrs` at runtime).
52
+
53
+ ### Home Assistant (`spiderfarmer`)
54
+
55
+ The integration's coordinator calls `BusMaster.tick()` once per HA
56
+ update (`DEFAULT_SCAN_INTERVAL` = 1 s in `custom_components/spiderfarmer/const.py`), so the Python
57
+ master tracks the OEM cadence internally.
58
+
59
+ | HA platform | What appears | Bus mapping |
60
+ |-------------|--------------|-------------|
61
+ | **Sensor** | Air temp, humidity, VPD, soil temp, PPFD on the hub; CO₂ ppm on `0x03` | Typed `SensorHubData` / `CO2SensorData` from coordinator data; unique ids `sf_0x0a_<key>` / `sf_0x03_co2` (hex lower case). |
62
+ | **Light** | One **Light 1** entity, attached to the **dimmer device (`0x04`)** so the user sees it under "Light Driver" rather than under the sensor hub | State from hub regs **18** (on/off) and **19** (reported %). `turn_on` writes hub reg 18 → 1, dimmer `0x04` reg **16** → 1 and reg **10** → % — **all FC06 blind**, neither the hub nor the dimmer echoes writes on this firmware. `turn_off` writes hub reg 18 → 0 and dimmer reg 10 → 0. Unique id stays `sf_0x0a_light1` (legacy, hub-scoped) so existing installs don't lose their entity. |
63
+ | **Fan** | **Blower** for each `BlowerData` (today: `0x06`) | HA fan entity with `translation_key` blower; unique id `sf_0x06_blower`. Writes FC06 → reg **14** (0–100 %), waits for echo. |
64
+ | **Number** | **Light brightness** (under "Light Driver") and **Blower speed** (under "Blower") — direct 0–100 % sliders visible on the dashboard / tile cards, complementing the on/off light + fan entities | Brightness writes the same dimmer reg 10 (with reg 16 = 1 latch) as the light entity but does **not** touch the hub gate. Blower speed writes blower reg 14, identical to the fan entity's `set_percentage`. Unique ids `sf_<dimmer>_brightness` / `sf_<blower>_blower_percent`. |
65
+
66
+ All four platforms use a **dynamic discovery** loop (`setup_dynamic_entities` in `entity.py`): entities are added both at first refresh **and** on subsequent coordinator updates as new addresses appear. This is what makes the CO₂ sensor at `0x03` show up reliably even when it misses the very first poll burst — without it, anything not on the bus during HA's startup tick would stay invisible until a full reload.
67
+
68
+ Device names in the HA UI are derived from the *parsed* device class
69
+ (`SensorHubData` → "Sensor Hub", `CO2SensorData` → "CO₂ Sensor",
70
+ `FanControllerData` → "Light Driver", `BlowerData` → "Blower" — see
71
+ `_ROLE_NAMES` in `entity.py`), **not** from `DeviceHeader.type_name`.
72
+ The OEM SKUs on this rig all mis-identify themselves in register 6
73
+ (0x03 CO₂ reports `fan`, 0x04 dimmer reports `fan`, 0x06 blower reports
74
+ `light`); using `type_name` for the device label is what produced the
75
+ "Blower device with a CO₂ sensor inside" misnaming on first install.
76
+
77
+ There is no separate switch platform: light enable is folded into the
78
+ light entity. Legacy `BusMaster` helpers (`set_fan_speed` /
79
+ `set_fan_enable` on reg 10 / 16) remain for a true fan-class actuator if
80
+ one is wired at `0x04` on another rig.
81
+
82
+ ## Common Header (all devices, regs 0–9)
83
+
84
+ | Reg | Meaning | Notes |
85
+ |-----|---------|-------|
86
+ | 0 | Self-reported Modbus address | matches polling addr |
87
+ | 1 | `0xAA` magic byte << 8 \| addr | e.g. `0xAA0A` for `0x0A` |
88
+ | 2–3 | Firmware version | ASCII `"HH.LL"` on SF-made units, binary `0xF784xxxx` on OEM‑branded LED/fan drivers |
89
+ | 4 | Model / product code | |
90
+ | 5 | Serial number fragment | |
91
+ | 6 | Device type (hi) : subtype (lo) | |
92
+ | 7 | Hardware version | |
93
+ | 8–9 | Reserved | always `0x0000` |
94
+
95
+ ---
96
+
97
+ ## Sensors
98
+
99
+ ### `0x03` — CO₂ sensor *(13 registers)*
100
+
101
+ Example FC03 read (`qty` ≥ 13; values illustrative):
102
+
103
+ | Reg | Hex | Decimal | Interpretation |
104
+ |-----|-----|---------|----------------|
105
+ | 0 | `0x0003` | 3 | self-addr |
106
+ | 1 | `0xAA03` | 43523 | magic + addr |
107
+ | 2 | `0x3035` | — | FW hi = `"05"` |
108
+ | 3 | `0x3132` | — | FW lo = `"12"` |
109
+ | 4 | `0x0330` | 816 | model |
110
+ | 5 | `0x7518` | 29976 | serial frag |
111
+ | 6 | `0x0308` | 776 | type / subtype (sensor-class, CO₂) |
112
+ | 7 | `0x0202` | 514 | hw |
113
+ | 8–9 | `0x0000` | 0 | reserved |
114
+ | **10** | `0x01B5` | **437** | **CO₂ concentration [ppm]** |
115
+ | 11–12 | `0x0000` | 0 | reserved (outside the 13-reg spec) |
116
+
117
+ The scan uses `DEFAULT_QTY[0x03]=13`, the response parses to `CO2SensorData`, and `co2_ppm = reg[10]`. Observed range across recent runs: **430–466 ppm**, consistent with an occupied indoor room. The value is confirmed real sensor data (not stale, not from another device).
118
+
119
+ ### `0x0A` — Sensor hub (air T / RH / soil T / PPFD) + Light 1 enable *(28 registers)*
120
+
121
+ Full 28-register reads work reliably since `RS485Transport` switched
122
+ to deterministic, fixed-size frame reads (`spiderwire.transport`);
123
+ the old silence-timing heuristic truncated long frames. The
124
+ authoritative snapshot below comes from
125
+ `docs/capture-20260418-1135.sal` (Saleae, Session 9), where the OEM
126
+ GSS hub pulls the full 61-byte response with CRC OK.
127
+
128
+ | Reg | Hex | Decimal | Interpretation |
129
+ |-----|-----|---------|----------------|
130
+ | 0–9 | — | — | header (self-addr `0x000A`, magic `0xAA0A`, FW `"05.12"`, model `0x0330`, type `0x0400`, hw `0x0202`) |
131
+ | **10** | `0x00F5` | **245** | **Air temperature × 10 → 24.5 °C** |
132
+ | **11** | `0x01B0` | **432** | **Air humidity × 10 → 43.2 %RH** |
133
+ | **12** | `0xFC18` | −1000 (signed) | **Soil temperature × 10** — `-1000` means probe not connected (`soil_temp_c = None`) |
134
+ | **13** | `0x0146` | **326** | **PPFD in µmol/m²/s** (matches in-app display 325–326) |
135
+ | 14 | `0x0163` | 355 | co-tracking light channel — peak / IR / DLI TBD |
136
+ | 15 | `0x23F8` | 9208 | constant — calibration / device config |
137
+ | 16–17 | `0x0000` | 0 | reserved |
138
+ | **18** | `0x0001` | 1 | **Light 1 enable flag** — written by master via **blind** FC 0x06 (the hub never echoes the write — `gss-ctrl light` and HA both timed out waiting for an echo before we switched to blind writes; reads of reg 18 still confirm the new value on the next poll) |
139
+ | **19** | `0x0064` | 100 | **Light 1 *reported* value** (read-only status echo of the dimmer at `0x04`; writing it does not change brightness — confirmed absent from all FC06 traffic in `capture-20260418-1152.sal`) |
140
+ | 20–21 | `0x0023`, `0x000A` | 35, 10 | zone / group (reg 21 matches device addr = 10) |
141
+ | 22–27 | `0x0000` | 0 | reserved / status flags |
142
+
143
+ `BusMaster.set_light_enable(addr=0x0A, enable)` writes reg 18. VPD is
144
+ computed client-side from reg 10 + reg 11 via the Tetens SVP formula in
145
+ `SensorHubData.vpd_kpa`. PPFD from reg 13 is exposed as a sensor entity
146
+ with translation key `ppfd` on the hub device in Home Assistant.
147
+
148
+ ---
149
+
150
+ ## Actuators
151
+
152
+ ### `0x04` — Light dimmer *(24 registers)*
153
+
154
+ Header type `0x0301` reads as "fan subtype" via `registers.py`, but the actual device wired at `0x04` in the current rig is the **main grow-light dimmer** — confirmed in `docs/capture-20260418-1152.sal`: every time the user moved the OEM app's brightness slider (0 → 53 → 30 → 53 → 0 %), the hub emitted FC06 writes to `0x04 reg 10` carrying the percent value directly. Whether this is a re-flashed fan MCU or the OEM assigning the fan product code to their light driver is unresolved; treat `reg 10` here as a 0-100 % brightness setpoint.
155
+
156
+ | Reg | Interpretation | Observed |
157
+ |-----|----------------|----------|
158
+ | 0–9 | header | type `0x0301`, model `0x0426`, FW `0xF784C96D`, hw `0x0105` |
159
+ | **10** | **Brightness 0-100 %** (direct percent) | written by master via FC 0x06; OEM writes it blind (see below) |
160
+ | 11–15 | reserved | `0` |
161
+ | **16** | **Enable flag** — must be `1` for reg 10 writes to take effect | Throughout `capture-20260418-1152.sal` the FC03 responses show `reg[16] = 1`; the OEM sets it during an earlier session and the value sticks. If you start a fresh master without setting this bit, reg 10 writes land on the dimmer but produce no visible change. `gss-ctrl light` (and the HA light entity) writes `reg 16 ← 1` blind alongside the brightness. |
162
+ | 17–23 | reserved | `0` |
163
+
164
+ **No Modbus echo on FC06 writes.** The device acts on the command but never sends the 8-byte echo back — `gss-ctrl light` and the HA integration use `write_register(..., wait_for_response=False)` for this address so they don't stall 300 ms per write. FC03 reads also time out most of the time in the current rig.
165
+
166
+ The legacy fan helpers — `BusMaster.set_fan_speed(addr, 0..25)` / `set_fan_enable(addr, bool)` — still target `reg 10` / `reg 16`. If you genuinely have a duct fan wired here (different rig), speed is 0-25 and speeds below ~11 stall the motor; see `protocol-analysis.md` §"Fan Speed Control".
167
+
168
+ ### `0x06` — Blower / ventilation *(16 registers)*
169
+
170
+ The OEM app labels this "Light 2" but the 0x06 SKU is physically wired
171
+ to the blower / ventilation fan. Confirmed in
172
+ `docs/capture-20260418-1452.sal`: the user drove the OEM slider
173
+ **0 → 74 → 60 → 25 → OFF** and every change produced an FC06 write to
174
+ `0x06 reg 14` carrying the percent directly; reg 10 stays `0` the entire
175
+ capture. The device's own 16-register broadcast echoes the live
176
+ setpoint back in reg 14 and latches reg 12 to 1 whenever the blower is
177
+ running.
178
+
179
+ | Reg | Interpretation | Notes |
180
+ |-----|----------------|-------|
181
+ | 0–9 | header | type `0x0204`, model `0x043D`, FW `0xF784C90F`, hw `0x0102` |
182
+ | 10 | unused | `0x0000` throughout every capture |
183
+ | 11 | paired flag | `0x0001` once the controller has linked, latches |
184
+ | **12** | **running flag** | `1` whenever the blower is actively driven, `0` when off |
185
+ | 13 | reserved | `0x0000` |
186
+ | **14** | **Blower % (0-100)** | master writes via FC 0x06; slave echoes |
187
+ | 15 | reserved | `0x0000` |
188
+
189
+ `BusMaster.set_blower(addr, percent)` issues `write_register` on reg 14
190
+ (`BLOWER_SETPOINT_REG` in `registers.py`) and waits for the standard FC06
191
+ response echo. The OEM UI enforces a 25 % floor above 0 (it refuses to
192
+ slide lower without going to OFF), but the device itself accepts any
193
+ 0–100 value — `set_blower` and the HA blower entity allow lower values
194
+ if desired.
195
+
196
+ ---
197
+
198
+ ## Addresses polled but silent
199
+
200
+ The OEM master polls `0x01, 0x02, 0x05, 0x07, 0x08, 0x0B–0x10, 0x13` every poll cycle. None of these respond in the current rig. The per-address register-count table in `registers.py` (`DEFAULT_QTY`) encodes the OEM's expected device class at each slot; nothing is physically installed there today.
201
+
202
+ > During `gss-ctrl poll`, the top-of-screen "N online" counter sometimes shows phantom devices at those addresses with data identical to `0x03`'s. Those are garbled frames that happened to pass CRC — not real devices. Only the clean `scan` output above should be trusted for device discovery.
203
+
204
+ ## Broadcast heartbeat (master → 0x00)
205
+
206
+ FC `0x10` @ register 1001, 26 words. `BusMaster.setpoints` holds the
207
+ live payload; `broadcast_setpoints()` ships it, and `tick()` fires one
208
+ every `heartbeat_interval` seconds (default 3.5 s, matching the OEM
209
+ hub). Peripherals enter a master-missing fail-safe if the heartbeat
210
+ stops. See `protocol-analysis.md` §"Broadcast Writes" for the
211
+ per-register layout; default non-zero values are `reg[1009]=7` and
212
+ `reg[1011]=1112` (both observed constant in every capture).
@@ -0,0 +1,37 @@
1
+ # Hardware Notes
2
+
3
+ Raw observations of the OEM SpiderFarmer GSS hub, kept separate from
4
+ the protocol reference.
5
+
6
+ ## OEM hub board
7
+
8
+ - MCU: **ESP32-S3-WROOM-1**
9
+ - Ethernet PHY: likely [Davicom DM9051](https://www.davicom.com.tw/production-item.php?lang_id=en)
10
+ (SPI MAC+PHY; S3 has no internal EMAC, and the pin-compatible DM9051
11
+ is what fits the 25 MHz crystal next to U6)
12
+ - RS-485 transceiver: TBD (on-board, DE-driven)
13
+ - LCD: parallel TFT (driver chip not yet identified — candidates
14
+ include ILI9341, ST7789V, ST7796, ILI9488)
15
+
16
+ See [`firmware/sf-gss.yaml`](../firmware/sf-gss.yaml) for a draft
17
+ ESPHome firmware that can flash this hardware as a drop-in replacement
18
+ for the OEM firmware — bring-up still needs the LCD pin mapping and
19
+ driver chip verified.
20
+
21
+ ## RJ12 pinout (peripheral-side)
22
+
23
+ | Pin colour | Signal |
24
+ |------------|--------|
25
+ | green | GND |
26
+ | blue | +12 V |
27
+ | white | +12 V |
28
+ | black | RS-485 |
29
+ | yellow | RS-485 |
30
+ | red | (unused / TBD) |
31
+
32
+ ## Saleae Logic probe wiring (reference)
33
+
34
+ | Logic signal | Wire colour path |
35
+ |--------------|------------------|
36
+ | RS-485 A | yellow → grey → blue at Saleae |
37
+ | RS-485 B | green → purple → brown at Saleae |