From 8780a911b9f7f3a0508457ae13230f7844de6723 Mon Sep 17 00:00:00 2001 From: aaumar25 Date: Mon, 22 Jun 2026 09:44:24 +0900 Subject: [PATCH 1/6] feat: Add CTO's firmware support --- build.zig | 37 +++ build.zig.zon | 4 + src/mcl/Axis.zig | 35 +++ src/mcl/Config.zig | 73 ++++++ src/mcl/Line.zig | 429 +++++++++++++++++++++++++++++++++ src/mcl/Station.zig | 172 +++++++++++++ src/mcl/cclink.zig | 77 ++++++ src/mcl/mcl.zig | 87 +++++++ src/mcl/registers.zig | 37 +++ src/mcl/registers/wr.zig | 233 ++++++++++++++++++ src/mcl/registers/ww.zig | 83 +++++++ src/mcl/registers/x.zig | 504 +++++++++++++++++++++++++++++++++++++++ src/mcl/registers/y.zig | 361 ++++++++++++++++++++++++++++ 13 files changed, 2132 insertions(+) create mode 100644 src/mcl/Axis.zig create mode 100644 src/mcl/Config.zig create mode 100644 src/mcl/Line.zig create mode 100644 src/mcl/Station.zig create mode 100644 src/mcl/cclink.zig create mode 100644 src/mcl/mcl.zig create mode 100644 src/mcl/registers.zig create mode 100644 src/mcl/registers/wr.zig create mode 100644 src/mcl/registers/ww.zig create mode 100644 src/mcl/registers/x.zig create mode 100644 src/mcl/registers/y.zig diff --git a/build.zig b/build.zig index bdf1f46..eeaf3a8 100644 --- a/build.zig +++ b/build.zig @@ -51,4 +51,41 @@ pub fn build(b: *std.Build) !void { ); gen_proto.dependOn(&protoc_step.step); + + // ***** Building for legacy MCL interface ***** + const mdfunc_lib_path = b.option( + []const u8, + "mdfunc", + "Specify the path to the MELSEC static library artifact.", + ) orelse if (target.result.cpu.arch == .x86_64) + "vendor/mdfunc/lib/x64/MdFunc32.lib" + else + "vendor/mdfunc/lib/mdfunc32.lib"; + + const mdfunc_mock_build = b.option( + bool, + "mdfunc_mock", + "Enable building a mock version of the MELSEC data link library.", + ) orelse (target.result.os.tag != .windows); + + const mdfunc = b.dependency("mdfunc", .{ + .target = target, + .optimize = optimize, + .mdfunc = mdfunc_lib_path, + .mock = mdfunc_mock_build, + }); + + const mcl = b.addModule("mcl", .{ + .root_source_file = b.path("src/mcl/mcl.zig"), + .imports = &.{ + .{ .name = "build.zig.zon", .module = build_zig_zon }, + .{ .name = "mdfunc", .module = mdfunc.module("mdfunc") }, + }, + .target = target, + .optimize = optimize, + }); + + const mcl_test = b.addTest(.{ .root_module = mcl }); + const run_mcl_tests = b.addRunArtifact(mcl_test); + test_step.dependOn(&run_mcl_tests.step); } diff --git a/build.zig.zon b/build.zig.zon index 8c940b0..1e13231 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -8,6 +8,10 @@ .url = "https://github.com/Arwalk/zig-protobuf/archive/7673d62.tar.gz", .hash = "protobuf-3.0.0-0e82auyUKACQqyLICPhWm6kJoAru5DNP-idAJMP1W94r", }, + .mdfunc = .{ + .url = "https://github.com/pmotionf/mdfunc.zig/archive/6d70ce0.tar.gz", + .hash = "mdfunc-0.0.5-k9D_qBiuAADEPbD9zlxCm_bwaA41w9vCZuVH-ENIdkFe", + }, }, .paths = .{""}, } diff --git a/src/mcl/Axis.zig b/src/mcl/Axis.zig new file mode 100644 index 0000000..ee33767 --- /dev/null +++ b/src/mcl/Axis.zig @@ -0,0 +1,35 @@ +//! This module provides convenient indexing of system axes, both in terms of +//! line-level indexing and local station-level indexing. +const Axis = @This(); + +const std = @import("std"); + +const MclStation = @import("Station.zig"); + +station: *const MclStation, +index: Index, +id: Id, + +pub const Index = struct { + station: Station, + line: Line, + + /// Local axis index within station. + pub const Station = std.math.IntFittingRange(0, 2); + /// Axis index within line. + pub const Line = std.math.IntFittingRange(0, 64 * 4 * 3 - 1); +}; + +pub const Id = struct { + station: Station, + line: Line, + + /// Local axis ID within station. + pub const Station = std.math.IntFittingRange(1, 3); + /// Axis ID within line. + pub const Line = std.math.IntFittingRange(1, 64 * 4 * 3); +}; + +test { + std.testing.refAllDecls(@This()); +} diff --git a/src/mcl/Config.zig b/src/mcl/Config.zig new file mode 100644 index 0000000..952d946 --- /dev/null +++ b/src/mcl/Config.zig @@ -0,0 +1,73 @@ +const Config = @This(); + +const std = @import("std"); +const cclink = @import("cclink.zig"); +const mcl = @import("mcl.zig"); + +lines: []Line, + +pub const Line = struct { + /// Total number of axes in line. + axes: mcl.Axis.Id.Line, + + /// CC-Link Station ranges. + ranges: []Range, + + pub const Range = struct { + /// CC-Link Channel. + channel: cclink.Channel, + /// CC-Link Station ID. Start of range, inclusive. + start: cclink.Id, + /// CC-Link Station ID. End of range, inclusive. + end: cclink.Id, + }; +}; + +pub fn validate(c: Config) !void { + if (c.lines.len == 0 or c.lines.len > 64 * 4) { + return error.ConfigInvalidNumberOfLines; + } + + var total_stations_num: usize = 0; + var used_cc_link_stations: [64 * 4]bool = .{false} ** (64 * 4); + + for (c.lines) |line| { + var total_line_stations: usize = 0; + if (line.axes == 0 or line.axes > 64 * 4 * 3) { + return error.ConfigInvalidLineAxes; + } + + const required_stations: usize = (line.axes - 1) / 3 + 1; + + for (line.ranges) |range| { + if (range.start == 0 or range.start > 64) { + return error.ConfigInvalidLineRangeStart; + } + if (range.end < range.start or range.end > 64) { + return error.ConfigInvalidLineRangeEnd; + } + const channel_offset: usize = + 64 * @as(usize, @intFromEnum(range.channel)); + for (range.start - 1..range.end) |range_index| { + if (used_cc_link_stations[channel_offset + range_index]) { + return error.ConfigOverlappingLineStationRanges; + } + used_cc_link_stations[channel_offset + range_index] = true; + total_stations_num += 1; + total_line_stations += 1; + } + } + + if (total_line_stations != required_stations) { + return error.ConfigInvalidLineAxesForStations; + } + } + + if (total_stations_num == 0 or total_stations_num > 64 * 4) { + return error.ConfigInvalidTotalNumberOfStations; + } +} + +test { + std.testing.refAllDecls(@This()); +} diff --git a/src/mcl/Line.zig b/src/mcl/Line.zig new file mode 100644 index 0000000..7bfb3cf --- /dev/null +++ b/src/mcl/Line.zig @@ -0,0 +1,429 @@ +const Line = @This(); + +const std = @import("std"); +const mdfunc = @import("mdfunc"); +const cclink = @import("cclink.zig"); +const Axis = @import("Axis.zig"); +const Station = @import("Station.zig"); +const Config = @import("Config.zig"); + +// The maximum number of stations is also the maximum number of lines, as +// there can be a minimum of one station per line. +pub const Index = Station.Index; +pub const Id = Station.Id; + +index: Index, +id: Id, + +/// Axes that make up line. Each axis contains both its own line index and +/// local station index. +axes: []Axis, + +/// Stations that make up line. +stations: []Station, + +x: []Station.X, +y: []Station.Y, +wr: []Station.Wr, +ww: []Station.Ww, + +connection: []Range, + +allocator: std.mem.Allocator, + +const Range = struct { + channel: cclink.Channel, + range: cclink.Range, +}; + +pub fn init( + allocator: std.mem.Allocator, + result: *Line, + line_index: Index, + config: Config.Line, +) !void { + result.index = line_index; + result.id = line_index + 1; + result.connection = try allocator.alloc(Range, config.ranges.len); + errdefer allocator.free(result.connection); + result.axes = try allocator.alloc(Axis, config.axes); + errdefer allocator.free(result.axes); + result.stations = try allocator.alloc(Station, (config.axes - 1) / 3 + 1); + errdefer allocator.free(result.stations); + result.x = try allocator.alloc(Station.X, result.stations.len); + errdefer allocator.free(result.x); + result.y = try allocator.alloc(Station.Y, result.stations.len); + errdefer allocator.free(result.y); + result.wr = try allocator.alloc(Station.Wr, result.stations.len); + errdefer allocator.free(result.wr); + result.ww = try allocator.alloc(Station.Ww, result.stations.len); + errdefer allocator.free(result.ww); + + @memset(result.x, std.mem.zeroes(Station.X)); + @memset(result.y, std.mem.zeroes(Station.Y)); + @memset(result.wr, std.mem.zeroes(Station.Wr)); + @memset(result.ww, std.mem.zeroes(Station.Ww)); + + var num_axes: usize = 0; + + for (config.ranges, 0..) |range, range_i| { + result.connection[range_i] = .{ + .channel = range.channel, + .range = .{ + .start = @intCast(range.start - 1), + .end = @intCast(range.end - 1), + }, + }; + for (0..range.end - range.start + 1) |station_i| { + const start_num_axes = num_axes; + for (0..3) |axis_i| { + if (num_axes >= result.axes.len) break; + result.axes[num_axes] = .{ + .station = &result.stations[station_i], + .index = .{ + .station = @intCast(axis_i), + .line = @intCast(num_axes), + }, + .id = .{ + .station = @intCast(axis_i + 1), + .line = @intCast(num_axes + 1), + }, + }; + num_axes += 1; + } + result.stations[station_i] = .{ + .line = result, + .index = @intCast(station_i), + .id = @intCast(station_i + 1), + .x = &result.x[station_i], + .y = &result.y[station_i], + .wr = &result.wr[station_i], + .ww = &result.ww[station_i], + .axes = result.axes[start_num_axes..num_axes], + .connection = .{ + .channel = range.channel, + .index = @intCast(range.start - 1 + station_i), + }, + }; + } + } + result.allocator = allocator; +} + +pub fn deinit(self: *Line) void { + self.allocator.free(self.axes); + self.allocator.free(self.stations); + self.allocator.free(self.x); + self.allocator.free(self.y); + self.allocator.free(self.wr); + self.allocator.free(self.ww); + self.allocator.free(self.connection); + self.* = undefined; +} + +pub fn poll(line: Line) !void { + var range_offset: usize = 0; + for (line.connection) |range| { + const path = try range.channel.openedPath(); + const range_len: usize = + @as(usize, range.range.end - range.range.start) + 1; + defer range_offset += range_len; + + const x_read_bytes = try mdfunc.receiveEx( + path, + 0, + 0xFF, + .DevX, + @as(i32, range.range.start) * @bitSizeOf(Station.X), + std.mem.sliceAsBytes(line.x[range_offset..][0..range_len]), + ); + if (x_read_bytes != @sizeOf(Station.X) * range_len) { + return cclink.Error.UnexpectedReadSizeX; + } + + const y_read_bytes = try mdfunc.receiveEx( + path, + 0, + 0xFF, + .DevY, + @as(i32, range.range.start) * @bitSizeOf(Station.Y), + std.mem.sliceAsBytes(line.y[range_offset..][0..range_len]), + ); + if (y_read_bytes != @sizeOf(Station.Y) * range_len) { + return cclink.Error.UnexpectedReadSizeY; + } + + const wr_read_bytes = try mdfunc.receiveEx( + path, + 0, + 0xFF, + .DevWr, + @as(i32, range.range.start) * 16, // 16 from MELSEC manual. + std.mem.sliceAsBytes(line.wr[range_offset..][0..range_len]), + ); + if (wr_read_bytes != @sizeOf(Station.Wr) * range_len) { + return cclink.Error.UnexpectedReadSizeWr; + } + } +} + +pub fn pollX(line: Line) (cclink.Error || mdfunc.Error)!void { + var range_offset: usize = 0; + for (line.connection) |range| { + const path = try range.channel.openedPath(); + const range_len: usize = + @as(usize, range.range.end - range.range.start) + 1; + defer range_offset += range_len; + + const read_bytes = try mdfunc.receiveEx( + path, + 0, + 0xFF, + .DevX, + @as(i32, range.range.start) * @bitSizeOf(Station.X), + std.mem.sliceAsBytes(line.x[range_offset..][0..range_len]), + ); + if (read_bytes != @sizeOf(Station.X) * range_len) { + return cclink.Error.UnexpectedReadSizeX; + } + } +} + +pub fn pollY(line: Line) (cclink.Error || mdfunc.Error)!void { + var range_offset: usize = 0; + for (line.connection) |range| { + const path = try range.channel.openedPath(); + const range_len: usize = + @as(usize, range.range.end - range.range.start) + 1; + defer range_offset += range_len; + + const read_bytes = try mdfunc.receiveEx( + path, + 0, + 0xFF, + .DevY, + @as(i32, range.range.start) * @bitSizeOf(Station.Y), + std.mem.sliceAsBytes(line.y[range_offset..][0..range_len]), + ); + if (read_bytes != @sizeOf(Station.Y) * range_len) { + return cclink.Error.UnexpectedReadSizeY; + } + } +} + +pub fn pollWr(line: Line) (cclink.Error || mdfunc.Error)!void { + var range_offset: usize = 0; + for (line.connection) |range| { + const path = try range.channel.openedPath(); + const range_len: usize = + @as(usize, range.range.end - range.range.start) + 1; + defer range_offset += range_len; + + const read_bytes = try mdfunc.receiveEx( + path, + 0, + 0xFF, + .DevWr, + @as(i32, range.range.start) * 16, // 16 from MELSEC manual. + std.mem.sliceAsBytes(line.wr[range_offset..][0..range_len]), + ); + if (read_bytes != @sizeOf(Station.Wr) * range_len) { + return cclink.Error.UnexpectedReadSizeWr; + } + } +} + +pub fn pollWw(line: Line) (cclink.Error || mdfunc.Error)!void { + var range_offset: usize = 0; + for (line.connection) |range| { + const path = try range.channel.openedPath(); + const range_len: usize = + @as(usize, range.range.end - range.range.start) + 1; + defer range_offset += range_len; + + const read_bytes = try mdfunc.receiveEx( + path, + 0, + 0xFF, + .DevWw, + @as(i32, range.range.start) * 16, // 16 from MELSEC manual. + std.mem.sliceAsBytes(line.ww[range_offset..][0..range_len]), + ); + if (read_bytes != @sizeOf(Station.Ww) * range_len) { + return cclink.Error.UnexpectedReadSizeWw; + } + } +} + +pub fn send(line: Line) (cclink.Error || mdfunc.Error)!void { + var range_offset: usize = 0; + for (line.connection) |range| { + const path = try range.channel.openedPath(); + const range_len: usize = + @as(usize, range.range.end - range.range.start) + 1; + defer range_offset += range_len; + + const y_sent_bytes = try mdfunc.receiveEx( + path, + 0, + 0xFF, + .DevY, + @as(i32, range.range.start) * @bitSizeOf(Station.Y), + std.mem.sliceAsBytes(line.y[range_offset..][0..range_len]), + ); + if (y_sent_bytes != @sizeOf(Station.Y) * range_len) { + return cclink.Error.UnexpectedSendSizeY; + } + + const ww_sent_bytes = try mdfunc.receiveEx( + path, + 0, + 0xFF, + .DevWw, + @as(i32, range.range.start) * 16, // 16 from MELSEC manual. + std.mem.sliceAsBytes(line.ww[range_offset..][0..range_len]), + ); + if (ww_sent_bytes != @sizeOf(Station.Ww) * range_len) { + return cclink.Error.UnexpectedSendSizeWw; + } + } +} + +pub fn sendY(line: Line) (cclink.Error || mdfunc.Error)!void { + var range_offset: usize = 0; + for (line.connection) |range| { + const path = try range.channel.openedPath(); + const range_len: usize = + @as(usize, range.range.end - range.range.start) + 1; + defer range_offset += range_len; + + const sent_bytes = try mdfunc.receiveEx( + path, + 0, + 0xFF, + .DevY, + @as(i32, range.range.start) * @bitSizeOf(Station.Y), + std.mem.sliceAsBytes(line.y[range_offset..][0..range_len]), + ); + if (sent_bytes != @sizeOf(Station.Y) * range_len) { + return cclink.Error.UnexpectedSendSizeY; + } + } +} + +pub fn sendWw(line: Line) (cclink.Error || mdfunc.Error)!void { + var range_offset: usize = 0; + for (line.connection) |range| { + const path = try range.channel.openedPath(); + const range_len: usize = + @as(usize, range.range.end - range.range.start) + 1; + defer range_offset += range_len; + + const sent_bytes = try mdfunc.receiveEx( + path, + 0, + 0xFF, + .DevWw, + @as(i32, range.range.start) * 16, // 16 from MELSEC manual. + std.mem.sliceAsBytes(line.ww[range_offset..][0..range_len]), + ); + if (sent_bytes != @sizeOf(Station.Ww) * range_len) { + return cclink.Error.UnexpectedSendSizeWw; + } + } +} + +/// Return the axis of the specified slider, if found in the system. If the +/// slider is split across two axes, then the auxiliary axis will be included +/// in the result tuple. +pub fn search(line: *const Line, slider_id: u16) ?struct { Axis, ?Axis } { + var result: struct { Axis, ?Axis } = .{ undefined, null }; + + for (line.axes) |axis| { + const station = axis.station; + const wr = station.wr; + if (wr.slider_number.axis(axis.index.station) == slider_id) { + result.@"0" = axis; + + if (axis.index.station == 2 and axis.id.line < line.axes.len) { + const next_axis = line.axes[axis.index.line + 1]; + const next_station = next_axis.station; + const next_wr = next_station.wr; + + if (next_wr.slider_number.axis( + next_axis.index.station, + ) == slider_id) { + result.@"1" = next_axis; + } + } + + break; + } + } else { + return null; + } + + // If there are two detected contiguous axes, determine which is primary + // and auxiliary. + if (result.@"1") |*aux| { + const main: *Axis = &result.@"0"; + const station = main.station; + const wr = station.wr; + const state = wr.slider_state.axis(main.index.station); + if (state == .NextAxisAuxiliary or state == .NextAxisCompleted or + state == .PrevAxisAuxiliary or state == .PrevAxisCompleted) + { + const temp = main.*; + main.* = aux.*; + aux.* = temp; + } else if (state == .None) { + const aux_station: *const Station = aux.station; + const aux_wr = aux_station.wr; + const aux_state = aux_wr.slider_state.axis(aux.index.station); + if (aux_state != .None and + aux_state != .NextAxisAuxiliary and + aux_state != .NextAxisCompleted and + aux_state != .PrevAxisAuxiliary and + aux_state != .PrevAxisCompleted) + { + const temp = main.*; + main.* = aux.*; + aux.* = temp; + } + } + } + + return result; +} + +test "Line search" { + var line: Line = undefined; + var _ranges: [1]Config.Line.Range = .{.{ + .channel = .cc_link_1slot, + .start = 1, + .end = 3, + }}; + try Line.init(std.testing.allocator, &line, 0, .{ + .axes = 9, + .ranges = &_ranges, + }); + defer line.deinit(); + + line.stations[1].wr.slider_number.axis3 = 1; + line.stations[2].wr.slider_number.axis1 = 1; + line.stations[1].wr.slider_state.axis3 = .NextAxisCompleted; + line.stations[2].wr.slider_state.axis1 = .PosMoveCompleted; + + const _result = line.search(1); + try std.testing.expect(_result != null); + const result = _result.?; + try std.testing.expect(result.@"1" != null); + const main = result.@"0"; + const aux = result.@"1".?; + try std.testing.expectEqual(7, main.id.line); + try std.testing.expectEqual(6, aux.id.line); +} + +test { + std.testing.refAllDecls(@This()); +} diff --git a/src/mcl/Station.zig b/src/mcl/Station.zig new file mode 100644 index 0000000..28003ec --- /dev/null +++ b/src/mcl/Station.zig @@ -0,0 +1,172 @@ +const Station = @This(); + +const std = @import("std"); +const mdfunc = @import("mdfunc"); +const registers = @import("registers.zig"); +const cclink = @import("cclink.zig"); +const Line = @import("Line.zig"); +const Axis = @import("Axis.zig"); + +pub const X = registers.X; +pub const Y = registers.Y; +pub const Wr = registers.Wr; +pub const Ww = registers.Ww; + +/// Index within configured line, spanning across connection ranges. +pub const Index = std.math.IntFittingRange(0, 64 * 4 - 1); +pub const Id = std.math.IntFittingRange(1, 64 * 4); + +line: *const Line, +index: Index, +id: Id, +axes: []Axis, + +x: *X, +y: *Y, +wr: *Wr, +ww: *Ww, + +connection: struct { + channel: cclink.Channel, + index: cclink.Index, +}, + +pub fn prev(station: Station) ?Station { + if (station.index > 0) { + return station.line.stations[station.index - 1]; + } else return null; +} + +pub fn next(station: Station) ?Station { + if (station.index < station.line.stations.len - 1) { + return station.line.stations[station.index + 1]; + } else return null; +} + +pub fn setY( + station: Station, + /// Bitwise offset of desired field (0..). + offset: u6, +) (cclink.Error || mdfunc.Error)!void { + const path: i32 = try station.connection.channel.openedPath(); + const devno: i32 = @as(i32, station.connection.index) * @bitSizeOf(Y) + + @as(i32, offset); + try mdfunc.devSetEx(path, 0, 0xFF, .DevY, devno); +} + +pub fn resetY( + station: Station, + /// Bitwise offset of desired field (0..). + offset: u6, +) (cclink.Error || mdfunc.Error)!void { + const path: i32 = try station.connection.channel.openedPath(); + const devno: i32 = @as(i32, station.connection.index) * @bitSizeOf(Y) + + @as(i32, offset); + try mdfunc.devRstEx(path, 0, 0xFF, .DevY, devno); +} + +pub fn poll(station: Station) (cclink.Error || mdfunc.Error)!void { + try station.pollX(); + try station.pollY(); + try station.pollWr(); + try station.pollWw(); +} + +pub fn pollX(station: Station) (cclink.Error || mdfunc.Error)!void { + const path = try station.connection.channel.openedPath(); + const read_bytes = try mdfunc.receiveEx( + path, + 0, + 0xFF, + .DevX, + @as(i32, station.connection.index) * @bitSizeOf(X), + std.mem.asBytes(station.x), + ); + if (read_bytes != @sizeOf(X)) { + return cclink.Error.UnexpectedReadSizeX; + } +} + +pub fn pollY(station: Station) (cclink.Error || mdfunc.Error)!void { + const path = try station.connection.channel.openedPath(); + const read_bytes = try mdfunc.receiveEx( + path, + 0, + 0xFF, + .DevY, + @as(i32, station.connection.index) * @bitSizeOf(Y), + std.mem.asBytes(station.y), + ); + if (read_bytes != @sizeOf(Y)) { + return cclink.Error.UnexpectedReadSizeY; + } +} + +pub fn pollWr(station: Station) (cclink.Error || mdfunc.Error)!void { + const path = try station.connection.channel.openedPath(); + const read_bytes = try mdfunc.receiveEx( + path, + 0, + 0xFF, + .DevWr, + @as(i32, station.connection.index) * 16, // 16 from MELSEC manual. + std.mem.asBytes(station.wr), + ); + if (read_bytes != @sizeOf(Wr)) { + return cclink.Error.UnexpectedReadSizeWr; + } +} + +pub fn pollWw(station: Station) (cclink.Error || mdfunc.Error)!void { + const path = try station.connection.channel.openedPath(); + const read_bytes = try mdfunc.receiveEx( + path, + 0, + 0xFF, + .DevWw, + @as(i32, station.connection.index) * 16, // 16 from MELSEC manual. + std.mem.asBytes(station.ww), + ); + if (read_bytes != @sizeOf(Wr)) { + return cclink.Error.UnexpectedReadSizeWr; + } +} + +pub fn send(station: Station) (cclink.Error || mdfunc.Error)!void { + try station.sendWw(); + try station.sendY(); +} + +pub fn sendY(station: Station) (cclink.Error || mdfunc.Error)!void { + const path = try station.connection.channel.openedPath(); + const sent_bytes = try mdfunc.sendEx( + path, + 0, + 0xFF, + .DevY, + @as(i32, station.connection.index) * @bitSizeOf(Y), + std.mem.asBytes(station.y), + ); + if (sent_bytes != @sizeOf(Y)) { + return cclink.Error.UnexpectedSendSizeY; + } +} + +pub fn sendWw(station: Station) (cclink.Error || mdfunc.Error)!void { + const path = try station.connection.channel.openedPath(); + const sent_bytes = try mdfunc.sendEx( + path, + 0, + 0xFF, + .DevWw, + @as(i32, station.connection.index) * 16, + std.mem.asBytes(station.ww), + ); + if (sent_bytes != @sizeOf(Ww)) { + return cclink.Error.UnexpectedSendSizeWw; + } +} + +test { + std.testing.refAllDecls(@This()); +} diff --git a/src/mcl/cclink.zig b/src/mcl/cclink.zig new file mode 100644 index 0000000..433ac3a --- /dev/null +++ b/src/mcl/cclink.zig @@ -0,0 +1,77 @@ +const std = @import("std"); +const mdfunc = @import("mdfunc"); +const registers = @import("registers.zig"); + +pub const Index = std.math.IntFittingRange(0, 63); +pub const Id = std.math.IntFittingRange(1, 64); +pub const Range = struct { + start: Index, + end: Index, +}; + +// Restricts available channels for connection to 4 CC-Link slots. +pub const Channel = enum(u2) { + cc_link_1slot = 0, + cc_link_2slot = 1, + cc_link_3slot = 2, + cc_link_4slot = 3, + + pub fn path(c: Channel) ?i32 { + return paths[@intFromEnum(c)]; + } + + /// Get path of channel, asserting that the path is opened. + pub fn openedPath(self: Channel) Error!i32 { + const chan_idx: u2 = @intFromEnum(self); + if (paths[chan_idx]) |p| { + return p; + } else { + return Error.ChannelUnopened; + } + } + + pub fn toMdfunc(c: Channel) mdfunc.Channel { + return switch (c) { + .cc_link_1slot => mdfunc.Channel.@"CC-Link (1 slot)", + .cc_link_2slot => mdfunc.Channel.@"CC-Link (2 slot)", + .cc_link_3slot => mdfunc.Channel.@"CC-Link (3 slot)", + .cc_link_4slot => mdfunc.Channel.@"CC-Link (4 slot)", + }; + } + + pub fn open(c: Channel) mdfunc.Error!void { + const index: u2 = @intFromEnum(c); + if (mdfunc.open(c.toMdfunc())) |p| { + paths[index] = p; + } else |err| switch (err) { + mdfunc.Error.@"66: Channel-opened error" => {}, + else => |e| return e, + } + } + + pub fn close(channel: Channel) (Error || mdfunc.Error)!void { + const index: u2 = @intFromEnum(channel); + if (paths[index]) |p| { + try mdfunc.close(p); + paths[index] = null; + } else { + return Error.ChannelUnopened; + } + } +}; + +pub const Error = error{ + UnexpectedReadSizeX, + UnexpectedReadSizeY, + UnexpectedReadSizeWr, + UnexpectedReadSizeWw, + UnexpectedSendSizeY, + UnexpectedSendSizeWw, + ChannelUnopened, +}; + +var paths: [4]?i32 = .{null} ** 4; + +test { + std.testing.refAllDecls(@This()); +} diff --git a/src/mcl/mcl.zig b/src/mcl/mcl.zig new file mode 100644 index 0000000..6586445 --- /dev/null +++ b/src/mcl/mcl.zig @@ -0,0 +1,87 @@ +const std = @import("std"); +const mdfunc = @import("mdfunc"); + +pub const registers = @import("registers.zig"); +pub const connection = @import("cclink.zig"); + +pub const Config = @import("Config.zig"); +pub const Axis = @import("Axis.zig"); +pub const Station = @import("Station.zig"); +pub const Line = @import("Line.zig"); + +pub const version: std.SemanticVersion = + std.SemanticVersion.parse(@import("build.zig.zon").version) catch + @compileError("InvalidVersion"); + +pub var lines: []const Line = &.{}; + +// Identical slice of lines as above without the const modifier, allowing the +// MCL library to initialize lines but preventing consumers from mutating. +var _lines: []Line = &.{}; + +pub const Distance = registers.Distance; +pub const Direction = registers.Direction; + +var used_channels: [4]bool = .{false} ** 4; +var allocator: ?std.mem.Allocator = null; + +/// Initialize the MCL library. This must be run before any other MCL library +/// functions, except functions in `Config.zig`, are called. This must also be +/// re-run after every configuration change to the system. +pub fn init(a: std.mem.Allocator, config: Config) !void { + used_channels = .{false} ** 4; + _lines = try a.alloc(Line, config.lines.len); + errdefer a.free(_lines); + + for (config.lines) |line| { + for (line.ranges) |range| { + used_channels[@intFromEnum(range.channel)] = true; + } + } + + for (config.lines, 0..) |line, line_idx| { + try Line.init(a, &_lines[line_idx], @intCast(line_idx), line); + } + lines = _lines; + allocator = a; +} + +pub fn deinit() void { + if (allocator) |_| { + for (_lines) |*line| { + line.deinit(); + } + } + _lines = &.{}; + lines = &.{}; + allocator = null; +} + +/// Opens all channels used in all configured lines. +pub fn open() !void { + for (used_channels, 0..) |used, i| { + if (used) { + const chan: connection.Channel = @enumFromInt(i); + chan.open() catch |e| switch (e) { + mdfunc.Error.@"66: Channel-opened error" => {}, + else => return e, + }; + } + } +} + +/// Closes all channels used in all configured lines. +pub fn close() !void { + for (used_channels, 0..) |used, i| { + if (used) { + const chan: connection.Channel = @enumFromInt(i); + chan.close() catch |e| switch (e) { + else => return e, + }; + } + } +} + +test { + std.testing.refAllDecls(@This()); +} diff --git a/src/mcl/registers.zig b/src/mcl/registers.zig new file mode 100644 index 0000000..6d082ff --- /dev/null +++ b/src/mcl/registers.zig @@ -0,0 +1,37 @@ +const std = @import("std"); +pub const X = @import("registers/x.zig").X; +pub const Y = @import("registers/y.zig").Y; +pub const Wr = @import("registers/wr.zig").Wr; +pub const Ww = @import("registers/ww.zig").Ww; + +pub const Distance = packed struct(u32) { + mm: i16 = 0, + um: i16 = 0, + + pub fn toFloat(self: @This()) f32 { + return @as(f32, @floatFromInt(self.mm)) * 0.001 + + @as(f32, @floatFromInt(self.um)) * 0.000001; + } + + pub fn fromFloat(f: f32) @This() { + const mult: f32 = f * 1000.0; + const mm: f32 = @trunc(mult); + return .{ + .mm = @intFromFloat(mm), + .um = @intFromFloat((mult - mm) * 1000.0), + }; + } +}; + +pub const Direction = enum(u1) { + backward = 0, + forward = 1, + + pub fn flip(self: @This()) @This() { + return if (self == .backward) .forward else .backward; + } +}; + +test { + std.testing.refAllDecls(@This()); +} diff --git a/src/mcl/registers/wr.zig b/src/mcl/registers/wr.zig new file mode 100644 index 0000000..2e9afe2 --- /dev/null +++ b/src/mcl/registers/wr.zig @@ -0,0 +1,233 @@ +const std = @import("std"); +const registers = @import("../registers.zig"); + +const Distance = registers.Distance; + +/// Registers written through CC-Link's "DevWr" device. Used as a "read" +/// register bank. +pub const Wr = packed struct(u256) { + command_response: CommandResponseCode = .NoError, + slider_number: packed struct(u48) { + axis1: u16 = 0, + axis2: u16 = 0, + axis3: u16 = 0, + + pub fn axis(self: @This(), a: u2) u16 { + return switch (a) { + 0 => self.axis1, + 1 => self.axis2, + 2 => self.axis3, + 3 => { + std.log.err( + "Invalid axis index 3 for `slider_number`", + .{}, + ); + unreachable; + }, + }; + } + } = .{}, + slider_location: packed struct(u96) { + axis1: Distance = .{}, + axis2: Distance = .{}, + axis3: Distance = .{}, + + pub fn axis(self: @This(), a: u2) Distance { + return switch (a) { + 0 => self.axis1, + 1 => self.axis2, + 2 => self.axis3, + 3 => { + std.log.err( + "Invalid axis index 3 for `slider_location`", + .{}, + ); + unreachable; + }, + }; + } + } = .{}, + slider_state: packed struct(u48) { + axis1: SliderStateCode = .None, + axis2: SliderStateCode = .None, + axis3: SliderStateCode = .None, + + pub fn axis(self: @This(), a: u2) SliderStateCode { + return switch (a) { + 0 => self.axis1, + 1 => self.axis2, + 2 => self.axis3, + 3 => { + std.log.err( + "Invalid axis index 3 for `slider_state`", + .{}, + ); + unreachable; + }, + }; + } + } = .{}, + pitch_count: packed struct(u48) { + axis1: i16 = 0, + axis2: i16 = 0, + axis3: i16 = 0, + + pub fn axis(self: @This(), a: u2) i16 { + return switch (a) { + 0 => self.axis1, + 1 => self.axis2, + 2 => self.axis3, + 3 => { + std.log.err( + "Invalid axis index 3 for `pitch_count`", + .{}, + ); + unreachable; + }, + }; + } + } = .{}, + + pub const CommandResponseCode = enum(i16) { + NoError = 0, + InvalidCommand = 1, + SliderNotFound = 2, + HomingFailed = 3, + InvalidParameter = 4, + InvalidSystemState = 5, + SliderAlreadyExists = 6, + InvalidAxis = 7, + + pub fn throwError(code: CommandResponseCode) !void { + return switch (code) { + .NoError => {}, + .InvalidCommand => return error.InvalidCommand, + .SliderNotFound => return error.SliderNotFound, + .HomingFailed => return error.HomingFailed, + .InvalidParameter => return error.InvalidParameter, + .InvalidSystemState => return error.InvalidSystemState, + .SliderAlreadyExists => return error.SliderAlreadyExists, + .InvalidAxis => return error.InvalidAxis, + }; + } + }; + + pub const SliderStateCode = enum(i16) { + None = 0, + WarmupProgressing = 1, + WarmupCompleted = 2, + WarmupFault = 3, + CurrentBiasProgressing = 4, + CurrentBiasCompleted = 5, + HomeForward = 6, + HomeBackward = 7, + RampForwardProgressing = 8, + RampForwardCompleted = 9, + RampForwardFault = 10, + RampBackwardProgressing = 11, + RampBackwardCompleted = 12, + RampBackwardFault = 13, + // TODO: Clarify names of below + FwdEncProgressing = 14, + FwdEncCompleted = 15, + FwdEncFault = 16, + BwdEncProgressing = 17, + BwdEncCompleted = 18, + BwdEncFault = 19, + CurrentStepProgressing = 20, + CurrentStepCompleted = 21, + CurrentStepFault = 22, + SpeedStepProgressing = 23, + SpeedStepCompleted = 24, + SpeedStepFault = 25, + PosStepProgressing = 26, + PosStepCompleted = 27, + PosStepFault = 28, + PosMoveProgressing = 29, + PosMoveCompleted = 30, + PosMoveFault = 31, + ForwardCalibrationProgressing = 32, + ForwardCalibrationCompleted = 33, + BackwardIsolationProgressing = 34, + BackwardIsolationCompleted = 35, + ForwardRestartProgressing = 36, + ForwardRestartCompleted = 37, + BackwardRestartProgressing = 38, + BackwardRestartCompleted = 39, + SpdMoveProgressing = 40, + SpdMoveCompleted = 41, + SpdMoveFault = 42, + NextAxisAuxiliary = 43, + // Note: Next Axis Completed will show even when the next axis is + // progressing, if the slider is paused for collision avoidance on the + // next axis. + NextAxisCompleted = 44, + PrevAxisAuxiliary = 45, + // Note: Prev Axis Completed will show even when the prev axis is + // progressing, if the slider is paused for collision avoidance on the + // prev axis. + PrevAxisCompleted = 46, + ForwardIsolationProgressing = 47, + ForwardIsolationCompleted = 48, + Overcurrent = 50, + CommunicationError = 51, + PullForward = 52, + PullForwardCompleted = 53, + PullForwardFault = 54, + PullBackward = 55, + PullBackwardCompleted = 56, + PullBackwardFault = 57, + BackwardCalibrationProgressing = 58, + BackwardCalibrationCompleted = 59, + BackwardCalibrationFault = 60, + ForwardCalibrationFault = 61, + + ChainProgressing = 62, + ChainCompleted = 63, + ChainFault = 64, + ChainSlaveProgressing = 65, + ChainSlaveCompleted = 66, + }; + + pub fn format( + wr: Wr, + comptime _: []const u8, + _: std.fmt.FormatOptions, + writer: anytype, + ) !void { + try writer.writeAll("Wr: {\n"); + try writer.print( + "\tcommand_response: {},\n", + .{wr.command_response}, + ); + try writer.writeAll("\tslider_number: {\n"); + try writer.print("\t\taxis1: {},\n", .{wr.slider_number.axis1}); + try writer.print("\t\taxis2: {},\n", .{wr.slider_number.axis2}); + try writer.print("\t\taxis3: {},\n", .{wr.slider_number.axis3}); + try writer.writeAll("\t},\n"); + try writer.writeAll("\tslider_location: {\n"); + try writer.print("\t\taxis1: {},\n", .{wr.slider_location.axis1}); + try writer.print("\t\taxis2: {},\n", .{wr.slider_location.axis2}); + try writer.print("\t\taxis3: {},\n", .{wr.slider_location.axis3}); + try writer.writeAll("\t},\n"); + try writer.writeAll("\tslider_state: {\n"); + try writer.print("\t\taxis1: {},\n", .{wr.slider_state.axis1}); + try writer.print("\t\taxis2: {},\n", .{wr.slider_state.axis2}); + try writer.print("\t\taxis3: {},\n", .{wr.slider_state.axis3}); + try writer.writeAll("\t},\n"); + try writer.writeAll("\tpitch_count: {\n"); + try writer.print("\t\taxis1: {},\n", .{wr.pitch_count.axis1}); + try writer.print("\t\taxis2: {},\n", .{wr.pitch_count.axis2}); + try writer.print("\t\taxis3: {},\n", .{wr.pitch_count.axis3}); + try writer.writeAll("\t},\n"); + try writer.writeAll("}\n"); + } +}; + +test "Wr" { + try std.testing.expectEqual(32, @sizeOf(Wr)); +} + +test { + std.testing.refAllDecls(@This()); +} diff --git a/src/mcl/registers/ww.zig b/src/mcl/registers/ww.zig new file mode 100644 index 0000000..d8a09eb --- /dev/null +++ b/src/mcl/registers/ww.zig @@ -0,0 +1,83 @@ +const std = @import("std"); +const registers = @import("../registers.zig"); + +const Distance = registers.Distance; + +/// Registers written through CC-Link's "DevWw" device. Used as a "write" +/// register bank. +pub const Ww = packed struct(u256) { + command_code: CommandCode = .None, + command_slider_number: u16 = 0, + target_axis_number: u16 = 0, + location_distance: Distance = .{}, + speed_percentage: u16 = 0, + acceleration_percentage: u16 = 0, + _112: u144 = 0, + + pub const CommandCode = enum(i16) { + None = 0, + Home = 17, + // "By Position" commands calculate slider movement by constant hall + // sensor position feedback, and is much more precise in destination. + MoveSliderToAxisByPosition = 18, + MoveSliderToLocationByPosition = 19, + MoveSliderDistanceByPosition = 20, + // "By Speed" commands calculate slider movement by constant hall + // sensor speed feedback. It should mostly not be used, as the + // destination position becomes far too imprecise. However, it is + // meant to maintain a certain speed while the slider is traveling, and + // to avoid the requirement of having a known system position. + MoveSliderToAxisBySpeed = 21, + MoveSliderToLocationBySpeed = 22, + MoveSliderDistanceBySpeed = 23, + IsolateForward = 24, + IsolateBackward = 25, + Calibration = 26, + RecoverSystemSliders = 27, + RecoverSliderAtAxis = 28, + PushAxisSliderForward = 30, + PushAxisSliderBackward = 31, + PullAxisSliderForward = 32, + PullAxisSliderBackward = 33, + MoveSliderChain = 34, + }; + + pub fn format( + ww: Ww, + comptime _: []const u8, + _: std.fmt.FormatOptions, + writer: anytype, + ) !void { + try writer.writeAll("Ww: {\n"); + try writer.print("\tcommand_code: {},\n", .{ww.command_code}); + try writer.print( + "\tcommand_slider_number: {},\n", + .{ww.command_slider_number}, + ); + try writer.print( + "\ttarget_axis_number: {},\n", + .{ww.target_axis_number}, + ); + try writer.print( + "\tlocation_distance: {},\n", + .{ww.location_distance}, + ); + try writer.print( + "\tspeed_percentage: {},\n", + .{ww.speed_percentage}, + ); + try writer.print( + "\tacceleration_percentage: {},\n", + .{ww.acceleration_percentage}, + ); + try writer.writeAll("}\n"); + } +}; + +test "Ww" { + try std.testing.expectEqual(32, @sizeOf(Ww)); +} + +test { + std.testing.refAllDecls(@This()); +} diff --git a/src/mcl/registers/x.zig b/src/mcl/registers/x.zig new file mode 100644 index 0000000..f010493 --- /dev/null +++ b/src/mcl/registers/x.zig @@ -0,0 +1,504 @@ +const std = @import("std"); +const registers = @import("../registers.zig"); + +const Direction = registers.Direction; + +/// Registers written through CC-Link's "DevX" device. Used as a "read" +/// register bank. +pub const X = packed struct(u64) { + cc_link_enabled: bool = false, + service_enabled: bool = false, + ready_for_command: bool = false, + servo_active: packed struct(u3) { + axis1: bool = false, + axis2: bool = false, + axis3: bool = false, + + pub fn axis(self: @This(), a: u2) bool { + return switch (a) { + 0 => self.axis1, + 1 => self.axis2, + 2 => self.axis3, + 3 => { + std.log.err( + "Invalid axis index 3 for `servo_active`", + .{}, + ); + unreachable; + }, + }; + } + } = .{}, + servo_enabled: bool = false, + emergency_stop_enabled: bool = false, + paused: bool = false, + axis_slider_info_cleared: bool = false, + command_received: bool = false, + axis_enabled: packed struct(u3) { + axis1: bool = false, + axis2: bool = false, + axis3: bool = false, + + pub fn axis(self: @This(), a: u2) bool { + return switch (a) { + 0 => self.axis1, + 1 => self.axis2, + 2 => self.axis3, + 3 => { + std.log.err( + "Invalid axis index 3 for `axis_enabled`", + .{}, + ); + unreachable; + }, + }; + } + } = .{}, + in_position: packed struct(u3) { + axis1: bool = false, + axis2: bool = false, + axis3: bool = false, + + pub fn axis(self: @This(), a: u2) bool { + return switch (a) { + 0 => self.axis1, + 1 => self.axis2, + 2 => self.axis3, + 3 => { + std.log.err("Invalid axis index 3 for `in_position`", .{}); + unreachable; + }, + }; + } + } = .{}, + entered_front: packed struct(u3) { + axis1: bool = false, + axis2: bool = false, + axis3: bool = false, + + pub fn axis(self: @This(), a: u2) bool { + return switch (a) { + 0 => self.axis1, + 1 => self.axis2, + 2 => self.axis3, + 3 => { + std.log.err( + "Invalid axis index 3 for `entered_front`", + .{}, + ); + unreachable; + }, + }; + } + } = .{}, + entered_back: packed struct(u3) { + axis1: bool = false, + axis2: bool = false, + axis3: bool = false, + + pub fn axis(self: @This(), a: u2) bool { + return switch (a) { + 0 => self.axis1, + 1 => self.axis2, + 2 => self.axis3, + 3 => { + std.log.err( + "Invalid axis index 3 for `entered_back`", + .{}, + ); + unreachable; + }, + }; + } + } = .{}, + transmission_stopped: packed struct(u2) { + to_prev: bool = false, + to_next: bool = false, + + pub fn to(self: @This(), dir: Direction) bool { + return switch (dir) { + .backward => self.to_prev, + .forward => self.to_next, + }; + } + } = .{}, + errors_cleared: bool = false, + communication_error: packed struct(u2) { + to_prev: bool = false, + to_next: bool = false, + + pub fn to(self: @This(), dir: Direction) bool { + return switch (dir) { + .backward => self.to_prev, + .forward => self.to_next, + }; + } + } = .{}, + inverter_overheat_detected: bool = false, + overcurrent_detected: packed struct(u3) { + axis1: bool = false, + axis2: bool = false, + axis3: bool = false, + + pub fn axis(self: @This(), a: u2) bool { + return switch (a) { + 0 => self.axis1, + 1 => self.axis2, + 2 => self.axis3, + 3 => { + std.log.err( + "Invalid axis index 3 for `overcurrent_detected`", + .{}, + ); + unreachable; + }, + }; + } + } = .{}, + control_failure: packed struct(u3) { + axis1: bool = false, + axis2: bool = false, + axis3: bool = false, + + pub fn axis(self: @This(), a: u2) bool { + return switch (a) { + 0 => self.axis1, + 1 => self.axis2, + 2 => self.axis3, + 3 => { + std.log.err( + "Invalid axis index 3 for `control_failure`", + .{}, + ); + unreachable; + }, + }; + } + } = .{}, + hall_alarm: packed struct(u6) { + axis1: packed struct(u2) { + back: bool = false, + front: bool = false, + } = .{}, + axis2: packed struct(u2) { + back: bool = false, + front: bool = false, + } = .{}, + axis3: packed struct(u2) { + back: bool = false, + front: bool = false, + } = .{}, + + pub fn axis(self: @This(), a: u2) packed struct(u2) { + back: bool, + front: bool, + } { + return switch (a) { + 0 => .{ + .back = self.axis1.back, + .front = self.axis1.front, + }, + 1 => .{ + .back = self.axis2.back, + .front = self.axis2.front, + }, + 2 => .{ + .back = self.axis3.back, + .front = self.axis3.front, + }, + 3 => { + std.log.err("Invalid axis index 3 for `hall_alarm`", .{}); + unreachable; + }, + }; + } + } = .{}, + self_pause: packed struct(u3) { + axis1: bool = false, + axis2: bool = false, + axis3: bool = false, + + pub fn axis(self: @This(), a: u2) bool { + return switch (a) { + 0 => self.axis1, + 1 => self.axis2, + 2 => self.axis3, + 3 => { + std.log.err("Invalid axis index 3 for `self_pause`", .{}); + unreachable; + }, + }; + } + } = .{}, + pulling_slider: packed struct(u3) { + axis1: bool = false, + axis2: bool = false, + axis3: bool = false, + + pub fn axis(self: @This(), a: u2) bool { + return switch (a) { + 0 => self.axis1, + 1 => self.axis2, + 2 => self.axis3, + 3 => { + std.log.err( + "Invalid axis index 3 for `pulling_slider`", + .{}, + ); + unreachable; + }, + }; + } + } = .{}, + control_loop_max_time_exceeded: bool = false, + hall_alarm_abnormal: packed struct(u6) { + axis1: packed struct(u2) { + back: bool = false, + front: bool = false, + } = .{}, + axis2: packed struct(u2) { + back: bool = false, + front: bool = false, + } = .{}, + axis3: packed struct(u2) { + back: bool = false, + front: bool = false, + } = .{}, + + pub fn axis(self: @This(), a: u2) packed struct(u2) { + back: bool, + front: bool, + } { + return switch (a) { + 0 => .{ + .back = self.axis1.back, + .front = self.axis1.front, + }, + 1 => .{ + .back = self.axis2.back, + .front = self.axis2.front, + }, + 2 => .{ + .back = self.axis3.back, + .front = self.axis3.front, + }, + 3 => { + std.log.err( + "Invalid axis index 3 for `hall_alarm_abnormal`", + .{}, + ); + unreachable; + }, + }; + } + } = .{}, + chain_enabled: packed struct(u6) { + axis1: packed struct(u2) { + backward: bool = false, + forward: bool = false, + } = .{}, + axis2: packed struct(u2) { + backward: bool = false, + forward: bool = false, + } = .{}, + axis3: packed struct(u2) { + backward: bool = false, + forward: bool = false, + } = .{}, + + pub fn axis(self: @This(), a: u2) packed struct(u2) { + backward: bool, + forward: bool, + } { + return switch (a) { + 0 => .{ + .backward = self.axis1.backward, + .forward = self.axis1.forward, + }, + 1 => .{ + .backward = self.axis2.backward, + .forward = self.axis2.forward, + }, + 2 => .{ + .backward = self.axis3.backward, + .forward = self.axis3.forward, + }, + 3 => { + std.log.err( + "Invalid axis index 3 for `chain_enabled`", + .{}, + ); + unreachable; + }, + }; + } + } = .{}, + _60: u4 = 0, + + pub fn format( + x: X, + comptime _: []const u8, + _: std.fmt.FormatOptions, + writer: anytype, + ) !void { + try writer.writeAll("X{\n"); + try writer.print("\tcc_link_enabled: {},\n", .{x.cc_link_enabled}); + try writer.print("\tservice_enabled: {},\n", .{x.service_enabled}); + try writer.print( + "\tready_for_command: {},\n", + .{x.ready_for_command}, + ); + try writer.writeAll("\tservo_active: {\n"); + try writer.print("\t\taxis1: {},\n", .{x.servo_active.axis1}); + try writer.print("\t\taxis2: {},\n", .{x.servo_active.axis2}); + try writer.print("\t\taxis3: {},\n", .{x.servo_active.axis3}); + try writer.writeAll("\t},\n"); + try writer.print("\tservo_enabled: {},\n", .{x.servo_enabled}); + try writer.print( + "\temergency_stop_enabled: {},\n", + .{x.emergency_stop_enabled}, + ); + try writer.print("\tpaused: {},\n", .{x.paused}); + try writer.print( + "\taxis_slider_info_cleared: {},\n", + .{x.axis_slider_info_cleared}, + ); + try writer.print( + "\tcommand_received: {},\n", + .{x.command_received}, + ); + try writer.writeAll("\taxis_enabled: {\n"); + try writer.print("\t\taxis1: {},\n", .{x.axis_enabled.axis1}); + try writer.print("\t\taxis2: {},\n", .{x.axis_enabled.axis2}); + try writer.print("\t\taxis3: {},\n", .{x.axis_enabled.axis3}); + try writer.writeAll("\t},\n"); + try writer.writeAll("\tin_position: {\n"); + try writer.print("\t\taxis1: {},\n", .{x.in_position.axis1}); + try writer.print("\t\taxis2: {},\n", .{x.in_position.axis2}); + try writer.print("\t\taxis3: {},\n", .{x.in_position.axis3}); + try writer.writeAll("\t},\n"); + try writer.writeAll("\tentered_front: {\n"); + try writer.print("\t\taxis1: {},\n", .{x.entered_front.axis1}); + try writer.print("\t\taxis2: {},\n", .{x.entered_front.axis2}); + try writer.print("\t\taxis3: {},\n", .{x.entered_front.axis3}); + try writer.writeAll("\t},\n"); + try writer.writeAll("\tentered_back: {\n"); + try writer.print("\t\taxis1: {},\n", .{x.entered_back.axis1}); + try writer.print("\t\taxis2: {},\n", .{x.entered_back.axis2}); + try writer.print("\t\taxis3: {},\n", .{x.entered_back.axis3}); + try writer.writeAll("\t},\n"); + try writer.writeAll("\ttransmission_stopped: {\n"); + try writer.print( + "\t\tto_prev: {},\n", + .{x.transmission_stopped.to_prev}, + ); + try writer.print( + "\t\tto_next: {},\n", + .{x.transmission_stopped.to_next}, + ); + try writer.writeAll("\t},\n"); + try writer.print("\terrors_cleared: {},\n", .{x.errors_cleared}); + try writer.writeAll("\tcommunication_error: {\n"); + try writer.print( + "\t\tto_prev: {},\n", + .{x.communication_error.to_prev}, + ); + try writer.print( + "\t\tto_next: {},\n", + .{x.communication_error.to_next}, + ); + try writer.writeAll("\t},\n"); + try writer.print( + "\tinverter_overheat_detected: {},\n", + .{x.inverter_overheat_detected}, + ); + try writer.writeAll("\tovercurrent_detected: {\n"); + try writer.print( + "\t\taxis1: {},\n", + .{x.overcurrent_detected.axis1}, + ); + try writer.print( + "\t\taxis2: {},\n", + .{x.overcurrent_detected.axis2}, + ); + try writer.print( + "\t\taxis3: {},\n", + .{x.overcurrent_detected.axis3}, + ); + try writer.writeAll("\t},\n"); + try writer.writeAll("\tcontrol_failure: {\n"); + try writer.print("\t\taxis1: {},\n", .{x.control_failure.axis1}); + try writer.print("\t\taxis2: {},\n", .{x.control_failure.axis2}); + try writer.print("\t\taxis3: {},\n", .{x.control_failure.axis3}); + try writer.writeAll("\t},\n"); + try writer.writeAll("\thall_alarm: {\n"); + for (0..3) |_i| { + const i: u2 = @intCast(_i); + try writer.print("\t\taxis{}: {{\n", .{i + 1}); + try writer.print( + "\t\t\tback: {},\n", + .{x.hall_alarm.axis(i).back}, + ); + try writer.print( + "\t\t\tfront: {},\n", + .{x.hall_alarm.axis(i).front}, + ); + try writer.writeAll("\t\t},\n"); + } + try writer.writeAll("\t},\n"); + try writer.writeAll("\tself_pause: {\n"); + try writer.print("\t\taxis1: {},\n", .{x.self_pause.axis1}); + try writer.print("\t\taxis2: {},\n", .{x.self_pause.axis2}); + try writer.print("\t\taxis3: {},\n", .{x.self_pause.axis3}); + try writer.writeAll("\t},\n"); + try writer.writeAll("\tpulling_slider: {\n"); + try writer.print("\t\taxis1: {},\n", .{x.pulling_slider.axis1}); + try writer.print("\t\taxis2: {},\n", .{x.pulling_slider.axis2}); + try writer.print("\t\taxis3: {},\n", .{x.pulling_slider.axis3}); + try writer.writeAll("\t},\n"); + try writer.print( + "\tcontrol_loop_max_time_exceeded: {},\n", + .{x.control_loop_max_time_exceeded}, + ); + try writer.writeAll("\thall_alarm_abnormal: {\n"); + for (0..3) |_i| { + const i: u2 = @intCast(_i); + try writer.print("\t\taxis{}: {{\n", .{i + 1}); + try writer.print( + "\t\t\tback: {},\n", + .{x.hall_alarm_abnormal.axis(i).back}, + ); + try writer.print( + "\t\t\tfront: {},\n", + .{x.hall_alarm_abnormal.axis(i).front}, + ); + try writer.writeAll("\t\t},\n"); + } + try writer.writeAll("\t},\n"); + try writer.writeAll("}\n"); + try writer.writeAll("\tchain_enabled: {\n"); + for (0..3) |_i| { + const i: u2 = @intCast(_i); + try writer.print("\t\taxis{}: {{\n", .{i + 1}); + try writer.print( + "\t\t\tback: {},\n", + .{x.chain_enabled.axis(i).backward}, + ); + try writer.print( + "\t\t\tfront: {},\n", + .{x.chain_enabled.axis(i).forward}, + ); + try writer.writeAll("\t\t},\n"); + } + try writer.writeAll("\t},\n"); + try writer.writeAll("}\n"); + } +}; + +test "X" { + try std.testing.expectEqual(8, @sizeOf(X)); +} + +test { + std.testing.refAllDecls(@This()); +} diff --git a/src/mcl/registers/y.zig b/src/mcl/registers/y.zig new file mode 100644 index 0000000..e862b55 --- /dev/null +++ b/src/mcl/registers/y.zig @@ -0,0 +1,361 @@ +const std = @import("std"); +const registers = @import("../registers.zig"); + +const Direction = registers.Direction; + +/// Registers written through CC-Link's "DevY" device. Used as a "write" +/// register bank. +pub const Y = packed struct(u64) { + cc_link_enable: bool = false, + service_enable: bool = false, + start_command: bool = false, + reset_command_received: bool = false, + _4: u1 = 0, + axis_servo_release: bool = false, + servo_release: bool = false, + emergency_stop: bool = false, + temporary_pause: bool = false, + stop_driver_transmission: packed struct(u2) { + to_prev: bool = false, + to_next: bool = false, + + pub fn to(self: @This(), dir: Direction) bool { + return switch (dir) { + .backward => self.to_prev, + .forward => self.to_next, + }; + } + + pub fn setTo( + self: *align(8:9:8) @This(), + dir: Direction, + val: bool, + ) void { + switch (dir) { + .backward => self.to_prev = val, + .forward => self.to_next = val, + } + } + } = .{}, + clear_errors: bool = false, + clear_axis_slider_info: bool = false, + prev_axis_isolate_link: bool = false, + next_axis_isolate_link: bool = false, + _15: u1 = 0, + reset_pull_slider: packed struct(u3) { + axis1: bool = false, + axis2: bool = false, + axis3: bool = false, + + pub fn axis(self: @This(), local_axis: u2) bool { + return switch (local_axis) { + 0 => self.axis1, + 1 => self.axis2, + 2 => self.axis3, + 3 => { + std.log.err( + "Invalid axis index 3 for `reset_pull_slider`", + .{}, + ); + unreachable; + }, + }; + } + + pub fn setAxis( + self: *align(8:16:8) @This(), + local_axis: u2, + val: bool, + ) void { + switch (local_axis) { + 0 => self.axis1 = val, + 1 => self.axis2 = val, + 2 => self.axis3 = val, + 3 => { + std.log.err( + "Invalid axis index 3 for `reset_pull_slider`", + .{}, + ); + unreachable; + }, + } + } + } = .{}, + recovery_use_hall_sensor: packed struct(u2) { + back: bool = false, + front: bool = false, + + pub fn side(self: @This(), dir: Direction) bool { + return switch (dir) { + .backward => self.back, + .forward => self.front, + }; + } + + pub fn setSide( + self: *align(8:19:8) @This(), + dir: Direction, + val: bool, + ) void { + switch (dir) { + .backward => self.back = val, + .forward => self.front = val, + } + } + } = .{}, + link_chain: packed struct(u6) { + axis1: packed struct(u2) { + backward: bool = false, + forward: bool = false, + } = .{}, + axis2: packed struct(u2) { + backward: bool = false, + forward: bool = false, + } = .{}, + axis3: packed struct(u2) { + backward: bool = false, + forward: bool = false, + } = .{}, + + pub fn axis(self: @This(), a: u2) packed struct(u2) { + backward: bool, + forward: bool, + } { + return switch (a) { + 0 => .{ + .backward = self.axis1.backward, + .forward = self.axis1.forward, + }, + 1 => .{ + .backward = self.axis2.backward, + .forward = self.axis2.forward, + }, + 2 => .{ + .backward = self.axis3.backward, + .forward = self.axis3.forward, + }, + 3 => { + std.log.err("Invalid axis index 3 for `link_chain`", .{}); + unreachable; + }, + }; + } + + pub fn setAxis(self: *align(8:21:8) @This(), a: u2, val: struct { + backward: ?bool = null, + forward: ?bool = null, + }) void { + switch (a) { + 0 => { + if (val.backward) |b| { + self.axis1.backward = b; + } + if (val.forward) |f| { + self.axis1.forward = f; + } + }, + 1 => { + if (val.backward) |b| { + self.axis2.backward = b; + } + if (val.forward) |f| { + self.axis2.forward = f; + } + }, + 2 => { + if (val.backward) |b| { + self.axis3.backward = b; + } + if (val.forward) |f| { + self.axis3.forward = f; + } + }, + 3 => { + std.log.err("Invalid axis index 3 for `link_chain`", .{}); + unreachable; + }, + } + } + } = .{}, + unlink_chain: packed struct(u6) { + axis1: packed struct(u2) { + backward: bool = false, + forward: bool = false, + } = .{}, + axis2: packed struct(u2) { + backward: bool = false, + forward: bool = false, + } = .{}, + axis3: packed struct(u2) { + backward: bool = false, + forward: bool = false, + } = .{}, + + pub fn axis(self: @This(), a: u2) packed struct(u2) { + backward: bool, + forward: bool, + } { + return switch (a) { + 0 => .{ + .backward = self.axis1.backward, + .forward = self.axis1.forward, + }, + 1 => .{ + .backward = self.axis2.backward, + .forward = self.axis2.forward, + }, + 2 => .{ + .backward = self.axis3.backward, + .forward = self.axis3.forward, + }, + 3 => { + std.log.err( + "Invalid axis index 3 for `unlink_chain`", + .{}, + ); + unreachable; + }, + }; + } + + pub fn setAxis(self: *align(8:27:8) @This(), a: u2, val: struct { + backward: ?bool = null, + forward: ?bool = null, + }) void { + switch (a) { + 0 => { + if (val.backward) |b| { + self.axis1.backward = b; + } + if (val.forward) |f| { + self.axis1.forward = f; + } + }, + 1 => { + if (val.backward) |b| { + self.axis2.backward = b; + } + if (val.forward) |f| { + self.axis2.forward = f; + } + }, + 2 => { + if (val.backward) |b| { + self.axis3.backward = b; + } + if (val.forward) |f| { + self.axis3.forward = f; + } + }, + 3 => { + std.log.err( + "Invalid axis index 3 for `unlink_chain`", + .{}, + ); + unreachable; + }, + } + } + } = .{}, + _33: u31 = 0, + + pub fn format( + y: Y, + comptime _: []const u8, + _: std.fmt.FormatOptions, + writer: anytype, + ) !void { + try writer.writeAll("Y{\n"); + try writer.print("\tcc_link_enable: {},\n", .{y.cc_link_enable}); + try writer.print("\tservice_enable: {},\n", .{y.service_enable}); + try writer.print("\tstart_command: {},\n", .{y.start_command}); + try writer.print( + "\treset_command_received: {},\n", + .{y.reset_command_received}, + ); + try writer.print( + "\taxis_servo_release: {},\n", + .{y.axis_servo_release}, + ); + try writer.print("\tservo_release: {},\n", .{y.servo_release}); + try writer.print("\temergency_stop: {},\n", .{y.emergency_stop}); + try writer.print("\ttemporary_pause: {},\n", .{y.temporary_pause}); + try writer.writeAll("\tstop_driver_transmission: {\n"); + try writer.print( + "\t\tto_prev: {},\n", + .{y.stop_driver_transmission.to_prev}, + ); + try writer.print( + "\t\tto_next: {},\n", + .{y.stop_driver_transmission.to_next}, + ); + try writer.writeAll("\t},\n"); + try writer.print("\tclear_errors: {},\n", .{y.clear_errors}); + try writer.print( + "\tclear_axis_slider_info: {},\n", + .{y.clear_axis_slider_info}, + ); + try writer.print( + "\tprev_axis_isolate_link: {},\n", + .{y.prev_axis_isolate_link}, + ); + try writer.print( + "\tnext_axis_isolate_link: {},\n", + .{y.next_axis_isolate_link}, + ); + try writer.writeAll("\treset_pull_slider: {\n"); + try writer.print("\t\taxis1: {},\n", .{y.reset_pull_slider.axis1}); + try writer.print("\t\taxis2: {},\n", .{y.reset_pull_slider.axis2}); + try writer.print("\t\taxis3: {},\n", .{y.reset_pull_slider.axis3}); + try writer.writeAll("\t},\n"); + try writer.writeAll("\trecovery_use_hall_sensor: {\n"); + try writer.print( + "\t\tback: {},\n", + .{y.recovery_use_hall_sensor.back}, + ); + try writer.print( + "\t\tfront: {},\n", + .{y.recovery_use_hall_sensor.front}, + ); + try writer.writeAll("\t},\n"); + try writer.writeAll("\tlink_chain: {\n"); + for (0..3) |_i| { + const i: u2 = @intCast(_i); + try writer.print("\t\taxis{}: {{\n", .{i + 1}); + try writer.print( + "\t\t\tbackward: {},\n", + .{y.link_chain.axis(i).backward}, + ); + try writer.print( + "\t\t\tforward: {},\n", + .{y.link_chain.axis(i).forward}, + ); + try writer.writeAll("\t\t},\n"); + } + try writer.writeAll("\t},\n"); + try writer.writeAll("\tunlink_chain: {\n"); + for (0..3) |_i| { + const i: u2 = @intCast(_i); + try writer.print("\t\taxis{}: {{\n", .{i + 1}); + try writer.print( + "\t\t\tbackward: {},\n", + .{y.unlink_chain.axis(i).backward}, + ); + try writer.print( + "\t\t\tforward: {},\n", + .{y.unlink_chain.axis(i).forward}, + ); + try writer.writeAll("\t\t},\n"); + } + try writer.writeAll("\t},\n"); + try writer.writeAll("}\n"); + } +}; + +test "Y" { + try std.testing.expectEqual(8, @sizeOf(Y)); +} + +test { + std.testing.refAllDecls(@This()); +} From 430954bb07e4b69520e24c27345662697e125325 Mon Sep 17 00:00:00 2001 From: aaumar25 Date: Mon, 22 Jun 2026 09:48:41 +0900 Subject: [PATCH 2/6] ci: Fix windows test --- .github/workflows/test.yml | 7 ++++++- build.zig | 32 +++++++++++++++++--------------- 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cdda204..46cdecc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -37,5 +37,10 @@ jobs: if: matrix.os == 'Linux' run: zig fmt --check . - - name: Run module tests + - name: Run Linux tests + if: matrix.os == 'Linux' run: zig build test --summary all + + - name: Run Windows tests + if: matrix.os == 'Windows' + run: zig build test -Dmdfunc=$MDFUNC_PATH --summary all diff --git a/build.zig b/build.zig index eeaf3a8..f899d69 100644 --- a/build.zig +++ b/build.zig @@ -5,6 +5,23 @@ pub fn build(b: *std.Build) !void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); + // ***** Options for legacy MCL interface ***** + const mdfunc_lib_path = b.option( + []const u8, + "mdfunc", + "Specify the path to the MELSEC static library artifact.", + ) orelse if (target.result.cpu.arch == .x86_64) + "vendor/mdfunc/lib/x64/MdFunc32.lib" + else + "vendor/mdfunc/lib/mdfunc32.lib"; + + const mdfunc_mock_build = b.option( + bool, + "mdfunc_mock", + "Enable building a mock version of the MELSEC data link library.", + ) orelse (target.result.os.tag != .windows); + // ***** Options for legacy MCL interface ***** + const protobuf_dep = b.dependency("protobuf", .{ .target = target, .optimize = optimize, @@ -53,21 +70,6 @@ pub fn build(b: *std.Build) !void { gen_proto.dependOn(&protoc_step.step); // ***** Building for legacy MCL interface ***** - const mdfunc_lib_path = b.option( - []const u8, - "mdfunc", - "Specify the path to the MELSEC static library artifact.", - ) orelse if (target.result.cpu.arch == .x86_64) - "vendor/mdfunc/lib/x64/MdFunc32.lib" - else - "vendor/mdfunc/lib/mdfunc32.lib"; - - const mdfunc_mock_build = b.option( - bool, - "mdfunc_mock", - "Enable building a mock version of the MELSEC data link library.", - ) orelse (target.result.os.tag != .windows); - const mdfunc = b.dependency("mdfunc", .{ .target = target, .optimize = optimize, From 0b50b4f8a934666d2934d3c24a46e2e06062838d Mon Sep 17 00:00:00 2001 From: aaumar25 Date: Mon, 22 Jun 2026 10:48:53 +0900 Subject: [PATCH 3/6] ci: build test with mdfunc mock --- build.zig | 55 ++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 36 insertions(+), 19 deletions(-) diff --git a/build.zig b/build.zig index f899d69..43d8c02 100644 --- a/build.zig +++ b/build.zig @@ -5,23 +5,6 @@ pub fn build(b: *std.Build) !void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); - // ***** Options for legacy MCL interface ***** - const mdfunc_lib_path = b.option( - []const u8, - "mdfunc", - "Specify the path to the MELSEC static library artifact.", - ) orelse if (target.result.cpu.arch == .x86_64) - "vendor/mdfunc/lib/x64/MdFunc32.lib" - else - "vendor/mdfunc/lib/mdfunc32.lib"; - - const mdfunc_mock_build = b.option( - bool, - "mdfunc_mock", - "Enable building a mock version of the MELSEC data link library.", - ) orelse (target.result.os.tag != .windows); - // ***** Options for legacy MCL interface ***** - const protobuf_dep = b.dependency("protobuf", .{ .target = target, .optimize = optimize, @@ -70,6 +53,21 @@ pub fn build(b: *std.Build) !void { gen_proto.dependOn(&protoc_step.step); // ***** Building for legacy MCL interface ***** + const mdfunc_lib_path = b.option( + []const u8, + "mdfunc", + "Specify the path to the MELSEC static library artifact.", + ) orelse if (target.result.cpu.arch == .x86_64) + "vendor/mdfunc/lib/x64/MdFunc32.lib" + else + "vendor/mdfunc/lib/mdfunc32.lib"; + + const mdfunc_mock_build = b.option( + bool, + "mdfunc_mock", + "Enable building a mock version of the MELSEC data link library.", + ) orelse (target.result.os.tag != .windows); + const mdfunc = b.dependency("mdfunc", .{ .target = target, .optimize = optimize, @@ -77,7 +75,7 @@ pub fn build(b: *std.Build) !void { .mock = mdfunc_mock_build, }); - const mcl = b.addModule("mcl", .{ + _ = b.addModule("mcl", .{ .root_source_file = b.path("src/mcl/mcl.zig"), .imports = &.{ .{ .name = "build.zig.zon", .module = build_zig_zon }, @@ -87,7 +85,26 @@ pub fn build(b: *std.Build) !void { .optimize = optimize, }); - const mcl_test = b.addTest(.{ .root_module = mcl }); + const mdfunc_mock = b.dependency("mdfunc", .{ + .target = target, + .optimize = optimize, + .mdfunc = mdfunc_lib_path, + .mock = mdfunc_mock_build, + }); + + const test_mod = b.createModule(.{ + .root_source_file = b.path("src/mcl/mcl.zig"), + .imports = &.{ + .{ .name = "build.zig.zon", .module = build_zig_zon }, + .{ .name = "mdfunc", .module = mdfunc_mock.module("mdfunc") }, + }, + .target = target, + .optimize = optimize, + }); + + const mcl_test = b.addTest(.{ + .root_module = test_mod, + }); const run_mcl_tests = b.addRunArtifact(mcl_test); test_step.dependOn(&run_mcl_tests.step); } From 31418728c18c0469077991236e57dfdc8e1e7534 Mon Sep 17 00:00:00 2001 From: aaumar25 Date: Mon, 22 Jun 2026 16:45:12 +0900 Subject: [PATCH 4/6] fix: mock build --- build.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.zig b/build.zig index 43d8c02..a954818 100644 --- a/build.zig +++ b/build.zig @@ -89,7 +89,7 @@ pub fn build(b: *std.Build) !void { .target = target, .optimize = optimize, .mdfunc = mdfunc_lib_path, - .mock = mdfunc_mock_build, + .mock = true, }); const test_mod = b.createModule(.{ From 01d85bdfcdaf2b4ac12fa348ed9ee27a12cc8fb0 Mon Sep 17 00:00:00 2001 From: aaumar25 Date: Tue, 23 Jun 2026 08:04:13 +0900 Subject: [PATCH 5/6] refactor: Implement nested write --- src/mcl/registers.zig | 82 ++++++++++++++++++++ src/mcl/registers/wr.zig | 34 +-------- src/mcl/registers/ww.zig | 31 +------- src/mcl/registers/x.zig | 159 +-------------------------------------- src/mcl/registers/y.zig | 92 +--------------------- 5 files changed, 90 insertions(+), 308 deletions(-) diff --git a/src/mcl/registers.zig b/src/mcl/registers.zig index 6d082ff..1dcf786 100644 --- a/src/mcl/registers.zig +++ b/src/mcl/registers.zig @@ -32,6 +32,88 @@ pub const Direction = enum(u1) { } }; +pub fn nestedWrite( + name: []const u8, + val: anytype, + indent: usize, + writer: *std.Io.Writer, +) !usize { + var written_bytes: usize = 0; + const ti = @typeInfo(@TypeOf(val)); + switch (ti) { + .@"struct" => { + try writer.splatBytesAll(" ", indent); + written_bytes += 4 * indent; + try writer.print("{s}: {{\n", .{name}); + written_bytes += name.len + 4; + inline for (ti.@"struct".fields) |field| { + if (field.name[0] == '_') { + continue; + } + written_bytes += try nestedWrite( + field.name, + @field(val, field.name), + indent + 1, + writer, + ); + } + try writer.splatBytesAll(" ", indent); + written_bytes += 4 * indent; + try writer.writeAll("},\n"); + written_bytes += 3; + }, + .bool, .int => { + try writer.splatBytesAll(" ", indent); + written_bytes += 4 * indent; + try writer.print("{s}: ", .{name}); + written_bytes += name.len + 2; + try writer.print("{},\n", .{val}); + written_bytes += std.fmt.count("{},\n", .{val}); + }, + .float => { + try writer.splatBytesAll(" ", indent); + written_bytes += 4 * indent; + try writer.print("{s}: ", .{name}); + written_bytes += name.len + 2; + try writer.print("{d},\n", .{val}); + written_bytes += std.fmt.count("{d},\n", .{val}); + }, + .@"enum" => { + try writer.splatBytesAll(" ", indent); + written_bytes += 4 * indent; + try writer.print("{s}: ", .{name}); + written_bytes += name.len + 2; + try writer.print("{s},\n", .{@tagName(val)}); + written_bytes += std.fmt.count("{s},\n", .{@tagName(val)}); + }, + .@"union" => { + try writer.splatBytesAll(" ", indent); + written_bytes += 4 * indent; + try writer.print("{s} (union): {{\n", .{name}); + written_bytes += name.len + 4; + inline for (ti.@"union".fields) |field| { + if (field.name[0] == '_') { + continue; + } + written_bytes += try nestedWrite( + field.name, + @field(val, field.name), + indent + 1, + writer, + ); + } + try writer.splatBytesAll(" ", indent); + written_bytes += 4 * indent; + try writer.writeAll("},\n"); + written_bytes += 3; + }, + else => { + unreachable; + }, + } + return written_bytes; +} + test { std.testing.refAllDecls(@This()); } diff --git a/src/mcl/registers/wr.zig b/src/mcl/registers/wr.zig index 2e9afe2..d2052b1 100644 --- a/src/mcl/registers/wr.zig +++ b/src/mcl/registers/wr.zig @@ -189,38 +189,8 @@ pub const Wr = packed struct(u256) { ChainSlaveCompleted = 66, }; - pub fn format( - wr: Wr, - comptime _: []const u8, - _: std.fmt.FormatOptions, - writer: anytype, - ) !void { - try writer.writeAll("Wr: {\n"); - try writer.print( - "\tcommand_response: {},\n", - .{wr.command_response}, - ); - try writer.writeAll("\tslider_number: {\n"); - try writer.print("\t\taxis1: {},\n", .{wr.slider_number.axis1}); - try writer.print("\t\taxis2: {},\n", .{wr.slider_number.axis2}); - try writer.print("\t\taxis3: {},\n", .{wr.slider_number.axis3}); - try writer.writeAll("\t},\n"); - try writer.writeAll("\tslider_location: {\n"); - try writer.print("\t\taxis1: {},\n", .{wr.slider_location.axis1}); - try writer.print("\t\taxis2: {},\n", .{wr.slider_location.axis2}); - try writer.print("\t\taxis3: {},\n", .{wr.slider_location.axis3}); - try writer.writeAll("\t},\n"); - try writer.writeAll("\tslider_state: {\n"); - try writer.print("\t\taxis1: {},\n", .{wr.slider_state.axis1}); - try writer.print("\t\taxis2: {},\n", .{wr.slider_state.axis2}); - try writer.print("\t\taxis3: {},\n", .{wr.slider_state.axis3}); - try writer.writeAll("\t},\n"); - try writer.writeAll("\tpitch_count: {\n"); - try writer.print("\t\taxis1: {},\n", .{wr.pitch_count.axis1}); - try writer.print("\t\taxis2: {},\n", .{wr.pitch_count.axis2}); - try writer.print("\t\taxis3: {},\n", .{wr.pitch_count.axis3}); - try writer.writeAll("\t},\n"); - try writer.writeAll("}\n"); + pub fn format(wr: Wr, writer: anytype) !void { + _ = try registers.nestedWrite("Wr", wr, 0, writer); } }; diff --git a/src/mcl/registers/ww.zig b/src/mcl/registers/ww.zig index d8a09eb..cff0031 100644 --- a/src/mcl/registers/ww.zig +++ b/src/mcl/registers/ww.zig @@ -42,35 +42,8 @@ pub const Ww = packed struct(u256) { MoveSliderChain = 34, }; - pub fn format( - ww: Ww, - comptime _: []const u8, - _: std.fmt.FormatOptions, - writer: anytype, - ) !void { - try writer.writeAll("Ww: {\n"); - try writer.print("\tcommand_code: {},\n", .{ww.command_code}); - try writer.print( - "\tcommand_slider_number: {},\n", - .{ww.command_slider_number}, - ); - try writer.print( - "\ttarget_axis_number: {},\n", - .{ww.target_axis_number}, - ); - try writer.print( - "\tlocation_distance: {},\n", - .{ww.location_distance}, - ); - try writer.print( - "\tspeed_percentage: {},\n", - .{ww.speed_percentage}, - ); - try writer.print( - "\tacceleration_percentage: {},\n", - .{ww.acceleration_percentage}, - ); - try writer.writeAll("}\n"); + pub fn format(ww: Ww, writer: anytype) !void { + _ = try registers.nestedWrite("Ww", ww, 0, writer); } }; diff --git a/src/mcl/registers/x.zig b/src/mcl/registers/x.zig index f010493..3ce2ef6 100644 --- a/src/mcl/registers/x.zig +++ b/src/mcl/registers/x.zig @@ -335,163 +335,8 @@ pub const X = packed struct(u64) { } = .{}, _60: u4 = 0, - pub fn format( - x: X, - comptime _: []const u8, - _: std.fmt.FormatOptions, - writer: anytype, - ) !void { - try writer.writeAll("X{\n"); - try writer.print("\tcc_link_enabled: {},\n", .{x.cc_link_enabled}); - try writer.print("\tservice_enabled: {},\n", .{x.service_enabled}); - try writer.print( - "\tready_for_command: {},\n", - .{x.ready_for_command}, - ); - try writer.writeAll("\tservo_active: {\n"); - try writer.print("\t\taxis1: {},\n", .{x.servo_active.axis1}); - try writer.print("\t\taxis2: {},\n", .{x.servo_active.axis2}); - try writer.print("\t\taxis3: {},\n", .{x.servo_active.axis3}); - try writer.writeAll("\t},\n"); - try writer.print("\tservo_enabled: {},\n", .{x.servo_enabled}); - try writer.print( - "\temergency_stop_enabled: {},\n", - .{x.emergency_stop_enabled}, - ); - try writer.print("\tpaused: {},\n", .{x.paused}); - try writer.print( - "\taxis_slider_info_cleared: {},\n", - .{x.axis_slider_info_cleared}, - ); - try writer.print( - "\tcommand_received: {},\n", - .{x.command_received}, - ); - try writer.writeAll("\taxis_enabled: {\n"); - try writer.print("\t\taxis1: {},\n", .{x.axis_enabled.axis1}); - try writer.print("\t\taxis2: {},\n", .{x.axis_enabled.axis2}); - try writer.print("\t\taxis3: {},\n", .{x.axis_enabled.axis3}); - try writer.writeAll("\t},\n"); - try writer.writeAll("\tin_position: {\n"); - try writer.print("\t\taxis1: {},\n", .{x.in_position.axis1}); - try writer.print("\t\taxis2: {},\n", .{x.in_position.axis2}); - try writer.print("\t\taxis3: {},\n", .{x.in_position.axis3}); - try writer.writeAll("\t},\n"); - try writer.writeAll("\tentered_front: {\n"); - try writer.print("\t\taxis1: {},\n", .{x.entered_front.axis1}); - try writer.print("\t\taxis2: {},\n", .{x.entered_front.axis2}); - try writer.print("\t\taxis3: {},\n", .{x.entered_front.axis3}); - try writer.writeAll("\t},\n"); - try writer.writeAll("\tentered_back: {\n"); - try writer.print("\t\taxis1: {},\n", .{x.entered_back.axis1}); - try writer.print("\t\taxis2: {},\n", .{x.entered_back.axis2}); - try writer.print("\t\taxis3: {},\n", .{x.entered_back.axis3}); - try writer.writeAll("\t},\n"); - try writer.writeAll("\ttransmission_stopped: {\n"); - try writer.print( - "\t\tto_prev: {},\n", - .{x.transmission_stopped.to_prev}, - ); - try writer.print( - "\t\tto_next: {},\n", - .{x.transmission_stopped.to_next}, - ); - try writer.writeAll("\t},\n"); - try writer.print("\terrors_cleared: {},\n", .{x.errors_cleared}); - try writer.writeAll("\tcommunication_error: {\n"); - try writer.print( - "\t\tto_prev: {},\n", - .{x.communication_error.to_prev}, - ); - try writer.print( - "\t\tto_next: {},\n", - .{x.communication_error.to_next}, - ); - try writer.writeAll("\t},\n"); - try writer.print( - "\tinverter_overheat_detected: {},\n", - .{x.inverter_overheat_detected}, - ); - try writer.writeAll("\tovercurrent_detected: {\n"); - try writer.print( - "\t\taxis1: {},\n", - .{x.overcurrent_detected.axis1}, - ); - try writer.print( - "\t\taxis2: {},\n", - .{x.overcurrent_detected.axis2}, - ); - try writer.print( - "\t\taxis3: {},\n", - .{x.overcurrent_detected.axis3}, - ); - try writer.writeAll("\t},\n"); - try writer.writeAll("\tcontrol_failure: {\n"); - try writer.print("\t\taxis1: {},\n", .{x.control_failure.axis1}); - try writer.print("\t\taxis2: {},\n", .{x.control_failure.axis2}); - try writer.print("\t\taxis3: {},\n", .{x.control_failure.axis3}); - try writer.writeAll("\t},\n"); - try writer.writeAll("\thall_alarm: {\n"); - for (0..3) |_i| { - const i: u2 = @intCast(_i); - try writer.print("\t\taxis{}: {{\n", .{i + 1}); - try writer.print( - "\t\t\tback: {},\n", - .{x.hall_alarm.axis(i).back}, - ); - try writer.print( - "\t\t\tfront: {},\n", - .{x.hall_alarm.axis(i).front}, - ); - try writer.writeAll("\t\t},\n"); - } - try writer.writeAll("\t},\n"); - try writer.writeAll("\tself_pause: {\n"); - try writer.print("\t\taxis1: {},\n", .{x.self_pause.axis1}); - try writer.print("\t\taxis2: {},\n", .{x.self_pause.axis2}); - try writer.print("\t\taxis3: {},\n", .{x.self_pause.axis3}); - try writer.writeAll("\t},\n"); - try writer.writeAll("\tpulling_slider: {\n"); - try writer.print("\t\taxis1: {},\n", .{x.pulling_slider.axis1}); - try writer.print("\t\taxis2: {},\n", .{x.pulling_slider.axis2}); - try writer.print("\t\taxis3: {},\n", .{x.pulling_slider.axis3}); - try writer.writeAll("\t},\n"); - try writer.print( - "\tcontrol_loop_max_time_exceeded: {},\n", - .{x.control_loop_max_time_exceeded}, - ); - try writer.writeAll("\thall_alarm_abnormal: {\n"); - for (0..3) |_i| { - const i: u2 = @intCast(_i); - try writer.print("\t\taxis{}: {{\n", .{i + 1}); - try writer.print( - "\t\t\tback: {},\n", - .{x.hall_alarm_abnormal.axis(i).back}, - ); - try writer.print( - "\t\t\tfront: {},\n", - .{x.hall_alarm_abnormal.axis(i).front}, - ); - try writer.writeAll("\t\t},\n"); - } - try writer.writeAll("\t},\n"); - try writer.writeAll("}\n"); - try writer.writeAll("\tchain_enabled: {\n"); - for (0..3) |_i| { - const i: u2 = @intCast(_i); - try writer.print("\t\taxis{}: {{\n", .{i + 1}); - try writer.print( - "\t\t\tback: {},\n", - .{x.chain_enabled.axis(i).backward}, - ); - try writer.print( - "\t\t\tfront: {},\n", - .{x.chain_enabled.axis(i).forward}, - ); - try writer.writeAll("\t\t},\n"); - } - try writer.writeAll("\t},\n"); - try writer.writeAll("}\n"); + pub fn format(x: X, writer: anytype) !void { + _ = try registers.nestedWrite("x", x, 0, writer); } }; diff --git a/src/mcl/registers/y.zig b/src/mcl/registers/y.zig index e862b55..7e2ebad 100644 --- a/src/mcl/registers/y.zig +++ b/src/mcl/registers/y.zig @@ -259,96 +259,8 @@ pub const Y = packed struct(u64) { } = .{}, _33: u31 = 0, - pub fn format( - y: Y, - comptime _: []const u8, - _: std.fmt.FormatOptions, - writer: anytype, - ) !void { - try writer.writeAll("Y{\n"); - try writer.print("\tcc_link_enable: {},\n", .{y.cc_link_enable}); - try writer.print("\tservice_enable: {},\n", .{y.service_enable}); - try writer.print("\tstart_command: {},\n", .{y.start_command}); - try writer.print( - "\treset_command_received: {},\n", - .{y.reset_command_received}, - ); - try writer.print( - "\taxis_servo_release: {},\n", - .{y.axis_servo_release}, - ); - try writer.print("\tservo_release: {},\n", .{y.servo_release}); - try writer.print("\temergency_stop: {},\n", .{y.emergency_stop}); - try writer.print("\ttemporary_pause: {},\n", .{y.temporary_pause}); - try writer.writeAll("\tstop_driver_transmission: {\n"); - try writer.print( - "\t\tto_prev: {},\n", - .{y.stop_driver_transmission.to_prev}, - ); - try writer.print( - "\t\tto_next: {},\n", - .{y.stop_driver_transmission.to_next}, - ); - try writer.writeAll("\t},\n"); - try writer.print("\tclear_errors: {},\n", .{y.clear_errors}); - try writer.print( - "\tclear_axis_slider_info: {},\n", - .{y.clear_axis_slider_info}, - ); - try writer.print( - "\tprev_axis_isolate_link: {},\n", - .{y.prev_axis_isolate_link}, - ); - try writer.print( - "\tnext_axis_isolate_link: {},\n", - .{y.next_axis_isolate_link}, - ); - try writer.writeAll("\treset_pull_slider: {\n"); - try writer.print("\t\taxis1: {},\n", .{y.reset_pull_slider.axis1}); - try writer.print("\t\taxis2: {},\n", .{y.reset_pull_slider.axis2}); - try writer.print("\t\taxis3: {},\n", .{y.reset_pull_slider.axis3}); - try writer.writeAll("\t},\n"); - try writer.writeAll("\trecovery_use_hall_sensor: {\n"); - try writer.print( - "\t\tback: {},\n", - .{y.recovery_use_hall_sensor.back}, - ); - try writer.print( - "\t\tfront: {},\n", - .{y.recovery_use_hall_sensor.front}, - ); - try writer.writeAll("\t},\n"); - try writer.writeAll("\tlink_chain: {\n"); - for (0..3) |_i| { - const i: u2 = @intCast(_i); - try writer.print("\t\taxis{}: {{\n", .{i + 1}); - try writer.print( - "\t\t\tbackward: {},\n", - .{y.link_chain.axis(i).backward}, - ); - try writer.print( - "\t\t\tforward: {},\n", - .{y.link_chain.axis(i).forward}, - ); - try writer.writeAll("\t\t},\n"); - } - try writer.writeAll("\t},\n"); - try writer.writeAll("\tunlink_chain: {\n"); - for (0..3) |_i| { - const i: u2 = @intCast(_i); - try writer.print("\t\taxis{}: {{\n", .{i + 1}); - try writer.print( - "\t\t\tbackward: {},\n", - .{y.unlink_chain.axis(i).backward}, - ); - try writer.print( - "\t\t\tforward: {},\n", - .{y.unlink_chain.axis(i).forward}, - ); - try writer.writeAll("\t\t},\n"); - } - try writer.writeAll("\t},\n"); - try writer.writeAll("}\n"); + pub fn format(y: Y, writer: anytype) !void { + _ = try registers.nestedWrite("Y", y, 0, writer); } }; From 6ab55a05ad394ca1cec481356d1e59dbac09708e Mon Sep 17 00:00:00 2001 From: aaumar25 Date: Thu, 25 Jun 2026 14:23:02 +0900 Subject: [PATCH 6/6] fix: Wrong unit used in `Distance` conversion --- src/mcl/registers.zig | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/mcl/registers.zig b/src/mcl/registers.zig index 1dcf786..5d4ce9d 100644 --- a/src/mcl/registers.zig +++ b/src/mcl/registers.zig @@ -8,17 +8,18 @@ pub const Distance = packed struct(u32) { mm: i16 = 0, um: i16 = 0, + /// Cast distance value to millimeter floating point pub fn toFloat(self: @This()) f32 { - return @as(f32, @floatFromInt(self.mm)) * 0.001 + - @as(f32, @floatFromInt(self.um)) * 0.000001; + return @as(f32, @floatFromInt(self.mm)) * 1 + + @as(f32, @floatFromInt(self.um)) * 0.001; } + /// Cast millimiter unit value to Distance type pub fn fromFloat(f: f32) @This() { - const mult: f32 = f * 1000.0; - const mm: f32 = @trunc(mult); + const mm: f32 = @trunc(f); return .{ .mm = @intFromFloat(mm), - .um = @intFromFloat((mult - mm) * 1000.0), + .um = @intFromFloat((f - mm) * 1000.0), }; } };