Compare commits
No commits in common. "dc61988af7dfc1752c1c54d819e34649a4ec23f0" and "c35962e50a72130f32cade45b02f4f8f62d155d4" have entirely different histories.
dc61988af7
...
c35962e50a
19
README.md
19
README.md
|
@ -1,20 +1 @@
|
||||||
This code is experimental.
|
This code is experimental.
|
||||||
|
|
||||||
## NeoPixel
|
|
||||||
|
|
||||||
Put [this](https://raw.githubusercontent.com/adafruit/Adafruit_CircuitPython_NeoPixel/main/neopixel.py) in `lib` folder of CircuitPython disk (compile to `mpy` for nice!nano.
|
|
||||||
|
|
||||||
## Compile mpy files for nice!nano kmk
|
|
||||||
|
|
||||||
``` bash
|
|
||||||
docker run -v /scratch/kmk_firmware:/opt/kmk_firmware --rm -it --entrypoint=/bin/bash python:latest
|
|
||||||
cd /opt
|
|
||||||
wget https://adafruit-circuit-python.s3.amazonaws.com/bin/mpy-cross/mpy-cross.static-amd64-linux-8.0.5
|
|
||||||
chmod a+x mpy-cross.static-amd64-linux-8.0.5
|
|
||||||
./mpy-cross.static-amd64-linux-8.0.5 --help
|
|
||||||
mv mpy-cross.static-amd64-linux-8.0.5 /usr/bin/mpy-cross
|
|
||||||
mpy-cross --help
|
|
||||||
cd /opt/kmk_firmware
|
|
||||||
python util/compile.py
|
|
||||||
cp ~.compiled/kmk~ folder -> mcu
|
|
||||||
```
|
|
||||||
|
|
|
@ -1,27 +0,0 @@
|
||||||
from kmk.modules.combos import Chord
|
|
||||||
|
|
||||||
# TODO: fix up pass by reference trick (wrapped in array) thats used below
|
|
||||||
# likely deserves a global but KmN couldnt figure it out
|
|
||||||
class ArduxChord(Chord):
|
|
||||||
# Override default constructor to allow passing of required fields
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
match: Tuple[Union[Key, int], ...],
|
|
||||||
result: Key,
|
|
||||||
fast_reset=None,
|
|
||||||
per_key_timeout=None,
|
|
||||||
timeout=None,
|
|
||||||
match_coord=None,
|
|
||||||
ardux_keyboard=[],
|
|
||||||
layers=[],
|
|
||||||
):
|
|
||||||
super().__init__(match, result, fast_reset, per_key_timeout, match_coord)
|
|
||||||
self.ardux_keyboard = ardux_keyboard
|
|
||||||
self.layers = layers
|
|
||||||
|
|
||||||
# Override standard kmk match logic to first check active vs allowed layers
|
|
||||||
def matches(self, key: Key, int_coord: int):
|
|
||||||
if self.ardux_keyboard is None or len(self.ardux_keyboard) == 0 or len(self.layers) == 0 or any(i in self.ardux_keyboard[0].active_layers for i in self.layers):
|
|
||||||
return super().matches(key, int_coord)
|
|
||||||
|
|
||||||
return False
|
|
55
ardux/kb.py
55
ardux/kb.py
|
@ -1,55 +0,0 @@
|
||||||
import os
|
|
||||||
|
|
||||||
import board
|
|
||||||
from kmk.quickpin.pro_micro.kb2040 import pinout as pins
|
|
||||||
|
|
||||||
from kmk.kmk_keyboard import KMKKeyboard
|
|
||||||
from kmk.scanners.keypad import KeysScanner
|
|
||||||
|
|
||||||
from kmk.modules.layers import Layers
|
|
||||||
from kmk.modules.combos import Combos, Chord
|
|
||||||
from ardux.chord import ArduxChord
|
|
||||||
|
|
||||||
from kmk.keys import KC
|
|
||||||
|
|
||||||
class ArduxKeyboard(KMKKeyboard):
|
|
||||||
coord_mapping = [
|
|
||||||
0, 1, 2, 3,
|
|
||||||
4, 5, 6, 7,
|
|
||||||
]
|
|
||||||
|
|
||||||
keymap = [
|
|
||||||
[KC.S, KC.T, KC.R, KC.A,
|
|
||||||
KC.O, KC.I, KC.Y, KC.E]
|
|
||||||
]
|
|
||||||
|
|
||||||
# Init / constructor / setup
|
|
||||||
def __init__(self):
|
|
||||||
# Enable debugging if appropriate
|
|
||||||
if os.getenv('ARDUX_KMK_DEBUGGING'):
|
|
||||||
self.debug_enabled = True
|
|
||||||
|
|
||||||
# setup modules/extensions arrays
|
|
||||||
self.modules = []
|
|
||||||
self.extensions = []
|
|
||||||
|
|
||||||
# Direct wire & matrix setup
|
|
||||||
self.matrix = KeysScanner([pins[16], pins[17], pins[18], pins[19], pins[12], pins[13], pins[14], pins[15]])
|
|
||||||
|
|
||||||
# Layers
|
|
||||||
self.modules.append(Layers())
|
|
||||||
|
|
||||||
# Combos
|
|
||||||
self.combo_module = Combos()
|
|
||||||
self.modules.append(self.combo_module)
|
|
||||||
self.setup_combos()
|
|
||||||
|
|
||||||
# Define combos for ardux
|
|
||||||
def setup_combos(self):
|
|
||||||
self.combo_module.combos = []
|
|
||||||
|
|
||||||
combo_enter = ArduxChord((KC.A, KC.E), KC.ENTER, ardux_keyboard=[self], layers=[0])
|
|
||||||
self.combo_module.combos.append(combo_enter)
|
|
||||||
|
|
||||||
combo_space = ArduxChord((KC.O, KC.I, KC.Y, KC.E), KC.SPACE, ardux_keyboard=[self], layers=[1])
|
|
||||||
self.combo_module.combos.append(combo_space)
|
|
34
boot.py
34
boot.py
|
@ -1,8 +1,17 @@
|
||||||
print('START boot.py')
|
print('START boot.py')
|
||||||
|
|
||||||
# Used http://kmkfw.ioy/docs/boot/ as starting point
|
# Used http://kmkfw.io/docs/boot/ as starting point
|
||||||
|
|
||||||
|
import supervisor
|
||||||
|
import board
|
||||||
|
import digitalio
|
||||||
import os
|
import os
|
||||||
|
import storage
|
||||||
|
import usb_cdc
|
||||||
|
import usb_hid
|
||||||
|
|
||||||
|
from kmk.quickpin.pro_micro.kb2040 import pinout as pins
|
||||||
|
from usb_hid import Device
|
||||||
|
|
||||||
# Print env vars if debugging enabled
|
# Print env vars if debugging enabled
|
||||||
if os.getenv('ARDUX_KMK_DEBUGGING'):
|
if os.getenv('ARDUX_KMK_DEBUGGING'):
|
||||||
|
@ -18,10 +27,8 @@ if os.getenv('ARDUX_KMK_DEBUGGING'):
|
||||||
else:
|
else:
|
||||||
print('debugging disabled')
|
print('debugging disabled')
|
||||||
|
|
||||||
# If this/these key(s) is/are held during boot, don't run the code which hides the storage and disables serial
|
# If this key is held during boot, don't run the code which hides the storage and disables serial
|
||||||
# bottom row, index finger key / bottom row pinky key
|
# bottom row, index finger key / bottom row pinky key
|
||||||
import digitalio
|
|
||||||
from kmk.quickpin.pro_micro.kb2040 import pinout as pins
|
|
||||||
key_1 = digitalio.DigitalInOut(pins[12])
|
key_1 = digitalio.DigitalInOut(pins[12])
|
||||||
key_2 = digitalio.DigitalInOut(pins[15])
|
key_2 = digitalio.DigitalInOut(pins[15])
|
||||||
|
|
||||||
|
@ -29,29 +36,22 @@ key_2 = digitalio.DigitalInOut(pins[15])
|
||||||
key_1.switch_to_input(pull=digitalio.Pull.UP)
|
key_1.switch_to_input(pull=digitalio.Pull.UP)
|
||||||
key_2.switch_to_input(pull=digitalio.Pull.UP)
|
key_2.switch_to_input(pull=digitalio.Pull.UP)
|
||||||
|
|
||||||
# Pull up means 'active low' so invert pin values for less convoluted logic below
|
# Pull up means 'active low' so invert pin values for positive logic below
|
||||||
key_1_val = not (key_1.value)
|
key_1_val = not (key_1.value)
|
||||||
key_2_val = not (key_2.value)
|
key_2_val = not (key_2.value)
|
||||||
|
|
||||||
# Check for key hold and disable any dangerous features if not held
|
# Check for key hold and disable any dangerous features
|
||||||
if not (key_1_val or key_2_val) and not os.getenv('ARDUX_KMK_DEBUGGING'):
|
if not (key_1_val or key_2_val) and not os.getenv('ARDUX_KMK_DEBUGGING'):
|
||||||
# dont expose storage by default
|
|
||||||
if not os.getenv('ARDUX_KMK_USB_DISK_ALWAYS'):
|
if not os.getenv('ARDUX_KMK_USB_DISK_ALWAYS'):
|
||||||
import storage
|
|
||||||
storage.disable_usb_drive()
|
storage.disable_usb_drive()
|
||||||
# disable usb cdc stuff thats only useful when debugging
|
|
||||||
import usb_cdc
|
|
||||||
usb_cdc.disable() # Equivalent to usb_cdc.enable(console=False, data=False)
|
usb_cdc.disable() # Equivalent to usb_cdc.enable(console=False, data=False)
|
||||||
|
|
||||||
# Enable use w/ bios when not debugging (serial device from debug messes things up)
|
|
||||||
# this only works if *both* cdc and storage are disabled above ; add added logic to avoid crash on boot
|
|
||||||
import usb_hid
|
|
||||||
from usb_hid import Device
|
|
||||||
if not os.getenv('ARDUX_KMK_USB_DISK_ALWAYS'):
|
|
||||||
usb_hid.enable((Device.KEYBOARD,), boot_device=1)
|
|
||||||
|
|
||||||
# Deinit pins so they can be setup per the kmk keymap post-boot
|
# Deinit pins so they can be setup per the kmk keymap post-boot
|
||||||
key_1.deinit()
|
key_1.deinit()
|
||||||
key_2.deinit()
|
key_2.deinit()
|
||||||
|
|
||||||
|
# Enable use w/ BIOS
|
||||||
|
|
||||||
|
usb_hid.enable(boot_device=1)
|
||||||
|
|
||||||
print('END boot.py')
|
print('END boot.py')
|
||||||
|
|
27
kb.py
Normal file
27
kb.py
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import board
|
||||||
|
|
||||||
|
from kmk.kmk_keyboard import KMKKeyboard as _KMKKeyboard
|
||||||
|
from kmk.quickpin.pro_micro.kb2040 import pinout as pins
|
||||||
|
from kmk.scanners.keypad import KeysScanner
|
||||||
|
|
||||||
|
# Direct wire config
|
||||||
|
_KEY_CFG = [
|
||||||
|
pins[16],
|
||||||
|
pins[17],
|
||||||
|
pins[18],
|
||||||
|
pins[19],
|
||||||
|
pins[12],
|
||||||
|
pins[13],
|
||||||
|
pins[14],
|
||||||
|
pins[15]
|
||||||
|
]
|
||||||
|
|
||||||
|
class KMKKeyboard(_KMKKeyboard):
|
||||||
|
def __init__(self):
|
||||||
|
# create and register the scanner for direct wire
|
||||||
|
self.matrix = KeysScanner(_KEY_CFG)
|
||||||
|
|
||||||
|
coord_mapping = [
|
||||||
|
0, 1, 2, 3,
|
||||||
|
4, 5, 6, 7,
|
||||||
|
]
|
180
lib/neopixel.py
180
lib/neopixel.py
|
@ -1,180 +0,0 @@
|
||||||
# SPDX-FileCopyrightText: 2016 Damien P. George
|
|
||||||
# SPDX-FileCopyrightText: 2017 Scott Shawcroft for Adafruit Industries
|
|
||||||
# SPDX-FileCopyrightText: 2019 Carter Nelson
|
|
||||||
# SPDX-FileCopyrightText: 2019 Roy Hooper
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
"""
|
|
||||||
`neopixel` - NeoPixel strip driver
|
|
||||||
====================================================
|
|
||||||
|
|
||||||
* Author(s): Damien P. George, Scott Shawcroft, Carter Nelson, Rose Hooper
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import board
|
|
||||||
import digitalio
|
|
||||||
from neopixel_write import neopixel_write
|
|
||||||
|
|
||||||
import adafruit_pixelbuf
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Used only for typing
|
|
||||||
from typing import Optional, Type
|
|
||||||
from types import TracebackType
|
|
||||||
import microcontroller
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
__version__ = "0.0.0+auto.0"
|
|
||||||
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_NeoPixel.git"
|
|
||||||
|
|
||||||
|
|
||||||
# Pixel color order constants
|
|
||||||
RGB = "RGB"
|
|
||||||
"""Red Green Blue"""
|
|
||||||
GRB = "GRB"
|
|
||||||
"""Green Red Blue"""
|
|
||||||
RGBW = "RGBW"
|
|
||||||
"""Red Green Blue White"""
|
|
||||||
GRBW = "GRBW"
|
|
||||||
"""Green Red Blue White"""
|
|
||||||
|
|
||||||
|
|
||||||
class NeoPixel(adafruit_pixelbuf.PixelBuf):
|
|
||||||
"""
|
|
||||||
A sequence of neopixels.
|
|
||||||
|
|
||||||
:param ~microcontroller.Pin pin: The pin to output neopixel data on.
|
|
||||||
:param int n: The number of neopixels in the chain
|
|
||||||
:param int bpp: Bytes per pixel. 3 for RGB and 4 for RGBW pixels.
|
|
||||||
:param float brightness: Brightness of the pixels between 0.0 and 1.0 where 1.0 is full
|
|
||||||
brightness
|
|
||||||
:param bool auto_write: True if the neopixels should immediately change when set. If False,
|
|
||||||
`show` must be called explicitly.
|
|
||||||
:param str pixel_order: Set the pixel color channel order. GRBW is set by default.
|
|
||||||
|
|
||||||
Example for Circuit Playground Express:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
import neopixel
|
|
||||||
from board import *
|
|
||||||
|
|
||||||
RED = 0x100000 # (0x10, 0, 0) also works
|
|
||||||
|
|
||||||
pixels = neopixel.NeoPixel(NEOPIXEL, 10)
|
|
||||||
for i in range(len(pixels)):
|
|
||||||
pixels[i] = RED
|
|
||||||
|
|
||||||
Example for Circuit Playground Express setting every other pixel red using a slice:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
import neopixel
|
|
||||||
from board import *
|
|
||||||
import time
|
|
||||||
|
|
||||||
RED = 0x100000 # (0x10, 0, 0) also works
|
|
||||||
|
|
||||||
# Using ``with`` ensures pixels are cleared after we're done.
|
|
||||||
with neopixel.NeoPixel(NEOPIXEL, 10) as pixels:
|
|
||||||
pixels[::2] = [RED] * (len(pixels) // 2)
|
|
||||||
time.sleep(2)
|
|
||||||
|
|
||||||
.. py:method:: NeoPixel.show()
|
|
||||||
|
|
||||||
Shows the new colors on the pixels themselves if they haven't already
|
|
||||||
been autowritten.
|
|
||||||
|
|
||||||
The colors may or may not be showing after this function returns because
|
|
||||||
it may be done asynchronously.
|
|
||||||
|
|
||||||
.. py:method:: NeoPixel.fill(color)
|
|
||||||
|
|
||||||
Colors all pixels the given ***color***.
|
|
||||||
|
|
||||||
.. py:attribute:: brightness
|
|
||||||
|
|
||||||
Overall brightness of the pixel (0 to 1.0)
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
pin: microcontroller.Pin,
|
|
||||||
n: int,
|
|
||||||
*,
|
|
||||||
bpp: int = 3,
|
|
||||||
brightness: float = 1.0,
|
|
||||||
auto_write: bool = True,
|
|
||||||
pixel_order: str = None
|
|
||||||
):
|
|
||||||
if not pixel_order:
|
|
||||||
pixel_order = GRB if bpp == 3 else GRBW
|
|
||||||
elif isinstance(pixel_order, tuple):
|
|
||||||
order_list = [RGBW[order] for order in pixel_order]
|
|
||||||
pixel_order = "".join(order_list)
|
|
||||||
|
|
||||||
self._power = None
|
|
||||||
if (
|
|
||||||
sys.implementation.version[0] >= 7
|
|
||||||
and getattr(board, "NEOPIXEL", None) == pin
|
|
||||||
):
|
|
||||||
power = getattr(board, "NEOPIXEL_POWER_INVERTED", None)
|
|
||||||
polarity = power is None
|
|
||||||
if not power:
|
|
||||||
power = getattr(board, "NEOPIXEL_POWER", None)
|
|
||||||
if power:
|
|
||||||
try:
|
|
||||||
self._power = digitalio.DigitalInOut(power)
|
|
||||||
self._power.switch_to_output(value=polarity)
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
super().__init__(
|
|
||||||
n, brightness=brightness, byteorder=pixel_order, auto_write=auto_write
|
|
||||||
)
|
|
||||||
|
|
||||||
self.pin = digitalio.DigitalInOut(pin)
|
|
||||||
self.pin.direction = digitalio.Direction.OUTPUT
|
|
||||||
|
|
||||||
def deinit(self) -> None:
|
|
||||||
"""Blank out the NeoPixels and release the pin."""
|
|
||||||
self.fill(0)
|
|
||||||
self.show()
|
|
||||||
self.pin.deinit()
|
|
||||||
if self._power:
|
|
||||||
self._power.deinit()
|
|
||||||
|
|
||||||
def __enter__(self):
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __exit__(
|
|
||||||
self,
|
|
||||||
exception_type: Optional[Type[BaseException]],
|
|
||||||
exception_value: Optional[BaseException],
|
|
||||||
traceback: Optional[TracebackType],
|
|
||||||
):
|
|
||||||
self.deinit()
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return "[" + ", ".join([str(x) for x in self]) + "]"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def n(self) -> int:
|
|
||||||
"""
|
|
||||||
The number of neopixels in the chain (read-only)
|
|
||||||
"""
|
|
||||||
return len(self)
|
|
||||||
|
|
||||||
def write(self) -> None:
|
|
||||||
""".. deprecated: 1.0.0
|
|
||||||
|
|
||||||
Use ``show`` instead. It matches Micro:Bit and Arduino APIs."""
|
|
||||||
self.show()
|
|
||||||
|
|
||||||
def _transmit(self, buffer: bytearray) -> None:
|
|
||||||
neopixel_write(self.pin, buffer)
|
|
65
main.py
65
main.py
|
@ -1,10 +1,15 @@
|
||||||
import board
|
import board
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
from kb import KMKKeyboard
|
||||||
|
from kmk.keys import KC
|
||||||
|
|
||||||
|
keyboard = KMKKeyboard()
|
||||||
|
|
||||||
#####
|
#####
|
||||||
# Main keyboard object
|
# Enable debugging
|
||||||
from ardux.kb import ArduxKeyboard
|
if os.getenv('ARDUX_KMK_DEBUGGING'):
|
||||||
ardux_keyboard = ArduxKeyboard()
|
keyboard.debug_enabled = True
|
||||||
|
|
||||||
#####
|
#####
|
||||||
# NeoPixel on kb2040 (tune accordingly / remove if different mcu)
|
# NeoPixel on kb2040 (tune accordingly / remove if different mcu)
|
||||||
|
@ -14,12 +19,60 @@ rgb_ext = RGB(
|
||||||
num_pixels=1,
|
num_pixels=1,
|
||||||
val_limit=100,
|
val_limit=100,
|
||||||
val_default=25,
|
val_default=25,
|
||||||
animation_mode=AnimationModes.BREATHING_RAINBOW
|
animation_mode=AnimationModes.RAINBOW
|
||||||
)
|
)
|
||||||
ardux_keyboard.extensions.append(rgb_ext)
|
keyboard.extensions.append(rgb_ext)
|
||||||
|
|
||||||
|
#####
|
||||||
|
# Layers
|
||||||
|
from kmk.modules.layers import Layers
|
||||||
|
keyboard.modules.append(Layers())
|
||||||
|
|
||||||
|
#####
|
||||||
|
# Combos
|
||||||
|
from kmk.modules.combos import Combos, Chord
|
||||||
|
combos = Combos()
|
||||||
|
keyboard.modules.append(combos)
|
||||||
|
|
||||||
|
class KmNChord(Chord):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
match: Tuple[Union[Key, int], ...],
|
||||||
|
result: Key,
|
||||||
|
fast_reset=None,
|
||||||
|
per_key_timeout=None,
|
||||||
|
timeout=None,
|
||||||
|
match_coord=None,
|
||||||
|
keyboard=None,
|
||||||
|
layers=[],
|
||||||
|
):
|
||||||
|
super().__init__(match, result, fast_reset, per_key_timeout, match_coord)
|
||||||
|
self.keyboard = keyboard
|
||||||
|
self.layers = layers
|
||||||
|
|
||||||
|
def matches(self, key: Key, int_coord: int):
|
||||||
|
if keyboard is None or len(self.layers) == 0 or any(i in self.keyboard.active_layers for i in self.layers):
|
||||||
|
return super().matches(key, int_coord)
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
combos.combos = []
|
||||||
|
|
||||||
|
combo_enter = KmNChord((KC.A, KC.E), KC.ENTER, keyboard=keyboard, layers=[0])
|
||||||
|
combos.combos.append(combo_enter)
|
||||||
|
|
||||||
|
combo_space = KmNChord((KC.O, KC.I, KC.Y, KC.E), KC.SPACE, keyboard=keyboard, layers=[1])
|
||||||
|
combos.combos.append(combo_space)
|
||||||
|
|
||||||
|
#####
|
||||||
|
# Keymap
|
||||||
|
keyboard.keymap = [
|
||||||
|
[KC.S, KC.T, KC.R, KC.A,
|
||||||
|
KC.O, KC.I, KC.Y, KC.E]
|
||||||
|
]
|
||||||
|
|
||||||
#####
|
#####
|
||||||
# Main
|
# Main
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
ardux_keyboard.go()
|
keyboard.go()
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
CIRCUITPY_BLE_NAME = "ARDUX [L|R] [board]"
|
CIRCUITPY_BLE_NAME = "ARDUX [L|R] [board]"
|
||||||
#ARDUX_KMK_DEBUGGING = 1 # Code only looks for value ; Uncomment/Comment to enable/disable
|
ARDUX_KMK_DEBUGGING = 1 # Code only looks for value ; Uncomment/Comment to enable/disable
|
||||||
ARDUX_KMK_USB_DISK_ALWAYS = 1 # Code only looks for value ; Uncomment/Comment to enable/disable
|
ARDUX_KMK_USB_DISK_ALWAYS = 1 # Code only looks for value ; Uncomment/Comment to enable/disable
|
||||||
ARDUX_SIZE = "[STANDARD|BIG|40%]"
|
ARDUX_SIZE = "[STANDARD|BIG|40%]"
|
||||||
ARDUX_HAND = "[LEFT|RIGHT]"
|
ARDUX_HAND = "[LEFT|RIGHT]"
|
||||||
|
|
Loading…
Reference in a new issue