roboreactor-firmata 0.1.0__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,21 @@
1
+ MIT License
2
+
3
+ Copyright (C) 2026 RoboReactor
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,113 @@
1
+ Metadata-Version: 2.4
2
+ Name: roboreactor-firmata
3
+ Version: 0.1.0
4
+ Summary: A unified, dynamic Firmata control library supporting GPIO, I2C, SPI, UART, and CAN-bus on STM32 and ESP32 microcontrollers
5
+ Author-email: RoboReactor <info@roboreactor.com>
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: License :: OSI Approved :: MIT License
8
+ Classifier: Operating System :: OS Independent
9
+ Requires-Python: >=3.7
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: pyfirmata2>=2.4.4
13
+ Requires-Dist: pyserial>=3.5
14
+ Dynamic: license-file
15
+
16
+ # roboreactor-firmata
17
+
18
+ A unified, dynamic Firmata client library for configuring and controlling **GPIO**, **I2C**, **SPI**, **UART**, and **CAN-bus** peripherals on **STM32** and **ESP32** microcontrollers.
19
+
20
+ Instead of being limited to hardcoded hardware pins, `roboreactor-firmata` allows you to dynamically assign peripheral pins at runtime from Python using a custom extended SysEx protocol.
21
+
22
+ ---
23
+
24
+ ## Installation
25
+
26
+ ```bash
27
+ pip install roboreactor-firmata
28
+ ```
29
+
30
+ ---
31
+
32
+ ## Firmware Flashing
33
+
34
+ Before running the Python client, flash the matching custom `.ino` sketch located inside the `firmware/` directory to your microcontroller:
35
+
36
+ - **STM32**: Flash `StandardFirmataCustom_STM32.ino` (Requires `STM32duino` core and optional `STM32_CAN` library).
37
+ - **ESP32**: Flash `StandardFirmataCustom_ESP32.ino` (Uses ESP32's native Arduino core and built-in `TWAI` CAN driver).
38
+
39
+ ---
40
+
41
+ ## Quick Start Examples
42
+
43
+ ### 1. STM32 Control (e.g., STM32F401RCT6)
44
+
45
+ ```python
46
+ import time
47
+ from roboreactor_firmata import STM32Board
48
+
49
+ # Connect to the board
50
+ board = STM32Board('/dev/ttyUSB0', mcu='STM32F401RCT6')
51
+ board.samplingOn(50) # Start polling thread
52
+
53
+ # --- 1. Dynamic I2C Config ---
54
+ board.configure_custom_i2c(sda_pin="PB9", scl_pin="PB8")
55
+ board.i2c_config()
56
+
57
+ def i2c_callback(data):
58
+ print(f"I2C Data: {data}")
59
+
60
+ board.i2c_read(0x68, 0x75, 1, callback=i2c_callback)
61
+
62
+ # --- 2. Dynamic UART Config ---
63
+ def uart_callback(data):
64
+ print(f"UART Rx: {data}")
65
+
66
+ board.configure_custom_uart(rx_pin="PA3", tx_pin="PA2", baudrate=9600, callback=uart_callback)
67
+ board.uart_write(b"Hello STM32")
68
+
69
+ # --- 3. Dynamic SPI Config ---
70
+ def spi_callback(data):
71
+ print(f"SPI Rx: {data}")
72
+
73
+ board.configure_custom_spi(mosi="PA7", miso="PA6", sclk="PA5", cs="PA4")
74
+ board.spi_transfer("PA4", [0x9F, 0x00, 0x00, 0x00], callback=spi_callback)
75
+
76
+ time.sleep(2)
77
+ board.exit()
78
+ ```
79
+
80
+ ### 2. ESP32 Control
81
+
82
+ ```python
83
+ import time
84
+ from roboreactor_firmata import ESP32Board
85
+
86
+ # Connect to the board (ESP32 defaults to 115200 baud)
87
+ board = ESP32Board('/dev/ttyUSB0', baudrate=115200)
88
+ board.samplingOn(50)
89
+
90
+ # --- 1. Dynamic I2C Config ---
91
+ # Route Wire to GPIO 21 (SDA) and GPIO 22 (SCL)
92
+ board.configure_custom_i2c(sda_pin=21, scl_pin=22)
93
+ board.i2c_config()
94
+
95
+ # --- 2. Dynamic CAN-bus Config ---
96
+ def can_callback(msg_id, payload, is_extended):
97
+ print(f"CAN Rx - ID: {hex(msg_id)}, Payload: {payload}")
98
+
99
+ # Route TWAI CAN to GPIO 4 (RX) and GPIO 5 (TX) at 500kbps
100
+ board.configure_custom_can(rx_pin=4, tx_pin=5, baudrate=500000, callback=can_callback)
101
+
102
+ # Transmit CAN frame
103
+ board.can_write(msg_id=0x123, data_bytes=[0xAA, 0xBB, 0xCC], is_extended=0)
104
+
105
+ time.sleep(2)
106
+ board.exit()
107
+ ```
108
+
109
+ ---
110
+
111
+ ## License
112
+
113
+ This project is licensed under the MIT License - see the `LICENSE` file for details.
@@ -0,0 +1,98 @@
1
+ # roboreactor-firmata
2
+
3
+ A unified, dynamic Firmata client library for configuring and controlling **GPIO**, **I2C**, **SPI**, **UART**, and **CAN-bus** peripherals on **STM32** and **ESP32** microcontrollers.
4
+
5
+ Instead of being limited to hardcoded hardware pins, `roboreactor-firmata` allows you to dynamically assign peripheral pins at runtime from Python using a custom extended SysEx protocol.
6
+
7
+ ---
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ pip install roboreactor-firmata
13
+ ```
14
+
15
+ ---
16
+
17
+ ## Firmware Flashing
18
+
19
+ Before running the Python client, flash the matching custom `.ino` sketch located inside the `firmware/` directory to your microcontroller:
20
+
21
+ - **STM32**: Flash `StandardFirmataCustom_STM32.ino` (Requires `STM32duino` core and optional `STM32_CAN` library).
22
+ - **ESP32**: Flash `StandardFirmataCustom_ESP32.ino` (Uses ESP32's native Arduino core and built-in `TWAI` CAN driver).
23
+
24
+ ---
25
+
26
+ ## Quick Start Examples
27
+
28
+ ### 1. STM32 Control (e.g., STM32F401RCT6)
29
+
30
+ ```python
31
+ import time
32
+ from roboreactor_firmata import STM32Board
33
+
34
+ # Connect to the board
35
+ board = STM32Board('/dev/ttyUSB0', mcu='STM32F401RCT6')
36
+ board.samplingOn(50) # Start polling thread
37
+
38
+ # --- 1. Dynamic I2C Config ---
39
+ board.configure_custom_i2c(sda_pin="PB9", scl_pin="PB8")
40
+ board.i2c_config()
41
+
42
+ def i2c_callback(data):
43
+ print(f"I2C Data: {data}")
44
+
45
+ board.i2c_read(0x68, 0x75, 1, callback=i2c_callback)
46
+
47
+ # --- 2. Dynamic UART Config ---
48
+ def uart_callback(data):
49
+ print(f"UART Rx: {data}")
50
+
51
+ board.configure_custom_uart(rx_pin="PA3", tx_pin="PA2", baudrate=9600, callback=uart_callback)
52
+ board.uart_write(b"Hello STM32")
53
+
54
+ # --- 3. Dynamic SPI Config ---
55
+ def spi_callback(data):
56
+ print(f"SPI Rx: {data}")
57
+
58
+ board.configure_custom_spi(mosi="PA7", miso="PA6", sclk="PA5", cs="PA4")
59
+ board.spi_transfer("PA4", [0x9F, 0x00, 0x00, 0x00], callback=spi_callback)
60
+
61
+ time.sleep(2)
62
+ board.exit()
63
+ ```
64
+
65
+ ### 2. ESP32 Control
66
+
67
+ ```python
68
+ import time
69
+ from roboreactor_firmata import ESP32Board
70
+
71
+ # Connect to the board (ESP32 defaults to 115200 baud)
72
+ board = ESP32Board('/dev/ttyUSB0', baudrate=115200)
73
+ board.samplingOn(50)
74
+
75
+ # --- 1. Dynamic I2C Config ---
76
+ # Route Wire to GPIO 21 (SDA) and GPIO 22 (SCL)
77
+ board.configure_custom_i2c(sda_pin=21, scl_pin=22)
78
+ board.i2c_config()
79
+
80
+ # --- 2. Dynamic CAN-bus Config ---
81
+ def can_callback(msg_id, payload, is_extended):
82
+ print(f"CAN Rx - ID: {hex(msg_id)}, Payload: {payload}")
83
+
84
+ # Route TWAI CAN to GPIO 4 (RX) and GPIO 5 (TX) at 500kbps
85
+ board.configure_custom_can(rx_pin=4, tx_pin=5, baudrate=500000, callback=can_callback)
86
+
87
+ # Transmit CAN frame
88
+ board.can_write(msg_id=0x123, data_bytes=[0xAA, 0xBB, 0xCC], is_extended=0)
89
+
90
+ time.sleep(2)
91
+ board.exit()
92
+ ```
93
+
94
+ ---
95
+
96
+ ## License
97
+
98
+ This project is licensed under the MIT License - see the `LICENSE` file for details.
@@ -0,0 +1,26 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "roboreactor-firmata"
7
+ version = "0.1.0"
8
+ authors = [
9
+ { name = "RoboReactor", email = "info@roboreactor.com" }
10
+ ]
11
+ description = "A unified, dynamic Firmata control library supporting GPIO, I2C, SPI, UART, and CAN-bus on STM32 and ESP32 microcontrollers"
12
+ readme = "README.md"
13
+ requires-python = ">=3.7"
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Operating System :: OS Independent",
18
+ ]
19
+ dependencies = [
20
+ "pyfirmata2>=2.4.4",
21
+ "pyserial>=3.5"
22
+ ]
23
+
24
+ [tool.setuptools.packages.find]
25
+ where = ["."]
26
+ include = ["roboreactor_firmata*"]
@@ -0,0 +1,4 @@
1
+ from .stm32 import STM32Board
2
+ from .esp32 import ESP32Board
3
+
4
+ __all__ = ['STM32Board', 'ESP32Board']
@@ -0,0 +1,262 @@
1
+ import time
2
+ from pyfirmata2 import Board
3
+
4
+ # Standard Firmata Commands
5
+ I2C_REQUEST = 0x76
6
+ I2C_REPLY = 0x77
7
+ I2C_CONFIG = 0x78
8
+
9
+ # Custom Dynamic Peripherals Commands
10
+ CUSTOM_I2C_CONFIG = 0x10
11
+ CUSTOM_SPI_CONFIG = 0x11
12
+ CUSTOM_UART_CONFIG = 0x12
13
+ CUSTOM_SPI_TRANSFER = 0x13
14
+ CUSTOM_CAN_CONFIG = 0x14
15
+ CUSTOM_CAN_WRITE = 0x15
16
+ CUSTOM_CAN_READ = 0x16
17
+ CUSTOM_UART_WRITE = 0x17
18
+ CUSTOM_ULTRASONIC_CONFIG = 0x18
19
+ CUSTOM_ULTRASONIC_DATA = 0x19
20
+ CUSTOM_ULTRASONIC_STOP = 0x1A
21
+
22
+
23
+ class RoboReactorBoard(Board):
24
+ """
25
+ Base Board class for RoboReactor microcontrollers.
26
+ Inherits from pyfirmata2.Board and adds support for dynamic I2C, SPI, UART, and CAN-bus.
27
+ """
28
+ def __init__(self, port, layout, baudrate=57600, *args, **kwargs):
29
+ self.i2c_callbacks = {}
30
+ self.spi_callback = None
31
+ self.uart_callback = None
32
+ self.can_callback = None
33
+ self.ultrasonic_callbacks = {}
34
+
35
+ # Initialize base Board
36
+ super().__init__(port, layout, baudrate, *args, **kwargs)
37
+
38
+ # Register standard command handlers
39
+ self.add_cmd_handler(I2C_REPLY, self._handle_i2c_reply)
40
+
41
+ # Register custom command handlers
42
+ self.add_cmd_handler(CUSTOM_SPI_TRANSFER, self._handle_spi_reply)
43
+ self.add_cmd_handler(CUSTOM_UART_CONFIG, self._handle_uart_incoming)
44
+ self.add_cmd_handler(CUSTOM_CAN_READ, self._handle_can_incoming)
45
+ self.add_cmd_handler(CUSTOM_ULTRASONIC_DATA, self._handle_ultrasonic_incoming)
46
+
47
+ def get_pin_index(self, pin_name):
48
+ """Must be implemented by subclasses to map physical names to pin indices."""
49
+ raise NotImplementedError("get_pin_index() must be implemented by subclasses.")
50
+
51
+ # --- Convenience Write/Read Methods ---
52
+ def digital_write(self, pin_name, value):
53
+ """Write HIGH (1) or LOW (0) to a digital pin by name."""
54
+ pin_idx = self.get_pin_index(pin_name)
55
+ self.digital[pin_idx].mode = 1 # OUTPUT
56
+ self.digital[pin_idx].write(value)
57
+
58
+ def digital_read(self, pin_name):
59
+ """Read state from a digital pin by name."""
60
+ pin_idx = self.get_pin_index(pin_name)
61
+ self.digital[pin_idx].mode = 0 # INPUT
62
+ return self.digital[pin_idx].read()
63
+
64
+ def pwm_write(self, pin_name, duty_cycle):
65
+ """Write PWM value (0.0 to 1.0) to a pin by name."""
66
+ pin_idx = self.get_pin_index(pin_name)
67
+ self.digital[pin_idx].mode = 3 # PWM
68
+ self.digital[pin_idx].write(duty_cycle)
69
+
70
+ def servo_write(self, pin_name, angle):
71
+ """Write servo angle (0 to 180 degrees) to a pin by name."""
72
+ pin_idx = self.get_pin_index(pin_name)
73
+ self.digital[pin_idx].mode = 4 # SERVO
74
+ self.digital[pin_idx].write(angle)
75
+
76
+ # --- Dynamic I2C Config (SDA / SCL redirection) ---
77
+ def configure_custom_i2c(self, sda_pin, scl_pin):
78
+ """Redirect and configure dynamic SDA and SCL pins for the Wire bus."""
79
+ sda_idx = self.get_pin_index(sda_pin)
80
+ scl_idx = self.get_pin_index(scl_pin)
81
+ self.send_sysex(CUSTOM_I2C_CONFIG, [sda_idx, scl_idx])
82
+
83
+ # --- Standard I2C Engine ---
84
+ def i2c_config(self, delay_us=0):
85
+ """Configure I2C bus delay (in microseconds)."""
86
+ data = self._encode_8bit_to_7bit([delay_us & 0xFF, (delay_us >> 8) & 0xFF])
87
+ self.send_sysex(I2C_CONFIG, data)
88
+
89
+ def i2c_write(self, address, register, data_bytes):
90
+ """Write data bytes to an I2C slave register."""
91
+ mode = 0 # Write Mode
92
+ payload = [register] + list(data_bytes)
93
+ data = [address & 0x7F, mode & 0x7F]
94
+ data.extend(self._encode_8bit_to_7bit(payload))
95
+ self.send_sysex(I2C_REQUEST, data)
96
+
97
+ def i2c_read(self, address, register, num_bytes, callback=None):
98
+ """Read data bytes from an I2C slave register."""
99
+ if callback:
100
+ self.i2c_callbacks[(address, register)] = callback
101
+ mode = 1 # Read Once Mode
102
+ payload = [register, num_bytes]
103
+ data = [address & 0x7F, mode & 0x7F]
104
+ data.extend(self._encode_8bit_to_7bit(payload))
105
+ self.send_sysex(I2C_REQUEST, data)
106
+
107
+ def _handle_i2c_reply(self, *data):
108
+ if len(data) < 4:
109
+ return
110
+ address = data[0]
111
+ register = data[1] | (data[2] << 7)
112
+ raw_payload = data[3:]
113
+ decoded_bytes = self._decode_7bit_pairs(raw_payload)
114
+ callback = self.i2c_callbacks.get((address, register))
115
+ if callback:
116
+ callback(decoded_bytes)
117
+
118
+ # --- Dynamic SPI Config & Transfer ---
119
+ def configure_custom_spi(self, mosi, miso, sclk, cs):
120
+ """Configure dynamic MOSI, MISO, SCLK, and CS pins for SPI."""
121
+ mosi_idx = self.get_pin_index(mosi)
122
+ miso_idx = self.get_pin_index(miso)
123
+ sclk_idx = self.get_pin_index(sclk)
124
+ cs_idx = self.get_pin_index(cs)
125
+ self.send_sysex(CUSTOM_SPI_CONFIG, [mosi_idx, miso_idx, sclk_idx, cs_idx])
126
+
127
+ def spi_transfer(self, cs_pin, data_bytes, callback=None):
128
+ """Perform full-duplex SPI read/write transfer and register callback for reply."""
129
+ if callback:
130
+ self.spi_callback = callback
131
+ cs_idx = self.get_pin_index(cs_pin)
132
+ payload = [cs_idx] + self._encode_8bit_to_7bit(data_bytes)
133
+ self.send_sysex(CUSTOM_SPI_TRANSFER, payload)
134
+
135
+ def _handle_spi_reply(self, *data):
136
+ decoded = self._decode_7bit_pairs(data)
137
+ if self.spi_callback:
138
+ self.spi_callback(decoded)
139
+
140
+ # --- Dynamic UART Config & Operations ---
141
+ def configure_custom_uart(self, rx_pin, tx_pin, baudrate, callback=None):
142
+ """Configure dynamic RX/TX pins for a UART serial port and register read callback."""
143
+ if callback:
144
+ self.uart_callback = callback
145
+ rx_idx = self.get_pin_index(rx_pin)
146
+ tx_idx = self.get_pin_index(tx_pin)
147
+ baud_bytes = [
148
+ baudrate & 0x7F,
149
+ (baudrate >> 7) & 0x7F,
150
+ (baudrate >> 14) & 0x7F
151
+ ]
152
+ self.send_sysex(CUSTOM_UART_CONFIG, [rx_idx, tx_idx] + baud_bytes)
153
+
154
+ def uart_write(self, data_bytes):
155
+ """Write raw bytes to the dynamic UART serial port."""
156
+ payload = self._encode_8bit_to_7bit(data_bytes)
157
+ self.send_sysex(CUSTOM_UART_WRITE, payload)
158
+
159
+ def _handle_uart_incoming(self, *data):
160
+ decoded = self._decode_7bit_pairs(data)
161
+ if self.uart_callback:
162
+ self.uart_callback(bytes(decoded))
163
+
164
+ # --- Dynamic CAN-bus Config & Operations ---
165
+ def configure_custom_can(self, rx_pin, tx_pin, baudrate, callback=None):
166
+ """Configure dynamic RX/TX pins for CAN-bus and register frame receive callback."""
167
+ if callback:
168
+ self.can_callback = callback
169
+ rx_idx = self.get_pin_index(rx_pin)
170
+ tx_idx = self.get_pin_index(tx_pin)
171
+ baud_bytes = [
172
+ baudrate & 0x7F,
173
+ (baudrate >> 7) & 0x7F,
174
+ (baudrate >> 14) & 0x7F,
175
+ (baudrate >> 21) & 0x7F
176
+ ]
177
+ self.send_sysex(CUSTOM_CAN_CONFIG, [rx_idx, tx_idx] + baud_bytes)
178
+
179
+ def can_write(self, msg_id, data_bytes, is_extended=0):
180
+ """Write a CAN frame to the CAN bus."""
181
+ id_bytes = [
182
+ msg_id & 0x7F,
183
+ (msg_id >> 7) & 0x7F,
184
+ (msg_id >> 14) & 0x7F,
185
+ (msg_id >> 21) & 0x7F,
186
+ (msg_id >> 28) & 0x7F
187
+ ]
188
+ length = len(data_bytes)
189
+ payload = id_bytes + [is_extended & 0x7F, length & 0x7F]
190
+ payload.extend(self._encode_8bit_to_7bit(data_bytes))
191
+ self.send_sysex(CUSTOM_CAN_WRITE, payload)
192
+
193
+ def _handle_can_incoming(self, *data):
194
+ if len(data) < 7:
195
+ return
196
+ msg_id = 0
197
+ for i in range(5):
198
+ msg_id |= (data[i] << (i * 7))
199
+ is_extended = data[5]
200
+ length = data[6]
201
+ raw_payload = data[7:]
202
+ decoded_payload = self._decode_7bit_pairs(raw_payload)
203
+
204
+ if self.can_callback:
205
+ self.can_callback(msg_id, bytes(decoded_payload[:length]), bool(is_extended))
206
+
207
+ # --- Helper methods for encoding / decoding ---
208
+ def _encode_8bit_to_7bit(self, data):
209
+ """Encodes standard 8-bit bytes into 7-bit pairs for Firmata Sysex."""
210
+ encoded = []
211
+ for val in data:
212
+ encoded.append(val & 0x7F)
213
+ encoded.append((val >> 7) & 0x7F)
214
+ return encoded
215
+
216
+ def _decode_7bit_pairs(self, data):
217
+ """Decodes 7-bit pairs from Firmata Sysex back into 8-bit bytes."""
218
+ decoded = []
219
+ for i in range(0, len(data), 2):
220
+ if i + 1 < len(data):
221
+ val = data[i] | (data[i+1] << 7)
222
+ decoded.append(val)
223
+ return decoded
224
+
225
+ # --- Dynamic Ultrasonic Config & Readings ---
226
+ def configure_ultrasonic(self, trig_pin, echo_pin, interval_ms, callback=None):
227
+ """Configure dynamic Trigger and Echo pins for an ultrasonic sensor (HC-SR04) with background polling."""
228
+ trig_idx = self.get_pin_index(trig_pin)
229
+ echo_idx = self.get_pin_index(echo_pin)
230
+ if callback:
231
+ self.ultrasonic_callbacks[(trig_idx, echo_idx)] = callback
232
+
233
+ interval_bytes = [
234
+ interval_ms & 0x7F,
235
+ (interval_ms >> 7) & 0x7F
236
+ ]
237
+ self.send_sysex(CUSTOM_ULTRASONIC_CONFIG, [trig_idx, echo_idx] + interval_bytes)
238
+
239
+ def stop_ultrasonic(self, trig_pin, echo_pin):
240
+ """Stop background polling for the specified ultrasonic sensor."""
241
+ trig_idx = self.get_pin_index(trig_pin)
242
+ echo_idx = self.get_pin_index(echo_pin)
243
+ if (trig_idx, echo_idx) in self.ultrasonic_callbacks:
244
+ del self.ultrasonic_callbacks[(trig_idx, echo_idx)]
245
+ self.send_sysex(CUSTOM_ULTRASONIC_STOP, [trig_idx, echo_idx])
246
+
247
+ def _handle_ultrasonic_incoming(self, *data):
248
+ if len(data) < 7:
249
+ return
250
+ trig_idx = data[0]
251
+ echo_idx = data[1]
252
+
253
+ duration = 0
254
+ for i in range(5):
255
+ duration |= (data[2 + i] << (i * 7))
256
+
257
+ # Speed of sound is 343 m/s -> roundtrip distance calculation in cm (duration / 58.3)
258
+ distance_cm = duration / 58.3 if duration > 0 else -1.0
259
+
260
+ callback = self.ultrasonic_callbacks.get((trig_idx, echo_idx))
261
+ if callback:
262
+ callback(distance_cm)
@@ -0,0 +1,55 @@
1
+ from .board import RoboReactorBoard
2
+
3
+ class ESP32Board(RoboReactorBoard):
4
+ """
5
+ Board implementation specifically for ESP32 microcontrollers.
6
+ Pins are configured directly by their GPIO index (e.g., GPIO12 or 12).
7
+ """
8
+ def __init__(self, port, baudrate=115200, *args, **kwargs):
9
+ # ESP32 has up to 40 GPIO pins (0-39)
10
+ # Note: Baudrate defaults to 115200 for ESP32 standard uploads
11
+ num_digital_pins = 40
12
+ num_analog_pins = 16 # standard 16 analog channels max (ADC1/ADC2)
13
+ pwm_pins = tuple(range(num_digital_pins))
14
+
15
+ layout = {
16
+ 'digital': tuple(range(num_digital_pins)),
17
+ 'analog': tuple(range(num_analog_pins)),
18
+ 'pwm': pwm_pins,
19
+ 'use_ports': True,
20
+ 'disabled': (1, 3) # TX0 (1) / RX0 (3) are default disabled
21
+ }
22
+
23
+ super().__init__(port, layout, baudrate, *args, **kwargs)
24
+
25
+ def get_pin_index(self, pin_name):
26
+ """Translate a pin name (e.g., 'GPIO12', 'IO12', or '12') to its numeric index."""
27
+ if isinstance(pin_name, int):
28
+ return pin_name
29
+ pin_name = str(pin_name).upper().strip()
30
+ if pin_name.startswith('GPIO'):
31
+ try:
32
+ return int(pin_name[4:])
33
+ except ValueError:
34
+ pass
35
+ elif pin_name.startswith('IO'):
36
+ try:
37
+ return int(pin_name[2:])
38
+ except ValueError:
39
+ pass
40
+ try:
41
+ return int(pin_name)
42
+ except ValueError:
43
+ raise ValueError(f"Invalid ESP32 pin name: {pin_name}. Use format like 'GPIO12', 'IO12', or integer 12.")
44
+
45
+ def get_gpio(self, pin_name, mode):
46
+ """
47
+ Configure and return a pin object using the physical pin name.
48
+ """
49
+ pin_idx = self.get_pin_index(pin_name)
50
+
51
+ if mode == 'a':
52
+ # Analog channel mapping for ESP32
53
+ return self.get_pin(f'a:{pin_idx}:i')
54
+ else:
55
+ return self.get_pin(f'd:{pin_idx}:{mode}')
@@ -0,0 +1,89 @@
1
+ from .board import RoboReactorBoard
2
+
3
+ # Pin mappings for different STM32 microcontrollers in the STM32duino Core
4
+ PIN_MAPS = {
5
+ # STM32F401RCT6 (64-pin package)
6
+ 'STM32F401RCT6': {
7
+ **{f'PA{i}': i for i in range(16)},
8
+ **{f'PB{i}': i + 16 for i in range(16)},
9
+ **{f'PC{i}': i + 32 for i in range(16)},
10
+ 'PD2': 48,
11
+ 'PH0': 49,
12
+ 'PH1': 50,
13
+ },
14
+ # STM32F401CCU6 (48-pin package / Blackpill)
15
+ 'STM32F401CCU6': {
16
+ **{f'PA{i}': i for i in range(16)},
17
+ **{f'PB{i}': i + 16 for i in range(16)},
18
+ 'PC13': 32,
19
+ 'PC14': 33,
20
+ 'PC15': 34,
21
+ 'PH0': 35,
22
+ 'PH1': 36,
23
+ },
24
+ # STM32F103C8T6 (48-pin package / Bluepill)
25
+ 'STM32F103C8T6': {
26
+ **{f'PA{i}': i for i in range(16)},
27
+ **{f'PB{i}': i + 16 for i in range(16)},
28
+ 'PC13': 32,
29
+ 'PC14': 33,
30
+ 'PC15': 34,
31
+ 'PD0': 35,
32
+ 'PD1': 36,
33
+ }
34
+ }
35
+
36
+ class STM32Board(RoboReactorBoard):
37
+ """
38
+ Board implementation specifically for STM32 microcontrollers.
39
+ """
40
+ def __init__(self, port, mcu='STM32F401RCT6', baudrate=57600, *args, **kwargs):
41
+ self.mcu = mcu.upper()
42
+ if self.mcu not in PIN_MAPS:
43
+ raise ValueError(f"Unknown MCU type: {mcu}. Available options: {list(PIN_MAPS.keys())}")
44
+
45
+ self.pin_map = PIN_MAPS[self.mcu]
46
+
47
+ # Dynamically build the layout based on the number of pins on this MCU
48
+ num_digital_pins = max(self.pin_map.values()) + 1
49
+ num_analog_pins = 16 # Standard 16 analog channels max
50
+ pwm_pins = tuple(self.pin_map.values())
51
+
52
+ layout = {
53
+ 'digital': tuple(range(num_digital_pins)),
54
+ 'analog': tuple(range(num_analog_pins)),
55
+ 'pwm': pwm_pins,
56
+ 'use_ports': True,
57
+ 'disabled': (0, 1) # Default disabled serial pins (e.g. RX/TX)
58
+ }
59
+
60
+ super().__init__(port, layout, baudrate, *args, **kwargs)
61
+
62
+ def get_pin_index(self, pin_name):
63
+ """Translate a physical pin name (e.g., 'PA0') to its numeric index."""
64
+ pin_name = pin_name.upper().strip()
65
+ if pin_name not in self.pin_map:
66
+ raise ValueError(f"Pin {pin_name} is not available or exposed on {self.mcu}")
67
+ return self.pin_map[pin_name]
68
+
69
+ def get_gpio(self, pin_name, mode):
70
+ """
71
+ Configure and return a pin object using the physical pin name.
72
+
73
+ Modes:
74
+ 'i' - Input
75
+ 'o' - Output
76
+ 'a' - Analog Input
77
+ 'p' - PWM Output
78
+ 's' - Servo Output
79
+ """
80
+ pin_idx = self.get_pin_index(pin_name)
81
+
82
+ if mode == 'a':
83
+ if pin_name.startswith('PA') and int(pin_name[2:]) < 8:
84
+ analog_channel = int(pin_name[2:])
85
+ return self.get_pin(f'a:{analog_channel}:i')
86
+ else:
87
+ raise ValueError(f"Pin {pin_name} does not support analog input mode on {self.mcu}")
88
+ else:
89
+ return self.get_pin(f'd:{pin_idx}:{mode}')
@@ -0,0 +1,113 @@
1
+ Metadata-Version: 2.4
2
+ Name: roboreactor-firmata
3
+ Version: 0.1.0
4
+ Summary: A unified, dynamic Firmata control library supporting GPIO, I2C, SPI, UART, and CAN-bus on STM32 and ESP32 microcontrollers
5
+ Author-email: RoboReactor <info@roboreactor.com>
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: License :: OSI Approved :: MIT License
8
+ Classifier: Operating System :: OS Independent
9
+ Requires-Python: >=3.7
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: pyfirmata2>=2.4.4
13
+ Requires-Dist: pyserial>=3.5
14
+ Dynamic: license-file
15
+
16
+ # roboreactor-firmata
17
+
18
+ A unified, dynamic Firmata client library for configuring and controlling **GPIO**, **I2C**, **SPI**, **UART**, and **CAN-bus** peripherals on **STM32** and **ESP32** microcontrollers.
19
+
20
+ Instead of being limited to hardcoded hardware pins, `roboreactor-firmata` allows you to dynamically assign peripheral pins at runtime from Python using a custom extended SysEx protocol.
21
+
22
+ ---
23
+
24
+ ## Installation
25
+
26
+ ```bash
27
+ pip install roboreactor-firmata
28
+ ```
29
+
30
+ ---
31
+
32
+ ## Firmware Flashing
33
+
34
+ Before running the Python client, flash the matching custom `.ino` sketch located inside the `firmware/` directory to your microcontroller:
35
+
36
+ - **STM32**: Flash `StandardFirmataCustom_STM32.ino` (Requires `STM32duino` core and optional `STM32_CAN` library).
37
+ - **ESP32**: Flash `StandardFirmataCustom_ESP32.ino` (Uses ESP32's native Arduino core and built-in `TWAI` CAN driver).
38
+
39
+ ---
40
+
41
+ ## Quick Start Examples
42
+
43
+ ### 1. STM32 Control (e.g., STM32F401RCT6)
44
+
45
+ ```python
46
+ import time
47
+ from roboreactor_firmata import STM32Board
48
+
49
+ # Connect to the board
50
+ board = STM32Board('/dev/ttyUSB0', mcu='STM32F401RCT6')
51
+ board.samplingOn(50) # Start polling thread
52
+
53
+ # --- 1. Dynamic I2C Config ---
54
+ board.configure_custom_i2c(sda_pin="PB9", scl_pin="PB8")
55
+ board.i2c_config()
56
+
57
+ def i2c_callback(data):
58
+ print(f"I2C Data: {data}")
59
+
60
+ board.i2c_read(0x68, 0x75, 1, callback=i2c_callback)
61
+
62
+ # --- 2. Dynamic UART Config ---
63
+ def uart_callback(data):
64
+ print(f"UART Rx: {data}")
65
+
66
+ board.configure_custom_uart(rx_pin="PA3", tx_pin="PA2", baudrate=9600, callback=uart_callback)
67
+ board.uart_write(b"Hello STM32")
68
+
69
+ # --- 3. Dynamic SPI Config ---
70
+ def spi_callback(data):
71
+ print(f"SPI Rx: {data}")
72
+
73
+ board.configure_custom_spi(mosi="PA7", miso="PA6", sclk="PA5", cs="PA4")
74
+ board.spi_transfer("PA4", [0x9F, 0x00, 0x00, 0x00], callback=spi_callback)
75
+
76
+ time.sleep(2)
77
+ board.exit()
78
+ ```
79
+
80
+ ### 2. ESP32 Control
81
+
82
+ ```python
83
+ import time
84
+ from roboreactor_firmata import ESP32Board
85
+
86
+ # Connect to the board (ESP32 defaults to 115200 baud)
87
+ board = ESP32Board('/dev/ttyUSB0', baudrate=115200)
88
+ board.samplingOn(50)
89
+
90
+ # --- 1. Dynamic I2C Config ---
91
+ # Route Wire to GPIO 21 (SDA) and GPIO 22 (SCL)
92
+ board.configure_custom_i2c(sda_pin=21, scl_pin=22)
93
+ board.i2c_config()
94
+
95
+ # --- 2. Dynamic CAN-bus Config ---
96
+ def can_callback(msg_id, payload, is_extended):
97
+ print(f"CAN Rx - ID: {hex(msg_id)}, Payload: {payload}")
98
+
99
+ # Route TWAI CAN to GPIO 4 (RX) and GPIO 5 (TX) at 500kbps
100
+ board.configure_custom_can(rx_pin=4, tx_pin=5, baudrate=500000, callback=can_callback)
101
+
102
+ # Transmit CAN frame
103
+ board.can_write(msg_id=0x123, data_bytes=[0xAA, 0xBB, 0xCC], is_extended=0)
104
+
105
+ time.sleep(2)
106
+ board.exit()
107
+ ```
108
+
109
+ ---
110
+
111
+ ## License
112
+
113
+ This project is licensed under the MIT License - see the `LICENSE` file for details.
@@ -0,0 +1,12 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ roboreactor_firmata/__init__.py
5
+ roboreactor_firmata/board.py
6
+ roboreactor_firmata/esp32.py
7
+ roboreactor_firmata/stm32.py
8
+ roboreactor_firmata.egg-info/PKG-INFO
9
+ roboreactor_firmata.egg-info/SOURCES.txt
10
+ roboreactor_firmata.egg-info/dependency_links.txt
11
+ roboreactor_firmata.egg-info/requires.txt
12
+ roboreactor_firmata.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ pyfirmata2>=2.4.4
2
+ pyserial>=3.5
@@ -0,0 +1 @@
1
+ roboreactor_firmata
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+