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.
- roboreactor_firmata-0.1.0/LICENSE +21 -0
- roboreactor_firmata-0.1.0/PKG-INFO +113 -0
- roboreactor_firmata-0.1.0/README.md +98 -0
- roboreactor_firmata-0.1.0/pyproject.toml +26 -0
- roboreactor_firmata-0.1.0/roboreactor_firmata/__init__.py +4 -0
- roboreactor_firmata-0.1.0/roboreactor_firmata/board.py +262 -0
- roboreactor_firmata-0.1.0/roboreactor_firmata/esp32.py +55 -0
- roboreactor_firmata-0.1.0/roboreactor_firmata/stm32.py +89 -0
- roboreactor_firmata-0.1.0/roboreactor_firmata.egg-info/PKG-INFO +113 -0
- roboreactor_firmata-0.1.0/roboreactor_firmata.egg-info/SOURCES.txt +12 -0
- roboreactor_firmata-0.1.0/roboreactor_firmata.egg-info/dependency_links.txt +1 -0
- roboreactor_firmata-0.1.0/roboreactor_firmata.egg-info/requires.txt +2 -0
- roboreactor_firmata-0.1.0/roboreactor_firmata.egg-info/top_level.txt +1 -0
- roboreactor_firmata-0.1.0/setup.cfg +4 -0
|
@@ -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,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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
roboreactor_firmata
|