summaryrefslogtreecommitdiff
path: root/board
diff options
context:
space:
mode:
authorRandall Spangler <rspangler@chromium.org>2017-08-25 12:39:16 -0700
committerChromeOS Commit Bot <chromeos-commit-bot@chromium.org>2017-09-07 22:34:33 +0000
commitef272881641cbceb22764cde489b922c434a3769 (patch)
tree4cd889cf092d1732676592ac54432b17f0474057 /board
parentd52c9191331603dbfb66f69052e88fe9f4cd0c1b (diff)
downloadchrome-ec-ef272881641cbceb22764cde489b922c434a3769.tar.gz
cr50: Split servo state machine into its own file
This is the last state machine which used common/device_state.c. But servo is more complex than that, because it needs to differentiate state-isn't-known (debouncing) from state-isn't-knowable (Cr50 driving EC TX), so it's cleaner to split it out the way we did AP and EC state machines in previous CLs. BUG=b:35587387 BRANCH=cr50 TEST=manual with CR50_DEV=1 build // Test detect at boot, even with CCD connected Pull CCD_MODE_L low Pull DETECT_SERVO high Pull DETECT_EC high reboot -> 'Servo connect' // CCD is not driving EC UART TX ccd -> EC on, Servo connected, CCD enabled, EC UART RX // When servo disconnects CCD can drive EC TX Pull DETECT_SERVO low --> 'Servo disconnect' ccd -> EC on, Servo undetectable, CCD enabled, EC UART RX+TX // Can't detect servo reconnecting if we're driving EC TX Pull DETECT_SERVO high --> (no change) ccd -> EC on, Servo undetectable, CCD enabled, EC UART RX+TX // When we stop driving EC TX, can redetect servo Pull EC_DETECT low --> See 'EC off', 'Servo connected' ccd -> EC off, Servo connected, CCD enabled, EC UART disabled // Test debouncing at boot Pull DETECT_EC high Pull DETECT_SERVO low Pull CCD_MODE_L high reboot Within 1 sec, pull DETECT_SERVO high --> 'Servo connected' // Test debouncing after boot Pull DETECT_SERVO low then high < 1 sec --> (no message) Change-Id: I964bd36c35f52c8ef7b3ea3793b6e0764e93587c Signed-off-by: Randall Spangler <rspangler@chromium.org> Reviewed-on: https://chromium-review.googlesource.com/636047 Reviewed-by: Mary Ruthven <mruthven@chromium.org> (cherry picked from commit fe0a3b99ff8303113c52e4573361325b44574b5c) Reviewed-on: https://chromium-review.googlesource.com/656417 Commit-Queue: Vadim Bendebury <vbendeb@chromium.org> Tested-by: Vadim Bendebury <vbendeb@chromium.org> Reviewed-by: Vadim Bendebury <vbendeb@chromium.org>
Diffstat (limited to 'board')
-rw-r--r--board/cr50/board.c253
-rw-r--r--board/cr50/board.h19
-rw-r--r--board/cr50/build.mk2
-rw-r--r--board/cr50/ec_state.c1
-rw-r--r--board/cr50/gpio.inc2
-rw-r--r--board/cr50/rdd.c7
-rw-r--r--board/cr50/servo_state.c227
7 files changed, 248 insertions, 263 deletions
diff --git a/board/cr50/board.c b/board/cr50/board.c
index 27ec67fbd2..f496ecd8ee 100644
--- a/board/cr50/board.c
+++ b/board/cr50/board.c
@@ -8,7 +8,6 @@
#include "common.h"
#include "console.h"
#include "dcrypto/dcrypto.h"
-#include "device_state.h"
#include "ec_version.h"
#include "endian.h"
#include "extension.h"
@@ -84,9 +83,6 @@ uint32_t nvmem_user_sizes[NVMEM_NUM_USERS] = {
NVMEM_CR50_SIZE
};
-static int device_state_changed(enum device_type device,
- enum device_state state);
-
/* Board specific configuration settings */
static uint32_t board_properties; /* Mainly used as a cache for strap config. */
static uint8_t reboot_request_posted;
@@ -700,9 +696,6 @@ static void board_init(void)
/* Load case-closed debugging config. Must be after initvars(). */
ccd_config_init(ccd_init_state);
- /* Initialize write protect. Must be after CCD config init. */
- init_wp_state();
-
system_update_rollback_mask_with_both_imgs();
/* Indication that firmware is running, for debug purposes. */
@@ -715,9 +708,8 @@ static void board_init(void)
check_board_id_mismatch();
check_board_id_mismatch();
- /* Enable GPIO interrupts for device state machines */
+ /* Enable TPM reset GPIO interrupt */
gpio_enable_interrupt(GPIO_TPM_RST_L);
- gpio_enable_interrupt(GPIO_DETECT_SERVO);
/*
* Start monitoring AC detect to wake Cr50 from deep sleep. This is
@@ -726,6 +718,17 @@ static void board_init(void)
*/
init_ac_detect();
init_rdd_state();
+
+ /* Initialize write protect. Must be after CCD config init. */
+ init_wp_state();
+
+ /*
+ * Note that the AP, EC, and servo state machines do not have explicit
+ * init_xxx_state() functions, because they don't need to configure
+ * registers prior to starting their state machines. Their state
+ * machines run in HOOK_SECOND, which first triggers right after
+ * HOOK_INIT, not at +1.0 seconds.
+ */
}
DECLARE_HOOK(HOOK_INIT, board_init, HOOK_PRIO_DEFAULT);
@@ -924,49 +927,6 @@ int is_ec_rst_asserted(void)
return GREAD(RBOX, ASSERT_EC_RST);
}
-/**
- * Update device state with a change that has already happened
- *
- * Cancels any deferred call pending for this device.
- *
- * @param device Device to change
- * @param state New state
- * @return Passthru from device_set_state(). That is, non-zero if the last
- * known device state has changed to a known and different value.
- */
-static int device_state_changed(enum device_type device,
- enum device_state state)
-{
- /* Cancel any deferred call for this device */
- hook_call_deferred(device_states[device].deferred, -1);
-
- return device_set_state(device, state);
-}
-
-/**
- * Change the servo device to UNKNOWN if we can't tell if it's connected.
- *
- * @return 1 if we can't tell if servo is connected, 0 if we can tell.
- */
-static int servo_state_unknowable(void)
-{
- /*
- * If we are driving the UART transmit line to the EC, then we can't
- * check to see if servo is also doing so. If so, set the state of the
- * servo device to DEVICE_STATE_UNKNOWN.
- *
- * We also need to check if we're bit-banging the EC UART, because in
- * that case, the UART transmit line is directly controlled as a GPIO
- * and can be high even if UART TX is disconnected.
- */
- if (uart_tx_is_connected(UART_EC) || uart_bitbang_is_enabled(UART_EC)) {
- device_set_state(DEVICE_SERVO, DEVICE_STATE_UNKNOWN);
- return 1;
- }
-
- return 0;
-}
-
void enable_ccd_uart(int uart)
{
/*
@@ -1037,195 +997,6 @@ static void board_ccd_change_hook(void)
}
DECLARE_HOOK(HOOK_CCD_CHANGE, board_ccd_change_hook, HOOK_PRIO_DEFAULT);
-/**
- * Move a device from DEVICE_STATE_UNKNOWN to DEVICE_STATE_OFF.
- *
- * @return 1 if the device moved from UNKNOWN->OFF, or 0 (without changing
- * state) for any other requested state transition.
- */
-static int device_powered_off(enum device_type device)
-{
- /*
- * If the device state is on, then leave it there.
- *
- * When does this happen? If there's a race condition between a
- * state transition to ON in an interrupt handler and a transition to
- * OFF in a hook task level function?
- *
- * If that's the intent, this is *still* race conditiony, because the
- * call to device_state_changed() below can still happen after the
- * interrupt handler turns the device on.
- */
- if (device_get_state(device) == DEVICE_STATE_ON)
- return 0;
-
- /*
- * The device state is either unknown or off.
- *
- * If it's already off, then device_state_changed() will return 0,
- * and we'll exit with error unknown.
- *
- * If it was unknown, we'll change it to off and return success.
- */
- if (!device_state_changed(device, DEVICE_STATE_OFF))
- return 0;
-
- return 1;
-}
-
-/**
- * Deferred handler for servo removal debouncing.
- *
- * This will be called if servo has been detached for long enough, as
- * indicated by the UART transmit line to the EC (EC RX) being low.
- */
-static void servo_deferred(void)
-{
- /*
- * If we're already driving the transmit line, we don't know if servo
- * has actually been detached.
- */
- if (servo_state_unknowable())
- return;
-
- /*
- * Otherwise, we're done debouncing servo removal. If we're still in
- * the UNKNOWN state, move it to the OFF state.
- */
- if (device_powered_off(DEVICE_SERVO))
- uartn_tx_connect(UART_AP);
-}
-DECLARE_DEFERRED(servo_deferred);
-
-/* Note: this must EXACTLY match enum device_type! */
-struct device_config device_states[] = {
- [DEVICE_SERVO] = {
- .state = DEVICE_STATE_UNKNOWN,
- .deferred = &servo_deferred_data,
- .detect = GPIO_DETECT_SERVO,
- .name = "Servo"
- },
-};
-BUILD_ASSERT(ARRAY_SIZE(device_states) == DEVICE_COUNT);
-
-static void servo_attached(void)
-{
- /*
- * If we're not actually sure servo is attached because we're driving
- * the EC UART's transmit line, then return.
- */
- if (servo_state_unknowable())
- return;
-
- /* Update the device state */
- device_state_changed(DEVICE_SERVO, DEVICE_STATE_ON);
-
- /*
- * Disable UART bit banging. Note this must be done before
- * uartn_tx_disconnect() below, because this call will currently call
- * uartn_tx_connect()!
- */
- uart_bitbang_disable(bitbang_config.uart);
-
- /* Disconnect AP and EC UART when servo is attached */
- uartn_tx_disconnect(UART_AP);
- uartn_tx_disconnect(UART_EC);
-
- /* Disconnect i2cm interface to ina */
- usb_i2c_board_disable();
-}
-
-/*
- * Note: This is both an interrupt handler AND called from hook_task, and can
- * be re-entrant.
- */
-void device_state_on(enum gpio_signal signal)
-{
- gpio_disable_interrupt(signal);
-
- switch (signal) {
- case GPIO_DETECT_SERVO:
- servo_attached();
- break;
- default:
- CPRINTS("Device %d not supported", signal);
- return;
- }
-}
-
-/* Note: This is called for every device once a second from the HOOK task */
-void board_update_device_state(enum device_type device)
-{
- if (device == DEVICE_SERVO) {
- /*
- * If we're driving the transmit line to the EC UART, then we
- * can't tell if servo is connected. This check will also move
- * the servo device to DEVICE_STATE_UNKNOWN in that case.
- */
- if (servo_state_unknowable())
- return;
-
- /* Otherwise, continue to the checks below */
- }
-
- /*
- * If the device is currently on set its state immediately. If it
- * thinks the device is powered off debounce the signal.
- */
- if (gpio_get_level(device_states[device].detect)) {
- /*
- * If the device is already on, return now.
- *
- * Seems like we could rely on a 0 return value from
- * device_set_state() to tell us that, rather than making an
- * explicit check here.
- */
- if (device_get_state(device) == DEVICE_STATE_ON)
- return;
-
- /*
- * Otherwise, immediately call the interrupt handler for the
- * pin to move the device to DEVICE_STATE_ON.
- *
- * Note this may be a race condition.
- */
- device_state_on(device_states[device].detect);
- } else {
- /*
- * If the device is already off, return now.
- *
- * Seems like we could rely on a 0 return value from
- * device_set_state() to tell us that, rather than making an
- * explicit check here.
- */
- if (device_get_state(device) == DEVICE_STATE_OFF)
- return;
-
- /*
- * Otherwise, move the device to DEVICE_STATE_UNKNOWN.
- *
- * This is also likely a race condition.
- */
- device_set_state(device, DEVICE_STATE_UNKNOWN);
-
- /* Enable servo detect interrupt */
- gpio_enable_interrupt(device_states[device].detect);
-
- /*
- * The signal is low now, but this could be just a (EC, AP, or
- * Servo) UART transmitting 0-bits or PLT_RST_L pulsing. Let's
- * wait long enough to debounce in both cases, picking duration
- * slightly shorter than the (one second) device polling
- * interval.
- *
- * Interrupts from the appropriate source (platform dependent)
- * will cancel the deferred function if the signal is
- * deasserted within the deferral interval.
- */
- hook_call_deferred(device_states[device].deferred, 900 * MSEC);
- }
-}
-
/*
* This function duplicates some of the functionality in chip/g/gpio.c in order
* to configure a given strap pin to be either a low gpio output, a gpio input
diff --git a/board/cr50/board.h b/board/cr50/board.h
index 6a350d0e28..b900768115 100644
--- a/board/cr50/board.h
+++ b/board/cr50/board.h
@@ -76,9 +76,6 @@
/* Allow multiple concurrent memory allocations. */
#define CONFIG_MALLOC
-/* Detect the states of other devices */
-#define CONFIG_DEVICE_STATE
-
/* Enable debug cable detection */
#define CONFIG_RDD
@@ -171,13 +168,6 @@ enum usb_strings {
USB_STR_COUNT
};
-/* Device indexes for devices that require debouncing */
-enum device_type {
- DEVICE_SERVO = 0,
-
- DEVICE_COUNT
-};
-
/*
* Device states
*
@@ -243,13 +233,13 @@ enum nvmem_vars {
};
void board_configure_deep_sleep_wakepins(void);
-/* Interrupt handler */
-void tpm_rst_deasserted(enum gpio_signal signal);
void ap_detect_asserted(enum gpio_signal signal);
void ec_detect_asserted(enum gpio_signal signal);
-void device_state_on(enum gpio_signal signal);
-void post_reboot_request(void);
void ec_tx_cr50_rx(enum gpio_signal signal);
+void servo_detect_asserted(enum gpio_signal signal);
+void tpm_rst_deasserted(enum gpio_signal signal);
+
+void post_reboot_request(void);
/* Special controls over EC and AP */
void assert_sys_rst(void);
@@ -283,6 +273,7 @@ void disable_ccd_uart(int uart);
void print_ap_state(void);
void print_ec_state(void);
+void print_servo_state(void);
int ap_is_on(void);
int ec_is_on(void);
diff --git a/board/cr50/build.mk b/board/cr50/build.mk
index 68bf4a0d03..d6b40c294e 100644
--- a/board/cr50/build.mk
+++ b/board/cr50/build.mk
@@ -29,7 +29,7 @@ dirs-y += chip/$(CHIP)/dcrypto
dirs-y += $(BDIR)/tpm2
# Objects that we need to build
-board-y = board.o ap_state.o ec_state.o
+board-y = board.o ap_state.o ec_state.o servo_state.o
board-${CONFIG_RDD} += rdd.o
board-${CONFIG_USB_SPI} += usb_spi.o
board-${CONFIG_USB_I2C} += usb_i2c.o
diff --git a/board/cr50/ec_state.c b/board/cr50/ec_state.c
index fb8087ab54..a324bdf772 100644
--- a/board/cr50/ec_state.c
+++ b/board/cr50/ec_state.c
@@ -66,6 +66,7 @@ static void set_ec_on(void)
CPRINTS("EC RX only");
if (!uart_bitbang_is_enabled(UART_EC))
uartn_enable(UART_EC);
+
set_state(DEVICE_STATE_INIT_RX_ONLY);
return;
}
diff --git a/board/cr50/gpio.inc b/board/cr50/gpio.inc
index dcf5b38561..8288735a3f 100644
--- a/board/cr50/gpio.inc
+++ b/board/cr50/gpio.inc
@@ -64,7 +64,7 @@ GPIO_INT(DETECT_EC, PIN(1, 2), GPIO_INT_HIGH, ec_detect_asserted)
* in the config assume the pin definitions here.
*/
GPIO_INT(DETECT_SERVO, PIN(1, 3), GPIO_INT_HIGH | GPIO_PULL_DOWN,
- device_state_on)
+ servo_detect_asserted)
GPIO_INT(EC_TX_CR50_RX, PIN(1, 4), GPIO_INT_FALLING, ec_tx_cr50_rx)
/* Pull this low to interrupt the AP */
diff --git a/board/cr50/rdd.c b/board/cr50/rdd.c
index 03b1785d90..db38fbcd52 100644
--- a/board/cr50/rdd.c
+++ b/board/cr50/rdd.c
@@ -5,7 +5,6 @@
#include "case_closed_debug.h"
#include "console.h"
-#include "device_state.h"
#include "gpio.h"
#include "hooks.h"
#include "i2c.h"
@@ -51,11 +50,6 @@ static void uart_select_tx(int uart, int signal)
}
}
-int servo_is_connected(void)
-{
- return device_get_state(DEVICE_SERVO) == DEVICE_STATE_ON;
-}
-
void uartn_tx_connect(int uart)
{
/*
@@ -188,6 +182,7 @@ static int command_ccd(int argc, char **argv)
print_ap_state();
print_ec_state();
print_rdd_state();
+ print_servo_state();
ccprintf("CCD: %s\n", rdd_is_connected() ? "enabled" : "disabled");
ccprintf("AP UART: %s\n",
diff --git a/board/cr50/servo_state.c b/board/cr50/servo_state.c
new file mode 100644
index 0000000000..5192549ec6
--- /dev/null
+++ b/board/cr50/servo_state.c
@@ -0,0 +1,227 @@
+/* Copyright 2017 The Chromium OS Authors. All rights reserved.
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ *
+ * Servo state machine.
+ */
+#include "common.h"
+#include "console.h"
+#include "gpio.h"
+#include "hooks.h"
+#include "uart_bitbang.h"
+#include "uartn.h"
+#include "usb_i2c.h"
+
+#define CPRINTS(format, args...) cprints(CC_SYSTEM, format, ## args)
+
+static enum device_state state = DEVICE_STATE_INIT;
+
+void print_servo_state(void)
+{
+ ccprintf("Servo: %s\n", device_state_name(state));
+}
+
+int servo_is_connected(void)
+{
+ /*
+ * If we're connected, we definitely know we are. If we're debouncing,
+ * then we were connected and might still be. If we haven't
+ * initialized yet, we'd bettter assume we're connected until we prove
+ * otherwise. In any of these cases, it's not safe to allow ports to
+ * be connected because that would block detecting servo.
+ */
+ return (state == DEVICE_STATE_CONNECTED ||
+ state == DEVICE_STATE_DEBOUNCING ||
+ state == DEVICE_STATE_INIT ||
+ state == DEVICE_STATE_INIT_DEBOUNCING);
+}
+
+/**
+ * Set the servo state.
+ *
+ * Done as a function to make it easier to debug state transitions. Note that
+ * this ONLY sets the state (and possibly prints debug info), and doesn't do
+ * all the additional transition work that servo_disconnect(), etc. do.
+ *
+ * @param new_state State to set.
+ */
+static void set_state(enum device_state new_state)
+{
+#ifdef CR50_DEBUG_SERVO_STATE
+ /* Print all state transitions. May spam the console. */
+ if (state != new_state)
+ CPRINTS("Servo %s -> %s",
+ device_state_name(state), device_state_name(new_state));
+#endif
+ state = new_state;
+}
+
+/**
+ * Check if we can tell servo is connected.
+ *
+ * @return 1 if we can tell if servo is connected, 0 if we can't tell.
+ */
+static int servo_detectable(void)
+{
+ /*
+ * If we are driving the UART transmit line to the EC, then we can't
+ * check to see if servo is also doing so.
+ *
+ * We also need to check if we're bit-banging the EC UART, because in
+ * that case, the UART transmit line is directly controlled as a GPIO
+ * and can be high even if UART TX is disconnected.
+ */
+ return !(uart_tx_is_connected(UART_EC) ||
+ uart_bitbang_is_enabled(UART_EC));
+}
+
+/**
+ * Handle servo being disconnected
+ */
+static void servo_disconnect(void)
+{
+ if (!servo_is_connected())
+ return;
+
+ CPRINTS("Servo disconnect");
+ set_state(DEVICE_STATE_DISCONNECTED);
+
+ /* Reconnect AP and EC UART TX if needed */
+ uartn_tx_connect(UART_AP);
+ uartn_tx_connect(UART_EC);
+}
+
+/**
+ * Handle servo being connected.
+ *
+ * This can be called directly by servo_detect(), or as a deferred function.
+ * Both are in the HOOK task, so can't preempt each other.
+ */
+static void servo_connect(void)
+{
+ /*
+ * If we were debouncing disconnect, go back to connected. We never
+ * finished disconnecting, so nothing else is necessary.
+ */
+ if (state == DEVICE_STATE_DEBOUNCING)
+ set_state(DEVICE_STATE_CONNECTED);
+
+ /* If we're already connected, nothing else needs to be done */
+ if (state == DEVICE_STATE_CONNECTED)
+ return;
+
+ /*
+ * If we're still here, this is a real transition from a disconnected
+ * state, so we need to configure ports.
+ */
+ CPRINTS("Servo connect");
+ set_state(DEVICE_STATE_CONNECTED);
+
+ /*
+ * Disable UART bit banging. Note this must be done before
+ * uartn_tx_disconnect() below, because this call will currently call
+ * uartn_tx_connect()!
+ */
+ uart_bitbang_disable(bitbang_config.uart);
+
+ /*
+ * Disconnect AP and EC UART TX when servo is attached. It's ok to
+ * leave RX enabled, because only TX interferes with TX from servo;
+ * that's why we don't call disable_ccd_uart() here.
+ */
+ uartn_tx_disconnect(UART_AP);
+ uartn_tx_disconnect(UART_EC);
+
+ /* Disconnect i2cm interface to ina */
+ usb_i2c_board_disable();
+}
+DECLARE_DEFERRED(servo_connect);
+
+/**
+ * Servo state machine
+ */
+static void servo_detect(void)
+{
+ /* Disable interrupts if we had them on for debouncing */
+ gpio_disable_interrupt(GPIO_DETECT_SERVO);
+
+ /* If we're driving EC UART TX, we can't detect servo */
+ if (!servo_detectable()) {
+ /* We're driving one port; might as well drive them all */
+ servo_disconnect();
+
+ set_state(DEVICE_STATE_UNDETECTABLE);
+ return;
+ }
+
+ /* Handle detecting servo */
+ if (gpio_get_level(GPIO_DETECT_SERVO)) {
+ servo_connect();
+ return;
+ }
+
+ /*
+ * If servo has become detectable but wasn't detected above, assume
+ * it's disconnected.
+ *
+ * We know we were driving EC UART TX, so we want to give priority to
+ * our ability to drive it again. If we went to the debouncing state
+ * here, then we'd need to wait a second before we could drive it.
+ *
+ * This is similar to how if servo was driving EC UART TX, we go to the
+ * debouncing state below, because we want to give priority to servo
+ * being able to drive it again.
+ */
+ if (state == DEVICE_STATE_UNDETECTABLE)
+ set_state(DEVICE_STATE_DISCONNECTED);
+
+ /* Servo wasn't detected. If we're already disconnected, done. */
+ if (state == DEVICE_STATE_DISCONNECTED)
+ return;
+
+ /* If we were debouncing, we're now sure we're disconnected */
+ if (state == DEVICE_STATE_DEBOUNCING ||
+ state == DEVICE_STATE_INIT_DEBOUNCING) {
+ servo_disconnect();
+ return;
+ }
+
+ /*
+ * Otherwise, we were connected or initializing, and we're not sure if
+ * we're now disconnected or just sending a 0-bit. So start
+ * debouncing.
+ *
+ * During debouncing, servo_is_connected() will still return true, so
+ * that if both CCD and servo cables are connected, we won't start
+ * driving EC UART TX and become unable to determine the servo connect
+ * state.
+ */
+ if (state == DEVICE_STATE_INIT)
+ set_state(DEVICE_STATE_INIT_DEBOUNCING);
+ else
+ set_state(DEVICE_STATE_DEBOUNCING);
+ gpio_enable_interrupt(GPIO_DETECT_SERVO);
+}
+/*
+ * Do this at slightly elevated priority so it runs before rdd_check_pin() and
+ * ec_detect(). This increases the odds that we'll detect servo before
+ * detecting the EC. If ec_detect() ran first, it could turn on TX to the EC
+ * UART before we had a chance to detect servo. This is still a little bit of
+ * a race condition.
+ */
+DECLARE_HOOK(HOOK_SECOND, servo_detect, HOOK_PRIO_DEFAULT - 1);
+
+/**
+ * Interrupt handler for servo detect asserted
+ */
+void servo_detect_asserted(enum gpio_signal signal)
+{
+ gpio_disable_interrupt(GPIO_DETECT_SERVO);
+
+ /*
+ * If this interrupt is because servo is actually detectable (vs. we're
+ * driving the detect pin now), queue a transition back to connected.
+ */
+ if (servo_detectable())
+ hook_call_deferred(&servo_connect_data, 0);
+}