Servo manipulation via Firmata *should* be working.

This commit is contained in:
James Harton 2020-05-20 22:06:27 +12:00
parent d0b9d84481
commit 90e2cc6228
12 changed files with 513 additions and 86 deletions

View file

@ -4,10 +4,9 @@
enum FirmataCommands
{
TELEMETRY = 0x01,
SERVO_SET_ENABLE_PORT,
SERVO_SET_ENABLE_STARBOARD,
SERVO_GET_ENABLE_PORT,
SERVO_GET_ENABLE_STARBOARD
SERVO_DRIVER_ENABLED,
SERVO_CHANNEL_ENABLED,
SERVO_CHANNEL_POSITION,
};
enum FirmataTelemetryType

View file

@ -54,34 +54,146 @@ PowerTask powerTask(sensorMutex, firmataTask);
ServoDriverTask portServos(SERVO_ENABLE_PORT_PIN, SERVO_ADDRESS_PORT, servoMutex);
ServoDriverTask starboardServos(SERVO_ENABLE_STARBOARD_PIN, SERVO_ADDRESS_STARBOARD, servoMutex);
// bytes:
// set: channel, sign, value lsb, value msb
// get: channel, _
void servoChannelPosition(byte bytec, byte *payload)
{
if (bytec == 0)
return;
ServoDriverTask *servoTask;
uint8_t channel = payload[0];
uint8_t driverChannel = channel;
if (channel < 16)
servoTask = &portServos;
else if (channel < 32)
{
servoTask = &starboardServos;
driverChannel -= 16;
}
else
return;
if (bytec == 4)
{
// Set position
int16_t position = payload[2] + (payload[3] << 7);
if (payload[1] == 1)
position = 0 - position;
servoTask->setPosition(driverChannel, position);
// Just echo the new position back to the caller because the servo hasn't
// actually been moved yet.
firmataTask.sendCommand(FirmataCommands::SERVO_CHANNEL_POSITION, 4, payload);
}
else
{
// Get position
int16_t position = servoTask->getPosition(driverChannel);
uint16_t abs_position = abs(position);
byte lsb = abs_position & 0x7f;
byte msb = abs_position >> 7;
byte sign = 0;
if (position < 0)
sign = 1;
byte response[4];
response[0] = channel + (sign << 8);
response[1] = sign;
response[2] = lsb;
response[3] = msb;
firmataTask.sendCommand(FirmataCommands::SERVO_CHANNEL_POSITION, 4, (byte *)&response);
}
}
// bytes:
// set: channel, enabled bool
// get: channel
void servoChannelEnabled(byte bytec, byte *payload)
{
if (bytec == 0)
return;
ServoDriverTask *servoTask;
uint8_t channel = payload[0];
uint8_t driverChannel = channel;
if (channel < 16)
servoTask = &portServos;
else if (channel < 32)
{
servoTask = &starboardServos;
driverChannel -= 16;
}
else
return;
if (bytec == 2)
{
// Set Enabled/Disabled
servoTask->enableChannel(driverChannel, (bool)payload[1]);
}
// Get enabled/disabled.
byte response[2];
response[0] = channel;
response[1] = (byte)servoTask->isEnabled(driverChannel);
firmataTask.sendCommand(FirmataCommands::SERVO_CHANNEL_ENABLED, 2, (byte *)&response);
}
// bytes:
// set: driver, _, bool, _
// get: driver, _
void servoDriverEnabled(byte bytec, byte *payload)
{
if (bytec == 0)
return;
ServoDriverTask *servoTask;
if (payload[0] == 0)
{
servoTask = &portServos;
}
else
{
servoTask = &starboardServos;
}
if (bytec == 2)
{
// Setting the value
servoTask->enable((bool)payload[1]);
}
// Getting the value
byte response[2];
response[0] = payload[0] & 0x1;
response[1] = (byte)servoTask->isEnabled();
firmataTask.sendCommand(FirmataCommands::SERVO_DRIVER_ENABLED, 2, (byte *)&response);
}
void sysexCallback(byte command, byte bytec, byte *payload)
{
byte responseBuffer[1];
switch (command)
{
case FirmataCommands::SERVO_SET_ENABLE_PORT:
if (bytec > 0 && payload[0] > 0)
portServos.enable(true);
else
portServos.enable(false);
break;
case FirmataCommands::SERVO_DRIVER_ENABLED:
return servoDriverEnabled(bytec, payload);
case FirmataCommands::SERVO_SET_ENABLE_STARBOARD:
if (bytec > 0 && payload[0] > 0)
starboardServos.enable(true);
else
starboardServos.enable(false);
break;
case FirmataCommands::SERVO_CHANNEL_ENABLED:
return servoChannelEnabled(bytec, payload);
case FirmataCommands::SERVO_GET_ENABLE_PORT:
responseBuffer[0] = (byte)portServos.isEnabled();
Firmata.sendSysex(FirmataCommands::SERVO_GET_ENABLE_PORT, 1, (byte *)&responseBuffer);
break;
case FirmataCommands::SERVO_CHANNEL_POSITION:
return servoChannelPosition(bytec, payload);
case FirmataCommands::SERVO_GET_ENABLE_STARBOARD:
responseBuffer[0] = (byte)starboardServos.isEnabled();
Firmata.sendSysex(FirmataCommands::SERVO_GET_ENABLE_STARBOARD, 1, (byte *)&responseBuffer);
default:
break;
}
}

View file

@ -61,24 +61,24 @@ void FirmataTask::sendTelemetry(FirmataTelemetryType telemetryType, byte byteCou
}
}
// Write a sysex command with unencoded payload.
void FirmataTask::sendCommand(byte command, byte byteCount, byte *data)
{
byte encodedSize = 0;
byte *encodedData = new byte[byteCount * 2];
FIRMATA_PORT.write(START_SYSEX);
FIRMATA_PORT.write(command);
for (int i = 0; i < byteCount; i++)
{
byte value = data[i];
encodedData[encodedSize++] = value & 0x7f;
encodedData[encodedSize++] = (value >> 7) & 0x7f;
FIRMATA_PORT.write(data[i]);
}
TickType_t delay = Ticks::fromMillis(100L);
if (xSemaphoreTake(this->mutex, delay) == pdTRUE)
{
Firmata.sendSysex(command, encodedSize, encodedData);
xSemaphoreGive(this->mutex);
}
FIRMATA_PORT.write(END_SYSEX);
}
// Write a sysex command with encoded payload.
void FirmataTask::sendCommandEncoded(byte command, byte byteCount, byte *data)
{
Firmata.sendSysex(command, byteCount, data);
}
void FirmataTask::init()

View file

@ -14,6 +14,7 @@ public:
void run();
void sendCommand(byte command, byte byteCount, byte *data);
void sendCommandEncoded(byte command, byte byteCount, byte *data);
void sendTelemetry(FirmataTelemetryType telemetryType, byte byteCount, byte *data);
static void startTask(void *arg);

View file

@ -1,24 +1,38 @@
#include "servo_driver.hpp"
#include "ticks.hpp"
uint16_t degreesToPwm(int16_t degrees)
{
return map(degrees, SERVO_DEGMIN, SERVO_DEGMAX, SERVO_MIN, SERVO_MAX);
}
ServoDriverTask::ServoDriverTask(const uint8_t enablePin, const uint8_t busAddress, SemaphoreHandle_t &busMutex) : enablePin(enablePin), busMutex(busMutex)
{
this->pwmDriver = Adafruit_PWMServoDriver(busAddress, SERVO_I2C_BUS);
pinMode(this->enablePin, OUTPUT);
digitalWrite(this->enablePin, SERVO_DRIVER_DISABLED);
digitalWrite(this->enablePin, SERVO_DRIVER_PIN_DISABLED);
this->enablePinState = false;
for (int i = 0; i < 16; i++)
{
this->lastPosition[i] = 0;
this->nextPosition[i] = 0;
this->channelEnabled[i] = false;
this->lastPwm[i] = 0;
this->nextPwm[i] = 0;
}
}
void ServoDriverTask::enable(bool enable)
{
if (enable)
{
digitalWrite(this->enablePin, SERVO_DRIVER_ENABLED);
digitalWrite(this->enablePin, SERVO_DRIVER_PIN_ENABLED);
this->enablePinState = true;
}
else
{
digitalWrite(this->enablePin, SERVO_DRIVER_DISABLED);
digitalWrite(this->enablePin, SERVO_DRIVER_PIN_DISABLED);
this->enablePinState = false;
}
}
@ -28,6 +42,42 @@ bool ServoDriverTask::isEnabled()
return this->enablePinState;
}
bool ServoDriverTask::isEnabled(byte channel)
{
return this->channelEnabled[channel];
}
void ServoDriverTask::enableChannel(byte channel, bool enable)
{
if (channel >= 0 and channel < 16)
{
this->channelEnabled[channel] = enable;
if (enable)
this->nextPwm[channel] = degreesToPwm(this->nextPosition[channel]);
else
this->nextPwm[channel] = 0;
}
}
void ServoDriverTask::setPosition(byte channel, int16_t position)
{
if (channel >= 0 and channel < 16)
{
this->nextPosition[channel] = position;
if (this->channelEnabled[channel])
this->nextPwm[channel] = degreesToPwm(position);
}
}
int16_t ServoDriverTask::getPosition(byte channel)
{
if (channel >= 0 and channel < 16)
{
return this->lastPosition[channel];
}
return 0;
}
void ServoDriverTask::init()
{
TickType_t delay = Ticks::fromMillis(100L);
@ -47,10 +97,23 @@ void ServoDriverTask::init()
void ServoDriverTask::run()
{
TickType_t delay = Ticks::fromMillis(100L);
TickType_t delay = Ticks::fromHz((long)SERVO_FREQ);
while (1)
{
if (xSemaphoreTake(this->busMutex, 10L) == pdTRUE)
{
for (int i = 0; i < 16; i++)
{
if (this->nextPwm[i] != this->lastPwm[i])
{
this->pwmDriver.setPWM(i, this->nextPwm[i], 0);
this->lastPwm[i] = this->nextPwm[i];
}
this->lastPosition[i] = this->nextPosition[i];
}
xSemaphoreGive(this->busMutex);
}
vTaskDelay(delay);
}
}

View file

@ -17,6 +17,10 @@
#define SERVO_MAX 600
#endif
#ifndef SERVO_DEFAULT_POSITION
#define SERVO_DEFAULT_POSITION (SERVO_MAX - SERVO_MIN) / 2
#endif
#ifndef SERVO_USMIN
#define SERVO_USMIN 600
#endif
@ -25,8 +29,16 @@
#define SERVO_USMAX 2400
#endif
#define SERVO_DRIVER_ENABLED LOW
#define SERVO_DRIVER_DISABLED HIGH
#ifndef SERVO_DEGMIN
#define SERVO_DEGMIN -90
#endif
#ifndef SERVO_DEGMAX
#define SERVO_DEGMAX 90
#endif
#define SERVO_DRIVER_PIN_ENABLED LOW
#define SERVO_DRIVER_PIN_DISABLED HIGH
#include <FreeRTOS_TEENSY4.h>
#include <Arduino.h>
@ -45,12 +57,21 @@ public:
void enable(bool enable);
bool isEnabled();
bool isEnabled(byte channel);
void enableChannel(byte channel, bool enable);
void setPosition(byte channel, int16_t position);
int16_t getPosition(byte channel);
private:
uint8_t enablePin;
SemaphoreHandle_t &busMutex;
Adafruit_PWMServoDriver pwmDriver;
bool enablePinState;
int16_t lastPosition[16];
int16_t nextPosition[16];
bool channelEnabled[16];
uint16_t lastPwm[16];
uint16_t nextPwm[16];
};
#endif // __SERVO_DRIVER_TASK_HPP__

View file

@ -6,5 +6,6 @@
@import "~foundation-sites/scss/foundation";
@include foundation-everything();
@include foundation-meter-element;
@include foundation-range-input;
@import "sparkline.scss";
@import "../node_modules/nprogress/nprogress.css";

View file

@ -2,14 +2,66 @@ defmodule Augie.Firmata.Commands do
alias Augie.Firmata
alias MidiProto.Message
alias MidiProto.Message.SystemExclusive
use Bitwise
def servo_set_enable(:port, true), do: send_sysex(2, <<1, 0>>)
def servo_set_enable(:port, false), do: send_sysex(2, <<0, 0>>)
def servo_set_enable(:starboard, true), do: send_sysex(3, <<1, 0>>)
def servo_set_enable(:starboard, false), do: send_sysex(3, <<0, 0>>)
@moduledoc """
Handles sending Firmata commands to the bot.
"""
def servo_get_enable(:port), do: send_sysex(4, <<>>)
def servo_get_enable(:starboard), do: send_sysex(5, <<>>)
@servo_min -135
@servo_max 135
defguardp is_channel(n) when is_integer(n) and n >= 0 and n <= 32
defguardp is_servo_angle(n) when is_integer(n) and n >= @servo_min and n <= @servo_max
def servo_driver_enabled?(:port), do: send_sysex(2, <<0>>)
def servo_driver_enabled?(:starboard), do: send_sysex(2, <<1>>)
def servo_driver_enabled(:port, true), do: send_sysex(2, <<0, 1>>)
def servo_driver_enabled(:port, false), do: send_sysex(2, <<0, 0>>)
def servo_driver_enabled(:starboard, true), do: send_sysex(2, <<1, 1>>)
def servo_driver_enabled(:starboard, false), do: send_sysex(2, <<1, 0>>)
def servo_channel_enabled?(:port, channel), do: servo_channel_enabled?(channel)
def servo_channel_enabled?(:starboard, channel), do: servo_channel_enabled?(channel + 16)
def servo_channel_enabled?(channel) when is_channel(channel),
do: send_sysex(3, <<channel>>)
def servo_channel_enabled(:port, channel, enabled), do: servo_channel_enabled(channel, enabled)
def servo_channel_enabled(:starboard, channel, enabled),
do: servo_channel_enabled(channel + 16, enabled)
def servo_channel_enabled(channel, true)
when is_channel(channel),
do: send_sysex(3, <<channel, 1>>)
def servo_channel_enabled(channel, false)
when is_channel(channel),
do: send_sysex(3, <<channel, 0>>)
def servo_channel_position?(:port, channel), do: servo_channel_position?(channel)
def servo_channel_position?(:starboard, channel), do: servo_channel_position?(channel + 16)
def servo_channel_position?(channel)
when is_channel(channel),
do: send_sysex(4, <<channel>>)
def servo_channel_position(:port, channel, position),
do: servo_channel_position(channel, position)
def servo_channel_position(:starboard, channel, position),
do: servo_channel_position(channel + 16, position)
def servo_channel_position(channel, position)
when is_channel(channel) and is_servo_angle(position) do
sign = if position < 0, do: 1, else: 0
position = abs(position)
lsb = position &&& 0x7F
msb = position >>> 7
send_sysex(4, <<channel, sign, lsb, msb>>)
end
defp send_sysex(command, payload) do
command

View file

@ -40,21 +40,52 @@ defmodule Augie.Firmata.Decoder do
end
end
def decode(%SystemExclusive{vendor_id: 2, payload: binary}) do
{:ok, {:command, :servo_set_enable_port}, binary}
end
def decode(%SystemExclusive{vendor_id: 2, payload: <<0, 1>>}),
do: {:ok, {:command, :servo_driver_enabled}, {:port, true}}
def decode(%SystemExclusive{vendor_id: 3, payload: binary}) do
{:ok, {:command, :servo_set_enable_starboard}, binary}
end
def decode(%SystemExclusive{vendor_id: 2, payload: <<1, 1>>}),
do: {:ok, {:command, :servo_driver_enabled}, {:starboard, true}}
def decode(%SystemExclusive{vendor_id: 4, payload: binary}) do
{:ok, {:command, :servo_get_enable_port}, binary}
end
def decode(%SystemExclusive{vendor_id: 2, payload: <<0, 0>>}),
do: {:ok, {:command, :servo_driver_enabled}, {:port, false}}
def decode(%SystemExclusive{vendor_id: 5, payload: binary}) do
{:ok, {:command, :servo_get_enable_starboard}, binary}
end
def decode(%SystemExclusive{vendor_id: 2, payload: <<1, 0>>}),
do: {:ok, {:command, :servo_driver_enabled}, {:starboard, false}}
def decode(%SystemExclusive{vendor_id: 3, payload: <<channel, 1>>})
when channel >= 0 and channel < 16,
do: {:ok, {:command, :servo_channel_enabled}, {:port, channel, true}}
def decode(%SystemExclusive{vendor_id: 3, payload: <<channel, 1>>})
when channel >= 16 and channel < 32,
do: {:ok, {:command, :servo_channel_enabled}, {:starboard, channel - 16, true}}
def decode(%SystemExclusive{vendor_id: 3, payload: <<channel, 0>>})
when channel >= 0 and channel < 16,
do: {:ok, {:command, :servo_channel_enabled}, {:port, channel, false}}
def decode(%SystemExclusive{vendor_id: 3, payload: <<channel, 0>>})
when channel >= 16 and channel < 32,
do: {:ok, {:command, :servo_channel_enabled}, {:starboard, channel - 16, false}}
def decode(%SystemExclusive{vendor_id: 4, payload: <<channel, 0, lsb, msb>>})
when channel >= 0 and channel < 16,
do: {:ok, {:command, :servo_channel_position}, {:port, channel, lsb + (msb <<< 7)}}
def decode(%SystemExclusive{vendor_id: 4, payload: <<channel, 1, lsb, msb>>})
when channel >= 0 and channel < 16,
do: {:ok, {:command, :servo_channel_position}, {:port, channel, 0 - (lsb + (msb <<< 7))}}
def decode(%SystemExclusive{vendor_id: 4, payload: <<channel, 0, lsb, msb>>})
when channel >= 16 and channel < 32,
do:
{:ok, {:command, :servo_channel_position}, {:starboard, channel - 16, lsb + (msb <<< 7)}}
def decode(%SystemExclusive{vendor_id: 4, payload: <<channel, 1, lsb, msb>>})
when channel >= 16 and channel < 32,
do:
{:ok, {:command, :servo_channel_position},
{:starboard, channel - 16, 0 - (lsb + (msb <<< 7))}}
def decode(_), do: {:error, "Unknown message"}

View file

@ -29,13 +29,13 @@
<%= live_render(@socket, AugieWeb.OrientationLive, id: :orientation) %>
<%= live_render(@socket, AugieWeb.TemperatureLive, id: :temperature) %>
<%= live_render(@socket, AugieWeb.PowerLive, id: :power) %>
<%= live_render(@socket, AugieWeb.ServoLive, id: :port, session: %{"driver" => "port"})%>
<%= live_render(@socket, AugieWeb.ServoLive, id: :starboard, session: %{"driver" => "starboard"})%>
</div>
<div class="cell small-12 medium-6 large-4">
<%= live_render(@socket, AugieWeb.GpsLive, id: :gps) %>
</div>
<div class="cell small-12 medium-6 large-4">
<%= live_render(@socket, AugieWeb.CameraLive, id: :camera) %>
<%= live_render(@socket, AugieWeb.ServoLive, id: :port, session: %{"driver" => "port"}) %>
<%= live_render(@socket, AugieWeb.ServoLive, id: :starboard, session: %{"driver" => "starboard"}) %>
</div>
</div>

View file

@ -1,55 +1,160 @@
defmodule AugieWeb.ServoLive do
use Phoenix.LiveView
alias Augie.Firmata
alias Augie.Firmata.Commands
alias Phoenix.PubSub
@moduledoc false
def mount(_params, %{"driver" => driver}, socket) do
driver_as_atom = String.to_atom(driver)
driver_name = String.capitalize(driver)
driver = String.to_atom(driver)
firmata_connected = Firmata.connected?()
if connected?(socket) do
PubSub.subscribe(Augie.PubSub, "command.servo_get_enable_#{driver}")
IO.inspect({:wat, driver_as_atom})
Commands.servo_get_enable(driver_as_atom)
PubSub.subscribe(Augie.PubSub, "command.servo_driver_enabled")
PubSub.subscribe(Augie.PubSub, "command.servo_channel_enabled")
PubSub.subscribe(Augie.PubSub, "command.servo_channel_position")
PubSub.subscribe(Augie.PubSub, "serial_telemetry")
if firmata_connected do
Commands.servo_driver_enabled?(driver)
for i <- 0..15 do
Commands.servo_channel_enabled?(driver, i)
Commands.servo_channel_position?(driver, i)
end
end
end
servos =
0..15
|> Enum.map(fn i ->
{i,
%{
id: i,
name: "Servo #{i + 1}",
enabled: false,
value: 0
}}
end)
|> Enum.into(%{})
{:ok,
assign(socket,
driver: driver_as_atom,
driver: driver,
driver_name: driver_name,
switch_id: "#{driver}",
state_known: false,
enabled: false
enabled: false,
servos: servos,
connected: firmata_connected
)}
end
def handle_info({:servo_get_enable_port, <<0, 0>>}, socket) do
{:noreply, assign(socket, state_known: true, enabled: false)}
def handle_info({:connected, false}, socket), do: {:noreply, assign(socket, connected: false)}
def handle_info({:connected, _}, %{assigns: %{driver: driver}} = socket) do
Commands.servo_driver_enabled?(driver)
for i <- 0..15 do
Commands.servo_channel_enabled?(driver, i)
Commands.servo_channel_position?(driver, i)
end
{:noreply, assign(socket, connected: true)}
end
def handle_info({:servo_get_enable_port, <<1, 0>>}, socket) do
{:noreply, assign(socket, state_known: true, enabled: true)}
def handle_info(
{:servo_driver_enabled, {driver, value}},
%{assigns: %{driver: driver}} = socket
) do
{:noreply, assign(socket, state_known: true, enabled: value)}
end
def handle_info({:servo_get_enable_starboard, <<0, 0>>}, socket) do
{:noreply, assign(socket, state_known: true, enabled: false)}
def handle_info({:servo_driver_enabled, _}, socket), do: {:noreply, socket}
def handle_info(
{:servo_channel_enabled, {driver, channel, value}},
%{assigns: %{servos: servos, driver: driver}} = socket
) do
servo =
servos
|> Map.get(channel)
|> Map.put(:enabled, value)
servos =
servos
|> Map.put(channel, servo)
{:noreply, assign(socket, servos: servos)}
end
def handle_info({:servo_get_enable_starboard, <<1, 0>>}, socket) do
{:noreply, assign(socket, state_known: true, enabled: true)}
def handle_info({:servo_channel_enabled, _}, socket), do: {:noreply, socket}
def handle_info(
{:servo_channel_position, {driver, channel, value}},
%{assigns: %{servos: servos, driver: driver}} = socket
) do
servo =
servos
|> Map.get(channel)
|> Map.put(:value, value)
servos =
servos
|> Map.put(channel, servo)
{:noreply, assign(socket, servos: servos)}
end
def handle_info({:servo_channel_position, _}, socket), do: {:noreply, socket}
def handle_event("enable-servos", _, %{assigns: %{driver: driver}} = socket) do
Commands.servo_set_enable(driver, true)
Commands.servo_get_enable(driver)
Commands.servo_driver_enabled(driver, true)
{:noreply, socket}
end
def handle_event("disable-servos", _, %{assigns: %{driver: driver}} = socket) do
Commands.servo_set_enable(driver, false)
Commands.servo_get_enable(driver)
Commands.servo_driver_enabled(driver, false)
{:noreply, socket}
end
def handle_event(
"toggle-servo",
%{"servo-id" => servo_no},
%{assigns: %{servos: servos, driver: driver}} = socket
) do
servo_no =
servo_no
|> String.to_integer()
servo =
servos
|> Map.get(servo_no)
Commands.servo_channel_enabled(driver, servo_no, !servo.enabled)
{:noreply, assign(socket, servos: servos)}
end
def handle_event(
"set-servo",
%{"servo-value" => degrees, "servo-id" => servo_no},
%{assigns: %{servos: servos, driver: driver}} = socket
) do
degrees =
degrees
|> String.to_integer()
servo_no =
servo_no
|> String.to_integer()
Commands.servo_channel_position(driver, servo_no, degrees)
{:noreply, socket}
end
def handle_event("set-servo", _, socket), do: {:noreply, socket}
end

View file

@ -3,14 +3,56 @@
<h4><%= @driver_name %> Servo Driver</h4>
</div>
<div class="card-section">
<%= if @state_known do %>
<%= if @enabled do %>
<button type="button" class="button primary expanded" phx-click="disable-servos">Disable Servos</button>
<%= if @connected do %>
<%= if @state_known do %>
<%= if @enabled do %>
<button type="button" class="button primary expanded" phx-click="disable-servos">Disable Servos</button>
<% else %>
<button type="button" class="button alert expanded" phx-click="enable-servos">Enable Servos</button>
<% end %>
<% else %>
<button type="button" class="button alert expanded" phx-click="enable-servos">Enable Servos</button>
<button type="button" class="button secondary expanded" disabled>Updating...</button>
<% end %>
<%= for {id, servo} <- @servos do %>
<div class="grid-x">
<div class="call auto">
<strong><%= servo.name %></strong>
</div>
<div class="cell auto text-right">
<input id="servo_<%= id %>_enabled"
type="checkbox"
phx-click="toggle-servo"
phx-value-servo-id="<%= id %>"
<%= if servo.enabled, do: "checked" %>>
<label for="servo_<%= id %>_enabled">
<%= if servo.enabled do %>
<%= servo.value %>º
<% else %>
Enable
<% end %>
</label>
</div>
</div>
<div class="grid-x">
<div class="cell">
<form phx-change="set-servo">
<input type="hidden" name="servo-id" value="<%= id %>">
<input type="range"
min="-135"
max="135"
step="1"
name="servo-value"
value="<%= servo.value %>"
phx-debounce="500"
<%= unless servo.enabled, do: "disabled"%>>
</form>
</div>
</div>
<% end %>
<% else %>
<button type="button" class="button secondary expanded" disabled>Connecting...</button>
<% end %>
<em>Disconnected</em>
<% end %>
</div>
</div>