Compare commits

...

18 Commits

Author SHA1 Message Date
f84c8226a7 changed alarm to alarms for the alarms resource 2026-03-03 17:38:32 +01:00
024a212fe2 updating the alarm will now also update the cron schedule 2026-03-03 16:02:23 +01:00
5a818bae56 resolved remaining issues 2026-03-03 15:34:55 +01:00
a4457dc2d7 removed deprecated function 2026-03-03 15:32:35 +01:00
d1d7f89dfb removed evertything swagger related 2026-03-03 15:28:59 +01:00
a65f9852f3 showing id now also when executing a get Request 2026-03-03 15:27:51 +01:00
76067d76ac added http endpoint to update alarms. For now it will not update the schedules automatically 2026-03-03 15:15:06 +01:00
37b8fe5e56 title is now also part of the whole process 2026-03-03 12:58:45 +01:00
cf67c980a2 added sqlite to the devshell for debugging 2026-03-03 12:49:23 +01:00
0ad039a597 get endpoint works 2026-03-03 12:14:39 +01:00
3db69454e4 removed accidantially created file 2026-03-03 11:41:11 +01:00
a7947a31bc implementing get endpoint for alarms wip 2026-03-03 10:00:46 +01:00
d8416f3c99 get endpoint in progress 2026-03-02 23:01:45 +01:00
8ed9d872dc removed some deps, using heap to store the current ringer (makes it more dynamic) 2026-03-02 20:54:34 +01:00
a86a28fd85 same functionality now works with axum the same way it did with actix 2026-03-02 20:39:08 +01:00
05ba925d54 switched back to axum wip 2026-03-02 17:03:22 +01:00
f7e55536ab database persistence in progress 2026-02-05 23:22:00 +01:00
a398911527 updated things in the flake so the dev shell works now again 2026-02-05 21:48:03 +01:00
26 changed files with 3939 additions and 946 deletions

1910
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,7 +10,11 @@ chrono = { version = "0.4.42", features = ["serde"] }
axum = { version = "0.8.8", features = ["macros"] }
tokio = { version = "1.49.0", features = ["full"] }
serde = { version = "1.0.228", features = ["derive"] }
actix-web = "4.12.1"
paperclip = { version = "0.9.5", features = ["actix4"] }
apistos = { version = "0.6.0", features = ["swagger-ui"] }
schemars = { package = "apistos-schemars", version = "0.8" }
sea-orm = { version = "1.1.19", features = [
"macros",
"runtime-tokio",
"sqlx-sqlite",
] }
utoipa = { version = "5.4.0", features = ["axum_extras", "chrono"] }
utoipa-axum = "0.2.0"
utoipa-scalar = { version = "0.3.0", features = ["axum"] }

32
flake.lock generated
View File

@@ -6,11 +6,11 @@
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1763880175,
"narHash": "sha256-WfItZn6duisxCxyltbu7Hs7kxzNeylgZGOwCYwHe26g=",
"lastModified": 1770275419,
"narHash": "sha256-g2wfAevB/IFF6Y1C74TbhRERlUVFVGQsgGp/lLR4lQM=",
"owner": "nix-community",
"repo": "fenix",
"rev": "a563f057979806c59da53070297502eb7af22f62",
"rev": "aa3fbaab2bdc73c5c1e25a124c272dde295bc957",
"type": "github"
},
"original": {
@@ -47,11 +47,11 @@
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1763384566,
"narHash": "sha256-r+wgI+WvNaSdxQmqaM58lVNvJYJ16zoq+tKN20cLst4=",
"lastModified": 1769799857,
"narHash": "sha256-88IFXZ7Sa1vxbz5pty0Io5qEaMQMMUPMonLa3Ls/ss4=",
"owner": "nix-community",
"repo": "naersk",
"rev": "d4155d6ebb70fbe2314959842f744aa7cabbbf6a",
"rev": "9d4ed44d8b8cecdceb1d6fd76e74123d90ae6339",
"type": "github"
},
"original": {
@@ -63,11 +63,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1763678758,
"narHash": "sha256-+hBiJ+kG5IoffUOdlANKFflTT5nO3FrrR2CA3178Y5s=",
"lastModified": 1770197578,
"narHash": "sha256-AYqlWrX09+HvGs8zM6ebZ1pwUqjkfpnv8mewYwAo+iM=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "117cc7f94e8072499b0a7aa4c52084fa4e11cc9b",
"rev": "00c21e4c93d963c50d4c0c89bfa84ed6e0694df2",
"type": "github"
},
"original": {
@@ -95,16 +95,16 @@
},
"nixpkgs_3": {
"locked": {
"lastModified": 1688392541,
"narHash": "sha256-lHrKvEkCPTUO+7tPfjIcb7Trk6k31rz18vkyqmkeJfY=",
"lastModified": 1770197578,
"narHash": "sha256-AYqlWrX09+HvGs8zM6ebZ1pwUqjkfpnv8mewYwAo+iM=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "ea4c80b39be4c09702b0cb3b42eab59e2ba4f24b",
"rev": "00c21e4c93d963c50d4c0c89bfa84ed6e0694df2",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-22.11",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
@@ -119,11 +119,11 @@
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1763846202,
"narHash": "sha256-f5PvQONttEQCjnQ52zAEVJvXDZ5l2gbItLfDyfcyGgk=",
"lastModified": 1770200365,
"narHash": "sha256-Z3V5v8tSwZ3l4COVSt0b6Av0wZwTUf7Qj0SQ2/Z5RX0=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "50621856a594a357c3aff0c5176ba8db4118133d",
"rev": "1433910d1ffaff2c7a5fb7ba701f82ea578a99e3",
"type": "github"
},
"original": {

View File

@@ -2,7 +2,7 @@
inputs = {
fenix.url = "github:nix-community/fenix";
naersk.url = "github:nix-community/naersk/master";
nixpkgs.url = "github:NixOS/nixpkgs/nixos-22.11";
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
};
outputs =
@@ -101,23 +101,28 @@
in
{
shell = eachSystem (buildTargets) (
devShells = eachSystem (builtins.attrNames buildTargets) (
system:
let
pkgs = mkPkgs system null;
in
pkgs.mkShell {
buildInputs = with pkgs; [
gcc
openssl.dev
pkg-config
libiconv
rustc
cargo
sea-orm-cli
];
{
default = pkgs.mkShell {
name = "snooze-pal";
buildInputs = with pkgs; [
gcc
openssl.dev
pkg-config
libiconv
rustc
cargo
sea-orm-cli
sqlite
];
};
}
);
packages = eachCrossSystem (builtins.attrNames buildTargets) (
buildSystem: targetSystem:
let

2
migration/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/target
result

2129
migration/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

23
migration/Cargo.toml Normal file
View File

@@ -0,0 +1,23 @@
[package]
name = "migration"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
name = "migration"
path = "src/lib.rs"
[dependencies]
tokio = {version = "1.49.0", features = ["macros", "rt-multi-thread"]}
[dependencies.sea-orm-migration]
version = "1.1.0"
features = [
# Enable at least one `ASYNC_RUNTIME` and `DATABASE_DRIVER` feature if you want to run migration via CLI.
# View the list of supported features at https://www.sea-ql.org/SeaORM/docs/install-and-config/database-and-async-runtime.
# e.g.
"runtime-tokio-rustls", # `ASYNC_RUNTIME` feature
# "sqlx-postgres", # `DATABASE_DRIVER` feature
"sqlx-sqlite"
]

41
migration/README.md Normal file
View File

@@ -0,0 +1,41 @@
# Running Migrator CLI
- Generate a new migration file
```sh
cargo run -- generate MIGRATION_NAME
```
- Apply all pending migrations
```sh
cargo run
```
```sh
cargo run -- up
```
- Apply first 10 pending migrations
```sh
cargo run -- up -n 10
```
- Rollback last applied migrations
```sh
cargo run -- down
```
- Rollback last 10 applied migrations
```sh
cargo run -- down -n 10
```
- Drop all tables from the database, then reapply all migrations
```sh
cargo run -- fresh
```
- Rollback all applied migrations, then reapply all migrations
```sh
cargo run -- refresh
```
- Rollback all applied migrations
```sh
cargo run -- reset
```
- Check the status of all migrations
```sh
cargo run -- status
```

12
migration/src/lib.rs Normal file
View File

@@ -0,0 +1,12 @@
pub use sea_orm_migration::prelude::*;
mod m20220101_000001_create_table;
pub struct Migrator;
#[async_trait::async_trait]
impl MigratorTrait for Migrator {
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![Box::new(m20220101_000001_create_table::Migration)]
}
}

View File

@@ -0,0 +1,36 @@
use sea_orm_migration::{prelude::*, schema::*};
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
// Replace the sample below with your own migration scripts
manager.create_table(
Table::create()
.table(Alarm::Table)
.if_not_exists()
.col(pk_auto(Alarm::Id))
.col(date_time(Alarm::Time))
.col(boolean(Alarm::Enabled))
.col(string(Alarm::Title))
.to_owned(),
).await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
// Replace the sample below with your own migration scripts
manager.drop_table(Table::drop().table(Alarm::Table).to_owned()).await
}
}
#[derive(DeriveIden)]
enum Alarm {
Table,
Id,
Time,
Enabled,
Title
}

6
migration/src/main.rs Normal file
View File

@@ -0,0 +1,6 @@
use sea_orm_migration::prelude::*;
#[tokio::main]
async fn main() {
cli::run_cli(migration::Migrator).await;
}

BIN
snooze-pal.db Normal file

Binary file not shown.

45
src/dao/alarm.rs Normal file
View File

@@ -0,0 +1,45 @@
use std::i32;
use chrono::{DateTime, Utc};
use sea_orm::{
ActiveModelTrait, ActiveValue::Set, ColumnTrait, ConnectionTrait, DbErr, EntityTrait, QueryFilter
};
use crate::model::{self, alarm};
pub async fn create_alarm<C: ConnectionTrait>(
db: &C,
title: String,
time: DateTime<Utc>,
) -> Result<alarm::Model, DbErr> {
let alarm_to_create = model::alarm::ActiveModel {
title: Set(title),
time: Set(time.naive_utc()),
enabled: Set(true),
..Default::default()
};
Ok(alarm_to_create.insert(db).await?)
}
pub async fn get_alarms<C: ConnectionTrait>(db: &C, enabled: Option<bool>) -> Result<Vec<alarm::Model>, DbErr> {
let query = model::alarm::Entity::find();
let query = match enabled {
Some(enabled) => {
query.filter(model::alarm::Column::Enabled.eq(enabled))
},
None => {
query
}
};
query.all(db).await
}
pub async fn get_alarm<C: ConnectionTrait>(db: &C, id: i32) -> Result<Option<model::alarm::Model>, DbErr> {
let result = model::alarm::Entity::find_by_id(id)
.one(db).await;
return result
}

2
src/dao/mod.rs Normal file
View File

@@ -0,0 +1,2 @@
pub mod alarm;

View File

@@ -1,64 +1,81 @@
use std::sync::{Arc, Mutex};
use actix_web::{App, HttpServer, web};
use apistos::{SwaggerUIConfig, app::{BuildConfig, OpenApiWrapper}, info::Info, spec::Spec};
use cron_tab::Cron;
use gpio_cdev::Chip;
use sea_orm::Database;
use tokio::net::TcpListener;
use utoipa::OpenApi;
use utoipa_axum::router::OpenApiRouter;
use utoipa_scalar::{Scalar, Servable};
use crate::{ringer::BeepRinger, scheduler::Scheduler};
use crate::{ringer::{BeepRinger, Ringer, SilentRinger}, scheduler::Scheduler};
mod scheduler;
mod ringer;
mod types;
mod resources;
mod dao;
mod model;
#[derive(Clone)]
struct AppState {
scheduler: Arc<Scheduler<BeepRinger>>
scheduler: Arc<Scheduler>
}
fn app_state() -> AppState {
let chip: Arc<Mutex<Chip>> = Arc::new(Mutex::new(Chip::new("/dev/gpiochip0").unwrap()));
fn construct_ringer() -> Arc<Mutex<dyn Ringer>> {
let result = Chip::new("/dev/gpiochip0");
match result {
Ok(chip) => {
let chip = Arc::new(Mutex::new(chip));
Arc::new(Mutex::new(BeepRinger::new(chip))) as Arc<Mutex<dyn Ringer>>
},
Err(e) => {
println!("Error opening chip (falling back to silent ringer): {}", e);
Arc::new(Mutex::new(SilentRinger::new())) as Arc<Mutex<dyn Ringer>>
}
}
}
async fn app_state() -> AppState {
let cron = Arc::new(Mutex::new(Cron::new(chrono::Local)));
let db = Database::connect("sqlite://snooze-pal.db").await.unwrap();
let ringer = construct_ringer();
AppState {
scheduler: Arc::new(Scheduler::new(
Arc::new(Mutex::new(BeepRinger::new(chip.clone()))),
cron.clone()
ringer,
cron.clone(),
Arc::new(db)
))
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let app_state = app_state();
app_state.scheduler.start();
let _ = start_actix_server(app_state).await;
let app_state = app_state().await;
app_state.scheduler.start().await;
start_axum_server(app_state).await;
Ok(())
}
#[derive(OpenApi)]
struct ApiDocs;
async fn start_actix_server(app_state: AppState) -> std::io::Result<()> {
let _ = HttpServer::new(move || {
let spec = Spec {
info: Info {
title: "Snooze Pal".to_string(),
version: "0.0.1".to_string(),
..Default::default()
},
..Default::default()
};
async fn start_axum_server(app_state: AppState) {
let docs = ApiDocs::openapi();
App::new()
.document(spec)
.service(resources::v1())
.app_data(web::Data::new(app_state.clone()))
.build_with("/openapi.json", BuildConfig::default().with(SwaggerUIConfig::new(&"/swagger")))
let (router, spec) = OpenApiRouter::<AppState>::with_openapi(docs)
.nest("/v1", resources::router())
.with_state(app_state)
.split_for_parts();
}).bind("0.0.0.0:8080")?.run().await;
let router = router.merge(Scalar::with_url("/docs", spec));
let listener = TcpListener::bind("0.0.0.0:8080")
.await.expect("Failed to bind to port 8080. It may be taken by another process.");
Ok(())
axum::serve(listener, router)
.await.expect("Failed to serve");
}

18
src/model/alarm.rs Normal file
View File

@@ -0,0 +1,18 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "alarm")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub time: DateTime,
pub enabled: bool,
pub title: String,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}

5
src/model/mod.rs Normal file
View File

@@ -0,0 +1,5 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
pub mod prelude;
pub mod alarm;

1
src/model/prelude.rs Normal file
View File

@@ -0,0 +1 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19

View File

@@ -0,0 +1,65 @@
use std::i32;
use axum::{Json, debug_handler, extract::{Query, State}, http::StatusCode, response::IntoResponse};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use utoipa::{IntoParams, IntoResponses, ToSchema};
use crate::{AppState, types::Alarm};
#[derive(ToSchema, Serialize)]
pub struct OkResponse {
id: i32,
title: String,
enabled: bool,
time: DateTime<Utc>,
}
#[derive(IntoResponses)]
pub enum Responses {
#[response(status = 200)]
Ok(#[to_schema] Vec<OkResponse>),
#[response(status = 500)]
DBError(String),
}
impl IntoResponse for Responses {
fn into_response(self) -> axum::response::Response {
match self {
Responses::Ok(body) => (StatusCode::OK, Json(body)).into_response(),
Responses::DBError(message) => {
(StatusCode::INTERNAL_SERVER_ERROR, Json(message)).into_response()
}
}
}
}
#[derive(IntoParams, Deserialize)]
pub struct RequestQuery {
enabled: Option<bool>
}
impl From<Alarm> for OkResponse {
fn from(value: Alarm) -> Self {
Self {
id: value.id,
title: value.title,
enabled: value.enabled,
time: value.time,
}
}
}
#[utoipa::path(get, path = "", responses(Responses), params(RequestQuery))]
#[debug_handler]
pub async fn get_handler(State(AppState{ scheduler }): State<AppState>, Query(RequestQuery { enabled }): Query<RequestQuery>) -> Responses {
let result = scheduler.get_alarms(enabled).await;
match result {
Ok(alarms) => {
Responses::Ok(alarms.into_iter().map(|alarm| alarm.into()).collect())
},
Err(error) => {
Responses::DBError(error.to_string())
}
}
}

View File

@@ -1,7 +1,15 @@
mod get;
mod patch;
mod post;
use apistos::web;
pub fn resource() -> web::Resource {
web::resource("/alarm")
.route(web::post().to(post::post))
use utoipa_axum::{router::OpenApiRouter, routes};
use crate::AppState;
pub fn router() -> OpenApiRouter<AppState> {
OpenApiRouter::new().routes(routes!(
post::post_handler,
get::get_handler,
patch::patch_handler
))
}

View File

@@ -0,0 +1,73 @@
use axum::Json;
use axum::http::StatusCode;
use axum::{debug_handler, response::IntoResponse};
use axum::extract::{Path, State};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use utoipa::{IntoResponses, ToSchema};
use crate::AppState;
#[derive(ToSchema, Serialize, Deserialize)]
pub struct OkResponse {
id: i32,
title: String,
enabled: bool,
time: DateTime<Utc>,
}
#[derive(ToSchema, Deserialize)]
pub struct PatchRequestBody {
title: Option<String>,
enabled: Option<bool>,
time: Option<DateTime<Utc>>,
}
#[derive(IntoResponses)]
pub enum Responses {
#[response(status = 200)]
Ok(#[to_schema] OkResponse),
#[response(status = 500, description = "Something failed in the Database when trying to update the alarm")]
DbError(String),
#[response(status = 404, description = "The alarm you want to update was not found")]
NotFound,
}
impl IntoResponse for Responses {
fn into_response(self) -> axum::response::Response {
match self {
Responses::Ok(body) => (StatusCode::OK, Json(body)).into_response(),
Responses::DbError(message) => (StatusCode::INTERNAL_SERVER_ERROR, Json(message)).into_response(),
Responses::NotFound => (StatusCode::NOT_FOUND).into_response(),
}
}
}
impl From<crate::types::Alarm> for OkResponse {
fn from(value: crate::types::Alarm) -> Self {
OkResponse {
id: value.id,
title: value.title,
enabled: value.enabled,
time: value.time,
}
}
}
#[utoipa::path(patch, path = "/{id}", responses(Responses))]
#[debug_handler]
pub async fn patch_handler(State(AppState { scheduler }): State<AppState>, Path(id): Path<i32>, Json(body): Json<PatchRequestBody>) -> Responses {
let result = scheduler.update_alarm(id, body.title, body.enabled, body.time).await;
match result {
Ok(Some(alarm)) => {
Responses::Ok(alarm.into())
},
Ok(None) => {
Responses::NotFound
},
Err(error) => {
Responses::DbError(error.to_string())
}
}
}

View File

@@ -1,56 +1,75 @@
use std::{fmt::Display};
use actix_web::{ResponseError, web::{Data, Json}};
use apistos::{ApiComponent, ApiErrorComponent, actix::CreatedJson};
use chrono::{DateTime, Local};
use schemars::JsonSchema;
use serde::{Serialize, Deserialize};
use axum::extract::State;
use axum::{Json};
use axum::http::StatusCode;
use axum::debug_handler;
use axum::response::IntoResponse;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use utoipa::{IntoResponses, ToSchema};
use crate::AppState;
use crate::types::Alarm;
#[derive(Debug, Deserialize, JsonSchema, ApiComponent)]
#[derive(Debug, Deserialize, ToSchema)]
pub struct RequestBody {
time: DateTime<Local>,
title: String,
time: DateTime<Utc>,
}
#[derive(Debug, Serialize, JsonSchema, ApiComponent)]
pub struct SuccessResponse {
pub time: DateTime<Local>,
pub enabled: bool
#[derive(ToSchema, Serialize)]
pub struct OkResponseBody {
id: i32,
time: DateTime<Utc>,
title: String,
enabled: bool,
}
#[derive(Debug, Deserialize, Serialize, ApiErrorComponent)]
#[openapi_error(status(code = 500, description = "The alarm could not be created"))]
pub enum ErrorResponse {
InternalError(String)
#[derive(IntoResponses)]
pub enum Responses {
#[response(status = 200)]
Ok(#[to_schema] OkResponseBody),
#[response(status = 500)]
Error(String)
}
impl Display for ErrorResponse {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl IntoResponse for Responses {
fn into_response(self) -> axum::response::Response {
match self {
ErrorResponse::InternalError(e) => write!(f, "Internal error: {}", e),
Responses::Ok(body) => (StatusCode::OK, Json(body)).into_response(),
Responses::Error(message) => (StatusCode::INTERNAL_SERVER_ERROR, Json(message)).into_response(),
}
}
}
impl From<Alarm> for OkResponseBody {
fn from(value: Alarm) -> Self {
OkResponseBody {
time: value.time,
enabled: value.enabled,
title: value.title,
id: value.id
}
}
}
impl ResponseError for ErrorResponse {
fn status_code(&self) -> actix_web::http::StatusCode {
match self {
ErrorResponse::InternalError(_) => actix_web::http::StatusCode::INTERNAL_SERVER_ERROR,
}
}
}
#[apistos::api_operation(
summary = "Add new alarm",
description = r###"Creates new Alarm"###,
error_code= 500,
#[utoipa::path(
post,
path = "",
responses(
Responses
)
)]
pub async fn post(data: Data<AppState>, body: Json<RequestBody>) -> Result<CreatedJson<SuccessResponse>, ErrorResponse> {
let result = data.scheduler.add_alarm(body.time);
#[debug_handler]
pub async fn post_handler(State(AppState { scheduler, ..}): State<AppState>, Json(body): Json<RequestBody>) -> Responses {
let result = scheduler.add_alarm(body.time, body.title).await;
match result {
Ok(alarm) => Ok(CreatedJson(SuccessResponse { time: alarm.time, enabled: alarm.enabled })),
Err(e) => Err(ErrorResponse::InternalError(e)),
Ok(alarm) => {
Responses::Ok(alarm.into())
},
Err(e) => {
Responses::Error(e.to_string())
}
}
}

View File

@@ -1,5 +1,9 @@
use utoipa_axum::router::OpenApiRouter;
use crate::AppState;
mod alarm;
pub fn v1() -> apistos::web::Scope {
apistos::web::scope("/v1").service(alarm::resource())
pub fn router() -> OpenApiRouter<AppState> {
OpenApiRouter::new().nest("/alarms", alarm::router())
}

View File

@@ -1,8 +1,12 @@
use std::{sync::{Arc, Mutex}, thread};
use std::{
fmt::Debug,
sync::{Arc, Mutex},
thread,
};
use gpio_cdev::{Chip, LineRequestFlags};
pub trait Ringer: Send + Sync {
pub trait Ringer: Send + Sync + Debug {
fn ring(&self) -> Result<(), String>;
}
@@ -12,43 +16,40 @@ pub struct BeepRinger {
}
impl BeepRinger {
fn beep(times: u32, chip: &mut Chip) -> Result<(), String> {
let beeper = chip.get_line(17);
fn beep(times: u32, chip: &mut Chip) -> Result<(), String> {
let beeper = chip.get_line(17);
let beeper = match beeper {
Ok(beeper) => beeper,
Err(e) => {
println!("Error opening line: {}", e);
return Err("Could not open Line to Beeper".to_string());
}
};
let beeper = match beeper {
Ok(beeper) => beeper,
Err(e) => {
println!("Error opening line: {}", e);
return Err("Could not open Line to Beeper".to_string());
}
};
let beeper = beeper.request(LineRequestFlags::OUTPUT, 0, "my-gpio");
let beeper = match beeper {
Ok(beeper) => beeper,
Err(e) => {
println!("Error requesting line: {}", e);
return Err("Could not request Line to Beeper".to_string());
}
};
let beeper = beeper
.request(LineRequestFlags::OUTPUT, 0, "my-gpio");
let beeper = match beeper {
Ok(beeper) => beeper,
Err(e) => {
println!("Error requesting line: {}", e);
return Err("Could not request Line to Beeper".to_string());
}
};
for _ in 0..times {
beeper.set_value(1).map_err(|e| e.to_string())?;
thread::sleep(std::time::Duration::from_secs(1));
beeper.set_value(0).map_err(|e| e.to_string())?;
thread::sleep(std::time::Duration::from_secs(1));
}
for _ in 0..times {
beeper.set_value(1).map_err(|e| e.to_string())?;
thread::sleep(std::time::Duration::from_secs(1));
beeper.set_value(0).map_err(|e| e.to_string())?;
thread::sleep(std::time::Duration::from_secs(1));
}
return Ok(());
}
}
pub fn new(chip: Arc<Mutex<Chip>>) -> Self {
Self { chip }
}
pub fn new(chip: Arc<Mutex<Chip>>) -> Self {
Self { chip }
}
}
impl Ringer for BeepRinger {
@@ -63,8 +64,26 @@ impl Ringer for BeepRinger {
}
};
BeepRinger::beep(5, &mut *chip)?;
BeepRinger::beep(5, &mut *chip)?;
Ok(())
Ok(())
}
}
/// Used for local testing without an actual beeper or similar. The only thing it does is print
/// that it's ringing.
#[derive(Debug)]
pub struct SilentRinger;
impl SilentRinger {
pub fn new() -> Self {
Self {}
}
}
impl Ringer for SilentRinger {
fn ring(&self) -> Result<(), String> {
println!("Ringing");
Ok(())
}
}

View File

@@ -1,29 +1,40 @@
use std::collections::HashMap;
use std::{i32, usize};
use std::sync::{Arc, Mutex};
use chrono::{DateTime, Local, Timelike};
use chrono::{DateTime, Local, Timelike, Utc};
use cron_tab::Cron;
use sea_orm::ActiveValue::Set;
use sea_orm::{ActiveModelTrait, DatabaseConnection, DbErr};
use crate::dao::alarm::{self, create_alarm, get_alarm, get_alarms};
use crate::model;
use crate::ringer::Ringer;
use crate::types::Alarm;
#[derive(Debug)]
pub struct Scheduler<T: Ringer + 'static> {
ringer: Arc<Mutex<T>>,
pub struct Scheduler {
ringer: Arc<Mutex<dyn Ringer>>,
cron: Arc<Mutex<Cron<Local>>>,
alarms: Arc<Mutex<Vec<Alarm>>>,
db: Arc<DatabaseConnection>,
cron_jobs: Arc<Mutex<HashMap<i32, usize>>>
}
impl<T: Ringer> Scheduler<T> {
pub fn new(ringer: Arc<Mutex<T>>, cron: Arc<Mutex<Cron<Local>>>) -> Self {
impl Scheduler {
pub fn new(
ringer: Arc<Mutex<dyn Ringer>>,
cron: Arc<Mutex<Cron<Local>>>,
db: Arc<DatabaseConnection>,
) -> Self {
Self {
ringer,
cron,
alarms: Arc::new(Mutex::new(Vec::new())),
db,
cron_jobs: Arc::new(Mutex::new(HashMap::new()))
}
}
pub fn schedule(&self, cron_schedule: &str) -> Result<(), String> {
pub fn schedule(&self, cron_schedule: &str) -> Result<usize, String> {
let ringer = self.ringer.clone();
let cron = self.cron.clone();
@@ -37,22 +48,114 @@ impl<T: Ringer> Scheduler<T> {
}
});
job_result.expect("Faild to add job");
todo!()
Ok(job_result.expect("Faild to add job"))
}
pub fn add_alarm(&self, time: DateTime<Local>) -> Result<Alarm, String> {
let mut alarms = self.alarms.lock().map_err(|e| e.to_string())?;
let cron_schedule = format!("{} {} {} * * *", "*", time.minute(), time.hour());
let alarm = Alarm::new(true, time);
alarms.push(alarm.clone());
self.schedule(&cron_schedule).map_err(|e| e.to_string())?;
pub async fn add_alarm(&self, time: DateTime<Utc>, title: String) -> Result<Alarm, String> {
let cron_schedule = self.construct_cron_schedule(time);
let created_alarm = create_alarm(&*self.db, title, time)
.await
.map_err(|e| e.to_string())?;
let job_id = self.schedule(&cron_schedule).map_err(|e| e.to_string())?;
self.cron_jobs.lock().expect("Failed to lock cron_jobs").insert(created_alarm.id, job_id);
println!("Added alarm {}", cron_schedule);
Ok(alarm)
Ok(created_alarm.into())
}
pub fn start(&self) {
pub async fn get_alarms(&self, enabled: Option<bool>) -> Result<Vec<Alarm>, DbErr> {
get_alarms(&*self.db, enabled).await
.map(|alarms| alarms
.into_iter()
.map(|a| a.into())
.collect()
)
}
pub async fn update_alarm(&self, id: i32, title: Option<String>, enabled: Option<bool>, time: Option<DateTime<Utc>>) -> Result<Option<Alarm>, DbErr> {
let alarm = get_alarm(&*self.db, id).await?;
let Some(alarm) = alarm else {
return Ok(None)
};
let mut active_alarm: model::alarm::ActiveModel = alarm.into();
if let Some(title) = title {
active_alarm.title = Set(title.to_owned());
}
if let Some(enabled) = enabled {
active_alarm.enabled = Set(enabled);
}
if let Some(time) = time {
active_alarm.time = Set(time.naive_utc());
}
let updated_alarm = active_alarm.update(&*self.db).await?;
self.reregister_alarm(updated_alarm.clone().into());
return Ok(Some(updated_alarm.into()));
}
pub async fn start(&self) {
self.cron.lock().expect("Failed to lock cron").start();
self.register_existing_alarms().await;
}
async fn register_existing_alarms(&self) {
let alarms_result = self.get_alarms(Some(true)).await;
match alarms_result {
Ok(alarms) => {
self.register_alarms(alarms);
},
Err(e) => {
println!("Failed to get alarms: {}", e);
}
}
}
fn register_alarms(&self, alarms: Vec<Alarm>) {
alarms.into_iter().for_each(|a| {
self.register_alarm(a);
});
}
fn register_alarm(&self, alarm: Alarm) {
let cron_schedule = self.construct_cron_schedule(alarm.time);
let scheduling_result = self.schedule(&cron_schedule);
match scheduling_result {
Ok(job_id) => {
self.cron_jobs.lock().expect("Failed to lock cron_jobs").insert(alarm.id, job_id);
println!("Registered alarm {}", cron_schedule);
},
Err(e) => {
println!("Failed to register alarm {}: {}", cron_schedule, e);
}
}
}
fn construct_cron_schedule(&self, time: DateTime<Utc>) -> String {
let time = time.with_timezone(&Local);
format!("{} {} {} * * *", "*", time.minute(), time.hour())
}
fn reregister_alarm(&self, alarm: Alarm) {
let job_id = self.cron_jobs.lock().expect("Failed to lock cron_jobs")
.remove(&alarm.id);
if let Some(job_id) = job_id {
self.cron.lock().expect("Failed to lock cron handler").remove(job_id);
}
let new_job_id = self.schedule(&self.construct_cron_schedule(alarm.time)).expect("failed to register alarm");
self.cron_jobs.lock().expect("Failed to lock cron_jobs").insert(alarm.id, new_job_id);
}
}

View File

@@ -1,16 +1,24 @@
use apistos::ApiComponent;
use chrono::{DateTime, Local};
use schemars::JsonSchema;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema, ApiComponent)]
use crate::model;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Alarm {
pub id: i32,
pub enabled: bool,
pub time: DateTime<Local>,
pub time: DateTime<Utc>,
pub title: String,
}
impl Alarm {
pub fn new(enabled: bool, time: DateTime<Local>) -> Self {
Self { enabled, time }
impl From<model::alarm::Model> for Alarm {
fn from(value: model::alarm::Model) -> Self {
Self {
id: value.id,
enabled: value.enabled,
time: DateTime::from_naive_utc_and_offset(value.time, Utc),
title: value.title,
}
}
}