static void on_json_end() {
    HTTPLOG_PRINTLN(">j");

    if ( IrCtrl.state != IR_WRITING ) {
        HTTPLOG_PRINTLN("!E5");
        IR_dump();
        return;
    }

    IR_xmit();
    on_ir_xmit();
}
void irkit_http_loop() {
#ifdef USE_INTERNET
    // long poll
    if (TIMER_FIRED(polling_timer)) {
        TIMER_STOP(polling_timer);

        if (TIMER_RUNNING(suspend_polling_timer)) {
            // suspend GET /m for a while if we have received a POST /messages request from client
            // client is in wifi, we can ignore our server for a while
            TIMER_START(polling_timer, SUSPEND_GET_MESSAGES_INTERVAL);
        }
        else {
            int8_t result = irkit_httpclient_get_messages();
            if ( result < 0 ) {
                HTTPLOG_PRINTLN("!E3");
                // maybe time cures GS? (no it doesn't, let's hardware reset, software reset doesn't work here)
                // don't software reset AVR, because that cuts off serial logging (for debug purpose only)
                wifi_hardware_reset();
            }
            else {
                polling_cid = result;
            }
        }
    }

    if (TIMER_FIRED(suspend_polling_timer)) {
        TIMER_STOP(suspend_polling_timer);
    }
#endif
}
static int8_t on_request(int8_t cid, int8_t routeid, GSwifi::GSREQUESTSTATE state) {
    if ( (state == GSwifi::GSREQUESTSTATE_RECEIVED) &&
         (! gs.validRequest()) &&
         (! has_valid_pass) ) {
        HTTPLOG_PRINTLN("!E32");
        gs.writeHead(cid, 400);
        gs.writeEnd();
        ring_put( &commands, COMMAND_CLOSE );
        ring_put( &commands, cid );
        return -1;
    }

    switch (routeid) {
    case 0: // POST /messages
        return on_post_messages_request(cid, state);

    case 1: // POST /keys
        // when client requests for a new key,
        // we request server for one, and respond to client with the result from server
        return on_post_keys_request(cid, state);

    case 2: // GET /messages
        return on_get_messages_request(cid, state);

    case 3: // POST /wifi
        return on_post_wifi_request(cid, state);

    default:
        break;
    }
    return -1;
}
static int8_t on_post_messages_request(int8_t cid, GSwifi::GSREQUESTSTATE state) {
    while (! gs.bufferEmpty()) {
        char letter = gs.bufferGet();
        parse_json( letter );
    }

    if (state == GSwifi::GSREQUESTSTATE_RECEIVED) {
        // should be xmitting or idle (xmit finished)
        if (IrCtrl.state == IR_WRITING) {
            HTTPLOG_PRINTLN("!E7");
            // invalid json
            gs.writeHead(cid, 400);
            gs.writeEnd();
        }
        else {
            gs.writeHead(cid, 200);
            gs.writeEnd();
        }
        ring_put( &commands, COMMAND_CLOSE );
        ring_put( &commands, cid );

#ifdef USE_INTERNET
        TIMER_START( suspend_polling_timer, SUSPEND_GET_MESSAGES_INTERVAL );
#endif
    }

    return 0;
}
static int8_t on_get_messages_response(int8_t cid, uint16_t status_code, GSwifi::GSREQUESTSTATE state) {
    HTTPLOG_PRINT(P("< G /m ")); HTTPLOG_PRINTLN(status_code);

    if (status_code != 200) {
        gs.bufferClear();
    }

    switch (status_code) {
    case 200:
        while (! gs.bufferEmpty()) {
            char letter = gs.bufferGet();

            parse_json( letter );
        }

        if (state == GSwifi::GSREQUESTSTATE_RECEIVED) {
            // should not be WRITING here, should be XMITTING or IDLE (xmit finished)
            if (IrCtrl.state == IR_WRITING) {
                // prevent from locking in WRITING state forever
                IR_state( IR_IDLE );
            }

            ring_put( &commands, COMMAND_CLOSE );
            ring_put( &commands, cid );
            if ((polling_cid == cid) || (polling_cid == CID_UNDEFINED)) {
                polling_cid = CID_UNDEFINED;
                ring_put( &commands, COMMAND_START_POLLING );
            }
            // if polling_cid != cid
            // there's already an ongoing polling request, so request again when that one finishes
        }
        break;
    case HTTP_STATUSCODE_CLIENT_TIMEOUT:
        polling_cid = CID_UNDEFINED;
        ring_put( &commands, COMMAND_CLOSE );
        ring_put( &commands, cid );
        irkit_httpclient_start_polling( 5 );
        break;
    case HTTP_STATUSCODE_DISCONNECT:
        polling_cid = CID_UNDEFINED;
        irkit_httpclient_start_polling( 5 );
        break;
    // heroku responds with 503 if longer than 30sec,
    // or when deploy occurs
    case 503:
    default:
        if (state == GSwifi::GSREQUESTSTATE_RECEIVED) {
            ring_put( &commands, COMMAND_CLOSE );
            ring_put( &commands, cid );
            irkit_httpclient_start_polling( 5 );
        }
        break;
    }

    return 0;
}
static int8_t on_post_keys_response(int8_t cid, uint16_t status_code, GSwifi::GSREQUESTSTATE state) {
    HTTPLOG_PRINT(P("< P /k ")); HTTPLOG_PRINTLN(status_code);

    if (status_code != 200) {
        gs.bufferClear();
    }

    if (state != GSwifi::GSREQUESTSTATE_RECEIVED) {
        return 0;
    }

    gs.writeHead( post_keys_cid, status_code );

    switch (status_code) {
    case 200:
        while (! gs.bufferEmpty()) {
            char letter = gs.bufferGet();
            gs.write( letter );
        }
        gs.writeEnd();
        break;
    default:
        gs.writeEnd();
        break;
    }

    ring_put( &commands, COMMAND_CLOSE );
    ring_put( &commands, cid );
    ring_put( &commands, COMMAND_CLOSE );
    if (ring_isfull( &commands )) {
        HTTPLOG_PRINTLN("!E8");
        return -1;
    }
    ring_put( &commands, post_keys_cid );

    return 0;
}
static int8_t on_post_door_response(int8_t cid, uint16_t status_code, GSwifi::GSREQUESTSTATE state) {
    HTTPLOG_PRINT(P("< P /d ")); HTTPLOG_PRINTLN(status_code);

    gs.bufferClear();

    if (state != GSwifi::GSREQUESTSTATE_RECEIVED) {
        return 0;
    }

    switch (status_code) {
    case 200:
        keys.setKeyValid(true);
        // save only independent area, since sharedbuffer might be populated by IR or so.
        keys.save2();
        IR_state( IR_IDLE );

        ring_put( &commands, COMMAND_CLOSE );
        ring_put( &commands, cid );
#ifdef USE_INTERNET
        ring_put( &commands, COMMAND_START_POLLING );
#endif

        on_irkit_ready();

        break;
#ifdef USE_INTERNET
    case 401:
    case HTTP_STATUSCODE_CLIENT_TIMEOUT:
        // keys have expired, we have to start listening for POST /wifi again
        keys.clear();
        keys.save();
        software_reset();

        break;
    case 400: // must be program bug, happens when there's no hostname parameter
    case 408:
    case 503: // heroku responds with 503 if longer than 30sec
    default:
        // retry again on next loop
        ring_put( &commands, COMMAND_CLOSE );
        ring_put( &commands, cid );
        ring_put( &commands, COMMAND_POST_DOOR );
        break;
#endif
    }

    return 0;
}
static int8_t on_post_messages_response(int8_t cid, uint16_t status_code, GSwifi::GSREQUESTSTATE state) {
    HTTPLOG_PRINT(P("< P /m ")); HTTPLOG_PRINTLN(status_code);

    if (status_code != 200) {
        gs.bufferClear();
    }

    if (state != GSwifi::GSREQUESTSTATE_RECEIVED) {
        return 0;
    }

    ring_put( &commands, COMMAND_CLOSE );
    ring_put( &commands, cid );

    return 0;
}
static void on_json_start() {
    HTTPLOG_PRINTLN("j<");

    IR_state( IR_WRITING );
}
static void on_json_start() {
    HTTPLOG_PRINTLN("j<");

    IR_state( IR_WRITING );
    has_valid_pass = false;
}