root/src/add-ons/kernel/bus_managers/ps2/ps2_dev.cpp
/*
 * Copyright 2005-2014 Haiku, Inc. All rights reserved.
 * Distributed under the terms of the MIT License.
 *
 * Authors (in chronological order):
 *              Marcus Overhagen (marcus@overhagen.de)
 *              Clemens Zeidler (haiku@clemens-zeidler.de)
 */


/*!     PS/2 bus manager */

#include "ps2_dev.h"
#include "ps2_service.h"

#include "ps2_alps.h"
#include "ps2_elantech.h"
#include "ps2_standard_mouse.h"
#include "ps2_synaptics.h"
#include "ps2_trackpoint.h"

#include <fs/devfs.h>
#include <util/AutoLock.h>

#include <string.h>


//#define TRACE_PS2_DEV
#ifdef TRACE_PS2_DEV
#       define TRACE(x...) dprintf(x)
#else
#       define TRACE(x...)
#endif


ps2_dev ps2_device[PS2_DEVICE_COUNT];


status_t
ps2_reset_mouse(ps2_dev* dev)
{
        uint8 data[2];
        status_t status;

        TRACE("ps2: ps2_reset_mouse\n");

        status = ps2_dev_command(dev, PS2_CMD_RESET, NULL, 0, data, 2);

        if (status == B_OK && data[0] == 0xFE && data[1] == 0xAA) {
                // workaround for HP/Compaq KBCs timeout condition. #2867 #3594 #4315
                TRACE("ps2: KBC has timed out the mouse reset request. "
                        "Response was: 0x%02x 0x%02x. Requesting the answer data.\n",
                        data[0], data[1]);
                status = ps2_dev_command(dev, PS2_CMD_RESEND, NULL, 0, data, 2);
        }

        if (status == B_OK && (data[0] != 0xAA || data[1] != 0x00)) {
                TRACE("ps2: reset mouse failed, response was: 0x%02x 0x%02x\n",
                        data[0], data[1]);
                status = B_ERROR;
        } else if (status != B_OK) {
                TRACE("ps2: reset mouse failed\n");
        } else {
                TRACE("ps2: reset mouse success\n");
        }

        return status;
}


status_t
ps2_dev_detect_pointing(ps2_dev* dev, device_hooks** hooks)
{
        status_t status = ps2_reset_mouse(dev);
        if (status != B_OK) {
                INFO("ps2_dev: reset failed: %s\n", strerror(status));
                return B_ERROR;
        }

        // probe devices
        // the probe function has to set the dev name and the dev packet size

        status = probe_trackpoint(dev);
        if (status == B_OK) {
                *hooks = &gStandardMouseDeviceHooks;
                goto dev_found;
        }

        status = probe_synaptics(dev);
        if (status == B_OK) {
                *hooks = &gSynapticsDeviceHooks;
                goto dev_found;
        }

        status = probe_alps(dev);
        if (status == B_OK) {
                *hooks = &gALPSDeviceHooks;
                goto dev_found;
        }

        status = probe_elantech(dev);
        if (status == B_OK) {
                *hooks = &gElantechDeviceHooks;
                goto dev_found;
        }

        // reset the mouse for the case that the previous probes left the mouse in
        // a undefined state
        status = ps2_reset_mouse(dev);
        if (status != B_OK) {
                INFO("ps2: reset after probe failed\n");
                return B_ERROR;
        }

        status = probe_standard_mouse(dev);
        if (status == B_OK) {
                *hooks = &gStandardMouseDeviceHooks;
                goto dev_found;
        }

        return B_ERROR;

dev_found:
        if (dev == &(ps2_device[PS2_DEVICE_SYN_PASSTHROUGH]))
                synaptics_pass_through_set_packet_size(dev, dev->packet_size);

        return B_OK;
}


status_t
ps2_dev_init(void)
{
        ps2_device[0].name = "input/mouse/ps2/0";
        ps2_device[0].active = false;
        ps2_device[0].idx = 0;
        ps2_device[0].result_sem = -1;
        ps2_device[0].command = standard_command_timeout;

        ps2_device[1].name = "input/mouse/ps2/1";
        ps2_device[1].active = false;
        ps2_device[1].idx = 1;
        ps2_device[1].result_sem = -1;
        ps2_device[1].command = standard_command_timeout;

        ps2_device[2].name = "input/mouse/ps2/2";
        ps2_device[2].active = false;
        ps2_device[2].idx = 2;
        ps2_device[2].result_sem = -1;
        ps2_device[2].command = standard_command_timeout;

        ps2_device[3].name = "input/mouse/ps2/3";
        ps2_device[3].active = false;
        ps2_device[3].idx = 3;
        ps2_device[3].result_sem = -1;
        ps2_device[3].command = standard_command_timeout;

        ps2_device[4].name = "input/mouse/ps2/synaptics_passthrough";
        ps2_device[4].active = false;
        ps2_device[4].result_sem = -1;
        ps2_device[4].command = passthrough_command;

        ps2_device[5].name = "input/keyboard/at/0";
        ps2_device[5].active = false;
        ps2_device[5].result_sem = -1;
        ps2_device[5].flags = PS2_FLAG_KEYB;
        ps2_device[5].command = standard_command_timeout;

        int i;
        for (i = 0; i < PS2_DEVICE_COUNT; i++) {
                ps2_dev* dev = &ps2_device[i];
                dev->result_sem = create_sem(0, "ps2 result");
                if (dev->result_sem < 0)
                        goto err;
        }
        return B_OK;

err:
        ps2_dev_exit();

        return B_ERROR;
}


void
ps2_dev_exit(void)
{
        for (int i = 0; i < PS2_DEVICE_COUNT; i++) {
                ps2_dev* dev = &ps2_device[i];
                if (dev->result_sem >= 0) {
                        delete_sem(dev->result_sem);
                        dev->result_sem = -1;
                }
        }
}


void
ps2_dev_publish(ps2_dev* dev)
{
        status_t status = B_OK;
        TRACE("ps2: ps2_dev_publish %s\n", dev->name);

        if (dev->active)
                return;

        if (atomic_get(&dev->flags) & PS2_FLAG_KEYB) {
                status = devfs_publish_device(dev->name, &gKeyboardDeviceHooks);
        } else {
                // Check if this is the "pass-through" device and wait until
                // the parent_dev goes to enabled state. It is required to prevent
                // from messing up the Synaptics command sequences in synaptics_open.
                if (dev->parent_dev) {
                        const bigtime_t timeout = 2000000;
                        bigtime_t start = system_time();
                        while (!(atomic_get(&dev->parent_dev->flags) & PS2_FLAG_ENABLED)) {
                                if ((system_time() - start) > timeout) {
                                        status = B_BUSY;
                                        break;
                                }
                                snooze(timeout / 20);
                        }
                        TRACE("ps2: publishing %s: parent %s is %s; wait time %" B_PRId64
                                "\n", dev->name, dev->parent_dev->name,
                                status == B_OK ? "enabled" : "busy", system_time() - start);
                }

                if (status == B_OK) {
                        device_hooks* hooks;
                        status = ps2_dev_detect_pointing(dev, &hooks);
                        if (status == B_OK) {
                                status = devfs_publish_device(dev->name, hooks);
                        }
                }
        }

        dev->active = true;

        INFO("ps2: devfs_publish_device %s, status = 0x%08" B_PRIx32 "\n",
                dev->name, status);
}


void
ps2_dev_unpublish(ps2_dev* dev)
{
        status_t status;
        TRACE("ps2: ps2_dev_unpublish %s\n", dev->name);

        if (!dev->active)
                return;

        dev->active = false;

        status = devfs_unpublish_device(dev->name, true);

        if ((dev->flags & PS2_FLAG_ENABLED) && dev->disconnect)
                dev->disconnect(dev);

        INFO("ps2: devfs_unpublish_device %s, status = 0x%08" B_PRIx32 "\n",
                dev->name, status);
}


int32
ps2_dev_handle_int(ps2_dev* dev)
{
        const uint8 data = dev->history[0].data;
        uint32 flags;

        flags = atomic_get(&dev->flags);

        if (flags & PS2_FLAG_CMD) {
                if ((flags & (PS2_FLAG_ACK | PS2_FLAG_NACK)) == 0) {
                        int cnt = 1;
                        if (data == PS2_REPLY_ACK) {
                                atomic_or(&dev->flags, PS2_FLAG_ACK);
                        } else if (data == PS2_REPLY_RESEND || data == PS2_REPLY_ERROR) {
                                atomic_or(&dev->flags, PS2_FLAG_NACK);
                        } else if ((flags & PS2_FLAG_GETID)
                                && (data == 0 || data == 3 || data == 4)) {
                                // workaround for broken mice that don't ack the "get id"
                                // command
                                TRACE("ps2: ps2_dev_handle_int: mouse didn't ack the 'get id' "
                                        "command\n");
                                atomic_or(&dev->flags, PS2_FLAG_ACK);
                                if (dev->result_buf_cnt) {
                                        dev->result_buf[dev->result_buf_idx] = data;
                                        dev->result_buf_idx++;
                                        dev->result_buf_cnt--;
                                        if (dev->result_buf_cnt == 0) {
                                                atomic_and(&dev->flags, ~PS2_FLAG_CMD);
                                                cnt++;
                                        }
                                }
                        } else if ((flags & PS2_FLAG_RESEND)) {
                                TRACE("ps2: ps2_dev_handle_int: processing RESEND request\n");
                                atomic_or(&dev->flags, PS2_FLAG_ACK);
                                if (dev->result_buf_cnt) {
                                        dev->result_buf[dev->result_buf_idx] = data;
                                        dev->result_buf_idx++;
                                        dev->result_buf_cnt--;
                                        if (dev->result_buf_cnt == 0) {
                                                atomic_and(&dev->flags, ~PS2_FLAG_CMD);
                                                cnt++;
                                        }
                                }
                        } else {
//                              TRACE("ps2: ps2_dev_handle_int unexpected data 0x%02x while "
//                                      "waiting for ack\n", data);
                                TRACE("ps2: int1 %02x\n", data);
                                goto pass_to_handler;
                        }
                        release_sem_etc(dev->result_sem, cnt, B_DO_NOT_RESCHEDULE);
                        return B_INVOKE_SCHEDULER;
                } else if (dev->result_buf_cnt) {
                        dev->result_buf[dev->result_buf_idx] = data;
                        dev->result_buf_idx++;
                        dev->result_buf_cnt--;
                        if (dev->result_buf_cnt == 0) {
                                atomic_and(&dev->flags, ~PS2_FLAG_CMD);
                                release_sem_etc(dev->result_sem, 1, B_DO_NOT_RESCHEDULE);
                                return B_INVOKE_SCHEDULER;
                        }
                } else {
//                      TRACE("ps2: ps2_dev_handle_int unexpected data 0x%02x during "
//                              "command processing\n", data);
                        TRACE("ps2: int2 %02x\n", data);
                        goto pass_to_handler;
                }
                return B_HANDLED_INTERRUPT;
        }

pass_to_handler:

        if ((flags & PS2_FLAG_KEYB) == 0) {
                if (dev->history[0].error && data == 0xfd) {
                        INFO("ps2: hot removal of %s\n", dev->name);
                        ps2_service_notify_device_removed(dev);
                        return B_INVOKE_SCHEDULER;
                }
                if (data == 0x00 && dev->history[1].data == 0xaa
                        && (dev->history[0].time - dev->history[1].time) < 50000) {
                        INFO("ps2: hot plugin of %s\n", dev->name);
                        if (dev->active) {
                                INFO("ps2: device %s still active, republishing...\n",
                                        dev->name);
                                ps2_service_notify_device_republish(dev);
                        } else {
                                ps2_service_notify_device_added(dev);
                        }
                        return B_INVOKE_SCHEDULER;
                }
        }

        if (!dev->active) {
                TRACE("ps2: %s not active, data 0x%02x dropped\n", dev->name, data);
                if (gSetupComplete && data != 0x00 && data != 0xaa) {
                        INFO("ps2: possibly a hot plugin of %s\n", dev->name);
                        ps2_service_notify_device_added(dev);
                        return B_INVOKE_SCHEDULER;
                }
                return B_HANDLED_INTERRUPT;
        }

        if ((flags & PS2_FLAG_ENABLED) == 0) {
                TRACE("ps2: %s not enabled, data 0x%02x dropped\n", dev->name, data);
                return B_HANDLED_INTERRUPT;
        }

        return dev->handle_int(dev);
}


status_t
standard_command_timeout(ps2_dev* dev, uint8 cmd, const uint8* out,
        int out_count, uint8* in, int in_count, bigtime_t timeout)
{
        TRACE("ps2: ps2_dev_command cmd 0x%02x, out-count %d, in-count %d, "
                "dev %s\n", cmd, out_count, in_count, dev->name);
        for (int i = 0; i < out_count; i++)
                TRACE("ps2: ps2_dev_command tx: 0x%02x\n", out[i]);

        MutexLocker controllerLocker(gControllerLock);

        int32 sem_count;
        status_t res = get_sem_count(dev->result_sem, &sem_count);
        if (res == B_OK && sem_count != 0) {
                TRACE("ps2: ps2_dev_command: sem_count %" B_PRId32 ", fixing!\n",
                        sem_count);
                if (sem_count > 0)
                        acquire_sem_etc(dev->result_sem, sem_count, 0, 0);
                else
                        release_sem_etc(dev->result_sem, -sem_count, 0);
        }

        dev->result_buf_cnt = in_count;
        dev->result_buf_idx = 0;
        dev->result_buf = in;

        res = B_OK;
        for (int i = -1; res == B_OK && i < out_count; i++) {
                atomic_and(&dev->flags,
                        ~(PS2_FLAG_ACK | PS2_FLAG_NACK | PS2_FLAG_GETID | PS2_FLAG_RESEND));

                if (!(atomic_get(&dev->flags) & PS2_FLAG_KEYB)) {
                        uint8 prefix_cmd;
                        if (gActiveMultiplexingEnabled)
                                prefix_cmd = 0x90 + dev->idx;
                        else
                                prefix_cmd = 0xd4;
                        res = ps2_wait_write();
                        if (res == B_OK)
                                ps2_write_ctrl(prefix_cmd);
                }

                res = ps2_wait_write();
                if (res == B_OK) {
                        if (i == -1) {
                                if (cmd == PS2_CMD_GET_DEVICE_ID)
                                        atomic_or(&dev->flags, PS2_FLAG_CMD | PS2_FLAG_GETID);
                                else if (cmd == PS2_CMD_RESEND)
                                        atomic_or(&dev->flags, PS2_FLAG_CMD | PS2_FLAG_RESEND);
                                else
                                        atomic_or(&dev->flags, PS2_FLAG_CMD);
                                ps2_write_data(cmd);
                        } else {
                                ps2_write_data(out[i]);
                        }
                }

#ifdef TRACE_PS2_DEV
                bigtime_t start = system_time();
#endif
                res = acquire_sem_etc(dev->result_sem, 1, B_RELATIVE_TIMEOUT, timeout);

                if (res != B_OK)
                        atomic_and(&dev->flags, ~PS2_FLAG_CMD);

#ifdef TRACE_PS2_DEV
                TRACE("ps2: ps2_dev_command wait for ack res 0x%08" B_PRIx32 ", "
                        "wait-time %" B_PRId64 "\n", res, system_time() - start);
#endif
                if (atomic_get(&dev->flags) & PS2_FLAG_ACK) {
                        TRACE("ps2: ps2_dev_command got ACK\n");
                }

                if (atomic_get(&dev->flags) & PS2_FLAG_NACK) {
                        atomic_and(&dev->flags, ~PS2_FLAG_CMD);
                        res = B_IO_ERROR;
                        TRACE("ps2: ps2_dev_command got NACK\n");
                }

                if (res != B_OK)
                        break;
        }

        if (res == B_OK) {
                if (in_count == 0) {
                        atomic_and(&dev->flags, ~PS2_FLAG_CMD);
                } else {
#ifdef TRACE_PS2_DEV
                        bigtime_t start = system_time();
#endif
                        res = acquire_sem_etc(dev->result_sem, 1, B_RELATIVE_TIMEOUT,
                                timeout);

                        atomic_and(&dev->flags, ~PS2_FLAG_CMD);

                        if (dev->result_buf_cnt != 0) {
                                TRACE("ps2: ps2_dev_command error: %d rx bytes not received\n",
                                        dev->result_buf_cnt);
                                in_count -= dev->result_buf_cnt;
                                dev->result_buf_cnt = 0;
                                res = B_IO_ERROR;
                        }

#ifdef TRACE_PS2_DEV
                        TRACE("ps2: ps2_dev_command wait for input res 0x%08" B_PRIx32 ", "
                                "wait-time %" B_PRId64 "\n", res, system_time() - start);
                        for (i = 0; i < in_count; i++)
                                TRACE("ps2: ps2_dev_command rx: 0x%02x\n", in[i]);
#endif
                }
        }

        TRACE("ps2: ps2_dev_command result: %s\n", strerror(res));

        return res;
}


status_t
ps2_dev_command(ps2_dev* dev, uint8 cmd, const uint8* out, int out_count,
        uint8* in, int in_count)
{
        return ps2_dev_command_timeout(dev, cmd, out, out_count, in, in_count,
                4000000);
}


status_t
ps2_dev_command_timeout(ps2_dev* dev, uint8 cmd, const uint8* out,
        int out_count, uint8* in, int in_count, bigtime_t timeout)
{
        return dev->command(dev, cmd, out, out_count, in, in_count, timeout);
}


status_t
ps2_dev_sliced_command(ps2_dev* dev, uint8 cmd)
{
        uint8 val;
        if (ps2_dev_command(dev, PS2_CMD_MOUSE_SET_SCALE11) != B_OK)
                return B_ERROR;

        val = (cmd >> 6) & 3;
        if (ps2_dev_command(dev, PS2_CMD_MOUSE_SET_RES, &val, 1) != B_OK)
                return B_ERROR;

        val = (cmd >> 4) & 3;
        if (ps2_dev_command(dev, PS2_CMD_MOUSE_SET_RES, &val, 1) != B_OK)
                return B_ERROR;

        val = (cmd >> 2) & 3;
        if (ps2_dev_command(dev, PS2_CMD_MOUSE_SET_RES, &val, 1) != B_OK)
                return B_ERROR;

        val = cmd & 3;
        if (ps2_dev_command(dev, PS2_CMD_MOUSE_SET_RES, &val, 1) != B_OK)
                return B_ERROR;

        return B_OK;
}