"""
A mode for working with Circuit Python boards.
Copyright (c) 2015-2017 Nicholas H.Tollervey and others (see the AUTHORS file).
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
import os
import ctypes
import logging
from subprocess import check_output
from mu.modes.base import MicroPythonMode
from mu.modes.api import ADAFRUIT_APIS, SHARED_APIS
from mu.interface.panes import CHARTS
from mu.logic import Device
from adafruit_board_toolkit import circuitpython_serial
logger = logging.getLogger(__name__)
[docs]class CircuitPythonMode(MicroPythonMode):
"""
Represents the functionality required by the CircuitPython mode.
"""
name = _("CircuitPython")
short_name = "circuitpython"
description = _("Write code for boards running CircuitPython.")
icon = "circuitpython"
save_timeout = 0 #: No auto-save on CP boards. Will restart.
connected = True #: is the board connected.
force_interrupt = False #: NO keyboard interrupt on serial connection.
# Modules built into CircuitPython which mustn't be used as file names
# for source code.
module_names = {
"_bleio",
"_eve",
"_pew",
"_pixelbuf",
"_stage",
"_typing",
"adafruit_bus_device",
"aesio",
"alarm",
"array",
"analogio",
"audiobusio",
"audiocore",
"audioio",
"audiomixer",
"audiomp3",
"audiopwmio",
"binascii",
"bitbangio",
"bitmaptools",
"bitops",
"board",
"builtins",
"busio",
"camera",
"canio",
"collections",
"countio",
"digitalio",
"displayio",
"dualbank",
"errno",
"fontio",
"framebufferio",
"frequencyio",
"gamepad",
"gamepadshift",
"gc",
"gnss",
"hashlib",
"i2cperipheral",
"io",
"ipaddress",
"json",
"math",
"memorymonitor",
"microcontroller",
"msgpack",
"multiterminal",
"neopixel_write",
"network",
"nvm",
"os",
"ps2io",
"pulseio",
"pwmio",
"random",
"re",
"rgbmatrix",
"rotaryio",
"rtc",
"sdcardio",
"sdioio",
"sharpdisplay",
"socket",
"socketpool",
"ssl",
"storage",
"struct",
"supervisor",
"sys",
"terminalio",
"time",
"touchio",
"uheap",
"usb_cdc",
"usb_hid",
"usb_midi",
"ustack",
"vectorio",
"watchdog",
"wifi",
"wiznet",
"zlib",
}
[docs] def actions(self):
"""
Return an ordered list of actions provided by this module. An action
is a name (also used to identify the icon) , description, and handler.
"""
buttons = [
{
"name": "serial",
"display_name": _("Serial"),
"description": _("Open a serial connection to your device."),
"handler": self.toggle_repl,
"shortcut": "CTRL+Shift+U",
}
]
if CHARTS:
buttons.append(
{
"name": "plotter",
"display_name": _("Plotter"),
"description": _("Plot incoming REPL data."),
"handler": self.toggle_plotter,
"shortcut": "CTRL+Shift+P",
}
)
return buttons
[docs] def workspace_dir(self):
"""
Return the default location on the filesystem for opening and closing
files.
"""
device_dir = None
# Attempts to find the path on the filesystem that represents the
# plugged in CIRCUITPY board.
if os.name == "posix":
# We're on Linux or OSX
for mount_command in ["mount", "/sbin/mount"]:
try:
mount_output = check_output(mount_command).splitlines()
mounted_volumes = [x.split()[2] for x in mount_output]
for volume in mounted_volumes:
tail = os.path.split(volume)[-1]
if tail.startswith(b"CIRCUITPY") or tail.startswith(
b"PYBFLASH"
):
device_dir = volume.decode("utf-8")
break
except FileNotFoundError:
pass
except PermissionError as e:
logger.error(
"Received '{}' running command: {}".format(
repr(e), mount_command
)
)
m = _("Permission error running mount command")
info = _(
'The mount command ("{}") returned an error: '
"{}. Mu will continue as if a device isn't "
"plugged in."
).format(mount_command, repr(e))
self.view.show_message(m, info)
# Avoid crashing Mu, the workspace dir will be set to default
except Exception as e:
logger.error(
"Received '{}' running command: {}".format(
repr(e), mount_command
)
)
if os.path.exists("/mnt/chromeos"):
# We're on ChromeOS
if os.path.exists("/mnt/chromeos/removable/CIRCUITPY/"):
device_dir = "/mnt/chromeos/removable/CIRCUITPY/"
else:
m = _(
"If your Circuit Python device is plugged in,"
+ ' you need to "Share with Linux" on the CIRCUITPY drive'
+ ' in the "Files" app then restart Mu.'
)
self.view.show_message(m)
elif os.name == "nt":
# We're on Windows.
def get_volume_name(disk_name):
"""
Each disk or external device connected to windows has an
attribute called "volume name". This function returns the
volume name for the given disk/device.
Code from http://stackoverflow.com/a/12056414
"""
vol_name_buf = ctypes.create_unicode_buffer(1024)
ctypes.windll.kernel32.GetVolumeInformationW(
ctypes.c_wchar_p(disk_name),
vol_name_buf,
ctypes.sizeof(vol_name_buf),
None,
None,
None,
None,
0,
)
return vol_name_buf.value
#
# In certain circumstances, volumes are allocated to USB
# storage devices which cause a Windows popup to raise if their
# volume contains no media. Wrapping the check in SetErrorMode
# with SEM_FAILCRITICALERRORS (1) prevents this popup.
#
old_mode = ctypes.windll.kernel32.SetErrorMode(1)
try:
for disk in "ABCDEFGHIJKLMNOPQRSTUVWXYZ":
path = "{}:\\".format(disk)
if (
os.path.exists(path)
and get_volume_name(path) == "CIRCUITPY"
):
return path
finally:
ctypes.windll.kernel32.SetErrorMode(old_mode)
else:
# No support for unknown operating systems.
raise NotImplementedError('OS "{}" not supported.'.format(os.name))
if device_dir:
# Found it!
self.connected = True
return device_dir
else:
# Not plugged in? Just return Mu's regular workspace directory
# after warning the user.
wd = super().workspace_dir()
if self.connected:
m = _("Could not find an attached CircuitPython device.")
info = _(
"Python files for CircuitPython devices"
" are stored on the device. Therefore, to edit"
" these files you need to have the device plugged in."
" Until you plug in a device, Mu will use the"
" directory found here:\n\n"
" {}\n\n...to store your code."
)
self.view.show_message(m, info.format(wd))
self.connected = False
return wd
[docs] def compatible_board(self, port):
"""Use adafruit_board_toolkit to find out whether a board is running
CircuitPython. The toolkit sees if the CDC Interface name is appropriate.
"""
pid = port.productIdentifier()
vid = port.vendorIdentifier()
manufacturer = port.manufacturer()
serial_number = port.serialNumber()
port_name = self.port_path(port.portName())
# Find all the CircuitPython REPL comports,
# and see if any of their device names match the one passed in.
for comport in circuitpython_serial.repl_comports():
if comport.device == port_name:
return Device(
vid,
pid,
port_name,
serial_number,
manufacturer,
self.name,
self.short_name,
"CircuitPython board",
)
# No match.
return None
[docs] def api(self):
"""
Return a list of API specifications to be used by auto-suggest and call
tips.
"""
return SHARED_APIS + ADAFRUIT_APIS