Conversation
라인 PC 에서 discover 한 번으로 받은 값: 두 대 모두 3.6.2.9. 검사 프로그램이 도는 중에 그대로 뽑았다 — 탐색은
카메라를 열지 않으므로 라인을 세우지 않았다. 구성도 적는다: 카메라마다 호스트 NIC 와 서브넷이 따로라 포트를
공유하지 않고 SCPD 가 필요 없다. 8시간 런("한 포트에 두 대")과 같은 조건으로 오독되지 않게 하려는 것이다.
어댑터마다 ListBox 를 두고 전부 같은 Selected 에 두 방향으로 묶어 두었다. 한 목록에서 카메라를 고르면 다른 목록들이 자기 항목이 아닌 값을 받아 선택을 비우며 null 을 되밀어 방금 고른 것이 사라지고, 비우지 못한 목록에는 옛 강조가 남아 둘이 골라진 것처럼 보였다. 카메라가 NIC 두 개에 한 대씩인 라인 PC 에서 특히 그랬다. - 목록 하나(Rows: 어댑터 머리 행 + 그 카메라 행)로 합치고 SelectedRow 하나로 받는다. 재구성 중의 null 은 가드가 버린다. - SelectedItem 바인딩은 목록→VM 한 방향만 둔다. VM→목록은 두 방향 바인딩으로는 오지 않았다(실측: 항목이 있고 VM 도 골랐는데 SelectedIndex 가 -1). 코드비하인드가 SelectedRow 변경을 받아 직접 넣고, 목록을 막 채운 호출 안에서 붙지 않으면 한 차례 뒤 다시 넣는다. - 카메라↔어댑터를 바꿀 때 상대를 먼저 비운 뒤 SelectedRow 를 올린다 — 순서가 반대면 동기화가 옛 것을 되돌려 넣어 첫 클릭이 어긋났다. - 첫 검색에서 첫 카메라를 대신 골라 주지 않는다. 여러 대일 때 혼동만 준다. 고른 것을 MAC 으로 되찾는 것만 남긴다.
검토에서 실측된 결함: Regroup 이 Rows 를 다시 채운 뒤 어댑터 선택만 되살리고 카메라 선택은 되살리지 않았다. 어댑터 이름·상태가 바뀌어 LoadNics 경로로 다시 묶이면(시작 직후 연결 이름을 읽은 뒤, 또는 Scan 때 어댑터 변화) 목록에는 강조가 없는데 오른쪽은 카메라를 보이고 "Apply now" 가 살아 있는 채로 남았고, 장치가 그대로라 재검색은 아무것도 하지 않아 영영 그대로였다. 다시 채운 자리에서 SelectedRow 를 올려 되살린다. 함께: 고른 어댑터가 사라지면 선택을 비운다(없는 어댑터를 오른쪽에 계속 보이지 않는다). 같은 어댑터의 새 객체로 되찾을 때는 이름 칸·방화벽 조회를 건드리지 않는다(재검색마다 PowerShell 을 띄우고 치던 이름을 덮던 것). 카메라 열기와 방화벽 조회는 선택이 300 ms 멈춘 뒤에 한다 — 화살표 키로 목록을 훑을 때 행마다 열지 않도록. Ctrl+클릭의 선택 해제는 그대로 받는다. 목록 재시도는 한 번만 한다. Groups 는 화면이 읽지 않으므로 비공개로. 검증: 검토자의 헤드리스 하네스(실제 VM·XAML·코드비하인드, 벤치 Basler)에서 클릭 → 어댑터 서명 변화 → 재검색 10회 → 수동 Scan → 장치 변화 → 어댑터 선택 재구성 전부에서 목록과 VM 일치. GUI 캡처로 첫 화면·클릭·전환 확인.
GVCP 명령은 응답을 기다리기 전에 이미 장치로 나간다. 그래서 응답 유실·PENDING_ACK 뒤 시한 초과·응답 대기 중 취소로 쓰기가 예외를 내도 장치는 새 값을 들고 있을 수 있다. 그런데 레지스터 캐시 갱신과 겹치는 캐시 버리기, 노드 층의 의존 무효화가 전부 쓰기 성공 뒤에만 있어서, 실패한 쓰기 뒤의 다음 읽기가 장치에 묻지 않고 옛 값을 돌려줬다. 예외도 경고도 없이 정상처럼 보이는 값이다. 닿는 사슬 하나: 시작/정지 명령이 같은 레지스터에 1/0 을 쓰고 획득 모드의 pIsLocked 가 그 레지스터를 읽는 XML 에서, 정지의 응답만 유실되면 캐시가 "획득 중" 으로 남아 모드 쓰기가 장치에 묻지도 않고 잠김으로 거절됐다 (테스트로 재현 — 고치기 전 "Node 'AcquisitionMode' cannot be written: locked."). - RegisterCore.WriteAsync: 포트 쓰기가 어떤 예외로든 실패하면 자기 캐시와 주소가 겹치는 다른 레지스터의 캐시를 버리고 다시 던진다. 예외 종류로 가르지 않는다 — 장치가 거절했거나 보내기 전 실패였다면 비용은 다음 읽기 한 번이다. 쓰기 전용 레지스터의 그림자는 건드리지 않는다: "모름" 을 적을 자리가 없고, 지우면 형제 필드 비트가 0 이 되어 다음 쓰기가 확실히 틀린다. 옛 그림자는 틀릴 수 있을 뿐이고 다시 쓰면 바로잡힌다(회귀로 고정). - 노드 층(Integer·Float·String·StringReg·Boolean·Enumeration·Command·Register 의 쓰기 8곳): 쓰기가 실패하면 노드 자신과 값 사슬까지 포함한 의존 닫힘 전체를 버리고(Invalidate 와 같다) 다시 던진다. - GevStream.StartAsync: SCP 를 썼다는 표시를 보내기 전에 한다. SCP 쓰기 자체의 응답이 유실되면 장치가 포트를 받은 채 되돌리기를 건너뛰어 닫힌 포트로 쏠 수 있었다(테스트로 재현 — 장치에 포트가 남았다). 테스트: 실패 뒤 다음 읽기가 장치에 묻는 것, pInvalidator 청취자 캐시, 잠금 술어 사슬, 쓰기 전용 그림자 보존, SCP 되돌리기. 앞의 넷 중 셋과 SCP 는 고치기 전 코드에서 실패하는 것을 먼저 확인했다. 각 테스트에 성공한 쓰기는 캐시를 그대로 쓴다는 대조군을 같이 둔다. net8.0 1920건·net48 1668건 통과.
수식 엔진은 정수끼리의 나눗셈을 0 방향으로 잘랐고, 실수 노드도 같은 규칙으로 평가했다. 실수 노드의 변수는 대개 정수 레지스터에서 오므로(TO·pVariable 이 IntReg/Integer) 식 전체가 정수로 계산된 뒤에야 실수로 바뀌었다. 예외도 경고도 없이 값만 틀린다. - 프레임률 SwissKnife `1000000 / N` 이 정수로 떨어졌다. 실기 근거: 벤치 Crevis MG-A500M-22 를 CLI 로 덤프한 값이 `ResultingFrameRate = 21 Hz` 였다 — 정수 N 으로 실수 나눗셈 1e6/N 이 정확히 21.0 이 될 수 없다. - 0.1 dB 단위 이득 변환 `10 ** ((TO / 10) / 20)` 은 TO 가 200 미만이면 전부 1.0 이 됐다. 같은 식의 pMax 한계도 잘려 유효한 쓰기가 범위 밖으로 거절됐다(재현: 한계 10^1.2 인데 "outside the range 1..10" 으로 12.0 거절). - 고정소수점 `RAW / (1 << FRAC)` 은 0 이 됐다. 고침: 평가 규칙 FormulaMode 를 둔다. 실수 노드(SwissKnife·Converter — 본식, Expression, Converter 한계 계산 모두)는 Real, 정수 노드(IntSwissKnife·IntConverter·주소 수식)와 공개 Formula.Evaluate 는 지금 그대로 Integer. Real 에서는 `/`·`**` 가 정수끼리도 실수 결과를 내고, 정수끼리의 + - * 는 정확하므로 정수로 두되 넘치면 예외 대신 실수로 계속한다. 비트·시프트는 정수 연산이라 실수 피연산자를 0 방향으로 잘라 정수로 바꾼다 — 나눗셈이 실수가 되면서 `(N / 2) & 1` 같은 식이 예외로 깨지지 않게 하려는 것이고, 정수 나눗셈 뒤 비트 연산을 하던 결과와 같다(음이 아닌 값에서). NaN·범위 밖은 자르지 않고 예외다. 규칙은 FormulaScope 생성자의 필수 인자라 새 수식 노드가 고르지 않고 지나가지 못한다. 공개 API 는 바뀌지 않는다(모드는 내부 오버로드). 소비자가 받는 값이 바뀐다: 실수 노드 중 정수끼리 나누는 식을 가진 것(프레임률·이득·온도·감마 등)은 이 판부터 참값을 낸다. 테스트: 정수 레지스터에서 온 변수로 프레임률·중첩 Expression·시프트+나눗셈·dB 이득 읽기·pMax 와 Converter 한계를 통한 쓰기 — 다섯 모두 고치기 전 코드에서 실패를 먼저 확인했다. 대조군으로 같은 식의 IntSwissKnife 는 21 을 그대로 낸다. 수식 단위로 실수 규칙의 나눗셈·넘침·비트 절삭(정수 규칙과 같은 답인지 함께)·NaN/범위 밖 거절·0 나눗셈. 로컬 벤더 XML 두 벌(Crevis·Teledyne) 코퍼스 검사 통과. net8.0 1940건·net48 1688건 통과.
실수 규칙을 넣으며 Formula.EvaluateAsync 에 내부 오버로드가 생겨 클래스 주석의 cref 가 모호해졌고(CS0419), 생성자에 mode 한 개만 param 태그를 달아 나머지 인자가 경고를 냈다(CS1573). cref 를 오버로드까지 적어 못 박고, 생성자 설명은 summary 로 옮겼다. --no-incremental 솔루션 빌드 경고 0.
완성 판정이 "예상 패킷 수만큼 받았나" 뿐이었고, 예상 패킷 수는 트레일러가 조건 없이 덮어썼다. 장치가 블록을 중간에 끊고(정지 순간 등) 낮은 id 의 트레일러를 보내면 패킷 수는 맞아 IsComplete = true, MissingPackets = 0, PayloadSize = 리더 크기로 나가는데, 안 온 꼬리는 풀에서 다시 쓰는 버퍼라 이전 프레임의 픽셀이 남아 있었다. 흔적은 Debug 한 줄뿐이었다. DeliverIncompleteFrames = false 를 "완전한 프레임만" 으로 믿는 소비자에게 정상처럼 보이는 틀린 영상이다(테스트로 재현 — 버퍼를 정상 프레임으로 한 번 채운 뒤 끊긴 블록을 보내면 꼬리가 이전 프레임 바이트 그대로 완성으로 나왔다). - IsComplete 에 "리더가 크기를 알렸으면 받은 끝 ≥ 그 크기" 를 더한다. 마지막 패킷은 짧을 수 있어 패킷 수 × 패킷 크기가 아니라 실제로 받은 끝으로 본다. - 트레일러가 약속한 패킷은 다 왔는데 바이트가 모자라면 더 올 것이 없다 — 보존 시간까지 기다리지 않고 바로 불완전으로 닫는다(기다리면 뒤 프레임이 그만큼 막힌다). 경고는 스트림당 한 번(정지마다 끊는 장치면 단발 그랩마다 쌓이므로), 그 뒤는 불완전 통계·FrameDropped 로 세고 프레임별 줄은 Debug. - 불완전 보고의 예상/누락 패킷 수를 리더 바이트에서도 구한다(끊긴 블록이 "누락 0" 으로 찍히지 않게). 불완전 프레임을 내보낼 때 트레일러가 약속한 패킷 뒤의 꼬리도 0 으로 비운다. - 가변 높이: 트레일러의 줄 수를 슬롯에 남겨, 리더가 리센드로 트레일러 뒤에 와도 축소한다(전에는 리더의 최대 줄 수로 잡혀 높이 100 이 그대로 나갔다). 크기는 줄 간격 × 줄 수 대신 픽셀 포맷 규칙으로 다시 구한다 — 줄이 바이트 경계에서 끝나지 않는 패킹(Stride 0)에서 PayloadSize 가 0 이 되던 것도 같이 고친다(Mono12p 홀수 폭으로 재현). 회귀 축(새로 생긴 것): 우리가 리더에서 계산한 크기가 장치가 실제로 보내는 크기보다 크면 이제 모든 프레임이 불완전이 된다(전에는 꼬리가 쓰레기인 채 완성으로 나갔다 — evaluation.md 의 Mono12Packed 홀수 폭 96 바이트 건이 그 모양이었다). 조용한 쓰레기가 시끄러운 불완전으로 바뀌는 방향이고, 경고 문구가 두 크기를 함께 적는다. 벤치 실측(Basler acA2500-14gm, 방화벽 켠 채 필터 드라이버 없음, CLI grab, 끝에 AcquisitionStop): Mono8 2592x1944 60장 → 61 완성 0 불완전, Mono12Packed 2592 30장 → 31 완성 0 불완전, Mono12Packed 홀수 폭 2591 (Stride 0, 7,555,356 바이트) 30장 → 31 완성 0 불완전. 설정은 원복했다. Crevis 는 이때 응답이 없어 재지 못했다. 끊긴 블록 자체는 실기에서 본 적이 없다 — 시뮬레이터는 멈출 때 프레임을 끝까지 보내 이 길을 타지 않는다. 테스트: 끊긴 블록(불완전 전달 켬 — 꼬리가 이전 프레임이 아니고 0, 예상 5·누락 3 / 끔 — 다음 프레임이 막히지 않음), 리더가 트레일러 뒤에 온 가변 높이, Mono12p 홀수 폭 가변 높이. 넷 다 고치기 전 코드에서 실패하는 것을 먼저 확인했다. net8.0 1944건·net48 1692건 통과, --no-incremental 솔루션 빌드 경고 0.
GevDevice 는 자기가 연 스트림을 참조하지 않아 제어 상실(ControlLost)이나 장치 Dispose 가 스트림을 멈추지 않는다. 수신 스레드는 계속 듣고 IsStarted 는 참으로 남으며, 말없는 장치를 향한 ReceiveAsync 는 스스로 끝나지 않는다. 그런데 ReceiveAsync 문서는 시작 전·정지 뒤의 GevStreamClosedException 만 적었고 README 예제는 토큰 없이 기다려, 장치를 잃으면 영영 멈출 수 있는 코드를 보여 주고 있었다. - ReceiveAsync 문서·architecture.md(API 줄과 스레딩 절): 장치가 사라져도 대기는 안 풀린다, 토큰을 주거나 ControlLost 처리에서(그리고 장치를 닫기 전에) StopAsync 를 부르라. - README 예제: 프레임마다 CancelAfter 로 기한을 둔 토큰을 넘기고 이유를 한 줄 적었다. - DeviceLifecycleTests.Stream_OutlivesItsDevice_WaitingReceiveEndsOnlyByTokenOrStop: 장치를 닫은 뒤 스트림이 시작된 채이고, 토큰은 대기를 취소하고, 토큰 없는 대기는 StopAsync 로만 GevStreamClosedException 으로 끝나는 것을 시뮬레이터로 실측해 고정한다. 동작은 바꾸지 않았다(장치가 스트림을 닫게 하는 설계 변경은 하지 않음).
불완전 프레임을 내보낼 때 꼬리를 트레일러가 약속한 패킷 수 × 패킷 크기부터 비웠다. 블록이 패킷 가운데서 끊겨 마지막 페이로드가 짧으면 [받은 끝, 그 패킷 자리의 끝) 이 비워지지 않아 이전 프레임 바이트가 남았다(테스트로 재현 — 버퍼를 정상 프레임으로 한 번 채운 뒤 셋째 패킷을 100 바이트에서 끊으면 그 틈이 이전 프레임 그대로였다). 꼬리 시작을 실제로 받은 끝과 그 값 중 작은 쪽으로 당긴다. 받은 끝은 받은 바이트의 최대 끝이라 그 뒤를 비워도 받은 데이터는 건드리지 않는다. Gvsp 테스트 3회 연속 149/149, net8.0 1946건·net48 1693건 통과.
- architecture.md 수식 절: 정수 규칙 하나만 적혀 있던 것을 Integer(IntSwissKnife·IntConverter·주소 수식·공개 Formula.Evaluate)와 Real(SwissKnife·Converter) 두 규칙으로 바꾸고, Real 에서 나눗셈·거듭제곱·넘침·비트 연산이 어떻게 되는지 적는다. - architecture.md 무효화 절: 쓰기가 예외로 끝나면 장치가 새 값을 들고 있을 수 있으므로 자기 캐시·겹치는 캐시·Invalidate 와 같은 닫힘을 버린다는 것과 그림자를 건드리지 않는 이유. - sim-register-map.md: 시뮬레이터 Converter 의 TO / 10.0 을 '엔진이 정수 나눗셈을 자르므로' 로 설명하던 문단이 이제 사실이 아니다 — 실수 노드는 정수끼리도 실수로 나누며, 리터럴은 그 규칙에 기대지 않으려고 남겨 둔다고 고친다. - design-requirements.md: 이번에 이 라이브러리에서 찾은 결함 셋(실수 수식 절삭, 보낸 뒤 실패한 쓰기의 캐시, 패킷 수만 본 완성 판정)을 R27~R29 요구사항과 상태 행으로 보탠다. 상태 행의 테스트는 고치기 전 트리에서 실패하는 것을 본 것들이다.
완성 판정이 리더가 알린 바이트까지를 요구하게 되면서, 리더 크기를 우리가 장치보다 크게 계산하는 포맷에서는 모든 프레임이 불완전이 될 수 있다. 가장 유력한 자리를 벤치 Basler acA2500-14gm 에서 쟀다: Mono12Packed 2591x1943(픽셀 수 5,034,313, 홀수)에서 장치 PayloadSize 는 7,551,470(=ceil(p x 1.5), 마지막 한 픽셀이 2 바이트)인데 우리 연속 묶음 규칙은 7,551,471 로 1 바이트 크다. 그래도 그랩 10장은 11 완성 0 불완전이었다 — 장치가 마지막 패킷을 그보다 길게 채워 보내 복사가 한계에서 잘린다. 이 장치에서는 재현되지 않으므로 규칙과 판정은 바꾸지 않는다. - IsComplete 주석에 이 실측과 '마지막 패킷을 채우지 않는 장치라면 여기서 걸린다' 를 적는다. - 끊긴 블록 경고(스트림당 한 번)에 '모든 프레임이 이렇게 끝나면 끊긴 블록이 아니라 이 픽셀 포맷의 크기 불일치' 라는 안내를 덧붙인다 — 현장에서 원인을 가를 수 있게. - evaluation.md 홀수 폭 절에 홀수 픽셀 수 측정을 보탠다(설정은 Mono8 2592x1944 로 원복, PayloadSize 5,038,848 확인).
실수 규칙(SwissKnife·Converter)을 적대 검토한 결과(major 이상 0건)의 minor·nit 반영. - %: 실수 규칙에서 실수 피연산자를 비트 연산처럼 0 방향으로 잘라 정수 나머지를 낸다. 나눗셈이 실수가 되면서 (N / 2) % 2 가 N = 7 에서 1 이 아니라 1.5 가 되던 것 — 정수 나눗셈 뒤 나머지와 같은 답으로 되돌린다. 정수 규칙(공개 Formula.Evaluate)의 실수 나머지는 fmod 그대로. - **: 0 의 음수 거듭제곱은 지수가 실수여도 0 나눗셈 예외, 음수의 분수 거듭제곱처럼 결과가 정의되지 않으면 NaN 대신 예외. 실수 규칙에서는 지수가 나눗셈에서 실수로 오므로 정수 지수만 막던 검사로는 B ** (X / 2) 가 무한대, (-8) ** (1 / 3) 이 NaN 을 값으로 냈다. 엔진 계약(NaN·무한대를 값으로 흘리지 않는다)에 맞춘 것이라 정수 규칙의 실수 피연산자(0.0 ** -1, (-4.0) ** 0.5)도 같이 예외가 된다. 실수 범위를 넘는 크기는 다른 실수 산술처럼 무한대로 남는다. - Converter 한계: 대상의 큰 한계를 지수 변환에 넣어 무한대가 된 끝은 열린 끝(null → double.MaxValue)으로 읽는다. 예외로 바꾸면 4 바이트 이득 레지스터를 dB 로 읽는 Converter 의 쓰기가 통째로 막히고(고치기 전 벤더 XML 에서 그랬다), 무한대를 그대로 내면 '한계 없음' 값이 둘이 된다. - 문서: ** 는 0 이상 정수 지수면 정확한 정수로 남는다(실수 결과라고 적었던 것 정정), 비트·% 절삭이 정수 나눗셈과 같은 답이라는 조건은 '음이 아닌 값' 이 아니라 '부호와 무관, 2^53 이하'. 테스트는 고치기 전 코드에서 8건 모두 실패하는 것을 먼저 확인했다. net8.0 1955건·net48 1702건 통과, 로컬 벤더 XML 코퍼스 두 벌 통과, --no-incremental 경고 0.
릴리스 PR 의 마지막 커밋. 공개 API 추가가 없으므로 patch. 실수 수식 노드(SwissKnife·Converter)의 값이 바뀌는 것이 이 판의 첫 변경이다 — 정수끼리의 나눗셈을 실수로 계산한다.
3 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changed and why
Library
SwissKnifeandConverter(the formula, itsExpressions and the Converter limit mapping) used to evaluate integer ÷ integer as a truncating integer division, and their variables usually come from integer registers, so the whole formula ran in integers and only became a float at the end. Nothing failed; the values were just wrong. Seen on bench cameras: a frame-rate node1000000 / Nread exactly 21 Hz, and a fixed-point gammaTO / (1 << FRAC)read 0 (Basler: Gamma 0.5 written, 0 read back before, 0.5 now). Reproduced in tests: a 0.1 dB gain10 ** ((TO / 10) / 20)read 1.0 for every raw value below 200 and its truncated maximum rejected valid writes. Evaluating two vendor descriptions against an in-memory register space found 17 and 21 float formula nodes whose values change, all of them towards the correct value — including a gain that could not be written at all and time ranges that read 0..0. In float nodes/is now a real division;+ - * **between integers stay exact and continue in double on overflow; bitwise operators, shifts and%truncate real operands toward zero, so(N / 2) & 1and(N / 2) % 2give what they gave before.IntSwissKnife,IntConverter, address formulas and the publicFormula.Evaluatekeep the integer rules.**no longer returns NaN or a division by zero as a value (zero base with a negative exponent, negative base with a fractional exponent) in either rule, and a Converter limit that overflows to infinity is read as "no limit".pIsLockedreads; a lost reply to the stop kept the mode "locked" without asking the device. Now any write exception drops the register's cache, overlapping caches and the node's dependent closure before it propagates. The write-only shadow is left as it was.GevStream.StartAsyncalso resets SCP when the SCP write itself fails after sending.IsComplete = true,MissingPackets = 0, while the unreceived tail still held the previous frame from the reused pool buffer. A frame is now complete only when every byte the leader announced arrived. A block cut short is closed right away as incomplete (it does not hold later frames for the retention time), is reported with its real missing-packet count, and — when incomplete frames are delivered — has its tail zeroed, including the gap after a short last packet. The warning is logged once per stream. Variable-height frames now shrink even when the leader arrives after the trailer through a resend, and a bit-packed format with no byte stride (Mono12p at an odd width) no longer reportsPayloadSize0 after the shrink.PayloadSize— completes on the bench camera because it pads its last packet (measured 11/11). The one-time warning names both sizes if a device does not.GevDevicedoes not stop the streams it opened, so neitherControlLostnorDisposeAsyncends aReceiveAsyncthat is waiting for a frame. Pass a token or callStopAsyncfrom theControlLosthandler. The README example now passes a per-frame deadline. No behaviour change; pinned by a test.Tools
gevsharp-ipconfig.exeon the 0.4.0 release; this release carries the same build properly versioned.)Documentation
docs/architecture.md: the two formula rules, what a failed write invalidates, and the stream-lifetime contract.docs/design-requirements.md: R27–R29 for the three defects above, each with the tests that failed before the fix.docs/evaluation.md: MG-A320K-35 firmware and port layout; the odd-pixel-total packed measurement.docs/sim-register-map.md: the simulator'sTO / 10.0is no longer a workaround.Version
0.4.1 — patch: no public API was added. Values of float formula nodes change (first item above).
Checks
dotnet buildin the default configuration reports 0 warningsmainis an ancestor of this branch (the release-pr-guard job verifies it)mainmerged back intodevafterwards, anddevbumped to the next-devversion