Goblin 700 RAW Nitro (Benzin)

L2-Max

Авторотирует RAW Nitro просто замечательно. Низкий вес и большАя площадь ротора позволяют ротировать на более низких оборотах в сравнении с Thunder плюс меньшие потери в трансмиссии позволяют это делать дольше 😃

Сегодня время в авторотации приближалось к минуте. Конечно же это субъективно.

Мне больше нравится эффект производимый на публику, а именно, когда я летал электро так никто и не замечал когда я мотор выключал в 100 метрах над землей, а сейчас “заглохший” бензиныч замечают все.

L2-Max

Авторотации на RAW Nitro.

Холостой ход в режиме холд установлен на 4000 оборотов. Его удержанием занимается PID регулятор, a скрипт испоняется самим передатчиком. Частота опроса сенсора оборотов 5Hz, частота коррекции 2.5Hz. Скорость реакции регулятора не большая, но я это учел при наладке и сейчас система работает без овершотов и автоколебаний. При холодном старте, когда правильная позиция дросселя неизвестна система стабилизируется за 5-7 секунд, а уже при переключении между полетным режимом и холд за 4 секунды.

Код отлаженого регулятора.

local input = { { "Idle", SOURCE } }

local output = { "idlv", "rpmt", "rpm", "errp", "erri", "errd" }

local id_temp = getFieldInfo( "engT" ).id
local id_rpm = getFieldInfo( "RPM" ).id
local id_gvar1 = getFieldInfo( "gvar1" ).id
local id_gvar2 = getFieldInfo( "gvar2" ).id
local id_gvar3 = getFieldInfo( "gvar3" ).id
local id_gvar4 = getFieldInfo( "gvar4" ).id
local id_ls1 = getFieldInfo( "ls1" ).id

local idle_v = 0

local err_p = 0
local err_p_last = 0
local err_i = 0
local err_d = 0

local rpm_ctrl_min = 3000
local rpm_ctrl_range = 4000

local temp_min = 10
local temp_max = 75
local temp_range = ( temp_max - temp_min )

local temp_rpm_offset = 1750

local idle_source_weight = .2
local idle_source_setpoint = 0

local time_ctrl_next = 0
local time_ctrl_ramp_down = 0

local rpm = 0
local rpm_target = 0

local rpm_deadband = 2.

local mode_Off = 0
local mode_Idle = 1
local mode_Run = 2
local mode_RampDown = 3

local mode_ctrl = mode_Off

local function init_func()
   time_ctrl_next = getTime()
end

local function run_func( idle_source )

   if getTime() > time_ctrl_next then
      time_ctrl_next = time_ctrl_next + 40

      rpm = getValue( id_rpm )
      rpm_target = getValue( id_gvar1 ) * 10

      local eng_T = getValue( id_temp )
      local ls1 = getValue( id_ls1 )

      if eng_T < temp_min then
         rpm_target = rpm_target + temp_rpm_offset
      elseif eng_T < temp_max then
         rpm_target = rpm_target + ( temp_rpm_offset / ( temp_range ) * ( temp_max - eng_T ) )
      end

      if rpm < rpm_ctrl_min then
         mode_ctrl = mode_Off
      end

      if mode_ctrl == mode_Off then

         idle_source_setpoint = idle_source * idle_source_weight - ( 1024 * ( 1. - idle_source_weight ) )
         idle_v = idle_source_setpoint

         if rpm > rpm_ctrl_min and ls1 > 0 then
            err_i = 0
            mode_ctrl = mode_Idle
         end

      elseif mode_ctrl == mode_Idle then

         err_p =  ( rpm_target - rpm ) / ( rpm_ctrl_range  / idle_source_weight ) * 1024
         err_i = err_i + err_p

         if err_p < 10.24 * rpm_deadband and err_p > -10.24 * rpm_deadband then
            err_p = 0
         elseif err_p > 10.24 * rpm_deadband then
            err_p = err_p - 10.24 * rpm_deadband
         elseif err_p < -10.24 * rpm_deadband then
            err_p = err_p + 10.24 * rpm_deadband
         end

         err_d = err_p - err_p_last
         err_p_last = err_p

         if err_d < 10.24 * rpm_deadband and err_d > -10.24 * rpm_deadband then
            --err_d = 0
         end

         idle_v = idle_source_setpoint + err_p * ( getValue( id_gvar2 ) / 100 ) +
                                         err_i * ( getValue( id_gvar3 ) / 100 ) +
                                         err_d * ( getValue( id_gvar4 ) / 100 )

         if ls1 < 0 then
            idle_v = idle_v + 10.24
            idle_source_setpoint = idle_v

            err_i = 0 ---err_i
            err_p_last = 0

            mode_ctrl = mode_Run
         end

         if idle_v > ( -1024 * ( 1. - idle_source_weight ) ) then
            idle_v = ( -1024 * ( 1. - idle_source_weight ) )
         elseif idle_v < -1024 then
            idle_v = -1024
         end

      elseif mode_ctrl == mode_Run then

         if ls1 > 0 then
            time_ctrl_ramp_down = getTime() + 400
            mode_ctrl = mode_RampDown
         end

      elseif mode_ctrl == mode_RampDown then

         if ls1 > 0 then
            if time_ctrl_ramp_down < getTime() then
               mode_ctrl = mode_Idle
            end
         else
            mode_ctrl = mode_Run
         end

      end

   end

   return idle_v, rpm_target / 10 * 1024 * .01, rpm / 10 * 1024 * .01, err_p, err_i, err_d
end

return { input=input, output=output, run=run_func, init=init_func }
Anike

На Raw Nitro готовят ретрофит хвостового ремня.

Unfortunately, we are experiencing some “TAIL BELT” breaks on the RAW 700 NITRO model. [SG746]
The problem is not related to the project but is due to a defect in the belt.
We immediately contacted the supplier to have a new batch for these belts as soon as possible. The times are about 2 weeks
At the end of the month we will ship a retrofit KIT for the necessary replacement
Not all belts presents the problem but as we do not know which ones are right and which are not, for safety reasons it is recommended to wait for the new belt;
More details on how to get the replacement next week.
We apologize for this inconvenience.

Solo

Достойное поведение от САБ … правда немного запоздалое …
Но как говорится “лучше поздно чем никогда” 😉

Anike

В догонку. Отправлять будет сам САБ всем, кто напишет в поддержку. Требуется серийный номер и доказательство заказа.

RAW NITRO SG746 update

The new belts were shipped from the supplier to our factory in Vietnam.
As soon as they are delivered; we will pack the retrofit kits including the new belt and the M3 bolt for the tail bell crank.
To all customers who have already emailed support@goblin-helicoper we will ship with priority.
For those who have not done so, please send an email with your kit’s serial number and proof of purchase.

Thank you

L2-Max

Не понимаю зачем SAB просит регистрировать купленные модели у них на сайте, если он владельцам конкретной зарегистрированной модели не сообщает о признанном ими дефекте ремня и что его будет менять, а я узнаю об этом на русском форуме.

L2-Max

На новом моторе стремительно приближаюсь к отметке 15 литров. Уже нетерпится разобрать мотор и проинспектировать стопоры. Оригинальные запчасти от RCJapan на днях приехали и на данный момент у меня 6 пар запасных стопоров 😃

Еще два поршня с кольцами и новый лейнер лежат про запас, а старый мотор все еще в гарантийном ремонте.

Посмотрим. Если на стопорах будет виден износ, то буду их менять заблаговременно. Ну а если нет, тогда надо будет искать причину их вылета в другом месте.

P.S. С RCJapan мне сейчас получается дешевле покупать чем в местных магазинах даже с учетом доставки. Не знаю как они это делают, но таможня не задержала ни одну из двух посылок.

L2-Max

Есть новости по мотору, но не совсем приятные. Моя теория, что стопор стачивается о палец, теряет прочность и выпрыгивает не подтвердилась. Сегодня я разобрал мотор и ни на пальце ни на стопорах следов контакта как таковых нет, а обнаружилось следующее.

Если снизу мотора через окно я палец достал с умеренным усилием, то когда поршень был на столе и я достал второй стопор я увидел небольшую развальцовку канавки стопора с внешней стороны. Соответственно палец через поршень на вылет не проходит, а упирается в ту ступеньку, что свидетельствует о повреждении самого посадочного места стопора. 6мм разверткой я геометрию отверстия подправил, палец стал проходить, но как понимаете это не решение основной проблемы.

Развальцованное посадочное.

Более менее нормальное.

Развальцовка наблюдается в обоих посадочных местах стопора, но в этот раз сильнее в верхнем.

Заметная глазом разница в размерах стопоров. Верхний это тот, который я достал из развальцованного посадочного, а снизу из более менее нормального.

А вот то что у стопора в нижнем посадочном приподнято правое ухо я заметил уже когда просматривал фото. Такое впечатление будто под ним какой то мусор или наплыв материала самого поршня.

Не знаю пока что с этим делать и почему так происходит. Менять каждые 16 литров поршень - это так себе вариант. Завтра подправлю посадочные и поставлю новые стопоры ушами к днищу поршня где посадочные еще не разбитые, а после 10 литров снова проконтролирую состояние посадочных мест.

Ниже фото разобранной поршневой после спаленых 16+ литров бензина. Перед тем как делать фото ничего не мыл. Если вкратце, то состояние очень хорошее. Износ минимален.

Поршень и палец.


Лейнер. По всей поверхности присутствует следы хонинговки, но и встречаются незначительные потертости в основном вблизи камеры сгорания.

Остальное, что попало в кадр при разборке.


scgorodok
L2-Max:

состояние очень хорошее

ничего хорошего не увидел )

Solo

Мне то же все не сильно нравитЦо … 😦
Ты считаешь что такой нагар на поршне приемлемый?
Кольцо не должно притереться до зеркала?
Палец стремно выглядит … почему такой темный в местах контакта с поршнем? Смазки не хватает?

L2-Max

Когда делал фото, то акцент делал именно на износе трущихся поверхностей, а не на том чтоб фото получились красивые и всем нравились. Детали маленькие с большой кривизной потому у меня не получилось сделать фото так, чтоб например поршень по всей поверхности не имел отражений окружающих предметов, а это действительно воспринимается как потертости. Потому пробовал фокусиротся в центр и делать фото с нескольких сторон.

Ну а нагар это такое, он ведь постоянно и бесконечно не накапливается там, а вырастает до определенной толщины слоя и постоянно обновляется. Мне главное, что бы этот нагар не присутствовал на юбке. Примерно такое же количество нагара присутствовало на всех 4х тактниках которые я разбирал, будь то инжектор или карбюратор, а там масло в бензин не добавляется.

Вот фото отмытого поршня при дневном освещении.

Чтобы понять насколько изношена юбка нужно присмотреться к следу от резца который присутствовал на новом поршне. След все еще присутствует по всей площади юбки. При царапании юбки ногтем слышно характерный звук как от мелкой гребенки. О чем это говорит? О том что износ юбки поршня не превышает глубину канавки от резца после точения.

Вот новый поршень для сравнения.

Кольцо похоже все еще на 100% не приработано. При увеличении, на поверхности видна та шероховатость которая присутствовала на новом. Это к слову об эффективности обкатки на холостых. Если зеркала на кольце нет после 16+ литров в воздухе, то в чем смысл обкатывать бак на холостых.

Палец вместе контакта с поршнем похоже попросту не двигался достаточно, что бы оксидная пленка истиралась. Не знаю. Меня больше интересует откуда там синий цвет побежалости. Неужели палец до 500 градусов разогревался

Slava911

Замок у кольца симметричный. Надеюсь кольцо отметили как стояло, если перевернуть на 180 градусов то притирка начнется заново.

Палец в месте контакта с поршнем и не должен двигаться. Синий цвет это означает перегрев (бедная смесь, недостаточное охлаждение и т.п.).

Из практики: стопорные кольца пальца вылетают - неисправные подшипники коленвала (передний или коренной без разницы), в добавок разбивает нижнюю головку шатуна.

L2-Max

С кольцом момент я учел. Его не пришлось помечать, потому что нижняя сторона притерта к поршню и блестит, а верхняя черная и матовая.

Zigzag
Slava911:

Из практики: стопорные кольца пальца вылетают - неисправные подшипники коленвала (передний или коренной без разницы), в добавок разбивает нижнюю головку шатуна.

Поршень при этом долбит дном головку? Еще учтите возможность детонации, тоже замечательная штука.

L2-Max

Я немного ошибся с определением температуры по цвету побежалости. Синий это ~300 градусов, а не 500, что вполне ожидаемо учитывая сколько времени мотор работал на максимальной мощности. Такая температура хоть и кажется высокой, но не критична для поршневой.

Solo:

Палец стремно выглядит … почему такой темный в местах контакта с поршнем?

Получается что это тоже цвет побежалости - фиолетово/коричневый, а это ~270 градусов

В общем, посадочные я поправил и поставил новые стопоры. 2 бака успел отлетать. Решение именно менять стопоры принял потому, что старые уменьшились в диаметре на 0.5мм в свободном состоянии в сравнении с новыми и я подумал, что может в этом причина их вылета. Так же один старый слегка изменил свою форму.

Новый 7.1mm.

Старый 6.6mm.

Потом, на одном из стопоров я все таки обнаружил следы трения о палец, так что мое первое предположение, что стопоры истираются о палец, имеет место быть и его надо будет так же проверить при следующей разборке мотора.

L2-Max

Получается, что нужны именно для смазки. Палец свободно проворачивается как в шатуне так и в поршне.

Solo

Получается, что за такт работы он при поворачивается в поршне и “пилит” торцом “ушки” стопоров … 😦
Единственный простой вариант - полирнуть торцы пальца 😃

scgorodok

я полировал, чё потом стало не знаю) продал
раньше были другие стопра, плоские

L2-Max

Упал при выполнении авторотации. Немного не рассчитал точку приземления. На трех метрах щелкнул отмену, но было уже поздно. Обороты были слишком малы и мотор неуспел раскрутиться. Упал в траву на ноги. При дальнейшей попытке взлететь верт начал заваливаться на правый бок и на циклик почему то не реагировал. Холд и верт уже на боку.

И снова технический отказ. Вот в таком положении была серва элеватора после падения.

Я поначалу думал, что это при падении срезало винт, но присмотревшись обнаружил вот такие потертости как на суппорте так и на кронштейне серво.

По следам видно, что кронштейн был приподнят и в таком состоянии находился достаточно долго. Тоесть винт срезало не сегодня и не вчера. Однако, почему циклик был неуправляем по элеронам, а не по элеватору для меня загадка.

Остатки винта я высверлил и установил винт на 2мм длиннее, потому что покоцал начало резьбы, а когда начал монтировать суппорт на редуктор, то внем сорвало одну из двух М3 резьб, блин. Вроде бы и тянул без фанатизма. Короче, и там я поставил винт на 2мм длиннее (отверстие там сквозное, можно упереться болтом в зубчатое колесо!) и залил фиксатором.

Думаю или купить новый корпус редуктора или перенарежу под М4 резьбу. Пока буду так летать.

P.S. На редукторе, под суппортом, с той стороны где сорвало резьбу тоже было видно следы потертости от вибрации, но я не придал этому значение. Возможно вибрациями резьбу ослабило, но в том месте фиксатора было налито щедро. Винты очень туго откручивались.

L2-Max
Solo:

Единственный простой вариант - полирнуть торцы пальца

scgorodok:

я полировал, чё потом стало не знаю)

Думал об этом, но не полировал, так как потертости обнаружил уже после сборки мотора.

Еще момент, истирание ушек свидетельствует о не симметричности стопорного в плоскости. Я это проверил на новых комплектах и таки да. Ушки слегка загнуты в сторону. При сборке я интуитивно устанавливал стопоры так, что бы ушки смотрели наружу, что бы контакт стопора с пальцем происходил по всему периметру. Хотя это врятли решает проблему развальцовки посадочного места стопора.

Увидим какое состояние посадочных будет после следующих 10-15 литров.

А параллельно остальным проблемам я еще улучшил регулятор холостого хода. На преходных режимах реагирует быстрее и минимум осцилляций. Плюс добавил логи коэффициентов регулятора и частотный фильтр производной.

local input = { { "thri", SOURCE } }

local output = { "thro", "rpmt", "rpm", "errp", "erri", "errd" }

local id_temp = getFieldInfo( "engT" ).id
local id_rpm = getFieldInfo( "RPM" ).id
local id_gvar1 = getFieldInfo( "gvar1" ).id
local id_gvar2 = getFieldInfo( "gvar2" ).id
local id_gvar3 = getFieldInfo( "gvar3" ).id
local id_gvar4 = getFieldInfo( "gvar4" ).id
local id_ls1 = getFieldInfo( "ls1" ).id
local id_s2 = getFieldInfo( "s2" ).id
local id_sa = getFieldInfo( "sa" ).id
local id_sf = getFieldInfo( "sf" ).id

-------- PID State ----------
local throttle_out = 0

local err_last_input = 0
local err_last_error = 0

local err_p = 0
local err_i = 0
local err_d = 0
local err_d_tau = 0.6

local err_i_max = 20 * 10.24

-----------------------------
local err_input_max = 2000

local rpm_ctrl_min = 3000
local rpm_ctrl_range = 2000

local temp_min = 10
local temp_max = 75
local temp_range = ( temp_max - temp_min )

local temp_rpm_offset = 2000

local thr_source_weight = 0
local thr_source_setpoint = 0
local thr_source_setpoint_ref = 0
local thr_source_min = -1024 + 10.24 * 0

local time_ctrl_next = 0
local time_ctrl_ramp_down = 0

local time_ctrl_sim_next = 0

local rpm = 0
local rpm_target = 0

local rpm_deadband = 300

local rpm_ctrl_setpoint_ref = 6000

local mode_Off = 0
local mode_Idle = 1
local mode_Run = 2
local mode_RampDown = 3

local mode_ctrl = mode_Off

local pid_sample_time_S = .25

local sim_sample_time_S = .1

local sim_rpm_store_filtered = rpm_ctrl_min
local sim_rpm_store_not_filtered = 0
local sim_rpm_store = rpm_ctrl_min
local sim_rpm_accel_max = 1000

local is_sim_enabled = 0

local elapsedTime = 0

local function sim_rpm( set_point )
   local ls1 = getValue( id_ls1 )

   if mode_ctrl == mode_Run then
      sim_rpm_store = 14000
   elseif mode_ctrl == mode_Idle and thr_source_weight > 0 then
      sim_rpm_store = sim_rpm_store + ( sim_rpm_accel_max * sim_sample_time_S ) * ( ( ( -0.5 - 0.32 * ( 1 - ( rpm_ctrl_setpoint_ref - rpm_target ) / 2000 )  ) + set_point / thr_source_weight ) )
   end

   local rpm_a = .1
   sim_rpm_store_not_filtered = sim_rpm_store + 2000 * ( getValue( id_s2 ) / 1024 ) + -200 + math.random( 400 )

   sim_rpm_store_filtered = sim_rpm_store_not_filtered * rpm_a + ( 1 - rpm_a ) * sim_rpm_store_filtered

   return sim_rpm_store_filtered
end

local function recalc_with_deadband( anError, aDeadBand )
   local ret = anError

   if ret <= aDeadBand and ret >= -aDeadBand then
      ret = 0
   elseif ret > aDeadBand then
      ret = ret - aDeadBand
   elseif ret < -aDeadBand then
      ret = ret + aDeadBand
   end

   return ret
end

local function recalc_with_limits( aValue, aMin, aMax )
   local ret = aValue

   if ret < aMin then
      ret = aMin
   elseif ret > aMax then
      ret = aMax
   end

   return ret
end

local function recalc_with_expo( aValue, anExpo )
   return anExpo * ( aValue ^ 3 ) + ( 1 - anExpo ) * aValue
end

local function rpm_ctrl_loop()
   local err_input = recalc_with_limits( ( rpm_target - rpm ), -err_input_max, err_input_max )

   if err_input >= -rpm_deadband and err_input < 0 then
      err_input = 0
   elseif err_input < -rpm_deadband then
     err_input = err_input + rpm_deadband
   end

   --err_input = recalc_with_deadband( err_input, rpm_deadband )

   err_p = err_input * ( getValue( id_gvar2 ) / 100 ) * thr_source_weight

   if throttle_out > thr_source_min and throttle_out < thr_source_setpoint then
      err_i = err_i + ( ( err_input + err_last_error ) * ( getValue( id_gvar3 ) / 100 * pid_sample_time_S ) * 0.5 * thr_source_weight )
      err_i = recalc_with_limits( err_i, -err_i_max, err_i_max )
   end

   err_last_error = err_input

   err_d = ( ( ( err_last_input - rpm )  * ( getValue( id_gvar4 ) / 100 ) * 2. ) + ( 2. * err_d_tau - pid_sample_time_S ) * err_d ) /
                                                                                   ( 2. * err_d_tau + pid_sample_time_S ) * thr_source_weight

   err_last_input = rpm

   return thr_source_setpoint_ref + err_p + err_i + err_d
end

local function set_setpoint_ref( aRef )
   thr_source_setpoint_ref = aRef
   thr_source_weight = ( ( 1024 + thr_source_setpoint ) / 2 ) / rpm_ctrl_range

   err_i = 0
end

local function set_rpm_target( aRpm )
   --err_last_input = err_last_input + ( aRpm - rpm_target )

   rpm_target = aRpm
end

local g_log_file = nil

local function init_func()
   time_ctrl_next = getTime()
   time_ctrl_sim_next = getTime()
end

local function run_func( thr_source )
   local theNow = getTime()

   if theNow > time_ctrl_next then
      time_ctrl_next = time_ctrl_next + 100 * pid_sample_time_S

      elapsedTime = elapsedTime + 1000 * pid_sample_time_S

      if is_sim_enabled == 0 then
         rpm = getValue( id_rpm )
      end

      local ls1 = getValue( id_ls1 )

      if rpm < rpm_ctrl_min or getValue( id_sa ) > -512 or getValue( id_sf ) > 0 then
         err_p = 0
         err_i = 0
         err_d = 0

         mode_ctrl = mode_Off
      end

      if mode_ctrl == mode_Off then
         thr_source_setpoint = thr_source

         set_setpoint_ref( thr_source )
         set_rpm_target( rpm_ctrl_setpoint_ref )

         throttle_out = thr_source_setpoint

         if rpm > rpm_ctrl_min and ls1 > 0 then
            err_i = 0
            err_last_input = rpm

            mode_ctrl = mode_Idle
         end

      elseif mode_ctrl == mode_Idle then
         if ls1 < 0 then
            throttle_out = throttle_out + 10.24 * 1
            err_i = err_i + 10.24 * 1

            mode_ctrl = mode_Run
         else
            local theTargetRpm = getValue( id_gvar1 ) * 10

            local eng_T = 0

            if is_sim_enabled == 0 then
               eng_T = getValue( id_temp )
            else
               eng_T = 30
            end

            if eng_T < temp_min then
               theTargetRpm = theTargetRpm + temp_rpm_offset
            elseif eng_T < temp_max then
               theTargetRpm = theTargetRpm + ( temp_rpm_offset / ( temp_range ) * ( temp_max - eng_T ) )
            end

            if theTargetRpm < rpm_target - 200 * pid_sample_time_S then
               set_rpm_target( rpm_target - 200 * pid_sample_time_S )
            else
               set_rpm_target( theTargetRpm )
            end

            throttle_out = rpm_ctrl_loop()
            throttle_out = recalc_with_limits( throttle_out, thr_source_min, thr_source_setpoint )
         end

      elseif mode_ctrl == mode_Run then

         if ls1 > 0 then
            time_ctrl_ramp_down = theNow + 300
            mode_ctrl = mode_RampDown
         end

      elseif mode_ctrl == mode_RampDown then
         if ls1 > 0 then
            if time_ctrl_ramp_down < theNow then
               err_last_input = rpm

               if rpm > rpm_ctrl_setpoint_ref then
                  set_rpm_target( rpm )
               else
                  set_rpm_target( rpm_ctrl_setpoint_ref )
               end

               mode_ctrl = mode_Idle
            end
         else
            mode_ctrl = mode_Run
         end
      end

      ----------------------------------- Logging ---------------------------------------

      if g_log_file ~= nil then
         local datenow = getDateTime()

         io.write( g_log_file, datenow.year.."-"..string.format("%02d",datenow.mon).."-"..string.format("%02d",datenow.day)..","
                               ..string.format("%02d", ( elapsedTime / ( 60000 * 24 ) ) % 24 )..":"..string.format("%02d", ( elapsedTime / 60000 ) % 60 )..":"..string.format("%02d", ( elapsedTime / 1000 ) % 60 ).."."
                               ..string.format("%03d",( elapsedTime % 1000 ) )..",",
                               string.format("%.1f", ( 1024 + throttle_out ) / 10.24 ), ",", string.format("%.2f", rpm_target ), ",", string.format("%.2f", rpm ), ",",
                               string.format("%.2f", sim_rpm_store_filtered ), ",", string.format("%.2f", sim_rpm_store_not_filtered ), ",",
                               string.format("%.1f", err_p / 10.24 ), ",", string.format("%.1f", err_i / 10.24 ), ",", string.format("%.1f", err_d / 10.24 ), "\n" )
      end

      ------------------------------------------------------------------------------------

   end

   -------------------------------------- Simulation -------------------------------------

   if is_sim_enabled == 1 and getTime() > time_ctrl_sim_next then
      time_ctrl_sim_next = time_ctrl_sim_next + 100 * sim_sample_time_S

      if getValue( id_sf ) < 0 then
         if g_log_file == nil then
            g_log_file = assert( io.open( "/LOGS/_debug.csv", "wb" ), "Cannot open file" )
            io.write( g_log_file, "Date,Time,throttle_out,rpm_target,rpm,rpm_filtered,rpm_not_filtered,err_p,err_i,err_d\n" )
         end

         rpm = sim_rpm( ( 1024 + throttle_out ) / 10.24 / 200 )
      else
         if g_log_file ~= nil then
            io.close( g_log_file )
            g_log_file = nil
         end

         rpm = 0
      end
   end

   ----------------------------------------------------------------------------------------

   return throttle_out, rpm_target / 10 * 1024 * .01, rpm / 10 * 1024 * .01, err_p, err_i, err_d
end

return { input=input, output=output, run=run_func, init=init_func }