10 Commits

Author SHA1 Message Date
Daniel Svitan
c4f2006e8f 💚 Renames CI build file
Some checks failed
Gitea Build Action / build-go (push) Successful in 27s
Gitea Build Action / build-nuxt (push) Failing after 6s
2025-06-05 10:04:48 +02:00
Daniel Svitan
7b50441da2 💚 Adds CI build for web 2025-06-05 10:04:29 +02:00
Daniel Svitan
940f6ea1b5 🔧 Adds CORS
All checks were successful
Gitea Build Action / build (push) Successful in 25s
2025-06-05 09:53:46 +02:00
Daniel Svitan
a48d1238a2 🚧 Adds token form
All checks were successful
Gitea Build Action / build (push) Successful in 28s
2025-06-05 09:48:58 +02:00
Daniel Svitan
7aaa0c0aab 💄 Adds actual favicon
All checks were successful
Gitea Build Action / build (push) Successful in 26s
2025-06-04 21:47:55 +02:00
Daniel Svitan
00bf392931 🎉 Inits web
All checks were successful
Gitea Build Action / build (push) Successful in 36s
2025-06-04 21:38:24 +02:00
Daniel Svitan
7d2ad1cd49 🐛 Fixes memory overflow and removes server health check
All checks were successful
Gitea Build Action / build (push) Successful in 22s
2025-06-02 20:46:55 +02:00
Daniel Svitan
307b467ac1 🚧 Adds checking and reconnecting when wlan is down
All checks were successful
Gitea Build Action / build (push) Successful in 24s
2025-06-01 21:19:13 +02:00
Daniel Svitan
995bf90efb 🐛 Fixes peripheral delay on door open
All checks were successful
Gitea Build Action / build (push) Successful in 25s
2025-06-01 12:49:30 +02:00
Daniel Svitan
100bab01e4 🐛 Fixes API routes
All checks were successful
Gitea Build Action / build (push) Successful in 25s
2025-06-01 12:40:13 +02:00
19 changed files with 2256 additions and 34 deletions

View File

@@ -3,7 +3,7 @@ run-name: ${{ gitea.actor }} build
on: [push]
jobs:
build:
build-go:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -21,3 +21,17 @@ jobs:
run: cd admin && go get .
- name: "[admin] Build"
run: cd admin && go build -v ./...
build-nuxt:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Bun 1.2.0
run: oven-sh/setup-bun@v2
with:
bun-version: 1.2.0
- name: Display Bun version
run: bun --version
- name: "[web] Install dependencies"
run: cd web && bun install
- name: "[web] Build"
run: cd web && bun run build

View File

@@ -85,6 +85,7 @@ func makePostReq(url string, body any) ([]byte, error) {
}
req.Header.Set("Authorization", token)
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
@@ -119,8 +120,8 @@ func main() {
Action: test,
},
{
Name: "get",
Usage: "get door state",
Name: "open",
Usage: "get door state (opened or closed)",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "raw",
@@ -200,7 +201,7 @@ func getOpened(ctx context.Context, cmd *cli.Command) error {
return err
}
opened, err := makeGetReq(fmt.Sprintf("%s/read", server))
opened, err := makeGetReq(fmt.Sprintf("%s/open", server))
if err != nil {
return err
}
@@ -244,7 +245,7 @@ func getAlert(context.Context, *cli.Command) error {
return err
}
alert, err := makeGetReq(fmt.Sprintf("%s/alerts", server))
alert, err := makeGetReq(fmt.Sprintf("%s/alert", server))
if err != nil {
return err
}
@@ -305,7 +306,7 @@ func createManageAlert(alert bool) func(context.Context, *cli.Command) error {
data.For = secs
}
_, err = makePostReq(fmt.Sprintf("%s/alerts", server), data)
_, err = makePostReq(fmt.Sprintf("%s/alert", server), data)
if err != nil {
return err
}

BIN
door.xcf Normal file

Binary file not shown.

View File

@@ -1,3 +1,4 @@
import gc
import utime
import ujson
import network
@@ -7,7 +8,6 @@ from machine import Pin
THRESHOLD_DISTANCE = 15 # cm
SOUND_SPEED = 340 * 100 # m/s * centi = cm/s
MAX_CONNECTION_RETRIES = 50
ULTRA_OPENED_THRESHOLD = 3
class App:
@@ -18,7 +18,7 @@ class App:
opened: bool = False
previously_opened: bool = False
ultra_opened_counter: int = 0
wlan: network.WLAN
led = Pin(15, Pin.OUT)
trigger = Pin(2, Pin.OUT)
@@ -57,18 +57,18 @@ class App:
def connect(self):
print("Connecting to Wi-Fi...")
wlan = network.WLAN(network.STA_IF)
self.wlan = network.WLAN(network.STA_IF)
wlan.active(False)
self.wlan.active(False)
utime.sleep_ms(250)
wlan.active(True)
self.wlan.active(True)
utime.sleep_ms(250)
wlan.connect(self.ssid, self.password)
self.wlan.connect(self.ssid, self.password)
utime.sleep_ms(100)
retry_count = 0
while not wlan.isconnected():
while not self.wlan.isconnected():
if retry_count >= MAX_CONNECTION_RETRIES:
print("Max connection retries reached")
exit(1)
@@ -82,22 +82,14 @@ class App:
if retry_count % 10 == 0:
print("Attempting to restart connection...")
wlan.connect(self.ssid, self.password)
self.wlan.connect(self.ssid, self.password)
for _ in range(10):
self.led.toggle()
utime.sleep_ms(50)
print(f"Connected with IP {wlan.ifconfig()[0]}")
print(f"Connected with IP {self.wlan.ifconfig()[0]}")
self.update_server()
def health_check_server(self):
print("Health checking server...", end="\r")
try:
r = urequests.get(f"{self.server}/")
print(f"Server healthy [{r.status_code}]{" " * 8}")
except Exception as e:
print(f"Error occurred: {e}")
def update_server(self):
print("Updating state...", end="\r")
data = {"opened": self.opened}
@@ -113,24 +105,32 @@ class App:
except Exception as e:
print(f"Error occurred: {e}")
def measure_distance(self):
def measure_distance(self) -> float:
self.trigger.low()
utime.sleep_us(2)
self.trigger.high()
utime.sleep_us(10)
self.trigger.low()
start_time = utime.ticks_us()
sent_time = utime.ticks_us()
while self.echo.value() == 0:
print("hey", end="\r")
sent_time = utime.ticks_us()
if sent_time - start_time >= 100_000: # if it takes more than 100ms, stop
return -1
start_time = utime.ticks_us()
received_time = utime.ticks_us()
while self.echo.value() == 1:
print("ho", end="\r")
received_time = utime.ticks_us()
if received_time - start_time >= 100_000: # same
return -1
delta_time = received_time - sent_time
distance = delta_time / 1_000_000 * SOUND_SPEED / 2
print(f"Distance: {distance} cm")
print(f"Distance: {distance:0<5} cm; mem_free = {gc.mem_free()}")
return distance
@@ -141,38 +141,43 @@ class App:
i = 0
while True:
try:
if not self.wlan.isconnected():
self.connect()
distance = self.measure_distance()
self.opened = distance >= THRESHOLD_DISTANCE
if not self.opened:
self.led.low()
self.ultra_opened_counter = 0
# was it just closed?
if self.previously_opened:
self.update_server()
self.previously_opened = False
else:
self.led.high()
self.ultra_opened_counter += 1
if self.ultra_opened_counter >= ULTRA_OPENED_THRESHOLD:
# was it just opened? +wait delay
if not self.previously_opened:
self.led.high()
self.update_server()
self.previously_opened = True
if i >= 20:
print("Blinking...")
self.led.toggle()
utime.sleep_ms(100)
self.led.toggle()
utime.sleep_ms(400)
i = 0
self.health_check_server()
gc.collect()
else:
utime.sleep_ms(500)
except Exception as e:
print(f"Fatal exception occurred: {e}")
print(f"Fatal error occurred: {e}")
self.previously_opened = self.opened
i += 1
if __name__ == "__main__":
App().run()

View File

@@ -73,6 +73,10 @@ func main() {
LogLevel: log.ERROR,
}))
app.Use(middleware.Secure())
app.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: []string{"*"},
AllowHeaders: []string{echo.HeaderOrigin, echo.HeaderContentType, echo.HeaderAccept, echo.HeaderAuthorization},
}))
app.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
Format: "${time_custom} ${method} ${uri} ---> ${status} in ${latency_human} (${bytes_out} bytes)\n",
@@ -213,7 +217,8 @@ func main() {
func authed(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
if c.Request().Header.Get("Authorization") != token {
provided := c.Request().Header.Get("Authorization")
if provided != fmt.Sprintf("Bearer %s", token) {
return c.NoContent(http.StatusUnauthorized)
}

24
web/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Nuxt dev/build outputs
.output
.data
.nuxt
.nitro
.cache
dist
# Node dependencies
node_modules
# Logs
logs
*.log
# Misc
.DS_Store
.fleet
.idea
# Local env files
.env
.env.*
!.env.example

75
web/README.md Normal file
View File

@@ -0,0 +1,75 @@
# Nuxt Minimal Starter
Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
## Setup
Make sure to install dependencies:
```bash
# npm
npm install
# pnpm
pnpm install
# yarn
yarn install
# bun
bun install
```
## Development Server
Start the development server on `http://localhost:3000`:
```bash
# npm
npm run dev
# pnpm
pnpm dev
# yarn
yarn dev
# bun
bun run dev
```
## Production
Build the application for production:
```bash
# npm
npm run build
# pnpm
pnpm build
# yarn
yarn build
# bun
bun run build
```
Locally preview production build:
```bash
# npm
npm run preview
# pnpm
pnpm preview
# yarn
yarn preview
# bun
bun run preview
```
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.

9
web/app.vue Normal file
View File

@@ -0,0 +1,9 @@
<template>
<UApp>
<NuxtPage/>
</UApp>
</template>
<style>
@import "assets/css/main.css";
</style>

2
web/assets/css/main.css Normal file
View File

@@ -0,0 +1,2 @@
@import "tailwindcss";
@import "@nuxt/ui";

View File

@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

1999
web/bun.lock Normal file

File diff suppressed because it is too large Load Diff

6
web/nuxt.config.ts Normal file
View File

@@ -0,0 +1,6 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
compatibilityDate: '2025-05-15',
devtools: { enabled: true },
modules: ['@nuxt/icon', '@nuxt/ui', '@nuxtjs/tailwindcss']
})

17
web/pages/index.vue Normal file
View File

@@ -0,0 +1,17 @@
<template>
<main class="flex items-center justify-center min-w-screen min-h-screen">
<UButton icon="material-symbols:lock" size="xl" color="primary" variant="solid">Lock</UButton>
{{ res }}
</main>
</template>
<script lang="ts" setup>
const token = useCookie("token")
onMounted(() => {
if (!token.value) {
return navigateTo("/token")
}
console.log(token.value)
})
</script>

42
web/pages/token.vue Normal file
View File

@@ -0,0 +1,42 @@
<template>
<main class="flex items-center justify-center min-w-screen min-h-screen">
<UForm :schema="schema" :state="state" @submit="onSubmit" class="flex flex-col items-end justify-center gap-y-2">
<UFormField label="Token" name="token" size="xl" required>
<UInput v-model="state.token" placeholder="Your token..." size="xl"/>
</UFormField>
<UButton type="submit" size="xl">
Submit
</UButton>
</UForm>
</main>
</template>
<script lang="ts" setup>
import * as v from "valibot"
import type { FormSubmitEvent } from "@nuxt/ui"
const schema = v.object({
token: v.pipe(v.string(), v.nonEmpty("Please enter your token"))
})
type Schema = v.InferOutput<typeof schema>
const state = reactive({
token: ""
})
const toast = useToast()
async function onSubmit(event: FormSubmitEvent<Schema>) {
const token = event.data.token
const res = await $fetch("https://door.svitan.dev/open", {
method: "GET",
headers: {
Authorization: `Bearer ${token}`
}
})
console.log(token)
console.log(res)
toast.add({ title: "Token saved", color: "success" })
}
</script>

BIN
web/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

2
web/public/robots.txt Normal file
View File

@@ -0,0 +1,2 @@
User-Agent: *
Disallow: /

3
web/server/tsconfig.json Normal file
View File

@@ -0,0 +1,3 @@
{
"extends": "../.nuxt/tsconfig.server.json"
}

11
web/tailwind.config.js Normal file
View File

@@ -0,0 +1,11 @@
module.exports = {
purge: [],
darkMode: "class", // or 'media' or 'class'
theme: {
extend: {},
},
variants: {
extend: {},
},
plugins: [],
}

4
web/tsconfig.json Normal file
View File

@@ -0,0 +1,4 @@
{
// https://nuxt.com/docs/guide/concepts/typescript
"extends": "./.nuxt/tsconfig.json"
}