init: rough companion app stub

This commit is contained in:
2026-05-07 18:35:58 +02:00
commit 5f017ac05d
73 changed files with 3520 additions and 0 deletions
+25
View File
@@ -0,0 +1,25 @@
class User {
const User({required this.id, required this.username, required this.isAdmin});
final int id;
final String username;
final bool isAdmin;
factory User.fromJson(Map<String, dynamic> json) => User(
id: json['id'] as int,
username: json['username'] as String,
isAdmin: json['is_admin'] as bool,
);
}
class TokenResponse {
const TokenResponse({required this.token, required this.user});
final String token;
final User user;
factory TokenResponse.fromJson(Map<String, dynamic> json) => TokenResponse(
token: json['token'] as String,
user: User.fromJson(json['user'] as Map<String, dynamic>),
);
}
+68
View File
@@ -0,0 +1,68 @@
import 'position.dart';
class Room {
const Room({required this.id, required this.name, required this.polygon});
final String id;
final String name;
/// Polygon vertices in normalised 0..1 coordinates.
final List<Position> polygon;
Room copyWith({String? name, List<Position>? polygon}) =>
Room(id: id, name: name ?? this.name, polygon: polygon ?? this.polygon);
Map<String, dynamic> toJson() => {
'id': id,
'name': name,
'polygon': polygon.map((p) => p.toJson()).toList(),
};
factory Room.fromJson(Map<String, dynamic> json) => Room(
id: json['id'] as String,
name: json['name'] as String,
polygon: (json['polygon'] as List)
.map((p) => Position.fromJson(p as Map<String, dynamic>))
.toList(),
);
}
class FloorPlan {
const FloorPlan({
required this.id,
required this.floorId,
required this.metersPerUnit,
required this.rooms,
});
final String id;
final String floorId;
/// Scale: how many real-world meters one normalised unit represents.
final double metersPerUnit;
final List<Room> rooms;
FloorPlan copyWith({double? metersPerUnit, List<Room>? rooms}) => FloorPlan(
id: id,
floorId: floorId,
metersPerUnit: metersPerUnit ?? this.metersPerUnit,
rooms: rooms ?? this.rooms,
);
Map<String, dynamic> toJson() => {
'id': id,
'floor_id': floorId,
'meters_per_unit': metersPerUnit,
'rooms': rooms.map((r) => r.toJson()).toList(),
};
factory FloorPlan.fromJson(Map<String, dynamic> json) => FloorPlan(
id: json['id'] as String,
floorId: json['floor_id'] as String,
metersPerUnit: (json['meters_per_unit'] as num).toDouble(),
rooms: (json['rooms'] as List)
.map((r) => Room.fromJson(r as Map<String, dynamic>))
.toList(),
);
}
+1
View File
@@ -0,0 +1 @@
enum FloorPlanMode { view, edit }
+13
View File
@@ -0,0 +1,13 @@
enum OnboardingStatus {
/// No admin user exists yet.
notStarted,
/// Admin user created, floor plan not yet saved.
awaitingFloorPlan,
/// Floor plan saved, no sensors enrolled yet.
awaitingFirstSensor,
/// At least one sensor enrolled; onboarding considered complete.
complete,
}
+17
View File
@@ -0,0 +1,17 @@
/// Single particle in a particle filter cloud snapshot.
class Particle {
const Particle({required this.x, required this.y, required this.weight});
/// Normalised 0..1 coordinates (same space as [Position]).
final double x;
final double y;
/// Unnormalised likelihood weight.
final double weight;
factory Particle.fromJson(Map<String, dynamic> json) => Particle(
x: (json['x'] as num).toDouble(),
y: (json['y'] as num).toDouble(),
weight: (json['weight'] as num).toDouble(),
);
}
+25
View File
@@ -0,0 +1,25 @@
// Normalised coordinates: x and y are in the range 0..1 relative to the floor plan canvas.
class Position {
const Position({required this.x, required this.y});
final double x;
final double y;
Position copyWith({double? x, double? y}) =>
Position(x: x ?? this.x, y: y ?? this.y);
// Key names should match localiserd API fields.
Map<String, dynamic> toJson() => {'x': x, 'y': y};
factory Position.fromJson(Map<String, dynamic> json) => Position(
x: (json['x'] as num).toDouble(),
y: (json['y'] as num).toDouble(),
);
@override
bool operator ==(Object other) =>
other is Position && other.x == x && other.y == y;
@override
int get hashCode => Object.hash(x, y);
}
+56
View File
@@ -0,0 +1,56 @@
import 'position.dart';
enum SensorStatus { online, offline, provisioning }
class Sensor {
const Sensor({
required this.id,
required this.name,
required this.floorId,
required this.position,
required this.status,
this.lastSeen,
});
final String id;
final String name;
final String floorId;
final Position position;
final SensorStatus status;
final DateTime? lastSeen;
Sensor copyWith({
String? name,
Position? position,
SensorStatus? status,
DateTime? lastSeen,
}) =>
Sensor(
id: id,
name: name ?? this.name,
floorId: floorId,
position: position ?? this.position,
status: status ?? this.status,
lastSeen: lastSeen ?? this.lastSeen,
);
Map<String, dynamic> toJson() => {
'id': id,
'name': name,
'floor_id': floorId,
'position': position.toJson(),
'status': status.name,
'last_seen': lastSeen?.toIso8601String(),
};
factory Sensor.fromJson(Map<String, dynamic> json) => Sensor(
id: json['id'] as String,
name: json['name'] as String,
floorId: json['floor_id'] as String,
position: Position.fromJson(json['position'] as Map<String, dynamic>),
status: SensorStatus.values.byName(json['status'] as String),
lastSeen: json['last_seen'] == null
? null
: DateTime.parse(json['last_seen'] as String),
);
}
+18
View File
@@ -0,0 +1,18 @@
class ServerConfig {
const ServerConfig({required this.host, required this.port});
final String host;
final int port;
String get wsUrl => 'ws://$host:$port/socket/websocket';
ServerConfig copyWith({String? host, int? port}) =>
ServerConfig(host: host ?? this.host, port: port ?? this.port);
@override
bool operator ==(Object other) =>
other is ServerConfig && other.host == host && other.port == port;
@override
int get hashCode => Object.hash(host, port);
}
+54
View File
@@ -0,0 +1,54 @@
import 'position.dart';
class Tag {
const Tag({
required this.id,
required this.name,
this.currentRoomId,
this.lastPosition,
this.lastSeen,
});
final String id;
final String name;
final String? currentRoomId;
final Position? lastPosition;
final DateTime? lastSeen;
Tag copyWith({String? name, String? currentRoomId, Position? lastPosition, DateTime? lastSeen}) =>
Tag(
id: id,
name: name ?? this.name,
currentRoomId: currentRoomId ?? this.currentRoomId,
lastPosition: lastPosition ?? this.lastPosition,
lastSeen: lastSeen ?? this.lastSeen,
);
Map<String, dynamic> toJson() => {
'id': id,
'name': name,
'current_room_id': currentRoomId,
'last_position': lastPosition?.toJson(),
'last_seen': lastSeen?.toIso8601String(),
};
factory Tag.fromJson(Map<String, dynamic> json) => Tag(
id: json['id'] as String,
name: json['name'] as String,
currentRoomId: json['current_room_id'] as String?,
lastPosition: json['last_position'] == null
? null
: Position.fromJson(json['last_position'] as Map<String, dynamic>),
lastSeen: json['last_seen'] == null
? null
: DateTime.parse(json['last_seen'] as String),
);
}
// Live position snapshot pushed from localiserd over Phoenix channel.
class TagPosition {
const TagPosition({required this.tagId, required this.position});
final String tagId;
final Position position;
}