From fbad8d3b54790e6380ab4fbfe3d4faa570033b5c Mon Sep 17 00:00:00 2001 From: InitusNovus Date: Wed, 27 Aug 2025 09:14:21 +0900 Subject: [PATCH 01/15] chore(v0.2.1): scaffold projects Signal.CANdy.Core, Signal.CANdy, Signal.CANdy.CLI; wire references; keep build/tests green --- DBC_Parser.sln | 21 +++++++++++++++++++ src/Signal.CANdy.CLI/Program.fs | 2 ++ src/Signal.CANdy.CLI/Signal.CANdy.CLI.fsproj | 16 ++++++++++++++ src/Signal.CANdy.Core/Library.fs | 5 +++++ .../Signal.CANdy.Core.fsproj | 12 +++++++++++ src/Signal.CANdy/Library.fs | 5 +++++ src/Signal.CANdy/Signal.CANdy.fsproj | 16 ++++++++++++++ 7 files changed, 77 insertions(+) create mode 100644 src/Signal.CANdy.CLI/Program.fs create mode 100644 src/Signal.CANdy.CLI/Signal.CANdy.CLI.fsproj create mode 100644 src/Signal.CANdy.Core/Library.fs create mode 100644 src/Signal.CANdy.Core/Signal.CANdy.Core.fsproj create mode 100644 src/Signal.CANdy/Library.fs create mode 100644 src/Signal.CANdy/Signal.CANdy.fsproj diff --git a/DBC_Parser.sln b/DBC_Parser.sln index ee4dcab..917cfa8 100644 --- a/DBC_Parser.sln +++ b/DBC_Parser.sln @@ -11,6 +11,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{42F645A3 EndProject Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Generator.Tests", "tests\Generator.Tests\Generator.Tests.fsproj", "{4A234D75-019C-479E-8E35-17F85A776EF4}" EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Signal.CANdy.Core", "src\Signal.CANdy.Core\Signal.CANdy.Core.fsproj", "{5FA4B1F9-EC6B-47FF-84BC-44B9B2225731}" +EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Signal.CANdy", "src\Signal.CANdy\Signal.CANdy.fsproj", "{E7B40CFD-65C1-41BB-80DE-F77723D6BFCE}" +EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Signal.CANdy.CLI", "src\Signal.CANdy.CLI\Signal.CANdy.CLI.fsproj", "{2029AC91-E903-43ED-A2E1-53D261747CFA}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -28,9 +34,24 @@ Global {4A234D75-019C-479E-8E35-17F85A776EF4}.Debug|Any CPU.Build.0 = Debug|Any CPU {4A234D75-019C-479E-8E35-17F85A776EF4}.Release|Any CPU.ActiveCfg = Release|Any CPU {4A234D75-019C-479E-8E35-17F85A776EF4}.Release|Any CPU.Build.0 = Release|Any CPU + {5FA4B1F9-EC6B-47FF-84BC-44B9B2225731}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5FA4B1F9-EC6B-47FF-84BC-44B9B2225731}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5FA4B1F9-EC6B-47FF-84BC-44B9B2225731}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5FA4B1F9-EC6B-47FF-84BC-44B9B2225731}.Release|Any CPU.Build.0 = Release|Any CPU + {E7B40CFD-65C1-41BB-80DE-F77723D6BFCE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E7B40CFD-65C1-41BB-80DE-F77723D6BFCE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E7B40CFD-65C1-41BB-80DE-F77723D6BFCE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E7B40CFD-65C1-41BB-80DE-F77723D6BFCE}.Release|Any CPU.Build.0 = Release|Any CPU + {2029AC91-E903-43ED-A2E1-53D261747CFA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2029AC91-E903-43ED-A2E1-53D261747CFA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2029AC91-E903-43ED-A2E1-53D261747CFA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2029AC91-E903-43ED-A2E1-53D261747CFA}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {0F49A024-9812-48BA-8758-9560824B6BDD} = {3DE60BDA-DC11-4655-A9ED-5342DCD5577D} {4A234D75-019C-479E-8E35-17F85A776EF4} = {42F645A3-CE8D-435E-A671-749CEB739F43} + {5FA4B1F9-EC6B-47FF-84BC-44B9B2225731} = {3DE60BDA-DC11-4655-A9ED-5342DCD5577D} + {E7B40CFD-65C1-41BB-80DE-F77723D6BFCE} = {3DE60BDA-DC11-4655-A9ED-5342DCD5577D} + {2029AC91-E903-43ED-A2E1-53D261747CFA} = {3DE60BDA-DC11-4655-A9ED-5342DCD5577D} EndGlobalSection EndGlobal diff --git a/src/Signal.CANdy.CLI/Program.fs b/src/Signal.CANdy.CLI/Program.fs new file mode 100644 index 0000000..deb1c8f --- /dev/null +++ b/src/Signal.CANdy.CLI/Program.fs @@ -0,0 +1,2 @@ +// For more information see https://aka.ms/fsharp-console-apps +printfn "Hello from F#" diff --git a/src/Signal.CANdy.CLI/Signal.CANdy.CLI.fsproj b/src/Signal.CANdy.CLI/Signal.CANdy.CLI.fsproj new file mode 100644 index 0000000..48b98c0 --- /dev/null +++ b/src/Signal.CANdy.CLI/Signal.CANdy.CLI.fsproj @@ -0,0 +1,16 @@ + + + + Exe + net8.0 + + + + + + + + + + + diff --git a/src/Signal.CANdy.Core/Library.fs b/src/Signal.CANdy.Core/Library.fs new file mode 100644 index 0000000..53caaab --- /dev/null +++ b/src/Signal.CANdy.Core/Library.fs @@ -0,0 +1,5 @@ +namespace Signal.CANdy.Core + +module Say = + let hello name = + printfn "Hello %s" name diff --git a/src/Signal.CANdy.Core/Signal.CANdy.Core.fsproj b/src/Signal.CANdy.Core/Signal.CANdy.Core.fsproj new file mode 100644 index 0000000..d9b1a74 --- /dev/null +++ b/src/Signal.CANdy.Core/Signal.CANdy.Core.fsproj @@ -0,0 +1,12 @@ + + + + net8.0 + true + + + + + + + diff --git a/src/Signal.CANdy/Library.fs b/src/Signal.CANdy/Library.fs new file mode 100644 index 0000000..e4f9291 --- /dev/null +++ b/src/Signal.CANdy/Library.fs @@ -0,0 +1,5 @@ +namespace Signal.CANdy + +module Say = + let hello name = + printfn "Hello %s" name diff --git a/src/Signal.CANdy/Signal.CANdy.fsproj b/src/Signal.CANdy/Signal.CANdy.fsproj new file mode 100644 index 0000000..ba8f4cd --- /dev/null +++ b/src/Signal.CANdy/Signal.CANdy.fsproj @@ -0,0 +1,16 @@ + + + + net8.0 + true + + + + + + + + + + + From e294ebe903b2660ed6a1f3490d52f565b776e8e1 Mon Sep 17 00:00:00 2001 From: InitusNovus Date: Wed, 27 Aug 2025 09:14:49 +0900 Subject: [PATCH 02/15] chore: rename solution to Signal.CANdy.sln --- DBC_Parser.sln => Signal.CANdy.sln | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename DBC_Parser.sln => Signal.CANdy.sln (100%) diff --git a/DBC_Parser.sln b/Signal.CANdy.sln similarity index 100% rename from DBC_Parser.sln rename to Signal.CANdy.sln From 263e8768a4bf5cf41c21dbcb48aa31cc997392ed Mon Sep 17 00:00:00 2001 From: InitusNovus Date: Wed, 27 Aug 2025 09:16:08 +0900 Subject: [PATCH 03/15] feat(Core): add initial Api.fs stub (version) and include in project --- src/Signal.CANdy.Core/Api.fs | 4 ++++ src/Signal.CANdy.Core/Signal.CANdy.Core.fsproj | 1 + 2 files changed, 5 insertions(+) create mode 100644 src/Signal.CANdy.Core/Api.fs diff --git a/src/Signal.CANdy.Core/Api.fs b/src/Signal.CANdy.Core/Api.fs new file mode 100644 index 0000000..ca4109c --- /dev/null +++ b/src/Signal.CANdy.Core/Api.fs @@ -0,0 +1,4 @@ +module Signal.CANdy.Core.Api + +/// Returns the current library snapshot version. Placeholder until full API is moved. +let version () = "0.2.1-SNAPSHOT" diff --git a/src/Signal.CANdy.Core/Signal.CANdy.Core.fsproj b/src/Signal.CANdy.Core/Signal.CANdy.Core.fsproj index d9b1a74..d7f103e 100644 --- a/src/Signal.CANdy.Core/Signal.CANdy.Core.fsproj +++ b/src/Signal.CANdy.Core/Signal.CANdy.Core.fsproj @@ -7,6 +7,7 @@ + From 78563bdb310e13b15c703c59ff23ef6d99ac47c7 Mon Sep 17 00:00:00 2001 From: InitusNovus Date: Wed, 27 Aug 2025 09:23:22 +0900 Subject: [PATCH 04/15] feat(Core): expand Api.fs and project; add stubs (Ir.fs, Config.fs) groundwork --- src/Signal.CANdy.Core/Api.fs | 15 +++++++ src/Signal.CANdy.Core/Config.fs | 12 +++++ src/Signal.CANdy.Core/Ir.fs | 45 +++++++++++++++++++ .../Signal.CANdy.Core.fsproj | 4 +- 4 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 src/Signal.CANdy.Core/Config.fs create mode 100644 src/Signal.CANdy.Core/Ir.fs diff --git a/src/Signal.CANdy.Core/Api.fs b/src/Signal.CANdy.Core/Api.fs index ca4109c..2aae06a 100644 --- a/src/Signal.CANdy.Core/Api.fs +++ b/src/Signal.CANdy.Core/Api.fs @@ -1,4 +1,19 @@ module Signal.CANdy.Core.Api +open Signal.CANdy.Core.Ir +open Signal.CANdy.Core.Config + /// Returns the current library snapshot version. Placeholder until full API is moved. let version () = "0.2.1-SNAPSHOT" + +/// Parse a DBC file into IR. Stub for now. +let parseDbc (_path: string) : Result = + Error "NotImplemented: parseDbc" + +/// Generate code into outputPath using given config. Stub for now. +let generateCode (_ir: Ir) (_outputPath: string) (_config: Config) : Result = + Error "NotImplemented: generateCode" + +/// Validate configuration object. Stub for now. +let validateConfig (_config: Config) : Result = + Error "NotImplemented: validateConfig" diff --git a/src/Signal.CANdy.Core/Config.fs b/src/Signal.CANdy.Core/Config.fs new file mode 100644 index 0000000..d9643e5 --- /dev/null +++ b/src/Signal.CANdy.Core/Config.fs @@ -0,0 +1,12 @@ +namespace Signal.CANdy.Core + +module Config = + type Config = { + PhysType: string + PhysMode: string + RangeCheck: bool + Dispatch: string + CrcCounterCheck: bool + MotorolaStartBit: string + FilePrefix: string + } diff --git a/src/Signal.CANdy.Core/Ir.fs b/src/Signal.CANdy.Core/Ir.fs new file mode 100644 index 0000000..f3707ca --- /dev/null +++ b/src/Signal.CANdy.Core/Ir.fs @@ -0,0 +1,45 @@ +namespace Signal.CANdy.Core + +module Ir = + + type ByteOrder = + | Little + | Big + + type SignalType = + | Signed + | Unsigned + | Float + + type Signal = { + Name: string + StartBit: uint16 + Length: uint16 + Factor: float + Offset: float + Minimum: float option + Maximum: float option + Unit: string + IsSigned: bool + IsCrc: bool + IsCounter: bool + ByteOrder: ByteOrder + MultiplexerIndicator: string option + MultiplexerSwitchValue: int option + ValueTable: (int * string) list option + Receivers: string list + } + + type Message = { + Name: string + Id: uint32 + IsExtended: bool + Length: uint16 + Signals: Signal list + Sender: string + Receivers: string list + } + + type Ir = { + Messages: Message list + } diff --git a/src/Signal.CANdy.Core/Signal.CANdy.Core.fsproj b/src/Signal.CANdy.Core/Signal.CANdy.Core.fsproj index d7f103e..926a5e2 100644 --- a/src/Signal.CANdy.Core/Signal.CANdy.Core.fsproj +++ b/src/Signal.CANdy.Core/Signal.CANdy.Core.fsproj @@ -6,7 +6,9 @@ - + + + From e462f61bf1787cf763a717c8d9ab1dd4d202587d Mon Sep 17 00:00:00 2001 From: InitusNovus Date: Wed, 27 Aug 2025 09:27:12 +0900 Subject: [PATCH 05/15] feat(Core): introduce Errors DU and expand Api with typed stubs (parseDbc/validateConfig/generateCode + async + path orchestration) --- src/Signal.CANdy.Core/Api.fs | 29 ++++++++++++++----- src/Signal.CANdy.Core/Errors.fs | 24 +++++++++++++++ .../Signal.CANdy.Core.fsproj | 1 + 3 files changed, 46 insertions(+), 8 deletions(-) create mode 100644 src/Signal.CANdy.Core/Errors.fs diff --git a/src/Signal.CANdy.Core/Api.fs b/src/Signal.CANdy.Core/Api.fs index 2aae06a..bbea8a1 100644 --- a/src/Signal.CANdy.Core/Api.fs +++ b/src/Signal.CANdy.Core/Api.fs @@ -1,19 +1,32 @@ module Signal.CANdy.Core.Api +open System.Threading.Tasks open Signal.CANdy.Core.Ir open Signal.CANdy.Core.Config +open Signal.CANdy.Core.Errors /// Returns the current library snapshot version. Placeholder until full API is moved. let version () = "0.2.1-SNAPSHOT" /// Parse a DBC file into IR. Stub for now. -let parseDbc (_path: string) : Result = - Error "NotImplemented: parseDbc" - -/// Generate code into outputPath using given config. Stub for now. -let generateCode (_ir: Ir) (_outputPath: string) (_config: Config) : Result = - Error "NotImplemented: generateCode" +let parseDbc (_path: string) : Result = + Error (ParseError.Unknown "NotImplemented: parseDbc") /// Validate configuration object. Stub for now. -let validateConfig (_config: Config) : Result = - Error "NotImplemented: validateConfig" +let validateConfig (_config: Config) : Result = + Error (ValidationError.Unknown "NotImplemented: validateConfig") + +/// Generate code (sync) using IR and Config. +let generateCode (_ir: Ir) (_outputPath: string) (_config: Config) : Result = + Error (CodeGenError.Unknown "NotImplemented: generateCode") + +/// Generate code (async) using IR and Config. +let generateCodeAsync (ir: Ir) (outputPath: string) (config: Config) : Task> = task { + return generateCode ir outputPath config +} + +/// Convenience: parse dbc, load config path (optional), and generate. +let generateFromPaths (_dbcPath: string) (_outputPath: string) (_configPath: string option) : Task> = task { + // Placeholder orchestration to be implemented during migration + return Error (CodeGenError.Unknown "NotImplemented: generateFromPaths") +} diff --git a/src/Signal.CANdy.Core/Errors.fs b/src/Signal.CANdy.Core/Errors.fs new file mode 100644 index 0000000..6bb8171 --- /dev/null +++ b/src/Signal.CANdy.Core/Errors.fs @@ -0,0 +1,24 @@ +namespace Signal.CANdy.Core + +module Errors = + type ParseError = + | InvalidDbc of string + | IoError of string + | Unknown of string + + type CodeGenError = + | TemplateError of string + | IoError of string + | Unknown of string + + type ValidationError = + | InvalidValue of string + | MissingField of string + | IoError of string + | Unknown of string + + type GeneratedFiles = { + Sources: string list + Headers: string list + Others: string list + } diff --git a/src/Signal.CANdy.Core/Signal.CANdy.Core.fsproj b/src/Signal.CANdy.Core/Signal.CANdy.Core.fsproj index 926a5e2..a80d1c1 100644 --- a/src/Signal.CANdy.Core/Signal.CANdy.Core.fsproj +++ b/src/Signal.CANdy.Core/Signal.CANdy.Core.fsproj @@ -9,6 +9,7 @@ + From 184a59642eed513dda37a717fd5116371daee568 Mon Sep 17 00:00:00 2001 From: InitusNovus Date: Wed, 27 Aug 2025 09:34:11 +0900 Subject: [PATCH 06/15] feat(Core): add Dbc parser port (parseDbcFile) and wire into Api; add DbcParserLib dependency; fix regex quoting --- src/Signal.CANdy.Core/Api.fs | 5 +- src/Signal.CANdy.Core/Config.fs | 89 ++++++ src/Signal.CANdy.Core/Dbc.fs | 280 ++++++++++++++++++ .../Signal.CANdy.Core.fsproj | 8 +- 4 files changed, 379 insertions(+), 3 deletions(-) create mode 100644 src/Signal.CANdy.Core/Dbc.fs diff --git a/src/Signal.CANdy.Core/Api.fs b/src/Signal.CANdy.Core/Api.fs index bbea8a1..977f887 100644 --- a/src/Signal.CANdy.Core/Api.fs +++ b/src/Signal.CANdy.Core/Api.fs @@ -4,13 +4,14 @@ open System.Threading.Tasks open Signal.CANdy.Core.Ir open Signal.CANdy.Core.Config open Signal.CANdy.Core.Errors +open Signal.CANdy.Core.Dbc /// Returns the current library snapshot version. Placeholder until full API is moved. let version () = "0.2.1-SNAPSHOT" /// Parse a DBC file into IR. Stub for now. -let parseDbc (_path: string) : Result = - Error (ParseError.Unknown "NotImplemented: parseDbc") +let parseDbc (path: string) : Result = + Signal.CANdy.Core.Dbc.parseDbcFile path /// Validate configuration object. Stub for now. let validateConfig (_config: Config) : Result = diff --git a/src/Signal.CANdy.Core/Config.fs b/src/Signal.CANdy.Core/Config.fs index d9643e5..09e9b8d 100644 --- a/src/Signal.CANdy.Core/Config.fs +++ b/src/Signal.CANdy.Core/Config.fs @@ -1,5 +1,11 @@ namespace Signal.CANdy.Core +open System +open System.Collections.Generic +open System.Text.RegularExpressions +open YamlDotNet.Serialization +open Signal.CANdy.Core.Errors + module Config = type Config = { PhysType: string @@ -10,3 +16,86 @@ module Config = MotorolaStartBit: string FilePrefix: string } + + // --- Validation helpers --- + let private validPhysTypes = [ "float"; "fixed" ] + let private validPhysModes = [ "double"; "float"; "fixed_double"; "fixed_float" ] + let private validDispatch = [ "binary_search"; "direct_map" ] + let private validMoto = [ "msb"; "lsb" ] + let private prefixRegex = Regex(@"^[a-zA-Z_][a-zA-Z0-9_]*$") + + let validate (cfg: Config) : Result = + if not (List.contains (cfg.PhysType.ToLowerInvariant()) validPhysTypes) then + Error (ValidationError.InvalidValue (sprintf "Invalid phys_type '%s'" cfg.PhysType)) + elif not (List.contains (cfg.PhysMode.ToLowerInvariant()) validPhysModes) then + Error (ValidationError.InvalidValue (sprintf "Invalid phys_mode '%s'" cfg.PhysMode)) + elif not (List.contains (cfg.Dispatch.ToLowerInvariant()) validDispatch) then + Error (ValidationError.InvalidValue (sprintf "Invalid dispatch '%s'" cfg.Dispatch)) + elif not (List.contains (cfg.MotorolaStartBit.ToLowerInvariant()) validMoto) then + Error (ValidationError.InvalidValue (sprintf "Invalid motorola_start_bit '%s'" cfg.MotorolaStartBit)) + elif not (prefixRegex.IsMatch cfg.FilePrefix) then + Error (ValidationError.InvalidValue (sprintf "Invalid file_prefix '%s'" cfg.FilePrefix)) + else + Ok cfg + + // --- YAML loading helpers --- + let private tryGetString (map: IDictionary) (keys: string list) : string option = + keys + |> List.tryPick (fun k -> + match map.TryGetValue(k) with + | true, v when not (isNull v) -> + match v with + | :? string as s -> Some s + | _ -> Some (string v) + | _ -> None) + + let private tryGetBool (map: IDictionary) (keys: string list) : bool option = + keys + |> List.tryPick (fun k -> + match map.TryGetValue(k) with + | true, v when not (isNull v) -> + match v with + | :? bool as b -> Some b + | :? string as s -> + match Boolean.TryParse(s) with | true, b -> Some b | _ -> None + | _ -> None + | _ -> None) + + /// Load a YAML config file and return a validated Config + let loadFromYaml (configPath: string) = + try + use reader = new System.IO.StreamReader(configPath) + let yaml = reader.ReadToEnd() + let deserializer = DeserializerBuilder().Build() + let map = deserializer.Deserialize>(yaml) + + let phys = tryGetString map [ "phys_type"; "PhysType" ] |> Option.defaultValue "float" + let physModeRaw = tryGetString map [ "phys_mode"; "PhysMode" ] + let physMode = + match physModeRaw with + | Some m -> m + | None -> + match phys.ToLowerInvariant() with + | "float" -> "double" + | "fixed" -> "fixed_double" + | _ -> "double" + + let range = tryGetBool map [ "range_check"; "RangeCheck" ] |> Option.defaultValue false + let disp = tryGetString map [ "dispatch"; "Dispatch" ] |> Option.defaultValue "binary_search" + let crc = tryGetBool map [ "crc_counter_check"; "CrcCounterCheck" ] |> Option.defaultValue false + let moto = tryGetString map [ "motorola_start_bit"; "MotorolaStartBit" ] |> Option.defaultValue "msb" + let filePrefix = tryGetString map [ "file_prefix"; "FilePrefix" ] |> Option.defaultValue "sc_" + + let cfg = { + PhysType = phys + PhysMode = physMode + RangeCheck = range + Dispatch = disp + CrcCounterCheck = crc + MotorolaStartBit = moto + FilePrefix = filePrefix + } + + validate cfg + with ex -> + Error (ValidationError.IoError ex.Message) diff --git a/src/Signal.CANdy.Core/Dbc.fs b/src/Signal.CANdy.Core/Dbc.fs new file mode 100644 index 0000000..15635f7 --- /dev/null +++ b/src/Signal.CANdy.Core/Dbc.fs @@ -0,0 +1,280 @@ +namespace Signal.CANdy.Core + +open System +open System.IO +open System.Text.RegularExpressions + +open Signal.CANdy.Core.Ir +open Signal.CANdy.Core.Errors + +module Dbc = + + let private isVectorInternalMessageName (name: string) = + name = "VECTOR__INDEPENDENT_SIG_MSG" + + // Compute covered bit positions (0..(DLC*8-1)) for a signal, respecting byte order. + // For BE (Motorola), use sawtooth numbering with MSB-based start bit. + let private coveredBits (s: Signal) : int list = + let start = int s.StartBit + let len = int s.Length + match s.ByteOrder with + | ByteOrder.Little -> [ for i in 0 .. len - 1 -> start + i ] + | ByteOrder.Big -> + let byte0 = start / 8 + let bit0 = start % 8 // 7..0 + [ for i in 0 .. len - 1 -> + let mutable curByte = byte0 + let mutable curBit = bit0 - i + while curBit < 0 do curBit <- curBit + 8; curByte <- curByte + 1 + curByte * 8 + curBit ] + + let private validateDuplicates (messages: Message list) : string option = + messages + |> List.groupBy (fun m -> m.Id) + |> List.tryPick (fun (id, ms) -> if List.length ms > 1 then Some (sprintf "Duplicate message ID %u found." id) else None) + + // Determine whether two signals can coexist in the same frame instance. + let private canCoexist (a: Signal) (b: Signal) : bool = + let aMuxI, aMuxV = a.MultiplexerIndicator, a.MultiplexerSwitchValue + let bMuxI, bMuxV = b.MultiplexerIndicator, b.MultiplexerSwitchValue + match aMuxI, aMuxV, bMuxI, bMuxV with + | Some indA, Some va, Some indB, Some vb when indA = "m" && indB = "m" && va <> vb -> false + | _ -> true + + let private validateOverlaps (messages: Message list) : string option = + let overlapsInMessage (m: Message) : string option = + let rec checkPairs (signals: Signal list) : string option = + match signals with + | [] | [_] -> None + | s::rest -> + let sBits = coveredBits s |> Set.ofList + let conflict = + rest + |> List.tryPick (fun t -> + if canCoexist s t then + let tBits = coveredBits t |> Set.ofList + let inter = Set.intersect sBits tBits + if not (Set.isEmpty inter) then Some (sprintf "Signal '%s' in message '%s' overlaps with other signals." t.Name m.Name) else None + else None) + match conflict with + | Some e -> Some e + | None -> checkPairs rest + checkPairs m.Signals + messages |> List.tryPick overlapsInMessage + + let private validateExceedsDlc (messages: Message list) : string option = + let exceedInMessage (m: Message) : string option = + let totalBits = int m.Length * 8 + m.Signals + |> List.tryPick (fun s -> + let bits = coveredBits s + if bits |> List.exists (fun b -> b < 0 || b >= totalBits) then + Some (sprintf "Signal '%s' in message '%s' exceeds the message DLC of %d bytes." s.Name m.Name (int m.Length)) + else None) + messages |> List.tryPick exceedInMessage + + let private validateDuplicateIdsFromText (filePath: string) : string option = + try + let lines = File.ReadAllLines(filePath) + let ids = + lines + |> Seq.choose (fun line -> + let t = line.Trim() + if t.StartsWith("BO_ ") then + let parts = t.Split([|' '; ':'|], StringSplitOptions.RemoveEmptyEntries) + if parts.Length >= 3 then + let name = parts.[2] + if isVectorInternalMessageName name then None else + match Int32.TryParse(parts.[1]) with + | true, id -> Some id + | _ -> None + else None + else None) + |> Seq.toList + ids + |> List.groupBy id + |> List.tryPick (fun (id, xs) -> if List.length xs > 1 then Some (sprintf "Duplicate message ID %d found." id) else None) + with _ -> None + + let private tryBuildSignalMuxMap (filePath: string) : Map = + let mutable currentMsg : string option = None + let mutable entries : (string*string*(string option * int option)) list = [] + try + for raw in File.ReadLines(filePath) do + let line = raw.Trim() + if line.StartsWith("BO_ ") then + let parts = line.Split([|' '; ':'|], StringSplitOptions.RemoveEmptyEntries) + if parts.Length >= 3 then currentMsg <- Some parts.[2] + elif line.StartsWith("SG_") then + match currentMsg with + | None -> () + | Some msgName -> + let colonIdx = line.IndexOf(':') + if colonIdx > 0 then + let left = line.Substring(0, colonIdx) + let parts = left.Split([|' '|], StringSplitOptions.RemoveEmptyEntries) + if parts.Length >= 2 then + let sigName = parts.[1] + let tokens = parts |> Array.skip 2 + let mutable muxInd : string option = None + let mutable muxVal : int option = None + for t in tokens do + if t = "M" then muxInd <- Some "M" + elif t.Length >= 1 && t.[0] = 'm' then + muxInd <- Some "m" + if t.Length > 1 then + let vStr = t.Substring(1) + match Int32.TryParse(vStr) with + | true, v -> muxVal <- Some v + | _ -> () + if muxInd.IsSome || muxVal.IsSome then + entries <- (msgName, sigName, (muxInd, muxVal)) :: entries + entries |> List.fold (fun acc (m,s,meta) -> acc |> Map.add (m,s) meta) Map.empty + with _ -> Map.empty + + let private tryBuildSignalMetaMap (filePath: string) : Map = + let mutable currentMsg : string option = None + let mutable entries : (string*string*(bool*ByteOrder)) list = [] + try + for raw in File.ReadLines(filePath) do + let line = raw.Trim() + if line.StartsWith("BO_ ") then + let parts = line.Split([|' '; ':'|], StringSplitOptions.RemoveEmptyEntries) + if parts.Length >= 3 then currentMsg <- Some parts.[2] + elif line.StartsWith("SG_") then + match currentMsg with + | None -> () + | Some msgName -> + let parts = line.Split([|' '|], StringSplitOptions.RemoveEmptyEntries) + if parts.Length >= 2 then + let sigName = parts.[1] + let colonIdx = line.IndexOf(':') + if colonIdx > 0 && colonIdx < line.Length - 1 then + let after = line.Substring(colonIdx + 1).Trim() + let atIdx = after.IndexOf('@') + if atIdx >= 0 && atIdx + 2 < after.Length then + let endianCh = after.[atIdx + 1] + let signCh = after.[atIdx + 2] + if (signCh = '+' || signCh = '-') && (endianCh = '0' || endianCh = '1') then + let isSigned = signCh = '-' + let order = if endianCh = '0' then ByteOrder.Big else ByteOrder.Little + entries <- (msgName, sigName, (isSigned, order)) :: entries + entries |> List.fold (fun acc (m,s,meta) -> acc |> Map.add (m,s) meta) Map.empty + with _ -> Map.empty + + let private buildIdNameMap (filePath: string) : Map = + let mutable m : Map = Map.empty + try + for raw in File.ReadLines(filePath) do + let line = raw.Trim() + if line.StartsWith("BO_ ") then + let parts = line.Split([|' '; ':'|], StringSplitOptions.RemoveEmptyEntries) + if parts.Length >= 3 then + match Int32.TryParse(parts.[1]) with + | true, id -> + let name = parts.[2] + if not (isVectorInternalMessageName name) then + m <- m |> Map.add id name + | _ -> () + m + with _ -> Map.empty + + let private tryBuildValueTableMap (filePath: string) : Map = + try + let idName = buildIdNameMap filePath + let mutable map : Map = Map.empty + let rx = Regex(@"^VAL_\s+(\d+)\s+(\S+)\s+(.*);\s*$") + let rxPair = Regex(@"([+-]?\d+)\s+""([^""]*)""") + for raw in File.ReadLines(filePath) do + let line = raw.Trim() + let m = rx.Match(line) + if m.Success then + let idStr = m.Groups.[1].Value + let sigName = m.Groups.[2].Value + let pairsStr = m.Groups.[3].Value + match Int32.TryParse(idStr) with + | true, id when idName.ContainsKey id -> + let msgName = idName.[id] + let pairs = + rxPair.Matches(pairsStr) + |> Seq.cast + |> Seq.choose (fun mm -> + match Int32.TryParse(mm.Groups.[1].Value) with + | true, v -> Some (v, mm.Groups.[2].Value) + | _ -> None) + |> Seq.toList + if pairs.Length > 0 then + map <- map |> Map.add (msgName, sigName) pairs + | _ -> () + map + with _ -> Map.empty + + /// Parse DBC file into Core IR with validation + let parseDbcFile (filePath: string) : Result = + match validateDuplicateIdsFromText filePath with + | Some err -> Error (ParseError.InvalidDbc err) + | None -> + try + let metaMap = tryBuildSignalMetaMap filePath + let muxMap = tryBuildSignalMuxMap filePath + let valMap = tryBuildValueTableMap filePath + let dbc = DbcParserLib.Parser.ParseFromPath(filePath) + + let messages = + dbc.Messages + |> Seq.filter (fun msg -> not (isVectorInternalMessageName msg.Name)) + |> Seq.map (fun msg -> + let signals = + msg.Signals + |> Seq.map (fun s -> + let minVal = if Double.IsNaN s.Minimum then None else Some s.Minimum + let maxVal = if Double.IsNaN s.Maximum then None else Some s.Maximum + let inferredSigned, inferredOrder = + match metaMap |> Map.tryFind (msg.Name, s.Name) with + | Some (isS, ord) -> isS, ord + | None -> (s.Minimum < 0.0), ByteOrder.Little + let muxInd, muxVal = + match muxMap |> Map.tryFind (msg.Name, s.Name) with + | Some (i, v) -> i, v + | None -> None, None + { + Name = s.Name + StartBit = s.StartBit + Length = s.Length + Factor = s.Factor + Offset = s.Offset + Minimum = minVal + Maximum = maxVal + Unit = s.Unit + IsSigned = inferredSigned + IsCrc = s.Name.ToLowerInvariant().Contains("crc") || s.Name.ToLowerInvariant().Contains("checksum") + IsCounter = s.Name.ToLowerInvariant().Contains("counter") || s.Name.ToLowerInvariant().Contains("alive") + ByteOrder = inferredOrder + MultiplexerIndicator = muxInd + MultiplexerSwitchValue = muxVal + ValueTable = (valMap |> Map.tryFind (msg.Name, s.Name)) + Receivers = [] + } + ) + |> List.ofSeq + + { + Name = msg.Name + Id = msg.ID + IsExtended = (msg.ID > 0x7FFu) + Length = msg.DLC + Signals = signals + Sender = msg.Transmitter + Receivers = [] + } + ) + |> List.ofSeq + + let combineValidators validators = + validators |> List.tryPick id + + match combineValidators [ validateDuplicates messages; validateOverlaps messages; validateExceedsDlc messages ] with + | Some err -> Error (ParseError.InvalidDbc err) + | None -> Ok { Messages = messages } + with ex -> + Error (ParseError.IoError ex.Message) diff --git a/src/Signal.CANdy.Core/Signal.CANdy.Core.fsproj b/src/Signal.CANdy.Core/Signal.CANdy.Core.fsproj index a80d1c1..b59efbe 100644 --- a/src/Signal.CANdy.Core/Signal.CANdy.Core.fsproj +++ b/src/Signal.CANdy.Core/Signal.CANdy.Core.fsproj @@ -7,10 +7,16 @@ + + - + + + + + From d49d4c88ef37bc53a31d6a1d9a1e5b250fc31d5d Mon Sep 17 00:00:00 2001 From: InitusNovus Date: Wed, 27 Aug 2025 09:47:50 +0900 Subject: [PATCH 07/15] feat(Core): implement code generation in Core (utils/message/registry) and wire Api.generateCode; add Codegen.fs to project --- src/Signal.CANdy.Core/Api.fs | 40 +- src/Signal.CANdy.Core/Codegen.fs | 437 ++++++++++++++++++ .../Signal.CANdy.Core.fsproj | 1 + 3 files changed, 471 insertions(+), 7 deletions(-) create mode 100644 src/Signal.CANdy.Core/Codegen.fs diff --git a/src/Signal.CANdy.Core/Api.fs b/src/Signal.CANdy.Core/Api.fs index 977f887..ca8ccc7 100644 --- a/src/Signal.CANdy.Core/Api.fs +++ b/src/Signal.CANdy.Core/Api.fs @@ -5,6 +5,7 @@ open Signal.CANdy.Core.Ir open Signal.CANdy.Core.Config open Signal.CANdy.Core.Errors open Signal.CANdy.Core.Dbc +open Signal.CANdy.Core.Codegen /// Returns the current library snapshot version. Placeholder until full API is moved. let version () = "0.2.1-SNAPSHOT" @@ -14,12 +15,12 @@ let parseDbc (path: string) : Result = Signal.CANdy.Core.Dbc.parseDbcFile path /// Validate configuration object. Stub for now. -let validateConfig (_config: Config) : Result = - Error (ValidationError.Unknown "NotImplemented: validateConfig") +let validateConfig (config: Config) : Result = + Signal.CANdy.Core.Config.validate config /// Generate code (sync) using IR and Config. -let generateCode (_ir: Ir) (_outputPath: string) (_config: Config) : Result = - Error (CodeGenError.Unknown "NotImplemented: generateCode") +let generateCode (ir: Ir) (outputPath: string) (config: Config) : Result = + Signal.CANdy.Core.Codegen.generate ir outputPath config /// Generate code (async) using IR and Config. let generateCodeAsync (ir: Ir) (outputPath: string) (config: Config) : Task> = task { @@ -27,7 +28,32 @@ let generateCodeAsync (ir: Ir) (outputPath: string) (config: Config) : Task> = task { - // Placeholder orchestration to be implemented during migration - return Error (CodeGenError.Unknown "NotImplemented: generateFromPaths") +let generateFromPaths (dbcPath: string) (outputPath: string) (configPath: string option) : Task> = task { + // Load config (optional path -> YAML; otherwise sensible defaults) + let configResult : Result = + match configPath with + | Some p -> + match Signal.CANdy.Core.Config.loadFromYaml p with + | Ok cfg -> Ok cfg + | Error ve -> Error (CodeGenError.Unknown (sprintf "Config error: %A" ve)) + | None -> + Ok { + PhysType = "float" + PhysMode = "double" + RangeCheck = false + Dispatch = "binary_search" + CrcCounterCheck = false + MotorolaStartBit = "msb" + FilePrefix = "sc_" + } + + match configResult with + | Error e -> return Error e + | Ok cfg -> + // Parse DBC + match Signal.CANdy.Core.Dbc.parseDbcFile dbcPath with + | Error pe -> return Error (CodeGenError.Unknown (sprintf "Parse error: %A" pe)) + | Ok ir -> + // Delegate to codegen (currently stubbed) + return generateCode ir outputPath cfg } diff --git a/src/Signal.CANdy.Core/Codegen.fs b/src/Signal.CANdy.Core/Codegen.fs new file mode 100644 index 0000000..d85751e --- /dev/null +++ b/src/Signal.CANdy.Core/Codegen.fs @@ -0,0 +1,437 @@ +namespace Signal.CANdy.Core + +open System +open System.IO +open Signal.CANdy.Core.Ir +open Signal.CANdy.Core.Errors + +module Codegen = + + module Utils = + // Build a macro-safe header guard from prefix + base name + let private guard (prefix: string) (baseName: string) = + let raw = (prefix + baseName).ToUpperInvariant() + raw + |> Seq.map (fun ch -> if Char.IsLetterOrDigit ch then ch else '_') + |> Seq.toArray + |> fun arr -> new string(arr) + + let utilsHeaderName (config: Signal.CANdy.Core.Config.Config) = sprintf "%sutils.h" config.FilePrefix + let utilsSourceName (config: Signal.CANdy.Core.Config.Config) = sprintf "%sutils.c" config.FilePrefix + + let utilsHContent (config: Signal.CANdy.Core.Config.Config) = + let banner = sprintf "/* Generated by Signal CANdy\n file_prefix=%s, phys_type=%s, phys_mode=%s, dispatch=%s, motorola_start_bit=%s */\n" config.FilePrefix config.PhysType config.PhysMode config.Dispatch config.MotorolaStartBit + let g = guard config.FilePrefix "utils_h" + banner + (sprintf "#ifndef %s\n#define %s\n\n#include \n#include \n\n#ifdef __cplusplus\nextern \"C\" {\n#endif\n\n// Little-endian bit extraction functions\nuint64_t get_bits_le(const uint8_t* data, uint16_t start_bit, uint16_t length);\n\n// Little-endian bit insertion functions\nvoid set_bits_le(uint8_t* data, uint16_t start_bit, uint16_t length, uint64_t value);\n\n// Big-endian (Motorola) bit extraction\nuint64_t get_bits_be(const uint8_t* data, uint16_t start_bit, uint16_t length);\n\n// Big-endian (Motorola) bit insertion\nvoid set_bits_be(uint8_t* data, uint16_t start_bit, uint16_t length, uint64_t value);\n\n#ifdef __cplusplus\n}\n#endif\n\n#endif // %s" g g g) + + let utilsCContent (config: Signal.CANdy.Core.Config.Config) = + let uH = utilsHeaderName config + let banner = sprintf "/* Generated by Signal CANdy\n file_prefix=%s, phys_type=%s, phys_mode=%s, dispatch=%s, motorola_start_bit=%s */\n" config.FilePrefix config.PhysType config.PhysMode config.Dispatch config.MotorolaStartBit + banner + "#include \"" + uH + "\"\n\n// Little-endian bit extraction\nuint64_t get_bits_le(const uint8_t* data, uint16_t start_bit, uint16_t length) {\n uint64_t value = 0;\n uint16_t byte_offset = start_bit / 8;\n uint16_t bit_offset = start_bit % 8;\n for (uint16_t i = 0; i < 8 && (byte_offset + i) < 8; ++i) {\n value |= (uint64_t)data[byte_offset + i] << (i * 8);\n }\n value >>= bit_offset;\n value &= (1ULL << length) - 1;\n return value;\n}\n\n// Little-endian bit insertion\nvoid set_bits_le(uint8_t* data, uint16_t start_bit, uint16_t length, uint64_t value) {\n uint16_t byte_offset = start_bit / 8;\n uint16_t bit_offset = start_bit % 8;\n uint64_t clear_mask = ((1ULL << length) - 1) << bit_offset;\n for (uint16_t i = 0; i < 8 && (byte_offset + i) < 8; ++i) {\n data[byte_offset + i] &= ~(uint8_t)(clear_mask >> (i * 8));\n }\n uint64_t insert_value = (value & ((1ULL << length) - 1)) << bit_offset;\n for (uint16_t i = 0; i < 8 && (byte_offset + i) < 8; ++i) {\n data[byte_offset + i] |= (uint8_t)(insert_value >> (i * 8));\n }\n}\n\n// Big-endian (Motorola) bit extraction (DBC semantics, sawtooth)\nuint64_t get_bits_be(const uint8_t* data, uint16_t start_bit, uint16_t length) {\n uint64_t value = 0;\n int byte = start_bit / 8;\n int bit = start_bit % 8; // 7..0 within byte, 7 is MSB\n for (uint16_t i = 0; i < length; ++i) {\n int curByte = byte;\n int curBit = bit - (int)i;\n while (curBit < 0) { curBit += 8; ++curByte; } // move to next higher byte\n uint8_t b = data[curByte];\n uint8_t bitval = (uint8_t)((b >> curBit) & 1u);\n value = (value << 1) | bitval; // assemble MSB-first\n }\n return value;\n}\n\n// Big-endian (Motorola) bit insertion (DBC semantics, sawtooth)\nvoid set_bits_be(uint8_t* data, uint16_t start_bit, uint16_t length, uint64_t value) {\n int byte = start_bit / 8;\n int bit = start_bit % 8;\n for (uint16_t i = 0; i < length; ++i) {\n int curByte = byte;\n int curBit = bit - (int)i;\n while (curBit < 0) { curBit += 8; ++curByte; } // move to next higher byte\n uint8_t bitval = (uint8_t)((value >> (length - 1 - i)) & 1u); // MSB-first\n data[curByte] = (uint8_t)((data[curByte] & (uint8_t)~(1u << curBit)) | (uint8_t)(bitval << curBit));\n }\n}" + + // Helper to choose C accessor based on byte order + let accessorNames (byteOrder: ByteOrder) = + match byteOrder with + | ByteOrder.Little -> ("get_bits_le", "set_bits_le") + | ByteOrder.Big -> ("get_bits_be", "set_bits_be") + + // Convert Motorola (BE) start bit from LSB-convention to MSB-convention using sawtooth numbering. + let internal motorolaMsbFromLsb (start: int) (length: int) : int = + let steps = max 0 (length - 1) + let mutable byteIdx = start / 8 + let mutable bitIdx = start % 8 // 0..7, where 7 is MSB + for _ in 1 .. steps do + if bitIdx < 7 then bitIdx <- bitIdx + 1 + else (byteIdx <- byteIdx + 1; bitIdx <- 7) + byteIdx * 8 + bitIdx + + // Choose effective start bit depending on config for Motorola signals; LE stays unchanged. + let chooseStartBit (signal: Signal) (config: Signal.CANdy.Core.Config.Config) : int = + let start = int signal.StartBit + let len = int signal.Length + match signal.ByteOrder with + | ByteOrder.Big -> + match config.MotorolaStartBit.ToLowerInvariant() with + | "lsb" -> motorolaMsbFromLsb start len + | _ -> start // default "msb" + | _ -> start + + // Detect if factor equals 10^-n within tolerance and return integer scale (10^n) + let tryPowerOfTenScale (factor: float) : int64 option = + if factor <= 0.0 then None else + let eps = 1e-12 + let rec loop n = + if n > 9 then None + else + let scaleF = pown 10.0 n + if abs (factor - (1.0 / scaleF)) < eps then Some (int64 (pown 10 n)) else loop (n + 1) + loop 0 + + module Message = + open Utils + + // Sanitize a string to an uppercase C identifier (A-Z0-9_), prefix with N_ if starting with a digit or empty + let private sanitizeEnumIdent (s: string) : string = + let up = s.ToUpperInvariant() + let mapped = + up + |> Seq.map (fun ch -> if Char.IsLetterOrDigit ch then ch else '_') + |> Seq.toArray + |> fun arr -> new string(arr) + let trimmed = + mapped.Trim([|'_'|]) + |> fun t -> if String.IsNullOrWhiteSpace t then "N" else t + let start = trimmed.[0] + if Char.IsDigit start then "N_" + trimmed else trimmed + + let private fieldDecl (s: Signal) = sprintf " float %s;" s.Name + + let private genDecodeForSignal (s: Signal) (doRangeCheck: bool) (config: Signal.CANdy.Core.Config.Config) = + let len = int s.Length + let startEff = chooseStartBit s config + let (getFn, _) = accessorNames s.ByteOrder + let raw = sprintf "raw_%s" s.Name + let signFix = if s.IsSigned then sprintf " if (%s & (1ULL << (%d - 1))) { %s |= ~((1ULL << %d) - 1); }" raw len raw len else "" + let physAssignFloatDouble = sprintf " msg->%s = (float)((double)%s * %.17g + %.17g);" s.Name raw s.Factor s.Offset + let physAssignFloatFloat = sprintf " msg->%s = (float)(((float)%s * (float)%.17g) + (float)%.17g);" s.Name raw s.Factor s.Offset + let physAssign = + match config.PhysType.ToLowerInvariant() with + | "fixed" -> + match Utils.tryPowerOfTenScale s.Factor with + | Some scale when abs (s.Offset - Math.Round(s.Offset)) < 1e-12 -> + sprintf " msg->%s = (float)(((double)%s + (%.0f)) / (double)%d);" s.Name raw (Math.Round(s.Offset * (float scale))) scale + | _ -> (match config.PhysMode.ToLowerInvariant() with | "fixed_float" -> physAssignFloatFloat | _ -> physAssignFloatDouble) + | _ -> (match config.PhysMode.ToLowerInvariant() with | "float" -> physAssignFloatFloat | _ -> physAssignFloatDouble) + let rangeCheck = + if doRangeCheck then + match s.Minimum, s.Maximum with + | Some minV, Some maxV -> Some (sprintf " if (msg->%s < %.17g || msg->%s > %.17g) { return false; }" s.Name minV s.Name maxV) + | Some minV, None -> Some (sprintf " if (msg->%s < %.17g) { return false; }" s.Name minV) + | None, Some maxV -> Some (sprintf " if (msg->%s > %.17g) { return false; }" s.Name maxV) + | _ -> None + else None + [ sprintf " uint64_t %s = 0;" raw + sprintf " // %s: start=%d len=%d factor=%.17g offset=%.17g" s.Name startEff len s.Factor s.Offset + sprintf " %s = %s(data, %d, %d);" raw getFn startEff len + if signFix <> "" then signFix else null + physAssign + match rangeCheck with | Some r -> r | None -> null ] + |> List.choose (fun x -> if isNull (box x) then None else Some x) + |> String.concat "\n" + + let private genEncodeForSignal (s: Signal) (doRangeCheck: bool) (config: Signal.CANdy.Core.Config.Config) = + let len = int s.Length + let startEff = chooseStartBit s config + let (_, setFn) = accessorNames s.ByteOrder + let rangeChecks = + if doRangeCheck then + match s.Minimum, s.Maximum with + | Some minV, Some maxV -> Some (sprintf " if (msg->%s < %.17g || msg->%s > %.17g) { return false; }" s.Name minV s.Name maxV) + | Some minV, None -> Some (sprintf " if (msg->%s < %.17g) { return false; }" s.Name minV) + | None, Some maxV -> Some (sprintf " if (msg->%s > %.17g) { return false; }" s.Name maxV) + | _ -> None + else None + let computeRawDouble = + sprintf " double tmp_%s = ((double)msg->%s - %.17g) / %.17g;\n int64_t raw_%s = (int64_t)(tmp_%s >= 0 ? tmp_%s + 0.5 : tmp_%s - 0.5);" s.Name s.Name s.Offset s.Factor s.Name s.Name s.Name s.Name + let computeRawFloat = + sprintf " float tmp_%s = ((float)msg->%s - (float)%.17g) / (float)%.17g;\n int64_t raw_%s = (int64_t)llroundf(tmp_%s);" s.Name s.Name s.Offset s.Factor s.Name s.Name + let computeRaw = + match config.PhysType.ToLowerInvariant() with + | "fixed" -> + match Utils.tryPowerOfTenScale s.Factor with + | Some scale when abs (s.Offset - Math.Round(s.Offset)) < 1e-12 -> + sprintf " int64_t raw_%s = (int64_t)llround(((double)msg->%s - %.0f) * (double)%d);" s.Name s.Name (Math.Round s.Offset) scale + | _ -> (match config.PhysMode.ToLowerInvariant() with | "fixed_float" -> computeRawFloat | _ -> computeRawDouble) + | _ -> (match config.PhysMode.ToLowerInvariant() with | "float" -> computeRawFloat | _ -> computeRawDouble) + let setBits = sprintf " %s(data, %d, %d, (uint64_t)raw_%s);" setFn startEff len s.Name + [ match rangeChecks with | Some r -> yield r | None -> () + yield computeRaw + yield setBits ] + |> String.concat "\n" + + let private partitionMultiplex (message: Message) = + let switchOpt = message.Signals |> List.tryFind (fun s -> s.MultiplexerIndicator = Some "M") + let baseSignals = message.Signals |> List.filter (fun s -> s.MultiplexerIndicator.IsNone) + let branches = + message.Signals + |> List.choose (fun s -> + match s.MultiplexerIndicator, s.MultiplexerSwitchValue with + | Some ind, Some v when ind = "m" -> Some (v, s) + | _ -> None) + |> List.groupBy fst + |> List.map (fun (k, xs) -> k, xs |> List.map snd) + switchOpt, baseSignals, branches + + let generateMessageFiles (message: Message) (outputPath: string) (config: Signal.CANdy.Core.Config.Config) = + let messageNameLower = message.Name.ToLowerInvariant() + let messageHPath = Path.Combine(outputPath, "include", sprintf "%s.h" messageNameLower) + let messageCPath = Path.Combine(outputPath, "src", sprintf "%s.c" messageNameLower) + + let banner = sprintf "/* Generated by Signal CANdy\n file_prefix=%s, phys_type=%s, phys_mode=%s, dispatch=%s, motorola_start_bit=%s */\n" config.FilePrefix config.PhysType config.PhysMode config.Dispatch config.MotorolaStartBit + + let signalDeclarationsH = message.Signals |> List.map fieldDecl |> String.concat "\n" + + let switchOpt, baseSignals, branches = partitionMultiplex message + let isMux = match switchOpt, branches with | Some _, _ :: _ -> true | _ -> false + let validMacro (sigName: string) = sprintf "%s_VALID_%s" (message.Name.ToUpperInvariant()) (sigName.ToUpperInvariant()) + + let signalDecodeFor s = genDecodeForSignal s config.RangeCheck config + let signalDecodeWithValid s = let body = signalDecodeFor s in if isMux then body + (sprintf "\n msg->valid |= %s;" (validMacro s.Name)) else body + + let signalDecodeC = + match switchOpt, branches with + | Some sw, (_ :: _) -> + let rawVar = sprintf "raw_%s" sw.Name + let swBlock = + let body = signalDecodeWithValid sw + body + (sprintf "\n msg->mux_active = (%s_mux_e)((int)%s);" message.Name rawVar) + let baseBlock = baseSignals |> List.map signalDecodeWithValid |> String.concat "\n\n" + let branchesBlock = + branches + |> List.map (fun (k, sigs) -> + let inner = sigs |> List.map signalDecodeWithValid |> String.concat "\n\n" + [ sprintf " if ((int)%s == %d) {" rawVar k; inner; " }" ] |> String.concat "\n") + |> String.concat "\n" + [ if isMux then " msg->valid = 0u;" else "" + swBlock + baseBlock + branchesBlock ] + |> List.filter (fun s -> not (String.IsNullOrWhiteSpace s)) + |> String.concat "\n\n" + | _ -> message.Signals |> List.map signalDecodeFor |> String.concat "\n\n" + + let signalEncodeC = + match switchOpt, branches with + | Some sw, (_ :: _) -> + let len = int sw.Length + let startEff = Utils.chooseStartBit sw config + let (_, setFn) = Utils.accessorNames sw.ByteOrder + let rangeChecks = + if config.RangeCheck then + match sw.Minimum, sw.Maximum with + | Some minV, Some maxV -> Some (sprintf " if (msg->%s < %.17g || msg->%s > %.17g) { return false; }" sw.Name minV sw.Name maxV) + | Some minV, None -> Some (sprintf " if (msg->%s < %.17g) { return false; }" sw.Name minV) + | None, Some maxV -> Some (sprintf " if (msg->%s > %.17g) { return false; }" sw.Name maxV) + | _ -> None + else None + let computeRawDouble = + sprintf " double tmp_%s = ((double)msg->%s - %.17g) / %.17g;\n int64_t raw_%s = (int64_t)(tmp_%s >= 0 ? tmp_%s + 0.5 : tmp_%s - 0.5);" sw.Name sw.Name sw.Offset sw.Factor sw.Name sw.Name sw.Name sw.Name + let computeRawFloat = + sprintf " float tmp_%s = ((float)msg->%s - (float)%.17g) / (float)%.17g;\n int64_t raw_%s = (int64_t)llroundf(tmp_%s);" sw.Name sw.Name sw.Offset sw.Factor sw.Name sw.Name + let computeRaw = + match config.PhysType.ToLowerInvariant() with + | "fixed" -> + match Utils.tryPowerOfTenScale sw.Factor with + | Some scale when abs (sw.Offset - Math.Round(sw.Offset)) < 1e-12 -> + sprintf " int64_t raw_%s = (int64_t)llround(((double)msg->%s - %.0f) * (double)%d);" sw.Name sw.Name (Math.Round sw.Offset) scale + | _ -> (match config.PhysMode.ToLowerInvariant() with | "fixed_float" -> computeRawFloat | _ -> computeRawDouble) + | _ -> (match config.PhysMode.ToLowerInvariant() with | "float" -> computeRawFloat | _ -> computeRawDouble) + let setBits = sprintf " %s(data, %d, %d, (uint64_t)raw_%s);" setFn startEff len sw.Name + let baseBlock = baseSignals |> List.map (fun s -> genEncodeForSignal s config.RangeCheck config) |> String.concat "\n\n" + let branchesBlock = + branches + |> List.map (fun (k, sigs) -> + let inner = sigs |> List.map (fun s -> genEncodeForSignal s config.RangeCheck config) |> String.concat "\n\n" + [ sprintf " if ((int)raw_%s == %d) {" sw.Name k; inner; " }" ] |> String.concat "\n") + |> String.concat "\n" + [ match rangeChecks with | Some r -> yield r | None -> () + yield computeRaw + yield setBits + yield baseBlock + yield branchesBlock ] + |> List.filter (fun s -> not (String.IsNullOrWhiteSpace s)) + |> String.concat "\n\n" + | _ -> message.Signals |> List.map (fun s -> genEncodeForSignal s config.RangeCheck config) |> String.concat "\n\n" + + let headerContent = + let headerLines = System.Collections.Generic.List() + headerLines.Add(banner) + headerLines.Add (sprintf "#ifndef %s_H" (message.Name.ToUpperInvariant())) + headerLines.Add (sprintf "#define %s_H" (message.Name.ToUpperInvariant())) + headerLines.Add "" + headerLines.Add "#include " + headerLines.Add "#include " + headerLines.Add "" + headerLines.Add "#ifdef __cplusplus" + headerLines.Add "extern \"C\" {" + headerLines.Add "#endif" + headerLines.Add "" + // Value-table enums and to_string prototypes + let vtSignals = message.Signals |> List.choose (fun s -> s.ValueTable |> Option.map (fun vt -> s, vt)) + vtSignals |> List.iter (fun (s, vt) -> + let enumName = sprintf "%s_%s_e" message.Name s.Name + headerLines.Add (sprintf "typedef enum {") + let mutable used = Set.empty + vt |> List.iter (fun (v, name) -> + let baseLabel = sanitizeEnumIdent name + let rec uniqueLabel lbl idx = + let candidate = if idx = 0 then lbl else sprintf "%s_%d" lbl idx + if used.Contains candidate then uniqueLabel lbl (idx+1) else candidate + let label = uniqueLabel baseLabel 0 + used <- used.Add label + headerLines.Add (sprintf " %s_%s_%s = %d," (message.Name.ToUpperInvariant()) (s.Name.ToUpperInvariant()) label v) + ) + headerLines.Add (sprintf "} %s;" enumName) + headerLines.Add "" + headerLines.Add (sprintf "const char* %s_%s_to_string(int v);" message.Name s.Name) + headerLines.Add "" + ) + let switchOpt2, _, branches2 = partitionMultiplex message + let isMux2 = match switchOpt2, branches2 with | Some _, _ :: _ -> true | _ -> false + if isMux2 then + let enumName = sprintf "%s_mux_e" message.Name + headerLines.Add (sprintf "typedef enum { ") + let enumEntries = branches2 |> List.map (fun (k, _) -> sprintf " %s_MUX_%d = %d" (message.Name.ToUpperInvariant()) k k) |> String.concat ",\n" + headerLines.Add enumEntries + headerLines.Add (sprintf "} %s;" enumName) + headerLines.Add "" + message.Signals |> List.iteri (fun idx s -> headerLines.Add (sprintf "#define %s (1u << %d)" (sprintf "%s_VALID_%s" (message.Name.ToUpperInvariant()) (s.Name.ToUpperInvariant())) idx)) + headerLines.Add "" + headerLines.Add "typedef struct {" + headerLines.Add signalDeclarationsH + if isMux2 then + headerLines.Add " uint32_t valid;" + headerLines.Add (sprintf " %s_mux_e mux_active;" message.Name) + headerLines.Add (sprintf "} %s_t;" message.Name) + headerLines.Add "" + headerLines.Add (sprintf "bool %s_decode(%s_t* msg, const uint8_t data[], uint8_t dlc);" message.Name message.Name) + headerLines.Add (sprintf "bool %s_encode(uint8_t data[], uint8_t* out_dlc, const %s_t* msg);" message.Name message.Name) + headerLines.Add "" + headerLines.Add "#ifdef __cplusplus" + headerLines.Add "}" + headerLines.Add "#endif" + headerLines.Add "" + headerLines.Add (sprintf "#endif // %s_H" (message.Name.ToUpperInvariant())) + String.concat "\n" (List.ofSeq headerLines) + + let sourceContent = + let src = System.Collections.Generic.List() + src.Add(banner) + src.Add (sprintf "#include \"%s.h\"" messageNameLower) + let utilsHeader = Utils.utilsHeaderName config + src.Add (sprintf "#include \"%s\"" utilsHeader) + src.Add "#include " + src.Add "#include " + src.Add "" + let vtSignals = message.Signals |> List.choose (fun s -> s.ValueTable |> Option.map (fun vt -> s, vt)) + vtSignals |> List.iter (fun (s, vt) -> + src.Add (sprintf "const char* %s_%s_to_string(int v) {" message.Name s.Name) + src.Add " switch (v) {" + vt |> List.iter (fun (v, name) -> src.Add (sprintf " case %d: return \"%s\";" v (name.Replace("\"","\\\"")))) + src.Add " default: return \"UNKNOWN\";" + src.Add " }" + src.Add "}" + src.Add "" + ) + src.Add (sprintf "bool %s_decode(%s_t* msg, const uint8_t data[], uint8_t dlc) {" message.Name message.Name) + src.Add (sprintf " if (dlc < %d) { return false; }" (int message.Length)) + src.Add signalDecodeC + src.Add " return true;" + src.Add "}" + src.Add "" + src.Add (sprintf "bool %s_encode(uint8_t data[], uint8_t* out_dlc, const %s_t* msg) {" message.Name message.Name) + src.Add " memset(data, 0, 8);" + src.Add (sprintf " *out_dlc = %d;" (int message.Length)) + src.Add signalEncodeC + src.Add " return true;" + src.Add "}" + String.concat "\n" (List.ofSeq src) + + File.WriteAllText(messageHPath, headerContent) + File.WriteAllText(messageCPath, sourceContent) + messageHPath, messageCPath + + module Registry = + let generateRegistryFiles (ir: Ir) (outputPath: string) (config: Signal.CANdy.Core.Config.Config) = + let regHName = sprintf "%sregistry.h" config.FilePrefix + let regCName = sprintf "%sregistry.c" config.FilePrefix + let registryHPath = Path.Combine(outputPath, "include", regHName) + let registryCPath = Path.Combine(outputPath, "src", regCName) + + let guard = + (config.FilePrefix + "registry_h").ToUpperInvariant() + |> Seq.map (fun ch -> if Char.IsLetterOrDigit ch then ch else '_') + |> Seq.toArray + |> fun arr -> new string(arr) + let banner = sprintf "/* Generated by Signal CANdy\n file_prefix=%s, phys_type=%s, phys_mode=%s, dispatch=%s, motorola_start_bit=%s */\n" config.FilePrefix config.PhysType config.PhysMode config.Dispatch config.MotorolaStartBit + let registryHContent = + banner + sprintf "#ifndef %s\n#define %s\n\n#include \n#include \n\n#ifdef __cplusplus\nextern \"C\" {\n#endif\n\nbool decode_message(uint32_t id, const uint8_t data[], uint8_t dlc, void* msg);\n\n#ifdef __cplusplus\n}\n#endif\n\n#endif // %s" guard guard guard + File.WriteAllText(registryHPath, registryHContent) + + let includes = ir.Messages |> List.map (fun m -> sprintf "#include \"%s.h\"" (m.Name.ToLowerInvariant())) |> String.concat "\n" + + let body = + if config.Dispatch.ToLowerInvariant() = "direct_map" then + let cases = + ir.Messages + |> List.map (fun m -> sprintf " case %du: return %s_decode((%s_t*)msg, data, dlc);" (int m.Id) m.Name m.Name) + |> String.concat "\n" + sprintf "bool decode_message(uint32_t id, const uint8_t data[], uint8_t dlc, void* msg) {\n switch (id) {\n%s\n default: return false;\n }\n}" cases + else + let sorted = ir.Messages |> List.sortBy (fun m -> m.Id) + let entries = sorted |> List.map (fun m -> sprintf " { %du, (decode_func_t)%s_decode }" (int m.Id) m.Name) |> String.concat ",\n" + let table = sprintf "typedef bool (*decode_func_t)(void* msg, const uint8_t data[], uint8_t dlc);\n\ntypedef struct { uint32_t id; decode_func_t func; } decoder_entry_t;\n\nstatic const decoder_entry_t decoders[] = {\n%s\n};\n" entries + let search = + "bool decode_message(uint32_t id, const uint8_t data[], uint8_t dlc, void* msg) {\n int low = 0;\n int high = (int)(sizeof(decoders) / sizeof(decoder_entry_t)) - 1;\n while (low <= high) {\n int mid = low + (high - low) / 2;\n if (decoders[mid].id == id) {\n return decoders[mid].func(msg, data, dlc);\n }\n if (decoders[mid].id < id) low = mid + 1; else high = mid - 1;\n }\n return false;\n}\n" + table + search + let finalC = banner + "#include \n#include \n#include \"" + (sprintf "%sregistry.h" config.FilePrefix) + "\"\n" + includes + "\n\n" + body + File.WriteAllText(registryCPath, finalC) + registryHPath, registryCPath + + // Compatibility shims for legacy includes (utils.h, registry.h) + let private shimHeader (name: string) (target: string) = + let guard = (name.Replace('.', '_') + "_SHIM").ToUpperInvariant() + "#ifndef " + guard + "\n#define " + guard + "\n\n" + + "#ifdef __cplusplus\nextern \"C\" {\n#endif\n\n" + + "#include \"" + target + "\"\n\n" + + "#ifdef __cplusplus\n}\n#endif\n\n" + + "#endif // " + guard + + // Main entry: generate code and return file lists + let generate (ir: Ir) (outputPath: string) (config: Signal.CANdy.Core.Config.Config) : Result = + try + // Ensure output directories + Directory.CreateDirectory (Path.Combine(outputPath, "include")) |> ignore + Directory.CreateDirectory (Path.Combine(outputPath, "src")) |> ignore + + // Clean stale prefixed common files + let keepUtilsH = Utils.utilsHeaderName config + let keepUtilsC = Utils.utilsSourceName config + let keepRegH = sprintf "%sregistry.h" config.FilePrefix + let keepRegC = sprintf "%sregistry.c" config.FilePrefix + let includeDir = Path.Combine(outputPath, "include") + let srcDir = Path.Combine(outputPath, "src") + if Directory.Exists includeDir then + Directory.GetFiles(includeDir, "*utils.h") |> Array.iter (fun f -> if Path.GetFileName(f) <> keepUtilsH then try File.Delete f with _ -> ()) + Directory.GetFiles(includeDir, "*registry.h") |> Array.iter (fun f -> if Path.GetFileName(f) <> keepRegH then try File.Delete f with _ -> ()) + if Directory.Exists srcDir then + Directory.GetFiles(srcDir, "*utils.c") |> Array.iter (fun f -> if Path.GetFileName(f) <> keepUtilsC then try File.Delete f with _ -> ()) + Directory.GetFiles(srcDir, "*registry.c") |> Array.iter (fun f -> if Path.GetFileName(f) <> keepRegC then try File.Delete f with _ -> ()) + + // Generate utils + let uH = Utils.utilsHeaderName config + let uC = Utils.utilsSourceName config + let uHPath = Path.Combine(outputPath, "include", uH) + let uCPath = Path.Combine(outputPath, "src", uC) + File.WriteAllText(uHPath, Utils.utilsHContent config) + File.WriteAllText(uCPath, Utils.utilsCContent config) + + // Emit compatibility shims + let shimUtilsPath = Path.Combine(outputPath, "include", "utils.h") + let shimRegPath = Path.Combine(outputPath, "include", "registry.h") + File.WriteAllText(shimUtilsPath, shimHeader "utils.h" uH) + File.WriteAllText(shimRegPath, shimHeader "registry.h" keepRegH) + + // Messages + let msgFiles = + ir.Messages + |> List.map (fun m -> Message.generateMessageFiles m outputPath config) + // Registry + let regHPath, regCPath = Registry.generateRegistryFiles ir outputPath config + + let sources = msgFiles |> List.map snd |> fun xs -> uCPath :: regCPath :: xs + let headers = msgFiles |> List.map fst |> fun xs -> uHPath :: regHPath :: shimUtilsPath :: shimRegPath :: xs + let others : string list = [] + Ok { Sources = sources; Headers = headers; Others = others } + with ex -> + Error (CodeGenError.Unknown (sprintf "Codegen exception: %s" ex.Message)) diff --git a/src/Signal.CANdy.Core/Signal.CANdy.Core.fsproj b/src/Signal.CANdy.Core/Signal.CANdy.Core.fsproj index b59efbe..8e19584 100644 --- a/src/Signal.CANdy.Core/Signal.CANdy.Core.fsproj +++ b/src/Signal.CANdy.Core/Signal.CANdy.Core.fsproj @@ -11,6 +11,7 @@ + From 8960a088d247df5809d544e81ce99faafd5fb00b Mon Sep 17 00:00:00 2001 From: InitusNovus Date: Wed, 27 Aug 2025 10:03:37 +0900 Subject: [PATCH 08/15] feat(CLI): wire CLI to Core Api.generateFromPaths; fix F# indentation; smoke E2E PASS\nfeat(Facade): add C#-friendly thin facade wrapping Core (exceptions instead of Result)\ndocs: GEMINI Progress updated (2025-08-27) --- src/Signal.CANdy.CLI/Program.fs | 95 ++++++++++++++++++++++++++++++++- src/Signal.CANdy/Library.fs | 65 ++++++++++++++++++++-- 2 files changed, 155 insertions(+), 5 deletions(-) diff --git a/src/Signal.CANdy.CLI/Program.fs b/src/Signal.CANdy.CLI/Program.fs index deb1c8f..fb0c914 100644 --- a/src/Signal.CANdy.CLI/Program.fs +++ b/src/Signal.CANdy.CLI/Program.fs @@ -1,2 +1,93 @@ -// For more information see https://aka.ms/fsharp-console-apps -printfn "Hello from F#" +open System +open Signal.CANdy.Core +open Signal.CANdy.Core.Errors + +module Cli = + type Parsed = { + DbcPath: string option + OutDir: string option + ConfigPath: string option + ShowVersion: bool + ShowHelp: bool + Unknown: string list + } + + let empty: Parsed = + { DbcPath = None + OutDir = None + ConfigPath = None + ShowVersion = false + ShowHelp = false + Unknown = [] } + + let usage () = + String.concat "\n" [ + "Signal.CANdy.CLI — DBC → C code generator"; + ""; + "Usage:"; + " signal-candy --dbc --out [--config ]"; + " signal-candy --version"; + " signal-candy --help"; + ""; + "Options:"; + " --dbc Path to input DBC file (required)"; + " --out Output directory for generated C files (required)"; + " --config Optional YAML config (phys_type, range_check, dispatch, etc.)"; + " --version Print library version and exit"; + " --help, -h Show this help and exit" + ] + + let parse (argv: string array): Parsed = + let rec loop i (st: Parsed): Parsed = + if i >= argv.Length then st + else + match argv.[i] with + | "--dbc" when i + 1 < argv.Length -> loop (i + 2) { st with DbcPath = Some argv.[i + 1] } + | "--out" when i + 1 < argv.Length -> loop (i + 2) { st with OutDir = Some argv.[i + 1] } + | "--config" when i + 1 < argv.Length -> loop (i + 2) { st with ConfigPath = Some argv.[i + 1] } + | "--version" -> loop (i + 1) { st with ShowVersion = true } + | "--help" | "-h" -> loop (i + 1) { st with ShowHelp = true } + | unk -> loop (i + 1) { st with Unknown = st.Unknown @ [ unk ] } + loop 0 empty + +[] +let main argv: int = + let args = Cli.parse argv + + if args.ShowHelp then + printfn "%s" (Cli.usage ()) + 0 + elif args.ShowVersion then + printfn "%s" (Signal.CANdy.Core.Api.version ()) + 0 + elif args.Unknown |> List.isEmpty |> not then + eprintfn "Unknown arguments: %s" (String.Join(" ", args.Unknown)) + eprintfn "\n%s" (Cli.usage ()) + 2 + else + match args.DbcPath, args.OutDir with + | Some dbc, Some outDir -> + try + let cfgOpt = args.ConfigPath + let t = Signal.CANdy.Core.Api.generateFromPaths dbc outDir cfgOpt + let res = t.GetAwaiter().GetResult() + match res with + | Ok files -> + printfn "Code generation successful." + printfn "Headers: %d, Sources: %d, Others: %d" (files.Headers.Length) (files.Sources.Length) (files.Others.Length) + 0 + | Error err -> + let msg = + match err with + | CodeGenError.TemplateError s -> sprintf "Template error: %s" s + | CodeGenError.IoError s -> sprintf "IO error: %s" s + | CodeGenError.Unknown s -> sprintf "Error: %s" s + eprintfn "%s" msg + 1 + with ex -> + eprintfn "Unhandled error: %s" ex.Message + 1 + | _ -> + eprintfn "Missing required arguments." + eprintfn "\n%s" (Cli.usage ()) + 2 diff --git a/src/Signal.CANdy/Library.fs b/src/Signal.CANdy/Library.fs index e4f9291..c42717b 100644 --- a/src/Signal.CANdy/Library.fs +++ b/src/Signal.CANdy/Library.fs @@ -1,5 +1,64 @@ namespace Signal.CANdy -module Say = - let hello name = - printfn "Hello %s" name +open System +open System.Threading.Tasks + +/// Exceptions for C#-friendly facade API +type SignalCandyException(message: string) = + inherit Exception(message) + +type SignalCandyValidationException(message: string) = + inherit SignalCandyException(message) + +type SignalCandyParseException(message: string) = + inherit SignalCandyException(message) + +type SignalCandyCodeGenException(message: string) = + inherit SignalCandyException(message) + +/// C#-friendly Facade wrapping Signal.CANdy.Core +type GeneratorFacade() = + member _.Version : string = Signal.CANdy.Core.Api.version () + + member _.ValidateConfig(cfg: Signal.CANdy.Core.Config.Config) : unit = + match Signal.CANdy.Core.Config.validate cfg with + | Ok _ -> () + | Error e -> + let msg = match e with + | Signal.CANdy.Core.Errors.ValidationError.InvalidValue s -> s + | Signal.CANdy.Core.Errors.ValidationError.MissingField s -> s + | Signal.CANdy.Core.Errors.ValidationError.IoError s -> s + | Signal.CANdy.Core.Errors.ValidationError.Unknown s -> s + raise (SignalCandyValidationException(msg)) + + member _.ParseDbc(path: string) : Signal.CANdy.Core.Ir.Ir = + match Signal.CANdy.Core.Api.parseDbc path with + | Ok ir -> ir + | Error e -> + let msg = match e with + | Signal.CANdy.Core.Errors.ParseError.InvalidDbc s -> s + | Signal.CANdy.Core.Errors.ParseError.IoError s -> s + | Signal.CANdy.Core.Errors.ParseError.Unknown s -> s + raise (SignalCandyParseException(msg)) + + member _.GenerateCode(ir: Signal.CANdy.Core.Ir.Ir, outputPath: string, cfg: Signal.CANdy.Core.Config.Config) : Signal.CANdy.Core.Errors.GeneratedFiles = + match Signal.CANdy.Core.Api.generateCode ir outputPath cfg with + | Ok files -> files + | Error e -> + let msg = match e with + | Signal.CANdy.Core.Errors.CodeGenError.TemplateError s -> s + | Signal.CANdy.Core.Errors.CodeGenError.IoError s -> s + | Signal.CANdy.Core.Errors.CodeGenError.Unknown s -> s + raise (SignalCandyCodeGenException(msg)) + + member _.GenerateFromPathsAsync(dbcPath: string, outputPath: string, configPath: string) : Task = task { + let! res = Signal.CANdy.Core.Api.generateFromPaths dbcPath outputPath (if String.IsNullOrWhiteSpace configPath then None else Some configPath) + match res with + | Ok files -> return files + | Error e -> + let msg = match e with + | Signal.CANdy.Core.Errors.CodeGenError.TemplateError s -> s + | Signal.CANdy.Core.Errors.CodeGenError.IoError s -> s + | Signal.CANdy.Core.Errors.CodeGenError.Unknown s -> s + return raise (SignalCandyCodeGenException(msg)) + } From 6f896166bcf529b86ba246ff40685f0e7cf3abc3 Mon Sep 17 00:00:00 2001 From: InitusNovus Date: Wed, 27 Aug 2025 10:07:10 +0900 Subject: [PATCH 09/15] chore(pkg): add NuGet packaging metadata for Core and Facade projects --- src/Signal.CANdy.Core/Signal.CANdy.Core.fsproj | 9 +++++++++ src/Signal.CANdy/Signal.CANdy.fsproj | 13 +++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/Signal.CANdy.Core/Signal.CANdy.Core.fsproj b/src/Signal.CANdy.Core/Signal.CANdy.Core.fsproj index 8e19584..ac433f9 100644 --- a/src/Signal.CANdy.Core/Signal.CANdy.Core.fsproj +++ b/src/Signal.CANdy.Core/Signal.CANdy.Core.fsproj @@ -3,6 +3,15 @@ net8.0 true + InitusNovus + InitusNovus + Signal.CANdy.Core + Core library for Signal.CANdy: DBC parsing, config validation, and C99 code generation utilities. + https://github.com/InitusNovus/Signal-CANdy + git + CAN;DBC;codegen;C;F#;embedded + true + false diff --git a/src/Signal.CANdy/Signal.CANdy.fsproj b/src/Signal.CANdy/Signal.CANdy.fsproj index ba8f4cd..8084f25 100644 --- a/src/Signal.CANdy/Signal.CANdy.fsproj +++ b/src/Signal.CANdy/Signal.CANdy.fsproj @@ -3,14 +3,23 @@ net8.0 true + InitusNovus + InitusNovus + Signal.CANdy + C#-friendly facade for Signal.CANdy Core, wrapping Result with exceptions. + https://github.com/InitusNovus/Signal-CANdy + git + CAN;DBC;codegen;C;F#;facade + true + false - - + + From 5b2ebe208fbff58a402792102ef9b11a4c29efa0 Mon Sep 17 00:00:00 2001 From: InitusNovus Date: Wed, 27 Aug 2025 14:13:09 +0900 Subject: [PATCH 10/15] chore(packaging): add NuGet metadata, README.NuGet.md, and ignore artifacts; pack preview0 for Core/Facade --- .gitignore | 1 + README.md | 12 +++++ src/Signal.CANdy.Core/README.NuGet.md | 48 +++++++++++++++++++ .../Signal.CANdy.Core.fsproj | 8 ++++ src/Signal.CANdy/README.NuGet.md | 38 +++++++++++++++ src/Signal.CANdy/Signal.CANdy.fsproj | 11 +++++ 6 files changed, 118 insertions(+) create mode 100644 src/Signal.CANdy.Core/README.NuGet.md create mode 100644 src/Signal.CANdy/README.NuGet.md diff --git a/.gitignore b/.gitignore index 9a33a83..d98c357 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ bin/ obj/ tmp/ .fake +artifacts/ # External test data: ignore all contents by default; keep folder and docs only external_test/* !external_test/.gitkeep diff --git a/README.md b/README.md index 862839a..a47100c 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,18 @@ Languages: This README is in English. For Korean, see README.ko.md. This project generates portable C99 parser modules (headers/sources) from a `.dbc` file using an F# code generator. +## 📦 NuGet Packages (preview) + +- Signal.CANdy.Core — Core F# library (parsing, config, codegen) +- Signal.CANdy — C#-friendly facade over the Core + +Install: + +```pwsh +dotnet add package Signal.CANdy.Core --version 0.2.1-preview.0 +dotnet add package Signal.CANdy --version 0.2.1-preview.0 +``` + ## ⚡ Quick Start (5 minutes) 1) Check prerequisites diff --git a/src/Signal.CANdy.Core/README.NuGet.md b/src/Signal.CANdy.Core/README.NuGet.md new file mode 100644 index 0000000..4d85f68 --- /dev/null +++ b/src/Signal.CANdy.Core/README.NuGet.md @@ -0,0 +1,48 @@ +# Signal.CANdy.Core + +Core library for Signal.CANdy: parse DBC files, validate config, and generate C99 encode/decode code. + +- Repo: https://github.com/InitusNovus/Signal-CANdy +- License: MIT + +## Install + +``` +dotnet add package Signal.CANdy.Core --version 0.2.1-preview.0 +``` + +## Quick start (F#) + +```fsharp +open Signal.CANdy.Core + +let dbcPath = "examples/sample.dbc" +let outDir = "gen" + +match Api.parseDbc dbcPath with +| Ok ir -> + match Api.generateCode(ir, outDir, Config.defaults) with + | Ok files -> printfn "Generated %d headers, %d sources" files.Headers.Length files.Sources.Length + | Error e -> printfn "CodeGen error: %A" e +| Error e -> printfn "Parse error: %A" e +``` + +Or use the higher-level path-based API (loads optional YAML config): + +```fsharp +open System.Threading.Tasks +open Signal.CANdy.Core + +let run () : Task = task { + let! result = Api.generateFromPaths("examples/sample.dbc", "gen", None) + match result with + | Ok files -> printfn "OK: %A" files + | Error e -> printfn "Error: %A" e +} +``` + +## What's inside +- DBC parsing with validations (duplicate IDs, overlaps, DLC bounds) +- YAML config loader/validator (YamlDotNet) +- C99 codegen (headers/sources + registry/utils) +- Result-based API with discriminated union errors diff --git a/src/Signal.CANdy.Core/Signal.CANdy.Core.fsproj b/src/Signal.CANdy.Core/Signal.CANdy.Core.fsproj index ac433f9..97ed4aa 100644 --- a/src/Signal.CANdy.Core/Signal.CANdy.Core.fsproj +++ b/src/Signal.CANdy.Core/Signal.CANdy.Core.fsproj @@ -12,9 +12,16 @@ CAN;DBC;codegen;C;F#;embedded true false + 0.2.1-preview.0 + MIT + README.NuGet.md + true + snupkg + true + @@ -27,6 +34,7 @@ + diff --git a/src/Signal.CANdy/README.NuGet.md b/src/Signal.CANdy/README.NuGet.md new file mode 100644 index 0000000..de664c1 --- /dev/null +++ b/src/Signal.CANdy/README.NuGet.md @@ -0,0 +1,38 @@ +# Signal.CANdy + +C#-friendly facade over Signal.CANdy Core. Wraps Result-based F# API with exceptions and .NET-friendly types. + +- Repo: https://github.com/InitusNovus/Signal-CANdy +- License: MIT + +## Install + +``` +dotnet add package Signal.CANdy --version 0.2.1-preview.0 +``` + +## Quick start (C#) + +```csharp +using System.Threading.Tasks; +using Signal.CANdy; + +class Demo +{ + static async Task Main() + { + var facade = new GeneratorFacade(); + var files = await facade.GenerateFromPathsAsync( + dbcPath: "examples/sample.dbc", + outputPath: "gen", + configPath: null + ); + System.Console.WriteLine($"Headers: {files.Headers.Count}, Sources: {files.Sources.Count}"); + } +} +``` + +## What's inside +- Exceptions for validation/parse/codegen errors +- Simple async path-based API +- Access to Core IR and fine-grained APIs if needed diff --git a/src/Signal.CANdy/Signal.CANdy.fsproj b/src/Signal.CANdy/Signal.CANdy.fsproj index 8084f25..fded08b 100644 --- a/src/Signal.CANdy/Signal.CANdy.fsproj +++ b/src/Signal.CANdy/Signal.CANdy.fsproj @@ -12,9 +12,16 @@ CAN;DBC;codegen;C;F#;facade true false + 0.2.1-preview.0 + MIT + README.NuGet.md + true + snupkg + true + @@ -22,4 +29,8 @@ + + + + From 85951678bee21cd96cd2be86a709999239e3a2d9 Mon Sep 17 00:00:00 2001 From: InitusNovus Date: Wed, 27 Aug 2025 14:34:10 +0900 Subject: [PATCH 11/15] chore(ci,docs,version): add CI pack step, align Api.version to 0.2.1-preview.0, refine NuGet READMEs; build/tests green --- .github/workflows/ci.yml | 13 +++++++++++++ src/Signal.CANdy.Core/Api.fs | 2 +- src/Signal.CANdy.Core/README.NuGet.md | 2 +- src/Signal.CANdy/README.NuGet.md | 2 +- 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a0c72e2..91007a6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -125,6 +125,19 @@ jobs: run: | ./gen/build/test_runner test_fixed_suite_roundtrip + - name: Pack NuGet (Core/Facade) + run: | + dotnet pack -c Release src/Signal.CANdy.Core/Signal.CANdy.Core.fsproj -o artifacts + dotnet pack -c Release src/Signal.CANdy/Signal.CANdy.fsproj -o artifacts + + - name: Upload NuGet packages + uses: actions/upload-artifact@v4 + with: + name: nuget-packages + path: | + artifacts/*.nupkg + artifacts/*.snupkg + - name: Upload build artifacts (on failure) if: failure() uses: actions/upload-artifact@v4 diff --git a/src/Signal.CANdy.Core/Api.fs b/src/Signal.CANdy.Core/Api.fs index ca8ccc7..f9c7913 100644 --- a/src/Signal.CANdy.Core/Api.fs +++ b/src/Signal.CANdy.Core/Api.fs @@ -8,7 +8,7 @@ open Signal.CANdy.Core.Dbc open Signal.CANdy.Core.Codegen /// Returns the current library snapshot version. Placeholder until full API is moved. -let version () = "0.2.1-SNAPSHOT" +let version () = "0.2.1-preview.0" /// Parse a DBC file into IR. Stub for now. let parseDbc (path: string) : Result = diff --git a/src/Signal.CANdy.Core/README.NuGet.md b/src/Signal.CANdy.Core/README.NuGet.md index 4d85f68..bb119ea 100644 --- a/src/Signal.CANdy.Core/README.NuGet.md +++ b/src/Signal.CANdy.Core/README.NuGet.md @@ -22,7 +22,7 @@ let outDir = "gen" match Api.parseDbc dbcPath with | Ok ir -> match Api.generateCode(ir, outDir, Config.defaults) with - | Ok files -> printfn "Generated %d headers, %d sources" files.Headers.Length files.Sources.Length + | Ok files -> printfn "Generated: headers=%d sources=%d others=%d" (List.length files.Headers) (List.length files.Sources) (List.length files.Others) | Error e -> printfn "CodeGen error: %A" e | Error e -> printfn "Parse error: %A" e ``` diff --git a/src/Signal.CANdy/README.NuGet.md b/src/Signal.CANdy/README.NuGet.md index de664c1..41d6bf6 100644 --- a/src/Signal.CANdy/README.NuGet.md +++ b/src/Signal.CANdy/README.NuGet.md @@ -27,7 +27,7 @@ class Demo outputPath: "gen", configPath: null ); - System.Console.WriteLine($"Headers: {files.Headers.Count}, Sources: {files.Sources.Count}"); + System.Console.WriteLine($"Headers: {files.Headers.Count}, Sources: {files.Sources.Count}, Others: {files.Others.Count}"); } } ``` From bbb3ac910e7d9413cd5bab2d7fc248c870c9c726 Mon Sep 17 00:00:00 2001 From: InitusNovus Date: Wed, 27 Aug 2025 14:35:47 +0900 Subject: [PATCH 12/15] docs(facade): XML docs + error-handling examples; build/tests green --- src/Signal.CANdy/Library.fs | 32 ++++++++++++++++++++++++++++++++ src/Signal.CANdy/README.NuGet.md | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/src/Signal.CANdy/Library.fs b/src/Signal.CANdy/Library.fs index c42717b..c455269 100644 --- a/src/Signal.CANdy/Library.fs +++ b/src/Signal.CANdy/Library.fs @@ -4,22 +4,44 @@ open System open System.Threading.Tasks /// Exceptions for C#-friendly facade API +/// +/// Base exception for Signal.CANdy Facade. +/// type SignalCandyException(message: string) = inherit Exception(message) +/// +/// Configuration validation related errors. +/// type SignalCandyValidationException(message: string) = inherit SignalCandyException(message) +/// +/// DBC parsing related errors. +/// type SignalCandyParseException(message: string) = inherit SignalCandyException(message) +/// +/// Code generation related errors. +/// type SignalCandyCodeGenException(message: string) = inherit SignalCandyException(message) /// C#-friendly Facade wrapping Signal.CANdy.Core +/// +/// High-level .NET-friendly API over Signal.CANdy.Core. +/// Prefer for most use-cases. +/// type GeneratorFacade() = + /// + /// Library version string. + /// member _.Version : string = Signal.CANdy.Core.Api.version () + /// + /// Validates a configuration object; throws on error. + /// member _.ValidateConfig(cfg: Signal.CANdy.Core.Config.Config) : unit = match Signal.CANdy.Core.Config.validate cfg with | Ok _ -> () @@ -31,6 +53,9 @@ type GeneratorFacade() = | Signal.CANdy.Core.Errors.ValidationError.Unknown s -> s raise (SignalCandyValidationException(msg)) + /// + /// Parses a DBC file and returns the intermediate representation; throws on error. + /// member _.ParseDbc(path: string) : Signal.CANdy.Core.Ir.Ir = match Signal.CANdy.Core.Api.parseDbc path with | Ok ir -> ir @@ -41,6 +66,9 @@ type GeneratorFacade() = | Signal.CANdy.Core.Errors.ParseError.Unknown s -> s raise (SignalCandyParseException(msg)) + /// + /// Generates C code from IR and configuration; throws on error. + /// member _.GenerateCode(ir: Signal.CANdy.Core.Ir.Ir, outputPath: string, cfg: Signal.CANdy.Core.Config.Config) : Signal.CANdy.Core.Errors.GeneratedFiles = match Signal.CANdy.Core.Api.generateCode ir outputPath cfg with | Ok files -> files @@ -51,6 +79,10 @@ type GeneratorFacade() = | Signal.CANdy.Core.Errors.CodeGenError.Unknown s -> s raise (SignalCandyCodeGenException(msg)) + /// + /// High-level convenience: loads optional YAML config, parses DBC, and generates code. + /// Throws on error. + /// member _.GenerateFromPathsAsync(dbcPath: string, outputPath: string, configPath: string) : Task = task { let! res = Signal.CANdy.Core.Api.generateFromPaths dbcPath outputPath (if String.IsNullOrWhiteSpace configPath then None else Some configPath) match res with diff --git a/src/Signal.CANdy/README.NuGet.md b/src/Signal.CANdy/README.NuGet.md index 41d6bf6..e7ad044 100644 --- a/src/Signal.CANdy/README.NuGet.md +++ b/src/Signal.CANdy/README.NuGet.md @@ -36,3 +36,35 @@ class Demo - Exceptions for validation/parse/codegen errors - Simple async path-based API - Access to Core IR and fine-grained APIs if needed + +## Error handling + +```csharp +using System; +using System.Threading.Tasks; +using Signal.CANdy; + +class Demo +{ + static async Task Main() + { + var g = new GeneratorFacade(); + try + { + await g.GenerateFromPathsAsync("examples/fixed_suite.dbc", "gen", "examples/config.yaml"); + } + catch (SignalCandyValidationException ex) + { + Console.Error.WriteLine($"Config error: {ex.Message}"); + } + catch (SignalCandyParseException ex) + { + Console.Error.WriteLine($"DBC parse error: {ex.Message}"); + } + catch (SignalCandyCodeGenException ex) + { + Console.Error.WriteLine($"Codegen error: {ex.Message}"); + } + } +} +``` From 1ad5391b83cb6803f26feaffefa32e9168da00a5 Mon Sep 17 00:00:00 2001 From: InitusNovus Date: Wed, 27 Aug 2025 14:50:42 +0900 Subject: [PATCH 13/15] chore(release): bump to 0.2.1 stable (Core/Facade), update Api.version and docs; confirm pack; prep for v0.2.1 tag --- .github/workflows/release.yml | 24 ++++++++++++++++++- README.md | 6 ++--- src/Signal.CANdy.Core/Api.fs | 2 +- src/Signal.CANdy.Core/README.NuGet.md | 2 +- .../Signal.CANdy.Core.fsproj | 2 +- src/Signal.CANdy/README.NuGet.md | 2 +- src/Signal.CANdy/Signal.CANdy.fsproj | 2 +- 7 files changed, 31 insertions(+), 9 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 688d227..7edb812 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -31,9 +31,31 @@ jobs: run: | dotnet run --project src/Generator -- --dbc examples/sample.dbc --out gen --config examples/config.yaml - - name: Create GitHub Release + - name: Pack NuGet (Core/Facade) + run: | + dotnet pack -c Release src/Signal.CANdy.Core/Signal.CANdy.Core.fsproj -o artifacts + dotnet pack -c Release src/Signal.CANdy/Signal.CANdy.fsproj -o artifacts + + - name: Publish NuGet packages (stable only) + if: ${{ !contains(github.ref_name, '-') }} + env: + NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} + run: | + dotnet nuget push artifacts/*.nupkg --source https://api.nuget.org/v3/index.json --api-key "$NUGET_API_KEY" --skip-duplicate + + - name: Create GitHub Release (stable) + if: ${{ !contains(github.ref_name, '-') }} + uses: softprops/action-gh-release@v2 + with: + generate_release_notes: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Create GitHub Prerelease (preview tags) + if: ${{ contains(github.ref_name, '-') }} uses: softprops/action-gh-release@v2 with: generate_release_notes: true + prerelease: true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index a47100c..b27148f 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Languages: This README is in English. For Korean, see README.ko.md. This project generates portable C99 parser modules (headers/sources) from a `.dbc` file using an F# code generator. -## 📦 NuGet Packages (preview) +## 📦 NuGet Packages - Signal.CANdy.Core — Core F# library (parsing, config, codegen) - Signal.CANdy — C#-friendly facade over the Core @@ -21,8 +21,8 @@ This project generates portable C99 parser modules (headers/sources) from a `.db Install: ```pwsh -dotnet add package Signal.CANdy.Core --version 0.2.1-preview.0 -dotnet add package Signal.CANdy --version 0.2.1-preview.0 +dotnet add package Signal.CANdy.Core --version 0.2.1 +dotnet add package Signal.CANdy --version 0.2.1 ``` ## ⚡ Quick Start (5 minutes) diff --git a/src/Signal.CANdy.Core/Api.fs b/src/Signal.CANdy.Core/Api.fs index f9c7913..f605228 100644 --- a/src/Signal.CANdy.Core/Api.fs +++ b/src/Signal.CANdy.Core/Api.fs @@ -8,7 +8,7 @@ open Signal.CANdy.Core.Dbc open Signal.CANdy.Core.Codegen /// Returns the current library snapshot version. Placeholder until full API is moved. -let version () = "0.2.1-preview.0" +let version () = "0.2.1" /// Parse a DBC file into IR. Stub for now. let parseDbc (path: string) : Result = diff --git a/src/Signal.CANdy.Core/README.NuGet.md b/src/Signal.CANdy.Core/README.NuGet.md index bb119ea..95381a9 100644 --- a/src/Signal.CANdy.Core/README.NuGet.md +++ b/src/Signal.CANdy.Core/README.NuGet.md @@ -8,7 +8,7 @@ Core library for Signal.CANdy: parse DBC files, validate config, and generate C9 ## Install ``` -dotnet add package Signal.CANdy.Core --version 0.2.1-preview.0 +dotnet add package Signal.CANdy.Core --version 0.2.1 ``` ## Quick start (F#) diff --git a/src/Signal.CANdy.Core/Signal.CANdy.Core.fsproj b/src/Signal.CANdy.Core/Signal.CANdy.Core.fsproj index 97ed4aa..497b759 100644 --- a/src/Signal.CANdy.Core/Signal.CANdy.Core.fsproj +++ b/src/Signal.CANdy.Core/Signal.CANdy.Core.fsproj @@ -12,7 +12,7 @@ CAN;DBC;codegen;C;F#;embedded true false - 0.2.1-preview.0 + 0.2.1 MIT README.NuGet.md true diff --git a/src/Signal.CANdy/README.NuGet.md b/src/Signal.CANdy/README.NuGet.md index e7ad044..1162f3a 100644 --- a/src/Signal.CANdy/README.NuGet.md +++ b/src/Signal.CANdy/README.NuGet.md @@ -8,7 +8,7 @@ C#-friendly facade over Signal.CANdy Core. Wraps Result-based F# API with except ## Install ``` -dotnet add package Signal.CANdy --version 0.2.1-preview.0 +dotnet add package Signal.CANdy --version 0.2.1 ``` ## Quick start (C#) diff --git a/src/Signal.CANdy/Signal.CANdy.fsproj b/src/Signal.CANdy/Signal.CANdy.fsproj index fded08b..67d8f2e 100644 --- a/src/Signal.CANdy/Signal.CANdy.fsproj +++ b/src/Signal.CANdy/Signal.CANdy.fsproj @@ -12,7 +12,7 @@ CAN;DBC;codegen;C;F#;facade true false - 0.2.1-preview.0 + 0.2.1 MIT README.NuGet.md true From 9e6a35d83f3c4e793d2a1191d5475de98e192cf0 Mon Sep 17 00:00:00 2001 From: InitusNovus Date: Wed, 27 Aug 2025 15:05:59 +0900 Subject: [PATCH 14/15] fix(release): change package IDs to SignalCandy.Core and SignalCandy to avoid NuGet reservation conflict --- README.md | 8 ++++---- src/Signal.CANdy.Core/README.NuGet.md | 6 +++--- src/Signal.CANdy.Core/Signal.CANdy.Core.fsproj | 4 ++-- src/Signal.CANdy/README.NuGet.md | 10 +++++----- src/Signal.CANdy/Signal.CANdy.fsproj | 4 ++-- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index b27148f..9ea3c30 100644 --- a/README.md +++ b/README.md @@ -15,14 +15,14 @@ This project generates portable C99 parser modules (headers/sources) from a `.db ## 📦 NuGet Packages -- Signal.CANdy.Core — Core F# library (parsing, config, codegen) -- Signal.CANdy — C#-friendly facade over the Core +- SignalCandy.Core — Core F# library (parsing, config, codegen) +- SignalCandy — C#-friendly facade over the Core Install: ```pwsh -dotnet add package Signal.CANdy.Core --version 0.2.1 -dotnet add package Signal.CANdy --version 0.2.1 +dotnet add package SignalCandy.Core --version 0.2.1 +dotnet add package SignalCandy --version 0.2.1 ``` ## ⚡ Quick Start (5 minutes) diff --git a/src/Signal.CANdy.Core/README.NuGet.md b/src/Signal.CANdy.Core/README.NuGet.md index 95381a9..fe01c33 100644 --- a/src/Signal.CANdy.Core/README.NuGet.md +++ b/src/Signal.CANdy.Core/README.NuGet.md @@ -1,6 +1,6 @@ -# Signal.CANdy.Core +# SignalCandy.Core -Core library for Signal.CANdy: parse DBC files, validate config, and generate C99 encode/decode code. +Core library for SignalCandy: parse DBC files, validate config, and generate C99 encode/decode code. - Repo: https://github.com/InitusNovus/Signal-CANdy - License: MIT @@ -8,7 +8,7 @@ Core library for Signal.CANdy: parse DBC files, validate config, and generate C9 ## Install ``` -dotnet add package Signal.CANdy.Core --version 0.2.1 +dotnet add package SignalCandy.Core --version 0.2.1 ``` ## Quick start (F#) diff --git a/src/Signal.CANdy.Core/Signal.CANdy.Core.fsproj b/src/Signal.CANdy.Core/Signal.CANdy.Core.fsproj index 497b759..0a2df69 100644 --- a/src/Signal.CANdy.Core/Signal.CANdy.Core.fsproj +++ b/src/Signal.CANdy.Core/Signal.CANdy.Core.fsproj @@ -5,8 +5,8 @@ true InitusNovus InitusNovus - Signal.CANdy.Core - Core library for Signal.CANdy: DBC parsing, config validation, and C99 code generation utilities. + SignalCandy.Core + Core library for SignalCandy: DBC parsing, config validation, and C99 code generation utilities. https://github.com/InitusNovus/Signal-CANdy git CAN;DBC;codegen;C;F#;embedded diff --git a/src/Signal.CANdy/README.NuGet.md b/src/Signal.CANdy/README.NuGet.md index 1162f3a..a00e4bb 100644 --- a/src/Signal.CANdy/README.NuGet.md +++ b/src/Signal.CANdy/README.NuGet.md @@ -1,6 +1,6 @@ -# Signal.CANdy +# SignalCandy -C#-friendly facade over Signal.CANdy Core. Wraps Result-based F# API with exceptions and .NET-friendly types. +C#-friendly facade over SignalCandy Core. Wraps Result-based F# API with exceptions and .NET-friendly types. - Repo: https://github.com/InitusNovus/Signal-CANdy - License: MIT @@ -8,14 +8,14 @@ C#-friendly facade over Signal.CANdy Core. Wraps Result-based F# API with except ## Install ``` -dotnet add package Signal.CANdy --version 0.2.1 +dotnet add package SignalCandy --version 0.2.1 ``` ## Quick start (C#) ```csharp using System.Threading.Tasks; -using Signal.CANdy; +using SignalCandy; class Demo { @@ -42,7 +42,7 @@ class Demo ```csharp using System; using System.Threading.Tasks; -using Signal.CANdy; +using SignalCandy; class Demo { diff --git a/src/Signal.CANdy/Signal.CANdy.fsproj b/src/Signal.CANdy/Signal.CANdy.fsproj index 67d8f2e..c98ab5f 100644 --- a/src/Signal.CANdy/Signal.CANdy.fsproj +++ b/src/Signal.CANdy/Signal.CANdy.fsproj @@ -5,8 +5,8 @@ true InitusNovus InitusNovus - Signal.CANdy - C#-friendly facade for Signal.CANdy Core, wrapping Result with exceptions. + SignalCandy + C#-friendly facade for SignalCandy Core, wrapping Result with exceptions. https://github.com/InitusNovus/Signal-CANdy git CAN;DBC;codegen;C;F#;facade From f04a18da2f6223ee688b9f0ffac2c6c1e44355f6 Mon Sep 17 00:00:00 2001 From: InitusNovus Date: Wed, 27 Aug 2025 15:23:48 +0900 Subject: [PATCH 15/15] feat(docs,ci): add NuGet badges to README; add main-branch check to release workflow - Add NuGet version badges for SignalCandy and SignalCandy.Core packages - Modify release workflow to only deploy from main branch (prevents accidental releases from feature branches) - Release workflow now checks if tag is reachable from main before proceeding with NuGet publish --- .github/workflows/release.yml | 21 ++++++++++++++++++--- README.md | 3 +++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7edb812..bfeb955 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,35 +16,50 @@ jobs: with: fetch-depth: 0 + - name: Check if tag is on main branch + run: | + # Check if the tag points to a commit that's reachable from main + if ! git merge-base --is-ancestor ${{ github.sha }} origin/main 2>/dev/null; then + echo "Tag is not on main branch. Skipping release." + echo "SKIP_RELEASE=true" >> $GITHUB_ENV + else + echo "Tag is on main branch. Proceeding with release." + echo "SKIP_RELEASE=false" >> $GITHUB_ENV + fi + - name: Setup .NET + if: env.SKIP_RELEASE == 'false' uses: actions/setup-dotnet@v4 with: dotnet-version: '8.0.x' - name: Build & Test (Release) + if: env.SKIP_RELEASE == 'false' run: | dotnet restore dotnet build --configuration Release --nologo dotnet test --configuration Release -v minimal --nologo - name: Generate artifacts (optional) + if: env.SKIP_RELEASE == 'false' run: | dotnet run --project src/Generator -- --dbc examples/sample.dbc --out gen --config examples/config.yaml - name: Pack NuGet (Core/Facade) + if: env.SKIP_RELEASE == 'false' run: | dotnet pack -c Release src/Signal.CANdy.Core/Signal.CANdy.Core.fsproj -o artifacts dotnet pack -c Release src/Signal.CANdy/Signal.CANdy.fsproj -o artifacts - name: Publish NuGet packages (stable only) - if: ${{ !contains(github.ref_name, '-') }} + if: ${{ env.SKIP_RELEASE == 'false' && !contains(github.ref_name, '-') }} env: NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} run: | dotnet nuget push artifacts/*.nupkg --source https://api.nuget.org/v3/index.json --api-key "$NUGET_API_KEY" --skip-duplicate - name: Create GitHub Release (stable) - if: ${{ !contains(github.ref_name, '-') }} + if: ${{ env.SKIP_RELEASE == 'false' && !contains(github.ref_name, '-') }} uses: softprops/action-gh-release@v2 with: generate_release_notes: true @@ -52,7 +67,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Create GitHub Prerelease (preview tags) - if: ${{ contains(github.ref_name, '-') }} + if: ${{ env.SKIP_RELEASE == 'false' && contains(github.ref_name, '-') }} uses: softprops/action-gh-release@v2 with: generate_release_notes: true diff --git a/README.md b/README.md index 9ea3c30..2f99e60 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,9 @@ [![F#](https://img.shields.io/badge/F%23-language-blue.svg)](https://fsharp.org/) [![Version](https://img.shields.io/github/v/release/InitusNovus/Signal-CANdy?include_prereleases)](https://github.com/InitusNovus/Signal-CANdy/releases) +[![NuGet SignalCandy](https://img.shields.io/nuget/v/SignalCandy.svg)](https://www.nuget.org/packages/SignalCandy/) +[![NuGet SignalCandy.Core](https://img.shields.io/nuget/v/SignalCandy.Core.svg)](https://www.nuget.org/packages/SignalCandy.Core/) + [![C99](https://img.shields.io/badge/C-99-blue.svg)](https://en.wikipedia.org/wiki/C99) [![CAN DBC](https://img.shields.io/badge/protocol-CAN%20DBC-green.svg)](https://en.wikipedia.org/wiki/CAN_bus)