Compare commits

..

No commits in common. "main" and "v1.1.0" have entirely different histories.
main ... v1.1.0

26 changed files with 32 additions and 2474 deletions

View File

@ -3,7 +3,7 @@ run-name: ${{ gitea.actor }} build
on: [push]
jobs:
build-go:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@ -21,21 +21,3 @@ 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
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.2.0
- name: Display Bun version
run: bun --version
- name: test 1
run: ls
- name: test 2
run: cd web && ls
- name: "[web] Install dependencies"
run: cd web && bun install
- name: "[web] Build"
run: cd web && bun run build

3
.gitignore vendored
View File

@ -85,6 +85,8 @@ __pycache__/
## Node
node_modules
package-lock.json
package.json
/src/doc/rustc-dev-guide/mermaid.min.js
## Rustdoc GUI tests
@ -102,4 +104,3 @@ flake.lock
# Before adding new lines, see the comment at the top.
.env*
.idea/

View File

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

BIN
door.xcf

Binary file not shown.

View File

@ -1,3 +1 @@
__pycache__/
.env.json

View File

@ -1,4 +1,3 @@
import gc
import utime
import ujson
import network
@ -8,6 +7,7 @@ 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
wlan: network.WLAN
ultra_opened_counter: int = 0
led = Pin(15, Pin.OUT)
trigger = Pin(2, Pin.OUT)
@ -57,18 +57,18 @@ class App:
def connect(self):
print("Connecting to Wi-Fi...")
self.wlan = network.WLAN(network.STA_IF)
wlan = network.WLAN(network.STA_IF)
self.wlan.active(False)
wlan.active(False)
utime.sleep_ms(250)
self.wlan.active(True)
wlan.active(True)
utime.sleep_ms(250)
self.wlan.connect(self.ssid, self.password)
wlan.connect(self.ssid, self.password)
utime.sleep_ms(100)
retry_count = 0
while not self.wlan.isconnected():
while not wlan.isconnected():
if retry_count >= MAX_CONNECTION_RETRIES:
print("Max connection retries reached")
exit(1)
@ -82,12 +82,12 @@ class App:
if retry_count % 10 == 0:
print("Attempting to restart connection...")
self.wlan.connect(self.ssid, self.password)
wlan.connect(self.ssid, self.password)
for _ in range(10):
self.led.toggle()
utime.sleep_ms(50)
print(f"Connected with IP {self.wlan.ifconfig()[0]}")
print(f"Connected with IP {wlan.ifconfig()[0]}")
self.update_server()
def update_server(self):
@ -101,36 +101,28 @@ class App:
headers={"Authorization": self.token, "Content-Type": "application/json"},
data=raw
)
print(f"State updated [{r.status_code}] {r.content.decode()}")
print(f"State updated [{r.status_code}]")
except Exception as e:
print(f"Error occurred: {e}")
def measure_distance(self) -> float:
def measure_distance(self):
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:0<5} cm; mem_free = {gc.mem_free()}")
print(f"Distance: {distance} cm")
return distance
@ -141,43 +133,36 @@ 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:
if not 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:
# was it just opened? +wait delay
if not self.previously_opened:
self.led.high()
self.led.high()
self.ultra_opened_counter += 1
if self.ultra_opened_counter >= ULTRA_OPENED_THRESHOLD:
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
gc.collect()
else:
utime.sleep_ms(500)
except Exception as e:
print(f"Fatal error occurred: {e}")
print(f"Fatal exception occurred: {e}")
self.previously_opened = self.opened
i += 1
if __name__ == "__main__":
App().run()

View File

@ -73,10 +73,6 @@ 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",
@ -134,6 +130,7 @@ func main() {
}
if data.Opened == opened {
mut.Unlock()
return c.NoContent(http.StatusOK)
}
@ -217,8 +214,7 @@ func main() {
func authed(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
provided := c.Request().Header.Get("Authorization")
if provided != fmt.Sprintf("Bearer %s", token) {
if c.Request().Header.Get("Authorization") != token {
return c.NoContent(http.StatusUnauthorized)
}

View File

@ -1,10 +0,0 @@
.idea/
.vscode/
node_modules/
.output/
.data/
.nuxt/
.cache/
.env*

24
web/.gitignore vendored
View File

@ -1,24 +0,0 @@
# 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

View File

@ -1,6 +0,0 @@
{
"trailingComma": "es5",
"tabWidth": 4,
"semi": false,
"singleQuote": false
}

View File

@ -1,20 +0,0 @@
FROM oven/bun:1 AS build
WORKDIR /app
COPY package.json bun.lock .
RUN bun install --frozen-lockfile --production
COPY . .
RUN bun run build
FROM oven/bun:1
WORKDIR /app
COPY --from=build /app/.output .
ENV PORT 3000
ENV HOST 0.0.0.0
EXPOSE 3000
CMD ["bun", "/app/server/index.mjs"]

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -1,42 +0,0 @@
import type { FetchResponse } from "ofetch"
import { AxiosError, type AxiosResponse } from "axios"
export function useAPI(route: string = "/") {
return `https://api.door.svitan.dev${route}`
}
export async function handleRequestError(error: unknown) {
const toast = useToast()
let message = undefined
if (error instanceof Error) {
message = error.message
}
toast.add({
title: "Error occurred",
description: message,
color: "error",
})
}
export function handleResponse<T extends { toString(): string }>(
response: AxiosResponse<T>,
success: (response: AxiosResponse<T>) => void = () => {}
) {
const token = useToken()
const toast = useToast()
if (response.status === 200) {
success(response)
} else if (response.status === 401) {
toast.add({ title: "Token not valid", color: "error" })
token.value = ""
navigateTo("/token")
} else {
toast.add({
title: "Error occurred",
description: response.data.toString(),
color: "error",
})
}
}

View File

@ -1,10 +0,0 @@
export function useToken() {
return useCookie<string | undefined>("token")
}
export function useHeaders(override: string | undefined = undefined) {
const token = useToken()
return {
Authorization: `Bearer ${override ?? token.value}`,
}
}

View File

@ -1,6 +0,0 @@
// 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"],
});

View File

@ -1,26 +0,0 @@
{
"name": "nuxt-app",
"private": true,
"type": "module",
"scripts": {
"build": "nuxt build",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare"
},
"dependencies": {
"@nuxt/icon": "1.13.0",
"@nuxt/ui": "3.1.3",
"@nuxtjs/tailwindcss": "7.0.0-beta.0",
"axios": "^1.9.0",
"nuxt": "^3.17.5",
"typescript": "^5.6.3",
"valibot": "^1.1.0",
"vue": "^3.5.16",
"vue-router": "^4.5.1"
},
"devDependencies": {
"prettier": "^3.5.3"
}
}

View File

@ -1,138 +0,0 @@
<template>
<main
class="flex items-center justify-center min-w-screen min-h-screen gap-4 flex-wrap"
>
<div class="flex items-center justify-center flex-col gap-y-2 w-32">
<div
:class="`flex items-center justify-center border-solid border-2 border-${open ? (locked ? 'error' : 'secondary') : 'primary'} rounded-lg w-full h-32`"
>
<UIcon
:name="
open
? 'material-symbols:door-open'
: 'material-symbols:door-front'
"
class="size-12 !w-16 !h-16"
/>
</div>
<p
:class="`flex items-center justify-center text-${open ? (locked ? 'error' : 'secondary') : 'primary'} text-xl w-full h-10`"
>
{{ open ? "Opened" : "Closed" }}
</p>
</div>
<div class="flex items-center justify-center flex-col gap-y-2 w-32">
<div
:class="`flex items-center justify-center border-solid border-2 border-${locked ? 'primary' : 'secondary'} rounded-lg w-full h-32`"
>
<UIcon
:name="
locked
? 'material-symbols:lock'
: 'material-symbols:lock-open'
"
class="size-12 !w-16 !h-16"
/>
</div>
<UButton
:icon="
locked
? 'material-symbols:lock-open'
: 'material-symbols:lock'
"
size="xl"
:color="locked ? 'primary' : 'secondary'"
variant="solid"
class="flex items-center justify-center w-full h-10"
@click="toggleLock"
>
{{ locked ? "Unlock" : "Lock" }}
</UButton>
</div>
<div class="flex items-center justify-center flex-col gap-y-2 w-32">
<div
:class="`flex items-center justify-center border-solid border-2 border-${alert ? 'primary' : 'secondary'} rounded-lg w-full h-32`"
>
<UIcon
:name="
alert
? 'material-symbols:volume-up'
: 'material-symbols:volume-off'
"
class="size-12 !w-16 !h-16"
/>
</div>
<UButton
:icon="
alert
? 'material-symbols:volume-off'
: 'material-symbols:volume-up'
"
size="xl"
:color="alert ? 'primary' : 'secondary'"
variant="solid"
class="flex items-center justify-center w-full h-10"
@click="() => (alert = !alert)"
>
{{ alert ? "Turn off" : "Turn on" }}
</UButton>
</div>
</main>
</template>
<script lang="ts" setup>
import axios from "axios"
const token = useToken()
const open = ref(true)
const locked = ref(true)
const alert = ref(false)
const toast = useToast()
async function toggleLock() {
const desired = !locked.value
axios
.post(
useAPI("/lock"),
{
locked: desired,
},
{
headers: useHeaders(),
}
)
.then((res) => {
handleResponse(res, () => {
toast.add({
title: `The door was ${desired ? "locked" : "unlocked"}`,
})
locked.value = desired
})
})
.catch(handleRequestError)
}
onMounted(() => {
if (!token.value) {
return navigateTo("/token")
}
axios
.get<boolean>(useAPI("/lock"), {
headers: useHeaders(),
})
.then((res) => {
handleResponse(res, (response) => {
locked.value = response.data
})
})
.catch(handleRequestError)
})
</script>

View File

@ -1,60 +0,0 @@
<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"
import axios from "axios"
const token = useToken()
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()
function onSubmit(event: FormSubmitEvent<Schema>) {
const received = event.data.token
axios
.get<boolean>(useAPI("/open"), {
headers: useHeaders(received),
})
.then((res) => {
handleResponse(res, () => {
toast.add({ title: "Token saved", color: "success" })
token.value = received
navigateTo("/")
})
})
.catch(handleRequestError)
}
onMounted(() => {
if (token.value) {
return navigateTo("/")
}
})
</script>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 264 KiB

View File

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

View File

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

View File

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

View File

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