Add base code

This commit is contained in:
Austen Adler 2022-06-30 22:05:00 -04:00
commit 0403048e7b
17 changed files with 5738 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

1713
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

21
Cargo.toml Normal file
View File

@ -0,0 +1,21 @@
[package]
name = "ir-remote"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
tokio = {version = "1", features = ["full"]}
serialport = "4"
env_logger = "0.9"
log = "0.4"
serde = {version = "1", features = ["derive"]}
rocket = "0.5.0-rc.2"
serde_json = "1"
futures = "0.3"
strum_macros = "0.24"
strum = { version = "0.24", features = ["derive"] }
[build-dependencies]
ructe = "0.14"

5
README.adoc Normal file
View File

@ -0,0 +1,5 @@
= ir-remote
A personal project for remote controlling IR devices
link:https://raw.githubusercontent.com/probonopd/irdb/master/codes/Samsung/TV/7%2C7.csv[Samsung TV Codes]

3
Rocket.toml Normal file
View File

@ -0,0 +1,3 @@
[default.shutdown]
ctrlc = false
signals = ["term", "hup"]

23
arduino/Makefile Normal file
View File

@ -0,0 +1,23 @@
BOARD = arduino:avr:uno
PORT = /dev/ttyACM0
SERIAL = 9600
SCREEN_NAME = arduino
DEPS = IRremote
.PHONY: compile
compile:
arduino-cli compile -v --libraries ./libraries/ --fqbn arduino:avr:uno .
deploy: compile
# Screen can't be consuming the TTY
if screen -S $(SCREEN_NAME) -Q select .; then screen -XS $(SCREEN_NAME) quit; fi
# Upload the code now that screen isn't running
arduino-cli upload --fqbn $(BOARD) --port $(PORT) .
deps:
arduino-cli lib install $(DEPS)
screen:
# -dR - Reattach; logout remotely if required
screen -DRqS $(SCREEN_NAME) $(PORT) $(SERIAL)

93
arduino/arduino.ino Normal file
View File

@ -0,0 +1,93 @@
#include <IRremote.hpp>
#include <Arduino.h>
// define IR_SEND_PIN 4
int IR_RECEIVE_PIN = 11;
int IR_SEND_PIN = 4;
String delim = ",";
uint16_t sAddress = 0x0000;
uint8_t sCommand = 0x00;
uint8_t sRepeats = 0x00;
bool readyToSend = false;
void setup() {
Serial.begin(9600);
while (!Serial) {
;
}
// Receiver
Serial.print("Setting up receiver pin ");
Serial.println(IR_RECEIVE_PIN);
IrReceiver.begin(IR_RECEIVE_PIN, ENABLE_LED_FEEDBACK);
// Sender
Serial.print("Setting up sender pin ");
Serial.println(IR_SEND_PIN);
pinMode(IR_SEND_PIN, OUTPUT);
IrSender.begin(IR_SEND_PIN);
Serial.flush();
}
void loop() {
if (IrReceiver.decode()) {
IrReceiver.printIRResultShort(&Serial);
IrReceiver.resume();
}
/*
if (readyToSend) {
// Send out an actual result
sendData();
// TODO: Don't need to have this variable in the future probably
readyToSend = false;
} else {
// If we aren't sending data, then check for serial data
// Try to parse serial, and if you can, set readyToSend to true
readyToSend = parseSerial();
}
*/
if (parseSerial()) {
sendData();
}
delay(1000); // delay must be greater than 5 ms (RECORD_GAP_MICROS), otherwise the receiver sees it as one long signal
}
bool parseSerial() {
if (Serial.available() == 0) {
// There is no data for us
return false;
}
String input = Serial.readStringUntil('\n');
Serial.print("Got serial message: ");
Serial.println(input);
Serial.flush();
sAddress = atoi(strtok(input.c_str(), delim.c_str()));
sCommand = atoi(strtok(NULL, delim.c_str()));
sRepeats = atoi(strtok(NULL, delim.c_str()));
// If we have a real address and command, we successfully parsed
return sAddress && sCommand;
}
void sendData() {
Serial.print("Send now: protocol: Samsung address=0x");
Serial.print(sAddress, HEX);
Serial.print(" command=0x");
Serial.print(sCommand, HEX);
Serial.print(" repeats=");
Serial.println(sRepeats);
Serial.flush();
IrSender.sendSamsung(sAddress, sCommand, sRepeats);
}

9
build.rs Normal file
View File

@ -0,0 +1,9 @@
use ructe::Ructe;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut ructe = Ructe::from_env()?;
ructe.statics()?.add_files("static")?;
ructe.compile_templates("templates")?;
Ok(())
}

139
src/ir_types.rs Normal file
View File

@ -0,0 +1,139 @@
use rocket::request::FromRequest;
use rocket::FromFormField;
use serde::Deserialize;
use serde::Serialize;
use strum_macros::AsRefStr;
use strum_macros::EnumIter;
#[derive(Serialize, Deserialize)]
pub enum DeviceType {
Samsung,
}
impl DeviceType {
pub fn device_code(&self) -> u16 {
match self {
Self::Samsung => 0x707,
}
}
pub fn function_code(&self, function: &Function) -> u8 {
match self {
Self::Samsung => match function {
Function::InputSource => 0x1,
Function::Power => 0x2,
Function::Button1 => 0x4,
Function::Button2 => 0x5,
Function::Button3 => 0x6,
Function::VolumeUp => 0x7,
Function::Button4 => 0x8,
Function::Button5 => 0x9,
Function::Button6 => 0xa,
Function::VolumeDown => 0xb,
Function::Button7 => 0xc,
Function::Button8 => 0xd,
Function::Button9 => 0xe,
Function::Mute => 0xf,
Function::ChannelDown => 0x10,
Function::Button0 => 0x11,
Function::ChannelUp => 0x12,
Function::Last => 0x13,
Function::Menu => 0x1a,
Function::Info => 0x1f,
Function::AddSubtract => 0x25,
Function::Exit => 0x2d,
Function::EManual => 0x3f,
Function::Tools => 0x4b,
Function::Guide => 0x4f,
Function::Return => 0x58,
Function::CursorUp => 0x60,
Function::CursorDown => 0x61,
Function::CursorRight => 0x62,
Function::CursorLeft => 0x65,
Function::Enter => 0x68,
Function::ChannelList => 0x6b,
Function::SmartHub => 0x79,
Function::Button3D => 0x9f,
},
}
}
}
#[derive(Serialize, Deserialize, EnumIter, AsRefStr, FromFormField, Debug, Copy, Clone)]
pub enum Function {
InputSource,
Power,
Button1,
Button2,
Button3,
VolumeUp,
Button4,
Button5,
Button6,
VolumeDown,
Button7,
Button8,
Button9,
Mute,
ChannelDown,
Button0,
ChannelUp,
Last,
Menu,
Info,
AddSubtract,
Exit,
EManual,
Tools,
Guide,
Return,
CursorUp,
CursorDown,
CursorRight,
CursorLeft,
Enter,
ChannelList,
SmartHub,
Button3D,
}
impl Function {
pub fn as_ref_pretty(&self) -> &'static str {
match self {
Self::InputSource => "Input Source",
Self::Power => "Power",
Self::Button1 => "Button 1",
Self::Button2 => "Button 2",
Self::Button3 => "Button 3",
Self::VolumeUp => "Volume Up",
Self::Button4 => "Button 4",
Self::Button5 => "Button 5",
Self::Button6 => "Button 6",
Self::VolumeDown => "Volume Down",
Self::Button7 => "Button 7",
Self::Button8 => "Button 8",
Self::Button9 => "Button 9",
Self::Mute => "Mute",
Self::ChannelDown => "Channel Down",
Self::Button0 => "Button 0",
Self::ChannelUp => "Channel Up",
Self::Last => "Last",
Self::Menu => "Menu",
Self::Info => "Info",
Self::AddSubtract => "Add Subtract",
Self::Exit => "Exit",
Self::EManual => "E-Manual",
Self::Tools => "Tools",
Self::Guide => "Guide",
Self::Return => "Return",
Self::CursorUp => "Cursor Up",
Self::CursorDown => "Cursor Down",
Self::CursorRight => "Cursor Right",
Self::CursorLeft => "Cursor Left",
Self::Enter => "Enter",
Self::ChannelList => "Channel List",
Self::SmartHub => "Smart Hub",
Self::Button3D => "Button 3D",
}
}
}

101
src/main.rs Normal file
View File

@ -0,0 +1,101 @@
#![allow(unused_imports)]
mod ir_types;
mod serial_agent;
mod webui;
use env_logger::Env;
use futures::future::join_all;
use ir_types::DeviceType;
use ir_types::Function;
use log::{error, info};
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::mpsc::channel;
use tokio::sync::mpsc::Sender;
use tokio::task::JoinHandle;
use tokio::time;
type AppResult<T> = Result<T, Box<dyn std::error::Error + Send + Sync>>;
struct Options {
// device: DeviceType,
}
#[tokio::main]
async fn main() -> AppResult<()> {
env_logger::Builder::from_env(Env::default().default_filter_or("info")).init();
info!("Starting ir remote");
let options = Arc::new(Options {
// device: DeviceType::Samsung,
});
let (outbound_serial_tx, outbound_serial_rx) = channel(100);
let (inbound_serial_tx, inbound_serial_rx) = channel(100);
info!("Starting serial port agent");
let agent_handle: JoinHandle<AppResult<()>> = tokio::task::spawn(async move {
serial_agent::serialport_agent(inbound_serial_tx, outbound_serial_rx).await
});
// let (agent_shutdown_tx, agent_handle): (Sender<()>, JoinHandle<AppResult<()>>) = {
// let (shutdown_tx, shutdown_rx) = channel(1);
// (
// shutdown_tx,
// tokio::task::spawn(async move {
// serial_agent::serialport_agent(shutdown_rx, inbound_serial_tx, outbound_serial_rx)
// .await
// }),
// )
// };
// info!("Starting debugging");
// let debugging_handle: JoinHandle<AppResult<()>> = {
// let outbound_serial_tx = outbound_serial_tx.clone();
// tokio::task::spawn(async move {
// info!("In debugging handle");
// time::sleep(Duration::from_secs(10)).await;
// info!("Done waiting");
// let msg = format!(
// "{},{},{}\n",
// options.device.device_code(),
// options.device.function_code(&Function::Power),
// 3
// );
// outbound_serial_tx.send(msg).await?;
// info!("Done with debugging handle");
// Ok(())
// })
// };
info!("Starting webui");
let webui_handle: rocket::Shutdown = webui::launch(webui::Settings {
outbound_serial_tx: outbound_serial_tx.clone(),
})
.await?;
tokio::select!(
r = agent_handle => {
error!("Agent handle ended: {r:?}");
}
r = webui_handle => {
// agent_shutdown_tx.send(()).await?;
error!("Webserver handle ended: {r:?}");
}
);
// tokio::join!(
// tokio::spawn(async move {
// error!("Agent handle ended: {:?}", agent_handle.await);
// }),
// tokio::spawn(async move {
// let _ = agent_shutdown_tx.send(()).await;
// error!("Webui handle ended: {:?}", webui_handle.await);
// }),
// );
panic!("Ending program");
}

83
src/serial_agent.rs Normal file
View File

@ -0,0 +1,83 @@
use crate::AppResult;
use log::error;
use log::info;
use serialport::SerialPort;
use serialport::TTYPort;
use std::io::BufRead;
use std::io::BufReader;
use std::io::Write;
use std::time::Duration;
use tokio::sync::mpsc::Receiver;
use tokio::sync::mpsc::Sender;
use tokio::task::spawn_blocking;
pub async fn serialport_agent(
// mut shutdown_rx: Receiver<()>,
inbound_serial_tx: Sender<String>,
outbound_serial_rx: Receiver<String>,
) -> AppResult<()> {
info!("Opening serial port");
let (outbound_port, inbound_port): (TTYPort, TTYPort) =
spawn_blocking(move || -> AppResult<(TTYPort, TTYPort)> {
let port = serialport::new("/dev/ttyACM0", 9600)
.timeout(Duration::from_millis(1000))
.open_native()?;
let port_clone = port.try_clone_native()?;
Ok((port, port_clone))
})
.await??;
tokio::select!(
// _ = shutdown_rx.recv() => {
// return Ok(());
// }
r = spawn_blocking(move || outbound(Box::new(outbound_port), outbound_serial_rx)) => {
error!("Outbound thread crashed: {r:?}");
}
r = spawn_blocking(move || inbound(Box::new(inbound_port), inbound_serial_tx)) => {
error!("Inbound thread crashed: {r:?}");
}
);
error!("Some agent channel closed");
Ok(())
}
fn inbound(port: Box<dyn SerialPort>, inbound_serial_tx: Sender<String>) -> AppResult<()> {
let reader = BufReader::new(port);
for msg in reader.lines() {
match msg {
Err(e) if e.kind() == std::io::ErrorKind::TimedOut => {
// Timed out the message. Not a big deal
}
Err(e) => {
error!("Could not read line: {e:?}");
return Err(e.into());
}
Ok(m) => {
info!("Got message from inbound channel: {m}");
inbound_serial_tx.blocking_send(m)?;
}
}
}
// TODO: Return error since something crashed
Ok(())
}
fn outbound(
mut port: Box<dyn SerialPort>,
mut outbound_serial_rx: Receiver<String>,
) -> AppResult<()> {
while let Some(msg) = outbound_serial_rx.blocking_recv() {
info!("Sending message {msg}");
port.write_all(msg.as_bytes())?;
port.flush()?;
}
error!("The channel was closed");
Ok(())
}

135
src/webui.rs Normal file
View File

@ -0,0 +1,135 @@
include!(concat!(env!("OUT_DIR"), "/templates.rs"));
mod template_responder;
use crate::ir_types::DeviceType;
use crate::ir_types::Function;
use crate::AppResult;
use log::error;
use log::info;
use log::warn;
use rocket::form::Form;
use rocket::get;
use rocket::launch;
use rocket::post;
use rocket::response::Flash;
use rocket::response::Redirect;
use rocket::routes;
use rocket::uri;
use rocket::FromForm;
use rocket::State;
use template_responder::TemplateResponder;
use tokio::sync::mpsc::Sender;
pub struct Settings {
pub outbound_serial_tx: Sender<String>,
}
struct WebuiState {
outbound_serial_tx: Sender<String>,
}
// Static files
// use templates::statics::StaticFile;
#[derive(FromForm, Debug)]
struct FunctionForm {
function: Function,
}
#[get("/function?<function>")]
async fn function_get(function: Function, state: &State<WebuiState>) -> Flash<Redirect> {
info!("Got button value: {:?}", function);
match state
.outbound_serial_tx
.send(format!(
"{},{},{}\n",
DeviceType::Samsung.device_code(),
DeviceType::Samsung.function_code(&function),
1
))
.await
{
Err(e) => {
let error_msg = format!("{e:?}");
error!("{error_msg}");
Flash::error(Redirect::to(uri!(index)), error_msg)
}
Ok(()) => Flash::success(Redirect::to(uri!(index)), "Successfully changed"),
}
}
#[post("/function", data = "<data>")]
async fn function_post(data: Form<FunctionForm>, state: &State<WebuiState>) -> Flash<Redirect> {
function_get(data.function, state).await
// // info!("Got button value: {:?}", data.function);
// // match state
// // .outbound_serial_tx
// // .send(format!(
// // "{},{},{}\n",
// // DeviceType::Samsung.device_code(),
// // DeviceType::Samsung.function_code(&Function::Power),
// // 1
// // ))
// // .await
// // {
// // Err(e) => {
// // let error_msg = format!("{e:?}");
// // error!("{error_msg}");
// // Flash::error(Redirect::to(uri!(index)), error_msg)
// // }
// // Ok(()) => Flash::success(Redirect::to(uri!(index)), "Successfully changed"),
// // }
}
#[post("/power")]
async fn power(state: &State<WebuiState>) -> Flash<Redirect> {
match state
.outbound_serial_tx
.send(format!(
"{},{},{}\n",
DeviceType::Samsung.device_code(),
DeviceType::Samsung.function_code(&Function::Power),
1
))
.await
{
Err(e) => {
let error_msg = format!("{e:?}");
error!("{error_msg}");
Flash::error(Redirect::to(uri!(index)), error_msg)
}
Ok(()) => Flash::success(Redirect::to(uri!(index)), "Successfully changed"),
}
}
#[get("/")]
async fn index() -> Result<TemplateResponder, std::io::Error> {
let mut buf = Vec::new();
templates::index_html(&mut buf)?;
Ok(TemplateResponder::from(buf))
}
pub async fn launch(settings: Settings) -> AppResult<rocket::Shutdown> {
let state = WebuiState {
outbound_serial_tx: settings.outbound_serial_tx,
};
let rocket = rocket::build()
// let _rocket = rocket::build()
// .mount("/", routes![index, function, power])
// .manage(state)
// .launch()
// .await?;
.mount("/", routes![index, function_get, function_post, power])
.manage(state)
.ignite()
.await?;
let shutdown_handle = rocket.shutdown();
tokio::task::spawn(rocket.launch());
Ok(shutdown_handle)
// Ok(())
}

View File

@ -0,0 +1,20 @@
use rocket::{http::ContentType, response::Responder, Request, Response};
use std::io::Cursor;
pub struct TemplateResponder(pub Vec<u8>);
impl From<Vec<u8>> for TemplateResponder {
fn from(d: Vec<u8>) -> Self {
Self(d)
}
}
#[rocket::async_trait]
impl<'r> Responder<'r, 'static> for TemplateResponder {
fn respond_to(self, _: &'r Request<'_>) -> rocket::response::Result<'static> {
Response::build()
.header(ContentType::HTML)
.sized_body(self.0.len(), Cursor::new(self.0))
.ok()
}
}

1249
static/remote.svg Normal file

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 83 KiB

1055
static/remote2.svg Normal file

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 83 KiB

2
static/tacit-css.min.css vendored Normal file

File diff suppressed because one or more lines are too long

1086
templates/index.rs.html Normal file

File diff suppressed because it is too large Load Diff