Merge pull request #8317 from jepler/merge-82x

Merge 8.2.x into main
This commit is contained in:
Dan Halbert 2023-08-22 17:28:30 -04:00 committed by GitHub
commit e08ad22ef6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 195 additions and 70 deletions

View File

@ -1,6 +1,13 @@
# Derived from code on Eric Holscher's blog, found at:
# https://www.ericholscher.com/blog/2016/jul/25/integrating-jinja-rst-sphinx/
import re
def render_with_jinja(docname, source):
if re.search('^\s*.. jinja$', source[0], re.M):
return True
return False
def rstjinja(app, docname, source):
"""
Render our pages as a jinja template for fancy templating goodness.
@ -9,12 +16,12 @@ def rstjinja(app, docname, source):
if app.builder.format not in ("html", "latex"):
return
# we only want our one jinja template to run through this func
if "shared-bindings/support_matrix" not in docname:
# we only want specific files to run through this func
if not render_with_jinja(docname, source):
return
src = rendered = source[0]
print(docname)
print(f"rendering {docname} as jinja templates")
if app.builder.format == "html":
rendered = app.builder.templates.render_string(

View File

@ -69,28 +69,23 @@ ADDITIONAL_MODULES = {
"array": "CIRCUITPY_ARRAY",
# always available, so depend on something that's always 1.
"builtins": "CIRCUITPY",
"builtins.pow3": "CIRCUITPY_BUILTINS_POW3",
"busio.SPI": "CIRCUITPY_BUSIO_SPI",
"busio.UART": "CIRCUITPY_BUSIO_UART",
"collections": "CIRCUITPY_COLLECTIONS",
"fontio": "CIRCUITPY_DISPLAYIO",
"io": "CIRCUITPY_IO",
"keypad.KeyMatrix": "CIRCUITPY_KEYPAD_KEYMATRIX",
"keypad.Keys": "CIRCUITPY_KEYPAD_KEYS",
"keypad.ShiftRegisterKeys": "CIRCUITPY_KEYPAD_SHIFTREGISTERKEYS",
"os.getenv": "CIRCUITPY_OS_GETENV",
"select": "MICROPY_PY_USELECT_SELECT",
"terminalio": "CIRCUITPY_DISPLAYIO",
"sys": "CIRCUITPY_SYS",
"terminalio": "CIRCUITPY_DISPLAYIO",
"usb": "CIRCUITPY_USB_HOST",
}
MODULES_NOT_IN_BINDINGS = [
"_asyncio",
"array",
"binascii",
"builtins",
"collections",
"errno",
"json",
"re",
"select",
"sys",
"ulab",
]
MODULES_NOT_IN_BINDINGS = [ "binascii", "errno", "json", "re", "ulab" ]
FROZEN_EXCLUDES = ["examples", "docs", "tests", "utils", "conf.py", "setup.py"]
"""Files and dirs at the root of a frozen directory that should be ignored.
@ -117,7 +112,7 @@ def get_bindings():
bindings_modules = []
for d in get_circuitpython_root_dir().glob("ports/*/bindings"):
bindings_modules.extend(module.name for module in d.iterdir() if d.is_dir())
return shared_bindings_modules + bindings_modules + MODULES_NOT_IN_BINDINGS
return shared_bindings_modules + bindings_modules + MODULES_NOT_IN_BINDINGS + list(ADDITIONAL_MODULES.keys())
def get_board_mapping():

@ -1 +1 @@
Subproject commit d73fe315cc7f9148a0918490d3b75430c8444bf7
Subproject commit 21205e400515a698266abaaea902bd1ea897bb5d

@ -1 +1 @@
Subproject commit ecab2fa75e9d7675785d2b87f29a22f027da8ce5
Subproject commit ed2e7018718caebba2b7550517b556e7734357ef

View File

@ -31,8 +31,8 @@ msgstr ""
#: supervisor/shared/safe_mode.c
msgid ""
"\n"
"Please file an issue with your program at https://github.com/adafruit/"
"circuitpython/issues."
"Please file an issue with your program at github.com/adafruit/circuitpython/"
"issues."
msgstr ""
#: supervisor/shared/safe_mode.c
@ -393,10 +393,6 @@ msgstr ""
msgid "'continue' outside loop"
msgstr ""
#: py/objgenerator.c
msgid "'coroutine' object is not an iterator"
msgstr ""
#: py/compile.c
msgid "'data' requires at least 2 arguments"
msgstr ""
@ -664,11 +660,6 @@ msgstr ""
msgid "Buffer is not a bytearray."
msgstr ""
#: ports/cxd56/common-hal/camera/Camera.c shared-bindings/displayio/Display.c
#: shared-bindings/framebufferio/FramebufferDisplay.c
msgid "Buffer is too small"
msgstr ""
#: ports/stm/common-hal/audiopwmio/PWMAudioOut.c
#, c-format
msgid "Buffer length %d too big. It must be less than %d"
@ -688,6 +679,12 @@ msgstr ""
msgid "Buffer too short by %d bytes"
msgstr ""
#: ports/cxd56/common-hal/camera/Camera.c shared-bindings/displayio/Display.c
#: shared-bindings/framebufferio/FramebufferDisplay.c
#: shared-bindings/struct/__init__.c shared-module/struct/__init__.c
msgid "Buffer too small"
msgstr ""
#: ports/espressif/common-hal/imagecapture/ParallelImageCapture.c
msgid "Buffers must be same size"
msgstr ""
@ -1030,10 +1027,6 @@ msgstr ""
msgid "Failed to write internal flash."
msgstr ""
#: supervisor/shared/safe_mode.c
msgid "Fault detected by hardware."
msgstr ""
#: py/moduerrno.c
msgid "File exists"
msgstr ""
@ -1109,6 +1102,10 @@ msgstr ""
msgid "Half duplex SPI is not implemented"
msgstr ""
#: supervisor/shared/safe_mode.c
msgid "Hard fault: memory access or instruction error."
msgstr ""
#: ports/mimxrt10xx/common-hal/busio/SPI.c
#: ports/mimxrt10xx/common-hal/busio/UART.c ports/stm/common-hal/busio/I2C.c
#: ports/stm/common-hal/busio/SPI.c ports/stm/common-hal/busio/UART.c
@ -1755,6 +1752,10 @@ msgstr ""
msgid "Polygon needs at least 3 points"
msgstr ""
#: supervisor/shared/safe_mode.c
msgid "Power dipped. Make sure you are providing enough power."
msgstr ""
#: shared-bindings/_bleio/Adapter.c
msgid "Prefix buffer must be on the heap"
msgstr ""
@ -1986,10 +1987,6 @@ msgstr ""
msgid "The length of rgb_pins must be 6, 12, 18, 24, or 30"
msgstr ""
#: supervisor/shared/safe_mode.c
msgid "The power dipped. Make sure you are providing enough power."
msgstr ""
#: shared-module/audiomixer/MixerVoice.c
msgid "The sample's bits_per_sample does not match the mixer's"
msgstr ""
@ -2580,8 +2577,7 @@ msgstr ""
msgid "buffer slices must be of equal length"
msgstr ""
#: py/modstruct.c shared-bindings/struct/__init__.c
#: shared-module/struct/__init__.c
#: py/modstruct.c shared-module/struct/__init__.c
msgid "buffer too small"
msgstr ""
@ -3866,10 +3862,6 @@ msgstr ""
msgid "pixel coordinates out of bounds"
msgstr ""
#: shared-bindings/displayio/TileGrid.c shared-bindings/vectorio/VectorShape.c
msgid "pixel_shader must be displayio.Palette or displayio.ColorConverter"
msgstr ""
#: extmod/vfs_posix_file.c
msgid "poll on file not available on win32"
msgstr ""
@ -4181,7 +4173,7 @@ msgstr ""
msgid "type is not an acceptable base type"
msgstr ""
#: py/objgenerator.c py/runtime.c
#: py/runtime.c
msgid "type object '%q' has no attribute '%q'"
msgstr ""

View File

@ -184,7 +184,7 @@ size_t common_hal_camera_take_picture(camera_obj_t *self, uint8_t *buffer, size_
mp_raise_ValueError(translate("Size not supported"));
}
if (!camera_check_buffer_length(width, height, format, len)) {
mp_raise_ValueError(translate("Buffer is too small"));
mp_raise_ValueError(translate("Buffer too small"));
}
if (!camera_check_format(format)) {
mp_raise_ValueError(translate("Format not supported"));

View File

@ -2,6 +2,7 @@
#define MICROPY_HW_MCU_NAME "rp2040"
#define MICROPY_HW_NEOPIXEL (&pin_GPIO21)
#define CIRCUITPY_STATUS_LED_POWER (&pin_GPIO20)
#define DEFAULT_I2C_BUS_SCL (&pin_GPIO3)
#define DEFAULT_I2C_BUS_SDA (&pin_GPIO2)

View File

@ -33,7 +33,7 @@
void board_init(void) {
picodvi_framebuffer_obj_t *fb = &allocate_display_bus()->picodvi;
fb->base.type = &picodvi_framebuffer_type;
common_hal_picodvi_framebuffer_construct(fb, 640, 480,
common_hal_picodvi_framebuffer_construct(fb, 320, 240,
&pin_GPIO7, &pin_GPIO6,
&pin_GPIO9, &pin_GPIO8,
&pin_GPIO11, &pin_GPIO10,

View File

@ -252,14 +252,15 @@ uint32_t *port_heap_get_top(void) {
return port_stack_get_top();
}
uint32_t __uninitialized_ram(saved_word);
void port_set_saved_word(uint32_t value) {
// Store in a watchdog scratch register instead of RAM. 4-7 are used by the
// sdk. 0 is used by alarm. 1-3 are free.
watchdog_hw->scratch[1] = value;
// Store in RAM because the watchdog scratch registers don't survive
// resetting by pulling the RUN pin low.
saved_word = value;
}
uint32_t port_get_saved_word(void) {
return watchdog_hw->scratch[1];
return saved_word;
}
static volatile bool ticks_enabled;

View File

@ -52,7 +52,22 @@
//| `!MOSI`, `!MISO`. Its up to the client to manage the appropriate
//| select line, often abbreviated `!CS` or `!SS`. (This is common because
//| multiple secondaries can share the `!clock`, `!MOSI` and `!MISO` lines
//| and therefore the hardware.)"""
//| and therefore the hardware.)
//|
//| .. raw:: html
//|
//| <p>
//| <details>
//| <summary>Available on these boards</summary>
//| <ul>
//| {% for board in support_matrix_reverse["busio.SPI"] %}
//| <li> {{ board }}
//| {% endfor %}
//| </ul>
//| </details>
//| </p>
//|
//| """
//|
//| def __init__(
//| self,

View File

@ -44,7 +44,22 @@
// #define STREAM_DEBUG(...) mp_printf(&mp_plat_print __VA_OPT__(,) __VA_ARGS__)
//| class UART:
//| """A bidirectional serial protocol"""
//| """A bidirectional serial protocol
//|
//| .. raw:: html
//|
//| <p>
//| <details>
//| <summary>Available on these boards</summary>
//| <ul>
//| {% for board in support_matrix_reverse["busio.UART"] %}
//| <li> {{ board }}
//| {% endfor %}
//| </ul>
//| </details>
//| </p>
//|
//| """
//|
//| def __init__(
//| self,

View File

@ -77,6 +77,8 @@
//|
//| Tutorial for UART:
//| https://learn.adafruit.com/circuitpython-essentials/circuitpython-uart-serial
//|
//| .. jinja
//| """
STATIC const mp_rom_map_elem_t busio_module_globals_table[] = {

View File

@ -506,7 +506,7 @@ STATIC mp_obj_t displayio_display_obj_fill_row(size_t n_args, const mp_obj_t *po
displayio_display_core_fill_area(&self->core, &area, mask, result_buffer);
return result;
} else {
mp_raise_ValueError(translate("Buffer is too small"));
mp_raise_ValueError(translate("Buffer too small"));
}
}
MP_DEFINE_CONST_FUN_OBJ_KW(displayio_display_fill_row_obj, 1, displayio_display_obj_fill_row);

View File

@ -350,7 +350,7 @@ MP_DEFINE_CONST_FUN_OBJ_1(displayio_tilegrid_get_pixel_shader_obj, displayio_til
STATIC mp_obj_t displayio_tilegrid_obj_set_pixel_shader(mp_obj_t self_in, mp_obj_t pixel_shader) {
displayio_tilegrid_t *self = native_tilegrid(self_in);
if (!mp_obj_is_type(pixel_shader, &displayio_palette_type) && !mp_obj_is_type(pixel_shader, &displayio_colorconverter_type)) {
mp_raise_TypeError(translate("pixel_shader must be displayio.Palette or displayio.ColorConverter"));
mp_raise_TypeError_varg(translate("unsupported %q type"), MP_QSTR_pixel_shader);
}
common_hal_displayio_tilegrid_set_pixel_shader(self, pixel_shader);

View File

@ -317,7 +317,7 @@ STATIC mp_obj_t framebufferio_framebufferdisplay_obj_fill_row(size_t n_args, con
displayio_display_core_fill_area(&self->core, &area, mask, result_buffer);
return result;
} else {
mp_raise_ValueError(translate("Buffer is too small"));
mp_raise_ValueError(translate("Buffer too small"));
}
}
MP_DEFINE_CONST_FUN_OBJ_KW(framebufferio_framebufferdisplay_fill_row_obj, 1, framebufferio_framebufferdisplay_obj_fill_row);

View File

@ -35,7 +35,22 @@
#include "shared-bindings/util.h"
//| class KeyMatrix:
//| """Manage a 2D matrix of keys with row and column pins."""
//| """Manage a 2D matrix of keys with row and column pins.
//|
//| .. raw:: html
//|
//| <p>
//| <details>
//| <summary>Available on these boards</summary>
//| <ul>
//| {% for board in support_matrix_reverse["keypad.KeyMatrix"] %}
//| <li> {{ board }}
//| {% endfor %}
//| </ul>
//| </details>
//| </p>
//|
//| """
//|
//| def __init__(
//| self,

View File

@ -35,7 +35,22 @@
#include "shared-bindings/util.h"
//| class Keys:
//| """Manage a set of independent keys."""
//| """Manage a set of independent keys.
//|
//| .. raw:: html
//|
//| <p>
//| <details>
//| <summary>Available on these boards</summary>
//| <ul>
//| {% for board in support_matrix_reverse["keypad.Keys"] %}
//| <li> {{ board }}
//| {% endfor %}
//| </ul>
//| </details>
//| </p>
//|
//| """
//|
//| def __init__(
//| self,

View File

@ -35,7 +35,22 @@
#include "shared-bindings/util.h"
//| class ShiftRegisterKeys:
//| """Manage a set of keys attached to an incoming shift register."""
//| """Manage a set of keys attached to an incoming shift register.
//|
//| .. raw:: html
//|
//| <p>
//| <details>
//| <summary>Available on these boards</summary>
//| <ul>
//| {% for board in support_matrix_reverse["keypad.ShiftRegisterKeys"] %}
//| <li> {{ board }}
//| {% endfor %}
//| </ul>
//| </details>
//| </p>
//|
//| """
//|
//| def __init__(
//| self,

View File

@ -89,6 +89,8 @@ const mp_obj_property_t keypad_generic_events_obj = {
//|
//| For more information about working with the `keypad` module in CircuitPython,
//| see `this Learn guide <https://learn.adafruit.com/key-pad-matrix-scanning-in-circuitpython>`_.
//|
//| .. jinja
//| """
STATIC mp_rom_map_elem_t keypad_module_globals_table[] = {

View File

@ -41,6 +41,8 @@
//| """functions that an OS normally provides
//|
//| |see_cpython_module| :mod:`cpython:os`.
//|
//| .. jinja
//| """
//|
//| import typing
@ -88,7 +90,24 @@ MP_DEFINE_CONST_FUN_OBJ_0(os_getcwd_obj, os_getcwd);
//| def getenv(key: str, default: Optional[str] = None) -> Optional[str]:
//| """Get the environment variable value for the given key or return ``default``.
//|
//| This may load values from disk so cache the result instead of calling this often."""
//| This may load values from disk so cache the result instead of calling this often.
//|
//| On boards that do not support ``settings.toml`` reading in the core, this function will raise NotImplementedError.
//|
//| .. raw:: html
//|
//| <p>
//| <details>
//| <summary>Available on these boards</summary>
//| <ul>
//| {% for board in support_matrix_reverse["os.getenv"] %}
//| <li> {{ board }}
//| {% endfor %}
//| </ul>
//| </details>
//| </p>
//|
//| """
//| ...
//|
STATIC mp_obj_t os_getenv(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) {

View File

@ -186,7 +186,21 @@ STATIC void preflight_pins_or_throw(uint8_t clock_pin, uint8_t *rgb_pins, uint8_
//| flicker during updates.
//|
//| A RGBMatrix is often used in conjunction with a
//| `framebufferio.FramebufferDisplay`."""
//| `framebufferio.FramebufferDisplay`.
//|
//| :param int width: The overall width of the whole matrix in pixels. For a matrix with multiple panels in row, this is the width of a single panel times the number of panels across.
//| :param int tile: In a multi-row matrix, the number of rows of panels
//| :param int bit_depth: The color depth of the matrix. A value of 1 gives 8 colors, a value of 2 gives 64 colors, and so on. Increasing bit depth increases the CPU and RAM usage of the RGBMatrix, and may lower the panel refresh rate. The framebuffer is always in RGB565 format regardless of the bit depth setting
//| :param bool serpentine: In a multi-row matrix, True when alternate rows of panels are rotated 180°, which can reduce wiring length
//| :param Sequence[digitalio.DigitalInOut] rgb_pins: The matrix's RGB pins
//| :param Sequence[digitalio.DigitalInOut] addr_pins: The matrix's address pins
//| :param digitalio.DigitalInOut clock_pin: The matrix's clock pin
//| :param digitalio.DigitalInOut latch_pin: The matrix's latch pin
//| :param digitalio.DigitalInOut output_enable_pin: The matrix's output enable pin
//| :param bool doublebuffer: True if the output is double-buffered
//| :param Optional[WriteableBuffer] framebuffer: A pre-allocated framebuffer to use. If unspecified, a framebuffer is allocated
//| :param int height: The optional overall height of the whole matrix in pixels. This value is not required because it can be calculated as described above.
//| """
STATIC mp_obj_t rgbmatrix_rgbmatrix_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) {
enum { ARG_width, ARG_bit_depth, ARG_rgb_list, ARG_addr_list,

View File

@ -31,7 +31,11 @@
#include "shared-bindings/rgbmatrix/RGBMatrix.h"
//| """Low-level routines for bitbanged LED matrices"""
//| """Low-level routines for bitbanged LED matrices
//|
//| For more information about working with RGB matrix panels in CircuitPython, see
//| `the dedicated learn guide <https://learn.adafruit.com/rgb-led-matrices-matrix-panels-with-circuitpython>`_.
//| """
STATIC const mp_rom_map_elem_t rgbmatrix_module_globals_table[] = {
{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_rgbmatrix) },

View File

@ -82,6 +82,10 @@ MP_DEFINE_EXCEPTION(gaierror, OSError)
//| TCP_NODELAY: int
//|
//| IPPROTO_TCP: int
//| IPPROTO_IP: int
//|
//| IP_MULTICAST_TTL: int
//|
//| def socket(self, family: int = AF_INET, type: int = SOCK_STREAM) -> socketpool.Socket:
//| """Create a new socket
//|
@ -182,6 +186,8 @@ STATIC const mp_rom_map_elem_t socketpool_socketpool_locals_dict_table[] = {
{ MP_ROM_QSTR(MP_QSTR_TCP_NODELAY), MP_ROM_INT(SOCKETPOOL_TCP_NODELAY) },
{ MP_ROM_QSTR(MP_QSTR_IPPROTO_TCP), MP_ROM_INT(SOCKETPOOL_IPPROTO_TCP) },
{ MP_ROM_QSTR(MP_QSTR_IPPROTO_IP), MP_ROM_INT(SOCKETPOOL_IPPROTO_IP) },
{ MP_ROM_QSTR(MP_QSTR_IP_MULTICAST_TTL), MP_ROM_INT(SOCKETPOOL_IP_MULTICAST_TTL) },
{ MP_ROM_QSTR(MP_QSTR_EAI_NONAME), MP_ROM_INT(SOCKETPOOL_EAI_NONAME) },
};

View File

@ -45,6 +45,7 @@ typedef enum {
} socketpool_socketpool_addressfamily_t;
typedef enum {
SOCKETPOOL_IPPROTO_IP = 0,
SOCKETPOOL_IPPROTO_TCP = 6,
} socketpool_socketpool_ipproto_t;
@ -52,6 +53,10 @@ typedef enum {
SOCKETPOOL_TCP_NODELAY = 1,
} socketpool_socketpool_tcpopt_t;
typedef enum {
SOCKETPOOL_IP_MULTICAST_TTL = 5,
} socketpool_socketpool_ipopt_t;
typedef enum {
SOCKETPOOL_EAI_NONAME = -2,
} socketpool_eai_t;

View File

@ -92,7 +92,7 @@ STATIC mp_obj_t struct_pack_into(size_t n_args, const mp_obj_t *args) {
// negative offsets are relative to the end of the buffer
offset = (mp_int_t)bufinfo.len + offset;
if (offset < 0) {
mp_raise_RuntimeError(translate("buffer too small"));
mp_raise_RuntimeError(translate("Buffer too small"));
}
}
byte *p = (byte *)bufinfo.buf;
@ -151,7 +151,7 @@ STATIC mp_obj_t struct_unpack_from(size_t n_args, const mp_obj_t *pos_args, mp_m
// negative offsets are relative to the end of the buffer
offset = bufinfo.len + offset;
if (offset < 0) {
mp_raise_RuntimeError(translate("buffer too small"));
mp_raise_RuntimeError(translate("Buffer too small"));
}
}
p += offset;

View File

@ -11,6 +11,8 @@ Only those boards that provide those modules will be listed.
To exclude boards that provide a module, type a "-" in front of the module name.
You can also type a regular expression as a filter.
.. jinja
.. raw:: html
<p id="support-matrix-filter-block"><input placeholder="Filter the boards by available modules" id="support-matrix-filter" type="text"/><span id="support-matrix-filter-num">(all)</span></p>

View File

@ -225,7 +225,7 @@ STATIC mp_obj_t vectorio_vector_shape_obj_set_pixel_shader(mp_obj_t wrapper_shap
vectorio_vector_shape_t *self = MP_OBJ_TO_PTR(draw_protocol->draw_get_protocol_self(wrapper_shape));
if (!mp_obj_is_type(pixel_shader, &displayio_palette_type) && !mp_obj_is_type(pixel_shader, &displayio_colorconverter_type)) {
mp_raise_TypeError(translate("pixel_shader must be displayio.Palette or displayio.ColorConverter"));
mp_raise_TypeError_varg(translate("unsupported %q type"), MP_QSTR_pixel_shader);
}
common_hal_vectorio_vector_shape_set_pixel_shader(self, pixel_shader);

View File

@ -126,7 +126,7 @@ void shared_modules_struct_pack_into(mp_obj_t fmt_in, byte *p, byte *end_p, size
const mp_uint_t total_sz = shared_modules_struct_calcsize(fmt_in);
if (p + total_sz > end_p) {
mp_raise_RuntimeError(translate("buffer too small"));
mp_raise_RuntimeError(translate("Buffer too small"));
}
size_t i = 0;

View File

@ -154,7 +154,7 @@ void print_safe_mode_message(safe_mode_t reason) {
switch (reason) {
case SAFE_MODE_BROWNOUT:
message = translate("The power dipped. Make sure you are providing enough power.");
message = translate("Power dipped. Make sure you are providing enough power.");
break;
case SAFE_MODE_USER:
#if defined(BOARD_USER_SAFE_MODE_ACTION)
@ -209,7 +209,7 @@ void print_safe_mode_message(safe_mode_t reason) {
message = translate("Failed to write internal flash.");
break;
case SAFE_MODE_HARD_FAULT:
message = translate("Fault detected by hardware.");
message = translate("Hard fault: memory access or instruction error.");
break;
case SAFE_MODE_INTERRUPT_ERROR:
message = translate("Interrupt error.");
@ -228,7 +228,7 @@ void print_safe_mode_message(safe_mode_t reason) {
break;
}
serial_write_compressed(message);
serial_write_compressed(translate("\nPlease file an issue with your program at https://github.com/adafruit/circuitpython/issues."));
serial_write_compressed(translate("\nPlease file an issue with your program at github.com/adafruit/circuitpython/issues."));
}
// Always tell user how to get out of safe mode.