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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 131 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
name: CI

on:
push:
branches: [ ci-setup, dev-local, main ]
pull_request:
branches: [ main ]

jobs:
build-test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'

- name: Restore & Build
run: |
dotnet --info
dotnet restore
dotnet build --configuration Release --nologo

- name: Setup gen directory for tests (sanity)
run: |
dotnet run --project src/Generator -- --dbc examples/sample.dbc --out gen --config examples/config.yaml

- name: Run Tests (F#)
run: |
dotnet test -v minimal --nologo --configuration Release

- name: Codegen C (fixed_suite)
run: |
dotnet run --project src/Generator -- --dbc examples/fixed_suite.dbc --out gen --config examples/config_directmap_fixed.yaml --prefix sc_ --emit-main true

- name: Ensure Makefile in gen
run: |
cat > gen/Makefile <<'MAKEFILE'
CC = gcc
CFLAGS = -Wall -Wextra -std=c99
LDLIBS = -lm

BUILD_DIR = build
SRC_DIR = src
INCLUDE_DIR = include

# Discover all C sources under src and map to objects under build
SRCS := $(wildcard $(SRC_DIR)/*.c)
# Add stress test source if available
ifneq (,$(wildcard ../examples/stress_test.c))
SRCS += ../examples/stress_test.c
CFLAGS += -DHAVE_STRESS
endif

# If any prefixed common files exist, drop legacy unprefixed ones to avoid duplicates
ifneq (,$(filter-out $(SRC_DIR)/registry.c,$(wildcard $(SRC_DIR)/*registry.c)))
SRCS := $(filter-out $(SRC_DIR)/registry.c,$(SRCS))
endif
ifneq (,$(filter-out $(SRC_DIR)/utils.c,$(wildcard $(SRC_DIR)/*utils.c)))
SRCS := $(filter-out $(SRC_DIR)/utils.c,$(SRCS))
endif
OBJS := $(patsubst $(SRC_DIR)/%.c,$(BUILD_DIR)/%.o,$(SRCS))
OBJS := $(patsubst ../examples/%.c,$(BUILD_DIR)/%.o,$(OBJS))

TARGET = $(BUILD_DIR)/test_runner

.PHONY: all build test clean

all: build

build: $(TARGET)

# Generic build rule for any C file in src/
$(BUILD_DIR)/%.o: $(SRC_DIR)/%.c
mkdir -p $(@D)
$(CC) $(CFLAGS) -I$(INCLUDE_DIR) -c $< -o $@

# Rule for examples directory
$(BUILD_DIR)/%.o: ../examples/%.c
mkdir -p $(@D)
$(CC) $(CFLAGS) -I$(INCLUDE_DIR) -c $< -o $@

# Link all objects into the test runner
$(TARGET): $(OBJS)
mkdir -p $(@D)
$(CC) $(CFLAGS) $(OBJS) $(LDLIBS) -o $@

# Placeholder test target (adjust as needed)
test:
@echo "Running C tests... (Placeholder)"
@echo "No actual tests implemented yet. This is a placeholder."
# For example: ./$(TARGET) test_be_basic

clean:
rm -rf $(BUILD_DIR)
MAKEFILE

- name: Build generated C with Make
run: |
make -C gen build

- name: Smoke test runner
run: |
./gen/build/test_runner test_fixed_suite_roundtrip

- name: Upload build artifacts (on failure)
if: failure()
uses: actions/upload-artifact@v4
with:
name: gen-build-logs
path: |
gen/build
gen/include
gen/*.txt
gen/Makefile
gen/src
gen/*.log

lint:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Verify repo hygiene
run: |
if [ -d external_test ]; then echo "[lint] NOTE: external_test present (ignored by CI)"; fi
if [ -f TEST_SUMMARY.md ]; then echo "[lint] NOTE: TEST_SUMMARY.md should be local-only"; fi
26 changes: 26 additions & 0 deletions README.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,32 @@ dispatch: binary_search
crc_counter_check: false
```

```yaml
# 공통 생성 파일 접두사 설정(이름 충돌 회피)
# 결과: gen/include/sc_registry.h, gen/src/sc_registry.c 등
file_prefix: sc_
```

## CLI 플래그 (오버라이드)

일부 설정을 커맨드라인에서 덮어쓸 수 있습니다.

- `--prefix <str>`: 공통 생성 파일의 `file_prefix`를 오버라이드.
- `--emit-main <true|false>`: `examples/main.c`를 `gen/src/main.c`로 복사할지 제어.

예시

```bash
# 접두사 foo_ 사용, main.c 복사 생략
dotnet run --project src/Generator -- \
--dbc examples/sample.dbc \
--out gen \
--config examples/config.yaml \
--prefix foo_ \
--emit-main false
```
```

### 구성 파일 사용

```bash
Expand Down
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,31 @@ dispatch: binary_search
crc_counter_check: false
```

```yaml
# Prefix common generated files to avoid name collisions
# yields: gen/include/sc_registry.h, gen/src/sc_registry.c, etc.
file_prefix: sc_
```

## CLI flags (overrides)

You can override some config fields from the command line:

- `--prefix <str>`: overrides `file_prefix` for generated common files.
- `--emit-main <true|false>`: controls copying `examples/main.c` to `gen/src/main.c`.

Examples

```bash
# Use prefix foo_ and skip copying main.c
dotnet run --project src/Generator -- \
--dbc examples/sample.dbc \
--out gen \
--config examples/config.yaml \
--prefix foo_ \
--emit-main false
```

### Using a config

```bash
Expand Down
6 changes: 5 additions & 1 deletion examples/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
#include "sc_registry.h"
#include "sc_utils.h"

// Include stress test
// Include stress test only when available (controlled via -DHAVE_STRESS in Makefile)
#ifdef HAVE_STRESS
extern int test_stress_suite(void);
#endif

#if defined(__has_include)
# if __has_include("message_1.h")
Expand Down Expand Up @@ -449,9 +451,11 @@ int main(int argc, char *argv[]) {
return test_dispatch_external_multi();
}
#endif
#ifdef HAVE_STRESS
else if (strcmp(argv[1], "test_stress_suite") == 0) {
return test_stress_suite();
}
#endif
else {
printf("Unknown or unavailable test: %s\n", argv[1]);
return 1;
Expand Down
7 changes: 7 additions & 0 deletions src/Generator/Codegen.Message.fs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,11 @@ module Message =
let messageHPath = Path.Combine(outputPath, "include", sprintf "%s.h" messageNameLower)
let messageCPath = Path.Combine(outputPath, "src", sprintf "%s.c" messageNameLower)

// Banner for traceability
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"

Expand Down Expand Up @@ -226,6 +231,7 @@ module Message =

let headerContent =
let headerLines = System.Collections.Generic.List<string>()
headerLines.Add(banner)
headerLines.Add (sprintf "#ifndef %s_H" (message.Name.ToUpperInvariant()))
headerLines.Add (sprintf "#define %s_H" (message.Name.ToUpperInvariant()))
headerLines.Add ""
Expand Down Expand Up @@ -284,6 +290,7 @@ module Message =

let sourceContent =
let src = System.Collections.Generic.List<string>()
src.Add(banner)
src.Add (sprintf "#include \"%s.h\"" messageNameLower)
let utilsHeader = Utils.utilsHeaderName config
src.Add (sprintf "#include \"%s\"" utilsHeader)
Expand Down
6 changes: 3 additions & 3 deletions src/Generator/Codegen.Registry.fs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ module Registry =
|> Seq.map (fun ch -> if System.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 =
sprintf "#ifndef %s\n#define %s\n\n#include <stdint.h>\n#include <stdbool.h>\n\nbool decode_message(uint32_t id, const uint8_t data[], uint8_t dlc, void* msg);\n\n#endif // %s" guard guard guard
banner + sprintf "#ifndef %s\n#define %s\n\n#include <stdint.h>\n#include <stdbool.h>\n\nbool decode_message(uint32_t id, const uint8_t data[], uint8_t dlc, void* msg);\n\n#endif // %s" guard guard guard
File.WriteAllText(registryHPath, registryHContent)

let includes =
Expand All @@ -45,7 +46,6 @@ module Registry =
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 = "#include <stdint.h>\n#include <stdbool.h>\n#include \"" + regHName + "\"\n" + includes + "\n\n" + body
let finalC = banner + "#include <stdint.h>\n#include <stdbool.h>\n#include \"" + regHName + "\"\n" + includes + "\n\n" + body
File.WriteAllText(registryCPath, finalC)
()
6 changes: 4 additions & 2 deletions src/Generator/Codegen.Utils.fs
Original file line number Diff line number Diff line change
Expand Up @@ -78,12 +78,14 @@ module Utils =
let utilsSourceName (config: Generator.Config.Config) = sprintf "%sutils.c" config.FilePrefix

let utilsHContent (config: Generator.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"
sprintf "#ifndef %s\n#define %s\n\n#include <stdint.h>\n#include <stdbool.h>\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#endif // %s" g g g
banner + sprintf "#ifndef %s\n#define %s\n\n#include <stdint.h>\n#include <stdbool.h>\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#endif // %s" g g g

let utilsCContent (config: Generator.Config.Config) =
let uH = utilsHeaderName config
"#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}"
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) =
Expand Down
29 changes: 23 additions & 6 deletions src/Generator/Codegen.fs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,24 @@ open Generator.Registry

module Codegen =

let generateCode (ir: Ir) (outputPath: string) (config: Generator.Config.Config) =
let generateCode (ir: Ir) (outputPath: string) (config: Generator.Config.Config) (emitMain: bool) =
try
// Helper: find examples/main.c starting from likely roots (CWD, assembly base) and traversing upwards
let tryFindExampleMain () =
let candidatesFrom (startDir: string) =
seq {
let mutable d = startDir
for _ in 0 .. 6 do
let p = Path.Combine(d, "examples", "main.c")
if File.Exists p then yield p
let parent = Directory.GetParent(d)
if isNull parent then () else d <- parent.FullName
}
let bases = [ Directory.GetCurrentDirectory(); System.AppContext.BaseDirectory ]
bases
|> Seq.collect candidatesFrom
|> Seq.tryHead

// Create output directories
Directory.CreateDirectory (Path.Combine(outputPath, "include")) |> ignore
Directory.CreateDirectory (Path.Combine(outputPath, "src")) |> ignore
Expand Down Expand Up @@ -40,11 +56,12 @@ module Codegen =
// Generate registry files with prefix
Registry.generateRegistryFiles ir outputPath config

// Copy example main.c into output to act as test harness
let exampleMain = Path.Combine("examples", "main.c")
let outMain = Path.Combine(outputPath, "src", "main.c")
if File.Exists(exampleMain) then
File.Copy(exampleMain, outMain, true)
// Copy example main.c into output to act as test harness (optional)
if emitMain then
let outMain = Path.Combine(outputPath, "src", "main.c")
match tryFindExampleMain () with
| Some exampleMain -> File.Copy(exampleMain, outMain, true)
| None -> eprintfn "Warning: examples/main.c not found from working locations; skipping emit-main."

true

Expand Down
Loading