/* 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 . */ #include "AP_Generator_RichenPower.h" #if AP_GENERATOR_RICHENPOWER_ENABLED #include #include #include #include #include #include extern const AP_HAL::HAL& hal; // init method; configure communications with the generator void AP_Generator_RichenPower::init() { const AP_SerialManager &serial_manager = AP::serialmanager(); uart = serial_manager.find_serial(AP_SerialManager::SerialProtocol_Generator, 0); if (uart != nullptr) { const uint32_t baud = serial_manager.find_baudrate(AP_SerialManager::SerialProtocol_Generator, 0); uart->begin(baud, 256, 256); } // Tell frontend what measurements are available for this generator _frontend._has_current = true; _frontend._has_consumed_energy = false; _frontend._has_fuel_remaining = false; } // find a RichenPower message in the buffer, starting at // initial_offset. If dound, that message (or partial message) will // be moved to the start of the buffer. void AP_Generator_RichenPower::move_header_in_buffer(uint8_t initial_offset) { uint8_t header_offset; for (header_offset=initial_offset; header_offsetread(&u.parse_buffer[body_length], ARRAY_SIZE(u.parse_buffer)-body_length); if (nbytes == 0) { return false; } body_length += nbytes; move_header_in_buffer(0); // header byte 1 is correct. if (body_length < ARRAY_SIZE(u.parse_buffer)) { // need a full buffer to have a valid message... return false; } if (u.packet.headermagic2 != HEADER_MAGIC2) { move_header_in_buffer(1); return false; } // check for the footer signature: if (u.packet.footermagic1 != FOOTER_MAGIC1) { move_header_in_buffer(1); return false; } if (u.packet.footermagic2 != FOOTER_MAGIC2) { move_header_in_buffer(1); return false; } // calculate checksum.... uint16_t checksum = 0; const uint8_t *checksum_buffer = &u.parse_buffer[2]; for (uint8_t i=0; i<5; i++) { checksum += be16toh_ptr(&checksum_buffer[2*i]); } if (checksum != be16toh(u.packet.checksum)) { move_header_in_buffer(1); return false; } // check the version: const uint16_t version = be16toh(u.packet.version); const uint8_t major = version / 100; const uint8_t minor = (version % 100) / 10; const uint8_t point = version % 10; if (!protocol_information_anounced) { gcs().send_text(MAV_SEVERITY_INFO, "RichenPower: protocol %u.%u.%u", major, minor, point); protocol_information_anounced = true; } last_reading.runtime = be16toh(u.packet.runtime_hours) * 3600 + u.packet.runtime_minutes * 60 + u.packet.runtime_seconds; last_reading.seconds_until_maintenance = be16toh(u.packet.seconds_until_maintenance_high) * 65536 + be16toh(u.packet.seconds_until_maintenance_low); last_reading.errors = be16toh(u.packet.errors); last_reading.rpm = be16toh(u.packet.rpm); last_reading.output_voltage = be16toh(u.packet.output_voltage) * 0.01f; last_reading.output_current = be16toh(u.packet.output_current) * 0.01f; last_reading.mode = (Mode)u.packet.mode; last_reading_ms = AP_HAL::millis(); body_length = 0; // update the time we started idling at: if (last_reading.mode == Mode::IDLE) { if (idle_state_start_ms == 0) { idle_state_start_ms = last_reading_ms; } } else { idle_state_start_ms = 0; } return true; } // update the synthetic heat measurement we are keeping for the // generator. We keep a synthetic heat measurement as the telemetry // from the unit does not include the motor temperature, so we // approximate one by assuming it produces heat in proportion to its // current RPM. void AP_Generator_RichenPower::update_heat() { // assume heat increase is directly proportional to RPM. const uint32_t now = AP_HAL::millis(); uint16_t rpm = last_reading.rpm; if (now - last_reading_ms > 2000) { // if we're not getting updates, assume we're getting colder rpm = 0; // ... and resend the version information when we get something again protocol_information_anounced = false; } const uint32_t time_delta_ms = now - last_heat_update_ms; last_heat_update_ms = now; heat += rpm * time_delta_ms * (1/1000.0f); // cap the heat of the motor: heat = MIN(heat, 60 * RUN_RPM); // so cap heat at 60 seconds at run-speed // now lose some heat to the environment heat -= (heat * heat_environment_loss_factor * (time_delta_ms * (1/1000.0f))); // lose some % of heat per second } // returns true if the generator should be allowed to move into // the "run" (high-RPM) state: bool AP_Generator_RichenPower::generator_ok_to_run() const { return heat > heat_required_for_run(); } // returns an amount of synthetic heat required for the generator // to move into the "run" state: constexpr float AP_Generator_RichenPower::heat_required_for_run() { // assume that heat is proportional to RPM. Return a number // proportial to RPM. Reduce it to account for the cooling some%/s // cooling return (45 * IDLE_RPM) * heat_environment_loss_30s; } void AP_Generator_RichenPower::check_maintenance_required() { // don't bother the user while flying: if (hal.util->get_soft_armed()) { return; } if (!AP::generator()->option_set(AP_Generator::Option::INHIBIT_MAINTENANCE_WARNINGS)) { const uint32_t now = AP_HAL::millis(); if (last_reading.errors & (1U< 60000) { gcs().send_text(MAV_SEVERITY_NOTICE, "Generator: requires maintenance"); last_maintenance_warning_ms = now; } } } } /* update the state of the sensor */ void AP_Generator_RichenPower::update(void) { if (uart == nullptr) { return; } if (last_reading_ms != 0) { update_runstate(); check_maintenance_required(); } (void)get_reading(); update_heat(); update_frontend_readings(); Log_Write(); } // update_runstate updates the servo output we use to control the // generator. Which state we request the generator move to depends on // the RC inputcontrol and the temperature the generator is at. void AP_Generator_RichenPower::update_runstate() { // don't run the generator while the safety is on: // if (hal.util->safety_switch_state() == AP_HAL::Util::SAFETY_DISARMED) { // _servo_channel->set_output_pwm(SERVO_PWM_STOP); // return; // } static const uint16_t SERVO_PWM_STOP = 1200; static const uint16_t SERVO_PWM_IDLE = 1500; static const uint16_t SERVO_PWM_RUN = 1900; // if the vehicle crashes then we assume the pilot wants to stop // the motor. This is done as a once-off when the crash is // detected to allow the operator to rearm the vehicle, or we end // up in a catch-22 situation where we force the stop state on the // generator so they can't arm and can't start the generator // because the vehicle is crashed. if (AP::vehicle()->is_crashed()) { if (!vehicle_was_crashed) { gcs().send_text(MAV_SEVERITY_INFO, "Crash; stopping generator"); pilot_desired_runstate = RunState::STOP; vehicle_was_crashed = true; } } else { vehicle_was_crashed = false; } if (commanded_runstate != pilot_desired_runstate && !hal.util->get_soft_armed()) { // consider changing the commanded runstate to the pilot // desired runstate: commanded_runstate = RunState::IDLE; switch (pilot_desired_runstate) { case RunState::STOP: // The H2 will keep the motor running for ~20 seconds for cool-down // we must go via the idle state or the H2 disallows moving to stop! if (time_in_idle_state_ms() > 1000) { commanded_runstate = pilot_desired_runstate; } break; case RunState::IDLE: // can always switch to idle commanded_runstate = pilot_desired_runstate; break; case RunState::RUN: // must have idled for a while before moving to run: if (generator_ok_to_run()) { commanded_runstate = pilot_desired_runstate; } break; } } switch (commanded_runstate) { case RunState::STOP: SRV_Channels::set_output_pwm(SRV_Channel::k_generator_control, SERVO_PWM_STOP); break; case RunState::IDLE: SRV_Channels::set_output_pwm(SRV_Channel::k_generator_control, SERVO_PWM_IDLE); break; case RunState::RUN: SRV_Channels::set_output_pwm(SRV_Channel::k_generator_control, SERVO_PWM_RUN); break; } } // log generator status to the onboard log void AP_Generator_RichenPower::Log_Write() { #define MASK_LOG_ANY 0xFFFF if (!AP::logger().should_log(MASK_LOG_ANY)) { return; } if (last_logged_reading_ms == last_reading_ms) { return; } last_logged_reading_ms = last_reading_ms; AP::logger().WriteStreaming( "GEN", "TimeUS,runTime,maintTime,errors,rpm,ovolt,ocurr,mode", "s-------", "F-------", "QIIHHffB", AP_HAL::micros64(), last_reading.runtime, last_reading.seconds_until_maintenance, last_reading.errors, last_reading.rpm, last_reading.output_voltage, last_reading.output_current, last_reading.mode ); } // generator prearm checks; notably, if we never see a generator we do // not run the checks. Generators are attached/detached at will, and // reconfiguring is painful. bool AP_Generator_RichenPower::pre_arm_check(char *failmsg, uint8_t failmsg_len) const { if (uart == nullptr) { // not configured in serial manager return true; } if (last_reading_ms == 0) { // allow optional use of generator return true; } const uint32_t now = AP_HAL::millis(); if (now - last_reading_ms > 2000) { // we expect @1Hz hal.util->snprintf(failmsg, failmsg_len, "no messages in %ums", unsigned(now - last_reading_ms)); return false; } if (SRV_Channels::get_channel_for(SRV_Channel::k_generator_control) == nullptr) { hal.util->snprintf(failmsg, failmsg_len, "need a servo output channel"); return false; } uint16_t errors = last_reading.errors; // requiring maintenance isn't something that should stop // people flying - they have work to do. But we definitely // complain about it - a lot. errors &= ~(1U << uint16_t(Errors::MaintenanceRequired)); if (errors) { for (uint16_t i=0; i<16; i++) { if (errors & (1U << i)) { if (i < (uint16_t)Errors::LAST) { hal.util->snprintf(failmsg, failmsg_len, "error: %s", error_strings[i]); } else { hal.util->snprintf(failmsg, failmsg_len, "unknown error: 1U<<%u", i); } } } return false; } if (pilot_desired_runstate != RunState::RUN) { hal.util->snprintf(failmsg, failmsg_len, "requested state is not RUN"); return false; } if (commanded_runstate != RunState::RUN) { hal.util->snprintf(failmsg, failmsg_len, "Generator warming up (%.0f%%)", (heat *100 / heat_required_for_run())); return false; } if (last_reading.mode != Mode::RUN && last_reading.mode != Mode::CHARGE && last_reading.mode != Mode::BALANCE) { hal.util->snprintf(failmsg, failmsg_len, "not running"); return false; } return true; } // Update front end with voltage, current, and rpm values void AP_Generator_RichenPower::update_frontend_readings(void) { _voltage = last_reading.output_voltage; _current = last_reading.output_current; _rpm = last_reading.rpm; update_frontend(); } // healthy returns true if the generator is not present, or it is // present, providing telemetry and not indicating an errors. bool AP_Generator_RichenPower::healthy() const { const uint32_t now = AP_HAL::millis(); if (last_reading_ms == 0 || now - last_reading_ms > 2000) { return false; } if (last_reading.errors) { return false; } return true; } //send mavlink generator status void AP_Generator_RichenPower::send_generator_status(const GCS_MAVLINK &channel) { if (last_reading_ms == 0) { // nothing to report return; } uint64_t status = 0; if (last_reading.rpm == 0) { status |= MAV_GENERATOR_STATUS_FLAG_OFF; } else { switch (last_reading.mode) { case Mode::OFF: status |= MAV_GENERATOR_STATUS_FLAG_OFF; break; case Mode::IDLE: if (pilot_desired_runstate == RunState::RUN) { status |= MAV_GENERATOR_STATUS_FLAG_WARMING_UP; } else { status |= MAV_GENERATOR_STATUS_FLAG_IDLE; } break; case Mode::RUN: status |= MAV_GENERATOR_STATUS_FLAG_GENERATING; break; case Mode::CHARGE: status |= MAV_GENERATOR_STATUS_FLAG_GENERATING; status |= MAV_GENERATOR_STATUS_FLAG_CHARGING; break; case Mode::BALANCE: status |= MAV_GENERATOR_STATUS_FLAG_GENERATING; status |= MAV_GENERATOR_STATUS_FLAG_CHARGING; break; } } if (last_reading.errors & (uint8_t)Errors::Overload) { status |= MAV_GENERATOR_STATUS_FLAG_OVERCURRENT_FAULT; } if (last_reading.errors & (uint8_t)Errors::LowVoltageOutput) { status |= MAV_GENERATOR_STATUS_FLAG_REDUCED_POWER; } if (last_reading.errors & (uint8_t)Errors::MaintenanceRequired) { status |= MAV_GENERATOR_STATUS_FLAG_MAINTENANCE_REQUIRED; } if (last_reading.errors & (uint8_t)Errors::StartDisabled) { status |= MAV_GENERATOR_STATUS_FLAG_START_INHIBITED; } if (last_reading.errors & (uint8_t)Errors::LowBatteryVoltage) { status |= MAV_GENERATOR_STATUS_FLAG_BATTERY_UNDERVOLT_FAULT; } mavlink_msg_generator_status_send( channel.get_chan(), status, last_reading.rpm, // generator_speed std::numeric_limits::quiet_NaN(), // battery_current; current into/out of battery last_reading.output_current, // load_current; Current going to UAV std::numeric_limits::quiet_NaN(), // power_generated; the power being generated last_reading.output_voltage, // bus_voltage; Voltage of the bus seen at the generator INT16_MAX, // rectifier_temperature std::numeric_limits::quiet_NaN(), // bat_current_setpoint; The target battery current INT16_MAX, // generator temperature last_reading.runtime, (int32_t)last_reading.seconds_until_maintenance ); } // methods to control the generator state: bool AP_Generator_RichenPower::stop() { set_pilot_desired_runstate(RunState::STOP); return true; } bool AP_Generator_RichenPower::idle() { set_pilot_desired_runstate(RunState::IDLE); return true; } bool AP_Generator_RichenPower::run() { set_pilot_desired_runstate(RunState::RUN); return true; } #endif // AP_GENERATOR_RICHENPOWER_ENABLED