Compare commits
58 Commits
a479f5da7c
...
develop_mh
| Author | SHA1 | Date | |
|---|---|---|---|
| a117d417f4 | |||
| 32842d4b6e | |||
| 703a279ffa | |||
| 0d388b8c42 | |||
| ba25fada8e | |||
| ac06cd281f | |||
| a98aab4f45 | |||
| 36bea152fd | |||
| 90f89e08b8 | |||
| 7a99cd1dda | |||
| 9badb4dcd1 | |||
| 05f610bd86 | |||
| 91aefdba24 | |||
| fc61bc87f6 | |||
| 02932e8b79 | |||
| f7cedfd799 | |||
| 07c5d0fa2a | |||
| 69c98034d6 | |||
| 2f237acaec | |||
| 55715a20eb | |||
| 29841e2d15 | |||
| faaf8677e3 | |||
| ec259c981c | |||
| d4d30f1982 | |||
| 60138964cf | |||
| 9ce0efdb8c | |||
| 0047ed9587 | |||
| d9ad314b62 | |||
| 40f51173aa | |||
| e53449e001 | |||
| bdd562f0dd | |||
| cda965cb81 | |||
| 77c840064c | |||
| 31663fb9df | |||
| cbf28071b0 | |||
| 760561b10e | |||
| 2434c5b7aa | |||
| 9291a5d1db | |||
| ddb0d6a699 | |||
| a9b0fe80e4 | |||
| fbf93e80ae | |||
| 95a126953b | |||
| 30aa429c76 | |||
| 423460df66 | |||
| cb19bc990d | |||
| a958915d41 | |||
| 9ca6e10ac1 | |||
| def8d28c3b | |||
| cf3dae63f3 | |||
| f7131c827f | |||
| 32d0ac69c9 | |||
| eb3bc33509 | |||
| cd7620672f | |||
| 4a3d0313a2 | |||
| 8041453c5e | |||
| 6209b926e2 | |||
| 09068d9f70 | |||
| fed1b75fd2 |
12
.idea/dataSources.xml
generated
Normal file
12
.idea/dataSources.xml
generated
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
||||||
|
<data-source source="LOCAL" name="development.sqlite3" uuid="d3ab71a5-ebb0-4573-9e48-bc4bd013b015">
|
||||||
|
<driver-ref>sqlite.xerial</driver-ref>
|
||||||
|
<synchronize>true</synchronize>
|
||||||
|
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
|
||||||
|
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/storage/development.sqlite3</jdbc-url>
|
||||||
|
<working-dir>$ProjectFileDir$</working-dir>
|
||||||
|
</data-source>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
7
.idea/dictionaries/rubyon.xml
generated
Normal file
7
.idea/dictionaries/rubyon.xml
generated
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<component name="ProjectDictionaryState">
|
||||||
|
<dictionary name="rubyon">
|
||||||
|
<words>
|
||||||
|
<w>farmitry</w>
|
||||||
|
</words>
|
||||||
|
</dictionary>
|
||||||
|
</component>
|
||||||
3
.idea/farmitry_hvac.iml
generated
3
.idea/farmitry_hvac.iml
generated
@@ -53,6 +53,7 @@
|
|||||||
<orderEntry type="library" scope="PROVIDED" name="builder (v3.3.0, rbenv: 3.4.2) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="builder (v3.3.0, rbenv: 3.4.2) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="PROVIDED" name="bundler (v2.6.6, rbenv: 3.4.2) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="bundler (v2.6.6, rbenv: 3.4.2) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="PROVIDED" name="capybara (v3.40.0, rbenv: 3.4.2) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="capybara (v3.40.0, rbenv: 3.4.2) [gem]" level="application" />
|
||||||
|
<orderEntry type="library" scope="PROVIDED" name="ccutrer-serialport (v1.1.0, rbenv: 3.4.2) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="PROVIDED" name="concurrent-ruby (v1.3.5, rbenv: 3.4.2) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="concurrent-ruby (v1.3.5, rbenv: 3.4.2) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="PROVIDED" name="connection_pool (v2.5.0, rbenv: 3.4.2) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="connection_pool (v2.5.0, rbenv: 3.4.2) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="PROVIDED" name="crass (v1.0.6, rbenv: 3.4.2) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="crass (v1.0.6, rbenv: 3.4.2) [gem]" level="application" />
|
||||||
@@ -66,6 +67,7 @@
|
|||||||
<orderEntry type="library" scope="PROVIDED" name="erb_lint (v0.9.0, rbenv: 3.4.2) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="erb_lint (v0.9.0, rbenv: 3.4.2) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="PROVIDED" name="erubi (v1.13.1, rbenv: 3.4.2) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="erubi (v1.13.1, rbenv: 3.4.2) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="PROVIDED" name="et-orbi (v1.2.11, rbenv: 3.4.2) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="et-orbi (v1.2.11, rbenv: 3.4.2) [gem]" level="application" />
|
||||||
|
<orderEntry type="library" scope="PROVIDED" name="ffi (v1.17.2, rbenv: 3.4.2) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="PROVIDED" name="foreman (v0.88.1, rbenv: 3.4.2) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="foreman (v0.88.1, rbenv: 3.4.2) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="PROVIDED" name="fugit (v1.11.1, rbenv: 3.4.2) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="fugit (v1.11.1, rbenv: 3.4.2) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="PROVIDED" name="globalid (v1.2.1, rbenv: 3.4.2) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="globalid (v1.2.1, rbenv: 3.4.2) [gem]" level="application" />
|
||||||
@@ -129,6 +131,7 @@
|
|||||||
<orderEntry type="library" scope="PROVIDED" name="rubocop-rails-omakase (v1.1.0, rbenv: 3.4.2) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="rubocop-rails-omakase (v1.1.0, rbenv: 3.4.2) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="PROVIDED" name="ruby-progressbar (v1.13.0, rbenv: 3.4.2) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="ruby-progressbar (v1.13.0, rbenv: 3.4.2) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="PROVIDED" name="rubyzip (v2.4.1, rbenv: 3.4.2) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="rubyzip (v2.4.1, rbenv: 3.4.2) [gem]" level="application" />
|
||||||
|
<orderEntry type="library" scope="PROVIDED" name="rufus-scheduler (v3.9.2, rbenv: 3.4.2) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="PROVIDED" name="securerandom (v0.4.1, rbenv: 3.4.2) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="securerandom (v0.4.1, rbenv: 3.4.2) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="PROVIDED" name="selenium-webdriver (v4.31.0, rbenv: 3.4.2) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="selenium-webdriver (v4.31.0, rbenv: 3.4.2) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="PROVIDED" name="smart_properties (v1.17.0, rbenv: 3.4.2) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="smart_properties (v1.17.0, rbenv: 3.4.2) [gem]" level="application" />
|
||||||
|
|||||||
6
.idea/sqldialects.xml
generated
Normal file
6
.idea/sqldialects.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="SqlDialectMappings">
|
||||||
|
<file url="file://$PROJECT_DIR$/app/controllers/schedules_controller.rb" dialect="GenericSQL" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
2
Gemfile
2
Gemfile
@@ -69,4 +69,6 @@ group :test do
|
|||||||
gem "selenium-webdriver"
|
gem "selenium-webdriver"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
gem "ccutrer-serialport"
|
||||||
gem "rmodbus"
|
gem "rmodbus"
|
||||||
|
gem "rufus-scheduler"
|
||||||
|
|||||||
26
Gemfile.lock
26
Gemfile.lock
@@ -102,6 +102,8 @@ GEM
|
|||||||
rack-test (>= 0.6.3)
|
rack-test (>= 0.6.3)
|
||||||
regexp_parser (>= 1.5, < 3.0)
|
regexp_parser (>= 1.5, < 3.0)
|
||||||
xpath (~> 3.2)
|
xpath (~> 3.2)
|
||||||
|
ccutrer-serialport (1.1.0)
|
||||||
|
ffi (~> 1.9, >= 1.9.3)
|
||||||
concurrent-ruby (1.3.5)
|
concurrent-ruby (1.3.5)
|
||||||
connection_pool (2.5.0)
|
connection_pool (2.5.0)
|
||||||
crass (1.0.6)
|
crass (1.0.6)
|
||||||
@@ -126,6 +128,9 @@ GEM
|
|||||||
erubi (1.13.1)
|
erubi (1.13.1)
|
||||||
et-orbi (1.2.11)
|
et-orbi (1.2.11)
|
||||||
tzinfo
|
tzinfo
|
||||||
|
ffi (1.17.2-aarch64-linux-gnu)
|
||||||
|
ffi (1.17.2-arm64-darwin)
|
||||||
|
ffi (1.17.2-x86_64-linux-gnu)
|
||||||
foreman (0.88.1)
|
foreman (0.88.1)
|
||||||
fugit (1.11.1)
|
fugit (1.11.1)
|
||||||
et-orbi (~> 1, >= 1.2.11)
|
et-orbi (~> 1, >= 1.2.11)
|
||||||
@@ -189,18 +194,10 @@ GEM
|
|||||||
nio4r (2.7.4)
|
nio4r (2.7.4)
|
||||||
nokogiri (1.18.7-aarch64-linux-gnu)
|
nokogiri (1.18.7-aarch64-linux-gnu)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.7-aarch64-linux-musl)
|
|
||||||
racc (~> 1.4)
|
|
||||||
nokogiri (1.18.7-arm-linux-gnu)
|
|
||||||
racc (~> 1.4)
|
|
||||||
nokogiri (1.18.7-arm-linux-musl)
|
|
||||||
racc (~> 1.4)
|
|
||||||
nokogiri (1.18.7-arm64-darwin)
|
nokogiri (1.18.7-arm64-darwin)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.7-x86_64-linux-gnu)
|
nokogiri (1.18.7-x86_64-linux-gnu)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.7-x86_64-linux-musl)
|
|
||||||
racc (~> 1.4)
|
|
||||||
ostruct (0.6.1)
|
ostruct (0.6.1)
|
||||||
parallel (1.27.0)
|
parallel (1.27.0)
|
||||||
parser (3.3.8.0)
|
parser (3.3.8.0)
|
||||||
@@ -300,6 +297,8 @@ GEM
|
|||||||
rubocop-rails (>= 2.30)
|
rubocop-rails (>= 2.30)
|
||||||
ruby-progressbar (1.13.0)
|
ruby-progressbar (1.13.0)
|
||||||
rubyzip (2.4.1)
|
rubyzip (2.4.1)
|
||||||
|
rufus-scheduler (3.9.2)
|
||||||
|
fugit (~> 1.1, >= 1.11.1)
|
||||||
securerandom (0.4.1)
|
securerandom (0.4.1)
|
||||||
selenium-webdriver (4.31.0)
|
selenium-webdriver (4.31.0)
|
||||||
base64 (~> 0.2)
|
base64 (~> 0.2)
|
||||||
@@ -325,12 +324,8 @@ GEM
|
|||||||
railties (>= 7.1)
|
railties (>= 7.1)
|
||||||
thor (~> 1.3.1)
|
thor (~> 1.3.1)
|
||||||
sqlite3 (2.6.0-aarch64-linux-gnu)
|
sqlite3 (2.6.0-aarch64-linux-gnu)
|
||||||
sqlite3 (2.6.0-aarch64-linux-musl)
|
|
||||||
sqlite3 (2.6.0-arm-linux-gnu)
|
|
||||||
sqlite3 (2.6.0-arm-linux-musl)
|
|
||||||
sqlite3 (2.6.0-arm64-darwin)
|
sqlite3 (2.6.0-arm64-darwin)
|
||||||
sqlite3 (2.6.0-x86_64-linux-gnu)
|
sqlite3 (2.6.0-x86_64-linux-gnu)
|
||||||
sqlite3 (2.6.0-x86_64-linux-musl)
|
|
||||||
sshkit (1.24.0)
|
sshkit (1.24.0)
|
||||||
base64
|
base64
|
||||||
logger
|
logger
|
||||||
@@ -374,19 +369,15 @@ GEM
|
|||||||
PLATFORMS
|
PLATFORMS
|
||||||
aarch64-linux
|
aarch64-linux
|
||||||
aarch64-linux-gnu
|
aarch64-linux-gnu
|
||||||
aarch64-linux-musl
|
|
||||||
arm-linux-gnu
|
|
||||||
arm-linux-musl
|
|
||||||
arm64-darwin-24
|
arm64-darwin-24
|
||||||
x86_64-linux
|
x86_64-linux
|
||||||
x86_64-linux-gnu
|
|
||||||
x86_64-linux-musl
|
|
||||||
|
|
||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
amazing_print
|
amazing_print
|
||||||
bootsnap
|
bootsnap
|
||||||
brakeman
|
brakeman
|
||||||
capybara
|
capybara
|
||||||
|
ccutrer-serialport
|
||||||
cssbundling-rails
|
cssbundling-rails
|
||||||
debug
|
debug
|
||||||
erb_lint
|
erb_lint
|
||||||
@@ -399,6 +390,7 @@ DEPENDENCIES
|
|||||||
rails (~> 8.0.2)
|
rails (~> 8.0.2)
|
||||||
rmodbus
|
rmodbus
|
||||||
rubocop-rails-omakase
|
rubocop-rails-omakase
|
||||||
|
rufus-scheduler
|
||||||
selenium-webdriver
|
selenium-webdriver
|
||||||
solid_cable
|
solid_cable
|
||||||
solid_cache
|
solid_cache
|
||||||
|
|||||||
@@ -9,7 +9,14 @@
|
|||||||
--color-warning: rgb(255, 202, 40);
|
--color-warning: rgb(255, 202, 40);
|
||||||
--color-accept: rgb(121, 134, 203);
|
--color-accept: rgb(121, 134, 203);
|
||||||
--color-danger: rgb(239, 83, 80);
|
--color-danger: rgb(239, 83, 80);
|
||||||
--color-table-border: rgb(203, 213, 225);
|
--color-table-border: rgb(130, 144, 158);
|
||||||
|
|
||||||
|
/* theme */
|
||||||
|
--color-logo-color: rgb(21, 128, 120);
|
||||||
|
--color-base-background: rgb(39, 44, 56);
|
||||||
|
--color-base-secondary: rgb(55, 61, 71);
|
||||||
|
--color-base-text: rgb(255, 255, 255);
|
||||||
|
--color-base-border: rgb(77, 84, 102);
|
||||||
|
|
||||||
--base-font-size: 0.85rem;
|
--base-font-size: 0.85rem;
|
||||||
--text-xs: calc(var(--base-font-size) * 0.75);
|
--text-xs: calc(var(--base-font-size) * 0.75);
|
||||||
@@ -42,13 +49,23 @@
|
|||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
body {
|
body {
|
||||||
@apply text-sm text-default-slate-dark
|
@apply bg-base-background text-base-text font-['Pretendard-Regular']
|
||||||
}
|
}
|
||||||
|
|
||||||
hr {
|
hr {
|
||||||
@apply border-table-border
|
@apply border-table-border
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.background-pattern {
|
||||||
|
background-image: repeating-linear-gradient(
|
||||||
|
-45deg,
|
||||||
|
rgba(255, 255, 255, 0.01) 0,
|
||||||
|
rgba(255, 255, 255, 0.01) 24px,
|
||||||
|
transparent 1px,
|
||||||
|
transparent 36px
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
a.disabled {
|
a.disabled {
|
||||||
@apply pointer-events-none cursor-not-allowed opacity-50
|
@apply pointer-events-none cursor-not-allowed opacity-50
|
||||||
}
|
}
|
||||||
@@ -58,7 +75,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
@apply justify-center rounded-md bg-default-slate px-4 py-2 text-sm font-semibold leading-6 text-white
|
@apply justify-center rounded-md bg-default-slate px-4 py-2 font-semibold leading-6 text-white
|
||||||
shadow-xs hover:opacity-80 cursor-pointer
|
shadow-xs hover:opacity-80 cursor-pointer
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,16 +89,29 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.select-style {
|
.select-style {
|
||||||
@apply block w-full h-8 px-3 py-1.5 border border-default-slate/50 rounded-xs
|
@apply block w-full px-2 py-1.5 border border-default-slate/50 rounded-xs text-base-background appearance-none
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-style option {
|
||||||
|
color: theme('colors.base-background');
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-style {
|
.input-style {
|
||||||
@apply block w-full h-8 px-3 py-1.5 border border-default-slate/50 rounded-xs
|
@apply block w-full px-2 py-1.5 border border-default-slate/50 rounded-xs
|
||||||
}
|
}
|
||||||
|
|
||||||
.textarea-style {
|
.textarea-style {
|
||||||
@apply block w-full pl-4 flex-1 py-1.5 ring-1 ring-inset ring-default-slate/50 rounded-xs
|
@apply block w-full pl-4 flex-1 py-1.5 ring-1 ring-inset ring-default-slate/50 rounded-xs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*Toggle Button*/
|
||||||
|
.toggle_style {
|
||||||
|
@apply w-14 h-8 bg-danger rounded-full peer-checked:bg-notice transition-all duration-300
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle_btn {
|
||||||
|
@apply absolute left-1 top-1 w-6 h-6 bg-white rounded-full transition-all duration-300 peer-checked:translate-x-full shadow-sm
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer utilities {
|
@layer utilities {
|
||||||
@@ -90,7 +120,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.side-bar {
|
.side-bar {
|
||||||
@apply w-64 px-4 text-lg font-bold divide-y divide-default-slate-dark;
|
/*@apply w-[16rem] pl-4 text-lg font-bold divide-y divide-base-border;*/
|
||||||
|
@apply w-[16rem] pl-4 pr-4 lg:pr-0 text-lg font-bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
@apply lg:rounded-lg bg-base-secondary;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-title {
|
.page-title {
|
||||||
@@ -115,11 +150,19 @@
|
|||||||
|
|
||||||
/*Menu*/
|
/*Menu*/
|
||||||
.menu-group {
|
.menu-group {
|
||||||
@apply flex flex-row py-6 px-4 gap-x-4 justify-between items-center w-full
|
@apply flex flex-row py-2 px-4 my-4 gap-x-4 justify-between items-center w-full
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-group-active {
|
||||||
|
@apply bg-base-secondary text-white rounded-md;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-group:hover {
|
||||||
|
@apply bg-base-secondary text-white transition-colors rounded-md;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-group-icon {
|
.menu-group-icon {
|
||||||
@apply py-1 text-sm text-default-slate-dark font-medium justify-center
|
@apply py-1 text-sm font-medium justify-center
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-group-name {
|
.menu-group-name {
|
||||||
@@ -134,6 +177,11 @@
|
|||||||
@apply text-default-slate-dark text-center
|
@apply text-default-slate-dark text-center
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*Dashboard*/
|
||||||
|
.dashboard-group {
|
||||||
|
@apply rounded-md bg-base-background h-32 p-4 flex flex-col justify-between cursor-pointer
|
||||||
|
}
|
||||||
|
|
||||||
/*Pagy*/
|
/*Pagy*/
|
||||||
.pagy {
|
.pagy {
|
||||||
@apply flex items-center justify-center space-x-1 font-semibold text-sm text-default-slate;
|
@apply flex items-center justify-center space-x-1 font-semibold text-sm text-default-slate;
|
||||||
@@ -169,15 +217,15 @@
|
|||||||
|
|
||||||
/*Base Table*/
|
/*Base Table*/
|
||||||
.base-table {
|
.base-table {
|
||||||
@apply min-w-full divide-y divide-table-border whitespace-nowrap border border-table-border
|
@apply min-w-full whitespace-nowrap border border-table-border
|
||||||
}
|
}
|
||||||
|
|
||||||
.base-table th {
|
.base-table th {
|
||||||
@apply bg-default px-2 py-2 text-left font-semibold text-default-slate-dark lg:table-cell
|
@apply bg-base-background px-2 py-2 text-left font-bold lg:table-cell
|
||||||
}
|
}
|
||||||
|
|
||||||
.base-table td {
|
.base-table td {
|
||||||
@apply border-t border-table-border px-2 py-1 text-default-slate lg:table-cell whitespace-nowrap
|
@apply border-t border-table-border px-2 py-1 lg:table-cell whitespace-nowrap
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,3 +238,49 @@
|
|||||||
@apply text-lg font-bold;
|
@apply text-lg font-bold;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 기본 스크롤바 스타일 */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background-color: rgba(21, 21, 21, 0.4);
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background-color: rgba(100, 100, 100, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-style::-webkit-outer-spin-button,
|
||||||
|
.input-style::-webkit-inner-spin-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-style[type="number"] {
|
||||||
|
-moz-appearance: textfield;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*Fonts*/
|
||||||
|
@font-face {
|
||||||
|
font-family: 'GongGothicMedium';
|
||||||
|
src: url('https://fastly.jsdelivr.net/gh/projectnoonnu/noonfonts_20-10@1.0/GongGothicLight.woff') format('woff');
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Pretendard-Regular';
|
||||||
|
src: url('https://fastly.jsdelivr.net/gh/Project-Noonnu/noonfonts_2107@1.1/Pretendard-Regular.woff') format('woff');
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
@@ -1,4 +1,11 @@
|
|||||||
class ApplicationController < ActionController::Base
|
class ApplicationController < ActionController::Base
|
||||||
# Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has.
|
# Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has.
|
||||||
allow_browser versions: :modern
|
allow_browser versions: :modern
|
||||||
|
|
||||||
|
before_action :set_controllers
|
||||||
|
|
||||||
|
private
|
||||||
|
def set_controllers
|
||||||
|
@controllers = Controller.all
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
2
app/controllers/device_controller.rb
Normal file
2
app/controllers/device_controller.rb
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
class DeviceController < ApplicationController
|
||||||
|
end
|
||||||
6
app/controllers/modals_controller.rb
Normal file
6
app/controllers/modals_controller.rb
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
class ModalsController < ApplicationController
|
||||||
|
def open
|
||||||
|
@schedule = Schedule.new
|
||||||
|
render partial: "partials/modals_open", locals: { type: params[:type], id: params[:id], close_button: params[:close_button] }
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,37 +1,2 @@
|
|||||||
class ModbusController < ApplicationController
|
class ModbusController < ApplicationController
|
||||||
def index
|
|
||||||
@schedule = Schedule.all
|
|
||||||
@modbus_running = Modbus::PollingService.running?
|
|
||||||
end
|
|
||||||
|
|
||||||
def start
|
|
||||||
Modbus::PollingService.start
|
|
||||||
redirect_to modbus_index_path
|
|
||||||
end
|
|
||||||
|
|
||||||
def stop
|
|
||||||
Modbus::PollingService.stop
|
|
||||||
redirect_to modbus_index_path
|
|
||||||
end
|
|
||||||
|
|
||||||
def schedule_edit
|
|
||||||
@schedule = Schedule.all
|
|
||||||
end
|
|
||||||
|
|
||||||
def schedule_edit_update
|
|
||||||
error_hours = []
|
|
||||||
|
|
||||||
params[:schedule].each do |id, attributes|
|
|
||||||
schedule = Schedule.find_by(id: id)
|
|
||||||
unless schedule.update(temperature: attributes[:temperature])
|
|
||||||
error_hours << "#{schedule.hour}시"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if error_hours.any?
|
|
||||||
redirect_to modbus_index_path, alert: "#{error_hours.join(', ')}의 온도 업데이트에 실패하였습니다."
|
|
||||||
else
|
|
||||||
redirect_to modbus_index_path, notice: "스케줄이 성공적으로 업데이트되었습니다."
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
80
app/controllers/schedules_controller.rb
Normal file
80
app/controllers/schedules_controller.rb
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
class SchedulesController < ApplicationController
|
||||||
|
def index
|
||||||
|
@controllers = Controller.all
|
||||||
|
end
|
||||||
|
|
||||||
|
def view
|
||||||
|
@controller = Controller.find(params[:id])
|
||||||
|
@schedule = Schedule.where(controller_id: params[:id]).order(:hour, :minute)
|
||||||
|
end
|
||||||
|
|
||||||
|
def new
|
||||||
|
@schedule = Schedule.new
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
@schedule = Schedule.new(schedule_params)
|
||||||
|
|
||||||
|
if @schedule.save
|
||||||
|
redirect_to schedule_edit_schedule_path(@schedule.controller_id), notice: "스케쥴이 추가 되었습니다."
|
||||||
|
else
|
||||||
|
error_messages = @schedule.errors.full_messages.join(", ")
|
||||||
|
redirect_to schedule_edit_schedule_path(@schedule.controller_id), alert: "스케쥴 추가 실패: #{error_messages}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
@schedule = Schedule.find_by(id: params[:id])
|
||||||
|
|
||||||
|
if @schedule.destroy
|
||||||
|
redirect_to schedule_edit_schedule_path(@schedule.controller_id), notice: "스케줄이 삭제되었습니다."
|
||||||
|
else
|
||||||
|
error_messages = @schedule.errors.full_messages.join(", ")
|
||||||
|
redirect_to schedule_edit_schedule_path(@schedule.controller_id), alert: "스케쥴 삭제 실패: #{error_messages}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def reset
|
||||||
|
Schedule.where(controller_id: params[:id]).destroy_all
|
||||||
|
ActiveRecord::Base.connection.execute("DELETE FROM sqlite_sequence WHERE name='schedules'")
|
||||||
|
|
||||||
|
redirect_to schedule_edit_schedule_path(params[:id]), notice: "스케줄이 초기화되었습니다."
|
||||||
|
end
|
||||||
|
|
||||||
|
def schedule_edit
|
||||||
|
@controller = Controller.find(params[:id])
|
||||||
|
@schedule = Schedule.where(controller_id: params[:id]).order(:hour, :minute)
|
||||||
|
end
|
||||||
|
|
||||||
|
def schedule_edit_update
|
||||||
|
error_messages = []
|
||||||
|
|
||||||
|
params[:schedule]&.each do |id, attributes|
|
||||||
|
schedule = Schedule.find_by(id: id)
|
||||||
|
next unless schedule
|
||||||
|
|
||||||
|
unless schedule.update(
|
||||||
|
hour: attributes[:hour],
|
||||||
|
minute: attributes[:minute],
|
||||||
|
is_active: attributes[:is_active],
|
||||||
|
temperature: attributes[:temperature]
|
||||||
|
)
|
||||||
|
|
||||||
|
error_detail = schedule.errors.full_messages.join(", ")
|
||||||
|
time_label = "#{attributes[:hour]}시 #{attributes[:minute]}분"
|
||||||
|
error_messages << "#{time_label} - #{error_detail}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if error_messages.any?
|
||||||
|
redirect_to schedule_edit_schedule_path(params[:controller_id]), alert: error_messages.join("<br>")
|
||||||
|
else
|
||||||
|
redirect_to schedule_edit_schedule_path(params[:controller_id]), notice: "스케줄이 성공적으로 업데이트되었습니다."
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def schedule_params
|
||||||
|
params.require(:schedule).permit(:controller_id, :hour, :minute, :is_active, :temperature)
|
||||||
|
end
|
||||||
|
end
|
||||||
2
app/controllers/web_controller.rb
Normal file
2
app/controllers/web_controller.rb
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
class WebController < ApplicationController
|
||||||
|
end
|
||||||
2
app/helpers/device_helper.rb
Normal file
2
app/helpers/device_helper.rb
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
module DeviceHelper
|
||||||
|
end
|
||||||
2
app/helpers/modals_helper.rb
Normal file
2
app/helpers/modals_helper.rb
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
module ModalsHelper
|
||||||
|
end
|
||||||
2
app/helpers/schedules_helper.rb
Normal file
2
app/helpers/schedules_helper.rb
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
module SchedulesHelper
|
||||||
|
end
|
||||||
2
app/helpers/web_helper.rb
Normal file
2
app/helpers/web_helper.rb
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
module WebHelper
|
||||||
|
end
|
||||||
@@ -7,5 +7,11 @@ import { application } from "./application"
|
|||||||
import HelloController from "./hello_controller"
|
import HelloController from "./hello_controller"
|
||||||
application.register("hello", HelloController)
|
application.register("hello", HelloController)
|
||||||
|
|
||||||
|
import ModalsController from "./modals_controller"
|
||||||
|
application.register("modals", ModalsController)
|
||||||
|
|
||||||
|
import SidebarController from "./sidebar_controller"
|
||||||
|
application.register("sidebar", SidebarController)
|
||||||
|
|
||||||
import TimerController from "./timer_controller"
|
import TimerController from "./timer_controller"
|
||||||
application.register("timer", TimerController)
|
application.register("timer", TimerController)
|
||||||
|
|||||||
52
app/javascript/controllers/modals_controller.js
Normal file
52
app/javascript/controllers/modals_controller.js
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { Controller } from "@hotwired/stimulus"
|
||||||
|
import { useClickOutside } from 'stimulus-use'
|
||||||
|
|
||||||
|
// Connects to data-controller="modals"
|
||||||
|
export default class extends Controller {
|
||||||
|
connect() {
|
||||||
|
// useClickOutside(this)
|
||||||
|
this.boundHandleKeydown = this.handleKeydown.bind(this)
|
||||||
|
document.addEventListener("keydown", this.boundHandleKeydown)
|
||||||
|
window.addEventListener('modals:close', this.close.bind(this))
|
||||||
|
|
||||||
|
const panelElement = document.getElementById("modals-panel")
|
||||||
|
const backdropElement = document.getElementById("modals-backdrop")
|
||||||
|
|
||||||
|
if (panelElement && backdropElement) {
|
||||||
|
setTimeout(() => {
|
||||||
|
panelElement.classList.remove('opacity-0', 'translate-y-4', 'sm:scale-95')
|
||||||
|
panelElement.classList.add('opacity-100', 'translate-y-0', 'sm:scale-100')
|
||||||
|
backdropElement.classList.remove('opacity-0')
|
||||||
|
backdropElement.classList.add('opacity-75')
|
||||||
|
}, 10) // 애니메이션이 바로 시작되도록 약간의 지연 추가
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
document.removeEventListener('keydown', this.boundHandleKeydown)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleKeydown(event) {
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
this.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
close(event) {
|
||||||
|
const panelElement = document.getElementById("modals-panel")
|
||||||
|
const backdropElement = document.getElementById("modals-backdrop")
|
||||||
|
|
||||||
|
if (panelElement && backdropElement) {
|
||||||
|
panelElement.classList.remove("opacity-100", "translate-y-0", "sm:scale-100")
|
||||||
|
panelElement.classList.add("opacity-0", "translate-y-4", "sm:scale-95")
|
||||||
|
|
||||||
|
backdropElement.classList.remove("opacity-75")
|
||||||
|
backdropElement.classList.add("opacity-0")
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
const modalsFrame = document.querySelector("turbo-frame#modals")
|
||||||
|
modalsFrame.innerHTML = ""
|
||||||
|
}, 300) // 닫힘 애니메이션 시간과 동일하게 300ms 지연 후 모달 제거
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
15
app/javascript/controllers/sidebar_controller.js
Normal file
15
app/javascript/controllers/sidebar_controller.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { Controller } from "@hotwired/stimulus"
|
||||||
|
|
||||||
|
export default class extends Controller {
|
||||||
|
static targets = ["menu", "overlay"]
|
||||||
|
|
||||||
|
toggle() {
|
||||||
|
this.menuTarget.classList.toggle("-translate-x-full")
|
||||||
|
this.overlayTarget.classList.toggle("hidden")
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.menuTarget.classList.add("-translate-x-full")
|
||||||
|
this.overlayTarget.classList.add("hidden")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Controller } from "@hotwired/stimulus"
|
import {Controller} from "@hotwired/stimulus"
|
||||||
|
|
||||||
// Connects to data-controller="timer"
|
// Connects to data-controller="timer"
|
||||||
export default class extends Controller {
|
export default class extends Controller {
|
||||||
@@ -15,8 +15,7 @@ export default class extends Controller {
|
|||||||
|
|
||||||
updateTime() {
|
updateTime() {
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
const formatted = `${now.getFullYear()}년 ${String(now.getMonth() + 1).padStart(2, '0')}월 ${String(now.getDate()).padStart(2, '0')}일 ` +
|
this.outputTarget.textContent = `${now.getFullYear()}년 ${String(now.getMonth() + 1).padStart(2, '0')}월 ${String(now.getDate()).padStart(2, '0')}일 ` +
|
||||||
`${String(now.getHours()).padStart(2, '0')}시 ${String(now.getMinutes()).padStart(2, '0')}분 ${String(now.getSeconds()).padStart(2, '0')}초`
|
`${String(now.getHours()).padStart(2, '0')}시 ${String(now.getMinutes()).padStart(2, '0')}분 ${String(now.getSeconds()).padStart(2, '0')}초`
|
||||||
this.outputTarget.textContent = formatted
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
3
app/models/controller.rb
Normal file
3
app/models/controller.rb
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
class Controller < ApplicationRecord
|
||||||
|
has_many :schedules
|
||||||
|
end
|
||||||
@@ -1,6 +1,11 @@
|
|||||||
class Schedule < ApplicationRecord
|
class Schedule < ApplicationRecord
|
||||||
|
belongs_to :controller
|
||||||
|
|
||||||
|
validates :hour, presence: { message: "시간을 입력하세요" }
|
||||||
|
validates :minute, presence: { message: "분을 입력하세요" }
|
||||||
validates :temperature,
|
validates :temperature,
|
||||||
presence: true,
|
presence: { message: "온도를 입력하세요" },
|
||||||
numericality: true,
|
numericality: { message: "온도는 숫자여야 합니다" },
|
||||||
format: { with: /\A\d+(\.\d)?\z/ }
|
format: { with: /\A\d+(\.\d)?\z/, message: "온도는 소수점 한 자리까지 입력 가능합니다" }
|
||||||
|
validates :minute, uniqueness: { scope: [ :controller_id, :hour ], message: "같은 시:분의 스케쥴이 이미 존재합니다" }
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
# Modbus::Service.new(plc_id: 1, type: "bit_coil", start_address: 0, end_address: 15).execute
|
|
||||||
module Modbus
|
|
||||||
module Commands
|
|
||||||
def bit_coil(slave)
|
|
||||||
read_bit(slave, :read_coils)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
# Modbus::Service.new(plc_id: 1, type: "bit_input", start_address: 0, end_address: 15).execute
|
|
||||||
module Modbus
|
|
||||||
module Commands
|
|
||||||
def bit_input(slave)
|
|
||||||
read_bit(slave, :discrete_inputs)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
# Modbus::Service.new(plc_id: 1, type: "word_holding_float32", start_address: 0, end_address: 15).execute
|
|
||||||
module Modbus
|
|
||||||
module Commands
|
|
||||||
def word_holding_float32(slave)
|
|
||||||
read_float(slave, :holding_registers)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
# Modbus::Service.new(plc_id: 1, type: "word_holding_int16", start_address: 16, end_address: 31).execute
|
|
||||||
module Modbus
|
|
||||||
module Commands
|
|
||||||
def word_holding_int16(slave)
|
|
||||||
read_int(slave, 16, :holding_registers)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
# Modbus::Service.new(plc_id: 1, type: "word_input_float32", start_address: 0, end_address: 15).execute
|
|
||||||
module Modbus
|
|
||||||
module Commands
|
|
||||||
def word_input_float32(slave)
|
|
||||||
read_float(slave, :input_registers)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
# Modbus::Service.new(plc_id: 1, type: "word_input_int16", start_address: 0, end_address: 15).execute
|
|
||||||
module Modbus
|
|
||||||
module Commands
|
|
||||||
def word_input_int16(slave)
|
|
||||||
read_int(slave, 16, :input_registers)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
# Modbus::Service.new(plc_id: 1, type: "write_bit", start_address: 0, value: 1).execute
|
|
||||||
module Modbus
|
|
||||||
module Commands
|
|
||||||
def write_bit(slave)
|
|
||||||
write_coil(slave)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
# Modbus::Service.new(plc_id: 1, type: "write_bit_toggle", start_address: 0).execute
|
|
||||||
module Modbus
|
|
||||||
module Commands
|
|
||||||
def write_bit_toggle(slave)
|
|
||||||
write_toggle_coil(slave)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
# Modbus::Service.new(plc_id: 1, type: "float32", start_address: 0, value: 123.45).execute
|
|
||||||
module Modbus
|
|
||||||
module Commands
|
|
||||||
def write_float32(slave)
|
|
||||||
write_float(slave)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
# Modbus::Service.new(plc_id: 1, type: "int16", start_address: 0, value: 123).execute
|
|
||||||
module Modbus
|
|
||||||
module Commands
|
|
||||||
def write_int16(slave)
|
|
||||||
write_int(slave, 16)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
# app/lib/modbus/operations.rb
|
|
||||||
module Modbus
|
|
||||||
module Operations
|
|
||||||
# write method
|
|
||||||
def write_toggle_coil(slave)
|
|
||||||
with_timeout do
|
|
||||||
target_value = slave.read_coils(@start_address, 1).first == 0 ? 1 : 0
|
|
||||||
slave.coils[@start_address] = (target_value == 1)
|
|
||||||
loop do
|
|
||||||
break if slave.read_coils(@start_address, 1).first == target_value
|
|
||||||
sleep 0.1
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def write_coil(slave)
|
|
||||||
with_timeout do
|
|
||||||
slave.coils[@start_address] = @value
|
|
||||||
loop do
|
|
||||||
break if slave.read_coils(@start_address, 1).first == @value
|
|
||||||
sleep 0.1
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def write_int(slave, bits)
|
|
||||||
with_timeout do
|
|
||||||
regs = slave.holding_registers
|
|
||||||
if bits == 16
|
|
||||||
value = @value.present? ? @value.to_i : 123
|
|
||||||
regs[@start_address] = value
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def write_float(slave)
|
|
||||||
with_timeout do
|
|
||||||
regs = slave.holding_registers
|
|
||||||
value = @value.present? ? @value.to_f : 123.45
|
|
||||||
regs[@start_address], regs[@start_address + 1] = [ value ].pack("f").unpack("v*")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# read method
|
|
||||||
def read_bit(slave, type)
|
|
||||||
with_timeout do
|
|
||||||
value = case type
|
|
||||||
when :read_coils
|
|
||||||
slave.read_coils(@start_address, @end_address - @start_address + 1)
|
|
||||||
when :discrete_inputs
|
|
||||||
slave.discrete_inputs[@start_address..@end_address]
|
|
||||||
else
|
|
||||||
# type code here
|
|
||||||
end
|
|
||||||
Rails.logger.info "[#{Time.current}] Read Bit (#{type}): #{value}"
|
|
||||||
value
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def read_int(slave, bits, type)
|
|
||||||
with_timeout do
|
|
||||||
result = slave.send(type)
|
|
||||||
values = (@start_address..@end_address).map { |i| result[i] }
|
|
||||||
Rails.logger.info "[#{Time.current}] Read Float#{bits} (#{type}): #{values}"
|
|
||||||
values
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def read_float(slave, type)
|
|
||||||
with_timeout do
|
|
||||||
result = slave.send(type)
|
|
||||||
values = []
|
|
||||||
(@start_address..@end_address).step(2) do |i|
|
|
||||||
raw = [ result[i], result[i + 1] ]
|
|
||||||
packed = raw.pack("v*")
|
|
||||||
float = packed.unpack("f")[0].round(2)
|
|
||||||
values << float
|
|
||||||
end
|
|
||||||
Rails.logger.info "[#{Time.current}] Read Float32 (#{type}): #{values.inspect}"
|
|
||||||
values
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def with_timeout
|
|
||||||
seconds = ENV.fetch("OPERATION_TIME_OUT", 1).to_i
|
|
||||||
Timeout.timeout(seconds) do
|
|
||||||
yield
|
|
||||||
end
|
|
||||||
rescue Timeout::Error
|
|
||||||
Rails.logger.error "[#{Time.current}] TimeOut (#{seconds}s)"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,49 +1,60 @@
|
|||||||
|
# polling_service.rb
|
||||||
|
require "rufus-scheduler"
|
||||||
|
|
||||||
module Modbus
|
module Modbus
|
||||||
class PollingService
|
class PollingService
|
||||||
class << self
|
class << self
|
||||||
def start
|
def start
|
||||||
return if $modbus_polling_threads.any?(&:alive?)
|
return if @scheduler
|
||||||
|
|
||||||
thread = Thread.new do
|
puts "[#{Time.now}] Modbus polling service 시작됨 (Rufus)"
|
||||||
puts "[#{Time.current}] Modbus polling service 시작됨"
|
@scheduler ||= Rufus::Scheduler.new
|
||||||
last_logged_hour = nil
|
|
||||||
loop do
|
@scheduler.cron "0 * * * * *" do
|
||||||
begin
|
|
||||||
now = Time.now
|
now = Time.now
|
||||||
current_hour = now.hour
|
current_time = format("%02d:%02d", now.hour, now.min)
|
||||||
|
puts "# current time: #{current_time}.#{now.sec}"
|
||||||
if current_hour != last_logged_hour && now.min == 0
|
schedules = Schedule.where(hour: now.hour, minute: now.min)
|
||||||
schedule = Schedule.find_by(hour: current_hour)
|
schedules.each { |schedule| apply_schedule(schedule) }
|
||||||
|
|
||||||
Modbus::Service.new(
|
|
||||||
type: "write_int16",
|
|
||||||
start_address: 2,
|
|
||||||
end_address: 2,
|
|
||||||
value: schedule.temperature * 10
|
|
||||||
).execute
|
|
||||||
puts "[Schedule] #{current_hour}:00 -> Target temp: #{schedule.temperature}°C"
|
|
||||||
last_logged_hour = current_hour
|
|
||||||
end
|
end
|
||||||
rescue StandardError => e
|
end
|
||||||
error_message = "[#{Time.current}] 오류: #{e.message}"
|
|
||||||
|
private
|
||||||
|
def apply_schedule(schedule)
|
||||||
|
if schedule.is_active
|
||||||
|
run_script(
|
||||||
|
"power",
|
||||||
|
"#{schedule.controller.station_id}",
|
||||||
|
"0",
|
||||||
|
"[Schedule] #{schedule.controller.name} ON"
|
||||||
|
)
|
||||||
|
sleep 2
|
||||||
|
run_script(
|
||||||
|
"temp",
|
||||||
|
"#{schedule.controller.station_id}",
|
||||||
|
(schedule.temperature * 10).to_i.to_s,
|
||||||
|
"[Schedule] #{schedule.controller.name} #{format('%02d:%02d', schedule.hour, schedule.minute)} → #{schedule.temperature}°C"
|
||||||
|
)
|
||||||
|
else
|
||||||
|
run_script(
|
||||||
|
"power",
|
||||||
|
"#{schedule.controller.station_id}",
|
||||||
|
"1",
|
||||||
|
"[Schedule] #{schedule.controller.name} OFF"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def run_script(mode, controller_id, value, success_msg)
|
||||||
|
if system("ruby", "modbus.rb", mode, controller_id, value)
|
||||||
|
puts success_msg
|
||||||
|
Rails.logger.info success_msg
|
||||||
|
else
|
||||||
|
error_message = "[#{Time.now}] 실행 실패 (station_id: #{controller_id}, value: #{value})"
|
||||||
puts error_message
|
puts error_message
|
||||||
ensure
|
Rails.logger.error error_message
|
||||||
sleep 0.1
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
$modbus_polling_threads << thread
|
|
||||||
end
|
|
||||||
|
|
||||||
def stop
|
|
||||||
$modbus_polling_threads.each { |t| t.kill if t.alive? }
|
|
||||||
$modbus_polling_threads.clear
|
|
||||||
puts "[#{Time.now}] Modbus polling service 중지됨"
|
|
||||||
end
|
|
||||||
|
|
||||||
def running?
|
|
||||||
$modbus_polling_threads.any?(&:alive?)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
require_relative "operations"
|
|
||||||
|
|
||||||
Dir[File.join(__dir__.to_s, "commands", "**", "*.rb")].each do |file|
|
|
||||||
load file
|
|
||||||
end
|
|
||||||
|
|
||||||
class Modbus::Service
|
|
||||||
include Modbus::Operations
|
|
||||||
include Modbus::Commands
|
|
||||||
|
|
||||||
def initialize(type:, start_address:, end_address: nil, value: nil)
|
|
||||||
@type = type
|
|
||||||
@start_address = start_address.to_i
|
|
||||||
@end_address = end_address&.to_i
|
|
||||||
@value = value
|
|
||||||
@slave_id = 1
|
|
||||||
@plc_host = "rubyon.co.kr"
|
|
||||||
@plc_port = 502
|
|
||||||
end
|
|
||||||
|
|
||||||
def execute
|
|
||||||
ModBus::TCPClient.new(@plc_host, @plc_port) do |client|
|
|
||||||
client.with_slave(@slave_id) do |slave|
|
|
||||||
method_name = @type.strip
|
|
||||||
# Commands 모듈에 정의된 메서드만 허용
|
|
||||||
if Modbus::Commands.instance_methods(false).include?(method_name.to_sym)
|
|
||||||
return send(method_name, slave)
|
|
||||||
else
|
|
||||||
available_commands = Modbus::Commands.instance_methods(false).sort.map(&:to_s)
|
|
||||||
command_list = available_commands.join("\r\n")
|
|
||||||
raise ArgumentError, "지원되지 않는 type: #{@type}\r\n사용 가능한 명령어 목록:\r\n#{command_list}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
rescue => e
|
|
||||||
Rails.logger.error "[#{Time.current}] Service 오류: #{e.message}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
1
app/views/device/index.html.erb
Normal file
1
app/views/device/index.html.erb
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<div class="p-4">DEVICE</div>
|
||||||
@@ -23,15 +23,27 @@
|
|||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="h-screen overflow-hidden">
|
<body class="background-pattern min-h-screen min-w-full overflow-x-auto h-full whitespace-nowrap">
|
||||||
<main class="flex flex-col h-full divide-y divide-border-table-border">
|
<%= turbo_frame_tag :modals %>
|
||||||
|
<main class="flex flex-col h-full" data-controller="sidebar">
|
||||||
<%= render "partials/header" %>
|
<%= render "partials/header" %>
|
||||||
<div class="flex flex-row flex-1 w-full divide-x divide-border-table-border overflow-hidden">
|
<div class="flex flex-row flex-1 w-full">
|
||||||
<%= render "partials/sidebar" %>
|
<%= render "partials/sidebar" %>
|
||||||
<div class="w-full h-full">
|
<div class="flex-1 h-full pb-0 px-0 lg:pb-4 lg:px-4">
|
||||||
|
<div class="flex-1 h-full content">
|
||||||
|
<% if flash[:notice] %>
|
||||||
|
<div class="rounded-tl-md rounded-tr-md px-4 py-2 bg-accept text-white">
|
||||||
|
<%= raw flash[:notice] %>
|
||||||
|
</div>
|
||||||
|
<% elsif flash[:alert] %>
|
||||||
|
<div class="rounded-tl-md rounded-tr-md px-4 py-2 bg-danger text-white">
|
||||||
|
<%= raw flash[:alert] %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
<%= yield %>
|
<%= yield %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
50
app/views/layouts/device.html.erb
Normal file
50
app/views/layouts/device.html.erb
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title><%= content_for(:title) || "Farmitry Hvac" %></title>
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1">
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
|
<%= csrf_meta_tags %>
|
||||||
|
<%= csp_meta_tag %>
|
||||||
|
|
||||||
|
<%= yield :head %>
|
||||||
|
|
||||||
|
<%# Enable PWA manifest for installable apps (make sure to enable in config/routes.rb too!) %>
|
||||||
|
<%#= tag.link rel: "manifest", href: pwa_manifest_path(format: :json) %>
|
||||||
|
|
||||||
|
<link rel="icon" href="/icon.png" type="image/png">
|
||||||
|
<link rel="icon" href="/icon.svg" type="image/svg+xml">
|
||||||
|
<link rel="apple-touch-icon" href="/icon.png">
|
||||||
|
|
||||||
|
<%# Includes all stylesheet files in app/assets/stylesheets %>
|
||||||
|
<%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
|
||||||
|
<%= javascript_include_tag "application", "data-turbo-track": "reload", type: "module" %>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="h-screen overflow-hidden bg-base-background text-base-text min-w-[400px] overflow-x-auto">
|
||||||
|
<%= turbo_frame_tag :modals %>
|
||||||
|
<main class="flex flex-col h-full">
|
||||||
|
<%= render "partials/header" %>
|
||||||
|
<div class="flex flex-row flex-1 w-full overflow-hidden">
|
||||||
|
<%= render "partials/sidebar" %>
|
||||||
|
<div class="flex-1 h-full pb-4 pr-4">
|
||||||
|
<div class="flex-1 h-full content">
|
||||||
|
<% if flash[:notice] %>
|
||||||
|
<div class="m-4 rounded px-4 py-2 bg-accept text-white">
|
||||||
|
<%= raw flash[:notice] %>
|
||||||
|
</div>
|
||||||
|
<% elsif flash[:alert] %>
|
||||||
|
<div class="m-4 rounded px-4 py-2 bg-danger text-white">
|
||||||
|
<%= raw flash[:alert] %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<%= yield %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
47
app/views/layouts/web.html.erb
Normal file
47
app/views/layouts/web.html.erb
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title><%= content_for(:title) || "Farmitry Hvac" %></title>
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1">
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
|
<%= csrf_meta_tags %>
|
||||||
|
<%= csp_meta_tag %>
|
||||||
|
|
||||||
|
<%= yield :head %>
|
||||||
|
|
||||||
|
<%# Enable PWA manifest for installable apps (make sure to enable in config/routes.rb too!) %>
|
||||||
|
<%#= tag.link rel: "manifest", href: pwa_manifest_path(format: :json) %>
|
||||||
|
|
||||||
|
<link rel="icon" href="/icon.png" type="image/png">
|
||||||
|
<link rel="icon" href="/icon.svg" type="image/svg+xml">
|
||||||
|
<link rel="apple-touch-icon" href="/icon.png">
|
||||||
|
|
||||||
|
<%# Includes all stylesheet files in app/assets/stylesheets %>
|
||||||
|
<%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
|
||||||
|
<%= javascript_include_tag "application", "data-turbo-track": "reload", type: "module" %>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<%= turbo_frame_tag :modals %>
|
||||||
|
<main class="flex flex-col h-full divide-y divide-border-table-border">
|
||||||
|
<%= render "partials/header" %>
|
||||||
|
<div class="flex flex-row flex-1 w-full divide-x divide-border-table-border">
|
||||||
|
<div class="w-full h-full">
|
||||||
|
<% if flash[:notice] %>
|
||||||
|
<div class="m-4 rounded px-4 py-2 bg-accept text-white">
|
||||||
|
<%= raw flash[:notice] %>
|
||||||
|
</div>
|
||||||
|
<% elsif flash[:alert] %>
|
||||||
|
<div class="m-4 rounded px-4 py-2 bg-danger text-white">
|
||||||
|
<%= raw flash[:alert] %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<%= yield %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
<% if flash[:notice] %>
|
|
||||||
<div class="mb-4 rounded bg-green-100 px-4 py-2 text-green-800 border border-green-300">
|
|
||||||
<%= flash[:notice] %>
|
|
||||||
</div>
|
|
||||||
<% elsif flash[:alert] %>
|
|
||||||
<div class="mb-4 rounded bg-red-100 px-4 py-2 text-red-800 border border-red-300">
|
|
||||||
<%= flash[:alert] %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<div class="flex flex-col h-full divide-y divide-border-table-border">
|
|
||||||
<div class="flex-1 overflow-y-auto p-4 space-y-4">
|
|
||||||
<table class="base-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>시간</th>
|
|
||||||
<th>온도</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<% @schedule.each do |s| %>
|
|
||||||
<tr>
|
|
||||||
<td><%= s.hour %>시</td>
|
|
||||||
<td><%= s.temperature %> °C</td>
|
|
||||||
</tr>
|
|
||||||
<% end %>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<div>
|
|
||||||
<div class="flex gap-x-2">
|
|
||||||
<%= button_to "스케쥴러 시작", start_modbus_index_path, method: :post, class: "btn bg-accept" %>
|
|
||||||
<%= button_to "스케쥴러 정지", stop_modbus_index_path, method: :post, class: "btn bg-danger" %>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="py-4">
|
|
||||||
<span class="font-semibold">스케쥴러 상태:</span>
|
|
||||||
<% if @modbus_running %>
|
|
||||||
<span class="text-accept font-bold">실행 중</span>
|
|
||||||
<% else %>
|
|
||||||
<span class="text-danger font-bold">정지됨</span>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex p-4">
|
|
||||||
<%= link_to "수정", "/modbus/schedule_edit", class: "btn bg-default-slate" %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
<%= form_with url: schedule_edit_update_modbus_index_path, method: :post, class: 'flex flex-col h-full divide-y divide-border-table-border' do %>
|
|
||||||
<div class="flex-1 overflow-y-auto p-4">
|
|
||||||
<table class="base-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>시간</th>
|
|
||||||
<th>온도</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<% @schedule.each do |s| %>
|
|
||||||
<tr>
|
|
||||||
<td><%= s.hour %>시</td>
|
|
||||||
<td><%= number_field_tag "schedule[#{s.id}][temperature]", s.temperature, step: "0.1", inputmode: "decimal", class: "border px-2 py-1 rounded" %> °C</td>
|
|
||||||
</tr>
|
|
||||||
<% end %>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex p-4">
|
|
||||||
<%= submit_tag "업데이트", class: "btn bg-primary" %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
@@ -1,11 +1,14 @@
|
|||||||
<header class="flex header" data-controller="timer">
|
<header class="flex header justify-between items-center" data-controller="timer">
|
||||||
<div class="flex flex-row h-full">
|
<div class="flex flex-row h-full items-center">
|
||||||
<%= image_tag "svg/svg_logo.svg", class: "w-16 h-auto" %>
|
<%= image_tag "svg/svg_logo.svg", class: "w-auto h-8 mr-4" %>
|
||||||
<div class="flex h-full items-center tracking-[0.5rem]">FARMITRY</div>
|
<%= link_to root_path do %>
|
||||||
</div>
|
<div class="flex h-full items-center tracking-[0.5rem]"><span class="text-logo-color">FARM</span>ITRY</div>
|
||||||
<div data-timer-target="output" class="text-gray-600"></div>
|
<% end %>
|
||||||
<div class="flex flex-row items-center gap-x-4">
|
|
||||||
<i class="fa-solid fa-circle-user text-4xl text-default-slate-dark"></i>
|
|
||||||
<%= link_to "로그아웃", '', class: "btn" %>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div data-timer-target="output" class="hidden lg:block"></div>
|
||||||
|
|
||||||
|
<button data-action="click->sidebar#toggle" class="block lg:hidden p-2 cursor-pointer">
|
||||||
|
<i class="fa-solid fa-bars text-2xl"></i>
|
||||||
|
</button>
|
||||||
</header>
|
</header>
|
||||||
44
app/views/partials/_modals_open.html.erb
Normal file
44
app/views/partials/_modals_open.html.erb
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<%= turbo_frame_tag :modals do %>
|
||||||
|
<div class="relative z-50" aria-labelledby="modal-title" role="dialog" aria-modal="true">
|
||||||
|
<!--
|
||||||
|
Background backdrop, show/hide based on modal state.
|
||||||
|
|
||||||
|
Entering: "ease-out duration-300"
|
||||||
|
From: "opacity-0"
|
||||||
|
To: "opacity-100"
|
||||||
|
Leaving: "ease-in duration-200"
|
||||||
|
From: "opacity-100"
|
||||||
|
To: "opacity-0"
|
||||||
|
-->
|
||||||
|
<div id="modals-backdrop" class="fixed inset-0 bg-base-background transition-opacity opacity-0 ease-out duration-500" aria-hidden="true"></div>
|
||||||
|
|
||||||
|
<div class="fixed inset-0 z-50 w-screen overflow-y-auto py-4">
|
||||||
|
<div class="flex min-h-full items-center justify-center text-center sm:items-center">
|
||||||
|
<!--
|
||||||
|
Modal panel, show/hide based on modal state.
|
||||||
|
|
||||||
|
Entering: "ease-out duration-300"
|
||||||
|
From: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
To: "opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
Leaving: "ease-in duration-200"
|
||||||
|
From: "opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
To: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
-->
|
||||||
|
<div id="modals-panel" class="p-4 space-y-4 z-50 relative transform overflow-hidden rounded-md bg-base-secondary text-left shadow-xl transition-all w-full max-w-full mx-2 sm:mx-10 opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95 ease-out duration-500" data-controller="modals" data-action="modals:click:outside->modals#close">
|
||||||
|
<div>
|
||||||
|
<%= render "partials/modals/#{type}" %>
|
||||||
|
</div>
|
||||||
|
<% if close_button != "false" %>
|
||||||
|
<%= button_tag type: 'button',
|
||||||
|
data: {
|
||||||
|
action: 'modals#close'
|
||||||
|
},
|
||||||
|
class: 'inline-flex btn w-full' do %>
|
||||||
|
<div class="items-center"><i class="fa-solid fa-square-caret-down"></i> 닫기</div>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
@@ -1,39 +1,28 @@
|
|||||||
<nav class="side-bar overflow-y-auto">
|
<div data-sidebar-target="overlay" data-action="click->sidebar#close" class="fixed inset-0 bg-black/50 z-40 hidden lg:hidden"></div>
|
||||||
<div class="menu-group">
|
<nav class="side-bar overflow-y-auto transition-transform lg:translate-x-0 -translate-x-full bg-base-background lg:bg-transparent fixed lg:relative top-0 left-0 h-full z-40" data-sidebar-target="menu">
|
||||||
|
<%= link_to root_path, class: "menu-group #{'menu-group-active' if current_page?(root_path)}" do %>
|
||||||
<div class="menu-group-icon">
|
<div class="menu-group-icon">
|
||||||
<i class="fa-solid fa-house text-default-slate-dark text-4xl"></i>
|
<i class="fa-solid fa-chart-line text-base-text text-4xl"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="menu-group-name">
|
<div class="menu-group-name">
|
||||||
HOME
|
현황
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<% end %>
|
||||||
<div class="menu-group">
|
<hr >
|
||||||
|
<% @controllers.each do |c| %>
|
||||||
|
<%= link_to view_schedule_path(c.id), class: "menu-group #{'menu-group-active' if current_page?(view_schedule_path(c.id))}" do %>
|
||||||
<div class="menu-group-icon">
|
<div class="menu-group-icon">
|
||||||
<i class="fa-solid fa-temperature-high text-default-slate-dark text-4xl"></i>
|
<i class="fa-solid fa-temperature-high text-base-text text-4xl"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="menu-group-name">
|
<div class="menu-group-name">
|
||||||
메뉴 2
|
<%= c.name %>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<hr >
|
||||||
|
<% end %>
|
||||||
<div class="menu-group">
|
<div class="menu-group">
|
||||||
<div class="menu-group-icon">
|
<div class="menu-group-icon">
|
||||||
<i class="fa-solid fa-fan text-default-slate-dark text-4xl"></i>
|
<i class="fa-solid fa-gear text-base-text text-4xl"></i>
|
||||||
</div>
|
|
||||||
<div class="menu-group-name">
|
|
||||||
메뉴 3
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="menu-group">
|
|
||||||
<div class="menu-group-icon">
|
|
||||||
<i class="fa-solid fa-arrows-spin text-default-slate-dark text-4xl"></i>
|
|
||||||
</div>
|
|
||||||
<div class="menu-group-name">
|
|
||||||
메뉴 4
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="menu-group">
|
|
||||||
<div class="menu-group-icon">
|
|
||||||
<i class="fa-solid fa-gear text-default-slate-dark text-4xl"></i>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="menu-group-name">
|
<div class="menu-group-name">
|
||||||
설청
|
설청
|
||||||
|
|||||||
41
app/views/partials/modals/_add_schedule.html.erb
Normal file
41
app/views/partials/modals/_add_schedule.html.erb
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<%= form_with model: @schedule, method: :post, data: { turbo: false }, class: 'flex flex-col h-full space-y-4' do |form| %>
|
||||||
|
<%= form.hidden_field :controller_id, value: params[:id] %>
|
||||||
|
<div class="flex-1 text-white space-y-4">
|
||||||
|
<div class="text-2xl font-bold leading-10"><%= Controller.find(params[:id]).name %> 컨트롤러</div>
|
||||||
|
<table class="base-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>시간</th>
|
||||||
|
<th>분</th>
|
||||||
|
<th>ON/OFF</th>
|
||||||
|
<th>온도</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<%= form.select :hour,
|
||||||
|
options_for_select((0..23).map { |h| [h.to_s.rjust(2, '0'), h] }, @schedule.hour),
|
||||||
|
{},
|
||||||
|
class: "select-style text-white" %>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<%= form.number_field :minute,
|
||||||
|
min: 0, max: 59, step: 1, inputmode: "decimal", class: "input-style" %>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<%= check_box_tag "schedule[is_active]", "1", true %>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<%= form.number_field :temperature,
|
||||||
|
step: "0.1", inputmode: "decimal", class: "input-style" %>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex">
|
||||||
|
<%= submit_tag "추가", class: "btn bg-primary" %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
159
app/views/schedules/index.html.erb
Normal file
159
app/views/schedules/index.html.erb
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
<div class="flex-1 grid grid-cols-2 lg:grid-cols-4 gap-4 p-4">
|
||||||
|
<!-- 실내온도 -->
|
||||||
|
<div class="dashboard-group">
|
||||||
|
<div class="flex flex-col justify-between h-full">
|
||||||
|
<div class="text-xl">실내온도</div>
|
||||||
|
<div class="flex flex-row justify-between items-baseline w-full">
|
||||||
|
<i class="fa-solid fa-temperature-quarter text-base-text/80 text-4xl text-left w-full"></i>
|
||||||
|
<div class="items-baseline text-right w-full">
|
||||||
|
<span class="text-4xl font-['GongGothicMedium'] font-light">15.5</span>
|
||||||
|
<span class="text-2xl">°C</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 실내습도 -->
|
||||||
|
<div class="dashboard-group">
|
||||||
|
<div class="flex flex-col justify-between h-full">
|
||||||
|
<div class="text-xl">실내습도</div>
|
||||||
|
<div class="flex flex-row justify-between items-baseline w-full">
|
||||||
|
<i class="fa-solid fa-droplet text-base-text/80 text-4xl text-left w-full"></i>
|
||||||
|
<div class="items-baseline text-right w-full"><span class="text-4xl font-['GongGothicMedium']">60</span> <span class="text-2xl">%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- CO2 -->
|
||||||
|
<div class="dashboard-group">
|
||||||
|
<div class="flex flex-col justify-between h-full">
|
||||||
|
<div><span class="text-xl">CO</span><span class="text-sm">2</span></div>
|
||||||
|
<div class="flex flex-row justify-between items-baseline w-full">
|
||||||
|
<div class="size-10 bg-base-text/80 rounded-full text-base-background flex flex-col justify-center">
|
||||||
|
<div class="flex flex-row justify-center items-baseline">
|
||||||
|
<span class="text-xl">CO</span><span class="text-sm">2</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="items-baseline text-right"><span class="text-4xl font-['GongGothicMedium']">60</span> <span class="text-2xl">%</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- none -->
|
||||||
|
<div class="dashboard-group">1</div>
|
||||||
|
|
||||||
|
<!-- Fan -->
|
||||||
|
<div class="dashboard-group">
|
||||||
|
<div class="flex flex-col justify-between h-full">
|
||||||
|
<div class="text-xl">FAN</div>
|
||||||
|
<div class="flex flex-row justify-between items-baseline w-full">
|
||||||
|
<i class="fa-solid fa-fan text-base-text/80 text-4xl text-left w-full"></i>
|
||||||
|
<div class="items-baseline text-right w-full"><span class="text-4xl font-['GongGothicMedium']">60</span> <span class="text-2xl">%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 공조기 -->
|
||||||
|
<div class="dashboard-group">
|
||||||
|
<div class="flex flex-col justify-between h-full">
|
||||||
|
<div class="text-xl">공조기</div>
|
||||||
|
<div class="flex flex-row justify-between items-baseline w-full">
|
||||||
|
<i class="fa-solid fa-temperature-quarter text-base-text/80 text-4xl text-left w-full"></i>
|
||||||
|
<div class="items-baseline text-right w-full"><span class="text-4xl font-['GongGothicMedium']">15.5</span>
|
||||||
|
<span class="text-2xl">°C</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- LED -->
|
||||||
|
<div class="dashboard-group">
|
||||||
|
<div class="flex flex-col justify-between h-full">
|
||||||
|
<div class="text-xl">LED</div>
|
||||||
|
<div class="flex flex-row justify-between items-baseline w-full">
|
||||||
|
<i class="fa-solid fa-lightbulb text-base-text/80 text-4xl text-left w-full"></i>
|
||||||
|
<div class="items-baseline text-right w-full"><span class="text-4xl font-['GongGothicMedium']">60</span> <span class="text-2xl">%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- none -->
|
||||||
|
<div class="dashboard-group">1</div>
|
||||||
|
|
||||||
|
<!-- pH -->
|
||||||
|
<div class="dashboard-group">
|
||||||
|
<div class="flex justify-between items-baseline">
|
||||||
|
<div class="text-4xl">pH</div>
|
||||||
|
<div class="text-3xl font-['GongGothicMedium']">7.1</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between items-baseline">
|
||||||
|
<div class="text-2xl">
|
||||||
|
6.0 ~ 6.0
|
||||||
|
</div>
|
||||||
|
<div><i class="fa-solid fa-gear text-base-text/30 text-2xl"></i></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- EC -->
|
||||||
|
<div class="dashboard-group">
|
||||||
|
<div class="flex justify-between items-baseline">
|
||||||
|
<div class="text-4xl">EC</div>
|
||||||
|
<div class="text-3xl font-['GongGothicMedium']">0.1</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between items-baseline">
|
||||||
|
<div class="text-2xl">
|
||||||
|
0 ~ 1
|
||||||
|
</div>
|
||||||
|
<div><i class="fa-solid fa-gear text-base-text/30 text-2xl"></i></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 양액온도 -->
|
||||||
|
<div class="dashboard-group">
|
||||||
|
<div class="flex flex-col justify-between h-full">
|
||||||
|
<div class="text-xl">양액온도</div>
|
||||||
|
<div class="flex flex-row justify-between items-baseline w-full">
|
||||||
|
<!-- <i class="fa-solid fa-temperature-quarter text-base-text/80 text-4xl text-left w-full"></i>-->
|
||||||
|
<div class="items-baseline text-right w-full"><span class="text-4xl font-['GongGothicMedium']">20.96</span>
|
||||||
|
<span class="text-2xl">°C</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- none -->
|
||||||
|
<div class="dashboard-group">1</div>
|
||||||
|
|
||||||
|
<!-- none -->
|
||||||
|
<div class="dashboard-group">
|
||||||
|
<div class="flex flex-col justify-center space-y-4 h-full">
|
||||||
|
<i class="fa-solid fa-recycle text-base-text/80 text-5xl text-center w-full"></i>
|
||||||
|
<div class="text-xl text-center">자동화</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- none -->
|
||||||
|
<div class="dashboard-group">
|
||||||
|
<div class="flex flex-col justify-center space-y-4 h-full">
|
||||||
|
<i class="fa-solid fa-rotate-right text-base-text/80 text-5xl text-center w-full"></i>
|
||||||
|
<div class="text-xl text-center">초기화</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- none -->
|
||||||
|
<div class="dashboard-group">
|
||||||
|
<div class="flex flex-col justify-center space-y-4 h-full">
|
||||||
|
<i class="fa-solid fa-circle-pause text-base-text/80 text-5xl text-center w-full"></i>
|
||||||
|
<div class="text-xl text-center">긴급 정지</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- none -->
|
||||||
|
<div class="dashboard-group">
|
||||||
|
<div class="flex flex-col justify-center space-y-4 h-full">
|
||||||
|
<i class="fa-solid fa-faucet-drip text-base-text/80 text-5xl text-center w-full"></i>
|
||||||
|
<div class="text-xl text-center">1회 급수 설정</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
81
app/views/schedules/schedule_edit.html.erb
Normal file
81
app/views/schedules/schedule_edit.html.erb
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
<div class="flex flex-col h-full text-white overflow-y-auto">
|
||||||
|
<div class="flex flex-col flex-1 divide-y divide-base-border">
|
||||||
|
<%= form_with url: schedule_edit_update_schedules_path, method: :post, class: 'flex flex-col h-full' do %>
|
||||||
|
<%= hidden_field_tag :controller_id, params[:id] %>
|
||||||
|
<div class="divide-y divide-base-border">
|
||||||
|
<div class="flex justify-between p-4">
|
||||||
|
<div class="text-2xl font-bold leading-10"><%= @controller.name %> 컨트롤러</div>
|
||||||
|
<%= submit_tag "업데이트", class: "btn bg-primary" %>
|
||||||
|
</div>
|
||||||
|
<ul class="divide-y divide-base-border whitespace-nowrap h-full relative">
|
||||||
|
<% if @schedule.present? %>
|
||||||
|
<% @schedule.each do |s| %>
|
||||||
|
<li class="flex justify-between items-center px-4 py-2 space-x-4 h-full relative">
|
||||||
|
<div class="flex items-center space-x-4 basis-3/5 h-full">
|
||||||
|
<!-- 시간 -->
|
||||||
|
<div class="flex flex-col lg:flex-row h-full items-center space-x-0 space-y-2 lg:space-x-4 lg:space-y-0">
|
||||||
|
<div class="text-white text-left w-full lg:w-auto">시간</div>
|
||||||
|
<div class="text-xl flex items-center space-x-1 w-full h-full">
|
||||||
|
<%= select_tag "schedule[#{s.id}][hour]",
|
||||||
|
options_for_select((0..23).map { |h| [h.to_s.rjust(2, '0'), h] }, s.hour),
|
||||||
|
class: "select-style text-white min-w-[60px]" %>
|
||||||
|
<div>:</div>
|
||||||
|
<%= number_field_tag "schedule[#{s.id}][minute]",
|
||||||
|
s.minute, min: 0, max: 59, step: 1, inputmode: "decimal",
|
||||||
|
class: "input-style min-w-[60px]" %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 온도 -->
|
||||||
|
<div class="flex flex-col lg:flex-row h-full items-center space-x-0 space-y-2 lg:space-x-4 lg:space-y-0">
|
||||||
|
<div class="text-white text-left w-full lg:w-auto">온도 (°C)</div>
|
||||||
|
<div class="text-xl flex items-center">
|
||||||
|
<%= number_field_tag "schedule[#{s.id}][temperature]", s.temperature, min: -99, max: 99, step: "0.1", inputmode: "decimal", class: "input-style min-w-[68px]" %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center space-x-4 basis-2/5 justify-end h-full">
|
||||||
|
<!-- 사용 여부 -->
|
||||||
|
<div class="flex flex-col lg:flex-row items-center space-x-0 space-y-2 lg:space-x-4 lg:space-y-0 h-full">
|
||||||
|
<div class="text-white">ON/OFF</div>
|
||||||
|
<label class="relative inline-flex items-center cursor-pointer h-full">
|
||||||
|
<%= check_box_tag "schedule[#{s.id}][is_active]", "1", s.is_active == true || s.is_active == 1, class: "sr-only peer" %>
|
||||||
|
<div class="toggle_style"></div>
|
||||||
|
<div class="toggle_btn"></div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 삭제 -->
|
||||||
|
<div class="flex flex-col lg:flex-row items-center space-x-0 space-y-2 lg:space-x-4 lg:space-y-0 h-full relative">
|
||||||
|
<div class="text-white"> </div>
|
||||||
|
<%= link_to schedule_path(s),
|
||||||
|
data: { turbo_method: :delete, turbo_confirm: "정말 삭제하시겠습니까?" },
|
||||||
|
class: "btn bg-danger text-sm flex items-center px-2 py-0 my-1.5 lg:px-4 lg:py-1.5 lg:my-0" do %>
|
||||||
|
<div class="hidden lg:block">삭제</div>
|
||||||
|
<div class="block lg:hidden"><i class="fa-solid fa-xmark"></i></div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<% end %>
|
||||||
|
<% else %>
|
||||||
|
<li class="text-center p-4 w-full">저장된 스케줄이 없습니다.</li>
|
||||||
|
<% end %>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<div class="flex p-4 space-x-4">
|
||||||
|
<%= button_to "초기화", reset_schedule_path(params[:id]),
|
||||||
|
method: :post,
|
||||||
|
data: { turbo_confirm: "정말 초기화하시겠습니까? 모든 스케줄 데이터가 삭제됩니다." },
|
||||||
|
class: "btn bg-danger" %>
|
||||||
|
<%= button_to "추가하기", open_modals_path(type: "add_schedule", id: params[:id]),
|
||||||
|
class: "btn bg-default-slate",
|
||||||
|
data: {
|
||||||
|
turbo_method: :post,
|
||||||
|
turbo_frame: "modals"
|
||||||
|
} %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
74
app/views/schedules/view.html.erb
Normal file
74
app/views/schedules/view.html.erb
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
<div class="flex flex-col h-full divide-y divide-base-border text-white">
|
||||||
|
<div class="flex flex-col flex-1 space-y-4 overflow-y-hidden">
|
||||||
|
<div class="overflow-y-auto divide-y divide-base-border">
|
||||||
|
<div class="flex justify-between p-4">
|
||||||
|
<div class="text-2xl font-bold leading-10"><%= @controller.name %> 컨트롤러</div>
|
||||||
|
<%= link_to "수정", schedule_edit_schedule_path(@controller.id), class: "btn bg-default-slate" %>
|
||||||
|
</div>
|
||||||
|
<ul class="divide-y divide-base-border">
|
||||||
|
<% if @schedule.present? %>
|
||||||
|
<% @schedule.each do |s| %>
|
||||||
|
<li class="flex justify-between items-center px-4 py-2 space-x-4">
|
||||||
|
<div class="flex items-center space-x-6">
|
||||||
|
<!-- 시간 -->
|
||||||
|
<div class="flex flex-col lg:flex-row items-baseline space-x-2">
|
||||||
|
<div class="text-white">시간</div>
|
||||||
|
<div class="text-3xl flex items-baseline space-x-1">
|
||||||
|
<div class="text-center w-[36px]"><%= s.hour.to_s.rjust(2, '0') %></div>
|
||||||
|
<div>:</div>
|
||||||
|
<div class="text-center w-[36px]"><%= s.minute.to_s.rjust(2, '0') %></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 온도 -->
|
||||||
|
<div class="flex flex-col lg:flex-row items-baseline space-x-4">
|
||||||
|
<div class="text-white">온도</div>
|
||||||
|
<div class="text-3xl flex items-end space-x-2">
|
||||||
|
<div class="text-left w-[50px]"><%= s.temperature %></div>
|
||||||
|
<div class="text-white text-base leading-7">°C</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 사용 여부 -->
|
||||||
|
<div class="flex flex-col lg:flex-row items-baseline space-x-0 lg:space-x-4 items-end">
|
||||||
|
<div class="text-white mb-1">ON/OFF</div>
|
||||||
|
<div class="text-3xl w-8 mr-0">
|
||||||
|
<%#= s.is_active %>
|
||||||
|
<% if s.is_active %>
|
||||||
|
<i class="fa-solid fa-check text-notice"></i>
|
||||||
|
<% else %>
|
||||||
|
<i class="fa-solid fa-xmark text-danger"></i>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<% end %>
|
||||||
|
<% else %>
|
||||||
|
<li class="text-center p-4 w-full">저장된 스케줄이 없습니다.</li>
|
||||||
|
<% end %>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<!-- <table class="base-table">-->
|
||||||
|
<!-- <thead>-->
|
||||||
|
<!-- <tr>-->
|
||||||
|
<!-- <th>시간</th>-->
|
||||||
|
<!-- <th>분</th>-->
|
||||||
|
<!-- <th>사용여부</th>-->
|
||||||
|
<!-- <th>온도</th>-->
|
||||||
|
<!-- </tr>-->
|
||||||
|
<!-- </thead>-->
|
||||||
|
<!-- <tbody>-->
|
||||||
|
<%# @schedule.each do |s| %>
|
||||||
|
<!-- <tr>-->
|
||||||
|
<!-- <td><%#= s.hour %>시</td>-->
|
||||||
|
<!-- <td><%#= s.minute %>분</td>-->
|
||||||
|
<!-- <td><%#= s.is_active %></td>-->
|
||||||
|
<!-- <td><%#= s.temperature %> °C</td>-->
|
||||||
|
<!-- </tr>-->
|
||||||
|
<%# end %>
|
||||||
|
<!-- </tbody>-->
|
||||||
|
<!-- </table>-->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
1
app/views/web/index.html.erb
Normal file
1
app/views/web/index.html.erb
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<div>WEB</div>
|
||||||
@@ -28,5 +28,7 @@ module FarmitryHvac
|
|||||||
#
|
#
|
||||||
# config.time_zone = "Central Time (US & Canada)"
|
# config.time_zone = "Central Time (US & Canada)"
|
||||||
# config.eager_load_paths << Rails.root.join("extras")
|
# config.eager_load_paths << Rails.root.join("extras")
|
||||||
|
|
||||||
|
config.time_zone = "Asia/Seoul"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -10,14 +10,26 @@ Rails.application.routes.draw do
|
|||||||
# get "service-worker" => "rails/pwa#service_worker", as: :pwa_service_worker
|
# get "service-worker" => "rails/pwa#service_worker", as: :pwa_service_worker
|
||||||
|
|
||||||
# Defines the root path route ("/")
|
# Defines the root path route ("/")
|
||||||
root "modbus#index"
|
|
||||||
|
|
||||||
resources :modbus do
|
root "schedules#index"
|
||||||
collection do
|
|
||||||
post "start"
|
resources :schedules do
|
||||||
post "stop"
|
member do
|
||||||
|
get "view"
|
||||||
get "schedule_edit"
|
get "schedule_edit"
|
||||||
|
post "reset"
|
||||||
|
end
|
||||||
|
collection do
|
||||||
post "schedule_edit_update"
|
post "schedule_edit_update"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
resources :modals do
|
||||||
|
collection do
|
||||||
|
post :open
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
resources :device
|
||||||
|
resources :web
|
||||||
end
|
end
|
||||||
|
|||||||
10
db/migrate/20250416131430_create_controllers.rb
Normal file
10
db/migrate/20250416131430_create_controllers.rb
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
class CreateControllers < ActiveRecord::Migration[8.0]
|
||||||
|
def change
|
||||||
|
create_table :controllers do |t|
|
||||||
|
t.string :name
|
||||||
|
t.integer :station_id
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,10 +1,14 @@
|
|||||||
class CreateSchedules < ActiveRecord::Migration[8.0]
|
class CreateSchedules < ActiveRecord::Migration[8.0]
|
||||||
def change
|
def change
|
||||||
create_table :schedules do |t|
|
create_table :schedules do |t|
|
||||||
|
t.references :controller, null: false, foreign_key: true
|
||||||
t.integer :hour
|
t.integer :hour
|
||||||
|
t.integer :minute
|
||||||
|
t.boolean :is_active
|
||||||
t.float :temperature
|
t.float :temperature
|
||||||
|
|
||||||
t.timestamps
|
t.timestamps
|
||||||
end
|
end
|
||||||
|
add_index :schedules, [ :controller_id, :hour, :minute ], unique: true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
20
db/schema.rb
generated
20
db/schema.rb
generated
@@ -11,10 +11,24 @@
|
|||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[8.0].define(version: 2025_04_16_131440) do
|
ActiveRecord::Schema[8.0].define(version: 2025_04_16_131440) do
|
||||||
create_table "schedules", force: :cascade do |t|
|
create_table "controllers", force: :cascade do |t|
|
||||||
t.integer "hour"
|
t.string "name"
|
||||||
t.float "temperature"
|
t.integer "station_id"
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
create_table "schedules", force: :cascade do |t|
|
||||||
|
t.integer "controller_id", null: false
|
||||||
|
t.integer "hour"
|
||||||
|
t.integer "minute"
|
||||||
|
t.boolean "is_active"
|
||||||
|
t.float "temperature"
|
||||||
|
t.datetime "created_at", null: false
|
||||||
|
t.datetime "updated_at", null: false
|
||||||
|
t.index ["controller_id", "hour", "minute"], name: "index_schedules_on_controller_id_and_hour_and_minute", unique: true
|
||||||
|
t.index ["controller_id"], name: "index_schedules_on_controller_id"
|
||||||
|
end
|
||||||
|
|
||||||
|
add_foreign_key "schedules", "controllers"
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -8,6 +8,10 @@
|
|||||||
# MovieGenre.find_or_create_by!(name: genre_name)
|
# MovieGenre.find_or_create_by!(name: genre_name)
|
||||||
# end
|
# end
|
||||||
|
|
||||||
|
Controller.create(name: "3번방", station_id: 7)
|
||||||
|
Controller.create(name: "2번방", station_id: 8)
|
||||||
|
|
||||||
(0..23).each do | h |
|
(0..23).each do | h |
|
||||||
Schedule.create!(hour: h, temperature: 15.0)
|
Schedule.create!(controller_id: 1, hour: h, minute: 0, is_active: true, temperature: 15.0)
|
||||||
|
Schedule.create!(controller_id: 2, hour: h, minute: 0, is_active: true, temperature: 15.0)
|
||||||
end
|
end
|
||||||
|
|||||||
34
modbus.rb
Normal file
34
modbus.rb
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
require "rmodbus"
|
||||||
|
require "ccutrer-serialport"
|
||||||
|
require './sms/sms'
|
||||||
|
|
||||||
|
mode = ARGV[0]&.strip
|
||||||
|
controller_id = ARGV[1]&.to_i
|
||||||
|
value = ARGV[2]&.to_i
|
||||||
|
|
||||||
|
begin
|
||||||
|
ModBus::RTUClient.new("/dev/ttyUSB0", 9600) do |cl|
|
||||||
|
cl.with_slave(controller_id) do |slave|
|
||||||
|
regs = slave.holding_registers
|
||||||
|
case mode
|
||||||
|
when "temp"
|
||||||
|
regs[2] = value
|
||||||
|
when "power"
|
||||||
|
regs[22] = value
|
||||||
|
else
|
||||||
|
# type code here
|
||||||
|
end
|
||||||
|
sleep 0.1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
rescue
|
||||||
|
error_message = "[#{Time.now}] #{mode} 실행 실패 (station_id: #{controller_id}, value: #{value})"
|
||||||
|
res = Sms.send_one(
|
||||||
|
{
|
||||||
|
to: '01062619801',
|
||||||
|
from: '01062619801',
|
||||||
|
text: error_message
|
||||||
|
}
|
||||||
|
)
|
||||||
|
puts res
|
||||||
|
end
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
"@hotwired/stimulus": "^3.2.2",
|
"@hotwired/stimulus": "^3.2.2",
|
||||||
"@hotwired/turbo-rails": "^8.0.13",
|
"@hotwired/turbo-rails": "^8.0.13",
|
||||||
"@tailwindcss/cli": "^4.1.4",
|
"@tailwindcss/cli": "^4.1.4",
|
||||||
"tailwindcss": "^4.1.4"
|
"tailwindcss": "^4.1.4",
|
||||||
|
"stimulus-use": "^0.52.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
7
sms/config.json
Executable file
7
sms/config.json
Executable file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"api_key": "NCSK1QGPTBWMUKBO",
|
||||||
|
"api_secret": "RSGJ20CGUOTRKK33RFONFUFHY2SWMU9X",
|
||||||
|
"protocol": "https",
|
||||||
|
"domain": "api.coolsms.co.kr",
|
||||||
|
"prefix": ""
|
||||||
|
}
|
||||||
84
sms/request.rb
Executable file
84
sms/request.rb
Executable file
@@ -0,0 +1,84 @@
|
|||||||
|
require 'openssl'
|
||||||
|
require 'base64'
|
||||||
|
require 'net/http'
|
||||||
|
require 'json'
|
||||||
|
require 'securerandom'
|
||||||
|
|
||||||
|
module Request
|
||||||
|
@file = File.read File.join(File.dirname(__FILE__), './config.json')
|
||||||
|
@config = JSON.parse(@file)
|
||||||
|
|
||||||
|
def self.get_uri(path)
|
||||||
|
str = @config['protocol'] + '://' + @config['domain'] + @config['prefix']
|
||||||
|
puts str + path
|
||||||
|
uri = URI(str + path)
|
||||||
|
uri
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.header_get
|
||||||
|
api_key = @config['api_key']
|
||||||
|
api_secret = @config['api_secret']
|
||||||
|
date = Time.now.strftime('%Y-%m-%dT%H:%M:%S.%L%z')
|
||||||
|
salt = SecureRandom.hex
|
||||||
|
signature = OpenSSL::HMAC.hexdigest('SHA256', api_secret, date + salt)
|
||||||
|
'HMAC-SHA256 apiKey=' + api_key + ', date=' + date + ', salt=' + salt + ', signature=' + signature
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.post(path, body)
|
||||||
|
header = header_get
|
||||||
|
uri = get_uri(path)
|
||||||
|
http = Net::HTTP.new(uri.host, uri.port)
|
||||||
|
http.use_ssl = true
|
||||||
|
req = Net::HTTP::Post.new(uri.path, 'Content-Type' => 'application/json')
|
||||||
|
req.add_field('Authorization', header)
|
||||||
|
if body
|
||||||
|
req.body = body.to_json
|
||||||
|
end
|
||||||
|
res = http.request(req)
|
||||||
|
JSON.parse(res.body)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.put(path, body, headers = nil)
|
||||||
|
auth = header_get
|
||||||
|
uri = get_uri(path)
|
||||||
|
http = Net::HTTP.new(uri.host, uri.port)
|
||||||
|
http.use_ssl = true
|
||||||
|
req = Net::HTTP::Put.new(uri.path, 'Content-Type' => 'application/json')
|
||||||
|
req.add_field('Authorization', auth)
|
||||||
|
if headers
|
||||||
|
headers.each do |k, v|
|
||||||
|
req.add_field(k, v)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if body
|
||||||
|
req.body = body.to_json
|
||||||
|
end
|
||||||
|
res = http.request(req)
|
||||||
|
JSON.parse(res.body)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.get(path)
|
||||||
|
header = header_get
|
||||||
|
uri = get_uri(path)
|
||||||
|
http = Net::HTTP.new(uri.host, uri.port)
|
||||||
|
http.use_ssl = true
|
||||||
|
req = Net::HTTP::Get.new(uri.path, 'Content-Type' => 'application/json')
|
||||||
|
req.add_field('Authorization', header)
|
||||||
|
res = http.request(req)
|
||||||
|
JSON.parse(res.body)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.delete(path, body)
|
||||||
|
header = header_get
|
||||||
|
uri = get_uri(path)
|
||||||
|
http = Net::HTTP.new(uri.host, uri.port)
|
||||||
|
http.use_ssl = true
|
||||||
|
req = Net::HTTP::Delete.new(uri.path, 'Content-Type' => 'application/json')
|
||||||
|
req.add_field('Authorization', header)
|
||||||
|
res = http.request(req)
|
||||||
|
if body
|
||||||
|
req.body = body.to_json
|
||||||
|
end
|
||||||
|
JSON.parse(res.body)
|
||||||
|
end
|
||||||
|
end
|
||||||
11
sms/sms.rb
Executable file
11
sms/sms.rb
Executable file
@@ -0,0 +1,11 @@
|
|||||||
|
require_relative "request"
|
||||||
|
|
||||||
|
module Sms
|
||||||
|
def self.send_one(message)
|
||||||
|
Request.post("/messages/v4/send", { message: message })
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.send_many(messages)
|
||||||
|
Request.post("/messages/v4/send-many", { messages: messages })
|
||||||
|
end
|
||||||
|
end
|
||||||
7
start.sh
Executable file
7
start.sh
Executable file
@@ -0,0 +1,7 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
cd /home/farmitry/farmitry_hvac
|
||||||
|
export HOME=/home/farmitry
|
||||||
|
export RBENV_ROOT="$HOME/.rbenv"
|
||||||
|
export PATH="$RBENV_ROOT/bin:$PATH"
|
||||||
|
eval "$(rbenv init - bash)"
|
||||||
|
exec bin/dev
|
||||||
12
test.rb
Normal file
12
test.rb
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
require "rmodbus"
|
||||||
|
require "ccutrer-serialport"
|
||||||
|
|
||||||
|
value = ARGV[0]&.to_i
|
||||||
|
|
||||||
|
ModBus::RTUClient.new("/dev/ttyUSB0", 9600) do |cl|
|
||||||
|
cl.with_slave(7) do |slave|
|
||||||
|
regs = slave.holding_registers
|
||||||
|
regs[2] = value
|
||||||
|
sleep 0.1
|
||||||
|
end
|
||||||
|
end
|
||||||
7
test/controllers/device_controller_test.rb
Normal file
7
test/controllers/device_controller_test.rb
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class DeviceControllerTest < ActionDispatch::IntegrationTest
|
||||||
|
# test "the truth" do
|
||||||
|
# assert true
|
||||||
|
# end
|
||||||
|
end
|
||||||
7
test/controllers/modals_controller_test.rb
Normal file
7
test/controllers/modals_controller_test.rb
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class ModalsControllerTest < ActionDispatch::IntegrationTest
|
||||||
|
# test "the truth" do
|
||||||
|
# assert true
|
||||||
|
# end
|
||||||
|
end
|
||||||
7
test/controllers/schedules_controller_test.rb
Normal file
7
test/controllers/schedules_controller_test.rb
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class SchedulesControllerTest < ActionDispatch::IntegrationTest
|
||||||
|
# test "the truth" do
|
||||||
|
# assert true
|
||||||
|
# end
|
||||||
|
end
|
||||||
7
test/controllers/web_controller_test.rb
Normal file
7
test/controllers/web_controller_test.rb
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class WebControllerTest < ActionDispatch::IntegrationTest
|
||||||
|
# test "the truth" do
|
||||||
|
# assert true
|
||||||
|
# end
|
||||||
|
end
|
||||||
11
test/fixtures/controllers.yml
vendored
Normal file
11
test/fixtures/controllers.yml
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
|
||||||
|
|
||||||
|
# This model initially had no columns defined. If you add columns to the
|
||||||
|
# model remove the "{}" from the fixture names and add the columns immediately
|
||||||
|
# below each fixture, per the syntax in the comments below
|
||||||
|
#
|
||||||
|
one: {}
|
||||||
|
# column: value
|
||||||
|
#
|
||||||
|
two: {}
|
||||||
|
# column: value
|
||||||
7
test/models/controller_test.rb
Normal file
7
test/models/controller_test.rb
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class ControllerTest < ActiveSupport::TestCase
|
||||||
|
# test "the truth" do
|
||||||
|
# assert true
|
||||||
|
# end
|
||||||
|
end
|
||||||
@@ -571,6 +571,11 @@ picomatch@^2.3.1:
|
|||||||
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
|
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
|
||||||
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
|
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
|
||||||
|
|
||||||
|
stimulus-use@^0.52.2:
|
||||||
|
version "0.52.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/stimulus-use/-/stimulus-use-0.52.3.tgz#d6f35fa93277274957a2ed98a7b04b4d702cb1d6"
|
||||||
|
integrity sha512-stZ5dID6FUrGCR/ChWUa0FT5Z8iqkzT6lputOAb50eF+Ayg7RzJj4U/HoRlp2NV333QfvoRidru9HLbom4hZVw==
|
||||||
|
|
||||||
tailwindcss@4.1.4, tailwindcss@^4.1.4:
|
tailwindcss@4.1.4, tailwindcss@^4.1.4:
|
||||||
version "4.1.4"
|
version "4.1.4"
|
||||||
resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-4.1.4.tgz#27b3c910c6f1a47f4540451f3faf7cdd6d977a69"
|
resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-4.1.4.tgz#27b3c910c6f1a47f4540451f3faf7cdd6d977a69"
|
||||||
|
|||||||
Reference in New Issue
Block a user