Added register page, email field, finished login + register UI, reduced the number of templates generated.

This commit is contained in:
2025-12-06 14:45:23 +01:00
parent e749ec772e
commit 5732fe17de
27 changed files with 889 additions and 284 deletions

View File

@@ -5,10 +5,6 @@ DB_PASSWORD=1234
DB_HOST=pagerino_db
DB_NAME=pager_data
# App server specific settings
# === Chirpstack API
CHIRP_API_KEY=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJjaGlycHN0YWNrIiwiaXNzIjoiY2hpcnBzdGFjayIsInN1YiI6IjI3YzI5Y2M2LTdjMTUtNDI5Yy1iMjBmLTczNzliZWYzYTI1ZCIsInR5cCI6ImtleSJ9.RsMPIGgPaGluBllRz0Ma_EthxUj3xM9pTPy_uUEAbvk
# === MQTT broker settings
MQTT_ADDRESS=ssl://mosquitto:8883
MQTT_CLIENT_ID=app-server
@@ -16,7 +12,12 @@ MQTT_QOS=0
# This App's ID in Chirpstack
# + matches all IDs
# # matches all IDs and all(!) subtopics
APP_ID=d6ccd2ad-0cf7-46ab-8618-7a5a14b8676d
#APP_ID=d6ccd2ad-0cf7-46ab-8618-7a5a14b8676d
APP_ID=188c5d25-2d6b-43e3-aaf3-07dd0a8c386e
CHIRP_ADDRESS=chirpstack:8080
CHIRP_API_KEY=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJjaGlycHN0YWNrIiwiaXNzIjoiY2hpcnBzdGFjayIsInN1YiI6IjZlYTA1YTQ1LWUxODgtNDI3Yy1hMWMwLWZiNTQ1NWQ1N2QxZSIsInR5cCI6ImtleSJ9.i6as57Fod4m4oDvHw8LoV2wVz2wjnQFOi_l60zTnGy8
SERVER_API_PORT=50222
# Enviroment variables that will be available for every application
@@ -28,7 +29,6 @@ LOG_PREFIX=[Pagerino]
# Main app container name
SERVER_NAME=pagerino-app
SERVER_API_PORT=50222
# General preferred connection timeout
TIMEOUT=3s

View File

@@ -9,6 +9,10 @@ services:
- app_net
- pagerino_net
restart: on-failure:3
environment:
CHIRP_ADDRESS: ${CHIRP_ADDRESS}
SERVER_API_PORT: ${SERVER_API_PORT}
APP_ID: ${APP_ID}
pagerino_db:
image: postgres:15
@@ -27,6 +31,19 @@ services:
# ports:
# - "5432:5432"
migrate:
image: migrate/migrate
networks:
- app_net
volumes:
- ./migrations:/migrations
command: [
"-path", "/migrations",
"-database", "postgres://${DB_USER}:${DB_PASSWORD}@${DB_NAME}:5432/pagerino_db?sslmode=disable",
"up"
]
depends_on:
- pagerino_db
volumes:
dbdata:

View File

@@ -68,28 +68,13 @@ const (
// AppData holds data and connections needed througout the program
type AppData struct {
ExitSig chan os.Signal // Use on fatal error to graciously exit, otherwise potential leaks
DBPool *pgxpool.Pool // Execute queries on internal DB on this pool
APIServer *grpc.Server // Provide API for apps
MQTTClient mqtt.Client // Get Uplinks and downlinks
ChirpGateClient chirp_api.GatewayServiceClient // Check gateway stats and manage them
Timeout time.Duration // General connection/request timeout
}
// Uplink reflects Chirpstack's uplink format
type Uplink struct {
ApplicationID string `json:"applicationID"`
DevEUI string `json:"devEUI"`
FCnt uint32 `json:"fCnt"`
FPort uint8 `json:"fPort"`
Data string `json:"data"` // Base64 payload
RxInfo []struct {
GatewayID string `json:"gatewayID"`
Time string `json:"time"`
RSSI int32 `json:"rssi"`
SNR float64 `json:"snr"`
} `json:"rxInfo"`
DecodedPayload map[string]any `json:"decodedPayload,omitempty"`
ExitSig chan os.Signal // Use on fatal error to graciously exit, otherwise potential leaks
DBPool *pgxpool.Pool // Execute queries on internal DB on this pool
APIServer *grpc.Server // Provide API for apps
MQTTClient mqtt.Client // Get Uplinks and downlinks
ChirpGateClient chirp_api.GatewayServiceClient // Check gateway stats and manage them
ChirpDeviceCLient chirp_api.DeviceServiceClient // Check device stats
Timeout time.Duration // General connection/request timeout
}
// Downlink reflects Chirpstack's downlink format
@@ -155,6 +140,49 @@ func MakeTLSConfig() (*tls.Config, error) {
}, nil
}
func (data *AppData) printChirpStats() {
ctx_tmout, cancel_ctx := context.WithTimeout(context.Background(), data.Timeout)
defer cancel_ctx()
resp_gate, err := data.ChirpGateClient.List(ctx_tmout, &chirp_api.ListGatewaysRequest{
Limit: 10,
Offset: 0,
OrderByDesc: true,
})
if err != nil {
log.Println("Failed to get Chirpstack's API response: " + err.Error())
return
}
log.Println("Found the following gateways (first 10):")
for idx, gate := range resp_gate.Result {
log.Println(strconv.FormatInt(int64(idx), 10) + ": App[" +
gate.TenantId + "] GateID[" + gate.GatewayId + "] Name[" +
gate.Name + "]")
}
log.Println("[END]")
resp_device, err := data.ChirpDeviceCLient.List(ctx_tmout, &chirp_api.ListDevicesRequest{
Limit: 10,
Offset: 0,
OrderByDesc: true,
ApplicationId: FindEnv("APP_ID"),
})
if err != nil {
log.Println("Failed to get Chirpstack's API response: " + err.Error())
return
}
log.Println("Found the following devices (first 10):")
for idx, dev := range resp_device.Result {
log.Println(strconv.FormatInt(int64(idx), 10) + ": Name[" + dev.Name +
"] Description:[" + dev.Description +
"] DevEUI[" + dev.DevEui + "]",
)
}
log.Println("[END]")
}
// VerifyCredentials returns no error if password matches in DB
func VerifyCredentials(username string, password string, data *AppData) error {
if len(username) > MAX_CHAR_LEN || len(password) > MAX_CHAR_LEN {
@@ -233,41 +261,59 @@ func makeMQTTHandler(data *AppData) mqtt.MessageHandler {
log.Println("Uplink is responding in an unrecognized channel")
}
//data.printChirpStats()
// Query timeout
ctx, close := context.WithTimeout(context.Background(), data.Timeout)
defer close()
// Get device IDs
row := data.DBPool.QueryRow(ctx,
"SELECT id FROM Devices WHERE euid = '$1'",
"SELECT id FROM \"Devices\" WHERE euid = '$1';",
uplink.DevEUI,
)
var dev_id int32
if err = row.Scan(&dev_id); err != nil {
log.Println(ErrNotFound.Error() + ": Device.euid = " + uplink.DevEUI)
if err == pgx.ErrNoRows {
log.Println(
"Adding new device to DB with DeviceEUI[" + uplink.DevEUI +
"] and Name[" + uplink.DeviceName + "]",
)
data.DBPool.Exec(ctx,
"INSERT INTO \"Devices\" (euid, name) VALUES ($1, $2)",
uplink.DevEUI, uplink.DeviceName,
)
} else {
log.Println(err.Error() + ": Device.euid = " + uplink.DevEUI)
return
}
return
}
// Add Message to DB
_, err = data.DBPool.Exec(ctx,
"INSERT INTO Messages (sender_id, receiver_id, payload) VALUES ($1, $2, $3)",
"INSERT INTO \"Messages\" (sender_id, receiver_id, payload) VALUES ($1, $2, $3);",
dev_id, nil, decoded,
)
if err != nil {
log.Println(ErrModify.Error() + ": Message")
log.Println(ErrModify.Error() + ": " + err.Error())
return
}
// Get decoded custom payload
pager_msg, err := serder.Deserialize(decoded)
if err != nil {
log.Println(err.Error() + ": deserialization")
log.Println("Deserialization error: " + err.Error())
return
}
log.Println("Received Pagerino payload for AppID[", pager_msg.AppID, "]:")
log.Println(
"Received Pagerino payload for AppID[" +
strconv.FormatUint(uint64(pager_msg.AppID), 10) + "]:",
)
for _, field := range pager_msg.Fields {
log.Println(field)
}
@@ -354,7 +400,7 @@ Expected: [number]{h|m|s|ms|us|ns}...`)
return
}
log.Println("Created MQTT broker connection.")
log.Println("Created MQTT broker connection at: " + FindEnv("MQTT_ADDRESS"))
defer client.Disconnect(150)
Data.MQTTClient = client
@@ -378,7 +424,7 @@ Expected: [number]{h|m|s|ms|us|ns}...`)
// Serve gRPC
go func() {
if err := grpc_server.Serve(net_listen); err != nil {
log.Println("Failed to serve gRPC API")
log.Println("Failed to serve gRPC API: " + err.Error())
return
}
}()
@@ -391,42 +437,23 @@ Expected: [number]{h|m|s|ms|us|ns}...`)
grpc.WithTransportCredentials(insecure.NewCredentials()), // remove this when using TLS
}
grpc_conn, err := grpc.Dial("chirpstack:8080", dialOpts...)
grpc_conn, err := grpc.Dial(FindEnv("CHIRP_ADDRESS"), dialOpts...)
if err != nil {
log.Println("Failed to create a gRPC connection to Chirpstack's API: " + err.Error())
return
}
Data.ChirpGateClient = chirp_api.NewGatewayServiceClient(grpc_conn)
Data.ChirpDeviceCLient = chirp_api.NewDeviceServiceClient(grpc_conn)
defer grpc_conn.Close()
log.Println("Created Chirpstack's API connection.")
log.Println("Created Chirpstack's API connection at: " + FindEnv("CHIRP_ADDRESS"))
// Test gRPC connection
ctx_tmout, cancel_ctx = context.WithTimeout(context.Background(), Data.Timeout)
defer cancel_ctx()
resp, err := Data.ChirpGateClient.List(ctx_tmout, &chirp_api.ListGatewaysRequest{
Limit: 10,
Offset: 0,
OrderByDesc: true,
})
if err != nil {
log.Println("Failed to test Chirpstack's API connection: "+err.Error(), resp)
return
}
log.Println("Found the following gateways (first 10):")
for idx, gate := range resp.Result {
log.Println(strconv.FormatInt(int64(idx), 10) + ": App[" +
gate.TenantId + "] GateID[" + gate.GatewayId + "] Name[" +
gate.Name + "]")
}
log.Println("[END]")
Data.printChirpStats()
// === Listen for Uplinks
// Subscribe to topics
topic := "application/" + FindEnv("APP_ID") + "/device/+/event/up"
topic := "application/+/device/+/event/+"
qos, err := strconv.Atoi(FindEnv("MQTT_QOS"))
if err != nil {
log.Println("Format misconfiguration for MQTT_QOS: " + err.Error())
@@ -436,7 +463,35 @@ Expected: [number]{h|m|s|ms|us|ns}...`)
client.Subscribe(topic, byte(qos), makeMQTTHandler(&Data))
log.Println("Subscribed to uplink data on: " + topic + ".")
// === Simulate uplinks
go func() {
for {
time.Sleep(time.Second * 3)
uplink, err := MakePayload()
if err != nil {
log.Println("Error while creating uplink: " + err.Error())
break
}
payload, err := json.Marshal(*uplink)
if err != nil {
log.Println("Failed to marshal uplink json: " + err.Error())
break
}
client.Publish(
"application/"+FindEnv("APP_ID")+"/device/"+DeviceEUI+"/event/up",
0,
false,
payload,
)
}
log.Println("Stopping uplink loop...")
}()
// === Shutdown
<-Data.ExitSig // Continue here on force quit
log.Println("The server is shutting down.")
log.Println("The application is shutting down.")
}

View File

@@ -17,6 +17,8 @@ const (
NFC_LENGTH = 7
)
var MAX_BUFFER_LENGTH = 221
var (
ErrTooShort error = errors.New("insufficient bytes in message")
ErrNoType error = errors.New("encountered unknown data type")
@@ -76,6 +78,15 @@ type PagerMessage struct {
Fields []Field
}
// tryAppend is append function with custom contraints (like length)
func tryAppend[T byte](slice []T, elems ...T) (bool, []T) {
if len(slice)+len(elems) > MAX_BUFFER_LENGTH {
return false, slice
}
return true, append(slice, elems...)
}
// readArray reads a subset of bytes marked with DT_Array.
// This data is copied, it is not parsed.
func readArray(data []byte, offset int, dest *[]byte) (int, error) {
@@ -171,7 +182,10 @@ func readField(data []byte, offset int) (Field, int, error) {
// writeField encodes a field into buffer
func writeField(field *Field, buf []byte) ([]byte, error) {
buf = append(buf, field.DType)
ok, buf := tryAppend(buf, field.DType)
if !ok {
return buf, ErrTooLong
}
switch field.DType {
case DT_Array:
@@ -179,7 +193,11 @@ func writeField(field *Field, buf []byte) ([]byte, error) {
if !ok {
return buf, ErrInvalidData
}
buf = append(buf, sub_pair.DType)
ok, buf = tryAppend(buf, sub_pair.DType)
if !ok {
return buf, ErrTooLong
}
size, ok := DataSizeMap[sub_pair.DType]
if !ok {
@@ -192,7 +210,7 @@ func writeField(field *Field, buf []byte) ([]byte, error) {
}
length := len(val) / size
if length > 255 {
if length+len(buf)+1 > MAX_BUFFER_LENGTH {
return buf, ErrTooLong
}
@@ -204,7 +222,10 @@ func writeField(field *Field, buf []byte) ([]byte, error) {
return buf, ErrInvalidData
}
buf = append(buf, val)
ok, buf = tryAppend(buf, val)
if !ok {
return buf, ErrTooLong
}
case DT_Authorization, DT_NFCPair:
val, ok := field.Value.([7]byte)
@@ -212,7 +233,10 @@ func writeField(field *Field, buf []byte) ([]byte, error) {
return buf, ErrInvalidData
}
buf = append(buf, val[:]...)
ok, buf = tryAppend(buf, val[:]...)
if !ok {
return buf, ErrTooLong
}
case DT_Charge, DT_Temperature:
val, ok := field.Value.(float32)
@@ -220,6 +244,9 @@ func writeField(field *Field, buf []byte) ([]byte, error) {
return buf, ErrInvalidData
}
if len(buf)+4 > MAX_BUFFER_LENGTH {
return buf, ErrTooLong
}
buf = binary.LittleEndian.AppendUint32(buf, math.Float32bits(val))
case DT_Location:
@@ -228,6 +255,10 @@ func writeField(field *Field, buf []byte) ([]byte, error) {
return buf, ErrInvalidData
}
if len(buf)+20 > MAX_BUFFER_LENGTH {
return buf, ErrTooLong
}
buf = binary.LittleEndian.AppendUint64(buf, math.Float64bits(val.Latitude))
buf = binary.LittleEndian.AppendUint64(buf, math.Float64bits(val.Longitude))
buf = binary.LittleEndian.AppendUint32(buf, math.Float32bits(val.Altitude))
@@ -241,20 +272,27 @@ func writeField(field *Field, buf []byte) ([]byte, error) {
return buf, nil
}
func GetLengthFromDataRate(data_rate int) int {
if data_rate >= 0 && data_rate <= 3 {
return 51
} else if data_rate == 4 {
return 115
} else if data_rate >= 5 && data_rate <= 7 {
return 222
} else {
return 0
}
}
// SplitByLimit splits encoded message into parts
// with maximum length derived from data rate.
//
// These messages are not decodable on the server, as
// data is fractured. (TODO)
func SplitByLimit(buf []byte, data_rate int) ([][]byte, error) {
var limit int
if data_rate >= 0 && data_rate <= 3 {
limit = 51
} else if data_rate == 4 {
limit = 115
} else if data_rate >= 5 && data_rate <= 7 {
limit = 222
} else {
limit := GetLengthFromDataRate(data_rate)
if limit == 0 {
return nil, ErrUndefined
}

136
AppServer/src/uplink.go Normal file
View File

@@ -0,0 +1,136 @@
package main
import (
"encoding/base64"
"encoding/json"
"math/rand"
"time"
"server/serder"
)
// ---------- CONFIG ----------
const (
Broker = "tls://mosquitto:8883"
GatewayID = "54d4d6102c916aed"
DeviceEUI = "1122334455667788"
AppId = "188c5d25-2d6b-43e3-aaf3-07dd0a8c386e"
)
var FCount uint32 = 0
// ---------- DATA STRUCTURES ----------
type Location struct {
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
Altitude float64 `json:"altitude"`
}
type RXInfo struct {
GatewayID string `json:"gatewayID"`
Time time.Time `json:"time"`
RSSI int `json:"rssi"`
LoRaSNR float64 `json:"loRaSNR"`
Channel int `json:"channel"`
RFChain int `json:"rfChain"`
Board int `json:"board"`
Antenna int `json:"antenna"`
Location Location `json:"location"`
}
type TXInfo struct {
Frequency int `json:"frequency"`
DR int `json:"dr"`
ADR bool `json:"adr"`
CodeRate string `json:"codeRate"`
}
type Uplink struct {
ApplicationID string `json:"applicationID"`
ApplicationName string `json:"applicationName"`
DeviceName string `json:"deviceName"`
DevEUI string `json:"devEUI"`
RXInfo []RXInfo `json:"rxInfo"`
TXInfo TXInfo `json:"txInfo"`
FCnt uint32 `json:"fCnt"`
FPort uint8 `json:"fPort"`
Data string `json:"data"` // Base64-encoded payload
Object any `json:"object"` // decoded JSON payload if device uses JSON codec
}
// ---------- UTILITY FUNCTIONS ----------
// func generateRandomPHY(n int) []byte {
// payload := make([]byte, n)
// for i := 0; i < n; i++ {
// payload[i] = byte(rand.Intn(256))
// }
// return payload
// }
// Create a random uplink
func MakePayload() (*Uplink, error) {
// Generate random sensor data
custom_pay := serder.PagerMessage{
AppID: 1,
Fields: []serder.Field{
{
DType: serder.DT_Array,
Value: serder.Field{
DType: serder.DT_Char,
Value: []byte{
byte(5),
'H', 'e', 'l', 'l', 'o',
},
},
},
},
}
object, err := serder.Serialize(&custom_pay)
if err != nil {
return nil, err
}
// Encode object as JSON and then as base64 for Data field
objBytes, _ := json.Marshal(object)
dataB64 := base64.StdEncoding.EncodeToString(objBytes)
uplink := Uplink{
ApplicationID: AppId,
ApplicationName: "pagerino-app",
DeviceName: "pager-1",
DevEUI: DeviceEUI,
RXInfo: []RXInfo{
{
GatewayID: GatewayID,
Time: time.Now().UTC(),
RSSI: -30 + rand.Intn(10),
LoRaSNR: -5 + rand.Float64()*10,
Channel: 0,
RFChain: 0,
Board: 1,
Antenna: 0,
Location: Location{
Latitude: 52.0 + rand.Float64(),
Longitude: 13.0 + rand.Float64(),
Altitude: 100 + rand.Float64()*50,
},
},
},
TXInfo: TXInfo{
Frequency: 868100000,
DR: 5,
ADR: true,
CodeRate: "4/5",
},
FCnt: FCount,
FPort: 1,
Data: dataB64,
Object: object,
}
FCount++
return &uplink, err
}

Binary file not shown.