Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ pub fn build(b: *std.Build) void {
"vrs_inject_antenna",
b.option(bool, "vrs-inject-antenna", "Inject RTCM3 Type 1008 antenna descriptor for VRS rovers") orelse false,
);
// I/O backend 選択 (src/io.zig)。posix=host/Linux/クラウド、lwip=ESP-IDF(Tab5)。
// lwip backend は未実装 (io_lwip.zig TODO)。host ビルドは posix のまま。
const IoBackend = enum { posix, lwip };
options_step.addOption(
IoBackend,
"io_backend",
b.option(IoBackend, "io-backend", "I/O backend: posix (host/cloud) or lwip (ESP-IDF/Tab5)") orelse .posix,
);
const options_mod = options_step.createModule();

// ── "ntripcaster" library module (src/ tree exposed for tests) ──────────
Expand Down Expand Up @@ -139,4 +147,47 @@ pub fn build(b: *std.Build) void {
b.step("test-integration", "Run integration tests (TCP)").dependOn(
&b.addRunArtifact(int_tests).step,
);

// ── Embedded static library (M2: ESP-IDF/Tab5 link target) ─────────────
// ESP-IDF の CMake component がこの成果物 (libntripcaster.a) を firmware に
// リンクする。cross-compile の健全性検証用にも使う:
// zig build caster-lib -Dio-backend=lwip \
// -Dtarget=riscv32-freestanding -Dcpu=generic_rv32+m+a+f+c
const caster_mod = b.createModule(.{
.root_source_file = b.path("src/embedded.zig"),
.target = target,
.optimize = optimize,
// ESP-IDF provides newlib (malloc/free → PSRAM) and pthread. Zig
// needs libc "declared" to permit the extern "c" allocator decls;
// the actual symbols are resolved by the IDF final link. On the host
// this links the system libc for real (so the lib is host-buildable).
.link_libc = true,
.imports = &.{
.{ .name = "build_options", .module = options_mod },
},
});
// ESP-IDF component ビルドが FreeRTOS/lwip の include dir 群を
// `NTRIPCASTER_IDF_INCLUDES` (`;` 区切り) で渡してくる。io_lwip.zig /
// os_lwip.zig の @cImport がこれらを解決する。host ビルド (env 未設定)
// では素通り。
if (std.process.getEnvVarOwned(b.allocator, "NTRIPCASTER_IDF_INCLUDES")) |inc| {
var it = std.mem.tokenizeScalar(u8, inc, ';');
while (it.next()) |dir| {
if (dir.len == 0) continue;
caster_mod.addSystemIncludePath(.{ .cwd_relative = dir });
}
} else |_| {}

const caster_lib = b.addLibrary(.{
.name = "ntripcaster",
.linkage = .static,
.root_module = caster_mod,
});
// Bundle compiler-rt into the archive: std.fmt's float formatter pulls in
// 128-bit division (__udivti3) which the riscv32 libgcc esp-idf links does
// not provide. Zig's compiler-rt has it.
caster_lib.bundle_compiler_rt = true;
b.step("caster-lib", "Build embedded static library (ESP-IDF link target)").dependOn(
&b.addInstallArtifact(caster_lib, .{}).step,
);
}
38 changes: 20 additions & 18 deletions src/admin/server.zig
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
//! - Phase B で /api/v1/events (SSE) と / (UI) を追加

const std = @import("std");
const io = @import("../io.zig");
const os = @import("../os.zig");
const server_mod = @import("../server.zig");
const stats = @import("stats.zig");

Expand All @@ -36,17 +38,18 @@ pub const AdminState = struct {
server_started_at_ms: i64,

/// listen() がポートを bind した瞬間に set される
started_event: std.Thread.ResetEvent = .{},
started_event: os.ResetEvent = .{},
/// 実際にバインドされたアドレス
listen_address: std.net.Address = undefined,
/// active なリスナー(shutdown() で deinit + free)
listener: ?*std.net.Server = null,
listen_address: io.Address = undefined,
/// active なリスナー(shutdown() で deinit + free)。backend 差は
/// io.Listener が吸収する。
listener: ?*io.Listener = null,
/// shutdown() 時のリスナー free に使う
alloc: std.mem.Allocator,

pub fn shutdown(self: *AdminState) void {
if (self.listener) |l| {
std.posix.shutdown(l.stream.handle, .both) catch {};
l.shutdownAccept();
l.deinit();
self.alloc.destroy(l);
self.listener = null;
Expand All @@ -56,12 +59,11 @@ pub const AdminState = struct {

/// admin リスナーのメインループ。state.shutdown() を呼ぶと終了する。
pub fn listen(admin: *AdminState) !void {
const listener_ptr = try admin.alloc.create(std.net.Server);
const listener_ptr = try admin.alloc.create(io.Listener);
errdefer admin.alloc.destroy(listener_ptr);

const addr = try std.net.Address.parseIp(admin.bind, admin.port);
listener_ptr.* = try addr.listen(.{ .reuse_address = true });
admin.listen_address = listener_ptr.listen_address;
listener_ptr.* = try io.Listener.bind(admin.bind, admin.port);
admin.listen_address = listener_ptr.listenAddress();
admin.listener = listener_ptr;
admin.started_event.set();

Expand All @@ -77,7 +79,7 @@ pub fn listen(admin: *AdminState) !void {
};

const args = ConnArgs{ .stream = conn.stream, .admin = admin };
const t = std.Thread.spawn(.{}, handleConnection, .{args}) catch |err| {
const t = os.Thread.spawn(.{}, handleConnection, .{args}) catch |err| {
admin.state.logger.warn("admin Thread.spawn failed: {}", .{err});
conn.stream.close();
continue;
Expand All @@ -87,7 +89,7 @@ pub fn listen(admin: *AdminState) !void {
}

const ConnArgs = struct {
stream: std.net.Stream,
stream: io.Stream,
admin: *AdminState,
};

Expand All @@ -98,7 +100,7 @@ fn handleConnection(args: ConnArgs) void {
};
}

fn handleRequest(stream: std.net.Stream, admin: *AdminState) !void {
fn handleRequest(stream: io.Stream, admin: *AdminState) !void {
var header_buf: [4096]u8 = undefined;
const header_len = readHeader(stream, &header_buf) catch {
try sendStatus(stream, 400, "Bad Request", "text/plain", "bad request\n");
Expand Down Expand Up @@ -149,7 +151,7 @@ fn handleRequest(stream: std.net.Stream, admin: *AdminState) !void {

/// SSE: 1 秒間隔で composite snapshot を data: イベントで配信。
/// 書き込み失敗(クライアント切断)でループを抜ける。
fn handleSse(stream: std.net.Stream, admin: *AdminState) !void {
fn handleSse(stream: io.Stream, admin: *AdminState) !void {
const headers =
"HTTP/1.0 200 OK\r\n" ++
"Content-Type: text/event-stream\r\n" ++
Expand All @@ -175,7 +177,7 @@ fn handleSse(stream: std.net.Stream, admin: *AdminState) !void {
body.appendSlice(a, "\n\n") catch return;

stream.writeAll(body.items) catch break;
std.Thread.sleep(1 * std.time.ns_per_s);
os.sleep(1 * std.time.ns_per_s);
}
}

Expand All @@ -193,7 +195,7 @@ fn parseRequestLine(header: []const u8) ?RequestLine {
return .{ .method = method, .path = path };
}

fn readHeader(stream: std.net.Stream, buf: []u8) !usize {
fn readHeader(stream: io.Stream, buf: []u8) !usize {
var total: usize = 0;
while (total < buf.len) {
const n = try stream.read(buf[total..]);
Expand Down Expand Up @@ -235,7 +237,7 @@ fn checkBasicAuth(header: []const u8, user: []const u8, password: []const u8) bo
return false;
}

fn sendUnauthorized(stream: std.net.Stream) !void {
fn sendUnauthorized(stream: io.Stream) !void {
const body = "unauthorized\n";
var buf: [256]u8 = undefined;
const head = try std.fmt.bufPrint(
Expand All @@ -252,7 +254,7 @@ fn sendUnauthorized(stream: std.net.Stream) !void {
}

fn sendStatus(
stream: std.net.Stream,
stream: io.Stream,
status: u16,
reason: []const u8,
content_type: []const u8,
Expand Down Expand Up @@ -289,7 +291,7 @@ fn writeClientsAdapter(out: *std.ArrayList(u8), alloc: std.mem.Allocator, admin:
return stats.writeClientsJson(out, alloc, admin.state);
}

fn sendJsonBody(stream: std.net.Stream, admin: *AdminState, writer: *const JsonWriter) !void {
fn sendJsonBody(stream: io.Stream, admin: *AdminState, writer: *const JsonWriter) !void {
var arena = std.heap.ArenaAllocator.init(admin.alloc);
defer arena.deinit();
const alloc = arena.allocator();
Expand Down
8 changes: 5 additions & 3 deletions src/admin/stats.zig
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
//! 文字列フィールドは JSON 仕様に従いエスケープする。

const std = @import("std");
const io = @import("../io.zig");
const os = @import("../os.zig");
const server = @import("../server.zig");

/// ── JSON ヘルパー ────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -40,11 +42,11 @@ pub fn appendQuoted(
try out.append(alloc, '"');
}

/// std.net.Address を "ip:port" 形式で出力する(JSON 文字列として)。
/// io.Address を "ip:port" 形式で出力する(JSON 文字列として)。
pub fn appendAddr(
out: *std.ArrayList(u8),
alloc: std.mem.Allocator,
addr: std.net.Address,
addr: io.Address,
) !void {
var tmp: [128]u8 = undefined;
const formatted = std.fmt.bufPrint(&tmp, "{f}", .{addr}) catch
Expand All @@ -61,7 +63,7 @@ pub fn writeStatusJson(
version: []const u8,
server_started_at_ms: i64,
) !void {
const now_ms = std.time.milliTimestamp();
const now_ms = os.milliTimestamp();
const uptime_ms = now_ms - server_started_at_ms;

try out.append(alloc, '{');
Expand Down
Loading
Loading