diff --git a/.gitignore b/.gitignore index ce2a4ad..86c0f0f 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ coverage.out coverage.html /tmp +skread diff --git a/README.md b/README.md index 85fcd5d..43732b8 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ Based on original C# SDK for Windows from SEKONIC. - Sekonic C-700 - Sekonic C-800 +- Sekonic C-800-U (US Version. Basically the same as C-800) - Sekonic C-7000 (supports extended measurement configuration: FOV and Exposure Time) ## Supported (tested) platforms @@ -20,7 +21,15 @@ Based on original C# SDK for Windows from SEKONIC. - Windows - Linux -## Dependencies +## Known limitations + +Currently **only ambient** measuring mode is supported. + +TM-30, SSI and TLCI measurements are available for Sekonic C-7000 with FW version > 25 but parsing of these fields is **not implemented yet**. + +## Usage + +### Install USB driver Default implementation uses [gousb](https://github.com/google/gousb) wrapper for the libusb library. @@ -29,18 +38,107 @@ You must have [libusb-1.0](https://github.com/libusb/libusb/wiki) installed on y Installation for different platforms is covered in [gousb documentation](https://github.com/google/gousb/blob/master/README.md#dependencies). +_If you use Linux, you probably already have it._ +_If you use Mac ot Windows and was using USB devices that needed custom driver, you also probably have it._ + _Alternatively_ you can provide custom USB implementation with [simple interface](usbadapter.go) close to io.Reader. See the default [gousb based implementation](gousb_adapter.go) for reference. -## SDK usage +### Install Go + +You need `Go` programming language to be installed first. + +See the Go's [Download and install](https://go.dev/doc/install) page for installation instructions for different OS. + +On MacOS just use [Homebrew](https://brew.sh/): + +``` +homebrew install go +``` + +### Run example measurement program usung Go + +1. Connect your device. + +2. Run commands below: -See the [skread](cmd/skread/main.go) command implementation. +``` +go run ./cmd/skread -run -all +``` + +``` +go run ./cmd/skread -help +``` + +_Go will take care of dependencies when running this script for the first time._ + +### Build standalone program executable (if needed) -## Run example +``` +go build ./cmd/skread +``` + +_This will create binary executable file named `skread` in the same directory._ + +Run the executable: ``` -go run ./cmd/skread +./skread -help ``` +_Now you can run this file on any other machine with the same OS and architecture as the one you built it with. But remeber about USB driver dependency mentioned earlier, it is needed for program to run._ + +### Create your own measurement program (if needed) + +Note: the [cmd/skread/main.go](cmd/skread/main.go) is just an example of the library usage. If more specific funtionality is needed, you always can create your own Go program and import the library in the same way as example does. Go will fetch it automatically. + +Example: + +1. Create your project dir: + +``` +mkdir my_skreader && cd my_skreader +``` + +2. Initialise Go module named `my_skreader` and add `skreader` library as an external dependency: + +``` +go mod init my_skreader +go get github.com/akares/skreader +``` + +3. Download `main.go` example file there (or create manually): + +``` +curl -OL https://raw.githubusercontent.com/akares/skreader/main/cmd/skread/main.go +``` + +4. Run: + +``` +go run . -help +``` + +5. _Modify the program according to your needs._ + +6. Build standalone executable (if needed): + +``` +go build -o my_skreader ./main.go +./my_skreader -help +``` + +_You will still need to take care of the USB driver dependency mentioned earlier._ + +## Library API + +See the [skread](cmd/skread/main.go) command implementation for details. + +## Contribution + +1. Use `gofmt` +2. Ensure unit tests are okay: `go test ./...` +3. Ensure Go linter finds no errors: `golangci-lint run .` + ## License This project is licensed under the terms of the MIT license. diff --git a/cmd/skread/main.go b/cmd/skread/main.go index e28ec40..8799e0f 100644 --- a/cmd/skread/main.go +++ b/cmd/skread/main.go @@ -1,49 +1,174 @@ package main import ( + "flag" "fmt" + "os" "strconv" "github.com/akares/skreader" ) +//nolint:funlen,gocyclo func main() { - // Connect to SEKONIC device. - sk, err := skreader.NewDeviceWithAdapter(&skreader.GousbAdapter{}) - if err != nil { - panic(err) - } - defer sk.Close() + // Available commands + help := flag.Bool("help", false, "Shows usage information") + run := flag.Bool("run", false, "Runs a normal measurement and outputs the selected data") + + // Data to show + showInfo := flag.Bool("info", false, "Shows info about the connected device") + showAll := flag.Bool("all", false, "Shows all data from the connected device") + showIlluminance := flag.Bool("Illuminance", false, "Shows illuminance data") + showColorTemperature := flag.Bool("ColorTemperature", false, "Shows ColorTemperature data") + showTristimulus := flag.Bool("Tristimulus", false, "Shows Tristimulus data") + showCIE1931 := flag.Bool("CIE1931", false, "Shows CIE1931 data") + showCIE1976 := flag.Bool("CIE1976", false, "Shows CIE1976 data") + showDWL := flag.Bool("DWL", false, "Shows DWL data") + show := flag.Bool("CRI", false, "Shows CRI data") + showSpectra1nm := flag.Bool("Spectra1nm", false, "Shows Spectra1nm data") + showSpectra5nm := flag.Bool("Spectra5nm", false, "Shows Spectra5nm data") + + // Shown by default if no other flag is set + showLDi := flag.Bool("LDi", false, "Shows the most interesting data for LDs") - // Get some basic info of the device. - model, _ := sk.ModelName() - fw, _ := sk.FirmwareVersion() + flag.Parse() - fmt.Println(strconv.Quote(sk.String())) - fmt.Println("MN:", strconv.Quote(model)) - fmt.Println("FW:", fw) + if *help || len(os.Args) == 1 { + fmt.Println("Usage: skreader [options]") + fmt.Println("Example: skreader -run -all") + fmt.Println("\nOptions:") + flag.PrintDefaults() + + os.Exit(0) + } - // Get the current operational mode, knobs and buttons states of the device. - st, err := sk.State() - if err != nil { - panic(err) + if *run && len(os.Args) == 2 { + *showLDi = true } - fmt.Printf("ST: %+v\n", st) - // Run one measurement. - meas, err := sk.Measure() - if err != nil { - panic(err) + if *showAll { + *showInfo = true + *showIlluminance = true + *showColorTemperature = true + *showTristimulus = true + *showCIE1931 = true + *showCIE1976 = true + *showDWL = true + *show = true + *showSpectra1nm = true + *showSpectra5nm = true } - // Print the measurement result in various vays. - fmt.Printf("Meas: %s\n", meas.Repr()) - fmt.Printf("Meas: %s\n", meas.String()) - fmt.Printf( - "Lux=%s x=%s y=%s CCT=%s\n", - meas.Illuminance.Lux.Str, - meas.CIE1931.X.Str, - meas.CIE1931.Y.Str, - meas.ColorTemperature.Tcp.Str, - ) + if *run { + // Connect to SEKONIC device. + sk, err := skreader.NewDeviceWithAdapter(&skreader.GousbAdapter{}) + if err != nil { + panic(err) + } + defer sk.Close() + + // Get some basic info of the device. + model, _ := sk.ModelName() + fw, _ := sk.FirmwareVersion() + + // Get the current operational mode, knobs and buttons states of the device. + st, err := sk.State() + if err != nil { + panic(err) + } + + // Print the device info + if *showInfo { + fmt.Println(strconv.Quote(sk.String())) + fmt.Println("Model:", strconv.Quote(model)) + fmt.Println("Firmware:", fw) + fmt.Printf("State: %+v\n", st) + } + + // Run one measurement. + meas, err := sk.Measure() + if err != nil { + panic(err) + } + + if *showIlluminance { + fmt.Printf("------------\n") + fmt.Printf("Illuminance:\n") + fmt.Printf("LUX: %s\n", meas.Illuminance.Lux.Str) + fmt.Printf("Fc: %s\n", meas.Illuminance.FootCandle) + } + + if *showColorTemperature { + fmt.Printf("------------\n") + fmt.Printf("ColorTemperature:\n") + fmt.Printf("CCT: %s\n", meas.ColorTemperature.Tcp) + fmt.Printf("CCT DeltaUv: %s\n", meas.ColorTemperature.DeltaUv) + } + + if *showTristimulus { + fmt.Printf("------------\n") + fmt.Printf("Tristimulus:\n") + fmt.Printf("X: %s\n", meas.Tristimulus.X) + fmt.Printf("Y: %s\n", meas.Tristimulus.Y) + fmt.Printf("Z: %s\n", meas.Tristimulus.Z) + } + + if *showCIE1931 { + fmt.Printf("------------\n") + fmt.Printf("CIE1931:\n") + fmt.Printf("X: %s\n", meas.CIE1931.X) + fmt.Printf("Y: %s\n", meas.CIE1931.Y) + } + + if *showCIE1976 { + fmt.Printf("------------\n") + fmt.Printf("CIE1976:\n") + fmt.Printf("Ud: %s\n", meas.CIE1976.Ud) + fmt.Printf("Vd: %s\n", meas.CIE1976.Vd) + } + + if *showDWL { + fmt.Printf("------------\n") + fmt.Printf("DominantWavelength:\n") + fmt.Printf("Wavelength: %s\n", meas.DWL.Wavelength) + fmt.Printf("ExcitationPurity: %s\n", meas.DWL.ExcitationPurity) + } + + if *show { + fmt.Printf("------------\n") + fmt.Printf("CRI:\n") + fmt.Printf("RA: %s\n", meas.ColorRenditionIndexes.Ra) + for i := range meas.ColorRenditionIndexes.Ri { + fmt.Printf("R%d: %s\n", i+1, meas.ColorRenditionIndexes.Ri[i]) + } + } + + if *showSpectra1nm { + fmt.Printf("------------\n") + fmt.Printf("SpectralData 1nm:\n") + for i := range meas.SpectralData1nm { + // TODO: Missing one datapoint? + wavelength := 380 + i + fmt.Printf("%d,%f\n", wavelength, meas.SpectralData1nm[i].Val) + } + } + + if *showSpectra5nm { + fmt.Printf("------------\n") + fmt.Printf("SpectralData 5nm:\n") + for i := range meas.SpectralData5nm { + // TODO: Missing one datapoint? + wavelength := 380 + (i * 5) + fmt.Printf("%d,%f\n", wavelength, meas.SpectralData5nm[i].Val) + } + } + + if *showLDi { + fmt.Printf("LUX: %s\n", meas.Illuminance.Lux.Str) + fmt.Printf("CCT: %s\n", meas.ColorTemperature.Tcp) + fmt.Printf("CCT DeltaUv: %s\n", meas.ColorTemperature.DeltaUv) + fmt.Printf("RA: %s\n", meas.ColorRenditionIndexes.Ra) + fmt.Printf("R9: %s\n", meas.ColorRenditionIndexes.Ri[8]) + } + } } diff --git a/measurement.go b/measurement.go index 5517459..ad21573 100644 --- a/measurement.go +++ b/measurement.go @@ -19,8 +19,8 @@ type Measurement struct { ColorRenditionIndexes ColorRenditionIndexesValue // Color Rendition Indexes - SpectralData5nm [80]DecimalValue // Spectral Data (5nm) - SpectralData1nm [400]DecimalValue // Spectral Data (1nm) + SpectralData5nm [81]DecimalValue // Spectral Data (5nm) + SpectralData1nm [401]DecimalValue // Spectral Data (1nm) PeakWavelength int // Peak Wavelength (380...780nm) } @@ -109,37 +109,48 @@ type DominantWavelengthValue struct { // ColorRenditionIndexesValue represents a color rendition indexes Ra and Ri. type ColorRenditionIndexesValue struct { Ra DecimalValue - Ri [14]DecimalValue + Ri [15]DecimalValue } -// Not implemented here but available for C-7000 FW > 25 extended measurement data: +// TODO: Not implemented here but available for C-7000 FW > 25 extended measurement data: // TM30, SSI, TLCI // NewMeasurementFromBytes creates a new Measurement instance from the given raw // binary response from SEKONIC device. // Note: currently only ambient measuring mode results are supported. -// Magic numbers for limits and precisions are based on original C-7000 SDK from SEKONIC. // -//nolint:exhaustruct,gomnd,gocyclo +//nolint:exhaustruct,funlen,gomnd,gocyclo func NewMeasurementFromBytes(data []byte) (*Measurement, error) { if len(data) < MeasurementDataValidSize { return nil, fmt.Errorf("invalid measurement data size: %d < %d bytes", len(data), MeasurementDataValidSize) } - m := &Measurement{} + // Parse binary data to struct. + // + // Data offsets and sizes are based on SEKONIC USB data packet layout which + // seems to be stable between various devices. + // + // Magic numbers for limits and precisions are based on original C-7000 SDK. - // Parse binary data to struct + m := &Measurement{} + // Color temperature and deviation from the Planckian locus m.ColorTemperature.Tcp = toDecimalValue(parseFloat32(data, 50), 1563, 100000, 0) m.ColorTemperature.DeltaUv = toDecimalValue(parseFloat32(data, 55), -0.1, 0.1, 4) if m.ColorTemperature.DeltaUv.Range != RangeOk { // limit the CCT value (C-800 returns Tcp=50000 value instead of "Over" as C-7000 does) m.ColorTemperature.Tcp.Range = m.ColorTemperature.DeltaUv.Range } + + // Illuminance values in Lux and foot-candle units m.Illuminance.Lux = parseLuxToDecimalValue(data, 271, 100, 200000) m.Illuminance.FootCandle = parseLuxToDecimalValue(data, 276, 0.093000002205371857, 18580.607421875) + + // Tristimulus values in XYZ color space m.Tristimulus.X = toDecimalValue(parseFloat64(data, 281), 0, 1000000, 4) m.Tristimulus.Y = toDecimalValue(parseFloat64(data, 290), 0, 1000000, 4) m.Tristimulus.Z = toDecimalValue(parseFloat64(data, 299), 0, 1000000, 4) + + // CIE1931 (x, y, z) chromaticity coordinates m.CIE1931.X = toDecimalValue(parseFloat32(data, 308), 0, 1, 4) m.CIE1931.Y = toDecimalValue(parseFloat32(data, 313), 0, 1, 4) if m.CIE1931.X.Range != RangeOk { @@ -149,14 +160,23 @@ func NewMeasurementFromBytes(data []byte) (*Measurement, error) { } else { m.CIE1931.Z = toDecimalValue(1.0-m.CIE1931.X.Val-m.CIE1931.Y.Val, 0, 1, 4) } + + // CIE1976 (u', v') chromaticity coordinates m.CIE1976.Ud = toDecimalValue(parseFloat32(data, 328), 0, 1, 4) m.CIE1976.Vd = toDecimalValue(parseFloat32(data, 333), 0, 1, 4) + + // Dominant Wavelength m.DWL.Wavelength = toDecimalValue(parseFloat32(data, 338), -780, 780, 0) m.DWL.ExcitationPurity = toDecimalValue(parseFloat32(data, 343), 0, 100, 1) + + // CRI (Ra, Ri) m.ColorRenditionIndexes.Ra = toDecimalValue(parseFloat32(data, 348), -100, 100, 1) for i := range m.ColorRenditionIndexes.Ri { m.ColorRenditionIndexes.Ri[i] = toDecimalValue(parseFloat32(data, 353+i*5), -100, 100, 1) } + + // Boundaries check + if m.Illuminance.Lux.Range == RangeUnder { for i := range m.SpectralData5nm { m.SpectralData5nm[i].Range = RangeUnder